単体版無半音キーボード

無半音キーボードは、評価基板につないで使っていましたが、 これを単体のアプリケーションに仕立てます。

完成写真 こげこげ君

アジェンダ

ハードウェア

このアプリケーションでは、 評価ボードを使わずに 無半音キーボードで作成した基板の上に すべての部品を並べます。

回路図

このアプリケーションの回路は、 以下のようになっています。

また、アートワークは、 以下のようになっています。

部品リスト

必要な部品は、以下の通りです。

単体版無半音キーボード部品表
品名型番メーカ個数調達先
マイコンMC9S08QG8CPBEFreescale1Freescale
ICソケット16P DIP-1秋月電子
積層セラミック・コンデンサ0.1µF 50V-2秋月電子
ボタン電池ホルダCH25-2032SHOGYO1秋月電子
ボタン電池CR2032東芝1千石電商
カーボン抵抗2.7kΩ 1/6W-1秋月電子
圧電スピーカ--1梅澤無線
ピンヘッダ2×3-1秋月電子
ピンヘッダ1×10-2秋月電子
タクトスイッチ--9秋月電子
片面ガラスエポキシユニバーサル基板95mm×72mm-1秋月電子
丸ビス2mm×10mm-2手持在庫
6角ナット2mm-2手持在庫
スペーサ3mm×20mmマック84手持在庫
6角ナット3mm-4手持在庫

部品を配置して、半田付けしたら出来上がりです。

完成写真

ちょっと解説

このアプリケーションは、一枚の基板の上にすべての機能を搭載しています。 このため、電池も小型のボタン電池を使用しています。 一般的な3Vリチウム電池CR2032型を使用しています。

このアプリケーションは、BDMコネクタを接続してデバッグを行うため、 BDMピンヘッダを装備しています。 もし、プログラム済みのマイコンチップを挿入するなど オンボードでのデバッグが不要であれば、BDMピンヘッダは不要です。

省電力対応方針

ソフトウェアは、無半音キーボードで 使用したプログラムがそのまま使えます。 しかし、先のプログラムは、評価ボードから電源が供給されているため、 電源の消耗について全く考えられていません。

ところが、このアプリケーションの場合には、 ボタン電池を使用するため、使用しないときには電源を落とすなどの 電源制御をしないと、電池がすぐに干上がってしまいます。

ここでは、どのようにすれば電池の消耗を抑えられるかを考え、 低消費電力にするための方針を決めます。

頻繁に周波数の変更をしない

前回の課題でも書きましたが、 頻繁にPWMの周期を書き換えているためにビブラートのような音が 聞こえていました。 頻繁に周波数を書き換えるということは、 それだけCPUも頻繁に処理を行っているという事を意味しています。

周波数を変更する頻度は、 人間がキーを押したときに音が遅れて聞こえない程度でかまいません。 それ以上の頻繁な更新は、電池の無駄遣いです。

そこで、キーの検出と周波数の変更を行うための周期を リアルタイム割り込み(Real-Time Interrupt: RTI)機能を 使って32ミリ秒単位で行わせました。

遊ばないときには自動的に休眠する機能

おもちゃでの遊びにあきたら、 このキーボードはほったらかしにされてしまいます。 そんな時でも、自動的に休眠状態に入ってくれたら電池が長持ちします。

必要な機能は、 「ある時間ほったらかしにされているのに気がついたら、 可能な限り消費電流を絞る。」事です。

ほったらかし検知

ほったらかしにされているのを検知するためには、 最後にキーが押されなくなってから現在までの経過時間を知る必要があります。 検知すべき時間は、5分とします。

この時間もリアルタイム割り込み(Real-Time Interrupt: RTI)を 使って検出します。 リアルタイム割り込みは、キーの検出でも使うため、周期は32ミリ秒です。 ここから5分間(300秒間)を検出するには、リアルタイム割り込みの 回数を約9000回数えれば良い計算になります。

クロックをとめる

マイコンは、クロックに従って、プログラムの処理を行います。 そのため、クロックが動いていれば、マイコンは電力を消費します。 低消費電力にするためには、ぜひともクロックを止める必要があります。

不要なときにマイコンのクロックを止めるための命令が、 WAITSTOPです。

WAIT命令は、CPUのクロックを停止させますが、 周辺デバイスのクロックは停止させません。 このため、PWMは動作を続けることができます。 PWMで音を出す必要があるときには、この命令を使ってCPUを止めます。

STOP命令は、CPUと周辺デバイスのクロックを停止させます。 リアルタイム割り込みやキー入力を待つ時に音を出す必要が無いのであれば、 この命令を使ったほうが低消費電力になります。

STOP命令でリアルタイム割り込みは止まるか?

リアルタイム割り込みは、 内部クロックを数えて割り込みを発生させる機能です。 STOP命令を実行したらクロックも止まってしまうので、 STOP状態からリアルタイム割り込みで起き上がることはできないと 思われるかもしれません。

実は、リアルタイム割り込みでは、 CPUを動作させるクロックとは別のクロックが使用されているため、 STOP状態から起き上がることができる仕組みになっています。

リアルタイム割り込みが動作するように設定しておけば、 STOP状態でもリアルタイム割り込みにより起き上がることができます。 ただし、リアルタイム割り込み用のクロックが動き続けますので、 約300nA(ナノアンペア)の電流を消費します。

どのSTOPモードを使いますか?

HCS08は、レジスタの設定によって三種類のSTOP状態、 STOP1, STOP2, STOP3 を使い分けることができます。

