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

目次に戻る

前回の連載記事に戻る

[第7回] LEDのダイナミック点灯

本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第7回の本号では、micro:bitのボード上についているLEDを点滅したり、25個のLEDを制御したりしてドット文字の表示を試みる。「ダイナミック点灯」とよばれる方法で多数のLEDを制御するが、そのためにµT-Kernel 3.0の物理タイマの機能を使う。LED関連のハードウェア技術情報と合わせて理解しよう。

LEDのハードウェア

図1 micro:bitのLED部分の回路図
図1 micro:bitのLED部分の回路図

micro:bitの回路図(*1) のLEDの部分を図1に示す。25個のLEDが、ROW1..ROW5の5本の線とCOL1..COL5の5本の線との間のすべての組み合わせになる場所に、マトリックス(行列)状に配置されており、LEDマトリックスとよばれている。


まずは、左上のD2と書かれたLEDを一つ点滅させてみよう。このLEDを点灯させるためには、LEDの両端に電圧をかけて電流を流せばよい。LEDもダイオードの一種なので、電流の流れる向きは一方向に決まっており、この回路図の左上から右下の方向にのみ流れる。つまり、D2の左上の電圧を高く、右下の電圧を低くすれば、このLEDが点灯する。D2の左上側はROW1という線につながり、D2の右下側はCOL1という線につながっているので、ROW1を電圧の高いレベルに、COL1を電圧の低いレベルにすればよい。


ROW1..ROW5とCOL1..COL5の10本の線は、すべてmicro:bitのGPIOに接続されており、その対応関係は表1のとおりである。この情報は、micro:bitの回路図(*1) のTarget MCUの部分や、V2 pinmapの表(*2) から得られる。表1より、ROW1に接続されているGPIO P0のピン21に1を出力して電圧の高いレベル(H:High)に設定するとともに、COL1に接続されているGPIO P0のピン28に0を出力して電圧の低いレベル(L:Low)に設定すれば、D2のLEDが点灯する。一方、GPIO P0のピン21とピン28の状態がこれ以外の場合は、LEDに電流が流れず、LEDが点灯しない。つまり、ピン21に1を出力してピン28に0を出力した場合にのみD2のLEDが点灯し、ピン21に0を出力した場合や、ピン28に1を出力した場合には、D2のLEDが消灯するはずだ。

表1 LEDマトリックスとGPIOの接続
GPIOのポート番号とピン番号 LEDマトリックスのライン名称
P0.28 COL1
P0.11 COL2
P0.31 COL3
P1.05 COL4
P0.30 COL5
P0.21 ROW1
P0.22 ROW2
P0.15 ROW3
P0.24 ROW4
P0.19 ROW5

GPIOを出力用に初期設定

まず、GPIO P0のピン21とピン28を出力用に設定する必要がある。GPIOの使い方は連載第5回(TRONWARE VOL.199)で説明しているが、GPIOのピンを出力用に使うのは今回が初めてだ。


GPIOのピンnの機能を設定するには、PIN_CNF[n]レジスタに対して、入出力の方向(DIR)、入力時のピンの接続の有無(INPUT)、入力時のプルアップやプルダウンの指定(PULL)、出力時のドライブ方法の指定(DRIVE)などの機能に応じた設定値を書き込む。具体的な設定値はGPIOのマニュアル(*3) を参照して決めていく。今回は出力用の設定なので、DIRは出力(Output)を示す1とする。INPUTは使用しないので0、PULLはプルアップ/プルダウン無し(Disabled)の0である。DRIVEは出力の電気的な強さやハイインピーダンス(非接続状態)を制御する設定だが、これも標準(S0S1)を示す0でよい。これらの設定値を一つの32ビットデータにまとめてPIN_CNF[n]レジスタに書き込むのだが、最下位ビットに入るDIRのみが1で、それ以外のビットフィールドに入る設定値はすべて0なので、書き込むべき値は32ビット符号無し整数としての1となる。結局、GPIO P0のピン21とピン28を出力用に設定するには、PIN_CNF[21]とPIN_CNF[28]に1を書き込めばよい。

GPIOの各ピンに出力値を設定

GPIOの各ピンに0または1を出力するには、GPIOのOUT、OUTSET、OUTCLRのいずれかのレジスタを使う(表2)。いずれも32ビットのレジスタであり、各ビットがGPIOのピン0(最下位ビット)からピン31(最上位ビット)までの32本のピンに対応している。


OUTレジスタに書き込んだ場合は、書き込んだ32ビットのデータがそのまま32本のすべてのピンに出力される。つまり、ピン0からピン31までのすべてのピンの0または1を一括して設定することができる。逆に言えば、一部のピンのみの出力を設定し、それ以外のピンの出力を設定しない(変更しない)という使い方はできない。


