Raspberry Pi: C++ Libraries for working with I2C, SPI and UART
Tags: raspberrypi, cpp, i2c, uart
Published: 2021-08-23 06:44:52

The Raspberry Pi is a single board computer with now 4 revisions and a minimalistic zero variant. It is a popular choice for different projects because of its small size, efficient power consumption, processing speed and by being a full Linux based computer.

One way to connect multiple single-board computers and/or microcontrollers is direct wiring. For this purpose, the most common used protocols are I2C, SPI and UART. Preceding articles in the blog series explained the principles of these protocols, and presented specific C-libraries for the Arduino. In this article, I will explain C++ libraries that enable to work with these protocols on a Raspberry Pi. For each protocol, I researched useable libraries, and give a short explanation and code example. Please not that the examples are not developed by me, but come from the libraries documentation, and should serve as a basis for a concrete working example.

_This article originally appeared at [my blog admantium.com](https://admantium.com/blog/micro07_raspberry_pi_i2c_spi_uart_cpp/)_.

## I2C

I2C can be supported with the help of the SMBus protocol, which is described as a specific variant of the I2C bus. This protocol is available as [Linux Kernel module](https://www.kernel.org/doc/Documentation/i2c/smbus-protocol). To use it, you need to configure your Raspberry Pi. In a terminal, run `raspi-config`, select `3 Interfacing Options` and `P5 I2C`.

Following the [code example from kernel.org](https://www.kernel.org/doc/Documentation/i2c/dev-interface), you will need to open the device file that represents the connected I2C device, and then send SMBus commands by writing to the devices registers.

```c++
// SOURCE: https://www.kernel.org/doc/Documentation/i2c/dev-interface */
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>

int file;
int adapter_nr = 2;
char filename[20];

snprintf(filename, 19, "/dev/i2c-%d", adapter_nr);
file = open(filename, O_RDWR);
if (file < 0) {
  exit(1);
}

  int addr = 0x40;

if (ioctl(file, I2C_SLAVE, addr) < 0) {
  exit(1);
}

__u8 reg = 0x10;
__s32 res;
char buf[10];

res = i2c_smbus_read_word_data(file, reg);
if (res < 0) {
  /* ERROR HANDLING: i2c transaction failed */
} else {
  /* res contains the read word */
}

buf[0] = reg;
buf[1] = 0x43;
buf[2] = 0x65;
if (write(file, buf, 3) != 3) {
  /* ERROR HANDLING: i2c transaction failed */
}
```

## SPI

To work with the SPI, you also need to add a specific Kernel module: [Spidev](https://elixir.bootlin.com/linux/latest/source/drivers/spi/spidev.c). The Raspberry Pi supports this module, you need to configure it by invoking `raspi-config`, and then select `3 Interfacing Options` and `P4 SPI`.

To access SPI functions with C/C++, you can use the [spidev wrapper library](https://github.com/milekium/spidev-lib). Following the [example code](https://raw.githubusercontent.com/milekium/spidev-lib/master/sample/spidev-testcpp.cc), you need to configure the SPI connection, then open the device that you want to connect to, and then use the library method for reading and writing data.

```c++
//SOURCE: https://raw.githubusercontent.com/milekium/spidev-lib/master/sample/spidev-testcpp.cc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <spidev_lib++.h>

spi_config_t spi_config;
uint8_t tx_buffer[32];
uint8_t rx_buffer[32];

int  main( void)
{

  SPI *mySPI = NULL;

  spi_config.mode=0;
  spi_config.speed=1000000;
  spi_config.delay=0;
  spi_config.bits_per_word=8;

  mySPI=new SPI("/dev/spidev1.0",&spi_config);

  if (mySPI->begin())
  {
    memset(tx_buffer,0,32);
    memset(rx_buffer,0,32);
    sprintf((char*)tx_buffer,"hello world");
    printf("sending %s, to spidev2.0 in full duplex \n ",(char*)tx_buffer);
    mySPI->xfer(tx_buffer,strlen((char*)tx_buffer),rx_buffer,strlen((char*)tx_buffer));
    printf("rx_buffer=%s\n",(char *)rx_buffer);
    //mySPI->end();
    delete mySPI;
  }
 return 1;
}
```

## UART

An UART connection can be established with common C libraries. Following the very detailed explanation in [this article](https://blog.mbedded.ninja/programming/operating-systems/linux/linux-serial-ports-using-c-cpp/), you will need to open the device, then define various properties of the TTY device by using a `termios struct`, and then write to and read from the serial port.

```c++
// SOURCE: https://blog.mbedded.ninja/programming/operating-systems/linux/linux-serial-ports-using-c-cpp/
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>

int main() {
  int serial_port = open("/dev/ttyUSB0", O_RDWR);

  struct termios tty;

  if(tcgetattr(serial_port, &tty) != 0) {
      printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
      return 1;
  }

  tty.c_cflag &= ~PARENB;
  tty.c_cflag &= ~CSTOPB;
  tty.c_cflag &= ~CSIZE;
  tty.c_cflag |= CS8;
  tty.c_cflag &= ~CRTSCTS;
  tty.c_cflag |= CREAD | CLOCAL;

  tty.c_lflag &= ~ICANON;
  tty.c_lflag &= ~ECHO;
  tty.c_lflag &= ~ECHOE;
  tty.c_lflag &= ~ECHONL;
  tty.c_lflag &= ~ISIG;
  tty.c_iflag &= ~(IXON | IXOFF | IXANY);
  tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);

  tty.c_oflag &= ~OPOST;
  tty.c_oflag &= ~ONLCR;

  tty.c_cc[VTIME] = 10;
  tty.c_cc[VMIN] = 0;

  cfsetispeed(&tty, B9600);
  cfsetospeed(&tty, B9600);

  if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
      printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
      return 1;
  }

  unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
  write(serial_port, "Hello, world!", sizeof(msg));

  char read_buf [256];

  memset(&read_buf, '\0', sizeof(read_buf);

  int num_bytes = read(serial_port, &read_buf, sizeof(read_buf));

  if (num_bytes < 0) {
      printf("Error reading: %s", strerror(errno));
      return 1;
  }

  printf("Read %i bytes. Received message: %s", num_bytes, read_buf);

  close(serial_port)
  return 0;
```

## Universal GPIO Access

The library [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about/?h=v1.6.x) provides universal access to GPIOs of any device running Linux. It detects available GPIO, can read and write data to them, and wait for events to be triggered. With this, you can write ow code to talk UART to any connected device.

To install it, run the following commands:

```bash
apt-get install autoconf autoconf-archive libtool libkmod-dev pkg-config
git clone https://github.com/brgl/libgpiod.git

cd libgpiod
./autogen.sh --enable-tools=yes --prefix=/usr/local/bin
make
sudo make install
```

If compilation and installation is successful, in the subfolder `./tools` you will find binaries like `gpiodetect` and `gpioinfo` that you can use to explore the GPIOs. See the following examples.

```bash
$> ./tools/gpiodetect
gpiochip0 [pinctrl-bcm2711] (58 lines)
gpiochip1 [raspberrypi-exp-gpio] (8 lines)

./tools/gpioinfo
gpiochip0 - 58 lines:
 line   0:     "ID_SDA"       unused   input  active-high
 line   1:     "ID_SCL"       unused   input  active-high
 line   2:       "SDA1"       unused   input  active-high
 line   3:       "SCL1"       unused   input  active-high
 line   4:  "GPIO_GCLK"       unused   input  active-high
 line   5:      "GPIO5"       unused   input  active-high
...
```

If you want to work with the library, read [this article](https://www.beyondlogic.org/an-introduction-to-chardev-gpio-and-libgpiod-on-the-raspberry-pi/) for a detailed introduction.

## Conclusion

For working with I2C, SPI and UART on the Raspberry Pi, not only Python, but C++ libraries can be used as well. Specifically, you need to activate the I2C and SPI functions via `raspi-config`, which infernally loads the appropriate Kernel modules. Then you choose a client library and other necessary C++ headers. Working with the libraries follows the same principles: Determine the connected device file, configure a connection object, open the device file, then read from/write to it. Finally, the handy library libgpiod can help you to access all GPIO pins directly, which can be helpful for debugging.