Renesas Synergy™

FAQ 1006566 : 78K0S用ソフトウェアI2Cバス・マスタ通信プログラム(78K0S/Kx1+共通、プログラム例はKY1+をターゲット)

[はじめに]
I2CバスはマイコンにEEPROM、表示コントローラ、A/DやD/Aを接続するためのシリアル・バスとして幅広く使用されています。マイコンの中には、78K0/Kx2のように、専用のインタフェース回路を内蔵して、比較的容易にI2Cバスを使用できる製品もあります。
ここでは、専用インタフェースを内蔵していない78K0Sで通常のポートを用いて、シングルマスタモードでI2Cバスを制御するプログラムについて説明します。

[I2Cバスの概要]
I2Cバスはオープン・コレクタまたはオープン・ドレイン出力のクロック・ライン(SCL)とデータ・ライン(SDA)の2本の信号を用いて通信を行ないます。2つの信号線は抵抗でプルアップされており、未使用時にはハイ・レベルになっています。I2Cバスには、通信を制御するマスタと、マスタからの制御により通信を行なうスレーブが存在します。マスタがI2Cバスを使用する時には、以下の手順となります。

①スタート・コンディションを発行してバスの使用を宣言
②最初にアドレスを送信して、通信相手のスレーブと通信方向を指定
③指定したスレーブが応答すると、通信を開始
④データを受信した方は1つ(バイト)毎に応答を戻す
⑤通信が完了したらストップ・コンディションを発行してバスを開放

I2Cバスの詳細については、ホームページにFAQ「I2Cバスについての概要」があるので、こちらを参照してください。ここでは、最大400kbpsまで対応したファースト・モードに準拠した動作を対象とします。

[使用するポート]
I2Cバスはオープン・ドレイン出力でドライブする必要があります。しかし、通常のCMOS出力しかない場合にはポート使い方に工夫が必要です。具体的には、ポートの出力ラッチは0に固定しておき、入出力を切り替えることで処理します。つまり、以下のような処理を行ないます。

  • PMレジスタに0をセット → 出力ポート → ロウ・レベル
  • PMレジスタに1をセット → 入力ポート → ハイ・レベル

このような処理を行なうことで、バスへの出力とバスにハイ・レベルを出力した状態でのバスの状態の読み出しが可能になります。

[I2Cバスの主な動作/処理]
I2Cバスの主な動作を以下に示します。ここで、送信はマスタからスレーブへ、受信はスレーブからマスタへの通信を示します。

(1)バスの状態の確認
バス使用中にリセットがかかってCPUが処理をやり直した場合などで、スレーブがバス(SDA)をロウ・レベルに引いている場合があります。このような状態ではマスタはバスを使用することができません。従って、最初にバスが開放されているかを確認する必要があります。

(2)バスの開放
もし、バスが開放されていなければ、SDAがハイ・レベルになるまでSCLにダミー・クロックを出力します。通常は9クロック(データ+ACK分)以下でSDAはハイ・レベルになります。実際のプログラムではスレーブがウェイトをかけていた場合(この可能性は殆どないと考えられますが)にいつまでもバス開放を待つのはまずいので、通常のパルス幅のダミー・クロックを256個出力するまでにSDAがハイ・レベルになるかで判断します。SDAがハイ・レベルになったことを確認したら、ストップ・コンディションを発行して、バスを開放させます。
(たまたま、スレーブがハイ・レベルを出力したが、次のデータがロウ・レベルであった場合、SCLをロウ・レベルにすることで、スレーブが次のデータ(ロウ・レベル)を出力する可能性が考えられます。これを避けるため、ここではSDAをロウ・レベルにしてから再度ハイ・レベルに立ち上げます。SDAの立ち上げはスタート・コンディションの発行と解釈されますが、その後にストップ・コンディションを発行するので、タイミング規格を満足させます。)

(3)スタート・コンディションの発行
バスが開放されたら、スタート・コンディションを発行して、バスの使用をスレーブに通知します。

(4)スレーブ・アドレスの送信
実際の通信の先頭では、スレーブのアドレス(7ビット)と通信方向(1ビット)の合計8ビットのデータを送信します。送信したアドレスに合致したスレーブがACKを戻してこない場合には、該当するアドレスのスレーブが存在しないので、通信はそこで終了し、ストップ・コンディションを発行して通信を終了します。ACKが戻ってきたら、通信方向に従って通信を継続します。

(5)サブ・アドレスの送信
I2Cバスとしてはスレーブ・アドレスを送信することで通信相手のスレーブを指定できますが、スレーブの中にも内部のアドレス情報をもつものがあります。たとえば、EEPROMでは内部のアドレスを指定するために、スレーブ・アドレスの送信に引き続いて内部のアドレスを指定します。スレーブ内部のアドレスは容量が256バイト以下なら1バイトの指定です。ここで指定したアドレスは通常は保持され、読み書きを行なうごとに更新されるようになっています。

(6)データの送信
サブ・アドレスの送信に引き続いてデータを送信すると、そのデータが指定したサブ・アドレスに書き込まれます。(EEPROMではストップ・コンディションを受けてから実際の書き込みが行なわれます。)
サブ・アドレスの送信を含めて、データを送信し、スレーブがデータを正しく受け取るとACKを戻してきます。ACKが戻れば、次の処理に移りますが、ACKが戻らない場合には通信を終了し、ストップ・コンディションを発行して通信を終了します。

(7)データの受信
通信方向の指定で受信を指定した場合には、スレーブ・アドレスの送信の次からはデータ受信を行ないます。データを受信したマスタは、引き続いてデータを受信したい場合には、ACKを戻します。受信したデータが最後のデータで、それ以上のデータ受信を行なわない場合にはNACKを戻して、受信が完了したことをスレーブに示す必要があります。

(8)ストップ・コンディションの発行
通信を完了してバスを開放する場合には、SCLがハイ・レベルの状態でSDAをハイ・レベルに立ち上げることで、ストップ・コンディションを発行します。