"STOP1"と"STOP2"は、 起き上がった後はリセットベクタをフェッチしてリセットと同様の シーケンスを実行します。 今回のアプリケーションの場合、 STOPから起き上がった後はSTOP命令の直後の命令を実行したいので、 "STOP3"しか選択肢がありません。

"STOP3"モードでは、 約750nA(ナノアンペア)の電流を消費しますので、 先のリアルタイム割り込みと合わせて 約1.05µA(マイクロアンペア)の電流消費が見込まれます。

キーを押されたら起動する機能

休眠状態のマイコンは、そのままではキーボードとしての役に立ちません。 そこで、いずれかのキーを押すと起動するように考えます。

通常、このような用途には、キーボード割り込み(KeyBoard Interrupt: KBI)を 使うのが常套手段です。 しかし、このアプリケーションの場合、キーの接続された端子のすべてに キーボード割り込みを割り当てられる訳ではないので、 キーボード割り込みは使えません。

そこで、休眠中でもリアルタイム割り込みの周期でキーの検出を行い、 キー入力の有無を検査します。

起動と休眠を知らせる

このアプリケーションは、圧電スピーカしか出力デバイスを接続していません。 このため、LEDを点灯させるなどして、マイコンが、 休眠状態なのか、動作状態なのかを知らせる事ができません。

そこで、起動時と休眠時には、圧電スピーカから音を出して、 マイコンの状態が切り替わったことを知らせるように工夫します。

圧電スピーカから決められた長さの音を出すためには、 周期的なタイマで長さを測定する事が必要です。 このアプリケーションでは、この音の長さもリアルタイマ割り込みを 利用しています。

不要なモジュールを切る

MC9S08QG8は、汎用マイコンなので、色々な機能が搭載されています。 そのため、このアプリケーションでは使わない機能もいくつかあります。

こういったモジュールが動作していると、消費電流に影響を及ぼします。 そのため、不要なモジュールを切ることも必要です。

電流を消費する不要なモジュールの候補として、LVDが挙げられます。 LVDは、電源電圧が下がって、MCUの正常な動作が難しくなったときに リセット状態を保持する機能です。 ただ、LVDが動作していると約70µA(マイクロアンペア)もの 電流を消費してしまうので、 電池の消耗が気になる場合には、無効にしておいて、 あとは運に任せるという判断もできます。

省電力方針のまとめ

省電力にするための方針を見ると、 全体の動きがリアルタイム割り込みを中心にして構成されている事が わかります。 つまり、リアルタイム割り込みが発生するたびに何らかの処理を行い、 再びリアルタイム割り込みを待つ構成になっているのです。

このタイマ割り込みによって全体の時間を制御する方法は、 姉妹サイト「マイコンクラブ勉強会資料」の 005 待ち時間を稼ぐ方法にも 解説がありますので、参照してください。

プロジェクト作成

最初にCodeWarriorでベースになる新規プロジェクトを以下の手順で作成します。 表示されるダイアログなどの詳細は、 無半音キーボードを 参照してください。 ここでは、V5.1を使って説明します。

CodeWarriorの起動
スタートメニューからCodeWarriorを起動します。
"Startup"ダイアログの呼び出し
"Startup"ダイアログが出てこない場合には、 メニューから"File → Startup Dialog..."を選びます。
"HC(S)08 New Project"ダイアログを開く
"Create New Project"(新しいプロジェクトを作る)をクリックします。
"Device and Connection"(デバイスと接続方法)の設定
デバイスHCS08 → HCS08QG Family → MC9S08QG8
デフォルトの接続方法P&E Multilink/Cyclone Pro

「次へ」ボタンをクリックします。

"Project Parameters"(プロジェクトのパラメータ)の設定
使用する言語"C"
Project Name(プロジェクト名)Organ2.mcp
Location(プロジェクトを作成するディレクトリ) C:\Projects\CW\Organ22

「次へ」ボタンをクリックします。

"Add Additional Files"(追加するファイル)の設定

追加するファイルは無いので、そのまま「次へ」ボタンをクリックします。

"Processor Expert"(プロセッサエキスパート)の設定

"Processor Expert"を選んで「次へ」ボタンをクリックします。

"C/C++ Options"の設定
startup codeANSI startup code
memory modelSmall
floating point formatNone

「次へ」ボタンをクリックします。

"PC-Lint"の設定
"No"をチェックして「完了」ボタンをクリックします。
パッケージを選ぶ
"Select CPUs"(CPUを選ぶ)ダイアログで "MC9S08QG8_16"だけを選択して「OK」ボタンをクリックします。
使用する設定を選ぶ
"Select Configurations"(設定を選べ)ダイアログで "Debug"だけを選択して「OK」をクリックします。

以上で、プロセッサエキスパートを使用する 新規プロジェクトができました。

新規プロジェクト完成

リソースを設定する

ここから、 プロセッサエキスパートのプロパティを設定していきます。 設定項目は、無半音キーボードと ほぼ同じです。 ここでは、設定箇所だけ示しますので、詳細については、 三値デジタル太陽計を参照ください。

CPUの設定

このアプリケーションでは、WAITとSTOPを使用します。 初期状態のProcessorExpertでは、 これらの命令は使用不可能になっていますので、 使用できるように属性を変更します。

ビーンの属性を設定する

まず、プロジェクトパネルの"Cpu"をクリックして、 "Bean Inspector"パネルに"Cpu"ビーンを呼び出します。 次に以下のように属性を変更します。

