公開日:2020/10/17
title.png

13th Season


#13-2 MMLで三和音を演奏する

 UNOを使って、MML(Music Macro Language)を演奏🎼するプログラムを作ったことがあります(↓)。

  「#5-8 MMLで音楽を奏でる

 基本的な音の鳴らし方などの解説は、上の記事を復習してみてください。一言で言うと、鳴らしたい音の周波数に相当するPWMを出力、LPFで波形を作って、イヤホンに出力する感じです。

 タイマのPWM出力を使うのですが、PWMの周波数を変える必要があるため、1音辺り1本のタイマが必要となります。このため、タイマ数に限りのあるUNOでは、1音の演奏で留めてありました。今回はDUEを使います。DUEはタイマ数もさることながら、PWM出力に特化したPWMユニットから最大8チャンネルのPWMを出力することが出来るので、これを使います。
 さすがに8音も演奏できても、外付けの回路を構成するのが大変そうなので、筆者が35年以上昔に初めて使ったパソコンFM-7が演奏できた、三和音の演奏を実現したいと思います。

※MMLの演奏ではありませんでしたが、UNOで2音の同時演奏をした例として、「#8-4 電話をかける」で使ったDTMFというアプリケーションが有ります。今回はさらに1音追加します。

SAM3X8EのPWMユニット

 まずはDUEのMCU、SAM3X8Eに搭載されているPWMユニットの使い方を調べておきます。
 PWMユニットは、PWM信号の出力に特化したタイマで、モータなどを回す時に使うPWM波形などの生成もできるようになっています。PWMユニットには8チャンネルのタイマがあって、各チャンネルから2本のPWM信号(PWMLx,PWMHx)を出力することが可能です。

 今回出力したいのは、非常に基本的なデューティ50%のPWM波形ですので、それほど複雑な設定は要らないはずです。いつもの様LogiscopeとanalogWrite()関数によるPWM出力を使って、必要な設定項目を調査してみたいと思います。
 DUE端子表によると、analogWrite()関数がPWM出力可能な端子D2〜D13の中で、PWMユニットを使う端子は以下の4本だとわかります。

  D6(PC24/PWML7)
  D7(PC23/PWML6)
  D8(PC22/PWML5)
  D9(PC21/PWML4)

 そこで、Logiscopeを起動したDUEで以下の様に実験してみます。

  実験サンプル:【 】内はLogiscopeからの送信データ
  【dump4,40094000,64】
  【dump4,40094130,32】
  【dump4,40094200,64】
    : (結果省略)

 ↑PWMユニットの全レジスタ空間(リザーブ領域含む)を読み出してみました。ここで、PWMチャンネル4に相当するD9にanalogWrite()関数でPWMを出力するため、Logiscopeの【ana】コマンドを使用します(↓)。

  【ana,9,96】
  OK: analogWrite(9, 96)

 LogiscopeがanalogWrite()関数を実行したので、もう一度、PWMユニットのレジスタを読み出し直します(↓)。

  【dump4,40094000,64】
  【dump4,40094130,32】
  【dump4,40094200,64】
    : (結果省略)

 【ana】コマンド前後でレジスタ設定値の変化を比較したところ、以下のレジスタが設定されていました。

アドレス 設定値 レジスタ 解釈
0x40094000 0x000001a4 PWM_CLK PWM Clock Register PREB=0x0,
DIVB=0,
PREA=0x1,
DIVA=0xa4
0x4009400c 0x00000010 PWM_SR PWM Status Register channel#4 enable
0x40094200 0x00000001 PWM_CMR0 PWM Channel0 Mode Register CPRE=1(MCK/2)
0x40094280 0x0000000b PWM_CMR4 PWM Channel4 Mode Register CPRE=0xb(CLKA)
0x40094284 0x00000060 PWM_CDTY4 PWM Channel4 Duty Cycle デューティ
0x4009428c 0x000000ff PWM_CPRD4 PWM Channel4 Period Register 周期
0x40094294 0x00000087 PWM_CCNT4 PWM Channel4 Counter Register カウンタ

 コアライブラリのソースを解析するより、こうしてレジスタをダンプしてみる方が近道です。チャンネル4を設定したのですが、どういう訳かチャンネル0のモードレジスタが書き換わっていました(灰色着色)。たぶん設定不要なので、無視します。以下、設定値の解釈です。

 【dump4】コマンドで表示されたレジスタ数の数にうんざりしたかもしれませんが、普通にPWMを出力するだけなら、上述程度の簡単な設定でOKだと判りました。


 念のため、音楽演奏とanalogWrite()関数によるPWM出力が共存できるように、今回の音楽演奏には、analogWrite()関数で使用されていないチャンネルを使おうと思います。三和音演奏するためには3チャンネルのPWMが必要です。analogWrite()関数の使用チャンネルはPWMユニットのチャンネル4〜7ですので、音楽演奏にはチャンネル1〜3を使うことにします。
 DUE端子表によると、これらの端子は2列ピンソケットに引き出されています(以下)。

  D36(PC4/PWML1)
  D37(PC5/PWMH1)
  D38(PC6/PWML2)
  D39(PC7/PWMH2)
  D40(PC8/PWML3)
  D41(PC9/PWMH3)

 今回のプログラムでは、ピン番号が偶数のD36,D38,D40を使っています。

