Make Your Sensors Speak Rust

Rust vs the real world: An Epic battle.

Claus

7 minute read

Rust really loves strict typing, it makes the language efficient, fast and free of a noisy garbage collector. It’s great. Unfortunately though, dealing with the real world is - as usual - less well-defined. Enter the messy world of sensors and electronics!

Like videos better? Look no further!

As a Developer …

Within IT, there is often a friendly dispute between hardware and software people. The former insist on efficiency and have a low-level angle to things, whereas the latter - including me - enjoy a rather abstract view of the actual devices involved. Now with IoT coming along, it pays off to peak into previously unknown territory to have a sense of the other side.

Not only will this give a software developer the power to mess with the real world, but also get better at programming and shift views a little bit. Also it makes one respect the people who design and create electronics 😉

After finishing this small project, it should be easy for you to:

  • Understand how I2C drivers work
  • Use I2C devices with a Raspberry Pi
  • Create an I2C driver in Rust

Overengineering by Design

It’s important to start with something simple like a seriously overengineered thermometer. Using a Raspberry Pi (Zero W if you want), a BMP085 or BMP180 temperature sensor, and Rust it should be the perfect challenge. Let’s wire it up!

Raspberry Pi wire-up.

Image taken from http://www.pridopia.co.uk/pi-bmp085-temp.html

With the wiring in place, it’s time for figuring out how to talk to the device. The connecting pins already give it away: the sensor uses I2C to transfer its readings to the Pi. Consequently it’s important read up on I2C, so here are some quick facts and how to set it up on Linux devices:

Inter-Integrated Circuit

A fairly simple bus, it communicates over essentially two wires: SDA (data) and SCL (clock). Equally simple, there are registers on each device, complete with an 8 bit address and two methods to manipulate them: read and write. The device itself comes pre-addressed by the manufacturer, so plugging it in, loading the kernel modules should make it operational! The Pi’s Linux Kernel should have most of the prerequisites in place, learn how to configure Raspbian (and others) for I2C over at Adafruit. To go deeper, here is the wikipedia article.

Sensing Trouble

For Rust, there a crate called i2cdev provides access to the Linux I2C devices. With a basic understanding of the bus, the next step is to figure out how to use the bus to get the required information: the current temperature. By finding the data sheet and reference implementation the protocol can be assumed, which - in case of the BMP085/BMP180 - is simply:

  1. Calibrate
  2. Write a command (get temperature or get pressure) to the command register
  3. Wait!
  4. Read the results from the data register
  5. Combine with calibration data and calculate results

Looking at the reference implementation, fetching the data does not seem to be the hard part. In fact, that is provided by i2cdev along with a nice interface for thermometers/barometers. It’s much more the “making sense of what the sensor just read” that is hard to get right:

 bool BMP085::getPressureData()
{
  /* Note: The Pressure has up to 19 bits of data.
   * We need to combine three 8-bit numbers and shift them accordingly.
   */  

  uint8_t PressureRequest = PRESSURE_OSRS_0 + (oss<<6);
  
  if( writeToReg( INPUT_REG_ADDRESS, PressureRequest ) == false )
  {
    ROS_ERROR("BMP085::getPressureData():INVALID DATA");
    return false;
  }
 
  // Wait for the chip's pressure measurement to finish processing 
  // before accessing the data.  Wait time depends on the sampling mode.
  switch( oss )
  {
  case 0:
    usleep( 4500 ); 
    break;
  case 1:
    usleep( 7500 );
    break;
  case 2:
    usleep( 13500 );
    break;
  case 3:
    usleep( 25500 );
    break;
  default:
    usleep( 25500 );
    break;
  }

  unsigned char data[3]; 
  
  if( hardware_->read( this->getDeviceAddress(), this->getProtocol(), this->getFrequency(), this->getFlags(), MEAS_OUTPUT_MSB, data, 3 ) < 0 )
  {
    ROS_ERROR("BMP085::getPressureData():Invalid data");
    return false;
  }

  UP = (long)( ( ( (ulong)data[0]<<16) | ((ulong)data[1]<<8) | (ulong)data[2] ) >> (8-oss));
  // ROS_INFO("UP: %dl",UP);  // sanity check.
  long B3, B6, X1, X2, X3;  // coefficients derived from existing coefficients. 
  // Note: we calculated B5 from the temperature!
  ulong B4, B7;
  
  B6 = B5 - 4000;
  X1 = ( (B2 * ((B6 * B6)>>12)) >> 11 );
  X2 = ( ((long)AC2* B6) >> 11);
  X3 = X1 + X2;
  B3 = ( ( ( ((long)AC1*4) + X3)<< oss) + 2) >> 2;
  
  X1 = (((long)AC3*B6) >> 13);
  X2 = ( B1*((B6*B6) >> 12) ) >> 16;
  X3 = ( (X1 + X2) + 2 ) >> 2;
  B4 = ((long)AC4* (ulong)(X3 + 32768)) >> 15;
  
  B7 =  (ulong)(UP - B3)*(50000>>oss);
  if( B7 < 0x80000000 )
  {
    p = (B7 << 1)/B4;
  }
  else
  {
    p = (B7/B4) << 1;
  }
  X1 = (p >> 8)*(p >> 8);
  X1 = (X1*3038) >> 16;
  X2 = ((-7357)*p)>>16;
  p = p + ( ( X1 + X2 + 3791 ) >> 4);

  pressure_ = p / 1000.0; // in [kPa]
  //ROS_INFO("pressure: %f",pressure_);
  return true;
}

