KV260 が Linux をブートするまでのシーケンス (Image Selector編)

かつて投稿した[『KV260 が Linux をブートするまでのシーケンス』](https://qiita.com/ikwzm/items/aca938c98b636c0048bc)では Image Selector の所を省略してしまいましたが、この記事では Image Selector を詳細に説明します。また、前回投稿した[『KV260 の ブートファームウェアを更新』](https://qiita.com/ikwzm/items/4e5e954eabc988bd78d5) の「ファームウェアイメージの更新に失敗した場合の復帰方法」で、内部的にどのように動作しているかの説明も行います。


## Image Selector とは

[『KV260 が Linux をブートするまでのシーケンス』](https://qiita.com/ikwzm/items/aca938c98b636c0048bc)で説明したように、ZynqMP のステージ0ブートローダーはプライマリーブートデバイス(KV260 では SOM にある QSPI に接続されたフラッシュメモリ)の所定のアドレスにある BOOT.BIN 内の FSBL(First Stage Boot Loader) を内部メモリにロードして実行します。

しかし、KV260 では FSBL の代わりに Image Selector が入っています。Image Selector は、プライマリーブートデバイスに入っている二つ(リカバリー用を含めれば３つ) の BOOT.BIN から選択してロードします。


## KV260 のプライマリーブートデバイスの構造

KV260 のプライマリーブートデバイス(SOM にある QSPI に接続されたフラッシュメモリ)の構造は次のようになっています。

| デバイス名   |    開始位置   |  サイズ       |  説明 |
|:-----------|:------------:|:-----------:|:---------------------------------|
| /dev/mtd0  | 0x0000_00000 | 0x0080_0000 | Image Selector                   |
| /dev/mtd1  | 0x0008_00000 | 0x0080_0000 | Image Selector Golden            |
| /dev/mtd2  | 0x0010_00000 | 0x0002_0000 | Persistent Register              |
| /dev/mtd3  | 0x0012_00000 | 0x0002_0000 | Persistent Register Backup       |
| /dev/mtd4  | 0x0014_00000 | 0x000C_0000 | Open_1                           |
| /dev/mtd5  | 0x0020_00000 | 0x00D0_0000 | Image A (FSBL, PMU, ATF, U-Boot) | 
| /dev/mtd6  | 0x00F0_00000 | 0x0008_0000 | IMGSel Image A Cache             |
| /dev/mtd7  | 0x00F8_00000 | 0x00D0_0000 | Image B (FSBL, PMU, ATF, U-Boot) | 
| /dev/mtd8  | 0x01C8_00000 | 0x0008_0000 | IMGSel Image B Cache             |
| /dev/mtd9  | 0x01D0_00000 | 0x0010_0000 | Open_2                           |
| /dev/mtd10 | 0x01E0_00000 | 0x0020_0000 | Recovery Image                   |
| /dev/mtd11 | 0x0200_00000 | 0x0020_0000 | Recovery Image Backup            |
| /dev/mtd12 | 0x0220_00000 | 0x0002_0000 | U-Boot Storage variables         |
| /dev/mtd13 | 0x0222_00000 | 0x0002_0000 | U-Boot Storage variables Backup  |
| /dev/mtd14 | 0x0224_00000 | 0x0001_0000 | SHA256                           |
| /dev/mtd15 | 0x0225_00000 | 0x01DB_0000 | User                             |

フラッシュメモリの先頭アドレスにあるのが Image Selector です。この Image Selector が Image A や Image B (場合によっては Recovery Image) を選択して、実行します。

## Image Selector のフォーマット

ステージ0ブートローダーが内部メモリにロードして実行するために、Image Selector は BOOT.BIN のフォーマットに準拠しています。
試しに bootgen コマンドで読んでみると次のようになります。executable.elf が Image Selector の実行プログラムです。

```console
root@debian-fpga:~# bootgen -read /dev/mtd0


****** Xilinx Bootgen v2019.2
  **** Build date : Jan 16 2020-08:00:00
    ** Copyright 1986-2019 Xilinx, Inc. All Rights Reserved.

--------------------------------------------------------------------------------
   BOOT HEADER
--------------------------------------------------------------------------------
        boot_vectors (0x00) : 0x1400000014000000140000001400000014000000140000001400000014000000
     width_detection (0x20) : 0xaa995566
            image_id (0x24) : 0x584c4e58
 encryption_keystore (0x28) : 0x00000000
      header_version (0x2c) : 0xfffc0000
   fsbl_sourceoffset (0x30) : 0x00002800
         fsbl_length (0x34) : 0x00000000
   fsbl_load_address (0x38) : 0x00000000
   fsbl_exec_address (0x3C) : 0x0000f048
   fsbl_total_length (0x40) : 0x0000f048
    qspi_config-word (0x44) : 0x00000800
            checksum (0x48) : 0xfd1c4bb1
          iht_offset (0x98) : 0x000008c0
          pht_offset (0x9c) : 0x00001100
--------------------------------------------------------------------------------
   IMAGE HEADER TABLE
--------------------------------------------------------------------------------
             version (0x00) : 0x01020000        total_images (0x04) : 0x00000001
          pht_offset (0x08) : 0x00001100           ih_offset (0x0c) : 0x00000900
       hdr_ac_offset (0x10) : 0x00000000
--------------------------------------------------------------------------------
   IMAGE HEADER (executable.elf)
--------------------------------------------------------------------------------
          next_ih(W) (0x00) : 0x00000000
         next_pht(W) (0x04) : 0x00000440
    total_partitions (0x08) : 0x00000000
    total_partitions (0x0c) : 0x00000001
                name (0x10) : executable.elf
--------------------------------------------------------------------------------
   PARTITION HEADER TABLE (executable.elf.0)
--------------------------------------------------------------------------------
    encrypted_length (0x00) : 0x00003c12  unencrypted_length (0x04) : 0x00003c12
        total_length (0x08) : 0x00003c12           load_addr (0x0c) : 0x00000000
           exec_addr (0x10) : 0xfffc0000    partition_offset (0x14) : 0x00000000
          attributes (0x18) : 0xfffc0000       section_count (0x1C) : 0x00000000
     checksum_offset (0x20) : 0x00000a00          iht_offset (0x24) : 0x00000116
           ac_offset (0x28) : 0x00000001            checksum (0x3c) : 0x00073e72
 attribute list -
               trustzone [non-secure]            el [el-0]
              exec_state [aarch-64]     dest_device [none]
              encryption [no]                  core [none]
--------------------------------------------------------------------------------
   AUTHENTICATION CERTIFICATE (executable.elf.0)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
```

## Image Selector のソースコード

Image Selector のソースコードは Xilinx が Github にて MIT ライセンスで公開しています。

 * https://github.com/Xilinx/embeddedsw/tree/master/lib/sw_apps/imgsel

Image Selector はプライマリーデバイスの Persistent Register に格納された内容に従ってブートするイメージを選択します。イメージを選択する部分は lib/sw_apps/imgsel/src/xis_update_a_b.c の XIs_UpdateABMultiBootValue() です。

```C:lib/sw_apps/imgsel/src/xis_update_a_b.c
/*****************************************************************************/
/**
 * This function is used to update the multiboot value
 * @param	None.
 *
 * @return	returns error value defined in xis_error.h file on failure
 *			returns XST_SUCCESS on success
 *
 *
 ******************************************************************************/
int XIs_UpdateABMultiBootValue(void)
{
	int Status = XST_FAILURE;
	u32 *PerstRegPtr;
	u32 Offset;
	u8 CurrentImage;
	u8 ReadDataBuffer[XIS_SIZE_4K] __attribute__ ((aligned(32U)));

	Status = XIs_QspiInit();
	if (Status != XST_SUCCESS) {
		XIs_Printf(DEBUG_GENERAL, "QSPI Init failed\r\n");
		goto END;
	}

	Status = XIs_QspiRead(XIS_PERS_REGISTER_BASE_ADDRESS,
					(u8 *)ReadDataBuffer, XIS_SIZE_4K);
	if (Status != XST_SUCCESS) {
		XIs_Printf(DEBUG_GENERAL, "QSPI Read failed\r\n");
		goto END;
	}
	Status = XIs_DataValidations(ReadDataBuffer);
	if(Status != XST_SUCCESS) {
		XIs_Printf(DEBUG_GENERAL, "Data Validations Failed\r\n");
		goto END;
	}

	/**
	 * If Image A and Image B are both are marked as bootable,
	 * Requested image will be loaded
	 * If Image A or Image B any one of the image is marked as bootable,
	 * Requested image is not bootable bootable image is loaded
	 * If Last Image booted is non-bootable, bootable image will be loaded
	 * If None of the Image is marked as bootable, recovery image is loaded
	 */
	if((ReadDataBuffer[XIS_IMAGE_A_BOOTABLE] == TRUE) ||
			(ReadDataBuffer[XIS_IMAGE_B_BOOTABLE] == TRUE)) {
		if((ReadDataBuffer[XIS_LAST_BOOTED_IMAGE] == XIS_IMAGE_A) &&
					(ReadDataBuffer[XIS_IMAGE_A_BOOTABLE] == FALSE)) {
			CurrentImage = XIS_IMAGE_B;
			PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_B_OFFSET];
		}
		else if((ReadDataBuffer[XIS_LAST_BOOTED_IMAGE]
					== XIS_IMAGE_B) &&
					(ReadDataBuffer[XIS_IMAGE_B_BOOTABLE] == FALSE)){
			CurrentImage = XIS_IMAGE_A;
			PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_A_OFFSET];
		}
		else {
			if(ReadDataBuffer[XIS_REQUESTED_BOOT_IMAGE]
					== XIS_IMAGE_A) {
				CurrentImage = XIS_IMAGE_A;
				PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_A_OFFSET];
			}
			else {
				CurrentImage = XIS_IMAGE_B;
				PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_B_OFFSET];
			}
		}
	}
	else {
		PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_RECOVERY_IMAGE_OFFSET];
		Offset = (u32)(*PerstRegPtr / XIS_SIZE_32KB);
		XIs_UpdateMultiBootValue(Offset);
		goto END;
	}

	Offset = (u32)(*PerstRegPtr / XIS_SIZE_32KB);
	XIs_UpdateMultiBootValue(Offset);

	if(ReadDataBuffer[XIS_LAST_BOOTED_IMAGE] != CurrentImage) {
		ReadDataBuffer[XIS_LAST_BOOTED_IMAGE] = CurrentImage;
		(void)XIs_CheckSumCalculation((u32*)ReadDataBuffer, (u8)TRUE);
		Status = XIs_QspiWrite(XIS_PERS_REGISTER_BASE_ADDRESS,
						(u8 *)ReadDataBuffer, XIS_SIZE_4K);
		if(Status != XST_SUCCESS) {
			XIs_Printf(DEBUG_GENERAL, "QSPI Last image booted"
                                  " Write failed\r\n");
			goto END;
		}

		Status = XIs_QspiWrite(XIS_PERS_REGISTER_BACKUP_ADDRESS,
						(u8 *)ReadDataBuffer, XIS_SIZE_4K);
		if(Status != XST_SUCCESS) {
			XIs_Printf(DEBUG_GENERAL, "QSPI Last image booted"
                                  " Backup Write failed\r\n");
			goto END;
		}
	}

END:
	return Status;
}
```

この関数は次のように大きく３つに分けることができます。

  1. Persistent Register のロード
  2. Persistent Register の内容に基づいてイメージを選択
  3. どのイメージを選択したかを Persistent Register にストア

この記事では 2. のイメージ選択を次節でもう少し詳しく説明します。

## Image Selector のイメージ選択アルゴリズム

Image Selector の選択アルゴリズムは次の URL で詳しく説明されています。

  * [Boot Firmware Overview - A/B Boot Concept](https://xilinx.github.io/kria-apps-docs/bootfw/build/html/docs/bootfw_overview.html#a-b-boot-concept)

このページでは次のように説明されています。

  1. 両方のイメージが「起動可能」とマークされている場合、ImageSelectorは要求されたイメージを起動します。(いわゆる Normal Boot)
  2. 両方のイメージが「非起動可能」とマークされている場合、この状態はFWが不良状態にあることを意味します。これは、どちらのイメージもLinuxまで起動が確認されないため、システムはImageSelectorアプリケーションのImageRecoveryを起動します。通常のA/B更新メカニズムを使用している場合、この状態は発生しないことに注意してください。(いわゆる Error Recovery)
  3. 要求されたイメージが起動不可能で、最後に起動されたイメージが起動可能である場合、要求されたイメージが前回の起動中に更新されたことを意味します。
  4. 要求されたイメージが起動できず、最後に起動されたイメージが起動できない場合は、新しく更新されたイメージが起動に失敗したことを意味します。この場合、イメージセレクターは既知の正常なイメージにフォールバックします。

1.の Normal Boot と 2. の Error Recovery はわかるのですが、3.と4.が実際にどう動くのか理解しにくいです。

そこで前節で紹介した XIs_UpdateABMultiBootValue() のイメージ選択の部分だけをみてみると次のようになります。

```C:lib/sw_apps/imgsel/src/xis_update_a_b.c
	/**
	 * If Image A and Image B are both are marked as bootable,
	 * Requested image will be loaded
	 * If Image A or Image B any one of the image is marked as bootable,
	 * Requested image is not bootable bootable image is loaded
	 * If Last Image booted is non-bootable, bootable image will be loaded
	 * If None of the Image is marked as bootable, recovery image is loaded
	 */
	if((ReadDataBuffer[XIS_IMAGE_A_BOOTABLE] == TRUE) ||
			(ReadDataBuffer[XIS_IMAGE_B_BOOTABLE] == TRUE)) {
		if((ReadDataBuffer[XIS_LAST_BOOTED_IMAGE] == XIS_IMAGE_A) &&
					(ReadDataBuffer[XIS_IMAGE_A_BOOTABLE] == FALSE)) {
			CurrentImage = XIS_IMAGE_B;
			PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_B_OFFSET];
		}
		else if((ReadDataBuffer[XIS_LAST_BOOTED_IMAGE]
					== XIS_IMAGE_B) &&
					(ReadDataBuffer[XIS_IMAGE_B_BOOTABLE] == FALSE)){
			CurrentImage = XIS_IMAGE_A;
			PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_A_OFFSET];
		}
		else {
			if(ReadDataBuffer[XIS_REQUESTED_BOOT_IMAGE]
					== XIS_IMAGE_A) {
				CurrentImage = XIS_IMAGE_A;
				PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_A_OFFSET];
			}
			else {
				CurrentImage = XIS_IMAGE_B;
				PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_IMAGE_B_OFFSET];
			}
		}
	}
	else {
		PerstRegPtr = (u32 *)&ReadDataBuffer[XIS_RECOVERY_IMAGE_OFFSET];
		Offset = (u32)(*PerstRegPtr / XIS_SIZE_32KB);
		XIs_UpdateMultiBootValue(Offset);
		goto END;
	}
```

ここでちょっと問題なのは、コメントの内容と実際に記述されている C のコードとが微妙に違っていることです。

コメントでは

  1. イメージAとイメージBの両方が「起動可能」としてマークされている場合、要求されたイメージがロードされます。
  2. イメージAまたはイメージBのどちらか一方のイメージが「起動可能」としてマークされている場合、要求されたイメージは起動可能ではありませんので、起動可能なイメージがロードされます。
  3. 最後に起動したイメージが起動できない場合、起動可能なイメージが読み込まれます。
  4. どのイメージも「起動可能」としてマークされていない場合、リカバリイメージがロードされます。

となっています。

一方、C のコードでは

  1. いずれかのイメージが「起動可能」な場合、最後に起動したイメージが起動可能でない場合はもう片方のイメージがロードされます。
  2. いずれかのイメージが「起動可能」な場合かつ 1 でない場合は、要求されたイメージがロードされます。
  3. どのイメージも「起動可能」としてマークされていない場合、リカバリイメージがロードされます。

となっています。

まあ、結局 C のコードみるのが一番確実で分かりやすいですね。

[『KV260 の ブートファームウェアを更新』](https://qiita.com/ikwzm/items/4e5e954eabc988bd78d5) で説明した「ファームウェアイメージの更新に失敗した場合の復帰方法」で、正常にブートするファームウェアを書き込まなければいけない理由は、結局のところ Requested Boot Image が正常なブートイメージを指していないと、いつまでもブートに失敗し続けるということです。

## Image Selector の状態遷移例

ここでは[『KV260 の ブートファームウェアを更新』](https://qiita.com/ikwzm/items/4e5e954eabc988bd78d5)での例を用いて Persistent Register の内容がどのように遷移していくかを例をあげて説明します。

### 1 通常にブートしている状態

KV260 を初めてブートした際は次のようになっています。

| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           A            |         A         |         Y          |         Y          |

### 2 ブートファームウェアを現在使用していない方のパーティションに書き込んだ直後

1 の状態でimage_update コマンドを使って新しいブートファームウェアを書き込むとします。
image_update コマンドは、まず Persistent Register を読んで、現在ブートに使われていない方のパーティションに指定したファイルを新たな BOOT.BIN として書き込みます。
image_update コマンド実行直後は次のようになります。

| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           B            |         A         |         Y          |         N          |

Last Booted Image は現在のブートに使用されているブートファームウェアイメージを示しています。Last Booted Image が Image A だったので、新しいブートファームウェアは Image B に書き込まれました。その際、Image B はブート可能かどうかまだわからないため、Image B は Non Bootable になっています。
Requested Boot Image は次の電源投入時にブートを試すブートファームイメージを示しています。ここでは新しいブートファームウェアを書き込んだ Image B が選択されています。

### 3 電源を落として再投入

一度電源を落としてから再度電源を投入して KV260 をブートします。この時点では Last Boot Image が A で Image A Bootable も Y なので、Request Boot Image で指定された Image B がブートファームウェアイメージとしてブートを試みます。ブートする際には Last Boot Image を Request Boot Image で指定された Image B に更新します。

| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           B            |         B         |         Y          |         N          |

無事にブート出来た場合は 4 へ遷移します。
ブートに失敗したばあは 5 へ遷移します。

### 4 ブートに成功したら新たなブートファームウェアとして登録する

無事にブートしたならば、image_update コマンドの -v オプションを使って、Last Booted Image(今回の場合は Image B) を Bootable に変更します。

| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           B            |         B         |         Y          |         Y          |

これで無事にブートファームウェアの更新が終了しました。

### 5 ブートに失敗したらリセットボタンを押す

ブートに失敗した場合はのリセットボタンを押します。この時の状態は次のようになっています。

| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           B            |         B         |         Y          |         N          |

リセットにより再び Image Selector が起動します。
その際、Last Boot Image が B で、かつ Image B Bootable が N なので、C コードの説明での「1. いずれかのイメージが「起動可能」な場合、最後に起動したイメージが起動可能でない場合はもう片方のイメージがロードされます。」の条件に一致するため、Image B ではなく Image A が選択されてブートを試みます。ブートする際には Last Boot Image を Image A に更新します。


| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           B            |         A         |         Y          |         N          |

### 6 正常にブートするブートファームウェアを書き込む

Image A のブートファームウェアは、一度ブートすることが確認されているはずなので、正常にブートするはずです。

しかし、Requested Boot Image がブートに失敗する Image B のままになっているため、このままリブートしてもまた Image B が選択されてしまいます。そのため、正常にブートするブートファームウェアを Image B に上書きしておく必要があります。ブートファームウェアを Image B に上書きした直後は次のようになっています。

| Requested<p>Boot Image | Last<p>Boot Image | Image A<p>Bootable | Image B<p>Bootable |
|:----------------------:|:-----------------:|:------------------:|:------------------:|
|           B            |         A         |         Y          |         N          |

この状態で 「3 電源を落として再投入」を再度実行します。


## 参考

  * [Boot Firmware Overview](https://xilinx.github.io/kria-apps-docs/bootfw/build/html/docs/bootfw_overview.html)
  * https://github.com/Xilinx/embeddedsw/tree/master/lib/sw_apps/imgsel
  * [『KV260 が Linux をブートするまでのシーケンス』@Qiita](https://qiita.com/ikwzm/items/aca938c98b636c0048bc)
  * [『KV260 の ブートファームウェアを更新』@Qiita](https://qiita.com/ikwzm/items/4e5e954eabc988bd78d5)
  * [『KV260 の QSPI フラッシュメモリをのぞいてみる』@Qiita](https://qiita.com/ikwzm/items/67902c8e1446c13e8d81)