Rust talks I2C on Apache NuttX RTOS

đź“ť 22 Mar 2022

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

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

I2C is a great way to connect all kinds of Sensor Modules when we’re creating an IoT Gadget. Like sensors for temperature, light, motion, spectroscopy, soil moisture, GPS, LIDAR, … and many more!

But where will we get the Software Drivers for the I2C Sensors?

Embedded Rust has a large collection of drivers for I2C Sensors. And they will work on many platforms!

Today we shall experiment with the Rust Driver for Bosch BME280 Sensor (Temperature / Humdity / Air Pressure). And learn how we made it work on the (Linux-like) Apache NuttX RTOS.

We’ll run this on the BL602 RISC-V SoC (pic above), though it should work fine on ESP32 and other NuttX platforms.

Let’s dive into our Rust I2C App for NuttX…

Read Sensor Data from BME280

(Source)

§1 Read Sensor Data from BME280

Here’s how we read the Temperature, Humidity and Air Pressure from the BME280 Sensor: rust/src/bme280.rs

/// Read Temperature, Pressure and Humidity from BME280 Sensor over I2C
pub fn read_bme280() {

  //  Open I2C Port
  let i2c = nuttx_embedded_hal::I2c::new(
    "/dev/i2c0",  //  I2C Port
    400000,       //  I2C Frequency: 400 kHz
  ).expect("open failed");

We begin by opening the I2C Port “/dev/i2c0”, configured for 400 kHz.

(This halts with an error if the I2C Port doesn’t exist)

What’s nuttx_embedded_hal?

That’s the Hardware Abstraction Layer (HAL) for NuttX, coded in Rust. (More about this in a while)

Next we initialise the BME280 Driver…

  //  Init the BME280 Driver
  let mut bme280 = bme280::BME280::new(
    i2c,   //  I2C Port
    0x77,  //  I2C Address of BME280
    nuttx_embedded_hal::Delay  //  Delay Interface
  );

BME280 comes from the BME280 Driver Crate. (As we’ll see soon)

Before reading the BME280 Sensor, we initialise the sensor…

  //  Init the BME280 Sensor
  bme280.init()
    .expect("init failed");

(This halts with an error if the initialisation fails)

We’re ready to read the Temperature, Humidity and Air Pressure from the BME280 Sensor…

  //  Measure Temperature, Pressure and Humidity
  let measurements = bme280.measure()
    .expect("measure failed");

Finally we print the Sensor Data…

  //  Print the measurements
  println!("Relative Humidity = {}%", 
    measurements.humidity);
  println!("Temperature = {} deg C",  
    measurements.temperature);
  println!("Pressure = {} pascals",   
    measurements.pressure);
}

That’s all we need to read the Sensor Data from the BME280 Sensor!

Where is println defined?

println comes from our NuttX Embedded HAL. We import it at the top…

//  Import Libraries
use nuttx_embedded_hal::{  //  NuttX Embedded HAL
  println,                 //  Print a formatted message
};

Before running our Rust App, let’s connect the BME280 Sensor.

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

§2 Connect BME280

We connect BME280 to Pine64’s PineCone BL602 Board as follows (pic above)…

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

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

/* I2C Configuration */
#define BOARD_I2C_SCL \
  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_I2C | \
  GPIO_PIN4)
#define BOARD_I2C_SDA \
  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_I2C | \
  GPIO_PIN3)

(Which pins can be used? See this)

We disabled the UART1 Port because it uses the same pins as I2C: board.h

#ifdef TODO  /* Remember to check for duplicate pins! */
#define BOARD_UART_1_RX_PIN \
  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_UART | \
  GPIO_PIN3)
#define BOARD_UART_1_TX_PIN \
  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_UART | \
  GPIO_PIN4)
#endif  /* TODO */

(UART0 is used by the Serial Console)

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)

Run BME280 App

§3 Run BME280 App

We’re ready to run our Rust App on NuttX!

  1. Follow these steps to build, flash and run NuttX…

    “Build, Flash and Run NuttX”

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

    ls /dev
    
  3. We should see our I2C Port that’s connected to BME280…

    /dev:
     i2c0
     ...
    
  4. To read the BME280 Sensor, enter this command…

    rust_i2c
    
  5. We should see the Relative Humidity, Temperature and Air Pressure…

    read_bme280
    Relative Humidity = 89.284164%
    Temperature = 29.942907 deg C
    Pressure = 100483.04 pascals
    Done!
    

    (See the complete log)

