Embedded Rust & Embassy: I2C Temperature Sensing with BMP180
Tags: rust, tutorial, beginners, programming
Published: 2022-12-19 16:51:06

> This blog post is the fourth of a multi-part series of posts about Rust embassy for the STM32. This post is going to explore communicating over I2C using the embassy HAL. Please be aware that certain concepts in newer posts could depend on concepts in prior posts.    

#### If you find this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%} 

## Introduction

In this post, I will be configuring an STM32 I2C peripheral using the embassy HAL to collect ambient temperature measurement data from the [BMP180](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf) Digital Pressure Sensor. Note that the BMP180 provides both pressure and temperature data but I will only be collecting the latter. Temperature measurements will be continuously collected and sent to a PC terminal over UART. This code will be built without using any interrupts. This means that I will be leveraging only the embassy HAL without the executor or the `async` framework.

**🚨 Important Note:**

> The BMP180 provides both temperature and pressure sensor data which are collected in a very similar manner. I elected to collect only temperature data since the conversion equations are less. My target in this blog post is to focus on operating I2C more than the features of the BMP180 itself. However, the provided code can be easily expanded to collect and convert pressure data as well. I probably will be expanding the code anyway to create a BMP180 device driver at a later time.

### **📚** Knowledge Pre-requisites

To understand the content of this post, you need the following:

*   Basic knowledge of coding in Rust.
    
*   Familiarity with the basic template for creating embedded applications in Rust.
    
