Skip to content

Instantly share code, notes, and snippets.

@betaEncoder
Last active September 22, 2020 15:03
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save betaEncoder/1f056a51d93aae489df260cf29052918 to your computer and use it in GitHub Desktop.
Save betaEncoder/1f056a51d93aae489df260cf29052918 to your computer and use it in GitHub Desktop.
Tips for STM32 self programming

STM32のFlashメモリを書き換える時のメモ

アプリケーションによっては,製品出荷時にユニークなIDを記憶させたり,キャリブレーション値を保持する時にMCU内蔵のEEPROMへデータを書き込む事があります.
しかしながら,STM32シリーズのチップにはEEPROMがありません.どうやらFlashメモリをEEPROMのように書き換えて使う想定のようです.

Flash特有の懸念

FLASHメモリと言えば書き換え可能な回数が限られており,頻繁な書き換えは信頼性の低下が懸念されます.
そこでデータシートを参照してみると,最低でも1万回の書き換えが保証されているようでした.   これならループを回して書き換えたりしない限り,メモリの劣化は考えなくても良さそうです.
懸念はそれだけではありません.Flashメモリは1バイトづつ書き込む事はできるものの,消去はセクタ単位での一括消去となります.故に,書き換える場合には以下の手順が必要です.

書き換えたいセクタをSRAMへ読み出す
当該セクタを消去
SRAM上で書き換え
SRAMからFlashへ書き戻し

一般にFlashメモリのセクタサイズは数kByteから数百kByteと(組込みの世界では)比較的大きな単位になります.
そのため,セクタサイズが余りに大きいと書き込んであるデータをSRAMへ退避する事ができない場合があります.

STM32のFlashメモリ構成

STM32F411xCのメモリ構成を見てみましょう.
datasheet Sector0からSector7までの8つのセクタで構成されており,セクタサイズは16kByteから128kByteのようです.
更に,Flashメモリの後方へ行くほどセクタサイズは大きくなるようでSector5以降は128kByteものサイズになっています.
一般にプログラムはメモリの先頭から書き込まれていくのでメモリ後方ほど空いていることが期待できるため,後方の領域を書き換えて使いたくなります.
しかし前述した書き換え手順を踏むとすると,128kByteのSRAMを持つMCUで128kByteのSectorを丸ごと読み出すのは不可能です.
一方,16kByteのSector0についてはリセットベクタ等を含む割り込みベクタテーブルを配置しなければならず,アプリケーションを動作をさせる上で動かすことのできない重要な情報を配置するセクタになっています.
Sector0の後半を書き換え領域に使うアイデアもアリですが,リスクが付き纏います.   例えば,Sector0のerase直後や書き換え中に電源が落ちたりリセットしてしまった場合にはベクタテーブルが破壊されるため,暴走や文鎮化する可能性があります.

Sector1を書き換え用領域として予約する

そこで,リンカスクリプトを編集してSector1をアプリケーションから書き換えできる領域として予約することにします.16kByteならばSRAMへデータ退避用領域を確保できるでしょう.
STM32の開発を System Workbench for STM32 で CubeMX を使っている場合にはリンカスクリプトが自動生成され,次のようにメモリが定義されていました.
(STM32F411RETx_FLASH.ld)

MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 512K
}

Flashメモリが512kByteの一塊になっているので,これをセクタの切れ目で3つに分割します.

MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
FLASH0 (rx)      : ORIGIN = 0x8000000, LENGTH = 16K
FLASH1 (rx)      : ORIGIN = 0x8004000, LENGTH = 16K
FLASH2 (rx)      : ORIGIN = 0x8008000, LENGTH = 480K
}

このメモリ定義に合わせてセクションの定義を書き換えます.
Sector0に配置しなければならない.isr_vectorFlash0に,その他のセクションをSector2以降のFlash2に配置されるようにします.

  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH0
  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH2
(以下略)

こんな感じ.
これだけでもSector1には何もデータが書き込まれないことが保証されますが,明示的にセクションを追加しておきます.

  .userdata : {
    __USER_DATA = .;
    KEEP(*(.userdata))
    FILL(0xff)
  } > FLASH1

わかりやすくシンボル__USER_DATAを使ってコードからこのセクタへアクセスできるようにしました.
buildするにあたって,userdata領域へ書き込むオブジェクトは何もないのでオプトアウトされないようKEEPしておきます.
ついでにFlashメモリの消去後の初期値は0xffなので空き領域を0xffFILLしておきました.

Buildしてmapファイルを確認する

一旦buildして生成されたmapファイルを覗いて期待した通りに配置されたかどうか確認します.
(output.map)

(前略)
.isr_vector     0x08000000      0x198
                0x08000000                . = ALIGN (0x4)
 *(.isr_vector)
 .isr_vector    0x08000000      0x198 startup/startup_stm32f411xe.o
                0x08000000                g_pfnVectors
                0x08000198                . = ALIGN (0x4)

.userdata       0x08004000        0x0
                0x08004000                __USER_DATA = .
 *(.userdata)
 FILL mask 0xff

.text           0x08008000     0x4ec0
(後略)

うまくいったようです.

コードからアクセスする方法

Cソースの適当な場所で
extern const void * __USER_DATA;
を宣言しておく事でSector1の先頭アドレスが得られます.
STM32のHALドライバにはFLashを書き換えるためのドライバが用意されています.これを使えば
HAL_FLASH_Program(TYPEPROGRAM_BYTE, __USER_DATA, hoge);
このように書き込むことができます.

@peterleif
Copy link

Cソースの適当な場所で
extern const void * __USER_DATA;
を宣言しておく事でSector1の先頭アドレスが得られます.

__USER_DATA「の」アドレスを参照することでSector 1の先頭アドレスが得られます。
アドレスを取得するには&__USER_DATAとする必要があるので、__USER_DATAをポインタとして宣言しておくことでアドレスを取得できるような書き方はミスリーディングかと思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment