2014年6月19日木曜日

AVR-GCC 用内蔵 EEPROM 簡易ファイルシステム

ダウンロード

MEGA8 と TINY85 で動作確認したが、おそらく他の AVR でも使える。avr-gcc, avr-libc でプログラミングする人向け。

解説

多くの AVR マイコンには EEPROM (不揮発性メモリ) が内蔵されている。容量は少ない機種で 64Byte、多くても数キロバイト程度だが、電源を切っても消えないので、設定やデータをちょっと保存するのに便利だ。

AVR 用のライブラリ avr-libc には EEPROM を読み書きする関数があるので、バイト単位やワード単位でアドレスを指定して読み書きできる。しかし、EEPROM に複数の可変長のデータを保存したい時など、もう少しだけ扱いやすい読み書きの手段が欲しい。自分は赤外線リモコンを作った時に学習データ(可変長。複数。学習のたびに書き換えが必要)を保存するために必要だった。

そこで、AVR 内蔵 EEPROM 用に簡易ファイルシステムを作ってみた。C 言語でファイルアクセスする時の open(), close() といったスタイルをまねてはいるが、必要最小限の機能のみに絞った。原理はおそらく誰もが思いつく一番単純な方式。

コードサイズは1kB弱。使わない関数をコメントアウトすればもう少しだけ小さくできる。

使用例


例として、MEGA8 を PC のシリアルポートに接続して、MEGA8 から出力した文字列を PC に表示させる回路を作った。



MEGA8 に書き込んだデモのコードは次の通り。このプログラムは、文字列を直接シリアルポートに出力するのではなく、一旦ファイルに保存して、ファイルから読みだした文字列をシリアルポートに出力するというもの。

// EEPROM 用簡易ファイルシステムのデモ (ATMEGA8)
#include <avr/io.h>
#include <avr/eeprom.h>
#include "fs.h"

// シリアルポートの初期化(1200bps, MCU クロック=1MHz)
void uart_init(void) {
  UCSRB  = _BV(TXEN);
  UBRRH=0;  UBRRL= 51;
}

//
// シリアルポートに1文字出力
//
void  uart_putchar(char c) {
  loop_until_bit_is_set(UCSRA, UDRE);
  UDR = c;
  return;
}

// メイン
//
int main(void) {

  uint8_t fileName;
  char buf[100];
  char *pt;

  uart_init();
  fs_init();

  fileName = 0;
  if (fs_ropen(fileName) == FS_OK) {
    // ファイル0 がオープンできたら、ファイルからシリアルポートに出力
    // 1行目表示
    while (!fs_EOF()) uart_putchar(fs_read_b());
    // 2行目表示
    fileName = 1;
    fs_str_r(buf, fileName, 100);
    pt=buf;
    while (*pt) { uart_putchar(*pt); pt++; }
  } else {
    // ファイル0 がオープンできなかったら、文章をファイルに書き込む
    fileName = 0;
    if (fs_wopen(fileName) == FS_OK) {
      char* pt = "恥の多い生涯を送って来ました。"
        "自分には、人間の生活というものが、見当つかないのです。\r\n";
      while (*pt!='\0') { fs_write_b(*pt); pt++; }
      fs_wclose();
      fileName = 1;
      fs_str_w("自分は東北の田舎に生れましたので、汽車をはじめて見たのは、"
               "よほど大きくなってからでした。\r\n", fileName);
    }
  }
  while (1); // どちらかの処理をした後は無限ループ
  return 0;
}


このソースファイル(main.c) と同じ場所に "fs.c" と "fs.h" を置き、コンパイルするときに fs.c にある関数がリンクされるようにする。コンパイルの手順はこんな感じ。

# コンパイルとリンク,Intel HEX 形式の出力
% avr-gcc -g -Os -mmcu=atmega8 -o out.o main.c fs.c
% avr-objcopy -j .text -j .data -O ihex out.o out.hex

ヒューズは MEGA8 のデフォルト(hfuse=0xd9, lfuse=0xe1)にする。

生成した out.hex を MEGA8 に書きこんだら、上記の回路図に従って MEGA8  を PC のシリアルポートに接続して、PC 上で TeraTerm を起動する。TeraTerm でシリアルポートを 1200bps に設定する。

1回目の電源ONではファイルが存在しないため、fs_ropen(fileName) はエラーを返すので、ファイルに文字列が書き込まれるだけで、シリアルポートには何も出力されない。2回目以降の電源 ON ではファイルが存在するので fs_ropen(fileName) に成功して分岐し、ファイルから文字列が読み出されシリアルポートに出力される。Teraterm で見ると、2回目以降の実行ではちゃんと文字列が出力されている。



