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

目次に戻る

前回の連載記事に戻る

[第8回] ドレミファ音階の再生とティック時間

本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第8回の本号では、micro:bitから音を出してみよう。µT-Kernel 3.0の持つ時間関連の機能をいろいろ試しながら、ドレミファの音階を再生するところまで進めていく。

内蔵スピーカーのハードウェア

図1 micro:bitのスピーカー周辺の回路図
図1 micro:bitのスピーカー周辺の回路図
(*1のSpeaker部分から抜粋)

micro:bitでは、内蔵のスピーカー(Speaker)を使って音を鳴らすことができる。micro:bitのスピーカーは、LEDマトリックスのある面の反対側(裏面)の中央付近に斜めに搭載された、やや厚みのある四角いデバイスである。その左下の基板上には、"SPEAKER"というラベルが印刷されている。


例によって、最初にハードウェア仕様を確認しよう。micro:bitの回路図(*1) では、p.2の左上のSpeakerの欄に、スピーカーを駆動するためのアナログ回路の回路図が掲載されている(図1)。これを見ると、スピーカーに対してはKL27_DACとSPEAKERの2本の入力信号がある。このうちのKL27_DACは、開発用PCからのプログラム転送などに使用するInterface MCU(NXP KL27Z)に接続されている。この接続は、Interface MCUから音を出したい場合に利用する。もう一方のSPEAKERの入力は、Target MCUの回路図の右上にある同名のラベルから、Target MCUのP0.00/XL1に接続されている(連載第5回の図3参照)。P0.00というのは、GPIOのポートP0、ピン番号0を意味する。つまり、GPIO P0.00の出力がスピーカーにつながっている。V2 pinmapの表(*2) にも同じ情報があり、GPIO on nRF52833のP0.00に対するAllocationがSPEAKERとなっている。


GPIO P0.00の出力は、電圧の高いレベル(H:High)と電圧の低いレベル(L:Low)の2値のみをとるデジタル信号であり、連続的な電圧の変化が可能なアナログ信号ではない。そのため、きれいな波形の音色を出すのは難しいが、うまく制御すれば簡単なメロディや効果音を再生することができる。スピーカーに対して1と0(電圧のHとL)を繰り返す信号を送ると、その信号の変化に応じて振動板とよばれる部分が振動するため、その周囲の空気も振動して音波が発生する。これが音として人間の耳に聞こえる。この信号を0→1→0→1→……と切り替える速度が、音の周波数、つまり音の高さになる。1と0の信号変化の繰り返しが1秒間に1,000回であれば、1,000ヘルツ(Hz)の音になるわけだ。

タスク遅延を使ったブザー音

 

スピーカーから音を出すには、GPIO P0.00の出力を、0→1→0→1→……と高速で変化させればよい。そのための最初のプログラム例をリスト1に示す。このプログラムでは、スピーカーへの出力信号の変化があった時刻を実行後に確認し、発生した音の周波数がわかるようにするため、連載第4回で作成したログの記録機能を利用している。ログを記録するための関数や変数は連載第4回のときと同じであるが、ログの最大数(MAX_LOG)を50から2,000に増やした。


この先、処理内容を少しずつ変えながらいくつかの例題を実行するので、実行後のログの確認や整理がしやすくなるように、usermainの最初でプログラムの名称をログに記録しておく(リスト1の(※A))。また、今回はシステムタイマのティック時間(タイマ割込みの時間間隔)が重要な意味を持つので、この値を表すCNF_TIMER_PERIODもログに記録する。さらに、half_cycle_msの値もログに記録する。これらの値の意味については後で説明する。


次に、GPIO P0.00を出力に設定してから(※B)、スピーカーへの出力の0と1を周期的に反転するタスクSを生成して起動する(※C)。タスクSの優先度はusermainよりも低いため、usermainはtk_dly_tskで1秒間の時間待ちをして、その間にタスクSを実行させている。1秒間の時間待ちが終わった後は、タスクSの動作が必要なくなるので、tk_ter_tsk(*3) で強制終了し(※D)、usermainの最後にprint_logでログを表示する。


タスクSでは(※E)、for文による無限ループの中で、speaker_onoffという関数の呼び出しとtk_dly_tskによる時間待ち(※H)を繰り返す。speaker_onoffはスピーカーへの出力値の0と1を反転する関数であり(※F)、ログへの記録を行った後に、連載第7回で作成したout_gpio_pinを使って0または1を出力する。出力先がGPIO P0.00なので、out_gpio_pinではport=0, pin=0を指定している(※G)。この出力値は静的変数speaker_valに保存されており、speaker_onoffがよばれるごとに0と1を反転する(※J)。


