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:

PyBBIO on the BeagleBone Green

The Seeed Studio BeagleBone Green is a lower cost, stripped down derivative of the BeagleBone Black. It has no HDMI output or DC barrel jack, bringing the price down to $39 USD, and there are two Grove connectors for easy expansion using the Seeed Grove system.

BeagleBone Green

The BeagleBone Green ships with the PyBBIO Python library already installed, and in just a few steps you can get it updated to the latest version and get up and running with some example programs.

The first step is to get your BBG powered up and connected to the Internet. The quickest way to do that is to first connect up an Ethernet cable, then connect the Micro-USB cable for power. If you don't have hardwired access to a network you could try a USB WiFi dongle, or a quick search will find you plenty of tutorials on sharing your Internet connection with your BeagleBone over the USB connection.

Once you're BBG is booted and you have terminal access (e.g. over SSH or using the bash terminal in the Cloud9 IDE at http://beaglebone.local:3000), use pip (the Python package manager) to update PyBBIO to the latest and greatest version:

root@beaglebone:~# pip install --upgrade PyBBIO

Then you can head to the PyBBIO examples directory and run the blink.py demo to make sure everything is working:

root@beaglebone:~# cd /usr/local/lib/PyBBIO/examples/
root@beaglebone:/usr/local/lib/PyBBIO/examples# ls
ADS786x_test.py            encoder_test.py          mma7660_test.py
adt7310_test.py            EventIO_test.py          phant_test.py
ADXL345_test.py            fade.py                  remote_temp_control.py
analog_test.py             HTU21D_test.py           SafeProcess_test.py
available_pins.py          interrupt.py             security_cam.py
BBIOServer_mobile_test.py  knock.py                 serial_echo.py
BBIOServer_test.py         LiquidCrystal_clock.py   serial_server.py
blink.py                   LiquidCrystal_fps.py     Servo_sweep.py
BMP183_test.py             LiquidCrystal_glyphs.py  switch.py
DACx311_test.py            MAX31855_test.py         webcam_bbioserver_test.py
digitalRead.py             MLX90614_test.py         webcam_test.py
root@beaglebone:/usr/local/lib/PyBBIO/examples# python blink.py

You should see the two LEDs labeled D4 and D5 next to the Ethernet connector blinking back and forth. You now have the latest version of PyBBIO ready to go!

Be sure to check out the other example programs in that same directory, which show off a ton of PyBBIO's cool features and supported hardware, and take a look through the documentation over on Github.

Updated BeagleBone Pinout

I finally got around to updating PyBBIO's BeagleBone pinout diagram. It now includes the pinouts for all supported peripherals, with the new additions being SPI, I2C and eQEP, and it is color-coded by subsystem.


beaglebone_pinout


The PyBBIO documentation can be found at https://github.com/graycatlabs/PyBBIO/wiki

BeagleBone weather station

Here's a quick demo of PyBBIO's BMP183 and HTU21D libraries. This will push temperature, relative humidity, pressure and dew point data to ThingSpeak every 30 seconds.

import requests
from bbio import *
from bbio.libraries.BMP183 import BMP183
from bbio.libraries.HTU21D import HTU21D

bmp = BMP183(SPI0)
htu = HTU21D(I2C2)

API_KEY = "0000000000000000" # Put your write API here

def postData(*data):
  url = "https://api.thingspeak.com/update"
  params = {'api_key' : API_KEY}
  for i in range(0, len(data)):
    params['field%i' % (i+1)] = data[i]
  print params
  response = requests.post(url, params=params)
  print response

def setup():
  pass

def loop():
  pressure = bmp.getPressure() / 1000.0 # in kPa
  rh = htu.getHumidity()
  temp = htu.getTemp()
  dew_point = htu.calculateDewPoint(rh, temp)
  postData(temp, rh, pressure, dew_point)
  delay(30000)

run(setup, loop)

This uses the requests library to send the data to Thingspeak every 30 seconds in HTTP POST requests using the ThingSpeak API. You'll need to create a ThingSpeak channel first, then replace "0000000000000000" with the API key for your channel.

And here's the weather at Gray Cat Labs: https://thingspeak.com/channels/33102/

PyBBIO update – working towards v0.9.5

Working towards the v0.9.5 release, PyBBIO has a couple cool new libraries:

