【AVR ATtiny10】Tinyバトルサウンド (3)おたのしみ編

💡 この記事は 2022.05.29 に公開した記事を最新コードに合わせて書き直したものです
おさらい
前回までにバトルサウンドで有名なメロディIC GSE3568 の同等品の M09 のサウンド構成の分析を終えました。クロック単位のサウンドの構成やノイズ合成のアルゴリズムも特定することができたので、いよいよ ATtiny10 で実装します。
おたのしみ
まずは実際に作っておたのしみいただけるよう、回路図とソースコードと HEXファイルを公開します。

TinyBattleSound 回路図
回路図について
64Ω以上程度の高めのインピーダンスのスピーカーを直接駆動できるよう、差動出力(SOUND+、SOUND-)に対応しています。SOUND-はBTNOUTと共有です。
が、後述しますが SOUND+ だけを使いトランジスタによるシングルエンドドライブをおすすめします。水色のラインはプログラマ接続用です。
JP1およびJP2は通常はショートですが、プログラミング時は開放してください。ボタンやスピーカーなどをプログラミングインタフェースから(実用上問題無い程度に)切り離します。実際の製作に当たっては、作品に載せる前にプログラミングしてしまえばこれらジャンパーは要りません。
12ボタン?
え、サウンドは 8種類だったはずなのになぜボタンが 12個あるのかって?
なんか作ってたらこんなんなっちゃいました。よくある話です。
そしてソースコードと HEXファイルはこちらです。
ソースコードと HEXファイル
- src/main.asm … 各種定義やメインループがあります
- src/button.asm … キーボード入力処理です
- src/sound.asm … サウンドジェネレータです
- src/sound_list.asm … ボタンとサウンドの対応付けです
- src/sound_1.asm ~ … 個別のサウンドシーケンスです
- src/wait.asm … クロック単位の時間待ちを行うマクロです
- TinyBattleSound.hex … アセンブル済みの HEXファイルです
- TinyBattleSound - 20230903b.png … 回路図です
ソースコードは 4 TAB です。
アセンブル済みの HEXファイルが同梱されているので、ブレッドボードででもなんでも回路を組んで書き込めば試せます。

ブレッドボードで仮組み例
とりあえずデモでも。
TinyBattleSound サウンドデモ
⚠ 音量注意!
💡 このデモではボタンの定義順を入れ替えていて、下のボタンから SOUND1、SOUND2 と順に鳴るようにしたものです。作った作品のボタン配置に合わせてお好きに定義できます(再アセンブルが必要です)。
雑に製作例
プリント基板の一例としてはこんな感じです。方眼紙に鉛筆書きでパターンを描いて作ったので元々電子データはありません。スキャナで取り込んで、見やすいように mspaint でベタ塗りにはしておきました。片面基板で、パターン側からの図です。キャン★ドゥの 500円玉ケースに組み込むのに合わせたサイズになっています。 なおこの基板は、シングルエンドでスピーカーをドライブしています。
CAD?なにそれおいしいの?
| パターン | 部品配置 |
|---|---|
![]() |
![]() |
この方眼紙はここで入手できます
3.5-3.0 のやつです。ケースの穴あけにも使えます。
よかったら製作過程もどうぞ。
製作ギャラリー

パターン描き
生基板に穴を開けたりマジックでパターンを描きます。一部界隈ではパターンを描くのに青マッキー人気が高いそうです。レジストペン要りません。

エッチング
温めたエッチング液に浸してエッチングします。この量だと電子レンジで 10秒ほどチンするとちょうどよく温まります。使用後の液は容器に入れて取っておけば、結構長く再利用できます。パターンの隙間はベタにして、溶ける銅の量が少なくなるようにすれば、より長持ちします。

プリント基板完成
エッチングが済んだらクリームクレンザーで磨いたりアルコールで洗ってインクを落とし、フラックスを塗っておきます。アルコールは、ホームセンターとかで売ってるガソリンの水抜き剤が入手しやすいと思います。あれはほぼイソプロピルアルコール(IPA)です。

