micro:bitでµT-Kernel 3.0を動かそう

目次に戻る

前回の連載記事に戻る

[第12回]I2C経由で加速度センサーを使ってみよう

本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第12回の本号では、micro:bitの加速度センサーの機能を試す。加速度センサーにより、ボードの傾きや振動を知ることができる。I2Cという通信用のインタフェースを経由するので、まずはI2Cの使い方についてマスターしよう。

micro:bitの加速度センサー

micro:bitのボード上には加速度センサーが搭載されており、ボードに加わった加速度を計測することができる。加速度というのは速度が変化する度合いを表現する数値であり、静止している場合や一定の速度で動いている場合の加速度は0であるが、加速する場合はプラスの加速度が発生し、減速する場合はマイナスの加速度が発生する。たとえば、micro:bitを持って電車や車に乗ると、発車後には進行方向にプラスの加速度がかかり、停車前のブレーキの際にはマイナスの加速度がかかる。物体は一般に3次元の空間の中を動くので、加速度も前後方向、左右方向、高さ方向のように3次元の数値(ベクトル)で表現される。micro:bitの加速度センサーもこのような3次元の加速度データを計測する機能を持っており、3軸対応とよばれている。ボードを振ったりして振動させると、いろいろな方向に加速度がかかるので、加速度センサーにより振動の検出が可能である。


加速度センサーでは地球の重力による重力加速度も計測できる。micro:bitのボードや加速度センサーから見ると、ボードが静止している場合の水平方向の加速度は0であるが、高さ方向には一定値の重力加速度がかかっている。したがって、加速度センサーの3軸(X軸、Y軸、Z軸)の各方向に対する重力加速度の割合を計測すれば、ボードの表裏の向きやボードの傾きを知ることができる。


なお、重力は地球の中心方向に向いているが、加速度センサーで計測される重力加速度の向きはその反対で、地球から離れる方向がプラスとなる値を返す。力の向きと加速度の向きが反対になるのは、電車が加速する際に後向きにかかる力(慣性力)を感じたり、減速する際に前向きにかかる力を感じたりするのと同じ原理である。


micro:bitの加速度センサーは、ボード上にmicro:bitと書かれた表の面(LEDマトリックスのある面とは反対側)の左下に付いている。ACCELEROMETERという白文字のラベルが付いた、黒くて小さな四角い部品である。加速度センサーと同じ部品には地磁気センサー(方位センサー)も入っているので、COMPASSというラベルも書かれている。


図1 micro:bitの加速度センサーの回路図
図1 micro:bitの加速度センサーの回路図
(*1)のMotion sensorの部分より引用





micro:bitと書かれた面を上にしたままボードを水平にして、エッジコネクタが手前になる向きに置いた場合、加速度センサーのX軸はボードの左右の方向で、右側がプラス側である。Y軸は手前(エッジコネクタ側)から奥(USBコネクタ側)に向かう方向で、奥側がプラス側である。Z軸はボードの裏から表の面に向かう方向、すなわち垂直の方向で、上側(地面から離れる方向)がプラス側である。この場合、Z軸方向にプラスの重力加速度がかかった状態になっている。


µT-Kernel 3.0のプログラムからmicro:bitの加速度センサーを使うため、例によって、ハードウェア仕様や回路図(*1)を確認する。回路図では、2ページ左下のMotion sensorの欄にLSM303AGRTRと書かれたブロックが記載されており、これが加速度センサー本体の部品(モジュール)である(図1)。PDF版の回路図で加速度センサーの型番を示すLSM303AGRTRの部分をクリックすると、この部品のハードウェア仕様をまとめたドキュメント(データシート)のURLが表示される。さらに、そのURL(*2)をクリックすると、LSM303AGRのデータシートが開き、加速度センサーのハードウェア仕様が閲覧できる。なお、末尾に“TR”の付かないLSM303AGRとLSM303AGRTRの違いは、この部品がメーカーから供給される際の提供形態の違いであり、部品自体はまったく同じものである。

I2Cの通信手順

micro:bitの加速度センサーは、I2C(Inter-Integrated Circuit)とよばれる通信インタフェースを使ってTarget MCUと接続されている。I2Cとは、1980年代にオランダのフィリップス社(現在のNXP社)によって開発された通信方式であり、正式な読み方は「アイ・スクエアド・シー」であるが、「アイ・ ツー・シー」や「アイ・アイ・シー」とよばれる場合もある。I2Cでは、データを転送するSDA(Serial Data)と、ビット単位のデータ転送のタイミングを司るためのSCL(Serial Clock)とよばれる2本の信号線を使って、複数の入出力デバイスとの間で通信を行う。2本の信号線に多数のデバイスを並列に接続可能、すなわちバス結合が可能なので、入出力デバイスが増えても信号線や配線が増えないというメリットがある。I2Cは、一つの機器やボードの内部において、CPUとセンサー類などの部品同士を接続するために利用されることが多い。USBのように外部の別の機器を接続するものではないので、IT機器のエンドユーザが意識することは少ない。


I2Cの通信では、SCLとSDAを最初に操作して通信を開始する側をマスター、マスターの要求に応じる形で通信の相手をする側をスレーブとよぶ(*3)。I2Cで接続された回路やデバイスのうち、マスターは一つのみであり、通常はコンピュータ本体側のCPUなどがマスターになる。micro:bitの場合は、Target MCUのnRF52833がマスターである。一方、スレーブになるのはセンサーなどの周辺デバイスである。I2Cには複数のスレーブを接続することができ、複数のスレーブはI2Cアドレスにより識別される。I2Cアドレスは7ビットで表現されるが(*4)、予約されて使えないアドレス値もあるため、それらを除いた最大112個のアドレス値に対応したスレーブが接続可能である。micro:bitの回路図の加速度センサーの部分(図1)には、“I2C addresses (7bit): accelerometer: 0x19”という説明が書いてあるが、これはmicro:bitの加速度センサー(accelerometer)のI2Cアドレスが0x19であることを示す。同じチップ内の地磁気センサーのI2Cアドレスは0x1Eである。I2Cでは、1組の信号線(SCLとSDA)だけを使って、多数のセンサー等のデバイスを接続できるのが特長だ。


