CircuitsFT232H.USB (circuits_ft232h v0.1.0)

Copy Markdown

Thin USB transport layer for the FT232H.

Wraps the :usb NIF (libusb) with FT232H-specific endpoint addresses, FTDI SIO control-transfer helpers, and modem-status-byte stripping for bulk reads.

This module is internal — higher layers (CircuitsFT232H.MPSSE, CircuitsFT232H.Device) compose it into something useful. You normally shouldn't need to call it directly.

Summary

Types

t()

An open FT232H USB handle.

Functions

Releases the interface, re-attaches the kernel driver if we detached it on open, and closes the device.

Decodes a raw USB string descriptor binary into an Elixir string.

Disables the FTDI event/error characters (we don't use them in MPSSE).

Puts the chip into MPSSE mode. After this, write/2 to the bulk OUT endpoint is interpreted as a stream of MPSSE commands.

Resets bitmode to its default (UART). Useful to drop out of MPSSE before releasing the device.

Lists every FT232H currently attached to the host's USB bus.

Opens descriptor and prepares the chip for MPSSE traffic.

Purges the chip's RX FIFO.

Purges the chip's TX FIFO.

Reads exactly length bytes of MPSSE payload from the chip.

Reads a USB string descriptor at the given index.

Resets the FTDI SIO. Equivalent to issuing SIO_RESET with wValue = 0.

Sets the bitmode (high byte) and pin direction mask (low byte) on the low byte port (ADBUS).

Sets the chip's USB latency timer to milliseconds (1..255).

Strips the 2-byte modem-status prefix from every USB packet in data.

Sends raw MPSSE bytes to the chip over the bulk OUT endpoint.

Types

t()

@type t() :: %CircuitsFT232H.USB{
  address: 0..255,
  bus: 0..255,
  detached_kernel_driver?: boolean(),
  handle: :usb.device_handle(),
  max_packet_size: pos_integer()
}

An open FT232H USB handle.

Functions

close(usb)

@spec close(t()) :: :ok

Releases the interface, re-attaches the kernel driver if we detached it on open, and closes the device.

Always returns :ok. Errors during teardown are logged but not surfaced — by the time we're closing there is nothing useful for the caller to do.

decode_string_descriptor(arg1)

@spec decode_string_descriptor(binary()) :: {:ok, String.t()} | {:error, term()}

Decodes a raw USB string descriptor binary into an Elixir string.

The on-wire format is <<length, 0x03, utf16_le_bytes::binary>>. Returns {:error, :invalid_descriptor} if the input doesn't fit that shape (typical when the chip returns 0xFF-filled garbage for an unprogrammed string), and {:error, :invalid_utf16} if the bytes after the header don't decode as UTF-16LE.

disable_special_characters(usb, timeout \\ 1000)

@spec disable_special_characters(t(), timeout()) :: :ok | {:error, term()}

Disables the FTDI event/error characters (we don't use them in MPSSE).

enter_mpsse_mode(usb, timeout \\ 1000)

@spec enter_mpsse_mode(t(), timeout()) :: :ok | {:error, term()}

Puts the chip into MPSSE mode. After this, write/2 to the bulk OUT endpoint is interpreted as a stream of MPSSE commands.

exit_bitmode(usb, timeout \\ 1000)

@spec exit_bitmode(t(), timeout()) :: :ok | {:error, term()}

Resets bitmode to its default (UART). Useful to drop out of MPSSE before releasing the device.

list_devices()

@spec list_devices() :: {:ok, [CircuitsFT232H.USB.Descriptor.t()]} | {:error, term()}

Lists every FT232H currently attached to the host's USB bus.

Identification is by VID 0x403, PID 0x6014 — the values used by the raw FT232H and the Adafruit FT232H Breakout.

open(descriptor, opts \\ [])

@spec open(
  CircuitsFT232H.USB.Descriptor.t(),
  keyword()
) :: {:ok, t()} | {:error, term()}

Opens descriptor and prepares the chip for MPSSE traffic.

Detaches the kernel driver if one is attached (typically ftdi_sio on Linux or Apple's FTDI VCP driver on macOS) and claims the FT232H's single USB interface. Once open, use write/2 and read/3 to push and pull MPSSE bytes, and the various set_*/purge_*/reset/1 helpers to configure the SIO via FTDI control transfers.

Pair every successful open/2 with close/1.

purge_rx(usb, timeout \\ 1000)

@spec purge_rx(t(), timeout()) :: :ok | {:error, term()}

Purges the chip's RX FIFO.

purge_tx(usb, timeout \\ 1000)

@spec purge_tx(t(), timeout()) :: :ok | {:error, term()}

Purges the chip's TX FIFO.

read(usb, length, timeout \\ 1000)

@spec read(t(), non_neg_integer(), timeout()) :: {:ok, binary()} | {:error, term()}

Reads exactly length bytes of MPSSE payload from the chip.

Each USB packet from FT232H is prefixed by 2 modem-status bytes; this function strips them before returning the result, so length refers to real payload bytes.

read_string_descriptor(device, index)

@spec read_string_descriptor(:usb.device(), 1..255) ::
  {:ok, String.t()} | {:error, term()}

Reads a USB string descriptor at the given index.

Used for retrieving the FTDI serial number, manufacturer string, or product string. The first call reads the supported language IDs (index 0), then re-reads at the requested index in the first language.

Most failures are normal: :access if the udev rule isn't installed, :invalid_descriptor if the chip has no programmed string at that index (common for FT232Hs whose EEPROM hasn't been written).

reset(usb, timeout \\ 1000)

@spec reset(t(), timeout()) :: :ok | {:error, term()}

Resets the FTDI SIO. Equivalent to issuing SIO_RESET with wValue = 0.

set_bitmode(usb, mode, direction, timeout \\ 1000)

@spec set_bitmode(t(), 0..255, 0..255, timeout()) :: :ok | {:error, term()}

Sets the bitmode (high byte) and pin direction mask (low byte) on the low byte port (ADBUS).

The direction byte sets each pin to output (1) or input (0). In MPSSE mode the MPSSE engine overrides this for SCK/MOSI/MISO/CS, but the initial value matters for any GPIOs.

set_latency_timer(usb, milliseconds, timeout \\ 1000)

@spec set_latency_timer(t(), 1..255, timeout()) :: :ok | {:error, term()}

Sets the chip's USB latency timer to milliseconds (1..255).

Lower values give snappier reads at the cost of more USB traffic; MPSSE applications typically use 1-16 ms. The chip default is 16 ms.

strip_status(data, packet_size)

@spec strip_status(binary(), pos_integer()) :: binary()

Strips the 2-byte modem-status prefix from every USB packet in data.

packet_size is the bulk endpoint's max packet size (typically 512 for high-speed). Exposed because it's a pure function and the easiest piece of this module to unit-test.

write(usb, data, timeout \\ 1000)

@spec write(t(), iodata(), timeout()) :: :ok | {:error, term()}

Sends raw MPSSE bytes to the chip over the bulk OUT endpoint.