組み立て
ボタン、抵抗、トランジスタ、プログラミング済みの ATtiny10 などをはんだ付けして、ケースに組み込みます。はんだ付けは背の低い部品からやりましょう。

完成
ボタントップはアイロンビーズです。軽くアイロンを当ててフランジを作っておき、穴を開けたケースの裏からはめ込みます。これで完成です。あと基板をケースから浮かせるスペーサーもアイロンビーズです。
このケースはキャン★ドゥの 500円玉ケースです。仕切りを除去すれば単3電池 2本を入れるのにちょうどいいスペースを作れます。単4電池 2本なら 1円玉ケースがぴったりで、どちらも愛用しています。ダイソーのケースは段が付いていて、こういう用途には向きません。
💡 アイロンビーズの高さは 5mm なので、これをスペーサーにしてさらにボタントップを付けるなら、タクトスイッチの高さは 4.3mm 以下を選ばないと入りません

単4電池版
これが 1円玉ケースで作ったものです。結構小さく作れます。基板はボタン以外表面実装品を使っています。事情により比較写真はありません。ポンコツですみません。キャン★ドゥのコインケース売り場で大きさは見られます。
解説のはじまり
前置き
おたのしみ編のイージーモードはここで終了です。
ここから先のハードモードではプログラムの仕組みなどを長々と解説していきます。次回にまたがりアセンブラでゴリゴリ書いていくので、どうぞお楽しみください。
なお、ここでもコードの説明はするのですが、サウンドを作る処理は次回に譲り、今回はソースコードの構成や周辺処理の説明を行います。
作品概要
以下 TinyBattleSound の主なスペックです。
- 使用マイコン … AVR ATtiny10
- 電源 … 1.8V~5V (乾電池 2本やリチウムコイン電池などの 3V 駆動を想定)
- システムクロック … 最大500kHz
- サウンドジェネレータ … ソフトウェア式 1bit 出力(タイマ/カウンタ不使用)
- サウンドクロック … 125kHz
- 実装サウンド … メロディIC GSE3568 や他の何かを彷彿とさせるサウンド 12種
- サウンド出力 … 差動出力に対応
当初トーンの発声にはタイマ/カウンタを使おうかと思っていましたが、何かと面倒だったのでソフトウェア式にしました。ソフトウェア式ではプログラムでタイミングを取って出力ピンを ON/OFF して音声を出力します。なので今回のコードの肝はクロック数管理です。
元のメロディIC のクロックは 128kHz でしたが、TinyBattleSound では 500kHz を 4分周した 125kHz とします。分析によって得たパルス幅などをそのまま使うと、この周波数の違いによって音程にして 0.4半音(
サウンドの差動出力
スピーカーの 2本の線の片側を GND などに固定して、もう 1本の電圧を動かすことをシングルエンドドライブと言います。

シングルエンドドライブ: 振幅 1
2本の線のそれぞれを、一方がプラスならもう一方はマイナスにと、それぞれ逆位相に動かすことを差動ドライブと言います。

差動ドライブ: 振幅 2
差動ドライブでは、スピーカーにとって駆動電圧が 2倍になったことと等価で、電力は 4倍(電圧が 2倍になれば電流も 2倍になり、電力は 2倍の 2倍)になります。つまり、限られた電源電圧で大きな出力を得ることができるのが差動ドライブです。
TinyBattleSound では初回公開後に差動出力を実装してみました。インピーダンス高めのスピーカーなら GPIO での直接ドライブもできるかと考えてのことでしたが、やはり GPIO の出力インピーダンスではスピーカーを直接ドライブするには力不足のようで、貧弱な音しか出ませんでした。というわけで、実用の際はトランジスタによるシングルエンドドライブをおすすめします。ですがコードとしては残しているので、実際にどんなもんかお試しいただけます。
ソースコードの特殊事項
最初に公開した時はソースコードの構成もシンプルだったんですが、いろいろとありまして、ファイルが細分化されました。なのでその説明です。
ボタンへのサウンド割り当て方法
どのボタンにどのサウンドを割り当てるか、これは sound_list.asm に書きます。使いたいサウンド宣言のマクロをボタン順に並べます。
; ボタン順にサウンドを並べる(1個以上)
SOUND_TBL:
USE_SOUND1
USE_SOUND2
USE_SOUND3
USE_SOUND4
USE_SOUND5
USE_SOUND6
USE_SOUND7
USE_SOUND8
USE_SOUNDX1
USE_SOUNDX2
USE_SOUNDX3
USE_SOUNDX4
SOUND_TBL_END:
SOUND_TBL から SOUND_TBL_END の間に USE_SOUNDx のようにマクロを並べると、1つ目はボタン0、2つ目はボタン1 というように順に対応していきますので、お好きな順に並べ替えたり、省いたり、だぶって登録することもできます。
このマクロは sound.asm でこんな風に定義されています。
; ==============================================================================
; 使用サウンド選択マクロ sound_list.asm に並べる
; ==============================================================================
.SET BTN_NUM = 0 ; 使用ボタン数
.SET _IMPL_SOUND1 = 0
.MACRO USE_SOUND1
rjmp SOUND1
.SET _IMPL_SOUND1 = 1
.SET BTN_NUM = BTN_NUM + 1
.ENDM
:
この USE_SOUND1 が使われると、まずサウンドルーチンへの rjmp SOUND1 を置きます。そして、「SOUND1 が必要だよフラグ」である _IMPL_SOUND1 を立て、ボタンの数である BTN_NUM をインクリメントします。
各サウンド処理は、SOUND1 なら sound_1.asm といった具合に個別にファイル化されています。そしてこれらファイルの冒頭では _IMPL_SOUNDx を確認し、自分が使われる場合のみその実装を展開します。
; ==============================================================================
; SOUND 1
; RIFLE
; ==============================================================================
.if _IMPL_SOUND1
:
.endif
なので、USE_SOUNDx が行われたサウンドだけ ROM に焼きこまれるというわけです。
これらのサウンド処理のファイルは、sound_list.asm で全て .include しています。
; 必要なものが選択実装されるので全部並べておいてよい
; テーブルアライメントの問題が起きたら順番をいろいろ入れ替えてみる
.include "sound_1.asm" ; RIFLE
.include "sound_2.asm" ; RIFLE(ECHO)
.include "sound_3.asm" ; TV GAME
.include "sound_4.asm" ; DUAL TONE
.include "sound_5.asm" ; BOMB 1
.include "sound_6.asm" ; BOMB 2
.include "sound_7.asm" ; GUN 1
.include "sound_8.asm" ; GUN 2
.include "sound_X1.asm" ; PAC MAN
.include "sound_X2.asm" ; XEVIOUS Blaster
.include "sound_X3.asm" ; SMB DOKAN
.include "sound_X4.asm" ; SMB KICK
そして SOUND_TBL の中身は、マクロによって使用されているサウンドへの rjmp が並べられ、こんなふうになります。
; ボタン順にサウンドを並べる(1個以上)
SOUND_TBL:
rjmp SOUND1 ; USE_SOUND1
rjmp SOUND2 ; USE_SOUND2
rjmp SOUND3 ; USE_SOUND3
rjmp SOUND4 ; USE_SOUND4
rjmp SOUND5 ; USE_SOUND5
rjmp SOUND6 ; USE_SOUND6
rjmp SOUND7 ; USE_SOUND7
rjmp SOUND8 ; USE_SOUND8
rjmp SOUNDX1 ; USE_SOUNDX1
rjmp SOUNDX2 ; USE_SOUNDX2
rjmp SOUNDX3 ; USE_SOUNDX3
rjmp SOUNDX4 ; USE_SOUNDX4
SOUND_TBL_END:
ボタンが押されたとき、SOUND_TBL + ボタン番号 のアドレスへジャンプしてくることで、ここから対応するサウンド処理へ rjmp されるわけです。
テーブルアライメントの調整
データを置いたテーブルを作り、その先頭から ofs バイト目の値を参照したいとき、こんな風に書くでしょう。
; DATA_TBL からデータを取得する
ldi XL, LOW(ADDR(DATA_TBL))
ldi XH, HIGH(ADDR(DATA_TBL))
add XL, ofs ; DATA_TBL + ofs のアドレスを算出
adc XH, ZERO ; 👈繰り上がり処理
:
DATA_TBL: .db 0x00, 0x01, 0x02, 0x03, ...
アドレスは 16bit ですので、下位 8bit の計算で繰り上がりが出たら上位 8bit にそれを反映しないといけないのです。ただ、これが必要なのは繰り上がりが発生する場合だけで、繰り上がりが発生しない場合にはただの無駄な処理です。
では、もし DATA_TBL が有効な ofs を足しても絶対に下位バイトから上位バイトへの繰り上がりが発生しない場所に置けたら?
そう、繰り上がり処理 adc XH, ZERO は不要になります。1ワード(2バイト)、1クロックの削減です。これは活用したいですよね。
でも、アセンブラにちょうどいい場所に置いてくれなんて指示は出せません。ではどうするか。
こうします。
DATA_TBL: .db 0x00, 0x01, 0x02, 0x03, ...
DATA_TBL_END:
.if HIGH(DATA_TBL * 2) != HIGH(DATA_TBL_END * 2)
.error "DATA_TBL is over the alignment"
.endif
DATA_TBL が終わった直後に DATA_TBL_END を置きます。そして、DTA_TBL と DATA_TBL_END のアドレスの上位バイトが等しければ、下位バイトから上位バイトへの繰り上がりは起こりえないということが分かります。もし上位バイトと下位バイトが異なれば繰り上がりが起こり得ますから、アセンブル時に DATA_TBL is over the alignment のようなエラーを出してアセンブルを失敗させます。
💡
DATA_TBL * 2のようにしているのは、ラベルのアドレスは 16bit長である機械語命令を示すワードアドレスなので、ROM にアクセスするバイトアドレスを計算する際には 2倍する必要があるためです
このエラーが出たら、エラーが出なくなるまでテーブルの位置をあちこちに動かしていい位置を探します。これはそのことを言っています。
; テーブルアライメントの問題が起きたら順番をいろいろ入れ替えてみる
.include "sound_1.asm" ; RIFLE
.include "sound_2.asm" ; RIFLE(ECHO)
:
ボタンの並びは SOUND_TBL で定義されるので順序を入れ替えることはできませんが、サウンド処理の .include 順は入れ替えることができますので、エラーが出なくなるまで .include を入れ替えてアセンブルを繰り返します。
そして、SOUND_TBL についても繰り上がり処理ケチりメソッドが適用されており、こんな感じになっています。
ldi ZL, LOW(SOUND_TBL)
ldi ZH, HIGH(SOUND_TBL)
add ZL, ARG
.if HIGH(SOUND_TBL * 2) != HIGH(SOUND_TBL_END * 2)
; 繰り上がりが起こりえるアドレス配置(起こらないよう配置を最適化すること)
.warning "Check SOUND_TBL Address Alignment"
adc ZH, ZERO
.endif
ここではエラーではなく警告としていて、かつこの場合には繰り上がり処理を追加するようにしています。これは、SOUND_TBL についてはテーブル移動の選択肢が少ないかもしれないとの配慮です。しかしもしこの警告が出たら、SOUND_TBL を持っている sound_list.asm を .include してる位置をあれこれ変えてみる価値はあります。
ファイルがやたら細かく分かれているのは、この調整を行えるようにというのも理由のひとつです。
再び差動出力
ところで ATtiny10 には足が 6本あり、電源 2本と RESETピンを除くと残りは GPIO 3本です。ボタン入力のため GPIO を 2本使用すると、残りは 1本です。
💡 多ボタン入力は以下のシリーズで扱っています
そして差動出力を行うには 2本の出力が必要ですから、1本足りません。いよいよここが、RESETピン GPIOピン化という伝家の宝刀を抜く時でしょうか。これは GPIO が 1本増えるがマイコンの再プログラミングは難しくなる諸刃の剣、それなりの覚悟が必要です。さて…
もったいつけましたが回路図にも出ちゃってるので結論を言えば、ボタン入力の制御ピンの 1本 BTNOUT を SOUND- と共用する、という手に打って出ました。宝刀を抜くのも面白いのですが、抜いた後の再プログラミング手法の記事もまだ書いてないですし、共用化の方がテクニカルなにおいがしますよね。
BTNOUT の役割
BTNOUT はこんな感じで制御電圧を出力しています。
STEP1 スリープ状態
スリープ状態では
BTNOUTは Low で、ボタン押下時にボタン入力であるBTNINを Low に引き下げられるようにしています。STEP2 押下ボタン判定中
BTNOUTを High にします。押下ボタンに応じたアナログ電圧が作られ、これをBTNINから読み取ります。STEP3 ボタン解放の検出
BTNOUTは Low にします。これでボタンが押されている間BTNINは Low ですが、ボタンが離されるとBTNINの Pull UP によって High になり、解放を検出します。
上記 STEP3 は、TinyBattleSound においてはサウンドが鳴っている間の状態です。サウンド出力が終わる時にボタンの状態を確認し、押されたままならサウンドを繰り返します。
ボタン解放の検出
ボタン解放は BTNIN が High になったことで検出できます。BTNIN は PB2 ですので、PINB の bit2 を確認すればよいのです。
sbis PINB, 2 ; 👈PB2 の入力が High だったらスキップ、解放された
rjmp まだ押されてる
解放された:
解放されたよ
ただこの方法は、この判定をしたその時点で解放されているのかを判別できますが、サウンドを鳴らしている間に解放された(ことがある)かは判別できません。長いサウンドが鳴っている間にボタンを離し、サウンドが鳴り止む前に再びボタンを押してしまえば、離されたことは分かりません。なので、もう少しハードウェアの支援を受けてみます。
ピン変化割り込みフラグ
PB2 には INT0 が割り当てられています。TinyBattleSound でも、sleep での眠りから覚めるための割り込みとしてこれを使っています。そして、INT0 がどうなったら割り込みがかかるのかは EICRA レジスタで設定します。
EICRA - 外部割込み制御レジスタA
7 6 5 4 3 2 1 0 - - - - - - ISC01 ISC00
ISC01 ISC00 割り込み発生条件 0 0 INT0が Low0 1 INT0の論理変化1 0 INT0の立ち下がり1 1 INT0の立ち上がり
POWER DOWN 状態の sleep から目覚める時には 00(Low割り込み)しか選べませんが、目覚めた後ではどのモードも使えます。なのでボタン解放検出では、01 の INT0 が変化した時の割り込み条件を選ぶことにします。そして割り込み条件が整うと、EIFR の INTF0 ビットが立ちます。
EIFR - 外部割込み要求フラグレジスタ
7 6 5 4 3 2 1 0 - - - - - - - INTF0
これによって、プロセッサが割り込みを実行できる状態では INT0割り込み が発生します。ただし TinyBattleSound では、割り込みまでは掛けません。条件が整った(= BTNIN が変化した = ボタンが離されたことがある)ことだけを後で確認します。
sbis EIFR, INTF0 ; 👈過去に INT0 が変化していたらスキップ、解放された
rjmp まだ押されてる
解放された:
解放されたよ
これで、この確認を行った瞬間だけではなく、この確認を行うまでの間のどこかでボタンが離されていても、そのことを検出できます。
BTNOUT と SOUND- の共有
といった事情で、ボタンが離されたことを検出するためには BTNOUT は Low に維持しておく必要がありますが、このピンにサウンド出力 SOUND- も割り当てるのであれば、Low に固定しておくわけにもいきません。サウンド出力のために BTNOUT を High にした瞬間、ボタン押下状態での BTNIN を Low に留めておけなくなり、ボタンが離された と判断されてしまいます。どうしたら良い?
そんなの、BTNIN のデジタル入力をやめてしまえばいいのです。
ちょっとトリッキーだと思いますが、ADC でアナログ電圧を読み取る時には、そのピンのデジタル入力を止めてアナログ入力に切り替えます。そしてアナログ入力に切り替えている間、そのピンのデジタル入力は 0 として扱われます。

データシートより: デジタル入力禁止時、そのピンはゼロとして読める
💡 ATtiny4-5-9-10-Data-Sheet-DS40002060A.pdf - microchip.com
リンク禁止なので英語版を引用していますが、avr.jp で検索すれば日本語データシートが見つかるはずです
もう少し詳しく説明すると、SOUND- に High を出力する時にはデジタル入力禁止、Low を出力するときにはデジタル入力再開します。これでボタン解放を検出できるのは SOUND- が Low の時だけに限られますが、サウンドの周波数を考えれば十分な検出周期を確保できます。
これを行うコードが sound.asm にあるこの OUTPUT_SOUND マクロです。
TMP に、PNO_SOUND のビット位置に SOUND+ へ出力する 0 または 1 の反転のみを入れた状態でこのマクロを使用します。PNO_SOUND2 は SOUND- の出力に対応します。
.MACRO OUTPUT_SOUND
; 👇 TMP レジスタ上の SOUND+ の反転を SOUND- の位置に付け加える謎の演算
subi TMP, -(((1 << ((PNO_SOUND2) - (PNO_SOUND))) - 1) << PNO_SOUND) ; +0 差動出力生成
; 👇 SOUND- が H の場合には BTNIN のデジタル入力を禁止する
sbrc TMP, PNO_SOUND2 ; +1
sbi DIDR0, PNO_BTNIN ; +2 PNO_SOUND2 が H になる場合はそれに先行してデジタル入力禁止
; 👇 SOUND+ と SOUND- を出力
out PORTB, TMP ; +3 * SGOUT
; 👇 SOUND- が L の場合には BTNIN のデジタル入力を再開する
sbrs TMP, PNO_SOUND2 ; 0 ここが 4n クロック目になるように
cbi DIDR0, PNO_BTNIN ; 1 PNO_SOUND2 が L になったらデジタル入力許可
; 2
.ENDM
差動出力生成の謎の演算は汎用的に書かれていて分かりづらいので、今回のケースに置き換えて整理するとこうなります。
subi TMP, -(((1 << ((PNO_SOUND2) - (PNO_SOUND))) - 1) << PNO_SOUND)
↓
subi TMP, -0x01
↓
addi TMP, 0x01
SOUND+ は PB0(bit0)、SOUND- は PB1(bit1) ですので、TMP の初期値は SOUND+ の状態だけを含む 0x01 または 0x00 です(SOUND+ に出力したい論理の反転)。そしてこれに対して addi TMP, 0x01 を行うと、TMP は 0x01 → 0x02 または 0x00 → 0x01 となり、SOUND+ と SOUND- が反転の関係にあるビットパターンが得られるというわけです。
汎用的というのは、SOUND+ と SOUND- のビット位置に隙間が空いていても必要十分な計算できるようにしていることです。
また DIDR0 の操作が 2か所に分かれていますが、SOUND- に High を出力する時はその前に、Low を出力する時はその後に操作しないと、意図しない状態になる一瞬のタイミングができてしまうからです。
次回予告
今回はおたのしみ編ということでしたが、はやりイージーモードで実際にモノを作ってもらうのが楽しいかなと思います。ハードモードでのコード説明はお目当てのサウンドの生成とは遠いものですから、なかなかモチベを維持できなかったかもしれません。しかし実際にソースコードを当たっていただいた時にはきっと必要になる説明ですし、よく噛めばじんわり旨味が出てくるスルメのようなものだと思って記憶の隅に置いておいていただければ幸いです。
次回はいよいよサウンド生成処理の核心です。正確なタイミングで処理をするためにひたすらクロックを数えます。スルメより硬くなりそうです。
このシリーズ
🧩 【AVR ATtiny10】Tinyバトルサウンド / 全4回


Discussion