I2CではSCLとSDAの信号線を使ってビット単位のシリアル通信を行うが、I2Cを制御するハードウェアがビット単位の通信データを1バイト(8ビット)単位にまとめてくれるので、プログラムから見ると1バイト単位の通信を行うことになる。I2Cの通信手順としては、マスターからスレーブにデータを送信する「書き込み(WR:write)」の手順と、スレーブからマスターにデータを送信する「読み出し(RD:read)」の手順が定められており、複数のバイトデータを連続して送ることも可能である。これらの手順を使ったI2Cのデータ転送の状況を見ていこう。


図2 I2Cにおけるマスターからスレーブへの書き込みの通信手順
図2 I2Cにおけるマスターからスレーブへの書き
込みの通信手順
I2CアドレスがYYのスレーブに2バイトのデータを
書き込む場合

図3 I2Cにおけるスレーブからマスターへの読み出しの通信手順
図3 I2Cにおけるスレーブからマスターへの読み
出しの通信手順
I2CアドレスがYYのスレーブから2バイトのデータ
を読み出す場合

図4 I2Cスレーブのデバイス内のレジスタへの書き込み
図4 I2Cスレーブのデバイス内のレジスタへの書き
込み
I2CアドレスがYY、レジスタアドレスがBBの制御用
レジスタに、1バイトのデータDDを書き込む場合

図5 I2Cにおいて書き込みと読み出しを連続して行う場合の通信手順
図5 I2Cにおいて書き込みと読み出しを連続して
行う場合の通信手順
I2CアドレスがYYのスレーブに1バイトのデータを
書き込んでから1バイトのデータを読み出す場合

図6 I<sup>2</sup>Cスレーブのデバイス内のレジスタからの読み出し
図6 I2Cスレーブのデバイス内のレジスタからの
読み出し
I2CアドレスがYY、レジスタアドレスがBBの制御用
レジスタから、1バイトのデータEEを読み出す場合




書き込み(WR)を行う場合は、最初にマスターからスレーブにI2Cアドレスを含む1バイトのデータを送り、その後に書き込むべきデータを1バイトごとに送る。その具体的な手順は次のようになる(図2)。まず、マスターはSCLとSDAを使ってStart Conditionという信号を送り(図2の(a))、通信の開始を示す。次に1バイトのデータを送るが、この中の上位7ビットには転送相手となるスレーブのI2Cアドレスが入っており、最下位の1ビットには書き込み(WR)を表す0が入っている(b)。すべてのスレーブはこのデータを見ていて、上位7ビットが自分自身のI2Cアドレスと一致しているかどうかをチェックする。I2Cアドレスの一致したスレーブは、SDAを電圧の低いレベル(L:Low)に落とすことによってACK(Acknowledge)とよばれる1ビットの応答を返し(c)、これ以後の通信を継続する。一方、それ以外のスレーブは、今回のデータ転送の通信相手ではないことを確認したので、このデータ転送が終わるまで何も応答しない。このように、I2Cでは転送データの一種であるI2Cアドレスによって複数のスレーブを区別するので、特定のスレーブを選択するための信号線を個別に設ける必要がないのである。


マスターは、スレーブからACKが返ったことを確認した後に、書き込むべきデータの最初の1バイトを送る(d)。I2Cアドレスの一致したスレーブは、そのデータを正常に受信できたら再度1ビットのACKを返す(e)。複数のバイトデータを書き込む場合は、マスターからスレーブへの1バイトデータの送信と、スレーブからマスターへの1ビットのACKの応答を繰り返す(f)(g)。データの書き込みが終わったら、マスターからスレーブにStop Conditionという信号を送り(h)、これで一連の書き込みのデータ転送を終了する。


一方、読み出し(RD)を行う場合は(図3)、マスターが最初に送るI2Cアドレス(上位7ビット)の最下位の1ビットに、読み出し(RD)を表す1を入れる(図3の(b))。また、それ以降のデータ転送の方向やACKを送る方向は、書き込みの場合と逆になる。すなわち、I2Cアドレスの一致したスレーブは、1ビットのACKを返した後に(c)、スレーブ側から読み出したデータをマスターに送る(d)。ただし、この場合でもクロック(SCL)はマスター側から出力されているので、スレーブ側ではSCLの変化を見ながら、そのタイミングに合わせて1ビットずつデータを送る必要がある。マスター側でそのデータを正常に受信できたら、マスターからスレーブに1ビットのACKを返す(e)。複数のバイトデータを読み出す場合は、この処理を必要な回数だけ繰り返す(f)(g)。データの読み出しが終わったら、マスターからスレーブにStop Conditionの信号を送り(h)、これで一連の読み出しのデータ転送を終了する。


ところで、今回の加速度センサーの場合もそうであるが、I2Cのスレーブとなるデバイスは複数の機能や高度な機能を持っていることが多く、それらの機能を利用するためには、デバイスの内部に設けられた多数の制御用レジスタを操作する必要がある。これまでの連載で紹介したGPIO、PWM、SAADCの場合は、これらの機能に関連した制御用のレジスタがTarget MCUに内蔵されており、それらのレジスタを読み書きすることによって各機能を実現していた。加速度センサーの場合も、使い方のイメージとしてはこれと同様であるが、Target MCUの中の制御用レジスタではなく、I2C経由で接続された別のデバイスの中の制御用レジスタを操作しなければならない。加速度センサーを動作させるにはデバイス内の特定の制御用レジスタを設定する必要があるし、センサーから得られるX軸、Y軸、Z軸の加速度データを取得するにも、それぞれ特定のレジスタを読み出す必要がある(P.37の表2)。


スレーブ側のデバイスの持つ制御用のレジスタの数は数個から数十個の場合が多く、これらのレジスタは1バイト(8ビット)のアドレスで区別できる。各レジスタのサイズ(ビット幅)も、I2Cの仕様に合わせて8ビット以下の場合が多い。それでも、デバイス内のレジスタにデータを書き込もうとすると、レジスタのアドレスを指定するために1バイト、書き込むべきデータとして少なくとも1バイト、合わせて2バイト以上のデータを送る必要がある。このために、前述したような複数のバイトデータを連続して送る機能を利用する。さらに、I2Cの通信手順全体を見た場合には、この前にI2Cアドレスと書き込み(WR)の指示を示す1バイトのデータも送る必要がある(図4)。なお、ここではI2Cのスレーブとして接続されたデバイス同士を区別するための「I2Cアドレス」と、一つのデバイス内のレジスタを区別するための「レジスタアドレス」という2種類の「アドレス」が出てくるが、両者は違うものなので注意してほしい。