ファイルシステム fs.c の使い方

 使い方は次の通り。ファイル名(というかファイル番号) は、0~255 の値1バイトのみ。

ファイルから読み込む場合は、まず fs_ropen(fileName) でファイルを読み込みモードでオープンする。続けて fs_read_b() (8bitの場合)または fs_read_w() (16bitの場合) を繰り返して読み込む。読むと自動でポインタが進む。最後まで読むと fs_EOF() が真になる。読み込みモードでオープンした場合はクローズはしなくてもよい。

ファイルに書き込む場合は、まず fs_wopen(fileName) でファイルを書き込みモードでオープンする。fs_write_b(8bit値) または fs_write_w(16bit値) を繰り返して書き込む。書き込むと自動でポインタが進む。書き終わったら fs_wclose() でクローズする必要がある。

オープンできるファイルは、読み込みと書き込み合わせて1つだけ。同時オープンはできない(EEPROM の書きこみ/読み込み位置を保持している変数が1つしかないため)。

オープン、クローズの手続きが不要で、文字列を一気に読み込む fs_str_r(buf, fileName, maxlen) という関数と、 同じく文字列を一気に書き込む fs_str_w(buf、fileName) という関数もある。文字列を扱うならそちらの方が便利。

構造

全てのファイルの先頭に3バイトのヘッダを付けて EEPROM に格納する。ヘッダの1バイト目はファイル名、2~3バイト目はファイルサイズ。ファイルは詰めて記録し、最後のファイルの後に終了マークとして 0xFF を3バイト書く。構造はこれだけ。

ファイルが何もない状態から、ファイル名=2 のファイル(サイズ=10バイト)を書き込み、次にファイル名=0のファイル(サイズ=15バイト)を書き込むと、EEPROM は下の図のような状態になる。

もし下記のような状態でファイル2が削除されたときは、その下にあるファイル0を上方向(低アドレス側)に詰める。つまりファイル0のヘッダとファイル0の実体、終了マーク全体を13バイト上にずらす EEPROM のコピーの動作が自動的に行われる。非効率的で遅いが、EEPROM のサイズは小さいので、このような力技でもなんとかなる。もっとも頻繁にファイルの削除や更新が発生する用途やタイムクリティカルな用途には向かない。

fs.h で指定された EEPROM のアドレス範囲外には書き込みが行われないようにアドレス範囲をチェックしていて、もしアドレス範囲をオーバーしている時は実際の書き込みは行わず、戻り値でエラー(FS_DISKFULL) を返す。ファイルの書き込み後にはファイルクローズが必要だが、クローズ時にアドレス範囲のオーバーが発生していないことを再度確認している。なので、書き込み後にはファイルをクローズして fs_wclose() の戻り値が FS_OK になっていることを確認する必要がある。

EEPROM のデータ構造 (ファイル2とファイル0の2個のファイルが保存されている状態)


定義

注: ソースファイルの方の変数の型の書き方を変更しました (例: unsigned char → uint8_t など)。プログラムの動作は変わりませんが、下記の説明の型とは食い違っています。

定数
#define EEPROM_FS ((uint8_t *)0)
ファイルシステムで使用したい EEPROM の先頭アドレスを fs.h 内のこの #define 文で指定すること。普通はこの例のようにアドレスに 0 を指定すればよい。 EEPROM の先頭アドレスから使用される。


定数
#define FS_SIZE (E2END-(unsigned int)EEPROM_FS+1-4)
ファイルシステムで使用したい EEPROM のサイズを fs.h 内のこの #define 文で指定すること。E2END が EEPROM の最終アドレス(avr-libc で定義済み)。EEPROM の全領域を使用するなら E2END-EEPROM_FS+1 を指定する。ただし EEPROM の末尾 4バイトはフラッシュメモリの書き込み回数の記録に使われることが多いので、普通はこの例のように -4 して末尾 4 バイトを空けたほうが良い。

マクロ
char fs_EOF()
fs_ropen() による読み込みオープン中のみ有効。オープン中のファイルに、まだ読めるデータが残っていれば偽、すでに最後まで読み終わっていれば真になる。実体はマクロ。

引数
  なし
戻り値
  ファイルを最後まで読み終わっていれば真

マクロ
void fs_format()
ファイルをすべて削除する。ファイルシステムに割り当てられた EEPROM の全領域に 0xff を書き込む(実際は先頭3バイトと末尾3バイトのみに 0xff を書きこめば事足りるが、コードの簡略化のため全領域に書いている)。出荷時の AVR の EEPROM は全領域が 0xff なので、フォーマット済みとして扱える。実体はマクロ。

引数
  なし
戻り値
  なし

関数
void fs_init(void);
ファイルシステム関係の変数を初期化する。コードの先頭で1回実行する。

