Apache NuttX Driver for BME280 Sensor: Ported from Zephyr OS to NuttX RTOS

📝 10 Mar 2022

Bosch BME280 Sensor connected to Pine64 PineCone BL602 RISC-V Board

“Will Apache NuttX RTOS talk I2C with Bosch BME280 Sensor… On the BL602 RISC-V SoC?”

…A friend and I pondered (8,000 miles apart) while working on the Temperature + Humidity + Air Pressure combo sensor.

Sounds like a fun challenge…

Read on to find out how we solved the challenge and created this driver…

(This BME280 Driver should work OK on other NuttX platforms, like ESP32)

Note: The NuttX Sensor API has been updated in Jul / Aug 2022. (See the changes)

Bus Pirate connected to BME280

§1 Test and Connect BME280

Before we begin our exploration, verify that our BME280 Sensor works OK.

(Because we’ll see some unexpected behaviour in a while)

We used Bus Pirate (pic above) for testing our BME280 Sensor.

(Here’s how)

Next we connect BME280 to Pine64’s PineCone BL602 Board

BL602 PinBME280 PinWire Colour
GPIO 1SDAGreen
GPIO 2SCLBlue
3V33.3VRed
GNDGNDBlack

Pine64 PineCone BL602 RISC-V Board connected to Bosch BME280 Sensor

The I2C Pins on BL602 are defined here: board.h

/* I2C Configuration */
#define BOARD_I2C_SCL \
  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_I2C | \
  GPIO_PIN2)
#define BOARD_I2C_SDA \
  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_I2C | \
  GPIO_PIN1)

(Which pins can be used? See this)

What if we’re connecting to ESP32?

For ESP32: The GPIO Pin Numbers for the I2C Port (I2C0) are defined in Kconfig and menuconfig…

config ESP32_I2C0_SCLPIN
  int "I2C0 SCL Pin"
  default 22
  range 0 39

config ESP32_I2C0_SDAPIN
  int "I2C0 SDA Pin"
  default 23
  range 0 39

Do we need Pull-Up Resistors?

We’re using the SparkFun BME280 Breakout Board, which has Pull-Up Resistors. (So we don’t need to add our own)

What happens now?

Coming up we have a lengthy exploration of BME280 on BL602.

If we’re only interesting in running the NuttX BME280 Driver (on BL602, ESP32 and other platforms), jump to this section for the instructions…

But if we’re keen to go deep inside the BME280 driver… Read on!

§2 Start with BMP280 Driver

NuttX doesn’t have a BME280 Driver, but there’s a NuttX Driver for BMP280, the downsized (rightsized?) sibling of BME280.

(BMP280 works like BME280 for sensing Air Pressure and Temperature, but without Humidity)

Let’s test the BMP280 Driver on BL602 and fix any I2C quirks.

  1. Follow these steps to download and configure NuttX…

    “Download NuttX”

    “Configure NuttX”

    Enable the I2C Port and I2C Character Driver

  2. Enable the BMP280 Driver in menuconfig…

    Check the box for “Device Drivers”“Sensor Device Support”

    Select “Sensor Device Support”

    Check the box for “Bosch BMP280 Barometic Pressure Sensor”

    BMP280 Driver

§2.1 Change I2C Address and Device ID

Will NuttX’s BMP280 Driver really work with our BME280?

Yep the BMP280 Driver ought to read the Air Pressure and Temperature from our BME280 Sensor. (But not Humidity)

Just that we need to we change the I2C Address and Device ID in the BMP280 Driver: bmp280.c

//  Previously: I2C Address of BMP280
//  #define BMP280_ADDR     0x76

//  Testing: I2C Address of BME280
#define BMP280_ADDR         0x77

//  Previously: Device ID of BMP280
//  #define DEVID           0x58

//  Testing: Device ID of BME280
#define DEVID               0x60

(FYI: Zephyr uses the same driver for BMP280 and BME280)

Change I2C Address and Device ID

(Source)

§2.2 Register BMP280 Driver

Next we load the BMP280 Driver at startup: bl602_bringup.c

#ifdef CONFIG_SENSORS_BMP280
#include <nuttx/sensors/bmp280.h>
#endif  //  CONFIG_SENSORS_BMP280
...
int bl602_bringup(void) {
...
#ifdef CONFIG_SENSORS_BMP280
  //  Init I2C bus for BMP280
  struct i2c_master_s *bmp280_i2c_bus = 
    bl602_i2cbus_initialize(0);
  if (!bmp280_i2c_bus) {
    _err("ERROR: Failed to get I2C%d interface\n", 0);
  }

  //  Register the BMP280 driver
  ret = bmp280_register(0, bmp280_i2c_bus);
  if (ret < 0) {
    _err("ERROR: Failed to register BMP280\n");
  }
#endif  //  CONFIG_SENSORS_BMP280

(bmp280_register is defined here)

For ESP32: Edit the function esp32_bringup in esp32_bringup.c so that it calls bmp280_register

Register BMP280 Driver

(Source)

§2.3 Invalid Device ID

We’re ready to test the BMP280 Driver! Build, flash and run NuttX with these steps…

  1. “Build NuttX”

  2. “Flash NuttX”

  3. “Run NuttX”

Something unexpected happens when we boot NuttX on BL602…

bl602_i2c_transfer: i2c transfer success
bmp280_checkid:  devid: 0x00
bmp280_checkid:  Wrong Device ID! 00
bmp280_register: Failed to register driver: -19
bl602_bringup:   ERROR: Failed to register BMP280

(See the complete log)

This says that the BMP280 Driver was expecting Device ID 0x60… But it got 0x00 instead!

Let’s find out why 🤔

Invalid Device ID

(Source)

§2.4 Incorrect Register ID

To track down why we’re not reading the right Device ID, let’s connect a Logic Analyser and inspect the bits on the I2C Bus…

Logic Analyser connected between BL602 and BME280

The I2C Data captured by our Logic Analyser looks surprising…

I2C Data captured by Logic Analyser

The pic above shows that BL602 sent the wrong Register ID to BME280…

Write  [0xEE]
0x00 + ACK (Register ID is 0x00, which is incorrect!)
Read   [0xEF]
0x00 + NAK (Invalid result, because Register ID is incorrect!)

To read the Device ID, the Register ID should be 0xD0, not 0x00!

Let’s fix this 🤔

(Here’s why Register ID should be 0xD0)

§2.5 Set I2C Sub Address

Is there something special about BL602’s I2C Port?

BL602 has a peculiar I2C Port…

We need to send the I2C Sub Address (Register ID) separately from the I2C Data!

(Which might have caused the BMP280 Driver to fail)

I2C Sub Address

(From BL602 Reference Manual)

When we browse the NuttX code for BL602’s I2C Driver, we realise that it supports I2C Sub Addresses: bl602_i2c.c

static int bl602_i2c_transfer(struct i2c_master_s *dev, struct i2c_msg_s *msgs, int count) {
  ...
  //  if msgs[i].flag I2C_M_NOSTOP,means start i2c with subddr
  if (msgs[i].flags & I2C_M_NOSTOP) {
    priv->subflag = 1;
    priv->subaddr = 0;
    for (j = 0; j < msgs[i].length; j++) {
      priv->subaddr += msgs[i].buffer[j] << (j * 8);
    }
    priv->sublen = msgs[i].length;
    i++;

But it needs the flag I2C_M_NOSTOP to be set!

Let’s patch the NuttX BMP280 Driver to send the Register ID as I2C Sub Address (instead of I2C Data) when we’re reading a BMP280 Register: bmp280.c

//  Read a single BME280 Register
static uint8_t bmp280_getreg8(FAR struct bmp280_dev_s *priv, uint8_t regaddr) {
  ...
  msg[0].frequency = priv->freq;
  msg[0].addr      = priv->addr;

#ifdef CONFIG_BL602_I2C0
  //  For BL602: Register ID must be passed as I2C Sub Address
  msg[0].flags     = I2C_M_NOSTOP;
#else
  //  Otherwise pass Register ID as I2C Data
  msg[0].flags     = 0;
#endif  //  CONFIG_BL602_I2C0

  msg[0].buffer    = &regaddr;
  msg[0].length    = 1;

(We patch bmp280_getregs and bmp280_putreg8 too)

What about writing to BMP280 Registers? Do we need to set the I2C Sub Address?

Yes we need to send the Register ID and Register Value as I2C Sub Address, followed by an I2C Read.

(More about BL602 I2C Quirks)

Send the Register ID as I2C Sub Address (instead of I2C Data) when we’re reading a BMP280 Register

§2.6 BMP280 Driver Loads OK

We’ve patched the BMP280 Driver to send the Register ID as I2C Sub Address. Let’s run it!

sensor_custom_register: Registering /dev/uorb/sensor_baro0
bmp280_register: BMP280 driver loaded successfully!
NuttShell (NSH) NuttX-10.2.0-RC0

(See the complete log)

Yep our patched BMP280 Driver loads successfully!

(Because it receives the correct Device ID from BMP280)

BMP280 appears on NuttX as /dev/uorb/sensor_baro0

nsh> ls /dev/uorb
/dev/uorb:
 sensor_baro0

Let’s read the baro0 device.

(How we enable logging for BL602 I2C Driver)

Our patched BMP280 Driver loads successfully

§2.7 Run Sensor Test App

NuttX provides a Sensor Test App that will read our BMP280 Device at /dev/uorb/sensor_baro0

(We’ll study the code later)

We configure NuttX to enable the Sensor Test App

(Details here)

Enable the Sensor Test App

Build, flash and run NuttX. To read 10 sensor values from /dev/uorb/sensor_baro0, enter this at the NuttX Shell…

nsh> sensortest -n 10 baro0
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
baro0: timestamp:30680000 value1:674.93 value2:22.18
baro0: timestamp:30680000 value1:674.93 value2:22.18
baro0: timestamp:30680000 value1:674.93 value2:22.18
baro0: timestamp:30680000 value1:674.93 value2:22.18
baro0: timestamp:30680000 value1:674.93 value2:22.18
baro0: timestamp:30690000 value1:674.93 value2:22.18
baro0: timestamp:30690000 value1:674.93 value2:22.18
baro0: timestamp:30690000 value1:674.93 value2:22.18
baro0: timestamp:30690000 value1:1006.21 value2:30.78
baro0: timestamp:30690000 value1:1006.21 value2:30.78
SensorTest: Received message: baro0, number:10/10

(See the complete log)

We see the Air Pressure and Temperature: 1,006.21 millibars at 30.78 °C.

(Air Pressure at Sea Level is 1,013.25 millibars)

Yep this looks reasonable for Sunny Singapore by the Seaside!

Read sensor values from BMP280

Now let’s port the BME280 Driver from Zephyr OS to NuttX, so we can get the Humidity.

NuttX BMP280 Driver vs Zephyr BME280 Driver

NuttX BMP280 Driver vs Zephyr BME280 Driver

§3 Port BME280 Driver from Zephyr OS

NuttX’s BMP280 Driver works OK with our BME280 Sensor. But we’re missing one thing: Humidity.

Let’s port the BME280 Driver from Zephyr OS to NuttX…

Why not code the BME280 Driver based on the datasheet?

Well yes we could… But then the rest of this article would become an academic exercise 😉

Why port from Zephyr OS?

Zephyr has an extensive collection of drivers.

The NuttX porting steps that we establish today might work for other Zephyr drivers, with minimal changes!

Porting a driver from Zephyr to NuttX sounds hard!

Zephyr’s BME280 Driver looks highly similar to NuttX’s BMP280 Driver. (Pic above)

Thus porting Zephyr’s BME280 Driver to NuttX might not be so hard!

§3.1 Zephyr to NuttX

Zephyr’s BME280 Driver exposes these functions: bme280-nuttx/bme280.c

Zephyr BME280 Driver

(bme280_sample_fetch and bme280_channel_get are explained in the Zephyr Sensor Docs)

But NuttX expects a different interface for Sensor Drivers: bme280-nuttx/driver.c

NuttX Driver

Our job is to map the Zephyr BME280 Driver to the Sensor Driver Interface expected by NuttX…

Zephyr BME280 Driver mapped to NuttX Driver

Zephyr DriverNuttX Driver
bme280_chip_init,
bme280_pm_action
bme280_register
bme280_pm_actionbme280_activate
bme280_sample_fetch,
bme280_channel_get
bme280_fetch
(Static Interval)bme280_set_interval

We’ll cover the details in the next section.

What about bme280_set_interval?

Zephyr assumes that the Standby Interval is Static. (Defined at compile time)

Whereas NuttX expects the Standby Interval to be set at runtime.

To handle this, we code the bme280_set_interval function ourselves in NuttX.

§3.2 Read Sensor Data from Zephyr Driver

Our NuttX BME280 Driver reads the Sensor Data from the Zephyr Driver in two steps…

  1. Fetch a sample from the BME280 Sensor

    (bme280_sample_fetch)

  2. Get the Temperature, Humidity and Pressure from the fetched sample

    (bme280_channel_get)

This is how we do it: driver.c

//  Fetch pressure, temperature and humidity from sensor
static int bme280_fetch(
  FAR struct device *priv,  //  NuttX Device Struct
  FAR struct sensor_baro *baro_data,  //  Returns pressure and temperature (if non-null)
  FAR struct sensor_humi *humi_data   //  Returns humidity (if non-null)
) {

  //  Zephyr BME280 Driver assumes that sensor is not in sleep mode
  if (!priv->activated) {
    snerr("Device must be active before fetch\n");
    return -EIO;
  }

We begin by verifying that the sensor is in Normal Power Mode.

(activated is set by bme280_activate)

Next we call the Zephyr Driver to fetch a sample from BME280…

  //  Fetch the sensor sample (from Zephyr BME280 Driver)
  int ret = bme280_sample_fetch(
    priv,            //  NuttX Device Struct
    SENSOR_CHAN_ALL  //  Fetch Temperature, Humidity and Pressure
  );
  if (ret < 0) { return ret; }

(bme280_sample_fetch is defined here)

The fetched sample contains Temperature, Humidity and Pressure.

To get the Pressure we call the Zephyr Driver…

  //  Get the pressure (from Zephyr BME280 Driver)
  struct sensor_value val;
  ret = bme280_channel_get(
    priv,               //  NuttX Device Struct
    SENSOR_CHAN_PRESS,  //  Get Pressure from fetched sample
    &val                //  Pressure in Fixed Point format
  );
  if (ret < 0) { return ret; }

(bme280_channel_get is defined here)

This returns a Zephyr sensor_value struct that stores the Pressure as a Fixed-Point Number.

We convert the Pressure (kilopascal) from Fixed Point to Float (in millibars)…

  //  Convert pressure to float, scale up by 10 to convert kilopascal to millibars
  float pressure = get_sensor_value(&val) * 10;

(get_sensor_value is defined here)

We do the same to get the Temperature (°C) from the fetched sample…

  //  Get the temperature (from Zephyr BME280 Driver)
  ret = bme280_channel_get(
    priv,                      //  NuttX Device Struct
    SENSOR_CHAN_AMBIENT_TEMP,  //  Get Temperature from fetched sample
    &val                       //  Temperature in Fixed Point format
  );
  if (ret < 0) { return ret; }
  float temperature = get_sensor_value(&val);

And the Humidity (relative %)

  //  Get the humidity (from Zephyr BME280 Driver)
  ret = bme280_channel_get(
    priv,                  //  NuttX Device Struct
    SENSOR_CHAN_HUMIDITY,  //  Get Humidity from fetched sample
    &val                   //  Humidity in Fixed Point format
  );
  if (ret < 0) { return ret; }
  float humidity = get_sensor_value(&val);

Sensor Values in NuttX are timestamped. We get the timestamp like so…

  //  Get the timestamp  
  struct timespec ts;
  clock_systime_timespec(&ts);
  uint64_t timestamp = 1000000ull * ts.tv_sec + ts.tv_nsec / 1000;

Finally we return the Pressure and Temperature data…

  //  Return the pressure and temperature data
  if (baro_data != NULL) {
    baro_data->pressure    = pressure;
    baro_data->temperature = temperature;
    baro_data->timestamp   = timestamp;
  }

And the Humidity

  //  Return the humidity data
  if (humi_data != NULL) {
    humi_data->humidity    = humidity;
    humi_data->timestamp   = timestamp;
  }
  return 0;
}

That’s how we call the Zephyr Driver to fetch the BME280 Sensor Data!

Why did we return the Sensor Data as two structs: baro_data and humi_data?

That’s because our NuttX BME280 Driver is implemented as a Composite Sensor: Barometer Sensor + Humidity Sensor.

More about this in the next chapter.

Read Sensor Data from Zephyr Driver

§3.3 Power Management

Mapping the Power Management functions (Low Power Mode) from Zephyr to NuttX is straightforward: driver.c

//  If enable is true, set Power Mode to normal. Else set to sleep mode.
static int bme280_activate(
    FAR struct device *priv,  //  NuttX Device Struct
    bool enable               //  True for Normal Mode, False for Sleep Mode
) {
  int ret = 0;
  if (enable) {
    //  Set power mode to normal (from Zephyr BME280 Driver)
    ret = bme280_pm_action(
      priv,                     //  NuttX Device Struct
      PM_DEVICE_ACTION_RESUME   //  Resume from Sleep Mode
    );
  } else {
    //  Set to sleep mode (from Zephyr BME280 Driver)
    ret = bme280_pm_action(
      priv,                     //  NuttX Device Struct
      PM_DEVICE_ACTION_SUSPEND  //  Suspend to Sleep Mode
    );
  }
  //  Remember the power mode
  if (ret >= 0) { priv->activated = enable; }
  return ret;
}

BME280 Standby Interval

(From BME280 Datasheet)

§3.4 Standby Interval

What’s the Standby Interval?

BME280 automatically measures the Temperature, Humidity and Pressure at periodic intervals. (Without any intervention from our microcontroller)

The time interval between measurements is the Standby Interval.

(t_standby in the pic above)

Standby Intervals work differently in Zephyr vs NuttX?

Zephyr defines the Standby Interval at Compile Time, it can’t be changed at runtime. (See this)

But NuttX configures the Standby Interval at runtime. That’s why we coded this function to set the Standby Interval: driver.c

//  Set Standby Interval for the sensor
static int bme280_set_interval(
  FAR struct device *priv,     //  NuttX Device Struct
  FAR unsigned int *period_us  //  Standby Interval in microseconds
) {
  //  Convert the Standby Interval to BME280 constant
  int ret = 0;
  uint8_t regval;
  switch (*period_us) {
    case 500: 
      regval = BME280_STANDBY_05_MS; break;
    ...
    case 4000000:
      regval = BME280_STANDBY_4000_MS; break;
    default:
      ret = -EINVAL; break;
  }

  //  Set the Standby Interval
  if (ret == 0) {
    ret = bme280_set_standby(priv, regval);
  }
  return ret;
}

bme280_set_standby is defined below: driver.c

//  Set Standby Duration. Zephyr assumes that Standby Duration is static, so we set it in NuttX.
static int bme280_set_standby(
  FAR struct device *priv,  //  NuttX Device Struct
  uint8_t value             //  BME280 constant for Standby Duration
) {
  //  Set the standby duration value by writing to BME280 Register
  int ret = bme280_reg_read(priv, BME280_REG_CONFIG, &v_data_u8, 1);
  uint8_t v_data_u8 = (v_data_u8 & ~(0x07 << 5)) | (value << 5);
  ret = bme280_reg_write(priv, BME280_REG_CONFIG, v_data_u8);
  ...
  //  Check the standby duration value by reading from BME280 Register
  ret = bme280_reg_read(priv, BME280_REG_CONFIG, &v_data_u8, 1);
  uint8_t v_sb_u8 = (v_data_u8 >> 5) & 0x07;
  //  Omitted: Return error if v_sb_u8 != value

(bme280_reg_read and bme280_reg_write are defined here)

(bme280_set_interval and bme280_set_standby were cloned from the NuttX BMP280 Driver)

We’ll cover bme280_register in the next chapter.

Combined Barometer and Humidity Sensor

§4 Combined Barometer and Humidity Sensor

What kinds of Sensors are supported by NuttX?

Temperature, Humidity, Pressure, GPS, ECG, PM 2.5 and many more. (Here’s the list)

NuttX’s BMP280 Driver implements a Barometer Sensor that provides Pressure and Temperature values.

What about BME280? Is there a NuttX Sensor that supports Pressure + Temperature + Humidity?

Sadly no. Thus our BME280 Driver shall implement a Composite Sensor that behaves like two NuttX Sensors combined…

We mash together the two sensors like so: device.h

//  NuttX Device for BME280
struct device {
  //  Barometer Sensor (Pressure + Temperature)
  FAR struct sensor_lowerhalf_s sensor_baro;  
  //  Humidity Sensor
  FAR struct sensor_lowerhalf_s sensor_humi;  

  FAR struct i2c_master_s *i2c;  //  NuttX I2C interface
  uint8_t addr;                  //  BME280 I2C address
  int     freq;                  //  BME280 Frequency <= 3.4MHz
  bool    activated;             //  True if device is not in sleep mode
  char    *name;                 //  Name of the device (BME280)
  struct  bme280_data *data;     //  Compensation parameters (bme280.c)
};

Let’s dive into the implementation of the two NuttX Sensors.

§4.1 Sensor Operations

Earlier we talked about the functions exposed by our NuttX Sensor Driver…

Zephyr BME280 Driver mapped to NuttX Driver

Now we expose the functions twice (double exposure?) as the Sensor Operations for our Barometer and Humidity Sensors: driver.c

//  Operations for Barometer Sensor
static const struct sensor_ops_s g_baro_ops = {
  .activate      = bme280_activate_baro,
  .fetch         = bme280_fetch_baro,
  .set_interval  = bme280_set_interval_baro,
};

//  Operations for Humidity Sensor
static const struct sensor_ops_s g_humi_ops = {
  .activate      = bme280_activate_humi,
  .fetch         = bme280_fetch_humi,
  .set_interval  = bme280_set_interval_humi,
};

Barometer Sensor vs Humidity Sensor… How different are the Sensor Operations?

The Sensor Operations look highly similar. This is how we fetch the Barometer Sensor Data: driver.c

//  Called by NuttX to fetch pressure and temperature from sensor
static int bme280_fetch_baro(
  FAR struct sensor_lowerhalf_s *lower,  //  Barometer Sensor
  FAR struct file *filep,  // NuttX File
  FAR char *buffer,  //  Buffer to return Sensor Data
  size_t buflen      //  Size of Buffer
) {
  //  Get NuttX Device Struct
  FAR struct device *priv = container_of(
    lower,              //  Barometer Sensor
    FAR struct device,  //  NuttX Device Struct
    sensor_baro         //  Barometer Sensor Struct
  );

  //  Validate buffer size
  struct sensor_baro baro_data;
  if (buflen != sizeof(baro_data)) { return -EINVAL; }

  //  Fetch the sensor data
  int ret = bme280_fetch(priv, &baro_data, NULL);
  if (ret < 0) { return ret; }

  //  Return the sensor data
  memcpy(buffer, &baro_data, sizeof(baro_data));
  return buflen;
}

And this is how we fetch the Humidity Sensor Data: driver.c

//  Called by NuttX to fetch humidity from sensor
static int bme280_fetch_humi(
  FAR struct sensor_lowerhalf_s *lower,  //  Humidity Sensor
  FAR struct file *filep,  // NuttX File
  FAR char *buffer,  //  Buffer to return Sensor Data
  size_t buflen      //  Size of Buffer
) {
  //  Get NuttX Device Struct
  FAR struct device *priv = container_of(
    lower,              //  Humidity Sensor
    FAR struct device,  //  NuttX Device Struct
    sensor_humi         //  Humidity Sensor Struct
  );

  //  Validate buffer size
  struct sensor_humi humi_data;
  if (buflen != sizeof(humi_data)) { return -EINVAL; }

  //  Fetch the sensor data
  int ret = bme280_fetch(priv, NULL, &humi_data);
  if (ret < 0) { return ret; }

  //  Return the sensor data
  memcpy(buffer, &humi_data, sizeof(humi_data));
  return buflen;
}

(We’ve seen bme280_fetch earlier)

The other Sensor Operations look similar too…

Register Sensors

§4.2 Register Sensors

At NuttX Startup we register the Barometer and Humidity Sensors: driver.c

//  Register the BME280 device at startup
int bme280_register(
  int devno,  //  Instance number for driver
  FAR struct i2c_master_s *i2c  //  I2C Interface
) {
  //  Initialize the NuttX Device Struct
  FAR struct device *priv = (FAR struct device *)kmm_zalloc(sizeof(struct device));
  ...
  //  Allocate the Compensation Parameters
  struct bme280_data *data = (FAR struct bme280_data *)kmm_zalloc(sizeof(struct bme280_data));
  priv->data = data;
  ...
  //  Initialize the Barometer Sensor
  priv->sensor_baro.ops  = &g_baro_ops;
  priv->sensor_baro.type = SENSOR_TYPE_BAROMETER;

  //  Initialize the Humidity Sensor
  priv->sensor_humi.ops  = &g_humi_ops;
  priv->sensor_humi.type = SENSOR_TYPE_RELATIVE_HUMIDITY;

  //  Initialize the Sensor Hardware (Zephyr BME280 Driver)
  int ret = bme280_chip_init(priv);
  ...
  //  Set Power Mode to sleep (Zephyr BME280 Driver)
  ret = bme280_pm_action(priv, PM_DEVICE_ACTION_SUSPEND);
  ...
  //  Register the Barometer Sensor
  ret = sensor_register(&priv->sensor_baro, devno);
  ...
  //  Register the Humidity Sensor
  ret = sensor_register(&priv->sensor_humi, devno);

The function above is called at startup by bl602_bringup: bl602_bringup.c

#ifdef CONFIG_SENSORS_BME280
#include <nuttx/sensors/bme280.h>
#endif /* CONFIG_SENSORS_BME280 */
...
int bl602_bringup(void) {
  ...
#ifdef CONFIG_SENSORS_BME280
  //  Init I2C bus for BME280
  struct i2c_master_s *bme280_i2c_bus = bl602_i2cbus_initialize(0);
  if (!bme280_i2c_bus) { _err("ERROR: Failed to get I2C%d interface\n", 0); }

  //  Register the BME280 driver
  ret = bme280_register(0, bme280_i2c_bus);
  if (ret < 0) { _err("ERROR: Failed to register BME280\n"); }
#endif  //  CONFIG_SENSORS_BME280

For ESP32: Edit the function esp32_bringup in esp32_bringup.c so that it calls bme280_register

One last thing before running the BME280 Driver: We need to bundle together the NuttX and Zephyr Drivers.

Zephyr BME280 Driver mapped to NuttX Driver

§5 Bundle NuttX and Zephyr Drivers

Our BME280 Driver consists of two source files: bme280.c and driver.c. Will this work with NuttX?

Nope this won’t compile with NuttX, because it assumes that every Sensor Driver lives in its own file. (Like bmp280.c)

To work around this, we bundle together the Zephyr Driver (bme280.c) and NuttX Driver (driver.c) into a single source file

And we create a Symbolic Link at drivers/sensors/bme280.c

## Go to the NuttX Sensors Source directory
pushd nuttx/nuttx/drivers/sensors

## Add the BME280 Driver as a submodule
git submodule add \
  https://github.com/lupyuen/bme280-nuttx \
  bme280

## Create the Symbolic Link to the bundled source file
ln -s \
  bme280/bundle.c \
  bme280.c

## Return to the top directory
popd

We do the same for the bundled include file bme280-nuttx/bundle.h

## Go to the NuttX Sensors Include directory
pushd nuttx/nuttx/include/nuttx/sensors

## Create the Symbolic Link to the bundled include file
ln -s \
  ../../../drivers/sensors/bme280/bundle.h \
  bme280.h

## Return to the top directory
popd

Which is Sym-Linked at include/nuttx/sensors/bme280.h

This bundling looks complicated. Why not merge the Zephyr and NuttX Drivers into a single source file?

We wanted to reuse the Zephyr Driver with minimal changes so that…

And hopefully we can apply the same steps to port other Zephyr Drivers to NuttX.

Let’s look inside the bundle.

Inside the bundle

(Source)

§5.1 Inside the Bundle

What’s inside the bundled source file bundle.c?

We define the Constants and Macros needed by the Zephyr Driver: bundle.c

//  Zephyr BME280 Options from
//  https://github.com/zephyrproject-rtos/zephyr/blob/main/drivers/sensor/bme280/Kconfig
#define CONFIG_BME280_MODE_NORMAL        //  Normal Sampling Mode (continuous measurements)
#define CONFIG_BME280_TEMP_OVER_2X       //  Temperature Oversamling 2x
#define CONFIG_BME280_PRESS_OVER_16X     //  Pressure Oversampling 16x
#define CONFIG_BME280_HUMIDITY_OVER_16X  //  Humidity Oversampling 16x
#define CONFIG_BME280_STANDBY_1000MS     //  Standby Time 1000ms. Note: Will be overwritten in bme280_set_standby
#define CONFIG_BME280_FILTER_4           //  Filter Coefficient 4
#define CONFIG_PM_DEVICE                 //  Enable Power Management

//  Other Zephyr Defines
#define BME280_BUS_I2C  0  //  I2C Bus
#define BME280_BUS_SPI  0  //  SPI Bus
#define __ASSERT_NO_MSG DEBUGASSERT  //  Assertion check
#define LOG_DBG         sninfo       //  Log info message
#define K_MSEC(ms)      (ms * 1000)  //  Convert milliseconds to microseconds
#define k_sleep(us)     usleep(us)   //  Sleep for microseconds
#define sys_le16_to_cpu(x) (x)       //  Convert from little endian to host endian. TODO: Handle big endian

Followed by the Zephyr Types: bundle.c

//  Zephyr Sensor Channel to be fetched from the sensor
enum sensor_channel {
  SENSOR_CHAN_ALL,           //  All Channels
  SENSOR_CHAN_AMBIENT_TEMP,  //  Ambient Temperature
  SENSOR_CHAN_PRESS,         //  Pressure
  SENSOR_CHAN_HUMIDITY,      //  Humidity
};

//  Zephyr Power Management Action
enum pm_device_action {
  PM_DEVICE_ACTION_SUSPEND,  //  Suspend the sensor
  PM_DEVICE_ACTION_RESUME,   //  Resume the sensor
};

//  Zephyr Power Management State
enum pm_device_state {
  PM_DEVICE_STATE_ACTIVE,     //  Sensor is active
  PM_DEVICE_STATE_SUSPENDED,  //  Sensor is suspended
};
 
//  Zephyr Sensor Value
struct sensor_value {
  int32_t val1;  //  Integer part of the value
  int32_t val2;  //  Fractional part of the value (in one-millionth parts)
};

And finally we embed the source code of the Zephyr and NuttX Drivers: bundle.c

//  Embed Zephyr BME280 Driver
#include "bme280/bme280.c"

//  Embed NuttX Driver Wrapper
#include "bme280/driver.c"

Yes this looks peculiar…

§5.2 Zephyr Driver Modified For NuttX

Sacrilege! We’re #include-ing C source files?!

Yeah this is probably the only way to port the Zephyr Driver to NuttX with the fewest changes.

Check out the minor modifications we made to the Zephyr BME280 Driver while porting to NuttX…

In future we might merge the source files to clean this up. Not that hard!

Changes to Zephyr BME280 Driver

(Source)

§6 Run BME280 Driver

We’re finally ready to run our BME280 Driver!

  1. Follow these steps to build, flash and run NuttX (with our driver inside)…

    “Build, Flash and Run NuttX”

  2. At the NuttX Shell, enter this command to list the NuttX Sensors

    ls /dev/uorb
    
  3. We should see our BME280 Barometer and Humidity Sensors

    /dev/uorb:
     baro0
     humi0
    
  4. To read the BME280 Barometer Sensor once, enter this command…

    sensortest -n 1 baro0
    
  5. We should see the Air Pressure and Temperature readings…

    SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
    baro0: 
      timestamp: 43760000 
      value1:     1011.29 
      value2:       29.09
    SensorTest: Received message: baro0, number:1/1
    

    This says that the Air Pressure is 1,011.29 millibars and the Temperature is 29.09 °C.

  6. Now enter this command to read the BME280 Humidity Sensor once…

    sensortest -n 1 humi0
    
  7. We should see the Relative Humidity

    SensorTest: Test /dev/uorb/sensor_humi0 with interval(1000000us), latency(0us)
    humi0: 
      timestamp: 60120000 
      value:     90.58
    SensorTest: Received message: humi0, number:1/1
    

    This says that the Relative Humidity is 90.58 %.

    (See the complete log)

Yep our BME280 Driver runs successfully on NuttX!

Running our NuttX BME280 Driver

§7 Sensor Test App

Just now we ran the Sensor Test App to read BME280’s Barometer and Humidity Sensors (pic above)…

nsh> sensortest -n 1 baro0
baro0: 
  timestamp: 43760000 
  value1:     1011.29 
  value2:       29.09

nsh> sensortest -n 1 humi0
humi0: 
  timestamp: 60120000 
  value:     90.58

(That’s 1,011.29 millibars, 29.09 °C, 90.58 % Humidity)

Let’s take a quick look inside the Sensor Test App, so that we can code our own Sensor Apps: sensortest.c

//  Sensor Test App
int main(int argc, FAR char *argv[]) {
  ...
  //  Allocate buffer for Sensor Data.
  //  len is Sensor Data size: sizeof(struct sensor_baro)
  FAR char *buffer = calloc(1, len);

The app begins by allocating a buffer for the Sensor Data Struct: sensor_baro (Barometer) or sensor_humi (Humidity).

Next we open the Sensor Device: /dev/uorb/sensor_baro0 (Barometer) or /dev/uorb/sensor_humi0 (Humidity)…

  //  Open the Sensor Device.
  //  devname looks like "/dev/uorb/sensor_baro0"
  int fd = open(devname, O_RDONLY | O_NONBLOCK);
  if (fd < 0) { /* Handle error */ ... }

We set the Standby Interval and Batch Latency

  //  Set Standby Interval
  int ret = ioctl(fd, SNIOC_SET_INTERVAL, interval);
  if (ret < 0) { /* Handle error */ ... }

  //  Set Batch Latency
  ret = ioctl(fd, SNIOC_BATCH, latency);
  if (ret < 0) { /* Handle error */ ... }

We prepare to poll our sensor

  //  Prepare to poll the Sensor
  struct pollfd fds;
  fds.fd     = fd;
  fds.events = POLLIN;

Then we poll, read and print the Sensor Data

  //  Repeat until all samples have been read
  while ((!count || received < count) && !g_should_exit) {

    //  If Sensor Data is available...
    if (poll(&fds, 1, -1) > 0) {

      //  Read the Sensor Data
      if (read(fd, buffer, len) >= len) {

        //  Print the Sensor Data
        received++;
        g_sensor_info[idx]
          .print(buffer, name);
      }
    }
  }

(We’ll see g_sensor_info and print in a while)

Finally we close the sensor

  //  Close the Sensor Device and free the buffer
  close(fd);
  free(buffer);

Let’s talk about the printing of Sensor Data.

Earlier we saw this code for printing Sensor Data

//  Print the Sensor Data
g_sensor_info[idx]
  .print(buffer, name);

What’s g_sensor_info?

g_sensor_info is a Lookup Table that defines the Print Function for every Sensor Type.

(Remember that the Sensor Test App needs to handle all Sensor Types)

Here’s the Print Function for our Barometer Sensor: sensortest.c

//  Print Barometer Sensor Data: Pressure and Temperature
static void print_valf2(const char *buffer, const char *name) {

  //  Cast buffer as Barometer Sensor Data
  struct sensor_baro *event = 
    (struct sensor_baro *) buffer;

  //  Print Pressure and Temperature
  printf(
    "%s: timestamp:%" PRIu64 " value1:%.2f value2:%.2f\n",
    name, 
    event->timestamp, 
    event->pressure, 
    event->temperature
  );
}

And here’s the Print Function for our Humidity Sensor: sensortest.c

//  Print Humidity or Proximity Sensor Data
static void print_valf(const char *buffer, const char *name) {

  //  Cast buffer as Proximity Sensor Data,
  //  which has same size as Humidity Sensor Data
  struct sensor_prox *event = 
    (struct sensor_prox *) buffer;

  //  Print Humidity or Proximity
  printf(
    "%s: timestamp:%" PRIu64 " value:%.2f\n",
    name, 
    event->timestamp, 
    event->proximity
  );
}

But we’re printing Humidity, not Proximity… Why cast to sensor_prox?

Yep we should cast to sensor_humi instead…

//  Cast buffer as Humidity Sensor Data
struct sensor_humi *event = 
  (struct sensor_humi *) buffer;

The Sensor Test App reuses the same function print_valf for printing both Humidity and Proximity Sensor Data. Which is OK because both structs contain the same field types.

How is the Print Function defined for each Sensor Type?

The Print Functions are defined in the Lookup Table g_sensor_info: sensortest.c

//  Lookup Table of all Sensor Types
static const struct sensor_info g_sensor_info[] = {
  ...
  //  Print Barometer Sensor Data with print_valf2
  { print_valf2, 
    sizeof(struct sensor_baro), 
    "baro" },

  //  Print Humidity Sensor Data with print_valf
  { print_valf,  
    sizeof(struct sensor_humi), 
    "humi" },

For our own Sensor App we probably won’t need this Lookup Table, since we know which Sensor Type we’re using.

(And we don’t need to cast our Sensor Data Structs too)

Connect BME280 to #RISCV PineCone #BL602 @PINE64 … Preferably not on a cooking pot 😂

§8 What’s Next

Today we described the steps for porting the BME280 Driver from Zephyr OS to NuttX.

(And we fixed an I2C quirk on BL602)

I hope you’ll be inspired to port other Zephyr drivers to NuttX!

Besides Zephyr OS, where else can we find drivers for NuttX?

Embedded Rust might be a terrific source of drivers for NuttX.

(Check out the Embedded Rust drivers)

But first we need to wrap up the NuttX I2C ioctl Interface as a Rust Embedded HAL.

We’ll cover this in the next article…

The BME280 Driver can also be called from Zig…

Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/bme280.md

§9 Notes

  1. This article is the expanded version of this Twitter Thread

  2. The design of the NuttX Sensor API is discussed here…

    “Unified Management for Sensor”

  3. How to access the I2C Port in a NuttX App via ioctl? See this…

    “Read I2C Register in C (NuttX App)”

Log I2C Transfers

§10 Appendix: Quirks in BL602 NuttX I2C Driver

The BL602 NuttX I2C Driver works in mysterious ways (because of the I2C Sub Address). In this section we’ll explain how to make the driver work correctly, and attempt to decipher the original intentions of the driver’s creator.

BL602 has a peculiar I2C Port that requires us to send the Register ID as I2C Sub Address (I2C_SUB_ADDR), separately from the I2C Data…

I2C Sub Address

(From BL602 Reference Manual)

Will BL602 I2C work if we disable I2C Sub Address?

Nope. If we disable I2C Sub Address, the Register ID sent will be 0x00. (See this)

Thus we must set the Register ID as I2C Sub Address when reading and writing I2C Registers. Here’s how we do this by passing the I2C_M_NOSTOP flag…

§10.1 Read I2C Register

When reading an I2C Register, we set the flag I2C_M_NOSTOP like so: bme280-nuttx/driver.c

//  Read from 8-bit BME280 registers
static int bme280_reg_read(const struct device *priv,
  uint8_t start, uint8_t *buf, int size) {
  struct i2c_msg_s msg[2];
  int ret;

  //  Send Register ID
  msg[0].frequency = priv->freq;
  msg[0].addr      = priv->addr;
#ifdef CONFIG_BL602_I2C0
  //  For BL602: Register ID must be passed as I2C Sub Address
  msg[0].flags     = I2C_M_NOSTOP;
#else
  //  Otherwise pass Register ID as I2C Data
  msg[0].flags     = 0;
#endif  //  CONFIG_BL602_I2C0
  msg[0].buffer    = &start;
  msg[0].length    = 1;

  //  Receive Register Value
  msg[1].frequency = priv->freq;
  msg[1].addr      = priv->addr;
  msg[1].flags     = I2C_M_READ;
  msg[1].buffer    = buf;
  msg[1].length    = size;

  //  Execute I2C transfers
  ret = I2C_TRANSFER(priv->i2c, msg, 2);

This transmits the Register ID as I2C Sub Address, then receives the Register Value.

§10.2 Write I2C Register

When writing an I2C Register, we need two steps…

  1. Send the Register ID and Register Value (in a single chunk) as I2C Sub Address

  2. Followed by I2C Read

Here’s how: bme280-nuttx/driver.c

//  Write to an 8-bit BME280 register
static int bme280_reg_write(const struct device *priv, uint8_t reg,
  uint8_t val) {
  struct i2c_msg_s msg[2];
  uint8_t txbuffer[2];
  uint8_t rxbuffer[1];
  int ret;

  //  Transmit buffer contains Register ID and value
  txbuffer[0] = reg;
  txbuffer[1] = val;

  //  Send Register ID and value
  msg[0].frequency = priv->freq;
  msg[0].addr      = priv->addr;
#ifdef CONFIG_BL602_I2C0
  //  For BL602: Register ID and value must be passed as I2C Sub Address
  msg[0].flags     = I2C_M_NOSTOP;
#else
  //  Otherwise pass Register ID and value as I2C Data
  msg[0].flags     = 0;
#endif  //  CONFIG_BL602_I2C0
  msg[0].buffer    = txbuffer;
  msg[0].length    = 2;

  //  For BL602: We read I2C Data because this 
  //  forces BL602 to send the first message correctly
  msg[1].frequency = priv->freq;
  msg[1].addr      = priv->addr;
  msg[1].flags     = I2C_M_READ;
  msg[1].buffer    = rxbuffer;
  msg[1].length    = sizeof(rxbuffer);

  //  Execute I2C transfers
  ret = I2C_TRANSFER(priv->i2c, msg, 2);

§10.3 Must Read After Write

Why must we do I2C Read after sending the Register ID and value?

We’re not sure why this happens with the BL602 I2C Driver. But it works correctly when we checked with a Logic Analyser.

Here’s what happens when we run the code above to write 0xA0 to Register ID 0xF5 at I2C Address 0x77

Setup Write to [0xEE] + ACK
0xF5 + ACK
0xA0 + ACK
Setup Read to [0xEF] + ACK
0xA0 + NAK

Write 0xA0 to Register 0xF4

Which looks correct. There’s an extra I2C Read at the end, but it’s harmless.

What if we write to the I2C Register without reading?

The I2C Address is sent incorrectly (0x02) and the I2C Write gets truncated…

Setup Write to [0x02] + NAK

Write to I2C Register without reading

What if we send the Register ID and Register Value as I2C Data (flags = 0) instead of I2C Sub Address?

The Register ID and value are sent incorrectly as 0x00 0x00

Setup Write to [0xEE] + ACK
0x00 + ACK
0x00 + ACK
(...600 microseconds later...)
Setup Read to [0xEF] + ACK
0x00 + NAK

Send the Register ID and Register Value as I2C Data instead of I2C Sub Address

Let’s try to make sense of this mysterious behaviour…

§10.4 I2C Sub Address

How is I2C Sub Address supposed to work on BL602 anyway?

Let’s compare with the I2C Driver from Bouffalo Lab’s official BL602 IoT SDK

Here’s the code that reads Register ID 0xD0 from I2C Address 0x77

//  Define I2C message and buffer
static i2c_msg_t read_msg;    //  Message for reading I2C Data
static uint8_t read_buf[32];  //  Buffer for reading I2C Data

static void test_i2c_start_read(char *buf, int len, int argc, char **argv) {
  //  Start reading data from I2C device
  //  Expect result 0x60 for BME280, 0x58 for BMP280
  int data_len = 1;  //  Bytes to be read
  memset(read_buf, 0, sizeof(read_buf));

  //  Set the I2C operation    
  read_msg.i2cx    = 0;            //  I2C Port
  read_msg.direct  = I2C_M_READ;   //  Read I2C data
  read_msg.block   = I2C_M_BLOCK;  //  Wait until data has been read

  //  Set the I2C buffer
  read_msg.buf     = read_buf;     //  Read buffer
  read_msg.len     = data_len;     //  Number of bytes to be read
  read_msg.idex    = 0;            //  Index of next byte to be read into buf

  //  Set device address and register address
  read_msg.addr    = 0x77;   //  BME280 I2C Secondary Address (Primary Address is 0x76)
  read_msg.subflag = 1;      //  Enable Register Address
  read_msg.subaddr = 0xd0;   //  Register Address (BME280 Chip ID)
  read_msg.sublen  = 1;      //  Length of Register Address (bytes)

  //  Start the I2C transfer and enable I2C interrupts
  gpstmsg = &read_msg;
  i2c_transfer_start(&read_msg);

Note that this code executes only one I2C Transfer.

(BL602 NuttX requires two I2C Transfers: One for Register ID, another for Register Value)

From BL602 IoT SDK we see that each I2C Transfer is allowed to specify an I2C Sub Address on top of each I2C Read / Write Transfer.

§10.5 BL602 I2C Driver

But on NuttX it seems that this overlay of I2C Sub Address on I2C Read / Write Transfers is not supported.

The creator of the BL602 NuttX I2C Driver has decided to split the I2C Sub Address and I2C Read / Write as two separate I2C Transfers.

We see this in the BL602 NuttX I2C Driver: bl602_i2c.c

//  Execute the I2C transfers
static int bl602_i2c_transfer(struct i2c_master_s *dev, struct i2c_msg_s *msgs, int count) {
  ...
  priv->msgv = msgs;

  //  Execute each I2C Transfer...
  for (i = 0; i < count; i++)
    {
      priv->bytes    = 0;
      priv->i2cstate = EV_I2C_END_INT;

      bl602_i2c_set_freq(msgs[i].frequency);

      //  If I2C_M_NOSTOP is specified, 
      //  send entire message as I2C Sub Address
      if (msgs[i].flags & I2C_M_NOSTOP)
        {
          priv->subflag = 1;
          priv->subaddr = 0;
          for (j = 0; j < msgs[i].length; j++)
            {
              priv->subaddr += msgs[i].buffer[j] << (j * 8);
            }

          //  Note: Entire message is the I2C Sub Address!
          priv->sublen = msgs[i].length;
          i++;
        }
      //  Else send message as I2C Data
      //  (Read / Write Transfer)
      else
        {
          priv->subflag = 0;
          priv->subaddr = 0;
          priv->sublen  = 0;
        }

      priv->msgid = i;
      bl602_i2c_start_transfer(priv);

This driver code shows that each I2C Transfer is either…

  1. I2C Sub Address

  2. Or I2C Data (Read / Write Transfer)

No mixing of the two is allowed.

I haven’t deciphered the rest of the I2C Driver, but I think this separation of I2C Sub Address causes problems for I2C Write. Hence the need to do I2C Read after I2C Write.

In summary: The creator of the BL602 NuttX I2C Driver might have force-fitted I2C Sub Address into the NuttX I2C Interface, causing problems for I2C Write.

The solution? We might need to extend the NuttX I2C Interface to support I2C Sub Addresses. (Instead of I2C_M_NOSTOP)

(Besides BL602, NXP Microcontrollers are probably the only ones that support I2C Sub Addresses)

§10.6 Configure I2C Transfer

How is the I2C Sub Address configured on NuttX?

Below is the code from the BL602 I2C Driver that configures the I2C Sub Address for each I2C Transfer: bl602_i2c.c

//  Configure the parameters for an I2C Transfer
static void bl602_i2c_config_para(struct bl602_i2c_priv_s *priv) {
  struct i2c_msg_s *msg = &priv->msgv[priv->msgid];

  //  Set the I2C Direction: Read or write
  if (msg->flags & I2C_M_READ)
    {
      modifyreg32(BL602_I2C_CONFIG, 0, I2C_CONFIG_CR_I2C_PKT_DIR);
    }
  else
    {
      modifyreg32(BL602_I2C_CONFIG, I2C_CONFIG_CR_I2C_PKT_DIR, 0);
    }

  //  Set the I2C Address
  modifyreg32(BL602_I2C_CONFIG,
              I2C_CONFIG_CR_I2C_SLV_ADDR_MASK,
              msg->addr << I2C_CONFIG_CR_I2C_SLV_ADDR_SHIFT);

  //  If this I2C Transfer is for I2C Sub Address...
  if (priv->subflag > 0)
    {
      //  Enable I2C Sub Address
      modifyreg32(BL602_I2C_CONFIG, 0, I2C_CONFIG_CR_I2C_SUB_ADDR_EN);
      //  Set the I2C Sub Address length
      modifyreg32(BL602_I2C_CONFIG,
                  I2C_CONFIG_CR_I2C_SUB_ADDR_BC_MASK,
                  (priv->sublen - 1) << I2C_CONFIG_CR_I2C_SUB_ADDR_BC_SHIFT);
    }
  else
    {
      //  Else disable I2C Sub Address
      modifyreg32(BL602_I2C_CONFIG, I2C_CONFIG_CR_I2C_SUB_ADDR_EN, 0);
    }

  //  Set the I2C Message Length
  modifyreg32(BL602_I2C_CONFIG,
              I2C_CONFIG_CR_I2C_PKT_LEN_MASK,
              (msg->length - 1) << I2C_CONFIG_CR_I2C_PKT_LEN_SHIFT);

  //  If this I2C Transfer is for I2C Sub Address...
  if (priv->subflag > 0)
    {
      //  Set the I2C Sub Address
      putreg32(priv->subaddr, BL602_I2C_SUB_ADDR);
    }
}

§11 Appendix: Log I2C Transfers

Here’s how we modded the BL602 I2C Driver on NuttX to log the I2C transfers

For sending I2C data: bl602_i2c.c

static void bl602_i2c_send_data(struct bl602_i2c_priv_s *priv)
{
  ...
  putreg32(temp, BL602_I2C_FIFO_WDATA);
  priv->bytes += count;
  //  Insert this
  i2cinfo("count=%d, temp=0x%x\n", count, temp);
}

For receiving I2C data: bl602_i2c.c

static void bl602_i2c_recvdata(struct bl602_i2c_priv_s *priv)
{
  ...
  count = msg->length - priv->bytes;
  temp  = getreg32(BL602_I2C_FIFO_RDATA);
  //  Insert this
  i2cinfo("count=%d, temp=0x%x\n", count, temp);

For I2C transfers: bl602_i2c.c

static int bl602_i2c_transfer(struct i2c_master_s *dev,
                              struct i2c_msg_s *   msgs,
                              int                      count)
{
  ...
  for (i = 0; i < count; i++)
    {
      ...
      priv->msgid = i;
      //  Insert this before bl602_i2c_start_transfer
      i2cinfo("subflag=%d, subaddr=0x%x, sublen=%d\n", priv->subflag, priv->subaddr, priv->sublen);

§12 Appendix: Build, Flash and Run NuttX

(For BL602, BL604 and ESP32)

Below are the steps to build, flash and run NuttX on BL602, BL604 and ESP32.

The instructions below will work on Linux (Ubuntu), WSL (Ubuntu) and macOS.

(Instructions for other platforms)

(See this for Arch Linux)

§12.1 Download NuttX

To use the NuttX BME280 Driver, download the modified source code for NuttX OS and NuttX Apps

mkdir nuttx
cd nuttx
git clone --recursive https://github.com/lupyuen/nuttx nuttx
git clone --recursive https://github.com/lupyuen/nuttx-apps apps

Or if we prefer to add the BME280 Driver to our NuttX Project, follow these instructions…

(For PineDio Stack BL604: The BME280 Driver is already preinstalled)

§12.2 Configure NuttX

Now we configure our NuttX project…

  1. Install the build prerequisites…

    “Install Prerequisites”

  2. Configure the build…

    cd nuttx
    
    ## For BL602: Configure the build for BL602
    ./tools/configure.sh bl602evb:nsh
    
    ## For PineDio Stack BL604: Configure the build for BL604
    ./tools/configure.sh bl602evb:pinedio
    
    ## For ESP32: Configure the build for ESP32.
    ## TODO: Change "esp32-devkitc" to our ESP32 board.
    ./tools/configure.sh esp32-devkitc:nsh
    
    ## Edit the Build Config
    make menuconfig 
    
  3. Enable I2C0 Port

    For BL602 / BL604: Check the box for “System Type”“BL602 Peripheral Support”“I2C0”

    For ESP32: Check the box for “System Type”“ESP32 Peripheral Select”“I2C 0”

    Hit “Exit” until the Top Menu appears. (“NuttX/x64_64 Configuration”)

    Enable the I2C Port and I2C Character Driver

  4. Enable I2C Character Driver

    Check the box for “Device Drivers”“I2C Driver Support”“I2C Character Driver”

    Hit “Exit” until the Top Menu appears. (“NuttX/x64_64 Configuration”)

  5. Enable BME280 Driver

    Check the box for “Device Drivers”“Sensor Device Support”

    Select “Sensor Device Support”

    Check the box for “Bosch BME280 Sensor”

    Hit “Exit” until the Top Menu appears. (“NuttX/x64_64 Configuration”)

    The BME280 Driver will appear in NuttX at /dev/uorb/sensor_baro0 and /dev/uorb/sensor_humi0

  6. Enable Sensor Driver Test App

    Check the box for “Application Configuration”“Testing”“Sensor Driver Test”

    Set “Sensor Driver Test Stack Size” to 4096

    Hit “Exit” until the Top Menu appears. (“NuttX/x64_64 Configuration”)

    Enable the Sensor Test App

  7. Enable ls command…

    Select “Application Configuration”“NSH Library”“Disable Individual commands”

    Uncheck “Disable ls”

    Hit “Exit” until the Top Menu appears. (“NuttX/x64_64 Configuration”)

  8. Enable Logging and Assertion Checks

    Select “Build Setup”“Debug Options”

    Check the boxes for the following…

    Enable Debug Features
    Enable Error Output
    Enable Warnings Output
    Enable Informational Debug Output
    Enable Debug Assertions
    I2C Debug Features
    I2C Error Output
    I2C Warnings Output
    I2C Informational Output  
    Sensor Debug Features
    Sensor Error Output
    Sensor Warnings Output  
    Sensor Informational Output 
    

    Hit “Exit” until the Top Menu appears. (“NuttX/x64_64 Configuration”)

  9. Save the configuration and exit menuconfig

    (See the .config for BL602)

§12.3 Build NuttX

Follow these steps to build NuttX for BL602, BL604 or ESP32…

  1. To build NuttX, enter this command…

    make
    
  2. We should see…

    LD: nuttx
    CP: nuttx.hex
    CP: nuttx.bin
    

    (See the complete log for BL602 / BL604)

  3. For WSL: Copy the NuttX Firmware to the c:\blflash directory in the Windows File System…

    ##  /mnt/c/blflash refers to c:\blflash in Windows
    mkdir /mnt/c/blflash
    cp nuttx.bin /mnt/c/blflash
    

    For WSL we need to run blflash under plain old Windows CMD (not WSL) because it needs to access the COM port.

  4. In case of problems, refer to the NuttX Docs

    “BL602 / BL604 NuttX”

    “ESP32 NuttX”

    “Installing NuttX”

Building NuttX

§12.4 Flash NuttX

For ESP32: See instructions here (Also check out this article)

For BL602 / BL604: Follow these steps to install blflash

  1. “Install rustup”

  2. “Download and build blflash”

We assume that our Firmware Binary File nuttx.bin has been copied to the blflash folder.

Set BL602 / BL604 to Flashing Mode and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to High (Like this)

  2. Disconnect the USB cable and reconnect

    Or use the Improvised Reset Button (Here’s how)

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the H Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Connect BL10 to the USB port

  2. Press and hold the D8 Button (GPIO 8)

  3. Press and release the EN Button (Reset)

  4. Release the D8 Button

For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to 3.3V

  3. Reconnect the board to the USB port

Enter these commands to flash nuttx.bin to BL602 / BL604 over UART…

## For Linux: Change "/dev/ttyUSB0" to the BL602 / BL604 Serial Port
blflash flash nuttx.bin \
  --port /dev/ttyUSB0 

## For macOS: Change "/dev/tty.usbserial-1410" to the BL602 / BL604 Serial Port
blflash flash nuttx.bin \
  --port /dev/tty.usbserial-1410 \
  --initial-baud-rate 230400 \
  --baud-rate 230400

## For Windows: Change "COM5" to the BL602 / BL604 Serial Port
blflash flash c:\blflash\nuttx.bin --port COM5

(See the Output Log)

For WSL: Do this under plain old Windows CMD (not WSL) because blflash needs to access the COM port.

(Flashing WiFi apps to BL602 / BL604? Remember to use bl_rfbin)

(More details on flashing firmware)

Flashing NuttX

§12.5 Run NuttX

For ESP32: Use Picocom to connect to ESP32 over UART…

picocom -b 115200 /dev/ttyUSB0

(More about this)

For BL602 / BL604: Set BL602 / BL604 to Normal Mode (Non-Flashing) and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to Low (Like this)

  2. Disconnect the USB cable and reconnect

    Or use the Improvised Reset Button (Here’s how)

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the L Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Press and release the EN Button (Reset)

For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to GND

  3. Reconnect the board to the USB port

After restarting, connect to BL602 / BL604’s UART Port at 2 Mbps like so…

For Linux:

screen /dev/ttyUSB0 2000000

For macOS: Use CoolTerm (See this)

For Windows: Use putty (See this)

Alternatively: Use the Web Serial Terminal (See this)

Press Enter to reveal the NuttX Shell

NuttShell (NSH) NuttX-10.2.0-RC0
nsh>

Congratulations NuttX is now running on BL602 / BL604!

(More details on connecting to BL602 / BL604)

Running NuttX

macOS Tip: Here’s the script I use to build, flash and run NuttX on macOS, all in a single step: run.sh

Script to build, flash and run NuttX on macOS

(Source)