Read a Sensor over I2C
Goal: Read temperature from an I2C sensor (LM75 or similar) using bare-metal register-level I2C on STM32. Understand the I2C protocol at the wire level.
Prerequisites: UART SPI I2C, Memory-Mapped IO, GPIO and Digital IO, ADC and DAC
What We’re Building
Read temperature from an LM75/TMP102 sensor connected to PB8 (SCL) and PB9 (SDA) via I2C1. Print the result over UART.
STM32 (master) ──SCL──┬── LM75 (slave, addr 0x48)
──SDA──┘ 4.7kΩ pull-ups to 3.3V
Step 1: I2C Protocol Recap
I2C uses two open-drain lines with pull-up resistors:
Idle: SDA = HIGH, SCL = HIGH
START: SDA falls while SCL is HIGH
────╲___
Address byte: 7-bit address + R/W bit (0=write, 1=read)
Data bytes: 8 bits, MSB first
ACK: Receiver pulls SDA LOW for one clock (NACK = HIGH)
STOP: SDA rises while SCL is HIGH
___╱────
Full read transaction for LM75 temperature register:
START → [0x48<<1 | 0] → ACK → [0x00] → ACK →
RESTART → [0x48<<1 | 1] → ACK → [MSB] → ACK → [LSB] → NACK → STOP
Step 2: Configure I2C1
// Register definitions
#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830)
#define RCC_APB1ENR (*(volatile uint32_t *)0x40023840)
#define GPIOB_BASE 0x40020400
#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_AFRH (*(volatile uint32_t *)(GPIOB_BASE + 0x24))
#define I2C1_BASE 0x40005400
#define I2C1_CR1 (*(volatile uint32_t *)(I2C1_BASE + 0x00))
#define I2C1_CR2 (*(volatile uint32_t *)(I2C1_BASE + 0x04))
#define I2C1_OAR1 (*(volatile uint32_t *)(I2C1_BASE + 0x08))
#define I2C1_DR (*(volatile uint32_t *)(I2C1_BASE + 0x10))
#define I2C1_SR1 (*(volatile uint32_t *)(I2C1_BASE + 0x14))
#define I2C1_SR2 (*(volatile uint32_t *)(I2C1_BASE + 0x18))
#define I2C1_CCR (*(volatile uint32_t *)(I2C1_BASE + 0x1C))
#define I2C1_TRISE (*(volatile uint32_t *)(I2C1_BASE + 0x20))
void i2c_init(void) {
// Enable clocks: GPIOB + I2C1
RCC_AHB1ENR |= (1 << 1); // GPIOBEN
RCC_APB1ENR |= (1 << 21); // I2C1EN
// PB8 (SCL) and PB9 (SDA): AF4 (I2C1), open-drain, pull-up
GPIOB_MODER &= ~((3 << 16) | (3 << 18));
GPIOB_MODER |= ((2 << 16) | (2 << 18)); // alternate function
GPIOB_OTYPER |= ((1 << 8) | (1 << 9)); // open-drain (required for I2C)
GPIOB_OSPEEDR |= ((3 << 16) | (3 << 18)); // high speed
GPIOB_PUPDR &= ~((3 << 16) | (3 << 18));
GPIOB_PUPDR |= ((1 << 16) | (1 << 18)); // pull-up
GPIOB_AFRH &= ~((0xF << 0) | (0xF << 4));
GPIOB_AFRH |= ((4 << 0) | (4 << 4)); // AF4 = I2C1
// Reset I2C1
I2C1_CR1 |= (1 << 15); // SWRST
I2C1_CR1 &= ~(1 << 15);
// Configure I2C: 100 kHz standard mode at 16 MHz APB1
I2C1_CR2 = 16; // APB1 clock = 16 MHz
I2C1_CCR = 80; // 16MHz / (2 * 100kHz) = 80
I2C1_TRISE = 17; // (16MHz / 1MHz) + 1 = 17
I2C1_CR1 |= (1 << 0); // PE: enable I2C
}Why open-drain
I2C is a shared bus. Open-drain means any device can pull the line LOW, but no device drives it HIGH — the pull-up resistor does that. This prevents bus contention. See GPIO and Digital IO output modes.
Step 3: I2C Low-Level Operations
static void i2c_wait_flag(volatile uint32_t *reg, uint32_t flag) {
while (!(*reg & flag)) {}
}
void i2c_start(void) {
I2C1_CR1 |= (1 << 8); // generate START
i2c_wait_flag(&I2C1_SR1, (1 << 0)); // wait for SB (start bit)
}
void i2c_stop(void) {
I2C1_CR1 |= (1 << 9); // generate STOP
}
void i2c_send_addr(uint8_t addr, uint8_t rw) {
I2C1_DR = (addr << 1) | rw;
i2c_wait_flag(&I2C1_SR1, (1 << 1)); // wait for ADDR
(void)I2C1_SR2; // clear ADDR by reading SR2
}
void i2c_write_byte(uint8_t data) {
I2C1_DR = data;
i2c_wait_flag(&I2C1_SR1, (1 << 7)); // wait for TXE
}
uint8_t i2c_read_byte_ack(void) {
I2C1_CR1 |= (1 << 10); // ACK = 1
i2c_wait_flag(&I2C1_SR1, (1 << 6)); // wait for RXNE
return I2C1_DR;
}
uint8_t i2c_read_byte_nack(void) {
I2C1_CR1 &= ~(1 << 10); // ACK = 0 (NACK)
i2c_stop(); // generate STOP before reading last byte
i2c_wait_flag(&I2C1_SR1, (1 << 6)); // wait for RXNE
return I2C1_DR;
}Why NACK on the last byte
The I2C spec requires the master to NACK the last byte it wants to read. This tells the slave to stop driving the bus and let the master generate STOP.
Step 4: Read Temperature from LM75
int16_t read_temperature(uint8_t sensor_addr) {
// Write: select temperature register (0x00)
i2c_start();
i2c_send_addr(sensor_addr, 0); // write mode
i2c_write_byte(0x00); // temperature register pointer
// Read: 2 bytes (MSB, LSB)
i2c_start(); // repeated start
i2c_send_addr(sensor_addr, 1); // read mode
uint8_t msb = i2c_read_byte_ack();
uint8_t lsb = i2c_read_byte_nack();
// LM75: temperature = (MSB:LSB) >> 5, in 0.125°C steps
int16_t raw = (msb << 8) | lsb;
return raw >> 5; // 11-bit signed value, each bit = 0.125°C
}Step 5: Main Loop
// Assumes uart_init(), uart_puts(), uart_print_int() from Tutorial 08
void main(void) {
uart_init();
i2c_init();
uart_puts("=== I2C Temperature Reader ===\r\n");
while (1) {
int16_t raw = read_temperature(0x48);
// Convert: raw value × 0.125°C
int whole = raw / 8;
int frac = (raw % 8) * 125; // millidegrees
if (frac < 0) frac = -frac;
uart_puts("Temp: ");
uart_print_int(whole);
uart_putc('.');
uart_print_int(frac);
uart_puts(" C\r\n");
delay(1000000); // ~1 second
}
}Test
make flash
screen /dev/ttyACM0 115200
# Output:
# === I2C Temperature Reader ===
# Temp: 23.375 C
# Temp: 23.500 C
# ...If you don’t have a physical sensor, you can test the I2C code with a logic analyzer or by connecting SDA to ground briefly — the bus should detect the condition and the code should hang at the flag wait (add timeouts in exercises).
Debugging I2C
| Problem | Likely Cause |
|---|---|
| Hangs at ADDR flag | Wrong slave address, missing pull-ups, SDA/SCL swapped |
| Hangs at SB flag | I2C not enabled, clock not configured |
| Reads 0xFF | Slave not responding (NACK), bus stuck |
| Reads wrong data | Wrong register pointer, byte order, or data format |
# Logic analyzer is the best I2C debug tool
# Or use sigrok/PulseView with a $10 USB logic analyzerExercises
-
Timeout protection: Add a timeout to each flag wait. If the bus is stuck, reset I2C and retry. Never hang forever in production firmware.
-
I2C scanner: Write a function that tries all 127 addresses and prints which ones ACK. This is the embedded equivalent of
i2cdetect -y 1. -
Multiple sensors: Read from two I2C sensors (e.g., LM75 at 0x48 and 0x49) and print both temperatures. I2C addressing allows multiple devices on the same bus.
-
SPI comparison: Wire up an SPI temperature sensor (e.g., MAX31855). Compare the code complexity, speed, and wire count with I2C.
Next: 10 - PID Controller Simulation — close the control loop in software.