項目名設定する値
Clock settings (クロックの設定)
Low-power modes settings (低電力モードの設定)
STOP instruction enabled (STOP命令を有効にする) yes (有効)

最終的に属性は以下のようになります。

Cpuビーンの設定終了

メソッドの定義

WAITとSTOPは、メソッドから呼び出されます。 以下のメソッドを有効にします。

Cpuで有効にするメソッド
メソッド有効・無効
SetWaitMode(void)有効
SetStopMode(void)有効

最終的にメソッドは以下のようになります。

Cpuのメソッドの設定終了

キー入力の設定

キーは、PORTBとPORTAにまたがって配置されています。 一つ一つのキーをBitIOビーンで定義することもできますが、 ここでは、PORTBをByteIOビーンで定義し、 PTA1をBitIOビーンで定義します。

PORTB入力の設定

PORTBには、ByteIOビーンを割り当てます。 "Bean Selector"から "CPU Internal Peripherals → Port I/O → ByteIO"を ダブルクリックで呼び出したら、 以下の項目を設定します。

PORTB入力の設定
項目名設定する値
Port(ポート) PTB
Pull Resitor(プル抵抗) pull up(プルアップ)
Direction(方向) Input(入力)

最終的に属性は以下のようになります。

PORTBの設定終了

PTA1入力の設定

PTA1には、BitIOビーンを割り当てます。 "Bean Selector"から "CPU Internal Peripherals → Port I/O → BitIO"を ダブルクリックで呼び出したら、 以下の項目を設定します。

PTA1入力の設定
項目名設定する値
Pin for I/O(入出力ピン) PTA1_KBIP1_ADP1_ACMPMINUS
Pull Resitor(プル抵抗) pull up(プルアップ)
Direction(方向) Input(入力)

最終的に属性は以下のようになります。

PTA1の設定終了

PPG出力の設定

このアプリケーションでは、 タイマチャネル0が接続されたPTA0から音を出力します。 音を出すときに使用するのがPPGという機能です。

ビーンの属性を設定する

まず、"Bean Selector"から "CPU Internal Peripherals → Timer → PPG"を ダブルクリックで呼び出します。

次に以下の項目について設定しますが、 一部、デフォルトの状態では表示されない項目があります。 これらの項目を表示させるために、"Bean Inspector"パネルの下のほうにある "EXPERT"ボタンをクリックしてから設定を始めます。

PPG出力の設定
項目名設定する値
Output pin (出力端子) PTA0_KBIP0_TPMCH0_ADP0_ACMPPLUS
TPM clock source in the high speed mode (高速モードでTPMが使用するクロック源) TPMBusClk
Period (周期) 500Hz

Runtime setting type(実行時設定方式) from time interval(範囲内の時間を設定する)
Init. value (最初の値) 500Hz
Low limit (最小周期) 1200Hz
High limit (最大周期) 400Hz
Min. resolution (最小分解能) 0.1%
Starting pulse width (最初のパルス幅) 1ms

ここで最初のパルス幅として設定した1ミリ秒は、 最初の周期500Hz(2ミリ秒)のちょうど半分に相当する値です。

周期の部分は、 &quote;...&quote;ボタンを クリックしてダイアログを出して詳細な設定を行います。

PPGの周期の設定

最終的に属性は以下のようになります。

PPGの設定終了

以上の設定で、プログラムの実行中にPPGの周波数を 400Hzから1200Hzの範囲で変更することができるようになります。

最小周期と最大周期

ここでは、最小周期に1200Hzを最大周期に400Hzを設定しました。 これを見て、 「最小が1200で、最大が400?逆じゃないの?」と思われるかも知れません。 ここで設定すべき値は、「最小・最大周期」であって「最小・最大周波数」では ありません。 このため、最小周期に最大周波数を最大周期に最小周波数を設定するという 一見おかしなことになってしまっています。

英語の表記の"Low limit"(本来の意味は下限)と "High limit"(本来の意味は上限)にも 周期の下限と上限を設定するとは明記されていないので、 混乱する一因になっています。

メソッドの定義

次にアプリケーションに必要なメソッド有効にし、 不要なメソッドを無効にします。

以下の表を参考にメソッドを有効あるいは無効にしてください。

PPGで有効あるいは無効にするメソッド
メソッド有効・無効
Enable(void)有効
Disable(void)有効
SetPeriodTicks16(word Ticks)(void)無効
SetPeriodTicks32(dword Ticks)(void)無効
SetPeriodUS(word Time)(void)無効
SetPeriodMS(word Time)(void)無効
SetPeriodSec(word Time)(void)無効
SetPeriodReal(float Time)(void)無効
SetFreqkHz(word Freq)(void)無効
SetFreqMHz(word Freq)(void)無効
SetRatio16(word Ratio)(void)無効
SetDutyUS(word Time)(void)無効
SetDutyMS(word Time)(void)無効

最終的にメソッドは以下のようになります。

PPGのメソッドの設定終了

スタック領域を広げる

PPGの周波数設定メソッドは結構複雑な計算をします。 そのため、スタックの領域を広げなくてはなりませんでした。

PPGに周波数を与えると、そこからPWM周期のクロック数を計算して PWMの周期を決定してくれます。 この時に1秒のクロック数8000000を周波数、 例えば、500で割る演算を行います。 すると、割られる数のビット数が多いため、 "Processor Expert"はこの計算を行うために、 64ビットの数値演算を行おうとします。 64ビットの数値演算は、純粋な8ビットマイコンであるHC08にとっては 重労働なので、スタック領域をたっぷり使ってしまい、 デフォルトで用意されたスタック領域では足りなくなってしまうというわけです。

スタック領域を広げるためには、 "Cpu"ビーンの"Build Options"タブの設定を変更します。

項目名設定する値
Generate PRM fileyes
Stack specificationsize
Stack size96

デフォルトでは、64バイトであったところを96バイトに拡張しました。

スタックサイズの拡張

この問題は、マイコンにプログラムを書き込んでしばらくすると 暴走してしまうという現象になって現れます。 しかも再現性がかなり悪いのです。

リアルタイム割り込みの設定

このアプリケーションでは、 リアルタイム割り込みで駆動されるステートマシンを使います。

ビーンの属性を設定する

まず、"Bean Selector"から "CPU Internal Peripherals → Timer → TimerInt"を ダブルクリックで呼び出します。

次に"Bean Inspector"パネルで以下の項目について設定します。

リアルタイム割り込みの設定
項目名設定する値
Timer (使用するタイマ) RTIFree (フリーランニングリアルタイム割り込み)
Interrupt Period (割り込み周期) 32ms

最終的に属性は以下のようになります。

RTIのビーンの設定終了

コードの生成

次は、Processor Expertにソースコードを生成させます。

プロジェクト・パネルの Makeボタンを クリックするとコードが生成されます。

Makeボタン

プログラミング

ベースになるプロジェクトができましたので、 ここからいよいよ、プログラミングに入ります。

状態遷移を考える

省電力対応にするための動きを状態遷移図にすると、 以下のようになります。

全部で五つの状態があり、 それぞれの状態間を遷移しながら処理を行います。 リセット直後は、PROLOGUE状態から処理が始まります。

PROLOGUEは、起動音を出している状態です。 起動音を出し終わるまでこの状態を維持し、 IDLE状態に遷移します。

IDLE状態は、キー入力を待っている状態です。 もし、この状態が5分以上継続したら、 EPILOGUEに遷移します。 もし、5分以内にキー入力が検出されたら、 RUN状態に遷移します。

RUN状態は、押されたキーに対応する音を出している状態です。 キー入力の状態が変化するまでこの状態を維持し、 IDLE状態に遷移します。

EPILOGUEは、休眠音を出している状態です。 休眠音を出し終わるまでこの状態を維持し、 STOP状態に遷移します。

STOPは、電源を遮断している状態です。 いずれかのキー入力が検出されるまでこの状態を維持し、 PROLOGUE状態に遷移します。

これらの状態間を次々と遷移していくステートマシンを書けば、 プログラムは完成します。

リアルタイム割り込みで状態遷移を実装するには

実は、状態遷移図から割り込み駆動型のステートマシンを構成するのは 結構大変です。 状態遷移図は、処理の流れを示しますが、 割り込み駆動型は、その流れを分断して記述しなくてはなりません。

そこで、ある状態での処理を二つのパートに分けて記述してみます。

void XXX_init(void)
この関数は、現在の状態とは異なる状態に遷移した時に呼び出されます。 この関数の中では、新たな状態で必要になる変数の初期化を行います。
void XXX_proc(void)
この関数は、割り込みが発生したときに呼び出されます。 割り込み発生時に実行すべき処理はすべてこの中に記述します。

これら二つの関数を使い分けることで割り込みの処理を簡単にします。

メインループの実装

このプログラムは、無限ループの処理中に割り込みを受け入れることで 割り込み駆動処理を行います。

先のコード生成の結果、"Organ2.c"というファイルが生成されています。 プロジェクト・パネルに"Organ2.c"というファイルが見えます。

プロジェクトパネルのOrgan2.cファイル

このファイルには、 マイコンがリセットされた後で実行される "main()"関数が記述されています。 ここをダブルクリックすると、 "Organ2.c"ファイルを編集するテキストエディタが開きます。 コメントの下に以下のメインループを記述します。

  /* Write your code here */
  /* For example: for(;;) { } */
  prologue_init();          // (A) Specify an initial state
  for (;;) {                // (B) Inifnite loop to wait an interrupt.
    switch (state_code) {   // (C) Recognize current state.
      case PROLOGUE_STATE:
      case RUN_STATE:
      case EPILOGUE_STATE:
        Cpu_SetWaitMode();  // (D) Enter to WAIT mode.
        break;
      case IDLE_STATE:
      case STOP_STATE:
        Cpu_SetStopMode();  // (E) Enter to STOP mode.
        break;
      default:
        prologue_init();    // (F) Starts from PROLOGUE
    }
  }

まず、(A)でリセット後の状態を決定します。 状態決定の方法は簡単です。 "prologue_init(void)"関数を呼び出すとPROLOGUE状態に 設定されます。

続く(B)の"for()"ループでは、割り込みを待ちます。 このループの中で割り込みが発生したら、割り込みハンドラに飛びます。 割り込みハンドラは、"Event.c"ファイルに記述されています。

(C)の"switch()"文で"state_code"に核のされている現在の状態コードにより 割り込みを待つ方法を"WAIT"と"STOP"から選択します。 五つの状態のうち、 PROLOGUERUNEPILOGUEの時は、 (D)の"WAIT"命令で割り込みを待ち、 IDLESTOPの場合は、 (E)の"STOP"命令で割り込みを待ちます。

