/ adafruit_miniesptool.py
adafruit_miniesptool.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2018 ladyada for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_miniesptool` 24 ==================================================== 25 26 ROM loader for ESP chips, works with ESP8266 or ESP32. 27 This is a 'no-stub' loader, so you can't read MD5 or firmware back on ESP8266. 28 29 See this document for protocol we're implementing: 30 https://github.com/espressif/esptool/wiki/Serial-Protocol 31 32 See this for the 'original' code we're miniaturizing: 33 https://github.com/espressif/esptool/blob/master/esptool.py 34 35 There's a very basic Arduino ROM loader here for ESP32: 36 https://github.com/arduino-libraries/WiFiNINA/tree/master/examples/Tools/FirmwareUpdater 37 38 * Author(s): ladyada 39 40 Implementation Notes 41 -------------------- 42 43 **Hardware:** 44 45 **Software and Dependencies:** 46 47 * Adafruit CircuitPython firmware for the supported boards: 48 https://github.com/adafruit/circuitpython/releases 49 50 51 """ 52 53 import os 54 import time 55 import struct 56 from digitalio import Direction 57 58 __version__ = "0.0.0-auto.0" 59 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_miniesptool.git" 60 61 SYNC_PACKET = b"\x07\x07\x12 UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU" 62 ESP32_DATAREGVALUE = 0x15122500 63 ESP8266_DATAREGVALUE = 0x00062000 64 65 # Commands supported by ESP8266 ROM bootloader 66 # pylint: disable=bad-whitespace 67 ESP_FLASH_BEGIN = 0x02 68 ESP_FLASH_DATA = 0x03 69 ESP_FLASH_END = 0x04 70 ESP_MEM_BEGIN = 0x05 71 ESP_MEM_END = 0x06 72 ESP_MEM_DATA = 0x07 73 ESP_SYNC = 0x08 74 ESP_WRITE_REG = 0x09 75 ESP_READ_REG = 0x0A 76 ESP_SPI_SET_PARAMS = 0x0B 77 ESP_SPI_ATTACH = 0x0D 78 ESP_CHANGE_BAUDRATE = 0x0F 79 ESP_SPI_FLASH_MD5 = 0x13 80 ESP_CHECKSUM_MAGIC = 0xEF 81 82 ESP8266 = 0x8266 83 ESP32 = 0x32 84 # pylint: enable=bad-whitespace 85 86 FLASH_SIZES = { 87 "512KB": 0x00, 88 "256KB": 0x10, 89 "1MB": 0x20, 90 "2MB": 0x30, 91 "4MB": 0x40, 92 "2MB-c1": 0x50, 93 "4MB-c1": 0x60, 94 "8MB": 0x80, 95 "16MB": 0x90, 96 } 97 98 99 class miniesptool: # pylint: disable=invalid-name 100 """A miniature version of esptool, a programming command line tool for 101 ESP8266 and ESP32 chips. This version is minimized to work on CircuitPython 102 boards, so you can burn ESP firmware direct from the CPy disk drive. Handy 103 when you have an ESP module wired to a board and need to upload new AT 104 firmware. Its slow! Expect a few minutes when programming 1 MB flash.""" 105 106 FLASH_WRITE_SIZE = 0x200 107 FLASH_SECTOR_SIZE = 0x1000 # Flash sector size, minimum unit of erase. 108 ESP_ROM_BAUD = 115200 109 110 def __init__( 111 self, 112 uart, 113 gpio0_pin, # pylint: disable=too-many-arguments 114 reset_pin, 115 *, 116 flashsize, 117 baudrate=ESP_ROM_BAUD 118 ): 119 gpio0_pin.direction = Direction.OUTPUT 120 reset_pin.direction = Direction.OUTPUT 121 self._gpio0pin = gpio0_pin 122 self._resetpin = reset_pin 123 self._uart = uart 124 self._uart.baudrate = baudrate 125 self._debug = False 126 self._efuses = [0] * 4 127 self._chipfamily = None 128 self._chipname = None 129 self._flashsize = flashsize 130 # self._debug_led = DigitalInOut(board.D13) 131 # self._debug_led.direction = Direction.OUTPUT 132 133 @property 134 def debug(self): 135 """Print out all sent/received UART data plus some debugging output""" 136 return self._debug 137 138 @debug.setter 139 def debug(self, flag): 140 self._debug = flag 141 142 @property 143 def baudrate(self): 144 """The baudrate of the UART connection. On ESP8266 we cannot change 145 this once we've started syncing. On ESP32 we must start at 115200 and 146 then manually change to higher speeds if desired""" 147 return self._uart.baudrate 148 149 @baudrate.setter 150 def baudrate(self, baud): 151 if self._chipfamily == ESP8266: 152 raise NotImplementedError("Baud rate can only change on ESP32") 153 buffer = struct.pack("<II", baud, 0) 154 self.check_command(ESP_CHANGE_BAUDRATE, buffer) 155 self._uart.baudrate = baud 156 time.sleep(0.05) 157 self._uart.reset_input_buffer() 158 self.check_command(ESP_CHANGE_BAUDRATE, buffer) 159 160 def md5(self, offset, size): 161 """On ESP32 we can ask the ROM bootloader to calculate an MD5 on the 162 SPI flash memory, from a location over a size in bytes. Returns a 163 string with the MD5 in lowercase""" 164 if self._chipfamily == ESP8266: 165 raise NotImplementedError("MD5 only supported on ESP32") 166 self.check_command(ESP_SPI_ATTACH, bytes([0] * 8)) 167 buffer = struct.pack("<IIII", offset, size, 0, 0) 168 md5 = self.check_command(ESP_SPI_FLASH_MD5, buffer, timeout=2)[1] 169 return "".join([chr(i) for i in md5]) 170 171 @property 172 def mac_addr(self): 173 """The MAC address burned into the OTP memory of the ESP chip""" 174 mac_addr = [0] * 6 175 mac0, mac1, mac2, mac3 = self._efuses 176 if self._chipfamily == ESP8266: 177 if mac3 != 0: 178 oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF) 179 elif ((mac1 >> 16) & 0xFF) == 0: 180 oui = (0x18, 0xFE, 0x34) 181 elif ((mac1 >> 16) & 0xFF) == 1: 182 oui = (0xAC, 0xD0, 0x74) 183 else: 184 raise RuntimeError("Couldnt determine OUI") 185 186 mac_addr[0] = oui[0] 187 mac_addr[1] = oui[1] 188 mac_addr[2] = oui[2] 189 mac_addr[3] = (mac1 >> 8) & 0xFF 190 mac_addr[4] = mac1 & 0xFF 191 mac_addr[5] = (mac0 >> 24) & 0xFF 192 if self._chipfamily == ESP32: 193 mac_addr[0] = mac2 >> 8 & 0xFF 194 mac_addr[1] = mac2 & 0xFF 195 mac_addr[2] = mac1 >> 24 & 0xFF 196 mac_addr[3] = mac1 >> 16 & 0xFF 197 mac_addr[4] = mac1 >> 8 & 0xFF 198 mac_addr[5] = mac1 & 0xFF 199 return mac_addr 200 201 @property 202 def chip_type(self): 203 """ESP32 or ESP8266 based on which chip type we're talking to""" 204 if not self._chipfamily: 205 datareg = self.read_register(0x60000078) 206 if datareg == ESP32_DATAREGVALUE: 207 self._chipfamily = ESP32 208 elif datareg == ESP8266_DATAREGVALUE: 209 self._chipfamily = ESP8266 210 else: 211 raise RuntimeError("Unknown Chip") 212 return self._chipfamily 213 214 @property 215 def chip_name(self): 216 """The specific name of the chip, e.g. ESP8266EX, to the best 217 of our ability to determine without a stub bootloader.""" 218 self.chip_type # pylint: disable=pointless-statement 219 self._read_efuses() 220 221 if self.chip_type == ESP32: 222 return "ESP32" 223 if self.chip_type == ESP8266: 224 if self._efuses[0] & (1 << 4) or self._efuses[2] & (1 << 16): 225 return "ESP8285" 226 return "ESP8266EX" 227 return None 228 229 def _read_efuses(self): 230 """Read the OTP data for this chip and store into _efuses array""" 231 if self._chipfamily == ESP8266: 232 base_addr = 0x3FF00050 233 elif self._chipfamily == ESP32: 234 base_addr = 0x6001A000 235 else: 236 raise RuntimeError("Don't know what chip this is") 237 for i in range(4): 238 self._efuses[i] = self.read_register(base_addr + 4 * i) 239 240 def get_erase_size(self, offset, size): 241 """Calculate an erase size given a specific size in bytes. 242 Provides a workaround for the bootloader erase bug on ESP8266.""" 243 244 sectors_per_block = 16 245 sector_size = self.FLASH_SECTOR_SIZE 246 num_sectors = (size + sector_size - 1) // sector_size 247 start_sector = offset // sector_size 248 249 head_sectors = sectors_per_block - (start_sector % sectors_per_block) 250 if num_sectors < head_sectors: 251 head_sectors = num_sectors 252 253 if num_sectors < 2 * head_sectors: 254 return (num_sectors + 1) // 2 * sector_size 255 return (num_sectors - head_sectors) * sector_size 256 257 def flash_begin(self, *, size=0, offset=0): 258 """Prepare for flashing by attaching SPI chip and erasing the 259 number of blocks requred.""" 260 if self._chipfamily == ESP32: 261 self.check_command(ESP_SPI_ATTACH, bytes([0] * 8)) 262 # We are hardcoded for 4MB flash on ESP32 263 buffer = struct.pack( 264 "<IIIIII", 0, self._flashsize, 0x10000, 4096, 256, 0xFFFF 265 ) 266 self.check_command(ESP_SPI_SET_PARAMS, buffer) 267 268 num_blocks = (size + self.FLASH_WRITE_SIZE - 1) // self.FLASH_WRITE_SIZE 269 if self._chipfamily == ESP8266: 270 erase_size = self.get_erase_size(offset, size) 271 else: 272 erase_size = size 273 timeout = 13 274 stamp = time.monotonic() 275 buffer = struct.pack( 276 "<IIII", erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset 277 ) 278 print( 279 "Erase size %d, num_blocks %d, size %d, offset 0x%04x" 280 % (erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset) 281 ) 282 283 self.check_command(ESP_FLASH_BEGIN, buffer, timeout=timeout) 284 if size != 0: 285 print( 286 "Took %.2fs to erase %d flash blocks" 287 % (time.monotonic() - stamp, num_blocks) 288 ) 289 return num_blocks 290 291 def check_command( 292 self, opcode, buffer, checksum=0, timeout=0.1 293 ): # pylint: disable=unused-argument 294 """Send a command packet, check that the command succeeded and 295 return a tuple with the value and data. 296 See the ESP Serial Protocol for more details on what value/data are""" 297 self.send_command(opcode, buffer) 298 value, data = self.get_response(opcode, timeout) 299 if self._chipfamily == ESP8266: 300 status_len = 2 301 elif self._chipfamily == ESP32: 302 status_len = 4 303 else: 304 if len(data) in (2, 4): 305 status_len = len(data) 306 if data is None or len(data) < status_len: 307 raise RuntimeError("Didn't get enough status bytes") 308 status = data[-status_len:] 309 data = data[:-status_len] 310 # print("status", status) 311 # print("value", value) 312 # print("data", data) 313 if status[0] != 0: 314 raise RuntimeError("Command failure error code 0x%02x" % status[1]) 315 return (value, data) 316 317 def send_command(self, opcode, buffer): 318 """Send a slip-encoded, checksummed command over the UART, 319 does not check response""" 320 self._uart.reset_input_buffer() 321 322 # self._debug_led.value = True 323 checksum = 0 324 if opcode == 0x03: 325 checksum = self.checksum(buffer[16:]) 326 # self._debug_led.value = False 327 328 packet = [0xC0, 0x00] # direction 329 packet.append(opcode) 330 packet.extend(struct.pack("H", len(buffer))) 331 packet.extend(self.slip_encode(struct.pack("I", checksum))) 332 packet.extend(self.slip_encode(buffer)) 333 packet += [0xC0] 334 if self._debug: 335 print([hex(x) for x in packet]) 336 print("Writing:", bytearray(packet)) 337 self._uart.write(bytearray(packet)) 338 339 def get_response(self, opcode, timeout=0.1): # pylint: disable=too-many-branches 340 """Read response data and decodes the slip packet, then parses 341 out the value/data and returns as a tuple of (value, data) where 342 each is a list of bytes""" 343 reply = [] 344 345 stamp = time.monotonic() 346 packet_length = 0 347 escaped_byte = False 348 while (time.monotonic() - stamp) < timeout: 349 if self._uart.in_waiting > 0: 350 c = self._uart.read(1) # pylint: disable=invalid-name 351 if c == b"\xDB": 352 escaped_byte = True 353 elif escaped_byte: 354 if c == b"\xDD": 355 reply += b"\xDC" 356 elif c == b"\xDC": 357 reply += b"\xC0" 358 else: 359 reply += [0xDB, c] 360 escaped_byte = False 361 else: 362 reply += c 363 if reply and reply[0] != 0xC0: 364 # packets must start with 0xC0 365 del reply[0] 366 if len(reply) > 1 and reply[1] != 0x01: 367 del reply[0] 368 if len(reply) > 2 and reply[2] != opcode: 369 del reply[0] 370 if len(reply) > 4: 371 # get the length 372 packet_length = reply[3] + (reply[4] << 8) 373 if len(reply) == packet_length + 10: 374 break 375 # Check to see if we have a complete packet. If not, we timed out. 376 if len(reply) != packet_length + 10: 377 if self._debug: 378 print("Timed out after {} seconds".format(timeout)) 379 return (None, None) 380 if self._debug: 381 print("Packet:", [hex(i) for i in reply]) 382 print("Reading:", bytearray(reply)) 383 value = reply[5:9] 384 data = reply[9:-1] 385 if self._debug: 386 print("value:", [hex(i) for i in value], "data:", [hex(i) for i in data]) 387 return (value, data) 388 389 def read_register(self, reg): 390 """Read a register within the ESP chip RAM, returns a 4-element list""" 391 if self._debug: 392 print("Reading register 0x%08x" % reg) 393 packet = struct.pack("I", reg) 394 register = self.check_command(ESP_READ_REG, packet)[0] 395 return struct.unpack("I", bytearray(register))[0] 396 397 def reset(self, program_mode=False): 398 """Perform a hard-reset into ROM bootloader using gpio0 and reset""" 399 print("Resetting") 400 self._gpio0pin.value = not program_mode 401 self._resetpin.value = False 402 time.sleep(0.1) 403 self._resetpin.value = True 404 time.sleep(1.0) 405 406 def flash_block(self, data, seq, timeout=0.1): 407 """Send one block of data to program into SPI Flash memory""" 408 self.check_command( 409 ESP_FLASH_DATA, 410 struct.pack("<IIII", len(data), seq, 0, 0) + data, 411 self.checksum(data), 412 timeout=timeout, 413 ) 414 415 def flash_file(self, filename, offset=0, md5=None): 416 """Program a full, uncompressed binary file into SPI Flash at 417 a given offset. If an ESP32 and md5 string is passed in, will also 418 verify memory. ESP8266 does not have checksum memory verification in 419 ROM""" 420 filesize = os.stat(filename)[6] 421 with open(filename, "rb") as file: 422 print("\nWriting", filename, "w/filesize:", filesize) 423 blocks = self.flash_begin(size=filesize, offset=offset) 424 seq = 0 425 written = 0 426 address = offset 427 stamp = time.monotonic() 428 while filesize - file.tell() > 0: 429 print( 430 "\rWriting at 0x%08x... (%d %%)" 431 % ( 432 address + seq * self.FLASH_WRITE_SIZE, 433 100 * (seq + 1) // blocks, 434 ), 435 end="", 436 ) 437 block = file.read(self.FLASH_WRITE_SIZE) 438 # Pad the last block 439 block = block + b"\xff" * (self.FLASH_WRITE_SIZE - len(block)) 440 # print(block) 441 self.flash_block(block, seq, timeout=2) 442 seq += 1 443 written += len(block) 444 print("Took %.2fs to write %d bytes" % (time.monotonic() - stamp, filesize)) 445 if md5: 446 print("Verifying MD5sum ", md5) 447 calcd = self.md5(offset, filesize) 448 if md5 != calcd: 449 raise RuntimeError("MD5 mismatch, calculated:", calcd) 450 451 def _sync(self): 452 """Perform a soft-sync using AT sync packets, does not perform 453 any hardware resetting""" 454 self.send_command(0x08, SYNC_PACKET) 455 for _ in range(8): 456 reply, data = self.get_response( # pylint: disable=unused-variable 457 0x08, 0.1 458 ) 459 if not data: 460 continue 461 if len(data) > 1 and data[0] == 0 and data[1] == 0: 462 return True 463 return False 464 465 def sync(self): 466 """Put into ROM bootload mode & attempt to synchronize with the 467 ESP ROM bootloader, we will retry a few times""" 468 self.reset(True) 469 470 for _ in range(5): 471 if self._sync(): 472 time.sleep(0.1) 473 return True 474 time.sleep(0.1) 475 476 raise RuntimeError("Couldn't sync to ESP") 477 478 @staticmethod 479 def checksum(data, state=ESP_CHECKSUM_MAGIC): 480 """ Calculate checksum of a blob, as it is defined by the ROM """ 481 for b in data: 482 state ^= b 483 return state 484 485 @staticmethod 486 def slip_encode(buffer): 487 """Take a bytearray buffer and return back a new bytearray where 488 0xdb is replaced with 0xdb 0xdd and 0xc0 is replaced with 0xdb 0xdc""" 489 encoded = [] 490 for b in buffer: 491 if b == 0xDB: 492 encoded += [0xDB, 0xDD] 493 elif b == 0xC0: 494 encoded += [0xDB, 0xDC] 495 else: 496 encoded += [b] 497 return bytearray(encoded)