サウンド出力回路の検討

 基本的にはUNOの時に使ったのと同様な回路となります。以下、参考までに再掲しておきます。

ar58_schem.png


 今回の差分は、三和音演奏のためPWM出力が3本になるという事です。PWM出力2本を使ったDTMFでは、以下の様にLPFで波形成形した後で直結(ショート)していました(以下、再掲)。

ar84_schem.png

 対象はアナログのサウンド出力信号なので、ショートした時の電流値さえ気を付けておけば、ショートでも良さそうなものです。しかし、今回は手持ちに容量の大きなコンデンサが無く、100nFのセラミックコンデンサを使わざるを得ず、時定数を合わせるために抵抗値の方を1MΩと言う大きな値にせざるを得ませんでした。
 この場合、LPF回路に流れる電流値が極端に絞られるため、LTspiceでシミュレーションしてみたところ、直結(ショート)では信号レベルの安定に時間がかかることが判りました。対策として、今回はオペアンプの加算回路を使っておこうと思います。


 以下、今回の三和音演奏回路図です。

a132_schem1.png

a132_schem2.png

a132_schem3.png

  ポイント:

 上で示した回路の様に、LPFで波形を作ってイヤホンなどへ出力するのが推奨ですが、「#3-7 ブザーとサウンドセンサ」で紹介した様に、3本のPWM出力を各々3個のパッシブブザーに接続すると、ずっと簡単に三和音演奏が可能です。
 手元にパッシブブザーが1個しか無かったので、1音だけ鳴らしてみたのですが、UNOの時には甲高くて耳障りだった音が、DUEでは少し柔らかくなっている気がしました。I/O出力が3.3Vになって、パワーが落ちたため?でしょうか?……ただし耳障りな音であることに変わりは無いので、推奨はあくまで、上で示した回路の方です。

音の鳴らし方

 基本的には、UNO版「#5-8 MMLで音楽を奏でる」と同じです。MMLを1音づつ解析してPWM周波数を決めたら、割り込みを使って音の長さを制御します。こうすることで、バックグラウンドでMML演奏が可能となって、フォアグラウンドでは別な処理を継続することが出来ます。UNO+TFTシールドで作ったゲームArdiousが良い例です。

 今回の変更点は、三和音(3チャンネル出力)への対応と、無音部分の扱い、PWMユニットの制御の3点です。

三和音(3チャンネル出力)対応

 UNO版と同様、play_mml()関数を作ります。play_mml()関数は、MML文字列へのポインタと、チャンネル番号(0〜2)を引数として取ります。即ち、play_mml()関数自体は、三和音の処理を一切しません。受け付けるMMLは、従来の1音のみのMMLとします。

  int play_mml (const char *mbuf, int n_chn=0);

 上のプロトタイプ宣言の様になっていて、チャンネル番号にn_chn=0のデフォルト値が指定されているので、引数n_chnを省略すると、常にチャンネル0を使って演奏することになります。三和音を使用しない場合には、UNOの時と全く同じ作法でMMLを演奏可能にする工夫です。

 次回、MMLを三和音に拡張しますが、ユーザが三和音対応MMLを各チャンネル毎に分解して、チャンネル毎にplay_mml()関数を実行してください。
 ひっくり返すと、play_mml()関数自体は三和音の処理を一切しないので、3チャンネルを同期させて「和音」として使おうが、非同期に別々な(無関係な)音を鳴らそうが、ユーザ側の自由に委ねられるという事です。

無音部分の扱い

 実はUNOの時には、無音部分でPWMを停止して出力をLOWに固定していました。PWM出力→LOW出力に変化する過渡状態で、意図しない周波数の信号が出力されるので、音の鳴り終わり(と、鳴り始め)に意図しない音が聞こえる可能性はありましたが、恐らく気にならない(聞き取ることができない)だろうと踏んでいました。
 今回もその状況は同じなのですが、加算回路で3音のアナログ信号を加算する都合上、LOW出力を加算してしまうと加算結果のバイアスレベルがずれてしまい、オペアンプの出力電圧範囲(GND〜VOH)でクリッピングされた結果、波形が崩れてしまう恐れがあります。

 つまり今回の回路構成では、無音部分の音声信号出力をバイアスレベル(Vb=Vdd/2)にする必要があり、このため、無音部分でもPWM波形を停止することが出来ません。PWMが出力されていても、それが音として聞こえなければ良いので、可聴域20〜20k[Hz]を外れた30k〜40k[Hz]程度のPWMを出力して、無音部分を形成します。