デバイス内のレジスタからデータを読み出す場合は、もう少し複雑な手順になる。データを読み出す場合も、読み出しの対象となるレジスタのアドレスはマスターが指定する。すなわち、レジスタのアドレスについてはマスター側からスレーブ側へのデータ転送を行う必要があり、I2Cの通信手順としては書き込み(WR)になる。一方、スレーブ側のレジスタから読み出されたデータをマスター側に転送する手順は、I2Cの読み出し(RD)である。したがって、I2Cの全体の通信手順としては、読み出し対象のレジスタアドレスをデータとした1バイトの書き込み(WR)を行った直後に、読み出されたデータをマスター側で受け取るための読み出し(RD)の手順を実行する必要がある。


このようなケースを想定した通信を行うために、I2Cでは、書き込みと読み出しを連続して行う通信手順が用意されている(図5)。この場合、通常の書き込み(WR)の最後にマスターが送るStop Conditionは送らず、その直後の読み出し(RD)では、最初のStart Conditionの代わりにRepeated Start Conditionという信号を送る(図5の(f))。この点を除き、書き込みと読み出しの通信手順は先に説明した内容とまったく同じである。書き込みや読み出しの対象となるスレーブは同一のものなので、書き込み時と読み出し時に同じI2Cアドレスが2回送られる点に注意してほしい。Repeated Start Conditionを使えば、複数回の書き込みや読み出しを連続して実行することができ、その間に別のスレーブとのデータ転送などが割り込むことはない。この機能を使ってI2Cスレーブのデバイス内の制御用レジスタからデータを読み出す際の全体的な通信手順を図6に示す。


ここまで、I2Cの通信手順(プロトコル)の概要を紹介してきた。I2Cでは、このほかにも物理的、電気的な仕様やビット単位の詳細な通信手順を定めているが、通常はハードウェアで処理されプログラミングには直接影響しないので、本稿では説明を省略している。I2Cを解説した資料はウェブサイト等にも多数出ているので、さらに興味があればそういった資料を参照されたい。

micro:bitでI2Cを使う

 

micro:bitの加速度センサーなど、I2Cのスレーブ側のデバイスを操作するためのプログラムを書くには、micro:bitのTarget MCUでI2Cの通信を行う必要がある。そのため、今回もTarget MCUであるnRF52833のマニュアルを参照する。このマニュアルでは、I2Cの通信を行う機能をTWI(Two-Wire Interface)とよんでいる。マニュアルのTWIの章には、I2Cの詳細な通信手順や、Target MCUをI2Cのマスターとして動作させる際の制御用レジスタの使い方の説明などが書かれている(*5)


Target MCUには、TWI0およびTWI1という2組(2チャンネル)のI2Cを接続することができ、TWI0に対する制御用レジスタのベースアドレスは0x40003000、TWI1に対するベースアドレスは0x40004000である。I2Cの各チャンネルに対して、表1で示すような制御用レジスタが割り当てられている。動作の開始や停止を指示するレジスタをTASKS_*、動作の完了など状態の変化を示すレジスタをEVENTS_*といった名称にしているのは、GPIO、PWM、SAADCの場合と同様である。

表1 A/D変換の基本動作に必要なSAADCのレジスタ
レジスタ名称 アドレスの
オフセット
説明
TASKS_STARTRX 0x000 TWIの受信開始
TASKS_STARTTX 0x008 TWIの送信開始
TASKS_STOP 0x014 TWIの停止
TASKS_SUSPEND 0x01C TWIの一時停止
TASKS_RESUME 0x020 TWIの再開
EVENTS_STOPPED 0x104 TWIの停止状態
EVENTS_RXDREADY 0x108 RXDに1バイトデータを受信済
EVENTS_TXDSENT 0x11C TXDの1バイトデータを送信済
EVENTS_ERROR 0x124 TWIのエラー
EVENTS_BB 0x138 TWIのバイトデータ境界
EVENTS_SUSPENDED 0x148 TWIの一時停止状態
SHORTS 0x200 イベントとタスクの間のショートカット有効
INTENSET 0x304 割込み許可
INTENCLR 0x308 割込み禁止
ERRORSRC 0x4C4 エラー要因
ENABLE 0x500 TWIの有効化
PSEL.SCL 0x508 SCLのピン選択
PSEL.SDA 0x50C SDAのピン選択
RXD 0x518 データ受信用レジスタ
TXD 0x51C データ送信用レジスタ
FREQUENCY 0x524 TWIのクロック周波数
ADDRESS 0x588 TWI転送で使用するI2Cアドレス


I2Cで使うSCLとSDAの信号線は、GPIOの入出力ピンのいずれかに割り当てられる。この指定を行うレジスタがPSEL.SCLとPSEL.SDAである。回路図(*1) を見ると、加速度センサー(Motion sensor)のSCLの信号線はI2C_INT_SCLを経由してTarget MCUのGPIOのP0.08に接続され、SDAの信号線はI2C_INT_SDAを経由してTarget MCUのP0.16に接続されている。また、micro:bitのエッジコネクタのP19がI2C_EXT_SCLを経由してTarget MCUのGPIOのP0.26に接続され、エッジコネクタのP20がI2C_EXT_SDAを経由してTarget MCUのP1.00に接続されている。すなわち、micro:bitのボード内部のI2Cで接続された加速度センサーとは別に、ボードの外部にもI2Cのスレーブとなるデバイスをエッジコネクタ(P19、P20)経由で接続することができ(*6) 、両者は別々のI2Cチャンネルとして動作する。Target MCUではTWI0とTWI1の二つのI2Cチャンネルが使えるので、ボードに搭載された加速度センサーをTWI0で使用し、ボード外部に接続するI2CデバイスをTWI1で使えばよい。


