Adafruit_CircuitPython_mini.../adafruit_miniesptool.py
2025-05-16 16:53:14 +00:00

469 lines
16 KiB
Python

# SPDX-FileCopyrightText: 2018 ladyada for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_miniesptool`
====================================================
ROM loader for ESP chips, works with ESP8266 or ESP32.
This is a 'no-stub' loader, so you can't read MD5 or firmware back on ESP8266.
See this document for protocol we're implementing:
https://github.com/espressif/esptool/wiki/Serial-Protocol
See this for the 'original' code we're miniaturizing:
https://github.com/espressif/esptool/blob/master/esptool.py
There's a very basic Arduino ROM loader here for ESP32:
https://github.com/arduino-libraries/WiFiNINA/tree/master/examples/Tools/FirmwareUpdater
* Author(s): ladyada
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
import os
import struct
import time
from digitalio import Direction
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_miniesptool.git"
SYNC_PACKET = b"\x07\x07\x12 UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
ESP32_DATAREGVALUE = 0x15122500
ESP8266_DATAREGVALUE = 0x00062000
# Commands supported by ESP8266 ROM bootloader
ESP_FLASH_BEGIN = 0x02
ESP_FLASH_DATA = 0x03
ESP_FLASH_END = 0x04
ESP_MEM_BEGIN = 0x05
ESP_MEM_END = 0x06
ESP_MEM_DATA = 0x07
ESP_SYNC = 0x08
ESP_WRITE_REG = 0x09
ESP_READ_REG = 0x0A
ESP_SPI_SET_PARAMS = 0x0B
ESP_SPI_ATTACH = 0x0D
ESP_CHANGE_BAUDRATE = 0x0F
ESP_SPI_FLASH_MD5 = 0x13
ESP_CHECKSUM_MAGIC = 0xEF
ESP8266 = 0x8266
ESP32 = 0x32
FLASH_SIZES = {
"512KB": 0x00,
"256KB": 0x10,
"1MB": 0x20,
"2MB": 0x30,
"4MB": 0x40,
"2MB-c1": 0x50,
"4MB-c1": 0x60,
"8MB": 0x80,
"16MB": 0x90,
}
class miniesptool:
"""A miniature version of esptool, a programming command line tool for
ESP8266 and ESP32 chips. This version is minimized to work on CircuitPython
boards, so you can burn ESP firmware direct from the CPy disk drive. Handy
when you have an ESP module wired to a board and need to upload new AT
firmware. Its slow! Expect a few minutes when programming 1 MB flash."""
FLASH_WRITE_SIZE = 0x200
FLASH_SECTOR_SIZE = 0x1000 # Flash sector size, minimum unit of erase.
ESP_ROM_BAUD = 115200
def __init__(
self,
uart,
gpio0_pin,
reset_pin,
*,
flashsize,
baudrate=ESP_ROM_BAUD,
):
gpio0_pin.direction = Direction.OUTPUT
reset_pin.direction = Direction.OUTPUT
self._gpio0pin = gpio0_pin
self._resetpin = reset_pin
self._uart = uart
self._uart.baudrate = baudrate
self._debug = False
self._efuses = [0] * 4
self._chipfamily = None
self._chipname = None
self._flashsize = flashsize
# self._debug_led = DigitalInOut(board.D13)
# self._debug_led.direction = Direction.OUTPUT
@property
def debug(self):
"""Print out all sent/received UART data plus some debugging output"""
return self._debug
@debug.setter
def debug(self, flag):
self._debug = flag
@property
def baudrate(self):
"""The baudrate of the UART connection. On ESP8266 we cannot change
this once we've started syncing. On ESP32 we must start at 115200 and
then manually change to higher speeds if desired"""
return self._uart.baudrate
@baudrate.setter
def baudrate(self, baud):
if self._chipfamily == ESP8266:
raise NotImplementedError("Baud rate can only change on ESP32")
buffer = struct.pack("<II", baud, 0)
self.check_command(ESP_CHANGE_BAUDRATE, buffer)
self._uart.baudrate = baud
time.sleep(0.05)
self._uart.reset_input_buffer()
self.check_command(ESP_CHANGE_BAUDRATE, buffer)
def md5(self, offset, size):
"""On ESP32 we can ask the ROM bootloader to calculate an MD5 on the
SPI flash memory, from a location over a size in bytes. Returns a
string with the MD5 in lowercase"""
if self._chipfamily == ESP8266:
raise NotImplementedError("MD5 only supported on ESP32")
self.check_command(ESP_SPI_ATTACH, bytes([0] * 8))
buffer = struct.pack("<IIII", offset, size, 0, 0)
md5 = self.check_command(ESP_SPI_FLASH_MD5, buffer, timeout=2)[1]
return "".join([chr(i) for i in md5])
@property
def mac_addr(self):
"""The MAC address burned into the OTP memory of the ESP chip"""
mac_addr = [0] * 6
mac0, mac1, mac2, mac3 = self._efuses
if self._chipfamily == ESP8266:
if mac3 != 0:
oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF)
elif ((mac1 >> 16) & 0xFF) == 0:
oui = (0x18, 0xFE, 0x34)
elif ((mac1 >> 16) & 0xFF) == 1:
oui = (0xAC, 0xD0, 0x74)
else:
raise RuntimeError("Couldnt determine OUI")
mac_addr[0] = oui[0]
mac_addr[1] = oui[1]
mac_addr[2] = oui[2]
mac_addr[3] = (mac1 >> 8) & 0xFF
mac_addr[4] = mac1 & 0xFF
mac_addr[5] = (mac0 >> 24) & 0xFF
if self._chipfamily == ESP32:
mac_addr[0] = mac2 >> 8 & 0xFF
mac_addr[1] = mac2 & 0xFF
mac_addr[2] = mac1 >> 24 & 0xFF
mac_addr[3] = mac1 >> 16 & 0xFF
mac_addr[4] = mac1 >> 8 & 0xFF
mac_addr[5] = mac1 & 0xFF
return mac_addr
@property
def chip_type(self):
"""ESP32 or ESP8266 based on which chip type we're talking to"""
if not self._chipfamily:
datareg = self.read_register(0x60000078)
if datareg == ESP32_DATAREGVALUE:
self._chipfamily = ESP32
elif datareg == ESP8266_DATAREGVALUE:
self._chipfamily = ESP8266
else:
raise RuntimeError("Unknown Chip")
return self._chipfamily
@property
def chip_name(self):
"""The specific name of the chip, e.g. ESP8266EX, to the best
of our ability to determine without a stub bootloader."""
self.chip_type
self._read_efuses()
if self.chip_type == ESP32:
return "ESP32"
if self.chip_type == ESP8266:
if self._efuses[0] & (1 << 4) or self._efuses[2] & (1 << 16):
return "ESP8285"
return "ESP8266EX"
return None
def _read_efuses(self):
"""Read the OTP data for this chip and store into _efuses array"""
if self._chipfamily == ESP8266:
base_addr = 0x3FF00050
elif self._chipfamily == ESP32:
base_addr = 0x6001A000
else:
raise RuntimeError("Don't know what chip this is")
for i in range(4):
self._efuses[i] = self.read_register(base_addr + 4 * i)
def get_erase_size(self, offset, size):
"""Calculate an erase size given a specific size in bytes.
Provides a workaround for the bootloader erase bug on ESP8266."""
sectors_per_block = 16
sector_size = self.FLASH_SECTOR_SIZE
num_sectors = (size + sector_size - 1) // sector_size
start_sector = offset // sector_size
head_sectors = sectors_per_block - (start_sector % sectors_per_block)
if num_sectors < head_sectors:
head_sectors = num_sectors
if num_sectors < 2 * head_sectors:
return (num_sectors + 1) // 2 * sector_size
return (num_sectors - head_sectors) * sector_size
def flash_begin(self, *, size=0, offset=0):
"""Prepare for flashing by attaching SPI chip and erasing the
number of blocks requred."""
if self._chipfamily == ESP32:
self.check_command(ESP_SPI_ATTACH, bytes([0] * 8))
# We are hardcoded for 4MB flash on ESP32
buffer = struct.pack("<IIIIII", 0, self._flashsize, 0x10000, 4096, 256, 0xFFFF)
self.check_command(ESP_SPI_SET_PARAMS, buffer)
num_blocks = (size + self.FLASH_WRITE_SIZE - 1) // self.FLASH_WRITE_SIZE
if self._chipfamily == ESP8266:
erase_size = self.get_erase_size(offset, size)
else:
erase_size = size
timeout = 13
stamp = time.monotonic()
buffer = struct.pack("<IIII", erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset)
print(
"Erase size %d, num_blocks %d, size %d, offset 0x%04x"
% (erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset)
)
self.check_command(ESP_FLASH_BEGIN, buffer, timeout=timeout)
if size != 0:
print("Took %.2fs to erase %d flash blocks" % (time.monotonic() - stamp, num_blocks))
return num_blocks
def check_command(self, opcode, buffer, checksum=0, timeout=0.1):
"""Send a command packet, check that the command succeeded and
return a tuple with the value and data.
See the ESP Serial Protocol for more details on what value/data are"""
self.send_command(opcode, buffer)
value, data = self.get_response(opcode, timeout)
if self._chipfamily == ESP8266:
status_len = 2
elif self._chipfamily == ESP32:
status_len = 4
elif len(data) in {2, 4}:
status_len = len(data)
if data is None or len(data) < status_len:
raise RuntimeError("Didn't get enough status bytes")
status = data[-status_len:]
data = data[:-status_len]
# print("status", status)
# print("value", value)
# print("data", data)
if status[0] != 0:
raise RuntimeError("Command failure error code 0x%02x" % status[1])
return (value, data)
def send_command(self, opcode, buffer):
"""Send a slip-encoded, checksummed command over the UART,
does not check response"""
self._uart.reset_input_buffer()
# self._debug_led.value = True
checksum = 0
if opcode == 0x03:
checksum = self.checksum(buffer[16:])
# self._debug_led.value = False
packet = [0xC0, 0x00] # direction
packet.append(opcode)
packet.extend(struct.pack("H", len(buffer)))
packet.extend(self.slip_encode(struct.pack("I", checksum)))
packet.extend(self.slip_encode(buffer))
packet += [0xC0]
if self._debug:
print([hex(x) for x in packet])
print("Writing:", bytearray(packet))
self._uart.write(bytearray(packet))
def get_response(self, opcode, timeout=0.1):
"""Read response data and decodes the slip packet, then parses
out the value/data and returns as a tuple of (value, data) where
each is a list of bytes"""
reply = []
stamp = time.monotonic()
packet_length = 0
escaped_byte = False
while (time.monotonic() - stamp) < timeout:
if self._uart.in_waiting > 0:
c = self._uart.read(1)
if c == b"\xdb":
escaped_byte = True
elif escaped_byte:
if c == b"\xdd":
reply += b"\xdb"
elif c == b"\xdc":
reply += b"\xc0"
else:
reply += [0xDB, c]
escaped_byte = False
else:
reply += c
if reply and reply[0] != 0xC0:
# packets must start with 0xC0
del reply[0]
if len(reply) > 1 and reply[1] != 0x01:
del reply[0]
if len(reply) > 2 and reply[2] != opcode:
del reply[0]
if len(reply) > 4:
# get the length
packet_length = reply[3] + (reply[4] << 8)
if len(reply) == packet_length + 10:
break
# Check to see if we have a complete packet. If not, we timed out.
if len(reply) != packet_length + 10:
if self._debug:
print(f"Timed out after {timeout} seconds")
return (None, None)
if self._debug:
print("Packet:", [hex(i) for i in reply])
print("Reading:", bytearray(reply))
value = reply[5:9]
data = reply[9:-1]
if self._debug:
print("value:", [hex(i) for i in value], "data:", [hex(i) for i in data])
return (value, data)
def read_register(self, reg):
"""Read a register within the ESP chip RAM, returns a 4-element list"""
if self._debug:
print("Reading register 0x%08x" % reg)
packet = struct.pack("I", reg)
register = self.check_command(ESP_READ_REG, packet)[0]
return struct.unpack("I", bytearray(register))[0]
def reset(self, program_mode=False):
"""Perform a hard-reset into ROM bootloader using gpio0 and reset"""
print("Resetting")
self._gpio0pin.value = not program_mode
self._resetpin.value = False
time.sleep(0.1)
self._resetpin.value = True
time.sleep(1.0)
def flash_block(self, data, seq, timeout=0.1):
"""Send one block of data to program into SPI Flash memory"""
self.check_command(
ESP_FLASH_DATA,
struct.pack("<IIII", len(data), seq, 0, 0) + data,
self.checksum(data),
timeout=timeout,
)
def flash_file(self, filename, offset=0, md5=None):
"""Program a full, uncompressed binary file into SPI Flash at
a given offset. If an ESP32 and md5 string is passed in, will also
verify memory. ESP8266 does not have checksum memory verification in
ROM"""
filesize = os.stat(filename)[6]
with open(filename, "rb") as file:
print("\nWriting", filename, "w/filesize:", filesize)
blocks = self.flash_begin(size=filesize, offset=offset)
seq = 0
written = 0
address = offset
stamp = time.monotonic()
while filesize - file.tell() > 0:
print(
"\rWriting at 0x%08x... (%d %%)"
% (
address + seq * self.FLASH_WRITE_SIZE,
100 * (seq + 1) // blocks,
),
end="",
)
block = file.read(self.FLASH_WRITE_SIZE)
# Pad the last block
block = block + b"\xff" * (self.FLASH_WRITE_SIZE - len(block))
# print(block)
self.flash_block(block, seq, timeout=2)
seq += 1
written += len(block)
print("Took %.2fs to write %d bytes" % (time.monotonic() - stamp, filesize))
if md5:
print("Verifying MD5sum ", md5)
calcd = self.md5(offset, filesize)
if md5 != calcd:
raise RuntimeError("MD5 mismatch, calculated:", calcd)
def _sync(self):
"""Perform a soft-sync using AT sync packets, does not perform
any hardware resetting"""
self.send_command(0x08, SYNC_PACKET)
for _ in range(8):
reply, data = self.get_response( # noqa: F841
0x08, 0.1
)
if not data:
continue
if len(data) > 1 and data[0] == 0 and data[1] == 0:
return True
return False
def sync(self):
"""Put into ROM bootload mode & attempt to synchronize with the
ESP ROM bootloader, we will retry a few times"""
self.reset(True)
for _ in range(5):
if self._sync():
time.sleep(0.1)
return True
time.sleep(0.1)
raise RuntimeError("Couldn't sync to ESP")
@staticmethod
def checksum(data, state=ESP_CHECKSUM_MAGIC):
"""Calculate checksum of a blob, as it is defined by the ROM"""
for b in data:
state ^= b
return state
@staticmethod
def slip_encode(buffer):
"""Take a bytearray buffer and return back a new bytearray where
0xdb is replaced with 0xdb 0xdd and 0xc0 is replaced with 0xdb 0xdc"""
encoded = []
for b in buffer:
if b == 0xDB:
encoded += [0xDB, 0xDD]
elif b == 0xC0:
encoded += [0xDB, 0xDC]
else:
encoded += [b]
return bytearray(encoded)