AVR の EEPROM 非同期書き込み関数

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 address = -1;
void Poll() {
  if (address < 0 || !eeprom_is_ready()) {
    return;
  }
  eeprom_write_async(ADDRESS_OFFSET + address, data[address]);
  if (++address >= MAX_ADDRESS) {
    address = -1;
  }
}

eeprom_write_async() が非同期書き込み関数です。指定されたアドレスに1バイトのデータを書き込みすぐに戻ってきます。eeprom_is_ready() は、EEWE ビットを確認する avr-libc マクロです。

EEPROM 書き込みの基本的な手順

詳しくはデータシートに書いてありますが、ブートローダを使っていない場合の EEPROM 書き込みの手順は以下の通りです

  1. EEPROM 制御レジスタ EECR の書き込みビット EEWE が 0 になるのを待つ (1 の場合前の書き込みがまだ終わっていない)
  2. EEPROM アドレスレジスタ EEAR に値を入れる
  3. EEPROM データレジスタ EEDR に値を入れる
  4. EECR のマスタ書き込みビット EEMWE を 1 にセットする
  5. 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);
}

われわれの非同期の書き込み関数を書く場合、次の点に注意しないといけません

  • 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

-> Github で見る

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

AVR の EEPROM 非同期書き込み関数」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください