2024年11月23日
かつて東京の地下鉄で聴けた発車ブザーの音を再現するおもちゃを自作しました。ここでは、周波数がわずかに異なる2つの矩形波を出力するプログラムの作成について、説明します。
ひとつ前の記事では、地下鉄の発車ブザー音を出すための回路を製作しました。この回路では、PICマイコンPIC12F1822のI/Oピンを用いて、周波数の近い2つの矩形波を生成すれば、その波形を合成・増幅してスピーカーを鳴らすことができます。
ここからは、基板に搭載するマイコンPIC12F1822のプログラムについて説明します。
ソースコードのファイルは、以下のとおりです。MPLAB X IDEでアセンブラpic-asを使ってビルドするときに使えます。
ビルドするためには、リンカのオプションとして、"-Wl,-presetVec=0h -Wl,-pintrVec=04h"を指定する必要があります。詳しくは、この記事の後ろのほうを参照してください。
このソースコードを使う場合は、以下の点を守ってください。
個人的な趣味で使う場合は、ダウンロードして使っていただいて差し支えありません。改造も可能です。
プログラムは無保証です。作者はプログラムやホームページの掲載内容について、責任を負いません。第三者の知的財産権侵害などについても、保証しません。
業務での使用はお控えください。
プログラムは、統合開発環境MPLAB X IDEを用いて、アセンブラで開発します。アセンブラには、xc8というCコンパイラ(無料モード)をインストールすると使えるpic-asを使います。統合開発環境・アセンブラとも、無料で使えるようです。
MPLAB X IDEの画面
なお、MPLAB X IDEそのものの使い方は、別途次の記事で説明します。
プログラムでは、命令を一定回数実行するたびに、I/Oピンの出力を反転させる方法で、一定の周波数を生成します。この方法を使うためには、プログラム内の命令数を正確に把握する必要があります。
ここでの命令数とは、マイコンのデータシートに記載された機械語(に対応するアセンブラの命令)の数となるので、命令数を正確に把握できるアセンブラで開発を行うと、分かりやすいです。
ちなみに、マイコンにはタイマ機能があり、これを使っても一定周期毎に行う処理を実現できます。ただし、2つの波形を同時並行で正しいタイミングで発生させることは、タイマだけでは難しいです。
プログラムの構成要素は、以下の通りです。
No | 項目 | 説明 |
---|---|---|
1 | マイコンの選択 | 開発する対象のマイコン(PIC12F1822)を指定します。 |
2 | コンフィギュレーションビットの設定 | マイコンのハードウェアに対する設定である「コンフィギュレーションビット」の値を決めます。接続された発振子の種類、各種内蔵機能の利用可否などを設定します。これが正しくないと、マイコンが正常に起動しません。 |
3 | 変数や定数の定義 | 変数のためにメモリ領域を予約したり、定数の値を設定したりします。 |
4 | 初期化処理 | プログラムが起動したらまず、各種内蔵機能の設定を行います。I/OポートをデジタルI/Oの出力ポートとして設定します。 |
5 | 発振開始待ち処理 | 電源の状態が落ち着くまで少し待ちます。マイコンの電源やオーディオアンプが落ち着く前に大きな音を出そうとすると、電源が不安定になって、マイコンが止まったり、ブザーの音質が悪くなったりする可能性があるためです。 |
6 | 発振ループ | 2つの周波数の矩形波を、I/Oピン(RA0, RA1)から出力するループ処理です。この処理を無限ループで続けます。 |
ここからは、ソースコードを最初から最後まで、少しずつ説明します。
ソースコードの冒頭では、使用するマイコン(PIC12F1822)を指定します。
PROCESSOR 12F1822 #include <xc.inc>
1行目でマイコンを指定し、2行目でそのマイコンに関する各種ファイルをインクルードします。
今回の例では、"pic12f1822.inc"というファイルがインクルードされます。このファイルは、作者の環境(Windows 10)では、以下のディレクトリにありました。(ディレクトリの名前はツールのバージョンなどにより変化する可能性があります。)
C:\Program Files\Microchip\xc8\v2.50\pic\include\proc
このファイルの中には、ソースコードで指定できる各種レジスタの名前やビットの名前などが定義されています。
なお、マイコンの指定は、ソースコードだけでなく、MPLAB X IDEのプロジェクトのプロパティとしても別途行う必要があります。
ソースコードでは次に、マイコンのコンフィギュレーションビットを設定します。コンフィギュレーションビットは、マイコンの不揮発メモリに書き込む設定データのことです。
; PIC12F1822 Configuration Bit Settings ; CONFIG1 CONFIG FOSC = XT ; Oscillator Selection (XT Oscillator, Crystal/resonator connected between OSC1 and OSC2 pins) CONFIG WDTE = OFF ; Watchdog Timer Enable (WDT disabled) CONFIG PWRTE = ON ; Power-up Timer Enable (PWRT enabled) CONFIG MCLRE = ON ; MCLR Pin Function Select (MCLR/VPP pin function is MCLR) CONFIG CP = OFF ; Flash Program Memory Code Protection (Program memory code protection is disabled) CONFIG CPD = OFF ; Data Memory Code Protection (Data memory code protection is disabled) CONFIG BOREN = OFF ; Brown-out Reset Enable (Brown-out Reset disabled) CONFIG CLKOUTEN = OFF ; Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin) CONFIG IESO = OFF ; Internal/External Switchover (Internal/External Switchover mode is disabled) CONFIG FCMEN = OFF ; Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled) ; CONFIG2 CONFIG WRT = OFF ; Flash Memory Self-Write Protection (Write protection off) CONFIG PLLEN = ON ; PLL Enable (4x PLL enabled) CONFIG STVREN = OFF ; Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset) CONFIG BORV = LO ; Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.) CONFIG DEBUG = OFF ; In-Circuit Debugger Mode (In-Circuit Debugger disabled, ICSPCLK and ICSPDAT are general purpose I/O pins) CONFIG LVP = ON ; Low-Voltage Programming Enable (Low-voltage programming enabled)
PICマイコンには様々な機能が搭載されており、それぞれの機能を使うか使わないかを選ぶ必要があります。大部分の機能は、マイコンを起動してからプログラムで機能レジスタを操作することで設定できますが、一部の機能はマイコンが起動する以前に、コンフィギュレーションビットにより選んでおく必要があります。
具体的に何をどう設定すべきかは、マイコンのデータシートを読んで決める必要があります。今回は、以下のように設定しました。
No | 項目 | 設定内容 |
---|---|---|
1 | 発振子 | 外部発振子(XT)を使用する |
2 | ウォッチドックタイマ | 不使用 |
3 | パワーアップタイマ | 使用する |
4 | MCLRピンの用途 | MCLRピンとして使用する |
5 | コードプロテクション・ライトプロテクション | 使用しない |
6 | ブラウンアウトディテクト | 不使用 |
7 | クロックの外部出力 | 不使用 |
8 | 発振子の内蔵/外付の切換 | 不使用 |
9 | 発振子のフェールセーフ機能 | 不使用 |
10 | 周波数4倍化PLL | 使用する |
11 | スタックオーバーフローによるリセット | 不使用 |
12 | インサーキットデバッグモード | 不使用 |
13 | 低電圧プログラミングモード | 使用する |
動作を単純にするため、かなりの機能をオフに設定しています。
今回の開発では、マイコンに接続された発振子は4MHzですが、周波数を4倍にするPLL機能をオンとしたため、マイコンは16MHzで動作します。ただし、4クロックあたりで1命令を実行する仕組みなので、1秒あたりの命令実行回数は約400万回となります。
Configuration Bitsを設定する作業は、MPLAB X IDEでは、[Window]→[Target Memory Views]→[Configuration Bits]の順にメニューをたどると、GUIで簡単に行うことができます。
プログラムではさらに、変数と定数の定義を行います。
consta EQU 169 constb EQU 171 ; PSECT udata_bank0 ; PSECT udata_bank1 PSECT udata_shr counta: ; counter of channel A DS 1 countb: ; counter of channel B DS 1 bufa: ; output buffer of PORTA DS 1
前半が定数の定義です。constaを169、constbを171としています。この数値が、2つの矩形波の周波数を決めています。プログラムのメインループをこの回数繰り返すたびに、それぞれの矩形波の波形を反転させることで、適切な周波数で発振できるようにしています。
その下の "PSECT udata_shr"は、「ここから、各メモリバンクで共通してアクセスできるRAMエリア(アドレス0x070からの16バイトの領域)にシンボルを定義します」という意味です。
PIC12F1822では、変数を使えるエリアは以下の3つです。
領域名 | 説明 | |
---|---|---|
1 | udata_bank0 | バンク0のRAM領域(0x020からはじまる80バイト) |
2 | udata_bank1 | バンク1のRAM領域(0x0A0からはじまる32バイト) |
3 | udata_shr | 各バンク共通のRAM領域(0x070からはじまる16バイト) |
なお、バンクを指定したRAM領域にアクセスするには、別途バンク切り替えの処理が必要になります。
変数名は、"counta:"のように、後ろにコロンを付けて書きます。変数名はラベルとして扱われており、変数名には変数のアドレス値が割り当てられるようです。
" DS 1"は、RAMの領域を1バイト確保することを意味しています。ソースコードでは、各変数に1バイトずつ確保しています。
このプログラムの変数は3個なので、汎用のRAMを3バイトしか使いません。パソコンのソフトなどでは考えられないほど少ないです。
変数や定数の定義が終わったら、プログラム本体のアセンブリコードを書きます。
プログラムは、初期化処理から始まります。
PSECT resetVec,class=CODE,delta=2 resetVec: goto main PSECT intrVec,class=CODE,delta=2 intrVec: goto main main: ; Initialize CPU clrf INTCON ; Disable Interruption movlb 0H ; Select Bank 0 movlw 02H movwf bufa movwf PORTA ; Initialize PORTA (A0=clr, A1=set) movlb 2H ; Select Bank 2 movwf LATA ; Latch data of PORTA movlb 3H ; Select Bank 3 clrf ANSELA ; Select Digital I/O movlb 1H ; Select Bank 1 movlw 00111000B movwf TRISA ; Set A0-A2 as output movlb 0H ; Select Bank 0
最初の"PSECT resetVec,class=CODE,delta=2"は、プログラムが始まるアドレスなどを決める部分です。
resetVecは、電源が入ったりリセットされたりしたときに、プログラムの実行が始まるアドレスで、0番地を意味します。
"class=CODE,delta=2"の部分は、マイコンの機種ごとに決まっているようです。
pic12f1822.incの最後のほうに、"psect code,class=CODE,space=SPACE_CODE,delta=2"と書かれた行がありますが、ここで、このマイコンであれば"class"が"CODE"で、"delta"が"2"だと分かります。なお、"space=SPACE"の部分は付ける必要がないようです。
resetVecから起動した場合は、mainラベルまでジャンプする命令を実行します。
"intrVec"は、割り込みが発生したときにジャンプする先のアドレスで、4番地を意味します。今回のプログラムでは、割り込み機能は使いませんが、こちらから実行が始まった場合も、mainラベルに進むようにしています。
mainラベルからは、実際の初期化処理です。
割り込み機能は使わず、Aポート(RA0, RA1, RA2)をデジタルI/Oの出力とし、Aポートの初期状態は、RA1のピンが1(セット)、RA0のピンが0(クリア)となるようにしています。
bufaは、PORTAに出力する値を入れておくための変数です。後で使います。
ちなみに、PORTAで、RA0をクリア、RA1をセットとして開始するのは、起動時にコンデンサC3のマイコンに近いほうの端子の電位がクリア、セットの間の電圧になるようにしておきたいからです。完全にクリアやセットの電位になっていると、矩形波の発振を始めてしばらくの間は、音がおかしくなってしまう可能性があります。
初期化処理が終わったら、少し待ちます。電源が入ってすぐに、スピーカーに大きな電流を流す処理を行うと、マイコンの電源が不安定になったり、オーディオアンプが安定する前のため変な音が出たりする可能性があるからです。
; Initial wait for about 131ms (525312ops) clrf counta clrf countb waitloop1: nop nop nop nop decf counta, F btfss STATUS, STATUS_ZERO_POSITION goto waitloop1 nop decf countb, F btfss STATUS, STATUS_ZERO_POSITION goto waitloop1 nop
waitloop1のラベルから数えて、525312回命令を実行することで、約131ミリ秒待ちます。
nopは、何もしないという意味ですが、時間は命令1回分消費します。
ちなみに、gotoは実行するのに2命令分かかります。btfssとbtfscは、条件が成立しない場合に直後の命令をnopに置き換えて実行するように数えます。
プログラムの最後は、2つの矩形波を出すための発振ループです。
beepstart: movlw consta movwf counta movlw constb movwf countb beeploop1: ; 22 operations time per loop decf counta,F btfss STATUS,STATUS_ZERO_POSITION goto beeploop2 ; A ch (porta bit0) is time to be changed. movlw 01H xorwf bufa,F ; invert A0 movlw consta movwf counta ; set counter value again goto beeploop3 beeploop2: ; Adjust operation counts nop nop nop nop nop beeploop3: decf countb,F btfss STATUS,STATUS_ZERO_POSITION goto beeploop4 ; B ch (porta bit1) is time to be changed. movlw 02H xorwf bufa,F ; invert A1 movlw constb movwf countb ; set counter value again. goto beeploop5 beeploop4: ; Adjust operation counts nop nop nop nop nop beeploop5: movf bufa,W movwf PORTA ; Update PORTA I/O goto beeploop1 END resetVec
RA0のピンはcountaでカウントしてconsta回、RA1のピンはcountbでカウントしながらconstb回ループすると、I/Oピンの状態を反転させる内容となっています。
ループは、条件分岐の成否によらず22命令サイクルで1周となるようにしています。いつもぴったりこの周期なので、矩形波の反転するタイミングが乱れないようになっています。
出力の内容はまずbufaに保存し、ループの最後でPORTAに反映するようにしています。
この結果、169回ループするたびに反転するRA0ピンは約537.9Hz、171回ループするたびに反転するRA1ピンは約531.6Hzで発振することになります。うなりは毎秒約6.3回発生します。なお、基板に搭載するセラミック発振子の周波数に±0.5%の誤差が見込まれることから、矩形波の周波数もその程度の誤差を伴うことになります。
プログラムは無限ループになっています。電源を切るまで発振を続けます。
プログラムの最後の "END resetVec" は、resetVecラベルから始まったアセンブラの命令がここで終わることを意味します。
プログラムをマイコンに書き込める形で完成させるためには、resetVecとintrVecのアドレス(0番地と4番地)をリンカに伝える必要があります。リンカオプションの設定が必要です。
プロジェクトプロパティのpic-as Global Optionsのうち、pic-as Linkerを選択し、Additional Optionsの欄に "-Wl,-presetVec=0h -Wl,-pintrVec=04h"と書き込むと、それぞれ0x000番地と0x004番地であることをリンカに伝えられます。なお、「"」の記号は不要です。
上記のプログラムをビルドしてマイコンに書き込むと、回路もソフトも完成です。
電池をセットして電源を入れると、ブザーっぽい音が出ました。しかし、音が割れている様子であまり望ましい音質ではありません。求める音質に近づけるには、半固定抵抗を調整する必要があります。
マイコンの出力側にあるVR1, VR2は最大値(47kΩくらい)とその半分くらいの組み合わせ、VR3は小さめ(数キロオーム)とすると、以下の音声信号が得られました。
完成したブザーの音声波形
上記の音声は、スピーカーに並列に電極を押し当てて、パソコンで音声信号をキャプチャーして得たものです。
かなりそれっぽく聞こえます。波形からは、周波数が互いに少し異なる矩形波を、異なる振幅で合成した信号であることが分かります。
少しとがった感じの波形になっているのは、オーディオアンプの入力がハイパスフィルタの形になっていることと関係があるかもしれません。
スピーカーは、購入したときに入っていた箱の中に入れたままのほうが、雰囲気のある音になりました。箱から出すと、部屋で聴くには少々うるさい感じになってしまいます。
完成した様子
ちなみに、上記のプログラムと音声は、一度作ったあとに改良した後の状態のものです。
最初に作ったときは、プログラムが少し違っていて、以下のような音が出ていました。
改良前のソフトで記録した実際の音声(出だしの部分)
音の出始めが少し割れたように聞こえます。
これは、初期版ではPORTAの初期出力状態を00Hから始めていた(RA0, RA1とも0(クリア)の状態から波形の出力を開始していた)ので、コンデンサC3のマイコンに近い側の電位が0付近の状態で発振が始まったものの、最初のうちはコンデンサの電位が底打ちしてしまい、歪んだ波形になってしまったからだと考えています。
そこで、PORTAの初期出力状態を02H(RA1はセット、RA0はクリア)に変更しました。
さらに、初期版のプログラムでは最初の待ち時間を約66ミリ秒しかとっていませんでしたが、改良時には約131ミリ秒に増やして、オーディオアンプの状態がもっと落ち着いてから発振を始めるようにしました。
この結果、コンデンサC3のマイコンに近い側の電位が、クリア(0)とセット(1)の間の状態から発振を始められるようになったので、電圧が底打ちする現象は起こりにくくなったと考えられます。
また、一方がセット、一方がクリアの状態で波形の生成を開始すると、合成後の音量が最も小さい状態からの発振開始となるので、音の出だしが目立ちにくくなるという点でも、出だしが歪んだ感じに聞こえにくくなったのかもしれません。
改良後のソフト(この記事の最初に掲載したソースコード)では、出だしの部分は以下のように、あまりつぶれない波形になっています。また、I/Oポートを初期化してから発振開始までの時間を長くした結果、最初のポップノイズから発振音が出始めるまでの時間が長くなっています。
改良後のソフトで記録した実際の音声(出だしの部分)
ソフト次第で音を細かく変えられるところが、マイコンで作る面白さだったりします。なお、コンデンサC3の初期の電位は、バイアスの回路を工夫するなどして、ハードとして解決したほうがよい可能性もあります。
このようにして、地下鉄ブザーは完成しました。スイッチを入れるといつでも、地下鉄の発車ブザーのような音が出るので、駅にいるかのような雰囲気を楽しめます。
改めて、本物の地下鉄のブザーの音(渋谷駅)と聞き比べてみると、本物の音声は作ったおもちゃと比べると、以下の点で少し違っているようです。
本物のほうが、深みのある音がします。今回作ったものは、ハイパスフィルタを通して音声信号を増幅していることもあってか、本来は不要な高い周波数の音が多く混ざっている可能性があります。
本物のほうが、うなる回数が少ないように聞こえます。発振させる周波数を変えることで、調整できるかもしれません。
本物のほうが、うなりそのものがもう少し弱く設定されているようにも聞こえます。
やはり、本物の音は素晴らしいです。今回作ったものを、より本物に近づけるように改良していくと、面白いかもしれません。
次の記事では、統合開発環境MPLAB X IDEで、上記のプログラムを作りビルドするまでの、パソコンの操作方法を紹介します。
次の記事:地下鉄ブザーを作る--MPLAB X IDEによるアセンブラ開発環境の使い方
製作・著作:杉原 俊雄(すぎはら としお)
(c)2024 Toshio Sugihara. All rights reserved.