First, Get The Data Types Right

This C++ snippet harbors a lot of explicit and implicit type meddling and many of these variables started out as a long or ulong types (which are commonly 32 bits wide, but they might be larger 😉). What can happen if those variables are multiplied? Overflows, which are bad news.

In addition to that, there is a lot of bitshifting going on. These shifts can only work if the data types are correctly interpreted on reading (ulong or long?), so getting those types right is critical as well. However all of this is expected to run with little memory, it’s also not wise to just i64 everything either, but narrowing them down instead. This C++ implementation together with the Rust compiler provide a great starting point to create an effective and efficient driver. A process that is unique for each driver.

Leveraging Rust’s Types

Rust has great built-in types with explicit names, along with the ability to group literals together with enums that can refer to hex literals. These data structures are used to define register addresses in rust-bmp085:

enum Register {
    Bmp085CalAC1 = 0xAA, // R   Calibration data (16 bits)
    Bmp085CalAC2 = 0xAC, // R   Calibration data (16 bits)
    Bmp085CalAC3 = 0xAE, // R   Calibration data (16 bits)
    Bmp085CalAC4 = 0xB0, // R   Calibration data (16 bits)
    Bmp085CalAC5 = 0xB2, // R   Calibration data (16 bits)
    Bmp085CalAC6 = 0xB4, // R   Calibration data (16 bits)
    Bmp085CalB1 = 0xB6, // R   Calibration data (16 bits)
    Bmp085CalB2 = 0xB8, // R   Calibration data (16 bits)
    Bmp085CalMb = 0xBA, // R   Calibration data (16 bits)
    Bmp085CalMc = 0xBC, // R   Calibration data (16 bits)
    Bmp085CalMd = 0xBE, // R   Calibration data (16 bits)
    Bmp085Control = 0xF4,
    Bmp085Data = 0xF6, // Pressure & Temp
}

The address is now just a simple type cast away, but the code is way easier to maintain! Similarly enum types can define commands, bitmaps or other useful things.

Then, Test

C++ (or Python) might not care much about overflows, but Rust certainly does - so the second big issue is to find and implement tests. Especially with the calculation above, there’s a lot of stuff that can go wrong and physics provides loads of weird input to crash the driver. Luckily the BMP085 specs provide … a test. With this, at least the calculation can be double-checked and severe errors can be avoided, but it’s still easy to crash on overflows or other unexpected input.

Fuzz testing can be a good starting point (like this project), or if the spec provides a range of possible input values, iterating over those can be helpful as well.

Finally, Publish.

Wouldn’t it be great to see projects using your driver? Or next time you buy a new sensor, someone else has gone through the trouble of enabling your next project?

With an API for a device it’s very easy for others to build great things with it. In this blog post a general outline of how to get the resources required to be able to speak Rust with a previously incompatible device is presented; in hopes that eventually there is a greater availability of device drivers for Rust.

Rust, as a safe and efficient language is perfectly suited for IoT applications like reading sensor data somewhere on an embedded device! Until venors see that too and supply Rust APIs for their products enthusiasts can enable makers and thus expand the community!

TL; DR

If you are interested in this sort of stuff, read my other articles on IoT, Rust and follow me on twitter.