(9)リスタート・コンディションの発行
バスの使用権を確保している状態でデータ転送方向を切り替える場合に再度スタート・コンディションを発行します。これがリスタートで、この直後はスレーブ・アドレスの送信となり、通信方向を再指定できます。
一般的な使い方としては、内部のアドレスを指定してスレーブからデータを読み出す場合に使用します。通常は、最初に送信方向でスレーブ・アドレスを送信し、スレーブ内のアドレスをサブ・アドレスとして指定します。次にリスタート・コンディションを発行し、続いて、受信方向を指定してスレーブ・アドレスを送信します。これで、スレーブ内の指定したアドレスからの読み出しが可能となり、以降は指定したアドレスから順次読み出すことができます。

[バスの主な状態(参考)]
バスの主な状態は以下のようになります。

  状態 SCL SDA
(1) バスは未使用 H H
(2) データ変化時 L データは変化
(3) データは安定 H H/L
(4) スタート・コンディション発行後 H L
(5) アドレス送信直後(9クロック・ウェイト状態) L ACK→H
(6) 9クロック・ウェイト状態(次の通信待ち状態) L 次データ
(7) アドレス送信後(受信指定で通信開始) L 受信データ
(8) アドレス送信後(送信指定で通信開始) L 送信データ
(9) 8クロック・ウェイト状態 L L=ACK/H=NACK
(10) ストップ・コンディション発行後(バスは未使用) H H

 

  • バスが使用されていない状態(1)や(10)では、2つの信号線SCLとSDAはハイ・レベル状態です。
  • 基本的にSDAはSCLがLのときに変化して、SCLがHの状態ではSDAは安定しています。
  • スタート・コンディションの発行(SCLがHの状態でSDAを立ち下げる)後にはSCLがHでSDAがLの状態となります。この後で、スレーブのアドレスを送信するには、まずSCLを立ち下げます。
  • アドレスの送信やデータ通信でSCLが9クロック送られた後の(5)や(6)ではSCLがLの状態で次の通信の準備を行ないます。次の通信がない場合には、SDAはHとなっています。そうでない場合(7)や(8)には次のデータの先頭ビットが出力されます。
  • 8ビット・データの送受信後(9)にSCLはLとなりACK応答待ちの状態となります。
  • (6)や(9)で、スレーブはSCLをロウ・レベルに引くことで通信を待たせることができます。従って、マスタはSCLがハイ・レベルになったことを確認してから次の処理に移る必要があります。

[プログラム例]
基本的な制御はアセンブラのプログラムで実現しますが、これらはC記述のプログラムから呼び出せるようにしてあります。(このプログラムはあくまで参考用です。)
このプログラムでは、78K0S/KY1+は高速内蔵発振器で動作させ、P2.2をSDA信号用に、P2.3をSCL信号用に使用します。なお、P2のほかのビットを操作すると、P2.2及びP2.3の出力ラッチが書き換わってしまいます。P2の他のビットを出力ポートとして使用する場合には、出力ラッチのデータのイメージを内蔵RAMに準備しておきます。ポートの出力を操作したい場合にはRAMのデータを操作し、結果をポートに書き込むようにしてください。

ひとこと注意1

処理速度が要求されるような場合には、プログラムはアセンブラで記述し、大きな制御ではC言語で記述することがよくあります。この場合には、C言語のプログラムからアセンブラ記述のサブルーチンを関数としてコールします。そのときに、サブルーチンの名前の付け方に注意が必要です。アセンブラ記述のサブルーチンの名前は必ず_ _(2個のアンダースコア)で始まる必要があります。C言語のプログラムからこれを呼び出すときには_(1個のアンダースコア)で始まるように指定します。
たとえば、I2Cで使用するポートの初期化プログラムはアセンブラ記述部分では「__setup_i2c_port」の名前が付けられていますが、これをC言語のプログラムで呼び出すときには「_setup_i2c_port();」と記述します


また、内蔵RAMには実行結果のエラー・ステータス用のバイト変数(i2c_error)、スレーブに対してACKを戻すかどうかを指定するビット変数(acke)を準備します。以下に宣言の例を示します。これらの変数は全て専用のプログラムで制御するものとします。

comdata DSEG     SADDR      ; saddr領域に変数を確保します。
i2c_error: DS 1 ; エラー・フラグ
BSEG   ; ビット変数領域であることを示します。
acke DBIT ; ACK制御フラグ

(1)ポートの定義
PMSW_SDAがSDA信号用で使用する名称です。これを1にすると端子は入力ポートとなり、PSW_SDAを読むことで、SDA信号の状態を知ることができます。スレーブが出力していなければSDA信号はハイ・レベルになります。これを0にすると、出力ポートとなり、出力ラッチが0なのでSDA信号をロウ・レベルにします。
PMSW_SCLがSCL信号用で使用する名称です。これを1にすると端子は入力ポートになりPSW_SCLを読むことで、SCL信号の状態を知ることができます。通常はSCLをハイ・レベルにするときに、1に設定します。スレーブがウェイトをかけてなければ、外部のプルアップ抵抗によりハイ・レベルに引かれます。これを0にすると出力ポートとなり、SCL信号をロウ・レベルにできます。
・アセンブラでの定義例(使用ポートの定義)

PMSW_SDA    equ PM2.2       ; SDA信号制御で使用
PSW_SDA equ P2.2 ; SDA信号の読出しで使用
PSW_I2C EQU P2 ; 使用するのはポート2
PMSW_SCL equ PM2.3 ; SCL信号制御で使用
PSW_SCL equ P2.3 ; SCL信号の読出しで使用

実際のポートの操作(モードレジスタの操作)は意味を分かりやすくするために、以下のようなマクロを定義しています。
・ポート操作の定義

_scl_hi         macro       ; _scl_hiと言うマクロの定義を示す
set1 PMSW_SCL   ; PM2.3をセットして入力にする
endm   ; マクロ定義の終了を示す
_scl_lo macro
clr1 PMSW_SCL ; PM2.3をクリアしてSCLにロウ出力
endm
_sda_hi macro
set1 PMSW_SDA ; PM2.2をセットして入力にする
endm
_sda_lo macro
clr1 PMSW_SDA ; PM2.2をクリアしてSDAにロウ出力
ひとこと1