tk_dly_tskの引数はhalf_cycle_msで指定されているが、この値はできるだけ小さくしたいので、システムタイマのティック時間と同じ10ミリ秒(ms)とした(※K)。この時間ごとにfor文のループが回るが、スピーカーへの出力値はループ1回ごとに0→1あるいは1→0のどちらか一方の変化のみ行うので、0→1→0の1周期にはループ2回分の時間がかかる。つまり、forループ1回の時間は音の振動の周期時間の半分にあたる。

リスト1のプログラムをmicro:bitで実行してみよう。すると、たしかに1秒間だけ音が聞こえるのだが、ちょっと間の抜けたブザー音のようである。


リスト1を実行した際のコンソール出力をリスト2に示す。6行目のspeaker_offから20ミリ秒後に7行目のspeaker_onが実行され、さらにその20ミリ秒後に8行目のspeaker_offが実行されている。つまり、スピーカーへの出力値が0→1→0と変化する際の周期時間は、6行目から8行目までの40ミリ秒である。したがって、この音の周波数は1,000ミリ秒÷40ミリ秒=25Hzと計算できる(*4)


人間の耳に聞こえる音の周波数範囲は、20Hzから20,000Hz程度の間だといわれている。25Hzはその下限に近い低い音である。実用的な音を出すには、もっと高い周波数で変化する信号をスピーカーに送らなければならない。そのためには、tk_dly_tsk(※H)による遅延時間をもっと短くして、forループを高速で回す必要がある。

 

リスト1 タスク遅延を使ったブザー制御のプログラム

