AVRで秋月のI2C接続小型キャラクターLCDを動かす

ひさしぶりに、電子工作で作りたいものができたので、ちょっとリハビリを兼ねて、AVRの触ったことない機能&触ったことないデバイスを触ってみよう、と思い、秋月でI2C接続小型キャラクターLCDを買ってきて動かしたので、今日はそのメモです。

AVRでI2Cデバイスを動かす場合、内蔵のTWI機能を使うのが基本です。TWIを使うことで、I2Cデバイスのマスターもスレイブも簡単に作れますし、クロックストレッチ(スレーブ側の処理が間に合わないときにスレーブがクロック信号をGNDに押さえつけることで、クロックを一時停止させ、通信を遅らせる仕組み)やマスターが複数いる場合の競合の検出などもハードできちんと対応してくれています。その代わり、TWIはmega系など、8bit AVR の中でもややリッチめなデバイスにしか搭載されていなかったりしますので、それ以外のマイコンではソフトで実装してやる必要があります。競合検出なんかの実装は結構面倒なので、ソフト実装で対応する場合はそのような面倒な仕様は省略してシングルマスターのみ対応とかで妥協しちゃうことが多いんじゃないでしょうか。

ともあれ、今日は素直にTWI使って実装してみました。

ソースコード

ソースコードを全文張っておきます。使用AVRは mega88p で、他には、対象LCDと電源、I2C用プルアップ抵抗x2だけの単純な回路です。(コード先頭のコメント参照)

/********************************************************************
 * I2C LCD (ST7032i) sample
 *
 * build and write:
 *   avr-gcc -g -O2 -Wall -mmcu=atmega88 -DF_CPU=1000000UL main.c -o main.elf
 *   avr-objcopy -j .text -j .data -O ihex main.elf main.ihex
 *   avrdude $AVR_PROG_OPT -pm88p -U flash:w:main.ihex
 *
 * schematic:
 *
 *                 mega88p      VCC----ST7032i:VCC
 *              +-----u-----+   VCC----ST7032i:RST
 * ISP:RST<<----|RESET   PC5|-----+----ST7032i:SCL
 *              |PD0     PC4|---+-|----ST7032i:SDA
 *              |PD1     PC3|   | | +--ST7032i:GND
 *              |PD2     PC2|   | +-|--[2k]--VCC
 *              |PD3     PC1|   +---|--[2k]--VCC
 *              |PD4     PC0|       |
 *       VCC----|VCC     GND|-------+--GND
 *       GND----|GND    AREF|
 *              |PB6    AVCC|----VCC
 *              |PB7     PB5|---<<ISP:SCK
 *              |PD5     PB4|--->>ISP:MISO
 *              |PD6     PB3|---<<ISP:MOSI
 *              |PD7     PB2|
 *              |PB0     PB1|
 *              +-----------+
 *
 ********************************************************************/

#include <avr/io.h>
#include <util/delay.h>
#include <util/twi.h>

/* I2C control using TWI */

static inline void I2C_Init()
{
	// TWSR[TWPS] = 00(div1) & TWBR=2 -> 400kHz in 8MHz CPU
	TWSR = 0;
	TWBR = 2;
}

static inline void I2C_Start()
{
	TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN);
	loop_until_bit_is_set(TWCR, TWINT);
}

static inline void I2C_Stop()
{
	TWCR = _BV(TWINT) | _BV(TWSTO) | _BV(TWEN);
	loop_until_bit_is_set(TWCR, TWSTO);
}

static inline void I2C_Send(uint8_t data)
{
	TWDR = data;
	TWCR = _BV(TWINT) | _BV(TWEN);
	loop_until_bit_is_set(TWCR, TWINT);
}

static inline uint8_t I2C_Recv(int ack)
{
	if (ack) {
		TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWEA);
	} else {
		TWCR = _BV(TWINT) | _BV(TWEN);
	}
	loop_until_bit_is_set(TWCR, TWINT);
	return TWDR;
}

/* I2C_LCD : ST7032i(I2C charactor LCD) */

static uint8_t I2C_LCD_SendControls(uint8_t data[], uint8_t num)
{
	uint8_t i;

	I2C_Start();

	I2C_Send(0x7c);
	if (TW_STATUS != TW_MT_SLA_ACK) {
		return -1;
	}

	I2C_Send(0x00); // Co:0(continuous) RS:0
	if (TW_STATUS != TW_MT_DATA_ACK) {
		return -1;
	}

	for (i = 0; i < num; i++) {
		I2C_Send(data[i]);
		if (TW_STATUS != TW_MT_DATA_ACK) {
			return -1;
		}
		_delay_us(27);
	}

	I2C_Stop();

	return 0;
}

static uint8_t I2C_LCD_SendString(uint8_t addr, char *data, uint8_t num)
{
	uint8_t i;

	I2C_Start();

	I2C_Send(0x7c);
	if (TW_STATUS != TW_MT_SLA_ACK) {
		return -1;
	}

	I2C_Send(0x80); // Co:1(one shot) RS:0
	if (TW_STATUS != TW_MT_DATA_ACK) {
		return -1;
	}

	I2C_Send(0x80 | addr); // Set DDRAM address
	if (TW_STATUS != TW_MT_DATA_ACK) {
		return -1;
	}

	I2C_Send(0x40); // Co:0(continuous) RS:1
	if (TW_STATUS != TW_MT_DATA_ACK) {
		return -1;
	}

	for (i = 0; i < num; i++) {
		I2C_Send(data[i]);
		if (TW_STATUS != TW_MT_DATA_ACK) {
			return -1;
		}
		_delay_us(27);
	}

	I2C_Stop();

	return 0;
}


static uint8_t I2C_LCD_Init()
{
	uint8_t init_data1[] = {0x38, 0x39, 0x14, 0x70, 0x56, 0x6c};
	uint8_t init_data2[] = {0x38, 0x0C, 0x01};
	uint8_t st;

	_delay_ms(40);

	I2C_Init();

	st = I2C_LCD_SendControls(init_data1, sizeof(init_data1));
	if (st != 0) {
		return st;
	}
	_delay_ms(200);

	st = I2C_LCD_SendControls(init_data2, sizeof(init_data2));
	if (st != 0) {
		return st;
	}
	_delay_ms(2);

	return 0;
}

/* main */

int main(int argc, char *argv[])
{
	I2C_LCD_Init();
	I2C_LCD_SendString(0x00, "Hello", 5);
	I2C_LCD_SendString(0x40, "I2C LCD", 7);
	
	return 0;
}

I2C_で始まる関数が、TWIでI2Cの操作をしているもので、I2C_Init()で初期化、I2C_Start()/I2C_Stop()は Start condition や stop condition の発行、I2C_Send()/I2C_Recv()は1バイト(アドレス、データ問わず)の送信/受信です。今回の実装に割り込みは使っておらず、すべて処理の完了を待つ、ブロッキングインターフェースです。

I2C_LCD_で始まる関数が、液晶向けのI2Cコマンドを送る関数です。トリッキーな事は何もしていないので、読むぶんにはすんなり読めるでしょう。


以下、ハマったところのメモです。

LCD の I2C コマンドの Control byte の先頭ビットの意味

秋月のモジュールについてきたペラ紙の説明ではちょっとわかりにくいのですが、1回のI2Cコマンドで複数のI2Cのコマンドを発行できるようになっています。その際、連続発行のスタイルとして2つのスタイルがあり、

  1. 「Co = 1 の control byte」+「data byte」をペアにして、それを複数送る
  2. 「Co = 0 の control byte」の後、複数の「data byte」を送る(以降はStopBitまでずっと data byte として扱われる)

ということができます。前者はRSが異なるものを送りたい時、後者はRSが同じものを連続して送りたい時に便利です。また、両者を1つのI2Cコマンドに混ぜることも可能です。(一度後者のモードで送り始めるとStop Conditioん以外では止める手段が無いため、混ぜる場合は「前者」が幾つか+「後者」という形になります。)

例を幾つかあげましょう。表記は "[" を start condition "]" を stop condition とし、数値は1バイトのデータ送信とします。)

画面クリア(前者の例。1ペアのみ)

	[0x7c 0x80 0x01]
              Co=1
              RS=0

