Fully featured I2C and SPI driver for CEVA (Hilcrest)'s BNO080 and FSM300 Inertial Measurement Units.

Dependents:   BNO080-Examples BNO080-Examples

BNO080 Driver

by Jamie Smith / USC Rocket Propulsion Lab

After lots of development, we are proud to present our driver for the Hilcrest BNO080 IMU! This driver is inspired by SparkFun and Nathan Seidle's Arduino driver for this chip, but has been substantially rewritten and adapted.

It supports the main features of the chip, such as reading rotation and acceleration data, as well as some of its more esoteric functionality, such as counting steps and detecting whether the device is being hand-held.

Features

  • Support for 15 different data reports from the IMU, from acceleration to rotation to tap detection
  • Support for reading of sensor data, and automatic checking of update rate against allowed values in metadata
  • BNO_DEBUG switch enabling verbose, detailed output about communications with the chip for ease of debugging
  • Ability to tare sensor rotation and set mounting orientation
  • Can operate in several execution modes: polling I2C, polling SPI, and threaded SPI (which handles timing-critical functions in a dedicated thread, and automatically activates when the IMU has data available)
    • Also has experimental support for using asynchronous SPI transactions, allowing other threads to execute while communication with the BNO is occurring. Note that this functionality requires a patch to Mbed OS source code due to Mbed bug #13941
  • Calibration function
  • Reasonable code size for what you get: the library uses about 4K of flash and one instance of the object uses about 1700 bytes of RAM.

Documentation

Full Doxygen documentation is available online here

Example Code

Here's a simple example:

BNO080 Rotation Vector and Acceleration

#include <mbed.h>
#include <BNO080.h>

int main()
{
	Serial pc(USBTX, USBRX);

	// Create IMU, passing in output stream, pins, I2C address, and I2C frequency
	// These pin assignments are specific to my dev setup -- you'll need to change them
	BNO080I2C imu(&pc, p28, p27, p16, p30, 0x4a, 100000); 

	pc.baud(115200);
	pc.printf("============================================================\n");

	// Tell the IMU to report rotation every 100ms and acceleration every 200ms
	imu.enableReport(BNO080::ROTATION, 100);
	imu.enableReport(BNO080::TOTAL_ACCELERATION, 200);

	while (true)
	{
		wait(.001f);
		
		// poll the IMU for new data -- this returns true if any packets were received
		if(imu.updateData())
		{
			// now check for the specific type of data that was received (can be multiple at once)
			if (imu.hasNewData(BNO080::ROTATION))
			{
				// convert quaternion to Euler degrees and print
				pc.printf("IMU Rotation Euler: ");
				TVector3 eulerRadians = imu.rotationVector.euler();
				TVector3 eulerDegrees = eulerRadians * (180.0 / M_PI);
				eulerDegrees.print(pc, true);
				pc.printf("\n");
			}
			if (imu.hasNewData(BNO080::TOTAL_ACCELERATION))
			{
				// print the acceleration vector using its builtin print() method
				pc.printf("IMU Total Acceleration: ");
				imu.totalAcceleration.print(pc, true);
				pc.printf("\n");
			}
		}
	}

}


If you want more, a comprehensive, ready-to-run set of examples is available on my BNO080-Examples repository.

Credits

This driver makes use of a lightweight, public-domain library for vectors and quaternions available here.

Changelog

Version 2.1 (Nov 24 2020)

  • Added BNO080Async, which provides a threaded implementation of the SPI driver. This should help get the best performance and remove annoying timing requirements on the code calling the driver
  • Added experimental USE_ASYNC_SPI option
  • Fixed bug in v2.0 causing calibrations to fail

Version 2.0 (Nov 18 2020)

  • Added SPI support
  • Refactored buffer system so that SPI could be implemented as a subclass. Unfortunately this does substantially increase the memory usage of the driver, but I believe that the benefits are worth it.

Version 1.3 (Jul 21 2020)

  • Fix deprecation warnings and compile errors in Mbed 6
  • Fix compile errors in Arm Compiler (why doesn't it have M_PI????)

Version 1.2 (Jan 30 2020)

  • Removed accidental IRQ change
  • Fixed hard iron offset reading incorrectly due to missing cast

Version 1.1 (Jun 14 2019)

  • Added support for changing permanent orientation
  • Add FRS writing functions
  • Removed some errant printfs

Version 1.0 (Dec 29 2018)

  • Initial Mbed OS release
Revision:
8:199c7fad233d
Parent:
6:5ba996be5312
Child:
9:430f5302f9e1
--- a/BNO080.h	Tue Jul 21 21:43:47 2020 -0700
+++ b/BNO080.h	Wed Nov 18 18:07:27 2020 -0800
@@ -22,6 +22,7 @@
 #define HAMSTER_BNO080_H
 
 #include <mbed.h>
+#include <Stream.h>
 #include <quaternion.h>
 
 #include "BNO080Constants.h"
@@ -34,24 +35,14 @@
   
   There should be one instance of this class per IMU chip. I2C address and pin assignments are passed in the constructor.
 */
-class BNO080
+class BNO080Base
 {
+protected:
 	/**
 	 * Serial stream to print debug info to.  Used for errors, and debugging output if debugging is enabled.
 	 */
 	Stream * _debugPort;
 
-	/**
-	 * I2C port object.  Provides physical layer communications with the chip.
-	 */
-	I2C _i2cPort;
-
-	/// user defined port speed
-	int  _i2cPortSpeed;
-
-	/// i2c address of IMU (7 bits)
-	uint8_t _i2cAddress;
-
 	/// Interrupt pin -- signals to the host that the IMU has data to send
 	DigitalIn _int;
 	
@@ -63,20 +54,30 @@
 
 #define SHTP_HEADER_SIZE 4
 
-	// Arbitrarily chosen, but should hopefully be large enough for all packets we need.
+	// Size of the largest individual packet we can receive.
+	// Min value is set by the advertisement packet (272 bytes)
 	// If you enable lots of sensor reports and get an error, you might need to increase this.
-#define STORED_PACKET_SIZE 128 
+#define SHTP_RX_PACKET_SIZE 272
+
+	// Size of largest packet that we need to transmit (not including header)
+#define SHTP_MAX_TX_PACKET_SIZE 17
 
-	/// Each SHTP packet has a header of 4 uint8_ts
-	uint8_t shtpHeader[SHTP_HEADER_SIZE];
+	// scratch space buffers
+	uint8_t txPacketBuffer[SHTP_HEADER_SIZE + SHTP_MAX_TX_PACKET_SIZE];
+	uint8_t rxPacketBuffer[SHTP_HEADER_SIZE + SHTP_RX_PACKET_SIZE + SHTP_HEADER_SIZE];  // need a second header worth of extra scratch space to write the header of a continued packet
 
-	/// Stores data contained in each packet.  Packets can contain an arbitrary amount of data, but 
+	/// Each SHTP packet starts with a header of 4 uint8_ts
+	uint8_t * txShtpHeader = txPacketBuffer;
+	uint8_t * rxShtpHeader = rxPacketBuffer;
+
+	/// Stores data contained in each packet.  Packets can contain an arbitrary amount of data, but
 	/// rarely get over a hundred bytes unless you have a million sensor reports enabled.
 	/// The only long packets we actually care about are batched sensor data packets.
-	uint8_t shtpData[STORED_PACKET_SIZE];
+	uint8_t * txShtpData = txPacketBuffer + SHTP_HEADER_SIZE;
+	uint8_t * rxShtpData = rxPacketBuffer + SHTP_HEADER_SIZE;
 
 	/// Length of packet that was received into buffer.  Does NOT include header bytes.
-	uint16_t packetLength;
+	uint16_t rxPacketLength;
 
 	/// Current sequence number for each channel, incremented after transmission. 
 	uint8_t sequenceNumber[6];
@@ -84,7 +85,6 @@
 	/// Commands have a seqNum as well. These are inside command packet, the header uses its own seqNum per channel
 	uint8_t commandSequenceNumber;
 
-
 	// frs metadata
 	//-----------------------------------------------------------------------------------------------------------------
 
@@ -384,20 +384,8 @@
 	 * Just tie it to VCC per the datasheet.
 	 *
 	 * @param debugPort Serial port to write output to.  Cannot be nullptr.
-	 * @param user_SDApin Hardware I2C SDA pin connected to the IMU
-	 * @param user_SCLpin Hardware I2C SCL pin connected to the IMU
-	 * @param user_INTPin Input pin connected to HINTN
-	 * @param user_RSTPin Output pin connected to NRST
-	 * @param i2cAddress I2C address.  The BNO defaults to 0x4a, but can also be set to 0x4b via a pin.
-	 * @param i2cPortSpeed I2C frequency.  The BNO's max is 400kHz.
 	 */
-	BNO080(Stream *debugPort,
-	       PinName user_SDApin, 
-		   PinName user_SCLpin, 
-		   PinName user_INTPin, 
-		   PinName user_RSTPin,
-		   uint8_t i2cAddress=0x4a, 
-		   int i2cPortSpeed=100000);
+	BNO080Base(Stream *debugPort, PinName user_INTPin, PinName user_RSTPin);
 
 	/**
 	 * Resets and connects to the IMU.  Verifies that it's connected, and reads out its version
@@ -487,12 +475,15 @@
 	 * If there are packets queued, receives all of them and updates
 	 * the class variables with the results.
 	 *
+	 * Note that with some backends (SPI), sending commands will also update data, which can
+	 * cause updateData() to return false even though new data has been received.  hasNewData()
+	 * is a more reliable way to determine if a sensor has new data.
+	 *
 	 * @return True iff new data packets of any kind were received.  If you need more fine-grained data change reporting,
 	 * check out hasNewData().
 	 */
 	bool updateData();
 
-
 	/**
 	 * Gets the status of a report as a 2 bit number.
 	 * per SH-2 section 6.5.1, this is interpreted as: <br>
@@ -591,7 +582,7 @@
 	 */
 	void printMetadataSummary(Report report);
 
-private:
+protected:
 
 	// Internal metadata functions
 	//-----------------------------------------------------------------------------------------------------------------
@@ -637,7 +628,7 @@
 	 * @param timeout how long to wait for the packet
 	 * @return true if the packet has been received, false if it timed out
 	 */
-	bool waitForPacket(int channel, uint8_t reportID, float timeout = .125f);
+	bool waitForPacket(int channel, uint8_t reportID, std::chrono::milliseconds timeout = 125ms);
 
 	/**
 	 * Given a Q value, converts fixed point floating to regular floating point number.
@@ -728,7 +719,7 @@
 	 *
 	 * @return whether a packet was recieved.
 	 */
-	bool receivePacket(float timeout=.2f);
+	virtual bool receivePacket(std::chrono::milliseconds timeout=200ms) = 0;
 
 	/**
 	 * Sends the current shtpData contents to the BNO.  It's a good idea to disable interrupts before you call this.
@@ -737,16 +728,16 @@
 	 * @param dataLength How many bits of shtpData to send
 	 * @return
 	 */
-	bool sendPacket(uint8_t channelNumber, uint8_t dataLength);
+	virtual bool sendPacket(uint8_t channelNumber, uint8_t dataLength) = 0;
 
 	/**
 	 * Prints the current shtp packet stored in the buffer.
 	 * @param length
 	 */
-	void printPacket();
+	void printPacket(uint8_t * buffer);
 
 	/**
-	 * Erases the current SHTP packet buffer so new data can be written
+	 * Erases the current SHTP TX packet buffer
 	 */
 	 void zeroBuffer();
 
@@ -759,5 +750,121 @@
 
 };
 
+// TODO list:
+// - Better handling of continued packets (discard as an error)
+// - Unified TX/RX SPI comms function
+
+
+/**
+ * Version of the BNO080 driver which uses the I2C interface
+ */
+class BNO080I2C : public BNO080Base
+{
+	/**
+	 * I2C port object.  Provides physical layer communications with the chip.
+	 */
+	I2C _i2cPort;
+
+	/// user defined port speed
+	int  _i2cPortSpeed;
+
+	/// i2c address of IMU (7 bits)
+	uint8_t _i2cAddress;
+public:
+
+	/**
+	 * Construct a BNO080 driver for the I2C bus, providing pins and parameters.
+	 *
+	 * This doesn't actally initialize the chip, you will need to call begin() for that.
+	 *
+	 * NOTE: while some schematics tell you to connect the BOOTN pin to the processor, this driver does not use or require it.
+	 * Just tie it to VCC per the datasheet.
+	 *
+	 * @param debugPort Serial port to write output to.  Cannot be nullptr.
+	 * @param user_SDApin Hardware I2C SDA pin connected to the IMU
+	 * @param user_SCLpin Hardware I2C SCL pin connected to the IMU
+	 * @param user_INTPin Input pin connected to HINTN
+	 * @param user_RSTPin Output pin connected to NRST
+	 * @param i2cAddress I2C address.  The BNO defaults to 0x4a, but can also be set to 0x4b via a pin.
+	 * @param i2cPortSpeed I2C frequency.  The BNO's max is 400kHz.
+	 */
+	BNO080I2C(Stream *debugPort, PinName user_SDApin, PinName user_SCLpin, PinName user_INTPin, PinName user_RSTPin, uint8_t i2cAddress=0x4a, int i2cPortSpeed=400000);
+
+private:
+
+	bool receivePacket(std::chrono::milliseconds timeout=200ms) override;
+
+	bool sendPacket(uint8_t channelNumber, uint8_t dataLength) override;
+};
+
+// typedef for compatibility with old version of driver where there was no SPI
+typedef BNO080I2C BNO080;
+
+/**
+ * Version of the BNO080 driver which uses the SPI interface.
+ * WARNING: The SPI interface, unlike the I2C interface, of this chip
+ * has some god-awful timing requirements that are difficult to satisfy.
+ *
+ * In order for the chip to produce data, you must call updateData() at at more than
+ * twice the frequency of the fastest sensor poll rate you have set.
+ * Otherwise, for reasons that I don't exactly understand, the IMU
+ * will have some kind of internal watchdog timeout error and shut itself down.
+ * Also, you have about 500ms after calling begin() to configure reports and start
+ * receiving data, or the same thing happens.
+ *
+ * If this timing error happens to you, the symptoms are strange: the IMU will just stop sending data, several seconds later.
+ * No error or anything, just no data.
+ * To recover from the error, you would have to call begin() again and reconfigure it from scratch.
+ *
+ */
+class BNO080SPI : public BNO080Base
+{
+	/**
+	 * I2C port object.  Provides physical layer communications with the chip.
+	 */
+	SPI _spiPort;
+
+	// Wake pin to signal the IMU to wake up
+	DigitalOut _wakePin;
+
+	/// user defined port speed
+	int  _spiSpeed;
+public:
+
+	/**
+	 * Construct a BNO080 driver for the SPI bus, providing pins and parameters.
+	 *
+	 * This doesn't actually initialize the chip, you will need to call begin() for that.
+	 *
+	 * NOTE: while some schematics tell you to connect the BOOTN pin to the processor, this driver does not use or require it.
+	 * Just tie it to VCC per the datasheet.
+	 *
+	 * @param debugPort Serial port to write output to.  Cannot be nullptr.
+	 * @param rstPin Hardware reset pin, resets the IMU
+	 * @param intPin Hardware interrupt pin, this is used for the IMU to signal the host that it has a message to send
+	 * @param wakePin Hardware wake pin, this is used by the processor to signal the BNO to wake up and receive a message
+	 * @param misoPin SPI MISO pin
+	 * @param mosiPin SPI MOSI pin
+	 * @param sclkPin SPI SCLK pin
+	 * @param csPin SPI CS pin
+	 * @param spiSpeed SPI frequency.  The BNO's max is 3MHz.
+	 */
+	BNO080SPI(Stream *debugPort, PinName rstPin, PinName intPin, PinName wakePin, PinName misoPin, PinName mosiPin, PinName sclkPin, PinName csPin, int spiSpeed=3000000);
+
+private:
+
+	bool receivePacket(std::chrono::milliseconds timeout=200ms) override;
+
+	bool sendPacket(uint8_t channelNumber, uint8_t dataLength) override;
+
+	/**
+	 * Assuming that at least a packet header has been read into the RX buffer, receive the remainder of the packet.
+	 * @param bytesRead The number of bytes (including the header) of the packet that have already been read.
+	 * @return
+	 */
+	bool receiveCompletePacket(size_t bytesRead, std::chrono::milliseconds timeout=200ms);
+};
+
+
 
 #endif //HAMSTER_BNO080_H