これで必要なハードウェア情報は揃ったので、あとは上記のマニュアルやI2Cの通信手順を書いたドキュメントを丹念に読んでいけば、I2C経由で加速度センサーと通信するプログラムを書けるはずである。しかし、今回は先を急ぎたいので、パーソナルメディアが作成したmicro:bit用のI2Cドライバをそのまま利用することとし、I2Cの通信プログラムの動作の詳細に関する説明は省略する。I2Cドライバのソースコードには要所にコメントが入っており、割込みを使ったµT-Kernel用の通信プログラムとしても実用的な例になっているので、ぜひともソースコード全体を読んで具体的な動作を理解してほしい。


このI2Cドライバには、ヘッダファイルiic.hとドライバ本体のnrf5_iic.cが含まれており、iic.hのコメントの中にドライバの使い方の説明がある(リスト1)。ドライバの中には、I2C経由のデータ転送を行う基本的な関数としてiicxferがあり、これを実行することでI2Cの送信や受信ができる。この関数の中では、送信や受信の終了を待つための待ち状態(tk_wai_flgを利用)に入ることがある。また、すでに別のタスクが同じチャンネルのI2Cでデータ転送中の場合には、そのデータ転送が終了するまで待たされる場合もある(高速マルチロックMLockを使った排他制御を利用)。このため、割込みハンドラや割込み禁止状態からこの関数を呼び出すことはできない。送信や受信の終了は割込みによって通知されるが、割込み関連の操作はドライバの内部で処理されるため、ドライバの利用者が意識する必要はない。


リスト1 micro:bit用I2Cドライバのヘッダファイル(iic.h)

/*
*  @(#)iic.h 2024-04-18
*
*  I2C 入出力インタフェース定義
*  Copyright (C) 2024 by Personal Media Corporation
*
*  I2C を IIC と表記している。
*/
              ・
              ・
              ・
/*
   IIC 入出力

   ER iicxfer(W ch, UH *cmddat, W words, W *xwords)

   IIC デバイスに対し、データの送受信を行う。

       ch: 使用 IIC チャンネル
       cmddat: コマンド/送受信データを格納する領域の先頭ポインタ
       words:  コマンド/送受信データのワード数
       xwords: 成功したコマンド/送受信データのワード数
           (NULL の場合は格納しない)

       戻り値: E_OK もしくはエラー (E_IO, E_TMOUT, E_PAR)

   割込みハンドラ、もしくは割込みが禁止された状態から呼び出しては
   ならない。

   コマンド/送受信データは、以下のように記述する。

   例1)    1バイトのデータを送信する場合
       {
           IIC_START   | (address << 1) | 0x00,    // R/W# = 0
           IIC_SEND    | IIC_TOPDATA  | IIC_LASTDATA | txdata0,
           IIC_STOP,
       }

       送信するデータは、IIC_SEND を指定したワードの下位 8bit に
       格納しておく。

   例2)    複数バイトのデータを送信する場合
       {
           IIC_START   | (address << 1) | 0x00,    // R/W# = 0
           IIC_SEND    | IIC_TOPDATA  | txdata0,
           IIC_SEND    |                txdata1
               :
           IIC_SEND    | IIC_LASTDATA | txdataN,
           IIC_STOP,
       }

   例3)    送受信の切り替えを伴う場合
       {
           IIC_START   | (address << 1) | 0x00,    // R/W# = 0
           IIC_SEND    | IIC_TOPDATA  | IIC_LASTDATA | txdata0,
           IIC_RESTART | (address << 1) | 0x01,    // R/W# = 1
           IIC_RECV    | IIC_TOPDATA,              // rxdata0
           IIC_RECV,                               // rxdata1
               :
           IIC_RECV    | IIC_LASTDATA,             // rxdataN
           IIC_STOP,
       }

       受信したデータは、IIC_RECV を指定したワードの下位 8bit に
       格納される (上位 8bit は不変)。

   コマンド/送受信データの最終ワードは、必ず IIC_STOP を記述すること。

   転送中にエラーが発生した場合は、ストップコンディションを自動的に
   発生させる。
*/

IMPORT ER iicxfer(W ch, UH *cmddat, W words, W *xwords);

#define IIC_RESTART (3 << 14)       // リスタートコンディションを送信
#define IIC_START   (2 << 14)       // スタートコンディションを送信
#define IIC_STOP    (1 << 14)       // ストップコンディションを送信
#define IIC_SEND    (1 << 13)       // データを送信
#define IIC_RECV    (1 << 12)       // データを受信
#define IIC_TOPDATA (1 << 11)       // 送受信データの先頭
#define IIC_LASTDATA    (1 << 10)   // 送受信データの末端

IMPORT ER iicsetup( BOOL start );
              ・
              ・
              ・


図7 I<sup>2</sup>Cドライバで使用するコマンドパケットの個々のデータの構成
図7 I2Cドライバで使用するコマンドパケットの個々のデータの構成





関数iicxferでは四つの引数を指定する。1番目のchはI2Cのチャンネルを示すものであり、0でTWI0を使ったボード内部のI2C、1でTWI1を使った外部デバイス用のI2Cを指定する。加速度センサーはTWI0に接続されているので、今回のプログラムではchに0を指定する。2番目のcmddataは最も重要な引数であり、データ通信用のコマンドパケット(メモリブロック)の先頭アドレスを示す。3番目のwordsはこのコマンドパケットのデータ数を示す引数であり、4番目のxwordsは転送に成功したデータ数を返す領域(変数)へのポインタである。


cmddataで指定されるコマンドバケットは16ビット幅のデータ(データタイプUH)の配列として構成されており、各データの下位8ビットには、I2Cで転送する1バイト単位のデータやI2Cアドレスが入る。データの書き込み(WR)の場合は、送信するデータをコマンドパケットの下位8ビットに設定してからiicxferを呼び出す。つまり、下位8ビットの部分はiicxferの実行に対する引数になっており、iicxferの実行後も値は変化しない。一方、データの読み出し(RD)の場合は、iicxferの実行により受信したデータがコマンドパケットの下位8ビットに設定される。つまり、下位8ビットの部分はiicxferを実行した結果を返す戻り値を入れる領域になっており、iicxferの実行前の値は上書きされて消える。


