Skip to content

Commit 9b0a4d6

Browse files
committed
tools/mesptool.py: A minimal ESP32 bootloader protocol implementation.
* This tool implements a subset of the ESP32 ROM bootloader protocol, and it's mainly intended for updating Nina WiFi firmware from MicroPython, but can be used to flash any ESP32 chip.
1 parent 0c5880d commit 9b0a4d6

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

micropython/mesptool/example_flash.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import mesptool
2+
3+
md5sum = b"9a6cf1257769c9f1af08452558e4d60e"
4+
path = "NINA_W102-v1.5.0-Nano-RP2040-Connect.bin"
5+
6+
esp = mesptool()
7+
# Enter bootloader download mode, at 115200
8+
esp.bootloader()
9+
# Can now chage to higher/lower baudrate
10+
esp.set_baudrate(921600)
11+
# Init the flash functions, must be called first.
12+
esp.flash_init()
13+
# Write firmware image from internal storage.
14+
esp.flash_write(path)
15+
# Compares file and flash MD5 checksum.
16+
esp.flash_verify(path, md5sum)
17+
# Resets the ESP32 chip.
18+
esp.reboot()

micropython/mesptool/manifest.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
metadata(
2+
version="0.1",
3+
description="Provides a minimal ESP32 bootloader protocol implementation.",
4+
)
5+
6+
module("mesptool.py")