一方、OUTSETレジスタに書き込んだ場合は、1を書き込んだビットに対応するピンには1が出力されるが、0を書き込んだビットに対応するピンには影響がなく、それ以前の出力値(0または1)がそのまま保持される。同様に、OUTCLRレジスタに書き込んだ場合は、1を書き込んだビットに対応するピンには0が出力されるが、0を書き込んだビットに対応するピンには影響がない。


GPIOの各ピンはいろいろな用途に使われており、今回の例でも、GPIO P0はLEDのほかにボタンスイッチやUART、スピーカーなど、他の周辺機器にも接続されている。LEDの制御のために特定のピンの出力を変えようとして、他の周辺機器が使っているピンに影響を与えるのは望ましくない。このような場合に便利な機能がOUTSETレジスタやOUTCLRレジスタであり、他のピンの状態を変化させずに特定のピンの出力のみを制御することができる。

表2 micro:bitのGPIOの出力用レジスタ
Register Offset Description
OUT 0x504 Write GPIO port
OUTSET 0x508 Set individual bits in GPIO port
OUTCLR 0x50C Clear individual bits in GPIO port

最初に一つのLEDを点滅

 

必要な技術情報がそろったので、実際のプログラムを作成する(リスト1)。D2のLEDを点灯させる条件は前述のとおりだが、その原理の確認も兼ねて、GPIO P0のピン21が0の場合と1の場合、ピン28が0の場合と1の場合の4通りの組み合わせの状態を順に試しながら、それぞれの場合におけるLEDの点灯状況を見られるようにした。4通りの状態のうち、ピン21が1でピン28が0の場合にのみD2のLEDが点灯し、他の3通りの場合はLEDが消灯するので、結果的にこのLEDは点滅するはずである。


プログラムの最初に、関数led_initを実行してGPIOの初期設定を行う。led_initの中では、LEDの制御に必要なGPIOピンを出力に設定する(リスト1の(※A))。D2のLEDの点灯に必要なGPIOピンはP0.21(ROW1に接続)とP0.28(COL1に接続)の2本のみであるが、本稿の後半では他のLEDの制御も行うので、ROW1..ROW5とCOL1..COL5の10本の線に接続されたGPIOピンをすべて出力に設定しておく。


また、GPIOの特定のピンに0または1を出力するための関数として、out_gpio_pinを定義している(※B)。この関数には、GPIOのポート番号を指定するport、出力先のピン番号を指定するpin、0または1の出力値を指定するvalの三つのパラメータがある。GPIOのOUTSETレジスタまたはOUTCLRレジスタの機能を使って、portとpinで指定されたGPIOピンに、valで指定された値を出力する。pinで指定された以外のピンの状態は変化しない。portとvalの値に応じて場合分けを行い、valが0の場合にはOUTCLRレジスタ、1の場合にはOUTSETレジスタを使って出力値を書き込むだけの簡単な処理であるが、LEDの制御では何度も出てくる処理なので、一つの関数にまとめた。


メインプログラムであるusermainの中では、led_initによるGPIOの初期設定を行ってから、out_gpio_pinを使ってCOL1からCOL5に1を出力しておく(※C)。これは、D2以外のLEDが点灯しないようにするための処理である。その後でfor文の無限ループを実行し、GPIO P0.21とGPIO P0.28の出力を0または1に変化させる処理を約1秒ごとに繰り返す。GPIO P0.21とGPIO P0.28に出力すべき値はgpio_p0_21とgpio_p0_28の変数に格納されており、tm_printfを使ってこれらの値をコンソールに表示した後に(※D)、out_gpio_pinを使ってGPIOから実際に出力する(※E)。


無限ループの後半では、gpio_p0_21とgpio_p0_28の値を変化させるための処理を行う。まずgpio_p0_21に1を加え(※F)、その値が2になった場合は0に戻すとともにgpio_p0_28に対して同様の処理を行って、この値も変化させる。その結果、この二つの変数値の組(gpio_p0_21, gpio_p0_28)は、(0,0)→(1,0)→(0,1)→(1,1)→(0,0)... と変化する。micro:bitのLED回路のハードウェア仕様により、このうち(gpio_p0_21, gpio_p0_28)=(1,0)の時のみD2のLEDが点灯し、それ以外の(0,0)、(0,1)、(1,1)の場合はLEDが点灯しないはずである。4回の変化で最初の状態に戻るため、LEDの点滅の周期は約1秒×4回=約4秒である。


このプログラムの実行結果をリスト2に示す。想定したとおり、約4秒の周期でLEDが点滅し、GPIO P0.21(→ROW1)が1でGPIO P0.28(→COL1)が0の時のみLEDが点灯することがわかった。


