Linux I2C and SPI in C

My last post was about the serbus library, which provides clean and simple C and Python APIs for controlling I2C and SPI buses from userspace on GNU/Linux systems. I wrote serbus as a way to offer a friendlier Python API than the alternative libraries provide, and to do so while keeping it as low-latency as possible I first created an API in C that I could interface with from inside Python C extensions. In the last post I showed a few examples of using the serbus Python package, but because serbus consists of C extensions that use a separate C API, that C API can be used by itself, providing a clean level of abstraction above the ioctl calls used to interface with I2C and SPI device files. So here I'll show a couple of examples of using serbus in C.

As serbus is really just a wrapper for the standard Linux I2C and SPI ioctl calls, it requires that I2C and SPI kernel drivers be loaded to expose /dev/i2c-N and /dev/spidevX.Y device files. For example, on the BeagleBone Black you could load the I2C or SPI Device Tree overlays using cape manager, e.g.:

# echo I2C1 > /sys/devices/bone_capemgr.*/slots
# echo SPIDEV0 > /sys/devices/bone_capemgr.*/slots

Other GNU/Linux systems will have different ways to enable I2C and SPI.

I2C

This program shows how to read the current relative humidity and temperature from the HTU21D sensor:

/**
 * @file i2c_htu21d.c
 * @author Alex Hiam - <alex@graycat.io>
 *
 * @brief Uses serbus to get RH and temperature data from an HTU21D.
 *
 * Requires an I2C Kernel driver be loaded to expose a /dev/i2c-N interface
 * and an HTU21D be connected on the I2C bus.
 */


#include "i2cdriver.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#define HTU21D_BUS      1    // Connected to /dev/i2c-1
#define HTU21D_ADDR     0x40 // HTU21D slave address
#define HTU21D_CMD_TEMP 0xe3 // Command to read temperature
#define HTU21D_CMD_RH   0xe5 // Command to read relative humidity

/**
 * @brief Reads and returns the current temperature from the HUT21D
 *
 * @param i2c_fd I2C bus file descriptor
 *
 * @return the current temperature in Celsius
 */

float getTemp(int i2c_fd) {
  uint8_t rx_buffer[3];
  int raw_value;
  // Set the slave address:
  if (I2C_setSlaveAddress(i2c_fd, HTU21D_ADDR) < 0) {
    printf("*Could set slave address to %d\n", HTU21D_ADDR);
    exit(0);
  }
  // Read the 3 bytes of data:
  I2C_readTransaction(i2c_fd, HTU21D_CMD_TEMP, (void*) rx_buffer, 3);
  // The crc (cyclic redundancy check) can be used to verify the data was
  // received without error - ignore it here
  // Combine the high and low bytes:
  raw_value = (rx_buffer[0]<<8) | rx_buffer[1];
  // Clear the two status bits (see datasheet):
  raw_value &= ~0b11;
  // Convert to Celsius and return (conversion from datasheet):
  return -46.85 + 175.72 * (((float)raw_value)/65536.0);
}

/**
 * @brief Reads and returns the current humidity from the HUT21D
 *
 * @param i2c_fd I2C bus file descriptor
 *
 * @return the current temperature in Celsius
 */

float getRH(int i2c_fd) {
  uint8_t rx_buffer[3];
  int raw_value;
  if (I2C_setSlaveAddress(i2c_fd, HTU21D_ADDR) < 0) {
    printf("*Could set slave address to %d\n", HTU21D_ADDR);
    exit(0);
  }
  I2C_readTransaction(i2c_fd, HTU21D_CMD_RH, (void*) rx_buffer, 3);
  raw_value = (rx_buffer[0]<<8) | rx_buffer[1];
  raw_value &= ~0b11;
  // Convert to %RH and return (conversion from datasheet):
  return -6.0 + 125.0 * (raw_value/65536.0);
}

int main() {
  int i2c_fd;
  float temp, rh;
  // Open the I2C device file:
  i2c_fd = I2C_open(HTU21D_BUS);
  if (i2c_fd < 0) {
    printf("*Could not open I2C bus %d\n", HTU21D_BUS);
    exit(0);
  }
  // Read and print the current temp and rh:
  temp = getTemp(i2c_fd);
  rh = getRH(i2c_fd);
  printf("Temp : %5.2fC\n", temp);
  printf("RH   : %5.2f%%\n", rh);
  // Close the I2C file descriptor:
  I2C_close(i2c_fd);
  return 0;
}

