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

目次に戻る

前回の連載記事に戻る

[第13回]2台のmicro:bitをシリアル通信で接続

本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第13回の本号では、micro:bitを2台用意して、その間での通信を試みる。送信1本、受信1本の信号線で接続してデータを転送するために、UARTEとよばれる機能を使ったシリアル通信を行う。µT-KernelのプログラムとMakeCodeで開発したプログラムを連携させることも可能なので、micro:bitの応用範囲がさらに拡がるはずだ。

シリアル通信とUART

micro:bitは手軽に購入できる小型のボードなので、複数台のmicro:bitを持っている人もいるだろう。そうすると、2台のmicro:bitを同時に動かして、何らかの連携動作ができれば面白いのではないか。そこで、今回は2台のmicro:bitのエッジコネクタ同士を接続し、その間で通信を行うことを考えてみよう。


micro:bitのエッジコネクタ端子(*1) には、連載第5回(*2) で説明したGPIOのピンが接続されている。したがって、2台のmicro:bitのエッジコネクタの一部の端子をワニ口クリップの電線などを使って相互に接続すれば、何らかの信号を送ること、つまり通信することは可能である。たとえば、エッジコネクタの左端付近にある「0」と書かれた大きなリング状の端子(RING0)は、GPIOのP0.02に接続されている。そこで、2台のmicro:bitのRING0同士を電線で接続すれば、一方のmicro:bitでGPIO P0.02に出力したデータ(0または1)を、もう一方のmicro:bitのGPIO P0.02から入力して読み取ることができる。


これで最小限の通信はできるはずだが、1本の信号線について0または1という1ビットのデータしか送れないのは非常に効率が悪い。より多くの情報を送るにはどうすればよいだろうか。一つの方法は、データを送る信号線を増やすことである。エッジコネクタ端子は、リング状の大きなものが三つ(RING0、1、2)、細かい短冊状のものまで含めると、P0(=RING0)からP16とP19、P20の19本がある。ただし、これらのエッジコネクタ端子には、LEDやボタンスイッチなどと共用しているものもあるので、実際にすべての端子をデータ通信に使えるわけではない。それでも、このうちの10本を接続すれば10ビットのデータ通信が可能となり、英数字1文字程度の情報を送ることはできる。


図1 1秒間に5ビットのデータを転送するシリアル通信の例
図1 1秒間に5ビットのデータを転送するシリアル通信の例

図2 UART通信のデータ通信用の信号線と接続方法
図2 UART通信のデータ通信用の信号線と接続方法

図3 UART通信による1バイトデータの通信手順
図3 UART通信による1バイトデータの通信手順





しかし、10ビットのデータを送信できても、通信のデータ量としては五十歩百歩であり、実用的にはもっと多くのデータを送る必要がある。そのためには、複数のビットデータを逐次的に信号線に載せていく方法が考えられる。たとえば、1秒間を5分割して、最初の0.2秒が経過するまでは一つ目のビット、0.4秒までは二つ目のビット、といった手順で0.2秒ごとにデータを送るようにすれば、1本の線でも1秒間に5ビットのデータを送信できる(図1)。このように、1本の信号線のみを使って複数のビットデータを逐次的に流していく通信方式を、広い意味でのシリアル通信(方式)とよぶ。これは古くから実用的に使われてきた方式であり、現在普及しているUSBや有線LAN(Ethernet)も広い意味でのシリアル通信方式を使っているし、前回説明したI2Cもそうである。


シリアル通信方式の一種に「調歩同期式シリアル通信」とよばれる通信方式がある。単に「シリアル通信」といった場合には、この調歩同期式を意味する場合がほとんどである。この通信方式はコンピュータの登場以前から使われており、テレタイプとモデムをつなぐ規格として有名なRS-232でも調歩同期式シリアル通信を行っていた。RS-232は、マイコンが誕生した1970年前後の時代に、当時のホストコンピュータ(メインフレーム)やミニコン、ごく初期のマイコンと、その端末を接続するために用いられた。その後はPCと通信用のモデムを接続するために利用され、PCには「シリアルポート」や「RS-232」などの名称でそのためのコネクタが付いていた。RS-232のコネクタはD-subとよばれる細長い台形状のもので、初期のPCまでは25ピン、DOS/V以降のPCでは9ピンであった。


当時はこの規格で接続されるPC用の周辺機器も多かったし、トロンフォーラムの前身であるT-Engineフォーラムが2002年に組込みボード用の仕様として標準化したT-Engineボードの仕様においても、コンソール端末との接続のためにシリアルポートとRS-232の接続ケーブルを備えていた。しかし、その後USBが普及するにつれて、ほとんどの周辺機器との通信インタフェースはUSBに変わっていったため、現在のPCでシリアルポートやRS-232を利用することは減っている。とはいえ、最近でも産業用PCなどではRS-232を持つものがあるし、開発中のボードのデバッグなどを行う際にはシリアルポート経由のシリアル通信を使う場合がある。


調歩同期式シリアル通信を行うために、PCやマイコンのボード上などに搭載する入出力デバイスをUART(Universal Asynchronous Receiver/Transmitter)とよぶ。UARTでは、1バイト(8ビット)単位のデータと、1ビット単位の逐次的な(=シリアルの)データとの相互変換を行うことによって、シリアル通信を行う。このため、調歩同期式シリアル通信を「UART通信」とよぶことがあり、以下、本稿でもこの名称を使うことにする。


UART通信では二つの機器の間の通信のみが可能であり、一方の機器の送信用の信号線を相手側の機器の受信用の信号線として接続するだけである(図2)。各信号線のデータ転送方向も固定されているので、仕様はシンプルである。なお、送受信用の信号線のほかに、通信の可否や準備状況などを示すフロー制御用の信号線(CTS、RTSなど)を接続する場合もあるが、今回は使用しないので説明を省略する。


