wlink Documentation
This site is the documentation for wlink, the command-line flash and debug tool for WCH-Link probes and WCH RISC-V MCUs.
Its main part is a protocol reference for the WCH-LinkRV USB protocol — the
private protocol spoken between the host and the WCH-Link probe. The old
protocol.md at the repository root was written in 2023 and had stopped
tracking the implementation; this reference is rebuilt from the source code so
that the byte-level description and src/commands/ cannot silently diverge.
This is a reverse-engineered description, not an official WCH specification. WCH does not publish a protocol specification for the WCH-Link. Everything here comes from reading wlink’s implementation, from USB captures, and from other open-source implementations listed in External References. Entries marked
unverifiedare observations only.
Where to start
| If you want to… | Read |
|---|---|
| Understand the wire format | USB Transport, then Frame Format |
| Implement a command | Command Reference and Control Subcommands |
| Understand how a flash operation is sequenced | Operation Flows |
| Work with the RISC-V debug module | DMI and the Debug Module |
| Look up an ID, flag or constant | Constants and Chip Families |
| Find unimplemented or uncertain commands | Unverified Commands and Captures |
| Wire up a specific MCU | The Chip Notes section in the sidebar |
Source of truth
Protocol knowledge is derived from these files in the repository. When the documentation and the code disagree, the code wins — please open a PR that corrects the documentation.
| File | What it defines |
|---|---|
src/commands/mod.rs | Frame construction and parsing, command IDs, payload layouts |
src/commands/control.rs | The complete 0x0d control subcommand set, firmware version encoding |
src/probe.rs | USB IDs, endpoints, raw send/receive, mode switching |
src/operations.rs | Command sequences for attach, erase, program, read, reset |
src/chips.rs | Chip family IDs and the ChipID-to-part-name database |
src/flash_op.rs | Per-family flash algorithm blobs sent over the data endpoint |
src/dmi.rs, src/regs.rs | DMI operations and RISC-V debug register addresses |
Building this site locally
The site is built with mdBook:
cargo install mdbook --locked
mdbook build # output in book/
mdbook serve # live preview at http://localhost:3000
The rendered site is published from the gh-pages branch; see
.github/workflows/docs.yml. The intended public URL is
https://ch32-rs.github.io/wlink/.
Overview and Status
The WCH-Link probe exposes a vendor-specific USB bulk protocol. There is no public specification from WCH; this reference documents the protocol as implemented by wlink, which was reconstructed from USB captures, WCH’s toolchain, and other open-source implementations.
Status of this document
This reference replaces the repository-root protocol.md. That file was last
updated in November 2023 and had fallen behind the implementation — it had an
empty 0x01 section, two empty DMI tables, a dozen ??? markers, listed
0x0c as unknown (it is SetSpeed), omitted 0x11 entirely, listed only 7
chip families (there are now 28), and stopped at firmware v2.8.
The tables in this reference are derived from the current source on main.
Every command page names the Rust symbol that implements it. Anything that
could not be confirmed from the implementation is collected in
Unverified Commands and Captures and marked unverified
rather than guessed.
If you find a mismatch between this document and the code, the code is correct and the document needs a PR.
Notation
| Convention | Meaning |
|---|---|
0x81 0d 01 01 | Hex bytes in transmission order |
u32_be | 32-bit unsigned integer, most significant byte first |
LEN | Payload byte count, one byte |
() | Command succeeds with no payload of interest |
unverified | Seen in a USB capture or an older note, not implemented/confirmed in wlink |
riscvchip | The one-byte chip family code, see Constants |
All multi-byte integers in command payloads are big-endian. Memory read data from the data endpoint is an exception: wlink reverses every 4-byte word after reading it (see Operation Flows).
Layer model
Host (wlink CLI)
│
├─ Command endpoint (0x01 OUT / 0x81 IN)
│ 81 CMD LEN PAYLOAD → 82 CMD LEN PAYLOAD
│ all control, configuration and DMI operations
│
├─ Data endpoint (0x02 OUT / 0x82 IN)
│ unframed bulk stream: flash algorithm blobs, firmware bytes, memory dumps
│
└─ DMI operations (command 0x08)
└─ RISC-V Debug Module registers (0x04 … 0x40)
- USB Transport describes the device, interfaces and endpoints.
- Frame Format describes the command frames.
- Command Reference lists every command ID and payload.
- Control Subcommands expands the multi-purpose
0x0dcommand. - DMI and the Debug Module covers the debug layer above command
0x08.
Known limitations of the protocol itself
LENis a single byte. wlink reads responses into a 64-byte buffer, so a response payload is limited to 61 bytes in practice; large transfers always go through the data endpoint.- The response parser does not check that the response
CMDbyte matches the request; it only distinguishes the0x82success marker from the0x81error marker. - Error frames carry an opaque one-byte
REASON. Only0x55(failed to connect to the target) has a known meaning. - Some subcommands were observed only in captures from WCH’s Windows utility;
they are listed separately as
unverified.
Further reading
- External References — WCH manuals, schematics, firmware archives and other FOSS implementations.
CHANGELOG.mdin the repository records protocol-level fixes over time, for example the correction that0x0cis a speed command rather than a verify command.
USB Transport
WCH-Link uses USB bulk transfers. There is no kernel driver in the loop on
Linux/macOS: wlink opens the device with nusb, claims interface 0, and reads
and writes bulk endpoints directly. On 32-bit Windows an optional native
WCHLinkDLL.dll (CH375) backend is available, falling back to nusb.
Source: src/probe.rs
(constants), src/usb_device.rs
(backend implementations).
USB identifiers
| Mode | VID | PID | Notes |
|---|---|---|---|
| RV (RISC-V debug/flash) | 0x1a86 | 0x8010 | The mode wlink operates in |
| DAP (ARM SWD) | 0x1a86 | 0x8012 | wlink refuses to flash in this mode |
The same physical probe appears under one PID or the other depending on its
mode. wlink mode-switch flips between them.
Endpoints
| Endpoint | Direction | Purpose |
|---|---|---|
0x01 | OUT | Command frames (81 CMD LEN PAYLOAD) |
0x81 | IN | Command responses (82 … / 81 …) |
0x02 | OUT | Data endpoint: flash algorithm blob, firmware chunks |
0x82 | IN | Data endpoint: memory dump, per-chunk write acknowledgement |
0x02 | OUT (DAP mode only) | Raw mode-switch frame |
Command reads use a 64-byte buffer; a single bulk read is treated as one
response frame. Data-endpoint transfers are streams, not frames: wlink loops
over 64-byte bulk reads until the requested byte count is reached, and pads the
final written chunk with 0xff to the family’s data packet size.
The nusb backend stores a 5-second default in NusbDevice::timeout; the
current bulk read/write calls do not apply it and rely on nusb’s own blocking
behaviour.
Selecting a probe
With several probes attached, enumeration order is not stable. wlink accepts either an index or a USB serial number:
wlink list
# <WCH-Link#0 nusb device> ID 1a86:8010 Serial F93D8F067E10 (USB-FS 12 Mbps) (RV mode)
# <WCH-Link#1 nusb device> ID 1a86:8010 Serial D7708F065C7F (USB-FS 12 Mbps) (RV mode)
wlink -d 1a86:8010:D7708F065C7F status # probe-rs style VID:PID:SERIAL
wlink -d D7708F065C7F status # serial alone
wlink -d 1 status # legacy index
Serial matching is case-insensitive and works in either RV or DAP mode; the VID/PID in the selector are validated but not used for matching.
Mode switching
Switching from RV to DAP mode sends a command frame on the normal command endpoint:
| Direction | Raw bytes | Meaning |
|---|---|---|
| RV → DAP | 81 ff 01 41 | RawCommand<0xff> with payload 0x41 |
| DAP → RV | 81 ff 01 52 | written raw to endpoint 0x02 while in DAP mode |
Both operations are experimental; the CH549-based first-generation probe can
only switch modes by reflashing its firmware. Support is reported by
WchLinkVariant::support_switch_mode in src/probe.rs.
Variants
| Code | Variant | Hardware | Capabilities |
|---|---|---|---|
0x01 | WchLinkVariant::Ch549 | WCH-Link (CH549) | No CH32V00X, no mode switch, no power/SDI functions |
0x02 / 0x12 | WchLinkVariant::ECh32v305 | WCH-LinkE (CH32V305) | Full support, including SDI print |
0x03 | WchLinkVariant::SCh32v203 | WCH-LinkS (CH32V203) | Full chip support |
0x05 / 0x85 | WchLinkVariant::WCh32v208 | WCH-LinkW (CH32V208) | Wireless variant; no CH56X/CH57X/CH582/CH59X |
| other | — | — | Error::UnknownLinkVariant |
See Probe Info and Firmware Version for how the variant byte is returned and how firmware versions are decoded.
Frame Format
Every command exchange uses the command endpoints (0x01 OUT / 0x81 IN).
The data endpoints are a separate, unframed stream.
Source: Command::to_raw and Response::from_raw in
src/commands/mod.rs,
and WchLink::send_command in
src/probe.rs.
Request
| Byte 0 | Byte 1 | Byte 2 | Bytes 3 … n |
|---|---|---|---|
0x81 | CMD | LEN | PAYLOAD |
#![allow(unused)]
fn main() {
let mut bytes = vec![0x81, Self::COMMAND_ID, 0x00];
bytes.extend(self.payload());
bytes[2] = bytes.len() as u8 - 3; // LEN = PAYLOAD.len()
}
LEN is a single byte holding the payload length, so the format allows up to
255 payload bytes. In practice wlink reads responses into a 64-byte buffer,
which caps a response payload at 61 bytes; all implemented commands use far
smaller payloads.
Example — GetProbeInfo (0x0d, subcommand 0x01):
81 0d 01 01
^^ ^^ ^^ ^^
| | | └─ PAYLOAD[0] = 0x01 (GetProbeInfo)
| | └──── LEN = 1
| └─────── CMD = 0x0d (Control)
└────────── request marker
Success response
| Byte 0 | Byte 1 | Byte 2 | Bytes 3 … n |
|---|---|---|---|
0x82 | CMD | LEN | PAYLOAD |
The parser checks resp[0] == 0x82, verifies LEN == resp[3..].len(), then
hands resp[3..] to the response type. It does not verify that CMD
matches the request.
Example — GetProbeInfo responding with firmware 2.15 on a WCH-LinkE:
82 0d 03 02 0f 02
^^^^^^^^^^ PAYLOAD = [0x02, 0x0f, 0x02]
Error response
| Byte 0 | Byte 1 | Byte 2 | Bytes 3 … n |
|---|---|---|---|
0x81 | REASON | LEN | PAYLOAD |
An 0x81 first byte means failure regardless of CMD. wlink validates the
length and surfaces Error::Protocol(reason, response_bytes).
REASON | Meaning |
|---|---|
0x55 | Failed to connect to the target RISC-V chip |
| other | Opaque; the full frame is preserved in the error |
Response decoding by type
| Rust type | Rule |
|---|---|
() | Payload ignored, success is enough |
u8 | Payload must be exactly 1 byte |
bool | Payload must be 1 byte; 0x01 means success (SetSpeed) |
Vec<u8> | Raw payload returned |
AttachChipResponse | Exactly 5 bytes: family u8 + ChipID u32_be |
DmiOpResponse | Exactly 6 bytes: addr u8 + data u32_be + op u8 |
ProbeInfo | At least 3 bytes: major, minor, optional variant |
ESignature | Non-standard 20-byte raw response, see Chip Info |
| invalid marker | Error::InvalidPayload |
| length mismatch | Error::InvalidPayloadLength |
Data endpoint stream
The data endpoints carry no 81/82 header. Two shapes occur:
- Bulk output: the flash algorithm blob for the chip family, then firmware
data split into
write_pack_sizechunks, each padded with0xfftodata_packet_size(seeWchLink::write_data_with_progress). - Bulk input:
read_data(n)accumulates 64-byte bulk reads untilnbytes are collected. During a fast-program cycle, each written chunk is followed by a 4-byte acknowledgement on0x82; wlink checks that the last byte is0x04(observed41 01 01 04).
ProbeSession::read_memory reverses each 4-byte word of the returned memory
buffer to compensate for the probe’s word order.
Command Reference
CMD is byte 1 of a request frame (see Frame Format). All
payload integers are big-endian. Source links point at
src/commands/mod.rs
unless noted otherwise; 0x0d subcommands live in
src/commands/control.rs
and are expanded in Control Subcommands.
Command ID summary
CMD | Name | Payload | Response | Status |
|---|---|---|---|---|
0x01 | SetWriteMemoryRegion | 8 bytes: address, length | () | implemented |
0x02 | Program | 1 byte: subcommand | u8 | implemented |
0x03 | SetReadMemoryRegion | 8 bytes: address, length | () | implemented |
0x06 | ConfigChip | 1–8 bytes: protection/config | u8 | partially implemented |
0x08 | DmiOp | 6 bytes | 6 bytes | implemented |
0x0b | Reset | 1 byte | () | implemented |
0x0c | SetSpeed | 2 bytes: riscvchip, speed | bool | implemented |
0x0d | Control | subcommand + args | varies | implemented — see Control Subcommands |
0x0e | DisableDebug | 1 byte 0x01 | () | implemented |
0x0f | Get device mode | 1 byte | unknown | unverified |
0x11 | GetChipInfo (ESIG) | 1 byte: 0x09 / 0x06 | 20 raw bytes | implemented |
0xfe | Disable encryption | 1 byte | unknown | unverified |
0xff | Mode switch | 1 byte 0x41 | — | implemented (RV → DAP) |
The old protocol.md listed 0x0c as ??? and 0x0f as “begin verify”.
Both are wrong: 0x0c is SetSpeed (corrected in CHANGELOG for v0.0.6), and
0x0f only appears in a GetDeviceMode capture.
0x01 — SetWriteMemoryRegion
Sets the target address and byte count for the following write operation.
| Offset | Type | Field |
|---|---|---|
| 0 | u32_be | start_addr (typically 0x08000000 code flash or 0x00000000 SRAM) |
| 4 | u32_be | len (firmware length in bytes) |
Request: 81 01 08 <addr:4> <len:4>. Response: ().
Source: SetWriteMemoryRegion. Used at the start of ProbeSession::write_flash.
0x02 — Program
One-byte subcommand selector. Response is a single status byte; wlink checks
the returned value for the 0x07 step during flashing.
| Sub | Rust variant | Meaning |
|---|---|---|
0x01 | EraseFlash | Erase code flash |
0x02 | WriteFlash | Begin fast-program (bulk data follows on the data endpoint) |
0x04 | WriteFlashAndVerify | Write flash with verification |
0x05 | WriteFlashOP | Begin writing the per-family flash algorithm blob |
0x06 | Prepare | Prepare before setting the RAM address (not used by current flows) |
0x07 | Unknown07AfterFlashOPWritten | Commit step after the flash blob was written; response must be 0x07 |
0x08 | End | End program |
0x0b | Unknown0BAfterFlashOPWritten | Alternative commit step for riscvchip == 1 |
0x0c | ReadMemory | Start a memory read on the data endpoint |
Older notes listed 0x03 “execute ram”, 0x05 “begin bulk transfer”, 0x07
“verify”, 0x09 “bulk transfer ends”, 0x0a “verify” and 0x0b “= verify for
riscvchip 1”. Those labels are unverified; see
Unverified Commands.
0x03 — SetReadMemoryRegion
Identical layout to 0x01: start_addr and len, both u32_be. Response ().
Followed by 0x02 / 0x0c (ReadMemory) and a bulk read on endpoint 0x82.
Source: SetReadMemoryRegion, ProbeSession::read_memory.
0x06 — ConfigChip
Flash read/write protection and option-byte configuration. Response is a status
u8 for the query variants.
| PAYLOAD[0] | Rust variant | Payload | Meaning / response |
|---|---|---|---|
0x01 | CheckReadProtect | 1 byte | 0x01 protected, 0x02 unprotected |
0x02 | Unprotect | 1 byte | Remove read protection (mass-erases option bytes) |
0x03 | Protect | 1 byte | Set read protection |
0x04 | CheckReadProtectEx | 1 byte | 0x11 write-protected, 0x00 unprotected |
0x02 + arg | UnprotectEx(b) | 8 bytes: 02 b ff ff ff ff ff ff | Extended unprotect; b is 0xff or 0xbf |
0x03 + arg | ProtectEx(b) | 8 bytes: 03 b ff ff ff ff ff ff | Extended protect |
| — | Config { data, wrp } | 8 bytes | Unimplemented in wlink (todo!()); capture: 81 06 08 02 3f 00 00 ff ff ff ff |
Flags (ConfigChip associated constants): FLAG_READ_PROTECTED = 0x01,
FLAG_READ_UNPROTECTED = 0x02, FLAG_WRITE_PROTECTED = 0x11,
FLAG_WRITE_UNPROTECTED = 0x00.
Changing protection requires a fresh
0x0d / 0x02attach afterwards. wlink skipsUnprotectwhen flash is already unprotected, because the probe firmware mass-erases the option-byte page (USER/Data/WRPR).
0x08 — DmiOp
Six-byte payload, six-byte response. See DMI and the Debug Module for register addresses and status handling.
| Offset | Size | Request field | Response field |
|---|---|---|---|
| 0 | 1 | addr (DMI register) | addr |
| 1–4 | 4 | data (u32_be) | data (u32_be) |
| 5 | 1 | op: 0 NOP, 1 READ, 2 WRITE | op: 0x00 success, 0x02 failed, 0x03 busy |
Example — read DMSTATUS (0x11): 81 08 06 11 00 00 00 00 01.
0x0b — Reset
| PAYLOAD[0] | Rust variant | Meaning |
|---|---|---|
0x01 | Reset::Soft | Quit reset / reset and run (most common); wlink waits ~300 ms afterwards |
0x02 | Reset::Chip | Chip reset (memory is not reset) |
0x03 | Reset::Normal | Normal reset |
Response: (). Source: Reset, ProbeSession::soft_reset.
0x0c — SetSpeed
| Offset | Field | Values |
|---|---|---|
| 0 | riscvchip | Chip family code, see Constants |
| 1 | speed | 0x01 High (6000 kHz, default), 0x02 Medium (4000 kHz), 0x03 Low (400 kHz) |
Response payload 0x01 means success (Response for bool). wlink sends this
before attaching and again after auto-detection. Source: SetSpeed, Speed.
0x0d — Control
The multi-purpose control command. Its first payload byte selects a subcommand; the remainder depends on it. See Control Subcommands for the complete table.
Quick reference: 0x01 probe info, 0x02 attach chip, 0x04/0x05 ROM/RAM
split, 0x08 erase by RST pin, 0x09–0x0c power output, 0x0e RST pin,
0x0f erase by power-off, 0xee SDI print, 0xff detach.
0x0e — DisableDebug
Payload 0x01, response (). Only used for specific riscvchip families
(older notes mention families 2 and 3). Frame: 81 0e 01 01.
Source: DisableDebug.
0x11 — GetChipInfo / ESignature
| PAYLOAD[0] | Rust variant | Notes |
|---|---|---|
0x09 | GetChipInfo::V1 | Used with firmware older than v2.9 |
0x06 | GetChipInfo::V2 | Introduced in firmware v2.9 |
This command does not use the standard response frame — see
Chip Info and ESIG for the raw 20-byte layout. Source:
GetChipInfo, ESignature.
0xff — Mode switch
RawCommand<0xff> with payload 0x41 (81 ff 01 41) switches an RV-mode
probe into DAP mode. The reverse direction writes 81 ff 01 52 raw to endpoint
0x02 while the probe is in DAP mode. See USB Transport.
Control Subcommands (0x0d)
0x0d is a multiplexed command: PAYLOAD[0] selects an operation and the
remaining bytes are its arguments. Its Rust implementation is
src/commands/control.rs.
Frame shape: 81 0d LEN SUB [ARGS…].
Complete subcommand table
SUB | Rust symbol | Request payload | Response | Notes |
|---|---|---|---|---|
0x01 | GetProbeInfo | 01 | ≥3 bytes | Firmware version and variant; see Probe Info |
0x02 | AttachChip | 02 | 5 bytes | Auto-detect chip family + ChipID; see Chip Info |
0x03 | — | — | — | unverified, seen in older notes as a post-attach stage for riscvchip 1 |
0x04 | GetChipRomRamSplit | 04 | u8 | SRAM/CODE split mode 0..3; CH32V2/V3, CH56X |
0x05 | SetChipRomRamSplit(mode) | 05 mode | () | Set split mode 0..3 |
0x06 | — | 06 | — | unverified, capture labels it CheckQE |
0x07 | — | 07 | — | unverified, capture labels it EnableQE |
0x08 | EraseCodeFlash::ByPinRST | 08 riscvchip | () | Erase via the nRST line; requires an RST pin connection |
0x09 | SetPower::Enable3v3 | 09 | () | Enable 3.3 V output |
0x0a | SetPower::Disable3v3 | 0a | () | Disable 3.3 V output |
0x0b | SetPower::Enable5v | 0b | () | Enable 5 V output |
0x0c | SetPower::Disable5v | 0c | () | Disable 5 V output |
0x0e | SetRSTPin | 0e 13 / 0e 14 / 0e 15 | () | Drive RST low / high / floating |
0x0f | EraseCodeFlash::ByPowerOff | 0f riscvchip | () | Power-cycle erase; see the length caveat below |
0x11 | — | 11 … | — | unverified, capture labels it SetAccessAddress |
0xee | SetSdiPrintEnabled | ee 00 (on) / ee 01 (off) | u8 | 0x00 success, 0xff unsupported |
0xff | OptEnd | ff | () | Detach / end the session |
0x09–0x0c require a probe with power functions (WchLinkVariant::ECh32v305
or WCh32v208). 0xee requires WCH-LinkE with firmware ≥ 2.10.
Length caveat for 0x0f
The captured frame from WCH’s utility is:
81 0d 01 0f 09
^^ ^^ ^^ ^^
│ │ │ └─ chip family (0x09 = CH32V003)
│ │ └──── SUB = 0x0f
│ └─────── LEN = 1 ← inconsistent: two payload bytes follow
└────────── CMD = 0x0d
LEN is wrong in the capture. wlink assembles the frame from its own
to_raw, producing 81 0d 02 0f 09 with the correct length. Treat the
captured length byte as a firmware quirk, not as the protocol rule.
Special-erase operations
0x08 and 0x0f bypass chip attach entirely: the probe performs the erase
without a debug connection, which is useful when read protection or a broken
auto-detect prevents attaching. Because no detection happens, the caller must
supply the chip family:
wlink erase --method power-off --chip CH32H41X
wlink erase --method pin-rst --chip CH32V003
The riscvchip argument is the one-byte family code from
Constants.
SDI print
0xee enables the probe’s virtual serial port used by WCH’s SDI print
feature. Captures also show 81 0d 02 ee 00 / 02 / 03 labelled
SetSDLineMode; this three-value form is unverified and not implemented by
wlink. wlink only uses ee 00 (enable) and ee 01 (disable).
Session lifecycle
A typical session opens with 0x01 (probe info), then 0x02 (attach), runs
operations, and ends with 0xff (OptEnd). wlink sends OptEnd on detach
unless --no-detach is given; commands that leave the MCU halted deliberately
skip detach so the halt is preserved.
Probe Info and Firmware Version
Source: GetProbeInfo, ProbeInfo and the version helpers in
src/commands/control.rs;
WchLinkVariant in
src/probe.rs.
0x0d / 0x01 — GetProbeInfo
Request: 81 0d 01 01.
Response payload (ProbeInfo::from_payload requires at least 3 bytes):
| Offset | Field | Notes |
|---|---|---|
| 0 | major_version | Raw major byte as reported by the probe |
| 1 | minor_version | Raw minor byte (may exceed 15 — see below) |
| 2 | variant | Only interpreted when the payload is 4 bytes long; otherwise wlink assumes Ch549 |
| 3 | — | Present in the 4-byte form; meaning unverified |
Example: 82 0d 04 02 0f 02 00 → major 0x02, minor 0x0f, variant
0x02 (WCH-LinkE-CH32V305), with a fourth payload byte of unknown meaning.
A 3-byte payload carries no variant and makes wlink assume Ch549.
Firmware version encoding
The probe does not report a decimal version. Its two bytes are a packed
value, and WCH’s tooling displays versions as major * 10 + minor. This is why
README says “firmware version 2.15 is actually v35”.
For raw bytes (major, minor):
packed = 16 * major + minor
version_code = if packed < 0x30 { 10 * major + minor } else { packed - 12 }
display = (packed >> 4, packed & 0x0f)
version_code is monotonic and is what wlink uses for all comparisons.
display_version reconstructs the human-readable version by treating the
packed value as two nibbles, so an “impossible” minor carries into the major.
| Raw bytes | packed | version_code | Displayed as | Note |
|---|---|---|---|---|
02 08 | 0x28 | 28 | 2.8 | Older firmware |
02 0f | 0x2f | 35 | 2.15 (v35) | Current baseline in README |
02 16 | 0x36 | 42 | 3.6 | wlink source spells newer builds as 2.22 |
02 0a | 0x2a | 30 | 2.10 (v30) | First firmware with SDI print |
Minimum firmware versions used by wlink
| Constant | Value | Purpose |
|---|---|---|
MIN_FW_VERSION_CHIP_INFO_V2 | 29 (v2.9) | Use GetChipInfo::V2 (0x11 / 0x06) instead of V1 |
MIN_FW_VERSION_CH32V205 | 42 (v3.6, sometimes written 2.22) | First firmware that reports the new-generation CH32V205 family (0xce) correctly |
Older firmware misdetects CH32V205 as CH32V20X with garbage ChipID/ESIG; wlink emits a warning but continues.
Variant codes and capabilities
WchLinkVariant::try_from_u8 accepts:
| Byte | Variant |
|---|---|
0x01 | Ch549 |
0x02, 0x12 | ECh32v305 |
0x03 | SCh32v203 |
0x05, 0x85 | WCh32v208 |
| other | Error::UnknownLinkVariant |
| Variant | Mode switch | Power output | SDI print | Chip restrictions |
|---|---|---|---|---|
Ch549 | no | no | no | No CH32V003 / CH32X035 / CH643 |
ECh32v305 | yes | yes | yes | none |
SCh32v203 | yes | no | no | none |
WCh32v208 | yes | yes | no | No CH56X / CH57X / CH582 / CH59X |
Hardware variants (historical table)
The old protocol.md listed these; they are retained here for orientation but
have not been re-verified against current hardware.
| Probe MCU | Variant | Note |
|---|---|---|
| CH549 | WCH-Link-R1-1v1 | Switch mode by reflashing firmware |
| CH32V305F | WCH-LinkE-R0-1v3 | Switch mode by button or EEPROM setting |
| CH32V203 | WCH-LinkS-CH32V203 | — |
unverified | WCH-LinkB | Hardware MCU unknown |
See USB Transport for VID/PID and mode-switch frames.
Chip Info and ESIG
Two commands identify the attached MCU: 0x0d / 0x02 (attach) returns the
chip family and ChipID, while 0x11 returns the “ESIG” block with flash size,
UID and a second ChipID field.
Sources: AttachChipResponse in
src/commands/control.rs,
GetChipInfo / ESignature in
src/commands/mod.rs,
and src/chips.rs
for the ChipID database.
0x0d / 0x02 — AttachChip
Request: 81 0d 01 02.
Response payload must be exactly 5 bytes:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | riscvchip — chip family code, decoded via RiscvChip::try_from_u8 |
| 1–4 | 4 | chip_id (u32_be), 0 when not reported |
Example: 82 0d 05 05 20 36 05 10 → family 0x05 (CH32V20X), ChipID
0x20360510.
ProbeSession::attach retries this command up to 3 times with a 100 ms delay
because a freshly powered target can fail the first attach. When --chip is
given, the returned family must match or wlink raises ChipMismatch.
The ChipID-to-part-name table lives in src/chips.rs
(chip_id_to_chip_name); the per-family tables in the Chip Notes section of
this site list the known part numbers.
0x11 — GetChipInfo / ESignature
| PAYLOAD[0] | Variant | When used |
|---|---|---|
0x09 | GetChipInfo::V1 | Firmware older than v2.9 (version_code < 29) |
0x06 | GetChipInfo::V2 | Firmware v2.9 and newer |
This command does not follow the standard
82 CMD LEN PAYLOADresponse format.ESignatureoverridesResponse::from_rawand reads a raw 20-byte buffer directly, with no0x82/0x81marker validation. This is tracked in wlink#58.
Observed raw response (from a source comment in src/commands/mod.rs):
ffff 0020 aeb4abcd 16c6bc45 e339e339 20360510
^^^^ ^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^
│ │ │ │ │ └─ ChipID (0x20360510)
│ │ │ │ └────────── erased-flash pattern 0xe339e339
│ │ │ └─────────────────── UID words 1 and 2 (see byte order below)
│ │ └─────────────────────────── flash size in KB (0x0020 = 32 KB)
│ └──────────────────────────────── prefix / header, meaning unverified
└───────────────────────────────────── prefix / header, meaning unverified
ESignature::from_raw only parses the first 12 bytes:
| Offset | Size | Field |
|---|---|---|
| 2–3 | 2 | flash_size_kb (u16_be) |
| 4–7 | 4 | UID word 1 (u32_be) |
| 8–11 | 4 | UID word 2 (u32_be) |
The trailing 8 bytes (erased-flash marker and ChipID) are observed but not
parsed by wlink; their semantics are unverified.
UID byte order
wlink stores the two u32_be words and reinterprets them as native-endian
bytes for display, producing the same order as wchisp:
words: aeb4abcd 16c6bc45
bytes: cd ab b4 ae 45 bc c6 16
UID: cd-ab-b4-ae-45-bc-c6-16
Erased flash-size field
0xE339 (per halfword) is what erased flash reads back on these MCUs, not
0xFFFF. When the flash-size field equals ESignature::ERASED_FLASH_SIZE
(0xE339), wlink substitutes the family’s fixed flash size
(RiscvChip::fixed_flash_size_kb). A programmed value always wins.
Example from a test in src/commands/mod.rs (CH32V407VET):
ffff 0240 50ad061a ab33546c e339e339 46700000 → 576 KB, ChipID
0x46700000.
Chip family codes
The one-byte riscvchip code is required by SetSpeed (0x0c), by the
special erase subcommands (0x0d / 0x08, 0x0d / 0x0f) and by several
per-family behaviours. The complete 28-entry table is in
Constants and Chip Families.
Operation Flows
This page describes the order in which wlink issues commands. Implementations
live in src/operations.rs
and src/probe.rs.
Command byte layouts are in Command Reference.
Attach (start of a session)
ProbeSession::attach:
- Check that the probe variant supports the requested chip. Without
--chip,CH32V103(0x01) is used as the provisional family. - Loop (up to 3 retries, 100 ms apart):
0x0c SetSpeedwith the provisionalriscvchipand the selected speed;0x0d / 0x02 AttachChip— break on success.
- If
--chipwas given, verify the detected family matches; otherwise issue0x0c SetSpeedagain using the detected family. - Run the family-specific post-init hook (
RiscvChip::do_post_init).
81 0c 02 01 01 SetSpeed(riscvchip=0x01, High)
81 0d 01 02 AttachChip
82 0d 05 05 20 36 05 10 → CH32V20X, ChipID 0x20360510
81 0c 02 05 01 SetSpeed again with the detected family
The CH32V205 family requires firmware v3.6 (version_code >= 42); older
firmware misdetects it as CH32V20X and wlink warns.
Status / chip info
ProbeSession::dump_info:
0x11 / 0x06 GetChipInfo::V2whenversion_code >= 29, else0x11 / 0x09 GetChipInfo::V1— see Chip Info.0x06 / 0x01 CheckReadProtect→ report whether flash is read-protected.- If the family supports it,
0x0d / 0x04 GetChipRomRamSplit→ SRAM/CODE split mode.
Querying info halts the MCU. wlink re-enables execution on detach.
Erase (default method)
ProbeSession::erase_flash:
- If the family supports flash protection:
0x06 / 0x01 CheckReadProtect; protected →unprotect_flash();- if unprotected,
0x06 / 0x04 CheckReadProtectEx; write-protected →unprotect_flash().
0x02 / 0x01 Program::EraseFlash.0x0d / 0x02 AttachChipagain to re-establish the session.
Unprotect
ProbeSession::unprotect_flash starts with a fresh attach
(0x0d / 0xff OptEnd then 0x0d / 0x02), then:
0x06 / 0x01 CheckReadProtect; if0x01,0x06 / 0x02 Unprotect, reattach, re-check.0x06 / 0x04 CheckReadProtectEx; if0x11,0x06 02 ff ff ff ff ff ff ff(UnprotectEx(0xff)), reattach, re-check.
wlink deliberately skips Unprotect when flash is already unprotected: the
probe firmware mass-erases the option-byte page, destroying USER/Data/WRPR.
Flash programming
ProbeSession::write_flash(data, address):
1 (optional) unprotect_flash()
2 0x01 SetWriteMemoryRegion(address, len)
3 0x02 / 0x05 Program::WriteFlashOP
4 data EP 0x02: write the family's flash algorithm blob
(padded to data_packet_size, see src/flash_op.rs)
5 0x02 / 0x07 Program::Unknown07AfterFlashOPWritten
→ response must be 0x07
6 0x02 / 0x02 Program::WriteFlash
7 for each write_pack_size chunk:
data EP 0x02: write chunk (0xff-padded to data_packet_size)
data EP 0x82: read 4-byte ack, last byte must be 0x04
(observed 41 01 01 04)
8 0x02 / 0x08 Program::End
data_packet_size and write_pack_size are per-family; see
RiscvChip::data_packet_size / write_pack_size in src/lib.rs.
Memory read
ProbeSession::read_memory(address, length):
- Round
lengthup to a multiple of 4. 0x03 SetReadMemoryRegion(address, len).0x02 / 0x0c Program::ReadMemory.0x82data endpoint: readlenbytes in 64-byte bulk chunks.- Reverse each 4-byte word to fix the probe’s word order.
A leading A9 BD F9 F3 sequence indicates a read from an invalid location or
a chip that has just entered debug mode; wlink logs a warning.
Special erase (power-off / RST pin)
erase --method power-off|pin-rst --chip <FAMILY> bypasses attach entirely:
0x0c SetSpeed(riscvchip=<--chip>, High)
0x0d / 0x0f EraseCodeFlash::ByPowerOff → 81 0d 02 0f <riscvchip>
or 0x0d / 0x08 EraseCodeFlash::ByPinRST → 81 0d 02 08 <riscvchip>
Both require a probe with power functions and a chip family whose
support_special_erase() is true. Because there is no auto-detection, --chip
is mandatory. The RST-pin variant additionally needs the nRST line wired to
the target.
Reset and detach
| CLI | Commands |
|---|---|
wlink reset (quit) | 0x0b / 0x01 Reset::Soft, then ~300 ms delay |
wlink reset run / halt | DMI halt/resume via 0x08 — see DMI |
| end of most commands | 0x0d / 0xff OptEnd (skipped with --no-detach, or when the MCU must stay halted) |
wlink sdi-print enable | 0x0d / 0xee 00 |
wlink set-power | 0x0d / 0x09…0x0c |
DMI and the Debug Module
The Debug Module Interface (DMI) is a separate layer above the WCH-Link command
protocol: a single 0x08 command carries one DMI transaction. The register
addresses and semantics come from the RISC-V Debug Specification 0.13.2 and
WCH’s QingKe debug manuals.
Sources: DmiOp / DmiOpResponse in
src/commands/mod.rs,
src/dmi.rs,
src/regs.rs.
0x08 transaction layout
Request payload (6 bytes):
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | addr — DMI register address |
| 1–4 | 4 | data (u32_be), ignored for NOP/READ |
| 5 | 1 | op — 0x00 NOP, 0x01 READ, 0x02 WRITE |
Response payload uses the same 6-byte shape; op is now a status:
op | Meaning | DmiOpResponse helper |
|---|---|---|
0x00 | Success | is_success() |
0x02 | Failed | is_failed() |
0x03 | Busy / failed | is_busy(), also counted by is_failed() |
Example — read DMSTATUS (0x11):
→ 81 08 06 11 00 00 00 00 01
← 82 08 06 11 00 00 00 00 00
dmi_read polls on busy (up to 100 retries with 10 ms sleeps) and raises
Error::NotAttached when it sees op == 0x03 && data == 0xffffffff && addr == 0x7d.
DMI register addresses
From src/regs.rs:
| Address | Name | Notes |
|---|---|---|
0x04 | DMDATA0 | Abstract command data |
0x05 | DMDATA1 | |
0x10 | DMCONTROL | Halt/resume, dmactive |
0x11 | DMSTATUS | allhalted, anyhalted, havereset |
0x12 | DMHARTINFO | |
0x16 | DMABSTRACTCS | cmderr, busy |
0x17 | DMCOMMAND | Abstract command register |
0x18 | DMABSTRACTAUTO | Not available on QingKeV2 / QingKeV3 |
0x20–0x27 | DMPROGBUF0–7 | Program buffer |
0x40 | DMHALTSUM0 |
CSR addresses used with abstract commands (16-bit command.regno):
0x300 MSTATUS, 0x301 MISA, 0x305 MTVEC, 0x340 MSCRATCH,
0x341 MEPC, 0x342 MCAUSE, 0x343 MTVAL, 0x7b1 DPC,
0xf12 MARCHID, 0xf13 MIMPID. General-purpose registers x0–x31
occupy 0x1000–0x101f.
Abstract command errors
AbstractcsCmdErr::try_from_cmderr maps the cmderr field of
DMABSTRACTCS:
| Value | Meaning |
|---|---|
| 0 | No error |
| 1 | Busy (register access during an abstract command) |
| 2 | Command not supported |
| 3 | Exception during execution |
| 4 | Hart in wrong state (not halted/running) |
| 5 | Bus error (alignment, size, timeout) |
| 6 | Parity error (WCH extension) |
| 7 | Other |
ProbeSession::clear_abstractcs_cmderr writes 0b111 to the cmderr field
to clear it (W1C), and clear_dmstatus_havereset acknowledges havereset.
Flash/option unlock keys
src/dmi.rs defines two keys used when unlocking flash/option-byte writes
through the probe’s debug interface:
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB
Higher-level operations
DebugModuleInterface (implemented for WchLink) provides dmi_nop,
dmi_read, dmi_write and typed register helpers. ProbeSession builds on
them for:
ensure_mcu_halt/ensure_mcu_resume— write0x80000001toDMCONTROL(0x10) and pollDMSTATUS;- register and memory inspection via abstract commands and the program buffer;
dump_regs,dump_core_csrs,dump_pmp_csrs,dump_dmi.
The RISC-V Debug Specification details are out of scope for this project documentation. See External References for the QingKeV2 debug manual and other implementations.
Constants and Chip Families
riscvchip — chip family codes
Source: RiscvChip in
src/lib.rs. The
one-byte value is returned by AttachChip and is passed to SetSpeed and to
the special-erase subcommands.
| Code | Family | Series / notes |
|---|---|---|
0x01 | CH32V103 | RISC-V3A |
0x02 | CH57X | CH571/CH573, RISC-V3A BLE 4.2 |
0x03 | CH56X | CH565/CH569, RISC-V3A |
0x04 | CH32F10X | Cortex-M (not a RISC-V target) |
0x05 | CH32V20X | RISC-V4B/V4C |
0x06 | CH32V30X | RISC-V4C/V4F, shares type 5 traits |
0x07 | CH582 | CH581/CH582/CH583, RISC-V4A BLE 5.3 |
0x08 | CH32F20X | Cortex-M (not a RISC-V target) |
0x09 | CH32V003 | RISC-V2A |
0x0a | CH8571 | Undocumented RISC-V EC controller |
0x0b | CH59X | CH59x RISC-V4C BLE 5.4, falls back as CH58X |
0x0c | CH643 | RISC-V4C, RGB display driver |
0x0d | CH32X035 | CH32X035/033, RISC-V4C USB-PD |
0x0e | CH32L103 | RISC-V4C low-power USB-PD |
0x0f | CH564 | RISC-V4J |
0x46 | CH645 | CH645/CH653, RISC-V4C |
0x49 | CH641 | RISC-V2A USB-PD, falls back as CH32V003 |
0x4b | CH585 | CH585/CH584, RISC-V3C BLE 5.4 / NFC / USB HS |
0x4e | CH32V00X | CH32V002/4/5/6/7, CH32M007 |
0x86 | CH32V317 | RISC-V4 |
0x8b | CH570 | CH570/CH572, RISC-V BLE |
0x8e | CH32M030 | RISC-V motor control |
0xa6 | CH32V4X7 | CH32V407/CH32V467 |
0xab | CH586 | CH586/CH587, RISC-V BLE 5.4 |
0xc6 | CH32H41X | CH32H415/416/417, RISC-V5F + RISC-V3F |
0xce | CH32V205 | New-generation CH32V205/CH32V203 (QingKe V3B), unrelated to CH32V20X |
0xe6 | CH32X3X5 | CH32X315/CH32X305, RISC-V3F |
RiscvChip also implements clap’s ValueEnum, so --chip accepts the family
name plus part-number aliases such as CH32H417QEU or CH32X315MCU6. The
ChipID-to-part-name database is chip_id_to_chip_name in
src/chips.rs.
Speed codes (0x0c SetSpeed)
| Code | Speed variant | Clock |
|---|---|---|
0x01 | High (default) | 6000 kHz |
0x02 | Medium | 4000 kHz |
0x03 | Low | 400 kHz |
Protection flags (0x06 ConfigChip)
| Constant | Value |
|---|---|
FLAG_READ_PROTECTED | 0x01 |
FLAG_READ_UNPROTECTED | 0x02 |
FLAG_WRITE_PROTECTED | 0x11 |
FLAG_WRITE_UNPROTECTED | 0x00 |
CheckReadProtect returns the read flags; CheckReadProtectEx returns the
write flags.
Error reasons
REASON | Meaning |
|---|---|
0x55 | Failed to connect to the target RISC-V chip |
| other | Unknown; wlink preserves the whole frame in Error::Protocol(reason, bytes) |
USB constants
| Constant | Value |
|---|---|
VENDOR_ID | 0x1a86 |
PRODUCT_ID (RV) | 0x8010 |
PRODUCT_ID_DAP | 0x8012 |
ENDPOINT_OUT / ENDPOINT_IN | 0x01 / 0x81 |
DATA_ENDPOINT_OUT / DATA_ENDPOINT_IN | 0x02 / 0x82 |
ENDPOINT_OUT_DAP | 0x02 |
Flash algorithm blobs
Each supported family has a binary “flash OP” blob in
src/flash_op.rs,
sent over the data endpoint after 0x02 / 0x05 Program::WriteFlashOP. They are
opaque byte arrays selected by RiscvChip::get_flash_op, for example
CH32V307: [u8; 446], CH32V003: [u8; 498], CH32H417: [u8; 618],
CH564: [u8; 1532]. Some families have an _ALT variant. Packet and write
chunk sizes come from RiscvChip::data_packet_size and
RiscvChip::write_pack_size in src/lib.rs.
Debug interface keys
| Constant | Value |
|---|---|
KEY1 | 0x45670123 |
KEY2 | 0xCDEF89AB |
Used to unlock flash/option-byte writes through the debug interface; see DMI.
Unverified Commands and Captures
This page collects protocol knowledge that wlink does not implement and
cannot confirm. It exists so that the old protocol.md’s ??? markers are not
silently dropped: they are either resolved (and documented on other pages) or
listed here as observations.
unverifiedmeans: seen in a USB capture from WCH’s utility, or written in an older note, without a confirmed implementation in wlink. Do not rely on these byte sequences in new code without re-capturing.
Raw captures recorded in the source
Copied verbatim from the comment block at the end of
src/commands/mod.rs:
81 0D 05 11 SetAccessAddress
81 0F 01 02 GetDeviceMode
81 0D 01 07 EnableQE
81 0D 01 06 CheckQE
81 FE 01 00 DisEncrypt
81 0D 01 0F ClearCodeFlashB
81 0D 02 08 xx ClearCodeFlash
81 11 01 0D unknown in query info, before GetChipRomRamSplit
81 0D 02 EE 00/02/03 SetSDLineMode
81 0F 01 01 SetIAPMode
Observations:
0x0fappears as a command ID inGetDeviceMode/SetIAPModecaptures, not as a “begin verify”0x02subcommand as the old document claimed.0xfeappears once asDisEncrypt.81 0D 01 0F(ClearCodeFlashB) hasLEN = 1while two payload bytes follow — see the discussion of the analogous0x0flength quirk in Control Subcommands.81 11 01 0Dis a0x11variant not used by wlink, which only sends0x09(V1) and0x06(V2).81 0D 02 EE 00/02/03suggests additional SDI line modes; wlink only usesee 00/ee 01.
Subcommands not implemented by wlink
CMD / SUB | Capture label | Status |
|---|---|---|
0x0d / 0x03 | “stage after connect chip” (old note, riscvchip 1) | unverified |
0x0d / 0x06 | CheckQE | unverified |
0x0d / 0x07 | EnableQE | unverified |
0x0d / 0x11 | SetAccessAddress | unverified |
0x0f / 0x01 | SetIAPMode | unverified |
0x0f / 0x02 | GetDeviceMode | unverified |
0xfe / 0x00 | DisEncrypt | unverified |
0x06 Config { data, wrp } | option-byte config flags | known capture 81 06 08 02 3f 00 00 ff ff ff ff, but todo!() in wlink |
Old protocol.md questions and their resolution
| Old entry | Resolution |
|---|---|
0x01 - Set RAM address and size (empty section) | SetWriteMemoryRegion, 8-byte payload — Command Reference |
0x02 subcommand guesses (0x03 execute ram, 0x05 begin bulk transfer, 0x07 verify, 0x09 bulk transfer ends, 0x0a verify, 0x0b verify for riscvchip 1) | Replaced by the implemented Program enum; unconfirmed labels are unverified |
0x08 - DMI OP (two empty tables) | Filled in — DMI |
0x0c - ??? | SetSpeed; see also the v0.0.6 CHANGELOG correction |
0x0d - Info in the command table vs 0x0d - Control as a heading | Both refer to the Control command — Control Subcommands |
0x0e (unnamed) | DisableDebug |
0x0f - ? begin verify | unverified; captures point at GetDeviceMode / SetIAPMode |
0x03 Reset, normal? | Reset::Normal (0x03) |
0x03 ? stage after connect chip | Still unverified |
Error Reason: 0x55 only | Still the only reason with a known meaning |
RiscvChip with 7 families | 28 entries — Constants |
Firmware Versions: v2.8 | Current baseline v2.15 (v35); encoding explained in Probe Info |
Variants: ?? / WCH-LinkB | Still unverified |
beigin, buck, Swith typos | Corrected |
Contributing a correction
- Capture the exchange with
wlink -vv(trace logs printsendandrecvhex for every frame) or with a USB sniffer. - Identify the Rust symbol in
src/commands/that produces or parses it. - Update the relevant page here, and move the entry out of this page if it is now confirmed. If the implementation changes, update the code and the documentation in the same PR.
CH32V003
RISC-V2A, 16KB Flash, 2KB SRAM, 48MHz.
Note CH32V003 is a riscv32ec core, which is not supported by ofiicial Rust yet. ch32-rs team maintains a fork of Rust at https://github.com/ch32-rs/rust. You can check Noxim’s Blog for riscv32ec support.
Debug support
1-wire debug.
SWDIO <-> D1/DIO@MCU, PD1
GND <-> GND@MCU
3V3 <-> 3V3@MCU (No need if you connect your MCU board with USB-C power supply)
- Flash support
- Memory dump support
- Reset / Resume
- Reg read/write support
> wlink -v flash ./firmware.bin
13:41:49 [INFO] WCH-Link v2.8 (WCH-LinkE-CH32V305)
13:41:49 [INFO] attached chip: CH32V003(0x00300500)
13:41:49 [DEBUG] (1) wlink::operations: Chip UID: cd-ab-80-17-48-bc-95-7f
13:41:49 [DEBUG] (1) wlink::operations: flash protected: false
13:41:49 [DEBUG] (1) wlink::operations: SRAM CODE mode: 3
13:41:49 [DEBUG] (1) wlink::operations: already halted
13:41:49 [DEBUG] (1) wlink::operations: RISC-V core version: Some("WCH-V2A")
13:41:49 [INFO] flash 860 bytes
13:41:49 [DEBUG] (1) wlink::operations: Code start address 0x08000000
13:41:49 [DEBUG] (1) wlink::operations: flash op written
13:41:49 [DEBUG] (1) wlink::operations: fastprogram done
13:41:49 [INFO] flash done
13:41:50 [INFO] now reset...
13:41:51 [INFO] resume executing...
13:41:51 [WARN] resume fails
Chips
* CH32V003F4P6-0x003005x0
* CH32V003F4U6-0x003105x0
* CH32V003A4M6-0x003205x0
* CH32V003J4M6-0x003305x0
CH32V103
SWDIO - PA13 @ MCU SWCLK - PA14 @ MCU
“Deprotect” or “接触代码保护” must be run before using wlin.(use WCHISPTool or wchisp).
USBISP
BOOT0 to VCC
Chips
* CH32F103C8T6-0x20004102
* CH32F103R8T6-0x2000410F
CH32V20x
- PA13 SWDIO
- PA14 SWCLK
Dev Boards
- nanoCH32V203 by Muse Lab - CH32V203C8T6
- FlappyBoard - CH32V203G6U6
Notes
- Erased bytes
39 e3 39 e3
Chips
* CH32V203C8U6-0x203005x0
* CH32V203C8T6-0x203105x0
* CH32V203K8T6-0x203205x0
* CH32V203C6T6-0x203305x0
* CH32V203K6T6-0x203505x0
* CH32V203G6U6-0x203605x0
* CH32V203G8R6-0x203B05x0
* CH32V203F8U6-0x203E05x0
* CH32V203F6P6-0x203705x0-0x203905x0
* CH32V203F8P6-0x203A05x0
* CH32V203RBT6-0x203405xC
* CH32V208WBU6-0x208005xC
* CH32V208RBT6-0x208105xC
* CH32V208CBU6-0x208205xC
* CH32V208GBU6-0x208305xC
CH32V30x
- CH32V303
- CH32V305
- CH32V307
wlink -v dump 0x08000000 0x10`
14:40:22 [INFO] WCH-Link v2.8 (WCH-LinkE-CH32V305)
14:40:22 [INFO] attached chip: CH32V30X(0x30700518)
14:40:22 [DEBUG] (1) wlink::operations: Chip UID: 30-78-3e-26-3b-38-a9-d6
14:40:22 [DEBUG] (1) wlink::operations: flash protected: false
14:40:22 [DEBUG] (1) wlink::operations: SRAM CODE mode: 3
14:40:22 [DEBUG] (1) wlink::operations: already halted
14:40:22 [DEBUG] (1) wlink::operations: RISC-V core version: Some("WCH-V4A")
14:40:22 [INFO] Read memory from 0x08000000 to 0x08000010
08000000: b7 00 00 08 67 80 80 00 73 50 40 30 73 50 40 34 ×00•g××0sP@0sP@4
Notes
- erased flash
39 e3 39 e3
Chips
* CH32V303CBT6-0x303305x4
* CH32V303RBT6-0x303205x4
* CH32V303RCT6-0x303105x4
* CH32V303VCT6-0x303005x4
* CH32V305FBP6-0x305205x8
* CH32V305RBT6-0x305005x8
* CH32V305GBU6-0x305B05x8
* CH32V307WCU6-0x307305x8
* CH32V307FBP6-0x307205x8
* CH32V307RCT6-0x307105x8
* CH32V307VCT6-0x307005x8
CH32X035/CH32X033
RISC-V4C, 62KB Flash, 20KB SRAM.
OPA/PGA/CMP, USB-PD/Type-C.
Debug Pins
- SWDIO - PC18/DIO @ MCU
- SWCLK - PC19/DCK @ MCU
PC17 to High, enable USB ISP DOWNLOAD MODE
Boards
Official CH32X035EVT
- CH32X035F8U6-R0
- CH32X035G8U6-R0
- CH32X035C8T6-R0
LED1, LED2 blue LED, active low, open, connected to open jumper
Chips
CH32X035R8T6-0x035006x1
CH32X035C8T6-0x035106x1
CH32X035F8U6-0x035E06x1
CH32X035G8U6-0x035606x1
CH32X035G8R6-0x035B06x1
CH32X035F7P6-0x035706x1
CH32X033F8P6-0x035A06x1
CH32L103
RISC-V4C, 96MHz, 64KB Flash, 20KB SRAM.
RTC, ADC/TKey, CAN, OPA, CMP, USB-PD/Type-C.
Chips
* CH32L103C8U6-0x103007x0
* CH32L103C8T6-0x103107x0
* CH32L103F8P6-0x103A07x0
* CH32L103G8R6-0x103B07x0
* CH32L103K8U6-0x103207x0
* CH32L103F8U6-0x103D07x0
* CH32L103F7P6-0x103707x0
CH569/CH565
RISC-V3A, 448KB Code Flash, 32KB Data Flash, 32/64/96KB SRAM(RAMX).
BUS8, USB-SS, ETH, EMMC, SerDes
- CH569: HSPI
- CH565: DVP
Debug Pins
- TCK=HTACK=PA10
- TIO=HTCLK=PA11
Boards
- CH569W-R0-1v0
CH57X & CH58X & CH59X
BLE MCUs. They are sharing almost the same preipheral set
Debug Pins
- PB15 TCK
- PB14 TIO
Notes
- Enable debug using WCHISPTool
- Always specify chip type using
--chip xxxparameter during connect, auto-detection mechanism does not work on these targets - Similiarly, use
-c "chip_id CH59x"while using official OpenOCD release from MRS, as there’s no way to know the chip type before connecting to it - After debug enabled, flash should be prgrammed at lease once
a9 bd f9 f3means erased flash or protected flash. The detailed mechanism is unknown
CH641
RISC-V2A, 48MHz, 16KB Flash, 2KB SRAM.
USB-PD/Type-C, BC1.2, HV DCP. ISP/ISN, QII.
Note CH641 is a riscv32ec core, which is not supported by ofiicial Rust yet. ch32-rs team maintains a fork of Rust at https://github.com/ch32-rs/rust. You can check Noxim’s Blog for riscv32ec support.
Chips
* CH641F-0x641005x0
* CH641D-0x641105x0
* CH641U-0x641505x0
* CH641P-0x641605x0
CH643
RISC-V4C, 62KB Flash, 20KB SRAM.
OPA/PGA/CMP, LEDPWM, USB-PD/Type-C.
Chips
* CH643W-0x64300601
* CH643Q-0x64310601
* CH643L-0x64330601
* CH643U-0x64340601
References
Real Shot
Feature matrix
| Feature | WCH-Link | WCH-LinkE | WCH-LinkW | WCH-DAPLink |
|---|---|---|---|---|
| RISC-V mode | ✓ | ✓ | ✓ | |
| ARM-SWD mode (HID device) | ✓ | |||
| ARM-SWD mode (WinUSB device) | ✓ | ✓ | ✓ | ✓ |
| ARM-JTAG mode (HID device) | ✓ | |||
| ARM-JTAG mode (WinUSB device) | ✓ | ✓ | ✓ | |
| ModeS button | ✓ | ✓ | ✓ | |
| DFU via 2-wire | ✓ | |||
| DFU via serial-port | ✓ | |||
| DFU via USB | ✓ | ✓ | ✓ | |
| Power Supply (3.3v) | ✓ | ✓ | ✓ | ✓ |
| Power Supply (5.0v) | ✓ | ✓ | ✓ | ✓ |
| Power Supply (Configurable) | ✓ | ✓ | ✓ | |
| USB 2.0 to JTAG | ✓ | |||
| Wireless mode | ✓ | |||
| Download via MounRiver Studio | ✓ | ✓ | ✓ | ✓ |
| Download via WCH-LinkUtility | ✓ | ✓ | ✓ | |
| Download via Keil | ≥ v5.25 | ≥ v5.25 | ≥ v5.25 | ✓ |
Supported Chip matrix
| Chip | WCH-Link | WCH-LinkE | WCH-LinkW | WCH-DAPLink |
|---|---|---|---|---|
| JTAG interface | ✓ | ✓ | ✓ | |
| SWD interface | ✓ | ✓ | ✓ | ✓ |
| CH32F10x | ✓ | ✓ | ✓ | ✓ |
| CH32F20x | ✓ | ✓ | ✓ | ✓ |
| CH32V003 | ✓ | ✓ | ||
| CH32V10x | ✓ | ✓ | ✓ | |
| CH32V20X | ✓ | ✓ | ✓ | |
| CH32V30X | ✓ | ✓ | ✓ | |
| CH569 | ✓ | ✓ | ||
| CH573 | ✓ | ✓ | ||
| CH579 | ✓ | ✓ | ✓ | ✓ |
| CH583 | ✓ | ✓ |
Supported Baud matrix
| Baud | WCH-Link | WCH-LinkE | WCH-LinkW | WCH-DAPLink |
|---|---|---|---|---|
| 1200 | ✓ | ✓ | ✓ | ✓ |
| 2400 | ✓ | ✓ | ✓ | ✓ |
| 4800 | ✓ | ✓ | ✓ | ✓ |
| 9600 | ✓ | ✓ | ✓ | ✓ |
| 14400 | ✓ | ✓ | ✓ | ✓ |
| 19200 | ✓ | ✓ | ✓ | ✓ |
| 38400 | ✓ | ✓ | ✓ | ✓ |
| 57600 | ✓ | ✓ | ✓ | ✓ |
| 115200 | ✓ | ✓ | ✓ | ✓ |
| 230400 | ✓ | ✓ | ✓ | ✓ |
| 460800 | ✓ | ✓ | ✓ | |
| 921600 | ✓ | ✓ | ✓ |
SWD PIN matrix
| Chip | SWDIO | SWCLK |
|---|---|---|
| CH32F10x | PA13 | PA14 |
| CH32F20x | PA13 | PA14 |
| CH32V003 | PD1 | |
| CH32V10x | PA13 | PA14 |
| CH32V20X | PA13 | PA14 |
| CH32V30X | PA13 | PA14 |
| CH32X035 | PC18 | PC19 |
| CH569 | PA11 | PA10 |
| CH573 | PB14 | PB15 |
| CH579 | PB16 | PB17 |
| CH583 | PB14 | PB15 |
| CH59x | PB14 | PB15 |
| CH643 | PC18 | PC19 |
Documentation
ISP
- WCHISPTool_Setup.exe v3.3
Firmware
Other FOSS implementation
- https://github.com/openwch/ch32v003 RISC-V QingKeV2 Microprocessor Debug Manual
- https://github.com/cnlohr/ch32v003fun A miniwchlink implementation
- https://github.com/blackmagic-debug/blackmagic/pull/1399
- MounRiver Studio compatible WCH-Link OpenOCD source code https://github.com/treideme/openocd-hacks