本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第5回の本号では、micro:bitの周辺デバイス操作の手始めとして、ボード上にある二つのボタンスイッチから入力するプログラムを作成する。周辺デバイスとしてはもっとも簡単なものであるが、ボードのハードウェアに依存した処理を行うので、開発にはmicro:bitのハードウェア技術情報が必要である。そこで今回の記事では、micro:bitの技術情報が書かれたマニュアルの読み方なども含めて、できるだけ詳しく説明した。プログラムからハードウェアを操作する際の基礎的な知識なので、しっかり理解しよう。
図1 micro:bitのボタンスイッチ
図2 ボタンスイッチとGPIOに関する部分のシステム構成
図3 micro:bitのTarget MCU周辺の回路図
(*1のmicro:bitの回路図より引用)
図4 micro:bitのボタンスイッチ周辺の回路図
(*1のmicro:bitの回路図より引用)
micro:bitには、ボードの表面の左右にボタンA、ボタンBの二つの押しボタンスイッチがある(図1)。今回は、これらのボタンスイッチが押されているか否かをチェックして、その押下状態をコンソールに出力するプログラムを開発する。
スイッチのON、OFFのような状態を返す入力装置は、その状態を電圧の高低などのデジタル信号で表現する。その信号は、コンピュータ側の入出力インタフェースの一つであるGPIO(General-Purpose Input/Output)に入力される。コンピュータ側では、GPIOの状態をプログラムから読み出すことによって、スイッチなどの入力装置の状態を判定する。GPIOは、プログラムやデータを格納するメモリや他の入出力インタフェースと同じメモリ空間上に配置され、共通のメモリアドレスが割り当てられている。そのため、プログラムで使用する変数などと同じように、プログラムやµT-KernelのタスクからGPIOの状態を読み込んだり、設定値をGPIOに書き込んだりすることが可能である。関連するシステム構成を図2に示す。
GPIOの各信号線を「ピン」とよび、複数のピンをグループ化して一度に読み書きできる単位としたものを「ポート」とよぶ。ポートには入出力用のメモリアドレスが割り当てられている。micro:bitのGPIOは、メインのマイクロコンピュータであるTarget MCUのnRF52833に内蔵されており、GPIO P0とGPIO P1の二つのポートがある。GPIO P0はP0.00からP0.31の32本、GPIO P1はP1.00からP1.09の10本のピンを持っており、その合計で最大42本のGPIOピンを使える計算になる。GPIOピンは、ボタンスイッチのほか、ボード上のLEDやエッジコネクタにも接続されており、LEDの制御やエッジコネクタ経由の入出力にも利用される。
ただし、全42本のGPIOピンの中には、ボタンスイッチなどの入出力デバイスやエッジコネクタに接続されておらず、実際にはmicro:bitのボードで使えないものも含まれている。また、これらのGPIOピンの一部は、アナログ入力、UART、I2C、SPIなど他の入出力インタフェースと共用しているため、こういった他の入出力機能を使う場合には、通常のデジタル信号の入出力のために使用できるGPIOピンが少なくなる。
GPIOピンとボタンスイッチ、LED、エッジコネクタなどとの具体的な接続に関する情報は、micro:bitの回路図(*1) に明記されている。ボタンスイッチに関しては、回路図のTarget MCUの部分(図3)を見ると、nRF52833のGPIOピンの一つであるP0.14にBTN_Aが接続され、P0.23にBTN_Bが接続されていることがわかる。BTN_AとBTN_Bの接続先の回路図はButtonsの部分(図4)に示されており、SW2(ボタンスイッチA)を押した場合にはBTN_AがGNDレベル(電圧の低いレベル)、押さなかった場合にはプルアップ用抵抗R4を介してVREGレベル(電圧の高いレベル)になる。SW3(ボタンスイッチB)とBTN_Bについても同様である。これらの接続関係を概念的にまとめたものが図2である。
また、V2 pinmap(*2) とよばれる資料にも、GPIOピンとボタンスイッチやエッジコネクタとの接続関係が示されている。この資料のうち、ボタンスイッチに関連した部分の抜粋(表1)を見ると、BTN_AとGPIO P0.14、BTN_BとGPIO P0.23が接続されていることがわかる。さらに、BTN_AやBTN_BがエッジコネクタのP5やP11と接続されていることもわかる。
GPIO on nRF52833 | Allocation | Edge Connector name |
---|---|---|
P0.14 |
BTN_A | P5 |
P0.23 | BTN_B | P11 |
GPIOの各ピンは機能面での自由度が高く、入力用としても出力用としても使用できるほか、入力の場合にはプルアップ用抵抗の接続を指定したり、入力信号の変化に応じて割込みを発生させたりするような動作も可能である。逆に言えば、GPIOで自分の使いたい機能を動かすには、最初にGPIOへの設定の操作が必要である。各機能に対応した設定値をGPIOのレジスタに書き込んでいくのだが、具体的な設定方法はGPIOによって異なるため、そのマニュアルを読んで情報を得る。micro:bitのGPIOマニュアルは、Target MCUであるnRF52833のドキュメントに含まれている(*3) 。
GPIOマニュアルに書かれた情報のうち、今回のプログラムで必要な部分を抜粋したものが表2と表3である。micro:bitのGPIOにはP0とP1の二つのポートがあるが、どちらのポートも設定方法やレジスタの仕様は共通であり、レジスタのアドレスのみ異なっている。各GPIOポートは多数のレジスタを持ち、そのアドレスはベースアドレス+オフセットで示される。表2からわかるように、GPIO P0のベースアドレス(Base address)は0x50000000、GPIO P1のベースアドレスは0x50000300である。各レジスタに対するオフセット(Offset)はどちらのポートも共通で、表3のとおりである。
Base address | Peripheral | Instance | Description | Configuration |
---|---|---|---|---|
0x50000000 |
GPIO | P0 | General purpose input and output, port 0 | P0.00 to P0.31 implemented |
0x50000300 | GPIO | P1 | General purpose input and output, port 1 | P1.00 to P1.09 implemented |
Register | Offset | Description |
---|---|---|
IN |
0x510 | Read GPIO port |
DIR | 0x514 | Direction of GPIO pins |
PIN_CNF[14] | 0x738 | Configuration of GPIO pins |
PIN_CNF[23] | 0x75C | Configuration of GPIO pins |
GPIOの各ピンの機能を設定するには、表3のPIN_CNF[n](n=14,23)のレジスタに対して、機能に応じた値を書き込んで、各GPIOピンの構成(Configuration)を指定する。具体的な設定方法はGPIOマニュアルのPIN_CNF[n](n=0..31)の欄で説明されており、入出力の方向(DIR)のほか、入力時のピンの接続の有無(INPUT)、入力時のプルアップやプルダウンの指定(PULL)、出力時のドライブ方法の指定(DRIVE)など、多くの機能が利用できる。ただ、今回はボタンスイッチからの電圧レベルを単純な入力として取得するだけなので、DIRは入力(Input)を表す0、INPUTは接続有(Connect)を表す0とし、他の項目もすべて未使用や初期値を表す0とする。すなわち、GPIOのピンnを単純な入力用として設定するには、PIN_CNF[n]のレジスタに0を書き込めばよい。
ちなみに、PIN_CNF[n]レジスタとは別に、GPIOのポート全体のピンの入出力方向を一括して設定するためのDIRレジスタも存在する。これはどちらを使ってもよい。各ピンの入出力の方向や細かい機能を個別に設定する場合にはPIN_CNF[n]レジスタを使うのが便利であり、ポート全体のピンの入出力方向をまとめて設定する場合にはDIRレジスタを使うのが便利だ。
結局、ボタンスイッチからGPIOへの入力を行うには、GPIO P0のPIN_CNF[14]とPIN_CNF[23]のレジスタに0を書き込んで、GPIO P0.14とGPIO P0.23を入力用に設定する。これらのレジスタの具体的なメモリアドレスは、GPIO P0のベースアドレスである0x50000000に、PIN_CNF[14]とPIN_CNF[23]のレジスタのオフセットである0x738と0x75Cを加算した値、すなわち0x50000738と0x5000075Cである。したがって、これらのアドレスに対して、µT-Kernel 3.0のI/Oポートアクセスサポート機能であるout_wを使って0を書き込めばよい。
ボタンスイッチAとBの押下状態は、GPIO P0.14とGPIO P0.23に入力される。これらの入力を読み出すためのレジスタがGPIOのINレジスタ(表3)である。INレジスタの構成は、ピン00からピン31までの各ピンの入力状態をビット対応に並べて、32ビットの1ワードにしたものである。ピン00が最下位のビット、ピン31が最上位のビットに対応し、ピンの電圧レベルが低い場合にそのビットの値が0、高い場合に1となる。
ボタンスイッチAが押されていない場合は、GPIO P0.14の入力がプルアップされたまま、電圧の高い状態になっている。したがって、この状態ではGPIO P0のINレジスタの最下位から14番目のビットが1である。ボタンスイッチAが押されると、GPIO P0.14の入力がGNDと短絡して電圧の低い状態となるため、GPIO P0のINレジスタの最下位から14番目のビットが0になる。この変化をプログラムでチェックすれば、ボタンスイッチAの押下状態を判定できる。ボタンスイッチBについては、これと同様に、最下位から23番目のビットをチェックすればよい。なお、押した場合に0、押されていない場合に1というのは、常識的な感覚とは逆であるが、このような対応関係を負論理とよぶ。
一方、GPIO P0のINレジスタの他のビットは、エッジコネクタなど他の入出力インタフェースに接続されていたり、他の用途に使われていたりするため、その値は不定である(図5)。ボタンスイッチAの押下状態を判定する際には、INレジスタの最下位から14番目以外のビットの影響を受けないように、これ以外のビットをマスクする(強制的に0にする)ようなプログラミングが必要である。
図5 ボタンスイッチAの押下状態とINレジスタの各ビットの値
上記で説明した技術情報をもとに、実際にボタンスイッチの押下状態を確認するプログラムを作ってみよう。
ボタンスイッチの入力に使うGPIOレジスタのメモリアドレスは先に説明したとおりだが、実は、micro:bit用µT-Kernel 3.0のソースプログラムに含まれるヘッダファイルsysdef.h(*4) の中で、GPIOレジスタのアドレスを示すマクロが定義されている。このマクロを使えば、GPIOの具体的なアドレスを知らなくてもGPIOの操作やGPIOによる入出力が可能であるし、プログラムのハードウェア依存性も少なくなる。sysdef.hの中で、GPIOのマクロ定義に関する部分をリスト1に示す。
/* * General purpose input/output (GPIO) */ #define GPIO(p, r) (GPIO_##p##_BASE + GPIO_##r) #define GPIO_P0_BASE 0x50000000 #define GPIO_P1_BASE 0x50000300 #define GPIO_OUT (0x504) #define GPIO_OUTSET (0x508) #define GPIO_OUTCLR (0x50C) #define GPIO_IN (0x510) #define GPIO_DIR (0x514) #define GPIO_DIRSET (0x518) #define GPIO_DIRCLR (0x51C) #define GPIO_LATCH (0x520) #define GPIO_DETECTMODE (0x524) #define GPIO_PIN_CNF(n) (0x700 + (n)*4) |
リスト1のGPIOのマクロ定義のうち、プログラムから直接利用するのは、最初に書かれたGPIO(p, r)である。GPIO(p, r)の一つ目の引数pはポートの区別(表2)を表し、P0またはP1を入れる。二つ目の引数rは各ポート内のレジスタの区別(表3)を表し、IN、DIR、PIN_CNF(n)などを入れる。
プログラムの初期設定の中では、ボタンスイッチA、Bに接続されたGPIO P0.14とGPIO P0.23を入力用に設定する。このため、GPIO P0のPIN_CNF[14]とPIN_CNF[23]のレジスタに0を書き込むが、GPIOのマクロ定義を使うと、これらのレジスタのアドレスはGPIO(P0, PIN_CNF(14))およびGPIO(P0, PIN_CNF(23))と表現できる。したがって、これらのピンを入力用に設定するプログラムは以下のようになる。
// GPIO P0の14ピンと23ピンを入力に設定 out_w(GPIO(P0, PIN_CNF(14)), 0); out_w(GPIO(P0, PIN_CNF(23)), 0); |
念のため、Eclipseの機能を使ってこのプログラムのソースプログラム中のマクロを展開し、GPIO(P0, PIN_CNF(14))の示す実際のメモリアドレス値を確認してみると、PIN_CNF(14)の値はPIN_CNF[14]レジスタのオフセットである(0x700+14*4)、GPIO(P0, PIN_CNF(14))の値はこれにGPIO P0のベースアドレスを加えた0x50000738になっている(図6)。
ボタンスイッチA、Bの状態を入力するには、GPIO P0のINレジスタを読みだした上で、最下位から14番目と23番目のビットの値をチェックする。GPIO P0のINレジスタのアドレスはGPIO(P0, IN)と表現できるので、このレジスタを読んで変数gpio_p0inに設定するプログラムは以下のようになる。
// GPIO P0のINレジスタを読んでgpio_p0inに設定 gpio_p0in = in_w(GPIO(P0, IN)); |
gpio_p0inの最下位から14番目のビットをチェックするには、C言語によるビット演算を行う。具体的には、最下位から14番目のビットのみが1で、他のビットが0になった符号無し整数を生成し、この整数とgpio_p0inとのビット単位のAND演算を行えばよい(図7)。最下位から14番目のビットのみが1となった符号無し整数は、C言語のシフト演算子を使って(1 << 14)と記述できるので、ボタンスイッチAが押されている場合にTRUEとなる変数btn_aを設定するプログラムは、以下のようになる。
btn_a = ((gpio_p0in & (1 << 14)) == 0); // P0.14が0の場合にTRUE |
ボタンスイッチBについても、gpio_p0inの最下位から23番目のビットを使って同じように処理する。
ボタンスイッチの押下状態を判定できるようになったので、その結果を約0.5秒(500ms)の間隔で繰り返しながらコンソールに出力するプログラムを作成する。できあがったプログラムがリスト2だ。このプログラムをEclipseの画面(図6)に入れてコンパイル、micro:bitの上で実行し、ボタンスイッチA、Bを何度か押してみると、その押下状態に応じてコンソール出力が変化することを確認できる(リスト3)。
/*----------------------------------------------------- * micro:bitのボタンスイッチの入力判定(µT-Kernel 3.0用) * * Copyright (C) 2022-2023 by T3 WG of TRON Forum *-----------------------------------------------------*/ #include |
microT-Kernel Version 3.00 Button_SW_A: off, Button_SW_B: off Button_SW_A: off, Button_SW_B: off Button_SW_A: on , Button_SW_B: off ←ボタンスイッチAを押す(ON) Button_SW_A: on , Button_SW_B: off Button_SW_A: on , Button_SW_B: off Button_SW_A: off, Button_SW_B: off ←ボタンスイッチAを離す(OFF) Button_SW_A: off, Button_SW_B: off Button_SW_A: off, Button_SW_B: on ←ボタンスイッチBを押す(ON) Button_SW_A: off, Button_SW_B: on Button_SW_A: on , Button_SW_B: on ←ボタンスイッチAとBを押す(ON) Button_SW_A: on , Button_SW_B: on Button_SW_A: off, Button_SW_B: off ←ボタンスイッチAとBを離す(OFF) Button_SW_A: off, Button_SW_B: off |
* * *
今回作成したプログラムは、結果的にはごく簡単で短いものであったが、ボタンスイッチというデバイスから入力を行うために、ハードウェアのマニュアルや回路図を参照して技術情報を確認し、それらの情報を使ってプログラミングを行った。組込み機器やIoTエッジノードを開発する際には、もっと複雑な周辺デバイスの入出力処理を行う場合も多いが、ハードウェアの技術情報の理解がその前提となるのは、今回のボタンスイッチの場合と同じである。本稿がその手ほどきとなれば幸いである。
次回も引き続き、micro:bitの周辺デバイスを扱う。