各ビットのデータを送る時間的なタイミングは、ボーレート(baud rate)とよばれるデータ転送速度によって決められる。I2Cのように、データ用とは別にクロック用の信号線を設けているわけではない。ボーレートとは、1ビットのデータの送受信にかかる時間の逆数であり、大雑把にいえば1秒間に通信可能なビット数である(*3) 。単位はbits per secondの略でbpsを使う。図1の例では、1ビットあたりのデータの送受信時間が0.2秒なので、ボーレートはその逆数の5bpsとなる。ただし、実際の送受信データに加えて、通信の始まりや終わりを示すためのスタートビットやストップビット(後述)も送る必要があるため、1秒間に通信可能な実際のデータのビット数は、ボーレートよりもやや少なくなる。


送信側と受信側の想定するボーレートはあらかじめ合意しておく必要があり、ボーレートが一致していないと通信ができない。これは、頭では分かっていても、実際に機器を動かす現場では忘れやすい要注意点だ。micro:bitを含めて、多くのボードやPCで利用可能なボーレートとしては、ある程度のパターンが決まっており、1,200bps、9,600bps、38,400bps、115,200bpsなどが用いられる。


データの通信は8ビット、すなわち1バイト単位で行う(*4) 。通信していない状態では、送信側が信号線上のデータを‘1’の状態で保持している。通信を始める際は、「スタートビット」として、まず‘0’を送る。具体的には、各データビットと同じ時間だけ信号線上のデータを‘0’とする。受信側はこれを見て通信の開始を認識する。その後は、送信側が各ビットのデータを最下位ビット(LSB)側から順に1ビットずつ送っていく。各ビットのデータを保持する時間はボーレートの逆数である。8ビットのデータを送り終えたら、最後に「ストップビット」として‘1’を送る。これで8ビットのデータ転送を終了する。この通信手順を図3に示す。

nRF52833のUARTE

 

それでは、micro:bitでUARTを使った通信を行うプログラミングを始めよう。今回もまず、Target MCUであるnRF52833のUARTのマニュアル(*5) を参照する。しかし、このUARTをアプリケーションから使うのは支障があることがわかる。nRF52833のUARTは一つしかなく、これをµT-Kernel 3.0のコンソールとの通信用に使っているため、他の用途と共用するのは難しいのだ。開発用ホストとコンソールとの通信はUSBなので、UARTの信号線が直接外に出ているわけではなく、Interface MCUによってUART通信からUSBに変換され、開発用ホストに接続されている(連載第1回(*6) の図2参照)。


代替方法として、nRF52833にはUARTE(UART with EasyDMA)という機能がある(*5) 。UARTでは1バイトずつのデータ通信を行うが、UARTEの場合はメモリ上に置かれた複数バイトのデータに対して、UART通信を使ってまとめて送信や受信ができる。こちらの方が高機能だが、両者の違いは一度に扱えるデータが1バイトか複数バイトかという点だけであり、シリアル通信を制御するための基本的な使い方は共通である。UARTEの内部構成を図4のブロック図に示す。


nRF52833は、UARTE0とUARTE1の二つのUARTEを持っている。しかし、UARTE0のベースアドレスは、コンソール用に使っているUARTと同じ0x40002000に割り当てられているため、UARTE0とUARTを同時に使うことはできない。コンソール用のUARTとの競合を避けるには、ベースアドレスとして0x40028000が割り当てられたUARTE1を使う必要がある。


UARTEの主な制御用レジスタを表1に示す。通信などの動作の開始や停止を指示するレジスタをTASKS_*、通信完了などの状態の変化を示すレジスタをEVENTS_*といった名称にしているのは、GPIO、PWM、SAADC、I2Cなどの場合と同様である。また、nRF52833に限らないが、UART通信では送信をTX、受信をRXの略称でよぶことが多く、レジスタや信号線の名称の中にもTXとRXの名称が使われている。


UART通信を行う準備として、CONFIGレジスタでフロー制御の有無やパリティビットの有無、ストップビットの長さなどを指定し、BAUDRATEレジスタでボーレートを設定しておく必要がある。また、送信用および受信用の信号線として使用するGP図2 実習セミナー風景IOピンの番号を、PSEL.TXDとPSEL.RXDのレジスタで指定する。さらに、UARTEを使ってUART通信を行う場合は、ENABLEレジスタに8を設定する。


データ送信を開始するには、送信データを入れたメモリの先頭アドレスをTXD.PTRに、送信するデータのバイト数をTXD.MAXCNTに設定した後に、TASKS_STARTTXのレジスタに1を書き込む。これで、UART通信によるデータ送信の処理が始まる。データの送信中は、すでに送信の終わったデータのバイト数がTXD.AMOUNTに入る。すべてのデータの送信が終了するとEVENTS_ENDTXが1になるので、このレジスタをチェックすれば送信の終了を判断できる。フロー制御によって送信を抑止されていないかぎり、データの送信は一定時間内に終了する。


データを受信する場合は、受信データを入れるメモリの先頭アドレスをRXD.PTRに、受信するデータのバイト数をRXD.MAXCNTに設定した後に、TASKS_STARTRXのレジスタに1を書き込む。データの受信中は、受信してメモリに格納済のデータのバイト数がRXD.AMOUNTに入る。RXD.MAXCNTに設定したバイト数のデータの受信とメモリへの格納が終わると、EVENTS_ENDRXが1になるので、このレジスタをチェックして受信の終了を判断する。接続相手の機器からデータが送られてこない場合や、送られてきたデータのバイト数がRXD.MAXCNTの設定値よりも少ない場合は、残りのデータの到着を待ったまま、いつまで経ってもデータの受信が終了しない状況になる。


UARTEには、上記以外にも、エラーの発生状態を示すEVENTS_ERRORおよびERRORSRC、割込みを制御するためのINTEN、INTENSET、INTENCLRなど多数の制御レジスタが用意されている。詳細はnRF52833のUARTEのマニュアル(*5) を参照されたい。


なお、nRF52833で1バイトずつの送受信を行うUARTと、EasyDMAを使って複数バイトの送受信を行うUARTEでは、大部分の制御レジスタが共通になっており、両者で異なるのは送受信データの所在や転送バイト数を示すTXD.PTR、TXD.MAXCNT、TXD.AMOUNT、RXD.PTR、RXD.MAXCNT、RXD.AMOUNTのみである。UARTE0とUARTのベースアドレスは同じなので、大部分の制御レジスタの実際のアドレスもまったく同じである。では、どのような方法でUARTEとUARTの動作を区別するのかといえば、動作を始める際にENABLEレジスタに設定する値が異なっている。UARTEの場合はENABLEに8を設定して動作を開始するのに対して、UARTの場合は4を設定する。