コマンドバケットの個々のデータの構成を図7に示す。上位8ビットには、転送の開始や終了を示すフラグ、データの送信と受信を区別するフラグ、複数バイトの連続した書き込みや読み出しを指定するフラグなどが入る。この部分は書き込み、読み出しのいずれの場合もiicxferの実行に対する引数であり、iicxferの実行後も値は変化しない。I2Cドライバでは、これらのフラグの指定に応じて、I2Cの信号線にStart Conditionなどの信号を送る。


コマンドバケットの配列要素である16ビットのデータとI2Cで転送するバイトデータ(I2Cアドレスを含む)は一対一に対応しており、一つの16ビットデータでI2Cアドレスの送信、1バイトデータの書き込み(WR)、1バイトデータの読み出し(RD)のいずれかを指定する。複数のバイトデータを連続して転送したり、転送の途中で書き込み(WR)と読み出し(RD)を切り替えたりすることも可能である。


最初にI2Cアドレスを送る場合は、上位8ビットにI2C転送の最初を示すIIC_STARTを指定し、下位8ビットのうちの上位7ビットにはI2Cアドレスを指定する。最下位のビットは、書き込み(WR)の場合に0、読み出し(RD)の場合に1とする。すなわち、I2Cによるデータ転送の最初にマスター側から送信する8ビットのデータを、そのままコマンドパケットの下位8ビットに入れる。I2Cドライバでは、IIC_STARTのフラグを見てStart Conditionの信号を送ってから、下位8ビットのデータ、すなわちI2Cアドレスに書き込み(WR)と読み出し(RD)の指定を加えたデータを送る。


途中で書き込み(WR)と読み出し(RD)を切り替える場合は、転送の切り替えを示すIIC_RESTARTを上位8ビットに指定する。下位8ビットについてはIIC_STARTの場合と同じであり、このうちの上位7ビットにI2Cアドレスを、最下位のビットに書き込み(WR)か読み出し(RD)かの指定を入れる。I2Cドライバでは、IIC_RESTARTのフラグを見てRepeated Start Conditionの信号を送ってから、下位8ビットのI2Cアドレスなどを送る。


IIC_STARTやIIC_RESTARTの後でデータの書き込み(WR)を行う場合は、データ送信を示すIIC_SENDを上位8ビットに指定する。また、連続したデータ転送の区切りを示すために、最初に書き込むバイトデータにはIIC_TOPDATA、最後に書き込むバイトデータにはIIC_LASTDATAのフラグを指定する。書き込みを行うデータが1バイトのみであれば、そのデータに対して、IIC_TOPDATAとIIC_LASTDATAの両方のフラグを指定する。下位8ビットには、書き込みを行う1バイト単位のデータを設定しておく。

データの読み出し(RD)を行う場合は、データ受信を示すIIC_RECVを上位8ビットに指定する。また、書き込みの場合と同じく、連続したデータ転送の区切りを示すために、IIC_TOPDATAとIIC_LASTDATAのフラグを指定する。下位8ビットには、iicxferの実行によってスレーブ側から読み出されたデータが、1バイト単位で格納される。


コマンドパケットの最後には、データ転送の終了を示すフラグとしてIIC_STOPを入れた16ビットのデータを置く。I2Cドライバでは、このフラグを見てStop Conditionの信号を送り、I2Cのデータ転送を終了する。


I2Cのドライバとしては、このほかにドライバの起動や終了の処理を行うためのiicsetupという関数が用意されており、I2Cを使う際は最初にこの関数を呼び出しておく。各チャンネルのSCLとSDAに対するGPIOピンの割り当てについても、この関数の中で設定している。


なお、ここではiicxferなどの関数をI2Cの「ドライバ」とよんでいるが、これはµT-Kernel 3.0仕様書のデバイス管理機能で定義されるデバイスドライバではない。デバイスを操作するという機能面の意味では「ドライバ」であるが、ソフトウェア構成上の位置づけとしては、汎用的な処理を行うライブラリ関数のようなものである。

I2Cデバイスのレジスタ操作

 

I2Cドライバのiicxferにより、I2Cを使った基本的な通信はできるようになった。iicxferは柔軟で自由度の高い通信機能を提供しており、いろいろなI2Cデバイスに対応できる。しかし、通信を行うたびにコマンドパケットを用意する必要があり、使用方法がやや煩雑である。加速度センサーなどのI2Cデバイスを使うには、デバイス側の制御用レジスタの読み書きができればよいので、そのような用途に対して使いやすい関数が欲しい。自分で作っても難しいものではないが、I2Cドライバと一緒に使う想定でパーソナルメディアが作成したレジスタ操作用の関数があるので、今回はこれをそのまま利用する。ソースコードのファイル名はiic_reg.cであり(リスト2)、これを読めば、I2Cドライバ(iicxfer)の具体的な使い方を確認できる。


iic_reg.cで利用できる関数には、I2Cのスレーブデバイスの一つのレジスタに対して書き込みを行うwrite_regと、読み出しを行うread_regがある。write_regには三つの引数があり、1番目のadrで対象となるスレーブデバイスのI2Cアドレスを指定する。2番目のregと3番目のdatでは、書き込みを行うレジスタのアドレスと1バイトの書き込みデータを指定する。read_regには二つの引数があり、1番目のadrでI2Cアドレスを、2番目のregで読み出しを行うレジスタのアドレスを指定する。読み出した1バイトのデータは戻り値として返る。なお、使用するI2Cのチャンネルは0に固定しているので(I2C_CH=0)、ボード外部に接続したI2Cデバイスに対しては使えない。


このほか、六つのレジスタに対して一括して書き込みや読み出しを行うwrite_reg_6とread_reg_6が用意されている。加速度センサーから取得したいデータはX軸、Y軸、Z軸の3軸に対する加速度であるが、各軸のデータは2バイト(16ビット)なので、8ビットのレジスタを二つずつ使う。すなわち、3軸の加速度データは合わせて6個のレジスタに分かれて入っている。この6個のレジスタはアドレスが連続しており、I2Cの複数バイトデータの読み出し手順を利用して一括して読み出すことができる。また、今回は使用しないが、LSM303AGRに含まれている地磁気センサーを使う際も、X軸、Y軸、Z軸に対して各2バイト、合わせて6個のバイトデータを扱う必要があるし、補正用として6個のバイトデータをデバイス側のレジスタに書き込みたい場合もある。こういった用途を想定した関数がwrite_reg_6とread_reg_6である。


