Skip to content

Commit f5e492a

Browse files
committed
feat: Add magic baud rate to enter download mode
This feature allows updating the bridge firmware over the same USB cable without physical BOOT/RESET access. Configure the magic baud in menuconfig under **Bridge Configuration** (`CONFIG_BRIDGE_DOWNLOAD_MAGIC_BAUD`, default `1200`). Set to `0` to disable. Rebuild and flash after changing the value. The host `bit_rate` must match exactly. To trigger download mode from the host (adjust `PORT` and baud if configured differently): ```bash python3 -c "import serial; s=serial.Serial('PORT', 1200); s.close()" ```
1 parent e74457c commit f5e492a

7 files changed

Lines changed: 190 additions & 18 deletions

File tree

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ idf.py -p /dev/ttyACMx flash monitor
5252

5353
Please note that [esptool](https://github.com/espressif/esptool) or any terminal program can connect to the virtual serial port as well.
5454

55+
### Updating Bridge Firmware (Magic Baud Download Mode)
56+
57+
The bridge chip (ESP32-S2 or ESP32-S3) can reset **itself** into ROM download mode when the host sets the USB CDC port to a configured magic baud rate and then closes the port. This allows updating EUB firmware over the same USB cable without physical BOOT/RESET access.
58+
59+
The magic baud **arms** the reset; clearing DTR on port close **fires** it. The bridge then sets `RTC_CNTL_FORCE_DOWNLOAD_BOOT` and restarts. The ROM bootloader exposes a USB download interface on the same cable. This affects the **bridge MCU only**. While the bridge application is running, DTR/RTS on this port still drive the target's BOOT and RST lines; they are unrelated to this self-reset path except that clearing DTR is what fires it once armed.
60+
61+
Do **not** use the magic baud for normal target serial traffic. Opening the CDC at that rate and then clearing DTR (port close, or a host reset sequence that deasserts DTR) puts the **bridge** into download mode instead of communicating with the target.
62+
63+
Configure the magic baud in menuconfig under **Bridge Configuration** (`CONFIG_BRIDGE_DOWNLOAD_MAGIC_BAUD`, default `1200`). Set to `0` to disable. Prefer a baud that host tools will not use for the target. Rebuild and flash after changing the value. The host `bit_rate` must match exactly.
64+
65+
To trigger download mode from the host, adjust `PORT` and baud if configured differently.
66+
DTR must be asserted while the magic baud is set, then cleared to fire the reset.
67+
`pyserial` asserts DTR on open by default, so closing the port is enough:
68+
69+
```bash
70+
python3 -c "import serial; s=serial.Serial('PORT', 1200); s.close()"
71+
```
72+
73+
After the bridge reboots, flash over USB:
74+
75+
```bash
76+
idf.py -p PORT flash
77+
```
78+
79+
The feature requires `EFUSE_DIS_FORCE_DOWNLOAD` and `EFUSE_DIS_DOWNLOAD_MODE` not to be burned (default on development chips).
80+
5581
## JTAG Bridge
5682

5783
The ESP USB Bridge provides a JTAG device. The following command can be used to connect to an ESP32 target MCU.

main/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ idf_component_register(SRCS "main.c"
1111
"msc.c"
1212
"eub_vendord.c"
1313
"serial_bridge.c"
14+
"usb_phy.c"
1415
REQUIRES ${dependencies}
1516
LDFRAGMENTS "noflash.lf"
1617
INCLUDE_DIRS "." "public_include")

main/Kconfig.projbuild

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,16 @@ menu "Bridge Configuration"
6969
help
7070
Volume label shown in the MSC disc. Max length is 11 ASCII characters.
7171

72+
config BRIDGE_DOWNLOAD_MAGIC_BAUD
73+
int "Magic baud rate to enter download mode"
74+
default 1200
75+
help
76+
When the host sets the CDC serial port to this baud rate, download
77+
mode reset is armed. Closing the port (clearing DTR) then resets the
78+
bridge chip (ESP32-S2 or ESP32-S3) into download mode so its firmware
79+
can be updated over USB. Do not use this baud for normal target
80+
serial traffic; any DTR clear while armed triggers bridge download
81+
mode. Prefer a baud that host tools will not use for the target.
82+
Set to 0 to disable.
83+
7284
endmenu

main/main.c

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD
2+
* SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
@@ -17,10 +17,10 @@
1717
#include "driver/gpio.h"
1818
#include "sdkconfig.h"
1919
#include "esp_mac.h"
20-
#include "esp_private/usb_phy.h"
2120
#include "eub_vendord.h"
2221
#include "debug_probe.h"
2322
#include "usb_defs.h"
23+
#include "usb_phy.h"
2424
#include "led_io.h"
2525

2626
static const char *TAG = "bridge_main";
@@ -229,27 +229,13 @@ static void init_led_gpios(void)
229229
ESP_LOGI(TAG, "LED GPIO init done");
230230
}
231231

