DEV Community

Cover image for Flashing STM32 Firmware Directly from VS Code over UART: An Open-Source Extension
poli
poli

Posted on

Flashing STM32 Firmware Directly from VS Code over UART: An Open-Source Extension

If you have ever bought one of those small STM32 boards with a USB-C port, there is a good chance that port goes to a USB-UART chip (often a CH340). It does not go to the MCU's native USB, and there is no ST-Link on the board. That is fine for serial logs. It also turns out to be enough for flashing, because every STM32 has a ROM bootloader that speaks UART.

I wanted that workflow inside my editor, so I built STM32 Serial Flasher, an open-source VS Code extension. You build your project, press Cmd+Alt+F (Ctrl+Alt+F on Windows and Linux), and it enters the bootloader, erases, writes, verifies and resets the board into the new firmware. No ST-Link and no separate flashing app.

vscode-panel

Source (MIT): https://github.com/totok22/SerialFlash
Marketplace: https://marketplace.visualstudio.com/items?itemName=totok22.serialflash-stm32

How UART flashing works

When BOOT0 is high at reset, an STM32 runs ST's system-memory bootloader. Which pins and which USART it uses depends on the part, so check ST's AN2606 for your chip.

The UART protocol is small, and it is documented in AN3155. The link runs at 8 data bits, even parity, 1 stop bit. The extension defaults to 115200 baud.

  • The host sends the sync byte 0x7F. The chip detects the baud rate and answers with ACK (0x79) or NACK (0x1F).
  • Every command is two bytes: the opcode and its complement (CMD, CMD ^ 0xFF).
  • Addresses are 4 bytes, big-endian, followed by an XOR checksum.

The commands the extension uses:

Opcode Command Used for
0x00 GET bootloader version and supported commands
0x02 GET ID product ID
0x43 / 0x44 ERASE / EXTENDED ERASE page or mass erase, depending on the chip family
0x31 WRITE MEMORY up to 256 bytes per packet
0x11 READ MEMORY read-back verification
0x21 GO start the application
0x92 READOUT UNPROTECT removing read protection (this erases the whole flash)

The auto-download circuit

By hand you hold BOOT0 high and tap reset. Auto-download circuits do the same thing with the bridge's DTR and RTS lines. The extension has presets for two common designs.

Classic CH340C circuit (dtr-low-rts-high, the default). Two transistors (one NPN, one PNP) and a few resistors turn DTR and RTS into reset and BOOT0. DTR drives the NPN transistor that pulls NRST low, and RTS drives BOOT0 through the PNP transistor. When DTR and RTS are in the same state, both transistors are off and the board's pull resistors decide the pin levels. When they differ, the circuit either pulses reset or raises BOOT0. The entry sequence the preset uses is: RTS low and DTR low, then DTR high, then about 800 ms of waiting, then the 0x7F sync byte.

ch340c-auto-download

Schematic: WCH official application note.

Direct CH340X circuit (ch340x). This one needs fewer external components. The CH340X has enhanced modem-control pins, so no transistors are needed. RTS# goes straight to NRST, and DTR# (the TNOW pin) goes to BOOT0 with a 4.7 kΩ pull-down that also configures the enhanced DTR function. The entry sequence is four steps with about a second of settling time before the sync byte:

(RTS, DTR) = (false, true) -> (true, true) -> (true, false) -> (true, true)
Enter fullscreen mode Exit fullscreen mode

ch340x-auto-download

Schematic: WCH official application note.

I have tested both presets on real boards, and flashing works with both circuits.

There is also a manual mode (none, hold BOOT0 and tap reset yourself), a generic reversed preset, and a custom mode where you map DTR and RTS states yourself for boards that do their own thing. Full schematics and timing are in the hardware notes.

Things that cost me time

Signal polarity. The code uses one convention everywhere: true means the pin is driven low, false means high. Node's serialport uses the opposite sense for these booleans.

The board does not run after flashing. If the serial port stays open, DTR and RTS can keep holding NRST low or BOOT0 high. This is a particular risk with the direct CH340X circuit. The extension closes the port when it is done by default.

Mass erase can be slow. Large devices can take tens of seconds to erase, so the erase step gets a 60 second timeout instead of the 2 second default.

Using it

