Project 9: I2C Sensor Driver and Calibration Log

An I2C driver that reads sensor registers, applies calibration, and logs both raw and calibrated data.

Quick Reference

Attribute Value
Difficulty Level 2: Intermediate
Time Estimate 1-2 weeks
Main Programming Language C (Alternatives: C++, Rust, Ada)
Alternative Programming Languages C++, Rust, Ada
Coolness Level Level 3: Genuinely Clever
Business Potential 1. The “Resume Gold”
Prerequisites GPIO AF mapping, I2C basics, UART logging
Key Topics I2C protocol, register maps, calibration

1. Learning Objectives

By completing this project, you will:

  1. Implement a robust I2C read/write driver with timeouts.
  2. Parse sensor register data and apply calibration.
  3. Log raw vs calibrated values for validation.
  4. Recover from I2C bus errors.

2. All Theory Needed (Per-Concept Breakdown)

I2C Protocol and Sensor Calibration

Fundamentals I2C is a two-wire serial bus used for connecting sensors and peripherals. It uses open-drain lines (SCL and SDA) with pull-up resistors and supports multiple devices on the same bus. Each transaction consists of a start condition, address, data bytes, and a stop condition. Sensors typically expose registers over I2C, so reliable driver code requires understanding the protocol and how to convert raw register values into physical units.

Deep Dive into the concept I2C is a synchronous, multi-master bus with arbitration and clock stretching. In practice on microcontrollers, it is usually used in a single-master configuration. The master generates the clock and the start/stop conditions; devices are addressed by a 7-bit or 10-bit address. Each byte is followed by an ACK/NACK bit. The bus is open-drain, so the lines are pulled high by resistors and devices only pull low. This allows multiple devices to share the same bus without contention. For sensor drivers, the typical sequence is: send a start, send device address with write bit, send register address, send a repeated start, send device address with read bit, then read one or more bytes. Timing matters. If the bus clock is too fast for a sensor, you may get NACKs. If pull-ups are too weak, rise times are too slow. The STM32F3 I2C peripheral supports hardware handling of start/stop and ACK, but you still need to handle error flags like NACK, bus error, or arbitration loss. Calibration adds another layer. Raw sensor data often includes offset, gain error, and temperature dependence. You must apply calibration constants and scaling factors to convert raw counts into real units (e.g., g, deg/s). A robust driver logs raw values, applies calibration, and provides both to the user. That makes debugging easier and validates the calibration pipeline. In embedded systems, I2C reliability issues are common. If you see bus lockup, you might need to manually toggle the clock line to recover from a stuck slave. Therefore, your project should include timeouts, retry logic, and a bus recovery procedure. This project teaches both protocol-level correctness and the data integrity pipeline needed to trust sensor values.

How this fit on projects In I2C Sensor Driver and Calibration Log, you implement an I2C sensor driver with error handling and calibration, then log calibrated measurements.

Definitions & key terms

  • Start/Stop -> Bus conditions indicating the beginning and end of a transaction.
  • ACK/NACK -> Acknowledgment bits that indicate successful reception.
  • Clock stretching -> Slave holds SCL low to delay master.
  • Register map -> Documentation of sensor registers and their meanings.
  • Calibration -> Process of correcting raw sensor values using offsets and scale factors.

Mental model diagram (ASCII)

START -> Addr(W) -> Reg -> RESTART -> Addr(R) -> Data -> STOP

How it works (step-by-step, with invariants and failure modes)

  1. Initialize I2C peripheral with correct clock speed and GPIO open-drain mode.
  2. Implement read/write routines with timeouts and error checks.
  3. Read sensor registers and log raw values.
  4. Apply calibration and scaling to produce physical units.
  5. Invariant: every transfer ends with a STOP; failure mode: bus lockup if STOP is missed.

Minimal concrete example

// Pseudocode: read sensor register
i2c_start();
i2c_write(ADDR << 1 | 0);
i2c_write(REG);
i2c_restart();
i2c_write(ADDR << 1 | 1);
uint8_t val = i2c_read_nack();
i2c_stop();

Common misconceptions

  • I2C is always reliable ignores bus errors and stuck lines.
  • Calibration is optional ignores sensor biases and drift.
  • Any pull-up value works ignores rise time constraints.

Check-your-understanding questions

  1. Why does I2C use open-drain signaling?
  2. What is the purpose of a repeated start?
  3. How do you recover a stuck I2C bus?

Check-your-understanding answers

  1. Open-drain allows multiple devices to share the bus without contention.
  2. It allows changing direction (write to read) without releasing the bus.
  3. Toggle SCL manually to release a stuck slave, then generate a STOP.

