
Reverse engineering the TI AM3358 boot ROM
私が初めて何枚かのBeaglebone Blackボードを手にしてから、おそらく18ヶ月が経った。ゴミ箱から救い出したものだ。残念ながら、そのボードはすぐには動かなかった。さて、これが私にとってこれらのボードを扱う初めての経験であり、そもそもシングルボードコンピュータを扱うこと自体が初めてだったので、問題が自分のやり方にあるのか、それともボード自体に何か問題があるのか(おそらくそれがそもそもゴミ箱行きになった理由なのか)判断がつかなかった。これらのボードを実際に起動させるまでには、かなり長い時間と多大な労力がかかったが、とにかくやり遂げた。そして、その過程で学んだことをここに記す。
このリポジトリには、いくつかのユーティリティと、リバースエンジニアリングでこれまでに取得したすべてのシンボルを含むGhidraからエクスポートしたXMLファイルを同梱した。この投稿を使って、実際のファームウェアなしでエクスポートし、著作権問題を回避した。万一に備えてだ。自分でブートROMをデバッグしたいなら、JTAGをすでに接続しているはずなので、自分でブートROM(0x20000から0x2BFFFまで)をダンプできる。
シンボルを読み込むには:
0x20000に設定し、ブロック名をbootromに設定する。main()またはMMC/SDカードブートハンドラにジャンプできる。最初に、これらのボードは標準のBeaglebone Blackのカスタム版だと分かっていたので、早い段階で、ボード識別子のような、ボード自体に何か欠けている可能性があると判断した。balenaEtcherでフォーマットした標準SDカードを起動したときに見えたのは、何もなかった。ボードのLEDが点滅し始めることを期待していたし、UART-USBケーブルを接続すればU-Bootの過程が見えるはずだと思っていた。しかし、UARTは沈黙していた。SDカードを抜くと、Cという文字が繰り返し出力された。これはUART/シリアルブートの期待通りの動作だ。確かに起動しようとしており、SDカードがこの動作を変えていたのだが、それ以上は見えなかった。Web上のほとんどのトラブルシューティングは、U-Bootの出力を出発点として問題を診断していた。その余裕は私にはないようだ。
この時点で、デバッグプローブを接続する価値があると考えた。残念ながら、既存のフットプリントに合うヘッダを持っていなかったので、自分で作ることにした。
BeagleboneボードにはP2という指定のヘッダがあり、JTAG接続を引き出している。このヘッダにワイヤを接続し、メスヘッダにまとめてJ-Linkで通信できるようにした。



Ozone(Seggerデバッガ)を起動してJ-Linkを設定し、まずエントリポイントを見つけることから始めた。リセットホールトで目的の場所に到達できると思っていた。それがエントリポイントが0x2148aであるという(誤った)仮定に至った理由だが、これが一貫していないことには確かに気づいていた。後になって、AM335xボードはJ-Linkのリセットホールトとはあまり相性が良くないことが分かった。実際には数百クロックサイクルの遅延があり、非決定的にブートハンドラ内のどこかに着地していた。(最終的には、J-LinkデバッグをサポートするTIのCode Composer Studio用のGELファイルを書くことでこれを回避した。リセット時にPCレジスタをリセットハンドラに設定し、レジスタをクリアし、命令モードをARMに強制する。)
TIフォーラムのスレッド(AM335x: TI employees, where can I get the ROM Bootloader source code/symbols?)から、いくつかのデバッグシンボルを得た: 0x231e0のSPI Initialize、0x23230のSPI ReadSectors、そして0x24bfaはUART読み取りを行うルーチンだ。それは確かに少し助けになった。ブートは0x402f0440の無限ループ、つまりデッドループに陥って失敗していることに気づいた。ふむ、他のブートROMからはかなり離れている。RAMかどこかにあるに違いない。そろそろテクニカルリファレンスマニュアル(TRM)に目を通す時だろう!
TRMの第26章には、ブートに関する大量の情報が含まれている。ブートROMの次の図が得られる:

説明:
パブリックROMコードのアーキテクチャを図26-1に示す。これはトップダウン方式で3つの主要レイヤに分かれている: 高レベル、ドライバ、ハードウェア抽象化レイヤ(HAL)である。あるレイヤは、統一インターフェースを通じて下位レイヤと通信する。 高レベルレイヤは、パブリックROMコードの主なタスクを担当する: ウォッチドッグとクロックの設定、およびメインブートルーチンである。 ドライバレイヤは、インターフェース仕様に従って、任意のブートデバイスに対する論理プロトコルと通信プロトコルを実装する。 最後にHALは、ハードウェア基盤IPと相互作用するための最下位レベルのコードを実装する。エンドブートデバイスはデバイスIOパッドに接続される。

