PIC24Fのフラッシュメモリにデータを書く

PIC24F64GAにはEEPROMがありません。
そのため、電源を切っても消えないようにデータを保存するにはプログラムが保存されているのと同じフラッシュメモリに保存する必要があります。
また、PIC16や18のEEPROM読み書き込みと違って少し複雑です。
その代わりプログラム領域全体をいじれます。プログラムを実行しながらプログラムを書き換えることも可能なわけなので面白い使い方もできるかもしれませんね。
今回はとりあえずEEPROMの代わりとしてデータを書いたり読んだりします

PSVとテーブル命令

フラッシュメモリにある内容を読み出すにはいくつか方法があります。
PSVという機能を使うと普通のRAMにある変数にアクセスするようにフラッシュメモリの内容にアクセスできます。
ただし、PSVの場合はアクセスできる範囲が制限されます。また、メモリは24bit幅ですが、PSVの場合は下の16bitにしかアクセス出来ません。

PSV以外のもう1つの方法がテーブル命令を使った方法です。
テーブル命令を使うと基本的にはメモリの全てにアクセスできます。
読み取りは非常に簡単です。好きなアドレスの好きなデータを持ってこれます。
問題は書き込みで、手順が難しい難しい。。。。
直接書き込むことも出来ません。しかも書き込む前にその辺りの数バイトを一気に消去してから
書き込みを行う必要があります。

<読み込み>
では、読み込みやからやってみましょう。ROMの0x200を読み込んでみます
こんなプログラムで読み込めます。

[c]
unsigned int page = __builtin_tblpage(0x200);
unsigned int offset = __builtin_tbloffset(0x200);
unsigned int val;
while(NVMCONbits.WR);
TBLPAG = page;
val = __builtin_tblrdl(offset);
[/c]

解説します。
テーブル命令は
1)アドレスを指定する。アドレスはおおまかな位置を示すページと そのページ内の位置を示すオフセットで構成される。
2)アドレスを指定した状態でテーブルリード命令を実行する
この2段階です。
まず、アドレスをページとオフセットに分割します。
__builtin_tblpage(0x200)
__builtin_tbloffset(0x200)
この2つのマクロを使っています。ちなみにこれらはそれぞれ
0x200 >> 16;
0x200 & 0xFFFF;
のようなことをしています。(全く一緒ではない)
これで0x200アドレスを指定しました。
while(NVMCONbits.WR);
書込み中等の場合はこれが1になるので終わるまで、つまり0になるまで待つものです。
TBLPAG = page;
return __builtin_tblrdl(offset);
ここが実際の書き込みを行うところですね。TBLPAGレジスタにページをセットして
__builtin_tblrdlマクロにoffsetを渡しています。
このマクロはテーブルリード命令を行うものです。
これでvalには0x200のデータが読み込まれます。

<const変数の読み込み>
さっきのは適当なアドレスの読み込みでしたが、こんどは書き込みをしましょう。
今回やりたいのは実行できるプログラムを書くというよりは、EEPROMの代わりに使ってデータを読んだり書いたりしたいわけです。なので、適当なアドレスを読み書きするわけじゃなく、自由に使ってもいい領域をあらかじめ確保しておきましょう。

[c]
const __attribute__((section(".const_eeprom"), space (prog))) unsigned int _flash_data;
[/c]

これでunsigned int 分の容量がフラッシュメモリが確保されました。
アドレスが何かはリンクした時に判明するのでまだわかんないです。
これでさっきのプログラムを変えると

[c]
unsigned int page = __builtin_tblpage(&_flash_data);
unsigned int offset = __builtin_tbloffset(&_flash_data);
unsigned int val;
while(NVMCONbits.WR);
TBLPAG = page;
val = __builtin_tblrdl(offset);
[/c]

こうなります。どんなにプログラムが長くなってもちゃんと確保された自由な領域を仕えています。
これで _flash_data=5;とかってしておくとちゃんと読み取れるはず。

書き込み

さて書き込みです。手順としては

  1. 同じようにページとオフセットを指定
  2. 消去
  3. 書き込みたいページとオフセットを指定
  4. 書き込み

となります。
書き込むためには一度消去しないといけないんです。
2つ問題があります。
1つは消去できる先頭アドレスが決まっていること。2つめは消去の量が1ページ分(8行分(1行が64命令なので 8*64命令文))
ということです。

まず1つめ。
リファレンスによると「ブロックの先頭は1536と192バイトの境界にアラインメントされている」とのこと。
どういうことかというと、0x9600は1536の25倍なのでここを指定して消去したり書き込んだりはできるが、
0x9601を指定して消去したり書き込んだり出来ないということです。

2つめ
これが重要で消去は8行(1行=64命令=192バイト(1命令は24bitですから3バイトなのです)))単位で行われます。
なので、1バイトだけ容量を確保してもそこだけを消すことはできずその近くまで全部消えるということです。だから書き込みたい時には消える範囲のデータを全てとっておいて、消去して、全部書き直す という作業が必要です。
めんどいよね〜。