Real-world applications

  • Environmental sensors and IMUs.
  • EEPROM configuration storage.
  • Battery fuel gauge communication.

Where you’ll apply it

References

  • NXP I2C-bus specification and user manual.
  • STM32F3 Reference Manual (I2C chapter).
  • Sensor datasheets for register maps and calibration constants.

Key insights

  • Reliable I2C drivers are equal parts protocol correctness and calibrated data handling.

Summary I2C connects your MCU to sensors, but the raw bytes mean nothing without calibration. A robust driver validates the bus, handles errors, and produces trustworthy units.

Homework/Exercises to practice the concept

  1. Implement I2C read with timeout and retry logic.
  2. Capture raw sensor output and compute calibrated units.
  3. Measure rise time on SDA/SCL with different pull-up resistors.

Solutions to the homework/exercises

  1. A timeout prevents infinite blocking; retries can recover transient errors.
  2. Apply offset and scale from datasheet to convert raw counts to units.
  3. Stronger pull-ups reduce rise time but increase power draw.

GPIO Modes and Alternate Functions (Pin Multiplexing)

Fundamentals GPIO is the MCU’s physical interface to the outside world. Each pin can be configured as input, output, analog, or alternate function. Alternate function routes internal peripheral signals (UART, SPI, timers, etc.) to the pin. On STM32, a pin can support multiple alternate functions, but only one can be active at a time. A correct GPIO configuration must include mode, pull-up/pull-down, output type, speed, and alternate function selection. Misconfiguring any of these leads to silent failure, which is why a pin map validation project is essential.

Deep Dive into the concept The STM32 GPIO subsystem is both electrical and logical. Electrically, you choose input or output driver types (push-pull or open-drain), internal pulls, and slew rate. Logically, you choose whether the pin is GPIO-controlled or assigned to a peripheral. These decisions interact: for example, I2C requires open-drain outputs with pull-ups, while SPI requires push-pull. Alternate functions are selected via the AFR registers, which are split into low and high halves for pins 0-7 and 8-15. Each pin can have up to 16 alternate function mappings (AF0-AF15), and the mapping is defined in the datasheet’s AF table. You cannot infer AF numbers; you must consult the table. Additionally, some pins are already occupied on the discovery board by LEDs, sensors, or the ST-LINK interface. That means your pin map must include board-level constraints, not just MCU capability. A thorough pin map process starts with the board schematic, identifies which pins are routed to headers, and then annotates each with possible peripheral functions and conflicts. You should then validate at least one alternate function per peripheral class with a visible effect: UART TX should print, a timer channel should output PWM, and an ADC input should show a voltage change. This validation proves that your clock, GPIO configuration, and pin selection are all correct. It also trains you to resolve conflicts: if two peripherals want the same pin, you must choose a different mapping or a different peripheral instance. In real products, pin mux decisions are architectural: they affect board layout, firmware flexibility, and even BOM cost. By building a pin map and verifying functions, you practice the exact process used in professional embedded hardware bring-up.

How this fit on projects In I2C Sensor Driver and Calibration Log, you document and verify a pin map that includes GPIO modes, alternate functions, and board-level conflicts.

Definitions & key terms

  • Push-pull -> Output driver actively drives high and low; strong drive for digital outputs.
  • Open-drain -> Output can only pull low; requires pull-up for high.
  • Alternate function -> Pin routing selection for peripheral signals.
  • Slew rate -> Output transition speed setting that affects EMI and signal integrity.
  • Pin mux -> The multiplexer that selects which internal signal appears on a pin.

Mental model diagram (ASCII)

[Peripheral A]--
           >--[Pin MUX]--> Pin PA9
[Peripheral B]--/
             (select AF mapping)

How it works (step-by-step, with invariants and failure modes)

  1. Identify required peripherals and candidate pins from the datasheet.
  2. Check board schematic for pins already used by LEDs, sensors, or debug interfaces.
  3. Configure GPIO mode, pull, speed, and alternate function for each selected pin.
  4. Validate each mapped pin by creating a physical or observable signal.
  5. Invariant: each pin has a single active function; failure mode: silent conflicts or wrong AF number.

Minimal concrete example

// Configure PA9 as USART1_TX (AF7)
GPIOA->MODER |= (2 << (9 * 2));
GPIOA->AFR[1] |= (7 << ((9 - 8) * 4));

Common misconceptions

  • Any pin can be mapped to any peripheral ignores fixed AF tables.
  • GPIO defaults are safe ignores that pins may power up in analog mode.
  • If a pin works once, it is always correct ignores board-level conflicts and alternate mappings.