図4 nRF52833のUARTEの内部構成
図4 nRF52833のUARTEの内部構成(*5 のマニュアルより引用)

表1 nRF52833のUARTEの主な制御レジスタ
レジスタ名称 アドレスの
オフセット
説明
TASKS_STARTRX 0x000 UART通信の受信開始
TASKS_STOPRX 0x004 UART通信の受信停止
TASKS_STARTTX 0x008 UART通信の送信開始
TASKS_STOPTX 0x00C UART通信の送信停止
EVENTS_ENDRX 0x110 受信完了の状態(受信バッファ一杯まで受信済)
EVENTS_ENDTX 0x120 送信完了の状態(最後のデータまで送信済)
EVENTS_ERROR 0x124 エラー検出状態
INTEN 0x300 割込みの許可と禁止の設定
INTENSET 0x304 割込み許可の設定
INTENCLR 0x308 割込み禁止の設定
ERRORSRC 0x480 エラーの発生原因
ENABLE 0x500 UARTEのイネーブル(動作開始)
PSEL_TXD 0x50C 送信用信号線(TXD)のGPIOピン設定
PSEL_RXD 0x514 受信用信号線(RXD)のGPIOピン設定
BAUDRATE 0x524 ボーレートの設定
RXD_PTR 0x534 受信データを入れるメモリアドレス
RXD_MAXCNT 0x538 受信データの最大バイト数
RXD_AMOUNT 0x53C 受信してメモリに格納済のデータのバイト数
TXD_PTR 0x544 送信データを入れるメモリアドレス
TXD_MAXCNT 0x548 送信データの最大バイト数
TXD_AMOUNT 0x54C 送信の終わったデータのバイト数
CONFIG 0x56C パリティやフロー制御の指定


UARTEを操作する関数

 

UARTEを使ったシリアル通信の動作確認を行う例題プログラムを作成する。まずはその準備として、UARTE1の初期設定を行う関数と、UARTE1を使ったデータ送信用およびデータ受信用の関数を作成する(リスト1)。


UARTE1の初期設定をう関数がinit_uarte1である(リスト1の(※A))。引数は、送信用および受信用の信号線として設定するGP行IOピンを示すpin_txd、pin_rxdである。init_uarte1では、まず送受信完了の状態を示すEVENTS_ENDRXおよびEVENTS_ENDTXと、エラーの発生状態を示すEVENTS_ERRORおよびERRORSRCを0にクリアした後、BAUDRATEのボーレートを設定する(※B)。ここでは、ボーレートの数値をそのまま設定するわけではなく、ボーレートに応じて個別に決められた特別な値を設定する。具体的な設定値はUARTEのマニュアル(*5) のBAUDRATEレジスタの説明の中に記載されているが、たとえば9,600bpsで動作させる場合は0x00275000を設定し、115,200bpsで動作させる場合は0x01D60000を設定する。本来であれば、すべてのボーレートに対する設定値をswitch文などで場合分けして定義しておくべきだが、今回はUARTEの動作確認が目的なので、特定のボーレートの設定値をソースプログラム中に埋め込んでいる。ボーレートを変更するには、コメント化してある行を調整する。次に、フロー制御無し、パリティビット無し、ストップビット長として1を指定するためにCONFIGに0を設定し、送受信用の信号線のGPIOピンを示すpin_txdとpin_rxdをPSEL_TXDとPSEL_RXDに設定する(※C)。最後に、ENABLEに8を設定してUARTEの動作を開始する(※D)。


UARTE1を使ってデータ送信を行う関数がuarte1_txである(※E)。この関数の引数は、送信するデータ数(バイト数)を示すtxcntと、送信データを入れたメモリの先頭アドレスを示すtxadrである。uarte1_txの中では、txadrをTXD_PTRのレジスタに、txcntをUARTEのTXD_MAXCNTのレジスタに設定し、送信終了状態を示すEVENTS_ENDTXを0にクリアしてから、TASKS_STARTTXに1を書き込んで(※F)、実際のデータ送信を開始する。あとはUARTEのハードウェアが自動的に調歩同期式シリアル通信の処理を進めてくれるのだが、すべてのデータ送信が終わった後でuarte1_txから戻るように、EVENTS_ENDTXの状態をポーリングで確認している(※G)。送信の完了を待たずにuarte1_txから戻ってもよいのだが、そうすると、戻った直後に次の送信データの準備を始めてしまい、txadr以下のメモリの内容が書き換えられてしまう心配がある。使う側で注意すればよいだけではあるが、今回はUARTEの動作を理解することが目的なので、効率よりも確実で間違いの起こりにくい動作を目指した。


データ受信を行う関数がuarte1_rxである(※H)。引数は、受信するデータ数(バイト数)を示すrxcntと、受信データを入れるメモリの先頭アドレスを示すrxadrである。これらの引数は、uarte1_txと同じようにUARTEのRXD_MAXCNTとRXD_PTRのレジスタに設定する。その後、受信完了状態を示すEVENTS_ENDRXを0にクリアしてから、TASKS_STARTRXに1を書き込んで(※J)、UART通信の受信処理を開始する。受信の完了はEVENTS_ENDRXの変化により判断できるので、この値が0以外に変わるまでポーリングで待ち(※K)、受信完了が確認できたらuarte1_rxから戻る。なお、ポーリングの待ち時間を稼ぐためにtk_dly_tsk(10)を実行しているが、連載第8回(*7) の図2で説明した理由により、実際のポーリングの時間間隔は約20ミリ秒となる。


 

リスト1 UARTE1を操作する関数

