Skip to content

FlashNVM

askn37 edited this page Jan 24, 2024 · 13 revisions

FlashNVM ツールリファレンス

フラッシュメモリ自己書換支援ツール。 これはスケッチプログラムを書き込んだ後の残りの、未使用フラッシュメモリ領域を スケッチの中から書換可能な不揮発記憶領域として扱うことを支援する。 フラッシュメモリ領域は EEPROM 領域に比して数桁大きなまとまった容量を扱える一方、 書換可能限度回数は1桁以上低い。

この機能は以下の対応ブートローダーが、 スケッチプログラムよりも前方に存在していることを前提に動作する。

他のSDK付属のブートローダーに例え同種の機能があっても、それには対応しないことに注意。

端的に言えばこれらのブートローダーがスケッチプログラムをフラッシュメモリ領域に書き込む能力を プログラムから借り受けて使用するものだ。 当然のことながら、記憶されているスケッチプログラムそれ自体を 破壊したり消したりも容易に出来ることに注意されたい。

  • この支援ツールの機能は namespace FlashNVMに属するメンバー関数として提供される。
  • AVR_DU と AVR_EB にある BOOTROW 領域の書き換えも、このツールで対応する。

USERROW 領域は <UrowNVM.h>支援される。

用例

#include <FlashNVM.h>

/* NVMEN領域に1ページ分のスペースを確保 */
const char nvm_store[PROGMEM_PAGE_SIZE] PGM_ALIGN NVMEM;

/* 機能サポート確認 */
if (! FlashNVM::spm_support_check()) {
  /* FlashNVM tools not supported */
  return;
}

/* SRAM上のバッファ確保 */
char nvm_buffer[PROGMEM_PAGE_SIZE];

/* <avr/pgmspace.h> の関数を使って */
/* NVMEM領域から SRAM にコピー : 領域位置が 64KiB以内の場合 : 以下同(接尾子 _P) */
memcpy_P(&nvm_buffer, &nvm_store, sizeof(nvm_buffer));

/* NVMEM領域から SRAM にコピー : 領域位置が 64KiB超の場合 : 以下同(接尾子 _PF) */
memcpy_PF(&nvm_buffer, pgm_get_far_address(nvm_store), sizeof(nvm_buffer));

/* nvm_storeバッファメモリを適当に書換えて。。。 */

/* ページ消去 */
if ( FlashNVM::page_erase_P( &nvm_store, sizeof(nvm_buffer) ) )
{ /* success */ }

if ( FlashNVM::page_erase_PF( pgm_get_far_address(nvm_store), sizeof(nvm_buffer) ) )
{ /* success */ }

/* ページ書込 */
if ( FlashNVM::page_update_P( &nvm_store, &nvm_buffer, sizeof(nvm_buffer) ) )
{ /* success */ }

if ( FlashNVM::page_update_PF( pgm_get_far_address(nvm_store), &nvm_buffer, sizeof(nvm_buffer) ) )
{ /* success */ }

概念

SPMスニペット

以下の PROGMEMアドレスに、以下の固定値が書かれている。

対応 Bootloader FWV=3.71、HWV=48,50,51,52,53

Series Address マジックナンバー : uint32_t (LE)
megaAVR-0 tinyAVR-0/1/2 (HWV=48) MAPPED_PROGMEM_START + 2 Byte 0x95089361
AVR_DA/DB/DD/DU/EA/EB PROGMEM_START + 2 Byte 0x95089361

MAPPED_PROGMEM_STARTは通常のデータ空間、PROGMEM_STARTは PROGMEM 空間にある。

使用可能なスニペットは2種あるいは4種存在する。 これらは実行コード権限のプログラムカウンタ(PC)検査を回避するために、ここに配置されている。

Offset HWV=48 HWV=50以上 OP-Code
$02 nvm_stz nvm_stz ST Z+, R22
$06 nvm_cmd nvm_ldz LD R24, Z+
$0A - nvm_spm SPM Z+
$0E - nvm_cmd (function)

これらは BOOT領域保護特権で CODE領域 / APPEND領域(そして一部品種の BOOTCODE領域)の FLASH消去/書換を行うのに使うことが出来る。

  • HWV=48は 16bitアドレス品種用のためSPM+が存在せず、LPM/LDの使い分けもない。
  • LDZ/STZは BOOTROW 非採用デバイスでは不要だが、コード互換性のために存在する。
  • C/C++言語からスニペットを呼ぶにはラッパーアセンブリが必要。
  • MCUdudeDxCoreでの同種の機能とは仕様が異なり、相互に互換性はない。

