RaspberryPi で ESP32 のヒューズビット設定・ファームウェア書き込みマシンを作成する

ESP32 のファームウェアを書いたりヒューズビットを設定したりするのは通常 PC から行いますがめんどくさいので RaspberryPi で自動書き込みができるようのものを作りました。


# 用意するもの

- RaspberryPi
- USBメモリ
- ターゲットボード
- 電子ブザー
- メスメスジャンパーケーブル
- RapsberryPi 用電源
-  書き込み治具
今回は、手持ちの RaspberryPi B+ を使用しています。

## 書き込み治具


DevKit のような開発ボードであれば書き込み治具は必要はありません。
しかしながら今回のターゲットはESP32のオリジナルボードなので、以下のようなものを用意しています。


![IMG_20231110_103746.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/7cdcb797-13b2-c4ca-6380-9d1819ebdb05.jpeg)


この治具は、ESP32ボードにこのようなメンテポートを作っていて、

![IMG_20231110_104455.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/7414bd4a-dec5-1e41-434d-56939bef39ca.jpeg)

そこにコンタクトプローブをクリップして書き込みを行っています。

![IMG_20231110_104514.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/692d10c1-7b42-8f8f-b592-504bbfe88a79.jpeg)


この治具は、ダウンロードモードでリセットを行うことができるスイッチを装備しています。

![IMG_20231110_104714.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/fb5c5a48-5467-a750-74e3-b1f08dc3c805.jpeg)

なおオリジナルボードでも、GPIO0をLoにしてResetをするとダウンロードモードになります。



# 作業

## OSの設定

OSイメージとして以下をダウンロード。

- Raspberry Pi OS Lite
  - Release date: October 10th 2023
  - System: 32-bit
  - Kernel version: 6.1
  - Debian version: 12 (bookworm)
  - Size: 568MB
 

ダウンロードした 2023-10-10-raspios-bookworm-armhf-lite.img.xz をこちらの記事の通り microSD カードに書き込み
「Ubuntu Linux で Raspian ZIP ファイルを microSD カードに書き込む ( xz ファイルも )」
https://qiita.com/nanbuwks/items/2c296549ff72dacf7adc

microSD カードを RaspberryPi B+  にセットして起動、

`$ sudo apt upgrade` したあとに以下をインストール

- vim
- git
- picocom

## ESP32 と接続

picocom を使用して、ESP32 のダウンロードモードダイアログが取得できることを確認。
```
$ picocom -b 115200 /dev/ttyUSB0
```

```
.
.
.
st:0x1 (POWERON_RESET),boot:0x3 (DOWNLOAD_BOOT(UART0/UART1/SDIO_REI_REO_V2))
waiting for download
```
となるのを確認します。

## esptool の設定

https://github.com/espressif/esptool より、 esptool をダウンロード

```
$ git clone https://github.com/espressif/esptool.git
$ cd esptool
```

https://qiita.com/nanbuwks/items/339c21478fd31664e5ff
の通り、必要なモジュールをインストールします。

```
$ sudo apt install python3-ecdsa python3-bitstring  python3-pip python3-intelhex python3-serial python3-yaml python3-cryptography

```
また、reedsolo を pip でインストールします。

```
$ sudo pip install reedsolo

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.
    
    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.
    
    For more information visit http://rptl.io/venv

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
```
というエラーが出たので、

cf/.
https://www.jeffgeerling.com/blog/2023/how-solve-error-externally-managed-environment-when-installing-pip3
```
$ sudo rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
```
としてから
```
$ sudo pip install reedsolo
```
とするとインストールできました。

なお、 cryptography は pip だとエラーが出たので

```
$ sudo pip3 install cryptography
Collecting cryptography
  Downloading cryptography-41.0.5.tar.gz (630 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 630.5/630.5 kB 2.2 MB/s eta 0:00:00
  Installing build dependencies ... error
  error: subprocess-exited-with-error
  
  × pip subprocess to install build dependencies did not run successfully.
  │ exit code: 1
  ╰─> [111 lines of output]
```

先に示した apt でインストールしています。


以下のようにして動作確認します。

```
$ python3 espefuse.py -p /dev/ttyUSB0 summary
```

うまくいくと、ヒューズビットが表示されます。