図26-2は、パブリックROMコードのブート手順の高レベルフローを示している。このデバイスでは、パブリックROMコードはセキュアスタートアップ(セキュアROMコードによって実行される)の完了時に開始する。その後、ROMコードはパブリックスタートアップ手順の一部としてプラットフォーム設定と初期化を実行する。ブートデバイスリストはSYSBOOTピンに基づいて作成される。ブートデバイスは、メモリブートデバイス(はんだ付けされたフラッシュメモリやメモリカードなどの一時的なブートデバイス)、またはホストに接続されたペリフェラルインターフェースである。 ブート手順のメインループはブートデバイスリストを順に調べ、現在選択されているブートデバイスからイメージを検索する。有効なブートイメージが見つかり正常に実行された場合、またはウォッチドッグの期限切れ時にこのループは終了する。イメージの認証手順は、HSデバイスでのイメージ実行の前に行われる。認証手順の失敗は、セキュアROM内の「デッドループ」(ウォッチドッグリセット待ち)への分岐につながる。
メモリマップ!例外ベクタ!フローチャート!このセクションには情報が盛りだくさんだ。私の仕事はずっと楽になった。
この時点でJTAGプローブを使ってファームウェアをいくつかのファイルにダウンロードし、Ghidraに読み込み始めた。便利な形式のSVDファイルやその他のレジスタマッピングは存在しないようで、これは本当に残念だ。なぜなら、メモリ領域やレジスタなどをすべて手動で定義する必要があるからだ。これは退屈な作業だったが、しばらくすると、AM3358用のシンボルをGhidraに読み込むためのPythonスクリプトを作成できた。心配事がひとつ減った!
私が持っているファイルは次のようにマッピングできるようだ:
0x402f_0440の無限ループが内部SRAM内の「ダウンロードされたイメージ」の先頭にある一方、例外ベクタは別の場所に格納されているのは興味深い。これは後で重要な手がかりになるかもしれない...
リセット時、プライベートブートROMはセキュリティ関連を処理し、リセットベクタを含む0x2 0000に分岐する。最初の命令は0x2 08d0への分岐で、これがエントリポイントであるはずだ。これはBX命令ではないので、おそらくその時点でもまだArmモードである。
これは最初に実行されるコードであり、パラメータを持つ「関数」というよりは、コンパイラが生成したスタートアップスクリプトのようなものだ。最初の基本ブロック:```arm ldr r4,[->Peripherals::CM_PER] mov r0,#0x2c ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r6,#0x2 str r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r0,#0x2c poll: ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL cmp r6,#0x2 bne poll
このブロックはOCMC RAMクロックを有効に設定します:
1. `CM_PER_OCMCRAM_CLKCTRL=0x2` を設定します
2. レジスタが設定されたか確認し、設定されていない場合はポーリングを続けます
`CM_PER_OCMCRAM_CLKCTRL` レジスタは `MODULEMODE` フィールドにビット0と1を使用し、これを `=0x2` に設定するとOCMC RAMへのクロックが有効になります。
次の基本ブロック:```arm
ldr r0,[PTR_control_status]
ldr r0,[r0,#0x0]=>control_status
and r0,r0,#0x700
mov r0,r0, lsr #0x8
cmp r0,#0x3
bne skip
ldr r0,[PTR_control_status]
ldr r0,[r0,#0x0]=>control_status
cpy r6,r0
and r0,r0,#0x1f
cmp r0,#0x1f
bleq GPMIC_init
skip: ...
このブロックは以下の処理を行います:
(control_status & 0x700) >> 8 == 0x3 かどうかを確認し、該当しない場合はスキップしますcontrol_status & 0x1f == 0x1f かどうかを確認し、該当する場合は control_status を r6 にロードした後、関数 GPMC_init を呼び出します次のブロックはコプロセッサを設定します:```arm msr cpsr_c,#0xd3 ldr r4,[->Exceptions::ROM_RESET_VECTOR] mcr p15,0x0,r4,cr12,cr0,0x0 bl LAB_00020934 bl LAB_00020938 bl LAB_0002093c bl LAB_00020940 bl LAB_00020944 bl LAB_00020948 bl LAB_0002094c bl LAB_00020950 mrc p15,0x0,r0,cr1,cr0,0x0 orr r0,r0,#0x800 mcr p15,0x0,r0,cr1,cr0,0x0 b LAB_000207f0
このブロックでの操作:
1. `11010011b` を CPSR 制御フィールド(`I=1`、`F=1`、`T=0`、`MODE=10011`)に移動する
1. `I` は割り込み無効化、`F` は高速割り込み無効化です(つまり `I=F=1` は割り込みが無効化されていることを意味します)
2. `T` は Thumb モードで、`0` に設定されています
3. `MODE=10011` はプロセッサモードをスーパーバイザモードに設定します([参照](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/ARM-processor-modes?lang=en#CIHGHDGI))
4. 詳細は[こちら](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/Program-Status-Registers--PSRs-)を参照してください
2. ROM リセットベクタのアドレスをロードする
3. [コプロセッサ 15](https://developer.arm.com/documentation/den0013/d/ARM-Processor-Modes-and-Registers/Registers/Coprocessor-15)(システム制御コプロセッサ)のセキュリティ拡張レジスタ `c12` にアクセスし、ROM リセットベクタを `VBAR`(ベクタベースアドレスレジスタ)にロードする

4. `nop` ジャンプのように見える? なぜ `b` ではなく `bl` なのか?
5. 分岐予測を有効にする(システム制御レジスタ `SCTLR` のビット 11 を設定する)

*VMSA 実装における CP15 `c1` レジスタ(システム制御レジスタ)*
`SCTLR` レジスタの説明:
> SCTLR は、メモリシステムを含むシステムのトップレベルの制御を提供します。
> このレジスタは、仮想メモリ制御レジスタの機能グループの一部です。
TRM の B4-1687 ページを参照してください。ビット 11 は *分岐予測有効* ビットで、これを有効に設定すると [分岐予測](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/Common-Memory-System-Architecture-Features/Caches/Branch-predictors) が有効になります。
6. 関数を呼び出す(分岐命令によるコール命令への分岐を介して)
### `0x20894` の関数(`__main`)
この関数には、別の初期ルーチンから分岐して到達します。スタック、場合によってはタイマーまたはウォッチドッグを初期化してから、`FUN_0002889c` を呼び出していると思われます(後に `main()` であることが判明します!)。```arm
ldr sp,[->RESERVED_EXCEPTION_BRANCH] ; 0x4030ce00
blx load_stack_1
ldr r12,[DWORD_1]
add r12,r12,pc
tst r12,#0x1
adrne lr,0x208bd
cpyeq lr,pc
bx r12 ;=>init_timers_maybe
adr r12,0x208bd
bx r12 ;=>LAB_000208bc
000208bc bl FUN_0002889c
000208c0 ddw 0x109
000208c4 addr RESERVED_EXCEPTION_BRANCH
...
RESERVED_EXCEPTION_BRANCH:
ldr pc=>LAB_00020090,[PTR_LAB_4030ce20]
; 20090 is a dead loop
ここでの操作は次のとおりです:
r0,r1,r2,r3,r4,lr をスタック(メモリマップ内のパブリックスタック)にプッシュする関数に分岐します
load_stack_1 は空の関数 bx lr に分岐し、次にスタックから r0,r1,r2,r3,r4,pc をポップします(基本的には、そのデータをレジスタに戻し、lr が保持している値を pc に格納してリターンします)pc + 0x109 が奇数かどうかを確認し、偶数の場合は 0x208bd を lr にロードし、それ以外の場合は pc を lr にコピーしますFUN_0002889c を呼び出しますこれは、起動フローチャートで参照されている __main() 関数のはずです:

つまり、次の関数が main 関数ということになります。
図26-8の上部に示すように、CPUはセキュアブートの初期化が完了すると、Public ROM Code のリセットベクタにジャンプします。パブリックモードに入ると、システム起動時にCPUはパブリック側の初期化とスタックのセットアップ(コンパイラが自動生成するC初期化、または「scatter loading」)を実行します。次に、ウォッチドッグタイマー1を設定し(3分に設定)、システムクロックの設定を実行します。最後に、起動ルーチンにジャンプします。
0x209b0)main が呼び出されると、SP レジスタは 0x4030ce00 を指しています。スタックはこの位置から始まり、0x4030 b800 に向かって下方向に成長します。また、4つのレジスタをプッシュした後(16バイト、つまり4ワードの差)に 0x4030 cdf0 のアドレスを指しているため、AAPCSと同様のフル降下スタックを使用していることになります。つまり、SP はスタック上の最も新しいワードを指し、下方向に成長します。
以下が逆コンパイルされた main() 関数です:```c
int main()
{
uint local_10;
uint local_c;
local_c = 0; local_10 = 0; check_stack_prm(&local_10); update_coldreset_tracing_vector(local_10); update_current_tracing_vector(1); main_clock_init(6,0); watchdog_softreset(); watchdog_write_disable_seq_data2(); set_watchdog(300000); if ((local_10 & 1) != 0) { update_current_tracing_vector(2); local_c = local_c & 0xffff | 1; } timer_func_1(); clock_init_func_4(&local_c); run_booting_loop(&local_c,local_10 & 0xff); return 0; }
私の目的にとって最も興味深いのは、`0x20a10` にある `run_booting_loop` 関数です。
### X-Loader ノート
スタートアップを調べて、"ISSW" や "CHSETTINGS"、"X-LOADER" のような文字列が含まれるこの部分に到達した後、これらの文字列が U-Boot 関連のコンテキストで他に出現しそうな場所を探し始めました。Nook ファームウェアをリバースエンジニアリングまたはクラッキングしている人々の[このスレッド](https://forum.xda-developers.com/t/discussion-on-the-boot-loader-cracked.1378886/)に偶然出くわし、[x-loader ソース](https://github.com/joelagnel/x-loader/blob/f3c74bc9b01dac58e553393d6ec1041353f2f1f7/scripts/signGP.c)には `CHSETTINGS` のようなものへの参照が含まれています。調べてみると、"ISSW" は非メモリデバイスからの起動を指している[ようです](https://github.com/u-boot/u-boot/blob/master/doc/README.ti-secure)。
初期化ドキュメントから、高レベルのコードを思い出してください:

注目すべき点:
- RNDIS
- FAR
- XMODEM
- BOOTP
- TFTP
- DFT
そろそろライブデバッグを再び試す時かもしれません。理解できない大量のデータを考えると、それらの構造体をすべてリバースエンジニアリングするのはおそらく苦痛でしょう...
やった! 見つけたベクタを使って PC と SP を手動で設定すると、ライブデバッグが機能します:

ソースとトレーシングベクタから、さまざまなブートオプションと、それらに割り当てられたデバイス番号をマッピングすることができました。これは後で非常に役立ちました。SD/MMC ブートハンドラにブレークポイントを設定する際に、MMC0 (8) と MMCSD1 (9) を区別する必要があったからです。
| タイプ | デバイス | デバイス ID |
| ---------- | ----------------- | ----------- |
| メモリ | XIP (MUX2) | 1 |
| メモリ | XIP w/WAIT (MUX2) | 2 |
| メモリ | XIP (MUX1) | 3 |
| メモリ | XIP w/WAIT (MUX1) | 4 |
| メモリ | NAND | 5 |
| メモリ | MMCSD1 | 7, 9 (eMMC) |
| メモリ | NAND_I2C | 10 |
| メモリ | MMC0 | 8, 12 (SD) |
| ペリフェラル | UART0 | 16 |
| ペリフェラル | USB | 20 |
| ペリフェラル | GPGMAC0 | 22 |
プロセッサの起動方法の詳細については、TRM と [Stack Exchange のこの回答](https://stackoverflow.com/a/31252989/8565545)を参照してください。要約すると:
1. ブート ROM は、SD カード上の MLO(Mmc LOader)ファイルを識別し、SRAM にコピーしました
2. これはセカンダリプログラムローダーであり、フル RAM を初期化し、実行のために完全な U-Boot バイナリをそこにコピーする小さなブートローダーです
3. U-Boot バイナリが実行された後、私たちは(というより U-Boot が)最終的にカーネルを起動します
### `run_booting_loop()`
これはメインブートループです。無限に実行されるか、実行が RAM にロードされる別のブートローダーに分岐するまで実行されます。
トレーシングベクタの更新を除いた、プロシージャの開始:
- デバイスタイプを検索
- デバイスタイプが 5(セキュアデバイス)の場合は、他の初期化を実行する
- `build_boot_list(int,buffer[],data[],int)` を実行
- `buffer[]` は `0xff` に初期化され、`data[]` にはデバイスタイプが含まれる(おそらく)```c
void run_booting_loop(uint32_t *r0_config,undefined4 param_2,undefined4 param_3,
undefined4 default_list)
{
int iVar1;
uint j;
uint i;
int device_type;
byte alt_list [12];
undefined4 boot_status;
byte boot_list [8];
uint8_t local_buffer [8];
update_current_tracing_vector(3);
/* Device type is 3 */
lookup_device_type(&device_type);
if ((device_type == AM335X_HIGH_SECURITY) && (iVar1 = return_zero_4(), iVar1 != 0)) {
init_something_1_small(&STATIC_DATA_1);
}
/* param1 = 1
param2 = 4030 ebc4
param3 = 4030 ebb4 */
build_boot_list(*(ushort *)r0_config,boot_list,alt_list,default_list);
do {
i = 0;
local_buffer[0] = 0xff;
local_buffer[1] = 0xff;
local_buffer[2] = 0xff;
local_buffer[3] = 0xff;
do {
if (boot_list[i] - 1 < 12) {
update_current_tracing_vector(4);
/* No return unless there is an error */
boot_device_1(r0_config,boot_list[i],local_buffer);
}
else if (boot_list[i] - 65 < 8) {
update_current_tracing_vector(5);
watchdog_write_disable_seq_data2();
boot_status = 0xffffffff;
boot_device_2((uint32_t)r0_config,boot_list[i],&boot_status,local_buffer);
watchdog_write_enable_seq_data2();
if (boot_status != 0xffffffff) {
local_buffer[0] = (undefined)boot_status;
local_buffer[1] = boot_status._1_1_;
local_buffer[2] = boot_status._2_1_;
local_buffer[3] = boot_status._3_1_;
if ((boot_status & 0xffff00ff) == 0xf0030006) {
update_current_tracing_vector(9);
boot_list[i + 1] = (byte)(boot_status >> 8);
}
else if (boot_status != 0xf0030002) {
update_current_tracing_vector(8);
j = 0;
do {
if (63 < boot_list[j]) {
boot_list[j] = 0;
}
j = j + 1 & 0xff;
} while (j < 8);
}
}
}
i = i + 1 & 0xff;
} while (i < 8);
update_current_tracing_vector(6);
} while( true );
}
0x23d7a に、私が boot_into_SRAM() と呼んでいる関数があり、SRAMへの分岐の前に最後に呼び出される関数であることがわかりました。そこから例外ハンドラに入ります。以前は、RAMの異なる状態を保存していましたが、もう一度ライブデバッグを行ったところ(1年以上後の2024年7月)、何が起きているのかを理解しました。ボードはSDカードからデータを正常に読み取り、カードからロードしたコードを実行していました!これを確認するには、SRAM内にSDカード上のデータと同じバイト列を見つける必要がありました。am335x-evm-linux-sdk-bin-.../board-support/prebuilt-images/ の中に、u-boot-spl.bin-am335x-evm というバイナリファイルがあり、このバイナリ内のコードがSRAMに現れるコードと一致することがわかりました。uboot SPLに到達しました!
SRAMへのブートに成功しました。次に、ubootに関する情報を表示するはずのUARTターミナルについて考えています。ハードウェア接続は以下のとおりです。

CuteComを使って115200、8-N-1でデバイスに接続し、SDカードを挿入しない場合は、単にCを繰り返し出力します。

しかし、SDカードを挿入すると、UARTは何も出力しません。メッセージも文字もありません。フォールトはブートプロセスの早すぎる段階で発生しているに違いありませんか?しかし、実行中のコードが何であるかも分かったので(ソースコードもあります)、そのためのデバッグシンボルをビルドして、適切なデバッグセッションを開始できるはずです。これは簡単ではないかもしれません。同じ方法でコードをコンパイルする必要があり、SDKがSDカードに何をロードしたのか、そしてそれをどうビルドするのかを正確に学ぶのに多少時間がかかるかもしれません。
U-BootはUARTにテキストを出力するはずですが、何も表示されないため、SPLのどこかで例外を捕捉しているのだと思います。
この時点で、GhidraでブートROMの逆コンパイル済みソースをリバースエンジニアリングして整理するのに何時間も費やし、構造体や各データメンバーが関数間でどのように使われているか(時にはネストされて)を調べ、あらゆる混乱を引き起こしていました。それが後回しになっている間に、独自のコードのデバッグとコンパイルも始める時期だと考えました。結局のところRAM上にいるのですから、SPLシンボルをロードして何が起きているのか確認してみてはどうでしょうか?
Ozone(J-Link用のSeggerデバッガ)でデバッグできます。TIのCode Composer Studio(CCS)や、そのVSCodeスタイルの「ライト」バージョンであるCCS Theiaも使用できます。このビデオに従って、SDK内のすべてをビルドできました: Sitara Linux Board Porting Series: Module 6。ビルドするコンポーネントは3つあります:
上記シリーズのModule 7 videoに従い、いくつかの注意点を踏まえて動作させることができました:
s_init() はもう存在しないシンボルがある?素晴らしい。これで実行中の動作を確認でき、リセットハンドラreset()から始まり、例外が発生する場所まで確認できます。原因を追跡するため、0x402f 0440の例外ハンドラにブレークポイントを設定し、リンクレジスタを確認しました。そこには最新の関数のアドレスがまだ格納されていました。そのアドレスは0x402f 76ceであることが判明しましたが、一貫していないようです。何がエラーの原因なのでしょうか?
注意: デバッグ時は、Module 7のビデオに従い、0x402f 0400まで実行してから、Load Memory()の部分を行います。これは再起動のたびに行う必要があります。
device_probe()(0x402f 74c4)という関数に入り、次に他のいくつかの関数に入ります。do_setup_dpll()から0x402f 07fcで分岐した後は、そこから抜けないので、さらに進んでみましょう。ステップ実行を続けると、crt0.S内の_main()(0x402f 14e0)に戻ります。board_init_f()から抜けて、spl_relocate_stack_gd()に進んでいるようです。この呼び出しからは抜けません。dm_fixup_for_gd_move()に到達します。これには0x402f 76c2で失敗する命令が含まれています。これが原因だと思います: 0x81ff ff20にアクセスしようとしています。どうやらダメなようです。SDRAM設定に問題があるという直感があります。TIフォーラムで同様の問題に関するスレッドをすべて探し、参考になるヒントを含むスレッドをいくつか見つけました。おそらく (a) EMIFチューニング または (b) ソフトウェアレベリング に関係していると結論付けました。
私のボードは以下のものです。

メモリはMicron製ですが、私が持っているBeagleBone Black(rev C3)の回路図では、Kingston DDR3メモリ、具体的にはD2516EC4BXGGBを使用しています。DDR3はU12で、Micronマーキングデコーダページを使って部品を特定できます:
実際に動作しているか確認するため、まず電源が供給されているかどうかを確認しました。データシートでは、1.5V±0.075Vでなければならないと指定されています。基板裏面のR6両端で1.506Vを測定しました。TP1とTP2の2つのテストポイントがあります。
何かの役に立つかもしれないので、テストポイントをいくつか紹介します。
メモリ回路の詳細は、ハードウェアデザインページに記載されています。
クロックイネーブルラインを確認しましょう。R96の両側を確認できます。片側は接地され、もう片側はハイに保持されているはずです。

CKEで1.5Vを確認しました。
次のステップは、クロック信号の確認です。ここではできる限りのことをしました。アンテナをRAMチップの方向に大まかに向けてtinySAを使いました。このような「スニッフィング」を行うことで、クロックが存在することはかなり確信しています。少なくとも今のところは十分です。
次に、外部メモリインターフェースを見ていきます。よく出てくるのがGELファイルの概念です。これはテキサス・インスツルメンツがCode Composer Studio用に開発したインタープリタ型言語で、General Extension Languageの略です。
GELファイルはDDRメモリ設定ツールに含まれています。
よし!チューニング手順に(できる限り)従い、GELファイルに最適な値を見つけることができました。```
The Slave Ratio Search Program Values are...
PARAMETER MAX | MIN | OPTIMUM | RANGE
DATA_PHY_RD_DQS_SLAVE_RATIO 0x071 | 0x005 | 0x03b | 0x06c DATA_PHY_FIFO_WE_SLAVE_RATIO 0x1b3 | 0x046 | 0x0fc | 0x16d DATA_PHY_WR_DQS_SLAVE_RATIO 0x0f7 | 0x01a | 0x088 | 0x0dd DATA_PHY_WR_DATA_SLAVE_RATIO 0x137 | 0x05a | 0x0c8 | 0x0dd
メモリブラウザでRAM関連の設定を調整してみる...うわっ!動いた!
というわけで、メモリは確かに動作しているようだが、SPLはまだ失敗している。つまり、SPLがSDRAMを初期化しようとする方法に関して何か問題があるのかもしれない?ああ、そうだ、調整手順にはまだ続きがあるのは当然だ!実際にSPLを更新しないと...
だいぶ近づいてきた。ファイル `board.c` は、ボードの種類を確認してDDRを初期化する。しかしこのボードでは、すべての関数(`board_is_evm_sk()`、`board_is_icev2()`、`board_is_bone_lt()` など)がfalseを返すため、`config_ddr(266, ...)` がデフォルトで使われる。ここで266はMHz単位のクロック周波数で、*本来は* 400 MHzであるべきだ。これは確かに問題になり得る。
`board_is_bone_lt` をバイパスして常にtrueを返すようにすべきだ。実際にそうしてみると、少し先に進めるようになったが、何かが気になっている。新しいファイルMLOをSDカードにロードしても機能しない。プログラムを直接ロードするのはうまくいくのに。何が起きているのか?SRAMにロードされているコードが、自分がコンパイルしたコードと同じではないことがわかる。実際、SDカードをフォーマットしても、デフォルトのSPLがとにかくSRAMにロードされているように見える!別のSDカードを使うとブートプロセスが続行しようとしないことも確認した。つまり、ブートローダは間違いなくSDカードのブートパーティションを探し、その後実行をSRAMに移しているが、まだデータをコピーしていないのだろうか?ううっ。それはどこから来ているのか??
この問題にはかなり手こずらされた。すべてのパーティションを削除し、MBRをゼロで埋め、ブートパーティションもゼロで埋め、別のSDカードも試したが、自分のカードだけがまだブートできた。つまり、そのカードにはまだどこかに *何らかの* ブート可能なデータが残っているに違いない。最終的に、カードの *全体* をゼロで埋めることで、この狂気に終止符を打つことができた。
また、この時点で、ブートROMのトラブルシューティング時にアクセスできる **トレーシングベクタ** についても学んだ。これらはリバースエンジニアリングにも非常に役立つことになった。すべてのトレース呼び出しがどこから来ているのかが分かり、それらの呼び出しに基づいて関数名などを割り当てることができたからだ。これらのトレーシングベクタを解釈するためのスプレッドシートを作成し、カードのパラメータを変更するにつれてブート手順がどのように変化するかを素早く理解するためにこれを使った。案の定、必死でカード全体をゼロで埋めるまで、SDカードのフォーマットを繰り返すたびに、CHSETTINGSが見つかったと何度も報告していた。
TIプロセッサと同じようにカードを読み取るには、`dd` を使うことができる。ブロックサイズ=512を指定し、最初の `n` 個をスキップすることで、最初のセクタ(どれかを確認するにはGPartedを使うとよい)を指定する。例:最初のセクタが2048、デバイスが `sda` の場合、最初のセクタだけを読み取る:```
sudo dd if=/dev/sda1 of=/home/sam/sector2048 bs=512 skip=2048 count=1
これを使ってSDカードから直接MBRとブートパーティションの先頭のイメージをダウンロードしました。どちらも後で役立つことになります。
Ghidraでのリバースエンジニアリングの取り組みにより、SDカードのブートハンドラ関数にたどり着き、カードにSDコマンドを送信する関数を確認できるようになり、ステップ実行してカードが何を返すかを確認できるようになりました。私は正しい場所を見ていると思っており、カードはすべてゼロを返していると考えていました。その後、eMMCハンドラ(同じハンドラだがデバイスIDが異なる)をステップ実行していた可能性があること、または何か他に問題があったことを知りました。SDカードには何の問題もなかったからです。それでも、これらのカードがどのように動作するのかを学ぶ時だと思いました。
カードが各ブロック要求中にすべてゼロを返し続ける理由を確認したいと思いました。明らかに、カード機能とソフトウェアは、過去にすでに読み取ったことがあるため、そのカードを読み取ることができます。それでも、配線を接続してロジックアナライザで確認する時が来ました。少しのマイクロはんだ付け、UV硬化エポキシで30awgワイヤを固定し、私のSaleaeでクリップ接続すれば、動作するものができあがります。

データを解析するためにこのアナライザを使いました。最初はカードを挿入せずに試しました。

最初の数コマンドではクロックレートは120 kHzです。当然、カードは応答しません(カードが存在しないため)。``` CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND ... CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND
カードが実際に挿入されると、設定後、周波数は約6 MHzに跳ね上がります。

SDモードの使用によるいくつかのクラッシュを乗り越えた後(ヒント: このSDカードでもMMCモードを使うべきです)、カードが妥当なデータを提供していることを確認できました。その後、SDカードのブートハンドラを理解する努力を倍増させ、正しいデータが*読み取られて*いたことに気付いたため、この件はこれで終わりにしました。それは手動でカードを`dd`したときに得たデータと同じものでした!まあ、楽しい寄り道でしたが、カードが動作しているという確信を得るのに役立ちました。
### 問題の発見
データは、アドレス`0x0000`とデバイス`8`に対して`0x25c2e`の分岐を実行した後、アドレス`0x4030c928`(スタック変数、512バイト配列)から取得されます(静的データは`0x4030d00c`を参照)。私が`MBR_detection`と呼んでいるメソッドに入ると、プログラムはマジックバイト`0xaa55`をチェックします。最初に2番目の2バイト`0xaa`をロードし、次に最初の`0xaa`をロードします。```
r0 = data[0x1ff]
r1 = data[0x1fe]
orr r0,r1,r0,lsl #8
sub r1,r0,#0xaa00
subs r1,#0x55
bne <return FAIL>
これは成功します。ただし、次のチェックは失敗します:``` r0 = data[0xc] => 0 r1 = data[0xb] => 0 orr r0,r1,r0, lsl #8 cmp r0,#0x200 bne
デコンパイラ疑似コード:```c
if (
data[0x1fe] != 0x55aa ||
data[0xb] != 0x200 ||
(data[0xd] != 1 && // bit 0
data[0xd] != 2 && // bit 1
data[0xd] != 4 && // bit 2
data[0xd] != 8 && // bit 3
data[0xd] != 0x10 && // bit 4
data[0xd] != 0x20 && // bit 5
data[0xd] != 0x40 && // bit 6
data[0xd] != 0x80) // bit 7
)
{
return 1;
}
これは、0xb = 11 のバイトが 0x200 と等しいかをチェックし、0xd のバイトが単一ビット値と等しいかをチェックします。両方の条件が満たされる必要があり、そうでなければ失敗を返します。
検出関数が 1 を返した後、ブートハンドラは次にそれを MBR として読み取ろうとし、最初のパーティションのオフセットをロードして、それが起動可能なパーティションかどうかを確認しようとします。手順は次のとおりです:```C
// Call block read function
// mmc_block_read_something(boot_device *dev,blk_read_struct blk)
ret = ((code *)blockread_struct->block_read_func)(blockread_struct->device_ptr,&block_read_info);
if (ret != 0) {
return 1;
}
// Check if device doesn't use MBR
ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct);
if (ret != 0) {
// It uses MBR, try each partition in the partition entries for a bootable
// partition
ret = MBR_check_entries(block_data,blockread_struct);
if (ret != 0) {
return 1;
}
ret = MBR_parse_entries(block_data,&blockread_struct->part_entry);
if (ret != 0) {
return 1;
}
// Get bootable partition offset
block_read_info = (blk_read_struct *)(blockread_struct->part_entry).first_sect_pos;
uStack_220 = 1;
pbStack_21c = block_data;
// Call block read function
// mmc_block_read_something(boot_device *dev,blk_read_struct blk)
ret = ((code *)blockread_struct->block_read_func)
(blockread_struct->device_ptr,&block_read_info);
if (ret != 0) {
return 1;
}
// Try and verify bootable partition again
ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct);
if (ret != 0) {
return 1;
}
}
さて、`0x800` でデータを読み取る地点までジャンプして、正しいダンプが取得できた。しかし、MBR 検出メソッドは正しいデータでも依然として 1 を返す(しかもそこには辿らなければならない間接参照がいくつかあって、ぐぬぬ)。だから、問題はそこにあるに違いない。
いよいよ大詰めだ。SD カードからの最初のメモリ読み取りは MBR であり、最大 4 つのパーティションテーブルエントリを持つ(TRM テーブル 26-20、26-21 参照)。ブートパーティションのパーティションテーブルエントリは、そのパーティションに `0x40000` セクタが含まれるとしている。しかし、パーティションのファイルシステム(TRM テーブル 26-23 参照)は、なぜか `0x3fff8` しかないとしている。そしてブート ROM はこれを検出して失敗する。
実験として、問題を引き起こしているジャンプをスキップしてみた(まずい結果になるかもしれないが…祈るような気持ちで…)。プログラムは確かに続行したが、どこに着地したのかははっきりしない。ガベージのように見える。しかし、それは無視して、デバイスは実際に起動する! `0x402f0400`(ロードされたイメージの先頭)にブレークポイントを設定すると、すべてが正常に進む。そろそろ UART を接続してみる頃か? UART はいいぞ!
SD カードの問題については? [Unix SE への質問](https://unix.stackexchange.com/questions/781715/why-do-the-mbr-partition-entry-and-partition-filesystem-disagree-on-the-number-o/781755) を投稿したが、当面の問題についてはあまり助けにはならなかった(それでも良い情報は得られたけど)。そこから、ついに修正できた! FAT16 ファイルシステムを構築するための `mkfs` コマンドを確認していたところ、[別のリファレンス](https://blog.billvanleeuwen.ca/porting-u-boot-onto-the-beaglebone) が `-a` フラグ(アライメントを無効化)を使っていることに気づいた。これが鍵だった。そのフラグを追加してビルドし直すと、セクタ数が一致し(`0x40000`)、システムが起動する。どうやらブート ROM はその種のアライメントをサポートしていないようだ。
カードを挿入した状態で、以下のメッセージがブートループで表示されるようになった。やった! あとはカーネルが起動しない理由を突き止めるだけだ。それで完璧だ! これまでの作業がようやく報われて、数枚の BeagleBone ボードが使えるようになるかもしれない。価値があったかって? それは誰にもわからない。```
U-Boot SPL 2021.01-00001-gc59bf25a382-dirty (Jul 24 2024 - 20:38:49 -0400)
Trying to boot from MMC1
U-Boot 2021.01-00001-gc59bf25a382-dirty (Jul 28 2024 - 20:36:46 -0400)
CPU : AM335X-GP rev 2.1
Model: TI AM335x BeagleBone Black
DRAM: 512 MiB
WDT: Started with servicing (60s timeout)
NAND: 0 MiB
MMC: OMAP SD/MMC: 0, OMAP SD/MMC: 1
Loading Environment from FAT... *** Warning - bad CRC, using default environment
<ethaddr> not set. Validating first E-fuse MAC
Net: eth2: ethernet@4a100000, eth3: usb_ether
Hit any key to stop autoboot: 2 <0x08><0x08><0x08> 1 <0x08><0x08><0x08> 0
WARNING: Could not determine device tree to use
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
Failed to load 'boot.scr'
Failed to load 'uEnv.txt'
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
<0x1b>7<0x1b>[r<0x1b>[999;999H<0x1b>[6n<0x1b>8Scanning disk [email protected]...
Scanning disk [email protected]...
** Unrecognized filesystem type **
Found 4 disks
No EFI system partition
BootOrder not defined
EFI boot manager: Cannot load any image
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
4997632 bytes read in 353 ms (13.5 MiB/s)
Failed to load '/boot/undefined'
Starting kernel ...
というわけで、デバイスツリーに問題があります(「WARNING: Could not determine device tree to use」)。カーネルやカーネルの起動に触れるのはこれが初めてなので、それが何を意味するのかまったく見当がつきません。
"カーネルの起動"には、私の理解では次のことが含まれます。
カーネル起動プロセスの重要な側面はデバイスツリーです。これは、ボード用の .dtb ファイル(デバイスツリーバイナリ。.dts デバイスツリーソースファイルと比較してください)に格納されています。
現在の問題は、U-Boot がボードのデバイスツリーをロードしていないことです。ログに reading /am335x-boneblack.dtb というメッセージがないからです。代わりに WARNING: Could not determine device tree to use が出力されます。これは有力な証拠です! おそらく EEPROM のボード ID が欠落しているためだと思います。
ボードを識別する方法の詳細は、TI フォーラムのこのスレッドにあります。
では、U-Boot はどのようにして自分自身を構成し、正しく起動する方法を知るのでしょうか? 私たちがビルドした U-Boot ソース内には、さまざまなボード用の defconfig ファイルを格納する configs/ というフォルダがあります。これらのファイルは、U-Boot のさまざまな構成パラメータを定義しており、ブートコマンドも含まれます。ブートコマンドは次のようになるかもしれません:```
if test ${boot_fit} -eq 1;
then run update_to_fit;
fi;
run findfdt;
run init_console;
run envboot;
run finduuid;
run distro_bootcmd
`make <boardname>_config` ターゲットを実行するときに、どのコンフィグを使用するかを定義します。`findfdt` 関数は、実行中のボードを識別し、デバイスツリーを適切に構成するために使用されます。これは次のようになります(`am335x_evm.h` で定義されています):```
"findfdt="\
"if test $board_name = A335BONE; then " \
"setenv fdtfile am335x-bone.dtb; fi; " \
"if test $board_name = A335BNLT; then " \
"setenv fdtfile am335x-boneblack.dtb; fi; " \
"if test $board_name = A335PBGL; then " \
"setenv fdtfile am335x-pocketbeagle.dtb; fi; " \
"if test $board_name = BBBW; then " \
"setenv fdtfile am335x-boneblack-wireless.dtb; fi; " \
"if test $board_name = BBG1; then " \
"setenv fdtfile am335x-bonegreen.dtb; fi; " \
"if test $board_name = BBGW; then " \
"setenv fdtfile am335x-bonegreen-wireless.dtb; fi; " \
"if test $board_name = BBBL; then " \
"setenv fdtfile am335x-boneblue.dtb; fi; " \
"if test $board_name = BBEN; then " \
"setenv fdtfile am335x-sancloud-bbe.dtb; fi; " \
"if test $board_name = A33515BB; then " \
"setenv fdtfile am335x-evm.dtb; fi; " \
"if test $board_name = A335X_SK; then " \
"setenv fdtfile am335x-evmsk.dtb; fi; " \
"if test $board_name = A335_ICE && test $ice_mii = rmii; then " \
"setenv fdtfile am335x-icev2.dtb; fi; " \
"if test $board_name = A335_ICE && test $ice_mii = mii; then " \
"setenv fdtfile am335x-icev2-prueth.dtb; fi; " \
"if test $fdtfile = undefined; then " \
"echo WARNING: Could not determine device tree to use; fi; \0" \
デフォルトの動作が必要なら、board_name 変数を変更するだけでいいのでしょうか? いや、それは違うかもしれません。少なくとも、それを変更するのに最適な場所がどこなのかは私にはわかりません。しかし、board_name を設定するだけではうまくいきませんでしたが、実際にデフォルトの .dtb ファイルも更新したところ、それは機能しました!```
_____ _____ _ _
| _ |___ ___ ___ ___ | _ |___ ___ ||__ | |
| | _| .'| . | . | | | | . | | | -| _| _|
|||| |__,| || || || ||| ||||
|| |___|
Arago Project http://arago-project.org am335x-evm ttyS0
Arago 2021.09 am335x-evm ttyS0
am335x-evm login: root
root@am335x-evm:~#
ようやくターミナルに辿り着いた。私のジャンクボードたちは生きている!
### 欠落しているEEPROM IDの修正
最後のステップとして、正しいボードIDをEEPROMに書き込みます。これはLinuxユーザー空間から簡単に行えます。SPLソースには、次のさまざまなボードIDがあります:
- `A335BONE` - Beagleboneボード
- `A335BNLT` - Beaglebone Blackボード
- `A335PBGL`
- `A335X_SK`
- `A33515BB`
- `A335_ICE`
また、オプションのボードリビジョンもあります。回路図によると、EEPROMはI2C0上にあり、チップ自体(私の回路図では24LC32A。ただし '256Kx8 と表示されています)はI2Cデバイスアドレス `0x50` を提供します(バイナリ `b1010` に続けて `000` のチップアドレス。5ピンパッケージには追加のアドレスピンがないためです)。最後に注意すべき点:WPピンは10kΩのプルアップでHIGHに引き上げられているため、書き込み保護がデフォルトで有効になっています。書き込みを行う前にLOWに結線する必要があります。そうしないと、EEPROMはACKを返すものの、実際には何も書き込みません。
EEPROMは、カーネル経由で `/sys/bus/i2c/devices/0-0050` にアクセスできます。その中に `eeprom` というファイルがあります。したがって、WPピンをLOWに引き下げた状態(DCジャック近くの上部にあるTP4をGNDに接続)で、数回の `echo` 呼び出しを行うだけです。Beaglebone Black System Reference Manual にその形式が記載されています。私はこれを[ここから](https://groups.google.com/g/beagleboard/c/di5O5JCl4yw)参考にしました。```sh
root@am335x-evm:~# cat fix_eeprom.sh
#!/bin/bash
# Fix board ID EEPROM
EEPROM_FILE=/tmp/eeprom.tmp
EEPROM=/sys/bus/i2c/devices/0-0050/eeprom
# header bytes
echo -ne "\xaa\x55\x33\xee" > ${EEPROM_FILE}
# Board ID
echo -n "A335BNLT" >> ${EEPROM_FILE}
# serial number (I left this basically as the template)
echo -n "000C24wwBBoxxxx" >> ${EEPROM_FILE}
dd if=${EEPROM_FILE} of=${EEPROM}
less を使用して確認すると、今後はデフォルトのSDカードから問題なく起動できるはずです。すべてのボードにEEPROMが書き込まれたら、SPLとU-Boot設定への変更はロールバックできます。
| 領域 | 開始アドレス | 長さ |
|---|
| Boot ROM (Public) | 0x4002_0000 | 0xBFFF |
| Boot ROM (Public, alias) | 0x0002_0000 | 0xBFFF |
| SRAM Internal | 0x402F_0400 | 0xFC00 |
| L3 OCM0 | 0x4030_0000 | 0x10000 |
| テストポイント | 接続 | 回路図シート | 基板面 |
|---|
| TP1 | DGND | 2 (D1) | 上面 |
| TP2 | VDD_MPUON (VDD_MPU_MON) | 5 (C4) | 上面 |
| TP3 | TESTOUT | 5 (B2) | 上面 |
| TP4 | Board ID WP | 11 (B1) | 上面 |