/*---------------------------------------------------------------
*  タスク遅延を使ったブザー制御(µT-Kernel 3.0用)
*
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/
              ・
              ・
              ・
//-------- スピーカーへの出力 -----------------------------------
LOCAL BOOL  speaker_val = FALSE;    	// 現在のスピーカーへの出力値

// スピーカーへの出力値の0と1を反転する関数(※F)
void speaker_onoff(void)
{
    if(speaker_val)                 	// speaker_valの値に応じてログを記録
        log("speaker_on", NO_PARAM);  // スピーカーへの出力が0から1に変化
    else
        log("speaker_off", NO_PARAM);	// スピーカーへの出力が1から0に変化

// スピーカーに接続されたGPIOピン(P0.00)にspeaker_valの値を出力
    out_gpio_pin(0, 0, speaker_val);  // port=0, pin=0を指定(※G)

// 次の出力のために値を反転(※J)
    speaker_val = (! speaker_val);
}

//-------- タスクによるスピーカーの出力制御 ---------------------
const INT half_cycle_ms = 10;   	// 音の周期時間の半分(ミリ秒単位)(※K)

// スピーカーへの出力を周期的に反転するタスクS(※E)
void task_S( INT stacd, void *exinf )
{
    log("Task S: start", NO_PARAM);

    for(;;){                        	// 以下を永久に繰り返し
        speaker_onoff();            	// スピーカーへの出力を反転
        tk_dly_tsk(half_cycle_ms);  	// 周期時間の半分だけ待つ(※H)
    }
}

// タスクSの関連情報
const T_CTSK ctskS = {0, (TA_HLNG | TA_RNG3), &task_S, 10, 1024, 0};
ID tidS;                            	// タスクSのID

EXPORT void usermain(void)
{
    // プログラムの名称とティック時間などの情報をログに記録(※A)
    log("List1: Buzzer using Delayed Task", NO_PARAM);
    log("CNF_TIMER_PERIOD=", CNF_TIMER_PERIOD);
    log("half_cycle_ms=", half_cycle_ms);
    log("Start User-main program", NO_PARAM);

    // スピーカーに接続されたGPIOピン(P0.00)を出力に設定(※B)
    out_w(GPIO(P0, PIN_CNF(0)), (1 << 0));

    tidS = tk_cre_tsk(&ctskS);      	// タスクS(優先度10)を生成
    tk_sta_tsk(tidS, 0);            	// タスクSを起動(※C)
    tk_dly_tsk(1000);               	// 1秒間の時間待ちの間にタスクSを実行
    tk_ter_tsk(tidS);               	// タスクSを強制終了(※D)

    log("End User-main program", NO_PARAM);
    print_log();                    	// ログを表示
    tk_slp_tsk(TMO_FEVR);           	// 永久待ち
}


リスト2 タスク遅延を使ったブザー制御(リスト1)のコンソール出力

microT-Kernel Version 3.00

1:     0ms: List1: Buzzer using Delayed Task
2:     0ms: CNF_TIMER_PERIOD=10
3:     0ms: half_cycle_ms=10
4:     0ms: Start User-main program
5:     0ms: Task S: start
6:     0ms: speaker_off
7:    20ms: speaker_on
8:    40ms: speaker_off
9:    60ms: speaker_on
10:    80ms: speaker_off
              ・
              ・
              ・
54:   960ms: speaker_off
55:   980ms: speaker_on
56:  1000ms: speaker_off
57:  1010ms: End User-main program


実際の待ち時間とティック時間

tk_dly_tskで指定する遅延時間が10ミリ秒なのに、forループが回る時間は20ミリ秒であった。µT-Kernel 3.0のシステムタイマのティック時間(タイマ割込み発生の時間間隔)が標準設定の10ミリ秒である限り、実際のタスク遅延時間は10ミリ秒ではなく20ミリ秒近くになってしまうのである。この点は、OSの時間に関する動作として重要なところなので、もう少し詳しく説明する。


µT-Kernel 3.0において、tk_dly_tskによる時間待ち状態の解除、タイムアウトによる待ち状態の解除、周期ハンドラやアラームハンドラの実行など、時間経過を契機とした動作を開始するのは、システムタイマによる割込みが発生した時に限られる。そのため、システムタイマのティック時間が10ミリ秒であれば、タスク遅延の時間待ちが解除される時刻も10ミリ秒間隔のタイミングに限られる。


たとえば、時刻Tにタイマ割込みが発生したとすると、その次にタイマ割込みが発生する時刻は(T+10ms)、(T+20ms)、(T+30ms)……である。この場合にtk_dly_tskの時間待ちが解除されるタイミングは、このうちのいずれかの時刻、すなわち、T、(T+10ms)、(T+20ms)、(T+30ms)……に限られる。この途中の時刻、たとえば(T+15ms)や(T+23ms)に時間待ちが解除されることはない。µT-Kernel 3.0仕様書では、「4.7.2 周期ハンドラ」の補足事項などにおいて、µT-Kernelの時間管理機能の処理における実際の時間分解能はタイマ割込み間隔(TTimPeriod)の値になると説明している。


図2 タイマ割込み発生のタイミングとtk_dly_tskの待ち時間
図2 タイマ割込み発生のタイミングとtk_dly_tskの待ち時間








一方、µT-Kernel 3.0の仕様では、RELTIMなどのデータタイプで指定される相対時間について、実際の動作が起こるのは指定時間以上の経過後になることを保証している(*5) 。tk_dly_tskの引数として指定する遅延時間(dlytim)のデータタイプもRELTIMなので、この説明が適用される。したがって、タスク遅延による時間待ちが解除されるまでの時間は、dlytimで指定した時間と同じか、それよりも長い時間でなければならない。待ち状態の解除が遅れることはあっても、早まることはないのである。


システムタイマのティック時間が10ミリ秒で、時刻Tにタイマ割込みが発生し、その直後に、遅延時間10ミリ秒を指定したtk_dly_tskを実行した場合を考える(図2)。この次のタイマ割込みは(T+10ms)の時刻に発生するが、その時点ではtk_dly_tskを実行したタスクの待ち時間が10ミリ秒よりも少し短い。タイマ割込みの時間間隔はちょうど10ミリ秒であるが、前回のタイマ割込み発生時刻Tからtk_dly_tskの実行開始までの時間EXがゼロではないので、EXの分だけ待ち時間が10ミリ秒に足りないからである。指定した値よりも待ち時間が短くなることは許されないため、時刻(T+10ms)の時点では、まだtk_dly_tskの時間待ちを解除できない。そうすると、次に時間待ちを解除できるタイミングとしては、その次のタイマ割込み発生時刻である(T+20ms)まで待たなければならない。この場合のtk_dly_tskの実際の待ち時間は、20ミリ秒よりEXだけ短い時間となる。

CNF_TIMER_PERIODでティック時間を変更

 

結局、tk_dly_tskによるタスク遅延時間を短くして25Hzよりも高いブザー音を出すには、システムタイマのティック時間を10ミリ秒より短くする以外に方法がない。そのためには、µT-Kernel 3.0のコンフィグレーションを変更する。


ティック時間については、µT-Kernel 3.0のソースコードに付属している「µT-Kernel3.0 共通実装仕様書」の「7.1.1 システムタイマ」の章に説明があり、CNF_TIMER_PERIODというコンフィグレーションで設定すると書かれている。単位はミリ秒である。また、コンフィグレーションの設定方法については、「11.1 基本コンフィグレーション」の章に説明があり、/config/config.hというOSのソースファイルの中で設定されると書かれている。設定値の変更後にはOSを再構築する必要があるが、今回使っているµT-Kernel 3.0の実装ではOSとusermainを一括してリンクする構成になっているので、OS側のファイルである/config/comfig.hに変更があった場合でも、これまでの実行例と同じくOSを含めたシステム全体のコンパイルとリンクを行うだけでよい。Eclipseを使えば、変更の影響を受けるソースプログラムが自動的に再コンパイルされるため、OS再構築の有無にかかわらず、Eclipse上でのコンパイルや実行の操作手順はこれまでと変わらない。


実際にEclipseを操作して、CNF_TIMER_PERIODの設定値を変更してみよう。まず、µT-Kernel 3.0のソースの中から/config/config.hのファイルを選択する。そのためには、左側のペインの「Project Explorer」をクリックして選択し(図3①)、その下に表示された「mtkernel_3」の左側の「>」をクリックしてその下の情報を開く(図3②)。さらに、その中に表示された「config」の左側の「>」をクリックして開くと(図3③)、「config.h」のファイル名が表示される(図3④)。このファイル名が/config/config.hのファイルを表しているので、この部分をダブルクリックする。そうすると、中ほどのペインに「config.h」のタブが追加され、このソースファイルの内容が表示されて編集可能になる。少し下の方にスクロールすると、41行目でCNF_TIMER_PERIODをマクロ定義しているので、その定義値の10を変更する(図3⑤)。ここでは、できるだけタスク遅延時間を短くして高い音が出せるように、最小値の1を設定した。これで、システムタイマのティック時間が1ミリ秒になった。


この状態でEclipseを操作して、リスト1のプログラムを再度micro:bitで実行する。1秒間のブザー音が先ほどよりも少し高くなった。この場合のコンソール出力をリスト3に示す。2行目にCNF_TIMER_PERIOD=1の表示があり、ティック時間が1ミリ秒になったことが確認できる。6行目から7行目までのtk_dly_tskの待ち時間は11ミリ秒であり、リスト2の20ミリ秒よりもだいぶ短くなった。スピーカーへの出力値が0→1→0と変化する際の周期時間、つまり6行目のspeaker_offから8行目のspeaker_offまでの時間は、22ミリ秒である。したがって、ブザー音の周波数は1,000ミリ秒÷22ミリ秒=45Hzである。


なお、連載第7回でも説明したが、ティック時間を短くするとタイマ割込みの回数が増えるため、システムのオーバーヘッドが増大するという問題がある。バッテリ駆動の機器では、消費電力が増えるといった悪影響も考えられる。必要以上にティック時間を短くすることは望ましくない場合もあるので、ティック時間を変更する際は注意してほしい。

 

図3 EclipseによるCNF_TIMER_PERIODの設定値の変更
図3 EclipseによるCNF_TIMER_PERIODの設定値の変更



リスト3 ティック時間を1ミリ秒にした場合のリスト1のコンソール出力

microT-Kernel Version 3.00

1:     0ms: List1: Buzzer using Delayed Task
2:     0ms: CNF_TIMER_PERIOD=1
3:     0ms: half_cycle_ms=10
4:     0ms: Start User-main program
5:     0ms: Task S: start
6:     0ms: speaker_off
7:    11ms: speaker_on
8:    22ms: speaker_off
9:    33ms: speaker_on
10:    44ms: speaker_off
            ・
            ・
            ・
94:   968ms: speaker_off
95:   979ms: speaker_on
96:   990ms: speaker_off
97:  1001ms: End User-main program


さらに高いブザー音を再生

 

ティック時間が1ミリ秒になったので、tk_dly_tskの遅延時間の指定も10ミリ秒より短くしてよいはずである。この値はapp_main.cの中のhalf_cycle_msとして指定されているので(リスト1の(※K))、この部分を変更する。Eclipseでconfig.hの編集操作をした後、usermainなどを含むapp_main.cのソースファイルの編集に戻りたい場合には、「app_main.c」のタブをクリックすればよい。app_main.cのソースプログラムが表示されるので、この中のhalf_cycle_msの設定値を10から1に変更し、tk_dly_tskの遅延時間を1ミリ秒にする。


変更したプログラムをmicro:bitで実行する。ブザー音はさらに高くなり、コンソール出力はリスト4のようになった。3行目にhalf_cycle_ms=1の表示があることが確認できる。6行目から7行目までのtk_dly_tskの待ち時間は2ミリ秒、6行目のspeaker_offから8行目のspeaker_offまでの周期時間は4ミリ秒なので、ブザー音の周波数は1,000ミリ秒÷4ミリ秒=250Hzとなった。これは、ピアノの鍵盤の真ん中付近にある「ド」(C4)の音(261.6Hz)とほぼ同じ音の高さである。


ただ、この場合でもループを回る時間は2ミリ秒であり、最小単位である1ミリ秒よりは長い。1ミリ秒の間隔で処理をしたい場合には、tk_dly_tskを使ったタスクによるループではなく、周期ハンドラを使う必要がある。

 

リスト4 タスク遅延時間を1ミリ秒にした場合のリスト1のコンソール出力

microT-Kernel Version 3.00

1:     0ms: List1: Buzzer using Delayed Task
2:     0ms: CNF_TIMER_PERIOD=1
3:     0ms: half_cycle_ms=1
4:     0ms: Start User-main program
5:     0ms: Task S: start
6:     0ms: speaker_off
7:     2ms: speaker_on
8:     4ms: speaker_off
9:     6ms: speaker_on
10:     8ms: speaker_off
            ・
            ・
504:   996ms: speaker_off
505:   998ms: speaker_on
506:  1000ms: speaker_off
507:  1001ms: End User-main program


周期ハンドラを使ったブザー音

 

周期ハンドラを使ったブザー制御のプログラムをリスト5に示す。周期ハンドラSとして呼び出されるプログラムには、リスト1で作成した関数speaker_onoffをそのまま利用する(リスト5の(※L))。それ以外のGPIO関係の関数や、ログを記録する関数についても、リスト1と同じである。forループやtk_dly_tskが不要なので、プログラムはリスト1よりも簡単になった。


usermainでは、タスクSの代わりに周期ハンドラSを生成して、その周期ハンドラの動作を開始する(※M)。スピーカーへの出力の周期時間の半分を定義するhalf_cycle_msは最小値の1ミリ秒とし(※N)、この値が周期ハンドラの起動時間間隔に設定される。周期ハンドラを含めた時間管理機能が1ミリ秒間隔のタイミングで動作するように、ティック時間を示すconfig.hのCNF_TIMER_PERIODについては、標準の10ではなく1のままにしておく。


リスト5のプログラムをmicro:bitで実行した結果のコンソール出力がリスト6である。5行目から6行目までの時間は1ミリ秒、5行目のspeaker_offから7行目のspeaker_offまでの周期時間は2ミリ秒となり、1ミリ秒ごとにスピーカーへの出力を反転できるようになった。ブザー音の周波数は1,000ミリ秒÷2ミリ秒=500Hzであり、先ほどより1オクターブ上の「ド」(C5)に近い音が出ている。

 

リスト5 周期ハンドラを使ったブザー制御のプログラム

/*---------------------------------------------------------------
*  周期ハンドラを使ったブザー制御(µT-Kernel 3.0用)
*
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/
              ・
              ・
              ・
//-------- 周期ハンドラによるスピーカーの出力制御 ---------------
const INT half_cycle_ms = 1; 	// 音の周期時間の半分(ミリ秒単位)(※N)

// 周期ハンドラSの関連情報(※L)
const T_CCYC ccycS = {0, TA_HLNG, &speaker_onoff, half_cycle_ms, 0};
ID cycidS;                          	// 周期ハンドラSのID

EXPORT void usermain(void)
{
// プログラムの名称とティック時間などの情報をログに記録
    log("List5: Buzzer using Cyclic Handler", NO_PARAM);
    log("CNF_TIMER_PERIOD=", CNF_TIMER_PERIOD);
    log("half_cycle_ms=", half_cycle_ms);
    log("Start User-main program", NO_PARAM);

    // スピーカーに接続されたGPIOピン(P0.00)を出力に設定
    out_w(GPIO(P0, PIN_CNF(0)), (1 << 0));

    cycidS = tk_cre_cyc(&ccycS);    	// 周期ハンドラSの生成
    tk_sta_cyc(cycidS);             	// 周期ハンドラSの動作開始(※M)
    tk_dly_tsk(1000);               	// 1秒間の時間待ち
    tk_stp_cyc(cycidS);             	// 周期ハンドラSの動作停止

    log("End User-main program", NO_PARAM);
    print_log();                    	// ログを表示
    tk_slp_tsk(TMO_FEVR);           	// 永久待ち
}


リスト6 周期ハンドラを使ったブザー制御(リスト5)のコンソール出力

microT-Kernel Version 3.00

1:     0ms: List5: Buzzer using Cyclic Handler
2:     0ms: CNF_TIMER_PERIOD=1
3:     0ms: half_cycle_ms=1
4:     0ms: Start User-main program
5:     2ms: speaker_off
6:     3ms: speaker_on
7:     4ms: speaker_off
8:     5ms: speaker_on
9:     6ms: speaker_off
10:     7ms: speaker_on
              ・
              ・
              ・
1002:   999ms: speaker_on
1003:  1000ms: speaker_off
1004:  1001ms: speaker_on
1005:  1001ms: End User-main program


物理タイマの利用

 

かなり高い音まで出せるようになったので、次の例題では周波数をうまく調整して、ドレミファの音階を流せるようにしたい。しかし、スピーカーから出る音の周波数は、周期ハンドラの起動時間間隔を1ミリ秒とした場合に500Hz、2ミリ秒とした場合に250Hz、3ミリ秒とした場合に167Hzである。周期ハンドラの起動時間間隔として、1ミリ秒と2ミリ秒の間の任意の値を指定できるわけではない。つまり、500Hzと250Hzの音は再生できるが、その中間の周波数の音を再生することはできないのである。これでは、残念ながらドレミファの音階にはならない。スピーカーを制御して任意の周波数の音が出せるようにするには、1ミリ秒前後の任意の時間間隔で周期的な処理ができるようなしくみが必要である。


このための機能として思い当たるのは、連載第7回のLEDのダイナミック点灯で説明した物理タイマである。物理タイマであれば、マイクロ秒(µs)の単位でハンドラの周期時間間隔を指定することができ、オーバーヘッドも少ない。連載第7回の例題で、物理タイマハンドラを定義して動作させるプログラムも開発済みである。これを使ってスピーカーへの出力反転を制御してみよう。


物理タイマを使ったブザー音の再生プログラムをリスト7に示す。周期ハンドラの代わりに物理タイマハンドラを使うようにusermainを変更し、物理タイマハンドラの定義と動作開始(※P)を行っている。また、ミリ秒(ms)単位で音の周期時間の半分を定義するhalf_cycle_msの代わりに、連載第7回のリスト5のcycle_usに合わせて、マイクロ秒(µs)単位で指定するhalf_cycle_usとした。この値には、ピアノの真ん中の「ド」(C4)の音である261.6Hzが再生できるように、1,000,000µs÷261.6÷2=1911を設定する(※Q)。この値がそのまま物理タイマハンドラの起動周期時間cycle_usとなり、連載第7回と同じ方法でcycle_usから物理タイマの上限値limitを計算する(※R)。これ以外の部分は、関数speaker_onoffを含めて、周期ハンドラを使ったリスト5とまったく同じである。


リスト7のプログラムを実行すると、C4の「ド」の音がスピーカーから再生され、コンソールからリスト8が出力される。5行目のspeaker_offから7行目のspeaker_offまでの周期時間は3ミリ秒であり、リスト4よりも短くなっているが、ここではミリ秒単位の時刻しかわからないので、正確な周波数を計算することができない。そこで5行目から527行目までを見ると、スピーカーへの出力反転の回数は527-5=522回で、0→1→0の1周期を1回と数えた場合の振動回数は522÷2=261回である。この間の所要時間は999-2=997ミリ秒なので、音の周波数は261×(1,000÷997)=261.8Hzと計算できる。リスト4の実行時の250Hzより少しだけ高くなり、C4の「ド」の音をほぼ正確に再現していることがわかった。


ちなみに、このプログラムの実行時にはティック時間のCNF_TIMER_PERIODが1のままだったが、物理タイマの動作はティック時間の影響を受けないので、CNF_TIMER_PERIODが1でも10でも再生される音は変わらない。ただし、この値を10にすると、ログに記録される時刻も10ミリ秒単位となり、ログの表示時刻がわかりにくくなってしまう。

 

リスト7 物理タイマを使ったブザー制御のプログラム

/*---------------------------------------------------------------
*  物理タイマを使ったブザー制御(µT-Kernel 3.0用)
*
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/
              ・
              ・
              ・
//-------- 物理タイマによるスピーカーの出力制御 -----------------
const INT half_cycle_us = 1911; // 音の周期時間の半分(マイクロ秒
                                                // 単位)(※Q)

// 物理タイマ関連情報
const T_DPTMR dptmr = {0, TA_HLNG, &speaker_onoff};
const UINT ptmrno = 1;               // 物理タイマ番号として1を使用
const INT ptmr_clk_mhz = 16;         // 物理タイマのクロック(MHz単位)

EXPORT void usermain(void)
{
    // プログラムの名称とティック時間などの情報をログに記録
    log("List7: Buzzer using Physical Timer", NO_PARAM);
    log("CNF_TIMER_PERIOD=", CNF_TIMER_PERIOD);
    log("half_cycle_us=", half_cycle_us);
    log("Start User-main program", NO_PARAM);

    // スピーカーに接続されたGPIOピン(P0.00)を出力に設定
    out_w(GPIO(P0, PIN_CNF(0)), (1 << 0));

    // 物理タイマハンドラの起動周期時間(マイクロ秒単位)と
    // 上限値の計算(※R)
    INT cycle_us = half_cycle_us;                  	// 起動周期時間
    INT limit = cycle_us * ptmr_clk_mhz - 1;  // 物理タイマの上限値

    DefinePhysicalTimerHandler(ptmrno, &dptmr);	// 物理タイマハンドラ定義
    StartPhysicalTimer(ptmrno, limit, TA_CYC_PTMR); // 物理タイマの動作
                                                       // 開始(※P)
    tk_dly_tsk(1000);                                  // 1秒間の時間待ち
    StopPhysicalTimer(ptmrno);                         // 物理タイマの動作停止

    log("End User-main program", NO_PARAM);
      print_log();                    		// ログを表示
      tk_slp_tsk(TMO_FEVR);           		// 永久待ち
}


リスト8 物理タイマを使ったブザー制御(リスト7)のコンソール出力

microT-Kernel Version 3.00

1:     0ms: List7: Buzzer using Physical Timer
2:     0ms: CNF_TIMER_PERIOD=1
3:     0ms: half_cycle_us=1911
4:     0ms: Start User-main program
5:     2ms: speaker_off
6:     4ms: speaker_on
7:     5ms: speaker_off
8:     7ms: speaker_on
9:     9ms: speaker_off
10:    11ms: speaker_on
              ・
              ・
              ・
525:   995ms: speaker_off
526:   997ms: speaker_on
527:   999ms: speaker_off
528:  1001ms: End User-main program


物理タイマを使ったドレミファ音階

 

任意の周波数の音が出せるようになったので、今回最後の例題として、スピーカーからドレミファの音階を再生してみよう。そのプログラムをリスト9に示す。もうログを見る必要はないので、ログ関係の関数や変数は外し、ティック時間のCNF_TIMER_PERIODは標準値の10に戻している。


指定した周波数の音を、指定した長さの時間だけ再生する関数として、play_speakerを作成した(リスト9の(※S))。この関数では、最初に引数freqで指定された周波数から物理タイマの上限値を計算する(※T)。音の周波数の逆数が1周期に対する秒単位の時間になるので、マイクロ秒単位の時間で表現した値はその100万倍である。つまり、スピーカーへの出力の1周期(0→1→0)に対するマイクロ秒単位の時間は、(1,000,000÷freq)として計算できる。また、この1周期の間には、0から1に変化する時と、1から0に変化するときに合わせて2回、物理タイマハンドラが起動される。そのため、この1周期の時間の半分を物理タイマハンドラの起動周期時間cycle_usとする。cycle_usから上限値limitを求める計算はリスト7と同様である。上限値が求まったら物理タイマの動作を開始し(※U)、引数play_timeで指定した時間だけ待ってから(※V)、物理タイマを動作停止する。


ドレミファの各音階の周波数は、配列変数doremiで定義している(※W)。usermainでは、スピーカーに出力するGPIOピンの設定と物理タイマハンドラの定義を行った後に、for文のループの中からplay_speakerをよび出して(※X)、ドレミファソラシドの各音階を500ミリ秒ずつ再生する。


このプログラムが理解できれば、再生する音階の周波数や音の長さのデータを適切に設定することで、好きなメロディを鳴らすことができるはずだ。

 

リスト9 物理タイマを使ったドレミファ再生のプログラム

/*---------------------------------------------------------------
*  物理タイマを使ったドレミファ再生(µT-Kernel 3.0用)
*
*  Copyright (C) 2022-2023 by T3 WG of TRON Forum
*---------------------------------------------------------------*/
                ・
                ・
                ・