//-------- UARTE1の初期化とピン設定(※A) ------------------------
LOCAL void init_uarte1(INT pin_txd, INT pin_rxd)
{
    out_w(UARTE(1, EVENTS_ENDRX), 0);
    out_w(UARTE(1, EVENTS_ENDTX), 0);
    out_w(UARTE(1, EVENTS_ERROR), 0);
    out_w(UARTE(1, ERRORSRC), 0);

    // 9600 baud(※B)
    out_w(UARTE(1, BAUDRATE), 0x00275000);
    // 115200 baud(※B)
    // out_w(UARTE(1, BAUDRATE), 0x01D7E000);

    // Hardware flow control無し、パリティ無し、One stop bit
    out_w(UARTE(1, CONFIG), 0);

    // TXDのGPIOピン設定(※C)
    out_w(UARTE(1, PSEL_TXD), pin_txd);
    // RXDのGPIOピン設定(※C)
    out_w(UARTE(1, PSEL_RXD), pin_rxd);
    // Enable UARTE(※D)
    out_w(UARTE(1, ENABLE), 8);
}

//-------- UARTE1の送信(※E) ------------------------------------
LOCAL void uarte1_tx(INT txcnt, UB *txadr)
{
    // 送信バッファ先頭アドレス
    out_w(UARTE(1, TXD_PTR), (UW) txadr);
    // 送信するバイト数
    out_w(UARTE(1, TXD_MAXCNT), txcnt);

    // 送信終了フラグのクリア
    out_w(UARTE(1, EVENTS_ENDTX), 0);
    // 送信開始(※F)
    out_w(UARTE(1, TASKS_STARTTX), 1);

    // 送信終了までポーリングで待ってから戻る(※G)
    while(! in_w(UARTE(1, EVENTS_ENDTX)))
        tk_dly_tsk(10);
}

//-------- UARTE1の受信(※H) ------------------------------------
LOCAL void uarte1_rx(INT rxcnt, UB *rxadr)
{
    // 受信バッファ先頭アドレス
    out_w(UARTE(1, RXD_PTR), (UW) rxadr);
    // 受信するバイト数
    out_w(UARTE(1, RXD_MAXCNT), rxcnt);

    // 受信終了フラグのクリア
    out_w(UARTE(1, EVENTS_ENDRX), 0);
    // 受信開始(※J)
    out_w(UARTE(1, TASKS_STARTRX), 1);

    // 受信終了までポーリングで待ってから戻る(※K)
    while(! in_w(UARTE(1, EVENTS_ENDRX)))
        tk_dly_tsk(10);
}


UARTEの動作確認プログラム

 

動作確認用の例題プログラムのうち、リスト1で作成した関数以外の部分をリスト2に示す。送信側と受信側のプログラムは別タスクとし、両者は並行動作を行う。UART通信の送信側のタスクでは、ボタンスイッチAやBの状態変化があった場合に、10バイトの文字列データを送信する。受信側のタスクでは、受信したメッセージをコンソールに表示するとともに、メッセージの内容に応じてLEDの点灯や消灯を行う。メッセージの通信が成功すれば、ボタンスイッチAやBを押したタイミングで、相手側のボード上の対応するLEDが点灯するはずだ。


送受信用の信号線として、リング状のエッジコネクタ端子のうち、左側のRING0(=P0)とRING1(=P1)を使う。後述するが、動作確認時のボードの接続方法によって送信用と受信用の信号線の割り当てを入れ替える必要があるため、defineマクロを使ってその指定を行う(リスト2の(※L))。具体的には、P0TX_P1RXマクロが定義されている場合はRING0(=P0)を送信用、RING1(=P1)を受信用の端子とする。P1TX_P0RXマクロが定義されている場合はその逆である。RING0(=P0)はGPIOのP0.02に、RING1(=P1)がGPIOのP0.03に接続されているので(*1) 、これらのマクロの定義に応じて、送信用と受信用のGPIOピンをtx_pinとrx_pinに設定する(※M)。


メインプログラムusermainでは、btn_initやled_initを使ってボタンスイッチ入力やLED制御のためのGPIOの初期設定を行った後に、特定のLEDのみを点灯するための準備として、LEDのCOL1..COL5に1を、ROW3に0を出力する(※N)。また、本プログラムが動作中であることを示すために、COL3に0を出力して真ん中のD26(ROW3-COL3)のLEDを点灯する(※P)。LEDの点灯や消灯のためにGPIOを操作しているが、このしくみの説明やout_gpio_pinの使い方については、連載第7回(*8) を参照されたい。制御するLEDはROW3の1行のみなので、ダイナミック点灯を使う必要はない。次に、init_uarte1を実行してUARTEの初期設定を行ってから、送信側のタスクであるtx_taskおよび受信側のタスクであるrx_taskの生成と起動を行い(※Q)、UART通信の確認動作を開始する。


tx_taskの中では(※R)、GPIOのP0.14とP0.23を見てボタンスイッチAとBの状態を読み出してから(※S)、その読み出し値を引数としてcheck_btnを実行する。この処理を100msごとに繰り返す。check_btnでは(※T)、ボタンスイッチAとBの現在の状態を以前の状態(*prev_btn)と比較して、変化があった場合、すなわちボタンスイッチを押したり離したりした場合には、uarte1_txを実行してUART通信に文字列データを送る(※U)。送信する文字列は“Ap_123450¥n”のような形式になっていて、1文字目(1バイト目)の‘A’または‘B’でボタンスイッチAとBの区別を示す。2文字目には、ボタンスイッチを押した場合にプッシュの‘p’、戻した(離した)場合にリリースの‘r’を入れる。4文字目以降に6桁の数字が入っているが、これはシステム起動後のミリ秒単位の時間を示す。この情報を入れたのは、メッセージの内容にできるだけ変化を与えるためである。この文字列はtm_printfを使って生成しており(※V)、長さは10バイト固定である。