Check-your-understanding questions

  1. Why does I2C require open-drain outputs?
  2. How do you find the correct AF number for a pin?
  3. What symptoms indicate that a pin conflict exists?

Check-your-understanding answers

  1. I2C uses wired-AND signaling where multiple devices pull the line low, requiring open-drain drivers and pull-ups.
  2. Consult the datasheet’s alternate function table for that specific pin and peripheral.
  3. Signals appear on the wrong pin, remain stuck, or multiple peripherals stop working when enabled together.

Real-world applications

  • Board bring-up and pin validation in new hardware.
  • Multi-peripheral embedded devices with strict pin budgets.
  • Signal integrity tuning by selecting appropriate slew rates.

Where you’ll apply it

References

  • STM32F3 Reference Manual (GPIO and AF registers).
  • STM32F3DISCOVERY board schematic (pin usage).
  • Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (I/O configuration).

Key insights

  • Pin muxing is an architectural choice; you must prove it with hardware behavior, not just code.

Summary GPIO configuration ties firmware to the physical board. Correct alternate-function selection, electrical mode, and board-level awareness are what turn a schematic into a working product.

Homework/Exercises to practice the concept

  1. Create a pin map table for 10 pins and include at least two alternate functions each.
  2. Verify one UART TX pin and one timer channel on hardware.
  3. Identify two pins that cannot be used because of on-board sensors.

Solutions to the homework/exercises

  1. A valid pin map includes port, pin, AF number, peripheral, and board conflict notes.
  2. UART TX can be verified by printing a known string at 115200 baud.
  3. On-board MEMS sensors occupy dedicated I2C/SPI pins; consult the schematic to identify them.

Board Schematic Reading and Pin Constraints

Fundamentals A datasheet tells you what the MCU can do, but the board schematic tells you what you can actually use. The STM32F3DISCOVERY connects LEDs, sensors, and debug interfaces to specific pins. Those pins are no longer ‘free’ for arbitrary functions. Reading the schematic lets you see which nets go to headers, which are tied to components, and which are shared. A pin map is only correct when it accounts for these board constraints.

Deep Dive into the concept A schematic is a connectivity map, not just a drawing. Each symbol connects to nets that represent electrical nodes, and each net corresponds to a MCU pin or off-board connector. When you read a board schematic, you trace a pin to see all the components attached to it. If a pin drives an LED through a resistor, you know it has a load and might be shared with a timer output. If a pin connects to an accelerometer, you know that I2C or SPI peripheral is already in use. The STM32F3DISCOVERY uses a dedicated ST-LINK interface that can occupy SWD pins and sometimes UART pins for virtual COM. Schematic reading also reveals power domains: which pins are powered at 3.3 V, which are analog, and how ground is routed. This matters because analog performance depends on low-noise grounding and clean Vref pins. By combining the schematic with the datasheet, you can build a pin constraint table. This table contains each pin, its board usage, any alternate functions available, and the conflicts. In real projects, this process informs both firmware and hardware decisions. If you need an extra PWM output but all timer channels are used by LEDs, you must choose a different timer, reroute on hardware, or change the product requirement. The key is to treat the board as a fixed system with constraints, not a blank MCU. In the context of the STM32F3DISCOVERY, a simple example is the user LEDs on port E: they are easy to use for status, but they also consume GPIOs and timer channels. If you plan to use PWM or advanced timers, you must cross-reference those pins. Thus, schematic reading is not a separate activity; it is a part of reliable firmware design.

How this fit on projects In I2C Sensor Driver and Calibration Log, you trace board nets to understand which pins are usable and which are already assigned to onboard hardware, then verify those decisions in firmware.

Definitions & key terms

  • Net -> An electrical connection that ties multiple pins together.
  • Header -> A connector that exposes MCU pins for external use.
  • Pull-up resistor -> A resistor that biases a line high when not actively driven.
  • Load -> Anything attached to a pin that draws current or affects signal integrity.
  • Constraint -> A limitation imposed by board routing or attached components.

Mental model diagram (ASCII)

MCU Pin -> Net -> (LED + Resistor) -> GND
    -> Net -> Header Pin
(two loads share one MCU pin)

How it works (step-by-step, with invariants and failure modes)

  1. Open the board schematic and locate the MCU pinout page.
  2. Trace each pin of interest to see which nets and components it connects to.
  3. Mark pins that are shared with sensors, LEDs, or debug interfaces.
  4. Build a constraint-aware pin map that lists usable pins and conflicts.
  5. Invariant: each firmware pin decision matches the board wiring; failure mode: conflicts with onboard components.