232-
static void int_usb_phy(void)
233-
{
234-
usb_phy_config_t phy_config = {
235-
.controller = USB_PHY_CTRL_OTG,
236-
.target = USB_PHY_TARGET_INT,
237-
.otg_mode = USB_OTG_MODE_DEVICE,
238-
.otg_speed = USB_PHY_SPEED_FULL,
239-
.ext_io_conf = NULL,
240-
.otg_io_conf = NULL,
241-
};
242-
usb_phy_handle_t phy_handle;
243-
usb_new_phy(&phy_config, &phy_handle);
244-
}
245-
246232
void app_main(void)
247233
{
248234
init_led_gpios(); // Keep this at the beginning. LEDs are used for error reporting.
249235

250236
init_serial_no();
251237

252-
int_usb_phy();
238+
ESP_ERROR_CHECK(eub_usb_phy_init());
253239

254240
ESP_ERROR_CHECK(serial_handler_init(TRANSPORT_TYPE_UART));
255241
serial_handler_register_tx_activity_callback(serial_tx_activity_callback);

main/serial_bridge.c

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD
2+
* SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
@@ -18,17 +18,24 @@
1818
#include "freertos/ringbuf.h"
1919
#include "freertos/semphr.h"
2020
#include "esp_timer.h"
21+
#include "esp_system.h"
22+
#include "sdkconfig.h"
2123
#include "util.h"
2224
#include "debug_probe.h"
25+
#include "usb_phy.h"
26+
#include "soc/rtc_cntl_reg.h"
2327

2428
#define USB_SEND_RINGBUFFER_SIZE (2 * 1024)
29+
/* tud_cdc_n_get_line_state(): bit 0 = DTR, bit 1 = RTS */
30+
#define CDC_LINE_STATE_DTR (1u << 0)
2531

2632
static const char *TAG = "serial_bridge";
2733

2834
static RingbufHandle_t usb_sendbuf;
2935
static SemaphoreHandle_t usb_tx_requested = NULL;
3036
static SemaphoreHandle_t usb_tx_done = NULL;
3137
static esp_timer_handle_t state_change_timer;
38+
static bool download_mode_armed;
3239

3340
// Transport data received callback - called by serial handler when data arrives
3441
static void transport_data_received_callback(const uint8_t *data, size_t len)
@@ -121,8 +128,22 @@ void tud_cdc_rx_cb(const uint8_t itf)
121128
}
122129
}
123130

