TR-909 クローンのシーケンスパタンは eeprom に書き込みますが、avr-libc ライブラリが提供している eeprom 関数には同期書き込みしかありません。
https://www.nongnu.org/avr-libc/user-manual/group__avr__eeprom.html
しかし、TR-909 クローンのファームウェアでは割り込みをほとんど使わずリアルタイム処理がなされていて、eeprom を同期書き込みすると書き込み中処理が止まってしまいます。これが様々な誤動作の原因になってしまうので、非同期書き込み関数を書きました。
EEPROM 書き込み関数の仕様
今書いているファームウェアでは、特にタスクスケジューラのようなものは使っていないので、EEPROM 書き込みはポーリングの手法で行うことにします。ポーリングで確認する必要があるのは二点:
- EEPROM に書くものがあるか
- EEPROM は書き込み可能か
この関数は、シーケンサのリズムパタンの書き込みに使うので、書き込みデータは配列で、配列のインデックスには範囲があります。このインデックスを変数で示してそのあたいが配列の中に収まっている場合書き込むデータがあると判断することにします。ポーリング関数は以下のような概要になります
extern eeprom_write_async(uint16_t address, uint8_t data);
static int16_t index = -1;
void Poll() {
if (index < 0 || !eeprom_is_ready()) {
return;
}
eeprom_write_async(ADDRESS_OFFSET + index, data[index]);
if (++index >= MAX_ADDRESS) {
index = -1;
}
}
eeprom_write_async()
が非同期書き込み関数です。指定されたアドレスに1バイトのデータを書き込みすぐに戻ってきます。eeprom_is_ready()
は、EEWE ビットを確認する avr-libc マクロです。
ちなみに、この実装では、他の誰も EEPROM に書き込みを行わないことを前提にしています。data[]
配列のデータ以外からも書き込みを行うような場合もう少し複雑なスケジューラを書かないといけません。
EEPROM 書き込みの基本的な手順
詳しくはデータシートに書いてありますが、ブートローダを使っていない場合の EEPROM 書き込みの手順は以下の通りです
- EEPROM 制御レジスタ EECR の書き込みビット EEWE が 0 になるのを待つ (1 の場合前の書き込みがまだ終わっていない)
- EEPROM アドレスレジスタ EEAR にアドレスを入れる
- EEPROM データレジスタ EEDR に書き込む値を入れる
- EECR のマスタ書き込みビット EEMWE を 1 にセットする
- 4クロック以内に EECR の書き込みビット EEWE を 1 にセットする
データシートに載っている C での実装例はこんな感じの同期関数です。
void EEPROM_write(unsigned int uiAddress, unsigned char ucData)
{
/* Wait for completion of previous write */
while (EECR & (1 << EEWE));
/* Set up address and data registers */
EEAR = uiAddress;
EEDR = ucData;
/* Write logical one to EEMWE */
EECR |= (1 << EEMWE);
/* Start eeprom write by setting EEWE */
EECR |= (1 << EEWE);
}
これは同期書き込みを行う関数で、EEPROM の書き込みが可能になるまで関数の中で待っています。EEPROM の書き込みは一般的に低速なので、それを待ってしまうと CPU クロックの無駄遣いになってしまいます。そこで書き込みが可能だと前提して待たずに実行する非同期の関数を使う必要があります。非同期の書き込み関数を書く場合、次の点に注意しないといけません
- EEWE ビットが0になるのを関数内で待たない(関数を呼ぶ前に判断しておく)。
- EEMWE をセットした後に割り込みが入ると次の行で EEWE をセットするのに確実に4クロックより多く経過してしまう
これを満たすように、最初に C/C++ 関数で以下のように書きました。cli()
, sei()
, は割り込みを禁止、許可するマクロです。この関数を汎用に使うには関数内でも EEWE ビットを確認するべきですが、今回の使用方法では関数が呼ばれるときには EEWE ビットは常に 0 なのがわかっていますし処理速度のために確認は省略しました。
void eeprom_write_async(uint16_t address, uint8_t data) {
EEAR = uiAddress;
EEDR = ucData;
cli();
EECR |= (1 << EEMWE);
EECR |= (1 << EEWE);
sei();
}
これでまあ動くはずで、基本的には動いたのですが、何故か動作が不安定で時々書き込みに失敗したりしました。今回の開発ではデバッガを使っていないのでこのようなタイミング問題が出ると修正がやっかいです。デバッガをつなぐのは面倒なので、ステップごとに消費クロック数がはっきりわかるアセンブラで書けばまあ問題が出ないでしょう、ということで、上の関数をアセンブラに書き換えました。
C/C++ から呼ぶアセンブラ関数
以下は、データシートに記載されている EEPROM 同期書き込み関数のアセンブラでの実装例なのですが、AVR-GCC でコンパイルする場合、実はこのままでは動きません。
EEPROM_write:
; Wait for completion of previous write
sbic EECR,EEWE
rjmp EEPROM_write
; Set up address (r18:r17) in address register
out EEARH, r18
out EEARL, r17
; Write data (r16) to data register
out EEDR,r16
; Write logical one to EEMWE
sbi EECR,EEMWE
; Start eeprom write by setting EEWE
sbi EECR,EEWE
ret
問題なのは、関数への引数です。上記の実装例では、アドレスレジスタ EEARH, EEARL への値は汎用レジスタ r18, r17 に格納、データレジスタ EEDR への値は汎用レジスタ r16 に格納されていますが、実際にはこれらのレジスタには値が入っていません。そもそも上記の関数では、引数がどこにも宣言されていませんが、C/C++ から使える関数をアセンブラで書く場合引数や戻り値はどうやって処理すれば良いのでしょうか?
C/C++ 言語では関数などのプログラムバイナリモジュールを再利用可能にするための規格があります。これは Application Binary Interface (ABI) と呼ばれていますが、その中でも特に関数を呼び出す際、関数が処理に使って良いレジスタや、引数や戻り値の受け渡しに使うレジスタなどを取り決める部分を Calling Convention と呼びます。AVR-GCC の Calling Convention によると、引数は R25 から降順に割り当ててゆき、引数のサイズが奇数だった場合奇数レジスタ番号を一個飛ばして偶数レジスタから開始するように割り当て、引数の LSB 側が割り当てたレジスタの低い番号になるようにあてはめます。つまり eeprom_write_async()
関数の引数は以下のようにレジスタに渡されます
- uint16_t address: r24:r25
- uint8_t data: r22
eeprom_write_async 関数の実装
#define __SFR_OFFSET 0
#include <avr/io.h>
.global eeprom_write_async
eeprom_write_async:
; Disable interrupts
cli
; Set up the address (r24:r25) to address register
out EEARH, r25
out EEARL, r24
; Write data (r22) to Data register
out EEDR, r22
; Write logical one to EEMWE
sbi EECR, EEMWE
; Start eeprom write by setting EEWE
sbi EECR, EEWE
; Enable interrupts
sei
ret
AVR-GCC の Calling Convention を守る以外にもいくつか注意点があります
- ファームウェアのソースコードは C++ で書かれているので、宣言に
extern "C"
を加える - ファイル拡張子は .S を使う。この拡張子のファイルには C と同様のプリプロセッサ処理がコンパイル時に行われるので、#include 文が使えるようになる
<avr/io.h>
を必ず include する。これがないと制御レジスタ名が認識できない<avr/io.h>
を include する前に__SFR_OFFSET 0
を定義する
最後の注意点は、avr-libc ソースコードの中の sfr_def.h ファイルに説明が書かれています
https://github.com/vancegroup-mirrors/avr-libc/blob/master/avr-libc/include/avr/sfr_defs.h