0. Introduction

In the project I’m currently developing I needed to integrate a small IMU IC (based on the BMM150 and BMI160 from BOSCH) and for this reason, after developing the PCB, I needed to develop also the driver interface to enable its usage from an ARM platform. In this tutorial I’ll show how to develop a sensor driver for sensors that are not so easy to implement, starting with the manufacturer API to a working C++ code. The sensor I’m gonna use is the BMM150 with I2C interface. You can find all these files on my Github repo .

1. Understanding API

The BMM150 Bosch Github API might seem a bit difficult to understand, but in this tutorial we’ll see how tame it. So, first of all, we have to create a “middle layer”, a script that will integrate the software in the Bosch repo in order to adapt it to our needs. Let’s create a project containing:

  • bmm150.cpp –> Bosch main file (rename the original bmm150.c file)
  • bmm150.h –> bmm150.cpp header
  • bmm150_defs.h –> all registers and constants definition
  • bmm150_interface.cpp –> file implementing the new functions
  • bmm150_interface.h –> bmm150_interface.cpp header
  • bmm150_example.cpp –> example that uses our driver

On the Bosch repo we find this example code:

struct bmm150_dev dev;
int8_t rslt = BMM150_OK;

/* Sensor interface over I2C */
dev.dev_id = BMM150_DEFAULT_I2C_ADDRESS;
dev.intf = BMM150_I2C_INTF;
dev.read = user_i2c_read;
dev.write = user_i2c_write;
dev.delay_ms = user_delay_ms;

rslt = bmm150_init(&dev);

2.Writing interface script and its header

We have to implement the user_i2c_read, user_i2c_write and user_delay_ms functions; for this reason let’s build the bmm150_interface.cpp script that contains that functions:

#include "bmm_interface.h"

BMM150::BMM150(){   //the main sensor class
}

int8_t i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len){
}

int8_t i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len){
}

void delay_ms(uint32_t period){
}

Now, knowing the basic functions we’ll need, we have to create a header (bmm150_interface.h) file to keep things clear; let’s start importing all the necessary libraries:

#ifndef _BMM150_INTERFACE_H_
#define _BMM150_INTERFACE_H_

#include  <linux/i2c-dev.h>  // manage i2c interface in ARM
#include  <stdint.h>         //All these libraries are needed to 
#include  <iostream>         //use the functions we'll implement, 
#include  <time.h>           //such as delay
#include  <stdio.h>
#include  <stdint.h>
#include  <stdlib.h>
#include  <string.h>
#include  <math.h>
#include  <unistd.h>
#include  <sys/ioctl.h>
#include  <sys/types.h>
#include  <sys/stat.h>
#include  <fcntl.h>
#include  <unistd.h>

#include "bmm150.h"             //include the bmm150 repo headers
#include "bmm150_defs.h"        //

#endif

Now we have to declare the functions:

#ifndef _BMM150_INTERFACE_H_
#define _BMM150_INTERFACE_H_

#include  ...

#include "bmm150.h"             //include the bmm150 repo headers
#include "bmm150_defs.h"        //

void delay_ms(uint32_t period);
int8_t i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);
int8_t i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);

class BMM150{
    public:
        BMM150();
};

#endif

Once we done this, we can go back to bmm150_interface.cpp and definite all the functions:

#include "bmm_interface.h"

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

BMM150::BMM150(){   //the main sensor interface class constructor
    sprintf(filename,"/dev/i2c-1");  //i2c interface 1  --> if needed, change with (i2c-0,i2c-2,..)
    file = open(filename, O_RDWR);//, O_NONBLOCK,O_NDELAY); // open the channel
    if (file < 0)
    {
        printf("Failed to open the bus.");
        /* ERROR HANDLING; you can check error */
        exit(1);
    }

    /* The I2C address */

    if (ioctl(file, I2C_SLAVE, BMM150_DEFAULT_I2C_ADDRESS) < 0)
    {
        printf("Failed to acquire bus access and/or talk to slave.");
        /* ERROR HANDLING; you can check errno to see what went wrong */
        exit(1);
    }
    else{
        printf("BMM150 found at 0x%02X\n",  BMM150_DEFAULT_I2C_ADDRESS);
    }
}

int8_t i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len){
    uint8_t buffer_write[2];
    memset(buffer_write,'\0',2);
    int n_writ;
    n_writ = 0;
    // Request data
    buffer_write[0] = reg_addr;

    n_writ = write(file,buffer_write,1);
    if (n_writ != 1)
    {
        /* ERROR HANDLING: i2c transaction failed */
        printf("BMM150 Reading Error (cannot request data): Failed to write");
        return -1;
    }
    int n_read;
    // Read data
    n_read = read(file,data,len);
    if (n_read != len)
    {
        /* ERROR HANDLING: i2c transaction failed */
        printf("BMM150 Reading Error (not enough data readed) :Failed to read");
        return -1;
    }
    return BMM150_OK;
}

