トップ 新規 編集 差分 一覧 ソース 検索 ヘルプ PDF RSS ログイン

Getting Started Notes - TWI (I2C)

最終更新時間:2009年08月05日 18時52分22秒

TWI(I2C)

一応動作確認はしました

 構造

プルアップされた2本の線 SDA , SCL が各デバイスに接続されている。普段はPullUpでHiだが、どのデバイスからもLoにできる。SDA,SCLの電圧は、各デバイスの出力のAND値となる。

各デバイスの「出力」は、普通言うポート出力とは異なる。Lowの出力時はポート出力Lo状態でいいが、High出力というのはHighのポート出力ではなく、ハイインピーダンスとなる。そうでなければHiとLoの混在はショート状態になってしまう。

 マスターとスレーブ

  • マスター=転送を開始し、リード(lead)する側。クロック(SCL)はマスターが操作する
  • スレーブ=マスターの要求に従って転送を行う。マスターが送るクロックに合わせて動作

送信・受信と混同しないように。マスターがクロックを送るのに合わせてスレーブがデータを送り、マスターがそれを受信する場合もある。

 開始・終了条件

マスターは、転送開始、転送終了の合図をするために、開始条件・終了条件という合図を送る。

  • 開始条件:SCL=Hiの状態の間にSDAを立ち下げる
  • 停止条件:SCL=Hiの状態の間にSDAを立ち上げる
  • 上記2つの間で、バスは開始条件を起こしたマスターにより使用中となる。
  • 1つの開始・終了条件間では1つの転送しか行えない。複数の転送を行いたければ、一度終了条件を送って再度開始条件から始める。
  • 終了条件を送らずに再度開始条件を送ることで次の転送手順を開始することもできる(再送開始条件)

 ビット送信

データ送信を行うときは、

  1. SDAのビットセット
  2. SDA一定のままSCLをHiへ
  3. SDA一定のままSCLをLoへ
  4. 次のビットセット

の手順を取る。開始条件・終了条件と区別するため、SDAを動かすときは必ずSCL=Loとする。以下は0と1を交互に送信している例。

スレーブは、マスターが送るクロックについて行けない場合は、SCLのLo時間を延長することでマスターを待たせる事が出来る。マスターはSCLをHiにしても、実際のSCLの電圧がLoのままである間は、次のクロック送出を待たなければならない。この機構によってマスターとクロックで想定するクロック速度が異なっていても速度を合わせることが出来る。

 バイト送信

  1. 基本的に、送る側がbit送信の方法で上位ビットから8bit送る。
  2. 最終ビット送信直後、送る側はSDAを入力に切り替え(プルアップでSDAはHiになる)、SCLに1bit分のクロック(立ち上がり→立ち下がり)を送る。
  3. 受ける側は送信側ACK/NACKという合図を送る。

0x55を送信する例。受信側がACKを返すと、赤線のようになる。

 通信の流れ

  • 開始条件→アドレスパケット送信→1つ以上のデータパケット→停止条件
  • アドレスパケットは、上位7bitにどのスレーブと通信を行うのかの情報(SLA)があり、最下位ビットにスレーブにデータを書き込むのかスレーブからデータを読み込むのかを決めるビット(R/W)がある。
  • スレーブから読み読みを指定するアドレスパケットをSLA+Rと呼ぶ。書き込みの方はSLA+Wと呼ぶ。
  • どの通信でもまずマスターが開始条件の直後にアドレスパケットを送るところから始まる。その後アドレスパケット指定で送信側が決定し、送信側からデータを送る。
  • アドレスパケット、データパケットとも、受信側がACK/NACKを送る。
  • データ転送が終了条件=受信側がACKを送らない 又は マスター側が終了条件や再送開始条件を送る

マスターの送信動作:アドレスパケット送信+データの送信

  1. マスターが、開始条件を送る
  2. マスターは、送信アドレスパケット(SLA+W)という8bit値を送信する。通信相手を指定する7bitアドレス+送信を指定する1bit
  3. マスターは送信完了後、スレーブが返してくるはずのACKを確認する。確認できなければエラー
  4. 続いてマスターからデータを8bitずつ送る。送信後スレーブが返してくるACKを確認する。
  5. データ送信を繰り返す。
  6. ACKが確認できない、または予定数のデータを送信し終わったらマスターはそれ以降のデータ送信を中止し、終了条件を送りバスを解放する。もしくは再送開始条件を送り他の転送を始める。