プログラムを作成する場合にマクロ機能を用いることで、定型的な処理を効率的に記述したり、プログラムを分かりやすくしたりできます。ここでは、処理そのものはポートモード・レジスタに対するビット操作命令です。効率的な記述よりも処理の分かり易さのためにマクロ機能を使用しています。CLR1 PMSW_SCLと記述するよりも、_scl_loと記述する方がどのような処理を行なうかが直感的に意味を捉えることができます。


(2)ポートの初期化(__setup_i2c_port)
使用するポートの出力ラッチを0にします(ポートに0をバイト単位で書き込む)。ポートのモードは入力にしておきます。このプログラムはC言語記述部分から呼び出せるように__で始まる名前をつけてpublic宣言します。

    public  __setup_i2c_port    ; 外部から使えるようにpublic宣言します
__setup_i2c_port:
MOV PSW_I2C,#0 ; 端子をオープン・ドレインと等価に
; 使用するのでデータは0に固定
_scl_hi ; PMを入力にする
_sda_hi
RET

(3)バスのビジー・チェック(__i2c_busy)
バスが空き状態かをチェックします。結果はキャリー・フラグで戻ります。自分自身で使用中を含めて、SCLかSDAがロウ・レベルであれば、ビジーと判断し、キャリー・フラグはセット(戻り値はtrue)されます。バスが開放されていればキャリー・フラグはクリア(戻り値はfalse)されます。ポートの状態を変えるような処理は行ないません。ここではアセンブラ記述で参照しやすいように専用に名前(i2c_busy)を追加します。

;bit    _i2c_busy(void)
public __i2c_busy
__i2c_busy:
i2c_busy:
SET1 CY ; ビジーフラグをセットしておく
BF PSW_SDA,$busbusy ; SDAラインのチェック
BF PSW_SCL,$busbusy ; SCLラインのチェック
CLR1 CY ; 開放されていればフラグリセット
busbusy:
RET
ひとこと2

78K0Sで1ビットのポートの状態をチェックするときには、上のプログラム例に記述されているように条件分岐命令(BF命令やBT命令)を使用します。BF命令を使用すると、1ビットのポートが0ならば分岐し、1ならば分岐しません。上記のプログラムではPSW_SDAとPSW_SCLが両方1ならば2つの条件分岐命令では分岐せず、CY(キャリー・フラグ)がクリアされます。


(4)バスの開放(__free_i2c_bus)
①バスの状態を確認しバスが開放されていない場合にはダミー・クロックを送ってバスが開放されるのを待ちます。256パルスを送ってもバスが開放されない場合にはエラーとしてキャリー・フラグをクリア(戻り値はfalse)して戻ります。

SDA信号とSCL信号がハイ・レベルになったならストップ・コンディションを発行します。

ひとこと注意2

この状態で、SCLをロウ・レベルにすると、スレーブが次のデータとしてロウ・レベルを出力する可能性があります(スレーブが出力していたデータがたまたま1になったが、次のデータが0の場合です)。そこで、ここではSCLがハイ・レベルの状態で、SDAをロウ・レベルにしてから再度ハイ・レベルに立ち上げます。SDAの立ち下げはスタート・コンディションの発行と解釈されますが、その後にSDAを立ち上げてストップ・コンディションを発行するので、無視されてしまいます。


②ストップ・コンディションを発行して、バスを開放したら、キャリー・フラグをセット(戻り値はtrue)して戻ります。
ここで使用するダミー・クロック送信では、SCLのロウ・レベル幅1.3μ秒を確保するため、動作クロック12クロックの時間を確保しています。ハイ・レベル幅はサブルーチン・コ-ル他のオーバーヘッドがあるので意識して確保はしません。スレーブがウェイトをかけている場合には、マスタが立ち上げてもバス上ではロウ・レベルのままになってしまいます。この状態が継続した場合には、I2Cバスは全く使えません。そのため、ここではSCLの立ち上がりを待つことはしません。マージンをもたせた256回で処理を終了します。

;bit    _free_i2c_bus(void)
public __free_i2c_bus
__free_i2c_bus:
free_i2c_bus:
_sda_hi ; 念のためにSDAを立ち上げる
_scl_hi ; 念のためにSCLを立ち上げる
CALL !i2c_busy ; バスの状態確認
NOT1 CY
BC $free_exit1 ; バスが開放されていれば抜ける
MOV A,#0 ; 限度を256回に設定
dumyclkloop:
CALL !scl_pulse ; SCLにダミークロックを出力
CALL !i2c_busy ; バスの状態確認
NOT1 CY
BC $free_exit1 ; バスが開放されていれば抜ける
DEC A ; 限度を超えていないか?
BNZ $dumyclkloop ; 限度以内なら継続
RET ; エラーならキャリーをセットして戻る
free_exit1:
PUSH PSW ; 念のためにフラグをセーブ
CALL !I2C_STPR ; ストップ・コンディション発行
POP PSW
RET

・I2Cバスの規格に準拠したダミー・クロックを生成します。

scl_pulse:
_scl_lo ; 6:SCLを立ち下げる
NOP ; 2:ロウ・レベル幅1.3μ秒を
NOP ; 2:確保するために、11クロック
NOP ; 2:以上時間を確保して立ち上げる
_scl_hi ; 6:SCLを立ち上げる
RET ; 6:これだけでハイ・レベルは十分

(5) スタート・コンディションの発行(__I2C_STR)
スタート・コンディション発行の準備として、SDAを立ち上げてからSCLを立ち上げます(SCLが既に立ち上がっていればこれはストップ・コンディションと同じです。リスタートの場合にはSCLがロウ・レベルになっている必要があります)。
その後にSCLが立ち上がっていることをBF命令で確認してからSDAを立ち下げることでスタート・コンディションを発行します(これは、スレーブが9クロック目でウェイトをかけていたときに解除されるのを待つために入れています。殆どの場合では不要と考えられます。BF命令を削除する場合にはNOP命令2個に置き換えてください)。

ひとこと3

I2CバスではスレーブがSCLをロウ・レベルに引くことで、通信を一時的に待たせる機能があります。そのため、マスタはSCLを立ち上げた後で、本当にSCLが立ち上がったかを確認します。その際に「BF PSW_SCL,$$」を使用しています。この命令により、SCLライン(PSW_SCLで読める)がロウ・レベルであれば、自分自身に分岐して命令を繰り返します。これにより、SCLが立ち上がると次に進むことができます。
    public  __I2C_STR
__I2C_STR:
I2C_STR:
_sda_hi ; 6:準備のためSDAをハイに設定する
NOP ; 2:
_scl_hi ; 6:SCLをハイに設定する
BF PSW_SCL,$$ ;10:SCLの立ち上がり検出
_sda_lo ; 6:スタート・コンディション発行
RET ; 6:

(6)ストップ・コンディションの発行(__I2C_STPR)
最初にSCLを立ち下げると次の転送がスタートするかもしれないので、SCLが既にロウ・レベルとして処理を行ないます。もし、SCLがハイ・レベルであれば、SDAをロウ・レベルにするとスタート・コンディションの発行と同じになります。しかし、次にストップ・コンディションを発行することで、おかしな状態にはならないと考えられます。
ここでも、SCLが立ち上がっていることをBF命令で確認してから次の処理を行ないます。これは上のスタート・コンディションの発行と同じ理由によるものです。

    public  __I2C_STPR
__I2C_STPR:
I2C_STPR:
_sda_lo ; 6:SDAを立ち下げる。これでスタート
NOP ; 2:コンディションと解釈されても、
NOP ; 2:直ぐにストップ・コンディションを
; 発行するので構わない。
_scl_hi ; 6:念のためにSCLをハイに設定する
BF PSW_SCL,$$ ;10:SCLの立ち上がり検出
_sda_hi ; 6:ストップ・コンディション発行
NOP ; 2:
NOP ; 2:
RET ; 6:

(7) I2C バスに8ビットデータを出力(__put_i2c)
C言語のノーマルモードで1バイトの引数の受け渡しに使用されるXレジスタで渡された8ビットのデータをI2Cバスに出力するルーチンです。SCLがロウ・レベルのときにMSBから順番に出力し、ACK応答を確認し、ACK応答があったときにはキャリー・フラグをセット(戻り値はtrue)して戻ります。ACK応答がないときにはキャリー・フラグをクリア(戻り値はfalse)して戻ります。
処理が終わった時点ではSCLはロウ・レベルになった状態です(9クロック・ウェイト状態)。

ひとこと4

送信開始時(前回の通信での9クロック・ウェイト状態)にウェイト解除待ちを行ない、確実にSCLが立ち上がったことを確認するためにSCLの立ち上がり待ち処理を行なっています。(この処理はなくてもいいかもしれないが念のために全てのビットに対して入るようになっています。本来ウェイトの入らない部分も確認しているので無駄と考えたら削除してNOP2個に置き換えてください。)


(プログラム・フロー)
以下にプログラムのフローを示します。



(プログラム例)

;bit    _put_i2c(u8 data);
public __put_i2c
__put_i2c:
put_i2c:
PUSH BC ; ループカウンタで使用のためセーブ
MOV B,#8
MOV A,X ; 入力データ(引数)は8ビット
;
;
OUTLOOP:
_scl_lo ; 6:SCLを立ち下げてデータ出力準備
NOP ; 2:立下りの時間調整用
ROL A,1 ; 2:MSBよりCYにシフトアウトする
BNC $OUTLO ; 6:データが0なら分岐
_sda_hi ; 6:1を出力
BR $CLKHI ; 6:SCLの立ち上げへ
OUTLO: _sda_lo ; 6:0を出力
NOP ; 2:セットアップ時間確保
CLKHI:
_scl_hi ; 6:
BF PSW_SCL,$$ ;10:SCLの立ち上がり検出(念のため)
DBNZ B,$OUTLOOP ; 6:

POP BC ; 6:
_scl_lo ; 6:8クロック目のSCLを立ち下げる
NOP ; 2:
_sda_hi ; 6:SDA を入力に切替え
NOP ; 2:
NOP ; 2:
NOP ; 2:
_scl_hi ; 6:9クロック目のSCLを立ち上げ
;
; ウェイト解除を待つ
;
SET1 CY ; 2:
BF PSW_SCL,$$ ; SCLの立ち上がり検出待ち
BF PSW_SDA,$ACKOK ;10:/ACK信号を取得
NOT1 CY ; 2:
ACKOK:
_scl_lo ; 6:SCLを立ち下げ(9クロック目)
NOP
RET

(8) I2C バスに8ビットデータを出力。エラーでバスを開放(__put_i2c2)
これは上記(7)のルーチンを使って、データを出力し、エラーが発生した場合には、ストップ・コンディションを発行して戻るものです。入出力条件は(7)と同じです。

;static bit  _put_i2c2(u8 data)
public __put_i2c2
__put_i2c2:
CALL !put_i2c ; 1バイト出力
BNC $PUTERROR ; エラーなら終了処理へ
RET ; 正常なら終了
PUTERROR:
CALL !I2C_STPR ; ストップ・コンディション発行
CLR1 CY ; falseにする(念のため)
RET

(9) I2C バスから8ビットデータを入力(__get_i2c)
I2Cバスの選択されたスレーブから8ビットのデータを受信し、受信データをCレジスタに格納して戻ります。スレーブからのデータはMSBから順にSCLがHのときに読み込みます。受信完了後に、ackeビットが1であればACK応答を行ない次のデータがあることをスレーブに通知します。ackeビットが0であればNACK応答を行ない、最後のデータを受信したことをスレーブに通知します。
処理が終わった時点ではSCLはロウ・レベルになった状態です(9クロック・ウェイト状態)。
(プログラム・フロー)
以下にプログラムのフローを示します。



(プログラム例)

;u8 _get_i2c(void);
public __get_i2c
__get_i2c:
MOV A,#11111110b ; シフトしてCY=0で終了
INLOOP:
NOP ; 2:
SET1 CY ; 2:初期値として1をセット
_scl_hi ; 6:SCLの立ち上げ
BF PSW_SCL,$$ ; SCLの立ち上がり検出

BT PSW_SDA,$DATAIS1 ;10:
NOT1 CY ; 2:データが0ならキャリーを反転
DATAIS1:
ROLC A,1 ; 2:読み込んだ結果をAレジスタにシフトイン
_scl_lo ; 6:SCLの立ち下げ
BC $INLOOP ; 6:8ビット分ループ

