Skip to content

pi-i2c-spi

tested on simulator

Tier Tier 3
Protocol I2C through Linux i2c-dev, SPI through Linux spidev
Covers I2C temperature and humidity sensors, SPI and I2C ADCs, SPI and I2C DACs, port expanders and PWM drivers, Raspberry Pi HATs
Hazard default low
Conformance full
Tested with fake smbus2 and spidev buses with simulated TMP102, MCP3008 and MCP4921 chips

Estimated reach. Hundreds of chip models. Most I2C and SPI sensors, converters and expanders expose fixed-width registers whose values are linear in the reading, which a register map covers. Distributors list thousands of I2C and SPI parts, but chips that need calibration math (BME280, most IMU fusion), SMBus block protocols with a length byte, or multi-step command sequences are not covered, so the estimate counts only the linear register-mapped ones that hobby and lab boards commonly carry.

The pi-i2c-spi driver reads and sets I2C and SPI chips on a Raspberry Pi, or any Linux board with i2c-dev and spidev. The driver uses smbus2 for I2C and spidev for SPI, and the device file is the register map. Each channel names a register, or the bytes to clock out over SPI, and where the value sits in the bytes that come back, so temperature sensors, ADCs, DACs and port expanders work without chip-specific code.

What it supports

  • I2C chips with 8-bit or 16-bit register addresses, and chips read without a register.
  • SPI chips that respond to a command in the same transfer, such as most ADCs, and chips that only take values, such as DACs.
  • Read channels of type number, bool and enum, decoded from any byte range or bit range, big or little endian, signed or unsigned, with scale, offset and a device unit.
  • Control channels that set one register, or clock out one SPI word, with the value placed in it.
  • preserve: true for I2C configuration registers, which reads the register and changes only the field's bits.
  • Actions that send fixed bytes to a register, with action arguments placed in them.
  • discover() lists /dev/i2c-* and /dev/spidev* without touching any bus.

Setup

uv tool install --compile-bytecode "openhal-hub[pi-i2c-spi] @ git+https://github.com/sohan-bhat/OpenHAL"

Enable I2C and SPI with sudo raspi-config (Interface Options), reboot, and add the hub's user to the i2c and spi groups. spidev builds only on Linux, so on other systems only the tests run.

Parameters

ParameterDefaultMeaning
busrequired1, i2c-1 or /dev/i2c-1 for I2C bus 1, spi0.1 or /dev/spidev0.1 for SPI bus 0 with chip select 1. The shorthand pi-i2c-spi: i2c-1 sets it
addressrequired for I2CThe chip's 7-bit address, such as 0x48. Not allowed for SPI
register_width1I2C register address size in bytes, 1 or 2
speed_hz500000SPI clock
mode0SPI mode, 0 to 3
timeout1 sHow long one transaction may take

Channel maps

read:
  temperature: { unit: degC, map: { register: 0x00, length: 2, signed: true, bit: 4, bits: 12, scale: 0.0625 } }
  potentiometer: { unit: V, map: { command: [0x01, 0x80, 0x00], byte: 1, length: 2, bits: 10, scale: 0.0032258 } }
write:
  shutdown: { type: bool, map: { register: 0x01, length: 2, bit: 8, bits: 1, preserve: true } }
  control_voltage: { unit: V, min: 0, max: 2.5, map: { data: [0x30, 0x00], length: 2, bits: 12, scale: 0.00080566 } }
actions:
  power_down: { map: { data: [0x00, 0x00] } }
KeyDefaultMeaning
registernoneI2C. The register to read or set. Commands and actions on I2C need one
read_lengththe field's endI2C. How many bytes to read from the register
commandrequired for SPI readsSPI. The bytes to clock out, padded with zeros to the field's end
datazerosCommands and actions. The bytes the rest of the payload carries. For SPI this is the whole word
preservefalseI2C commands. Read the register and change only this field's bits
byte, length0, 1Where the value starts in the payload and how many bytes it spans, 1 to 8
endianbigByte order, big or little
signedfalseTwo's complement
bit, bits0, the restThe bit range inside those bytes, counted from the least significant bit
scale, offset1, 0device value = raw × scale + offset
unitthe channel unitThe unit the device value is in, which the driver converts to the channel unit
on, off1, 0Raw values for bool channels
lookupnoneRaw numbers to enum values
statenoneControl channels. A read map that shows the result, often the same register
argsnoneActions. A field map for each action argument

How reads and commands behave

  • The driver only changes the registers and byte sequences that the device file's control channels and actions list, and connecting changes nothing. Configuration a chip needs before measuring, such as starting conversions, goes in an action so the configuration passes through the safety kernel.
  • Values are rounded to the field's raw step, and the reading a command returns shows the value actually sent. A value the field can't hold fails the command, and nothing is sent or clamped.
  • A control channel without state reads as the last value sent, or no value before the first command.
  • Because health() reads the first read channel, make the first read channel a register that is safe to read often rather than one that clears on read.
  • Transactions on one device run one at a time, and other devices on the same bus are separate device files whose transactions the Linux kernel's i2c-dev locking keeps apart.

Examples

  • tmp102-temperature.hal.yaml reads a TMP102 and sets its alert thresholds and shutdown bit.
  • mcp3008-adc.hal.yaml reads three channels of an MCP3008 ADC.
  • mcp4921-dac.hal.yaml sets an MCP4921 DAC that drives a fan controller's speed input.

Errors

  • A chip that doesn't acknowledge raises DriverError with the i2cdetect command to try.
  • A missing /dev/i2c-N or /dev/spidev node and a permission problem raise DriverError that says what to enable or which group to join.
  • A transaction that hangs raises TimeoutError. A reply of the wrong length and a raw value a bool or enum map doesn't name raise DriverError.

Limitations

  • The driver converts linear values only, so sensors that need calibration math, such as the BME280 compensation formulas, can be read as raw registers but not converted.
  • The driver doesn't support SMBus block protocols with a length byte, packet error checking or 10-bit I2C addresses.
  • SPI words are whole bytes.
  • describe() returns nothing, because a register map can't be read from the chip.

Hazard default and safe state

The default hazard is low, because most chips on these buses are sensors and converters. A DAC or driver chip can still switch a real load, so set the file's hazard to match what the chip controls. I2C and SPI have no emergency behavior, so safe_state_support is file and the hub sends the device file's safe state like any other value. A chip keeps its last output if the Pi stops, so wire pull resistors that make power loss safe.

Sources