マスターの受信動作:アドレスパケット送信+データの受信

  1. マスターは、開始条件を送る
  2. マスターは、受信アドレスパケット(SLA+R)という8bit値を送信する。通信相手を指定する7bit+受信を指定する1bit。
  3. マスターは、送信完了後、スレーブが返してくるはずのACKを確認する。確認できなければエラー
  4. 続いて、マスターはSCLクロックを9bit分送り、これに合わせてスレーブから8bit分データが送られてくる。8bit受信したら、9bit目SCLにあわせてマスター側からACKを送る。(AVRのTWI機構の場合、マスターから送るACKは受信動作前にTWI機構に予約登録しておいたものが送られるので、ACKを送るかどうかは受信前に設定する)その後到着したデータを取り出す(AVRの場合はTWDRから読み出す)
  5. 上記動作によるデータ受信を繰り返す。
  6. 予定数のデータを受信し終わったらマスター受信完了時ACKを送らず、スレーブにこれ以上のデータ送信をしないよう伝える。(AVRのTWI機構の場合は最終データ受信の前にNACK予約することになる)
  7. マスターは終了条件を送る。

スレーブ側の受信動作:アドレスパケット受信+データ受信

  1. スレーブは、開始条件の直後に自分宛の送信アドレスパケットに相当する8bitデータを感知したら、ACKを返し、それがSLA+Wだったら受信体制に入る
  2. マスターが送ってくる8bitデータを受信する。8bit受信するたびにスレーブはACKを返す。(AVRではこちらも受信開始前にACKをあらかじめセットしておくことになる)
  3. 予定数のデータを受信し終わったらスレーブはNACKを送る(ACKを送らない)。(AVRではこちらも最後の受信開始前にNACKをあらかじめセットしておくことになる)
  4. ACKが送られてこないことを検知したマスターが終了条件を送って転送終了。

スレーブ側の送信動作:アドレスパケット受信+データ送信

  1. スレーブは、開始条件の直後に自分宛の受信アドレスパケットに相当する8bitデータを感知し、それがSLA+RだったらACKを返し送信体制に入る
  2. マスターが送ってくるSCLに合わせて8bitデータを送信する。送信するたびにマスターが返すACKを確認する。
  3. マスターがACKを送ってこなかったら、転送終了なのでデータ送信を止める
  4. マスターが終了条件を送って転送終了。

 複数マスタのバスシステム調停

開始条件を送るときは、他のマスタが開始条件を発行した後でないかどうかをチェックして、もし発行済みなら開始条件を送らない。TWI転送をあきらめるか、他のマスタが終了条件を送ってバスが開放されるまで待つ。

AVRのTWI機構はこれを自動で行い、他のデバイスがマスタとして動作しているときは開始条件送出を指示しても開始条件を送らずエラーを返す。

同時に開始条件を送り、同時にアドレスパケット送信を始めてしまった場合は以下のように調整される

  • SCLについては、それぞれのマスターが以下を繰り返すことで、最も遅いクロックに合わせたクロックに自動的に調整される。
    1. SCLをLoにする
    2. SCL電圧をチェックし、実際に電圧がLoになってから一定時間Loを続ける
    3. SCLをHiにする
    4. SCL電圧をチェックし、実際に電圧がHiになってから一定時間Hiを続ける
  • SDAの方もモニターし、自分がSDAをHiにしているのにSDAがLoであるのを感知したら、そのマスターは他のマスターがあることを感知し、自身の送信を止める。必要ならスレーブ動作に移行する。
    • よって、例えばアドレスパケット0x85を送るマスターと0x86を送るマスターがあった場合、アドレスパケットの値が小さいマスターの方が優先され、アドレスパケットが大きい方は引き下がる。(上位ビットからチェックされ、先に0が出た方が生き残るため)。スレーブアドレスが小さい方が優先され、同じスレーブアドレス対象でも書き込み(LSB=0)が優先されます。
    • アドレスパケット+送受信ビットが全く同一の場合は、さらにデータ送信までSDAチェックが続けられる。同じ理由で送信データが小さい方が生き残ることになる。
    • 送信データも最後まで同じなら・・・・まあマスタースレーブとも困らないからよし。
    • 送信データ数が異なる場合はちょっと困る。一方が送信を続けようとしているときにもう一方が終了条件や再送開始条件を送ってしまう可能性がある。AVRのデータシートでは、(同じアドレスに対する)送信データパケット数を統一してこんな事態が起こらないようにしろ!と書いてある。
  • マスターが受信動作だと、SDAを送るのは1つのスレーブなので、SDAデータ部による調停は起こらない。マスタが送るACKでの調停は起こるのかな??そうだとすれば先にNACKを送った方が敗れ去り、より多くのデータパケットを要求するマスターが生き残ることになると思われます。データ数も同じなら誰も困らない。その後同時に再送開始条件が送られた場合は、最初の開始条件と同じように扱われるのかな??

AVRのTWI部概要

 レジスタ