The Rust Driver for BME280 runs successfully on NuttX!

Rust Driver for BME280

§4 Rust Driver for BME280

We ran the Rust Driver for BME280 on NuttX… Without any code changes?

Yeah amazing right? Earlier we saw this: rust/src/bme280.rs

  //  Init the BME280 Driver
  let mut bme280 = bme280::BME280
    ::new( ... );

BME280 comes from the Rust Embedded Driver for BME280 (pic above)…

That we have added to our Cargo.toml…

## External Rust libraries used by this module.  See crates.io.
[dependencies]

## BME280 Driver: https://crates.io/crates/bme280
bme280 = "0.2.1"

## NuttX Embedded HAL: https://crates.io/crates/nuttx-embedded-hal
nuttx-embedded-hal = "1.0.10"  

## Rust Embedded HAL: https://crates.io/crates/embedded-hal
embedded-hal = "0.2.7"  

The Rust Driver for BME280 works on NuttX because of NuttX Embedded HAL. Let’s look inside.

(BTW: Always use the latest version of NuttX Embedded HAL)

NuttX Embedded HAL

§5 NuttX Embedded HAL

What’s NuttX Embedded HAL?

NuttX Embedded HAL (Hardware Abstraction Layer) is the Rust Library that exposes a Standard Rust Interface for the Input / Output Ports on NuttX: GPIO, I2C, SPI, …

Earlier we called NuttX Embedded HAL to open the I2C Port: bme280.rs

//  Open I2C Port
let i2c = nuttx_embedded_hal::I2c::new(
  "/dev/i2c0",  //  I2C Port
  400000,       //  I2C Frequency: 400 kHz
).expect("open failed");

And we passed it to the BME280 Driver…

//  Init the BME280 Driver
let mut bme280 = bme280::BME280::new(
  i2c,   //  I2C Port
  0x77,  //  I2C Address of BME280
  nuttx_embedded_hal::Delay  //  Delay Interface
);

(Delay also comes from NuttX Embedded HAL)

But BME280 Driver doesn’t know anything about NuttX?

That’s OK because the BME280 Driver and NuttX Embedded HAL both talk through the same interface: Rust Embedded HAL. (Pic above)

Rust Embedded HAL is the standard interface used by Rust Embedded Drivers (like the BME280 Driver) to talk to the GPIO / I2C / SPI ports.

(Rust Driver for LoRa SX1262 works on NuttX too)

That’s why Rust Embedded Drivers can run on many platforms?

Yep because the Rust Embedded HAL has been implemented on many platforms: Linux, FreeBSD, nRF52, STM32 Blue Pill, …

And now NuttX! (As NuttX Embedded HAL)

Call NuttX Embedded HAL to read I2C register

(Source)

§6 Read I2C Register

Can we call NuttX Embedded HAL in our own Rust Programs?

Yes we can! This is how we call NuttX Embedded HAL to read an I2C Register on BME280: test.rs

/// Read an I2C Register
pub fn test_hal_read() {

  //  Open I2C Port
  let mut i2c = nuttx_embedded_hal::I2c::new(
    "/dev/i2c0",  //  I2C Port
    400000,       //  I2C Frequency: 400 kHz
  ).expect("open failed");

This opens the I2C Port “/dev/i2c0” at 400 kHz. (We’ve seen this earlier)

Next we prepare a one-byte Receive Buffer that will receive the Register Value…

  //  Buffer for received Register Value (1 byte)
  let mut buf = [0 ; 1];  //  Init to 0

Then we call NuttX Embedded HAL to read Register 0xD0 from I2C Address 0x77…

  //  Read I2C Register
  i2c.write_read(
    0x77,     //  I2C Address
    &[0xD0],  //  Register ID
    &mut buf  //  Buffer to be received (Register Value)
  ).expect("read register failed");

Our Receive Buffer now contains the Register Value 0x60…

  //  Register Value must be BME280 Device ID (0x60)
  assert_eq!(buf[0], 0x60);
}

That’s how we call NuttX Embedded HAL to read an I2C Register!

NuttX Embedded HAL

How did we implement I2C in the NuttX Embedded HAL?

NuttX Embedded HAL accesses the I2C Port by calling the NuttX I2C Interface: open(), ioctl() and close(). (Pic above)

Check out the details in the Appendix…

Write I2C Register

(Source)

§7 Write I2C Register

What about writing to I2C Registers?

This code calls NuttX Embedded HAL to write an I2C Register: test.rs

/// Write an I2C Register
pub fn test_hal_write() {

  //  Open I2C Port
  let mut i2c = nuttx_embedded_hal::I2c::new(
    "/dev/i2c0",  //  I2C Port
    400000,       //  I2C Frequency: 400 kHz
  ).expect("open failed");

  //  Write 0xA0 to Register 0xF5
  i2c.write(
    0x77,          //  I2C Address
    &[0xF5, 0xA0]  //  Register ID and value
  ).expect("write register failed");

The implementation of i2c.write in NuttX Embedded HAL is explained here…

When we connect a Logic Analyser, we’ll see something unusual…

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

Write 0xA0 to Register 0xF4

There’s an extra I2C Read at the end, right after writing the Register ID 0xF5 and Register Value 0xA0.

But it’s harmless. NuttX Embedded HAL does this to work around the I2C quirks on BL602…

What about GPIO and SPI on NuttX Embedded HAL?

Yep they have been implemented in NuttX Embedded HAL…

Rust I2C on NuttX

§8 What’s Next

I had lots of fun running Rust on NuttX, I hope you’ll enjoy it too!

If you’re keen to make Rust on NuttX better, or if there’s something I should port to Rust on NuttX, please lemme know! 🙏

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/rusti2c.md

§9 Notes

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

NuttX Embedded HAL

§10 Appendix: Read I2C Register in Embedded HAL

How was NuttX Embedded HAL implemented in Rust?

NuttX Embedded HAL accesses the I2C Port by calling the NuttX I2C Interface: open(), ioctl() and close(). (Pic above)

To understand why, let’s look at a NuttX Rust Program that reads an I2C Register on BME280: rust/src/test.rs

/// Read an I2C Register
pub fn test_i2c() {

  //  Open I2C Port
  let i2c = unsafe { 
    open(b"/dev/i2c0\0".as_ptr(), O_RDWR) 
  };
  assert!(i2c > 0);

We begin by calling open() to open the I2C Port.

This is flagged as “unsafe” because we’re calling a C Function. (See this)

Next we prepare the buffers that will be sent and received over I2C…

  //  Send the Register ID 0xD0 (1 byte)
  let mut start = [0xD0 ; 1];

  //  Receive the Register Value (1 byte)
  let mut buf   = [0 ; 1];

We’ll read the I2C Register in 2 steps…

  1. Send the Register ID 0xD0

  2. Receive the Register Value

We define the First Step: Send the Register ID…

  //  Compose I2C Transfers
  let msg = [

    //  First I2C Message: Send Register ID
    i2c_msg_s {
      frequency: 400000,  //  I2C Frequency: 400 kHz
      addr:      0x77,    //  I2C Address
      buffer:    start.as_mut_ptr(),      //  Buffer to be sent (Register ID)
      length:    start.len() as ssize_t,  //  Length of the buffer in bytes

      //  For BL602: Register ID must be passed as I2C Sub Address
      #[cfg(target_arch = "riscv32")]  //  If architecture is RISC-V 32-bit...
      flags:     I2C_M_NOSTOP,  //  I2C Flags: Send I2C Sub Address
        
      //  Otherwise pass Register ID as I2C Data
      #[cfg(not(target_arch = "riscv32"))]  //  If architecture is not RISC-V 32-bit...
      flags:     0,  //  I2C Flags: None

      //  TODO: Check for BL602 specifically, not just RISC-V 32-bit
    },

(I2C_M_NOSTOP is needed because of a BL602 quirk)

And here’s the Second Step: Receive the Register Value…

    //  Second I2C Message: Receive Register Value
    i2c_msg_s {
      frequency: 400000,  //  I2C Frequency: 400 kHz
      addr:      0x77,    //  I2C Address
      buffer:    buf.as_mut_ptr(),      //  Buffer to be received
      length:    buf.len() as ssize_t,  //  Length of the buffer in bytes
      flags:     I2C_M_READ,   //  I2C Flags: Read from I2C Device
    },
  ];

Finally we execute the two steps by calling ioctl()…

  //  Compose ioctl Argument
  let xfer = i2c_transfer_s {
    msgv: msg.as_ptr(),         //  Array of I2C messages for the transfer
    msgc: msg.len() as size_t,  //  Number of messages in the array
  };

  //  Execute I2C Transfers
  let ret = unsafe { 
    ioctl(
      i2c,              //  I2C Port
      I2CIOC_TRANSFER,  //  I2C Transfer
      &xfer             //  I2C Messages for the transfer
    )
  };
  assert!(ret >= 0);

The Register Value appears in our Receive Buffer…

  //  Register Value must be BME280 Device ID (0x60)
  assert!(buf[0] == 0x60);
     
  //  Close the I2C Port
  unsafe { close(i2c); }
}

(See the Output Log)

The above Rust code looks highly similar to the C version…

Let’s look at the NuttX Types and Constants that we have ported from C to Rust.

Read I2C Register

(Source)

§10.1 NuttX Types and Constants

What are i2c_msg_s and i2c_transfer_s in the code above?

They are NuttX I2C Types that we have ported from C to Rust.

i2c_msg_s is the I2C Message Struct that defines each message that will be sent or received over I2C: nuttx-embedded-hal/src/lib.rs

/// I2C Message Struct: I2C transaction segment beginning with a START. A number of these can
/// be transferred together to form an arbitrary sequence of write/read
/// transfer to an I2C device.
/// Ported from C: https://github.com/lupyuen/nuttx/blob/rusti2c/include/nuttx/i2c/i2c_master.h#L208-L215
#[repr(C)]
pub struct i2c_msg_s {
    /// I2C Frequency
    pub frequency: u32,
    /// I2C Address
    pub addr: u16,
    /// I2C Flags (I2C_M_*)
    pub flags: u16,
    /// Buffer to be transferred
    pub buffer: *mut u8,
    /// Length of the buffer in bytes
    pub length: ssize_t,
}

i2c_transfer_s contains an array of I2C Message Structs that will be sent / received when we call ioctl() to execute the I2C Transfer…

/// I2C Transfer Struct: This structure is used to communicate with the I2C character driver in
/// order to perform IOCTL transfers.
/// Ported from C: https://github.com/lupyuen/nuttx/blob/rusti2c/include/nuttx/i2c/i2c_master.h#L231-L235
#[repr(C)]
pub struct i2c_transfer_s {
    /// Array of I2C messages for the transfer
    pub msgv: *const i2c_msg_s,
    /// Number of messages in the array
    pub msgc: size_t,
}

What about I2C_M_NOSTOP, I2C_M_READ and I2CIOC_TRANSFER?

I2C_M_NOSTOP, I2C_M_READ and I2CIOC_TRANSFER are NuttX I2C Constants that we have ported from C to Rust.

(See this)

NuttX I2C Types

(Source)

§10.2 Into Embedded HAL

The code above goes into NuttX Embedded HAL like so…

Into Embedded HAL

(Source)

This conforms to the I2C Interface that’s expected by Rust Embedded HAL…

/// NuttX Implementation of I2C WriteRead
impl i2c::WriteRead for I2c {
  ...
  /// Write `wbuf` to I2C Port and read `rbuf` from I2C Port.
  /// We assume this is a Read I2C Register operation, with Register ID at `wbuf[0]`.
  /// TODO: Handle other kinds of I2C operations
  fn write_read(
      &mut self,       //  I2C Bus
      addr: u8,        //  I2C Address
      wbuf: &[u8],     //  Buffer to be sent (Register ID)
      rbuf: &mut [u8]  //  Buffer to be received
  ) -> Result<(), Self::Error>  //  In case of error, return an error code
  { ... }

(Source)

What about the calls to open() and close()?

We moved open() into the new() constructor: hal.rs

/// NuttX Implementation of I2C Bus
impl I2c {
  /// Create an I2C Bus from a Device Path (e.g. "/dev/i2c0")
  pub fn new(path: &str, frequency: u32) -> Result<Self, i32> {

    //  Open the NuttX Device Path (e.g. "/dev/i2c0") for read-write
    let fd = open(path, O_RDWR);
    if fd < 0 { return Err(fd) }

    //  Return the I2C Bus
    Ok(Self { fd, frequency })
  }
}

(open is defined here)

And we moved close() into the drop() destructor: hal.rs

/// NuttX Implementation of I2C Bus
impl Drop for I2c {

  /// Close the I2C Bus
  fn drop(&mut self) {
    unsafe { close(self.fd) };
  }
}

I2c Struct contains a NuttX File Descriptor and the I2C Frequency: hal.rs

/// NuttX I2C Bus
pub struct I2c {
  /// NuttX File Descriptor
  fd: i32,
  /// I2C Frequency in Hz
  frequency: u32,
}

(See the Output Log)

Write I2C Register in Embedded HAL

(Source)

§11 Appendix: Write I2C Register in Embedded HAL

BL602 has a peculiar I2C Port that uses I2C Sub Addresses…

We tried all sequences of I2C Read / Write / Sub Address. Only this strange sequence works for writing to I2C Registers…

  1. Write I2C Register ID and Register Value together as I2C Sub Address

  2. Followed by Read I2C Data

Here’s the implementation in NuttX Embedded HAL: hal.rs

/// NuttX Implementation of I2C Write
impl i2c::Write for I2c {
    /// Error Type
    type Error = i32;

    /// Write `buf` to I2C Port.
    /// We assume this is a Write I2C Register operation, with Register ID at `buf[0]`.
    /// TODO: Handle other kinds of I2C operations
    fn write(&mut self, addr: u8, buf: &[u8]) -> Result<(), Self::Error> {
        //  Copy to local buffer because we need a mutable reference
        let mut buf2 = [0 ; 64];
        assert!(buf.len() <= buf2.len());
        buf2[..buf.len()].copy_from_slice(buf);

        //  Buffer for received I2C data
        let mut rbuf = [0 ; 1];

        //  Compose I2C Transfer
        let msg = [
            //  First I2C Message: Send Register ID and I2C Data as I2C Sub Address
            i2c_msg_s {
                frequency: self.frequency,  //  I2C Frequency
                addr:      addr as u16,     //  I2C Address
                buffer:    buf2.as_mut_ptr(),     //  Buffer to be sent
                length:    buf.len() as ssize_t,  //  Number of bytes to send

                //  For BL602: Register ID must be passed as I2C Sub Address
                #[cfg(target_arch = "riscv32")]  //  If architecture is RISC-V 32-bit...
                flags:     crate::I2C_M_NOSTOP,  //  I2C Flags: Send I2C Sub Address
                
                //  Otherwise pass Register ID as I2C Data
                #[cfg(not(target_arch = "riscv32"))]  //  If architecture is not RISC-V 32-bit...
                flags:     0,  //  I2C Flags: None

                //  TODO: Check for BL602 specifically (by target_abi?), not just RISC-V 32-bit
            },
            //  Second I2C Message: Read I2C Data, because this forces BL602 to send the first message correctly
            i2c_msg_s {
                frequency: self.frequency,  //  I2C Frequency
                addr:      addr as u16,     //  I2C Address
                buffer:    rbuf.as_mut_ptr(),      //  Buffer to be received
                length:    rbuf.len() as ssize_t,  //  Number of bytes to receive
                flags:     I2C_M_READ,  //  I2C Flags: Read I2C Data
            },
        ];
        
        //  Compose ioctl Argument to write I2C Registers
        let xfer = i2c_transfer_s {
            msgv: msg.as_ptr(),         //  Array of I2C messages for the transfer
            msgc: msg.len() as size_t,  //  Number of messages in the array
        };

        //  Execute I2C Transfer to write I2C Registers
        let ret = unsafe { 
            ioctl(
                self.fd,          //  I2C Port
                I2CIOC_TRANSFER,  //  I2C Transfer
                &xfer             //  I2C Messages for the transfer
            )
        };
        assert!(ret >= 0);   
        Ok(())
    }
}

Our Logic Analyser shows that BL602 writes correctly to the I2C Register (with a harmless I2C Read at the end)…

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

BL602 writes correctly to the I2C Register! With a harmless I2C Read at the end

(See the Output Log)

Read I2C Register in C

(Source)

§12 Appendix: Read I2C Register in C

This is how we read an I2C Register in C from a NuttX App…

static int bme280_reg_read(const struct device *priv,
    uint8_t start, uint8_t *buf, int size)
{
  DEBUGASSERT(priv != NULL);
  DEBUGASSERT(buf != NULL);
  struct i2c_msg_s msg[2];
  int ret;

  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;

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

  ret = I2C_TRANSFER(priv->i2c, msg, 2);

(Source)

How do we call I2C_TRANSFER from a NuttX App? Thanks to the I2C Demo App we have the answer…

int i2ctool_get(FAR struct i2ctool_s *i2ctool, int fd, uint8_t regaddr,
                FAR uint16_t *result)
{
  struct i2c_msg_s msg[2];
  ...
  int ret = i2cdev_transfer(fd, msg, 2);

(Source)

i2cdev_transfer is defined as…

int i2cdev_transfer(int fd, FAR struct i2c_msg_s *msgv, int msgc)
{
  struct i2c_transfer_s xfer;

  /* Set up the IOCTL argument */

  xfer.msgv = msgv;
  xfer.msgc = msgc;

  /* Perform the IOCTL */

  return ioctl(fd, I2CIOC_TRANSFER, (unsigned long)((uintptr_t)&xfer));
}

(Source)

We ported the code above to NuttX Embedded HAL. (See this)

§12.1 C Types and Constants

Earlier we’ve seen i2c_msg_s and i2c_transfer_s. They are defined as…

struct i2c_msg_s
{
  uint32_t frequency;         /* I2C frequency */
  uint16_t addr;              /* Slave address (7- or 10-bit) */
  uint16_t flags;             /* See I2C_M_* definitions */
  FAR uint8_t *buffer;        /* Buffer to be transferred */
  ssize_t length;             /* Length of the buffer in bytes */
};

(Source)

struct i2c_transfer_s
{
  FAR struct i2c_msg_s *msgv; /* Array of I2C messages for the transfer */
  size_t msgc;                /* Number of messages in the array. */
};

(Source)

I2CIOC_TRANSFER is defined as…

#define I2CIOC_TRANSFER      _I2CIOC(0x0001)

(Source)

_I2CIOC is defined as…

#define _I2CIOC(nr)       _IOC(_I2CBASE,nr)

(Source)

_IOC and _I2CBASE are defined as…

#define _IOC(type,nr)   ((type)|(nr))

(Source)

#define _I2CBASE        (0x2100) /* I2C driver commands */

(Source)

We ported these C Types and Constants to NuttX Embedded HAL. (See this)

§13 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)

§13.1 Download NuttX

Download the modified source code for NuttX OS and NuttX Apps…

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

Or if we prefer to add the Rust Library and App to our NuttX Project, follow these instructions…

  1. “Install Rust Library”

  2. “Install Rust I2C App”

(For PineDio Stack BL604: The Rust Library and App are already preinstalled)

§13.2 Configure NuttX

Now we configure our NuttX project…

  1. Install the build prerequisites…

    “Install Prerequisites”

  2. Install Rust from rustup.rs

  3. 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 
    
  4. Enable our Rust Library…

    Check the box for “Library Routines” → “Rust Library”

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

  5. Enable our Rust I2C App…

    Check the box for “Application Configuration” → “Examples” → “Rust I2C App”

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

  6. 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

  7. 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”)

  8. 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”)

  9. 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  
    

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

  10. Save the configuration and exit menuconfig

    (See the .config for BL602)

§13.3 Configure Rust Target

For BL602 / BL604: Skip to the next section

For ESP32-C3 (RISC-V):

  1. Run this command to install the Rust Target…

    rustup target add riscv32imc-unknown-none-elf
    
  2. Edit apps/examples/rust_i2c/run.sh

  3. Set “rust_build_target” and “rust_build_target_folder” to…

    riscv32imc-unknown-none-elf

  4. Remove “-Z build-std=core” from “rust_build_options”

For ESP32 (Xtensa):

  1. Install the Rust compiler fork with Xtensa support. (See this)

  2. Edit apps/examples/rust_i2c/run.sh

  3. Set “rust_build_target” and “rust_build_target_folder” to…

    xtensa-esp32-none-elf

  4. Remove “-Z build-std=core” from “rust_build_options”

(run.sh is explained here)

(More about Rust Targets)

§13.4 Build NuttX

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

  1. To build NuttX with Rust, run the Rust Build Script run.sh…

    pushd apps/examples/rust_i2c
    ./run.sh
    popd
    

    (run.sh is explained here)

  2. We should see…

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

    (See the complete log for BL602 / BL604)

  3. Ignore the errors at the “Flash NuttX” and “Run NuttX” steps

  4. 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.

  5. In case of problems, refer to the NuttX Docs…

    “BL602 NuttX”

    “ESP32 NuttX”

    “Installing NuttX”

Building NuttX

§13.5 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

§13.6 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

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

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