;******************************************************
;* ACK応答の制御 *
;******************************************************
BF _acke,$ACKEND ;10:ACK応答するかをチェック
_sda_lo ; 6:ACKを戻す
ACKEND:
_scl_hi ; 6:9クロック目の立ち上げ
;
; ここではスレーブが8クロック・ウェイトしていたときの対応で
; SCL信号の立ち上がりを待っている。通常は不要か。
;
BF PSW_SCL,$$ ; SCLの立ち上がり検出(念のため)
NOP ; 2:
_scl_lo ; 6:9クロック目の立ち下げ

MOV C,A ; 4:return (u8)
_sda_hi ; 6:終わったらSDAは入力にしておく
RET
ひとこと5

ここで、ループをカウントするためにAレジスタを使用しています。Aレジスタへの受信データ(ビット)の取り込み(右からのシフトイン)処理とループカウント動作を兼用させています。Aレジスタに初期値として11111110bを設定しておくと、シフトインごとに1111110xb→111110xxb→11110xxxbと変化していきます。7回目が完了した段階では0xxxxxxxbとなり、ここまでのシフト結果ではキャリー・フラグはセットされています。8回目のシフトを行なうと、初期値の最後のビットである0がキャリーに送られます。従って、キャリーが0になったら8回が完了したことになります。


(10)エラー・フラグのクリア(__clear_i2c_error)
これは内部の変数をクリアするだけの処理です。エラー・フラグの状態を戻り値とします。

;static u8 _clear_i2c_error(void)
public __clear_i2c_error
__clear_i2c_error:
clear_i2c_error:
MOV i2c_error,#I2C_NO_ERROR ;
MOV C,#I2C_NO_ERROR
RET

(11) エラー・フラグのセット(__set_i2c_error)
内部の変数にエラーの状態をセットするものです。エラーは以下のように定義しています。

I2C_NO_ERROR        = 0x00:エラー無し
I2C_BUS_NOT_FREE = 0x10:バスが開放されていなかった
I2C_ADDR_NACK = 0x11:スレーブがアドレスに反応しなかった
I2C_RADDR_NACK = 0x12:スレーブがREADアドレスに反応しなかった
I2C_REG_ADDR_NACK = 0x13:スレーブから予期せぬ NACK が帰って来た(レジスタ)
I2C_DATA_NACK = 0x14:スレーブから予期せぬ NACK が帰って来た(データ)

;static u8 _set_i2c_error(u8 error)
public __set_i2c_error
__set_i2c_error:
set_i2c_error:
XCH A,X ; エラー・データを取り込む
MOV i2c_error,A ; エラー・フラグにセット
MOV C,A ; 戻り値をセット
XCH A,X
RET

(12) エラー・フラグの読み出し(__get_i2c_error)
エラー・フラグの内容を読み出します。エラーは上(11)のように定義しています。

;static u8 _get_i2c_error(void)
public __get_i2c_error
__get_i2c_error:
get_i2c_error:
PUSH AX
MOV A,i2c_error ; エラー・フラグの読み出し
MOV C,A ; 戻り値にセット
POP AX
RET

(13)ACK応答許可フラグのセット(__ACK_EN)
マスタ受信でのACK応答を許可する設定をおこないます。通常は許可にしておき、最後のデータ受信時には禁止します。

;void _ACK_EN(void)
public __ACK_EN
__ACK_EN:
ACK_EN:
SET1 acke ; ACK応答フラグセット
RET

(14) ACK応答フラグの禁止セット(__ACK_DS)
マスタ受信の最後のデータでNACK応答させる場合に使用します。

;void _ACK_DS(void)
public __ACK_DS
__ACK_DS:
ACK_DS:
CLR1 acke ; ACK応答フラグクリア
RET

(15) マスタ送信処理(__write_i2c_block)
I2Cバスを用いて256バイトのEEPROMへの書き込みを行ないます。このサブルーチンはC言語(ノーマルモード)からの利用を考慮して、必要なパラメータはC言語の引数と同じ形式で渡すものとします。引き渡すパラメータは以下の形式となります。

第1の引数:Xレジスタ   :I2Cバスのスレーブのアドレス
第2の引数:スタックの1段目   :スレーブのアクセスしたい内部アドレス
第3の引数:スタックの2段目   :データを保存するバッファのアドレス
第4の引数:スタックの3段目   :書き込むデータのバイト数


これらのパラメータを2バイト単位で第4の引数~第2の引数までこの順番でスタックにプッシュしてからこのサブルーチンをコールします。
このサブルーチンは、第1の引数で示されたスレーブ・アドレスのEEPROMの、第2の引数で示された内部アドレス以降に、第3の引数で示されたバッファメモリから、第4の引数で示されたバイト数のデータを書き込みます。正常に書き込みが完了したら、バスを開放して、エラー・フラグはクリアして戻ります。エラー・フラグと同じ値を戻り値としてCレジスタに格納して戻るので、戻り値をチェックすることで、エラーを確認できます。(エラーの内容は(11) エラー・フラグのセットを参照してください。)
(プログラム・フロー)
以下にプログラムのフローを示します。



バイト単位の送信処理にはアセンブラ記述の__put_i2c2関数を使用し、エラーが発生した場合にはバスを開放し、発生した状況に応じたエラーを戻します。エラーが発生しなかった場合には、バスを開放して、エラーなしを戻します。(EEPROMはストップ・コンディションを検出して、送信されたデータを内部のセルに書き込みます。)
このプログラム例と同じ処理を行なうプログラムをC言語で記述した例を[プログラムの使用例]の(1)に記載しておきます。細かな処理の順番は異なりますが、処理の内容は同じです。関数としての呼び出し方法と戻り値も同じです。

(プログラム例)

;**************************************************************
;* *
;* I2C デバイスにブロックデータ送信 *
;* 対象は256バイト以下のサブアドレスもつデバイス *
;* ADR(W), SUB_ADR, DATA... *
;**************************************************************
;* [i] : u8 i2c_adr ... I2C スレーブのアドレス *
;* [i] : u8 reg_adr ... スレーブ内部のアドレス *
;* [i] : u8 *data ... 書き込みデータのアドレス *
;* [i] : u8 size ... バイト数(0x00 = 256) *
;* [r] : u8 ... error code *
;* *
;* 実際の引数はスタックに16ビット単位で積まれて *
;* 渡されるので、SPの値をHLに持ってきて、HL相対に *
;* より必要な引数を得る。 *
;* *
;**************************************************************
;u8 _write_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size)