int8_t i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len){
    uint8_t buffer_write[len+1]; // cast needed???!?
    memset(buffer_write,'\0',len+1);
    int n_writ;
    buffer_write[0] = reg_addr;
    for(int i = 0; i<len; i++)
    {
        buffer_write[i+1] = data[i];
    }
    n_writ = write(file,buffer_write, len+1);
    if( n_writ < len+1)
    {
        /* ERROR HANDLING: i2c transaction failed */
        printf("BMM150 Writing Error :Failed to write");
        return -1;
    }
    return BMM150_OK;
}

void delay_ms(uint32_t period){
    usleep(period*1000);
}
</code>

If you need more details about my implementation of i2c_read and i2c_write, please write in the comments below so I will edit in order to make it easier to understand and explain in detail.

The last step is to add some functions that initialize the sensor, set it to on mode and get the sensor data. We can create three functions: initialize, set_sensor_settings and read_sensor_data. Let’s declare them in bmm150_interface.h:

#ifndef _BMM150_INTERFACE_H_
#define _BMM150_INTERFACE_H_

#include  ...

#include "bmm150.h"             //include the bmm150 repo headers
#include "bmm150_defs.h"        //

void delay_ms(uint32_t period);
int8_t i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);
int8_t i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);

class BMM150{
    public:
        BMM150();
        bmm150_dev initialize(int8_t &rslt);
        void set_sensor_settings(struct bmm150_dev *dev, int mode, int8_t &rslt);
        void read_sensor_data(struct bmm150_dev *dev, int8_t &rslt);
};

#endif

And define them in bmm150_interface.cpp (all these functions are showed in the Bosch repo, I just implemented them):

#include "bmm_interface.h"

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

BMM150::BMM150(){   //the main sensor interface class constructor
   ...
}

int8_t i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len){
  ...
}

int8_t i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len){
    ...
}

void delay_ms(uint32_t period){
    usleep(period*1000);
}

bmm150_dev BMM150::initialize(int8_t &rslt){
    struct bmm150_dev dev;
    
    dev.dev_id = BMM150_DEFAULT_I2C_ADDRESS;
    dev.intf = BMM150_I2C_INTF;
    dev.read = i2c_read;
    dev.write = i2c_write;
    dev.delay_ms = delay_ms;
    
    rslt = bmm150_init(&dev);
    
    return dev;
}

void BMM150::set_sensor_settings(struct bmm150_dev *dev, int mode, int8_t &rslt)
{
    if(mode == 0){
        /* Setting the power mode as normal */
        dev->settings.pwr_mode = BMM150_NORMAL_MODE;
        rslt = bmm150_set_op_mode(dev);
    }
    else{
        /* Setting the preset mode as Low power mode 
        i.e. data rate = 10Hz XY-rep = 1 Z-rep = 2*/
        dev->settings.preset_mode = BMM150_PRESETMODE_LOWPOWER;
        rslt = bmm150_set_presetmode(dev);
    }
}

void BMM150::read_sensor_data(struct bmm150_dev *dev, int8_t &rslt)
{

    /* Mag data for X,Y,Z axis are stored inside the
    bmm150_dev structure in int16_t format */
    rslt = bmm150_read_mag_data(dev);

    /* Print the Mag data */
    printf("\n Magnetometer data \n");
    printf("MAG X : %d \t MAG Y : %d \t MAG Z : %d \n"
        ,dev->data.x, dev->data.y, dev->data.z);
}

3.Writing an example

Now that our driver is ready, we can develop a simple example that uses it. First of all, we have to import the library and set the namespace in order to use cout function for debugging.

#include "bmm150_interface.h"
#include <iostream>

using namespace std;
</iostream>

Then let’s create an BMM150 instance:


#include ...

BMM150 bmm = BMM150();

In the main function, we have to call all the functions that allow to access to the sensor data:

#include ...

BMM150 bmm = BMM150();

int main(){
    int8_t rslt = BMM150_OK;
    bmm150_dev sensore;

    sensore = bmm.initialize(rslt);
    cout < < (rslt!=BMM150_OK) << endl;

    bmm150_dev* p_sensore = &sensore;
    bmm.set_sensor_settings(p_sensore, 0, rslt);
    cout << (rslt!=BMM150_OK) << endl;  
    for(int i = 0; i< 500; i++)
    {
    bmm.read_sensor_data(p_sensore, rslt);
        cout << (rslt!=BMM150_OK) << endl;
    usleep(100000);
    }
    return 0;
}
</code>

4. Compiling

Now that we have developed all necessary scripts, we can compile them:

g++ bmm150.cpp bmm150_interface.cpp bmm150_example.cpp -o bmm150

Now, in order to use it, just type:

./bmm150

5. Conclusion

I hope this tutorial will be useful to someone out there. In case you need more details or there is a particular part of code you’d like to be better explained, I will be glad to help you.