probe_rs/probe/wlink/
mod.rs

1//! WCH-LinkRV probe support.
2//!
3//! The protocol is mostly undocumented, and is changing between firmware versions.
4//! For more details see: <https://github.com/ch32-rs/wlink>
5
6use std::fmt;
7use std::time::Duration;
8
9use bitvec::{bitvec, field::BitField, order::Lsb0, vec::BitVec, view::BitView};
10use nusb::{DeviceInfo, MaybeFuture};
11use probe_rs_target::ScanChainElement;
12
13use self::{commands::Speed, usb_interface::WchLinkUsbDevice};
14use super::JtagAccess;
15use crate::{
16    architecture::riscv::{
17        communication_interface::{RiscvError, RiscvInterfaceBuilder},
18        dtm::jtag_dtm::JtagDtmBuilder,
19    },
20    probe::{
21        DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, JtagSequence, ProbeError,
22        ProbeFactory, WireProtocol,
23    },
24};
25
26mod commands;
27mod usb_interface;
28
29const VENDOR_ID: u16 = 0x1a86;
30const PRODUCT_ID: u16 = 0x8010;
31
32// See: RISC-V Debug Specification, 6.1 JTAG DTM Registers
33const DMI_VALUE_BIT_OFFSET: u32 = 2;
34const DMI_ADDRESS_BIT_OFFSET: u32 = 34;
35const DMI_OP_MASK: u128 = 0b11; // 2 bits
36
37const DMI_OP_NOP: u8 = 0;
38const DMI_OP_READ: u8 = 1;
39const DMI_OP_WRITE: u8 = 2;
40
41const REG_BYPASS_ADDRESS: u8 = 0x1f;
42const REG_IDCODE_ADDRESS: u8 = 0x01;
43const REG_DTMCS_ADDRESS: u8 = 0x10;
44const REG_DMI_ADDRESS: u8 = 0x11;
45
46const DTMCS_DMIRESET_MASK: u32 = 1 << 16;
47const DTMCS_DMIHARDRESET_MASK: u32 = 1 << 17;
48
49/// All WCH-Link probe variants, see-also: <http://www.wch-ic.com/products/WCH-Link.html>
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51#[repr(u8)]
52pub enum WchLinkVariant {
53    /// WCH-Link-CH549, does not support RV32EC
54    Ch549 = 1,
55    /// WCH-LinkE-CH32V305, the full featured version
56    ECh32v305 = 2,
57    /// WCH-LinkS-CH32V203
58    SCh32v203 = 3,
59    /// WCH-LinkW-CH32V208, a wirelessed version
60    WCh32v208 = 5,
61}
62
63impl fmt::Display for WchLinkVariant {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            WchLinkVariant::Ch549 => write!(f, "WCH-Link-CH549"),
67            WchLinkVariant::ECh32v305 => write!(f, "WCH-LinkE-CH32V305"),
68            WchLinkVariant::SCh32v203 => write!(f, "WCH-LinkS-CH32V203"),
69            WchLinkVariant::WCh32v208 => write!(f, "WCH-LinkW-CH32V208"),
70        }
71    }
72}
73
74impl WchLinkVariant {
75    fn try_from_u8(value: u8) -> Result<Self, WchLinkError> {
76        match value {
77            1 => Ok(Self::Ch549),
78            2 | 0x12 => Ok(Self::ECh32v305),
79            3 => Ok(Self::SCh32v203),
80            5 | 0x85 => Ok(Self::WCh32v208),
81            _ => Err(WchLinkError::UnknownDevice),
82        }
83    }
84}
85
86/// Currently supported RISC-V chip series/families. The IP core name is "Qingke".
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88#[repr(u8)]
89pub enum RiscvChip {
90    /// CH32V103 Qingke-V3A series
91    CH32V103 = 0x01,
92    /// CH571/CH573 Qingke-V3A BLE 4.2 series
93    CH57X = 0x02,
94    /// CH565/CH569 Qingke-V3A series
95    CH56X = 0x03,
96    /// CH32V20X Qingke-V4B/V4C series
97    CH32V20X = 0x05,
98    /// CH32V30X Qingke-V4C/V4F series, the same as CH32V20X
99    CH32V30X = 0x06,
100    /// CH58x Qingke-V4A BLE 5.3 series
101    CH58X = 0x07,
102    /// CH32V003 Qingke-V2A series
103    CH32V003 = 0x09,
104    // The only reference I can find is <https://www.wch.cn/news/606.html>.
105    /// RISC-V EC controller, undocumented.
106    CH8571 = 0x0A, // 10,
107    /// CH59x Qingke-V4C BLE 5.4 series, fallback as CH58X
108    CH59X = 0x0B, // 11
109    /// CH643 Qingke-V4C series, RGB Display Driver MCU
110    CH643 = 0x0C, // 12
111    /// CH32X035 Qingke-V4C USB-PD series, fallback as CH643
112    CH32X035 = 0x0D, // 13
113    /// CH32L103 Qingke-V4C low power series, USB-PD
114    CH32L103 = 0x0E, // 14
115    /// CH641 Qingke-V2A series, USB-PD, fallback as CH32V003
116    CH641 = 0x49,
117}
118
119impl RiscvChip {
120    fn try_from_u8(value: u8) -> Option<Self> {
121        match value {
122            0x01 => Some(RiscvChip::CH32V103),
123            0x02 => Some(RiscvChip::CH57X),
124            0x03 => Some(RiscvChip::CH56X),
125            0x05 => Some(RiscvChip::CH32V20X),
126            0x06 => Some(RiscvChip::CH32V30X),
127            0x07 => Some(RiscvChip::CH58X),
128            0x09 => Some(RiscvChip::CH32V003),
129            0x0A => Some(RiscvChip::CH8571),
130            0x0B => Some(RiscvChip::CH59X),
131            0x0C => Some(RiscvChip::CH643),
132            0x0D => Some(RiscvChip::CH32X035),
133            0x0E => Some(RiscvChip::CH32L103),
134            0x49 => Some(RiscvChip::CH641),
135            _ => None,
136        }
137    }
138
139    fn support_flash_protect(&self) -> bool {
140        matches!(
141            self,
142            RiscvChip::CH32V103
143                | RiscvChip::CH32V20X
144                | RiscvChip::CH32V30X
145                | RiscvChip::CH32V003
146                | RiscvChip::CH643
147                | RiscvChip::CH32L103
148                | RiscvChip::CH32X035
149                | RiscvChip::CH641
150        )
151    }
152}
153
154/// Factory for creating [`WchLink`] probes.
155#[derive(Debug)]
156pub struct WchLinkFactory;
157
158impl std::fmt::Display for WchLinkFactory {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.write_str("WchLink")
161    }
162}
163
164impl ProbeFactory for WchLinkFactory {
165    fn open(&self, selector: &DebugProbeSelector) -> Result<Box<dyn DebugProbe>, DebugProbeError> {
166        let device = WchLinkUsbDevice::new_from_selector(selector)?;
167        let mut wlink = WchLink {
168            device,
169            name: "WCH-Link".into(),
170            variant: WchLinkVariant::Ch549,
171            v_major: 0,
172            v_minor: 0,
173            chip_id: 0,
174            chip_family: RiscvChip::CH32V103,
175            last_dmi_read: None,
176            speed: Speed::default(),
177            idle_cycles: 0,
178        };
179
180        wlink.init()?;
181
182        Ok(Box::new(wlink))
183    }
184
185    fn list_probes(&self) -> Vec<DebugProbeInfo> {
186        list_wlink_devices()
187    }
188}
189
190/// A WCH-Link device (mod:RV)
191pub struct WchLink {
192    device: WchLinkUsbDevice,
193    name: String,
194    variant: WchLinkVariant,
195    v_major: u8,
196    v_minor: u8,
197    /// Chip family
198    chip_family: RiscvChip,
199    /// Chip id to identify the target chip variant
200    chip_id: u32,
201    // Hack to support NOP after READ
202    last_dmi_read: Option<(u8, u32, u8)>,
203    speed: commands::Speed,
204    idle_cycles: u8,
205}
206
207impl fmt::Debug for WchLink {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.debug_struct("WchLink")
210            .field("name", &self.name)
211            .field("variant", &self.variant)
212            .field("v_major", &self.v_major)
213            .field("v_minor", &self.v_minor)
214            .field("chip_family", &self.chip_family)
215            .field("chip_id", &self.chip_id)
216            .field("last_dmi_read", &self.last_dmi_read)
217            .field("speed", &self.speed)
218            .field("idle_cycles", &self.idle_cycles)
219            .finish()
220    }
221}
222
223impl WchLink {
224    fn get_probe_info(&mut self) -> Result<(), DebugProbeError> {
225        let probe_info = self.device.send_command(commands::GetProbeInfo)?;
226        self.v_major = probe_info.major_version;
227        self.v_minor = probe_info.minor_version;
228
229        if self.v_major != 0x02 && self.v_minor < 0x07 {
230            return Err(WchLinkError::UnsupportedFirmwareVersion("2.7").into());
231        }
232
233        self.variant = probe_info.variant;
234
235        Ok(())
236    }
237
238    fn init(&mut self) -> Result<(), DebugProbeError> {
239        // first stage of wlink_init
240        tracing::debug!("Initializing WCH-Link...");
241
242        self.get_probe_info()?;
243
244        // this is the official version format. So "v31" is actually a 2.11
245        let version_code = self.v_major * 10 + self.v_minor;
246
247        tracing::info!(
248            "WCH-Link variant: {}, firmware version: {}.{} (v{})",
249            self.variant,
250            self.v_major,
251            self.v_minor,
252            version_code
253        );
254
255        if self.v_major != 0x02 && self.v_minor < 0x7 {
256            return Err(WchLinkError::UnsupportedFirmwareVersion("2.7").into());
257        }
258        self.name = format!("{} v{}.{}", self.variant, self.v_major, self.v_minor);
259
260        Ok(())
261    }
262
263    fn dmi_op_read(&mut self, addr: u8) -> Result<(u8, u32, u8), DebugProbeError> {
264        let resp = self.device.send_command(commands::DmiOp::read(addr))?;
265
266        Ok((resp.addr, resp.data, resp.op))
267    }
268
269    fn dmi_op_write(&mut self, addr: u8, data: u32) -> Result<(u8, u32, u8), DebugProbeError> {
270        let resp = self
271            .device
272            .send_command(commands::DmiOp::write(addr, data))?;
273
274        Ok((resp.addr, resp.data, resp.op))
275    }
276
277    fn dmi_op_nop(&mut self) -> Result<(u8, u32, u8), DebugProbeError> {
278        let resp = self.device.send_command(commands::DmiOp::nop())?;
279
280        Ok((resp.addr, resp.data, resp.op))
281    }
282}
283
284impl DebugProbe for WchLink {
285    fn get_name(&self) -> &str {
286        &self.name
287    }
288
289    fn speed_khz(&self) -> u32 {
290        self.speed.to_khz()
291    }
292
293    fn set_speed(&mut self, speed_khz: u32) -> Result<u32, DebugProbeError> {
294        let speed =
295            Speed::from_khz(speed_khz).ok_or(DebugProbeError::UnsupportedSpeed(speed_khz))?;
296        self.speed = speed;
297        self.device
298            .send_command(commands::SetSpeed(self.chip_family, speed))?;
299        Ok(speed.to_khz())
300    }
301
302    /// Attach chip
303    fn attach(&mut self) -> Result<(), DebugProbeError> {
304        // second stage of wlink_init
305        tracing::trace!("attach to target chip");
306
307        self.device
308            .send_command(commands::SetSpeed(self.chip_family, self.speed))?;
309
310        let resp = self.device.send_command(commands::AttachChip)?;
311
312        self.chip_family = resp.chip_family;
313
314        tracing::info!("attached riscv chip {:?}", self.chip_family);
315
316        self.chip_id = resp.chip_id;
317
318        if self.chip_family.support_flash_protect() {
319            self.device.send_command(commands::CheckFlashProtection)?;
320            self.device.send_command(commands::UnprotectFlash)?;
321        }
322
323        Ok(())
324    }
325
326    fn detach(&mut self) -> Result<(), crate::Error> {
327        tracing::trace!("Detach chip");
328        self.device.send_command(commands::DetachChip)?;
329
330        Ok(())
331    }
332
333    fn target_reset(&mut self) -> Result<(), DebugProbeError> {
334        self.device.send_command(commands::ResetTarget)?;
335        Ok(())
336    }
337
338    fn target_reset_assert(&mut self) -> Result<(), DebugProbeError> {
339        tracing::info!("target reset assert");
340        self.device
341            .send_command(commands::DmiOp::write(0x10, 0x80000001))?;
342        Ok(())
343    }
344
345    fn target_reset_deassert(&mut self) -> Result<(), DebugProbeError> {
346        tracing::info!("target reset deassert");
347        self.device
348            .send_command(commands::DmiOp::write(0x10, 0x00000001))?;
349        Ok(())
350    }
351
352    fn select_protocol(&mut self, protocol: WireProtocol) -> Result<(), DebugProbeError> {
353        // Assume Jtag, as it is the only supported protocol for riscv
354        match protocol {
355            WireProtocol::Jtag => Ok(()),
356            _ => Err(DebugProbeError::UnsupportedProtocol(protocol)),
357        }
358    }
359
360    fn active_protocol(&self) -> Option<WireProtocol> {
361        Some(WireProtocol::Jtag)
362    }
363
364    fn into_probe(self: Box<Self>) -> Box<dyn DebugProbe> {
365        self
366    }
367
368    fn has_riscv_interface(&self) -> bool {
369        true
370    }
371
372    fn try_get_riscv_interface_builder<'probe>(
373        &'probe mut self,
374    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, RiscvError> {
375        Ok(Box::new(JtagDtmBuilder::new(self)))
376    }
377}
378
379/// Wrap WCH-Link's USB based DMI access as a fake JtagAccess
380impl JtagAccess for WchLink {
381    fn set_scan_chain(&mut self, _scan_chain: &[ScanChainElement]) -> Result<(), DebugProbeError> {
382        Ok(())
383    }
384
385    fn scan_chain(&mut self) -> Result<&[ScanChainElement], DebugProbeError> {
386        Ok(&[])
387    }
388
389    fn tap_reset(&mut self) -> Result<(), DebugProbeError> {
390        Ok(())
391    }
392
393    fn read_register(&mut self, address: u32, len: u32) -> Result<BitVec, DebugProbeError> {
394        tracing::debug!("read register 0x{:08x}", address);
395        assert_eq!(len, 32);
396
397        let mut ret = bitvec![0; len as usize];
398        match address as u8 {
399            REG_IDCODE_ADDRESS => {
400                // using hard coded idcode 0x00000001, the same as WCH's openocd fork
401                tracing::debug!("using hard coded idcode 0x00000001");
402                ret[0..8].store_le::<u8>(0x1);
403                Ok(ret)
404            }
405            REG_DTMCS_ADDRESS => {
406                // See: RISC-V Debug Specification, 6.1.4
407                // 0x71: abits=7, version=1(1.0)
408                ret[0..8].store_le::<u8>(0x71);
409                Ok(ret)
410            }
411            REG_BYPASS_ADDRESS => Ok(bitvec![0; 4]),
412            _ => panic!("unknown read register address {address:08x}"),
413        }
414    }
415
416    fn set_idle_cycles(&mut self, idle_cycles: u8) -> Result<(), DebugProbeError> {
417        self.idle_cycles = idle_cycles;
418        Ok(())
419    }
420
421    fn idle_cycles(&self) -> u8 {
422        self.idle_cycles
423    }
424
425    fn write_register(
426        &mut self,
427        address: u32,
428        data: &[u8],
429        len: u32,
430    ) -> Result<BitVec, DebugProbeError> {
431        match address as u8 {
432            REG_DTMCS_ADDRESS => {
433                let val = u32::from_le_bytes(data.try_into().unwrap());
434                if val & DTMCS_DMIRESET_MASK != 0 {
435                    tracing::debug!("DMI reset");
436                    self.dmi_op_write(0x10, 0x00000000)?;
437                    self.dmi_op_write(0x10, 0x00000001)?;
438                    // dmcontrol.dmactive is checked later
439                } else if val & DTMCS_DMIHARDRESET_MASK != 0 {
440                    return Err(WchLinkError::UnsupportedOperation.into());
441                }
442
443                let mut ret = bitvec![0; len as usize];
444                ret[0..8].store_le::<u8>(0x71);
445                Ok(ret)
446            }
447            REG_DMI_ADDRESS => {
448                assert_eq!(
449                    len, 41,
450                    "should be 41 bits: 8 bits abits + 32 bits data + 2 bits op"
451                );
452                let register_value: u128 = u128::from_le_bytes(data.try_into().unwrap());
453
454                let dmi_addr = ((register_value >> DMI_ADDRESS_BIT_OFFSET) & 0x3f) as u8;
455                let dmi_value = ((register_value >> DMI_VALUE_BIT_OFFSET) & 0xffffffff) as u32;
456                let dmi_op = (register_value & DMI_OP_MASK) as u8;
457
458                tracing::trace!(
459                    "dmi op={} addr 0x{:02x} data 0x{:08x}",
460                    dmi_op,
461                    dmi_addr,
462                    dmi_value,
463                );
464
465                let (addr, data, op) = match dmi_op {
466                    DMI_OP_READ => {
467                        let (addr, data, op) = self.dmi_op_read(dmi_addr)?;
468                        tracing::trace!("dmi read 0x{:02x} 0x{:08x} op={}", addr, data, op);
469                        self.last_dmi_read = Some((addr, data, op));
470                        (addr, data, op)
471                    }
472                    DMI_OP_NOP => {
473                        // No idea why NOP with zero addr should return the last read value.
474                        // see-also: RiscvCommunicationInterface::read_dm_register_untyped
475                        let (addr, data, op) = if dmi_addr == 0 && dmi_value == 0 {
476                            self.last_dmi_read.unwrap()
477                        } else {
478                            self.dmi_op_nop()?
479                        };
480                        tracing::trace!("dmi nop 0x{:02x} 0x{:08x} op={}", addr, data, op);
481                        (addr, data, op)
482                    }
483                    DMI_OP_WRITE => {
484                        let (addr, data, op) = self.dmi_op_write(dmi_addr, dmi_value)?;
485                        tracing::trace!("dmi write 0x{:02x} 0x{:08x} op={}", addr, data, op);
486                        if dmi_addr == 0x10 && dmi_value == 0x40000001 {
487                            // needs additional sleep for a resume operation
488                            std::thread::sleep(Duration::from_millis(10));
489                        }
490                        (addr, data, op)
491                    }
492                    _ => unreachable!("unknown dmi_op {dmi_op}"),
493                };
494
495                let ret = ((addr as u128) << DMI_ADDRESS_BIT_OFFSET)
496                    | ((data as u128) << DMI_VALUE_BIT_OFFSET)
497                    | (op as u128);
498
499                let ret_bytes = ret.to_le_bytes();
500                Ok(ret_bytes
501                    .iter()
502                    .fold(BitVec::with_capacity(128), |mut acc, s| {
503                        acc.extend_from_bitslice(s.view_bits::<Lsb0>());
504                        acc
505                    }))
506            }
507            _ => unreachable!("unknown register address 0x{:08x}", address),
508        }
509    }
510
511    fn write_dr(&mut self, _data: &[u8], _len: u32) -> Result<BitVec, DebugProbeError> {
512        Err(DebugProbeError::NotImplemented {
513            function_name: "write_dr",
514        })
515    }
516
517    fn shift_raw_sequence(&mut self, _sequence: JtagSequence) -> Result<BitVec, DebugProbeError> {
518        Err(DebugProbeError::NotImplemented {
519            function_name: "shift_raw_sequence ",
520        })
521    }
522}
523
524fn get_wlink_info(device: &DeviceInfo) -> Option<DebugProbeInfo> {
525    if matches!(device.product_string(), Some("WCH-Link") | Some("WCH_Link")) {
526        Some(DebugProbeInfo::new(
527            "WCH-Link",
528            VENDOR_ID,
529            PRODUCT_ID,
530            device.serial_number().map(|s| s.to_string()),
531            &WchLinkFactory,
532            None,
533            false,
534        ))
535    } else {
536        None
537    }
538}
539
540#[tracing::instrument(skip_all)]
541fn list_wlink_devices() -> Vec<DebugProbeInfo> {
542    tracing::debug!("Searching for WCH-Link(RV) probes");
543    let devices = match nusb::list_devices().wait() {
544        Ok(devices) => devices,
545        Err(e) => {
546            tracing::warn!("error listing WCH-Link devices: {e}");
547            return vec![];
548        }
549    };
550    let probes: Vec<_> = devices
551        .filter(|device| device.vendor_id() == VENDOR_ID && device.product_id() == PRODUCT_ID)
552        .filter_map(|device| get_wlink_info(&device))
553        .collect();
554
555    tracing::debug!("Found {} WCH-Link probes total", probes.len());
556    probes
557}
558
559#[derive(thiserror::Error, Debug, docsplay::Display)]
560pub(crate) enum WchLinkError {
561    /// Unknown WCH-Link device.
562    UnknownDevice,
563    /// The firmware on the probe is outdated, and not supported by probe-rs. The minimum supported firmware version is {0}.
564    UnsupportedFirmwareVersion(&'static str),
565    /// Not enough bytes written.
566    NotEnoughBytesWritten { is: usize, should: usize },
567    /// Not enough bytes read.
568    NotEnoughBytesRead { is: usize, should: usize },
569    /// Usb endpoint not found.
570    EndpointNotFound,
571    /// Invalid payload.
572    InvalidPayload,
573    /// Protocol error.
574    Protocol(u8, Vec<u8>),
575    /// Unknown chip {0:#04x}.
576    UnknownChip(u8),
577    /// Unsupported operation.
578    UnsupportedOperation,
579}
580
581impl ProbeError for WchLinkError {}