hyokure さん改造は進んでいるとのことで、すばらしいです。
いつも対応が遅めになって申し訳ないのですが、プログラムをしっかり読む時間がやっとできたので、ざっとプログラムの概要をまとめたいと思います。全部を説明するのは非常に長くなるので関係の深そうな部分に絞っています。もっと詳しい説明が必要なところがあったらお知らせください。
AVR のプログラムは、main() 関数から始まるので最初にここを読むと良いのですが、MiniBoard の main 関数は以下のようになっています。
int main(void) {
iosetup(); // IO ポートを初期化
init_uart(); // シリアルを初期化
init_timer(); // タイマーを初期化
init_midi_decoder(); // MIDI デコーダモジュールを初期化
init_midi_controllers(); // MIDI コントローラモジュールを初期化
key_prev = 0;
tone_factor = 0;
sei();
// このループでは、MIDI 入力を監視し続けて入力があればデコードし CV と Gate として出力する
while (true) {
uint16_t rxByte = uart_getchar();
if (rxByte != -1) {
digest(rxByte);
}
}
}
MIDI を使わない場合、初期化が終わってしまったら、 main プログラムはループを空回りさせるだけで実質何もしません。
AVR マイクロプロセッサには、ペリフェラル(周辺装置?)といって、特定の仕事をするモジュールが何種類か内蔵されていて、MiniBoard はこれらのうち以下を使っています。これらが main プログラムの初期化部分で設定されています。init_uart() という関数と init_timer() という関数です。
- UART (シリアル入力) -- MIDI 信号の読み取りに使う
- Timer0 -- ノート(音階) CV の出力に使う (PWM という技術を使います)
- Timer1 -- ベンド(音階) CV の出力に使う (PWM という技術を使います)
このうちキー読み取りに関連の深いのは Timer1 ですが詳細は後ろに書きます。
マイクロプロセッサのプログラミングでこの他に重要なのは、入出力ピンに適切な役割を設定することで、これは main 関数から呼ばれている iosetup() という関数で設定が行われています。ピンの割り当てについては後で書きます。
初期化の終わった main 関数が実質何もしないのにプログラムがどうやって働くかというと、割り込みという機能を使っています。割り込みは、何か「割り込みイベント」というコトが起こるたびにプロセッサの処理に割り込んで実行する関数です。割り込み関数は名前が決まっていて ISR() という名前の関数があれば、それが割り込みです(厳密には関数でなくてマクロなんですが細かいことは省略)。引数はイベントの種類でプロセッサが対応しているものがあらかじめ決められています。キーの読み取りで重要なのが Timer1 の周期が終わるたびに呼ばれる ISR(TIMER1_OVF_vect) です。この関数の中でキーの読み取りをしている、つまり Timer1 の周期が終わるたびにキーの読み取りが行われるのですが、その詳細に入る前に、キー読み取りに使われる入出力ピンの割り当てと押されたキーのデコード方法について説明します。
回路図を見てもわかるかと思いますが、キーの読み取りには 4本の入出力ピンが使われています。それらは、PB4 (16), PB5 (17), PB6 (18), PB7 (19) です。かっこの中はピン番号ですがプログラムの中には出て来ず、PBx という記述で表されます。ATTiny2313 には A, B, D という三種類の入出力ポートがあって、プログラム内では、PORTA, PORTB, PORTD (出力), PINA, PINB, PIND (入力) という特殊な 8ビット整数の形で読み書きができます。PB4, PB5, PB6, PB7 というのは、8ビットのうち、0 から始まって 4, 5, 6, 7 番目のビットに割り当てられているという意味です(要は上位4ビットです)。
次に MiniBoard のハードウェア部分ですが、ダイオードを使って、押したキーが二進数に変換されるようになっています。割り当ては以下の通りです。プログラム内部でビットを反転して読み取るので内部での値は表の右端のようになります。
状態 | PB7 | PB6 | PB5 | PB4 | 値 | 値(反転) |
何も押していない | 1 | 1 | 1 | 1 | 0xf | 0x0 |
lowC | 1 | 1 | 1 | 0 | 0xe | 0x1 |
C# | 1 | 1 | 0 | 1 | 0xd | 0x2 |
D | 1 | 1 | 0 | 0 | 0xc | 0x3 |
D# | 1 | 0 | 1 | 1 | 0xb | 0x4 |
E | 1 | 0 | 1 | 0 | 0xa | 0x5 |
F | 1 | 0 | 0 | 1 | 0x9 | 0x6 |
F# | 1 | 0 | 0 | 0 | 0x8 | 0x7 |
G | 0 | 1 | 1 | 1 | 0x7 | 0x8 |
G# | 0 | 1 | 1 | 0 | 0x6 | 0x9 |
A | 0 | 1 | 0 | 1 | 0x5 | 0xa |
A# | 0 | 1 | 0 | 0 | 0x4 | 0xb |
B | 0 | 0 | 1 | 1 | 0x3 | 0xc |
highC | 0 | 0 | 1 | 0 | 0x2 | 0xd |
ここで、Timer1 の割り込み関数を見てみると、以下のようになっています。この割り込み部分でキーの読み取りを行なっていて、改造するならこの部分のコード変更が必要です。
ISR(TIMER1_OVF_vect) {
static uint16_t button_count = 0;
uint8_t key;
uint8_t octave;
// ピッチベンド更新部は省略
// check the keyboard
octave = ((PIND >> 3) ^ 0x0f) * 12; // オクターブスイッチを読み取り
key = ((PINB >> 4) ^ 0x0f); // PB4, PB5, PB6, PB7 を読み取り <-- キー数を拡張する場合ここを変更する必要あり
if (key) {
key += octave;
}
if (key != key_prev) {
if (key_prev) {
note_off(key_prev + C0);
}
if (key) {
note_on(key + C0, 127);
}
}
key_prev = key;
if (! (PIND & _BV(PD2))) {
++button_count;
}
else {
button_count = 0;
}
if (button_count >= 19531) {
tone_factor ^= _BV(PORTB0);
button_count = 0;
}
}
読み取りの処理
key = ((PINB >> 4) ^ 0x0f);
は、少しわかりにくいですが、もっとわかりやすく分解すると以下のような処理をしています。
uint8_t pin_values = PINB; // PINB レジスタからピン情報を読む。4, 5, 6, 7 ビット目にキー情報が入っている。
// 例えば lowC キーが押された場合、この時点で pin_values は 0xe0 (16進数) あるいは 11100000 (2進数)
pin_values = (pin_values >> 4); // 4ビット右にシフトして 4, 5, 6, 7 ビット目を 0, 1, 2, 3 ビット目に移動する。 4, 5, 6, 7 ビット目には 0 が入る
// 例えば lowC の場合この時点で pin_values は 0x0e (16進数) あるいは 00001110 (2進数)
key = pin_values ^ 0x0f; // 0x0f は二進数であらわすと 00001111。 ^ は xor 演算子で 1111 を掛け合わせると入力ビットが反転する。
// 例えば、lowC の入力値はこの段階で 00001110 で、00001111 と掛け合わせると 00000001 になる。(0xe -> 0x1)
この辺りがキー入力のコードを読んだり改造したりするために重要な部分です。長くなってしまったので、改造についてのご質問への答えは別の投稿に書きます。