STM32F407のファームウェアをUSBメモリでアップデートする

## 背景
組込み機器の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メモリが見つかって、特定のファイルの有無で判断

など、いくつか考えられます。
手っ取り早いのは前者です。
後者はサービスマンに親切ですね。
これは利用する環境や会社の風土に応じて決めればいいでしょう。
![スクリーンショット 2025-03-09 21.44.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/f52846db-1380-41fa-9a28-d25c541819a5.png)

### 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は[ウィキペディア](https://ja.wikipedia.org/wiki/S-record)に解説がありました。

### STM32F407の内蔵フラッシュを書き換えられること
STM32マイコンはここが優秀です！
内蔵フラッシュを書き込む関数が用意されているのはどのメーカーも同じですが、プログラムが内蔵フラッシュで動いていても、内蔵フラッシュを書き換えることができます。
国内ベンダーだと、内蔵フラッシュを書き換えるプログラムはRAM上にないと、内蔵フラッシュを書き換えることができません。
STマイクロのフラッシュコントローラは優秀です！
しかも、1バイトずつ書き込むことができます。
他社の内蔵フラッシュでは、512バイトのアラインメント縛りがありました。

#### セクタの消去
```c:セクタの消去
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バイトプログラム
```c:1バイトプログラム
void write1Byte(unsigned long addr, char data)
{
	HAL_FLASH_Unlock();
	HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr, data);
	HAL_FLASH_Lock();
}
```
#### 複数バイトプログラム
```c:複数バイトプログラム
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言語での記述は、ジャンプ先を直にアドレスで指定します。
```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**というマクロがコメントアウトされているので、まずはこのマクロを生かします。
```c:このマクロを有効にする
#define USER_VECT_TAB_ADDRESS
```
次に、**VECT_TAB_OFFSET**を変更します。
VECT_TAB_OFFSETの初期値は0x0000_0000で、実際はこの数値に0x0800_0000を加算されたものがベクタテーブルのアドレスになります。
VECT_TAB_OFFSETを64KByteに変更します。
```c:ベクタテーブルのオフセットを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番地なのです。
```c:メインプログラムへジャンプ
((void(*)(void))0x08010004)( );
```

## 出来上がったSレコード
Sレコードをテキストエディタで見てみましょう。
プログラムの開始位置が0x0801_0000になっています。
これを内蔵フラッシュに書き込めばOKです。

## 実運用上は...
今回は主に仕組みと、簡単な使い方を解説しました。
実際はいろんなガード処理や、決まり事が必要です。

*　決まったファイル名しか書き換えない
*　ダウングレードを許すか
*　ファイルブラウザはあったほうがいい
*　アップデートの過程をUARTに出力する
*　失敗した時の表示(LED点滅とか)

STM32F407では、ガードなどの処理を省けば、ひとまず簡単に試すことができます。
ちなみに、STM32F407だけでなく、他のSTM32Fシリーズでも同じようなことができることを確認しています。

## 最後に

もともと、ファームウェア書き換えの専用機が高価で、サービスマンの人数分用意できないという要望から、細々と開発をしたものです。
実際使ってみると、現場での書き換えがUSBメモリを挿すだけでいいので、結構簡単で便利です。

いろいろ機能を追加したり、セキュリティなどを工夫することで、ファームウェアのアップデート作業が効率よくできるようになると思います。

ぜひ活用してみてください。