micropython/mesptool/mesptool.py

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# This file is part of the MicroPython project, http://micropython.org/
2+
#
3+
# The MIT License (MIT)
4+
#
5+
# Copyright (c) 2022 Ibrahim Abdelkader <[email protected]>
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in
15+
# all copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
# THE SOFTWARE.
24+
#
25+
# A minimal esptool implementation to communicate with ESP32 ROM bootloader.
26+
# Note this tool does Not support advanced features, other ESP chips or stub loading.
27+
# This is only meant to be used for updating the U-blox Nina module firmware.
28+
29+
import os
30+
import struct
31+
from machine import Pin
32+
from machine import UART
33+
from micropython import const
34+
from time import sleep
35+
36+
37+
class mesptool:
38+
FLASH_ID = const(0)
39+
FLASH_BLOCK_SIZE = const(64 * 1024)
40+
FLASH_SECTOR_SIZE = const(4 * 1024)
41+
FLASH_PAGE_SIZE = const(256)
42+
43+
CMD_SYNC = const(0x08)
44+
CMD_CHANGE_BAUDRATE = const(0x0F)
45+
CMD_SPI_ATTACH = const(0x0D)
46+
CMD_SPI_FLASH_MD5 = const(0x13)
47+
CMD_SPI_FLASH_PARAMS = const(0x0B)
48+
CMD_SPI_FLASH_BEGIN = const(0x02)
49+
CMD_SPI_FLASH_DATA = const(0x03)
50+
CMD_SPI_FLASH_END = const(0x04)
51+
52+
ESP_ERRORS = {
53+
0x05: "Received message is invalid",
54+
0x06: "Failed to act on received message",
55+
0x07: "Invalid CRC in message",
56+
0x08: "Flash write error",
57+
0x09: "Flash read error",
58+
0x0A: "Flash read length error",
59+
0x0B: "Deflate error",
60+
}
61+
62+
def __init__(
63+
self, reset=3, gpio0=2, uart_id=1, uart_tx=Pin(8), uart_rx=Pin(9), log_enabled=False
64+
):
65+
self.uart_id = uart_id
66+
self.uart_tx = uart_tx
67+
self.uart_rx = uart_rx
68+
self.uart_buf = 4096
69+
self.uart_baudrate = 115200
70+
self.log = log_enabled
71+
self.reset_pin = Pin(reset, Pin.OUT, Pin.PULL_UP)
72+
self.gpio0_pin = Pin(gpio0, Pin.OUT, Pin.PULL_UP)
73+
self.set_baudrate(115200)
74+
75+
def _log(self, data, out=True):
76+
if self.log:
77+
size = len(data)
78+
print(
79+
f"out({size}) => " if out else f"in({size}) <= ",
80+
"".join("%.2x" % (i) for i in data[0:10]),
81+
)
82+
83+
def set_baudrate(self, baudrate, timeout=350):
84+
if baudrate != self.uart_baudrate:
85+
print(f"Changing baudrate => {baudrate}")
86+
self.uart_drain()
87+
self.command(CMD_CHANGE_BAUDRATE, struct.pack("<II", baudrate, 0))
88+
self.uart_baudrate = baudrate
89+
self.uart = UART(
90+
self.uart_id,
91+
baudrate,
92+
tx=self.uart_tx,
93+
rx=self.uart_rx,
94+
rxbuf=self.uart_buf,
95+
txbuf=self.uart_buf,
96+
timeout=timeout,
97+
)
98+
self.uart_drain()
99+
100+
def uart_drain(self):
101+
while self.uart.read(1) is not None:
102+
pass
103+
104+
def write_slip(self, pkt):
105+
pkt = pkt.replace(b"\xDB", b"\xdb\xdd").replace(b"\xc0", b"\xdb\xdc")
106+
self.uart.write(b"\xC0" + pkt + b"\xC0")
107+
self._log(pkt)
108+
109+
def read_slip(self):
110+
pkt = None
111+
# Find the packet start.
112+
if self.uart.read(1) == b"\xC0":
113+
pkt = bytearray()
114+
while True:
115+
b = self.uart.read(1)
116+
if b is None or b == b"\xC0":
117+
break
118+
pkt += b
119+
pkt = pkt.replace(b"\xDB\xDD", b"\xDB").replace(b"\xDB\xDC", b"\xC0")
120+
self._log(b"\xC0" + pkt + b"\xC0", False)
121+
return pkt
122+
123+
def esperror(self, err):
124+
if err in self.ESP_ERRORS:
125+
return self.ESP_ERRORS[err]
126+
return "Unknown error"
127+
128+
def checksum(self, data):
129+
checksum = 0xEF
130+
for i in data:
131+
checksum ^= i
132+
return checksum
133+
134+
def command(self, cmd, payload=b"", checksum=0):
135+
self.write_slip(struct.pack(b"<BBHI", 0, cmd, len(payload), checksum) + payload)
136+
for i in range(10):
137+
pkt = self.read_slip()
138+
if pkt is not None and len(pkt) >= 8:
139+
(inout, cmd_id, size, val) = struct.unpack("<BBHI", pkt[:8])
140+
if inout == 1 and cmd == cmd_id:
141+
status = list(pkt[-4:])
142+
if status[0] == 1:
143+
raise Exception(f"Command {cmd} failed {self.esperror(status[1])}")
144+
return pkt[8:]
145+
raise Exception(f"Failed to read response to command {cmd}.")
146+
147+
def bootloader(self, retry=6):
148+
for i in range(retry):
149+
self.gpio0_pin(1)
150+
self.reset_pin(0)
151+
sleep(0.1)
152+
self.gpio0_pin(0)
153+
self.reset_pin(1)
154+
sleep(0.1)
155+
self.gpio0_pin(1)
156+
157+
if "POWERON_RESET" not in self.uart.read():
158+
continue
159+
160+
for i in range(10):
161+
self.uart_drain()
162+
try:
163+
# 36 bytes: 0x07 0x07 0x12 0x20, followed by 32 x 0x55
164+
self.command(CMD_SYNC, b"\x07\x07\x12\x20" + 32 * b"\x55")
165+
self.uart_drain()
166+
return True
167+
except Exception as e:
168+
print(e)
169+
170+
raise Exception("Failed to enter download mode!")
171+
172+
def flash_init(self, flash_size=2 * 1024 * 1024):
173+
self.command(CMD_SPI_ATTACH, struct.pack("<II", 0, 0))
174+
self.command(
175+
CMD_SPI_FLASH_PARAMS,
176+
struct.pack(
177+
"<IIIIII",
178+
self.FLASH_ID,
179+
flash_size,
180+
self.FLASH_BLOCK_SIZE,
181+
self.FLASH_SECTOR_SIZE,
182+
self.FLASH_PAGE_SIZE,
183+
0xFFFF,
184+
),
185+
)
186+
187+
def flash_write(self, path, blksize=0x1000):
188+
size = os.stat(path)[6]
189+
total_blocks = (size + blksize - 1) // blksize
190+
erase_blocks = 1
191+
print(f"Flash write size: {size} total_blocks: {total_blocks} block size: {blksize}")
192+
with open(path, "rb") as f:
193+
seq = 0
194+
subseq = 0
195+
for i in range(total_blocks):
196+
buf = f.read(blksize)
197+
if len(buf) < blksize:
198+
# The last data block should be padded to the block size with 0xFF bytes.
199+
buf += b"\xFF" * (blksize - len(buf))
200+
checksum = self.checksum(buf)
201+
if seq % erase_blocks == 0:
202+
# print(f"Erasing {seq} -> {seq+erase_blocks}...")
203+
self.command(
204+
self.CMD_SPI_FLASH_BEGIN,
205+
struct.pack(
206+
"<IIII", erase_blocks * blksize, erase_blocks, blksize, seq * blksize
207+
),
208+
)
209+
print(f"Writing sequence number {seq}/{total_blocks}...")
210+
self.command(
211+
self.CMD_SPI_FLASH_DATA,
212+
struct.pack("<IIII", len(buf), seq % erase_blocks, 0, 0) + buf,
213+
checksum,
214+
)
215+
seq += 1
216+
217+
print("Flash write finished")
218+
219+
def flash_verify(self, path, md5sum, offset=0):
220+
size = os.stat(path)[6]
221+
data = self.command(CMD_SPI_FLASH_MD5, struct.pack("<IIII", offset, size, 0, 0))
222+
print(f"Flash verify file MD5 {md5sum}")
223+
print(f"Flash verify flash MD5 {bytes(data[0:32])}")
224+
if md5sum == data[0:32]:
225+
print("Firmware write verified")
226+
else:
227+
raise Exception(f"Firmware verification failed")
228+
229+
def reboot(self):
230+
payload = struct.pack("<I", 0)
231+
self.write_slip(
232+
struct.pack(b"<BBHI", 0, self.CMD_SPI_FLASH_END, len(payload), 0) + payload
233+
)

0 commit comments

Comments
 (0)