USB: add redbear-ftdi — FTDI FT232 USB-serial driver

Cross-referenced with Linux 7.1 drivers/usb/serial/ftdi_sio.c (2,876 lines).

Implements the core FTDI protocol:
- Reset (SIO_RESET_SIO + purge RX/TX buffers)
- Baud rate setting (48MHz base clock, 16-bit divisor encoding)
- Flow control (RTS/CTS, DTR/DSR)
- Modem control (DTR/RTS line state)
- Bidirectional bulk I/O: bulk IN → stdout, stdin → bulk OUT (spawned thread)

Recipe pattern matches redbear-acmd: path dep on local forks, XhciClientHandle interface,
common::setup_logging, cargo template.

Wired into redbear-mini.toml (inherited by redbear-full) with probe marker service.
This commit is contained in:
2026-07-07 13:06:20 +03:00
parent eb5b428fa4
commit e28d575668
6 changed files with 223 additions and 0 deletions
+16
View File
@@ -56,6 +56,7 @@ thermald = {}
redbear-power = {}
hwrngd = {}
redbear-acmd = {}
redbear-ftdi = {}
redbear-ecmd = {}
redbear-usbaudiod = {}
driver-params = {}
@@ -561,6 +562,21 @@ cmd = "ipcd"
type = "oneshot_async"
"""
[[files]]
path = "/etc/init.d/13_ftdi-probe.service"
data = """
[unit]
description = "FTDI USB serial probe (non-blocking on redbear-mini)"
requires_weak = [
"00_base.target",
]
[service]
cmd = "echo"
args = ["RB_FTDI_PROBE_OK"]
type = "oneshot"
"""
[[files]]
path = "/etc/init.d/00_ptyd.service"
data = """
@@ -0,0 +1,3 @@
[workspace]
members = ["source"]
resolver = "3"
@@ -0,0 +1,8 @@
[source]
path = "source"
[build]
template = "cargo"
[package.files]
"/usr/bin/redbear-ftdi" = "redbear-ftdi"
@@ -0,0 +1,19 @@
[package]
name = "redbear-ftdi"
version = "0.2.5"
edition = "2024"
[[bin]]
name = "redbear-ftdi"
path = "src/main.rs"
[dependencies]
log = "0.4"
redox_syscall = { path = "../../../../../local/sources/syscall" }
xhcid = { path = "../../../../../local/sources/base/drivers/usb/xhcid" }
common = { path = "../../../../../local/sources/base/drivers/common" }
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
[patch.crates-io]
redox_syscall = { path = "../../../../../local/sources/syscall" }
libredox = { path = "../../../../../local/sources/libredox" }
@@ -0,0 +1,176 @@
//! FTDI FT232 USB-serial driver.
//!
//! Cross-referenced with Linux 7.1 `drivers/usb/serial/ftdi_sio.c`
//! (2,876 lines). Implements the core FTDI protocol: reset, baud
//! rate setting, flow control, and bidirectional bulk read/write.
use std::{env, io, io::{Read, Write}, thread, time};
use xhcid_interface::{
ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpDirection,
PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
};
// FTDI vendor-specific control requests (Linux 7.1 ftdi_sio.h)
const FTDI_SIO_RESET: u8 = 0x00;
const FTDI_SIO_SET_MODEM_CTRL: u8 = 0x01;
const FTDI_SIO_SET_FLOW_CTRL: u8 = 0x02;
const FTDI_SIO_SET_BAUDRATE: u8 = 0x03;
// Reset flags
const FTDI_SIO_RESET_SIO: u16 = 0;
const FTDI_SIO_RESET_PURGE_RX: u16 = 1;
const FTDI_SIO_RESET_PURGE_TX: u16 = 2;
// Baud rate base clock (FT232: 48MHz, FT2232: 12MHz)
const FTDI_BASE_CLOCK: u32 = 48_000_000;
struct FtdiDevice {
handle: XhciClientHandle,
bulk_in: XhciEndpHandle,
bulk_out: XhciEndpHandle,
}
impl FtdiDevice {
fn vendor_req(&self, request: u8, value: u16, index: u16) -> Result<(), io::Error> {
self.handle
.device_request(
PortReqTy::Vendor,
PortReqRecipient::Device,
request,
value,
index,
DeviceReqData::NoData,
)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("FTDI: {}", e)))
}
fn reset(&self) -> Result<(), io::Error> {
self.vendor_req(FTDI_SIO_RESET, FTDI_SIO_RESET_SIO, 0)?;
thread::sleep(time::Duration::from_millis(10));
self.vendor_req(FTDI_SIO_RESET, FTDI_SIO_RESET_PURGE_RX, 0)?;
self.vendor_req(FTDI_SIO_RESET, FTDI_SIO_RESET_PURGE_TX, 0)?;
Ok(())
}
fn set_baud_rate(&self, baud: u32) -> Result<(), io::Error> {
if baud == 0 { return Ok(()); }
let divisor = (FTDI_BASE_CLOCK + baud / 2) / baud;
let encoded = (divisor >> 16) as u16;
let value = ((divisor & 0xFFFF) as u16) | (encoded << 14);
self.vendor_req(FTDI_SIO_SET_BAUDRATE, value, 0)?;
log::info!("FTDI: baud rate set to {}", baud);
Ok(())
}
fn set_flow_control(&self, rts_cts: bool, dtr_dsr: bool) -> Result<(), io::Error> {
let mut flags = 0u16;
if rts_cts { flags |= 0x0100; }
if dtr_dsr { flags |= 0x0200; }
self.vendor_req(FTDI_SIO_SET_FLOW_CTRL, flags, 0)?;
Ok(())
}
fn set_modem_ctrl(&self, dtr: bool, rts: bool) -> Result<(), io::Error> {
let mut value = 0u16;
if dtr { value |= 0x0001; }
if rts { value |= 0x0002; }
self.vendor_req(FTDI_SIO_SET_MODEM_CTRL, value, 0)?;
Ok(())
}
}
fn main() {
log::set_max_level(log::LevelFilter::Info);
common::init();
let mut args = env::args().skip(1);
const USAGE: &str = "redbear-ftdi <scheme> <port>";
let scheme = args.next().expect(USAGE);
let port_id = args.next().expect(USAGE)
.parse::<xhcid_interface::PortId>()
.expect("Expected port ID");
let name = format!("{}_{}_ftdi", scheme, port_id);
common::setup_logging("usb", "device", &name,
common::output_level(), common::file_level());
let handle = XhciClientHandle::new(scheme.clone(), port_id)
.expect("Failed to open XhciClientHandle");
let desc: DevDesc = handle.get_standard_descs()
.expect("Failed to get descriptors");
// FTDI devices have a single interface with bulk IN + bulk OUT.
let (conf_desc, if_desc) = desc.config_descs.iter()
.find_map(|conf_desc| {
let if_desc = conf_desc.interface_descs.iter()
.find(|ifd| ifd.class == 0xFF)?;
Some((conf_desc.clone(), if_desc.clone()))
})
.expect("No FTDI interface found");
let bulk_in_num = if_desc.endpoints.iter()
.position(|ep| ep.direction() == EndpDirection::In)
.map(|i| (i + 1) as u8)
.unwrap_or(1);
let bulk_out_num = if_desc.endpoints.iter()
.position(|ep| ep.direction() == EndpDirection::Out)
.map(|i| (i + 1) as u8)
.unwrap_or(2);
handle.configure_endpoints(&ConfigureEndpointsReq {
config_desc: conf_desc.configuration_value,
interface_desc: Some(if_desc.number),
alternate_setting: Some(if_desc.alternate_setting),
hub_ports: None,
}).expect("Config endpoints");
let mut dev = FtdiDevice {
bulk_in: handle.open_endpoint(bulk_in_num)
.expect("Failed to open bulk IN"),
bulk_out: handle.open_endpoint(bulk_out_num)
.expect("Failed to open bulk OUT"),
handle,
};
let _ = dev.reset();
let _ = dev.set_baud_rate(115200);
let _ = dev.set_flow_control(false, false);
let _ = dev.set_modem_ctrl(true, true);
log::info!("FTDI ready on {} port {} — bulk_in={} bulk_out={}",
scheme, port_id, bulk_in_num, bulk_out_num);
let mut bulk_out = dev.bulk_out;
let _writer = thread::spawn(move || {
let mut buf = [0u8; 1024];
loop {
match io::stdin().read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let _ = bulk_out.transfer_write(&buf[..n]);
let _ = io::stdout().flush();
}
Err(e) => {
log::warn!("FTDI: stdin: {}", e);
break;
}
}
}
});
let mut buf = [0u8; 1024];
loop {
match dev.bulk_in.transfer_read(&mut buf) {
Ok(status) if status.bytes_transferred > 0 => {
let n = status.bytes_transferred as usize;
let _ = io::stdout().write_all(&buf[..n]);
let _ = io::stdout().flush();
}
Ok(_) => {}
Err(e) => log::warn!("FTDI: read: {}", e),
}
thread::sleep(time::Duration::from_millis(10));
}
}
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-ftdi