Public Access
Support both Nano bootloaders for flashing over WiFi
- Uploader sends 'FLASH <baud>' first; the ESP32 restarts the Nano and bridges at that baud rate, then answers OK - --baud=auto (default) tries 57600 (old ATmegaBOOT) then 115200 (Optiboot) - Optional nano_reset GPIO (NPN to Nano RESET) pulses reset after power on, needed for Optiboot which skips the bootloader after a power cycle - Clear errors for outdated ESP32 firmware and missing wires - README: bootloader table, reset wire, troubleshooting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bea5b9daa1
commit
cefb76c1ad
8 files changed
+204
-61
No files matched your search
@@ -17,9 +17,13 @@ See README.md for the overview.
|
|||||||
- If an ESP32 build fails with `No module named 'intelhex'`, run
|
- If an ESP32 build fails with `No module named 'intelhex'`, run
|
||||||
`~/.platformio/penv/Scripts/python.exe -m pip install intelhex`.
|
`~/.platformio/penv/Scripts/python.exe -m pip install intelhex`.
|
||||||
- `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP)
|
- `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP)
|
||||||
talks to `NanoFlashBridge` on the ESP32 (port 2323), which power cycles the Nano into
|
talks to `NanoFlashBridge` on the ESP32 (port 2323). The uploader first sends
|
||||||
its old bootloader (57600 baud) and passes the bytes through. avrdude 6.3's `net:` port is
|
`FLASH <baud>\n`. The ESP power cycles the Nano (plus an optional reset pulse on
|
||||||
broken on Windows (`ser_drain()` fails), which is why the uploader is a Python script.
|
`nano_reset`), answers `OK\n`, then passes the bytes through at that baud rate.
|
||||||
|
`--baud=auto` tries 57600 (old ATmegaBOOT, which also starts after a power cycle), then
|
||||||
|
115200 (Optiboot, which only starts after a reset-pin reset, so it needs `nano_reset`).
|
||||||
|
avrdude 6.3's `net:` port is broken on Windows (`ser_drain()` fails), which is why the
|
||||||
|
uploader is a Python script.
|
||||||
The uploader only uses the Python standard library. Test it against a simulated
|
The uploader only uses the Python standard library. Test it against a simulated
|
||||||
bootloader before changing it.
|
bootloader before changing it.
|
||||||
- `src/esp32/libs.h` defines globals in the header, so it can only be included from
|
- `src/esp32/libs.h` defines globals in the header, so it can only be included from
|
||||||
|
|||||||
@@ -92,10 +92,11 @@ pio run -e nano_ota -t upload
|
|||||||
What happens:
|
What happens:
|
||||||
|
|
||||||
1. The uploader ([tools/nano_net_upload.py](tools/nano_net_upload.py)) connects to the
|
1. The uploader ([tools/nano_net_upload.py](tools/nano_net_upload.py)) connects to the
|
||||||
ESP32 on TCP port **2323**.
|
ESP32 on TCP port **2323** and tells it the bootloader's baud rate.
|
||||||
2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. This
|
2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. If
|
||||||
restarts the Nano, and its bootloader starts.
|
the reset wire is fitted, it also pulses the Nano's RESET pin. This restarts the Nano,
|
||||||
3. The ESP32 switches its link to the bootloader's 57600 baud and passes bytes between
|
and its bootloader starts.
|
||||||
|
3. The ESP32 switches its link to the bootloader's baud rate and passes bytes between
|
||||||
WiFi and the Nano.
|
WiFi and the Nano.
|
||||||
4. The uploader writes the new firmware and reads it back to verify it.
|
4. The uploader writes the new firmware and reads it back to verify it.
|
||||||
5. When the uploader disconnects, the ESP32 switches the relay off. The clock powers the
|
5. When the uploader disconnects, the ESP32 switches the relay off. The clock powers the
|
||||||
@@ -108,24 +109,43 @@ The display goes dark for the duration of the upload, which takes a few seconds.
|
|||||||
- The reply wire must be connected: Nano D1 (TX) → voltage divider → ESP32 GPIO 16. The
|
- The reply wire must be connected: Nano D1 (TX) → voltage divider → ESP32 GPIO 16. The
|
||||||
uploader needs the bootloader's answers.
|
uploader needs the bootloader's answers.
|
||||||
- The ESP32 must be running firmware with the flash bridge (this version or newer).
|
- The ESP32 must be running firmware with the flash bridge (this version or newer).
|
||||||
- The Nano must have the **old bootloader** (the `nanoatmega328` board, 57600 baud), which
|
|
||||||
also runs at power-on. The newer Optiboot bootloader only runs after a reset-pin reset,
|
**Bootloaders**
|
||||||
so the relay power cycle would not start it.
|
|
||||||
|
Flashing the Nano's firmware never changes its bootloader. Both common Nano bootloaders
|
||||||
|
are supported:
|
||||||
|
|
||||||
|
| Bootloader | Baud | Starts after a power cycle | Reset wire |
|
||||||
|
|------------|------|----------------------------|------------|
|
||||||
|
| Old bootloader (ATmegaBOOT), the one this Nano has now | 57600 | Yes | Not needed |
|
||||||
|
| Optiboot ("new bootloader") | 115200 | No, only after a reset-pin reset | **Required** |
|
||||||
|
|
||||||
|
By default (`--baud=auto` in `upload_flags`) the uploader tries 57600 first and then
|
||||||
|
115200. Set `--baud=57600` or `--baud=115200` to skip the other attempt.
|
||||||
|
|
||||||
|
If you ever replace the bootloader with Optiboot:
|
||||||
|
|
||||||
|
- Fit the reset wire: ESP32 GPIO → 1 kΩ → base of an NPN transistor (e.g. 2N2222 or
|
||||||
|
BC547). Connect the collector to Nano RESET and the emitter to GND. Set `nano_reset` in
|
||||||
|
`src/esp32/libs.h` to that GPIO, for example `4`. The transistor keeps the Nano's 5 V
|
||||||
|
reset line away from the 3.3 V ESP32 pin.
|
||||||
|
- For uploads over USB, change `board` in the `nano` environment to `nanoatmega328new`.
|
||||||
|
|
||||||
**Settings**
|
**Settings**
|
||||||
|
|
||||||
- ESP32 IP address: `upload_port` in the `nano_ota` environment in `platformio.ini`. You
|
- ESP32 IP address: `upload_port` in the `nano_ota` environment in `platformio.ini`. You
|
||||||
can add a port number: `10.1.1.198:2323`. For a one-time different address, use
|
can add a port number: `10.1.1.198:2323`. For a one-time different address, use
|
||||||
`pio run -e nano_ota -t upload --upload-port 10.1.1.50`.
|
`pio run -e nano_ota -t upload --upload-port 10.1.1.50`.
|
||||||
- Port, baud rate and power-off time: `NANO_FLASH_PORT`, `NANO_BOOTLOADER_BAUD` and
|
- Port, reset pin and timings: `NANO_FLASH_PORT`, `nano_reset`, `NANO_POWER_OFF_TIME` and
|
||||||
`NANO_POWER_OFF_TIME` in `src/esp32/libs.h`.
|
`NANO_RESET_DELAY` in `src/esp32/libs.h`.
|
||||||
|
|
||||||
**If it fails**
|
**If it fails**
|
||||||
|
|
||||||
| Message | Cause |
|
| Message | Cause |
|
||||||
|---------|-------|
|
|---------|-------|
|
||||||
| `can't connect to ...` | Wrong IP address, or the ESP32 is offline or running older firmware |
|
| `can't connect to ...` | Wrong IP address, or the ESP32 is offline |
|
||||||
| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot |
|
| `the ESP32 flash bridge didn't accept the request` | The ESP32 runs older firmware without the flash bridge. Flash it first (`esp32` or `esp32_usb`). |
|
||||||
|
| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot and the reset wire is missing |
|
||||||
| `bad reply to command ...` / `verify failed` | Connection problem. Try again. A failed upload can leave the Nano without working firmware, but the bootloader itself is never overwritten, so flashing again (over WiFi or USB) always fixes it. |
|
| `bad reply to command ...` / `verify failed` | Connection problem. Try again. A failed upload can leave the Nano without working firmware, but the bootloader itself is never overwritten, so flashing again (over WiFi or USB) always fixes it. |
|
||||||
|
|
||||||
The ESP32 ends the session after 10 seconds without data, so the clock always resumes,
|
The ESP32 ends the session after 10 seconds without data, so the clock always resumes,
|
||||||
@@ -143,6 +163,7 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o
|
|||||||
|-------|------|-------|
|
|-------|------|-------|
|
||||||
| GPIO 17 (TX2) | D0 (RX) | Display commands |
|
| GPIO 17 (TX2) | D0 (RX) | Display commands |
|
||||||
| GPIO 16 (RX2) | D1 (TX) | `READY` and `ERR` replies, and bootloader answers when flashing over WiFi. Use a 5 V → 3.3 V voltage divider (e.g. 1 kΩ / 2 kΩ), because the ESP32 is not 5 V tolerant. Without it, the ESP waits a fixed 3 s after power-on, and `nano_ota` doesn't work. |
|
| GPIO 16 (RX2) | D1 (TX) | `READY` and `ERR` replies, and bootloader answers when flashing over WiFi. Use a 5 V → 3.3 V voltage divider (e.g. 1 kΩ / 2 kΩ), because the ESP32 is not 5 V tolerant. Without it, the ESP waits a fixed 3 s after power-on, and `nano_ota` doesn't work. |
|
||||||
|
| GPIO set in `nano_reset` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). |
|
||||||
| GND | GND | Common ground |
|
| GND | GND | Common ground |
|
||||||
| GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) |
|
| GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) |
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -41,11 +41,13 @@ monitor_port = COM10
|
|||||||
|
|
||||||
; Same Nano firmware, flashed over WiFi through the ESP32 (see README.md).
|
; Same Nano firmware, flashed over WiFi through the ESP32 (see README.md).
|
||||||
; Needs the Nano TX -> ESP32 GPIO 16 wire. upload_port = ESP32 IP[:port], default port 2323.
|
; Needs the Nano TX -> ESP32 GPIO 16 wire. upload_port = ESP32 IP[:port], default port 2323.
|
||||||
|
; --baud: bootloader baud rate. auto tries 57600 (old bootloader), then 115200 (Optiboot).
|
||||||
[env:nano_ota]
|
[env:nano_ota]
|
||||||
extends = env:nano
|
extends = env:nano
|
||||||
upload_protocol = custom
|
upload_protocol = custom
|
||||||
upload_port = 10.1.1.198
|
upload_port = 10.1.1.198
|
||||||
upload_command = "$PYTHONEXE" "$PROJECT_DIR/tools/nano_net_upload.py" "$SOURCE" $UPLOAD_PORT
|
upload_flags = --baud=auto
|
||||||
|
upload_command = "$PYTHONEXE" "$PROJECT_DIR/tools/nano_net_upload.py" "$SOURCE" $UPLOAD_PORT $UPLOAD_FLAGS
|
||||||
|
|
||||||
; ---------------------------------------------------------------------------
|
; ---------------------------------------------------------------------------
|
||||||
; ESP32 - controller, talks to the Nano over serial
|
; ESP32 - controller, talks to the Nano over serial
|
||||||
|
|||||||
+6
-1
@@ -50,6 +50,9 @@ const int relay = 13; // Pin of the relay, powers the panels and the Nano (LOW =
|
|||||||
HardwareSerial &nanoSerial = Serial2;
|
HardwareSerial &nanoSerial = Serial2;
|
||||||
const int nano_rx = 16; // UART2 RX, wired to the Nano TX (through a 5V -> 3.3V divider)
|
const int nano_rx = 16; // UART2 RX, wired to the Nano TX (through a 5V -> 3.3V divider)
|
||||||
const int nano_tx = 17; // UART2 TX, wired to the Nano RX
|
const int nano_tx = 17; // UART2 TX, wired to the Nano RX
|
||||||
|
// Optional: GPIO that resets the Nano through an NPN transistor (HIGH = reset), -1 = not wired.
|
||||||
|
// Needed to flash a Nano with the Optiboot bootloader, which doesn't start after a power cycle.
|
||||||
|
const int nano_reset = -1;
|
||||||
bool relay_on = false;
|
bool relay_on = false;
|
||||||
// Max wait for the Nano's READY after power on (also the wait when Nano TX is not wired to our RX)
|
// Max wait for the Nano's READY after power on (also the wait when Nano TX is not wired to our RX)
|
||||||
const uint32_t NANO_BOOT_TIMEOUT = 3000;
|
const uint32_t NANO_BOOT_TIMEOUT = 3000;
|
||||||
@@ -57,8 +60,10 @@ const uint32_t NANO_BAUD = 115200;
|
|||||||
|
|
||||||
// Flashing the Nano over WiFi (see README.md, pio run -e nano_ota -t upload)
|
// Flashing the Nano over WiFi (see README.md, pio run -e nano_ota -t upload)
|
||||||
const uint16_t NANO_FLASH_PORT = 2323;
|
const uint16_t NANO_FLASH_PORT = 2323;
|
||||||
const uint32_t NANO_BOOTLOADER_BAUD = 57600; // old Nano bootloader
|
// The bootloader baud rate is chosen by the uploader (57600 old bootloader, 115200 Optiboot)
|
||||||
const uint32_t NANO_POWER_OFF_TIME = 1000; // relay off time to make sure the Nano resets
|
const uint32_t NANO_POWER_OFF_TIME = 1000; // relay off time to make sure the Nano resets
|
||||||
|
const uint32_t NANO_RESET_DELAY = 200; // after power on, before the reset pulse
|
||||||
|
const uint32_t NANO_RESET_PULSE = 50;
|
||||||
|
|
||||||
int hour = 0;
|
int hour = 0;
|
||||||
int minute = 0;
|
int minute = 0;
|
||||||
|
|||||||
+20
-4
@@ -60,18 +60,29 @@ void relayOff()
|
|||||||
pinMode(nano_tx, INPUT);
|
pinMode(nano_tx, INPUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flash session started: power cycle the Nano so its bootloader runs, and talk to the
|
// Flash session started: restart the Nano so its bootloader runs, and talk to the
|
||||||
// bootloader at its baud rate. The PC uploader keeps sending sync bytes until it answers.
|
// bootloader at its baud rate. The PC uploader keeps sending sync bytes until it answers.
|
||||||
void startNanoFlash()
|
// The old bootloader also starts after a power cycle; Optiboot only after a reset pulse.
|
||||||
|
void startNanoFlash(uint32_t baud)
|
||||||
{
|
{
|
||||||
if (relay_on)
|
if (relay_on)
|
||||||
relayOff();
|
relayOff();
|
||||||
delay(NANO_POWER_OFF_TIME);
|
delay(NANO_POWER_OFF_TIME);
|
||||||
|
|
||||||
nanoSerial.begin(NANO_BOOTLOADER_BAUD, SERIAL_8N1, nano_rx, nano_tx);
|
nanoSerial.begin(baud, SERIAL_8N1, nano_rx, nano_tx);
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
|
||||||
|
if (nano_reset >= 0)
|
||||||
|
{
|
||||||
|
delay(NANO_RESET_DELAY);
|
||||||
|
digitalWrite(nano_reset, HIGH);
|
||||||
|
delay(NANO_RESET_PULSE);
|
||||||
|
digitalWrite(nano_reset, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop anything the Nano sent before its bootloader started
|
||||||
while (nanoSerial.available())
|
while (nanoSerial.available())
|
||||||
nanoSerial.read();
|
nanoSerial.read();
|
||||||
digitalWrite(relay, LOW);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flash session ended: switch off, the clock powers the Nano on again with the new firmware
|
// Flash session ended: switch off, the clock powers the Nano on again with the new firmware
|
||||||
@@ -312,6 +323,11 @@ void setup()
|
|||||||
otaPanicMode = false;
|
otaPanicMode = false;
|
||||||
|
|
||||||
pinMode(relay, OUTPUT);
|
pinMode(relay, OUTPUT);
|
||||||
|
if (nano_reset >= 0)
|
||||||
|
{
|
||||||
|
digitalWrite(nano_reset, LOW);
|
||||||
|
pinMode(nano_reset, OUTPUT);
|
||||||
|
}
|
||||||
relayOn();
|
relayOn();
|
||||||
|
|
||||||
display.begin(&nanoSerial);
|
display.begin(&nanoSerial);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "nano_flash_bridge.h"
|
#include "nano_flash_bridge.h"
|
||||||
|
|
||||||
NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(), void (*onEnd)())
|
NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(uint32_t baud), void (*onEnd)())
|
||||||
: _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
|
: _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,19 @@ bool NanoFlashBridge::handle()
|
|||||||
if (!client)
|
if (!client)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
uint32_t baud = _readHeader(client);
|
||||||
|
if (!baud)
|
||||||
|
{
|
||||||
|
client.print("ERR expected FLASH <baud>\n");
|
||||||
|
client.stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
_client = client;
|
_client = client;
|
||||||
_client.setNoDelay(true);
|
_client.setNoDelay(true);
|
||||||
_active = true;
|
_active = true;
|
||||||
_onStart();
|
_onStart(baud);
|
||||||
|
_client.print("OK\n");
|
||||||
_last_activity = millis();
|
_last_activity = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +67,34 @@ bool NanoFlashBridge::handle()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the "FLASH <baud>" line. Returns the baud rate, or 0 when it is missing or invalid.
|
||||||
|
uint32_t NanoFlashBridge::_readHeader(WiFiClient &client)
|
||||||
|
{
|
||||||
|
char line[32];
|
||||||
|
uint8_t len = 0;
|
||||||
|
uint32_t start = millis();
|
||||||
|
while (millis() - start < HEADER_TIMEOUT && client.connected())
|
||||||
|
{
|
||||||
|
if (!client.available())
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char c = client.read();
|
||||||
|
if (c == '\n')
|
||||||
|
{
|
||||||
|
line[len] = '\0';
|
||||||
|
unsigned long baud;
|
||||||
|
if (sscanf(line, "FLASH %lu", &baud) == 1 && baud >= 9600 && baud <= 1000000)
|
||||||
|
return baud;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (c != '\r' && len < sizeof(line) - 1)
|
||||||
|
line[len++] = c;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void NanoFlashBridge::_end()
|
void NanoFlashBridge::_end()
|
||||||
{
|
{
|
||||||
_client.stop();
|
_client.stop();
|
||||||
|
|||||||
@@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
// WiFi to serial bridge for flashing the Nano from the PC (pio run -e nano_ota -t upload).
|
// WiFi to serial bridge for flashing the Nano from the PC (pio run -e nano_ota -t upload).
|
||||||
//
|
//
|
||||||
// When a TCP client connects, onStart() is called to restart the Nano into its bootloader.
|
// The uploader (tools/nano_net_upload.py) connects and first sends one line:
|
||||||
// After that, bytes are copied between the TCP connection and the Nano's serial port
|
// FLASH <baud>\n
|
||||||
|
// with the baud rate of the Nano's bootloader (57600 old bootloader, 115200 Optiboot).
|
||||||
|
// onStart(baud) then restarts the Nano into its bootloader, the bridge answers "OK\n",
|
||||||
|
// and from then on bytes are copied between the TCP connection and the Nano's serial port
|
||||||
// until the client disconnects or is idle too long. onEnd() is then called.
|
// until the client disconnects or is idle too long. onEnd() is then called.
|
||||||
// The uploader on the PC side is tools/nano_net_upload.py.
|
|
||||||
class NanoFlashBridge
|
class NanoFlashBridge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(), void (*onEnd)());
|
NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(uint32_t baud), void (*onEnd)());
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
@@ -21,16 +23,19 @@ public:
|
|||||||
bool handle();
|
bool handle();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Max wait for the FLASH line after connecting
|
||||||
|
static const uint32_t HEADER_TIMEOUT = 2000;
|
||||||
// Give up when no data moves in either direction for this long
|
// Give up when no data moves in either direction for this long
|
||||||
static const uint32_t IDLE_TIMEOUT = 10000;
|
static const uint32_t IDLE_TIMEOUT = 10000;
|
||||||
|
|
||||||
WiFiServer _server;
|
WiFiServer _server;
|
||||||
WiFiClient _client;
|
WiFiClient _client;
|
||||||
HardwareSerial &_serial;
|
HardwareSerial &_serial;
|
||||||
void (*_onStart)();
|
void (*_onStart)(uint32_t baud);
|
||||||
void (*_onEnd)();
|
void (*_onEnd)();
|
||||||
bool _active = false;
|
bool _active = false;
|
||||||
uint32_t _last_activity = 0;
|
uint32_t _last_activity = 0;
|
||||||
|
|
||||||
|
uint32_t _readHeader(WiFiClient &client);
|
||||||
void _end();
|
void _end();
|
||||||
};
|
};
|
||||||
+87
-34
@@ -1,25 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Flash the Arduino Nano over WiFi, through the ESP32's flash bridge.
|
"""Flash the Arduino Nano over WiFi, through the ESP32's flash bridge.
|
||||||
|
|
||||||
Usage: python nano_net_upload.py firmware.hex HOST[:PORT]
|
Usage: python nano_net_upload.py firmware.hex HOST[:PORT] [--baud auto|57600|115200]
|
||||||
|
|
||||||
The ESP32 power cycles the Nano when we connect (so its bootloader starts) and then
|
--baud is the Nano bootloader's baud rate:
|
||||||
passes bytes between this TCP connection and the Nano's serial port. This script talks
|
57600 old Nano bootloader (ATmegaBOOT)
|
||||||
the STK500v1 protocol of the Arduino bootloader directly. avrdude can't be used here:
|
115200 Optiboot ("new bootloader"); needs the ESP32 reset wire
|
||||||
its network ports are unreliable on Windows.
|
auto try 57600 first, then 115200 (default)
|
||||||
|
|
||||||
|
The ESP32 restarts the Nano when we connect (so its bootloader starts) and then passes
|
||||||
|
bytes between this TCP connection and the Nano's serial port. This script talks the
|
||||||
|
STK500v1 protocol of the Arduino bootloaders directly. avrdude can't be used here: its
|
||||||
|
network ports are unreliable on Windows.
|
||||||
|
|
||||||
Normally run by PlatformIO: pio run -e nano_ota -t upload
|
Normally run by PlatformIO: pio run -e nano_ota -t upload
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
DEFAULT_PORT = 2323
|
DEFAULT_PORT = 2323
|
||||||
PAGE_SIZE = 128 # ATmega328P flash page in bytes
|
PAGE_SIZE = 128 # ATmega328P flash page in bytes
|
||||||
FLASH_SIZE = 30720 # 32 KB minus the 2 KB bootloader
|
|
||||||
SIGNATURE = b"\x1e\x95\x0f" # ATmega328P
|
SIGNATURE = b"\x1e\x95\x0f" # ATmega328P
|
||||||
|
|
||||||
|
# Bootloaders: baud rate -> (name, flash space left for the firmware)
|
||||||
|
BOOTLOADERS = {
|
||||||
|
57600: ("old bootloader", 32768 - 2048),
|
||||||
|
115200: ("Optiboot", 32768 - 512),
|
||||||
|
}
|
||||||
|
AUTO_ORDER = [57600, 115200]
|
||||||
|
|
||||||
# STK500v1
|
# STK500v1
|
||||||
STK_OK = 0x10
|
STK_OK = 0x10
|
||||||
STK_INSYNC = 0x14
|
STK_INSYNC = 0x14
|
||||||
@@ -32,7 +44,8 @@ PROG_PAGE = 0x64
|
|||||||
READ_PAGE = 0x74
|
READ_PAGE = 0x74
|
||||||
READ_SIGN = 0x75
|
READ_SIGN = 0x75
|
||||||
|
|
||||||
SYNC_TIMEOUT = 10.0 # seconds to wait for the bootloader after connecting
|
BRIDGE_TIMEOUT = 5.0 # wait for the ESP32's OK (it power cycles the Nano first)
|
||||||
|
SYNC_TIMEOUT = 4.0 # wait for the bootloader to answer after the restart
|
||||||
REPLY_TIMEOUT = 1.0
|
REPLY_TIMEOUT = 1.0
|
||||||
|
|
||||||
|
|
||||||
@@ -40,8 +53,12 @@ class UploadError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoBootloader(UploadError):
|
||||||
|
"""The bootloader didn't answer (wrong baud rate, missing wire, ...)."""
|
||||||
|
|
||||||
|
|
||||||
def read_hex(path):
|
def read_hex(path):
|
||||||
"""Parse an Intel HEX file into a bytearray (gaps filled with 0xFF)."""
|
"""Parse an Intel HEX file into a bytearray (gaps filled with 0xFF), padded to whole pages."""
|
||||||
image = bytearray()
|
image = bytearray()
|
||||||
base = 0
|
base = 0
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
@@ -68,9 +85,6 @@ def read_hex(path):
|
|||||||
base = ((data[0] << 8) | data[1]) << 4
|
base = ((data[0] << 8) | data[1]) << 4
|
||||||
elif rtype == 0x04: # extended linear address
|
elif rtype == 0x04: # extended linear address
|
||||||
base = ((data[0] << 8) | data[1]) << 16
|
base = ((data[0] << 8) | data[1]) << 16
|
||||||
if len(image) > FLASH_SIZE:
|
|
||||||
raise UploadError(f"firmware is {len(image)} bytes, the Nano has room for {FLASH_SIZE}")
|
|
||||||
# pad to whole pages
|
|
||||||
if len(image) % PAGE_SIZE:
|
if len(image) % PAGE_SIZE:
|
||||||
image.extend(b"\xff" * (PAGE_SIZE - len(image) % PAGE_SIZE))
|
image.extend(b"\xff" * (PAGE_SIZE - len(image) % PAGE_SIZE))
|
||||||
return image
|
return image
|
||||||
@@ -81,7 +95,6 @@ class Bootloader:
|
|||||||
self.sock = sock
|
self.sock = sock
|
||||||
|
|
||||||
def _recv(self, n, timeout=REPLY_TIMEOUT):
|
def _recv(self, n, timeout=REPLY_TIMEOUT):
|
||||||
self.sock.settimeout(timeout)
|
|
||||||
data = b""
|
data = b""
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
while len(data) < n:
|
while len(data) < n:
|
||||||
@@ -112,7 +125,7 @@ class Bootloader:
|
|||||||
return reply[1:-1]
|
return reply[1:-1]
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
"""Keep sending GET_SYNC until the bootloader answers (it starts after the power cycle)."""
|
"""Keep sending GET_SYNC until the bootloader answers (it starts after the restart)."""
|
||||||
deadline = time.monotonic() + SYNC_TIMEOUT
|
deadline = time.monotonic() + SYNC_TIMEOUT
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
self.sock.sendall(bytes([GET_SYNC, CRC_EOP]))
|
self.sock.sendall(bytes([GET_SYNC, CRC_EOP]))
|
||||||
@@ -122,9 +135,7 @@ class Bootloader:
|
|||||||
self._drain()
|
self._drain()
|
||||||
self.command([GET_SYNC])
|
self.command([GET_SYNC])
|
||||||
return
|
return
|
||||||
raise UploadError(
|
raise NoBootloader(f"no answer from the Nano bootloader within {SYNC_TIMEOUT:.0f} s")
|
||||||
f"no answer from the Nano bootloader within {SYNC_TIMEOUT:.0f} s. "
|
|
||||||
"Is the Nano TX -> ESP32 GPIO 16 wire connected?")
|
|
||||||
|
|
||||||
def load_address(self, byte_addr):
|
def load_address(self, byte_addr):
|
||||||
word = byte_addr // 2
|
word = byte_addr // 2
|
||||||
@@ -147,26 +158,44 @@ def progress(label, done, total, step=PAGE_SIZE):
|
|||||||
print(f"{label} {percent:3d}% ({done}/{total} bytes)", flush=True)
|
print(f"{label} {percent:3d}% ({done}/{total} bytes)", flush=True)
|
||||||
|
|
||||||
|
|
||||||
def upload(hex_path, host, port):
|
def connect(host, port, baud):
|
||||||
image = read_hex(hex_path)
|
"""Connect to the ESP32 bridge and have it restart the Nano into its bootloader."""
|
||||||
print(f"Firmware: {hex_path} ({len(image)} bytes)")
|
|
||||||
|
|
||||||
print(f"Connecting to the ESP32 flash bridge at {host}:{port} ...")
|
|
||||||
try:
|
try:
|
||||||
sock = socket.create_connection((host, port), timeout=10)
|
sock = socket.create_connection((host, port), timeout=10)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UploadError(f"can't connect to {host}:{port}: {e}")
|
raise UploadError(f"can't connect to {host}:{port}: {e}")
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
|
|
||||||
with sock:
|
sock.sendall(f"FLASH {baud}\n".encode())
|
||||||
boot = Bootloader(sock)
|
reply = b""
|
||||||
print("Waiting for the Nano bootloader (the ESP32 power cycles the display) ...")
|
deadline = time.monotonic() + BRIDGE_TIMEOUT
|
||||||
boot.sync()
|
while not reply.endswith(b"\n") and time.monotonic() < deadline:
|
||||||
|
sock.settimeout(max(deadline - time.monotonic(), 0.01))
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(64)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
reply += chunk
|
||||||
|
if reply != b"OK\n":
|
||||||
|
sock.close()
|
||||||
|
raise UploadError(
|
||||||
|
f"the ESP32 flash bridge didn't accept the request (reply: {reply!r}). "
|
||||||
|
"Is the ESP32 running up-to-date firmware?")
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def flash(sock, image, baud):
|
||||||
|
name, flash_size = BOOTLOADERS[baud]
|
||||||
|
if len(image) > flash_size:
|
||||||
|
raise UploadError(f"firmware is {len(image)} bytes, but with the {name} the Nano has room for {flash_size}")
|
||||||
|
|
||||||
|
boot = Bootloader(sock)
|
||||||
signature = boot.command([READ_SIGN], reply_len=3)
|
signature = boot.command([READ_SIGN], reply_len=3)
|
||||||
if signature != SIGNATURE:
|
if signature != SIGNATURE:
|
||||||
raise UploadError(f"unexpected device signature {signature.hex()} (expected ATmega328P)")
|
raise UploadError(f"unexpected device signature {signature.hex()} (expected ATmega328P)")
|
||||||
print("Found ATmega328P")
|
print(f"Found ATmega328P, bootloader: {name} ({baud} baud)")
|
||||||
|
|
||||||
boot.command([ENTER_PROGMODE])
|
boot.command([ENTER_PROGMODE])
|
||||||
|
|
||||||
@@ -180,23 +209,47 @@ def upload(hex_path, host, port):
|
|||||||
if actual != expected:
|
if actual != expected:
|
||||||
for i, (a, b) in enumerate(zip(actual, expected)):
|
for i, (a, b) in enumerate(zip(actual, expected)):
|
||||||
if a != b:
|
if a != b:
|
||||||
raise UploadError(
|
raise UploadError(f"verify failed at 0x{addr + i:04x}: read 0x{a:02x}, expected 0x{b:02x}")
|
||||||
f"verify failed at 0x{addr + i:04x}: read 0x{a:02x}, expected 0x{b:02x}")
|
|
||||||
progress("Verifying", addr + PAGE_SIZE, len(image))
|
progress("Verifying", addr + PAGE_SIZE, len(image))
|
||||||
|
|
||||||
boot.command([LEAVE_PROGMODE])
|
boot.command([LEAVE_PROGMODE])
|
||||||
|
|
||||||
|
|
||||||
|
def upload(hex_path, host, port, bauds):
|
||||||
|
image = read_hex(hex_path)
|
||||||
|
print(f"Firmware: {hex_path} ({len(image)} bytes)")
|
||||||
|
|
||||||
|
for i, baud in enumerate(bauds):
|
||||||
|
print(f"Connecting to the ESP32 flash bridge at {host}:{port}, "
|
||||||
|
f"restarting the Nano into its bootloader at {baud} baud ...", flush=True)
|
||||||
|
with connect(host, port, baud) as sock:
|
||||||
|
try:
|
||||||
|
Bootloader(sock).sync()
|
||||||
|
except NoBootloader:
|
||||||
|
if i + 1 < len(bauds):
|
||||||
|
print(f"No answer at {baud} baud, trying {bauds[i + 1]} ...", flush=True)
|
||||||
|
continue
|
||||||
|
raise NoBootloader(
|
||||||
|
f"no answer from the Nano bootloader at {', '.join(map(str, bauds))} baud. "
|
||||||
|
"Check the Nano TX -> ESP32 GPIO 16 wire. A Nano with Optiboot also needs "
|
||||||
|
"the reset wire (nano_reset in src/esp32/libs.h).")
|
||||||
|
flash(sock, image, baud)
|
||||||
print("Done. The ESP32 restarts the Nano with the new firmware.")
|
print("Done. The ESP32 restarts the Nano with the new firmware.")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) != 3:
|
parser = argparse.ArgumentParser(description="Flash the Nano over WiFi through the ESP32.")
|
||||||
print(__doc__)
|
parser.add_argument("hex", help="firmware .hex file")
|
||||||
return 2
|
parser.add_argument("target", help=f"ESP32 address, HOST[:PORT] (default port {DEFAULT_PORT})")
|
||||||
hex_path, target = sys.argv[1], sys.argv[2]
|
parser.add_argument("--baud", default="auto", choices=["auto"] + [str(b) for b in BOOTLOADERS],
|
||||||
host, _, port = target.partition(":")
|
help="bootloader baud rate (default: auto)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
host, _, port = args.target.partition(":")
|
||||||
|
bauds = AUTO_ORDER if args.baud == "auto" else [int(args.baud)]
|
||||||
try:
|
try:
|
||||||
upload(hex_path, host, int(port) if port else DEFAULT_PORT)
|
upload(args.hex, host, int(port) if port else DEFAULT_PORT, bauds)
|
||||||
except UploadError as e:
|
except UploadError as e:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
print(f"ERROR: {e}", file=sys.stderr)
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
|||||||
Reference in new issue
Block a user