I've also added documentation for the LiquidCrystal library.

The I2C back-end has also been completely rewritten, replacing the smbus dependency with a custom I2C driver written in C and a C Extension to interface with it (I did it in two parts so the C driver could potentially be used in other programs). The API is a lot simpler, while also being a lot more versatile, so it should support a broader range of devices now. Since it is completely implemented in C, it should also be quicker when writing and reading multiple bytes.

PyBBIO update – version 0.9.4

Version 0.9.4 of PyBBIO brings some back-end changes and bug fixes.

SPI The SPI driver has been replaced with a new C SPI driver and a C extension to interface to it. The C driver can potentially be used on any Linux system, and moving everything that was previously implemented in Python into a C extension should provide a nice performance increase when shifting multiple bytes at a time.

Bug fixes

  • Turns out there was a memory leak introduced in the GPIO C extension, which is now fixed.
  • There was a type check missing in the sysfs C extension, which, among other potential issues, was breaking the RotaryEncoder library.

Stay tuned for the v0.9.5 release, which should include some more exciting changes!

PyBBIO update – version 0.9.3

Version 0.9.3 of PyBBIO is out tonight, which includes some great performance upgrades and a new LCD library.

GPIO Speed

People tend to worry about the speed of GPIO operations when using sysfs entries. While it's in many ways arbitrary for timing sensitive applications because no GPIO access from userspace is real-time anyway, version 0.9.3 brings significantly faster GPIO operations. All of the GPIO related code is now in C extensions, and all GPIO state file descriptors used in a program are kept open until it exits.

Running this test program with v0.9.2:

from bbio import *
pinMode(GPIO1_28, OUTPUT)
while 1:
  digitalWrite(GPIO1_28, HIGH)
  digitalWrite(GPIO1_28, LOW)

The pin is toggled at about 1.5kHz. Running the same program with v0.9.3 now toggles the pin at about 25kHz, or around 17x faster.

LiquidCrystal

There's also a new library for driving HD44780 compatible character LCDs. It supports the majority of the LCD's functionality (though not all of it is tested yet!), including scrolling and custom glyphs. API docs coming soon...

--

The rest of the major changes are:

  • Moved all GPIO code to C extension
  • Added LiquidCrystal character LCD library
  • Improved sysfs interface for faster kernel driver file access
  • Removed 3.2 support, use 0.9.2 if you're still running 3.2 for some reason...
  • Libraries are now contained within the bbio package. This changes importing a bit, user code will need updating
  • Simplifies setup.py
    • Examples are no longer copied
    • DT overlays are distributed compiled and copied with setuptool as data_files
  • BBIOServer now serves from ~/.BBIOServer instead of from inside the package
  • Started adding stubs for universion-io support, not yet implemented

Project teaser – BeagleBone Black thermal imaging

Disclaimer: this is just a little teaser for a project I started working on recently. Details, code and hardware design to come!

I was inspired by Noah Feehan's awesome work on his GRID-EYE BLE thermal imaging camera (also on hackaday.io), based on Panasonic's AMG88xx Grid-EYE sensors. These are 8x8 thermal array sensors with a 60 degree viewing angle, and cost around $40 in single quantities.

I connected the AMG8852 to my BeagleBone Black, then wrote a Python program using PyBBIO and OpenCV to grab temperature arrays from the sensor, convert the temperatures to RGB color values within a linear gradient, scale up the 8x8 image, then save the frames to a video file.

Here's a sample scaling up to 250x250px at 4fps:

(I walk into frame, wave my arms, then walk out of frame and give a thumbs up)

And trying out a few different color mappings:

I have bigger plans for this sensor, so stay tuned!

PyBBIO update – version 0.9.2

A few new features have been added in version 0.9.2:

WebCam library

The last of the GSoC 2014 additions from rseetham, the WebCam library allows live streaming of WebCam video over TCP, as well as capturing of still images to JPG files.

BBIOServer updates

The included JQuery has been updated to version 1.11.1. Also merged an update from Ikario that adds a range slider input element.

I2C improvement

ycoroneos added new quickwrite() and readTransaction() methods to the I2C objects that allow communicating with a wider range of devices.
To update to v0.9.2:

# pip install --upgrade PyBBIO