@toit/esptool.js
Version:
TypeScript port of the esptool
712 lines • 25.4 kB
JavaScript
"use strict";
// Copyright (C) 2021 Toitware ApS. All rights reserved.
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.
Object.defineProperty(exports, "__esModule", { value: true });
exports.EspLoader = exports.ESP_ROM_BAUD = exports.ChipFamily = void 0;
const errors_1 = require("./errors");
const reader_1 = require("./reader");
const stubs_1 = require("./stubs");
const util_1 = require("./util");
var ChipFamily;
(function (ChipFamily) {
ChipFamily["ESP32"] = "esp32";
ChipFamily["ESP8266"] = "esp8266";
ChipFamily["ESP32S2"] = "esp32S2";
})(ChipFamily = exports.ChipFamily || (exports.ChipFamily = {}));
const FLASH_WRITE_SIZE = 0x400;
const STUB_FLASH_WRITE_SIZE = 0x4000;
// Flash sector size, minimum unit of erase.
const FLASH_SECTOR_SIZE = 0x1000;
exports.ESP_ROM_BAUD = 115200;
const SYNC_PACKET = (0, util_1.toByteArray)("\x07\x07\x12 UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU");
const CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000;
const ESP32_DETECT_MAGIC_VALUE = 0x00f01d83;
const ESP8266_DETECT_MAGIC_VALUE = 0xfff0c101;
const ESP32S2_DETECT_MAGIC_VALUE = 0x000007c6;
const UART_CLKDIV_REG = 0x3ff40014;
const UART_CLKDIV_MASK = 0xfffff;
// Commands supported by ESP8266 ROM bootloader
const ESP_FLASH_BEGIN = 0x02;
const ESP_FLASH_DATA = 0x03;
const ESP_FLASH_END = 0x04;
const ESP_MEM_BEGIN = 0x05;
const ESP_MEM_END = 0x06;
const ESP_MEM_DATA = 0x07;
const ESP_SYNC = 0x08;
const ESP_READ_REG = 0x0a;
const ESP_ERASE_FLASH = 0xd0;
const ESP_SPI_SET_PARAMS = 0x0b;
const ESP_SPI_ATTACH = 0x0d;
const ESP_CHANGE_BAUDRATE = 0x0f;
const ESP_CHECKSUM_MAGIC = 0xef;
const ESP_RAM_BLOCK = 0x1800;
// Timeouts
const DEFAULT_TIMEOUT = 3000; // timeout for most flash operations
const CHIP_ERASE_TIMEOUT = 120000; // timeout for full chip erase
const MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2; // longest any command can run
const SYNC_TIMEOUT = 100; // timeout for syncing with bootloader
const ERASE_REGION_TIMEOUT_PER_MB = 30000; // timeout (per megabyte) for erasing a region
const MEM_END_ROM_TIMEOUT = 50;
class EspLoader {
constructor(serialPort, options) {
this.isStub = false;
this.baudRate = exports.ESP_ROM_BAUD;
this._sendCommandBuffer = new util_1.Uint8BufferSlipEncode();
this._flashBlockBuffer = new util_1.Uint8Buffer();
this._memBlockBuffer = new util_1.Uint8Buffer();
this.options = Object.assign({
flashSize: 4 * 1024 * 1024,
logger: console,
debug: false,
}, options || {});
this.reader = new reader_1.Reader(serialPort);
this.serialPort = serialPort;
}
get logger() {
return this.options.logger;
}
async writeToStream(msg) {
const writer = this.serialPort.writable.getWriter();
try {
await writer.write(msg);
}
finally {
writer.releaseLock();
}
}
/**
* Put into ROM bootload mode & attempt to synchronize with the
* ESP ROM bootloader, we will retry a few times
*/
async connect(retries = 7) {
this.reader.start();
let connected = false;
for (let i = 0; i < retries; i++) {
if (i > 0) {
this.options.logger.log("retrying...");
}
if (await this.try_connect()) {
connected = true;
break;
}
}
if (!connected) {
throw errors_1.ConnectError;
}
await this.reader.waitSilent(1, 200);
await this.chipFamily();
}
async try_connect() {
await this.serialPort.setSignals({ dataTerminalReady: false, requestToSend: true });
await (0, util_1.sleep)(100);
await this.serialPort.setSignals({ dataTerminalReady: true, requestToSend: false });
await (0, util_1.sleep)(50);
await this.serialPort.setSignals({ dataTerminalReady: false, requestToSend: false });
// Wait until device has stable output.
const wasSilent = await this.reader.waitSilent(20, 1000);
if (!wasSilent) {
this.options.logger.log("failed to enter bootloader");
return false;
}
this.options.logger.log("trying to sync with bootloader...");
// Try sync.
this.options.logger.debug("sync started");
for (let i = 0; i < 7; i++) {
try {
if (await this.sync()) {
this.options.logger.log("synced with bootloader");
return true;
}
}
catch (e) {
this.options.logger.debug("sync error", e);
}
await (0, util_1.sleep)(50);
}
this.options.logger.debug("sync stopped");
this.options.logger.log("failed to sync with bootloader");
return false;
}
/**
* shutdown the read loop.
*/
async disconnect() {
const err = await this.reader.stop();
if (err !== undefined) {
throw err;
}
}
async crystalFrequency() {
const uart_div = (await this.readRegister(UART_CLKDIV_REG)) & UART_CLKDIV_MASK;
const ets_xtal = (this.baudRate * uart_div) / 1000000 / 1;
let norm_xtal;
if (ets_xtal > 33) {
norm_xtal = 40;
}
else {
norm_xtal = 26;
}
if (Math.abs(norm_xtal - ets_xtal) > 1) {
this.logger.debug("WARNING: Unsupported crystal in use");
}
return norm_xtal;
}
/**
* @name macAddr
* Read MAC from OTP ROM
*/
async macAddr() {
const efuses = await this.efuses();
const chipFamily = await this.chipFamily();
const macAddr = new Uint8Array(6).fill(0);
const mac0 = efuses[0];
const mac1 = efuses[1];
const mac2 = efuses[2];
const mac3 = efuses[3];
let oui;
if (chipFamily === ChipFamily.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 "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 (chipFamily === ChipFamily.ESP32 || chipFamily === ChipFamily.ESP32S2) {
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 {
throw errors_1.UnknownChipFamilyError;
}
let res = macAddr[0].toString(16).toUpperCase().padStart(2, "0");
for (let i = 1; i < 6; i++) {
res += ":" + macAddr[i].toString(16).toUpperCase().padStart(2, "0");
}
return res;
}
/**
* Read the OTP data for this chip.
*/
async readEfuses() {
const chipFamily = await this.chipFamily();
let baseAddr;
if (chipFamily == ChipFamily.ESP8266) {
baseAddr = 0x3ff00050;
}
else if (chipFamily === ChipFamily.ESP32 || chipFamily === ChipFamily.ESP32S2) {
baseAddr = 0x6001a000;
}
else {
throw errors_1.UnknownChipFamilyError;
}
const efuses = new Uint32Array(4);
for (let i = 0; i < 4; i++) {
efuses[i] = await this.readRegister(baseAddr + 4 * i);
}
return efuses;
}
async efuses() {
if (this._efuses === undefined) {
this._efuses = await this.readEfuses();
}
return this._efuses;
}
/**
* Read a register within the ESP chip RAM.
*/
async readRegister(reg) {
if (this.options.debug) {
this.logger.debug("Reading Register", reg);
}
const packet = pack("I", reg);
const register = await this.checkCommand(ESP_READ_REG, packet);
return unpack("I", register)[0];
}
/**
* ESP32, ESP32S2 or ESP8266 based on which chip type we're talking to.
*/
async chipFamily() {
if (this._chipfamily === undefined) {
const datareg = await this.readRegister(CHIP_DETECT_MAGIC_REG_ADDR);
if (datareg == ESP32_DETECT_MAGIC_VALUE) {
this._chipfamily = ChipFamily.ESP32;
}
else if (datareg == ESP8266_DETECT_MAGIC_VALUE) {
this._chipfamily = ChipFamily.ESP8266;
}
else if (datareg == ESP32S2_DETECT_MAGIC_VALUE) {
this._chipfamily = ChipFamily.ESP32S2;
}
else {
throw errors_1.UnknownChipFamilyError;
}
}
return this._chipfamily;
}
/**
* The specific name of the chip.
*/
async chipName() {
const efuses = await this.efuses();
const chipFamily = await this.chipFamily();
if (chipFamily == ChipFamily.ESP32) {
return "ESP32";
}
if (chipFamily == ChipFamily.ESP32S2) {
return "ESP32-S2";
}
if (chipFamily == ChipFamily.ESP8266) {
if (efuses[0] & (1 << 4) || efuses[2] & (1 << 16)) {
return "ESP8285";
}
return "ESP8266EX";
}
throw errors_1.UnknownChipFamilyError;
}
/**
* Send a command packet, check that the command succeeded.
*/
async checkCommand(opcode, buffer, checksum = 0, timeout = DEFAULT_TIMEOUT) {
timeout = Math.min(timeout, MAX_TIMEOUT);
const unlisten = this.reader.listen();
try {
await this.sendCommand(opcode, buffer, checksum);
const resp = await this.getResponse(opcode, timeout);
const data = resp.data;
const value = resp.value;
if (data.length > 4) {
return data;
}
else {
return value;
}
}
finally {
unlisten();
}
}
async sendCommand(opcode, buffer, checksum = 0) {
const packet = this._sendCommandBuffer;
packet.reset();
packet.push(0xc0, 0x00); // direction
packet.push(opcode);
packet.pack("H", buffer.length);
packet.slipEncode = true;
packet.pack("I", checksum);
packet.copy(buffer);
packet.slipEncode = false;
packet.push(0xc0);
const res = packet.view();
if (this.options.debug) {
this.logger.debug("Writing", res.length, "byte" + (res.length == 1 ? "" : "s") + ":", res);
}
await this.writeToStream(res);
}
async getResponse(opcode, timeout = DEFAULT_TIMEOUT) {
try {
const reply = await this.reader.packet(12, timeout);
if (this.options.debug) {
this.logger.debug("Reading", reply.length, "byte" + (reply.length == 1 ? "" : "s") + ":", reply);
}
const opcode_ret = reply[1];
if (opcode !== opcode_ret) {
this.logger.debug("invalid opcode response. expected", opcode, "got", opcode_ret);
throw "invalid opcode response";
}
const res = Array.from(reply);
const value = res.slice(4, 8);
const data = res.slice(8, -1);
if (this.options.debug) {
this.logger.debug("value:", value, "data:", data);
}
return { value, data };
}
catch (e) {
throw e;
}
}
static checksum(data, state = ESP_CHECKSUM_MAGIC) {
for (const b of data) {
state ^= b;
}
return state;
}
/**
* Change the baud rate for the serial port.
*/
async setBaudRate(prevBaud, baud) {
this.logger.log("Attempting to change baud rate from", prevBaud, "to", baud, "...");
// Signal ESP32 stub that we will change the baud rate
const buffer = pack("<II", baud, this.isStub ? prevBaud : 0);
await this.checkCommand(ESP_CHANGE_BAUDRATE, buffer);
// Close the read loop and port
await this.disconnect();
await this.serialPort.close();
// Reopen the port and read loop
await this.serialPort.open({ baudRate: baud });
this.reader.start();
await (0, util_1.sleep)(50);
const wasSilent = await this.reader.waitSilent(10, 200);
if (!wasSilent) {
this.logger.debug("after baud change reader was not silent");
}
// Baud rate was changed
this.logger.log("Changed baud rate to", baud);
this.baudRate = baud;
}
/**
* Put into ROM bootload mode & attempt to synchronize with the
* ESP ROM bootloader, we will retry a few times
*/
async sync() {
const unlisten = this.reader.listen();
try {
await this.sendCommand(ESP_SYNC, SYNC_PACKET);
const { data } = await this.getResponse(ESP_SYNC, SYNC_TIMEOUT);
if (data.length > 1 && data[0] == 0 && data[1] == 0) {
return true;
}
return false;
}
finally {
unlisten();
}
}
getFlashWriteSize() {
if (this.isStub) {
return STUB_FLASH_WRITE_SIZE;
}
return FLASH_WRITE_SIZE;
}
/**
* Write data to the flash.
*/
async flashData(binaryData, offset = 0, progressCallback = undefined, encrypted = false) {
binaryData = padTo(binaryData, encrypted ? 32 : 4);
const filesize = binaryData.byteLength;
this.logger.log("Writing data with filesize:", filesize);
const blocks = await this.flashBegin(filesize, offset);
let seq = 0;
const address = offset;
let position = 0;
const stamp = Date.now();
const flashWriteSize = this.getFlashWriteSize();
let block;
while (filesize - position > 0) {
if (this.options.debug) {
this.logger.debug("Writing at " + (0, util_1.toHex)(address + seq * flashWriteSize, 8) + "... (", (seq + 1) / blocks, "%)");
}
if (progressCallback) {
progressCallback(seq, blocks);
}
if (filesize - position >= flashWriteSize) {
block = binaryData.subarray(position, position + flashWriteSize);
}
else {
// Pad the last block
block = binaryData.subarray(position, filesize);
}
await this.flashBlock(block, flashWriteSize, seq, 2000);
seq += 1;
position += flashWriteSize;
}
if (this.isStub) {
await this.readRegister(CHIP_DETECT_MAGIC_REG_ADDR);
}
if (this.options.debug) {
this.logger.debug("Took", Date.now() - stamp, "ms to write", filesize, "bytes");
}
}
async flashBlock(data, flashWriteSize, seq, timeout = 100) {
const buffer = this._flashBlockBuffer;
buffer.reset();
buffer.pack("<IIII", flashWriteSize, seq, 0, 0);
buffer.copy(data);
if (data.length < flashWriteSize) {
buffer.fill(0xff, flashWriteSize - data.length);
}
await this.checkCommand(ESP_FLASH_DATA, buffer.view(), EspLoader.checksum(data), timeout);
}
async flashBegin(size = 0, offset = 0, encrypted = false) {
let eraseSize;
const buffer = new util_1.Uint8Buffer(32);
const chipFamily = this.isStub ? null : await this.chipFamily();
const flashWriteSize = this.getFlashWriteSize();
if (chipFamily === ChipFamily.ESP32 || chipFamily === ChipFamily.ESP32S2) {
await this.checkCommand(ESP_SPI_ATTACH, new Uint8Array(8).fill(0));
}
if (chipFamily == ChipFamily.ESP32) {
// We are hardcoded for 4MB flash on ESP32
buffer.pack("<IIIIII", 0, this.options.flashSize, 0x10000, 4096, 256, 0xffff);
await this.checkCommand(ESP_SPI_SET_PARAMS, buffer.view());
}
const numBlocks = Math.floor((size + flashWriteSize - 1) / flashWriteSize);
if (chipFamily == ChipFamily.ESP8266) {
eraseSize = EspLoader.getEraseSize(offset, size);
}
else {
eraseSize = size;
}
let timeout;
if (this.isStub) {
timeout = DEFAULT_TIMEOUT;
}
else {
timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
}
const stamp = Date.now();
buffer.reset();
buffer.pack("<IIII", eraseSize, numBlocks, flashWriteSize, offset);
if (chipFamily == ChipFamily.ESP32S2) {
buffer.pack("<I", encrypted ? 1 : 0);
}
this.logger.log("Write size", eraseSize, " blocks ", numBlocks, " block size ", flashWriteSize, " offset " + (0, util_1.toHex)(offset, 4) // + ", encrypted " + (encrypted ? "yes" : "no")
);
await this.checkCommand(ESP_FLASH_BEGIN, buffer.view(), 0, timeout);
if (size != 0 && !this.isStub) {
this.logger.log("Took", Date.now() - stamp, "ms to erase", numBlocks, "bytes");
}
return numBlocks;
}
/**
* Leave flash mode and run/reboot
*
* @param reboot wheather or not to reboot
*/
async flashFinish(reboot = false) {
await this.flashBegin(0, 0);
const buffer = pack("<I", reboot ? 0 : 1);
await this.checkCommand(ESP_FLASH_END, buffer);
}
/**
* Calculate an erase size given a specific size in bytes.
* Provides a workaround for the bootloader erase bug.
*/
static getEraseSize(offset, size) {
const sectorsPerBlock = 16;
const sectorSize = FLASH_SECTOR_SIZE;
const numSectors = Math.floor((size + sectorSize - 1) / sectorSize);
const 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;
}
async memBegin(size, blocks, blockSize, offset) {
if (this.isStub) {
const chipFamily = await this.chipFamily();
const stub = getStub(chipFamily);
const load_start = offset;
const load_end = offset + size;
this.logger.log(load_start, load_end);
this.logger.log(stub.dataStart, stub.data.length, stub.textStart, stub.text.length);
for (const [start, end] of [
[stub.dataStart, stub.dataStart + stub.data.length],
[stub.textStart, stub.textStart + stub.text.length],
]) {
if (load_start < end && load_end > start) {
throw ("Software loader is resident at " +
(0, util_1.toHex)(start, 8) +
"-" +
(0, util_1.toHex)(end, 8) +
". " +
"Can't load binary at overlapping address range " +
(0, util_1.toHex)(load_start, 8) +
"-" +
(0, util_1.toHex)(load_end, 8) +
". " +
"Try changing the binary loading address.");
}
}
}
return this.checkCommand(ESP_MEM_BEGIN, pack("<IIII", size, blocks, blockSize, offset));
}
async memBlock(data, seq) {
const buffer = this._memBlockBuffer;
buffer.reset();
buffer.pack("<IIII", data.length, seq, 0, 0);
buffer.copy(data);
return await this.checkCommand(ESP_MEM_DATA, buffer.view(), EspLoader.checksum(data));
}
async memFinish(entrypoint = 0) {
const data = pack("<II", entrypoint === 0 ? 1 : 0, entrypoint);
await this.checkCommand(ESP_MEM_END, data, 0, MEM_END_ROM_TIMEOUT);
}
/**
* loads the stub onto the device.
*
* @param stub Stub to load
*/
async loadStub(stub) {
const ramBlock = ESP_RAM_BLOCK;
const writeMem = async (data, offset) => {
const length = data.length;
const blocks = Math.floor((length + ramBlock - 1) / ramBlock);
await this.memBegin(length, blocks, ramBlock, offset);
for (let seq = 0; seq < blocks; seq++) {
const fromOffs = seq * ramBlock;
let toOffs = fromOffs + ramBlock;
if (toOffs > length) {
toOffs = length;
}
await this.memBlock(data.slice(fromOffs, toOffs), seq);
}
};
const chipFamily = await this.chipFamily();
if (stub === undefined) {
stub = getStub(chipFamily);
}
this.logger.log("Uploading stub...");
await writeMem(stub.text, stub.textStart);
await writeMem(stub.data, stub.dataStart);
this.logger.log("Running stub...");
const unlisten = this.reader.listen();
try {
await this.memFinish(stub.entry);
const p = await this.reader.packet(6, 1000);
const str = String.fromCharCode(...p);
if (str !== "OHAI") {
throw "Failed to start stub. Unexpected response: " + str;
}
}
finally {
unlisten();
}
this.logger.log("Stub is now running...");
this.isStub = true;
this._chipfamily = undefined;
this._efuses = undefined;
}
/**
* erase the flash of the device
*
* @param timeoutMs the timeout of erasing
*/
async eraseFlash(timeoutMs = CHIP_ERASE_TIMEOUT) {
if (!this.isStub) {
throw "Only supported on stub";
}
await this.checkCommand(ESP_ERASE_FLASH, emptyByteArray, 0, timeoutMs);
}
}
exports.EspLoader = EspLoader;
function getStub(chipFamily) {
switch (chipFamily) {
case ChipFamily.ESP32:
return stubs_1.ESP32;
default:
throw "Unsupported chipFamily: " + chipFamily;
}
}
function padTo(image, alignment, padding = 0xff) {
const pad = image.byteLength % alignment;
if (pad == 0) {
return image;
}
const res = new Uint8Array(image.byteLength + (alignment - pad));
res.set(image);
res.fill(padding, image.byteLength);
return res;
}
/**
* Scales timeouts which are size-specific
*/
function timeoutPerMb(secondsPerMb, sizeBytes) {
const result = Math.floor(secondsPerMb * (sizeBytes / 0x1e6));
if (result < DEFAULT_TIMEOUT) {
return DEFAULT_TIMEOUT;
}
return result;
}
function pack(format, ...args) {
let pointer = 0;
const data = args;
if (format.replace(/[<>]/, "").length != data.length) {
throw "Pack format to Argument count mismatch";
}
const bytes = [];
let littleEndian = true;
for (let i = 0; i < format.length; i++) {
if (format[i] == "<") {
littleEndian = true;
}
else if (format[i] == ">") {
littleEndian = false;
}
else if (format[i] == "B") {
pushBytes(data[pointer], 1);
pointer++;
}
else if (format[i] == "H") {
pushBytes(data[pointer], 2);
pointer++;
}
else if (format[i] == "I") {
pushBytes(data[pointer], 4);
pointer++;
}
else {
throw "Unhandled character in pack format";
}
}
function pushBytes(value, byteCount) {
for (let i = 0; i < byteCount; i++) {
if (littleEndian) {
bytes.push((value >> (i * 8)) & 0xff);
}
else {
bytes.push((value >> ((byteCount - i) * 8)) & 0xff);
}
}
}
return Uint8Array.from(bytes);
}
function unpack(format, bytes) {
let pointer = 0;
const data = [];
for (const c of format) {
if (c == "B") {
data.push(bytes[pointer] & 0xff);
pointer += 1;
}
else if (c == "H") {
data.push((bytes[pointer] & 0xff) | ((bytes[pointer + 1] & 0xff) << 8));
pointer += 2;
}
else if (c == "I") {
data.push((bytes[pointer] & 0xff) |
((bytes[pointer + 1] & 0xff) << 8) |
((bytes[pointer + 2] & 0xff) << 16) |
((bytes[pointer + 3] & 0xff) << 24));
pointer += 4;
}
else {
throw "Unhandled character in unpack format";
}
}
return data;
}
const emptyByteArray = new Uint8Array();
//# sourceMappingURL=index.js.map