pub mod error;
use std::collections::VecDeque;
use std::io::{self, Read, Write};
use std::time::Duration;
use nusb::{
DeviceInfo, MaybeFuture,
descriptors::TransferType,
transfer::{ControlOut, ControlType, Direction, Recipient},
};
use error::FtdiError;
use tracing::{debug, trace, warn};
use crate::probe::DebugProbeError;
use crate::probe::usb_util::InterfaceExt;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ChipType {
Am,
Bm,
FT2232C,
R,
FT2232H,
FT4232H,
FT232H,
FT230X,
}
#[repr(C)]
#[expect(unused)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BitMode {
Reset = 0,
Bitbang = 1,
Mpsse = 2,
SyncBb = 4,
Mcu = 8,
Opto = 16,
Cbus = 32,
SyncFf = 64,
Ft1284 = 128,
}
#[repr(C)]
#[expect(unused)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Interface {
A = 1,
B = 2,
C = 3,
D = 4,
}
impl Interface {
fn read_ep(self) -> u8 {
match self {
Interface::A => 0x81,
Interface::B => 0x83,
Interface::C => 0x85,
Interface::D => 0x87,
}
}
fn write_ep(self) -> u8 {
match self {
Interface::A => 0x02,
Interface::B => 0x04,
Interface::C => 0x06,
Interface::D => 0x08,
}
}
fn index(&self) -> u16 {
*self as u16
}
}
struct FtdiContext {
handle: nusb::Interface,
interface: Interface,
usb_read_timeout: Duration,
usb_write_timeout: Duration,
read_queue: VecDeque<u8>,
read_buffer: Box<[u8]>,
max_packet_size: usize,
bitbang: Option<BitMode>,
}
impl FtdiContext {
fn sio_write(&mut self, request: u8, value: u16) -> Result<()> {
self.handle
.control_out(
ControlOut {
control_type: ControlType::Vendor,
recipient: Recipient::Device,
request,
value,
index: self.interface.index(),
data: &[],
},
self.usb_write_timeout,
)
.wait()
.map_err(|e| FtdiError::Usb(std::io::Error::from(e)))?;
tracing::debug!("Control transfer {:02X}/{:04X} completed", request, value);
Ok(())
}
fn usb_reset(&mut self) -> Result<()> {
const SIO_RESET_REQUEST: u8 = 0;
const SIO_RESET_SIO: u16 = 0;
self.sio_write(SIO_RESET_REQUEST, SIO_RESET_SIO)
}
fn usb_purge_tx_buffer(&mut self) -> Result<()> {
const SIO_RESET_REQUEST: u8 = 0;
const SIO_RESET_PURGE_TX: u16 = 2;
self.sio_write(SIO_RESET_REQUEST, SIO_RESET_PURGE_TX)
}
fn usb_purge_rx_buffer(&mut self) -> Result<()> {
const SIO_RESET_REQUEST: u8 = 0;
const SIO_RESET_PURGE_RX: u16 = 1;
self.sio_write(SIO_RESET_REQUEST, SIO_RESET_PURGE_RX)?;
self.read_queue.clear();
Ok(())
}
fn usb_purge_buffers(&mut self) -> Result<()> {
self.usb_purge_tx_buffer()?;
self.usb_purge_rx_buffer()?;
Ok(())
}
fn set_latency_timer(&mut self, value: u8) -> Result<()> {
const SIO_SET_LATENCY_TIMER_REQUEST: u8 = 0x09;
self.sio_write(SIO_SET_LATENCY_TIMER_REQUEST, value as u16)
}
fn set_bitmode(&mut self, bitmask: u8, mode: BitMode) -> Result<()> {
const SIO_SET_BITMODE_REQUEST: u8 = 0x0B;
self.sio_write(
SIO_SET_BITMODE_REQUEST,
u16::from_le_bytes([bitmask, mode as u8]),
)?;
self.bitbang = (mode != BitMode::Reset).then_some(mode);
Ok(())
}
fn read_data(&mut self, mut data: &mut [u8]) -> io::Result<usize> {
let mut total = 0;
while !data.is_empty() {
if !self.read_queue.is_empty() {
let read = self.read_queue.read(data).unwrap();
tracing::debug!("Copied {} bytes from queue", read);
data = &mut data[read..];
total += read;
}
if !data.is_empty() {
let read = self.handle.read_bulk(
self.interface.read_ep(),
&mut self.read_buffer,
self.usb_read_timeout,
)?;
tracing::debug!("Read {:02x?} bytes from USB", &self.read_buffer[..read]);
if read <= 2 {
break;
}
let (status, read_data) = self.read_buffer[..read].split_at(2);
tracing::debug!("Status: {:02X?} [{} data]", status, read);
let copy = read_data.len().min(data.len());
let (to_buffer, to_save) = read_data.split_at(copy);
if copy > 0 {
data[..copy].copy_from_slice(to_buffer);
data = &mut data[copy..];
tracing::debug!("Copied {} bytes from USB", copy);
total += copy;
}
if !to_save.is_empty() {
tracing::debug!("Queued {} bytes from USB", to_save.len());
self.read_queue.extend(to_save);
break;
}
}
}
tracing::debug!("read {} bytes", total);
Ok(total)
}
fn write_data(&mut self, data: &[u8]) -> io::Result<usize> {
let mut total = 0;
for chunk in data.chunks(self.max_packet_size) {
total +=
self.handle
.write_bulk(self.interface.write_ep(), chunk, self.usb_write_timeout)?;
}
tracing::debug!("wrote {} bytes", total);
Ok(total)
}
}
pub struct Builder {
interface: Interface,
read_timeout: Duration,
write_timeout: Duration,
}
impl Builder {
pub const fn new() -> Self {
Self {
interface: Interface::A,
read_timeout: Duration::from_secs(5),
write_timeout: Duration::from_secs(5),
}
}
pub const fn with_interface(mut self, interface: Interface) -> Self {
self.interface = interface;
self
}
pub const fn with_read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = timeout;
self
}
pub const fn with_write_timeout(mut self, timeout: Duration) -> Self {
self.write_timeout = timeout;
self
}
pub fn usb_open(self, usb_device: DeviceInfo) -> Result<Device, DebugProbeError> {
let mut device = Device::open(usb_device, self.interface)?;
device.context.usb_read_timeout = self.read_timeout;
device.context.usb_write_timeout = self.write_timeout;
Ok(device)
}
}
pub struct Device {
context: FtdiContext,
chip_type: Option<ChipType>,
vendor_id: u16,
product_id: u16,
product_string: Option<String>,
}
impl std::fmt::Debug for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Device")
.field("chip_type", &self.chip_type)
.finish()
}
}
impl Device {
fn open(usb_device: DeviceInfo, interface: Interface) -> Result<Self, DebugProbeError> {
fn open_error(e: std::io::Error, while_: &'static str) -> DebugProbeError {
let help = if cfg!(windows) {
"(this error may be caused by not having the WinUSB driver installed; use Zadig (https://zadig.akeo.ie/) to install it for the FTDI device; this will replace the FTDI driver)"
} else {
""
};
DebugProbeError::Usb(std::io::Error::other(format!(
"error while {while_}: {e}{help}",
)))
}
let handle = usb_device
.open()
.wait()
.map_err(|e| open_error(e.into(), "opening the USB device"))?;
let configs: Vec<_> = handle.configurations().collect();
let conf = &configs[0];
if configs.len() != 1 {
warn!("device has {} configurations, expected 1", configs.len());
if configs.len() > 1 {
let configuration = handle
.active_configuration()
.map_err(FtdiError::ActiveConfigurationError)?
.configuration_value();
if configuration != conf.configuration_value() {
handle
.set_configuration(conf.configuration_value())
.wait()
.map_err(|e| FtdiError::Usb(e.into()))?;
}
}
}
debug!("scanning {} interfaces", conf.interfaces().count());
trace!("active configuration descriptor: {:#x?}", conf);
let mut usb_interface = None;
for intf in conf.interfaces() {
trace!("interface #{} descriptors:", intf.interface_number());
for descr in intf.alt_settings() {
trace!("{:#x?}", descr);
let endpoints: Vec<_> = descr.endpoints().collect();
trace!("endpoint descriptors: {:#x?}", endpoints);
if endpoints
.iter()
.any(|ep| ep.transfer_type() != TransferType::Bulk)
{
warn!(
"encountered non-bulk endpoints, skipping interface: {:#x?}",
endpoints
);
continue;
}
let endpoint_count = endpoints.len();
let Ok::<[_; 2], _>([read_ep, write_ep]) = endpoints.try_into() else {
warn!(
"skipping interface with {} endpoints, expected 2",
endpoint_count
);
continue;
};
let (read_ep, write_ep) = if read_ep.direction() == Direction::In {
(read_ep, write_ep)
} else {
(write_ep, read_ep)
};
if read_ep.address() != interface.read_ep()
|| write_ep.address() != interface.write_ep()
{
debug!(
"interface {} does not match requested interface {:?}",
descr.interface_number(),
interface
);
continue;
}
if let Some((intf, _)) = usb_interface {
Err(FtdiError::Other(format!(
"found multiple matching USB interfaces ({} and {})",
intf,
descr.interface_number()
)))?
}
usb_interface = Some((descr.interface_number(), write_ep.max_packet_size()));
debug!("Interface is #{}", descr.interface_number());
}
}
let Some((intf, max_packet_size)) = usb_interface else {
Err(FtdiError::Other("device is not a FTDI device".to_string()))?
};
let chip_type = match (
usb_device.device_version(),
usb_device.serial_number().unwrap_or(""),
) {
(0x400, _) | (0x200, "") => Some(ChipType::Bm),
(0x200, _) => Some(ChipType::Am),
(0x500, _) => Some(ChipType::FT2232C),
(0x600, _) => Some(ChipType::R),
(0x700, _) => Some(ChipType::FT2232H),
(0x800, _) => Some(ChipType::FT4232H),
(0x900, _) => Some(ChipType::FT232H),
(0x1000, _) => Some(ChipType::FT230X),
(version, _) => {
tracing::warn!("Unknown FTDI device version: {:X?}", version);
None
}
};
let handle = handle
.detach_and_claim_interface(intf)
.wait()
.map_err(|e| open_error(e.into(), "taking control over USB device"))?;
tracing::debug!("Opened FTDI device: {:?}", chip_type);
Ok(Self {
context: FtdiContext {
handle,
interface,
usb_read_timeout: Duration::from_secs(5),
usb_write_timeout: Duration::from_secs(5),
read_queue: VecDeque::new(),
read_buffer: vec![0; max_packet_size].into_boxed_slice(),
max_packet_size,
bitbang: None,
},
chip_type,
vendor_id: usb_device.vendor_id(),
product_id: usb_device.product_id(),
product_string: usb_device.product_string().map(|s| s.to_string()),
})
}
pub fn usb_reset(&mut self) -> Result<()> {
self.context.usb_reset()
}
pub fn usb_purge_buffers(&mut self) -> Result<()> {
self.context.usb_purge_buffers()
}
pub fn set_latency_timer(&mut self, value: u8) -> Result<()> {
self.context.set_latency_timer(value)
}
pub fn set_bitmode(&mut self, bitmask: u8, mode: BitMode) -> Result<()> {
self.context.set_bitmode(bitmask, mode)
}
pub fn chip_type(&self) -> Option<ChipType> {
self.chip_type
}
pub fn vendor_id(&self) -> u16 {
self.vendor_id
}
pub fn product_id(&self) -> u16 {
self.product_id
}
pub fn product_string(&self) -> Option<&str> {
self.product_string.as_deref()
}
pub fn set_pins(&mut self, level: u16, direction: u16) -> Result<()> {
self.write_all(&[0x80, level as u8, direction as u8])?;
self.write_all(&[0x82, (level >> 8) as u8, (direction >> 8) as u8])?;
Ok(())
}
pub fn disable_loopback(&mut self) -> Result<()> {
Ok(self.write_all(&[0x85])?)
}
pub fn disable_divide_by_5(&mut self) -> Result<()> {
Ok(self.write_all(&[0x8A])?)
}
pub fn enable_divide_by_5(&mut self) -> Result<()> {
Ok(self.write_all(&[0x8B])?)
}
pub fn configure_clock_divider(&mut self, divisor: u16) -> Result<()> {
let [l, h] = divisor.to_le_bytes();
Ok(self.write_all(&[0x86, l, h])?)
}
}
impl Read for Device {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.context.read_data(buf)
}
}
impl Write for Device {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.context.write_data(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub type Result<T, E = FtdiError> = std::result::Result<T, E>;