```
espefuse.py v4.7-dev
Connecting..........
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting...
Detecting chip type... ESP32

=== Run "summary" command ===
EFUSE_NAME (Block) Description  = [Meaningful Value] [Readable/Writeable] (Hex Value)
----------------------------------------------------------------------------------------
Calibration fuses:
ADC_VREF (BLOCK0)                                  True ADC reference voltage                         = 1107 R/W (0b00001)

Config fuses:
WR_DIS (BLOCK0)                                    Efuse write disable mask                           = 0 R/W (0x0000)
RD_DIS (BLOCK0)                                    Disable reading from BlOCK1-3                      = 0 R/W (0x0)
DISABLE_APP_CPU (BLOCK0)                           Disables APP CPU                                   = False R/W (0b0)
DISABLE_BT (BLOCK0)                                Disables Bluetooth                                 = False R/W (0b0)
DIS_CACHE (BLOCK0)                                 Disables cache                                     = False R/W (0b0)
CHIP_CPU_FREQ_LOW (BLOCK0)                         If set alongside EFUSE_RD_CHIP_CPU_FREQ_RATED; the = False R/W (0b0)
                                                    ESP32's max CPU frequency is rated for 160MHz. 24
                                                   0MHz otherwise                                    
CHIP_CPU_FREQ_RATED (BLOCK0)                       If set; the ESP32's maximum CPU frequency has been = True R/W (0b1)
                                                    rated                                            
BLK3_PART_RESERVE (BLOCK0)                         BLOCK3 partially served for ADC calibration data   = False R/W (0b0)
CLK8M_FREQ (BLOCK0)                                8MHz clock freq override                           = 52 R/W (0x34)
VOL_LEVEL_HP_INV (BLOCK0)                          This field stores the voltage level for CPU to run = 0 R/W (0b00)
                                                    at 240 MHz; or for flash/PSRAM to run at 80 MHz.0
                                                   x0: level 7; 0x1: level 6; 0x2: level 5; 0x3: leve
                                                   l 4. (RO)                                         
CODING_SCHEME (BLOCK0)                             Efuse variable block length scheme                
   = NONE (BLK1-3 len=256 bits) R/W (0b00)
CONSOLE_DEBUG_DISABLE (BLOCK0)                     Disable ROM BASIC interpreter fallback             = True R/W (0b1)
DISABLE_SDIO_HOST (BLOCK0)                                                                            = False R/W (0b0)
DISABLE_DL_CACHE (BLOCK0)                          Disable flash cache in UART bootloader             = False R/W (0b0)

Flash fuses:
FLASH_CRYPT_CNT (BLOCK0)                           Flash encryption is enabled if this field has an o = 0 R/W (0b0000000)
                                                   dd number of bits set                             
FLASH_CRYPT_CONFIG (BLOCK0)                        Flash encryption config (key tweak bits)           = 0 R/W (0x0)

Identity fuses:
CHIP_PACKAGE_4BIT (BLOCK0)                         Chip package identifier #4bit                      = False R/W (0b0)
CHIP_PACKAGE (BLOCK0)                              Chip package identifier                            = 1 R/W (0b001)
CHIP_VER_REV1 (BLOCK0)                             bit is set to 1 for rev1 silicon                   = True R/W (0b1)
CHIP_VER_REV2 (BLOCK0)                                                                                = True R/W (0b1)
WAFER_VERSION_MINOR (BLOCK0)                                                                          = 1 R/W (0b01)
WAFER_VERSION_MAJOR (BLOCK0)                       calc WAFER VERSION MAJOR from CHIP_VER_REV1 and CH = 3 R/W (0b011)
                                                   IP_VER_REV2 and apb_ctl_date (read only)          
PKG_VERSION (BLOCK0)                               calc Chip package = CHIP_PACKAGE_4BIT << 3 + CHIP_ = 1 R/W (0x1)
                                                   PACKAGE (read only)                               

Jtag fuses:
JTAG_DISABLE (BLOCK0)                              Disable JTAG                                       = False R/W (0b0)

Mac fuses:
MAC (BLOCK0)                                       MAC address                                       
   = a0:a3:b3:0d:13:d4 (CRC 0xa7 OK) R/W 
MAC_CRC (BLOCK0)                                   CRC8 for MAC address                               = 167 R/W (0xa7)
MAC_VERSION (BLOCK3)                               Version of the MAC field                           = 0 R/W (0x00)

Security fuses:
UART_DOWNLOAD_DIS (BLOCK0)                         Disable UART download mode. Valid for ESP32 V3 and = False R/W (0b0)
                                                    newer; only                                      
ABS_DONE_0 (BLOCK0)                                Secure boot V1 is enabled for bootloader image     = False R/W (0b0)
ABS_DONE_1 (BLOCK0)                                Secure boot V2 is enabled for bootloader image     = False R/W (0b0)
DISABLE_DL_ENCRYPT (BLOCK0)                        Disable flash encryption in UART bootloader        = False R/W (0b0)
DISABLE_DL_DECRYPT (BLOCK0)                        Disable flash decryption in UART bootloader        = False R/W (0b0)
KEY_STATUS (BLOCK0)                                Usage of efuse block 3 (reserved)                  = False R/W (0b0)
SECURE_VERSION (BLOCK3)                            Secure version for anti-rollback                   = 0 R/W (0x00000000)
BLOCK1 (BLOCK1)                                    Flash encryption key                              
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
BLOCK2 (BLOCK2)                                    Security boot key                                 
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
BLOCK3 (BLOCK3)                                    Variable Block 3                                  
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 

Spi Pad fuses:
SPI_PAD_CONFIG_HD (BLOCK0)                         read for SPI_pad_config_hd                         = 0 R/W (0b00000)
SPI_PAD_CONFIG_CLK (BLOCK0)                        Override SD_CLK pad (GPIO6/SPICLK)                 = 0 R/W (0b00000)
SPI_PAD_CONFIG_Q (BLOCK0)                          Override SD_DATA_0 pad (GPIO7/SPIQ)                = 0 R/W (0b00000)
SPI_PAD_CONFIG_D (BLOCK0)                          Override SD_DATA_1 pad (GPIO8/SPID)                = 0 R/W (0b00000)
SPI_PAD_CONFIG_CS0 (BLOCK0)                        Override SD_CMD pad (GPIO11/SPICS0)                = 0 R/W (0b00000)

Vdd fuses:
XPD_SDIO_REG (BLOCK0)                              read for XPD_SDIO_REG                              = False R/W (0b0)
XPD_SDIO_TIEH (BLOCK0)                             If XPD_SDIO_FORCE & XPD_SDIO_REG                   = 1.8V R/W (0b0)
XPD_SDIO_FORCE (BLOCK0)                            Ignore MTDI pin (GPIO12) for VDD_SDIO on reset     = False R/W (0b0)

Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V)
```