リスト2 一つのLED(左上のD2)を点滅するプログラムのコンソール出力

microT-Kernel Version 3.00

GPIO P0.21=0(ROW1), GPIO P0.28=0(COL1) → D2のLEDが消灯 ●
GPIO P0.21=1(ROW1), GPIO P0.28=0(COL1) → D2のLEDが点灯 〇
GPIO P0.21=0(ROW1), GPIO P0.28=1(COL1) → D2のLEDが消灯 ●
GPIO P0.21=1(ROW1), GPIO P0.28=1(COL1) → D2のLEDが消灯 ●
GPIO P0.21=0(ROW1), GPIO P0.28=0(COL1) → D2のLEDが消灯 ●
GPIO P0.21=1(ROW1), GPIO P0.28=0(COL1) → D2のLEDが点灯 〇
GPIO P0.21=0(ROW1), GPIO P0.28=1(COL1) → D2のLEDが消灯 ●
 

リスト1 一つのLED(左上のD2)を点滅するプログラム

/*--------------------------------------------------------
  *  micro:bitの1つのLED(左上のD2)を点滅(µT-Kernel 3.0用)
  *
  *  Copyright (C) 2022-2023 by T3 WG of TRON Forum
  *------------------------------------------------------*/
  #include <tk/tkernel.h>
  #include <tm/tmonitor.h>

  // LED制御のためのGPIOの初期設定
  LOCAL void led_init(void)
  {
      // COL1..COL5とROW1..ROW5に接続されたGPIOピンを
      // 出力に設定(※A)
      out_w(GPIO(P0, PIN_CNF(28)), 1);
  			// GPIO P0.28(COL1に接続)
      out_w(GPIO(P0, PIN_CNF(11)), 1);
  			// GPIO P0.11(COL2に接続)
      out_w(GPIO(P0, PIN_CNF(31)), 1);
  			// GPIO P0.31(COL3に接続)
      out_w(GPIO(P1, PIN_CNF(05)), 1);
  			// GPIO P1.05(COL4に接続)
      out_w(GPIO(P0, PIN_CNF(30)), 1);
  			// GPIO P0.30(COL5に接続)
      out_w(GPIO(P0, PIN_CNF(21)), 1);
  			// GPIO P0.21(ROW1に接続)
      out_w(GPIO(P0, PIN_CNF(22)), 1);
  			// GPIO P0.22(ROW2に接続)
      out_w(GPIO(P0, PIN_CNF(15)), 1);
  			// GPIO P0.15(ROW3に接続)
      out_w(GPIO(P0, PIN_CNF(24)), 1);
  			// GPIO P0.24(ROW4に接続)
      out_w(GPIO(P0, PIN_CNF(19)), 1);
  			// GPIO P0.19(ROW5に接続)
  }

  // GPIOの特定のピンpinの出力をval(0または1)に設定(※B)
  //  pinで指定された以外のピンの状態は変化しない
  LOCAL void out_gpio_pin(UW port, UW pin, UW val)
  {
      INT port_addr;

      if(port == 1){                          // P1の場合
          if(val)
              port_addr = GPIO(P1, OUTSET);   // P1でvalが1の場合
          else
              port_addr = GPIO(P1, OUTCLR);   // P1でvalが0の場合

      } else {                                // P0の場合
          if(val)
              port_addr = GPIO(P0, OUTSET);   // P0でvalが1の場合
              else
              port_addr = GPIO(P0, OUTCLR);   // P0でvalが0の場合
      }
      out_w(port_addr, (1 << pin));	  // 指定のピンに1を出力
  }

  // 1つのLED(左上のD2)を点滅するプログラム
  EXPORT void usermain(void)
  {
      UW gpio_p0_21 = 0;          // GPIO P0.21の出力、0または1
      UW gpio_p0_28 = 0;          // GPIO P0.28の出力、0または1

      led_init();

      out_gpio_pin(0, 28, 1);     // GPIO P0.28-COL1に1を出力(※C)
      out_gpio_pin(0, 11, 1);     // GPIO P0.11-COL2に1を出力(※C)
      out_gpio_pin(0, 31, 1);     // GPIO P0.31-COL3に1を出力(※C)
      out_gpio_pin(1,  5, 1);     // GPIO P1.05-COL4に1を出力(※C)
      out_gpio_pin(0, 30, 1);     // GPIO P0.30-COL5に1を出力(※C)

      for (;;) {                  // 約1秒ごとに永久に繰り返し

          // GPIO P0.21とGPIO P0.28の出力値をコンソールに表示(※D)
          tm_printf("GPIO P0.21=%d(ROW1),
                     GPIO P0.28=%d(COL1)\n",
                     gpio_p0_21, gpio_p0_28);

          // GPIOへの出力(※E)
          out_gpio_pin(0, 21, gpio_p0_21);
  				// GPIO P0.21-ROW1にgpio_p0_21を出力
          out_gpio_pin(0, 28, gpio_p0_28);
  				// GPIO P0.28-COL1にgpio_p0_28を出力

          // gpio_p0_21とgpio_p0_28を更新して4通りの状態を順に生成
          //  (gpio_p0_21,gpio_p0_28)が(0,0)→(1,0)→(0,1)→(1,1)→
          //  (0,0)...と変化
          if(++gpio_p0_21 >= 2){
  				// gpio_p0_21に1を加えて更新(※F)
              // gpio_p0_21が1から2に更新された場合
              gpio_p0_21 = 0;            // gpio_p0_21を0に戻す
              if(++gpio_p0_28 >= 2){    // gpio_p0_28に1を加えて更新
                  // gpio_p0_28が1から2に更新された場合
                  gpio_p0_28 = 0;        // gpio_p0_28を0に戻す
              }
          }
          tk_dly_tsk(1000);
  		            // 1秒(1000ms)の間隔でループ
      }
  }