一方、受信側のタスクであるrx_taskの中では(※W)、uarte1_rxを実行してUART通信による10バイトの文字列データを読み出してから(※X)、その文字列をそのままコンソールに表示する。さらに、文字列の1文字目が‘A’で2文字目が‘p’であればボタンスイッチAに近いD22のLEDを点灯し(※Y)、2文字目が‘r’であればD22のLEDを消灯する(※Z)。文字列の1文字目が‘B’の場合は、ボタンスイッチBに近いD30のLEDに対して同様の処理を行う。この結果、正常なUART通信ができていれば、ボタンスイッチAを押した場合にD22のLEDが点灯し、ボタンから手を離せばD22のLEDが消灯する。ボタンスイッチBに対してはD30のLEDが同様に反応する。

実際に通信してみる


まずはmicro:bitを1台のみ使い、送信用と受信用の信号線を直結して、自分自身から送ったデータを受信できることを確認する。これは、ループバックとよばれる手法であり、通信機能のテスト方法としては定番の一つである。特にUART通信の場合は、送信側の処理と受信側の処理が完全に独立しているので、設定を変更する必要もなく、送受信の信号線を結ぶだけでループバックによる通信テストが可能である。


ループバックでの通信を確認するために、エッジコネクタ端子のRING0とRING1をワニ口クリップなどを使って接続する。開発用ホストPCでは、コンソール出力が読めるように、Tera Termなどの端末ソフトを動かしておく。リスト2のプログラムを実行すると、真ん中のD26のLEDが点灯する。この状態でボタンスイッチAを押すと、送信側タスクからUARTEを経由して10バイトの文字列データがエッジコネクタ端子のRING0に出力され、それがそのままRING1から入力されてUARTEに受信される。受信側タスクでは、その文字列データを見てD22のLEDを点灯するとともに、受信データをそのままコンソールに出力する(リスト3)。ボタンスイッチAを離すと、D22のLEDは消灯する。ただし、ボタンスイッチをチェックするポーリングの間隔が100ミリ秒なので、受信側タスクの反応がやや遅いと感じられるかもしれない。同様に、ボタンスイッチBを押した場合にはD30のLEDが点灯する。これらの動作により、UARTEによる通信の機能が確認できた。


対照実験として、RING0とRING1との接続を外した状態にしてから、再度リスト2のプログラムを実行する。この場合は、ボタンスイッチAやBを押してもLEDは点灯しないし、コンソールに文字列が出力されることもない。これで、最初の実験においては、たしかにRING0からRING1に通信していたことがわかる。


次はいよいよ、2台のmicro:bitを使い、その間で通信できることを確認する。そのためには、まず2台のmicro:bit同士を接続する必要がある。2台のmicro:bitの間をワニ口クリップで結んでもよいのだが、電子工作に馴染みがない人でもスマートに接続できるように、連載第11回(*9) で登場した「ワールドオブモジュール センサーキット」の拡張ボードを使用することにした。拡張ボードもmicro:bitと同じく2枚必要なので、拡張ボードを1枚買い足している(*10)


2台のmicro:bitの拡張ボード同士を接続するには、ロッカーモジュールや光センサーとの接続に使用した4Pの接続ケーブルを使う。このケーブルで、2台の拡張ボードの左上の[3.3V P0 P1 GND]と書かれたコネクタの間をつなぐ(図5)。そうすると、2台のmicro:bitの3.3Vの電源とGNDに加えて、RING0(=P0)とRING1(=P1)のエッジコネクタ端子同士が接続される。RING0(=P0)を1台目のmicro:bitから2台目のmicro:bitへのUART通信に利用し、RING1(=P1)を逆方向のUART通信に利用する。


さらに、4Pのコネクタにより3.3Vの電源とGNDも接続されたので、接続相手となるmicro:bitにはUSBの電源を供給する必要がない。一方のmicro:bitにUSBから5V電源を供給すれば、その電源がmicro:bitで3.3Vに変換され、4Pの接続ケーブル経由でもう一方のmicro:bitにも供給されるのである。


2台のmicro:bitで通信する場合、送信と受信に使用するエッジコネクタ端子を逆にする必要がある。一方はリスト2のとおりP0が送信、P1が受信でよいが、もう一方はP0を受信、P1を送信にする。そのため、もう一方のmicro:bitではP0TX_P1RXマクロの定義をコメント化し、代わりにP1TX_P0RXマクロの定義を有効にする必要がある(リスト4)。つまり、micro:bit上で実行するプログラムは2台で異なるものになる。もし、間違って2台のmicro:bit上でどちらも同じリスト2のプログラムを実行すると、両方のmicro:bitから同じP0端子に対してデータが送信されるため、出力が競合してハードウェア的に無理がかかった状態になる。故障の原因になる可能性もあるので気をつけよう。


図5の一方のmicro:bitでリスト2のプログラムを実行し、もう一方でリスト4のプログラムを実行する。プログラムをダウンロードするためにはUSBケーブルを接続する必要があるが、ダウンロードして正常に実行できることが確認できれば、そのプログラムはフラッシュROMに格納済なので、USBケーブルを外しても電源の投入やリセットにより再実行が可能である。つまり、1台目のmicro:bitをUSBケーブルで開発用ホストPCと接続してリスト2のプログラムを実行した後に、1台目のケーブルを外してから2台目のmicro:bitをUSBケーブルで接続してリスト4のプログラムを実行し、その後で図5のように拡張ボードと4Pの接続ケーブルを使って1台目と2台目のmicro:bitをつなげばよい。2台のmicro:bitを同時に開発用ホストPCに接続すると、PC側から2台の区別ができなくなる可能性があり、混乱するので避けた方がよい。


この状態で2台のmicro:bitのプログラムを実行し、一方のmicro:bitのボタンスイッチAやBを押すと、もう一方のmicro:bitのLEDが点灯する。逆方向の動作も同じように可能である。開発用ホストPCに接続されていれば、リスト3と同様のメッセージがコンソールに表示される。2台のmicro:bitがエッジコネクタ端子を経由して相互に通信していることが確認できた。

MakeCodeで動くmicro:bitとの通信

最後に、MakeCodeで動いているmicro:bitとのUART通信の動作も確認してみよう。MakeCodeを使えば、micro:bitの各種の入出力デバイスを容易に操作できるので、応用範囲がさらに拡がるはずだ。