TWBR TWIボーレートレジスタ

  • TWBR=TWI Bitrate Registor、マスタのみ要指定。10以上にすること。これに加えて分周比をTWSRレジスタのTWPSビットで指定
  • SCL周波数 = CPU_CLOCK / (16+2*(TWBR)*分周比)
  • 逆にSCL周波数からTWBRを決めたければ、TWBR = (CPU_CLOCK/SCL周波数-16)/分周比/2

TWCR TWIコントロールレジスタ

bit名 動作
TWINT TWI割り込み要求
TWEA TWI確認応答(ACK)許可。データ受信時、受信直後ACKを返すことを指定する
TWSTA TWI開始(STArt)条件生成許可
TWSTO TWI停止(STOp)条件生成許可
TWWC TWI上書きエラーフラグ
TWEN TWI動作許可
TWIE TWE割り込み許可
  • TWENはTWIを使用するかどうかを指定。これが0だとTWI関連ピンは通常のI/Opinとして動作。
  • TWINTは、TWIの動作が終了して次の動作指示を待つときにセットされます。これがセットしている間のみTWIの設定やデータの出し入れができます。

1を書き込むことでクリアすることができます。クリアすると予定されている次の動作へ移ります。

  • ただし、転送動作中でなければTWINTは1にならないようなので、開始条件送出時やスレーブ待期時などはTWINTは0。ほかの動作はTWINT=1であることを確認して(もしくはTWI割り込みルーチン内で)行う必要があるが、開始条件だけは例外となる。

TWSR TWIステータスレジスタ

bit名 動作
TWS7- TWS3 TWIステータス。
TWPS1-TWPS0 TWIプリスケーラ選択。値0、1、2、3が分周比1、4、16、64に相当

TWDR TWIデータレジスタ

TWAR TWIスレーブアドレスレジスタ

スレーブとしてのアドレス値を設定する
bit bit名 動作
7-1 TWA6-TWA0 スレーブアドレス値
0 TWGCE 一斉呼び出しに応答許可

 AVR-TWI機能の使い方(割り込みを使わない版)

参考:AVR-LibCのTWIデモプロジェクト

TWINTの使い方

  • TWI機構は、何か動作が終わるたびにTWCRのTWINTビットを1にして停止する
  • このビットが1ということは、先に行った動作が終了し、次の動作が行えることを示している。*このビットが立っていない時はTWIに操作を加えてはならない(開始条件は例外)し、TWDRやTWSRからデータを取得してもその値には意味がない。
  • 先に行った動作の結果や、先に送ったデータに対するACKの有無の情報などがTWSRのbit TWS7- TWS3 に現れるので、これを取得して次の動作を決める。
  • 次の動作を指示するためにTWCRやTWDRに値をセットして、TWCRのTWINTに1を書き込むと、TWI機構が動き出す
  • その後繰り返し

扱いやすいようにマクロを組んでみました。

まずは割込を使わない版を作ってみます。

  • twi.h(avrgccにあり c:\winavr\avr\include\compat\twi.h)
#include <util/twi.h> 
//TWIのステータス取得に便利なTW_STATUS(TWSR&0xF8)が定義され、
//その値の解釈に役立つ値の定義があります。
  • twi2.h を作ってみました。ディレクトリが異なるから問題ないとはいえ、名前は変えたほうがいいかもしれない(^^;)
#include <util/twi.h> 
//定義など:割り込み対応にするためSET_TWCR(x)を変更
#define SET_TWCR(x)   {TWCR=(x)|_BV(TWINT)|_BV(TWEN);}while(0)
//#define SET_TWCR(x)   {TWCR=(x)|_BV(TWINT)|_BV(TWEN)|_BV(TWIE);}while(0)
#define TWI_START SET_TWCR(_BV(TWSTA))
#define TWI_ACK   SET_TWCR(_BV(TWEA))
#define TWI_NACK  SET_TWCR(0)
#define TWI_NEXT  SET_TWCR(0)
#define TWI_STOP  SET_TWCR(_BV(TWSTO))
#define TWI_END   SET_TWCR(0)
#define WAIT_TWINT     loop_until_bit_is_set(TWCR,TWINT)
//SCL周波数.これとF_CPU(システムクロック)を使ってTWBRの設定を行います。
//F_CPUはmakefile内のF_CPU定義で設定できます。もしお使いのmakefileで
//F_CPU定義ができなければソースファイル内でF_CPUを定義してください。
#define F_SCL (100000L)	/*100kHz*/
#define TWBR_VALUE ((F_CPU/F_SCL-16)/2)

extern void twi_init(void); //twi初期化、有効にする
extern uint8_t TWI_wait(void); //TWINTビットが1になるのを待ち、TWSRを読み込み値を返す
extern uint8_t TWI_write(uint8_t x); //送信+TWINT待ち、TWSR値を返す
extern uint8_t TWI_read(char ack);   //受信+ack予約
extern char sla_set(uint8_t sla_rw); //開始条件送出とSLA+WまたはSLA+R送信を行う。
//エラーが起こったら必要に応じて終了条件を送った後エラーコード(TWSR値)を返す。
//順調に行けばTW_MT_SLA_ACKまたはTW_MR_SLA_ACK値が返される。
  • twi2.c