Minimal concrete example

// Example pin map entry
// PE8: LED3 -> TIM1_CH1 possible, but LED load affects PWM brightness
// PA13/PA14: SWD -> reserved for debug

Common misconceptions

  • If the MCU datasheet lists it, I can use it ignores board wiring constraints.
  • LED pins are always free ignores timer channel sharing and current load.
  • Debug pins are optional ignores the need for SWD during development.

Check-your-understanding questions

  1. Why might a PWM output on an LED pin distort a sensor signal?
  2. How can the ST-LINK interface restrict your pin choices?
  3. What is the first step in creating a constraint-aware pin map?

Check-your-understanding answers

  1. Because the LED and resistor create an electrical load and can inject noise or limit voltage swing.
  2. ST-LINK uses SWD pins and sometimes a UART bridge, which reserves those pins.
  3. Start with the board schematic to identify which pins are already connected to components.

Real-world applications

  • Early-stage board bring-up when peripherals do not respond.
  • Pin budget planning in resource-constrained designs.
  • Avoiding electrical conflicts in mixed-signal systems.

Where you’ll apply it

References

  • STM32F3DISCOVERY board schematic (net mapping).
  • ‘Practical Electronics for Inventors’ (basic schematic reading).
  • ST application notes on board bring-up and pin multiplexing.

Key insights

  • The board schematic is your ground truth for what pins are truly available.

Summary You cannot assign pins from the datasheet alone. The schematic tells you what the board already uses, and a correct firmware design respects those constraints.

Homework/Exercises to practice the concept

  1. Identify three pins that are reserved by ST-LINK or sensors.
  2. Find one pin that is routed to a header and list two possible alternate functions.
  3. Document the power rail connected to the analog pins.

Solutions to the homework/exercises

  1. SWD pins and sensor bus pins are typically reserved; confirm in the schematic.
  2. A header pin might support UART or timer functions; confirm using the AF table.
  3. Analog pins share AVDD/AGND rails; note the decoupling components shown in the schematic.

3. Project Specification

3.1 What You Will Build

A driver for an onboard I2C sensor that reads registers, applies calibration, and logs measurements with error handling.

3.2 Functional Requirements

  1. I2C Read/Write: Implement register read/write with timeout and retry.
  2. Sensor Init: Configure sensor registers on boot.
  3. Calibration: Apply offset/scale calibration to raw data.
  4. Logging: Log raw and calibrated values at fixed intervals.

3.3 Non-Functional Requirements

  • Performance: Read sensor at 50-100 Hz without missed transfers.
  • Reliability: Recover from bus errors within 2 seconds.
  • Usability: Log format suitable for spreadsheet analysis.

3.4 Example Usage / Output

RAW: ax=1024 ay=-36 az=16384
CAL: ax=0.06g ay=-0.00g az=1.00g
Status: PASS

3.5 Data Formats / Schemas / Protocols

Log format: RAW: ax=<int> ay=<int> az=<int> and CAL: ax=<g> ay=<g> az=<g>

3.6 Edge Cases

  • I2C NACK on address.
  • Bus stuck low due to sensor lockup.
  • Calibration constants not loaded.
  • Wrong register endianness.

3.7 Real World Outcome

You will read sensor data reliably, apply calibration, and verify output consistency.

3.7.1 How to Run (Copy/Paste)

$ make flash
$ screen /dev/tty.usbmodem* 115200

3.7.2 Golden Path Demo (Deterministic)

  • Keep the board flat; accelerometer Z should read ~1 g.

3.7.3 CLI Transcript (Success)

RAW: ax=100 ay=20 az=16300
CAL: ax=0.01g ay=0.00g az=0.99g
RESULT=PASS
# Exit code: 0

3.7.4 Failure Demo (Bus Error)

I2C_ERR=NACK
RESULT=FAIL
# Exit code: 2

4. Solution Architecture

I2C driver reads sensor registers; calibration module scales data; logger outputs raw/calibrated values.

4.1 High-Level Design

I2C Bus -> Sensor Reg Read -> Calibration -> UART Log

4.2 Key Components

Component Responsibility Key Decisions
I2C Driver Read/write registers with error handling Timeouts and retries enabled
Sensor Init Configure sensor modes and ranges Apply datasheet defaults
Calibration Apply offset/scale Store constants in flash or compile-time

4.3 Data Structures (No Full Code)

typedef struct {
int16_t raw[3];
float cal[3];
} imu_sample_t;

4.4 Algorithm Overview

