ESP32でLチカ!でも実はベアメタル1
ESP32でLチカしよう!(今更)
クリスマスも近いし,アセンブラを書いて,GPIOのレジスタを操作して,ESP32のLEDをチカチカさせよう!(唐突)
ESP32をLチカするには一般的にアセンブラを書く必要があります.(大嘘)
ベアメタルってやつだと思われる.(適当)(ベアメタルという単語の意味をよく知らないので実はベアメタルでもないのかもしれない
適当に書いてるので嘘ついてたら教えてください(丸投げ)
この記事はnittc procon Advent Calendar 2025の記事です.ほかの記事もぜひ読んでください!(OBだけど参加している.というかここに登録している人ほとんどOBなんだよな
ESP32 Advent Calendar 2025にも勝手に参加しました.ほかの方の記事もぜひご覧ください.
ESP32
ESP32 WROOM 32が使われてます.
この評価ボードでは,GPIO2に基板上のLEDがつながっています.今回はこのLEDをLチカさせます.
ESP32のマニュアルを読む
マニュアルに書いてあるはずということで,読んでいきます.
ESP32 Technical Reference Manualを開いてGPIOとかで検索すると6.3 Peripheral Output via GPIO Matrixが見つかります.
とくに6.3.3 Simple GPIO Outputによると
To configure a pin as simple GPIO output, the GPIO Matrix GPIO_FUNCx_OUT_SEL register is configured with a special peripheral index value (0x100).
とあります.
まあよくわからんけど,GPIO_FUNCx_OUT_SEL=0x100して,GPIO_ENABLE_REGのビットを立てると,GPIO_OUT_DATAにセットされた値が出力されるってことでしょう.
GPIO_ENABLE_REGで検索すると6.12 Register Summaryが出てきます.
次の章は6.13 Registersです.
ここら辺にはGPIOに関連するレジスタの説明が載ってます.
読むとGPIOを操作するにあたって使えそうなレジスタがわかります.
GPIO_OUT_W1TS_REG, GPIO_OUT_W1TC_REG.
これらのレジスタはGPIO_OUT_REGを便利に操作できるレジスタです.
GPIO_OUT_REGに直接値を入れることもできますが特定のBitだけ操作したいときにちょっとめんどくさいです.
そこでこれらのレジスタを使うと,Bitが立っているところだけ,GPIO_OUT_REGのBitを操作してくれます.
BitをセットしたいときはGPIO_OUT_W1TS_REG,
BitをクリアしたいときはGPIO_OUT_W1TC_REG,を使います.
GPIO_ENABLE_REGにも同じように操作できる,GPIO_ENABLE_W1TS_REGがあるのでこれを使います.
GPIO_FUNCx_OUT_SELについてですが,これはGPIO_FUNCn_OUT_SEL_CFG_REGの中にあります.
流れ
今回操作したいGPIOはGPIO2です.
Lチカの流れは以下の通りになります.
-
初期化. GPIO_FUNC2_OUT_SEL = 0x100
GPIO_FUNCx_OUT_SELはGPIO_FUNCn_OUT_SEL_CFG_REGにある.GPIO2なので,x=2でGPIO_FUNC2_OUT_SEL_CFG_REG.
アドレスは0x3FF44538 -
初期化2. GPIO_ENABLE_W1TS_REG = 0b100
GPIOからの出力を有効にします.
レジスタの説明を見るとわかる通り,操作したいGPIOに対応するBitを立てます.
今回はGPIO2なので,2^2 = 0b100です.
アドレスは0x3FF44024 -
GPIOをHIGHにする. GPIO_OUT_W1TS_REG = 0b100
セットしたいのでGPIO_OUT_W1TS_REGです.
アドレスは0x3FF44008 -
待機
適当にループして過ごしましょう. -
GPIOをLOWにする. GPIO_OUT_W1TC_REG = 0b100
セットしたいのでGPIO_OUT_W1TS_REGです.
アドレスは0x3FF4400C -
待機
-
3に戻る
アセンブリ
ESP32 WROOM 32にはXtensa® 32-bit LX6が使われてるみたいです.
「xtensa instruction set」とかで調べると以下のPDFが見つかるので,これを見ながら書きましょう.
Xtensa® Instruction Set
Architecture (ISA) Summary
Compiler Explorerでサンプルコードを生成しながら書いてもいいと思います.
最終的には以下のようになりました.
.text
.globl _start
GPIO_FUNC2_OUT_SEL_CFG_REG:
.word 0x3ff44538
GPIO_ENABLE_W1TS_REG:
.word 0x3ff44024
GPIO_OUT_W1TS_REG:
.word 0x3ff44008
GPIO_OUT_W1TC_REG:
.word 0x3ff4400c
.equ TARGET_GPIO, 0b100
.equ SLEEP_TIME, 0x100000
.equ SLEEP_TIME, 0x100000
_start:
l32r a10, GPIO_FUNC2_OUT_SEL_CFG_REG
movi a11, 0x100
s16i a11, a10, 0
l32r a10, GPIO_ENABLE_W1TS_REG
movi a11, TARGET_GPIO
s32i a11, a10, 0
high_led:
l32r a10, GPIO_OUT_W1TS_REG
movi a11, TARGET_GPIO
s32i a11, a10, 0
movi a10, SLEEP_TIME
sleep:
addi a10, a10, -1
bnez a10, sleep
low_led:
l32r a10, GPIO_OUT_W1TC_REG
movi a11, TARGET_GPIO
s32i a11, a10, 0
movi a10, SLEEP_TIME
sleep2:
addi a10, a10, -1
bnez a10, sleep2
j high_led
まあなんとなくでわかるだろうから解説はしない!!!!!!!!!!!(めんどいだけ
命令がわからなければ,さっきのPDFで調べれば出ます.
アセンブリがわからなければ以下のチュートリアルでとりあえずx86_64のアセンブリに入門してみましょう.(丸投げ)(許して)
まあ流れで書いた流れと同じです.
環境
xtensa-esp-elfをダウンロードしてください.これらでビルドします.
esptoolという,ESP32に書き込みとかいろいろやってくれるツールもインストールして置いてください.
pip install esptool
でできるっぽいです.
Build
以下のコマンドでビルドします.
xtensa-esp32-elf-as -o main.o main.s
xtensa-esp32-elf-ld -Ttext=0x40080000 -e _start -o main.elf main.o
esptool --chip ESP32 elf2image --flash-mode="dio" --flash-freq "40m" --flash-size "4MB" main.elf
解説
xtensa-esp32-elf-as -o main.o main.s
ここでXtensaの機械語にしてオブジェクトファイルを作ってます.
xtensa-esp32-elf-ld -Ttext=0x40080000 -e _start -o main.elf main.o
リンカでいろいろ設定して,オブジェクトファイルをelfにしてます.
xtensa-esp32-elf-ld -Ttext=0x40080000 -e _start -o main.elf main.o
-eで_startをエントリポイントにしてます.
-Ttext=0x40080000では,起動したときにbootloaderというプログラムがROM上にあるこのアプリ(main.bin)を読み込むのですが,その際にRAM上のどこにアプリのプログラムを置くかを指定しています.(適当なのでそんな信用しないで)
なぜ0x40080000かというと,
ESP-IDF allocates part of Internal SRAM0 region (defined in the Technical Reference Manual) for instruction RAM. Except for the first 64 kB block which is used for PRO and APP CPU caches, the rest of this memory range (i.e. from 0x40080000 to 0x400A0000) is used to store parts of application which need to run from RAM.
とあるように 0x40080000~0x400A0000にアプリを配置すると書いてあるからです(適当.
またTechnical Reference Manualのp68のTable 3.3-2. Embedded Memory Address Mappingあたりに説明があります.
3.3.2.3 Internal SRAM 0
The capacity of Internal SRAM 0 is 192 KB. Hardware can be configured to use the first 64 KB to cache external memory access. When not used as cache, the first 64 KB can be read and written by either CPU at addresses 0x4007_0000 ~ 0x4007_FFFF of the instruction bus. The remaining 128 KB can always be read and written by either CPU at addresses 0x4008_0000 ~ 0x4009_FFFF of instruction bus
翻訳
内部SRAM 0の容量は192KBです。ハードウェア設定により、最初の64KBを外部メモリアクセスのキャッシュに使用することが可能です。キャッシュとして使用しない場合、最初の64KBは命令バスのアドレス0x4007_0000~0x4007_FFFFで、どちらのCPUからも読み書き可能です。残りの128KBは、命令バスのアドレス0x4008_0000~0x4009_FFFFで、どちらのCPUからも常に読み書き可能です。
らしい.
esptool --chip ESP32 elf2image --flash-mode="dio" --flash-freq "40m" --flash-size "4MB" main.elf
esp32ではファームウェアのフォーマットが決まっています.
なのでこのフォーマットに対応させたファイル(Firmware Image. image)を作るため,ELFからimageに変換しています.
書き込み
esptoolでやります.
--port /dev/ttyUSB0部分は自分の環境に合わせて変えてください.(windowsならCOMなんたら)
esptool --chip esp32 --port /dev/ttyUSB0 --baud 921600 write-flash --flash-mode dio --flash-freq 40m --flash-size detect 0x1000 main.bin
解説
esptoolのwrite-flashを使ってます.
0x1000 main.binの部分で,Flashの0x1000に配置するように指定しています.
なぜ0x1000かというとFirst stage bootloaderはFlashの0x1000からファームウェア(image)を読み込むからです.
これはApplication Startup Flowにかいてあります.
First stage bootloader in ROM loads second-stage bootloader image to RAM (IRAM & DRAM) from flash offset 0x1000.
https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32s2/api-guides/startup.html
今回はSecond stage bootloaderを使わないので,アプリのimageを0x1000に直接おいています.
動作
うごくかな?
うごいた~~~!(マクドナルドのCMに出てくる小学生並みの感想
やったね.これでキラキラしたクリスマスを過ごせます!
実験
今回は0x1000に直接おいて動かしました.
試しにSecond stage bootloaderを使って動かしてみましょう.
Second stage bootloaderを使う
Second stage bootloaderを使って動かすには,アプリのイメージのほかにSecond stage bootloaderのイメージも書き込む必要があります.
Second stage bootloaderですが,どうやって作るのかわからなかったので,ArduinoIDEとかで適当なプロジェクト作ってビルドします.
プロジェクト名.ino.bootloader.bin
プロジェクト名.ino.partitions.bin
すると以上のような二つのファイルができると思うのでこれを使います.(もしくは書き込んでしまってあとでアプリのイメージだけを書き換えてもいいです.)
プロジェクト名.ino.bootloader.binは0x1000に書き込みます.
プロジェクト名.ino.partitions.binのほうは以下のとおり0x8000に書き込みます.
Second stage bootloader reads the partition table found at offset 0x8000. See partition tables documentation for more information. The bootloader finds factory and OTA partitions, and decides which one to boot based on data found in OTA info partition.
https://espressif-docs.readthedocs-hosted.com/projects/esp-idf/en/latest/api-guides/general-notes.html#second-stage-bootloader
Partition Tablesにはアプリのイメージの場所がどこにあるかなどが書かれてるっぽいです.これをSecond stage bootloaderが読み込んでアプリをロードしてくれるのでしょう.
In both cases the factory app is flashed at offset 0x10000
https://espressif-docs.readthedocs-hosted.com/projects/esp-idf/en/latest/api-guides/partition-tables.html
アプリの場所は0x10000にあると書いてあるみたいです.
また以下のスクリプトを使うと,partitionのバイナリフォーマットからCSVにしてくれるみたいなので,さっきのファイルを見てみましょう.
~/esp32AsmChallenge
❯ python3 gen_esp32part.py sketch_nov13a.ino.partitions.bin
Parsing binary partition input...
Verifying table...
# Espressif ESP32 Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,20K,
otadata,data,ota,0xe000,8K,
app0,app,ota_0,0x10000,1280K,
app1,app,ota_1,0x150000,1280K,
spiffs,data,spiffs,0x290000,1408K,
coredump,data,coredump,0x3f0000,64K,
するとapp0,app,ota_0,0x10000,1280K,とあるので,アプリのイメージは0x10000に書き込めばよさそうですね.
これらのことから以下のように書き込みます.
esptool --chip esp32 --port /dev/ttyUSB0 --baud 921600 write-flash \
--flash-mode dio --flash-freq 40m --flash-size detect \
0x1000 ./sketch_nov13a.ino.bootloader.bin \
0x8000 ./sketch_nov13a.ino.partitions.bin \
0x10000 main.bin \
(sketch_nov13aの部分は置き換えてね)
では動作を見て
0x1000に書き込んだときとSecond stage bootloaderを使ったときを比較してみましょう.
考察
比較してみると直接おいたほうはちょっとちらついているように見えましたよね.
点滅速度も全然違います.
なんででしょうか?
理由を考えましたが,本当に適当なので,信じないでください.詳しい人教えてください.
おそらく嘘ついてた適当な考察
正直あまりわかってないのですが,ESP32には2つのコアがあり,そのコアが違うからだと思われます(本当に適当なので,信じないで.詳しい人教えて
コアは
- core 0 (PRO CPU
- core 1 (APP CPU
の二つです.
First stage bootloader
After SoC reset, PRO CPU will start running immediately,
https://espressif-docs.readthedocs-hosted.com/projects/esp-idf/en/latest/api-guides/general-notes.html#first-stage-bootloader
Note that it's a legacy name. The initial design for the ESP32 called for an asymmetric multiprocessor setup, with CPU0 running all the PROtocol handling code, while the APPlication would run on CPU1. We changed that to a symmetric multiprocessor setup later on, and at the moment the two CPUs are (with very few small exceptions) fully inter-exchangable, and the PRO and APP names are nothing but some names remaining from the earlier design.
https://esp32.com/viewtopic.php?t=8558
などとあり,なんか最初はPRO CPUでアプリはAPP CPUで呼ばれるのではないでしょうか?(本当に適当なので,詳しい人教えて
その切り替えがSecond stage bootloaderに存在しているため,Second stage bootloaderを経由すると点滅速度が速くなったのではないかと思われます.(適当
GPTに聞いてみると,CPUのクロックソースが違うからだといってきました.
GPTによるとデフォルトだとXTL_CLK(2~40MHz)を使うらしいです.デフォルトでXTL_CLであるというソースは見つけられていないので本当かは謎.
でもSecond stage bootloaderで何らかの設定がなされていると思われるので,かなりそれっぽいですね.
CPUのクロック指定もできるので,いずれそれも実験してみたいですね.
終わり
読んでくれてありがとうございました!
Lチカをしようとしている初心者に投げると面白いんじゃないでしょうか?
最後の考察の部分は本当に適当なので信じないでください.詳しい人おしえてくれー
この記事はnittc procon Advent Calendar 2025の記事です.ほかの記事もぜひ読んでください!(OBだけど参加している.というかここに登録している人ほとんどOBなんだよな
ESP32 Advent Calendar 2025にも勝手に参加しました.ほかの方の記事もぜひご覧ください.
おまけ
Makefile
CC = xtensa-esp32-elf-gcc
OBJCOPY = xtensa-esp32-elf-objcopy
AS = xtensa-esp32-elf-as
LD = xtensa-esp32-elf-ld
OBJCOPY = xtensa-esp32-elf-objcopy
SAMPLE_PROJECT_NAME = sketch_nov13a
TARGET = main
ENTRY = _start
PORT = /dev/ttyUSB0
all: $(TARGET).bin
$(TARGET).o: $(TARGET).s
$(AS) -o $@ $<
$(TARGET).elf: $(TARGET).o
$(LD) -Ttext=0x40080000 -e _start -o $@ $<
$(TARGET).bin: $(TARGET).elf
esptool --chip ESP32 elf2image --flash-mode="dio" --flash-freq "40m" --flash-size "4MB" $<
flash: $(TARGET).bin
esptool --chip esp32 --port $(PORT) --baud 921600 write-flash \
--flash-mode dio --flash-freq 40m --flash-size detect \
0x1000 ./$(SAMPLE_PROJECT_NAME).ino.bootloader.bin \
0x8000 ./$(SAMPLE_PROJECT_NAME).ino.partitions.bin \
0x10000 $(TARGET).bin \
flash_only_app: $(TARGET).bin
esptool --chip esp32 --port $(PORT) --baud 921600 write-flash \
--flash-mode dio --flash-freq 40m --flash-size detect \
0x1000 $(TARGET).bin \
clean:
rm -f $(TARGET).o $(TARGET).elf $(TARGET).bin
.PHONY: all clean flash
タイトル1