通常、"state_code"は、上の五つの値しかとりません。 しかし、ノイズなどにより不正な状態コードになってしまう事も 想定できます。 このような場合に備えて、不正な状態コードを検出したら、 (F)で強制的にPROLOGUE状態に遷移させます。 この行は、プログラムが正常に処理されていれば実行されることはありません。

上記"main(void)"関数では、いくつか未定義の名前が使用されているため、 このままでは、コンパイル時に未定義エラーが出てしまいます。 そこで、"main()"関数宣言の直前に以下の定義文を挿入します。

#include "IO_Map.h"

/*
    State codes of the state machine.
*/
const  byte   PROLOGUE_STATE = 0;
const  byte   IDLE_STATE     = 1;
const  byte   RUN_STATE      = 2;
const  byte   EPILOGUE_STATE = 3;
const  byte   STOP_STATE     = 4;
static byte   state_code     = PROLOGUE_STATE;

/*
    Pre-defined function.
*/
void prologue_init(void);

/*
    main(void) : 
      Main function of the application.
*/
void main(void)

前半は、状態コードとその値に関する部分で、 後半は、"prologue_init(void)"関数の宣言部分です。 これで、コンパイルエラーは発生しなくなりましたが、 "prologue_init(void)"関数は実体が無いのでリンクエラーが発生します。

割り込みハンドラの実装

次は、割り込みハンドラを実装します。 割り込みハンドラは、 プロジェクト・パネルから見える"Event.c"というファイルにあります。

プロジェクトパネルのEvent.cファイル

このファイルには、 リアルタイム割り込みが発生するたびに呼び出される "TI1_OnInterrupt(void)"関数が記述されています。 ファイルをダブルクリックすると、 "Event.c"ファイルを編集するテキストエディタが開きます。 "TI1_OnInterrupt(void)"関数を以下のように記述します。

void TI1_OnInterrupt(void)
{
  /* Write your code here ... */
  extern void rti_handler(void);
  
  rti_handler();      // Call handler routine for RTI.
}

この関数の中に、 割り込み発生時のすべての処理を記述することもできるのですが、 ここでは、"rti_handler(void)"関数を呼び出すだけとしました。 "rti_handler(void)"関数は、"Organ2.c"ファイルの"Pre-defined function"の コメントの所に以下のように記述します。

/*
    Pre-defined function.
*/
void prologue_init(void);
void prologue_proc(void);
void idle_proc(void);
void run_proc(void);
void epilogue_proc(void);
void stop_proc(void);

/*
    RTI interrupt handler.
*/
void rti_handler(void) {
  switch (state_code) {
    case PROLOGUE_STATE: prologue_proc(); break;
    case IDLE_STATE:     idle_proc();     break;
    case RUN_STATE:      run_proc();      break;
    case EPILOGUE_STATE: epilogue_proc(); break;
    case STOP_STATE:     stop_proc();     break;
    default:             prologue_init();
  }
}

リアルタイム割り込みが発生したら、 現在の状態により選択した処理を実行します。 "default:"節で"prologue_init(void)"関数を呼び出しているのは、 不正な状態コードが検出された場合に備えた記述です。

状態別処理ルーチンの実装

枠組みができたので、あとは細かい実装をしていきます。

PROLOGUE状態とEPILOGUE状態

まずは、PROLOGUE状態とEPILOGUE状態を 考えます。 これらの状態には、以下の共通点があります。

  • ある楽譜を演奏する
  • 楽譜の終わりまで達したら別の状態に遷移する

そこで、楽譜の演奏をする共通部分をくくりだして、 同時に実装します。

/*
    PROLOGUE state procedures
*/
const byte prologue_score[] = {
  12,  T_DO+O_3,
  12,  T_MI+O_3,
  12,  T_SO+O_3,
  48,  T_DO+O_4,
  12,  0,
  0
};

void prologue_init(void) {
  state_code = PROLOGUE_STATE;  // (A) Specify state.
  score_init(prologue_score);   // (B) Provide a score.
}

void prologue_proc(void) {
  if (play_by_score()) {        // (C) Play and check score.
    idle_init();                // (D) Transition to IDLE.
  }
}
/*
    EPILOGUE state procedures
*/
const byte epilogue_score[] = {
  12,  T_DO+O_4,
  12,  T_SO+O_3,
  12,  T_MI+O_3,
  48,  T_DO+O_3,
  12,  0,
  0
};

void epilogue_init(void) {
  state_code = EPILOGUE_STATE;  // (A) Specify state.
  score_init(epilogue_score);   // (B) Provide a score.
}

void epilogue_proc(void) {
  if (play_by_score()) {        // (C) Play and check score.
    stop_init();                // (D) Transition to STOP.
  }
}

上の二組の関数は、良く似ていますが、少し異なっています。

  • 演奏する楽譜が違う。

    これらの状態で演奏する楽譜は、(B)の部分で "score_init(const byte [])"に渡すbyte型の配列で 与えられます。 PROLOGUE状態ではprologue_score[]EPILOGUE状態ではepilogue_score[]を 使っています。

  • 楽譜の演奏が終わった後の遷移先が違う。

    (C)の部分で楽譜の演奏が終わったら、 "play_by_score(void)"がTRUEを返してきます。 この時に(D)の部分で遷移先を指定するのですが、 PROLOGUE状態では"idle_init(void)"関数を呼び出して IDLE状態に遷移し、 EPILOGUE状態では"stop_init(void)"関数を呼び出して STOP状態に遷移します。