ページ粒度

支援ツールが出来ることは、フラッシュメモリ領域の指定ページを消去することと、 そしてそれに書き込むことだけだ。 消去と書込は個別の機能関数である。

消去操作はフラッシュメモリ領域に固有の ページ粒度 を最小単位とする。 ページ粒度は 使用するAVRの型番に固有 の大きさを持っている。 そのバイト量はPROGMEM_PAGE_SIZE定数で知ることが出来る。

PROGMEM_PAGE_SIZEの大きさは 64、128、512 の何れかだ。256 の存在は現在知られていない。
詳細は[modernAVR 周辺機能比較一覧]フラッシュメモリ量目項を参照されたい。

ページ境界

書き換えるフラッシュメモリ領域の開始点もまたPROGMEM_PAGE_SIZEに支配されている。 従ってそれは正しいページアライメントに整列していなければならないが、 スペースを確保する際にPGM_ALIGN属性を付加することで調整できる。

スケッチプログラム自体を破壊せずに済む、安全な書換可能領域を予約するには NVMEMまたはROMEM属性およびPGM_ALIGN属性を付加し、 PROGMEM_PAGE_SIZEを意識した構造体宣言などを与えなければならない。

ページ数

書換可能な空領域が具体的にどの程度あるかを知ることは難しい。 これはプログラムが実際にビルドされ、リンクプロセスを経るまで知ることは出来ないからだ。 ただし目安として以下の値を参照することは出来る。

宣言名 説明 備考
PROGMEM_END フラッシュメモリ終了番地 0x1FFFF等(16bit以下とは限らない)
__data_load_end フラッシュメモリ未使用先頭番地 要extern参照シンボル

extern const uint8_t* __data_load_end;

__data_load_endに続くページアライメント境界がNVMEM領域最初の先頭位置となる。 従って両者からその差を求めPROGMEM_PAGE_SIZEで割った商は、使用可能な最大ページ数に等しい。

消去と書込

フラッシュメモリ領域を確実に書き換えるにはその前に、そのページ内容を消去しなくてはならない。 これはページ全体を0xFFで埋め尽くすことに等しい。 そしてフラッシュメモリ領域の書込とは、新たな値と現在そこにある値との論理積をとって、それを書き残すことである。

例えば:旧値0xFF AND 新値0x5A → 結果0x5A

もしページ内容を確実に消さずに何らかの以前の値がそこに残っていた場合、 書込後の値は書換えたかった本当の結果とは異なるだろう。

例えば:旧値0x6C AND 新値0x5A → 結果0x48

よって 書換は常に消去と書込操作のペア でなくてはならない。

  • 実装回路レベルで言うと消去は領域内の各ビットに対して0→1に変化させる操作しかできず、
    書込は1→0に変化させる操作しかできない。

一方で、消去操作はフラッシュメモリ領域の寿命を必ず損ねる。 これは書込操作に比して常にダメージが大きい。 そこで書込結果が論理和になることを積極的に利用したストレージ設計を用いる戦略を考えることもできる。 消去と書込が別機能になっているのはそのような用法を可能にする。

例えば、ページ消去操作を減らすために空きページと使用ページの違いを詳細に管理したい場合、 空き/使用ページフラグを管理するビットマップページを論理和書込だけで更新する方法が考えられる。 この場合空きページが足りなくなるまでは一括消去操作(コンパクション)を遅らせることが可能だろう。

動作電圧

NVM領域消去と書換には十分な電力供給が必要で、不足すると不正な結果になりうる。 書換後には<avr/pgmspace.h>に含まれるstrncmp_Pまたはstrncmp_PFで比較確認するのが好ましい。

操作中少なくとも 2.85V 以上は維持されるべきだ。サーボモーターや無線デバイスを操作している最中にこれらを実行するのは危険である。
一部の製品は低電圧下での NVM消去と書き換え結果が保証されていない。eratta情報を確認すること。

<api/memspace.h>

依存性:<avr/io.h> <avr/pgmspace.h>

#define NVMEM

セクション".nvmem"に指定要素を配置する属性。

これを指定した要素は、初期値を与えても無視される。 ゼロ初期化もされない。FRO 確保領域はプログラムコードより後方に配置される。 従って 64KiB 境界より前に配置されるとは限らない。