*   Familiarity with [I2C communication basics](https://en.wikipedia.org/wiki/I%C2%B2C).
    
*   Familiarity with [UART communication basics](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter).
    

### **💾** Software Setup

All the code presented in this post in addition to instructions for the environment and toolchain setup are available on the [apollolabsdev Nucleo-F401RE](https://github.com/apollolabsdev/stm32-nucleo-f401re) git repo. Note that if the code on the git repo is slightly different then it means that it was modified to enhance the code quality or accommodate any HAL/Rust updates.

In addition to the above, you would need to install some sort of serial communication terminal on your host PC. Some recommendations include:

**For Windows**:

*   [**PuTTy**](https://www.putty.org/)
    
*   [**Teraterm**](https://ttssh2.osdn.jp/index.html.en)
    
*   [**Serial Studio**](https://serial-studio.github.io/)
    

**For Mac and Linux**:

*   [**minicom**](https://wiki.emacinc.com/wiki/Getting_Started_With_Minicom)
    
*   [**Serial Studio**](https://serial-studio.github.io/)
    

Apart from Serial Studio, some detailed instructions for the different operating systems are available in the [**Discovery Book**](https://docs.rust-embedded.org/discovery/microbit/06-serial-communication/index.html).

For me, Serial Studio comes highly recommended. I personally came across [**Serial Studio**](https://serial-studio.github.io/) recently and found it to be awesome for two main reasons. First is that you can skip many of those instructions for other tools, especially in Mac and Linux systems. Second, if you are you want to graph data over UART, it has a really nice and easy-to-configure setup. It's also open-source and free to use.

### **🛠** Hardware Setup

#### **👔** Materials

*   [Nucleo-F401RE board](https://amzn.to/3yn6AIb)
    
![Nucleo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/96ikdsoq1vxy83ohhp1m.jpg)

*   [BMP 180 GY-68 Barometric Pressure and Temperature Sensor](https://www.amazon.com/HiLetgo-Digital-Barometric-Pressure-Replace/dp/B01F527EXS)
    
![BMP180](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9r76p406f3kyz94u8wwz.jpg)

#### **🔌** Connections

*   BMP180 module SCL pin connected to Nucleo board pin PB8.
    
*   BMP180 module SDA pin connected to Nucleo board pin PB9.
    
*   BMP180 module Vcc pin connected to Nucleo board 3.3V.
    
*   BMP180 module GND pin connected to Nucleo board GND.
    
*   The UART Tx line that connects to the PC through the onboard USB bridge is via pin PA2 on the microcontroller. This is a hardwired pin, meaning you cannot use any other for this setup. Unless you are using a different board other than the Nucleo-F401RE, you have to check the relevant documentation (reference manual or datasheet) to determine the number of the pin.
    

## **👨‍🎨**Software Design

The software design for this application can be obtained directly from the [BMP180 datasheet](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf). The [datasheet](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf) for the BMP180 provides a full flow chart describing the algorithmic steps. The flow chart also provides a list of conversion formulas needed to calculate the compensated temperature and pressure. The display temperature value state at the end of the flow would be the step where I would send the result over UART.

![Flow Chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ellpq5u7pvbsuoig9t5.png)

From the above flow chart, I will only be implementing the parts to do with temperature collection. The same code can be easily expanded to pressure as well. One would only have to implement the additional equations.

## **👨‍💻** Code Implementation

### **📥** Crate Imports

In this implementation, the following crates are required:

*   The `cortex_m_rt` crate for startup code and minimal runtime for Cortex-M microcontrollers.
    
*   The `heapless` crate to import and create a fixed capacity `String`.
    
*   The `core::fmt` crate will allow us to use the `writeln!` macro for easy printing.
    
*   The `panic_halt` crate to define the panicking behavior to halt on panic.
    
*   The `embassy_time` crate to import timekeeping capabilities.
    
*   The `embassy_stm32` crate to import the embassy STM32 series microcontroller device hardware abstractions. The needed abstractions are imported accordingly.
    
*   The `panic_halt` crate to define the panicking behavior to halt on panic.
    

```rust
use core::fmt::Write;
use cortex_m::prelude::_embedded_hal_blocking_delay_DelayMs;
use heapless::String;

use cortex_m_rt::entry;
use embassy_stm32::dma::NoDma;
use embassy_stm32::i2c::I2c;
use embassy_stm32::interrupt;
use embassy_stm32::time::hz;
use embassy_stm32::usart::{Config, UartTx};
use embassy_time::Delay;
use panic_halt as _;
```

### **🎛** Peripheral Configuration Code

#### I2C Peripheral Configuration:

1️⃣ **Initialize MCU and obtain a handle for the device peripherals**: A device peripheral handler `p` is created:

```rust
let p = embassy_stm32::init(Default::default());
```

2️⃣ **Configure the I2C peripheral channel**: Looking into the [Nucleo-F401RE board pinout](https://os.mbed.com/platforms/ST-Nucleo-F401RE/), the SDA and SCL lines pins PB8 and PB9 connect to the I2C1 peripheral in the microcontroller device. As such, this means we need to configure I2C1 and somehow pass it to the handles of the pins we want to use.

In the example I am creating in this post, I will be using I2C only in a blocking manner. This would typically mean that no interrupts are required as all tasks would be polled. Though in configuring I2C in embassy, as it stands, it seems that an interrupt still needs to be attached although we will not be using it. This means that an additional step is required ahead of configuring I2C. In order to attach an interrupt to the I2C configuration, a handle for the interrupt source needs to be created using the `take!` macro. There are also two interrupt sources listed for I2C in embassy, `I2C2_EV` (for I2C interrupt event) and `I2C2_ER` (for I2C interrupt error). The interrupt handle `irq` is created as follows:

```rust
let irq = interrupt::take!(I2C1_EV);
```

In the embassy I2C driver [documentation](https://docs.embassy.dev/embassy-stm32/git/stm32f030c6/i2c/struct.I2c.html) a `new` method exists to configure I2C and looks as follows:

```rust
pub fn new(
    peri: impl Peripheral<P = T> + 'd,
    scl: impl Peripheral<P = impl SclPin<T>> + 'd,
    sda: impl Peripheral<P = impl SdaPin<T>> + 'd,
    irq: impl Peripheral<P = T::Interrupt> + 'd,
    tx_dma: impl Peripheral<P = TXDMA> + 'd,
    rx_dma: impl Peripheral<P = RXDMA> + 'd,
    freq: Hertz
) -> Self
```

Where `peri` expects an instance to an I2C peripheral, `scl` and `sda` expect instances of a GPIO pin, `irq` expects a handle to an interrupt, `tx_dma` and `rx_dma` expect instances to a DMA channel, `freq` expects I2C transaction frequency. What I cam to realize is that when using the shown method, I got a compile error that I wasn't including a `Config` parameter. It turns out that the documentation in this case is outdated and the source code contains a slightly different signature:

```rust
pub fn new(
        peri: impl Peripheral<P = T> + 'd,
        scl: impl Peripheral<P = impl SclPin<T>> + 'd,
        sda: impl Peripheral<P = impl SdaPin<T>> + 'd,
        irq: impl Peripheral<P = T::Interrupt> + 'd,
        tx_dma: impl Peripheral<P = TXDMA> + 'd,
        rx_dma: impl Peripheral<P = RXDMA> + 'd,
        freq: Hertz,
        config: Config,
    ) -> Self 
```

The difference includes a `Config` type which includes an I2C configuration. As such, we can create a handle `i2c` for I2C1 as follows:

```rust
    let mut i2c = I2c::new(
        p.I2C1,
        p.PB8,
        p.PB9,
        irq,
        NoDma,
        NoDma,
        hz(100000),
        Default::default(),
    );
```

Note how the different handles created earlier are passed as arguments as expected. Also the frequency is set to 100 kHz for operation, and `NoDma` is passed for DMA channel. The BMP180 datasheet states that the device can handle up to 3.4Mbit/sec so I only chose an arbitrary value under the stated limit. Finally, for the `Config` type, the `default` instance is passed, similar to what has been done before. The implementation for `Default` can be found in the same source code and looks as follows:

```rust
impl Default for Config {
    fn default() -> Self {
        Self {
            sda_pullup: false,
            scl_pullup: false,
        }
    }
}
```

Which from what can be seen is that `Config`, configures the state of the I2C line pull-up resistors. That's it for I2C configuration, now moving on to UART.

#### UART Serial Communication Peripheral Configuration:

1️⃣ **Configure UART and obtain handle**: On the Nucleo-F401RE board pinout, the Tx line pin PA2 connects to the USART2 peripheral in the microcontroller device. Similar to what was done in the [**embassy UART post**](https://apollolabsblog.hashnode.dev/embedded-rust-embassy-uart-serial-communication), an instance of USART2 is attached to the `usart` handle as follows:

```rust
let mut usart = UartTx::new(p.USART2, p.PA2, NoDma, Config::default());
```

Also, similar to before a `String` type `msg` handle is created to store the formatted text that will be transmitted over UART:

```rust
let mut msg: String<64> = String::new();
```

#### **Delay Configuration:**

In the algorithm, a blocking delay will need to be introduced to wait for the ADC conversion in the BMP180 to finish. A `delay` handle is created as follows as follows:

```rust
 let mut delay = Delay;
```

This is it for configuration! Let's now jump into the application code.

### **📱** Application Code

In the software design described, the first step requires that we read a bunch of calibration data from the EEPROM of BMP180. In order to that, I would need to set up and initialize some sort of a struct to save the calibration data. I called the struct type `Coeffs` and defined it as follows:

```rust
    struct Coeffs {
        ac5: i16,
        ac6: i16,
        mc: i16,
        md: i16,
    }
```

After which I instantiate `calib_coeffs` of type `Coeffs` initialized to all zeros:

```rust
    let mut calib_coeffs = Coeffs {
        ac5: 0,
        ac6: 0,
        mc: 0,
        md: 0,
    };
```

**📝 Note:**

> In the datasheet for the BMP180 there are 11 different calibration coefficients that need to be captured. Here I am capturing only the ones needed for temperature calculation.

Next, I define a bunch of constants that reflect the addresses for the calibration coefficients in the BMP180 EEPROM, the I2C address of the BMP180 itself (`BMP180_ADDR`), and the address to retrieve the BMP180 device ID (`REG_ID_ADDR`).

```rust
    const BMP180_ADDR: u8 = 0x77;
    const REG_ID_ADDR: u8 = 0xD0;
    const AC5_MSB_ADDR: u8 = 0xB2;
    const AC6_MSB_ADDR: u8 = 0xB4;
    const MC_MSB_ADDR: u8 = 0xBC;
    const MD_MSB_ADDR: u8 = 0xBE;
    const CTRL_MEAS_ADDR: u8 = 0xF4;
    const MEAS_OUT_LSB_ADDR: u8 = 0xF7;
    const MEAS_OUT_MSB_ADDR: u8 = 0xF6;
```

I also define two variables, a `[u8; 2]` array I named `rx_buffer` and an `i16` named `rx_word`. I will be using `rx_buffer` later to buffer data I read over I2C from the BMP180. `rx_word` will be used to reconstruct the read bytes into a 16-bit value.

```rust
let mut rx_buffer: [u8; 2] = [0; 2];
let mut rx_word: i16;
```

Before doing anything, we have to understand how the BMP180 communicates over I2C. Essentially, the way the BMP180 communication works, first there is a write (control) cycle indicating what internal BMP180 EEPROM address we want to read. Second, there is a read cycle where the data in the address that was requested in the write cycle is provided.

The first thing I need to read according to the algorithmic steps is the calibration coefficients. The datasheet recommends, however, that before reading any calibration coefficients from the BMP180, the BMP180 device ID is read as a sanity check. The device should provide back the value of `0x55`. To do that, digging into the stm32 embassy I2C method [documentation](https://docs.embassy.dev/embassy-stm32/git/stm32f030c6/i2c/struct.I2c.html), I found a `blocking_write` and `blocking_read` method with the following signatures:

```rust
pub fn blocking_write(
    &mut self, 
    address: u8, 
    bytes: &[u8]
) -> Result<(), Error>
```

and

```rust
pub fn blocking_read(
    &mut self,
    address: u8,
    buffer: &mut [u8]
) -> Result<(), Error>
```

Although there wasn't much description in the documentation, it can be seen that both are more or less similar with a minor difference. Both take two arguments and return a `Result`. The first argument `address` is the device address in which case for us will be the BMP180 device address `BMP180_ADDR`. The second argument for the `blocking_write` method is `bytes` and is an array slice containing the bytes being written. The second argument for the `read` method is `buffer` and is a mutable array slice containing the bytes being returned back by the addressed device.

There was also another method that caught my attention that looked useful. It was the `blocking_write_read` method. Again, although there wasn't much description, it works similar to the `write_read` method from the embedded-hal. The `blocking_write_read` method does a write cycle immediately followed by a read cycle. This will be useful in many contexts and save some lines of code. The `blocking_write_read` method has the following signature:

```rust
pub fn blocking_write_read(
    &mut self,
    address: u8,
    bytes: &[u8],
    buffer: &mut [u8]
) -> Result<(), Error>
```

The parameters are more or less the same as the earlier methods, only combined in one method.

So, now that I have what I need, to do our sanity check, the first step is reading from the BMP180 the device ID:

```rust
i2c.blocking_write(BMP180_ADDR, &[REG_ID_ADDR]).unwrap();
i2c.blocking_read(BMP180_ADDR, &mut rx_buffer).unwrap();
```

Analyzing the code, the first argument of the write and read methods is the device address `BMP180_ADDR`, the second argument of the write method is a slice containing the device ID retrieval address `REG_ID_ADDR`. In the read method, the second argument is the receive buffer `rx_buffer` that will contain the recieved data. Note that all the arguments I am passing I have been created at an earlier point in the application. The below `if` statement checks if the received ID is correct and sends the appropriate message accordingly over UART. Note that from the BMP180 datasheet, since `REG_ID_ADDR` returns only a single byte, I only had to check the first index in the buffer `rx_buffer[0]`

```rust
if rx_buffer[0] == 0x55 {
        core::writeln!(&mut msg, "Device ID is {}\r", rx_buffer[0]).unwrap();
        usart.blocking_write(msg.as_bytes()).unwrap();
   msg.clear();
    } else {
        core::writeln!(&mut msg, "Device ID Cannot be Detected \r").unwrap();
        usart.blocking_write(msg.as_bytes()).unwrap();
        msg.clear();
    }
```

Now that its verified that the device can be detected, let's start with the first step in the algorithmic steps which is collecting the calibration coefficients. Here's the code for retrieving calibration coefficient AC5:

```rust
i2c.blocking_write_read(BMP180_ADDR, &[AC5_MSB_ADDR], &mut rx_buffer)
        .unwrap();
rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
core::writeln!(&mut msg, "AC5 = {} \r", rx_word).unwrap();
usart.blocking_write(msg.as_bytes()).unwrap();
msg.clear();
calib_coeffs.ac5 = rx_word;
```

As might be noticed I used the `blocking_write_read` method exactly in a simiar manner that I did earlier. There are three differences to note here though:

1.  Recall I mentioned that the BMP180 provides a 16-bit value for each calibration coefficient. Since I2C communicates in bytes, each coefficient is broken down into two bytes MSB and LSB, with each having its own address in the BMP180 EEPROM. This means that technically I would need to retrieve each address separately and reconstruct the 16-bit value. Though it turns out that by sending only the MSB address to the BMP180 (`AC5_MSB_ADDR` in this case) the device sends back both the MSB followed by the LSB without needing to address the LSB separately. The MSB will be located in the first index of the buffer and the LSB in the second index. So the point here is that I needed to call the `read_write` method only once using `AC5_MSB_ADDR` to retrieve both `AC5_MSB_ADDR` and `AC5_LSB_ADDR`.
    
2.  Since an `i16` will be provided back, the line `rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;` takes the MSB bits from `rx_buffer[0]`, casts them to an `i16` and shifts the 1 byte (8 times) to the left using the `<<` operator, then ORs the result using the `|` operator with the LSB bits in `rx_buffer[1]` that are also cast as `i16`. The result is finally stored in `rx_word` and then sent over the UART channel.
    
3.  The `rx_word` is stored in the `calib_coeffs` struct I had created earlier in the statement `calib_coeffs.ac5 = rx_word;`. This would also allow me to reuse rx\_word for the following operations.
    

This coefficient obtaining code is repeated in the exact same manner three times to retrieve the AC6, MC, and MD coefficients. Obviously, the only differences would be the address sent to the BMP180, and the name of the member I store in the `calib_coeffs` struct.

Now that the calibration coefficients are available, the measurement loop can be started. The first step as indicated by the software design is to kick-off the temperature measurement in the BMP180 by writing 0x2E in register with address 0xF4 ( keyed in as `CTRL_MEAS_ADDR`). This is done using two write cycles first sending `0x2E` followed by `CTRL_MEAS_ADDR`. This can be done in a single line as well. Since the `blocking_write` method accepts a slice in the `bytes` parameter in its signature, all the bytes that need to be sent can be included in the slice as follows:

```rust
i2c.blocking_write(BMP180_ADDR, &[CTRL_MEAS_ADDR, 0x2E]).unwrap();
```

This statement will send the `CTRL_MEAS_ADDR` to the BMP180 followed by the value `0x2E`. For those familiar with I2C terms, although I didn't verify at the low level, nor does the documentation specify, but here a "repeated start" should be what is occurring.

After kicking off the BMP180 temperature measurement, the datasheet tells us we need to wait at least 4.5ms. As such, a 5ms is introduced to stay on the safe side. This is done using the `delay` handle created earlier:

```rust
 delay.delay_ms(5_u32);
```

Now the temperature measurement should be ready to collect by reading the measurement BMP180 MSB and LSB EEPROM addresses. This can be done using the similar approach to before where only the MSB address is sent. Alternatively, here a different approach is adopted by reading the MSB and the LSB separately:

```rust
// Read Measurement MSB
i2c.blocking_write(BMP180_ADDR, &[MEAS_OUT_MSB_ADDR]).unwrap();
i2c.blocking_read(BMP180_ADDR, &mut rx_buffer).unwrap();
rx_word = (rx_buffer[0] as i16) << 8;
// Read Measurement LSB
i2c.blocking_write(BMP180_ADDR, &[MEAS_OUT_LSB_ADDR]).unwrap();
i2c.blocking_read(BMP180_ADDR, &mut rx_buffer).unwrap();
rx_word |= rx_buffer[0] as i16;
```

Finally, since the temperature value received is uncompensated. The datasheet provides us with a bunch of formulas to calculate the compensated temperature based on the calibration coefficients that were collected earlier. The following code implements the formulas in the datasheet to calculate the temperature followed by sending the result over UART:

```rust
let x1 = (rx_word as i32 - calib_coeffs.ac6 as i32) * (calib_coeffs.ac5 as i32) >> 15;
let x2 = ((calib_coeffs.mc as i32) << 11) / (x1 + calib_coeffs.md as i32);
let b5 = x1 + x2;
let t = ((b5 + 8) >> 4) / 10;

// Print Temperature Value
core::writeln!(&mut msg, "Temperature = {:} \r", t).unwrap();
usart.blocking_write(msg.as_bytes()).unwrap();
msg.clear();
```

Note how values are cast as `i32` so to prevent overflow from the multiplication operations.

This is it!

## **📀** Full Application Code

Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the [apollolabsdev Nucleo-F401RE](https://github.com/apollolabsdev/stm32-nucleo-f401re) git repo.

```rust
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]

use core::fmt::Write;
use cortex_m::prelude::_embedded_hal_blocking_delay_DelayMs;
use heapless::String;

use cortex_m_rt::entry;
use embassy_stm32::dma::NoDma;
use embassy_stm32::i2c::I2c;
use embassy_stm32::interrupt;
use embassy_stm32::time::hz;
use embassy_stm32::usart::{Config, UartTx};
use embassy_time::Delay;
use panic_halt as _;

#[entry]
fn main() -> ! {
    // Initialize and create handle for devicer peripherals
    let p = embassy_stm32::init(Default::default());

    let irq = interrupt::take!(I2C1_EV);
    // I2C Configuration
    let mut i2c = I2c::new(
        p.I2C1,
        p.PB8,
        p.PB9,
        irq,
        NoDma,
        NoDma,
        hz(100000),
        Default::default(),
    );

    //Configure UART
    let mut usart = UartTx::new(p.USART2, p.PA2, NoDma, Config::default());

    // Create empty String for message
    let mut msg: String<64> = String::new();

    // Delay Handle
    let mut delay = Delay;

    struct Coeffs {
        ac5: i16,
        ac6: i16,
        mc: i16,
        md: i16,
    }

    let mut calib_coeffs = Coeffs {
        ac5: 0,
        ac6: 0,
        mc: 0,
        md: 0,
    };

    const BMP180_ADDR: u8 = 0x77;
    const REG_ID_ADDR: u8 = 0xD0;
    const AC5_MSB_ADDR: u8 = 0xB2;
    const AC6_MSB_ADDR: u8 = 0xB4;
    const MC_MSB_ADDR: u8 = 0xBC;
    const MD_MSB_ADDR: u8 = 0xBE;
    const CTRL_MEAS_ADDR: u8 = 0xF4;
    const MEAS_OUT_LSB_ADDR: u8 = 0xF7;
    const MEAS_OUT_MSB_ADDR: u8 = 0xF6;

    let mut rx_buffer: [u8; 2] = [0; 2];
    let mut rx_word: i16;

    // Read Device ID as Sanity Check
    i2c.blocking_write(BMP180_ADDR, &[REG_ID_ADDR]).unwrap();
    i2c.blocking_read(BMP180_ADDR, &mut rx_buffer).unwrap();

    if rx_buffer[0] == 0x55 {
        core::writeln!(&mut msg, "Device ID is {}\r", rx_buffer[0]).unwrap();
        // Transmit Message
        usart.blocking_write(msg.as_bytes()).unwrap();
        // Clear String for next message
        msg.clear();
    } else {
        core::writeln!(&mut msg, "Device ID Cannot be Detected \r").unwrap();
        usart.blocking_write(msg.as_bytes()).unwrap();
        msg.clear();
    }

    // Read Calibration Coefficients
    // Read AC5
    i2c.blocking_write_read(BMP180_ADDR, &[AC5_MSB_ADDR], &mut rx_buffer)
        .unwrap();
    rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
    core::writeln!(&mut msg, "AC5 = {} \r", rx_word).unwrap();
    usart.blocking_write(msg.as_bytes()).unwrap();
    msg.clear();
    calib_coeffs.ac5 = rx_word;

    // Read AC6
    i2c.blocking_write_read(BMP180_ADDR, &[AC6_MSB_ADDR], &mut rx_buffer)
        .unwrap();
    rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
    core::writeln!(&mut msg, "AC6 = {} \r", rx_word).unwrap();
    usart.blocking_write(msg.as_bytes()).unwrap();
    msg.clear();
    calib_coeffs.ac6 = rx_word;

    // Read MC
    i2c.blocking_write_read(BMP180_ADDR, &[MC_MSB_ADDR], &mut rx_buffer)
        .unwrap();
    rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
    core::writeln!(&mut msg, "MC = {} \r", rx_word).unwrap();
    usart.blocking_write(msg.as_bytes()).unwrap();
    msg.clear();
    calib_coeffs.mc = rx_word;

    // Read MD
    i2c.blocking_write_read(BMP180_ADDR, &[MD_MSB_ADDR], &mut rx_buffer)
        .unwrap();
    rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
    core::writeln!(&mut msg, "MD = {} \r", rx_word).unwrap();
    usart.blocking_write(msg.as_bytes()).unwrap();
    msg.clear();
    calib_coeffs.md = rx_word;

    // Application Loop
    loop {
        // Kick off Temperature Measurement by writing 0x2E in register 0xF4
        i2c.blocking_write(BMP180_ADDR, &[CTRL_MEAS_ADDR, 0x2E])
            .unwrap();
        // Wait at least 4.5 ms for measurment to complete as specified by the datasheet
        delay.delay_ms(5_u32);

        // Collect Temperature Measurment
        // Read Measurement MSB
        // Achieving same as above using an alternate method syntax here to do a write followed by read
        i2c.blocking_write(BMP180_ADDR, &[MEAS_OUT_MSB_ADDR])
            .unwrap();
        i2c.blocking_read(BMP180_ADDR, &mut rx_buffer).unwrap();
        rx_word = (rx_buffer[0] as i16) << 8;
        // Read Measurement LSB
        i2c.blocking_write(BMP180_ADDR, &[MEAS_OUT_LSB_ADDR])
            .unwrap();
        i2c.blocking_read(BMP180_ADDR, &mut rx_buffer).unwrap();
        rx_word |= rx_buffer[0] as i16;

        // Uncomment following line to print raw uncompenstated temperature value
        //writeln!(tx, "UT = {} \r", rx_word).unwrap();

        // Calculate Temperature According to Datasheet Formulas
        let x1 = (rx_word as i32 - calib_coeffs.ac6 as i32) * (calib_coeffs.ac5 as i32) >> 15;
        let x2 = ((calib_coeffs.mc as i32) << 11) / (x1 + calib_coeffs.md as i32);
        let b5 = x1 + x2;
        let t = ((b5 + 8) >> 4) / 10;

        // Print Temperature Value
        core::writeln!(&mut msg, "Temperature = {:} \r", t).unwrap();
        usart.blocking_write(msg.as_bytes()).unwrap();
        msg.clear();
    }
}
```

## 🔬 Further Experimentation/Ideas:

Some ideas to experiment with include:

*   Expanding the code to collect pressure measurement data including calibration data and calculate barometric pressure.
    
*   The BMP180 has different accuracy modes in which more accurate measurements can be provided but also different wait times apply as well. You can refactor the code or even create functions that can select one of the desired modes.
    

## Conclusion

In this post, an I2C temperature measurement application was created STM32 using the embassy framework HAL by controlling and collecting data from an external BMP180 sensor. This application leverages an I2C peripheral for the STM32F401RE microcontroller on the Nucleo-F401RE development board. The resulting measurement is also sent over to a host PC over a UART connection. All code was based on polling (without interrupts). Have any questions? Share your thoughts in the comments below 👇.

#### If you found this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%}