"const"は、なぜ必要か?

constという修飾子が出てきました。 本来、この修飾子は、 プログラムの実行中に配列の中身が変更されない事を積極的にコンパイラに 知らせるためにあります。 コンパイラは、この配列が変更されないことが保証されるという事実を利用して、 最適化を行うことができます。

このプログラムでも、 配列が変更されないことをコンパイラに通知するという目的は同じです。 ただし、その効果として期待されるのは、最適化されたプログラムよりも 配列がROMに配置されることです。 コンパイラは、変更されない配列はROMに配置してしまいます。

もし、constがなかったら、 コンパイラは、配列をRAMに配置して、その初期値データをROMに置くという もったいないプログラムを生成してしまいます。 このような、プログラムでは決して変更されることの無いデータは、 積極的にconstを付けておきましょう。

それぞれの関数に関する詳細は、以下に続きます。

楽譜を演奏するプログラム

楽譜の演奏を行うプログラムは、 演奏する楽譜を指定する関数"score_init(const byte * const)"と 一定時間ごとに指定された楽譜をなぞる 関数"play_by_score(void)"からなっています。

/*
    SCORE manager.
*/
const byte    *score;           // Score to be played.
byte          sono_index;       // Pointer for a score.
byte          sono_count;       // Sound length counter.

void score_init(const byte * const score_p) {
  score = score_p;              // (A) Store a score to be played.
  sono_index = 0;               // (B) Point top of the score.
  sono_count = 1;               // (C) Set sound length counter.
}

bool play_by_score(void) {  
  if (--sono_count <= 0) {      // (D) sound length exceeded ?
    sono_count = score[sono_index];   // (E) Get sound length.
    if (sono_count == 0) {      // (F) Zero-length means score ends.
      return TRUE;              // (G) Return to indicate score ends.
    }
    sono_index++;               // (H) Adjust to point sound key.
    play_by_key(score[sono_index++]); // (J) Play with a sound key.
  } 
  return FALSE;                 // (K) Return FALSE to indicate not completed
}

この部分では、三つの大域変数が使用されています。

const byte *score

"score_init(const byte * const)"関数で 与えられた楽譜を保存する変数です。

"score"という変数そのものは、プログラムの実行中に更新されますが、 "score"変数が指し示す楽譜そのものは変更されません。 そのため、"const byte *"という修飾子がついています。

byte sono_index

楽譜の中の現在の演奏箇所を示します。

楽譜は、二組の"byte"で一つの音を表現しており、 一つ目が音の長さを二つ目が音程をあらわしています。 "sono_index"は、通常は音の長さの部分を指しているので、 偶数になります。

byte sono_count

現在発している音の残り時間を示します。

この値が"0"になったら、次の音に移ります。

二つの関数は、以下のように動作しています。

"score_init(const byte * const)"関数

ここでは、受け取った楽譜をもとに 三つの大域変数の初期化を行います。

(A)では、与えられた楽譜を"score"に格納します。 また、(B)で楽譜上の演奏位置"sono_index"を楽譜の最初に、 (C)で現在出している音の残り時間"sono_count"を"1"としています。 残り時間を"1"とすることで、 次に"play_by_score(void)"関数を実行したときに 楽譜の最初から演奏が始まるという仕掛けです。

"play_by_score(void)"関数

ここでは、 一単位時間だけ楽譜を進めます。 一単位時間は、この関数が呼び出される時間のことで、 このプログラムの場合は、リアルタイム割り込みの周期に相当します。

(D)で現在出している音の残り時間が"0"になったかどうかを調べます。 残り時間がまだあるのであれば、 (K)から呼び出し元のプログラムに戻ります。 (E)では、楽譜から次の音の長さを取り出し、 続いて(F)で音の長さが"0"であるかどうかを調べます。 この楽譜では、長さが"0"の音で楽譜の最後を示すので、 (G)から呼び出し元のプログラムに戻ります。 音の長さが"0"ではない時には、(H)で楽譜を先に進め、 (J)で楽譜から取り出した音を "void play_by_key(const byte)"関数を使って出します。

このプログラムでは、呼び出し元に戻る方法が(G)と(K)の二種類あります。 これらには、(G)はTRUEを返し、 (K)はFALSEを返しているという違いがあります。 ここで返す値によって、 楽譜の演奏が終了したかを呼び出し元に教えることができるので、 "prologue_proc(void)"関数と"epilogue_proc(void)"関数での 条件分岐に使えるというわけです。

音を出すプログラム

楽譜を演奏するプログラムでは、 音を出すために"void play_by_key(const byte)"関数を 使用しています。 また、押されたキーに対応した音を出力するのもこの関数の仕事です。

この関数は、音の高さを8ビットのコードとして受け取り、 PPGビーン周波数として渡す役割をします。

