本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第9回の本号では、micro:bitのCPUに内蔵されているPWM(Pulse Width Modulation)を使って、音の再生やLEDの調光制御を試してみよう。
前回の連載では、µT-Kernel 3.0の時間管理機能や物理タイマを使ってタイミングを制御しながら、内蔵スピーカーに接続されたGPIOへの出力値をソフトウェアによって0→1→0→...と変化させ、micro:bitからブザー音やドレミファの音階を鳴らしてみた。これと同じように、0→1→0→...と変化する信号をハードウェアで発生するのがPWMである。PWMの‘P’はPulse(パルス)に由来しているが、その意味は0→1→0→...といった変化を続ける信号である。パルスの日本語訳として、脈拍とか振動といった意味もある。‘W’と‘M’の意味は後で説明するとして、振動する信号をスピーカーに出力すれば、音を再生できる。
PWMはTarget MCUであるnRF52833が内蔵する機能の一つである。したがって、PWMの具体的な機能や使い方を知るには、nRF52833のマニュアルの中のPWMの章を参照する(*1) 。これを読んで理解すればPWMを使った音の再生ができるのだが、このPWMには非常に多くの機能があり、その設定方法は複雑である。そこで、まずは音の再生に必須となる一部の機能のみを説明する。
PWMの基本的な動作は、一定周波数のクロックを数え続けるカウンタである。1回のクロックに対して、カウント数が一つ増える。カウンタには上限値(COUNTERTOP)と比較値(COMP)を設定することができ、カウント数が上限値に達すると0に戻る。PWMの出力は、カウント数が比較値よりも小さい場合に0、比較値よりも大きい場合に1となる。この様子を図1に示す。PWMの出力は0と1を繰り返すが、比較値を上限値の半分にしておけば、出力が0の時間と1の時間が1周期の半分ずつになる。
PWM出力の接続先として、GPIOのポート番号とピン番号を指定する。したがって、内蔵スピーカーに接続されたP0.00を指定すれば、PWMの出力によって内蔵スピーカーを制御することができる。また、LEDのROW1..ROW5やCOL1..COL5に接続されたGPIOのポート番号とピン番号を指定すれば、PWMからLEDを制御することもできる。これら以外にも、GPIOに接続されたデバイスであれば、PWMで制御することが可能である。
nRF52833は、PWM0(unit=0)からPWM3(unit=3)まで四つの独立したPWMを持っている。個々のPWMの区別をユニット(unit)とよび、unit番号に応じて設定用レジスタのアドレスが決まっている(表1)。PWMの主な設定用レジスタを表2に示す。
レジスタ | アドレスのオフセット | 説明 |
---|---|---|
TASKS_STOP | 0x004 | PWMの停止 |
TASKS_SEQSTART[0] | 0x008 | PWMのシーケンス0の開始 |
TASKS_SEQSTART[1] | 0x00C | PWMのシーケンス1の開始 |
EVENTS_LOOPSDONE | 0x11C | ループ終了状態 |
ENABLE | 0x500 | PWMのイネーブル |
MODE | 0x504 | カウンタの操作モード |
COUNTERTOP | 0x508 | カウンタ上限値 |
PRESCALER | 0x50C | クロック入力の分周設定 |
DECODER | 0x510 | デコーダ構成 |
LOOP | 0x514 | ループ回数設定 |
SEQ[0].PTR | 0x520 | シーケンス0の比較値を入れるRAMアドレス |
SEQ[0].CNT | 0x524 | シーケンス0の比較値の個数 |
SEQ[0].REFRESH | 0x528 | シーケンス0のリフレッシュ回数 |
SEQ[1].PTR | 0x540 | シーケンス1の比較値を入れるRAMアドレス |
SEQ[1].CNT | 0x544 | シーケンス1の比較値の個数 |
SEQ[1].REFRESH | 0x548 | シーケンス1のリフレッシュ回数 |
PSEL.OUT[0] | 0x560 | チャンネル0出力のGPIOピン選択 |
PSEL.OUT[1] | 0x564 | チャンネル1出力のGPIOピン選択 |
PSEL.OUT[2] | 0x568 | チャンネル3出力のGPIOピン選択 |
PSEL.OUT[3] | 0x56C | チャンネル3出力のGPIOピン選択 |
PWMを使ってドレミファの音階を再生するプログラムをリスト1に示す。micro:bit用のµT-Kernel 3.0のソースプログラムにはPWM関連の定義が含まれていないので、プログラムの最初にdefineマクロを使ってPWMの各レジスタのアドレスを定義している(リスト1の(※A))。
関数pwm_start_freqでは、PWMによる音の再生を開始する(※B)。使用するPWMのユニット番号unitと再生したい音の周波数freqを引数としており、再生音の周波数freqに応じてPWMのレジスタを設定する。
PWMによる再生音の周波数は、入力クロックの周波数とカウンタ上限値によって決まる。PWMの入力クロックの周波数はマニュアル(*1) のPRESCALERレジスタの部分に書かれており、このレジスタの設定によって16MHzの元クロックを分周(クロックをカウントして周波数を落とすこと)できることがわかる。たとえばDIV_4を設定すると、16MHzの元クロックが4分周され、元クロックを4回カウントした段階で一つのクロックがPWMに送られる。この場合、PWMの入力クロックの周波数は、16MHzの4分の1である4MHzとなる。今回の例題では、DIV_16を設定して元クロックを16分周し、PWMの入力クロックを1MHz(=1,000,000Hz)としている(※C)。1クロックあたりの時間は、ちょうど1マイクロ秒(=1秒÷1,000,000)となり、時間の計算がしやすい。
再生音の周波数、すなわちPWM出力の周波数をfreqにするには、PWM出力を1秒間にfreqの回数だけ振動(0→1→0と変化)させればよい。この場合の1回の振動の周期時間は、(1秒間÷freq)である。この周期時間をマイクロ秒単位としたcycle_usは、(1,000,000マイクロ秒÷freq) と計算できる(※D)。一方、PWMの入力クロック1回の時間は1マイクロ秒なので、PWMのカウンタの値は1マイクロ秒ごとに一つずつ増える。そうすると、PWMのカウンタ上限値をNとした場合に、この上限値に達するまでの時間はNマイクロ秒である。この時間がPWM出力の周期時間になるので、cycle_usの値をそのままカウンタ上限値Nとして設定すればよい(※E)。
PWMのカウンタ比較値pwm_comp0は、上限値の半分に設定する(※F)。比較値が上限値の半分なので、PWM出力が0の時間と1の時間はどちらも1周期の半分になる。この比較値はPWMのレジスタに設定するのではなく、16ビットの変数としてメモリ上に置き、そのアドレスをPWMのレジスタに設定する。このPWMは、複数の比較値を使って同時に複数のPWM出力を得る機能や、複数の比較値を並べて順に変化させる機能を持っており、その個数も任意に決められる。そのため、比較値はメモリ上の配列としておき、その先頭アドレスとデータの個数をPWMのレジスタに設定する形になっている。さらに、比較値の変化パターンとして、シーケンス0とシーケンス1の2種類を定義する機能もある。この例題ではシーケンス0の比較値を一つ使うだけなので、比較値pwm_comp0は_UH型の変数としてメモリ上に置き、そのアドレスをSEQ[0].PTRに設定するとともに、比較値の個数である1をSEQ[0].CNTに設定している(※G)。
これ以外のPWMのレジスタ設定は、特別な機能を使わないデフォルトの設定である。ENABLEではPWMの機能を動かすために1を設定する。それ以外のレジスタは0を設定している。最後に、TASKS_SEQSTART[0]に1を設定することにより、PWMがシーケンス0の動作を開始する(※H)。これ以降は、上限値がcycle_us、比較値がpwm_comp0の設定でPWMの動作が継続し、PWM出力には0→1→0→1→...と振動する信号がfreqの周波数で出力される。
このPWMの動作を停止する関数がpwm_stopである。ここでは、TASKS_STOPのレジスタに1を書き込んでPWMの動作を停止させている(※J)。
pwm_start_freqとpwm_stopを使って、指定した周波数の音を指定した時間だけ再生する関数がplay_speaker_pwmである(※K)。この関数の動作は単純で、最初にpwm_start_freqを呼び出してPWMの動作を開始し、再生時間ptimeだけ待ってから、最後にpwm_stopを呼び出しているだけである。使用するPWMはunit=0のPWM0である。
メインプログラムのusermainでは、まず前回連載の例題と同じように、内蔵スピーカーに接続されたGPIOピン(P0.00)を出力に設定する(※L)。次に、PWMの出力先の設定を行う。これまでは単に「PWMの出力」と書いてきたが、実は、1ユニットのPWMが最大で4本の出力を持つことができ、個々の出力をチャンネル0、1、2、3とよんで区別している。各チャンネルの出力先はPSEL.OUT[0]、PSEL.OUT[1]、PSEL.OUT[2]、PSEL.OUT[3]のレジスタによって指定されるが、この例題で使用するのはチャンネル0のみなので、PSEL.OUT[0]のレジスタを設定する。マニュアル注1)のPSEL.OUT[n]の説明に従って、最下位から6ビット目にPWMの出力先となるGPIOのポート番号を指定し、最下位の5ビットでGPIOのピン番号を指定する。内蔵スピーカーに接続されたGPIOはP0.00であり、ポート番号もピン番号も0なので、PSEL.OUT[0]に設定すべき値も結局0である(※M)。
あとは、forループの中からplay_speaker_pwmを呼び出して、ドレミファの音階に相当する周波数を順に再生していけばよい(※N)。せっかくなので、音階が1オクターブ上がると周波数が倍になるという性質を利用し、外側のforループを使って4オクターブの音階を順に再生するプログラムを作ってみた。
/*------------------------------------------------------ * PWMを使ったドレミファ音階の再生(µT-Kernel 3.0用) * * Copyright (C) 2022-2023 by T3 WG of TRON Forum *----------------------------------------------------*/ #include <tk/tkernel.h> #include <tm/tmonitor.h> //-------- PWMのレジスタ定義(※A) -------------------------- #define PWM_BASE(un) ((un) == 0 ? 0x4001c000 : (un) == 1 ? 0x40021000 : (un) == 2 ? 0x40022000 : 0x4002d000) #define PWM(un, r) (PWM_BASE(un) + PWM_##r) #define PWM_TASKS_STOP 0x004 #define PWM_TASKS_SEQSTART(n) (0x008 + (n) * 0x04) #define PWM_LOOPSDONE 0x11C #define PWM_ENABLE 0x500 #define PWM_MODE 0x504 #define PWM_COUNTERTOP 0x508 #define PWM_PRESCALER 0x50c #define PWM_DECODER 0x510 #define PWM_LOOP 0x514 #define PWM_SEQ_PTR(n) (0x520 + (n) * 0x20) #define PWM_SEQ_CNT(n) (0x524 + (n) * 0x20) #define PWM_SEQ_REFRESH(n) (0x528 + (n) * 0x20) #define PWM_SEQ_ENDDELAY(n) (0x52c + (n) * 0x20) #define PWM_PSEL_OUT(n) (0x560 + (n) * 0x04) // 比較値COMP0を格納する16ビット幅のメモリ領域 static _UH pwm_comp0; // unitのPWMに周波数freq(Hz)を出力するように設定して動作開始(※B) LOCAL void pwm_start_freq(INT unit, INT freq) { tm_printf("pwm_start_freq: unit=%d, freq=%d\n", unit, freq); // 1周期のPWM出力に対するマイクロ秒単位の時間を計算(※D) // (=1MHzのクロックに対するカウント値) // この値をカウンタ上限値に設定する INT cycle_us = 1000000 / freq; out_w(PWM(unit, COUNTERTOP), cycle_us); // カウンタ上限値の設定(※E) pwm_comp0 = cycle_us / 2; // カウンタ上限値の半分を比較値に設定(※F) out_w(PWM(unit, ENABLE), (1 << 0)); out_w(PWM(unit, MODE), 0); out_w(PWM(unit, PRESCALER), 4); // DIV_16でクロック1MHzを指定(※C) out_w(PWM(unit, LOOP), 0); out_w(PWM(unit, DECODER), 0); // LOAD=commonを指定 // 比較値を入れるRAMアドレスとサイズの設定(※G) out_w(PWM(unit, SEQ_PTR(0)), (UW)&pwm_comp0); out_w(PWM(unit, SEQ_CNT(0)), 1); out_w(PWM(unit, SEQ_REFRESH(0)), 0); out_w(PWM(unit, SEQ_ENDDELAY(0)), 0); // PWMのシーケンス0の動作を開始(※H) out_w(PWM(unit, TASKS_SEQSTART(0)), 1); } // unitのPWMの動作を停止(※J) LOCAL void pwm_stop(INT unit) { out_w(PWM(unit, TASKS_STOP), 1); } const INT speaker_pwm = 0; // unit=0のPWMからSPEAKERに出力 // PWMを使って周波数freq(Hz)の音をptime(ミリ秒)間だけ // SPEAKERに出力(※K) LOCAL void play_speaker_pwm(INT freq, INT ptime) { pwm_start_freq(speaker_pwm, freq); tk_dly_tsk(ptime); pwm_stop(speaker_pwm); } // ドレミファソラシド(C4,D4,E4,F4,G4,A4,B4,C5)の周波数(Hz) const INT freqs[] = { 261, 293, 329, 349, 391, 440, 493, 523 }; EXPORT void usermain(void) { // SPEAKERに接続されたGPIOピン(P0.00)を出力に設定(※L) out_w(GPIO(P0, PIN_CNF(0)), (1 << 0)); // PWM(unit=0)のチャンネル0をGPIOピン(P0.00)経由で // SPEAKERに出力(※M) out_w(PWM(speaker_pwm, PSEL_OUT(0)), ((0 << 5) | (0 << 0))); // ドレミファ音階の再生(※N) for (INT octave = 0; octave < 4; octave++){ for (INT cnt = 0; cnt < 8; cnt++){ play_speaker_pwm((freqs[cnt] << octave), 200); } tk_dly_tsk(100); // 1オクターブごとに100ミリ秒待つ } tk_slp_tsk(TMO_FEVR); // 永久待ち |
内蔵スピーカーから音を出すために、PWMを発振器として利用した。しかし、PWMの本来の機能は、「デューティ比」(duty ratio)の設定が可能な発振器である。次の例題では、この動作を確認するために、LEDの調光制御を行ってみよう。
LEDは一定以上の電圧をかければ発光するが、低い電圧をかけても弱く発光するわけではない。発光するかしないかの二択のみを制御できるデジタルなデバイスであり、この点は低い電圧でも弱く発光する白熱電球とは異なっている。このLEDを弱く発光させるには、どうすればよいだろうか。
考えられるのは、LEDの発光のONとOFFの切替を高速で繰り返し、ONの時間的な割合を調整するような方法である。たとえば、1ミリ秒の周期で発光のONとOFFを繰り返し、1ミリ秒のうちの最初の800マイクロ秒は発光せず、最後の200マイクロ秒のみ発光させる。こうすると、LEDの発光している時間が5分の1なので、LEDの発光量も平均的に見れば5分の1になるはずだ。この場合は時間的に5分の1の割合で発光するようにしたが、この時間の割合を変化させれば、LEDの見かけの明るさも変わり、指定した明るさに調光制御できるだろう。連載第7回のLEDのダイナミック点灯で説明したように、LEDの点滅の速度が十分に速ければ、人間の目には連続して点灯しているように見える。
最初に「デューティ比」という語を使ってしまったが、このようにONとOFFの高速切替による制御を行う場合において、ONの時間の割合を意味するのが「デューティ比」である。上記のLEDの例では、全体の5分の1がONの時間なので、デューティ比は20%となる(図2)。
PWMを使えば、このデューティ比を柔軟に制御できる。そのために活用されるのが、PWMのカウンタ比較値である。PWMはカウンタを持っており、そのカウント値が比較値より小さい場合はPWM出力が0となり、比較値を超えた場合はPWM出力が1となる。たとえば、PWMのカウンタ上限値を1000、比較値を800に設定すると、カウント値が0から800までの間はPWM出力が0となり、カウント値が800から1000までの間はPWM出力が1となる。この場合には、PWM出力が0の時間と1の時間の比率は800:200なので、デューティ比は200÷1000=20%である(図2)。一般には、PWM出力が0の時間と1の時間の比率が(比較値-0):(上限値-比較値)となるので、デューティ比は(上限値-比較値)÷上限値 と計算できる。
PWMの‘W’はWidth(幅)、‘M’はModulation(変調)に由来しており、パルスの幅を調節するといった意味を持つ。カウンタ比較値の設定によってパルスの幅を調節し、デューティ比を自由に変更できるのは、PWMの本質的な機能である。
PWMを使ってデューティ比を変化させ、micro:bitの左上にあるD2のLEDに対する調光制御を試すプログラムをリスト2に示す。関数pwm_start_duty(※P)では、リスト1の関数pwm_start_freqと同じように、PWMの各レジスタを設定してからPWMの動作を開始する。ただし、pwm_start_dutyでは、周波数freqの代わりに、パーセント単位のデューティ比dutyを引数として指定する。一方、カウンタ上限値TOPは1000に固定している。デューティ比は(上限値-比較値)÷上限値 なので、ここから逆算すると、比較値=上限値-(上限値×デューティ比)となる。この式を使ってdutyからPWMのカウンタ比較値pwm_comp0を求める(※Q)。
PWMの動作を停止するpwm_stopはリスト1と同じである。このほか、LEDやGPIOを制御するために、連載第7回で作成したled_initとout_gpio_pinの関数を使う。
usermainでは、まずled_initを実行してLEDに出力するためのGPIOを設定する。また、D2のLEDはCOL1とROW1に接続されているので、これ以外のLEDが点灯しないようにCOL2..COL5には1を出力し、COL1にのみ0を出力する(※R)。
次に、PWMからLEDを制御するために、PWMの出力をLEDに接続する。PWM出力が1の時にD2のLEDを点灯させるには、COL1を0に固定した上で、PWM出力をROW1に接続すればよい。こうすると、PWM出力が0の時は(ROW1,COL1)=(0,0)となってD2のLEDは消灯し、PWM出力が1の時は(ROW1,COL1)=(1,0)となってD2が点灯する(連載第7回のリスト2参照)。PWMの出力はリスト1と同じくチャンネル0を使うことにして、その出力先を指定するPSEL.OUT[0]のレジスタには、ROW1に接続されたGPIO P0.21を設定する。具体的な設定値は、最下位から6ビット目がGPIOのポート番号を表す0、最下位の5ビットがGPIOのピン番号を表す21なので、合わせて21となる(※S)。
初期設定が終わった後は、内側のforループを使って500ミリ秒ごとにpwm_start_dutyを呼び出し、デューティ比を10%ずつ変えながらPWMを動作させてLEDを点灯する(※T)。PWMの入力クロックは1MHzなので、LEDを点滅させるPWM出力の周波数はこの1,000分の1、つまり1kHzとなる。この動作を、外側のforループで5回繰り返す。
このプログラムを実行すると、左上のD2のLEDが次第に明るくなっていくのがわかる。LEDは高速で点滅しているはずだが、その速度は1秒間に1,000回なので、人間の目には連続点灯のように見える。これで、PWMを使ったLEDの調光機能を確認することができた。
図2 PWMによるLEDの調光制御の例
クロックは1MHz(クロック1回、カウント数一つが1µs)
上限値が1000、比較値が800の場合はデューティ比が20%で明るさは5分の1
/*--------------------------------------------------------------- * PWMを使ったLED(左上のD2)の調光制御(µT-Kernel 3.0用) * * Copyright (C) 2022-2023 by T3 WG of TRON Forum *---------------------------------------------------------------*/ ・ ・ ・ // カウンタ上限値の定義 #define TOP 1000 // 比較値COMP0を格納する16ビット幅のメモリ領域 static _UH pwm_comp0; // パーセント単位のデューティ比dutyを指定してPWMの動作を開始(※P) LOCAL void pwm_start_duty(INT unit, INT duty) { tm_printf("pwm_start_duty: unit=%d, duty=%d%%\n", unit, duty); out_w(PWM(unit, COUNTERTOP), TOP); // カウンタ上限値の設定 // パーセント単位のデューティ比dutyからカウンタ比較値を // 計算して設定(※Q) pwm_comp0 = (TOP - (TOP * duty / 100)); ・ ・ ・ } ・ ・ ・ EXPORT void usermain(void) { const INT led_pwm = 0; // unit=0のPWMを使ってLEDを点灯 led_init(); out_gpio_pin(0, 28, 0); // GPIO P0.28-COL1に0を出力(※R) out_gpio_pin(0, 11, 1); // GPIO P0.11-COL2に1を出力(※R) out_gpio_pin(0, 31, 1); // GPIO P0.31-COL3に1を出力(※R) out_gpio_pin(1, 5, 1); // GPIO P1.05-COL4に1を出力(※R) out_gpio_pin(0, 30, 1); // GPIO P0.30-COL5に1を出力(※R) // PWMのチャンネル0をGPIOピン(P0.21)経由でROW1に出力(※S) // LED(D2)は GPIO P0.21=1(ROW1), GPIO P0.28=0(COL1) の時に点灯 out_w(PWM(led_pwm, PSEL_OUT(0)), ((0 << 5) | (21 << 0))); for(INT cnt = 0; cnt < 5; cnt++){ // 以下を5回繰り返し // 500ミリ秒ごとにデューティ比を10%ずつ増やしながら // LEDを点灯(※T) for(INT duty = 0; duty <= 100; duty += 10){ pwm_start_duty(led_pwm, duty); tk_dly_tsk(500); pwm_stop(led_pwm); } } tk_slp_tsk(TMO_FEVR); // 永久待ち } |
* * *
PWMは、ONとOFFのみというデジタルな制御を行いつつも、ONの時間の割合を変えることによって、アナログ的な連続量の調整をするための機能である。今回の例題のような調光制御のほか、トルクや回転角に応じて電圧や電流を細かく変化させるべきモーターの制御にも幅広く使われている。最小限の電力で効率よくモーターを回すためには、PWMの技術が不可欠であり、CO2削減への貢献も大きい。
micro:bit用のµT-Kernel 3.0を使えば、実際にPWMを設定してその動作を確認することができる。本稿を参考に、ぜひ試してみてほしい。
■ 本稿で説明したプログラムのソースコードのダウンロード |
---|
https://www.personal-media.co.jp/book/tw/tw_index/381.html ソースコードをご利用になる前に、必ず上記ページ掲載の「ご利用条件およびご利用上のご注意」をお読みください。 |