※低周波数側に外れた5〜10[Hz]程度のPWMでは、LOW出力(0Hz)と同じ事なのでダメです。高周波数側に外す必要があります。

 無音部分や演奏していない状態でも、かなり高速なPWMが出力され続けていることとなってしまい、消費電力とノイズ(EMI)的には宜しくないのですが、これしか解が無いので止むを得ません。

チャンネル間のズレ

 音符を演奏する長さは、ms単位の整数値で計算しています。計算した長さが経過したかどうかは、PWMカウンタのオーバフロウ割り込みで判定しています。即ち、演奏時間の解像度はPWMカウンタのオーバフロウ時間に依存します。
 例えば、A4(オクターブ4のラ音)の場合、周波数440Hz → 割り込み周期≒2.3msです。割り込みが起きた時に、演奏すべき時間を経過したかどうか?を判定しているので、最大で+2.3msの誤差を生じます。この誤差は常に正なので、A4を100音演奏すると、最大で230msのずれが生じる計算です。

 長いMMLを演奏させたり、コールバック関数を使ってチャンネル毎に演奏を継続したりする場合、msオーダの誤差が蓄積して行くと、チャンネル間のズレが気になる(聞き分けられる)ほどまで広がってしまう恐れがあります。定期的にチャンネル間の同期を取って演奏するようにしてください。

  play_mml (mml0);
  play_mml (mml1, 1);
  play_mml (mml2, 2);


 上の様に、全チャンネルを同時に演奏開始すれば、その時点で同期化されますので、さほど使い難い仕様制限ではないはずです。1曲丸々一気に演奏するのではなく、例えば1〜4小節毎に、上例の様に同期化して演奏する様にすれば良いだけです。

PWMユニットの制御

 まず、PWMユニットのカウンタを動かすクロック周波数を決めておきます。


 次に割り込みについて検討します。


 PWMユニットを使用するには、クロック供給(PMC)、PWMユニットの設定、ポートの設定(PORT)、割り込み設定(NVIC)が必要となります。以下に具体的な初期設定を示しておきます。

  // PMC setting
  // PWM peripheral id=36
  REG_PMC_PCER1=PMC_PCER1_PID36;

  // PWM setting
  REG_PWM_CPRD1=ply_MUTE;
  REG_PWM_CDTY1=ply_MUTE/2;
  REG_PWM_CMR1=PWM_CMR_CPRE_MCK_DIV_64;
  REG_PWM_CPRD2=ply_MUTE;
  REG_PWM_CDTY2=ply_MUTE/2;
  REG_PWM_CMR2=PWM_CMR_CPRE_MCK_DIV_64;
  REG_PWM_CPRD3=ply_MUTE;
  REG_PWM_CDTY3=ply_MUTE/2;
  REG_PWM_CMR3=PWM_CMR_CPRE_MCK_DIV_64;
  REG_PWM_IDR1=PWM_IDR1_CHID3 | PWM_IDR1_CHID2 | PWM_IDR1_CHID1;
  REG_PWM_ENA=PWM_ENA_CHID3 | PWM_ENA_CHID2 | PWM_ENA_CHID1;

  // PORT setting
  // pin_PWM0: D36(PC4/PWML1)
  // pin_PWM1: D38(PC6/PWML2)
  // pin_PWM2: D40(PC8/PWML3)
  digitalWrite (pin_PWM0, LOW);
  pinMode (pin_PWM0, OUTPUT);
  digitalWrite (pin_PWM1, LOW);
  pinMode (pin_PWM1, OUTPUT);
  digitalWrite (pin_PWM2, LOW);
  pinMode (pin_PWM2, OUTPUT);
  // disable port output, select B function
  REG_PIOC_PDR=PIO_PDR_P8 | PIO_PDR_P6 | PIO_PDR_P4;
  REG_PIOC_ABSR |= PIO_ABSR_P8 | PIO_ABSR_P6 | PIO_ABSR_P4;

  // NVIC setting
  NVIC_EnableIRQ (PWM_IRQn);


 各レジスタの設定内容についてはSAM3X8Eのデータシートでご確認ください。


 ちょっと中途半端なのですが、今回のところはここまでとします。次回、ちゃんと最後まで仕上げて行きたいと思います。


btn_arduino.png

btn_prev.png btn_next.png

home