Out-of-tree Linux kernel driver suite for FTDI Hi-Speed USB chips (FT232H, FT2232H, FT4232H and variants), built as a USB interface driver with child platform devices for UART, SPI, I2C, and GPIO.
Compared to userspace alternatives (libmpsse / libftdi), the kernel
driver delivers 1.1-4.2x higher throughput with lower latency and
zero verification failures across all I2C transfer sizes at 400 kHz.
See benchmarks/ for reproducible numbers and methodology.
USB
|
ftdi_mpsse.ko USB probe, EEPROM decode, mode setup,
(usb_driver) MPSSE + bulk I/O API, module param bus_mode
|
+-------+--------+--------+-------+
| | | | |
ftdi-uart ftdi-spi ftdi-i2c ftdi-gpio ftdi-gpio-bitbang
(platform) (platform)(platform)(platform) (platform)
| | | | |
/dev/ttyFTDIx spi_ctrl i2c_adap gpio_chip gpio_chip
spidevX.Y i2c-N (MPSSE) (bit-bang)
MPSSE-capable channels (FT232H A, FT2232H A/B, FT4232H A/B):
-> ftdi-uart | ftdi-spi | ftdi-i2c (one per channel, selected by bus_mode)
-> ftdi-gpio (MPSSE SET_BITS_LOW/HIGH, 16 pins)
Non-MPSSE channels (FT4232H C/D):
-> ftdi-uart (always, hardware-fixed)
-> ftdi-gpio-bitbang (async bit-bang, 8 pins, shared with UART)
The core module (ftdi_mpsse.ko) owns the USB interface. On probe it:
- Detects the chip type from
bcdDevice - Determines MPSSE capability per channel (FT4232H C/D lack MPSSE hardware)
- Reads and decodes the on-chip EEPROM (128 words / 256 bytes)
- Selects the channel mode (see Mode Selection below); non-MPSSE channels are forced to UART mode
- Creates child platform devices via
mfd_add_hotplug_devices() - Exposes EEPROM data via sysfs (see EEPROM Sysfs below)
MPSSE-capable children communicate through exported transport functions
(ftdi_mpsse_xfer, ftdi_mpsse_write, etc.), serialised by a mutex in
the core. Non-MPSSE children (UART, bit-bang GPIO) use vendor control
messages (ftdi_mpsse_cfg_msg) and direct bulk I/O via the endpoint
accessors.
| Module | Source Files | Description |
|---|---|---|
ftdi_mpsse.ko |
ftdi_mpsse_core.c, ftdi_eeprom.c |
USB probe, EEPROM decode, MPSSE transport, mode setup |
ftdi_uart.ko |
ftdi_uart.c |
serial_core uart_port, /dev/ttyFTDIx |
ftdi_spi.ko |
ftdi_spi.c |
spi_controller with MPSSE clocking commands |
ftdi_i2c.ko |
ftdi_i2c.c |
i2c_adapter with 3-phase I2C protocol |
ftdi_gpio.ko |
ftdi_gpio.c |
gpio_chip on MPSSE AD/AC pins (16 pins) |
ftdi_gpio_bitbang.ko |
ftdi_gpio_bitbang.c |
gpio_chip via async bit-bang (8 pins, non-MPSSE channels) |
Shared definitions live in ftdi_mpsse.h (platform data, transport API,
MPSSE opcodes). Internal EEPROM definitions live in ftdi_eeprom.h
(constants, struct ftdi_eeprom, decode functions).
| Chip | Channels | MPSSE | C/D Modes | PID |
|---|---|---|---|---|
| FT232H | 1 | A | -- | 0x6014 |
| FT2232H | 2 | A, B | -- | 0x6010 |
| FT4232H | 4 | A, B | UART + bit-bang GPIO | 0x6011 |
| FT4232HA | 4 | A, B | UART + bit-bang GPIO | 0x6048 |
| FT232HP | 1 | A | -- | 0x6045 |
| FT233HP | 1 | A | -- | 0x6044 |
| FT2232HP | 2 | A, B | -- | 0x6042 |
| FT2233HP | 2 | A, B | -- | 0x6040 |
| FT4232HP | 4 | A, B | UART + bit-bang GPIO | 0x6043 |
| FT4233HP | 4 | A, B | UART + bit-bang GPIO | 0x6041 |
All chips share the FTDI vendor ID 0x0403.
FT4232H channels A and B have full MPSSE capability (SPI, I2C, UART, GPIO). Channels C and D lack the MPSSE engine -- they support UART (native async serial) and GPIO via async bit-bang mode only. This is a silicon-level constraint.
make # build against running kernel
make KDIR=/path/to/kernel/build # cross-compile or target different headers
make tools # build userspace tools
make cleanStandard kbuild variables (CROSS_COMPILE, ARCH, etc.) work.
The PID table in ftdi_mpsse.ko is empty by default to avoid
conflicting with ftdi_sio. Four methods to bind the driver:
sudo modprobe ftdi_mpsse bus_mode=spi
# Add FTDI PID at runtime
echo "0403 6014" | sudo tee /sys/bus/usb/drivers/ftdi_mpsse/new_idsudo modprobe ftdi_mpsse bus_mode=spi
# Unbind ftdi_sio from the specific interface
echo 1-2:1.0 | sudo tee /sys/bus/usb/drivers/ftdi_sio/unbind
# Override to ftdi_mpsse
echo ftdi_mpsse | sudo tee /sys/bus/usb/devices/1-2:1.0/driver_override
echo 1-2:1.0 | sudo tee /sys/bus/usb/drivers/ftdi_mpsse/bind# /etc/udev/rules.d/99-ftdi-mpsse.rules
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0403", \
ATTR{idProduct}=="6014", \
RUN+="/bin/sh -c 'echo ftdi_mpsse > %S%p/%k:1.0/driver_override'"Use ftdi_eeprom to program a custom PID into the device EEPROM,
then add that PID to a static table or use new_id.
After binding, load child drivers:
sudo modprobe ftdi_gpio
sudo modprobe ftdi_spi # if bus_mode includes SPI
sudo modprobe ftdi_i2c # if bus_mode includes I2C
sudo modprobe ftdi_uart # if bus_mode=uartThe driver decides which child devices to create by evaluating two sources, in order of priority:
bus_modemodule parameter -- always wins if set to a known value- EEPROM channel type -- consulted when
bus_modeis empty (auto)
Set at module load time. Applies identically regardless of what the EEPROM is set:
sudo modprobe ftdi_mpsse bus_mode=spi # force SPI + GPIO
sudo modprobe ftdi_mpsse bus_mode=uart # force UART + GPIO
sudo modprobe ftdi_mpsse bus_mode=i2c # force I2C + GPIO
sudo modprobe ftdi_mpsse # auto (see below)Comma-separated values select per-interface modes on multi-channel chips (FT2232 and FT4232, not FT232):
# FT2232H: channel A = SPI, channel B = UART
sudo modprobe ftdi_mpsse bus_mode=spi,uart
# FT4232H: all 4 channels explicit (C/D only accept uart)
sudo modprobe ftdi_mpsse bus_mode=spi,i2c,uart,uartFT4232H accepts up to 4 comma-separated values. Channels C and D only
support uart -- if a non-UART value is specified for C/D, the driver
emits a warning and forces UART mode.
bus_mode= |
Child Devices Created | Mode | Use Case |
|---|---|---|---|
uart |
ftdi-uart, ftdi-gpio | Reset (UART native) | Serial port + MPSSE GPIO |
spi |
ftdi-spi, ftdi-gpio | MPSSE | SPI master + GPIO |
i2c |
ftdi-i2c, ftdi-gpio | MPSSE | I2C adapter + GPIO |
uart (C/D) |
ftdi-uart, ftdi-gpio-bitbang | Reset (UART native) | Serial port + bit-bang GPIO |
| (empty) | auto -- see below | depends | EEPROM-driven or MPSSE fallback |
When bus_mode is empty (the default), the driver reads the EEPROM
and applies a multi-level detection algorithm:
| EEPROM Channel Type | Value | Behaviour |
|---|---|---|
UART (factory) |
0x00 |
Enters UART mode, creates ftdi-uart + ftdi-gpio |
FIFO |
0x01 |
Rejected (-ENODEV): mode not supported |
OPTO |
0x02 |
Rejected (-ENODEV): mode not supported |
CPU |
0x04 |
Rejected (-ENODEV): mode not supported |
FT1284 |
0x08 |
Rejected (-ENODEV): mode not supported |
| (empty/unknown) | -- | Proceeds to protocol detection (Step 2) |
When the channel type is unknown or empty, the driver applies heuristics in priority order in order to guess the user's settings:
| Priority | Source | Detection Logic |
|---|---|---|
| 1 | Protocol hint byte (0x1A) | 'S'=SPI, 'I'=I2C, 'U'=UART |
| 2 | VCP driver bit | If VCP enabled -> UART (device is a COM port) |
| 3 | Product string | Contains "SPI" -> SPI; contains "I2C" -> I2C |
| 4 | Electrical settings | Slow slew + Schmitt -> I2C (open-drain optimized) |
| 5 | Default | SPI (most common MPSSE use case) |
The default is currently SPI-only. Use the protocol hint byte ('I') or bus_mode=i2c
parameter to select I2C mode instead.
The EEPROM channel type is set per-channel by FTDI programming tools
(ftdi_eeprom). Most factory-fresh devices ship with cha_type=UART or
with an empty EEPROM.
The FT4232H is a special case: channels A and B store their channel type in the EEPROM (byte 0x00 and 0x01, same as FT2232H) and can be configured as UART/FIFO/OPTO/CPU/FT1284. Channels C and D are hardware-fixed to UART regardless of EEPROM contents -- by design, they lack the MPSSE engine entirely.
The FTDI EEPROM has no native field for SPI vs I2C mode -- both use the
MPSSE engine. To provide explicit control, program a single byte at
offset 0x1A in the EEPROM user area:
| Value | Char | Mode |
|---|---|---|
| 0x53 | S |
SPI only (ftdi-spi + ftdi-gpio) |
| 0x49 | I |
I2C only (ftdi-i2c + ftdi-gpio) |
| 0x55 | U |
UART only (ftdi-uart + ftdi-gpio) |
| other | -- | Auto-detect via heuristics |
-
Create a 1-byte file containing the hint character:
echo -n 'S' > protocol_hint.bin # For SPI mode # or echo -n 'I' > protocol_hint.bin # For I2C mode
-
Add to your
ftdi_eepromconfiguration:# ft232h.conf vendor_id=0x0403 product_id=0x6014 manufacturer="ACME" product="SPI Adapter" # Also detected: "SPI" in product string # Protocol hint in user area user_data_addr=0x1a user_data_file="protocol_hint.bin" # Recommended electrical settings for SPI group0_schmitt=true group0_slew=fast group1_schmitt=true group1_slew=fast filename=ft232h.bin
-
Flash the EEPROM:
sudo ftdi_eeprom --flash-eeprom ft232h.conf
If you don't want to use the user area, include "SPI" or "I2C" in the product string:
product="My SPI Adapter" # Driver detects "SPI" -> SPI mode
product="I2C Bridge v2" # Driver detects "I2C" -> I2C modeIf no protocol hint or product string keyword is found, the driver examines the ADBUS electrical settings:
- Slow slew + Schmitt trigger -> likely a I2C mode (open-drain optimized)
- Fast slew -> likely a SPI mode (high-frequency clock optimized)
This heuristic reflects best practices: I2C benefits from slow edges and noise immunity, while SPI benefits from fast edges for higher clock rates.
To have the driver automatically choose UART mode without passing
bus_mode=uart, program the EEPROM channel type to UART:
# ft232h.conf for ftdi_eeprom
cha_type=UARTTo have it use SPI mode automatically, either:
- Set the protocol hint byte to
'S'(0x53) at offset 0x1A - Include "SPI" in the product string
- Use fast slew rate settings (driver will default to SPI)
To have it use I2C mode automatically, either:
- Set the protocol hint byte to
'I'(0x49) at offset 0x1A - Include "I2C" in the product string
- Use slow slew + Schmitt trigger settings
For predictable behaviour across different EEPROM states, always set
bus_mode explicitly.
On multi-channel chips, each channel is configured independently:
| Chip | Channels | MPSSE-Capable | Configuration |
|---|---|---|---|
| FT232H | 1 | A | Single mode: SPI xor I2C xor UART |
| FT2232H | 2 | A, B | Per-channel: e.g., A=SPI, B=I2C |
| FT4232H | 4 | A, B | A/B: SPI/I2C/UART; C/D: UART + bit-bang GPIO only |
SPI and I2C are mutually exclusive per channel because they use different MPSSE clocking modes (SPI uses edge-triggered, I2C uses 3-phase clocking). However, different channels on the same chip can use different protocols.
Example: FT2232H with channel A for SPI and channel B for I2C:
sudo modprobe ftdi_mpsse bus_mode=spi,i2cExample: FT4232H with A=SPI, B=I2C, C/D=UART:
sudo modprobe ftdi_mpsse bus_mode=spi,i2c,uart,uartThe EEPROM protocol hint at offset 0x1A applies to the channel associated with each USB interface (interface 0 = channel A, interface 1 = channel B). Channels C and D ignore EEPROM protocol hints and are always UART.
The UART child driver (ftdi_uart.ko) provides a full-featured
serial port on /dev/ttyFTDIx:
- Baud rates: 300 to 12 Mbaud (Hi-Speed divisor for >= 1200, BM divisor for < 1200)
- Data bits: 7, 8 (CS5 accepted as smartcard quirk; CS6 rejected)
- Parity: None, Odd, Even, Mark, Space (
CMSPAR) - Stop bits: 1, 2
- Flow control: RTS/CTS, DTR/DSR (sysfs), XON/XOFF (termios)
- Modem signals: DTR, RTS output; CTS, DSR, RI, DCD input
- RS-485: hardware TXDEN via CBUS pin (configured in EEPROM, auto-detected at probe;
TIOCSRS485ioctl reports error if no TXDEN pin is configured) - CBUS GPIO: FT232H exposes up to 4 CBUS pins as a
gpio_chip(pin availability determined by EEPROM configuration) - Double-buffered writes: 2 write URBs for pipelined TX
sysfs attributes (under the platform device):
| Attribute | Type | Default | Description |
|---|---|---|---|
latency_timer |
1-255 | 16 | RX buffer timeout in ms |
dtr_dsr_flow |
0 or 1 | 0 | Enable DTR/DSR hardware flow control |
The core module exposes EEPROM data via sysfs on the USB interface
device (e.g. /sys/bus/usb/devices/1-2:1.0/). Attributes are
created at probe time and are read-only.
Each USB interface exposes the EEPROM attributes for its own channel. The EEPROM is physically one per chip (256 bytes shared by all channels), but since each interface probes independently, the attributes are per-interface:
eeprom_channel_typeon interface 0 shows channel A's typeeeprom_channel_typeon interface 1 shows channel B's typeeeprom_channel_typeon interface 2 shows channel C's type (FT4232H)eeprom_channel_typeon interface 3 shows channel D's type (FT4232H)
Chip-wide fields (eeprom_vid, eeprom_pid, the raw eeprom binary,
etc.) are identical on all interfaces -- they read the same physical
EEPROM.
| Attribute | Format | Example | Description |
|---|---|---|---|
eeprom_valid |
%d\n |
1 |
1 if EEPROM was read successfully |
eeprom_empty |
%d\n |
0 |
1 if all bytes are 0xFF (blank EEPROM) |
eeprom_checksum |
%s\n |
ok |
ok, bad, or n/a (empty/invalid) |
eeprom_vid |
0x%04x\n |
0x0403 |
Vendor ID from EEPROM |
eeprom_pid |
0x%04x\n |
0x6014 |
Product ID from EEPROM |
eeprom_channel_type |
%s\n |
uart |
This interface's channel type: uart, fifo, opto, cpu, ft1284 |
eeprom_self_powered |
%d\n |
0 |
1 if self-powered flag set |
eeprom_remote_wakeup |
%d\n |
0 |
1 if remote wakeup flag set |
eeprom_max_power |
%u\n |
500 |
Maximum power draw in mA |
eeprom_cbus |
multi-line | CBUS0: tristate\nCBUS1: txled\n |
Per-pin CBUS function (FT232H: 10 pins) |
eeprom_drive |
multi-line | group0: 4mA slew=fast schmitt=off\n |
Drive strength, slew rate, Schmitt trigger |
eeprom_config |
INI format | vendor_id=0x0403\nproduct_id=0x6014\n... |
ftdi_eeprom-compatible config with # warnings |
# Quick read of all EEPROM attributes
for f in /sys/bus/usb/devices/1-2:1.0/eeprom_*; do
echo "$(basename $f): $(cat $f)"
done| Attribute | Size | Description |
|---|---|---|
eeprom |
256 B | Raw EEPROM contents (little-endian, as read from hardware) |
The binary dump is compatible with ftdi_eeprom:
# Dump raw EEPROM to file
cat /sys/bus/usb/devices/1-2:1.0/eeprom > eeprom_backup.bin
hexdump -C eeprom_backup.bin
# Compare with ftdi_eeprom decode
ftdi_eeprom --read-eeprom ft232h.conf
diff <(xxd eeprom_backup.bin) <(xxd ft232h.bin)The eeprom_config attribute outputs an INI-format configuration compatible
with the ftdi_eeprom tool's config file parser. Lines starting with #
are warnings for sub-optimal settings (Schmitt trigger off, fast slew rate):
cat /sys/bus/usb/devices/1-2:1.0/eeprom_configExample output (FT232H):
vendor_id=0x0403
product_id=0x6014
self_powered=false
remote_wakeup=false
max_power=90
use_serial=true
suspend_pull_downs=false
cha_type=UART
cha_vcp=true
group0_drive=4
group0_schmitt=true
group0_slew=slow
group1_drive=4
group1_schmitt=false
# WARNING: Schmitt trigger off on group1 (ADBUS)
group1_slew=fast
# WARNING: slew rate not limited on group1 (ADBUS)
cbush0=TRISTATE
...Example output (FT4232H, from interface 2 = channel C):
vendor_id=0x0403
product_id=0x6011
self_powered=false
remote_wakeup=false
max_power=500
use_serial=true
suspend_pull_downs=false
chc_type=UART
chc_vcp=true
chc_rs485=false
group0_drive=4
group0_schmitt=true
group0_slew=slow
group1_drive=4
group1_schmitt=true
group1_slew=slowWhen the I2C child driver (ftdi_i2c.ko) probes, it checks the EEPROM
drive settings on the data bus (ADBUS) and emits kernel messages for
sub-optimal configurations:
| Condition | Severity | Message |
|---|---|---|
| Schmitt trigger off | dev_warn |
Schmitt trigger OFF on data bus -- I2C requires Schmitt |
| Drive current <= 4 mA | dev_notice |
data bus drive current is N mA -- consider 8 or 16 mA |
| Fast slew rate | dev_notice |
fast slew rate on data bus -- slow slew recommended |
| Suspend pull-downs enabled | dev_notice |
suspend_pull_downs enabled -- may conflict with pull-ups |
Check with: dmesg | grep "EEPROM.*I2C\|EEPROM.*data bus\|EEPROM.*suspend"
When the UART child driver (ftdi_uart.ko) probes, it checks the EEPROM
drive settings on the data bus (ADBUS) and emits kernel messages for
sub-optimal configurations:
| Condition | Severity | Message |
|---|---|---|
| Schmitt trigger off | dev_warn |
Schmitt trigger OFF -- UART requires Schmitt for RX |
| Drive current < 8 mA | dev_notice |
data bus drive current is N mA -- 8 mA recommended |
| Suspend pull-downs enabled | dev_notice |
suspend_pull_downs -- may cause break on TXD |
| VCP not enabled (FT232H) | dev_notice |
VCP not enabled -- may not appear as serial port |
At runtime, set_termios checks the slew rate against the baud rate
(once per device instance):
| Condition | Severity | Message |
|---|---|---|
| Slow slew at baud > 1 Mbaud | dev_notice |
slow slew rate -- consider fast slew for >1 Mbaud |
| Fast slew at baud <= 1 Mbaud | dev_notice |
fast slew rate -- slow slew recommended |
Check with: dmesg | grep "EEPROM.*UART\|EEPROM.*data bus\|EEPROM.*slew\|EEPROM.*VCP\|EEPROM.*suspend"
When the SPI child driver (ftdi_spi.ko) probes, it checks the EEPROM
drive settings on the data bus (ADBUS) and emits kernel messages for
sub-optimal configurations:
| Condition | Severity | Message |
|---|---|---|
| Schmitt trigger off | dev_warn |
Schmitt trigger OFF -- SPI requires Schmitt for MISO |
| Suspend pull-downs enabled | dev_warn |
suspend_pull_downs -- all CS# pulled low during suspend |
| VCP driver enabled | dev_notice |
VCP enabled -- may prevent MPSSE access for SPI |
At runtime, transfer_one_message checks slew rate and drive current
against the SPI clock speed (once per controller):
| Condition | Severity | Message |
|---|---|---|
| Slow slew at speed > 15 MHz | dev_notice |
slow slew rate -- consider fast slew for >15 MHz |
| Fast slew at speed <= 5 MHz | dev_notice |
fast slew rate -- slow slew recommended |
| Drive < 12 mA at speed > 20 MHz | dev_notice |
drive N mA -- 12+ mA recommended for >20 MHz |
Check with: dmesg | grep "EEPROM.*SPI\|EEPROM.*data bus\|EEPROM.*slew\|EEPROM.*CS"
| Name | Value | Description |
|---|---|---|
tristate |
0x00 | High-impedance (default) |
txled |
0x01 | Pulses low during TX |
rxled |
0x02 | Pulses low during RX |
txrxled |
0x03 | Pulses low during TX or RX |
pwren |
0x04 | Low when USB active |
sleep |
0x05 | Low during USB suspend |
drive_0 |
0x06 | Output driven low |
drive_1 |
0x07 | Output driven high |
iomode |
0x08 | GPIO (bit-bang mode) |
txden |
0x09 | RS-485 transmit enable |
clk30 |
0x0A | 30 MHz clock output |
clk15 |
0x0B | 15 MHz clock output |
clk7_5 |
0x0C | 7.5 MHz clock output |
The core module exposes a read-only pinmap attribute on the USB
interface device (alongside the eeprom_* attributes) that shows the
kernel's active pin configuration at a glance:
cat /sys/bus/usb/devices/1-2:1.0/pinmapThe output includes the chip type, channel letter, active mode, bus-specific pin assignments with registered subsystem device names, and per-pin GPIO availability.
chip: FT232H
channel: A
mode: spi
spi: spi2
AD0 SCK
AD1 MOSI
AD2 MISO
AD3 CS0
AD4 CS1
gpio: gpiochip496
AD0 reserved (spi2)
AD1 reserved (spi2)
AD2 reserved (spi2)
AD3 reserved (spi2)
AD4 reserved (spi2)
AD5 available
AD6 available
AD7 available
AC0 available
...
AC7 available
chip: FT232H
channel: A
mode: i2c
i2c: i2c-8
AD0 SCL
AD1 SDA_OUT
AD2 SDA_IN
gpio: gpiochip496
AD0 reserved (i2c-8)
AD1 reserved (i2c-8)
AD2 reserved (i2c-8)
AD3 available
AD4 available
AD5 available
AD6 available
AD7 available
AC0 available
...
AC7 available
chip: FT232H
channel: A
mode: uart
uart: ttyFTDI0
AD0 TXD
AD1 RXD
AD2 RTS
AD3 CTS
AD4 DTR
AD5 DSR
AD6 DCD
AD7 RI
gpio: gpiochip496
AD0 reserved (ttyFTDI0)
AD1 reserved (ttyFTDI0)
...
AD7 reserved (ttyFTDI0)
AC0 available
AC1 available
...
AC7 available
chip: FT4232H
channel: C
mode: uart (non-MPSSE)
uart: ttyFTDI2
DBUS0 TXD
DBUS1 RXD
DBUS2 RTS
DBUS3 CTS
DBUS4 DTR
DBUS5 DSR
DBUS6 DCD
DBUS7 RI
gpio-bitbang: gpiochip504
DBUS0 reserved (ttyFTDI2)
DBUS1 reserved (ttyFTDI2)
DBUS2 available
DBUS3 available
DBUS4 available
DBUS5 available
DBUS6 available
DBUS7 available
Some UART features (RS-485 TXDEN, CBUS GPIO) depend on CBUS pin
assignments stored in the device EEPROM. The EEPROM can be
programmed with ftdi_eeprom.
The FT232H has 10 ACBUS pins (ACBUS0-ACBUS9). Each pin can be assigned one of these functions in the EEPROM:
| Function | Value | Description |
|---|---|---|
TRISTATE |
0x00 | Input, pull-up (default) |
TXLED |
0x01 | Pulses low during TX |
RXLED |
0x02 | Pulses low during RX |
TXRXLED |
0x03 | Pulses low during TX or RX |
PWREN |
0x04 | Low during USB suspend |
SLEEP |
0x05 | Low during USB suspend |
DRIVE_0 |
0x06 | Output low |
DRIVE_1 |
0x07 | Output high |
IOMODE |
0x08 | GPIO (bit-bang mode) -- used by CBUS GPIO driver |
TXDEN |
0x09 | RS-485 transmit enable -- asserted during TX |
CLK30 |
0x0A | 30 MHz clock output |
CLK15 |
0x0B | 15 MHz clock output |
CLK7_5 |
0x0C | 7.5 MHz clock output |
The driver reads the full EEPROM at probe and:
- Uses the channel type for auto-mode selection (see Mode Selection)
- Exposes decoded fields and raw dump via sysfs (see EEPROM Sysfs)
- Provides CBUS config to the UART child (pins set to
IOMODEbecome GPIO, pins set toTXDENenable RS-485 support)
Create a configuration file (e.g. ft232h.conf):
vendor_id=0x0403
product_id=0x6014
manufacturer="FTDI"
product="FT232H"
serial="12345678"
max_power=500
self_powered=false
# CBUS pin assignments (FT232H uses cbush0-cbush9)
cbush0=TRISTATE
cbush1=TRISTATE
cbush2=TRISTATE
cbush3=TRISTATE
cbush4=TXDEN # RS-485 transmit enable
cbush5=IOMODE # GPIO pin 0
cbush6=IOMODE # GPIO pin 1
cbush7=TRISTATE
cbush8=IOMODE # GPIO pin 2
cbush9=IOMODE # GPIO pin 3
# Channel configuration
cha_type=UART
cha_rs485=true
filename=ft232h.binFlash the EEPROM (requires ftdi_sio to be unloaded):
sudo ftdi_eeprom --flash-eeprom ft232h.confUnplug and replug the device for the new configuration to take effect.
tools/ftdi_eeprom_check checks whether the EEPROM of connected FTDI
devices is empty (erased). It uses raw Linux USB devfs ioctls and
has no userspace library dependencies (no libusb, no libftdi).
Build with make tools.
sudo tools/ftdi_eeprom_check# Child devices
ls /sys/bus/platform/devices/ftdi-*
# GPIO
gpioinfo | grep ftdi
# SPI
ls /sys/class/spi_master/
# I2C
i2cdetect -l
sudo i2cdetect -y <bus_number>
# UART
ls /dev/ttyFTDI*The SPI child driver (ftdi_spi.ko) provides a spi_controller with:
- SPI modes 0/1/2/3 (all CPOL/CPHA combinations)
- Clock speed: 30 MHz max, ~457 Hz min (60 MHz base, 16-bit divisor)
- Full duplex, TX-only, and RX-only transfers
- Sub-byte transfers: 1-8 bits per word (MPSSE bit-mode clocking)
- SPI_CS_HIGH: per-device active-high chip select polarity
- Configurable chip selects: 1-5 CS lines on AD3-AD7, per-channel on multi-channel chips (see
spi_csbelow) - Multi-transfer batching:
transfer_one_messageassembles MPSSE commands into a single buffer for fewer USB round-trips - Per-transfer speed: clock divisor updated only when speed changes
- LSB-first bit order (
SPI_LSB_FIRST) - Loopback (
SPI_LOOP) for self-test
Module parameters (set at load time on ftdi_mpsse.ko):
| Parameter | Type | Default | Description |
|---|---|---|---|
spi_cs |
string | "3" |
SPI CS AD pin numbers (3-7). Global or per-channel. Remaining AD pins available as GPIO. |
Global syntax applies to all SPI channels: spi_cs=3,4,5 assigns
CS0=AD3, CS1=AD4, CS2=AD5; AD6-AD7 become GPIO pins.
Per-channel syntax for multi-channel chips (FT2232H, FT4232H):
spi_cs=A:3,4,5;B:3 gives channel A three CS lines and channel B one.
If a channel is not listed, it defaults to CS0=AD3.
The I2C child driver (ftdi_i2c.ko) provides an i2c_adapter with:
- Clock speed: 10 kHz to 3.4 MHz (continuous, not just 100/400 kHz)
- 7-bit and 10-bit addressing (
I2C_FUNC_10BIT_ADDR) - Hardware open-drain on FT232H (MPSSE opcode 0x9E); software open-drain emulation on FT2232H/FT4232H
- Command batching: one USB round-trip per I2C message
- Clock stretching via MPSSE adaptive clocking (requires AD3 wired to SCL)
- Proper repeated START between multi-message transfers
- Bus recovery: 9-pulse SCL toggle via
i2c_generic_scl_recovery - SMBus emulation (
I2C_FUNC_SMBUS_EMUL)
Module parameters (set at load time):
| Parameter | Type | Default | Description |
|---|---|---|---|
i2c_speed |
uint | 100 | I2C bus speed in kHz (10-3400) |
clock_stretching |
bool | 0 | Enable adaptive clocking for clock stretching |
The MPSSE GPIO driver provides a 16-pin gpio_chip on channels with
MPSSE hardware (FT232H A, FT2232H A/B, FT4232H A/B):
- 16 pins: AD0-AD7 (low byte) + AC0-AC7 (high byte)
- Hardware state sync: pin values read from MPSSE at probe (no stale-zero writes or spurious IRQ edges)
- Open-drain on FT232H: per-pin
PIN_CONFIG_DRIVE_OPEN_DRAIN/PIN_CONFIG_DRIVE_PUSH_PULLviaset_config - Reserved pin masking: the parent (
ftdi_mpsse.ko) passes a per-modegpio_reserved_maskvia platform_data when creating the GPIO child --0x0007for I2C (AD0-AD2),0x0000for UART, and a dynamic mask for SPI covering AD0-AD2 (SCK/MOSI/MISO) plus each CS pin (e.g.0x000fwith CS0=AD3,0x003fwith CS0-CS2=AD3-AD5).init_valid_maskhides reserved pins from the GPIO core so userspace cannot access bus-driver pins. - Polled edge-triggered IRQ: 1 ms delayed_work polling, rising/falling/both
- Per-device quirks: custom pin names and direction overrides
- Transport: MPSSE
SET_BITS_LOW/SET_BITS_HIGHandGET_BITS_LOW/GET_BITS_HIGHopcodes
The bit-bang GPIO driver provides an 8-pin gpio_chip on FT4232H
channels C and D (which lack MPSSE hardware):
- 8 pins: DBUS0-DBUS7 (one byte, no high byte)
- Async bit-bang mode: uses
SET_BITMODEvendor command (bitmode 0x01) with direct bulk writes, not MPSSE opcodes - Reserved pin masking: TXD/RXD (DBUS0-DBUS1) are always reserved; DBUS2-DBUS7 are available as GPIO
- Shares bulk endpoints with the UART driver on the same channel
- Lower bandwidth than MPSSE GPIO (one USB round-trip per read/write vs batched MPSSE commands)
Low byte (AD0-AD7):
UART mode: AD0=TXD AD1=RXD AD2=RTS AD3=CTS AD4=DTR AD5=DSR AD6=DCD AD7=RI
SPI mode: AD0=SCK AD1=MOSI AD2=MISO AD3=CS0 AD4-AD7=GPIO/CS1-CS4
I2C mode: AD0=SCL AD1=SDA_OUT AD2=SDA_IN AD3-AD7=GPIO
High byte (AC0-AC7):
All modes: AC0-AC7 available as GPIO (MPSSE SET_BITS_HIGH)
DBUS0-DBUS7 (8 pins per channel, no high byte):
UART: DBUS0=TXD DBUS1=RXD DBUS2=RTS DBUS3=CTS
DBUS4=DTR DBUS5=DSR DBUS6=DCD DBUS7=RI
Bit-bang: DBUS0-DBUS7 (pins used by UART are reserved)
Channels C and D have no AC-bus pins. Bit-bang GPIO pins that overlap with active UART signals are masked as reserved.
- GPIO cache coherency: If a
set_bankUSB write fails, the software cache diverges from hardware. The next successful write corrects both values. - Per-interface mode via EEPROM only: The
bus_modeparameter supports comma-separated per-interface values (spi,uart), and auto mode reads the EEPROM channel type per-channel. - GPIO clobbers sibling pins:
set_bankrewrites the entire 8-bit bank, overwriting pins owned by SPI/I2C. Mitigated byinit_valid_maskpreventing direct GPIO access to reserved pins. - Bit-bang GPIO bandwidth: On FT4232H channels C/D, GPIO uses async bit-bang mode over USB bulk endpoints. Each read or write is a separate USB round-trip, unlike MPSSE GPIO which can batch multiple pin operations into one transfer.
- Bit-bang GPIO / UART endpoint sharing: On FT4232H channels C/D, bit-bang GPIO and UART share the same bulk endpoints. The bus_lock mutex serialises access.
- I2C speed limited to 400 kHz (fast mode). Higher modes are TODO:
- Fast-mode plus (Fm+, 1 MHz): requires pull-up resistors < 1 kΩ and higher bus drive current (20 mA). The MPSSE clock divisor supports it but the driver has not been validated at this speed.
- High-speed mode (HS, 3.4 MHz): requires a master code byte (00001xxx) sent in fast mode before switching to HS clock. The driver does not implement the HS-mode entry protocol.
- I2C 10-bit addressing: advertised (
I2C_FUNC_10BIT_ADDR) but not yet validated with a real 10-bit slave. TODO: patch Zephyr's DW I2C target driver (i2c_dw_set_slave_mode) to support 10-bit, then test with RP2040 EEPROM target at a 10-bit address.
This driver suite is independent of the in-kernel ftdi_sio USB serial
driver. It targets the same FTDI Hi-Speed chips but exposes their MPSSE
engine as SPI, I2C, and GPIO subsystem devices -- functionality that
ftdi_sio does not provide. For FT4232H, this driver also handles the
non-MPSSE channels C/D (UART + bit-bang GPIO), providing unified
management of all 4 interfaces under a single driver.
This section explains how the driver works internally, for developers who want to understand, modify, or extend the code.
FTDI Hi-Speed chips contain an MPSSE (Multi-Protocol Synchronous Serial Engine) -- a command-response processor inside the USB device. The host communicates with it by:
- Writing MPSSE opcodes + arguments to the USB bulk OUT endpoint
- Reading results from the USB bulk IN endpoint
Every bulk IN packet from FTDI hardware is prefixed with 2 modem-status
bytes (byte 0 = modem status, byte 1 = line status). These appear in
every response, even status-only packets with no payload data. The core
transport (ftdi_mpsse_bulk_read in ftdi_mpsse_core.c) strips these
bytes transparently -- child drivers never see them.
The MPSSE engine supports different clocking modes that are mutually exclusive per channel:
- SPI: edge-triggered clocking (opcodes 0x10-0x36), CLK/5 disabled for 60 MHz base clock
- I2C: 3-phase clocking (opcode 0x8C), open-drain (opcode 0x9E on FT232H), with SCL/SDA on AD0/AD1-AD2
- UART: not MPSSE at all -- the chip's native async serial path, set by resetting bitmode to 0x00
By design, a single MPSSE channel cannot run SPI and I2C simultaneously because they require different clocking configurations.
userspace write(/dev/spidevX.Y, buf, len)
-> spi_sync() -> spi_controller.transfer_one_message()
-> ftdi_spi_transfer_one_message() [ftdi_spi.c]
1. ftdi_mpsse_bus_lock() -- acquire cross-driver mutex
2. Build MPSSE command buffer:
DISABLE_CLK_DIV5 -- 60 MHz base clock
SET_CLK_DIVISOR(div) -- freq = 60M / ((1+div)*2)
SET_BITS_LOW(val, dir) -- configure SCK/MOSI/CS pins
CS assert (SET_BITS_LOW) -- drive CS# low
CLK_BYTES_OUT/IN/INOUT -- clock data (opcode+len+data)
CS deassert (SET_BITS_LOW) -- drive CS# high
3. ftdi_mpsse_xfer(tx_buf, tx_len, rx_buf, rx_len)
-> ftdi_mpsse_bulk_write() -- USB bulk OUT
-> ftdi_mpsse_bulk_read() -- USB bulk IN (strips 2 status bytes)
4. ftdi_mpsse_bus_unlock()
The main optimisation: transfer_one_message batches all MPSSE commands for
an entire spi_message (multiple transfers) into a single buffer before
flushing, minimising USB round-trips.
userspace ioctl(fd, I2C_RDWR, &msgs)
-> i2c_transfer() -> i2c_algorithm.xfer()
-> ftdi_i2c_xfer() [ftdi_i2c.c]
1. ftdi_mpsse_bus_lock()
2. For each i2c_msg:
START or repeated-START condition (SET_BITS_LOW sequence)
Address byte (CLK_BITS_OUT + read ACK via CLK_BITS_IN)
Data bytes:
Write: CLK_BITS_OUT + read ACK
Read: CLK_BITS_IN + send ACK/NACK
SEND_IMMEDIATE
3. ftdi_mpsse_xfer() -> single USB round-trip per message
4. Parse response: check ACK bits, copy read data
5. STOP condition (SET_BITS_LOW sequence)
6. ftdi_mpsse_bus_unlock()
The I2C driver uses 3-phase clocking (opcode 0x8C) which inserts an extra clock phase for the I2C ACK/NACK bit. Open-drain is handled differently per chip:
- FT232H: hardware open-drain via MPSSE opcode 0x9E (
DRIVE_ZERO_ONLY) - FT2232H/FT4232H: software open-drain -- drive low for '0', switch pin to input (tristate) for '1', relying on external pull-ups
userspace write(/dev/ttyFTDIx, buf, len)
-> tty_write() -> uart_write() -> start_tx()
-> ftdi_uart_start_tx() [ftdi_uart.c]
1. Dequeue data from tty tx FIFO
2. usb_fill_bulk_urb() -> usb_submit_urb() -- direct USB bulk OUT
(no MPSSE encoding -- the chip is in native UART mode)
UART mode bypasses MPSSE entirely. The chip is set to bitmode 0x00 (reset) which activates native async serial. Data is sent raw over USB bulk endpoints. The FTDI chip handles baud rate generation, start/stop bits, and parity in hardware.
This data flow is identical on MPSSE-capable channels (FT232H A, FT2232H A/B, FT4232H A/B) and non-MPSSE channels (FT4232H C/D). Both use direct bulk URBs and vendor control messages -- no MPSSE opcodes are involved.
Read path: a continuously-resubmitted read URB receives bulk IN packets.
Each packet starts with the 2-byte FTDI status header. The callback
(ftdi_uart_read_complete) processes modem/line status changes and
pushes received data into the tty flip buffer.
The core uses a pattern inspired by the DLN2 USB-to-I2C driver to handle hot-unplug and suspend safely:
struct ftdi_mpsse_dev {
spinlock_t disconnect_lock; -- protects disconnect + suspended flags
bool disconnect; -- set once in disconnect callback
bool suspended; -- set/cleared in suspend/resume
int active_transfers; -- refcount of in-progress I/O
wait_queue_head_t disconnect_wq; -- waited on by disconnect/suspend
};
Every transport function (ftdi_mpsse_xfer, ftdi_mpsse_cfg_msg) follows this pattern:
1. spin_lock(disconnect_lock)
if (disconnect || suspended) return -ESHUTDOWN
active_transfers++
spin_unlock(disconnect_lock)
2. Do USB I/O under io_lock mutex
3. spin_lock(disconnect_lock)
active_transfers--
spin_unlock(disconnect_lock)
if (disconnect) wake_up(disconnect_wq)
Disconnect uses usb_poison_urb() (not usb_kill_urb()) because
poison is permanent -- it cancels in-flight URBs AND causes all future
usb_submit_urb() to return -EPERM. This closes the TOCTOU race
where a transport function passes the disconnect check, then submits a
URB after kill has completed.
Suspend uses usb_kill_urb() (not poison) because suspend is
reversible -- URBs need to be submittable again after resume.
| Primitive | Location | Description |
|---|---|---|
bus_lock (mutex) |
ftdi_mpsse_core.c | Whole-transaction exclusion across drivers |
io_lock (mutex) |
ftdi_mpsse_core.c | Serialises USB bulk transfers |
disconnect_lock |
ftdi_mpsse_core.c | Guards disconnect/suspend state |
write_lock (spin) |
ftdi_uart.c | Protects UART tx_outstanding flag |
fg->lock (mutex) |
ftdi_gpio.c | Serialises GPIO cache + MPSSE I/O |
fg->lock (mutex) |
ftdi_gpio_bitbang.c | Serialises bit-bang GPIO + bulk I/O |
Lock ordering: bus_lock -> fg->lock -> io_lock -> disconnect_lock
On non-MPSSE channels (FT4232H C/D), bus_lock serialises access between
the UART driver and the bit-bang GPIO driver, since both share the same
bulk endpoints.
When bus_mode is empty (auto mode), the driver evaluates these sources
in order to decide which child devices to create:
1. bus_mode module parameter -> if set: use directly
2. EEPROM channel type byte -> UART(0x00), FIFO/OPTO/CPU/FT1284 rejected
3. EEPROM protocol hint (0x1A) -> 'S'=SPI, 'I'=I2C, 'U'=UART
4. VCP driver bit -> if set: UART
5. Product string keywords -> contains "SPI" or "I2C"
6. Electrical characteristics -> slow slew + Schmitt = I2C
7. Default -> SPI (most common MPSSE use case)
Steps 3-7 are implemented in ftdi_mpsse_detect_protocol(). The
cascade exits at the first match.
All module parameters across the driver suite:
| Module | Parameter | Type | Default | Description |
|---|---|---|---|---|
ftdi_mpsse |
bus_mode |
string | "" |
Channel mode: uart, spi, i2c, or empty for auto. Comma-separated for per-interface (spi,uart for FT2232H; spi,i2c,uart,uart for FT4232H). Non-MPSSE channels (FT4232H C/D) only accept uart; other values produce a warning and are forced to UART. |
ftdi_mpsse |
spi_cs |
string | "3" |
SPI chip-select AD pin numbers (3-7). Global: 3,4,5. Per-channel: A:3,4,5;B:3. |
ftdi_mpsse |
autosuspend |
bool | 0 |
Enable USB autosuspend. Chip loses all state during suspend |
ftdi_spi |
register_spidev |
bool | 1 |
Auto-register /dev/spidevX.Y devices for userspace access |
ftdi_i2c |
i2c_speed |
uint | 100 |
I2C bus speed in kHz (10-3400) |
ftdi_i2c |
clock_stretching |
bool | 0 |
Enable adaptive clocking (requires AD3 wired to SCL) |
sysfs attributes (runtime-tuneable):
| Module | Attribute | Path | Description |
|---|---|---|---|
ftdi_uart |
latency_timer |
platform device | RX buffer timeout in ms (1-255, default 16) |
ftdi_uart |
dtr_dsr_flow |
platform device | Enable DTR/DSR hardware flow control (0 or 1) |
ftdi_mpsse |
pinmap |
USB interface | Read-only: active pin assignments and GPIO availability |
ftdi_mpsse |
eeprom_* |
USB interface | Read-only: decoded EEPROM fields |
ftdi_mpsse |
eeprom |
USB interface | Read-only: raw 256-byte EEPROM binary |
- Linux kernel headers (for module build):
apt install linux-headers-$(uname -r) - For UML-based testing (x86 only):
- Linux kernel source tree (default:
~/dev/linux, override:LINUX_SRC=) - Static BusyBox:
apt install busybox-static - GCC,
cpio,gzip
- Linux kernel source tree (default:
- For static analysis:
sparse(endian/lock checking)checkpatch.pl(from kernel source)coccinellefor semantic patches
The fast development cycle is:
# One-time setup: build the UML kernel (~2-5 min)
make -C tests uml-kernel
# Edit driver source, then test (~5 sec per test):
make -C tests test-spi # rebuilds modules + initramfs automatically
# Run all default tests (SPI + I2C + UART):
make test
# Run everything including multi-channel:
make -C tests test-all
# Static analysis before submitting:
make check # sparse + checkpatchThe tests/Makefile dependency chain handles everything: if you edit
ftdi_spi.c and run make -C tests test-spi, it rebuilds the modules
against the UML kernel, rebuilds the initramfs, and runs the test -- all
in one command.
For debugging, you can run UML directly:
# Build everything first
make -C tests initramfs
# Run UML with console output and the test mode you want
tests/.uml-build/linux \
initrd=tests/initramfs.cpio.gz \
mem=256M \
ftdi_mode=spi \
con=null con0=fd:0,fd:1The full kernel dmesg is printed at the end. The ftdi_mode= parameter
selects the test (see tests/init.sh for all modes).
For protocol-level debugging outside UML:
# Terminal 1: start the emulator
ftdi-emulation/ftdi_usbip --chip ft232h --mode spi
# Terminal 2: attach via USB/IP
sudo modprobe vhci-hcd
sudo usbip attach -r 127.0.0.1 -b 1-1
# Load the driver
sudo insmod ftdi_mpsse.ko bus_mode=spi spi_cs=3,4,5
sudo insmod ftdi_spi.ko
sudo insmod ftdi_gpio.ko
echo "0403 6014" | sudo tee /sys/bus/usb/drivers/ftdi_mpsse/new_id
# Now you can test with real tools (spidev_test, gpioinfo, etc.)All child drivers follow the same pattern (see ftdi_spi.c for a
clean example):
-
Create
ftdi_foo.cwith aplatform_driver:static struct platform_driver ftdi_foo_driver = { .driver = { .name = "ftdi-foo" }, .probe = ftdi_foo_probe, }; module_platform_driver(ftdi_foo_driver); MODULE_ALIAS("platform:ftdi-foo");
-
In probe, get platform data and parent transport:
const struct ftdi_mpsse_pdata *pdata = dev_get_platdata(&pdev->dev); // Use ftdi_mpsse_xfer(), ftdi_mpsse_write(), ftdi_mpsse_read() // Always wrap multi-step operations in bus_lock/bus_unlock ftdi_mpsse_bus_lock(pdev); ret = ftdi_mpsse_xfer(pdev, tx, tx_len, rx, rx_len); ftdi_mpsse_bus_unlock(pdev);
-
Add to
Makefile: appendftdi_foo.otoobj-m -
Add MFD cell in
ftdi_mpsse_core.c: createmfd_cellandftdi_mpsse_pdataentries, add to the appropriate mode's cell array in the probe function -
Add to test initramfs: update
tests/Makefileandtests/init.sh
Key invariants:
- Always hold
bus_lockaround multi-step MPSSE transactions - Transport functions check
disconnect/suspendedinternally can_sleep = truefor GPIO chips -- all I/O goes over USB- Use
devm_*allocation where possible
For non-MPSSE children (e.g. ftdi_gpio_bitbang): use
ftdi_mpsse_cfg_msg() for vendor control messages and
ftdi_mpsse_get_endpoints() for direct bulk I/O, instead of
ftdi_mpsse_xfer(). Check ftdi_mpsse_is_mpsse_capable() at
probe to adapt behaviour.
-
Add a test mode to
tests/init.sh:- Add a
caseentry in theftdi_modeparser (line ~35) - Add test assertions using the
checkfunction
- Add a
-
Add a Makefile target to
tests/Makefile:test-foo: $(INITRAMFS) $(call run_test,foo)
-
(Optional) Add a C test tool if userspace I/O testing is needed:
- Create
tests/foo_test.c - Must compile with
-static(runs in initramfs) - Add build rule and include in
$(INITRAMFS)dependencies
- Create
-
Emulator error injection (for error path tests):
- The emulator supports
--error TYPE --error-count N - Available modes:
i2c-nak,i2c-stuck,usb-stall,usb-timeout - To add a new mode: add enum in
ftdi_emu.h, handle inftdi_emu_bulk_out()inftdi_emu.c
- The emulator supports
Test output is saved to tests/test-<mode>.log. Each log contains:
- PASS/FAIL results for all assertions
- Debug output from test tools
- Full kernel dmesg (useful for driver debug messages and stack traces)
| Component | Status | Notes |
|---|---|---|
| USB probe/detect | Tested | FT232H, FT2232H, FT4232H |
| UART ttyFTDI creation | Tested | With data transfer (uart_test) |
| EEPROM decode | Tested | Checksum verified |
| GPIO registration + I/O | Tested | gpio_test reads/writes pin 8 (AC0) |
| SPI transfers | Tested | spi_test via spidev |
| I2C transfers | Tested | i2c_test via i2c-dev |
| Hot-unplug | Tested | SPI + GPIO during disconnect |
| Suspend/resume | Tested | SPI + GPIO across USB suspend |
| Multi-channel (FT2232H) | Tested | Both channels probed, per-channel GPIO |
| Multi-channel (FT4232H A/B) | Tested | A+B MPSSE (SPI/I2C), per-channel GPIO |
| FT4232H C/D UART | Tested | uart_test on non-MPSSE channels |
| FT4232H C/D bit-bang GPIO | Tested | gpio_test on bit-bang GPIO pins |
| I2C NAK error path | Tested | Error injection via emulator |
| I2C bus stuck recovery | Tested | Frozen SDA simulation |
The most common issue: the in-kernel ftdi_sio driver binds to FTDI
devices before ftdi_mpsse can. Solutions:
# Method 1: unbind ftdi_sio, use driver_override
echo 1-2:1.0 | sudo tee /sys/bus/usb/drivers/ftdi_sio/unbind
echo ftdi_mpsse | sudo tee /sys/bus/usb/devices/1-2:1.0/driver_override
echo 1-2:1.0 | sudo tee /sys/bus/usb/drivers/ftdi_mpsse/bind
# Method 2: blacklist ftdi_sio
echo "blacklist ftdi_sio" | sudo tee /etc/modprobe.d/ftdi.conf
# Method 3: use new_id (runtime, triggers reprobe)
echo "0403 6014" | sudo tee /sys/bus/usb/drivers/ftdi_mpsse/new_idCheck dmesg for the reason:
dmesg | grep ftdi_mpsseCommon causes:
unsupported FTDI chip-- thebcdDevicevalue doesn't match any known chip typeEEPROM channel type 0xNN not supported-- FIFO/OPTO/CPU/FT1284 modes are not implementedunknown bus_mode-- typo in the module parameterbus_mode 'spi' not supported on non-MPSSE channel-- FT4232H C/D only support UART; the driver warns and forces UART mode
Factory-fresh FTDI chips may have an empty EEPROM. The driver handles this gracefully:
- Reports
EEPROM: empty (all 0xFF)in dmesg - Falls through to default SPI mode (or respects
bus_modeparameter) - Use
tools/ftdi_eeprom_checkto verify:sudo tools/ftdi_eeprom_check
Verify the mode was set correctly:
cat /sys/bus/usb/devices/1-2:1.0/pinmap # shows active mode and pin mapping
dmesg | grep "EEPROM hints" # shows auto-detection resultIf in the wrong mode, either set bus_mode explicitly or program the
EEPROM protocol hint byte.
# Check the full log
cat tests/test-<mode>.log
# Look for kernel oops or warnings
grep -E "FAIL:|WARNING:|Oops:" tests/test-<mode>.log
# Rebuild everything clean
make -C tests clean
make -C tests uml-kernel # if kernel source changed
make -C tests test-spiCommon test issues:
UML requires an x86 host-- UML only works on x86_64/i686- Missing
busybox-static--apt install busybox-static - Missing kernel source -- set
LINUX_SRC=/path/to/linux
GPL-2.0+