write_reg_6やread_reg_6では、3番目の引数が6個のバイトデータを入れる配列へのポインタになっており、write_reg_6ではこの配列の6個のデータを6個のレジスタに書き込む。read_reg_6では、6個のレジスタから読み出したデータをこの配列に格納する。1番目と2番目の引数はwrite_reg_6やread_reg_6と同様であり、1番目のadrでI2Cアドレスを、2番目のregで書き込みや読み出しを行うレジスタのアドレス(先頭アドレス)を指定する。


ところで、LSM303AGRなどのデバイスで使用するレジスタアドレスは実際には7ビットのみであり、レジスタアドレスの最上位ビット(MSB)には特殊な意味を持たせている。最上位ビット(MSB)が1の場合は、レジスタアドレスを一つずつ増やしながら連続したデータ転送を行うのである。この機能を利用するために、write_reg_6やread_reg_6では、レジスタアドレスを入れた8ビットデータの最上位ビット(MSB)に1を設定している。

 

リスト2 I2Cデバイスのレジスタ操作用の関数(iic_reg.c)

/*
*  @(#)iic_reg.c 2024-05-01
*
*  I2Cデバイスのレジスタ操作用の関数 (micro:bitのI2C_INT用)
*  Copyright (C) 2024 by Personal Media Corporation
*/
             ・
             ・
             ・
#define I2C_CH      0   /* I2C チャンネル (micro:bitのI2C_INT用) */

#define WR(da)      (((da) << 1) | 0)
#define RD(da)      (((da) << 1) | 1)

/*
 * レジスタ書き込み
 */
EXPORT ER write_reg( INT adr, INT reg, UB dat )
{
    UH  c[4];
    W   n;
    ER  err;

    c[0] = IIC_START | WR(adr);
    c[1] = IIC_SEND  | IIC_TOPDATA  | reg;
    c[2] = IIC_SEND  | IIC_LASTDATA | dat;
    c[3] = IIC_STOP;

    err = iicxfer(I2C_CH, c, sizeof(c) / sizeof(UH), &n);

#if VERBOSE
    tm_printf("write_reg 0x%02x 0x%02x <- 0x%02x : n=%d err=%d\n",
          adr, reg, dat, n, err);
#endif

    return err;
}
              ・
              ・
              ・
/*
 * レジスタ6個読み出し
 */
EXPORT ER read_reg_6( INT adr, INT reg, UB dat[6] )
{
    UH const cmd[] = {
        IIC_START   | WR(0),            /* [0] adr */
        IIC_SEND    | IIC_TOPDATA | IIC_LASTDATA | 0x80, /* [1] reg */
        IIC_RESTART | RD(0),            /* [2] adr */
        IIC_RECV    | IIC_TOPDATA,      /* [3] dat[0] */
        IIC_RECV,                       /* [4] dat[1] */
        IIC_RECV,                       /* [5] dat[2] */
        IIC_RECV,                       /* [6] dat[3] */
        IIC_RECV,                       /* [7] dat[4] */
        IIC_RECV    | IIC_LASTDATA,     /* [8] dat[5] */
        IIC_STOP
    };
    UH  c[sizeof(cmd) / sizeof(H)];
    W   n, i;
    ER  err;

    /* コマンドパケット生成 */
    knl_memcpy(c, cmd, sizeof(c));
    c[0] |= WR(adr);
    c[1] |= reg;
    c[2] |= RD(adr);

    /* レジスタ読み出し */
    err = iicxfer(I2C_CH, c, sizeof(c) / sizeof(UH), &n);

    /* データ取り出し */
    for ( i = 0; i < 6; ++i ) {
        dat[i] = c[3 + i] & 0xff;
    }

#if VERBOSE
    tm_printf("read_reg_6 0x%02x 0x%02x -> "
          "0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x : "
          "n=%d err=%d\n",
          adr, reg,
          dat[0], dat[1], dat[2], dat[3], dat[4], dat[5],
          n, err);
#endif

    return err;
}


加速度センサーのプログラミングと動作確認

 

I2Cの説明のために前置きが長くなってしまったが、いよいよ今回のメインテーマである加速度センサーの具体的な使い方について説明する。


加速度センサーの使い方は、LSM303AGRのデータシート(*2) に記載されている。そのうち、今回使用するLSM303AGRの制御用レジスタの一覧を表2に示す。この加速度センサーには多くの機能があり、ほかにも多くのレジスタを持っているが、表2のレジスタを使えば最小限の動作確認が可能である。

表2 本稿のプログラムで使用する加速度センサーの
制御用レジスタ
レジスタ名称 タイプ(*) レジス
タアド
レス
初期値 説明
WHO_AM_I_A R 0x0F 0x33 デバイス識別 (Who am I)
CTRL_REG1_A R/W 0x20 0x07 加速度センサー制御_1
OUT_X_L_A R 0x28 Output 加速度センサー出力_X軸下位バイト
OUT_X_H_A R 0x29 Output 加速度センサー出力_X軸上位バイト
OUT_Y_L_A R 0x2A Output 加速度センサー出力_Y軸下位バイト
OUT_Y_H_A R 0x2B Output 加速度センサー出力_Y軸上位バイト
OUT_Z_L_A R 0x2C Output 加速度センサー出力_Z軸下位バイト
OUT_Z_H_A R 0x2D Output 加速度センサー出力_Z軸上位バイト

(*) タイプ欄のRは読み出し専用のレジスタ、R/Wは読み出しと書き込みが可能なレジスタを示す。


このうち、WHO_AM_I_Aはデバイスの識別用に固定値を返すレジスタである。加速度センサーの機能とは直接関係しないが、動作確認のために使用した。重要なのは次のCTRL_REG1_Aのレジスタである。このうちの上位4ビット(ODR)ではデータレート(データの更新や転送の頻度)を指定し、下位4ビットではローパワーモード(LPen)、および加速度センサーのZ軸(Zen)、Y軸(Yen)、X軸(Xen)の動作の有効化を指定する(図8)。ODRの初期値はパワーダウンモードを示す0になっているため、これを設定しないと加速度センサーが動作しない。それ以下のOUT_X_L_Aなどのレジスタは、X軸、Y軸、Z軸の加速度データの上位8ビット(H)と下位8ビット(L)を返すレジスタである。

図8 LSM303AGRのCTRL_REG1_Aレジスタの構成
図8 LSM303AGRのCTRL_REG1_Aレジスタの構成
(*2 の資料の一部を要約)

I2Cドライバとiic_reg.cの関数を使って上記のレジスタを操作し、加速度センサーの動作確認を行うプログラムをリスト3に示す。app_main.cの最初では、iic関係のヘッダファイルのincludeを追加している(リスト3の(※A))。また、LSM303AGRのI2Cアドレスや使用するレジスタのアドレスをdefineマクロで定義し、iic_reg.cで定義された関数をインポートする。


usermainでは、最初にiicsetupを実行してI2Cドライバを起動してから(※B)、加速度センサーのWHO_AM_I_Aレジスタを読み出して(※C)、その値をコンソールに表示する。次に、write_regを使ってCTRL_REG1_Aに0x57を設定する(※D)。この設定値は100Hz(ODR=5)、ノーマルモード(LPen=0)、X軸、Y軸、Z軸とも有効(Xen=Yen=Zen=1)を意味する。ノーマルモードの場合、加速度データの出力は10ビットである注7)。その後、CTRL_REG1_Aに設定された値を再度読み出して確認し、コンソールに表示する(※E)。これで加速度センサーが動き出すはずなので、これ以後はfor文の無限ループの中で約500ミリ秒ごとに加速度のデータを取得し、コンソールに表示する処理を繰り返す。


for文の中では、read_reg_6を使ってX軸、Y軸、Z軸の加速度データをまとめて取得するが(※F)、以下で説明するような後処理が必要なので、そのための関数としてget_acc10bitを作成した(※G)。まず、データの上位8ビットと下位8ビットが分かれているので、それらを一つの16ビットデータにまとめる必要がある(※H)。また、LSM303AGRのデータシート(*2) のOUT_X_L_Aなどの説明には“The value is expressed as two's complement left-justified.”と書かれているので、これに対する処理を行う。ノーマルモードで有効なデータは16ビットのうちの10ビットのみであるが、このデータは左詰め、すなわち上位ビット側に入っているので、左詰めを解消するために右に6ビットシフトする必要があるのだ(※J)。このデータは2の補数、すなわち符号付き整数なので、データタイプとしてはUHではなくHを使う(※K)。


プログラムができたので、早速Eclipseを使ってコンパイルし、実行してみよう。今回はソースコードとしてiic.h、nrf5_iic.c、iic_reg.cが増えているので、これらのファイルをusermainが含まれるapp_main.cと同じフォルダ、すなわちapp_sampleフォルダの下に追加する必要がある。


そのためには、Eclipseの左ペインでプロジェクトのタブを選択し(図9①)、「mtkernel_3」の左側の「>」の部分(図9②)をクリックして下位のフォルダを展開してから、その中のapp_sampleフォルダ(図9③)の上に追加したいファイルをドラッグする。そうすると、そのファイルをコピー(Copy)するかリンク(Link)するかを確認するパネルが表示される(図10)。ここでCopyを選ぶと、これらのファイルがapp_sampleフォルダの下にコピーされる。

 

リスト3 I2C経由の加速度センサーの動作確認プログラム

/*---------------------------------------------------------------
*  I2C経由の加速度センサーの動作確認(μT-Kernel 3.0用)
*
*  Copyright (C) 2022-2024 by T3 WG of TRON Forum
*---------------------------------------------------------------*/
#include 
#include 

#include "iic.h"            // I2C関連ファイルのインクルード(※A)

//-------- LSM303AGRのI2Cアドレス -------------------------------
  #define IICADR_ACC  0x19    // 加速度センサー
  #define IICADR_MAG  0x1e    // 地磁気センサー

//-------- LSM303AGRの加速度センサーのレジスタアドレス ----------
  #define WHO_AM_I_A  0x0F    // デバイス識別(Who am I)
  #define CTRL_REG1_A 0x20    // 加速度センサー制御_1
  #define OUT_X_L_A   0x28    // 加速度センサー出力_X軸下位バイト
  #define OUT_X_H_A   0x29    // 加速度センサー出力_X軸上位バイト
  #define OUT_Y_L_A   0x2A    // 加速度センサー出力_Y軸下位バイト
  #define OUT_Y_H_A   0x2B    // 加速度センサー出力_Y軸上位バイト
  #define OUT_Z_L_A   0x2C    // 加速度センサー出力_Z軸下位バイト
  #define OUT_Z_H_A   0x2D    // 加速度センサー出力_Z軸上位バイト

//-------- iic_reg.cで定義された関数のインポート ----------------
  IMPORT INT read_reg( INT adr, INT reg );
  IMPORT ER write_reg( INT adr, INT reg, UB dat );
  IMPORT ER read_reg_6( INT adr, INT reg, UB dat[6] );

 //---------------------------------------------------------------
  // 加速度データの後処理(※G)
  LOCAL INT get_acc10bit(UB L_dat, UB H_dat){

      H   dat16bit, ret_dat;                  // 符号付き16ビット整数(※K)

      // 上位8ビットと下位8ビットを16ビットデータにまとめる(※H)
      dat16bit = (H_dat << 8) | L_dat ;

      // 左詰めだった10ビットの符号付きデータを右に6ビットシフト(※J)
      ret_dat = dat16bit >> 6;

      return(ret_dat);
  }

  //---------------------------------------------------------------
   EXPORT void usermain( void )
   {
      INT dat, x_acc, y_acc, z_acc;
      UB  outreg[6];
      ER  err;

      iicsetup(TRUE);                             // I2Cドライバ起動(※B)

      // WHO_AM_I_Aの読み出し(※C)
      dat = read_reg(IICADR_ACC, WHO_AM_I_A);
      tm_printf("WHO_AM_I_A = 0x%02x\n", dat);

      // ODR=5, LPen=0(※D)
      write_reg(IICADR_ACC, CTRL_REG1_A,  0x57);

      // CTRL_REG1_Aの読み出し(※E)
      dat = read_reg(IICADR_ACC, CTRL_REG1_A);
      tm_printf("CTRL_REG1_A = 0x%02x\n\n", dat);

      for(;;){
            // X軸、Y軸、Z軸の上位バイトと下位バイトをまとめて読み出す(※F)
            err = read_reg_6(IICADR_ACC, OUT_X_L_A, outreg);

            // get_acc10bitによる後処理
            x_acc = get_acc10bit(outreg[0], outreg[1]);
            y_acc = get_acc10bit(outreg[2], outreg[3]);
            z_acc = get_acc10bit(outreg[4], outreg[5]);

            tm_printf("Acc: x,y,z=%4d,%4d,%4d\n", x_acc, y_acc, z_acc);

            tk_dly_tsk(500);       // 約500ミリ秒の周期で永久に繰り返し
      }
  }