次に1列5個のLEDを制御

 

micro:bitのLEDは、5行5列で25個ある。一つのLEDの制御方法はわかったので、次に1行5個のLEDを同時に制御してみよう。ふたたび図1の回路図を見て確認する。1行目(ROW1)にはCOL1のD2からCOL5のD10まで5個のLEDがあるが、それらの接続方法は先ほど試したCOL1のD2と同じである。したがって、D2を制御したのとまったく同じ方法で、1行目(ROW1)のほかの4個のLEDも制御できそうである。そこで次の例題では、指定された5ビットのビットパターンを1行目(ROW1)の5個のLEDに表示し、その後は右側にスクロールして消えていくようにしてみよう。


作成したプログラムをリスト3に示す。この中では、プログラムで指定したビットパターンをそのままLEDに表示できるように、bitptn_gpio_pinおよびout_led_colという関数を定義した。これらの関数は、この後の例題でも使用する。このほか、リスト1で作成したled_initとout_gpio_pinについても、そのままリスト3で使用する。


bitptn_gpio_pin(リスト3の(※G))は、bitptnで指定したビットパターンの最下位からnビット目(n=1..5)のビット値をチェックし、そのビット値が1の場合に、GPIOのportとpinで指定されたピンにvalを出力する(※H)。逆に、そのビット値が0だった場合は、指定されたピンにvalの反対の値、すなわちvalが0だった場合は1を、valが0だった場合は1を出力する(※J)。たとえばbitptn=0b01101でn=3の場合、bitptnの右端(最下位ビット側)から左に三つ目のビット値が1なので、valの値をそのままGPIOのピンに出力する。bitptn=0b01101でn=2の場合は、チェックしたビット値が0なので、valの反対の値を出力する。GPIOへの出力部分は、リスト1で作成した関数out_gpio_pinを呼び出している。


out_led_col(※K)は、5ビットのビットパターンbitptnに応じてLEDを点灯させるために、LEDのCOL1..COL5に接続されたGPIOの各ピンに0または1を出力する関数である。ビットパターンbitptnの最下位から5ビット目、4ビット目...の順に、bitptn_gpio_pinを5回呼び出して、各ビットに対応したGPIOへの出力処理を行っている。


1行目(ROW1)のCOL1からCOL5のLEDを点灯させるには、ROW1を1に設定するとともに、COL1..COL5を0に設定する必要がある。そのため、bitptn_gpio_pinを呼び出す際の引数valには、すべて0を指定している(※L)。その結果、bitptnのチェック対象ビットが1の場合は、COL1..COL5に接続されたGPIOのピンが0になり、ROW1が1になっていればLEDが点灯する。逆に、bitptnのチェック対象ビットが0の場合は、COL1..COL5に接続されたGPIOのピンが1になり、ROW1が1か0かにかかわらずLEDは点灯しない。


この例題では、5ビットのビットパターンを5個のLEDに表示した後、そのビットパターンが右スクロールして消えていく。ビットパターンの表示にはout_led_colを使い、ビットパターンの右スクロールにはC言語の右シフト演算を使えばよい。これらの処理をまとめた関数がrow1_led_scrollである(※M)。usermainの中では、row1_led_scrollに何種類かのビットパターンを与えて動作を試せるようにした。micro:bitで実際に実行すると、表示されたビットパターンが1行目のLEDに表示され、右スクロールしていく様子が確認できる。

 