MakeCodeの概要については連載第1回(*6) で紹介したが、エッジコネクタ端子を使ったシリアル通信(UART通信)の機能も用意されている。この機能を使って、リスト4に似た動作をするMakeCodeのプログラムを作成した(リスト5)。リスト2が動作するmicro:bitとの間で通信できる。


「最初だけ」のブロックでは、P1TX_P0RXマクロを定義したリスト4と同じ動作をするように、送信端子をP1、受信端子をP2に設定する。また、通信速度(ボーレート)は9600に設定する。

ボタンスイッチを押したときの送信の動作では、処理を簡単にするために「ボタンAが押されたとき」のブロックを使っているが、実際にこのブロックが実行されるのはボタンを押してから離した後であり、ボタンを押したときと離したときの動作は区別していない。また、システム起動後のミリ秒単位の時間を文字列に埋め込む処理も省略している。「ボタンAが押されたとき」のブロックの中では、LEDマトリックスに‘A’を表示してから、「シリアル通信 文字列を書き出す」のブロックを使って“Ap_123456¥n”の文字列を送信する。その後、500ミリ秒待ってから、“Ar_654321¥n”の文字列を送信する。「文字コード10の文字」をつなげているのは、最後の改行コード(¥n)を送るためである。ボタンスイッチBに対する処理も同様だ。


シリアル通信の受信側の処理としては、「ずっと」のブロックの中で、シリアル通信から読み取った文字列をLEDマトリックスに表示する。文字列を読み取る際の終端は、「文字コード10の文字」、すなわち改行コード(¥n)によって判断する。


このプログラムを実行するMakeCodeのmicro:bitと、リスト2のプログラムを実行するµT-Kernelのmicro:bitとを接続して動作確認を行う。2台の間の接続方法は、リスト4のときの図5と同じである。µT-Kernelの動くmicro:bitには、USB経由で開発用ホストPCを接続し、端末ソフトを開いてコンソール出力を表示できるようにしておく。この状態でMakeCode側のmicro:bitのボタンスイッチAやBを押すと、ボタンを離した後でµT-Kernel側のD22やD30のLEDが500ミリ秒だけ点灯する(図6)。また、コンソールにはMakeCodeから送信した文字列が表示される。一方、µT-Kernel側のmicro:bitのボタンスイッチAやBを押すと、MakeCode側のmicro:bitのLEDマトリックスに、µT-Kernel側から送信した文字列がスクロールしながら表示される。


なお、連載第3回(*11) で説明したように、µT-Kernelの実行に使っていたmicro:bitでMakeCodeのプログラムを実行するには、開発用ホストPCにmicro:bitを接続した状態で、OutOfBoxExperience.hex のファイルを書き込む必要がある。逆に、MakeCodeの実行に使っていたmicro:bitでµT-Kernelを実行するには、コマンドプロンプトの画面で pyocd erase --mass を実行する必要がある。

より実用的な通信を考える

今回作成した例題プログラムでは、micro:bitのUARTEによるシリアル通信の最低限の動作確認を行う目的で、通信するデータのバイト数を10バイトに固定した。そのため、受信側のプログラムで一度に受信するデータは10バイトずつに決まっており、UARTEの設定や操作も簡単に済ませている。


しかし、一般的な通信では、これから受信するデータのバイト数が事前にわかっているケースは少ない。想定より多くのデータを受信した場合には、一度に受信できなかったデータを処理するために、UARTEの受信の操作を何度も繰り返す必要が生じる。一方、想定より少ないデータしか来なかった場合には、期待したバイト数のデータが受信できるまでUARTEの受信処理が完了せず、そのままでは永久に待ち続けることになる。これではシステム全体の動作に支障を生じるため、どこかの時点でタイムアウトを検出し、一旦UARTEの受信処理を停止する必要がある。UARTEには受信を中止するための制御レジスタTASKS_STOPRXが用意されているので、これを使えばよいのだが、処理は複雑なものになってしまう。


また、今回は理解しやすく簡単なプログラムにするため、送信や受信の完了を待つ際にはポーリングで処理した。しかし、この方法で実用的な通信性能を出すのは難しい。たとえば、115,200bpsのボーレートで通信する場合、毎秒10,000バイト程度の転送データ量になるので、今回のポーリングの間隔である20ミリ秒の間にも200バイト程度のデータが来ることになる。ポーリングで待っている間は、その次に来るデータの受信処理ができず、データを取りこぼしてしまう可能性が高い。ポーリングの間隔を短くする方法もあるが、ゼロにはできないし、CPUの負荷が高くなるという問題もある。


 

リスト2 UARTEの動作確認プログラム(P0が送信側)

/*---------------------------------------------------------------
*  UARTEの動作確認:P0が送信側(P0TX_P1RX) (μT-Kernel 3.0用)
*
*  Copyright (C) 2022-2024 by T3 WG of TRON Forum
*---------------------------------------------------------------*/
             ・
             ・
             ・
//---------------------------------------------------------------
// UARTEの送信(TX)と受信(RX)に使用するエッジコネクタ端子の
// 選択(※L)
//  以下の2行のうち一方のみコメントを外す
// RING0(=P0)から送信、RING1(=P1)から受信
#define     P0TX_P1RX   TRUE
// RING1(=P1)から送信、RING0(=P0)から受信
// #define  P1TX_P0RX   TRUE
              ・
              ・
              ・
//---------------------------------------------------------------
static W    i_time;             // システム起動時刻

// UARTEの送信データと受信データを入れるバッファメモリ
#define MAXBUF  100
static _UB  txbuf[MAXBUF];
static _UB  rxbuf[MAXBUF];

// 以前のボタンの状態を保持する変数
LOCAL BOOL prev_a = FALSE;
LOCAL BOOL prev_b = FALSE;

//---------------------------------------------------------------
// ボタンの状態変化のチェックと送信(※T)
LOCAL void check_btn(BOOL new_btn, BOOL *prev_btn,
UB btnchr)
{
    UB actchr;
    // 状態変化が無ければ何もせずに戻る
    if(new_btn ==  *prev_btn) return;

    *prev_btn = new_btn;
    // ボタンを押した時にp、離した時に
    actchr = (new_btn ? 'p' : 'r');

      // 送信する文字列を生成してtxbufに書き込む(※V)
      tm_sprintf((UB*) txbuf, "%c%c_%06d\n", btnchr,
      actchr, cur_time() - i_time);

      // txbufに設定した10バイトのデータをUARTE1で送信(※U)
      uarte1_tx(10, (UB*) txbuf);
}

