Skip to main content

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