Embedded Mastery: C, Ada, Rust & Zig

Embedded Mastery: C, Ada, Rust & Zig

A Project-Based Tutorial for Embedded Development

Embedded Mastery Project

2026

A comprehensive, project-based tutorial for mastering embedded development in C, Ada, Rust, and Zig. Build 15 projects from LED blinker to safety-critical systems, all verified in QEMU and Renode emulators.

Project 4: I2C Temperature Sensor Driver (BMP280)

Introduction

After mastering GPIO and UART in Phase 1, you’re ready to tackle peripheral communication protocols. I2C (Inter-Integrated Circuit) is ubiquitous in embedded systems — nearly every sensor you encounter will support it. This project builds a complete BMP280 temperature and pressure sensor driver from scratch in C, Rust, Ada, and Zig.

What you’ll learn:

I2C Protocol Basics

I2C is a synchronous, multi-master, multi-slave serial protocol using two wires: SDA (data) and SCL (clock). Unlike UART, it includes clock synchronization and device addressing on the same bus.

Start and Stop Conditions

Condition SDA SCL
START High → Low (while SCL high) High
STOP Low → High (while SCL high) High
Data valid Must be stable High
Data change Allowed only when SCL is low Low

The START condition alerts all devices on the bus that a transaction is beginning. The STOP condition releases the bus.

Addressing and ACK

After START, the master sends a 7-bit (or 10-bit) address followed by a read/write bit:

| 7-bit Address | R/W | ACK |
| 1 0 1 1 0 0 0 |  0  |  0  |  ← Write to address 0x58

The BMP280 uses two possible addresses depending on the SDO pin: - SDO grounded: 0x76 (0b1110110) - SDO to VDD: 0x77 (0b1110111)

I2C Transaction Flow

A typical register read looks like this:

START → [ADDR+W] → ACK → [REG] → ACK → RESTART → [ADDR+R] → ACK → [DATA] → NACK → STOP

The repeated START (RESTART) is critical — it keeps the bus locked between the write phase (setting the register address) and the read phase (getting the data).

BMP280 Sensor Overview

The Bosch BMP280 measures temperature (-40°C to +85°C) and pressure (300–1100 hPa) with high accuracy. It communicates over I2C (up to 3.4 MHz) or SPI.

Key Registers

Address Name Description Access
0x88 calib00 Calibration data start (24 bytes) R
0xE0 id Chip ID (should be 0x58) R
0xE3 reset Soft reset (write 0xB6) W
0xF3 status Measuring/im_update bits R
0xF4 ctrl_meas Oversampling + mode control R/W
0xF5 config Filter + standby time R/W
0xF7 press_msb Pressure data MSB R
0xFA temp_msb Temperature data MSB R

Control Measurement Register (0xF4)

| osrs_t[2:0] | osrs_p[2:0] | mode[1:0] |

Configuration Register (0xF5)

| t_sb[2:0] | filter[2:0] | spi3w_en |

Calibration Data

The BMP280 stores 24 bytes of factory calibration coefficients at 0x88–0xA1. These are essential — raw sensor values are meaningless without compensation. The coefficients are:

Reading the Datasheet: Compensation Math

Bosch provides a reference implementation for converting raw ADC values to compensated temperature and pressure. The math uses 32-bit and 64-bit intermediate values to avoid overflow.

Temperature Compensation Algorithm

var1 = (adc_temp / 16384.0 - dig_T1 / 1024.0) * dig_T2
var2 = ((adc_temp / 131072.0 - dig_T1 / 8192.0) *
        (adc_temp / 131072.0 - dig_T1 / 8192.0)) * dig_T3
t_fine = var1 + var2
temp_c = (t_fine * 5 + 128) / 256   // result in 0.01°C units

The t_fine value is reused for pressure compensation.

Fixed-Point Arithmetic

Floating-point is available on Cortex-M4F (hardware FPU), but fixed-point is still useful for determinism and portability. The BMP280 compensation can be done entirely in fixed-point:

Tip: Always read the “Recommended settings” table in the datasheet. For weather monitoring: osrs_t=1, osrs_p=16, normal mode, filter=16, standby=0.5ms gives you 0.16 hPa RMS noise.

Error Handling and Timeout Management

I2C is prone to bus hangs — a slave holding SDA low will block all communication. Your driver must handle:

  1. Bus busy: Another master or stuck slave
  2. NACK on address: Device not present
  3. NACK on data: Register doesn’t exist
  4. Arbitration lost: Multi-master collision
  5. Timeout: Clock stretching exceeded limit

Each language handles these differently, as you’ll see below.

Renode includes a BMP280 model at I2C address 0x76, making it ideal for development without hardware.

# Install Renode
sudo apt install renode  # Ubuntu/Debian
brew install renode      # macOS

# Or build from source
git clone https://github.com/renode/renode.git
cd renode && ./build.sh

Implementation

STM32F405 Hardware Setup

Memory Map (STM32F405RG)

Region Address Range Size
Flash 0x08000000–0x080FFFFF 1024K
SRAM 0x20000000–0x2001FFFF 128K

RCC Clock Enable (AHB1 for GPIO, APB1 for I2C)

On STM32F4, GPIO clocks are on AHB1 (not APB2 like STM32F1):

#define RCC_BASE        0x40023800UL
#define RCC_AHB1ENR     (*(volatile uint32_t *)(RCC_BASE + 0x30))
#define RCC_APB1ENR     (*(volatile uint32_t *)(RCC_BASE + 0x40))

#define RCC_AHB1ENR_GPIOB_EN  (1U << 1)
#define RCC_APB1ENR_I2C1_EN   (1U << 21)

/* Enable GPIOB and I2C1 clocks */
RCC_AHB1ENR |= RCC_AHB1ENR_GPIOB_EN;
RCC_APB1ENR |= RCC_APB1ENR_I2C1_EN;

GPIO Configuration (STM32F4 MODER/OTYPER/OSPEEDR/PUPDR/AFR)

STM32F4 replaces the STM32F1 CRL/CRH model with separate registers:

Register Description Bits per pin
MODER Mode: 00=in, 01=out, 10=AF, 11=analog 2
OTYPER Output type: 0=push-pull, 1=open-drain 1
OSPEEDR Speed: 00=2MHz, 01=25MHz, 10=50MHz, 11=100MHz 2
PUPDR Pull-up/down: 00=none, 01=up, 10=down 2
AFRL/AFRH Alternate function number (pins 0-7 / 8-15) 4

For I2C1 on PB6 (SCL) and PB7 (SDA), both use AF4:

#define GPIOB_BASE      0x40020400UL
#define GPIOB_MODER     (*(volatile uint32_t *)(GPIOB_BASE + 0x00))
#define GPIOB_OTYPER    (*(volatile uint32_t *)(GPIOB_BASE + 0x04))
#define GPIOB_OSPEEDR   (*(volatile uint32_t *)(GPIOB_BASE + 0x08))
#define GPIOB_PUPDR     (*(volatile uint32_t *)(GPIOB_BASE + 0x0C))
#define GPIOB_AFRL      (*(volatile uint32_t *)(GPIOB_BASE + 0x20))

/* Configure PB6 (SCL) and PB7 (SDA) as alternate function, open-drain */

/* MODER: set pins 6 and 7 to alternate function mode (10) */
GPIOB_MODER &= ~((0x3U << 12) | (0x3U << 14));  /* Clear bits for pin 6 and 7 */
GPIOB_MODER |=  (0x2U << 12) | (0x2U << 14);    /* Set AF mode (10) */

/* OTYPER: set pins 6 and 7 to open-drain (1) */
GPIOB_OTYPER |= (1U << 6) | (1U << 7);

/* OSPEEDR: set to high speed (10 = 50 MHz) */
GPIOB_OSPEEDR |= (0x2U << 12) | (0x2U << 14);

/* PUPDR: pull-up (01) for I2C idle-high */
GPIOB_PUPDR &= ~((0x3U << 12) | (0x3U << 14));
GPIOB_PUPDR |=  (0x1U << 12) | (0x1U << 14);

/* AFRL: set AF4 for pins 6 and 7 (4 bits each) */
GPIOB_AFRL &= ~((0xFU << 24) | (0xFU << 28));   /* Clear AF for pin 6 and 7 */
GPIOB_AFRL |=  (0x4U << 24) | (0x4U << 28);     /* Set AF4 (I2C1) */

I2C TIMINGR Values (STM32F4)

The STM32F4 replaces CCR/TRISE with a single TIMINGR register. The layout is:

| 31:28   | 27:24 | 23:20    | 19:16   | 15:8    | 7:0     |
| PRESC   | -     | SCLDEL   | SDADEL  | SCLH    | SCLL    |
Speed APB1 Clock TIMINGR Value PRESC SCLDEL SDADEL SCLH SCLL
100 kHz 16 MHz 0x00300D14 3 13 1 20 20
400 kHz 16 MHz 0x0010020A 1 2 0 10 10

#### Linker Script (`stm32f405rg.ld`)