//-------- 物理タイマを使ったドレミファ再生 ---------------------

// 物理タイマ関連情報
const T_DPTMR dptmr = {0, TA_HLNG, &speaker_onoff};
const UINT ptmrno = 1;              	// 物理タイマ番号として1を使用
const INT ptmr_clk_mhz = 16;        	// 物理タイマのクロック(MHz単位)

// 周波数freq(Hz)の音をplay_time(ミリ秒)の間だけスピーカーから再生(※S)
  LOCAL void play_speaker(INT freq, INT play_time)
  {
    // 物理タイマハンドラの起動周期時間(マイクロ秒単位)と上限値の計算(※T)
    INT cycle_us = 1000000 / freq / 2;    	    // 起動周期時間
    INT limit = cycle_us * ptmr_clk_mhz - 1;	    // 物理タイマの上限値

    StartPhysicalTimer(ptmrno, limit, TA_CYC_PTMR); // 物理タイマの動作
                                                    // 開始(※U)
    tm_printf("play_speaker_ptimer: freq=%4d, play_time=%4d\n",
                    freq, play_time);
    tk_dly_tsk(play_time);                          // 再生時間だけ待つ(※V)
    StopPhysicalTimer(ptmrno);
  }

// ドレミファソラシド(C5,D5,E5,F5,G5,A5,B5,C6)の周波数(Hz)の定義(※W)
const INT doremi[] = { 523, 587, 659, 698, 783, 880, 987, 1046 };