音の高さコード
octave → 0 1 2 3 4 5 6 7
↓ tone
0 C 0 12 24 36 48 60 72 84
休符 C1 C c c1 c2 c3 c4
1 C# 1 13 25 37 49 61 73 85
休符 C#1 C# c# c#1 c#2 c#3 c#4
2 D 2 14 26 38 50 62 74 86
休符 D1 D d d1 d2 d3 d4
3 D# 3 15 27 39 51 63 75 87
休符 D#1 D# d# d#1 d#2 d#3 d#4
4 E 4 16 28 40 52 64 76 88
休符 E1 E e e1 e2 e3 e4
5 F 5 17 29 41 53 65 77 89
休符 F1 F f f1 f2 f3 f4
6 F# 6 18 30 42 54 66 78 90
休符 F#1 F# f# f#1 f#2 f#3 f#4
7 G 7 19 31 43 55 67 79 91
休符 G1 G g g1 g2 g3 g4
8 G# 8 20 32 44 56 68 80 92
休符 G#1 G# g# g#1 g#2 g#3 g#4
9 A 9 21 33 45 57 69 81 93
休符 A1 A a a1 a2 a3 a4
10 A# 10 22 34 46 58 70 82 94
休符 A#1 a# a# a#1 a#2 a#3 a#4
11 B 11 23 35 47 59 71 83 95
休符 B1 B b b1 b2 b3 b4

音の高さコードは、平均律に従って、 1増えるごとに半音上がる構成になっています。 また、コード45が440Hzになるように調整されています。

/*
    Play a sound with a KEY
*/
const word freq_by_tone[] = {
  4186, // DO
  4435, // DO#
  4698, // RE
  4978, // RE#
  5274, // MI
  5588, // FA
  5920, // FA#
  6272, // SO
  6645, // SO#
  7040, // RA
  7459, // RA#
  7902, // SI
  8372  // DO
};

byte          lastKey = 0;      // Last key code.

void play_by_key(const byte key) {
  byte octave;                  // Octave part of the key code.
  byte tone;                    // Tone part of the key code.
  word freq;                    // Frequency according to the key code.
  
  if (key == lastKey) return;   // (A) return if key not changed.
  octave = key / 12;            // (B) Calculate octave code.
  tone = (byte)(key % 12);      // (C) Calculate tone code.
  (void)PPG1_Disable();         // (D) Stop PPG
  if (octave > 0) {             // (E) Octave code grater than 0 ?
    freq = freq_by_tone[tone] >> (byte)(7 - octave);
                                // (F) Calculate frequency.
    (void)PPG1_SetFreqHz(freq); // (G) Specify PPG's frequency.
    (void)PPG1_Enable();        // (H) Start PPG
  }
  lastKey = key;                // (J) Update last key code.
}

このプログラムでは、lastKeyという大域変数を使って、 現在出している音を記憶しています。 (A)では、要求された音コードkeyが、 現在出している音のコードlastKeuと等しいかどうかが 最初に調べられ、同じ音コードが要求されていた場合には、 何もせずに関数の呼び出し元に戻ります。

(B)と(C)では、 音コードをoctavetoneの 二つの部分に分ける処理を行っています。 このプログラムでは、これら二つの値から周波数を計算して PPGモジュールに渡しています。

以下の処理で、PPGに設定する周波数を変更するため、 (D)では、あらかじめPPGの出力を停止しておきます。

octaveが0になる音コードは、 音を停止するコードとして使用しています。 これを判定するのが、(E)のif文です。 音の周波数を決めている(F)から(H)までの処理は、 octaveが0よりも大きい時に限り処理されるため、 octaveが0の場合には、(D)で出力が停止したままになります。

(F)では、周波数を計算しています。 freq_by_tone[]は、 toneの値から上4点の周波数を割り出す配列です。 この配列から返された上4点の周波数を(7 - octave)の 数だけ右シフトすると与えられた音コードに相当する周波数が得られるという 仕組みです。

計算された周波数は、(G)でPPGに渡され、 (H)でPPGの出力が始まります。

最後の(J)では、現在出力している音のコードを更新しています。

IDLE状態

次は、IDLE状態を考えます。

/*
    IDLE state procedures
*/
word  idle_count;               // IDLE timer count.

void idle_init(void) {
  state_code = IDLE_STATE;      // (A) Specify state.
  play_by_key(0);               // (B) Stop sound.
  idle_count = 0;               // (C) Clear IDLE timer count.
}

void idle_proc(void) {
  if (key_detected()) {         // (D) A key pushed ?
    run_init();                 // (E) Transition to RUN.
  } else if (idle_count > IDLE_TIME) {
                                // (F) IDLE timer expired ?
    epilogue_init();            // (G) Transition to EPILOGUE.
  } else {
    idle_count++;               // (H) Increment IDLE timer.
  }
}

IDLE状態に遷移したら、 まず、"void idle_init(void)"関数で初期状態を設定します。 最初に(A)で状態変数を設定します。 次に(B)で"play_by_key(const byte)"関数を使って、 PPGが出している音を止めます。 そして、(C)でidle_countという変数をクリアします。 この変数は、IDLE状態がどれだけ長く続いているかを 示すタイマとして使用しています。

"void idle_proc(void)"関数では、 IDLE状態での仕事が記述されています。

(D)では、押しボタンが押されたかどうかを調べています。 押しボタンが押されていた場合には、 PPGから音を出すために、 (E)でRUN状態に遷移します。

(F)では、ほったらかし状態を判定します。 IDLE状態がIDLE_TIMEを超えて 長く続いた場合には、 「ほったらかし」にされたと判断します。 その場合には、(G)でEPILOGUE状態に遷移し、 最終的にSTOP状態に遷移します。 IDLE_TIMEは、以下のように宣言されています。

/*
    Time descriptions.
*/
const  word   IDLE_TIME      = 30*30;  // 30sec

IDLE_TIMEの単位は、 RTIの周期と等しく32msec、約1/30秒です。 そのため、「30*30」を計算して約30秒の設定にしています。

(H)では、タイマカウンタを進めてほったらかし時間を計ります。