// ボタンスイッチの入力判定と送信側タスク(※R)
LOCAL void tx_task(INT stacd, void *exinf)
{
    for(;;){                // 約100msごとに永久に繰り返し
        UW gpio_p0in;
        BOOL btn_a, btn_b;

        // GPIO P0のINレジスタを読んでgpio_p0inに設定
        gpio_p0in = in_w(GPIO(P0, IN));

        // P0.14が0の場合にTRUE(※S)
        btn_a = ((gpio_p0in & (1 << 14)) == 0);
        // P0.23が0の場合にTRUE(※S)
        btn_b = ((gpio_p0in & (1 << 23)) == 0);

        // ボタンAの変化のチェックと送信
        check_btn(btn_a, &prev_a, 'A');
        // ボタンBの変化のチェックと送信
        check_btn(btn_b, &prev_b, 'B');

        tk_dly_tsk(100);                // 100msのディレイ
    }
}

// 受信したメッセージをLEDとコンソールに表示する受信側
// タスク(※W)
LOCAL void rx_task(INT stacd, void *exinf)
{
    for(;;){                // 永久に繰り返し
         // UARTE1からrxbufに10バイトのデータを受信(※X)
        uarte1_rx(10, (UB*) rxbuf);

        // ボタンBの変化のチェックと送信
        rxbuf[9] = '¥0';
        // コンソールへのメッセージ出力
        tm_printf("%s¥n", (UB*) rxbuf);

        if(rxbuf[0] == 'A'){
            if(rxbuf[1] == 'p'){           // ボタンAがON("Ap")
                // ROW3のCOL1(D22)を点灯(ROW3=1,COL1=0)(※Y)
                out_gpio_pin(0, 28, 0);
                                    // GPIO P0.28-COL1に0を出力
            } else if(rxbuf[1] == 'r'){   // ボタンAがOFF("Ar")
                // ROW3のCOL1(D22)を消灯(ROW3=1,COL1=1)(※Z)
                out_gpio_pin(0, 28, 1);
                                    // GPIO P0.28-COL1に1を出力
            }
        } else if(rxbuf[0] == 'B'){
            if(rxbuf[1] == 'p'){           // ボタンBがON("Bp")
                // ROW3のCOL5(D30)を点灯(ROW3=1,COL5=0)
                out_gpio_pin(0, 30, 0);
                                    // GPIO P0.30-COL5に0を出力
            } else if(rxbuf[1] == 'r'){   // ボタンBがOFF("Br")
                // ROW3のCOL5(D30)を消灯(ROW3=1,COL5=1)
                out_gpio_pin(0, 30, 1);
                                    // GPIO P0.30-COL5に1を出力
            }
        }
      }
}

//---------------------------------------------------------------
// オブジェクトID番号
ID txtskid;             // 送信側タスク
ID rxtskid;             // 受信側タスク

// タスクの生成情報
const T_CTSK ctxtsk = {0, (TA_HLNG | TA_RNG3), &tx_task,
10, 1024, 0};
const T_CTSK crxtsk = {0, (TA_HLNG | TA_RNG3), &rx_task,
10, 1024, 0};

// UARTEの送信(TX)と受信(RX)に使用するエッジコネクタ端子の
// 定義(※M)
#ifdef P0TX_P1RX
// 送信をGPIO P0.02=端子P0(RING0)に設定
const INT tx_pin = 2;
// 受信をGPIO P0.03=端子P1(RING0)に設定
const INT rx_pin = 3;
#endif

#ifdef P1TX_P0RX
// 送信をGPIO P0.03=端子P1(RING0)に設定
const INT tx_pin = 3;
// 受信をGPIO P0.02=端子P0(RING0)に設定
const INT rx_pin = 2;
#endif

//---------------------------------------------------------------
EXPORT void usermain( void ){
    i_time = cur_time();            // システム起動時刻を保存
    // ボタンスイッチ入力のためのGPIOの初期設定
    btn_init();
    led_init();                     // LED制御用のGPIO設定

    // GPIO P0.15-ROW3に1を出力してもROW3のLEDを点灯させ
    // ないための設定(※N)
    out_gpio_pin(0, 28, 1);         // GPIO P0.28-COL1に1を出力
    out_gpio_pin(0, 11, 1);         // GPIO P0.11-COL2に1を出力
    out_gpio_pin(0, 31, 1);         // GPIO P0.31-COL3に1を出力
    out_gpio_pin(1,  5, 1);         // GPIO P1.05-COL4に1を出力
    out_gpio_pin(0, 30, 1);         // GPIO P0.30-COL5に1を出力

    // ROW3のLEDを点灯させるための設定(※N)
    out_gpio_pin(0, 15, 1);         // GPIO P0.15-ROW3に1を出力

    // 動作確認用にCOL3のLEDを点灯(※P)
    out_gpio_pin(0, 31, 0);         // GPIO P0.31-COL3に0を出力

    // UARTEの初期化と送受信用端子の指定
    init_uarte1(tx_pin, rx_pin);

    txtskid = tk_cre_tsk(&ctxtsk);  // 送信側タスクの生成(※Q)
    rxtskid = tk_cre_tsk(&crxtsk);  // 受信側タスクの生成(※Q)
    tk_sta_tsk(txtskid, 0);         // 送信側タスクの起動(※Q)
    tk_sta_tsk(rxtskid, 0);         // 受信側タスクの起動(※Q)

    tk_slp_tsk(TMO_FEVR);           // 永久待ち
}


リスト3 UARTEの動作確認プログラムのコンソール出力例

microT-Kernel Version 3.00

Ap_005170	←ボタンスイッチAを押す
Ar_005520	←ボタンスイッチAを離す
Ap_010270	←ボタンスイッチAを押す
Ar_010620	←ボタンスイッチAを離す
Bp_014710	←ボタンスイッチBを押す
Br_016710	←ボタンスイッチBを離す


図5 2台のmicro:bitの3.3V、P1、P0、GNDを接続
図5 2台のmicro:bitの3.3V、P1、P0、GNDを接続

リスト4 UARTEの動作確認プログラム(P1が送信側)

/*---------------------------------------------------------------
 *  UARTEの動作確認:P1が送信側(P1TX_P0RX) (μT-Kernel 3.0用)
 *
 *  Copyright (C) 2022-2024 by T3 WG of TRON Forum
 *---------------------------------------------------------------*/
              ・
              ・
              ・
//---------------------------------------------------------------
// UARTEの送信(TX)と受信(RX)に使用するエッジコネクタ端子の選択(※L)
//  以下の2行のうち一方のみコメントを外す
// #define  P0TX_P1RX   TRUE    // RING0(=P0)から送信、RING1(=P1)
から受信
#define     P1TX_P0RX   TRUE    // RING1(=P1)から送信、RING0(=P0)
から受信
              ・
              ・
              ・


   リスト5 シリアル通信を行うMakeCodeのプログラム
リスト5 シリアル通信を行うMakeCodeのプログラム



図6 MakeCodeで動くmicro:bitとの通信の実験の様子
図6 MakeCodeで動くmicro:bitとの通信の実験の様子

結局、実用的なUART通信を行うには、割込みを使って送信や受信の完了を待つ方法がほぼ必須である。本稿でその説明まではできないが、実は、すでにダウンロードして実行しているµT-Kernel 3.0のソースプログラムの中に、割込みを使ったUART通信のドライバが含まれている。


先にも少し触れたが、tm_printfなどを実行した際にコンソール出力される文字列は、nRF52833のUART(UARTEではない)を経由しており、µT-Kernel 3.0のシリアル通信ドライバを利用している。このドライバは、µT-Kernel 3.0仕様書のデバイス管理機能で定義される仕様に準拠したデバイスドライバである。もちろん、割込みを使って効率よく動作するように作られており、参考になるはずだ。ドライバのソースプログラムは /device/ser/sysdepend/nrf5 の下に入っている。


また、ソースプログラム付属のドキュメント「µT-Kernel 3.0 デバイスドライバ説明書」には、このシリアル通信ドライバの仕様の説明がある。micro:bit用のµT-Kernel 3.0では、nRF52833のUARTに対して“sera”のデバイス名のドライバが利用できる。ただし、このドライバが対象としているUARTは、開発用ホストPCとの通信用としてInterface MCUに接続されているため、他の用途には利用できない。繰り返しになるが、エッジコネクタ端子を経由して外部とのUART通信を行うには、UARTではなくUARTEを使う必要があり、UARTEのドライバは別途開発しなければならない。そのドライバの動作のうち、ごく簡単な部分を実際に試してみたものが、今回作成したリスト2などの例題プログラムである。


* * *

今回は、連載の中で初めて2台のmicro:bitを使用し、その間でのUART通信を試してみた。コンピュータ、特にIoTエッジノードにおいて通信は必須の機能であり、その応用範囲も広い。一方、通信は通信相手があっての機能なので、相手との調整の考慮など、何かと難しい点も多い。本稿で説明したUART通信は、通信方式としては最もシンプルなものであるが、それでも通信の難しさを経験することはできる。通信機能を使ったいろいろなアプリケーションを開発する際に、本稿の説明が参考になれば幸いである。


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

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

(*1)
 micro:bit V2のエッジコネクタ端子 https://tech.microbit.org/hardware/edgeconnector/
(*2)
 micro:bitでµT-Kernel 3.0を動かそう[第5回]ボタンスイッチ入力, TRONWARE VOL.199, 2023年2月
(*3)

 ボーレートとビット転送速度(bps)の実際の意味は異なる。ボーレートは変調(信号や電波に情報を載せること)の速度という意味であり、1回の変調で複数ビットの情報を載せるような変調方式を使った場合には、ボーレートとビット転送速度(bps)が一致しない。ただし、UART通信の場合は、変調といってもビット単位の2値のデータをそのまま使うだけであり、1回の変調で送る情報は1ビットなので、両者がほぼ同じ意味になる。

(*4)

 UART通信では、本文で説明した手順のほかに、7ビット単位でデータ通信を行ったり、ストップビットの送信時間を2倍(2ビット分)にしたり、ストップビットの直前にパリティビットとよばれる誤り検出用のビットを入れる場合もある。これらの通信手順はUARTの設定により選択するが、ボーレートと同じく、送信側と受信側で同じ設定にしておく必要がある。

(*5)

 nRF52833のUARTとUARTEのマニュアル
https://infocenter.nordicsemi.com/topic/ps_nrf52833/uart.html
https://infocenter.nordicsemi.com/topic/ps_nrf52833/uarte.html

(*6)

 micro:bitでµT-Kernel 3.0を動かそう[第1回]micro:bitの概要, TRONWARE VOL.195, 2022年6月

(*7)

 micro:bitでµT-Kernel 3.0を動かそう[第8回]ドレミファ音階の再生とティック時間, TRONWARE VOL.203, 2023年10月

(*8)

 micro:bitでµT-Kernel 3.0を動かそう[第7回]LEDのダイナミック点灯, TRONWARE VOL.202, 2023年8月

(*9)

 micro:bitでµT-Kernel 3.0を動かそう[第11回]A/D変換を使ってみよう, TRONWARE VOL.206, 2024年4月

(*10)

   マイクロビット用センサー拡張ボード
https://store.iftiny.com/products/yahboom-world-of-module-series-microbit-expansion-board

(*11)

   micro:bitでµT-Kernel 3.0を動かそう[第3回]µT-Kernel 3.0の実行, TRONWARE VOL.197, 2022年10月

  • 本ページは、「TRONWARE Vol.208」の掲載記事「micro:bitでµT-Kernel 3.0を動かそう 第13回 2台のmicro:bitをシリアル通信で接続」をWebで公開したものです。

ページの先頭に戻る

 

次回の連載記事に進む