EXPORT void usermain(void)
{
    // スピーカーに接続されたGPIOピン(P0.00)を出力に設定
    out_w(GPIO(P0, PIN_CNF(0)), (1 << 0));

    DefinePhysicalTimerHandler(ptmrno, &dptmr);	// 物理タイマハンドラ定義
    for (INT cnt = 0; cnt < 8; cnt++)
    play_speaker(doremi[cnt], 500);	// 各音階を500ミリ秒ずつ再生(※X)
    tk_slp_tsk(TMO_FEVR);                	// 永久待ち
}



* * *

今回はmicro:bitの内蔵スピーカーを制御して音を出してみた。GPIOから0と1を繰り返し出力するだけの単純な処理なのだが、高めのブザー音を出すには、ティック時間(システムタイマの割込み時間間隔)を標準の10ミリ秒よりも短くしてOSを再構築する必要がある。そのため、µT-Kernel 3.0の時間に関わる機能とティック時間との関係について詳しく説明した。また、ドレミファの音階を正確に再現するには、さらに細かい時間間隔でGPIO出力を制御する必要があり、前回に続いて物理タイマ機能を利用した。これらはいずれも、µT-Kernel 3.0の機能を使いつつ、ソフトウェア制御によって音を再生する方法であった。


では、ソフトウェア制御以外の方法を使ってmicro:bitから音を出すことはできないのだろうか。実は、Target MCUに内蔵されたPWM(Pulse Width Modulation)という機能を使えば、PWMが直接GPIO P0.00を制御して内蔵スピーカーから音を出すことが可能である。この場合、最初にプログラムでPWMを設定する必要はあるが、設定後のGPIO出力値の制御(0→1→0→……)はPWMのハードウェアが自動的に行ってくれる。したがって、音の再生開始後のソフトウェア制御は不要である。ただし、PWMには多くの機能があり、その設定方法は複雑である。