const char nvm_store[PROGMEM_PAGE_SIZE] PGM_ALIGN NVMEM;

PROGMEM属性との違いは配置される領域が異なるのと初期化されないことで、その他の機能は同じである。 なのでこの領域宣言は(64KiB境界を超えていなければ)<String.h>P()マクロでポインタ参照ができるからprintコマンドでも直接扱える。

#define ROMEM

セクション".progmem.nvm"に指定要素を配置する属性。

これを指定した要素は、初期値を与えることが出来る。 しなければゼロ初期化される。 確保領域はプログラムの前方(64KiB境界以前)の PROGMEM属性領域より後方かつ プログラムコード前方に優先して配置される。

const char nvm_store[] PGM_ALIGN ROMEM = "Hello World!";

PROGMEM属性との違いは配置される領域が異なるだけで、その他の機能は同じである。 なのでこの領域宣言は(64KiB境界を超えていなければ)<String.h>P()マクロでポインタ参照ができるからprintコマンドでも直接扱える。

#define PGM_ALIGN

指定要素をPROGMEM_PAGE_SIZE境界に配置する属性。

これを付加した要素は、ページ内で完結する。 つまりPROGMEM_PAGE_SIZEに満たない大きさの要素は 次の要素までの間に余白が空く。

const char nvm_store0[4] PGM_ALIGN NVMEM;
/* ここに隙間が空く : あるいは隣接するとは限らない */
const char nvm_store1[4] PGM_ALIGN NVMEM;
  • PGM_ALIGNまたはPAGE_ALIGNをない領域に消去/書込を行うと 同一ページ内に属していた他の情報は破壊されうる。

#define PAGE_ALIGN(sect)

後続要素を指定セクション"sect"PROGMEM_PAGE_SIZE境界に配置する属性。 セクション名は ".text" ".progmem" ".nvmem" が有効。

PGM_ALIGN属性と違って効果を発揮するのは直後の同じセクションに属する要素だけである。

PAGE_ALIGN(".nvmem");
const char nvm_store0[4] NVMEM;
/* ここに隙間は空かず密に配置される */
const char nvm_store1[4] NVMEM;

ただしこれは複数の要素を一度に書き換えねばならないのと同義なので メモリ管理を完全に自前で行うのでなければ使用することはないだろう。 普通は構造体(セクター)を定義して、ページ単位で領域割り付けと書き換えを行うべきだ。

  • PGM_ALIGNまたはPAGE_ALIGNを伴わない領域に消去/書込を行うと 同一ページ内に属していた他の情報は破壊されうる。

<FlashNVM.h>

依存性:<avr/io.h> <avr/pgmspace.h> <api/memspace.h>

bool FlashNVM::spm_support_check (void)

現在の環境がFlashNVMツールに対応しているなら真を返す。 これが偽の場合に他のメンバー関数を呼び出した場合の挙動は保証されない。

これは必要なスニペットが特定プログラムアドレスにマジックナンバーとして現れることを確認している。 正しく対応しているブートローダーが存在しなければ、マジックナンバーは合致しないだろう。

bool FlashNVM::page_erase_P (const void* _page_addr, size_t _page_size = 1)

_page_addr位置を含むページを消去する。 成功すると真を返す。

_page_sizeを省略したなら指定位置に該当する1ページだけが消される。 _page_sizeを指定すると(これはページ数ではない)その領域を含む複数ページが消される。 _page_addrがページ境界先頭でなかった場合、 _page_sizeは意図しない複数ページに跨っている場合があり、 その場合は境界を跨がれた全ページも消される。

消去操作はその操作毎に数十ミリ秒の遅滞を生じる。
_page_sizeの最大値は32767(INT_MAX)なので操作1回で消せるのは 32KiB までである。

FlashNVM::page_erase_P( &nvm_store, sizeof(nvm_buffer) )

_page_addrで指定するのは 64KiB境界を超えない範囲でなくてはならない。
現在の実装ではPROGMEM_PAGE_SIZEに同量の追加SRAMを一時使用するため STACK用 SRAM残量には注意が必要。特に AVR_Dx系統は 512byteを一時的に消費する。このため<TaskChanger.h>との併用は要注意だ。

bool FlashNVM::page_erase_PF (const nvmptr_t _page_addr, size_t _page_size = 1)

page_erase_Pを 64KiB を超える範囲に拡張した版。 _page_addrは常にpgm_get_far_addressマクロを介して与える。 成功すると真を返す。

