SNSへはこちら

ESP8266でベアメタル(3) - (取り急ぎ)SDKを卒業する

これまで NonOS SDK を使ってきました。しかしながら、この SDK には main 関数が存在しなかったり、「パーティションテーブル」という不可解な物があって正直不満です。

ということで、いきなりチャレンジングな内容ですが、毎回恒例(?)の脱ライブラリ化を図ろうと思います。これも苦労したんですよ。。。

また、取り急ぎの動作ということで、色々と把握しきれていません。具体的には...

  • gcc 付属の crt0 を使った
    • 何もしていないのに .bss が初期化されていたり、.data のコピーが既に行われている
    • 後述するように命令コード部分が自動的に RAM にコピーされているため?(不明)
  • リンカスクリプトわからん
    • なんか動いてるからヨシ!!!

と言った感じです。それでは日記形式でどうぞ。

プロジェクトを作っておく

Makefile とか作りました。最低限の構成ですが。テンポラリファイルとか入ってしまっていますが、この記事の末尾にて配布します。ご参考にどうぞ。

プロジェクト構成

上で述べたように、gcc 付属のスタートアップコード、crt0 を使います。準備するのは各種シンボル(リンカスクリプト)と main 関数だけ。main 関数は以下のようなコードを書いてあります。

#define GPIO_BASE 0x60000300
#define GPIO_OUT_OFFSET 0
#define GPIO_ENABLE_OFFSET 3
#define GPIO_IN_OFFSET 6

#define GPIO(reg) (*(volatile unsigned int *)(GPIO_BASE + 4 * GPIO_##reg##_OFFSET))

int main(void) {
    GPIO(ENABLE) = 1 << 4;
    while(1) {
        GPIO(OUT) ^= 1 << 4;
        for(volatile unsigned int i = 0; i < 1000000; i++);
    }
}

最初はこの crt0 を使わずに、_start からアセンブリでスタートアップコードを自作しましたよ。その後、こちらを導入したと言った感じです。

分かったこと

以下が本題です。いろいろ調べたり試行錯誤して分かってきたことをつらつらと書き記しておきます。

バイナリがやや特殊? - マジックナンバーが必要っぽい

最初は適当に gcc とかで bin を生成。具体的にこうやりました。

$ xtensa-lx106-elf-gcc -Os -ffunction-sections -fdata-sections -mtext-section-literals -std=c11 -mlongcalls -c -o obj/main.o src/main.c
$ xtensa-lx106-elf-gcc -Wl,--gc-sections -T linker.ld -o Register.elf obj/start.o obj/main.o
$ xtensa-lx106-elf-objcopy -O binary Register.elf Register.bin

至って普通だと思います。しかし、 esptool.py で書き込もうとすると以下のエラーが。

$ esptool.py write_flash --flash_size 4MB-c1 0x0 Register.bin
esptool.py v3.0
Found 2 serial ports
Serial port /dev/cu.usbmodem14302
Connecting........_____
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
MAC: 
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Warning: Image file at 0x0 doesn't look like an image file, so not changing any flash settings.
Compressed 1124 bytes to 105...
Wrote 1124 bytes (105 compressed) at 0x00000000 in 0.0 seconds (effective 646.4 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

なんか Warning が出ていますねえ。Flash の設定というものが何なのかはわからないですが、ともかく書き込みが完ぺきには行われていないようです。困ったものだ。
このバイナリを焼いた状態で起動した ESP8266 のブートメッセージはこんな感じに出ています。


ets Jan 8 2013,rst cause:2, boot mode:(3,6) ets_main.c

なんか ets_main.c という意味不明な所で止まっています。esptool.py のソースコードを見てみると、ここに当該のエラーメッセージがありました。どうやら、マジックナンバー(詳細は不明)がバイナリに書かれている必要があるっぽい。

esptool.py ではマジックナンバーを追記したバイナリが作れるということで、以下のコマンドを実行です。

$ esptool.py elf2image Register.elf
esptool.py v3.0
Creating image for ESP8266...

すると、同ディレクトリにRegister.elf-0x00000.bin が生成されます。これを書き込むと...

$ esptool.py write_flash 0x0 Register.elf-0x00000.bin
esptool.py v3.0
Found 2 serial ports
Serial port /dev/cu.usbmodem14302
Connecting....
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
MAC: 
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Compressed 1152 bytes to 125...
Wrote 1152 bytes (125 compressed) at 0x00000000 in 0.0 seconds (effective 575.2 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

ワーニングが消えた!!ブートログを見てみましょう。


ets Jan 8 2013,rst cause:2, boot mode:(3,6) load 0x40000000, len 2060, room 16 tail 12 chksum 0xd3 ho 0 tail 12 room 4 load 0x4000080c, len 88, room 12 tail 12 chksum 0x0c csum 0x0c user code done

起動後になぜか止まっているみたいだが、かなりいい感じっぽい。
しかし、「ユーザーコードは終了しました」とのこと。実行すらされていません。というか、無限ループで回すコードだから終了するはずがないんですがそれは...

この問題に関しては、以下のことを調べたらなんとなく分かりました。

ESP8266のハード構成

ESP8266 には ROM のあるアドレスに、リセットベクタが存在します。しかしこの SoC の ROM はマスクROM になっていて、ユーザーがプログラム等で変更できるものではアリません。

じゃあ救いようがないじゃないかと思われるかも知れませんが、このフォーラム投稿記事によると、そもそもユーザーが書いたプログラムコードは SPI FLASH に書かれるらしいんです。つまり esptool で書き込む先は SPI Flash であったということですね。これは憶測ですが、多分書き込み時に指定するアドレスで 0x0 というのは恐らく SPI Flash の先頭位置を意味するのだと思われます。

そして通常起動時にはそのデータが RAM からロードされるようです。つまり CPU は SPI Flash から直接命令を読み込むのではなく、ブート時に SPI FLASH から RAM (Instruction RAM というみたい)へのコピーが起こると考えられます。よく分かりませんが。

そして、その RAM アドレスは 0x40100000 にマッピングされます。つまり CPU の命令はここから実行されるようです。
よって、リンカスクリプトに書く ORIGIN はこの0x40100000である必要があるとわかります。僕が試した限りでは、アドレスを間違えるとこのマッピング先アドレス付近でコードが動いてくれないようなんです。ということで、ORIGIN0x40100000 にしてみました。

ENTRY(_start)
MEMORY {
    CODE_RAM(rx): ORIGIN = 0x40100000, LENGTH = 4M /* Code data in SPI Flash seems to be copied into CODE_RAM on booting. */
    DATA_RAM(wx): ORIGIN = 0x3FFE8000, LENGTH = 0x14000
}

すると...

動きました!!やはりメモリマップは必要ね。

その他、.data 領域に保存される初期値付きグローバル変数等は特に何もしなくても動きます。ビルドして出来たバイナリを探しても、特にデータコピーを行うルーチンはないようです。つまり RAM にデータがコピーされる関係で、通常のマイコンでは必要な値の処理が必要ないということだと考えられます。すっごい楽ですね。

そして、スタックポインタの初期値も設定不要です。リセット時に、自動的にデータ RAM 領域末尾のアドレスが代入されるので、何も気にしなくていいです。

プロジェクトの配布

若干ゴミが混じっていますが、こちらからダウンロードできます。

リンカスクリプト上にある .isr_vector というセクションは使っておりません。しかし今後のデバッグ・タイマー使用等に備えて取り敢えず残してあるのみです。邪魔だと感じる方はまるまる削除して構いません。