PWMを使った音の再生やPWMの応用方法については、次回の連載で紹介する予定だ。



■ 本稿で説明したプログラムのソースコードのダウンロード
https://www.personal-media.co.jp/book/tw/tw_index/380.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)
 µT-Kernel 3.0仕様書では、tk_ter_tskの補足事項として、一般のアプリケーションがこのAPIを使用するのは原則不可と説明している。これは、tk_ter_tskでタスクを強制終了したような場合に、そのタスクの利用していたミドルウェアなどの動作に不整合を生じる可能性があるからだ。しかしながら今回の例題の場合は、ミドルウェアを利用しておらず、tk_ter_tskの対象タスクはごく単純な動作をしているだけなので、tk_ter_tskを使っても特に問題は生じない。
(*4)
 ログの時刻もティック時間と同じ10ミリ秒の範囲で不正確な可能性がある。この不正確さによる影響を抑えるには、できるだけ長い時間範囲から周波数を計算する方がよい。そこで、念のためコンソール出力の6行目から56行目までの1,000ミリ秒の間における出力値の変化について、その周波数(0→1の変化と1→0の変化の合計回数の半分)を求めてみると、(56-6)÷2=25Hzで同じ結果になった。この例題の場合は、ログが規則正しいパターンで変化しているため、6行目から8行目の40ミリ秒のログだけを見て音の周波数を計算した場合にも、1秒間全体のログから計算した場合にも、同じ結果が得られたわけである。
(*5)
 µT-Kernel 3.0仕様書の「3.2.8 相対時間とシステム時刻」の補足事項に、「RELTIM, RELTIM_U, TMO, TMO_Uで指定された時間は、指定された時間以上経過した後にタイムアウト等が起こることを保証しなければならない」と記載されている。
  • 本ページは、「TRONWARE Vol.203」の掲載記事「micro:bitでµT-Kernel 3.0を動かそう 第8回 ドレミファ音階の再生とティック時間」をWebで公開したものです。

ページの先頭に戻る

 

次回の連載記事に進む