画面をクリアしたうえで"a"(0x51)と表示(前者の例。2ペア送信)

	[0x7c 0x80 0x01 0xa0 0x51]
              Co=1      Co=1   a
              RS=0      RS=1

"abc"と表示(後者の例)

	[0x7c 0x40 0x51 0x52 0x53]
              Co=0   a    b    c
              RS=1

カーソルを2行めに移して"abc"と表示(両者を混ぜた例)

	[0x7c 0x80 0xa0 0x40 0x51 0x52 0x53]
              Co=1      Co=0   a    b    c
              RS=0      RS=1

例をみて分かるとおり、RSが同じものを連続で送るのであれば、後者のフォーマットが楽です。特に、文字列データを送る場合は使わない手はないでしょう。初期化中も RS=0 のコマンドばかりなので使えますが、clear display など、コマンド送信後、長時間の待ちを入れる必要があるものがあるので注意です。

restart condition

restart (もしくは repeated start) condition は、I2Cバスの制御を掴んだまま連続して複数のI2Cコマンドを出す際に使用されるもので、通信後、Stopビットでコマンドを終えるのではなく、再度スタートビットを送ることで実現します。EEPROMのアドレス指定コマンドとReadコマンドみたいに、確実に連続して送らないとおかしくなる場合に使用されます。

TWI はこの restart condition を発行できますし、認識もできるようですが、買ってきたI2C液晶のコントローラは対応していないようで、restart 以降のコマンドが単に無視されてしまいました。

TWI の STOP ビットの待ちかた

TWI を操作する際、基本は

  1. TWCR にやってほしい事を書き込む
  2. それを契機に TWI 機能が動作
  3. 処理が終わったら、TWCR[TWINT] = 1 になる。(割り込みマスクが開いていれば割り込みがかかる)

となりますので、たとえば Start Condition を発行して、完了を待ちたいなら

	TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN);
	loop_until_bit_is_set(TWCR, TWINT);

となります。
ところが、Stop Condition を発行した場合も同じノリで TWINT を待っていても、永遠に上がってくることはありませんでした。

よくよくデータシートを見てみると、Stopの場合に TWINT が上がってくるとはどこにも書いてないんですよね。データシートの「22.5.5 Control Unit」には START/SREPEATED STARTコンディション後、と明示され、STOPは含まれていませんし、「22.6 Using the TWI」には、「Note that TWINT is NOT set after a STOP condition has been sent.」とはっきり書いてあります。

データシートに書いてあるフロー例だと、Stop発行要求したところで終わっていて、完了待ちなんかはしていないわけですが、じゃぁ、待たなくて良いか、というと、書いてみても動かなかったりしました。(今思えば、連続してI2Cコマンドを発行しようとしていたので、前のStopが完了しないうちにStartコンディションを要求したせいで、Restart扱いになっていたのかもしれません。この I2C LCD は Restart を認識できないので動いていなかったのではないかと。)

ともあれ、何とかしてStopの発行を待ちたい場合、TWCRのTWSTOビットが落ちるのを待つことになります。データシートによると、このビットはStopConditionを発行しおえた時は自動的に0になる仕様です。なお、TWSRはTWINTが飛んだ時しか有効ではないのでこの監視には使えません。

プルアップ抵抗

I2C は SCL と SDA の二本を各デバイスでつないでまわるだけ、というお手軽接続ですが、いずれの線もどこかでプルアップされている必要があります。

そこで横着をして、AVR の GPIO 内蔵のプルアップを使ってやろうと画策したのですが、全然通信できず、オシロで波形を見てみたところ、波形の立ち上がりが非常に遅くなっている状態でした。立ち上がりが遅い=プルアップ抵抗での引き上げが遅い=抵抗が大きすぎる、ということになります。これもデータシートで確認したところ I/O Pin Pull-up Resistor は 20k 〜 50 kオームでしたので、数kオームの外付けプルアップ抵抗に置き換えることで正常動作しました。

通信相手によっては内蔵プルアップでよい事もあるのでしょうが、バスに複数のものをぶら下げれば、バスの静電容量が上がってさらに波形がなまることになるので、基本は外付けのプルアップ必須ってことでしょうかね。