地下鉄ブザーを作る--マイコンのプログラムを作る

2024年11月23日


かつて東京の地下鉄で聴けた発車ブザーの音を再現するおもちゃを自作しました。ここでは、周波数がわずかに異なる2つの矩形波を出力するプログラムの作成について、説明します。

ひとつ前の記事では、地下鉄の発車ブザー音を出すための回路を製作しました。この回路では、PICマイコンPIC12F1822のI/Oピンを用いて、周波数の近い2つの矩形波を生成すれば、その波形を合成・増幅してスピーカーを鳴らすことができます。

ここからは、基板に搭載するマイコンPIC12F1822のプログラムについて説明します。

ソースコード

ソースコードのファイルは、以下のとおりです。MPLAB X IDEでアセンブラpic-asを使ってビルドするときに使えます。

アセンブラソースファイル(241122c8.S)

ビルドするためには、リンカのオプションとして、"-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パワーアップタイマ使用する
4MCLRピンの用途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つです。

領域名説明
1udata_bank0バンク0のRAM領域(0x020からはじまる80バイト)
2udata_bank1バンク1のRAM領域(0x0A0からはじまる32バイト)
3udata_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は小さめ(数キロオーム)とすると、以下の音声信号が得られました。

完成したブザーの音声波形

完成したブザーの音声(241122c4.mp3)

上記の音声は、スピーカーに並列に電極を押し当てて、パソコンで音声信号をキャプチャーして得たものです。

かなりそれっぽく聞こえます。波形からは、周波数が互いに少し異なる矩形波を、異なる振幅で合成した信号であることが分かります。

少しとがった感じの波形になっているのは、オーディオアンプの入力がハイパスフィルタの形になっていることと関係があるかもしれません。

スピーカーは、購入したときに入っていた箱の中に入れたままのほうが、雰囲気のある音になりました。箱から出すと、部屋で聴くには少々うるさい感じになってしまいます。

完成した様子

プログラム変更前の音声

ちなみに、上記のプログラムと音声は、一度作ったあとに改良した後の状態のものです。

最初に作ったときは、プログラムが少し違っていて、以下のような音が出ていました。

改良前のソフトで記録した実際の音声(出だしの部分)

改良前の音声(241122c7.mp3)

音の出始めが少し割れたように聞こえます。

これは、初期版では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.