CircuitsFT232H.I2C (circuits_ft232h v0.1.0)

Copy Markdown

Wires the FT232H's MPSSE engine up as an I2C master.

The FT232H has no native I2C peripheral — I2C is bit-banged via MPSSE using two FT232H-specific tricks:

  • The DRIVE_ZERO opcode (0x9E) makes the chosen pins behave as true open-drain outputs — 0 drives low, 1 tristates. With external pull-ups this matches I2C's electrical model.
  • ENABLE_3_PHASE_CLOCKING (0x8C) adds an extra phase per bit so that SDA setup/hold around SCL meets the I2C spec. The effective SCK rate becomes 2/3 of the MPSSE divisor rate, so this module compensates by programming SCK = 1.5 × the requested I2C rate.

Pin assignments (fixed for v0.1):

PinRole
ADBUS0SCL
ADBUS1SDA (out) — must be tied externally to ADBUS2
ADBUS2SDA (in) — sees what's on the SDA wire

External pull-ups on SCL and SDA are required: 4.7 kΩ for 100 kHz, 2.2 kΩ for 400 kHz, 1 kΩ for 1 MHz.

Public API lives on CircuitsFT232H.I2C.Backend and CircuitsFT232H.I2C.Bus. This module holds the shared MPSSE encoding logic and bus-name conventions.

Summary

Types

Resolved I2C bus configuration.

Functions

Validates and merges user options against the I2C defaults.

Builds the canonical I2C bus name for a device id.

Tears down the I2C-specific MPSSE config so the chip can be reused for a different mode without restarting the Device.

Configures the FT232H for I2C traffic with the given config. Assumes the chip is in baseline MPSSE state and the I2C mode lock has been claimed.

Returns the additional pins this backend reserves for a given config — used by CircuitsFT232H.I2C.Backend when claiming the I2C mode lock.

Looks up the USB.Descriptor whose id matches the I2C bus name.

Parses an I2C bus name into the underlying device id, or returns {:error, :invalid_bus_name}.

Reads count bytes from address.

Writes data to address. Empty data is the I2C device-present probe.

Writes write_data then reads read_count bytes, with a repeated start in between.

Types

config()

@type config() :: %{
  speed_hz: pos_integer(),
  clock_stretching: boolean(),
  flags: [Circuits.I2C.Bus.flag()]
}

Resolved I2C bus configuration.

Functions

build_config(opts)

@spec build_config(keyword()) :: {:ok, config()} | {:error, term()}

Validates and merges user options against the I2C defaults.

Options:

  • :speed_hz (default 100 000, max 1 000 000)
  • :clock_stretching (default false) — enable adaptive-clocking-based I2C clock stretching detection. Requires ADBUS0 (SCL) to be externally jumpered to ADBUS7 so MPSSE can see when a slave is holding SCL low. Reserves ADBUS7 (pin AD7) for the duration of the bus.

bus_name(id)

@spec bus_name(CircuitsFT232H.Device.id()) :: String.t()

Builds the canonical I2C bus name for a device id.

close(id)

@spec close(CircuitsFT232H.Device.id()) :: :ok

Tears down the I2C-specific MPSSE config so the chip can be reused for a different mode without restarting the Device.

configure(id, config)

@spec configure(CircuitsFT232H.Device.id(), config()) :: :ok | {:error, term()}

Configures the FT232H for I2C traffic with the given config. Assumes the chip is in baseline MPSSE state and the I2C mode lock has been claimed.

Also runs an I2C bus-recovery sequence — 16 SCL pulses with SDA released followed by a stop — to free any slave that's stuck mid-transaction (which can happen if a previous program crashed during a read).

extra_reserved_pins(arg1)

@spec extra_reserved_pins(config()) :: [CircuitsFT232H.Device.pin()]

Returns the additional pins this backend reserves for a given config — used by CircuitsFT232H.I2C.Backend when claiming the I2C mode lock.

With clock stretching enabled, ADBUS7 (AD7) is reserved as the SCL-feedback input.

find_descriptor(bus_name)

@spec find_descriptor(String.t()) ::
  {:ok, CircuitsFT232H.USB.Descriptor.t()}
  | {:error, :invalid_bus_name | :not_found | term()}

Looks up the USB.Descriptor whose id matches the I2C bus name.

parse_bus_name(name)

@spec parse_bus_name(String.t()) ::
  {:ok, CircuitsFT232H.Device.id()} | {:error, :invalid_bus_name}

Parses an I2C bus name into the underlying device id, or returns {:error, :invalid_bus_name}.

read(id, config, address, count, opts \\ [])

@spec read(CircuitsFT232H.Device.id(), config(), 0..127, pos_integer(), keyword()) ::
  {:ok, binary()} | {:error, term()}

Reads count bytes from address.

write(id, config, address, data, opts \\ [])

@spec write(CircuitsFT232H.Device.id(), config(), 0..127, binary(), keyword()) ::
  :ok | {:error, term()}

Writes data to address. Empty data is the I2C device-present probe.

write_read(id, config, address, write_data, read_count, opts \\ [])

@spec write_read(
  CircuitsFT232H.Device.id(),
  config(),
  0..127,
  binary(),
  pos_integer(),
  keyword()
) ::
  {:ok, binary()} | {:error, term()}

Writes write_data then reads read_count bytes, with a repeated start in between.