V1.3 dual-MCU board

The V1.3 revision (Open_FC_V1_3 REV A) adds four RS-485 VEX Smart ports next to the four legacy Competition-Switch ports. Two of the Smart ports live on a second MCU, so the board carries two firmware images and is flashed in two steps.
Prototype hardware and bring-up firmware. The V1.3 images published on this site are diagnostic builds — they speak the bu_* bring-up protocol, not the certified match protocol, and are not certified for competition. The production serial protocol applies to hardware revision 1.1 boards.

Architecture

ESP32-S3 (host)RP2350 (Serial Expander)
PartESP32-S3-WROOM, 8 MB flashRP2354A, flash-in-package
Talks to the PCUSB CDC via CH340C, JSON lines at 115200— (no USB wired)
Legacy V5 ports4 × RJ11, driven in unison—
Smart portsA, B (RS-485, direct)C, D (RS-485, direct)
LinkSPI master, 1 MHz, mode 1SPI slave (PL022 on spi0)
Programmed byesptool over the CH340The ESP32, over on-board SWD

The PC only ever talks to the ESP32. Anything for ports C or D is proxied over the SPI link, and results come back tagged with the port letter so a host does not need to know which MCU owns a port. The field-control engine for C/D runs on the RP2350 itself; the ESP32 forwards the engage/state commands and polls status.

Inter-MCU pin map

SignalESP32-S3RP2350Notes
SPI CSIO10GPIO17Active low
SPI MOSIIO11GPIO16ESP32 → RP2350
SPI SCLKIO12GPIO18Through R38
SPI MISOIO13GPIO19RP2350 → ESP32
RP2350_IRQIO21GPIO8RP2350 has a result ready
RP2350_RUNIO6RUNActive-low reset, 10 K pull-up (R39)
BOOT_CTRLIO7GPIO20Spare GPIO — not BOOTSEL
ESP_READYIO40GPIO10ESP32 has booted
SWCLKIO42SWCLK33 Ω series (R58)
SWDIOIO41SWDIO33 Ω series (R59)
Smart portMCUTXRXDE
AESP32-S3IO17IO18IO4
BESP32-S3IO15IO16IO5
CRP2350GPIO0GPIO1GPIO2
DRP2350GPIO4GPIO5GPIO6

Legacy ports use IO47 (ENABLE/DISABLE) and IO48 (DRIVER/AUT), shared across all four RJ11s, with the same polarity as revision 1.1. The RP2350's QSPI_SS/BOOTSEL and USB pins are unconnected, so its USB mass-storage bootloader is unreachable: SWD from the ESP32 is the only programming path.

Flashing from the browser

Open the V1.3 flasher in Chrome or Edge, connect the board over USB, and run the two steps in order.

  1. ESP32-S3. esptool-js writes the merged image (bootloader + partition table + app) at 0x0, exactly as on the standard flasher. The board reboots into the new firmware and answers get_info with a bu_hello event. About 30 seconds.
  2. RP2350. The browser never touches SWD: it streams the image to the ESP32 as base64 JSON commands over the live serial link, and the ESP32 does the programming. The flasher releases RUN, probes SWD, erases, writes 384-byte chunks (each acknowledged before the next is sent), then reboots the RP2350 into the new image. About 10 seconds for a 53 KB image.

Afterwards, SPI ping on the flasher (or the RP2350 panel of the bring-up console) should report the RP2350 answering. Ports C and D do nothing until it does.

After any ESP32 reflash the RP2350 is held in reset (RUN low) until something releases it. The flasher and the CLI do this for you; if you drive the protocol yourself, send {"cmd":"rp2350_ctrl","run":true} before the SWD probe or it will report no DPIDR.

Step 2 is only enabled once the connected board has identified itself with bu_hello. A board still running production firmware ignores the rp2350_* commands, which is why the ESP32 must be flashed first.

Flashing from the command line

The same sequence is available as a Python tool in the repository (needs pyserial). Build both images with PlatformIO first.

firmware-bringup-v1_3/
cd esp32  && pio run -t upload          # ESP32-S3 over the CH340
cd ../rp2350 && pio run                  # produces .pio/build/rpipico2/firmware.bin
cd ..
py tools/bringup_cli.py --port COM3 flash-rp2350 rp2350/.pio/build/rpipico2/firmware.bin
py tools/bringup_cli.py --port COM3 cmd '{"cmd":"rp2350_spi","op":"ping"}'

tools/publish_firmware.py merges the ESP32 image, copies the RP2350 image into public/firmware/ and prints the v13_releases manifest entry the web flasher reads.

RP2350 flash commands

Four bring-up commands carry the RP2350 image. All replies are bu_flash_status events with a stage field; wait for each reply before sending the next command.

CommandReplyMeaning
{"cmd":"rp2350_swd_probe"}bu_swd_probeConnects and identifies. ok:true with dpidr:"4C013477" and bootrom_magic:"0402754D" means the core is alive. Safe at any time.
{"cmd":"rp2350_flash_begin","size":N}stage:"begin"Reset-halts core0, runs the bootrom's flash init, and erases N bytes rounded up to 4 KB from flash offset 0. The RP2350 stays halted until end.
{"cmd":"rp2350_flash_data","b64":"…"}stage:"data"Appends up to 384 raw bytes (base64-encoded). Larger chunks are truncated. received is the running byte count.
{"cmd":"rp2350_flash_end"}stage:"end"Programs the last partial page, flushes the XIP cache, and pulses RUN so the new image boots. received should equal the image size.

On failure ok is false and fail names the stage that failed (e.g. halt, reset_halt, no_session, bad_b64). The ESP32 bit-bangs SWD, staying under the ~250 kHz ceiling that applies while a blank RP2350 waits in its bootrom.

Reference sequence (TypeScript, lib/rp2350-flash.ts)
send({cmd:'rp2350_ctrl', run:true});            // out of reset
await reply('bu_swd_probe');                    // expect ok:true
send({cmd:'rp2350_flash_begin', size: image.length});
await reply('bu_flash_status', 'begin');
for (let off = 0; off < image.length; off += 384) {
  send({cmd:'rp2350_flash_data', b64: base64(image.subarray(off, off + 384))});
  await reply('bu_flash_status', 'data');       // ok:true, received
}
send({cmd:'rp2350_flash_end'});
await reply('bu_flash_status', 'end');          // ok:true, received === image.length

What is verified

SubsystemStatus
Legacy port polarityConfirmed against a live V5 (2026-09-01).
ESP32 → RP2350 flashing over SWDErase + write + boot of a 53 KB image in ~10 s, repeatable.
SPI link37/37 bytes per frame, valid checksums, zero re-inits after the mode-1 fix.
Smart ports C and DBoth held live sessions with VEX controllers in parallel for 45 s at ~50 acks/s, driven by the RP2350 and reported over SPI.
Smart ports A and BSame handshake engine, on the ESP32 directly.

Troubleshooting

SymptomLikely cause
Step 2 disabled, "has not identified itself"The board is not running V1.3 bring-up firmware, or the bu_hello was missed. Click Identify; if nothing arrives, run step 1.
SWD probe fails / no DPIDRRP2350 held in reset. Release RUN (the flasher does this automatically; the bring-up console has a button). If it still fails, check the 33 Ω series resistors on SWCLK/SWDIO.
fail:"reset_halt" at beginThe reset catch missed. Retry; the flasher pulses RUN itself at the start of every session.
SPI ping says no response after a successful flashGive the RP2350 a moment after boot, then ping again. If it persists, confirm both firmwares share the same openfc_spi_link.h and SPI mode 1.
Port C or D shows no bytes with a controller attachedThe controller has usually gone to sleep — power it back on. A controller also sulks after a disengage until power-cycled.