Install it from the Marketplace or the terminal:

code --install-extension totok22.serialflash-stm32
Enter fullscreen mode Exit fullscreen mode

Then:

  1. Build your project so you have a .hex or .bin.
  2. Open the panel from the Activity Bar (or run SerialFlash: Open Flasher Panel) and pick the serial port, firmware and reset mode.
  3. Click Flash or use the shortcut, and watch the SerialFlash Output channel for the bootloader version, product ID, erase, write and verify steps.

HEX files are flashed at the base address stored in the file. BIN files go to 0x08000000 unless you set serialFlash.flashAddress, which you will need if a bootloader of your own occupies the start of flash.

A minimal workspace config:

{
  "serialFlash.firmware": "build/Debug/app.hex",
  "serialFlash.port": "/dev/tty.usbserial-10",
  "serialFlash.resetMode": "dtr-low-rts-high",
  "serialFlash.verifyAfterWrite": true
}
Enter fullscreen mode Exit fullscreen mode

It also contributes a task type, so "build and flash" can be a single task:

{
  "version": "2.0.0",
  "tasks": [
    { "label": "Build Firmware", "type": "shell", "command": "ninja -C build", "problemMatcher": [] },
    { "label": "SerialFlash: Flash Latest Firmware", "type": "serialFlash", "action": "flashLatest", "problemMatcher": [] },
    {
      "label": "Build & Flash",
      "dependsOrder": "sequence",
      "dependsOn": ["Build Firmware", "SerialFlash: Flash Latest Firmware"],
      "problemMatcher": []
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

If you work with several boards, SerialFlash: Create Project Profile saves named profiles (port, firmware, reset mode, address) that you can switch between from the Command Palette. Every command, setting and the troubleshooting table are in the usage reference.

Picking the firmware

A build folder usually holds more than one .hex or .bin: Debug and Release builds, an older bring-up variant, the same image in both formats. Taking the newest file is not always the right answer, so the extension does not do that. When no firmware path is configured, it scans the workspace and gives every candidate a score, then lists them best-first in a picker. Each entry shows the path, file size, score, format, firmware size, HEX base address and last-modified time, so you can tell the candidates apart before flashing. SerialFlash: Select Firmware opens the picker whenever you want it.

firmware-picker

The score combines how recently a file was built, its format, whether it sits in a build, debug or release folder, whether its name contains the workspace folder name, and whether it is the firmware you used last time. If one candidate clearly leads, the extension uses it without asking. Otherwise it opens the picker. A firmware path you set explicitly always wins over discovery.

Smaller things that help

  • Right-click to flash. Right-click a .hex or .bin in the Explorer or on its editor tab to flash it, verify the chip's flash against it, set it as the project firmware, or check its format, size and base address.
  • A useful port list. The port picker lists USB serial adapters first and shows the manufacturer, serial number and VID/PID of each one, which helps when more than one adapter is plugged in. The port you used last is marked "last used".
  • Hints when something fails. The panel suggests what to check for the usual failures: port busy, no permission (on Linux, the dialout group), no bootloader response, NACK, verify mismatch and a firmware file that cannot be parsed.
  • Run Diagnostics. One command prints the extension host platform, whether the native serialport module loaded, and every serial port that host can see. In Remote SSH, WSL and Dev Containers it also reminds you that the ports belong to the machine running the extension.

Limitations

  • It only works on parts and boards where a USART is available to the ROM bootloader and the USB port goes to a USB-UART bridge. It is not a replacement for SWD/ST-Link debugging, and it does not work on every STM32.
  • It needs VS Code Desktop 1.90 or newer on macOS, Windows or Linux. Browser-based editors such as vscode.dev cannot access serial ports. In Remote SSH, WSL and Dev Containers, the serial port belongs to the machine running the extension, and SerialFlash: Run Diagnostics shows what that host can see.
  • I have tested the CH340C and CH340X presets on real boards. Other USB-UART bridges and boards (CP2102, FT232 and others) are untested, and I would like hardware reports from them. If a preset does not fit your circuit, custom mode may cover it.

Feedback

If you try it, I would like to hear which board and USB-UART chip you used, and whether the bootloader entry worked first time. Issues and pull requests are welcome on GitHub.

Top comments (0)