Though this is specific to the HTU21D, this same strategy would apply to a huge number of other I2C sensors that use memory-mapped or command-response protocols. And there's also separate I2C_write() and I2C_read() functions for communicating with devices that don't use these types of protocols.

SPI

In this example we use the SPI driver to control an AD7390 digital-to-analog converter:

/**
 * @file spi_ad7390.c
 * @author Alex Hiam - <alex@graycat.io>
 *
 * @brief Uses serbus to control an AD7390 DAC.
 *
 * Requires an SPI Kernel driver be loaded to expose a /dev/spidevX.Y
 * interface and an AD7390 be connected on the SPI bus.
 */


#include "spidriver.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <signal.h>

#define AD7390_BUS       1        // Connected to /dev/spidev1.X bus
#define AD7390_CS        0        // Using chip select 0 (/dev/spidev1.0)
#define AD7390_FREQ      1000000  // SPI clock frequency in Hz
#define AD7390_BITS      16       // SPI bits per word
#define AD7390_CLOCKMODE 3        // SPI clock mode

static uint8_t running;

/**
 * @brief Sets the given SPI bus per the AD7390's required configuration
 *
 * @param spi_fd SPI bus file descriptor
 */

void AD7390_SPIConfig(int spi_fd) {
  SPI_setMaxFrequency(spi_fd, AD7390_FREQ);
  SPI_setBitsPerWord(spi_fd, AD7390_BITS);
  SPI_setClockMode(spi_fd, AD7390_CLOCKMODE);
  SPI_setCSActiveHigh(spi_fd);
  SPI_setBitOrder(spi_fd, SPI_MSBFIRST);
}

/**
 * @brief Sets the output of the AD7390 in the range 0-4095
 *
 * @param spi_fd SPI bus file descriptor
 * @param value DAC value in range 0-4095
 */

void AD7390_setValue(int spi_fd, uint16_t value) {
  SPI_write(spi_fd, (void*) &value, 1);
}


/**
 * @brief Called when Ctrl+C is pressed - triggers the program to stop.
 */

void stopHandler(int sig) {
  running = 0;
}

int main() {
  int spi_fd, value;
  // Open the SPI device file:
  spi_fd = SPI_open(AD7390_BUS, AD7390_CS);
  if (spi_fd < 0) {
    printf("*Could not open SPI bus %d\n", AD7390_BUS);
    exit(0);
  }
  // Configure the SPI bus:
  AD7390_SPIConfig(spi_fd);

  // Loop until Ctrl+C pressed:
  running = 1;
  signal(SIGINT, stopHandler);
  while(running) {
  // Ramp up from 0V to full-scale:
    for (value=0; value<4095; value++) {
      AD7390_setValue(spi_fd, value);
    }
    // Ramp down from full-scale to 0V:
    for (value=4096; value>0; value--) {
      AD7390_setValue(spi_fd, value);
    }
  }
  // Set DAC output to 0 and close the SPI file descriptor:
  AD7390_setValue(spi_fd, 0);
  SPI_close(spi_fd);
  return 0;
}

The SPI driver provides functions to configure all the various SPI settings, and simple read(), write() and transfer() functions for sending and receiving arbitrary sized chunks of data.

Note: Because the SPI protocol allows for multiple word sizes, the read(), write() and transfer() functions take void pointers for their send and receive buffers, as well as a parameter specifying the number of words to transfer. If the word size is a power of 2, data to write should be stored in an array of the corresponding size (e.g. uint8_t for 8-bit words, uint16_t for 16-bit words, etc.), and for other arbitrary word sizes the type used should be the smallest type that can fit the words (e.g. uint16_t for 12-bit words, uint32_t for 17-bit words, etc.). Any data received follows this same byte alignment pattern, and therefore the receive buffers should follow the same type rules.

Documentation

serbus

The I2C and SPI drivers that were written for PyBBIO are now available as their own library for C and Python, called serbus (like serial bus, get it?... I'm not very good at naming things...).

I wrote serbus as a way to get a friendly, clean API for I2C and SPI communication in Python, as I was not a huge fan of the APIs provided by the existing smbus-cffi and spidev libraries. It consists of two C files, which are really just wrappers for the ioctl calls supported by the Linux I2C and SPI drivers' device files (/dev/i2c-N and /dev/spidevX.Y), and two Python C extension providing a Python class for each. All the file I/O is done in the C code, making it about as low-latency as I2C and SPI from userspace Python can possibly be, and the two classes provide simple methods that cover pretty much any potential use case (if it's missing something let me know!).

