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 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
- The C API docs can be found at https://graycat.io/docs/serbus/
Thanks! Useful stuff. Really hard to find info – double thanks. Any idea if the spi port problems ever got addressed?
Not sure, haven’t looked at that in a while. I probably should have filed a bug with TI…