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

Share on RedditShare on LinkedInShare on FacebookTweet about this on TwitterShare on Google+