C++ Code for SPI communication between IMU and Raspberry Pi
This article deals with writing the C++ code for SPI communication between IMU and Raspberry PI. To check the fundamentals as in setting up Raspberry PI, hardware connections, verifying the connections and different configuration settings of IMU, refer to the following article.
Basics of SPI in C++
For SPI communication, we will use the header provided by linux, <linux/spi/spidev.h>. This provides an interface to define the SPI communication structure.
And for communication, we will use ioctl() under <sys/iotcl.h>.
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <time.h>
#include <errno.h>
#include <string.h>
#include <cmath>
#include <linux/spi/spidev.h>
Establishing Communication
f_dev = open("/dev/spidev0.0", O_RDWR);
if (f_dev < 0){
std::cout << "ERR (IMU.cpp:IMU()): Failed to open SPI communication on /dev/spidev0.0" << std::endl;
}
This opens a communication file for SPI. In “/dev/spidev0.0”, the first 0 is for bus and the second zero is for channel which is based on the chip select pin chosen on Raspberry Pi.
Configuring SPI parameters
if(ioctl(f_dev, SPI_IOC_WR_MODE, &mode) < 0){
close(f_dev);
}
if (ioctl(f_dev, SPI_IOC_RD_MODE, &mode) < 0) {
close(f_dev);
}
/* Set bits per word*/
if (ioctl(f_dev, SPI_IOC_WR_BITS_PER_WORD, &bits_per_word) < 0) {
close(f_dev);
}
if (ioctl(f_dev, SPI_IOC_RD_BITS_PER_WORD, &bits_per_word) < 0) {
close(f_dev);
}
/* Set SPI speed*/
if (ioctl(f_dev, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
close(f_dev);
}
if (ioctl(f_dev, SPI_IOC_RD_MAX_SPEED_HZ, &speed) < 0) {
close(f_dev);
}
The above code configures the SPI parameters. Please make sure to cross check the mode, bits_per_word and speed from the IMU’s datasheet.
Read Register Function
int readRegister(uint8_t register_add){
struct spi_ioc_transfer xfer[1] = {0};
// Write message for register address
uint8_t reg_addr = register_add | 0x80;
uint8_t data[2];
data[0] = reg_addr;
data[1] = 0x00;
xfer[0].tx_buf = (__u64)data; // output buffer
xfer[0].rx_buf = (__u64)data; // input buffer
xfer[0].len = (__u32)sizeof(data); // length of data to read
int retv = ioctl(f_dev, SPI_IOC_MESSAGE(1), &xfer);
if (retv < 0)
{
std::cout << "error in spi_read_reg8(): ioctl(SPI_IOC_MESSAGE(2))" << std::endl;
}
return data[1];
}
The above function reads value from a particular register and returns it. There are some very important components to understand the SPI communication in C++.
The <linux/spi/spidev.h> header provides a struct ``spi_ioc_transfer`` which is used to set the communication protocols. It has 3 major components, ``tx_buf`` (buffer array), ``rx_buf`` (buffer array), and ``len`` (length of data to read). The first element of the buffer array is the register address and after the communication through ioctl, the read values are stored in the second element, which is returned by the function.
Write Register Function
int writeRegister(uint8_t register_addr, uint8_t value) {
struct spi_ioc_transfer xfer[1] = {0};
// Write message for register address
uint8_t data[2];
data[0] = register_addr;
data[1] = value;
xfer[0].tx_buf = (__u64)data; // output buffer
xfer[0].rx_buf = (__u64)data; // input buffer
xfer[0].len = (__u32)sizeof(data); // length of data to write
int retv = ioctl(f_dev, SPI_IOC_MESSAGE(1), &xfer);
if (retv < 0)
{
std::cout << "error in spi_write_reg8(): ioctl(SPI_IOC_MESSAGE(2)) return" <<std::endl;
}
return retv;
}
The above function write to a particular register. It has the same structure as the read function.
Setting IMU Configuration
Now, before reading gyro and accln values from the IMU, we need to set the config for the same. The below function does the same. There are generally 4 measurement range available on both accelerometer and gyroscope which has different sensitivity level. The sensitiviy is stored in ``acc_lsb_to_g`` and ``gyro_lsb_to_g``. This value is mentioned in the datasheet.
Also, the ACCEL_CONFIG_ and GYRO_CONFIG_ is again mentioned in the datasheet and the instructions for the configuration values to write has been discussed in this article.
int setAccConfig(int config_num){
int status;
switch(config_num){
case 0: // range = +- 2 g
acc_lsb_to_g = 0.061;
status = writeRegister(ACCEL_CONFIG_, 0b10100010);
break;
case 1: // range = +- 4 g
acc_lsb_to_g = 0.122;
status = writeRegister(ACCEL_CONFIG_, 0b10101010);
break;
case 2: // range = +- 8 g
acc_lsb_to_g = 0.244;
status = writeRegister(ACCEL_CONFIG_, 0b10101110);
break;
case 3: // range = +- 16 g
acc_lsb_to_g = 0.488;
status = writeRegister(ACCEL_CONFIG_, 0b10100110);
break;
default: // error
status = 1;
break;
}
return status;
}
int setGyroConfig(int config_num){
int status;
switch(config_num){
case 0: // range = +- 250 deg/s
gyro_lsb_to_degsec = 8.75;
status = writeRegister(GYRO_CONFIG_, 0b10100000);
break;
case 1: // range = +- 500 deg/s
gyro_lsb_to_degsec = 17.5;
status = writeRegister(GYRO_CONFIG_, 0b10100100);
break;
case 2: // range = +- 1000 deg/s
gyro_lsb_to_degsec = 35;
status = writeRegister(GYRO_CONFIG_, 0b10101000);
break;
case 3: // range = +- 2000 deg/s
gyro_lsb_to_degsec = 70;
status = writeRegister(GYRO_CONFIG_, 0b10101100);
break;
default:
status = 1;
break;
}
return status;
}
Read Data From IMU
void fetchData(){
int16_t X, Y, Z;
X = readRegister(0x29) << 8 | readRegister(0x28);
Y = readRegister(0x2B) << 8 | readRegister(0x2A);
Z = readRegister(0x2D) << 8 | readRegister(0x2C);
accX = ((float)X) * acc_lsb_to_g / 1000 - accXoffset;
accY = ((float)Y) * acc_lsb_to_g / 1000 - accYoffset;
accZ = (!upsideDownMounting - upsideDownMounting) * ((float)Z) * acc_lsb_to_g / 1000 - accZoffset;
X = readRegister(0x23) << 8 | readRegister(0x24);
Y = readRegister(0x25) << 8 | readRegister(0x24);
Z = readRegister(0x27) << 8 | readRegister(0x26);
gyroX = ((float)X) * gyro_lsb_to_degsec / 1000- gyroXoffset;
gyroY = ((float)Y) * gyro_lsb_to_degsec / 1000 - gyroYoffset;
gyroZ = ((float)Z) * gyro_lsb_to_degsec / 1000 - gyroZoffset;
}
The above function reads data from respective registers. Each value is available in two registers, one for LSB (least significant bit) and MSB (most significant bit). This is converted to actual value through the following operation:
value =MSB << 8 | LSB
The register address is obtained from the datasheet. After getting the actual value, the value is scaled to a particular unit as in “g” for accelerometer and “deg/s” for gyroscope data.
The offset values are obtained by keeping the IMU stable initially for certain duration, and averaging the values obtained during that time.
In this way, the values can be obtained from the IMU in raspberry pi and then used as desired.
Enjoy !!