FlashNVM::page_erase_PF( pgm_get_far_address(nvm_store), sizeof(nvm_buffer) )

フラッシュ総量が 64KiB以下の環境で使っても問題はない。

bool FlashNVM::page_update_P (const void* _page_addr, const void* _data_addr, size_t _save_size)

_page_addr位置に対してSRAM領域_data_addrから_save_sizeバイトを書き込む。 _page_addrが指す領域は事前に消去されていることが期待される。 成功すると真を返す。

FlashNVM::page_update_P( &nvm_store, &nvm_buffer, sizeof(nvm_buffer) );

_page_addrで指定するのは 64KiB境界を超えない範囲でなくてはならない。

bool FlashNVM::page_update_PF (const nvmptr_t _page_addr, const void* _data_addr, size_t _save_size)

page_update_Pを 64KiB を超える範囲に拡張した版。 _page_addrは常にpgm_get_far_addressマクロを介して与える。 成功すると真を返す。

FlashNVM::page_update_PF( pgm_get_far_address(nvm_store), &nvm_buffer, sizeof(nvm_buffer) );

フラッシュ総量が 64KiB以下の環境で使っても問題はない。

bool FlashNVM::bootrow_clear (void)

BOOTROW 領域を消去する。NVM操作に成功すれば真を返す。

  • この関数は AVR_DU または AVR_EB 系統専用であり、他の品種では定義されていない。
  • BOOTROW 領域は全体で1ページのフラッシュメモリ領域である。データ保存専用で、コード実行はできない。

void FlashNVM::bootrow_load (void* _data_addr, size_t _save_size = BOOTROW_SIZE)

BOOTROW 領域を _data_addr が示すデータメモリに複写する。先頭からのバイト数は _save_size で与える。省略時は BOOTROW_SIZE 定数の値に従う。NVM操作に成功すれば真を返す。

読み込み開始位置は BOOTROW 領域先頭に固定である。

この関数はbootrow_saveを使う事前準備としての、構造体に複写したりするショートカットを提供する。

  • この関数は AVR_DU または AVR_EB 系統専用であり、他の品種では定義されていない。

bool FlashNVM::bootrow_save (const void* _data_addr, size_t _save_size = BOOTROW_SIZE)

BOOTROW 領域へ _data_addr が示すデータメモリ内容で書き換える。先頭からのバイト数は _save_size で与える。省略時は BOOTROW_SIZE 定数の値に従う。NVM操作に成功すれば真を返す。

書き込み開始位置は BOOTROW 領域先頭に固定である。 この関数はpage_update_P*関数同様に現在値との論理積を結果として残す。つまり事前にbootrow_clear関数で BOOTROW 領域全体を消去しておく前提だが、現在値を変更したくないバイト値には 0xFF を書くことで追記操作が可能である。

  • この関数は AVR_DU または AVR_EB 系統専用であり、他の品種では定義されていない。
uint8_t buf[BOOTROW_SIZE];
FlashNVM::bootrow_load(&buf);
Serial.printHex((const void *)&buf, sizeof(buf), ' ', 16).ln().ln();

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
  if ( FlashNVM::bootrow_clear()
    && FlashNVM::bootrow_save(&buf, sizeof(buf))
  )    Serial.println(F("[success]"));
  else Serial.print(F("*failed* 0x")).println(NVMCTRL_STATUS, ZHEX, 2);
}

Note

bootrow_clear / bootrow_save は正常終了しても NVMCTRL_STATUS0x20 を返却する。(WRITEPROTECT 領域への書き込み検出)

bool FlashNVM::bootrow_verify (const void* _data_addr, size_t _save_size = BOOTROW_SIZE)

BOOTROW 領域と _data_addr が示すデータメモリ内容で比較一致を試みる。先頭からのバイト数は _save_size で与える。省略時は BOOTROW_SIZE 定数の値に従う。一致すれば真を返す。

検査開始位置は BOOTROW 領域先頭に固定である。

  • この関数は AVR_DU または AVR_EB 系統専用であり、他の品種では定義されていない。
/* 比較ベリファイ */
if (FlashNVM::bootrow_verify(&nvm_buffer, sizeof(nvm_buffer)))
{ /* success */ }

LINKS

multix.jp/てくにかるむ(休眠中)
Multix Zinnia Product SDK [*AVR]
AVR.JP(日本語訳)
AVR-LIBC(日本語訳)

Clone this wiki locally