131+
static void enter_download_mode(void);
132+
124133
void tud_cdc_line_coding_cb(const uint8_t itf, cdc_line_coding_t const *p_line_coding)
125134
{
135+
if (CONFIG_BRIDGE_DOWNLOAD_MAGIC_BAUD > 0 && p_line_coding->bit_rate == CONFIG_BRIDGE_DOWNLOAD_MAGIC_BAUD) {
136+
ESP_LOGI(TAG, "Magic baud %" PRIu32 " detected, arming download mode reset",
137+
p_line_coding->bit_rate);
138+
download_mode_armed = true;
139+
// Fire immediately if the host already cleared DTR (e.g. baud change on a closed port).
140+
if (!(tud_cdc_n_get_line_state(itf) & CDC_LINE_STATE_DTR)) {
141+
enter_download_mode();
142+
}
143+
return;
144+
}
145+
146+
download_mode_armed = false;
126147
if (serial_handler_set_baudrate(p_line_coding->bit_rate) != ESP_OK) {
127148
ESP_LOGE(TAG, "Could not set the baudrate to %" PRIu32, p_line_coding->bit_rate);
128149
eub_abort();
@@ -131,6 +152,12 @@ void tud_cdc_line_coding_cb(const uint8_t itf, cdc_line_coding_t const *p_line_c
131152

132153
void tud_cdc_line_state_cb(const uint8_t itf, const bool dtr, const bool rts)
133154
{
155+
// Magic baud arms download mode; clearing DTR (typically on port close) fires it.
156+
if (download_mode_armed && !dtr) {
157+
enter_download_mode();
158+
return;
159+
}
160+
134161
// The following transformation of DTR & RTS signals to BOOT & RST is done based on auto reset circutry shown in
135162
// schematics of ESP boards.
136163

@@ -182,6 +209,20 @@ static void state_change_timer_cb(void *arg)
182209
serial_handler_set_boot_reset_pins(true, true); // BOOT=1, RST=1 (not in reset)
183210
}
184211

212+
static void enter_download_mode(void)
213+
{
214+
if (CONFIG_BRIDGE_DOWNLOAD_MAGIC_BAUD <= 0) {
215+
return;
216+
}
217+
218+
ESP_LOGI(TAG, "Forcing download mode and resetting bridge chip");
219+
// Tear down USB before restart; avoid logging after this point (PHY/console go away).
220+
// Called from tusb_device_task (CDC callbacks); esp_restart() never returns to tud_task.
221+
eub_usb_phy_deinit();
222+
REG_SET_BIT(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
223+
esp_restart();
224+
}
225+
185226
static void init_state_change_timer(void)
186227
{
187228
const esp_timer_create_args_t timer_args = {

main/usb_phy.c

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include "usb_phy.h"
8+
#include "esp_private/usb_phy.h"
9+
#include "esp_rom_sys.h"
10+
#include "sdkconfig.h"
11+
#include "tusb.h"
12+
#if CONFIG_IDF_TARGET_ESP32S3
13+
#include "soc/rtc_cntl_reg.h"
14+
#include "soc/usb_serial_jtag_reg.h"
15+
#include "soc/usb_pins.h"
16+
#include "driver/gpio.h"
17+
#endif // CONFIG_IDF_TARGET_ESP32S3
18+
19+
static usb_phy_handle_t s_usb_phy;
20+
21+
esp_err_t eub_usb_phy_init(void)
22+
{
23+
const usb_phy_config_t phy_config = {
24+
.controller = USB_PHY_CTRL_OTG,
25+
.target = USB_PHY_TARGET_INT,
26+
.otg_mode = USB_OTG_MODE_DEVICE,
27+
.otg_speed = USB_PHY_SPEED_FULL,
28+
.ext_io_conf = NULL,
29+
.otg_io_conf = NULL,
30+
};
31+
return usb_new_phy(&phy_config, &s_usb_phy);
32+
}
33+
34+
void eub_usb_phy_deinit(void)
35+
{
36+
/*
37+
* Detach from the bus before tearing down the PHY. With MSC mounted, the host
38+
* keeps bulk transfers in flight; deleting the PHY / reconfiguring the pads
39+
* while those are active can hang the teardown path so download-mode entry
40+
* never completes until the volume is ejected.
41+
*/
42+
tud_disconnect();
43+
esp_rom_delay_us(10 * 1000);
44+
45+
if (s_usb_phy) {
46+
usb_del_phy(s_usb_phy);
47+
s_usb_phy = NULL;
48+
}
49+
50+
#if CONFIG_IDF_TARGET_ESP32S3
51+
// Switch to hardware CDC+JTAG:
52+
// Clear SW override and SW PHY sel bits so hardware (eFuse default) takes over,
53+
// which maps internal PHY to USB Serial/JTAG
54+
CLEAR_PERI_REG_MASK(RTC_CNTL_USB_CONF_REG,
55+
(RTC_CNTL_SW_HW_USB_PHY_SEL | RTC_CNTL_SW_USB_PHY_SEL | RTC_CNTL_USB_PAD_ENABLE));
56+
57+
// Do not use external PHY for Serial/JTAG
58+
CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_PHY_SEL);
59+
60+
// Release GPIO pins from USB Serial/JTAG pad
61+
CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_USB_PAD_ENABLE);
62+
63+
// Force host to see a USB disconnect (BUS_RESET) by pulling D+/D- LOW
64+
gpio_set_direction(USBPHY_DM_NUM, GPIO_MODE_OUTPUT_OD);
65+
gpio_set_direction(USBPHY_DP_NUM, GPIO_MODE_OUTPUT_OD);
66+
gpio_set_level(USBPHY_DM_NUM, 0);
67+
gpio_set_level(USBPHY_DP_NUM, 0);
68+
69+
SET_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_USB_PAD_ENABLE);
70+
#endif // CONFIG_IDF_TARGET_ESP32S3
71+
}

main/usb_phy.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#pragma once
8+
9+
#include "esp_err.h"
10+
11+
#ifdef __cplusplus
12+
extern "C" {
13+
#endif
14+
15+
/**
16+
* @brief Initialize the internal USB OTG PHY used by the USB device stack
17+
*
18+
* @return esp_err_t ESP_OK on success
19+
*/
20+
esp_err_t eub_usb_phy_init(void);
21+
22+
/**
23+
* @brief Tear down the USB OTG PHY before forcing download mode
24+
*
25+
* Disconnects from the bus and deletes the OTG PHY created by
26+
* eub_usb_phy_init(). On ESP32-S3, also reconfigures the USB pads so the
27+
* built-in USB Serial/JTAG controller can take over and the host sees a bus
28+
* disconnect. On ESP32-S2 there is no USB Serial/JTAG block; only disconnect
29+
* and PHY delete are performed.
30+
*/
31+
void eub_usb_phy_deinit(void);
32+
33+
#ifdef __cplusplus
34+
}
35+
#endif

0 commit comments

Comments
 (0)