Register Read

  1. Write register address.
  2. Repeated start and read bytes.
  3. Assemble and scale data.

Complexity: O(1) per read.


5. Implementation Guide

5.1 Development Environment Setup

make init
make flash
screen /dev/tty.usbmodem* 115200

5.2 Project Structure

project-root/
|-- src/
|   |-- main.c
|   |-- drivers/
|   `-- app/
|-- include/
|-- Makefile
`-- README.md

5.3 The Core Question You’re Answering

“How do I get trustworthy sensor data over I2C?”

5.4 Concepts You Must Understand First

  1. I2C transaction sequence and ACK/NACK.
  2. Sensor register maps.
  3. Calibration and unit conversion.

5.5 Questions to Guide Your Design

  1. What is your retry strategy on NACK?
  2. How will you log raw and calibrated values?
  3. How will you detect bus lockups?

5.6 Thinking Exercise

Calibration Check

Board flat -> accel Z should read ~1 g
If Z != 1 g, compute offset and apply correction

5.7 The Interview Questions They’ll Ask

  1. Explain how an I2C register read works.
  2. What is clock stretching?
  3. How do you calibrate a sensor?

5.8 Hints in Layers

Hint 1: Start with WHO_AM_I register read. Hint 2: Add retries on NACK. Hint 3: Log raw data before applying calibration.

5.9 Books That Will Help

Topic Book Chapter
I2C protocol I2C-bus Specification Sections 3-5
Embedded drivers Making Embedded Systems Ch. 9

5.10 Implementation Phases

Phase 1: I2C Bring-Up (2-3 days)

Read WHO_AM_I and verify device address.

Phase 2: Continuous Reads (4 days)

Stream sensor data and log raw values.

Phase 3: Calibration (4 days)

Apply offsets and validate calibrated output.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Bus speed 100 kHz vs 400 kHz 400 kHz Faster reads if sensor supports it
Error handling None vs retries Retries + timeout Robustness

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Register decode Endian conversion checks
Integration Tests I2C transfers WHO_AM_I response
Edge Case Tests NACK handling Disconnect sensor

6.2 Critical Test Cases

  1. WHO_AM_I: Correct device ID read.
  2. Calibration: Z-axis reads ~1 g when flat.
  3. Bus Error Recovery: Driver recovers after NACK.

6.3 Test Data

WHO_AM_I=0x33
Z axis calibrated=0.99g

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Wrong pull-ups I2C lines stuck low Use correct pull-up resistors
Wrong address NACK on every transfer Verify address from datasheet
No calibration Offsets in output Apply bias correction

7.2 Debugging Strategies

  • Scan I2C bus to detect device presence.
  • Log error flags on NACK or bus error.
  • Use a logic analyzer to verify start/stop sequences.

7.3 Performance Traps

Too many retries can block the main loop; set reasonable timeouts.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a temperature register read if available.

8.2 Intermediate Extensions

  • Implement bus recovery by toggling SCL.

8.3 Advanced Extensions

  • Support multiple sensors on the same bus.

9. Real-World Connections

9.1 Industry Applications

  • IMU integration: Sensor data acquisition for motion systems.
  • IoT monitoring: Environmental sensor data collection.
  • Zephyr I2C drivers: Reference implementations for robust I2C.
  • STM32Cube drivers: Vendor HAL sensor examples.

9.3 Interview Relevance

  • I2C transaction details and error handling.
  • Sensor calibration and data interpretation.

10. Resources

10.1 Essential Reading

  • I2C-bus specification by NXP - Protocol details.
  • Making Embedded Systems by Elecia White - Driver development approach.

10.2 Video Resources

  • I2C bus analyzer demo.
  • STM32 I2C driver tutorial.

10.3 Tools & Documentation

  • Logic analyzer: Inspect I2C waveform.
  • STM32CubeIDE: Build and debug.

11. Self-Assessment Checklist

11.1 Understanding

  • I can describe an I2C read transaction.
  • I can explain the role of pull-ups.
  • I can apply calibration to raw sensor data.

11.2 Implementation

  • Sensor data reads consistently.
  • Calibration outputs are stable.
  • Error handling works on NACK.

11.3 Growth

  • I can extend the driver to another sensor.
  • I can debug I2C issues with a logic analyzer.
  • I can explain calibration in interviews.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • I2C read/write works.
  • Raw data logged.
  • Basic calibration applied.

Full Completion:

  • Error handling and retries implemented.
  • Bus recovery tested.

Excellence (Going Above & Beyond):

  • Multiple sensors supported or advanced calibration.