リスト3 5個のLED(ROW1)にビットパターンを表示するプログラム

/*---------------------------------------------------------------
*  micro:bitの5個のLED(ROW1)にビットパターンを表示
*  (µT-Kernel 3.0用)
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/

   (途中略)

// ビットパターンbitptnの指定のビットに応じてGPIOの指定の
// ピンを制御(※G)
// bitptnの最下位からnビット目(n=1..5)が1の場合はピンpinの
// 出力をvalに設定
// そのビットが0の場合はvalの反対の値(1→0,0→1)をピンpinの
// 出力に設定

LOCAL void bitptn_gpio_pin(UW bitptn, UW n, UW port, UW pin, UW val)
{
   if(bitptn & (1 << (n - 1))){     // bitptnの最下位からnビット目をチェック
       out_gpio_pin(port, pin, val);     // 1の場合はGPIOにvalを出力(※H)
   } else {
       out_gpio_pin(port, pin, (! val)); // 0の場合はvalの反対の値を出力(※J)
   }
}

// 5ビットのビットパターンbitptnに応じてLEDのCOL1..COL5を制御(※K)
LOCAL void out_led_col(UW bitptn)
{
   // 最下位から5番目のビットが1の場合にGPIO P0.28(COL1)を0に設定(※L)
   bitptn_gpio_pin(bitptn, 5, 0, 28, 0);

   // 最下位から4番目のビットが1の場合にGPIO P0.11(COL2)を0に設定(※L)
   bitptn_gpio_pin(bitptn, 4, 0, 11, 0);

   // 最下位から3番目のビットが1の場合にGPIO P0.31(COL3)を0に設定(※L)
   bitptn_gpio_pin(bitptn, 3, 0, 31, 0);

   // 最下位から2番目のビットが1の場合にGPIO P1.05(COL4)を0に設定(※L)
   bitptn_gpio_pin(bitptn, 2, 1, 5, 0);

   // 最下位のビットが1の場合にGPIO P0.30(COL5)を0に設定(※L)
   bitptn_gpio_pin(bitptn, 1, 0, 30, 0);
}

// ビットパターンを1行目(ROW1)の5個のLEDに表示してから右側に
// スクロール(※M)
LOCAL void row1_led_scroll(UW bitptn)
{
   out_gpio_pin(0, 21, 1);   // GPIO P0.21(ROW1に接続)を1に設定

   out_led_col(bitptn);    // 指定のbitptnを1行目(ROW1)の5個のLEDに表示
   tk_dly_tsk(1000);       // LEDに表示したまま1秒間(1000ms)待つ

   while(bitptn != 0){       // bitptnが0でない場合に繰り返し
       bitptn = (bitptn >> 1); // bitptnを1ビットだけ右シフト
       out_led_col(bitptn);  // シフト後のbitptnを1行目(ROW1)の5個の
                             // LEDに表示
       tk_dly_tsk(500);      // そのまま0.5秒間(500ms)待つ
   }
}

// ROW1の5個のLEDにビットパターンを表示するプログラム
EXPORT void usermain(void)
{
   led_init();

   row1_led_scroll(0b11111);   // 最初の点灯パターン: 〇〇〇〇〇
   row1_led_scroll(0b10101);   // 最初の点灯パターン: 〇●〇●〇
   row1_led_scroll(0b11000);   // 最初の点灯パターン: 〇〇●●●

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


5行5列のLEDをダイナミック点灯

 

いよいよ、micro:bitの5行5列25個すべてのLEDの表示に挑戦する。LEDマトリックスの制御という意味では、ここからが本題だ。


本稿の執筆中に、TRONのリアルタイムOSに対するIEEE Milestone受賞のニュースが届いた。日本国内での受賞例は累計でも40件程度という貴重な賞だが、その中の最古参に近いものとして、「電子式テレビジョンの開発」がある。1926年12月、高柳健次郎博士がテレビジョンとよばれる映像送信の実験に成功し、その後の世界的なテレビ普及の基礎的な技術となった(*4) 。この時にブラウン管に映し出された映像が、カタカナの「イ」の文字だったそうである。電子技術者の大先輩による約100年前の故事にあやかって、micro:bitのLEDマトリックスに「イ」の文字を表示してみることにしよう。


1行目(ROW1)の5個のLEDには表示できるようになったので、2行目(ROW2)以降も同じ方法で表示できそうである。しかし、図1の回路図から理解できるように、LEDの各列(COL1..COL5)への制御が、1行目(ROW1)、2行目(ROW2)を含めたすべての行に対して共通に作用してしまうという問題がある。つまり、1行目(ROW1)も2行目(ROW2)も表示させようとして、GPIO P0.21(ROW1に接続)にもGPIO P0.22(ROW2に接続)にも1を出力し、この状態でout_led_colを実行すると、1行目と2行目の5個のLEDに表示されるビットパターンがまったく同じになってしまう。結局、LEDの表示が縦方向に変化することはなく、一次元バーコードのような縞々の表示しかできない。


この問題を解決するのが、「ダイナミック点灯」とよばれる方法だ。LEDの各行を同時に表示するのではなく、短い時間間隔で1行目、2行目...を順に表示していくのである。1行目のLEDの表示中は、他の行のLEDはすべて消灯させる。2行目以降も同様であり、複数行のLEDが同時に点灯することはない。out_led_colによるビットパターンの指定はすべての行に作用するが、ROW1..ROW5を0にした行のLEDはout_led_colの指定にかかわらずまったく点灯しないので、1行ずつ順に点灯させるような制御が可能である。この表示の変化が十分に速く、人間の目にはわからない程度であれば、すべての行のLEDが連続して点灯しているように見える。ちなみに、高柳健次郎博士が研究開発した電子式テレビジョンも、ブラウン管の表面に当てた点光源をスキャンしながら2次元の画像を作り出しており、画面全体が同時に映っているわけではない。これも一種のダイナミック点灯であった。


ダイナミック点灯を使って、5行5列すべてのLEDに指定のビットパターンを表示するプログラム例をリスト4に示す。この中では二つの関数を定義している。out_row_gpio(リスト4の(※N))は、rowでROW1..ROW5のいずれかの行の番号を指定し、そこに接続されたGPIOピンにvalを設定する関数である。rowの番号に応じて不規則にGPIOのピン番号が変わるため、switch文を使ってrow=1からrow=5までの場合分けを行っている。set_row_gpio(※P)は、ROW1..ROW5に接続されたGPIOピンのうち、rowで指定した1本のみを1に設定し、他は0に設定する関数である。ROW1..ROW5のそれぞれに対して、forループを使ってout_row_gpioを合計5回呼び出し、rowと一致する1本にのみ1を、それ以外には0を設定する(※Q)。この設定により、rowで指定した行のLEDのみを点灯させる。このほか、リスト3と共通のled_init、out_gpio_pin、bitptn_gpio_pin、out_led_colを使う。


これらの関数を使って、LEDマトリックスに「イ」の文字を表示する。文字のビットパターンのデータは、ledptn_iという配列で定義した(※R)。LEDの各行を表示する時間間隔は0.1秒(100ms)としている(※S)。

このプログラムを実行すると、各行の表示の速度が遅いため、上から1行ずつ流れるように見えてしまう。残念ながら、「イ」の文字には見えない。とはいえ、これは想定内である。ダイナミック点灯の動作を確認するために、わざと長めのディレイを入れたのが原因だ。


最終的には、「イ」の文字が普通にLEDに表示されるようにしたい。ディレイが長いと行単位でバラバラの表示になってしまうので、このディレイを最小の10msにして試してみよう。リスト4の(※S)のtk_dly_tskの引数を100から10に変更すれば、LEDの各行を表示する時間が約10msとなり(*5) 、5行全部の表示にかかる時間は約50ms、1秒間に約20回のタイミングで「イ」の文字が表示される計算だ。


ディレイを10msにしたプログラムを実際に動かしてみる。一応は「イ」の文字が見えるものの、チラつきが気になり、普通の表示と表現するには難がある。チラつきを無くすには、もっとディレイの時間を短くして、各行の表示切替速度を速める必要がある。そのためには、10msよりも短い時間間隔でプログラムを制御しなければならない。

 

リスト4 5行5列のLEDに「イ」の文字を表示するプログラム

/*---------------------------------------------------------------
*  micro:bitの5行5列のLEDに「イ」の文字を表示
*  (µT-Kernel 3.0用)
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/

  (途中略)

// ROW1..ROW5のいずれか(rowで番号指定)に接続されたGPIOピンに
// valを設定(※N)
LOCAL void out_row_gpio(UW row, UW val)
{
    switch(row){
      case 1:
        out_gpio_pin(0, 21, val);  // GPIO P0.21(ROW1に接続)にvalを設定
        return;
      case 2:
        out_gpio_pin(0, 22, val);  // GPIO P0.22(ROW2に接続)にvalを設定
        return;
      case 3:
      out_gpio_pin(0, 15, val);  // GPIO P0.15(ROW3に接続)にvalを設定
        return;
      case 4:
        out_gpio_pin(0, 24, val);  // GPIO P0.24(ROW4に接続)にvalを設定
        return;
      case 5:
        out_gpio_pin(0, 19, val);  // GPIO P0.19(ROW5に接続)にvalを設定
        return;
}
}

// rowで指定した行のみを点灯させるためのGPIOピンの設定(※P)
//  ROW1..ROW5に接続されたGPIOピンのうち、
//  rowで指定した1本のみを1に設定し、他は0に設定する
LOCAL void set_row_gpio(UW row)
{
    UW  cnt, val;
    for(cnt = 1; cnt <= 5; cnt++){
        out_row_gpio(cnt, ((cnt == row) ? 1 : 0));
                // 指定の1本のみ1を設定(※Q)
    }
}

// 5行5列のLEDの点灯パターンの定義(※R)
//  「イ」の文字を表現
LOCAL UB ledptn_i[] = {
  0b00000001,                      // 点灯パターン: ●●●●〇
  0b00001110,                      // 点灯パターン: ●〇〇〇●
  0b00010100,                      // 点灯パターン: 〇●〇●●
  0b00000100,                      // 点灯パターン: ●●〇●●
  0b00000100                       // 点灯パターン: ●●〇●●
};

// 5行5列のLEDに「イ」の文字を表示
EXPORT void usermain(void)
{
   UW row;
   led_init();

   for(;;){                                // 永久に繰り返し
      for(row = 1; row <= 5 ; row++){  // 1行目..5行目(row=1..5)を
                                                         // 順に表示
          set_row_gpio(row);
          out_led_col(ledptn_i[row - 1]); // row行目の点灯パターン指定
          tk_dly_tsk(100);             // 0.1秒待ってから次の行へ(※S)
        }
    }
}


物理タイマの活用

 

µT-Kernel 3.0で制御する時間の単位を短くするには、システムタイマのティック時間(タイマ割込みの時間間隔)の設定を変更し、標準の10msよりも短い時間にする方法がある。これは、µT-Kernel 3.0のコンフィグレーションファイル(config/comfig.h)の中のCNF_TIMER_PERIODの設定を変更すれば可能である。しかし、この場合はタイマ割込みの回数が増えるため、システムのオーバーヘッドも増大するという問題がある。


そこで、今回は別の方法として、物理タイマを使ってみよう。物理タイマであれば、任意の時間間隔で物理タイマハンドラを起動することが可能であり、システムのオーバーヘッドも最小限で済む。物理タイマを使って、1msごとにLEDの表示行を切り替えるようにしたプログラムをリスト5に示す。


物理タイマに関する情報を得るためには、実装仕様書の「7.3 物理タイマ機能」の項目を参照する。micro:bit用のµT-Kernel 3.0では、5個の物理タイマが使用可能であり、1から5の物理タイマ番号が割り当てられていることがわかる。また、物理タイマのクロックは16MHzと記載されている。


物理タイマでは、このクロックをハードウェアで常時カウントしており、その数がStartPhysicalTimerで指定した上限値limitに達した場合に、その次のクロックで物理タイマハンドラが起動される。今回の物理タイマのクロックは16MHzなので、limitを16M(=16000000)にすると、ほぼ1秒ごとの周期でハンドラが起動される。この周期時間をP秒にするには、limitをP×16000000にすればよい。しかし、実際の周期時間は秒単位よりももっと短く、1ミリ秒(ms)前後を想定しているので、さらに細かい単位であるマイクロ秒(µs)を使って周期時間の設定ができるようにしておく。周期時間をQµsとすれば、P=Q÷1000000から、limit=(Q÷1000000)×16000000=Q×16と計算できる。クロック16MHzのM(メガ)の部分と、µs(マイクロ秒)単位のµの部分が相殺した形だ。このほか、物理タイマの仕様では、limitに達した次のクロックで物理タイマハンドラが起動されることになっているため、limitに設定する値は一つ減らしておく必要がある。リスト5では、このようにしてStartPhysicalTimerで指定する上限値limitを計算した(リスト5の(※T))。


リスト5では、LEDの表示行を1行ずつ切り替えるために、led_switch_row_hdrという関数を作成している(※U)。usermainでは、その関数を物理タイマハンドラとして定義してから(※V)、物理タイマを動作させる。これで、25個のLEDに表示された「イ」の文字が普通に見えるようになった(図2)。


図2 micro:bitのLEDに表示された「イ」の文字
図2 micro:bitのLEDに表示された「イ」の文字

 

リスト5 物理タイマを使ってmicro:bitのLEDに「イ」を表示するプログラム

/*---------------------------------------------------------------
*  物理タイマを使ってmicro:bitのLEDに「イ」を表示
*  (μT-Kernel 3.0用)
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/

   (途中略)

LOCAL W led_disp_row = 5;        // 現在表示中のLEDの行の番号(1..5)

// LEDの表示行を切り替える物理タイマハンドラ(※U)
LOCAL void led_switch_row_hdr(void *exinf)
{
   if(++led_disp_row > 5)      // 表示中のLEDの行の番号(1..5)を更新
       led_disp_row = 1;                    // 行の番号が5を超えたら1に戻る
   set_row_gpio(led_disp_row);              // 表示行のGPIOのピンを設定
   out_led_col(ledptn_i[led_disp_row - 1]); // 表示行の点灯パターンを指定
}

// 物理タイマによるダイナミック点灯
const T_DPTMR dptmr = {0, TA_HLNG, &led_switch_row_hdr};
                                            // ハンドラ定義情報
const UINT ptmrno = 1;             // 物理タイマ番号として1を使用
const INT ptmr_clk_mhz = 16;       // 物理タイマのクロック(MHz単位)

EXPORT void usermain(void)
{
   const INT cycle_us = 1000;     // ハンドラの起動周期(μs単位)、
                                   // 1000µs=1ms
   const INT limit = cycle_us * ptmr_clk_mhz - 1;
				     // 物理タイマの上限値(※T)

   led_init();

   DefinePhysicalTimerHandler(ptmrno, &dptmr);
				     // 物理タイマハンドラの定義(※V)
   StartPhysicalTimer(ptmrno, limit, TA_CYC_PTMR);
				     // 物理タイマの動作開始

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


LEDマトリックスの功罪

micro:bitの25個のLEDは行と列のマトリックスで接続され、その制御にはダイナミック点灯というやや面倒な処理が必要であった。実は、多数のLEDを行と列のマトリックスで接続して制御することは、micro:bitに限らず他の機器でも一般に行われている。大きなメリットとして、LEDに接続する配線の数を減らすことができるからだ。N行M列のLEDマトリックスを制御するには、電源と(N+M)本の線あれば済む。一方、N行M列のLEDをすべて個別に制御する配線では、電源のほかに(N×M)本の線が必要だ。NとMが大きくなると、この差も掛け算で大きくなる。たとえば、通勤電車のドアの上に設置されているLED表示器の一例として、縦24ドット×横160ドットという情報があった。このLEDをすべて個別に制御しようとすると、24×160=3840本の制御線が必要だが、マトリックス方式にすれば24+160=184本の制御線で済む。


しかしながら、ダイナミック点灯の動作原理から、LEDの各ドットが同時に点灯するわけでなく、1行ごと、あるいは1列ごとに時分割で点灯することになる。これは、人間の目で見るには違和感のないように調整されているが、短時間で切り取った写真にすると、すべての行や列が写らない事態となる。電車の先頭部の列車名や行先の表示にもダイナミック点灯のLEDマトリックスの使われている例が多いが、これは、いわゆる「撮り鉄」泣かせである。天気の良い日に短いシャッタースピードで撮影すると、列車名や行先を表示したLEDが一部しか写らず、文字や絵が途中で切れて読めなくなってしまうのだ。ダイナミック点灯により人間の目はだますことができても、より高性能なカメラをだますことはできないのである。一見して単純に制御できそうなLEDにも、奥深いところがある。


* * *

今回はmicro:bitのLEDの点灯を制御するプログラムを作成した。一つのLEDを点灯するだけなら簡単なのだが、多数のLEDを制御するにはダイナミック点灯が必要であり、µT-Kernel 3.0の物理タイマ機能が有効であった。


次回は、LEDとは別のデバイスを扱う予定だ。



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

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

(*1)
 micro:bitの回路図 https://github.com/microbit-foundation/microbit-v2-hardware/blob/main/V2.00/MicroBit_V2.0.0_S_schematic.PDF
(*2)
 micro:bit Schematics V2 pinmap https://tech.microbit.org/hardware/schematic/#v2-pinmap
(*3)
 nRF52833のGPIOマニュアル https://infocenter.nordicsemi.com/topic/ps_nrf52833/gpio.html?cp=4_1_0_5_7
(*4)
 IEEE Milestone(11) 電子式テレビジョンの開発 http://www.ieee-jp.org/japancouncil/jchc/adm/milestone/11takayanagi.pdf
(*5)
 tk_dly_tskの引数が10の場合でも、実際のディレイ時間は10msよりも長くなり、20ms近くになる場合もある。この理由については、µT-Kernel 3.0仕様書のタイマ割込み間隔(TTimPeriod)およびタイムアウトの説明を参照のこと。
  • 本ページは、「TRONWARE Vol.202」の掲載記事「micro:bitでµT-Kernel 3.0を動かそう 第7回 LEDのダイナミック点灯」をWebで公開したものです。

ページの先頭に戻る

 

次回の連載記事に進む