図9 Eclipseにソースファイルを追加
図9 Eclipseにソースファイルを追加
追加先フォルダ(app_sample)の上に追加したいファイルをドラッグする。

図10 Eclipseでソースファイルを追加する際のCopyとLinkの選択
図10 Eclipseでソースファイルを追加する際のCopyとLinkの選択


リスト4 加速度センサーの動作確認プログラムのコンソール出力

microT-Kernel Version 3.00

WHO_AM_I_A = 0x33
CTRL_REG1_A = 0x57

Acc: x,y,z= -10,  -6, 237	←ボードが水平(micro:bitの文字が上)
Acc: x,y,z=  -8,  -4, 247
Acc: x,y,z= -14, -11, 240
Acc: x,y,z=  36,   2, 241
Acc: x,y,z=  97,   3, 224	←ボードの左側を下に傾ける
Acc: x,y,z= 127,  17, 208
Acc: x,y,z= 144,   6, 201
Acc: x,y,z=  11,  12, 259
Acc: x,y,z=-112,  -5, 214	←ボードを右側を下に傾ける
Acc: x,y,z=-165,   2, 192
Acc: x,y,z=  -5,   5, 243
Acc: x,y,z= -11,-128, 204	←ボードを手前側を下に傾ける
Acc: x,y,z=  -3,-171, 167
Acc: x,y,z= -12, -11, 272
Acc: x,y,z=  30, -26,-184	←ボードを裏返す
Acc: x,y,z=  -4,  16,-252
Acc: x,y,z=  37, -55, 357
Acc: x,y,z= 445, 187, 367	←ボードを振って振動させる
Acc: x,y,z=-415, 165, 116
Acc: x,y,z=  78,  42,-124
Acc: x,y,z=-351,  21,-431



app_main.cもリスト3の内容に入れ替える必要があるが、古いapp_main.cを一旦削除してから上記と同じ方法で新しいapp_main.cを追加してもよいし、Eclipseのエディタでファイルの中身全体を入れ替えてもよい。


なお、ビルドツール(make)用の定義を示すMakefileについては、*.cのソースプログラムから*.oのオブジェクトコードを生成するという規則が自動的に適用されるため、上記のファイルが増えても特に修正する必要はない。この後のコンパイル、ビルドとプログラム実行の操作方法は、以前に説明したとおりである。


リスト3のプログラムを実行した際のコンソール出力をリスト4に示す。ボードのmicro:bitと書かれた面を上にして水平にすると、X軸とY軸の加速度はほぼ0で、Z軸にかかる重力加速度が240前後になっている。LSM303AGRのデータシートによれば、ノーマルモードにおける加速度データの単位は4[mg/digit]となっており(*7) 、mは「ミリ(1/1000)」の意味なので、Z軸方向に計測された加速度は240×4mg=960mg=0.96gと計算できる。gは重力加速度を表すので、ほぼ期待通りの値であることが確認できた。さらに、ボードを傾けたり、裏返しにしたり、振ってみたりすると、コンソールに出力されるX軸、Y軸、Z軸の加速度が変化することがわかる。



* * *

今回はI2Cという通信インタフェースを使って加速度センサーの動作を確認した。I2Cは各種のセンサーなどを接続するために使われることが多く、IoTには不可欠な技術の一つである。micro:bitでは、加速度センサーのほかに地磁気センサーもI2Cで接続されているし、エッジコネクタ経由で外付けのI2Cデバイスに接続することも可能である。micro:bitのオプション品にもI2Cで接続できるものが多いので、仕様をよく確認したうえで、他のデバイスとの接続も試してみてほしい。


■ 本稿で説明したプログラムのソースコードのダウンロード
https://www.personal-media.co.jp/book/tw/tw_index/384.html

ソースコードをご利用になる前に、必ず上記ページ掲載の「ご利用条件およびご利用上のご注意」をお読みください。

(*1)
 micro:bitの回路図
https://github.com/microbit-foundation/microbit-v2-hardware/blob/main/V2.00/MicroBit_V2.0.0_S_schematic.PDF
(*2)
 加速度センサーLSM303AGRTRのデータシート
https://www.st.com/resource/en/datasheet/lsm303agr.pdf
(*3)
 I2Cの最新の規格では、「マスター」と「スレーブ」の用語を「コントローラ」と「ターゲット」に変更している。
(*4)
 説明は省略するが、I2Cで10ビットのI2Cアドレスを使うことも可能である。
(*5)
 nRF52833のTWI(I2C)のマニュアル
https://infocenter.nordicsemi.com/topic/ps_nrf52833/twi.html
(*6)

 micro:bit V2のエッジコネクタ端子
https://tech.microbit.org/hardware/edgeconnector/

(*7)
 LSM303AGRのデータシート注2) P.27の「Table 14. Operating mode selection」を参照。
  • 本ページは、「TRONWARE Vol.207」の掲載記事「micro:bitでµT-Kernel 3.0を動かそう 第12回 I2C経由で加速度センサーを使ってみよう 」をWebで公開したものです。

ページの先頭に戻る

 

次回の連載記事に進む