引数
  なし
戻り値
  なし

関数
unsigned char fs_delete(unsigned char fn); 
指定したファイルを削除する。

引数
  fn: ファイル名
戻り値
  FS_OK: 削除に成功した
  FS_NOTFOUND: 指定されたファイルは存在しない

関数
unsigned char fs_ropen(unsigned char fn);
引数で指定したファイルを読み込みモードでオープンする。読み込みでオープンした場合はクローズはしなくてもよい。ファイルがオープンされている間は次の変数が使える(下記の変数は読み込みのみ。変更してはいけない)。

unsigned int fs_size: オープン中のファイルのサイズ
uint8_t *fs_pt: 現在の読み込み位置(EEPROM のアドレス)

引数
  fn: ファイル名
戻り値
  FS_OK: オープンに成功した
  FS_NOTFOUND: 指定されたファイルは存在しない

関数
unsigned char fs_read_b(void);
読み込みモードでオープン中のファイルから 8bit 値を読み込む。

引数
  なし
戻り値
  8bit値

関数
unsigned int fs_read_w(void);
読み込みモードでオープン中のファイルから 16bit 値を読み込む。EEPROM での 16bit 値のバイトオーダーは下位-上位の順(リトルエンディアン)。

引数
  なし
戻り値
  16bit値

関数
unsigned char fs_wopen(unsigned char fn);
指定したファイルを書き込みモードでオープンする。引数で指定したファイルがすでに存在していた場合は、書き込みモードでオープンした時点で削除される。ファイルがオープンされている間は次の変数が使える(下記の変数は読み込みのみ。変更してはいけない)。

unsigned int fs_size: 書き込んだサイズ
uint8_t *fs_pt: 現在の書き込み位置(EEPROM のアドレス)

引数
  fn: ファイル名
戻り値
  FS_OK: オープンに成功した

関数 
unsigned char fs_write_b(unsigned char val);
書き込みモードでオープン中のファイルに 8bit 値を書き込む。

引数
  val: 8bit値
戻り値
  FS_OK: 書き込みに成功した
  FS_NOTFOUND: 書き込みモードでオープンされていない
  FS_DISKFULL: 容量の限界のため書き込めなかった

関数
unsigned char fs_write_w(unsigned int val);
書き込みモードでオープン中のファイルに 16bit 値を書き込む。EEPROM での 16bit 値のバイトオーダーは下位-上位の順(リトルエンディアン)。

引数
  val: 16bit値
戻り値
  FS_OK: 書き込みに成功した
  FS_NOTFOUND: 書き込みモードでオープンされていない
  FS_DISKFULL: 容量の限界のため書き込めなかった

関数
unsigned char fs_wclose(void);
書き込みモードでオープン中のファイルをクローズする。この関数が FS_OK を返さなかった場合はファイルは書き込まれていない。書き込み後にこの関数を呼ばなかった場合も、ファイルには書きこまれない。

引数
  なし
戻り値
  FS_OK: 書き込みに成功した
  FS_NOTFOUND: 書き込みモードでオープンされていない
  FS_DISKFULL: 容量の限界のため書き込めなかった

関数
void fs_str_w(char* str, unsigned int fn);
fn で指定したファイルに、文字列 str を書き込む。 str は '\0' で終端されている必要がある。処理はこの関数だけで完結するので、別途ファイルをオープン、クローズする必要はない。

引数
  str: 書き込む文字列へのポインタ
  fn: ファイル名
戻り値
  なし

関数
char* fs_str_r(char* str, unsigned char fn, unsigned int maxlen); 
fn で指定したファイルから、文字列 str に読み込む。maxlen には str に読み込める最大の長さ(終端のための '\0' を含む)を指定する。読み終わった場合も、maxlen の制限で読み込みが中断した場合も str は '\0' で終端される。処理はこの関数だけで完結するので、別途ファイルをオープン、クローズする必要はない。

引数
  str: 書き込む文字列へのポインタ
  fn: ファイル名
  maxlen: str に読み込める最大の長さ(終端のための '\0' を含む)
戻り値
  str: str のポインタを返す

関数
unsigned int fs_free(void);
空き容量を返す。

引数
  なし
戻り値
  空き容量を返す
 

公開される変数
extern unsigned char fs_filename;
  オープン中のファイル名

公開される変数
extern unsigned int fs_size;
オープン中のファイルのサイズ。特定のファイルのファイルサイズを知りたいときは、試しにfs_ropen(unsigned char fn) してから fs_size の値を読む。


公開される変数
extern uint8_t *fs_pt;
  現在読み書き中のポインタ(EEPROM のアドレス)


0 件のコメント:

コメントを投稿