Skip to main content

probe_rs/probe/ftdi/
mod.rs

1//! FTDI-based debug probes.
2use crate::{
3    architecture::{
4        arm::{
5            ArmCommunicationInterface, ArmDebugInterface, ArmError,
6            communication_interface::DapProbe, sequences::ArmDebugSequence,
7        },
8        riscv::{
9            communication_interface::{RiscvError, RiscvInterfaceBuilder},
10            dtm::jtag_dtm::JtagDtmBuilder,
11        },
12        xtensa::communication_interface::{
13            XtensaCommunicationInterface, XtensaDebugInterfaceState, XtensaError,
14        },
15    },
16    probe::{
17        AutoImplementJtagAccess, DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector,
18        IoSequenceItem, JtagAccess, JtagDriverState, ProbeCreationError, ProbeFactory, RawJtagIo,
19        RawSwdIo, SwdSettings, WireProtocol,
20        list::{ProbeListItem, usb_probe_accessibility},
21    },
22};
23use bitvec::prelude::*;
24use nusb::{DeviceInfo, MaybeFuture};
25use std::{
26    io::{Read, Write},
27    sync::Arc,
28    time::{Duration, Instant},
29};
30
31mod command_compacter;
32mod ftdaye;
33
34use command_compacter::Command;
35use ftdaye::{ChipType, error::FtdiError};
36
37#[derive(Debug)]
38struct JtagAdapter {
39    device: ftdaye::Device,
40    speed_khz: u32,
41
42    command: Command,
43    commands: Vec<u8>,
44
45    /// For each command that captures bits, stores how many bits are captured.
46    in_bit_counts: Vec<usize>,
47    in_bits: BitVec,
48    ftdi: FtdiProperties,
49}
50
51impl JtagAdapter {
52    /// Map a USB interface number from `--probe VID:PID-INTERFACE` to an FTDI channel.
53    ///
54    /// Multi-channel FTDI chips (FT2232H, FT4232H) expose each channel as a
55    /// separate USB interface. The mapping is: 0 = Channel A, 1 = Channel B,
56    /// 2 = Channel C, 3 = Channel D. Defaults to Channel A when no interface
57    /// is specified.
58    fn map_interface(usb_interface: Option<u8>) -> ftdaye::Interface {
59        match usb_interface {
60            Some(0) | None => ftdaye::Interface::A,
61            Some(1) => ftdaye::Interface::B,
62            Some(2) => ftdaye::Interface::C,
63            Some(3) => ftdaye::Interface::D,
64            Some(n) => {
65                tracing::warn!(
66                    "FTDI interface number {n} is out of range (0..=3), defaulting to Channel A"
67                );
68                ftdaye::Interface::A
69            }
70        }
71    }
72
73    fn open(
74        ftdi: FtdiDevice,
75        usb_device: DeviceInfo,
76        usb_interface: Option<u8>,
77    ) -> Result<Self, DebugProbeError> {
78        let interface = Self::map_interface(usb_interface);
79        let device = ftdaye::Builder::new()
80            .with_interface(interface)
81            .with_read_timeout(Duration::from_secs(5))
82            .with_write_timeout(Duration::from_secs(5))
83            .usb_open(usb_device)?;
84
85        let ftdi = FtdiProperties::try_from((ftdi, device.chip_type()))?;
86
87        Ok(Self {
88            device,
89            speed_khz: 1000,
90            command: Command::default(),
91            commands: vec![],
92            in_bit_counts: vec![],
93            in_bits: BitVec::new(),
94            ftdi,
95        })
96    }
97
98    pub fn attach(&mut self) -> Result<(), FtdiError> {
99        self.device.usb_reset()?;
100        // 0x0B configures pins for JTAG
101        self.device.set_bitmode(0x0b, ftdaye::BitMode::Mpsse)?;
102        self.device.set_latency_timer(1)?;
103        self.device.usb_purge_buffers()?;
104
105        let mut junk = vec![];
106        let _ = self.device.read_to_end(&mut junk);
107
108        let (output, direction) = self.pin_layout();
109        self.device.set_pins(output, direction)?;
110
111        self.apply_clock_speed(self.speed_khz)?;
112
113        self.device.disable_loopback()?;
114
115        Ok(())
116    }
117
118    fn pin_layout(&self) -> (u16, u16) {
119        let (output, direction) = match (
120            self.device.vendor_id(),
121            self.device.product_id(),
122            self.device.product_string().unwrap_or(""),
123        ) {
124            // Digilent HS3
125            (0x0403, 0x6014, "Digilent USB Device") => (0x2088, 0x308b),
126            // Digilent HS2
127            (0x0403, 0x6014, "Digilent Adept USB Device") => (0x00e8, 0x60eb),
128            // Digilent HS1
129            (0x0403, 0x6010, "Digilent Adept USB Device") => (0x0088, 0x008b),
130            // Built-in Digilent HS1 (on-board)
131            (0x0403, 0x6010, "Digilent USB Device") => (0x0088, 0x008b),
132            // Other devices:
133            // TMS starts high
134            // TMS, TDO and TCK are outputs
135            _ => (0x0008, 0x000b),
136        };
137        (output, direction)
138    }
139
140    fn speed_khz(&self) -> u32 {
141        self.speed_khz
142    }
143
144    fn set_speed_khz(&mut self, speed_khz: u32) -> u32 {
145        self.speed_khz = speed_khz;
146        self.speed_khz
147    }
148
149    fn apply_clock_speed(&mut self, speed_khz: u32) -> Result<u32, FtdiError> {
150        // Disable divide-by-5 mode if available
151        if self.ftdi.has_divide_by_5 {
152            self.device.disable_divide_by_5()?;
153        } else {
154            // Force enable divide-by-5 mode if not available or unknown
155            self.device.enable_divide_by_5()?;
156        }
157
158        // If `speed_khz` is not a divisor of the maximum supported speed, we need to round up
159        let is_exact = self.ftdi.max_clock.is_multiple_of(speed_khz);
160
161        // If `speed_khz` is 0, use the maximum supported speed
162        let divisor =
163            (self.ftdi.max_clock.checked_div(speed_khz).unwrap_or(1) - is_exact as u32).min(0xFFFF);
164
165        let actual_speed = self.ftdi.max_clock / (divisor + 1);
166
167        tracing::info!(
168            "Setting speed to {} kHz (divisor: {}, actual speed: {} kHz)",
169            speed_khz,
170            divisor,
171            actual_speed
172        );
173
174        self.device.configure_clock_divider(divisor as u16)?;
175
176        self.speed_khz = actual_speed;
177        Ok(actual_speed)
178    }
179
180    fn read_response(&mut self) -> Result<(), DebugProbeError> {
181        if self.in_bit_counts.is_empty() {
182            return Ok(());
183        }
184
185        let mut t0 = Instant::now();
186        let timeout = Duration::from_millis(100);
187
188        let expected_bits = std::mem::take(&mut self.in_bit_counts);
189        let reply_slots = expected_bits.len();
190        let mut reply = Vec::with_capacity(reply_slots);
191        while reply.len() < reply_slots {
192            let read = self
193                .device
194                .read_to_end(&mut reply)
195                .map_err(FtdiError::from)?;
196
197            if read > 0 {
198                t0 = Instant::now();
199            }
200
201            if t0.elapsed() > timeout {
202                tracing::warn!("Read {} bytes, expected {}", reply.len(), reply_slots);
203                return Err(DebugProbeError::Timeout);
204            }
205        }
206
207        if reply.len() != reply_slots {
208            return Err(DebugProbeError::Other(format!(
209                "Read more data than expected. Expected {} bytes, got {} bytes",
210                reply_slots,
211                reply.len()
212            )));
213        }
214
215        for (byte, count) in reply.into_iter().zip(expected_bits) {
216            let bits = byte >> (8 - count);
217            self.in_bits
218                .extend_from_bitslice(&bits.view_bits::<Lsb0>()[..count]);
219        }
220
221        Ok(())
222    }
223
224    fn flush(&mut self) -> Result<(), DebugProbeError> {
225        self.finalize_command()?;
226        self.send_buffer()?;
227        self.read_response()?;
228
229        Ok(())
230    }
231
232    fn append_command(&mut self, command: Command) -> Result<(), DebugProbeError> {
233        tracing::trace!("Appending {:?}", command);
234        // 1 byte is reserved for the send immediate command
235        if self.commands.len() + command.len() + 1 >= self.ftdi.buffer_size {
236            self.send_buffer()?;
237            self.read_response()?;
238        }
239
240        command.add_captured_bits(&mut self.in_bit_counts);
241        command.encode(&mut self.commands);
242
243        Ok(())
244    }
245
246    fn finalize_command(&mut self) -> Result<(), DebugProbeError> {
247        if let Some(command) = self.command.take() {
248            self.append_command(command)?;
249        }
250
251        Ok(())
252    }
253
254    fn shift_bit(&mut self, tms: bool, tdi: bool, capture: bool) -> Result<(), DebugProbeError> {
255        if let Some(command) = self.command.append_jtag_bit(tms, tdi, capture) {
256            self.append_command(command)?;
257        }
258
259        Ok(())
260    }
261
262    fn send_buffer(&mut self) -> Result<(), DebugProbeError> {
263        if self.commands.is_empty() {
264            return Ok(());
265        }
266
267        // Send Immediate: This will make the FTDI chip flush its buffer back to the PC.
268        // See https://www.ftdichip.com/Support/Documents/AppNotes/AN_108_Command_Processor_for_MPSSE_and_MCU_Host_Bus_Emulation_Modes.pdf
269        // section 5.1
270        self.commands.push(0x87);
271
272        tracing::trace!("Sending buffer: {:X?}", self.commands);
273
274        self.device
275            .write_all(&self.commands)
276            .map_err(FtdiError::from)?;
277
278        self.commands.clear();
279
280        Ok(())
281    }
282
283    fn read_captured_bits(&mut self) -> Result<BitVec, DebugProbeError> {
284        self.flush()?;
285
286        Ok(std::mem::take(&mut self.in_bits))
287    }
288}
289
290/// A factory for creating [`FtdiProbe`] instances.
291#[derive(Debug)]
292pub struct FtdiProbeFactory;
293
294impl std::fmt::Display for FtdiProbeFactory {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        f.write_str("FTDI")
297    }
298}
299
300impl ProbeFactory for FtdiProbeFactory {
301    fn open(&self, selector: &DebugProbeSelector) -> Result<Box<dyn DebugProbe>, DebugProbeError> {
302        // Only open FTDI-compatible probes
303        let Some(ftdi) = FTDI_COMPAT_DEVICES
304            .iter()
305            .find(|ftdi| ftdi.id == (selector.vendor_id, selector.product_id))
306            .copied()
307        else {
308            return Err(DebugProbeError::ProbeCouldNotBeCreated(
309                ProbeCreationError::NotFound,
310            ));
311        };
312
313        let devices = nusb::list_devices()
314            .wait()
315            .map_err(|e| DebugProbeError::from(FtdiError::Usb(e.into())))?;
316
317        let mut probes = devices
318            .filter(|usb_info| selector.matches(usb_info))
319            .collect::<Vec<_>>();
320
321        if probes.is_empty() {
322            return Err(DebugProbeError::ProbeCouldNotBeCreated(
323                ProbeCreationError::NotFound,
324            ));
325        } else if probes.len() > 1 {
326            tracing::warn!("More than one matching FTDI probe was found. Opening the first one.");
327        }
328
329        let probe = FtdiProbe {
330            adapter: JtagAdapter::open(ftdi, probes.pop().unwrap(), selector.interface)?,
331            jtag_state: JtagDriverState::default(),
332            swd_settings: SwdSettings::default(),
333        };
334        tracing::debug!("opened probe: {:?}", probe);
335        Ok(Box::new(probe))
336    }
337
338    fn list_probes(&self) -> Vec<ProbeListItem> {
339        list_ftdi_devices()
340    }
341
342    fn list_probes_filtered(&self, selector: Option<&DebugProbeSelector>) -> Vec<ProbeListItem> {
343        // FTDI probes are enumerated as one entry per USB device. The interface/channel
344        // (A/B/C/D) is not stored in DebugProbeInfo; it is a runtime selection passed
345        // through to open(). The default list_probes_filtered() filters by interface,
346        // which causes "no probe found" when the user specifies e.g. `--probe VID:PID-1`
347        // to select Channel B on an FT2232H, because interface is always None in the
348        // listed entries.
349        //
350        // Match only on VID, PID, and (optionally) serial number, ignoring interface.
351        self.list_probes()
352            .into_iter()
353            .filter(|probe| {
354                selector.as_ref().is_none_or(|s| {
355                    probe.info.vendor_id == s.vendor_id
356                        && probe.info.product_id == s.product_id
357                        && s.serial_number.as_ref().is_none_or(|sn| {
358                            if let Some(probe_sn) = &probe.info.serial_number {
359                                probe_sn == sn
360                            } else {
361                                sn.is_empty()
362                            }
363                        })
364                })
365            })
366            .collect()
367    }
368}
369
370/// An FTDI-based debug probe.
371#[derive(Debug)]
372pub struct FtdiProbe {
373    adapter: JtagAdapter,
374    jtag_state: JtagDriverState,
375    swd_settings: SwdSettings,
376}
377
378impl DebugProbe for FtdiProbe {
379    fn get_name(&self) -> &str {
380        "FTDI"
381    }
382
383    fn speed_khz(&self) -> u32 {
384        self.adapter.speed_khz()
385    }
386
387    fn set_speed(&mut self, speed_khz: u32) -> Result<u32, DebugProbeError> {
388        Ok(self.adapter.set_speed_khz(speed_khz))
389    }
390
391    fn attach(&mut self) -> Result<(), DebugProbeError> {
392        tracing::debug!("Attaching...");
393
394        self.adapter.attach()?;
395        self.select_target(0)
396    }
397
398    fn detach(&mut self) -> Result<(), crate::Error> {
399        Ok(())
400    }
401
402    fn target_reset(&mut self) -> Result<(), DebugProbeError> {
403        // TODO we could add this by using a GPIO. However, different probes may connect
404        // different pins (if any) to the reset line, so we would need to make this configurable.
405        Err(DebugProbeError::NotImplemented {
406            function_name: "target_reset",
407        })
408    }
409
410    fn target_reset_assert(&mut self) -> Result<(), DebugProbeError> {
411        Err(DebugProbeError::NotImplemented {
412            function_name: "target_reset_assert",
413        })
414    }
415
416    fn target_reset_deassert(&mut self) -> Result<(), DebugProbeError> {
417        Err(DebugProbeError::NotImplemented {
418            function_name: "target_reset_deassert",
419        })
420    }
421
422    fn select_protocol(&mut self, protocol: WireProtocol) -> Result<(), DebugProbeError> {
423        if protocol != WireProtocol::Jtag {
424            Err(DebugProbeError::UnsupportedProtocol(protocol))
425        } else {
426            Ok(())
427        }
428    }
429
430    fn active_protocol(&self) -> Option<WireProtocol> {
431        // Only supports JTAG
432        Some(WireProtocol::Jtag)
433    }
434
435    fn try_as_jtag_probe(&mut self) -> Option<&mut dyn JtagAccess> {
436        Some(self)
437    }
438
439    fn try_get_riscv_interface_builder<'probe>(
440        &'probe mut self,
441    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, RiscvError> {
442        Ok(Box::new(JtagDtmBuilder::new(self)))
443    }
444
445    fn has_riscv_interface(&self) -> bool {
446        true
447    }
448
449    fn into_probe(self: Box<Self>) -> Box<dyn DebugProbe> {
450        self
451    }
452
453    fn try_get_arm_debug_interface<'probe>(
454        self: Box<Self>,
455        sequence: Arc<dyn ArmDebugSequence>,
456    ) -> Result<Box<dyn ArmDebugInterface + 'probe>, (Box<dyn DebugProbe>, ArmError)> {
457        Ok(ArmCommunicationInterface::create(self, sequence, true))
458    }
459
460    fn has_arm_interface(&self) -> bool {
461        true
462    }
463
464    fn try_get_xtensa_interface<'probe>(
465        &'probe mut self,
466        state: &'probe mut XtensaDebugInterfaceState,
467    ) -> Result<XtensaCommunicationInterface<'probe>, XtensaError> {
468        Ok(XtensaCommunicationInterface::new(self, state))
469    }
470
471    fn has_xtensa_interface(&self) -> bool {
472        true
473    }
474}
475
476impl AutoImplementJtagAccess for FtdiProbe {}
477impl DapProbe for FtdiProbe {}
478
479impl RawSwdIo for FtdiProbe {
480    fn swd_io<S>(&mut self, _swdio: S) -> Result<Vec<bool>, DebugProbeError>
481    where
482        S: IntoIterator<Item = IoSequenceItem>,
483    {
484        Err(DebugProbeError::NotImplemented {
485            function_name: "swd_io",
486        })
487    }
488
489    fn swj_pins(
490        &mut self,
491        _pin_out: u32,
492        _pin_select: u32,
493        _pin_wait: u32,
494    ) -> Result<u32, DebugProbeError> {
495        Err(DebugProbeError::CommandNotSupportedByProbe {
496            command_name: "swj_pins",
497        })
498    }
499
500    fn swd_settings(&self) -> &SwdSettings {
501        &self.swd_settings
502    }
503}
504
505impl RawJtagIo for FtdiProbe {
506    fn shift_bit(
507        &mut self,
508        tms: bool,
509        tdi: bool,
510        capture_tdo: bool,
511    ) -> Result<(), DebugProbeError> {
512        self.jtag_state.state.update(tms);
513        self.adapter.shift_bit(tms, tdi, capture_tdo)?;
514        Ok(())
515    }
516
517    fn read_captured_bits(&mut self) -> Result<BitVec, DebugProbeError> {
518        self.adapter.read_captured_bits()
519    }
520
521    fn state_mut(&mut self) -> &mut JtagDriverState {
522        &mut self.jtag_state
523    }
524
525    fn state(&self) -> &JtagDriverState {
526        &self.jtag_state
527    }
528}
529
530/// Known properties associated to particular FTDI chip types.
531#[derive(Debug)]
532struct FtdiProperties {
533    /// The size of the device's RX buffer.
534    ///
535    /// We can push down this many bytes to the device in one batch.
536    buffer_size: usize,
537
538    /// The maximum TCK clock speed supported by the device, in kHz.
539    max_clock: u32,
540
541    /// Whether the device supports the divide-by-5 clock mode for "FT2232D compatibility".
542    ///
543    /// Newer devices have 60MHz internal clocks, instead of 12MHz, however, they still
544    /// fall back to 12MHz by default. This flag indicates whether we can disable the clock divider.
545    has_divide_by_5: bool,
546}
547
548impl TryFrom<(FtdiDevice, Option<ChipType>)> for FtdiProperties {
549    type Error = FtdiError;
550
551    fn try_from((ftdi, chip_type): (FtdiDevice, Option<ChipType>)) -> Result<Self, Self::Error> {
552        let chip_type = match chip_type {
553            Some(ty) => ty,
554            None => {
555                tracing::warn!("Unknown FTDI chip. Assuming {:?}", ftdi.fallback_chip_type);
556                ftdi.fallback_chip_type
557            }
558        };
559
560        let properties = match chip_type {
561            ChipType::FT2232H | ChipType::FT4232H => Self {
562                buffer_size: 4096,
563                max_clock: 30_000,
564                has_divide_by_5: true,
565            },
566            ChipType::FT232H => Self {
567                buffer_size: 1024,
568                max_clock: 30_000,
569                has_divide_by_5: true,
570            },
571            ChipType::FT2232C => Self {
572                buffer_size: 128,
573                max_clock: 6_000,
574                has_divide_by_5: false,
575            },
576            not_mpsse => {
577                tracing::warn!("Unsupported FTDI chip: {:?}", not_mpsse);
578                return Err(FtdiError::UnsupportedChipType(not_mpsse));
579            }
580        };
581
582        Ok(properties)
583    }
584}
585
586#[derive(Debug, Clone, Copy)]
587struct FtdiDevice {
588    /// The (VID, PID) pair of this device.
589    id: (u16, u16),
590
591    /// FTDI chip type to use if the device is not recognized.
592    ///
593    /// "FTDI compatible" devices may use the same VID/PID pair as an FTDI device, but
594    /// they may be implemented by a completely third party solution. In this case,
595    /// we still try the same `bcdDevice` based detection, but if it fails, we fall back
596    /// to this chip type.
597    fallback_chip_type: ChipType,
598}
599
600impl FtdiDevice {
601    fn matches(&self, device: &DeviceInfo) -> bool {
602        self.id == (device.vendor_id(), device.product_id())
603    }
604}
605
606/// Known FTDI device variants.
607static FTDI_COMPAT_DEVICES: &[FtdiDevice] = &[
608    //
609    // --- FTDI VID/PID pairs ---
610    //
611    // FTDI Ltd. FT2232C/D/H Dual UART/FIFO IC
612    FtdiDevice {
613        id: (0x0403, 0x6010),
614        fallback_chip_type: ChipType::FT2232C,
615    },
616    // FTDI Ltd. FT4232H Quad HS USB-UART/FIFO IC
617    FtdiDevice {
618        id: (0x0403, 0x6011),
619        fallback_chip_type: ChipType::FT4232H,
620    },
621    // FTDI Ltd. FT232H Single HS USB-UART/FIFO IC
622    FtdiDevice {
623        id: (0x0403, 0x6014),
624        fallback_chip_type: ChipType::FT232H,
625    },
626    //
627    // --- Third-party VID/PID pairs ---
628    //
629    // Olimex Ltd. ARM-USB-OCD
630    FtdiDevice {
631        id: (0x15ba, 0x0003),
632        fallback_chip_type: ChipType::FT2232C,
633    },
634    // Olimex Ltd. ARM-USB-TINY
635    FtdiDevice {
636        id: (0x15ba, 0x0004),
637        fallback_chip_type: ChipType::FT2232C,
638    },
639    // Olimex Ltd. ARM-USB-TINY-H
640    FtdiDevice {
641        id: (0x15ba, 0x002a),
642        fallback_chip_type: ChipType::FT2232H,
643    },
644    // Olimex Ltd. ARM-USB-OCD-H
645    FtdiDevice {
646        id: (0x15ba, 0x002b),
647        fallback_chip_type: ChipType::FT2232H,
648    },
649];
650
651fn get_device_info(device: &DeviceInfo) -> Option<ProbeListItem> {
652    FTDI_COMPAT_DEVICES.iter().find_map(|ftdi| {
653        ftdi.matches(device).then(|| ProbeListItem {
654            info: DebugProbeInfo {
655                identifier: device.product_string().unwrap_or("FTDI").to_string(),
656                vendor_id: device.vendor_id(),
657                product_id: device.product_id(),
658                serial_number: device.serial_number().map(|s| s.to_string()),
659                probe_factory: &FtdiProbeFactory,
660                is_hid_interface: false,
661                interface: None,
662            },
663            accessibility: usb_probe_accessibility(device),
664        })
665    })
666}
667
668#[tracing::instrument(skip_all)]
669fn list_ftdi_devices() -> Vec<ProbeListItem> {
670    match nusb::list_devices().wait() {
671        Ok(devices) => devices
672            .filter_map(|device| get_device_info(&device))
673            .collect(),
674        Err(e) => {
675            tracing::warn!("error listing FTDI devices: {e}");
676            vec![]
677        }
678    }
679}