```ld
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

_estack = 0x20020000;

SECTIONS
{
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } > FLASH

    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.rodata)
        *(.rodata*)
        . = ALIGN(4);
        _etext = .;
    } > FLASH

    ._sidata = .;

    .data : AT (_sidata)
    {
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        *(.data*)
        . = ALIGN(4);
        _edata = .;
    } > RAM

    .bss :
    {
        . = ALIGN(4);
        _sbss = .;
        *(.bss)
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
    } > RAM
}

C: I2C Peripheral Driver + BMP280 Read

I2C Driver (i2c.h)

#ifndef I2C_DRIVER_H
#define I2C_DRIVER_H

#include <stdint.h>
#include <stdbool.h>

typedef enum {
    I2C_OK = 0,
    I2C_ERR_BUSY,
    I2C_ERR_NACK_ADDR,
    I2C_ERR_NACK_DATA,
    I2C_ERR_TIMEOUT,
    I2C_ERR_ARBITRATION,
} i2c_error_t;

typedef struct {
    volatile uint32_t *cr1;
    volatile uint32_t *cr2;
    volatile uint32_t *isr;
    volatile uint32_t *txdr;
    volatile uint32_t *rxdr;
    volatile uint32_t *oar1;
    volatile uint32_t *timingr;
    uint32_t i2c_clk;
} i2c_handle_t;

#define I2C_TIMEOUT_US  10000

i2c_error_t i2c_init(i2c_handle_t *hi2c, uint32_t speed_hz);
i2c_error_t i2c_write(i2c_handle_t *hi2c, uint8_t addr,
                      const uint8_t *data, uint16_t len);
i2c_error_t i2c_read(i2c_handle_t *hi2c, uint8_t addr,
                     uint8_t *data, uint16_t len);
i2c_error_t i2c_write_read(i2c_handle_t *hi2c, uint8_t addr,
                           const uint8_t *tx_data, uint16_t tx_len,
                           uint8_t *rx_data, uint16_t rx_len);

#endif

I2C Driver Implementation (i2c.c)

#include "i2c.h"

/* STM32F4 I2C register offsets (I2C2 peripheral shown) */
#define I2C2_BASE       0x40005800UL
#define I2C_CR1_OFF     0x00
#define I2C_CR2_OFF     0x04
#define I2C_OAR1_OFF    0x08
#define I2C_TIMINGR_OFF 0x10
#define I2C_ISR_OFF     0x18
#define I2C_TXDR_OFF    0x24
#define I2C_RXDR_OFF    0x28

#define CR1_PE          (1U << 0)
#define CR1_TXIE        (1U << 1)
#define CR1_RXIE        (1U << 2)
#define CR1_ERRIE       (1U << 3)

#define CR2_START       (1U << 13)
#define CR2_STOP        (1U << 14)
#define CR2_NACK        (1U << 15)
#define CR2_RD_WRN      (1U << 10)

#define ISR_TXE         (1U << 0)
#define ISR_TXIS        (1U << 1)
#define ISR_RXNE        (1U << 2)
#define ISR_ADDR        (1U << 3)
#define ISR_NACKF       (1U << 4)
#define ISR_STOPF       (1U << 5)
#define ISR_TC          (1U << 6)
#define ISR_BERR        (1U << 8)
#define ISR_ARLO        (1U << 9)
#define ISR_BUSY        (1U << 15)

static void delay_us(uint32_t us) {
    volatile uint32_t count = us * 4;  /* ~4 cycles per count at 16 MHz */
    while (count--) {
        __asm volatile ("nop");
    }
}

i2c_error_t i2c_init(i2c_handle_t *hi2c, uint32_t speed_hz) {
    hi2c->cr1 = (volatile uint32_t *)(I2C2_BASE + I2C_CR1_OFF);
    hi2c->cr2 = (volatile uint32_t *)(I2C2_BASE + I2C_CR2_OFF);
    hi2c->isr = (volatile uint32_t *)(I2C2_BASE + I2C_ISR_OFF);
    hi2c->txdr = (volatile uint32_t *)(I2C2_BASE + I2C_TXDR_OFF);
    hi2c->rxdr = (volatile uint32_t *)(I2C2_BASE + I2C_RXDR_OFF);
    hi2c->oar1 = (volatile uint32_t *)(I2C2_BASE + I2C_OAR1_OFF);
    hi2c->timingr = (volatile uint32_t *)(I2C2_BASE + I2C_TIMINGR_OFF);

    /* Disable peripheral during config */
    *hi2c->cr1 &= ~CR1_PE;

    /* Configure TIMINGR for 100 kHz at 16 MHz APB1 clock */
    /* TIMINGR = 0x00300D14: PRESC=3, SCLDEL=13, SDADEL=1, SCLH=20, SCLL=20 */
    *hi2c->timingr = 0x00300D14;

    /* Set own address (master mode, 7-bit) */
    *hi2c->oar1 = 0;

    /* Enable peripheral and error interrupts */
    *hi2c->cr1 = CR1_PE | CR1_ERRIE;

    return I2C_OK;
}

static i2c_error_t wait_flag(volatile uint32_t *isr, uint32_t flag,
                              uint32_t error_flags, uint32_t timeout_us) {
    uint32_t elapsed = 0;
    while (elapsed < timeout_us) {
        uint32_t status = *isr;
        if (status & error_flags) {
            if (status & ISR_NACKF) return I2C_ERR_NACK_ADDR;
            if (status & ISR_BERR)  return I2C_ERR_BUSY;
            if (status & ISR_ARLO)  return I2C_ERR_ARBITRATION;
        }
        if (status & flag) return I2C_OK;
        delay_us(1);
        elapsed++;
    }
    return I2C_ERR_TIMEOUT;
}

i2c_error_t i2c_write(i2c_handle_t *hi2c, uint8_t addr,
                      const uint8_t *data, uint16_t len) {
    if (*hi2c->isr & ISR_BUSY) return I2C_ERR_BUSY;

    /* Set slave address, number of bytes, auto-end mode, start */
    *hi2c->cr2 = ((uint32_t)addr << 1) | ((uint32_t)len << 16) |
                 (1U << 25) /* AUTOEND */ | CR2_START;

    for (uint16_t i = 0; i < len; i++) {
        i2c_error_t err = wait_flag(hi2c->isr, ISR_TXIS,
                                    ISR_NACKF | ISR_BERR | ISR_ARLO,
                                    I2C_TIMEOUT_US);
        if (err != I2C_OK) return err;
        *hi2c->txdr = data[i];
    }

    return wait_flag(hi2c->isr, ISR_STOPF, 0, I2C_TIMEOUT_US);
}

i2c_error_t i2c_read(i2c_handle_t *hi2c, uint8_t addr,
                     uint8_t *data, uint16_t len) {
    if (*hi2c->isr & ISR_BUSY) return I2C_ERR_BUSY;

    *hi2c->cr2 = ((uint32_t)addr << 1) | ((uint32_t)len << 16) |
                 (1U << 25) /* AUTOEND */ | CR2_RD_WRN | CR2_START;

    for (uint16_t i = 0; i < len; i++) {
        i2c_error_t err = wait_flag(hi2c->isr, ISR_RXNE,
                                    ISR_NACKF | ISR_BERR | ISR_ARLO,
                                    I2C_TIMEOUT_US);
        if (err != I2C_OK) return err;
        data[i] = (uint8_t)(*hi2c->rxdr & 0xFF);
    }

    return wait_flag(hi2c->isr, ISR_STOPF, 0, I2C_TIMEOUT_US);
}

i2c_error_t i2c_write_read(i2c_handle_t *hi2c, uint8_t addr,
                           const uint8_t *tx_data, uint16_t tx_len,
                           uint8_t *rx_data, uint16_t rx_len) {
    if (*hi2c->isr & ISR_BUSY) return I2C_ERR_BUSY;

    /* Write phase: send register address, no STOP (software end) */
    *hi2c->cr2 = ((uint32_t)addr << 1) | ((uint32_t)tx_len << 16) |
                 (0U << 25) /* SOFTEND */ | CR2_START;

    for (uint16_t i = 0; i < tx_len; i++) {
        i2c_error_t err = wait_flag(hi2c->isr, ISR_TXIS,
                                    ISR_NACKF | ISR_BERR | ISR_ARLO,
                                    I2C_TIMEOUT_US);
        if (err != I2C_OK) return err;
        *hi2c->txdr = tx_data[i];
    }

    /* Wait for transfer complete (repeated start next) */
    i2c_error_t err = wait_flag(hi2c->isr, ISR_TC, 0, I2C_TIMEOUT_US);
    if (err != I2C_OK) return err;

    /* Read phase: start + read with auto-end */
    *hi2c->cr2 = ((uint32_t)addr << 1) | ((uint32_t)rx_len << 16) |
                 (1U << 25) /* AUTOEND */ | CR2_RD_WRN | CR2_START;

    for (uint16_t i = 0; i < rx_len; i++) {
        err = wait_flag(hi2c->isr, ISR_RXNE,
                        ISR_NACKF | ISR_BERR | ISR_ARLO,
                        I2C_TIMEOUT_US);
        if (err != I2C_OK) return err;
        rx_data[i] = (uint8_t)(*hi2c->rxdr & 0xFF);
    }

    return wait_flag(hi2c->isr, ISR_STOPF, 0, I2C_TIMEOUT_US);
}

BMP280 Driver (bmp280.h)

#ifndef BMP280_H
#define BMP280_H

#include "i2c.h"
#include <stdint.h>

#define BMP280_ADDR         0x76
#define BMP280_CHIP_ID      0x58

/* Register addresses */
#define BMP280_REG_CHIPID   0xD0
#define BMP280_REG_RESET    0xE0
#define BMP280_REG_STATUS   0xF3
#define BMP280_REG_CTRL_MEAS 0xF4
#define BMP280_REG_CONFIG   0xF5
#define BMP280_REG_PRESS_MSB 0xF7
#define BMP280_REG_TEMP_MSB  0xFA
#define BMP280_REG_CALIB00   0x88

#define BMP280_RESET_VALUE  0xB6

/* Oversampling settings */
#define BMP280_OSRS_SKIPPED 0x00
#define BMP280_OSRS_X1      0x01
#define BMP280_OSRS_X2      0x02
#define BMP280_OSRS_X4      0x03
#define BMP280_OSRS_X8      0x04
#define BMP280_OSRS_X16     0x05

/* Power modes */
#define BMP280_MODE_SLEEP   0x00
#define BMP280_MODE_FORCED  0x01
#define BMP280_MODE_NORMAL  0x03

/* Filter coefficients */
#define BMP280_FILTER_OFF   0x00
#define BMP280_FILTER_2     0x01
#define BMP280_FILTER_4     0x02
#define BMP280_FILTER_8     0x03
#define BMP280_FILTER_16    0x04

/* Standby times (ms) */
#define BMP280_STANDBY_0_5  0x00
#define BMP280_STANDBY_62_5 0x01
#define BMP280_STANDBY_125  0x02

typedef struct {
    uint16_t dig_T1;
    int16_t  dig_T2;
    int16_t  dig_T3;
    uint16_t dig_P1;
    int16_t  dig_P2;
    int16_t  dig_P3;
    int16_t  dig_P4;
    int16_t  dig_P5;
    int16_t  dig_P6;
    int16_t  dig_P7;
    int16_t  dig_P8;
    int16_t  dig_P9;
} bmp280_calib_t;

typedef struct {
    i2c_handle_t *i2c;
    bmp280_calib_t calib;
    int32_t t_fine;
} bmp280_t;

typedef enum {
    BMP280_OK = 0,
    BMP280_ERR_I2C,
    BMP280_ERR_CHIP_ID,
    BMP280_ERR_TIMEOUT,
} bmp280_error_t;

bmp280_error_t bmp280_init(bmp280_t *dev, i2c_handle_t *i2c);
bmp280_error_t bmp280_configure(bmp280_t *dev, uint8_t osrs_t,
                                uint8_t osrs_p, uint8_t mode,
                                uint8_t filter, uint8_t standby);
bmp280_error_t bmp280_read_temperature(bmp280_t *dev, int32_t *temp_c_x100);
bmp280_error_t bmp280_read_pressure(bmp280_t *dev, uint32_t *pressure_pa);
bmp280_error_t bmp280_read_both(bmp280_t *dev, int32_t *temp_c_x100,
                                uint32_t *pressure_pa);

#endif

BMP280 Driver Implementation (bmp280.c)

#include "bmp280.h"

static bmp280_error_t read_regs(bmp280_t *dev, uint8_t reg,
                                uint8_t *data, uint16_t len) {
    i2c_error_t err = i2c_write_read(dev->i2c, BMP280_ADDR,
                                     &reg, 1, data, len);
    return (err == I2C_OK) ? BMP280_OK : BMP280_ERR_I2C;
}

static bmp280_error_t write_reg(bmp280_t *dev, uint8_t reg, uint8_t val) {
    uint8_t buf[2] = { reg, val };
    i2c_error_t err = i2c_write(dev->i2c, BMP280_ADDR, buf, 2);
    return (err == I2C_OK) ? BMP280_OK : BMP280_ERR_I2C;
}

bmp280_error_t bmp280_init(bmp280_t *dev, i2c_handle_t *i2c) {
    dev->i2c = i2c;
    dev->t_fine = 0;

    /* Reset the sensor */
    bmp280_error_t err = write_reg(dev, BMP280_REG_RESET, BMP280_RESET_VALUE);
    if (err != BMP280_OK) return err;

    /* Busy-wait for reset (max 2 ms per datasheet) */
    for (volatile int i = 0; i < 80000; i++);

    /* Verify chip ID */
    uint8_t chip_id = 0;
    err = read_regs(dev, BMP280_REG_CHIPID, &chip_id, 1);
    if (err != BMP280_OK) return err;
    if (chip_id != BMP280_CHIP_ID) return BMP280_ERR_CHIP_ID;

    /* Read calibration data (24 bytes starting at 0x88) */
    uint8_t calib_raw[26];
    err = read_regs(dev, BMP280_REG_CALIB00, calib_raw, 26);
    if (err != BMP280_OK) return err;

    dev->calib.dig_T1 = (uint16_t)(calib_raw[1] << 8) | calib_raw[0];
    dev->calib.dig_T2 = (int16_t)((calib_raw[3] << 8) | calib_raw[2]);
    dev->calib.dig_T3 = (int16_t)((calib_raw[5] << 8) | calib_raw[4]);
    dev->calib.dig_P1 = (uint16_t)(calib_raw[7] << 8) | calib_raw[6];
    dev->calib.dig_P2 = (int16_t)((calib_raw[9] << 8) | calib_raw[8]);
    dev->calib.dig_P3 = (int16_t)((calib_raw[11] << 8) | calib_raw[10]);
    dev->calib.dig_P4 = (int16_t)((calib_raw[13] << 8) | calib_raw[12]);
    dev->calib.dig_P5 = (int16_t)((calib_raw[15] << 8) | calib_raw[14]);
    dev->calib.dig_P6 = (int16_t)((calib_raw[17] << 8) | calib_raw[16]);
    dev->calib.dig_P7 = (int16_t)((calib_raw[19] << 8) | calib_raw[18]);
    dev->calib.dig_P8 = (int16_t)((calib_raw[21] << 8) | calib_raw[20]);
    dev->calib.dig_P9 = (int16_t)((calib_raw[23] << 8) | calib_raw[22]);

    return BMP280_OK;
}

bmp280_error_t bmp280_configure(bmp280_t *dev, uint8_t osrs_t,
                                uint8_t osrs_p, uint8_t mode,
                                uint8_t filter, uint8_t standby) {
    /* ctrl_meas: osrs_t[7:5] | osrs_p[4:2] | mode[1:0] */
    uint8_t ctrl = (osrs_t << 5) | (osrs_p << 2) | (mode & 0x03);
    bmp280_error_t err = write_reg(dev, BMP280_REG_CTRL_MEAS, ctrl);
    if (err != BMP280_OK) return err;

    /* config: t_sb[7:5] | filter[4:2] | spi3w_en[0] */
    uint8_t config = (standby << 5) | (filter << 2);
    return write_reg(dev, BMP280_REG_CONFIG, config);
}

static int32_t compensate_temperature(bmp280_t *dev, int32_t adc_temp) {
    const bmp280_calib_t *c = &dev->calib;

    /* Fixed-point compensation (Bosch reference algorithm) */
    int32_t var1 = ((((adc_temp >> 3) - ((int32_t)c->dig_T1 << 1))) *
                    ((int32_t)c->dig_T2)) >> 11;

    int32_t var2 = (((((adc_temp >> 4) - ((int32_t)c->dig_T1)) *
                      ((adc_temp >> 4) - ((int32_t)c->dig_T1))) >> 12) *
                    ((int32_t)c->dig_T3)) >> 14;

    dev->t_fine = var1 + var2;

    /* Temperature in 0.01°C units */
    return (dev->t_fine * 5 + 128) >> 8;
}

static uint32_t compensate_pressure(bmp280_t *dev, int32_t adc_press) {
    const bmp280_calib_t *c = &dev->calib;
    int64_t var1, var2, p;

    var1 = ((int64_t)dev->t_fine) - 128000;
    var2 = var1 * var1 * (int64_t)c->dig_P6;
    var2 = var2 + ((var1 * (int64_t)c->dig_P5) << 17);
    var2 = var2 + (((int64_t)c->dig_P4) << 35);
    var1 = ((var1 * var1 * (int64_t)c->dig_P3) >> 8) +
           ((var1 * (int64_t)c->dig_P2) << 12);
    var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)c->dig_P1) >> 33;

    if (var1 == 0) return 0;  /* Avoid division by zero */

    p = 1048576 - adc_press;
    p = (((p << 31) - var2) * 3125) / var1;
    var1 = (((int64_t)c->dig_P9) * (p >> 13) * (p >> 13)) >> 25;
    var2 = (((int64_t)c->dig_P8) * p) >> 19;

    p = ((p + var1 + var2) >> 8) + (((int64_t)c->dig_P7) << 4);

    return (uint32_t)(p >> 8);  /* Pressure in Pa */
}

bmp280_error_t bmp280_read_temperature(bmp280_t *dev, int32_t *temp_c_x100) {
    uint8_t raw[3];
    bmp280_error_t err = read_regs(dev, BMP280_REG_TEMP_MSB, raw, 3);
    if (err != BMP280_OK) return err;

    int32_t adc = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) |
                  ((int32_t)raw[2] >> 4);

    *temp_c_x100 = compensate_temperature(dev, adc);
    return BMP280_OK;
}

bmp280_error_t bmp280_read_pressure(bmp280_t *dev, uint32_t *pressure_pa) {
    uint8_t raw[3];
    bmp280_error_t err = read_regs(dev, BMP280_REG_PRESS_MSB, raw, 3);
    if (err != BMP280_OK) return err;

    int32_t adc = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) |
                  ((int32_t)raw[2] >> 4);

    /* Must read temperature first to populate t_fine */
    uint8_t temp_raw[3];
    err = read_regs(dev, BMP280_REG_TEMP_MSB, temp_raw, 3);
    if (err != BMP280_OK) return err;

    int32_t adc_temp = ((int32_t)temp_raw[0] << 12) |
                       ((int32_t)temp_raw[1] << 4) |
                       ((int32_t)temp_raw[2] >> 4);
    compensate_temperature(dev, adc_temp);

    *pressure_pa = compensate_pressure(dev, adc);
    return BMP280_OK;
}

bmp280_error_t bmp280_read_both(bmp280_t *dev, int32_t *temp_c_x100,
                                uint32_t *pressure_pa) {
    uint8_t raw[6];
    bmp280_error_t err = read_regs(dev, BMP280_REG_PRESS_MSB, raw, 6);
    if (err != BMP280_OK) return err;

    /* Pressure: bytes 0-2, Temperature: bytes 3-5 */
    int32_t adc_p = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) |
                    ((int32_t)raw[2] >> 4);
    int32_t adc_t = ((int32_t)raw[3] << 12) | ((int32_t)raw[4] << 4) |
                    ((int32_t)raw[5] >> 4);

    *temp_c_x100 = compensate_temperature(dev, adc_t);
    *pressure_pa = compensate_pressure(dev, adc_p);
    return BMP280_OK;
}

Main Application (main.c)

#include "bmp280.h"
#include <stdio.h>

static i2c_handle_t hi2c;
static bmp280_t bmp280;

int main(void) {
    /* Initialize I2C at 100 kHz */
    i2c_init(&hi2c, 100000);

    /* Initialize BMP280 */
    bmp280_error_t err = bmp280_init(&bmp280, &hi2c);
    if (err != BMP280_OK) {
        printf("BMP280 init failed: %d\n", err);
        return 1;
    }

    /* Configure: temp x1, pressure x16, normal mode, filter 16, 0.5ms standby */
    err = bmp280_configure(&bmp280, BMP280_OSRS_X1, BMP280_OSRS_X16,
                           BMP280_MODE_NORMAL, BMP280_FILTER_16,
                           BMP280_STANDBY_0_5);
    if (err != BMP280_OK) {
        printf("BMP280 configure failed: %d\n", err);
        return 1;
    }

    /* Continuous reading loop */
    while (1) {
        int32_t temp_x100;
        uint32_t pressure_pa;

        err = bmp280_read_both(&bmp280, &temp_x100, &pressure_pa);
        if (err == BMP280_OK) {
            int32_t temp_int = temp_x100 / 100;
            int32_t temp_frac = temp_x100 % 100;
            uint32_t pressure_hpa = pressure_pa / 100;
            uint32_t pressure_frac = pressure_pa % 100;

            printf("Temp: %ld.%02ld C  Pressure: %lu.%02lu hPa\n",
                   temp_int, temp_frac < 0 ? -temp_frac : temp_frac,
                   pressure_hpa, pressure_frac);
        } else {
            printf("Read error: %d\n", err);
        }

        /* Wait ~1 second between reads */
        for (volatile int i = 0; i < 400000; i++);
    }

    return 0;
}

Rust: Driver Using embedded-hal I2C Trait

// Cargo.toml
// [package]
// name = "bmp280-driver"
// version = "0.1.0"
// edition = "2021"
//
// [dependencies]
// embedded-hal = "1.0"
// nb = "1.1"

use embedded_hal::i2c::{I2c, Error, ErrorKind, ErrorType};

/// BMP280 register addresses
mod regs {
    pub const CHIP_ID: u8 = 0xD0;
    pub const RESET: u8 = 0xE0;
    pub const STATUS: u8 = 0xF3;
    pub const CTRL_MEAS: u8 = 0xF4;
    pub const CONFIG: u8 = 0xF5;
    pub const PRESS_MSB: u8 = 0xF7;
    pub const TEMP_MSB: u8 = 0xFA;
    pub const CALIB00: u8 = 0x88;
}

const BMP280_CHIP_ID: u8 = 0x58;
const BMP280_ADDR: u8 = 0x76;
const BMP280_RESET_VAL: u8 = 0xB6;

/// BMP280 driver error types
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Bmp280Error<I2cErr> {
    I2cError(I2cErr),
    ChipIdMismatch { expected: u8, found: u8 },
    Busy,
}

/// Calibration coefficients read from device
#[derive(Debug, Clone, Copy)]
pub struct Calibration {
    dig_t1: u16,
    dig_t2: i16,
    dig_t3: i16,
    dig_p1: u16,
    dig_p2: i16,
    dig_p3: i16,
    dig_p4: i16,
    dig_p5: i16,
    dig_p6: i16,
    dig_p7: i16,
    dig_p8: i16,
    dig_p9: i16,
}

/// Oversampling settings
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum Oversampling {
    Skipped = 0,
    X1 = 1,
    X2 = 2,
    X4 = 3,
    X8 = 4,
    X16 = 5,
}

/// Power mode
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum Mode {
    Sleep = 0,
    Forced = 1,
    Normal = 3,
}

/// IIR filter coefficient
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum Filter {
    Off = 0,
    Coeff2 = 1,
    Coeff4 = 2,
    Coeff8 = 3,
    Coeff16 = 4,
}

/// Standby time in normal mode
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum StandbyTime {
    Ms0_5 = 0,
    Ms62_5 = 1,
    Ms125 = 2,
    Ms250 = 3,
    Ms500 = 4,
    Ms1000 = 5,
}

/// BMP280 sensor reading
#[derive(Debug, Clone, Copy)]
pub struct SensorData {
    pub temperature_c: f32,
    pub pressure_pa: f32,
}

/// BMP280 driver with generic I2C backend
pub struct Bmp280<I2C> {
    i2c: I2C,
    calib: Calibration,
    t_fine: i32,
}

impl<I2C, I2cErr> Bmp280<I2C>
where
    I2C: I2c<Error = I2cErr>,
    I2cErr: Error,
{
    /// Create a new BMP280 driver and initialize the sensor
    pub fn new(mut i2c: I2C) -> Result<Self, Bmp280Error<I2cErr>> {
        let addr = BMP280_ADDR;

        // Reset sensor
        i2c.write(addr, &[regs::RESET, BMP280_RESET_VAL])
            .map_err(Bmp280Error::I2cError)?;

        // Short delay for reset (caller should provide delay impl in real code)
        cortex_m::asm::delay(80_000);

        // Verify chip ID
        let mut chip_id = [0u8];
        i2c.write_read(addr, &[regs::CHIP_ID], &mut chip_id)
            .map_err(Bmp280Error::I2cError)?;

        if chip_id[0] != BMP280_CHIP_ID {
            return Err(Bmp280Error::ChipIdMismatch {
                expected: BMP280_CHIP_ID,
                found: chip_id[0],
            });
        }

        // Read calibration data
        let mut calib_raw = [0u8; 26];
        i2c.write_read(addr, &[regs::CALIB00], &mut calib_raw)
            .map_err(Bmp280Error::I2cError)?;

        let calib = Calibration {
            dig_t1: u16::from_le_bytes([calib_raw[0], calib_raw[1]]),
            dig_t2: i16::from_le_bytes([calib_raw[2], calib_raw[3]]),
            dig_t3: i16::from_le_bytes([calib_raw[4], calib_raw[5]]),
            dig_p1: u16::from_le_bytes([calib_raw[6], calib_raw[7]]),
            dig_p2: i16::from_le_bytes([calib_raw[8], calib_raw[9]]),
            dig_p3: i16::from_le_bytes([calib_raw[10], calib_raw[11]]),
            dig_p4: i16::from_le_bytes([calib_raw[12], calib_raw[13]]),
            dig_p5: i16::from_le_bytes([calib_raw[14], calib_raw[15]]),
            dig_p6: i16::from_le_bytes([calib_raw[16], calib_raw[17]]),
            dig_p7: i16::from_le_bytes([calib_raw[18], calib_raw[19]]),
            dig_p8: i16::from_le_bytes([calib_raw[20], calib_raw[21]]),
            dig_p9: i16::from_le_bytes([calib_raw[22], calib_raw[23]]),
        };

        Ok(Bmp280 {
            i2c,
            calib,
            t_fine: 0,
        })
    }

    /// Configure sensor: oversampling, mode, filter, standby
    pub fn configure(
        &mut self,
        osrs_t: Oversampling,
        osrs_p: Oversampling,
        mode: Mode,
        filter: Filter,
        standby: StandbyTime,
    ) -> Result<(), Bmp280Error<I2cErr>> {
        let ctrl_meas = ((osrs_t as u8) << 5) | ((osrs_p as u8) << 2) | (mode as u8);
        self.i2c
            .write(BMP280_ADDR, &[regs::CTRL_MEAS, ctrl_meas])
            .map_err(Bmp280Error::I2cError)?;

        let config = ((standby as u8) << 5) | ((filter as u8) << 2);
        self.i2c
            .write(BMP280_ADDR, &[regs::CONFIG, config])
            .map_err(Bmp280Error::I2cError)?;

        Ok(())
    }

    /// Read temperature in centidegrees (0.01°C units)
    fn read_temperature_raw(&mut self) -> Result<i32, Bmp280Error<I2cErr>> {
        let mut raw = [0u8; 3];
        self.i2c
            .write_read(BMP280_ADDR, &[regs::TEMP_MSB], &mut raw)
            .map_err(Bmp280Error::I2cError)?;

        let adc = ((raw[0] as i32) << 12) | ((raw[1] as i32) << 4) | ((raw[2] as i32) >> 4);
        Ok(self.compensate_temperature(adc))
    }

    /// Read pressure in pascals
    fn read_pressure_raw(&mut self) -> Result<u32, Bmp280Error<I2cErr>> {
        let mut raw = [0u8; 3];
        self.i2c
            .write_read(BMP280_ADDR, &[regs::PRESS_MSB], &mut raw)
            .map_err(Bmp280Error::I2cError)?;

        let adc = ((raw[0] as i32) << 12) | ((raw[1] as i32) << 4) | ((raw[2] as i32) >> 4);
        Ok(self.compensate_pressure(adc))
    }

    /// Read both temperature and pressure
    pub fn read(&mut self) -> Result<SensorData, Bmp280Error<I2cErr>> {
        // Read all 6 bytes in one transaction (pressure then temperature)
        let mut raw = [0u8; 6];
        self.i2c
            .write_read(BMP280_ADDR, &[regs::PRESS_MSB], &mut raw)
            .map_err(Bmp280Error::I2cError)?;

        let adc_p = ((raw[0] as i32) << 12) | ((raw[1] as i32) << 4) | ((raw[2] as i32) >> 4);
        let adc_t = ((raw[3] as i32) << 12) | ((raw[4] as i32) << 4) | ((raw[5] as i32) >> 4);

        let temp_x100 = self.compensate_temperature(adc_t);
        let pressure = self.compensate_pressure(adc_p);

        Ok(SensorData {
            temperature_c: temp_x100 as f32 / 100.0,
            pressure_pa: pressure as f32 / 256.0,
        })
    }

    fn compensate_temperature(&mut self, adc: i32) -> i32 {
        let c = &self.calib;

        let var1 = ((((adc >> 3) - ((c.dig_t1 as i32) << 1))) * (c.dig_t2 as i32)) >> 11;

        let var2 = (((((adc >> 4) - (c.dig_t1 as i32))
            * ((adc >> 4) - (c.dig_t1 as i32)))
            >> 12)
            * (c.dig_t3 as i32))
            >> 14;

        self.t_fine = var1 + var2;
        (self.t_fine * 5 + 128) >> 8
    }

    fn compensate_pressure(&self, adc: i32) -> u32 {
        let c = &self.calib;

        let var1: i64 = (self.t_fine as i64) - 128000;
        let var2: i64 = var1 * var1 * (c.dig_p6 as i64);
        let var2 = var2 + ((var1 * (c.dig_p5 as i64)) << 17);
        let var2 = var2 + (((c.dig_p4 as i64) as i64) << 35);
        let var1 = ((var1 * var1 * (c.dig_p3 as i64)) >> 8)
            + ((var1 * (c.dig_p2 as i64)) << 12);
        let var1 = (((((1i64) << 47) + var1)) * (c.dig_p1 as i64)) >> 33;

        if var1 == 0 {
            return 0;
        }

        let mut p: i64 = 1048576 - (adc as i64);
        p = (((p << 31) - var2) * 3125) / var1;
        let var1 = ((c.dig_p9 as i64) * (p >> 13) * (p >> 13)) >> 25;
        let var2 = ((c.dig_p8 as i64) * p) >> 19;
        p = ((p + var1 + var2) >> 8) + ((c.dig_p7 as i64) << 4);

        (p >> 8) as u32
    }
}

// --- Example usage with stm32f4xx-hal (STM32F405) ---
//
// #[entry]
// fn main() -> ! {
//     let dp = stm32::Peripherals::take().unwrap();
//     let cp = cortex_m::Peripherals::take().unwrap();
//
//     let rcc = dp.RCC.constrain();
//     let clocks = rcc.cfgr.sysclk(48.MHz()).freeze();
//
//     let gpiob = dp.GPIOB.split();
//     let scl = gpiob.pb6.into_alternate::<4>();
//     let sda = gpiob.pb7.into_alternate::<4>();
//
//     let i2c = I2c::new(dp.I2C2, (scl, sda), 100.kHz(), clocks);
//
//     let mut bmp280 = Bmp280::new(i2c).expect("BMP280 init failed");
//     bmp280
//         .configure(
//             Oversampling::X1,
//             Oversampling::X16,
//             Mode::Normal,
//             Filter::Coeff16,
//             StandbyTime::Ms0_5,
//         )
//         .expect("BMP280 configure failed");
//
//     loop {
//         match bmp280.read() {
//             Ok(data) => {
//                 defmt::println!(
//                     "Temp: {:.2} C  Pressure: {:.2} hPa",
//                     data.temperature_c,
//                     data.pressure_pa / 100.0
//                 );
//             }
//             Err(e) => defmt::println!("Error: {:?}", e),
//         }
//         cortex_m::asm::delay(16_000_000); // ~1 second
//     }
// }

Ada: Generic Sensor Driver Package

-- bmp280.ads
with I2C_Driver; use I2C_Driver;

package BMP280 is

   -- I2C address (SDO grounded)
   BMP280_Address : constant I2C_Address := 16#76#;
   Chip_ID_Expected : constant UInt8 := 16#58#;

   -- Register addresses
   Reg_ChipID   : constant UInt8 := 16#D0#;
   Reg_Reset    : constant UInt8 := 16#E0#;
   Reg_Status   : constant UInt8 := 16#F3#;
   Reg_CtrlMeas : constant UInt8 := 16#F4#;
   Reg_Config   : constant UInt8 := 16#F5#;
   Reg_PressMSB : constant UInt8 := 16#F7#;
   Reg_TempMSB  : constant UInt8 := 16#FA#;
   Reg_Calib00  : constant UInt8 := 16#88#;

   Reset_Value : constant UInt8 := 16#B6#;

   -- Oversampling settings
   type Oversampling is (Skipped, X1, X2, X4, X8, X16);
   for Oversampling use (Skipped => 0, X1 => 1, X2 => 2,
                         X4 => 3, X8 => 4, X16 => 5);

   -- Power modes
   type Power_Mode is (Sleep, Forced, Normal);
   for Power_Mode use (Sleep => 0, Forced => 1, Normal => 3);

   -- Filter coefficients
   type Filter_Coeff is (Off, Coeff2, Coeff4, Coeff8, Coeff16);
   for Filter_Coeff use (Off => 0, Coeff2 => 1, Coeff4 => 2,
                         Coeff8 => 3, Coeff16 => 4);

   -- Standby times
   type Standby_Time is (Ms0_5, Ms62_5, Ms125);
   for Standby_Time use (Ms0_5 => 0, Ms62_5 => 1, Ms125 => 2);

   -- Calibration data with strong typing
   type Calibration_Data is record
      Dig_T1 : UInt16;
      Dig_T2 : Int16;
      Dig_T3 : Int16;
      Dig_P1 : UInt16;
      Dig_P2 : Int16;
      Dig_P3 : Int16;
      Dig_P4 : Int16;
      Dig_P5 : Int16;
      Dig_P6 : Int16;
      Dig_P7 : Int16;
      Dig_P8 : Int16;
      Dig_P9 : Int16;
   end record;

   -- Sensor reading result
   type Sensor_Reading is record
      Temperature_Centidegrees : Int32;  -- in 0.01°C units
      Pressure_Pa              : UInt32; -- in pascals
   end record;

   -- Error types
   type BMP280_Error is (OK, I2C_Error, Chip_ID_Mismatch, Timeout);

   -- Device handle
   type BMP280_Device is private;

   -- Initialize device and read calibration
   function Initialize
     (Port : access I2C_Port'Class)
      return BMP280_Device;

   -- Get initialization status
   function Is_Valid (Dev : BMP280_Device) return Boolean;
   function Get_Error (Dev : BMP280_Device) return BMP280_Error;

   -- Configure sensor
   procedure Configure
     (Dev      : in out BMP280_Device;
      OSRS_T   : Oversampling;
      OSRS_P   : Oversampling;
      Mode     : Power_Mode;
      Filter   : Filter_Coeff;
      Standby  : Standby_Time);

   -- Read sensor data
   function Read_Sensor
     (Dev : in out BMP280_Device)
      return Sensor_Reading;

private

   type BMP280_Device is record
      Port      : access I2C_Port'Class := null;
      Calib     : Calibration_Data;
      T_Fine    : Int32 := 0;
      Valid     : Boolean := False;
      Last_Err  : BMP280_Error := OK;
   end record;

end BMP280;
-- bmp280.adb
package body BMP280 is

   procedure Write_Reg
     (Dev  : in out BMP280_Device;
      Reg  : UInt8;
      Data : UInt8)
   is
      Buffer : UInt8_Array (1 .. 2) := (Reg, Data);
      Status : I2C_Status;
   begin
      if Dev.Port = null then
         Dev.Last_Err := I2C_Error;
         return;
      end if;
      I2C_Write (Dev.Port.all, BMP280_Address, Buffer, Status);
      if Status /= I2C_OK then
         Dev.Last_Err := I2C_Error;
      end if;
   end Write_Reg;

   procedure Read_Regs
     (Dev   : in out BMP280_Device;
      Reg   : UInt8;
      Data  : out UInt8_Array;
      Count : UInt16)
   is
      Status : I2C_Status;
   begin
      if Dev.Port = null then
         Dev.Last_Err := I2C_Error;
         return;
      end if;
      I2C_Write_Read (Dev.Port.all, BMP280_Address,
                      (1 => Reg), 1, Data, Count, Status);
      if Status /= I2C_OK then
         Dev.Last_Err := I2C_Error;
      end if;
   end Read_Regs;

   function Initialize
     (Port : access I2C_Port'Class)
      return BMP280_Device
   is
      Dev : BMP280_Device;
      Chip_ID : UInt8 := 0;
      Calib_Raw : UInt8_Array (1 .. 26);
   begin
      Dev.Port := Port;
      Dev.T_Fine := 0;
      Dev.Valid := False;
      Dev.Last_Err := OK;

      -- Reset sensor
      Write_Reg (Dev, Reg_Reset, Reset_Value);

      -- Delay for reset (implementation-dependent)
      delay 0.002;

      -- Verify chip ID
      Read_Regs (Dev, Reg_ChipID, (1 => Chip_ID), 1);
      if Dev.Last_Err /= OK then
         return Dev;
      end if;

      if Chip_ID /= Chip_ID_Expected then
         Dev.Last_Err := Chip_ID_Mismatch;
         return Dev;
      end if;

      -- Read calibration data
      Read_Regs (Dev, Reg_Calib00, Calib_Raw, 26);
      if Dev.Last_Err /= OK then
         return Dev;
      end if;

      -- Parse calibration with explicit endianness
      Dev.Calib.Dig_T1 := UInt16 (Calib_Raw (1)) or
                          (UInt16 (Calib_Raw (2)) * 256);
      Dev.Calib.Dig_T2 := Int16 (UInt16 (Calib_Raw (3)) or
                          (UInt16 (Calib_Raw (4)) * 256));
      Dev.Calib.Dig_T3 := Int16 (UInt16 (Calib_Raw (5)) or
                          (UInt16 (Calib_Raw (6)) * 256));
      Dev.Calib.Dig_P1 := UInt16 (Calib_Raw (7)) or
                          (UInt16 (Calib_Raw (8)) * 256);
      Dev.Calib.Dig_P2 := Int16 (UInt16 (Calib_Raw (9)) or
                          (UInt16 (Calib_Raw (10)) * 256));
      Dev.Calib.Dig_P3 := Int16 (UInt16 (Calib_Raw (11)) or
                          (UInt16 (Calib_Raw (12)) * 256));
      Dev.Calib.Dig_P4 := Int16 (UInt16 (Calib_Raw (13)) or
                          (UInt16 (Calib_Raw (14)) * 256));
      Dev.Calib.Dig_P5 := Int16 (UInt16 (Calib_Raw (15)) or
                          (UInt16 (Calib_Raw (16)) * 256));
      Dev.Calib.Dig_P6 := Int16 (UInt16 (Calib_Raw (17)) or
                          (UInt16 (Calib_Raw (18)) * 256));
      Dev.Calib.Dig_P7 := Int16 (UInt16 (Calib_Raw (19)) or
                          (UInt16 (Calib_Raw (20)) * 256));
      Dev.Calib.Dig_P8 := Int16 (UInt16 (Calib_Raw (21)) or
                          (UInt16 (Calib_Raw (22)) * 256));
      Dev.Calib.Dig_P9 := Int16 (UInt16 (Calib_Raw (23)) or
                          (UInt16 (Calib_Raw (24)) * 256));

      Dev.Valid := True;
      return Dev;
   end Initialize;

   function Is_Valid (Dev : BMP280_Device) return Boolean is
   begin
      return Dev.Valid;
   end Is_Valid;

   function Get_Error (Dev : BMP280_Device) return BMP280_Error is
   begin
      return Dev.Last_Err;
   end Get_Error;

   procedure Configure
     (Dev      : in out BMP280_Device;
      OSRS_T   : Oversampling;
      OSRS_P   : Oversampling;
      Mode     : Power_Mode;
      Filter   : Filter_Coeff;
      Standby  : Standby_Time)
   is
      Ctrl : UInt8;
      Config : UInt8;
   begin
      -- ctrl_meas: osrs_t[7:5] | osrs_p[4:2] | mode[1:0]
      Ctrl := (UInt8 (OSRS_T) * 32) or
              (UInt8 (OSRS_P) * 4) or
              UInt8 (Mode);
      Write_Reg (Dev, Reg_CtrlMeas, Ctrl);

      -- config: t_sb[7:5] | filter[4:2] | spi3w_en[0]
      Config := (UInt8 (Standby) * 32) or
                (UInt8 (Filter) * 4);
      Write_Reg (Dev, Reg_Config, Config);
   end Configure;

   function Compensate_Temperature
     (Dev : in out BMP280_Device;
      ADC_Temp : Int32)
      return Int32
   is
      Var1 : Int32;
      Var2 : Int32;
   begin
      Var1 := ((((ADC_Temp / 8) - (Int32 (Dev.Calib.Dig_T1) * 2)) *
                Int32 (Dev.Calib.Dig_T2)) / 2048);

      Var2 := (((((ADC_Temp / 16) - Int32 (Dev.Calib.Dig_T1)) *
                 ((ADC_Temp / 16) - Int32 (Dev.Calib.Dig_T1))) / 4096) *
                Int32 (Dev.Calib.Dig_T3)) / 16384;

      Dev.T_Fine := Var1 + Var2;

      return (Dev.T_Fine * 5 + 128) / 256;
   end Compensate_Temperature;

   function Compensate_Pressure
     (Dev : BMP280_Device;
      ADC_Press : Int32)
      return UInt32
   is
      Var1 : Int64;
      Var2 : Int64;
      P    : Int64;
   begin
      Var1 := Int64 (Dev.T_Fine) - 128_000;
      Var2 := Var1 * Var1 * Int64 (Dev.Calib.Dig_P6);
      Var2 := Var2 + ((Var1 * Int64 (Dev.Calib.Dig_P5)) * 131072);
      Var2 := Var2 + (Int64 (Dev.Calib.Dig_P4) * 34359738368);
      Var1 := ((Var1 * Var1 * Int64 (Dev.Calib.Dig_P3)) / 256) +
              ((Var1 * Int64 (Dev.Calib.Dig_P2)) * 4096);
      Var1 := (((Int64 (1) * 140737488355328) + Var1) *
               Int64 (Dev.Calib.Dig_P1)) / 8589934592;

      if Var1 = 0 then
         return 0;
      end if;

      P := 1_048_576 - Int64 (ADC_Press);
      P := (((P * 2147483648) - Var2) * 3125) / Var1;
      Var1 := (Int64 (Dev.Calib.Dig_P9) * (P / 8192) * (P / 8192)) / 33554432;
      Var2 := (Int64 (Dev.Calib.Dig_P8) * P) / 524288;
      P := ((P + Var1 + Var2) / 256) + (Int64 (Dev.Calib.Dig_P7) * 16);

      return UInt32 (P / 256);
   end Compensate_Pressure;

   function Read_Sensor
     (Dev : in out BMP280_Device)
      return Sensor_Reading
   is
      Raw : UInt8_Array (1 .. 6);
      ADC_Press : Int32;
      ADC_Temp  : Int32;
      Result    : Sensor_Reading;
   begin
      Result.Temperature_Centidegrees := 0;
      Result.Pressure_Pa := 0;

      if not Dev.Valid then
         Dev.Last_Err := I2C_Error;
         return Result;
      end if;

      Read_Regs (Dev, Reg_PressMSB, Raw, 6);
      if Dev.Last_Err /= OK then
         return Result;
      end if;

      ADC_Press := (Int32 (Raw (1)) * 4096) +
                   (Int32 (Raw (2)) * 16) +
                   (Int32 (Raw (3)) / 16);
      ADC_Temp  := (Int32 (Raw (4)) * 4096) +
                   (Int32 (Raw (5)) * 16) +
                   (Int32 (Raw (6)) / 16);

      Result.Temperature_Centidegrees :=
        Compensate_Temperature (Dev, ADC_Temp);
      Result.Pressure_Pa :=
        Compensate_Pressure (Dev, ADC_Press);

      return Result;
   end Read_Sensor;

end BMP280;
-- i2c_driver.ads (minimal interface)
with System;

package I2C_Driver is

   type UInt8 is mod 2**8;
   type UInt16 is mod 2**16;
   type UInt32 is mod 2**32;
   type Int16 is range -2**15 .. 2**15 - 1;
   type Int32 is range -2**31 .. 2**31 - 1;
   type Int64 is range -2**63 .. 2**63 - 1;

   type I2C_Address is range 0 .. 127;

   type UInt8_Array is array (Positive range <>) of UInt8;

   type I2C_Status is (I2C_OK, I2C_Busy, I2C_NACK, I2C_Timeout);

   type I2C_Port is limited private;

   procedure I2C_Init
     (Port : out I2C_Port;
      Speed_Hz : UInt32);

   procedure I2C_Write
     (Port   : in out I2C_Port;
      Addr   : I2C_Address;
      Data   : UInt8_Array;
      Status : out I2C_Status);

   procedure I2C_Write_Read
     (Port     : in out I2C_Port;
      Addr     : I2C_Address;
      Tx_Data  : UInt8_Array;
      Tx_Len   : UInt16;
      Rx_Data  : out UInt8_Array;
      Rx_Len   : UInt16;
      Status   : out I2C_Status);

private

   type I2C_Port is record
      Initialized : Boolean := False;
   end record;

end I2C_Driver;
-- main.adb
with BMP280; use BMP280;
with I2C_Driver; use I2C_Driver;
with Text_IO; use Text_IO;

procedure Main is
   Port : I2C_Port;
   Dev  : BMP280_Device;
   Reading : Sensor_Reading;
begin
   -- Initialize I2C at 100 kHz
   I2C_Init (Port, 100_000);

   -- Initialize BMP280
   Dev := Initialize (Port'Access);
   if not Is_Valid (Dev) then
      Put_Line ("BMP280 initialization failed: " &
                BMP280_Error'Image (Get_Error (Dev)));
      return;
   end if;

   -- Configure: temp x1, pressure x16, normal mode, filter 16
   Configure (Dev, X1, X16, Normal, Coeff16, Ms0_5);

   -- Continuous reading loop
   loop
      Reading := Read_Sensor (Dev);

      if Get_Error (Dev) = OK then
         Put ("Temp: ");
         Put (Integer (Reading.Temperature_Centidegrees / 100));
         Put (".");
         declare
            Frac : Integer :=
              Integer (Reading.Temperature_Centidegrees rem 100);
         begin
            if Frac < 0 then
               Frac := -Frac;
            end if;
            if Frac < 10 then
               Put ("0");
            end if;
            Put (Frac);
         end;
         Put (" C  Pressure: ");
         Put (Integer (Reading.Pressure_Pa / 100));
         Put (".");
         Put (Integer (Reading.Pressure_Pa rem 100));
         Put_Line (" hPa");
      else
         Put_Line ("Read error: " &
                   BMP280_Error'Image (Get_Error (Dev)));
      end if;

      delay 1.0;
   end loop;
end Main;

Zig: Comptime-Validated Register Map with Error Unions

// bmp280.zig
const std = @import("std");

/// I2C error types
pub const I2cError = error{
    Busy,
    NackAddr,
    NackData,
    Timeout,
    Arbitration,
};

/// I2C interface — platform implementations provide this
pub const I2cInterface = struct {
    ctx: *anyopaque,
    write: *const fn (ctx: *anyopaque, addr: u8, data: []const u8) I2cError!void,
    read: *const fn (ctx: *anyopaque, addr: u8, data: []u8) I2cError!void,
    writeRead: *const fn (ctx: *anyopaque, addr: u8, tx: []const u8, rx: []u8) I2cError!void,
};

/// BMP280 register map — validated at comptime
pub const Register = enum(u8) {
    chip_id = 0xD0,
    reset = 0xE0,
    status = 0xF3,
    ctrl_meas = 0xF4,
    config = 0xF5,
    press_msb = 0xF7,
    temp_msb = 0xFA,
    calib00 = 0x88,

    /// Verify register is in the valid BMP280 range
    pub fn isValid(reg: Register) bool {
        const v = @intFromEnum(reg);
        return (v >= 0x88 and v <= 0xA1) or  // calibration
               (v >= 0xD0 and v <= 0xF7);     // control/data
    }
};

/// Comptime assertion: all registers are valid
comptime {
    inline for (std.meta.tags(Register)) |reg| {
        std.debug.assert(Register.isValid(reg));
    }
}

const bmp280_addr: u8 = 0x76;
const chip_id_expected: u8 = 0x58;
const reset_value: u8 = 0xB6;

/// Calibration coefficients
pub const Calibration = struct {
    dig_t1: u16,
    dig_t2: i16,
    dig_t3: i16,
    dig_p1: u16,
    dig_p2: i16,
    dig_p3: i16,
    dig_p4: i16,
    dig_p5: i16,
    dig_p6: i16,
    dig_p7: i16,
    dig_p8: i16,
    dig_p9: i16,
};

/// Sensor reading
pub const SensorData = struct {
    temperature_c: f32,
    pressure_pa: f32,
};

/// Oversampling settings
pub const Oversampling = enum(u3) {
    skipped = 0,
    x1 = 1,
    x2 = 2,
    x4 = 3,
    x8 = 4,
    x16 = 5,
};

/// Power mode
pub const Mode = enum(u2) {
    sleep = 0,
    forced = 1,
    normal = 3,
};

/// Filter coefficient
pub const Filter = enum(u3) {
    off = 0,
    coeff2 = 1,
    coeff4 = 2,
    coeff8 = 3,
    coeff16 = 4,
};

/// Standby time
pub const StandbyTime = enum(u3) {
    ms0_5 = 0,
    ms62_5 = 1,
    ms125 = 2,
};

/// BMP280 error union
pub const Bmp280Error = I2cError || error{
    ChipIdMismatch,
    InvalidConfig,
};

/// BMP280 driver
pub const Bmp280 = struct {
    i2c: I2cInterface,
    calib: Calibration,
    t_fine: i32,

    pub fn init(i2c: I2cInterface) Bmp280Error!Bmp280 {
        var dev = Bmp280{
            .i2c = i2c,
            .calib = undefined,
            .t_fine = 0,
        };

        // Reset sensor
        try dev.writeReg(.reset, reset_value);

        // Delay for reset (caller should provide actual delay)
        var i: usize = 0;
        while (i < 80000) : (i += 1) {
            asm volatile ("nop");
        }

        // Verify chip ID
        const chip_id = try dev.readReg(.chip_id);
        if (chip_id != chip_id_expected) {
            return Bmp280Error.ChipIdMismatch;
        }

        // Read calibration data
        var calib_raw: [26]u8 = undefined;
        try dev.readRegs(.calib00, &calib_raw);

        dev.calib = Calibration{
            .dig_t1 = @as(u16, calib_raw[0]) | (@as(u16, calib_raw[1]) << 8),
            .dig_t2 = @bitCast(@as(u16, calib_raw[2]) | (@as(u16, calib_raw[3]) << 8)),
            .dig_t3 = @bitCast(@as(u16, calib_raw[4]) | (@as(u16, calib_raw[5]) << 8)),
            .dig_p1 = @as(u16, calib_raw[6]) | (@as(u16, calib_raw[7]) << 8),
            .dig_p2 = @bitCast(@as(u16, calib_raw[8]) | (@as(u16, calib_raw[9]) << 8)),
            .dig_p3 = @bitCast(@as(u16, calib_raw[10]) | (@as(u16, calib_raw[11]) << 8)),
            .dig_p4 = @bitCast(@as(u16, calib_raw[12]) | (@as(u16, calib_raw[13]) << 8)),
            .dig_p5 = @bitCast(@as(u16, calib_raw[14]) | (@as(u16, calib_raw[15]) << 8)),
            .dig_p6 = @bitCast(@as(u16, calib_raw[16]) | (@as(u16, calib_raw[17]) << 8)),
            .dig_p7 = @bitCast(@as(u16, calib_raw[18]) | (@as(u16, calib_raw[19]) << 8)),
            .dig_p8 = @bitCast(@as(u16, calib_raw[20]) | (@as(u16, calib_raw[21]) << 8)),
            .dig_p9 = @bitCast(@as(u16, calib_raw[22]) | (@as(u16, calib_raw[23]) << 8)),
        };

        return dev;
    }

    pub fn configure(
        self: *Bmp280,
        osrs_t: Oversampling,
        osrs_p: Oversampling,
        mode: Mode,
        filter: Filter,
        standby: StandbyTime,
    ) Bmp280Error!void {
        const ctrl_meas: u8 = (@as(u8, @intFromEnum(osrs_t)) << 5) |
                              (@as(u8, @intFromEnum(osrs_p)) << 2) |
                              @as(u8, @intFromEnum(mode));
        try self.writeReg(.ctrl_meas, ctrl_meas);

        const config: u8 = (@as(u8, @intFromEnum(standby)) << 5) |
                           (@as(u8, @intFromEnum(filter)) << 2);
        try self.writeReg(.config, config);
    }

    pub fn read(self: *Bmp280) Bmp280Error!SensorData {
        var raw: [6]u8 = undefined;
        try self.readRegs(.press_msb, &raw);

        const adc_p: i32 = (@as(i32, raw[0]) << 12) |
                           (@as(i32, raw[1]) << 4) |
                           (@as(i32, raw[2]) >> 4);
        const adc_t: i32 = (@as(i32, raw[3]) << 12) |
                           (@as(i32, raw[4]) << 4) |
                           (@as(i32, raw[5]) >> 4);

        const temp_x100 = self.compensateTemperature(adc_t);
        const pressure = self.compensatePressure(adc_p);

        return SensorData{
            .temperature_c = @as(f32, @floatFromInt(temp_x100)) / 100.0,
            .pressure_pa = @as(f32, @floatFromInt(pressure)) / 256.0,
        };
    }

    fn readReg(self: *Bmp280, reg: Register) Bmp280Error!u8 {
        var data: [1]u8 = undefined;
        try self.readRegs(reg, &data);
        return data[0];
    }

    fn readRegs(self: *Bmp280, reg: Register, data: []u8) Bmp280Error!void {
        const reg_byte: u8 = @intFromEnum(reg);
        self.i2c.writeRead(self.i2c.ctx, bmp280_addr, &.{reg_byte}, data) catch |err| {
            return switch (err) {
                I2cError.Busy => Bmp280Error.Busy,
                I2cError.NackAddr => Bmp280Error.NackAddr,
                I2cError.NackData => Bmp280Error.NackData,
                I2cError.Timeout => Bmp280Error.Timeout,
                I2cError.Arbitration => Bmp280Error.Arbitration,
            };
        };
    }

    fn writeReg(self: *Bmp280, reg: Register, value: u8) Bmp280Error!void {
        const reg_byte: u8 = @intFromEnum(reg);
        self.i2c.write(self.i2c.ctx, bmp280_addr, &.{ reg_byte, value }) catch |err| {
            return switch (err) {
                I2cError.Busy => Bmp280Error.Busy,
                I2cError.NackAddr => Bmp280Error.NackAddr,
                I2cError.NackData => Bmp280Error.NackData,
                I2cError.Timeout => Bmp280Error.Timeout,
                I2cError.Arbitration => Bmp280Error.Arbitration,
            };
        };
    }

    fn compensateTemperature(self: *Bmp280, adc: i32) i32 {
        const c = &self.calib;

        const var1: i32 = ((((adc >> 3) - (@as(i32, c.dig_t1) << 1)) *
                            @as(i32, c.dig_t2)) >> 11);

        const var2: i32 = (((((adc >> 4) - @as(i32, c.dig_t1)) *
                             ((adc >> 4) - @as(i32, c.dig_t1))) >> 12) *
                            @as(i32, c.dig_t3)) >> 14;

        self.t_fine = var1 + var2;

        return (self.t_fine * 5 + 128) >> 8;
    }

    fn compensatePressure(self: *const Bmp280, adc: i32) u32 {
        const c = &self.calib;

        var var1: i64 = @as(i64, self.t_fine) - 128000;
        var var2: i64 = var1 * var1 * @as(i64, c.dig_p6);
        var2 = var2 + ((var1 * @as(i64, c.dig_p5)) << 17);
        var2 = var2 + (@as(i64, c.dig_p4) << 35);
        var1 = ((var1 * var1 * @as(i64, c.dig_p3)) >> 8) +
               ((var1 * @as(i64, c.dig_p2)) << 12);
        var1 = ((((@as(i64, 1) << 47) + var1)) * @as(i64, c.dig_p1)) >> 33;

        if (var1 == 0) return 0;

        var p: i64 = 1048576 - @as(i64, adc);
        p = (((p << 31) - var2) * 3125) / var1;
        var1 = (@as(i64, c.dig_p9) * (p >> 13) * (p >> 13)) >> 25;
        var2 = (@as(i64, c.dig_p8) * p) >> 19;
        p = ((p + var1 + var2) >> 8) + (@as(i64, c.dig_p7) << 4);

        return @as(u32, @intCast(p >> 8));
    }
};
// main.zig
const std = @import("std");
const bmp280 = @import("bmp280.zig");

// Mock I2C implementation for demonstration
// In real code, this wraps your hardware peripheral
const MockI2c = struct {
    fn write(ctx: *anyopaque, addr: u8, data: []const u8) bmp280.I2cError!void {
        _ = ctx;
        _ = addr;
        _ = data;
        // Real implementation would write to I2C peripheral
    }

    fn read(ctx: *anyopaque, addr: u8, data: []u8) bmp280.I2cError!void {
        _ = ctx;
        _ = addr;
        _ = data;
        // Real implementation would read from I2C peripheral
    }

    fn writeRead(ctx: *anyopaque, addr: u8, tx: []const u8, rx: []u8) bmp280.I2cError!void {
        _ = ctx;
        _ = addr;
        _ = tx;
        _ = rx;
        // Real implementation would do write-then-read
    }

    fn interface() bmp280.I2cInterface {
        return bmp280.I2cInterface{
            .ctx = undefined,
            .write = write,
            .read = read,
            .writeRead = writeRead,
        };
    }
};

pub fn main() !void {
    const i2c = MockI2c.interface();

    var dev = try bmp280.Bmp280.init(i2c);

    try dev.configure(
        .x1,
        .x16,
        .normal,
        .coeff16,
        .ms0_5,
    );

    while (true) {
        const data = dev.read() catch |err| {
            std.debug.print("Read error: {}\n", .{err});
            continue;
        };

        std.debug.print("Temp: {d:.2} C  Pressure: {d:.2} hPa\n", .{
            data.temperature_c,
            data.pressure_pa / 100.0,
        });

        // Delay ~1 second
        var i: usize = 0;
        while (i < 400000) : (i += 1) {}
    }
}

Build and Run Instructions

C (ARM GCC)

# Install toolchain
sudo apt install gcc-arm-none-eabi gdb-multiarch

# Build
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O2 \
    -fno-common -ffunction-sections -fdata-sections \
    -Wall -Wextra -Werror \
    -T stm32f405rg.ld \
    -o bmp280.elf \
    main.c i2c.c bmp280.c startup_stm32f405xx.c

# Generate binary
arm-none-eabi-objcopy -O binary bmp280.elf bmp280.bin
arm-none-eabi-size bmp280.elf

Rust (embedded)

# Install toolchain
rustup target add thumbv7em-none-eabihf
cargo install flip-link

# Build
cargo build --release --target thumbv7em-none-eabihf

# Size
cargo bloat --release --target thumbv7em-none-eabihf

Ada (GNAT ARM ELF)

# Install GNAT for ARM
sudo apt install gnat-arm-elf

# Build with gprbuild
gprbuild -P bmp280.gpr -XTARGET=arm-elf -O2

# Or compile manually
arm-eabi-gcc -c -O2 -gnatp bmp280.adb
arm-eabi-gcc -c -O2 -gnatp main.adb
arm-eabi-gnatbind main.ali
arm-eabi-gnatlink main.ali -o main.elf

Zig

# Install Zig 0.11+
# https://ziglang.org/download/

# Build for bare-metal ARM
zig build-exe main.zig -target thumbv7em-freestanding-eabihf -OReleaseSmall

# Or build for host testing
zig build-exe main.zig -OReleaseFast

Renode Verification

Create a Renode platform file (bmp280.resc):

# Create STM32F405 machine
mach create

# Add CPU
machine LoadPlatformDescription @platforms/cpus/stm32f4.resc

# Add BMP280 at I2C address 0x76
i2c.bmp280: Peripherals.BMP280 @ i2c2 0x76

# Start emulation
mach start

# Monitor I2C bus activity
showAnalyzer sysbus.i2c2

# Set logging for I2C transactions
logLevel 3 sysbus.i2c2

Run it:

renode bmp280.resc

# In the Renode monitor:
# Load your binary
sysbus LoadELF bmp280.elf

# Start execution
start

# Watch I2C transactions in the analyzer window

Expected output in the analyzer:

[0x00001234] I2C: START -> 0xEC (0x76+W) -> ACK -> 0xD0 -> ACK -> RESTART -> 0xED (0x76+R) -> ACK -> 0x58 -> NACK -> STOP
[0x00002345] I2C: START -> 0xEC (0x76+W) -> ACK -> 0x88 -> ACK -> RESTART -> 0xED (0x76+R) -> ACK -> [26 bytes] -> NACK -> STOP
[0x00003456] I2C: START -> 0xEC (0x76+W) -> ACK -> 0xF4 -> ACK -> 0xD5 -> ACK -> STOP

What You Learned

Next Steps

Language Comparison

Feature C Rust Ada Zig
Error handling Enum return codes, manual checking Result<T, E> with ? operator Typed error enum, explicit checks Error unions with catch/try
Register map #define constants, no validation mod with const values Strongly typed constants enum(u8) with comptime validation
Calibration struct Plain struct, no guarantees Struct with private fields Record with explicit types Struct with @bitCast for signed
I2C abstraction Raw pointer to registers Generic embedded_hal::I2c trait Abstract I2C_Port tagged type Function pointer interface
Fixed-point math Manual shifts, overflow-prone Same as C but with as casts Ada arithmetic with range safety Explicit @as() and @bitCast()
Type safety None — easy to mix up registers Compile-time trait enforcement Strong typing prevents misuse Comptime catches invalid registers
Memory safety Manual — easy to overflow buffers Borrow checker prevents aliasing Bounds-checked arrays Explicit slices with length
Binary size ~4KB (minimal) ~6KB (with embedded-hal) ~8KB (runtime overhead) ~5KB (no runtime)

Deliverables

References

STMicroelectronics Documentation

ARM Documentation

Sensor Documentation

Tools & Emulation