public __write_i2c_block
__write_i2c_block:
PUSH HL ; レジスタをセーブ
PUSH AX
;
; このサブルーチンはC(ノーマル・モード)から関数コールされる。
; そのため、この段階でのスタックのデータは以下のようになっている
;
; sp : Xレジスタ(第1引数) :スレーブ・アドレス
; sp+1 : Aレジスタ :未使用(レジスタのセーブのみ)
; sp+2 : Lレジスタ :未使用(レジスタのセーブのみ)
; sp+3 : Hレジスタ :未使用(レジスタのセーブのみ)
; sp+4 : PCの下位 :戻りアドレス下位
; sp+5 : PCの上位 :戻りアドレス上位
; sp+6 : 第2引数 :スレーブ内アドレス
; sp+7 : :未使用
; sp+8 : 第3引数の下位 :書き込みデータの格納アドレス
; sp+9 : 第3引数の上位
; sp+10: 第4引数 :書き込みデータ数

MOVW AX,SP ; 2つ目以降の引数を取り出す準備
MOVW HL,AX
MOV X,#I2C_BUS_NOT_FREE
CALL !set_i2c_error ; ダミーでエラーをセットしておく
CALL !free_i2c_bus ; バスが開放できるか
BNC $EXITSUB ; 開放できなければ戻る

CALL !I2C_STR ; スタート・コンディション発行

MOV X,#I2C_ADDR_NACK
CALL !set_i2c_error ; ダミーでエラーをセットしておく
MOV A,[HL] ; スレーブ・アドレスを取り出す
AND A,#11111110b ; 方向フラグを送信に
MOV X,A
CALL !put_i2c2 ; 送信方向でスレーブアドレスを送信
BNC $EXITSUB ; スレーブから応答なければエラーで戻る

MOV X,#I2C_REG_ADDR_NACK ;
CALL !set_i2c_error ; ダミーでエラーをセットしておく
MOV A,[HL+6] ; スタックから第2引数を取り出す
MOV X,A
CALL !put_i2c2 ; スレーブ内アドレスを送信
BNC $EXITSUB ; スレーブから応答なければエラーで戻る

MOV A,[HL+10] ; スタックから第4引数を取り出す
MOV B,A ; データ数をBレジスタに設定
MOV A,[HL +8] ; スタックから第3引数を取り出す。
MOV X,A ; 第3引数(書き込みデータ格納アドレス)
MOV A,[HL+9] ; を
MOVW HL,AX ; HLレジスタに設定する
MOV X,#I2C_DATA_NACK ; ダミーでエラー・フラグにデータへの
CALL !set_i2c_error ; ACK応答無しを設定
WBLOOP:
MOV A,[HL] ; 書き込みデータをバッファから読み出す
MOV X,A
CALL !put_i2c2 ; データを書き込む
BNC $EXITSUB ; エラーが発生したら戻る

INCW HL ; データアドレスを更新
DBNZ B,$WBLOOP ; データ数分繰り返す

CALL !I2C_STPR ; バスを開放
CALL !clear_i2c_error ; エラー・フラグをクリア
EXITSUB:
MOV B,#0 ; 戻り値の上位をクリア(不要?)
POP AX
POP HL
RET

(16) マスタ受信処理(__read_i2c_block)
I2Cバスを使用して、256バイトのEEPROMからデータを読み出します。
第1の引数で示されたスレーブの第2の引数で示された内部アドレス以降から、第3のパラメータで示されたバッファに、第4のパラメータで示されたバイト数読み出します。
スレーブ・アドレスやスレーブ内部のアドレスの送信処理にはアセンブラ記述の__put_i2c2関数を使用し、エラーが発生した場合にはバスを開放し、発生した状況に応じたエラーを戻します。処理でエラーが発生しなかった場合には、バスを開放して、エラーなしを戻します。

(プログラム・フロー)
以下にプログラムのフローを示します。



(プログラム例)

;****************************************************************
;* *
;* I2C デバイスからブロックデータを受信 *
;* 対象は256バイト以下のサブ・アドレスをもつデバイス *
;* ADR(W), SUB_ADR, DATA... *
;****************************************************************
;* [i] : u8 i2c_adr ... I2C スレーブのアドレス *
;* [i] : u8 reg_adr ... スレーブ内部のアドレス *
;* [i] : u8 *data ... 書き込みデータのアドレス *
;* [i] : u8 size ... バイト数(0x00 = 256) *
;* [r] : u8 ... error code *
;* *
;* 実際の引数はスタックに16ビット単位で積まれて *
;* 渡されるので、SPの値をHLに持ってきて、HL相対に *
;* より必要な引数を得る。HLからのアクセス時の相対 *
;* アドレスは書き込みの方を参照のこと。 *
;****************************************************************
;u8 _read_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size)

public __read_i2c_block
__read_i2c_block:
PUSH HL
PUSH AX
MOVW AX,SP ; 2つ目以降の引数を取り出す準備
MOVW HL,AX ; ポインタをHLレジスタに設定
MOV X,#I2C_BUS_NOT_FREE
CALL !set_i2c_error ; ダミーでエラーをセットしておく
CALL !free_i2c_bus ; バスが開放できるか
BNC $EXITSUBR ; 開放できなければ戻る
CALL !I2C_STR ; スタート・コンディション発行

MOV X,#I2C_ADDR_NACK
CALL !set_i2c_error ; ダミーでエラーをセットしておく
MOV A,[HL] ; スレーブ・アドレスを取り出す
AND A,#11111110b ; 方向フラグを送信に
MOV X,A
CALL !put_i2c2 ; 送信方向でスレーブアドレスを送信
BNC $EXITSUBR ; スレーブから応答なければエラーで戻る