# 書き込みマシンに仕上げる

## ブザーを鳴らす

GPIO17にブザーをつけて3秒鳴るスクリプトを作成します。

![IMG_20231101_143118.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/c0422d4a-4e61-846f-a686-e4bbca9e75e4.jpeg)

```beep3sec.py


import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(17,GPIO.OUT)
GPIO.output(17,GPIO.HIGH)
time.sleep(3)
GPIO.output(17,GPIO.LOW)
GPIO.cleanup()

```
実行してうまく鳴ったら、/usr/local/bin に実行権をつけて保存しておきます。
同様に、以下のスクリプトも /usr/local/bin に作っておきます。


```booboo.py
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(17,GPIO.OUT)
GPIO.output(17,GPIO.HIGH)
time.sleep(1)
GPIO.output(17,GPIO.LOW)
time.sleep(0.4)
GPIO.output(17,GPIO.HIGH)
time.sleep(1)
GPIO.output(17,GPIO.LOW)
GPIO.cleanup()



```


```pipipipipi.py
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(17,GPIO.OUT)
GPIO.output(17,GPIO.HIGH)
time.sleep(0.04)
GPIO.output(17,GPIO.LOW)
time.sleep(0.1)
GPIO.output(17,GPIO.HIGH)
time.sleep(0.04)
GPIO.output(17,GPIO.LOW)
time.sleep(0.1)
GPIO.output(17,GPIO.HIGH)
time.sleep(0.04)
GPIO.output(17,GPIO.LOW)
time.sleep(0.1)
GPIO.output(17,GPIO.HIGH)
time.sleep(0.04)
GPIO.output(17,GPIO.LOW)
time.sleep(0.1)
GPIO.output(17,GPIO.HIGH)
time.sleep(0.04)
GPIO.output(17,GPIO.LOW)
GPIO.cleanup()

```

```espfuse.sh
#!/bin/bash

WRITECMD="/home/pi/esptool-master/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0xe000 /mnt/boot_app0.bin 0x1000 /mnt/bootloader_dio_80m.bin 0x10000 /mnt/firmware.bin 0x8000 /mnt/partitions.bin"

FUSECMD="/home/pi/esptool-master/espefuse.py  --do-not-confirm -p /dev/ttyUSB0  set_flash_voltage 3.3V"

FUSEOK="Flash voltage (VDD_SDIO) set to 3.3V by efuse."
FUSENOTYET="Flash voltage (VDD_SDIO) determined by GPIO12 on reset"
WRITEOK="Hash of data verified."

while true
do
  FUSECHECKRESULT=`/home/pi/esptool-master/espefuse.py  -p /dev/ttyUSB0  summary | tail -1`
  echo $FUSECHECKRESULT
  if [[ $FUSEOK = $FUSECHECKRESULT  ]]; then
        WRITERESULT=$(eval ${WRITECMD})
        if [[ $WRITERESULT = *"$WRITEOK"*  ]]; then
          echo $WRITEOK
          echo "OK"
          python3 /usr/local/bin/pipipipipi.py
        else
                echo $WRITERESULT
                echo "Fuse is already 3.3V set but firmware hashdata NG"
                python3 /usr/local/bin/booboo.py
        fi
  else
        if [[ "$FUSECHECKRESULT" =~ "$FUSENOTYET" ]]; then
            WRITERESULT=$(eval ${FUSECMD})
            echo $WRITERESULT

        else
                echo "fuse bit setting NG"
                python3 /usr/local/bin/booboo.py
        fi
  fi
done
```