#include <avr/io.h>
#include "twi2.h"

uint8_t TWI_wait(void)
{
	uint8_t r;	
	WAIT_TWINT;
	r=TW_STATUS;
	return (r);
}
char sla_set(uint8_t sla_rw)
{
/* if OK,return TW_MT_SLA_ACK. */
	char r;
	//printf_P(PSTR("PRESLA=%02X"),TWCR);
	//TWI_wait();
	TWI_START;
	r=TWI_wait();
	if (r!=TW_REP_START && r!=TW_START) goto sla_set_end;
	r=TWI_write(sla_rw);
	if (r==TW_MT_ARB_LOST) goto sla_set_end;
	if (r==TW_MT_SLA_ACK||r==TW_MR_SLA_ACK)  goto sla_set_end;
	TWI_STOP;
	sla_set_end:
	return (r);
}
uint8_t TWI_write(uint8_t x)
{
	TWDR=x;
	TWI_NEXT;
	return TWI_wait();
}
uint8_t TWI_read(char ack)
{
	uint8_t r;
        if (ack) { TWI_ACK; }
  
           else     { TWI_NACK; }

	TWI_wait();
	r=TWDR;
	return (r);
}
void twi_init(void)
{
	TWSR = 0;	   //TWIプリスケーラは0にしておく
	TWBR = TWBR_VALUE;
	TWCR = _BV(TWEN);
}

サンプル(マスター送信・受信)

秋月のRTCモジュールの読み書きをやってみました。

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/twi.h> 
#include "lcd.h"
#include "twi2.h"

char rtc_856b(void)
{
	char r;
	r=sla_set(0xA2|TW_WRITE);
	if (r!=TW_MT_SLA_ACK) return(r);
	TWI_write(0x00); /* address */
	TWI_write(0x00); /*Control-1:start*/
	TWI_write(0x56); /*Sec(BCD) bit7=VL 低電圧検知フラグ */
	TWI_write(0x34); /*Min(BCD)*/
	TWI_write(0x12); /*Hour(BCD)*/
	TWI_write(0x11); /*Day(BCD)*/
	TWI_write(0x01); /*Weekdays(0-6)*/
	TWI_write(0x07); /*Month(BCD)  bit7=C Yearsの繰り上がりで立ち上がる */
	TWI_write(0x05); /*Year(BCD)*/
	TWI_STOP;
	return (1);
}
volatile char sysflag;
#define F_OC1A _BV(0)

ISR(TIMER1_COMPA_vect)
{
	sysflag|=F_OC1A;
}
void lcd_hex2(uint8_t data)
{
	char h,l;
    h=data>>4;
    l=data&15;
    lcd_putch((h>9)?('@'+h):('0'+h));
    lcd_putch((l>9)?('@'+l):('0'+l));
}
//メインルーチン
int main(void)
{
	uint8_t r,h,m,s;
	
	DDRB=0xFF;
	DDRC=0xFF;
	lcd_init();
	twi_init();
	rtc_856b();
	TCCR1A=(1<<WGM11)|(1<<WGM10);
	TCCR1B=(1<<WGM13)|(1<<WGM12)|4;  
	//PS=1/256,16us/count@16MHz,FastPWM with TOP=OCR1A
	OCR1A=(F_CPU/256)-1;     //1secごとの比較一致割り込み生成
	TIMSK=(1<<OCIE1A);
	sei();
	for (;;) {
		if (sysflag & F_OC1A) {
			sysflag &= ~F_OC1A;
			r=sla_set(0xA2|TW_WRITE);
			TWI_write(0x02); /* address */
			sla_set(0xA2|TW_READ);
			h = 0x3F & TWI_read(1);
			m = 0x7F & TWI_read(1);
			s = 0x7F & TWI_read(0);
			TWI_STOP;
			lcd_goto(lcd_lin+1,0);
			lcd_hex2(h); lcd_putch(':');
			lcd_hex2(h); lcd_putch(':');
			lcd_hex2(h);
		}
	};

↑時刻を取得する所の順序が逆な気がします。

	r=sla_set(0xA2|TW_WRITE);
	TWI_write(0x02); /* address */ //0x02を指定
	sla_set(0xA2|TW_READ);
	s = 0x7F & TWI_read(1);	//0x02 Secondsレジスタを読み出し
	m = 0x7F & TWI_read(1);	//0x03 Minutesレジスタ読み出し
	h = 0x3F & TWI_read(0);	//0x04 Hoursレジスタ読み出し
	TWI_STOP;

の順ではないでしょうか?

スレーブ送受信動作