MOV X,#I2C_REG_ADDR_NACK
CALL !set_i2c_error ; ダミーでエラーをセットしておく
MOV A,[HL+6] ; スタックから第2引数を取り出す
MOV X,A
CALL !put_i2c2 ; スレーブ内アドレスを送信
BNC $EXITSUBR ; スレーブから応答なければエラーで戻る

CALL !I2C_STR ; リスタート・コンディション発行

MOV X,#I2C_RADDR_NACK
CALL !set_i2c_error ; ダミーでエラーをセットしておく
MOV A,[HL] ; スレーブ・アドレスを取り出す
OR A,#00000001b ; 方向フラグを受信に
MOV X,A
CALL !put_i2c2 ; 受信方向でスレーブアドレスを送信
BNC $EXITSUBR ; スレーブから応答なければエラーで戻る

SET1 acke ; ACK応答を設定
MOV A,[HL+10] ; データ数を
MOV B,A
MOV A,[HL+8] ; スタックから第3引数を取り出す。
MOV X,A ; 第3引数(書き込みデータ格納アドレス)
MOV A,[HL+9] ; を
MOVW HL,AX ; HLレジスタに設定する

BRLOOP:
MOV A,#1 ; 最後のデータかをチェックする
CMP A,B
BNZ $BRNEXT ; 最後のデータ(残り1バイト)なら
CLR1 acke ; NACK応答を設定
BRNEXT:
CALL !get_i2c ; データをリード
MOV A,C ;
MOV [hl],a ; 読み込んだデータをバッファに書き込む

INCW HL ; ポインタ更新
DBNZ B,$BRLOOP ; データ数分繰り返す

CALL !I2C_STPR ; バスを開放
CALL !clear_i2c_error ; エラー・フラグをクリア
EXITSUBR:
MOV B,#00H ; 余分?
POP AX
POP HL
RET

[プログラムの使用例]
以上で説明したアセンブラで記述したプログラムの使い方の例としてC言語で記述したプログラムから呼び出して使用する例を示します。
下記の(1)及び(2)の関数はそれぞれ、アセンブラでのプログラム例の(15)および(16)で示したプログラムと同じ処理を実現させるためのものです。これらの例では、アセンブラで記述した基本的な制御ルーチンを使用してI2Cバスを用いた256バイトのEEPROMの制御を行ないます。また、(3)はアセンブラでのプログラム例の(15)および(16)や下記の関数の例(1)及び(2)を使用する例となります。
なお、以下の例を使用する際には次に示すようなアセンブラで記述されたプログラムを外部から参照するための宣言や" unsigned char"を"u8"と記述するための宣言などを行なっておく必要があります。

#pragma SFR
#pragma EI
#pragma DI
#pragma NOP
typedef unsigned char u8, uint8, /* same as "byte" */
*u8Ptr, *uint8Ptr; /* same as "bytePtr" */
/* エラー・フラグ操作関係処理関数 */

extern u8 _set_i2c_error(u8 error);
extern u8 _get_i2c_error(void);
extern u8 _clear_i2c_error(void);

/* エラー・フラグ状態定義 */
enum{
I2C_NO_ERROR = 0x00, // エラー無し(0)
I2C_BUS_NOT_FREE = 0x10, // バスが開放されていなかった
I2C_ADDR_NACK = 0x11, // スレーブがアドレスに反応なし
I2C_RADDR_NACK = 0x12, // スレーブがREADアドレスに反応なし
I2C_REG_ADDR_NACK = 0x13, // 予期せぬ NACK が帰って来た(レジスタ)
I2C_DATA_NACK = 0x14 // 予期せぬ NACK が帰って来た(データ)
};

/* ACK応答制御関係処理関数 */

extern void _ACK_EN(void);
extern void _ACK_DS(void);

extern void _setup_i2c_port(void);
extern bit _i2c_busy(void);
extern bit _free_i2c_bus(void);

/* I2Cバス初期化関係処理関数 */

extern void _I2C_STR(void);
extern void _I2C_STPR(void);

/* I2Cバスバイト転送基本関数 */

extern bit _put_i2c(u8 data);
extern bit _put_i2c2(u8 data);
extern u8 _get_i2c(void);

/* I2Cバスブロック転送処理関数 */

extern u8 _write_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size);
extern u8 _read_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size);
ひとこと注意3

これらのプログラムは8MHzの内蔵発振器の出力をCPUのクロックとして動作させるものです。従って、78K0S/KY1+のPOCが解除されても、そのままでは8MHzでの動作ができませんので、電源電圧が4V以上になるのを待つ必要があります。そのためには初期化のためのhdwinit関数に以下のような処理を記述しておく必要があります。
    LVIS = 0x00;        /* select VLI=4V */
LVIM = 0b10000000; /* Start VLI detection*/
PCC = 0x00; /* CPU clock is fx/4 */
_clear_i2c_error(); /* I2C flag clear */
_setup_i2c_port(); /* Initialize I2C port*/
/*
wait for 0.17ms(=0.2ms-(32+30)*52/1000)
*/
for (work1 = 0; work1 < 10; work1++){
NOP();
}
WDTM = 0b01110000; /* Stop WDT */
LSRSTOP = 1; /* Stop Low Speed OSC */
while ( LVIF ){
NOP();
} /* wait VDD > 4V */

PPCC = 0; /* CPU clock is fx=fR */
IF0 = 0x00; /* Clear interrupt */ 

 

ひとこと6

ここでは、WDTや低速内蔵発振器を停止させたり、X1,X2端子をポートとしてI2Cバスで使用したりするためにオプションバイトで指定する必要があります。そのためには以下の内容のようなアセンブラ記述の定義ファイルを作成し、リンクしておく必要があります。
    @@OPTB  CSEG    AT  0080H
DB 10011100b
DB 11111111b
END