ヘッドレスで動作するので、起動したことがわかるように音を鳴らすようにします。
 /etc/rc.local の最後らへんを、書き換えます。
 
 書き換え前
 
```
.
.
.
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi


exit 0

```

書き換え後
 
```
.
.
.
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi


python3 /usr/local/bin/beep3sec.py
exit 0

```

再起動して、音が鳴るかどうかチェック。


## USBメモリでファームウェア管理

ファームウェアを USB メモリに入れて管理するようにします。

PC で Arduino IDE をCUIで起動、スケッチを書きこむところまで行います。そのときにコマンドラインに表示されるラインをコピーします。
```
python /home/nanbuwks/Downloads/ardublock/arduino/linux64/AKBONE2020_linux_1.8.13/portable/packages/esp32/tools/esptool_py/2.6.1/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0xe000 /home/nanbuwks/Downloads/ardublock/arduino/linux64/AKBONE2020_linux_1.8.13/portable/packages/esp32/hardware/esp32/1.0.4/tools/partitions/boot_app0.bin 0x1000 /home/nanbuwks/Downloads/ardublock/arduino/linux64/AKBONE2020_linux_1.8.13/portable/packages/esp32/hardware/esp32/1.0.4/tools/sdk/bin/bootloader_dio_80m.bin 0x10000 /tmp/arduino_build_335897/ESP32_DUALDIAL_OSS.ino.bin 0x8000 /tmp/arduino_build_335897/ESP32_DUALDIAL_OSS.ino.partitions.bin 
```

上記の例では、 ESP32_DUALDIAL_OSS というスケッチをビルドして書き込んでいますが、以下のファイルが使われていることがわかります。

- boot_app0.bin
- bootloader_dio_80m.bin
- ESP32_DUALDIAL_OSS.ino.bin
- ESP32_DUALDIAL_OSS.ino.partitions.bin 


Arduino IDE を閉じると tmp ファイルは消えてしまいます。なので IDE を閉じずに別ターミナルでこれらを USB メモリにコピーします。

- <スケッチ名>.ino.bin
- <スケッチ名>.ino.partitions.bin

については、 

- firmware.bin
- partitions.bin 

に名前を変更します。


![Screenshot from 2023-11-01 14-59-07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/3da2043c-14c0-cdfc-095b-bb80cc6ddbd1.png)

USBメモリを起動時にマウントするように、 /etc/rc.local を以下のように変更します。
```
.
.
.
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi


mount /dev/sda1 /mnt
python3 /usr/local/bin/beep3sec.py
exit 0
```
として再起動、ファームウェアが /mnt 下に見えることを確認します。


うまくいけば、以下のように自動起動を設定して完成です。
```


mount /dev/sda1 /mnt
python3 /usr/local/bin/beep3sec.py
/usr/local/bin/espfuse.sh
exit 0
```

         
# 使い方
![IMG_20231110_105212.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/ba5b29fd-20e4-fc5c-e7cc-2e1d6c0c5251.jpeg)

## 準備

 - ターゲットボードは「wait for download」 モードになるようにしておきます。
- RaspberryPi の電源が必要です。使用する RaspberryPi に応じて十分な電源を取るようにしてください。

## 起動
電源ONにして、起動完了までしばらく待ちます。起動完了するとブザーが長く一度鳴ります。


## 書き込みループ

起動完了すると USBシリアルで接続しているかどうかの無限ループに入り、接続していなかったり正常な応答が無ければブザーが2回鳴ります。



正常な応答があれば、各種設定を行って終了したらピピピピピとブザーが5回鳴ります。ブザーが5回鳴ったら、連続して書き込みが行われないようにすぐにESP32を取り外してください。



# 応用

もちろん、ESP32 でなくても USBシリアル経由で設定するもの、あるいはその他GPIOあんどで設定するものも、入れ替えれば対応できます。