I2C

Here's an example of using the I2CDev class to write some data to an I2C EEPROM (24LC256 or equivalent) then read it back:

import serbus, time

eeprom_addr = 0x50 # I2C slave address of EEPROM
start_msb   = 0x00 # High byte of location in EEPROM to write/read
start_lsb   = 0x00 # Low byte of location in EEPROM to write/read

data_to_write = range(10)

# Create an I2CDev instance for interfacing to /dev/i2c-2:
bus = serbus.I2CDev(2)
bus.open()

print "Writing data: {}".format(data_to_write)
# Write the data to the EEPROM:
bus.write(eeprom_addr, [start_msb, start_lsb] + data_to_write)
# The I2C write is asynchronous - give it a bit of time to complete:
time.sleep(0.01) # 10ms should be more than enough

# Read the data from the EEPROM:
bus.write(eeprom_addr, [start_msb, start_lsb])
read_data = bus.read(0x50, len(data_to_write))
print "Data read: {}".format(read_data)

if read_data == data_to_write:
  print "EEPROM write successful!"
else:
  print "EEPROM write failed, is WP enabled?"

bus.close()

The simple read() and write() methods make it a straight-forward task. I2CDev also provides a readTransaction() method, which writes a single byte then immediately reads a block of data. This can be used for tasks like reading from memory mapped I2C slave devices, or from I2C devices that take a command then return data, like the HTU21D relative humidity sensor used in this example:

import serbus, time

htu21d_bus      = 1    # Connected to /dev/i2c-1
htu21d_addr     = 0x40 # HTU21D slave address
htu21d_cmd_temp = 0xe3 # Command to read temperature
htu21d_cmd_rh   = 0xe5 # Command to read relative humidity

bus = serbus.I2CDev(1)
bus.open()

def getTemp():
  # Read the 3 bytes of data:
  msb, lsb, crc = bus.readTransaction(htu21d_addr, htu21d_cmd_temp, 3)
  # The crc (cyclic redundancy check) can be used to verify the data was
  # received without error - ignore it here
  # Combine the high and low bytes:
  raw_value = (msb<<8) | lsb
  # Clear the two status bits (see datasheet):
  raw_value &= ~0b11
  # Convert to Celsius and return (conversion from datasheet):
  return -46.85 + 175.72 * (raw_value/65536.0)

def getRH():
  msb, lsb, crc = bus.readTransaction(htu21d_addr, htu21d_cmd_rh, 3)
  raw_value = (msb<<8) | lsb
  raw_value &= ~0b11
  # Convert to %RH and return (conversion from datasheet):
  return -6.0 + 125.0 * (raw_value/65536.0)

try:
  while True:
    print
    print "Temperature:       {:0.2f}C".format(getTemp())
    print "Relative humidity: {:0.2f}%".format(getRH())
    time.sleep(1)

except KeyboardInterrupt:
  bus.close()

Since the readTransaction() method is implemented in C, it cuts down significantly on the latency that would be introduced by implementing the same routine in Python.

SPI

In this example the SPIDev class is used to control an AD7390 12-bit SPI DAC:

import serbus, time

# The SPI bus and chip select the AD7390 is connected to:
spi_bus = 1
cs      = 0
# /dev/spidev1.0

bus = serbus.SPIDev(spi_bus)
bus.open()

# Set bus configuration, as described in the AD7390 datasheet:
bus.setMaxFrequency(cs, 1000000)
bus.setBitsPerWord(cs, 16)
bus.setClockMode(cs, 3)
bus.setCSActiveHigh(cs)
bus.setMSBFirst(cs)

try:
  while True:
    # Ramp up from 0V to full-scale:
    for i in range(0, 2**12, 100):
      bus.write(cs, [i])
      time.sleep(0.01)

    # Ramp down from full-scale to 0V:
    for i in range(2**12-1, 0, -100):
      bus.write(cs, [i])
      time.sleep(0.01)

except KeyboardInterrupt:
  bus.close()

Like the I2CDev class, the SPIDev class has simple write() and read() methods which accept and return lists of bytes, allowing for reading or writing any number of words (up to the maximum 4096 bytes) with the same method calls.

Links

Check out the git repository and the documentation for more info on how to install and use serbus: