背景
組込み機器のMCUのファームウェアを書き換える方法はいくつかあります。
よく利用されるのは
- 専用ライターでデバッグ端子を使って書き換え
- PCとUSBで接続して、PCアプリを使って書き換える
というものでしょうか。
専用ライターはMCUメーカーだったり、サードパーティーから販売されています。
これ、意外と高価でして、5〜10万円強もします!
でも、メーカーの開発・製造現場で使うものとしては許容できる価格帯ですね。
しかし、市場で不具合や機能アップのためにファームウェアを変更するとしたらどうでしょう?
サービスマンが携帯したり、ユーザーにお願いするには上記の手法は使いづらいです。
そこで、私がメーカー勤務時に思いついた「USBメモリを使ってファームウェアを書き換える手法」をご紹介します。
対象MCU
本記事ではSTマイクロのSTM32F407を対象にします。
それ以外のSTM32、ルネサスのRX111でもUSBメモリを使ったファームウェアアップデートを開発しましたが、これらは後日気が向いたら記事にしようと思います。
STM32F407を搭載したマイコンボードはDiscoveryシリーズで提供されていて、容易に入手できます。
興味を持っていただけるようでしたら、ぜひ試してみてください!
ファームウェアの動作環境
基本はベアメタルで、RTOSなどのOSを使わないことを前提としています。
と言いながらも、uITRON準拠OSでの実績はあります。
USBメモリによるファームウェアアップデート用プログラム「アップデータ」を開発をする
プログラム自体の開発は、それほど難しくはありません。
これを実現するための仕組みを考えるのが大変でした。
ただ、これはARMマイコンのアーキテクチャだからできるという側面もあります。
一昔前に大ヒットした日立製作所の「SH」でも同じことができます。
実際にん〜十年前にやったことがあります。
アップデータに必要な機能
アップデータに必要な機能は下記に挙げるようなものです。
- アップデートの要・不要を判断する
- USBメモリにアクセスできること
- USBメモリ内のファイルを解析すること
- STM32F407の内蔵フラッシュを書き換えられること
- 新しいファームウェアへの実行移行
アップデートの要・不要を判断する
この方式を採用するにあたり、電源起動時にはまずこの「アップデータ」が起動することになります。
このアップデータが、ファームのアップデートが必要であれば、USBメモリからプログラムを読み込んで、内蔵フラッシュを書き換えます。
ファームのアップデートが不要なら、ファームへ実行アドレスを移すという動きをします。
この判断にはいくつか方法が考えられますね。
- ディップスイッチでファーム書き換えの要・不要を指定
- USBメモリが見つかって、特定のファイルの有無で判断
など、いくつか考えられます。
手っ取り早いのは前者です。
後者はサービスマンに親切ですね。
これは利用する環境や会社の風土に応じて決めればいいでしょう。
USBメモリにアクセスできること
これは難しくないですね。
STM32CubeMXを使って
- USB Driver
- USB Host Mass Strage Class
- File System (FatFs)
を利用できるよにすればOKです。
STマイクロからドライバやミドルウェアが提供されています。
そのまま利用させてもらいましょう。
USBメモリ内のファイルを解析すること
書き込み用のデータの代表格は
- Motorola S Record
- Intel Hex
です。
どちらでもよく、どのアドレスに何のデータが書くかを判定できればOKです。
個人的にはテキストエディタで見ることができる「Motorola S Record」が好きです。
Motorola S Recordはウィキペディアに解説がありました。
STM32F407の内蔵フラッシュを書き換えられること
STM32マイコンはここが優秀です!
内蔵フラッシュを書き込む関数が用意されているのはどのメーカーも同じですが、プログラムが内蔵フラッシュで動いていても、内蔵フラッシュを書き換えることができます。
国内ベンダーだと、内蔵フラッシュを書き換えるプログラムはRAM上にないと、内蔵フラッシュを書き換えることができません。
STマイクロのフラッシュコントローラは優秀です!
しかも、1バイトずつ書き込むことができます。
他社の内蔵フラッシュでは、512バイトのアラインメント縛りがありました。
セクタの消去
void eraseSector(int sectorNumber)
{
uint32_t pageError = 0;
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS; // select sector
erase.Sector = sectorNumber; // set selector11
erase.NbSectors = 1; // set to erase one sector
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; // set voltage range (2.7 to 3.6V)
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&erase, &pageError); // erase sector
HAL_FLASH_Lock();
}
1バイトプログラム
void write1Byte(unsigned long addr, char data)
{
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr, data);
HAL_FLASH_Lock();
}
複数バイトプログラム
void writeBytes(unsigned long startAddr, const char* container, int length)
{
HAL_FLASH_Unlock();
while(length-- != 0)
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, startAddr++, *container++);
HAL_FLASH_Lock();
}
新しいファームウェアへの実行移行
新しいファームウェアを書き込んだあとは、通常はリセットを待つのがいいですね。
あとはSTM32のソフトウェアリセットの機能を利用すれば、勝手にリセットして起動するからいいかもしれません。
アップデータが起動し、アップデートが必要ない時、どうやってファームウェアの実行をするか...
これは、単純にプログラム・カウンタを書き換えるだけでいけます。
アップデータが起動した時点で、スタックポインタは設定されていてすでに動作しています。
C言語での記述は、ジャンプ先を直にアドレスで指定します。
((void(*)(void))0x08010004)( );
ファームウェアのリンク設定の変更
電源を入れた時、まずアップデータが起動するので、アップデータは0番地スタートです。
STM32F407では0x0800_0000番地が該当します。
そのため、ファームウェア(メインのアプリ)はここからずらした場所に配置しなくてはいけません。
「何バイト先に配置するか」はアップデータのサイズによります。
私が当時開発したものは、シリアル通信でプログラムの状態を表示したり、簡単なファイルブラウザも実装しているため、おおよそ64KByte以内で収めるようにしていました。
そのため、単純なアップデートシステムならプログラムは64KByte先...つまり、0x0801_0000番地というわけです。
system_stm32f4xx.cの修正
ARM-Cortex M4はベクタテーブルの配置を自由に決められます。
Core/Src/system_stm32f4x.cの中でベクタテーブルの配置を決めています。
USER_VECT_TAB_ADDRESSというマクロがコメントアウトされているので、まずはこのマクロを生かします。
#define USER_VECT_TAB_ADDRESS
次に、VECT_TAB_OFFSETを変更します。
VECT_TAB_OFFSETの初期値は0x0000_0000で、実際はこの数値に0x0800_0000を加算されたものがベクタテーブルのアドレスになります。
VECT_TAB_OFFSETを64KByteに変更します。
#define VECT_TAB_OFFSET 0x00010000U
リンカスクリプトの修正
リンカスクリプトSTM32F407VGTX_FLASH.ldの中身を見てみましょう。
初期状態は下記のようになっています。
/* Memories definition */
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
プログラム配置の先頭が0x0800_0000になっているので、これを64KByte先のアドレスに変更します。
また、内蔵フラッシュの容量を64KByte引きます。
/* Memories definition */
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 960K
}
アップデータからメインプログラムへのジャンプ
書き込む先頭アドレスは0x0801_0000番地とわかっています。
しかし、ジャンプ先は0x0801_0000番地ではなく、0x0801_0004番地です。
詳細はARM Cortex-M4の資料を見ていただくとわかります。
0番地はスタックポインタの初期値が格納されています。
ジャンプ先は4番地に格納されています。
なので、ジャンプ先は0x0801_0004番地なのです。
((void(*)(void))0x08010004)( );
出来上がったSレコード
Sレコードをテキストエディタで見てみましょう。
プログラムの開始位置が0x0801_0000になっています。
これを内蔵フラッシュに書き込めばOKです。
実運用上は...
今回は主に仕組みと、簡単な使い方を解説しました。
実際はいろんなガード処理や、決まり事が必要です。
* 決まったファイル名しか書き換えない
* ダウングレードを許すか
* ファイルブラウザはあったほうがいい
* アップデートの過程をUARTに出力する
* 失敗した時の表示(LED点滅とか)
STM32F407では、ガードなどの処理を省けば、ひとまず簡単に試すことができます。
ちなみに、STM32F407だけでなく、他のSTM32Fシリーズでも同じようなことができることを確認しています。
最後に
もともと、ファームウェア書き換えの専用機が高価で、サービスマンの人数分用意できないという要望から、細々と開発をしたものです。
実際使ってみると、現場での書き換えがUSBメモリを挿すだけでいいので、結構簡単で便利です。
いろいろ機能を追加したり、セキュリティなどを工夫することで、ファームウェアのアップデート作業が効率よくできるようになると思います。
ぜひ活用してみてください。