(1)マスタ送信処理(u8 write_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size))
I2Cバスを用いて256バイトのEEPROMへの書き込みを行なう関数です。
第1の引数で示されたスレーブの第2の引数で示された内部アドレス以降に、第3のパラメータで示されたバッファの内容を、第4のパラメータで示されたバイト数書き込みます。
バイト単位の送信処理にはアセンブラ記述の__put_i2c2関数を使用し、エラーが発生した場合にはバスを開放し、発生した状況に応じたエラーを戻します。
エラーが発生しなかった場合には、バスを開放して、エラーなしを戻します。処理の手順は以下の通りです。

  • 最初にI2Cバスが開放状態であることを確認/バスを開放します。
  • 開放できなければ、エラーで戻ります。
  • スタート・コンディションを発行してバスの使用を開始します。
  • EEPROMのアドレスを指定して送信方向でアドレスを送信します。
  • 次にEEPROM内部の書き込みたいアドレスを送信します。
  • 以降は指定されたバイト数のデータを送信します。
  • 送信が完了してストップ・コンディションを発行します。(EEPROMはストップ・コンディションを検出して、送信されたデータを内部のセルに書き込みます。)
u8 write_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size)
{
i2c_adr &= 0xfe;
if (!_free_i2c_bus()) // バスを開放させる。
return _set_i2c_error(I2C_BUS_NOT_FREE); // バスが開放されない場合には
//エラー・フラグをセットして戻る

_I2C_STR(); // スタート・コンディション発行

if (!_put_i2c2(i2c_adr)) // I2C アドレス送信
return _set_i2c_error(I2C_ADDR_NACK); // 対象デバイスがノーリアクションなら
//エラー・フラグをセットして戻る

if (!_put_i2c2(reg_adr)) // レジスタアドレス送信
return _set_i2c_error(I2C_REG_ADDR_NACK); // サブ・アドレスでNACK発生なら
//エラー・フラグをセットして戻る


do{
if (!_put_i2c2(*data++)) // データ送信
return _set_i2c_error(I2C_DATA_NACK); // データ送信でNACK発生
}while(--size);
_I2C_STPR(); // 正常終了でバスを開放
return _clear_i2c_error(); // エラー・フラグをクリアして戻る
}

(2) マスタ受信処理(u8 read_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size))
I2Cバスを使用して、256バイトのEEPROMからデータを読み出します。
第1の引数で示されたスレーブの第2の引数で示された内部アドレス以降から、第3のパラメータで示されたバッファに、第4のパラメータで示されたバイト数読み出します。
スレーブ・アドレスやスレーブ内部のアドレスの送信処理にはアセンブラ記述の__put_i2c2関数を使用し、エラーが発生した場合にはバスを開放し、発生した状況に応じたエラーを戻します。処理でエラーが発生しなかった場合には、バスを開放して、エラーなしを戻します。通信の手順は以下の通りです。

  • 最初にI2Cバスが開放状態であることを確認/バスを開放します。
  • 開放できなければ、エラーで戻ります。
  • スタート・コンディションを発行してバスの使用を開始します。
  • EEPROMのアドレスを指定して送信方向でアドレスを送信します。
  • 次にEEPROM内部の読み出したいアドレスを送信します。
  • リスタートをかけます。
  • EEPROMのアドレスを指定して受信方向でアドレスを送信します。
  • 指定したアドレスからのデータの読み出しを行ないます。
  • 読み出しの際に、最後のデータでなければ、ackeをセットしてACK応答するようにしてアセンブラの__get_i2cを呼び出して処理します。
  • 最後のデータについてはackeをクリアしてNACK応答する設定で__get_i2cを呼び出して処理します。
u8 read_i2c_block(u8 i2c_adr, u8 reg_adr, u8 *data, u8 size)
{
u8 d;
i2c_adr &= 0xfe;
if (!_free_i2c_bus())
return _set_i2c_error(I2C_BUS_NOT_FREE);

_I2C_STR(); // スタート・コンディション発行

if (!_put_i2c2(i2c_adr)) // I2C アドレス送信
return _set_i2c_error(I2C_ADDR_NACK); // 対象デバイスがノーリアクションなら、
//エラー・フラグを設定して戻る

if (!_put_i2c2(reg_adr)) // レジスタアドレス送信
return _set_i2c_error(I2C_REG_ADDR_NACK); // サブ・アドレスでNACKなら
// エラー・フラグを設定して戻る

_I2C_STR(); // リスタート

if (!_put_i2c2(i2c_adr | 0x01)) // I2C アドレス(READ)送信
return _set_i2c_error(I2C_RADDR_NACK); // 対象デバイスがノーリアクションなら
// エラー・フラグを設定して戻る

_ACK_EN(); // ACK 許可
do{
if (size == 1) // 最後のバイト?
_ACK_DS(); // マスタ受信最後のバイトならNACKに設定
d = _get_i2c(); // データの読み込み
*data++ = d; // 読み込みデータをバッファにセット
}while(--size);

_I2C_STPR(); // バスを開放

return _clear_i2c_error(); // エラー・フラグをクリアして戻る
}

(3)EEPROMアクセス例
この使用例では受信用に32バイト、送信用に16バイトを準備してアセンブラのプログラム例(15)及び(16)や上記の(1) マスタ送信処理や(2) マスタ受信処理を使用します。使用するために必要な定義は以下の通りです。

u8      eep_read_buf[32];   // read buffer
u8 eep_write_buf[16]; // write buffer
u8 result; // for result flag

#define ADR_EEP 0xa0 // slave address
// 1010xxxyB
// ||||||||
// |||||||+-- R/W#
// |||||++--- P1/P0 (fixed 00)
// ||||+----- A2 (fixed 0)
// ++++------ I2C Addr (fix)
  • アセンブラ記述の関数を使用して、バッファ中の16バイトのデータをEEPROMの00~0F番地に書き込みます。
    result = _write_i2c_block(ADR_EEP, 0x00, eep_write_buf, 16);
  • アセンブラ記述の関数を使用して、EEPROMの00~1F番地のデータをバッファに読み出します。
    result = _read_i2c_block(ADR_EEP, 0x00, eep_read_buf, 32);
  • C言語記述の関数を使用して、バッファ中の16バイトのデータをEEPROMの00~0F番地に書き込みます。
    result = write_i2c_block(ADR_EEP, 0x00, eep_write_buf, 16);
  • C言語記述の関数を使用して、EEPROMの00~1F番地のデータをバッファに読み出します。
    result = read_i2c_block(ADR_EEP, 0x00, eep_read_buf, 32); 

 

適用製品

78K0S/Kx
他にご質問がございましたら、リクエストを送信してください