import { CHIP_FAMILY_ESP32, CHIP_FAMILY_ESP32S2, CHIP_FAMILY_ESP32S3, CHIP_FAMILY_ESP32C2, CHIP_FAMILY_ESP32C3, CHIP_FAMILY_ESP32C6, CHIP_FAMILY_ESP32H2, CHIP_FAMILY_ESP8266, MAX_TIMEOUT, DEFAULT_TIMEOUT, ERASE_REGION_TIMEOUT_PER_MB, ESP_CHANGE_BAUDRATE, ESP_CHECKSUM_MAGIC, ESP_FLASH_BEGIN, ESP_FLASH_DATA, ESP_FLASH_END, ESP_MEM_BEGIN, ESP_MEM_DATA, ESP_MEM_END, ESP_READ_REG, ESP_WRITE_REG, ESP_SPI_ATTACH, ESP_SYNC, FLASH_SECTOR_SIZE, FLASH_WRITE_SIZE, STUB_FLASH_WRITE_SIZE, MEM_END_ROM_TIMEOUT, ROM_INVALID_RECV_MSG, SYNC_PACKET, SYNC_TIMEOUT, USB_RAM_BLOCK, ESP_ERASE_FLASH, CHIP_ERASE_TIMEOUT, timeoutPerMb, ESP_ROM_BAUD, USB_JTAG_SERIAL_PID, ESP_FLASH_DEFL_BEGIN, ESP_FLASH_DEFL_DATA, ESP_FLASH_DEFL_END, getSpiFlashAddresses, DETECTED_FLASH_SIZES, CHIP_DETECT_MAGIC_REG_ADDR, CHIP_DETECT_MAGIC_VALUES, SlipReadError, } from "./const"; import { getStubCode } from "./stubs"; import { hexFormatter, sleep, slipEncode, toHex } from "./util"; // @ts-ignore import { deflate } from "pako/dist/pako.esm.mjs"; import { pack, unpack } from "./struct"; export class ESPLoader extends EventTarget { constructor(port, logger, _parent) { super(); this.port = port; this.logger = logger; this._parent = _parent; this.chipName = null; this._efuses = new Array(4).fill(0); this._flashsize = 4 * 1024 * 1024; this.debug = false; this.IS_STUB = false; this.connected = true; this.flashSize = null; this.state_DTR = false; } get _inputBuffer() { return this._parent ? this._parent._inputBuffer : this.__inputBuffer; } async initialize() { await this.hardReset(true); if (!this._parent) { this.__inputBuffer = []; // Don't await this promise so it doesn't block rest of method. this.readLoop(); } await this.sync(); // Determine chip family and name let chipMagicValue = await this.readRegister(CHIP_DETECT_MAGIC_REG_ADDR); let chip = CHIP_DETECT_MAGIC_VALUES[chipMagicValue >>> 0]; if (chip === undefined) { throw new Error(`Unknown Chip: Hex: ${toHex(chipMagicValue >>> 0, 8).toLowerCase()} Number: ${chipMagicValue}`); } this.chipName = chip.name; this.chipFamily = chip.family; // Read the OTP data for this chip and store into this.efuses array let FlAddr = getSpiFlashAddresses(this.getChipFamily()); let AddrMAC = FlAddr.macFuse; for (let i = 0; i < 4; i++) { this._efuses[i] = await this.readRegister(AddrMAC + 4 * i); } this.logger.log(`Chip type ${this.chipName}`); //this.logger.log("FLASHID"); } /** * @name readLoop * Reads data from the input stream and places it in the inputBuffer */ async readLoop() { if (this.debug) { this.logger.debug("Starting read loop"); } this._reader = this.port.readable.getReader(); try { while (true) { const { value, done } = await this._reader.read(); if (done) { this._reader.releaseLock(); break; } if (!value || value.length === 0) { continue; } this._inputBuffer.push(...Array.from(value)); } } catch (err) { console.error("Read loop got disconnected"); } // Disconnected! this.connected = false; this.dispatchEvent(new Event("disconnect")); this.logger.debug("Finished read loop"); } sleep(ms = 100) { return new Promise((resolve) => setTimeout(resolve, ms)); } async setRTS(state) { await this.port.setSignals({ requestToSend: state }); // # Work-around for adapters on Windows using the usbser.sys driver: // # generate a dummy change to DTR so that the set-control-line-state // # request is sent with the updated RTS state and the same DTR state // Referenced to esptool.py await this.setDTR(this.state_DTR); } async setDTR(state) { this.state_DTR = state; await this.port.setSignals({ dataTerminalReady: state }); } async hardReset(bootloader = false) { this.logger.log("Try hard reset."); if (bootloader) { // enter flash mode if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) { // esp32c3 esp32s3 etc. build-in USB serial. // when connect to computer direct via usb, using following signals // to enter flash mode automatically. await this.setDTR(false); await this.setRTS(false); await this.sleep(100); await this.setDTR(true); await this.setRTS(false); await this.sleep(100); await this.setRTS(true); await this.setDTR(false); await this.setRTS(true); await this.sleep(100); await this.setDTR(false); await this.setRTS(false); } else { // otherwise, esp chip should be connected to computer via usb-serial // bridge chip like ch340,CP2102 etc. // use normal way to enter flash mode. await this.setDTR(false); await this.setRTS(true); await this.sleep(100); await this.setDTR(true); await this.setRTS(false); await this.sleep(50); await this.setDTR(false); } } else { // just reset await this.setRTS(true); // EN->LOW await this.sleep(100); await this.setRTS(false); } await new Promise((resolve) => setTimeout(resolve, 1000)); } /** * @name macAddr * The MAC address burned into the OTP memory of the ESP chip */ macAddr() { let macAddr = new Array(6).fill(0); let mac0 = this._efuses[0]; let mac1 = this._efuses[1]; let mac2 = this._efuses[2]; let mac3 = this._efuses[3]; let oui; if (this.chipFamily == CHIP_FAMILY_ESP8266) { if (mac3 != 0) { oui = [(mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff]; } else if (((mac1 >> 16) & 0xff) == 0) { oui = [0x18, 0xfe, 0x34]; } else if (((mac1 >> 16) & 0xff) == 1) { oui = [0xac, 0xd0, 0x74]; } else { throw new Error("Couldnt determine OUI"); } macAddr[0] = oui[0]; macAddr[1] = oui[1]; macAddr[2] = oui[2]; macAddr[3] = (mac1 >> 8) & 0xff; macAddr[4] = mac1 & 0xff; macAddr[5] = (mac0 >> 24) & 0xff; } else if (this.chipFamily == CHIP_FAMILY_ESP32) { macAddr[0] = (mac2 >> 8) & 0xff; macAddr[1] = mac2 & 0xff; macAddr[2] = (mac1 >> 24) & 0xff; macAddr[3] = (mac1 >> 16) & 0xff; macAddr[4] = (mac1 >> 8) & 0xff; macAddr[5] = mac1 & 0xff; } else if (this.chipFamily == CHIP_FAMILY_ESP32S2 || this.chipFamily == CHIP_FAMILY_ESP32S3 || this.chipFamily == CHIP_FAMILY_ESP32C2 || this.chipFamily == CHIP_FAMILY_ESP32C3 || this.chipFamily == CHIP_FAMILY_ESP32C6 || this.chipFamily == CHIP_FAMILY_ESP32H2) { macAddr[0] = (mac1 >> 8) & 0xff; macAddr[1] = mac1 & 0xff; macAddr[2] = (mac0 >> 24) & 0xff; macAddr[3] = (mac0 >> 16) & 0xff; macAddr[4] = (mac0 >> 8) & 0xff; macAddr[5] = mac0 & 0xff; } else { throw new Error("Unknown chip family"); } return macAddr; } async readRegister(reg) { if (this.debug) { this.logger.debug("Reading from Register " + toHex(reg, 8)); } let packet = pack(" 0) { readBytes.push(this._inputBuffer.shift()); break; } else { await sleep(10); } } if (readBytes.length == 0) { let waitingFor = partialPacket === null ? "header" : "content"; throw new SlipReadError("Timed out waiting for packet " + waitingFor); } if (this.debug) this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes)); for (let b of readBytes) { if (partialPacket === null) { // waiting for packet header if (b == 0xc0) { partialPacket = []; } else { if (this.debug) { this.logger.debug("Read invalid data: " + hexFormatter(readBytes)); this.logger.debug("Remaining data in serial buffer: " + hexFormatter(this._inputBuffer)); } throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")"); } } else if (inEscape) { // part-way through escape sequence inEscape = false; if (b == 0xdc) { partialPacket.push(0xc0); } else if (b == 0xdd) { partialPacket.push(0xdb); } else { if (this.debug) { this.logger.debug("Read invalid data: " + hexFormatter(readBytes)); this.logger.debug("Remaining data in serial buffer: " + hexFormatter(this._inputBuffer)); } throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")"); } } else if (b == 0xdb) { // start of escape sequence inEscape = true; } else if (b == 0xc0) { // end of packet if (this.debug) this.logger.debug("Received full packet: " + hexFormatter(partialPacket)); return partialPacket; } else { // normal byte in packet partialPacket.push(b); } } } throw new SlipReadError("Invalid state"); } /** * @name getResponse * 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 */ async getResponse(opcode, timeout = DEFAULT_TIMEOUT) { for (let i = 0; i < 100; i++) { const packet = await this.readPacket(timeout); if (packet.length < 8) { continue; } const [resp, opRet, _lenRet, val] = unpack(" 1 && data[0] == 0 && data[1] == 0) { return true; } } catch (err) { // If read packet fails. } } return false; } /** * @name getFlashWriteSize * Get the Flash write size based on the chip */ getFlashWriteSize() { if (this.IS_STUB) { return STUB_FLASH_WRITE_SIZE; } return FLASH_WRITE_SIZE; } /** * @name flashData * 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 */ async flashData(binaryData, updateProgress, offset = 0, compress = false) { if (binaryData.byteLength >= 8) { // unpack the (potential) image header var header = Array.from(new Uint8Array(binaryData, 0, 4)); let headerMagic = header[0]; let headerFlashMode = header[2]; let headerFlashSizeFreq = header[3]; this.logger.log(`Image header, Magic=${toHex(headerMagic)}, FlashMode=${toHex(headerFlashMode)}, FlashSizeFreq=${toHex(headerFlashSizeFreq)}`); } let uncompressedFilesize = binaryData.byteLength; let compressedFilesize = 0; let dataToFlash; let timeout = DEFAULT_TIMEOUT; if (compress) { dataToFlash = deflate(new Uint8Array(binaryData), { level: 9, }).buffer; compressedFilesize = dataToFlash.byteLength; this.logger.log(`Writing data with filesize: ${uncompressedFilesize}. Compressed Size: ${compressedFilesize}`); timeout = await this.flashDeflBegin(uncompressedFilesize, compressedFilesize, offset); } else { this.logger.log(`Writing data with filesize: ${uncompressedFilesize}`); dataToFlash = binaryData; await this.flashBegin(uncompressedFilesize, offset); } let block = []; let seq = 0; let written = 0; let position = 0; let stamp = Date.now(); let flashWriteSize = this.getFlashWriteSize(); let filesize = compress ? compressedFilesize : uncompressedFilesize; while (filesize - position > 0) { if (this.debug) { this.logger.log(`Writing at ${toHex(offset + seq * flashWriteSize, 8)} `); } if (filesize - position >= flashWriteSize) { block = Array.from(new Uint8Array(dataToFlash, position, flashWriteSize)); } else { // Pad the last block only if we are sending uncompressed data. block = Array.from(new Uint8Array(dataToFlash, position, filesize - position)); if (!compress) { block = block.concat(new Array(flashWriteSize - block.length).fill(0xff)); } } if (compress) { await this.flashDeflBlock(block, seq, timeout); } else { await this.flashBlock(block, seq); } seq += 1; // If using compression we update the progress with the proportional size of the block taking into account the compression ratio. // This way we report progress on the uncompressed size written += compress ? Math.round((block.length * uncompressedFilesize) / compressedFilesize) : block.length; position += flashWriteSize; updateProgress(Math.min(written, uncompressedFilesize), uncompressedFilesize); } this.logger.log("Took " + (Date.now() - stamp) + "ms to write " + filesize + " bytes"); // Only send flashF finish if running the stub because ir causes the ROM to exit and run user code if (this.IS_STUB) { await this.flashBegin(0, 0); if (compress) { await this.flashDeflFinish(); } else { await this.flashFinish(); } } } /** * @name flashBlock * Send one block of data to program into SPI Flash memory */ async flashBlock(data, seq, timeout = DEFAULT_TIMEOUT) { await this.checkCommand(ESP_FLASH_DATA, pack(" 0) { // add a dummy write to a date register as an excuse to have a delay buffer.concat(pack(" 0) { await this.writeRegister(SPI_MOSI_DLEN_REG, mosiBits - 1); } if (misoBits > 0) { await this.writeRegister(SPI_MISO_DLEN_REG, misoBits - 1); } } else { let SPI_DATA_LEN_REG = spiAddresses.regBase + spiAddresses.usr1Offs; let SPI_MOSI_BITLEN_S = 17; let SPI_MISO_BITLEN_S = 8; let mosiMask = mosiBits == 0 ? 0 : mosiBits - 1; let misoMask = misoBits == 0 ? 0 : misoBits - 1; let value = (misoMask << SPI_MISO_BITLEN_S) | (mosiMask << SPI_MOSI_BITLEN_S); await this.writeRegister(SPI_DATA_LEN_REG, value); } } async waitDone(spiCmdReg, spiCmdUsr) { for (let i = 0; i < 10; i++) { let cmdValue = await this.readRegister(spiCmdReg); if ((cmdValue & spiCmdUsr) == 0) { return; } } throw Error("SPI command did not complete in time"); } async runSpiFlashCommand(spiflashCommand, data, readBits = 0) { // Run an arbitrary SPI flash command. // This function uses the "USR_COMMAND" functionality in the ESP // SPI hardware, rather than the precanned commands supported by // hardware. So the value of spiflash_command is an actual command // byte, sent over the wire. // After writing command byte, writes 'data' to MOSI and then // reads back 'read_bits' of reply on MISO. Result is a number. // SPI_USR register flags let SPI_USR_COMMAND = 1 << 31; let SPI_USR_MISO = 1 << 28; let SPI_USR_MOSI = 1 << 27; // SPI registers, base address differs ESP32* vs 8266 let spiAddresses = getSpiFlashAddresses(this.getChipFamily()); let base = spiAddresses.regBase; let SPI_CMD_REG = base; let SPI_USR_REG = base + spiAddresses.usrOffs; let SPI_USR2_REG = base + spiAddresses.usr2Offs; let SPI_W0_REG = base + spiAddresses.w0Offs; // SPI peripheral "command" bitmasks for SPI_CMD_REG let SPI_CMD_USR = 1 << 18; // shift values let SPI_USR2_COMMAND_LEN_SHIFT = 28; if (readBits > 32) { throw new Error("Reading more than 32 bits back from a SPI flash operation is unsupported"); } if (data.length > 64) { throw new Error("Writing more than 64 bytes of data with one SPI command is unsupported"); } let dataBits = data.length * 8; let oldSpiUsr = await this.readRegister(SPI_USR_REG); let oldSpiUsr2 = await this.readRegister(SPI_USR2_REG); let flags = SPI_USR_COMMAND; if (readBits > 0) { flags |= SPI_USR_MISO; } if (dataBits > 0) { flags |= SPI_USR_MOSI; } await this.setDataLengths(spiAddresses, dataBits, readBits); await this.writeRegister(SPI_USR_REG, flags); await this.writeRegister(SPI_USR2_REG, (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflashCommand); if (dataBits == 0) { await this.writeRegister(SPI_W0_REG, 0); // clear data register before we read it } else { data.concat(new Array(data.length % 4).fill(0x00)); // pad to 32-bit multiple let words = unpack("I".repeat(Math.floor(data.length / 4)), data); let nextReg = SPI_W0_REG; this.logger.debug(`Words Length: ${words.length}`); for (const word of words) { this.logger.debug(`Writing word ${toHex(word)} to register offset ${toHex(nextReg)}`); await this.writeRegister(nextReg, word); nextReg += 4; } } await this.writeRegister(SPI_CMD_REG, SPI_CMD_USR); await this.waitDone(SPI_CMD_REG, SPI_CMD_USR); let status = await this.readRegister(SPI_W0_REG); // restore some SPI controller registers await this.writeRegister(SPI_USR_REG, oldSpiUsr); await this.writeRegister(SPI_USR2_REG, oldSpiUsr2); return status; } async detectFlashSize() { this.logger.log("Detecting Flash Size"); let flashId = await this.flashId(); let manufacturer = flashId & 0xff; let flashIdLowbyte = (flashId >> 16) & 0xff; this.logger.log(`FlashId: ${toHex(flashId)}`); this.logger.log(`Flash Manufacturer: ${manufacturer.toString(16)}`); this.logger.log(`Flash Device: ${((flashId >> 8) & 0xff).toString(16)}${flashIdLowbyte.toString(16)}`); this.flashSize = DETECTED_FLASH_SIZES[flashIdLowbyte]; this.logger.log(`Auto-detected Flash size: ${this.flashSize}`); } /** * @name getEraseSize * Calculate an erase size given a specific size in bytes. * Provides a workaround for the bootloader erase bug on ESP8266. */ getEraseSize(offset, size) { let sectorsPerBlock = 16; let sectorSize = FLASH_SECTOR_SIZE; let numSectors = Math.floor((size + sectorSize - 1) / sectorSize); let startSector = Math.floor(offset / sectorSize); let headSectors = sectorsPerBlock - (startSector % sectorsPerBlock); if (numSectors < headSectors) { headSectors = numSectors; } if (numSectors < 2 * headSectors) { return Math.floor(((numSectors + 1) / 2) * sectorSize); } return (numSectors - headSectors) * sectorSize; } /** * @name memBegin (592) * Start downloading an application image to RAM */ async memBegin(size, blocks, blocksize, offset) { return await this.checkCommand(ESP_MEM_BEGIN, pack(" length) { toOffs = length; } await this.memBlock(stub[field].slice(fromOffs, toOffs), seq); } } } this.logger.log("Running stub..."); await this.memFinish(stub["entry"]); let pChar; const p = await this.readPacket(500); pChar = String.fromCharCode(...p); if (pChar != "OHAI") { throw new Error("Failed to start stub. Unexpected response: " + pChar); } this.logger.log("Stub is now running..."); const espStubLoader = new EspStubLoader(this.port, this.logger, this); // Try to autodetect the flash size as soon as the stub is running. await espStubLoader.detectFlashSize(); return espStubLoader; } async writeToStream(data) { const writer = this.port.writable.getWriter(); await writer.write(new Uint8Array(data)); try { writer.releaseLock(); } catch (err) { console.error("Ignoring release lock error", err); } } async disconnect() { if (this._parent) { await this._parent.disconnect(); return; } await this.port.writable.getWriter().close(); await new Promise((resolve) => { if (!this._reader) { resolve(undefined); } this.addEventListener("disconnect", resolve, { once: true }); this._reader.cancel(); }); this.connected = false; } } class EspStubLoader extends ESPLoader { constructor() { super(...arguments); /* The Stubloader has commands that run on the uploaded Stub Code in RAM rather than built in commands. */ this.IS_STUB = true; } /** * @name memBegin (592) * Start downloading an application image to RAM */ async memBegin(size, blocks, blocksize, offset) { let stub = await getStubCode(this.chipFamily); let load_start = offset; let load_end = offset + size; console.log(load_start, load_end); console.log(stub.data_start, stub.data.length, stub.text_start, stub.text.length); for (let [start, end] of [ [stub.data_start, stub.data_start + stub.data.length], [stub.text_start, stub.text_start + stub.text.length], ]) { if (load_start < end && load_end > start) { throw new Error("Software loader is resident at " + toHex(start, 8) + "-" + toHex(end, 8) + ". " + "Can't load binary at overlapping address range " + toHex(load_start, 8) + "-" + toHex(load_end, 8) + ". " + "Try changing the binary loading address."); } } } /** * @name getEraseSize * depending on flash chip model the erase may take this long (maybe longer!) */ async eraseFlash() { await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT); } }