3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ESP32でLチカ!でも実は..ベアメタル

Last updated at Posted at 2025-12-06

ESP32でLチカ!でも実はベアメタル1

ESP32でLチカしよう!(今更)

クリスマスも近いし,アセンブラを書いて,GPIOのレジスタを操作して,ESP32のLEDをチカチカさせよう!(唐突)

ESP32をLチカするには一般的にアセンブラを書く必要があります.(大嘘)

ベアメタルってやつだと思われる.(適当)(ベアメタルという単語の意味をよく知らないので実はベアメタルでもないのかもしれない

適当に書いてるので嘘ついてたら教えてください(丸投げ)



この記事はnittc procon Advent Calendar 2025の記事です.ほかの記事もぜひ読んでください!(OBだけど参加している.というかここに登録している人ほとんどOBなんだよな

ESP32 Advent Calendar 2025にも勝手に参加しました.ほかの方の記事もぜひご覧ください.

ESP32

ESP32 DEVKIT V1を使います.
image-8 (小).png

ESP32 WROOM 32が使われてます.

この評価ボードでは,GPIO2に基板上のLEDがつながっています.今回はこのLEDをLチカさせます.

ESP32のマニュアルを読む

マニュアルに書いてあるはずということで,読んでいきます.

ESP32 Technical Reference Manualを開いてGPIOとかで検索すると6.3 Peripheral Output via GPIO Matrixが見つかります.

image-1.png

image-2.png

image.png

とくに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が出てきます.

image-3.png

image-14.png

次の章は6.13 Registersです.

image-4.png

ここら辺にはGPIOに関連するレジスタの説明が載ってます.

読むとGPIOを操作するにあたって使えそうなレジスタがわかります.

image-6.png

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,を使います.

image-7.png

GPIO_ENABLE_REGにも同じように操作できる,GPIO_ENABLE_W1TS_REGがあるのでこれを使います.

image-15.png

GPIO_FUNCx_OUT_SELについてですが,これはGPIO_FUNCn_OUT_SEL_CFG_REGの中にあります.

流れ

今回操作したいGPIOはGPIO2です.
Lチカの流れは以下の通りになります.

  1. 初期化. 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. 初期化2. GPIO_ENABLE_W1TS_REG = 0b100
    GPIOからの出力を有効にします.
    レジスタの説明を見るとわかる通り,操作したいGPIOに対応するBitを立てます.
    今回はGPIO2なので,2^2 = 0b100です.
    アドレスは0x3FF44024

  3. GPIOをHIGHにする. GPIO_OUT_W1TS_REG = 0b100
    セットしたいのでGPIO_OUT_W1TS_REGです.
    アドレスは0x3FF44008

  4. 待機
    適当にループして過ごしましょう.

  5. GPIOをLOWにする. GPIO_OUT_W1TC_REG = 0b100
    セットしたいのでGPIO_OUT_W1TS_REGです.
    アドレスは0x3FF4400C

  6. 待機

  7. 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あたりに説明があります.

image-10.png

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

解説

esptoolwrite-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とかで適当なプロジェクト作ってビルドします.

image-12.png

プロジェクト名.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が読み込んでアプリをロードしてくれるのでしょう.

image-13.png

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

  1. 若おかみは小学生!でも実はヴェノム https://dic.pixiv.net/a/%E8%8B%A5%E3%81%8A%E3%81%8B%E3%81%BF%E3%81%AF%E5%B0%8F%E5%AD%A6%E7%94%9F%21%E3%81%A7%E3%82%82%E5%AE%9F%E3%81%AF%E3%83%B4%E3%82%A7%E3%83%8E%E3%83%A0 2

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?