めんどいので、今回はEraseで消える8行分のメモリを確保して、そのうちちょっとだけを使います。
そしたら消去されてもこの領域は使ってないので何がどうなっても良いということになります。

やってみましょう

[c]
const __attribute__((section(".const_eeprom"), space (prog), address (0x9600) )) unsigned int _flash_datas[96*8];

void Flash_set()
{
Flash_Erase(__builtin_tblpage(_flash_datas), __builtin_tbloffset(_flash_datas));
Flash_Write(__builtin_tblpage(_flash_datas), __builtin_tbloffset(_flash_datas),5);
}
void Flash_Erase(unsigned int page, unsigned int offset)
{
unsigned int i=0;

while(NVMCONbits.WR);
TBLPAG = page;
__builtin_tblwtl(offset, 0x0000);
NVMCON = 0x4042;
asm volatile ("disi #5");
__builtin_write_NVM();
while(NVMCONbits.WR);
}

void Flash_Write(unsigned int page, unsigned int offset, unsigned int val)
{
while(NVMCONbits.WR);

NVMCON = 0x4003;
TBLPAG = page;
__builtin_tblwtl(offset, val);
__builtin_tblwth(offset, 0xFF);
asm volatile ("disi #5");
__builtin_write_NVM();

while(NVMCONbits.WR);
}
[/c]

まず。メモリを確保します。addressを0x9600で指定しています。ちょっとリンクの結果を見てみると
const_eepromって名前(名前はなんでもいいですよ、僕はEEPROMっぽく使いたかったのでそうした)に
0x9600から0x9c00までの0x600が確保されているのが分かると思います。
これで0x9600を指定して消去してもプログラムのところまで消えちゃう心配はありません。

あとは何をやっているのかはなんとなく分かると思います。
NVMCONが何をするかの制御です。イレースか書き込み可の設定をして、他にページとかも設定して
テーブル命令で書き込みたい値をセットします。
しかし実際にはまだ書き込まれません。書き込み手順の命令を実行する必要があります。
ただし、ちょっとでも命令の手順を間違えると書き込まれません。
そこでその部分はマクロになっています。

__builtin_write_NVM();

これですね。最後にこれを実行する。また、これの実行中に割り込みが入っても困るのでDISI命令で次5回分の命令が終わるまでの割り込みを禁止しています。

今回はわざわざ消去したうちの一番上にだけ書き込みましたが、
残りの部分にももちろん書き込めます。

ある部分だけ上書き

8bitだけ書きたいのに他の部分が消えるのも嫌なので、もう少しまともなものにしてみましょう。
Eraseで8行分消えちゃうのですが、1行分はいつもデータを取っておいて、その中の8bitだけを変えても他のデータが残るようにしてみます。

[c]
void Flash_Bulk_Write_With_Erase(unsigned int *values, unsigned int head, unsigned int length) {
unsigned int i=0;
unsigned int buffer[64];
unsigned int page = __builtin_tblpage(_flash_datas);
unsigned int offset = __builtin_tbloffset(_flash_datas);

// rescue to buffer
TBLPAG = page;
for (i=0; i< 64; i++) {
buffer[i] = __builtin_tblrdl(offset + i*2);
}
// bulk over-write
for (i=0; i< length; i++) {
buffer[i + head] = values[i];
}

// Erase Page
TBLPAG = page;
__builtin_tblwtl(offset, 0x0000); // this is important see reference.
NVMCON = 0x4042;
asm volatile ("disi #5");
__builtin_write_NVM(); //erase 8line = 64*8
while(NVMCONbits.WR);

// Write 64 instructions
NVMCON = 0x4001;
TBLPAG = page;
for (i=0; i<64; i++) {
__builtin_tblwtl(offset + i*2, buffer[i]); // *2 is required because address is 0 2 4 6 ,,,,,
__builtin_tblwth(offset + i*2, 0xFF);
}
asm volatile ("disi #5");
__builtin_write_NVM();
while(NVMCONbits.WR);
}
[/c]

まず、_flash_datasのアドレスをpageとoffsetに分割します。
そして、1行分(64命令)のROMの下位16bitを配列変数に保存します。
この時にi*2となっていますが、ROMのアドレスは0の次は2といった感じで偶数になっています。(実は奇数は上位16bitにアサインされている)

Screen Shot 2015-01-15 at 20.44.52

今回は上位を無視して下位だけを使います。64命令文ですが、仮にoffsetが0x8300だとすると0x8300、0x8302、0x8304が読むべきアドレスです。

で、データを読み取れたら必要な分だけUpdateします。

そしてEraseします。Eraseはこの64命令分だけでないのに注意して下さい。

最後に配列変数を書き込んでいます。
書き込みはワード単位か1行単位でしか行えないので、今回は1行の書き込みを行います。
書き込みの際にはテーブル命令を使って64命令文書き込みます。
これはROMに書き込まれているわけでなく特殊な領域に書き込まれています。
上位8bitに0xFFを入れていますが、これはまちがってこの領域をプログラムとして実行した時に問題が起きないようにNop命令の命令番号を入れています。
そして最後に書き込み目入れを実行して一斉書き込みです。