キーの状態を調べる

"bool key_detected(void)"関数は、 いずれかのキーが押されたかどうかをbool型の 値で示します。

/*
    Key detection routine.
    Returns TRUE if any key pressed.
*/
bool key_detected(void) {
  if (Byte1_GetVal() != 0b11111111) return TRUE;
  if (!Bit1_GetVal()) return TRUE;
  return FALSE;
}

まず、"Byte1_GetVal()"関数でPORTBの8本の端子につながっている キーの状態を調べます。 キーが押されていなければ、この関数は"0b11111111"を返します。 そうでない時には、「PORTBのいずれかのキーが押された」と判断し、 関数の呼び出し元にTRUEを返します。

次に"Bit1_GetVal()"関数でPTA1端子につながっているキーの状態を調べます。 キーが押されていない場合、この関数は"TRUE"を返します。 そうでない場合には、「PTA1のキーが押された」と判断し、 関数の呼び出し元にTRUEを返します。

以上、二組のポートを調べてキーが押されていないと判断される場合には、 この関数は、"FALSE"を返します。

RUN状態

RUN状態を考えます。

/*
    RUN state procedures
*/
byte  pressed_key;              // Pressed key code.

void run_init(void) {
  state_code = RUN_STATE;       // (A) Specify state.
  pressed_key = get_pressed_key();
                                // (B) Get pressed key code.
  play_by_key(pressed_key);     // (C) Start sound.
}

void run_proc(void) {
  if (pressed_key != get_pressed_key()) {
                                // (D) Key code changed ?
    idle_init();                // (E) Transition to IDLE.
  }
}

RUN状態に遷移したら、"void run_init(void)"関数で 初期状態を設定します。 最初に(A)で状態変数を設定します。 次に(B)で押されているキーのコードを "byte get_pressed_key(void)"関数から取得し、 pressed_key変数に格納します。 最後に(C)で"play_by_key(const byte)"関数を使って、 pressed_keyのコードに相当する音を PPGから出し始めます。

"void run_proc(void)"関数では、 まず(D)でキーが押されている状態に変化が無いかを調べます。 pressed_key変数にはキーコードが格納されていますので、 "byte get_pressed_key(void)"関数が返すキーコードと比較して、 変化がないかどうかを判断します。 そして、変化があった場合には、IDLE状態に遷移し、 キーの状態によって動作を決めます。

押されたキーを調べる

"byte get_pressed_key(void)"関数は、 押されたキーに相当するキーコードを返します。

/*
    Pressed key search routine.
    Returns KEY code which is equal to TONE code.
    or 0 if no key pressed.
*/
byte get_pressed_key(void) {
  if (!Byte1_GetBit(7)) return T_DO+O_3;
  if (!Byte1_GetBit(6)) return T_RE+O_3;
  if (!Byte1_GetBit(5)) return T_MI+O_3;
  if (!Byte1_GetBit(4)) return T_FA+O_3;
  if (!Byte1_GetBit(3)) return T_SO+O_3;
  if (!Byte1_GetBit(2)) return T_RA+O_3;
  if (!Byte1_GetBit(1)) return T_SI+O_3;
  if (!Byte1_GetBit(0)) return T_DO+O_4;
  if (!Bit1_GetVal())   return T_RE+O_4;
  return 0;
}

このプログラムは、 無半音キーボードで作成した キー入力プログラムとそっくりですが、 返す値が異なっています。

無半音キーボードでは、 単純に1から9までのコードを割り当てていたのですが、 このプログラムでは、コードそのものが音階をあらわすように 作成されています。 ただ、コードそのままでは可読性に欠けるので、 以下のような定数宣言を行って可読性を上げています。

/*
    Key code table.
      Key codes are consisting of a SUM of
      Octave code and Tone code
*/
const byte    O_3  = 12*4;    // Octave = 3
const byte    O_4  = 12*5;    // Octave = 4
const byte    T_DO = 0;       // Tone = DO
const byte    T_RE = 2;       // Tone = RE
const byte    T_MI = 4;       // Tone = MI
const byte    T_FA = 5;       // Tone = FA
const byte    T_SO = 7;       // Tone = SO
const byte    T_RA = 9;       // Tone = RA
const byte    T_SI = 11;      // Tone = SI

STOP状態

最後にSTOP状態を考えます。

/*
    STOP state procedures
*/
void stop_init(void) {
  state_code = STOP_STATE;      // (A) Specify state.
}

void stop_proc(void) {
  if (key_detected()) {         // (B) Key pressed ?
    prologue_init();            // (C) Transition to PROLOGUE.
  }
}

STOP状態に遷移したら、 "void stop_init(void)"関数で初期状態を設定します。 ここでは、(A)で状態変数を設定しているのみです。

"void stop_proc(void)"関数では、 (B)でキーが押されているかどうかを調べます。 そして、キーが押されていた場合には、 (C)でPROLOGUE状態に遷移します。

-->

課題

音階を増やしたい

このアプリケーションでは、9個の音階しか出すことができません。 もっと多くの音階を出すためには、もっと多くのスイッチが 必要ですが、MC9S08QG8には、そんなに多くのスイッチをつなぐ端子は 残っていません。 さて、どのようにしたらもっと多くのスイッチをつなぐことができるように なるのでしょうか。

2006-11-19 不完全ながら発行。

Updated: $Date: 2006/11/19 13:57:00 $

Copyright (C) 2006 noritan.org ■