本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第10回の本号では、PWM(Pulse Width Modulation)の高度な応用を試す。まず、PWMの四つのチャンネルを使って、四つのLEDを同時に制御するプログラムや、LEDの明るさを徐々に変化させるプログラムを作成する。その後はPWM応用の総集編として、二つのPWMユニットを使って音の再生とLEDの制御を同時に行うプログラムを紹介する。連載第6回で説明したボタンスイッチによる割込み発生の動作も組み合わせて、ボタンを押したら音楽が始まるとともに、音階や音の長さに合わせてLEDのイルミネーションが光るようにした。
前回(連載第9回)のリスト2の例題でPWMの基本機能を確認できたので、次の例題ではPWMの4本の出力を同時に使い、四つのLEDを調光制御してみよう。このプログラムをリスト1に示す。なお、誌面上のリストでは、前回と共通する部分を省略している。micro:bit上でそのまま実行できる省略のないプログラムは、ダウンロードしたソースコード(P.95)をご覧いただきたい。
前回のリスト2では、PWMのチャンネル0の出力をROW1に接続し、左上のD2というLEDを点灯した。今回のリスト1では、PWMのチャンネル0、1、2、3の出力を、それぞれROW5、ROW4、ROW3、ROW2に接続する(※A)。下から1..4番目のLED(D42、D32、D22、D12)を調光制御して、少しずつ明るさの異なる四つのLEDを同時に点灯させる。
四つのLEDの明るさを変えるには、PWM出力のデューティ比もチャンネルごとに変える必要がある。つまり、PWMのカウンタ比較値も四つのチャンネルに応じて別々に必要である。micro:bitのTarget MCUに内蔵されたPWMはこのような機能を持っており、一つのPWMユニットに対して、最大で四つのカウンタ比較値の設定ができる。PWMのマニュアル(*1) では、チャンネルnを出力するためのカウンタ比較値をCOMPn(n=0..3)と表記している。リスト2の例題ではチャンネル0とCOMP0のみを使用していたが、この例題ではCOMP0..COMP3をすべて使用する。
PWMをこのように設定した後に動作開始する関数として、pwm_start_comp4を作成した。前回連載のリスト2のpwm_start_dutyと似ているが、四つの比較値を指定するので、SEQ_PTR(0)にアドレスを設定するpwm_compは、16ビット(データタイプ_UH)のデータを4個入れた配列になっている(※B)。また、SEQ_CNT(0)には、比較値のデータの個数を設定する。この場合は4であるが、次の例題ではさらに比較値の個数を増やすので、sizeofを使ってpwm_compの配列のサイズからデータの個数を求めるようにしておく(※C)。このほか、四つの比較値を使用して4チャンネルの出力を行うため、DECODERというレジスタにはindividualを意味する2を設定する(※D)。
pwm_compに入れる四つの比較値は、デューティ比を意味するDUTYというマクロを使って定義している(※E)。デューティ比は (上限値−比較値)÷上限値なので、ここから逆算すると、比較値=上限値-(上限値×デューティ比) となる。この式の上限値をTOPとし、デューティ比を%で書くために100で割ると、デューティ比から比較値を求めるDUTYマクロを定義できる。明るさの差がはっきりわかるように、比較値の設定はDUTY(100)、DUTY(25)、DUTY(5)、DUTY(1)とした(※B)。
usermainでは、led_initを実行した後、COL1..COL5をすべて0に設定している(※F)。前回のリスト2では、COL2..COL5を1に設定して左端の列(COL1)のLEDのみを点灯させていたが、せっかくなので、今回はすべての列のLEDを同じように点灯させるようにした。その後、PWMの各チャンネル出力をROW5..ROW2に接続してから(※A)、pwm_start_comp4を呼び出して実際にLEDを点灯する。
micro:bitでこのプログラムを実行した様子を図1に示す。明るさの異なる4行のLEDが同時に点灯している。
/*--------------------------------------------------------------- * PWMで4つのLEDを同時に調光制御(μT-Kernel 3.0用) * * Copyright (C) 2022-2024 by T3 WG of TRON Forum *---------------------------------------------------------------*/ ・ ・ ・ // カウンタ上限値とカウンタ比較値を定義するDUTYマクロ(※E) #define TOP 1000 #define DUTY(percent) (TOP - (TOP * percent / 100)) // 4つのカウンタ比較値を格納する16ビット幅の配列(※B) static _UH pwm_comp[] = {DUTY(100), DUTY(25), DUTY(5), DUTY(1)}; // unitのPWMに上限値と4つの比較値を設定して動作開始 LOCAL void pwm_start_comp4(INT unit) { tm_printf("pwm_start_comp4: unit=%d\n", unit); out_w(PWM(unit, ENABLE), (1 << 0)); out_w(PWM(unit, MODE), 0); out_w(PWM(unit, PRESCALER), 4); // DIV_16でクロック1MHzを指定 out_w(PWM(unit, COUNTERTOP), TOP); // カウンタ上限値の設定 out_w(PWM(unit, DECODER), 2); // LOAD=individual(single)を // 指定(※D) // カウンタ比較値の配列を入れたメモリアドレスとサイズの設定(※C) out_w(PWM(unit, SEQ_PTR(0)), (UW)&pwm_comp); out_w(PWM(unit, SEQ_CNT(0)), (sizeof(pwm_comp) / sizeof(_UH))); out_w(PWM(unit, SEQ_REFRESH(0)), 0); out_w(PWM(unit, SEQ_ENDDELAY(0)), 0); // PWMのシーケンス0の動作を開始 out_w(PWM(unit, LOOP), 0); out_w(PWM(unit, TASKS_SEQSTART(0)), 1); } const INT led_pwm = 0; // LED用PWMとしてunit=0を使用 EXPORT void usermain(void) { led_init(); // LEDを点灯させるためのGPIOの設定(※F) out_gpio_pin(0, 28, 0); // GPIO P0.28-COL1に0を出力 out_gpio_pin(0, 11, 0); // GPIO P0.11-COL2に0を出力 out_gpio_pin(0, 31, 0); // GPIO P0.31-COL3に0を出力 out_gpio_pin(1, 5, 0); // GPIO P1.05-COL4に0を出力 out_gpio_pin(0, 30, 0); // GPIO P0.30-COL5に0を出力 // PWMの各チャンネル出力をGPIOピン経由でROW5..ROW2に出力(※A) out_w(PWM(led_pwm, PSEL_OUT(0)), ((0 << 5) | (19 << 0))); // チャンネル0→P0.19(ROW5に接続) out_w(PWM(led_pwm, PSEL_OUT(1)), ((0 << 5) | (24 << 0))); // チャンネル1→P0.24(ROW4に接続) out_w(PWM(led_pwm, PSEL_OUT(2)), ((0 << 5) | (15 << 0))); // チャンネル2→P0.15(ROW3に接続) out_w(PWM(led_pwm, PSEL_OUT(3)), ((0 << 5) | (22 << 0))); // チャンネル3→P0.22(ROW2に接続) // PWMの動作を開始して4つのLEDを調光点灯 pwm_start_comp4(led_pwm); tk_slp_tsk(TMO_FEVR); // 永久待ち } |
micro:bitのPWMにはさらに高度な機能がある。デューティ比、つまりカウンタ比較値を、時間の経過に応じて自動的に変化させることができるのだ。この機能をPWMのシーケンス機能とよぶことにする。
デューティ比を変化させるシーケンス(変化パターン)のデータはメモリ上の配列として定義され、データの個数の制限はない。PWMは一定の短い時間ごとに自動的にその配列をスキャンしながら、比較値として使用するデータを変えていく。このデータの値は自由に定義できるので、単調な増加や減少に限らず、たとえば正弦波などの関数を近似したようなパターンの変化でも再現できる。モーター制御においては、PWM出力のデューティ比を特定のパターンで変化させたい場合が多く、そのような用途に便利な機能である。
具体的なシーケンス機能の動作を説明する。まず、PWMの出力が0→1→0と繰り返す1周期の時間的な単位を「サイクル」とよぶ。PWMのカウンタが0から上限値(COUNTERTOP)に達するまでの間が1サイクルである。今回の例題の設定では、1サイクルの時間がちょうど1ミリ秒になっている。
次に、同じカウンタ比較値を使い続ける時間的な単位を「ステップ」とよぶ。ステップは一つまたは複数の連続したサイクルから成る。一つのステップの間は比較値が変わらないので、PWM出力のデューティ比も変わらない。なお、「サイクル」と「ステップ」は、説明の便宜のために本稿で導入した用語であり、PWMのマニュアルの中にこれらの語が出てくるわけではない。
1ステップあたりのサイクル数は、PWMのREFRESHというレジスタに設定する。1ステップがq回のサイクルから成るのであれば、REFRESHには(q-1)の値を設定する。1サイクルごとに毎回比較値を変えたい場合、すなわち1ステップが1サイクルのみの場合は、このレジスタに0を設定する“REFRESH”という名称は、このレジスタに設定したサイクル数が経過するごとに比較値を新しくリフレッシュする、という意味に由来している。
複数のステップから構成されるデューティ比の変化パターンが「シーケンス」である。前述のとおり、この変化パターンはメモリ上の配列を使って自由に定義できる。図2はPWMのマニュアルに出てくるシーケンスの例であるが、この図の中段に描かれたギザギザのグラフがPWM出力のデューティ比の変化を示している。
一つのシーケンスに含まれるステップの回数は、PWMのCNTレジスタの設定値によって決まる。このレジスタには、一つのシーケンスで使用する比較値のデータの総数、すなわちメモリ上の配列の要素数を設定する。前回のリスト2では、出力として1チャンネルのみ使用していたので、1ステップに対して使用する比較値もCOMP0の一つのみであった。この場合は、データ数(配列の要素数)がそのままステップ数になる。一方、今回のリスト1では、4チャンネルの出力に対して別々のカウンタ比較値が必要なので、1ステップについて4個ずつの比較値(COMP0..COMP3)を使用する。この場合のデータ数(配列の要素数)は、ステップ数の4倍になる。このように、1ステップに対する比較値の個数はPWMのDECODERレジスタの設定に応じて変化する。なお、カウンタ比較値は16ビットの符号無し整数なので、配列のバイト数はデータ数の2倍である。シーケンスを定義するための比較値を入れる配列のデータ形式を図3に示す。
さらに、シーケンスとしてはSEQ[0]とSEQ[1]の2種類を定義することができ、SEQ[0]→SEQ[1]→SEQ[0]→SEQ[1]→... のように両者が連続しながら繰り返して実行される。繰り返しの回数はLOOPレジスタで指定する。繰り返しが幾重にも重なって複雑であるが、シーケンス機能の全体の動作を図4にまとめておく。
PWMのシーケンス機能の動作を確認するために、この機能を使ってLEDの明るさが徐々に変化するようなプログラムを作成した(リスト2)。リスト1と共通の部分が多いので、異なる部分のみ説明する。シーケンス0の比較値を入れる配列がpwm_seq0_compであり(※G)、4チャンネル×7ステップ、合計28個のデータを格納している。シーケンス1の比較値を入れる配列はpwm_seq1_compである(※H)。PWMのレジスタを設定して動作開始する関数pwm_start_seqの中では(※J)、シーケンス0と1の比較値を入れたアドレスやREFRESHレジスタの設定を行う(※K)。CNTレジスタの設定も行うが、この設定値はpwm_seq0_compやpwm_seq1_compの配列のサイズから自動的に計算される。また、LOOPレジスタには引数で指定された値を設定する(※L)。プログラムを実行すると、micro:bitのLEDが波打つように光る動作を5回繰り返す。この動作は、すべてPWMのハードウェアによって行われている。usermainの中で呼んでいるpwm_start_seqの引数を変えると、繰り返しの回数やLEDの点灯パターンの流れるスピードが変化する。
/*--------------------------------------------------------------- * PWMを使ってLEDの明るさを徐々に変化させる(μT-Kernel 3.0用) * * Copyright (C) 2022-2024 by T3 WG of TRON Forum *---------------------------------------------------------------*/ ・ ・ ・ // シーケンス0のカウンタ比較値を格納する16ビット幅の配列(※G) static _UH pwm_seq0_comp[] = { DUTY(0), DUTY(10), DUTY(50), DUTY(100), DUTY(5), DUTY(14), DUTY(40), DUTY(70), DUTY(15), DUTY(19), DUTY(32), DUTY(50), DUTY(30), DUTY(25), DUTY(25), DUTY(30), DUTY(50), DUTY(32), DUTY(19), DUTY(15), DUTY(70), DUTY(40), DUTY(14), DUTY(5), DUTY(100), DUTY(50), DUTY(10), DUTY(0) }; // シーケンス1のカウンタ比較値を格納する16ビット幅の配列(※H) static _UH pwm_seq1_comp[] = { DUTY(100), DUTY(50), DUTY(10), DUTY(0), DUTY(50), DUTY(50), DUTY(10), DUTY(0), DUTY(10), DUTY(10),DUTY(10), DUTY(0), DUTY(1), DUTY(1), DUTY(1), DUTY(0), DUTY(0), DUTY(0), DUTY(0), DUTY(0) }; // unitのPWMに上限値とシーケンスの比較値を設定して動作開始(※J) LOCAL void pwm_start_seq(INT unit, INT loopcnt, INT ref0, INT ref1) { tm_printf("pwm_start_seq: unit=%d, loop=%d, seq0ref=%d, seq1ref=%d\n", unit, loopcnt, ref0, ref1); ・ ・ ・ // シーケンス0のカウンタ比較値の配列を入れた // メモリアドレス(SEQ_PTR(0))、配列のデータ数(SEQ_CNT(0))、 // REFRESHレジスタの設定(※K) out_w(PWM(unit, SEQ_PTR(0)), (UW)&pwm_seq0_comp); out_w(PWM(unit, SEQ_CNT(0)), (sizeof(pwm_seq0_comp) / sizeof(_UH))); out_w(PWM(unit, SEQ_REFRESH(0)), ref0); out_w(PWM(unit, SEQ_ENDDELAY(0)), 0); // シーケンス1のカウンタ比較値の配列を入れた // メモリアドレス(SEQ_PTR(1))、配列のデータ数(SEQ_CNT(1))、 // REFRESHレジスタの設定(※K) out_w(PWM(unit, SEQ_PTR(1)), (UW)&pwm_seq1_comp); out_w(PWM(unit, SEQ_CNT(1)), (sizeof(pwm_seq1_comp) / sizeof(_UH))); out_w(PWM(unit, SEQ_REFRESH(1)), ref1); out_w(PWM(unit, SEQ_ENDDELAY(1)), 0); // LOOPレジスタに繰り返し回数を設定(※L) out_w(PWM(unit, LOOP), loopcnt); // PWMのシーケンス0の動作を開始 out_w(PWM(unit, TASKS_SEQSTART(0)), 1); } const INT led_pwm = 0; // LED用PWMとしてunit=0を使用 EXPORT void usermain(void) { ・ ・ ・ const INT loopcnt = 5; INT seq0_refresh = 50; INT seq1_refresh = 100; // PWMのシーケンス機能の設定と動作開始 pwm_start_seq(led_pwm, loopcnt, seq0_refresh, seq1_refresh); tk_slp_tsk(TMO_FEVR); // 永久待ち } |
前回の連載からPWMの説明が続いており、リアルタイムOSの話題とは少し離れてしまった。そこで、これまでの総集編として、µT-Kernelを使ってPWMによる音楽再生とLED点灯を同時に制御するプログラムを作成した。その一部をリスト3に示す。全体のリストはダウンロードしたソースコードをご覧いただきたい。連載第6回で説明した割込みの機能も使って、ボタンを押すたびに曲の再生が始まるようにしている。
このプログラムでは、曲の楽譜を記述するために、MML(Music Macro Language)とよばれる表記法を参考にした。たとえば、ドレミのドは“C”、8分音符は“8”のように表記する。ただし、音符の処理を簡単にするために、音符の音の長さ(“8”など)を音階(“C”など)の手前に書くようにしており、この点は本来のMMLと異なっている。プログラム中のmusic_kaeru(※M)、music_haru(※N)などの配列が楽譜のデータであり、これを処理する関数がpwm_musicである。各音符に対してpwm_musicの中から呼ばれる関数pwm_play(※P)では、指定された音階と音の長さに応じて、pwm_speaker_startを使ってスピーカー用PWMを設定して音の再生を行うとともに(※Q)、pwm_duty_startを使ってLED用PWMを設定し(※R)、LEDの点灯パターンを制御する。楽譜データの仕様や処理内容の詳細については、プログラム中のコメントをご覧いただきたい。
音階に合わせたLEDの点灯パターンを作るための比較値を入れた配列がcomp_tableである。この配列は3次元になっており、一つ目の添え字で点灯パターン(ptn)、二つ目の添え字でステップ、三つ目の添え字でチャンネルを区別する。起動直後にcomp_tableの全データを設定する関数がinit_pwm_dutyである。説明は省略するが、LEDのイルミネーションが滑らかに見えるように、データの作り方を工夫している。
pwm_playの中では、音階に応じてLEDの点灯パターンが変化するように、“C”(ド)を再生する際はptn=0、“D”(レ)を再生する際はptn=1... に対応する比較値の配列の先頭アドレスを、変数seq0_tableとseq1_tableを経由して(※S)、PWMのSEQ_PTR(0)とSEQ_PTR(1)に設定する。
割込みによってボタンスイッチからの入力を判定する部分は、連載第6回のプログラムのbtn_inthdrなどをそのまま使っており、ボタンが押されたことをイベントフラグで通知する。イベントフラグからの通知を処理するタスク(play_task)では(※T)、コンソールにメッセージを出力するのに加えて、音楽再生を始めるためにpwm_musicを呼び出す。
リスト3のmusic_kaeruやmusic_haruの部分に書かれた楽譜のデータを変えれば、他の曲も簡単に再生できるはずだ。自分の好きな曲に合わせて、micro:bitのLEDイルミネーションを楽しんでいただけるかと思う。
* * *
トロンフォーラムでは、2024年の前半から夏にかけて、µT-Kernel 3.0を使った「TRONプログラミングコンテスト」を開催している(*2) 。micro:bit用のµT-Kernel 3.0もコンテストの対象機種に含まれているので、本連載を通じて習得していただいた技術やノウハウを活用しながら、ぜひチャレンジしてみてほしい。コンテストへのエントリーの締切日は2024年3月31日、プログラムの応募期間は2024年8月31日までとなっている。
/*--------------------------------------------------------------- * PWMを使った音楽再生とLEDイルミネーション(μT-Kernel 3.0用) * * Copyright (C) 2022-2024 by T3 WG of TRON Forum *---------------------------------------------------------------*/ ・ ・ ・ // かえるの合唱(※M) B* music_kaeru[] = { "4C4D4E4F 4E4D4.C8R ", "4E4F4G4A 4G4F4.E8R ", "4C4R4C4R 4C4R4C4R ", "8C8C8D8D8E8E8F8F 4E4D4.C1R ", NULL}; // 春が来た(※N) B* music_haru[] = { "4G8E8F4G4A 4G8E8F4G4>C", "4A4G4.E8C 2.D4R", "4G8A8G4E4G 4>C8>D8>C4A4>C", "4G4>E4.>D8G 1>C R", NULL}; // 1つの音符の再生(※P) LOCAL void pwm_play(B m_char) { INT freq, doremi, ptn; INT seq0_time, seq1_time, seq0_refresh, seq1_refresh; _UH *seq0_table, *seq1_table; if(m_char == 'R'){ // 休符の場合 seq0_table = &comp_table[PTN_SEQ0_OFF][0][0]; // LEDを消灯 seq1_table = &comp_table[PTN_SEQ1_OFF][0][0]; } else { if((m_char < 'A') || (m_char > 'G')){ // 音階の指定 tm_printf("pwm_play: illegal char=%c(%02x)\n", m_char, m_char); return; } doremi = m_char - 'C'; // 音階のインデックス if(doremi < 0) { doremi += 7; } // 'A','B' の場合 ptn = doremi; freq = pwm_freqs[doremi]; // 音階に対応する周波数を取得 if(pwm_n_octave > 0){ // オクターブを上げる場合 freq = freq << pwm_n_octave; // 1オクターブで周波数2倍 ptn = 7; // 一番高い音のLED点灯パターン } else if(pwm_n_octave < 0){ // オクターブを下げる場合 freq = freq >> (- pwm_n_octave); // 1オクターブで周波数1/2 ptn = 0; // 一番低い音のLED点灯パターン } // スピーカー用PWMを制御して音符を再生(※Q) pwm_speaker_start(speaker_pwm, freq); // LED用PWMのパラメータとしてptnに応じた点灯パターンを設定(※S) seq0_table = &comp_table[PTN_SEQ0_MIN + ptn][0][0]; seq1_table = &comp_table[PTN_SEQ1_MIN + ptn][0][0]; } // ミリ秒単位のSEQ0時間 seq0_time = pwm_n_time * SEQ0_PERCENT / 100; // ミリ秒単位のSEQ1時間 seq1_time = pwm_n_time - seq0_time; // SEQ0,SEQ1のミリ秒単位の時間からREFRESHレジスタの設定値を計算 // 1ステップのサイクル数 = ミリ秒単位の1ステップの所要時間 // = (seq0_time / MAX_STEP) seq0_refresh = seq0_time / MAX_STEP - 1; if(seq0_refresh < 0) seq0_refresh = 0; // (-1)になった場合は0に戻す seq1_refresh = seq1_time / MAX_STEP - 1; if(seq1_refresh < 0) seq1_refresh = 0; // (-1)になった場合は0に戻す // LED用PWMを制御してLEDを点灯(※R) pwm_duty_start(led_pwm, seq0_table, seq1_table, seq0_refresh, seq1_refresh); prev_time += pwm_n_time; // 音符の再生を終了すべき時刻の計算 tk_dly_tsk(prev_time - cur_time()); // 終了すべき時刻までディレイ pwm_stop(speaker_pwm); // 音符の再生を終了 tk_dly_tsk(10); // 各音符の後のディレイ } ・ ・ ・ // ------------------------------------------------------- // ボタンA、Bが押されたら音楽を再生するタスク(※T) // ------------------------------------------------------- void play_task(INT stacd, void *exinf) { BOOL btn_a, btn_b; UINT flgptn; pwm_music(music_start, 80); for(;;){ // 永久に繰り返し // 割込み通知用イベントフラグの下位2ビットをOR待ち tk_wai_flg(flgid, 0b11, (TWF_ORW | TWF_CLR), &flgptn, TMO_FEVR); if((flgptn & (1 << 0)) != 0){ // 最下位ビットが1の場合 tm_printf("Button_SW_A: pushed\n"); // ボタンスイッチAの // 押下を表示 pwm_music(music_kaeru, 400); // 「かえるの合唱」を再生 } if((flgptn & (1 << 1)) != 0){ // 最下位から2番目のビットが1の場合 tm_printf("Button_SW_B: pushed\n"); // ボタンスイッチBの // 押下を表示 pwm_music(music_haru, 500); // 「春が来た」を再生 } // 500ミリ秒間待つ、その間の再度のボタン押下は // チャタリングとして無視 tk_dly_tsk(500); tk_clr_flg(flgid, 0); } } |
■ 本稿で説明したプログラムのソースコードのダウンロード |
---|
<https://www.personal-media.co.jp/book/tw/tw_index/382.html ソースコードをご利用になる前に、必ず上記ページ掲載の「ご利用条件およびご利用上のご注意」をお読みください。 |