Project 5: ADC Sensor Sampler + Logging
A timer-triggered ADC sampler that logs calibrated sensor values at a fixed rate with timestamps.
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 | Timer interrupts, ADC basics, UART logging |
| Key Topics | ADC sampling, quantization, timing, logging |
1. Learning Objectives
By completing this project, you will:
- Configure ADC sampling with timer triggers.
- Convert raw ADC codes to calibrated voltages.
- Log data without disturbing sampling timing.
- Detect and explain sampling noise and aliasing.
2. All Theory Needed (Per-Concept Breakdown)
ADC Sampling, Quantization, and Signal Conditioning
Fundamentals An ADC converts an analog voltage into a digital number. The conversion depends on a reference voltage (Vref), a sampling capacitor, and the selected resolution. Sampling rate determines how often you capture the signal, while resolution determines the size of each voltage step. If you sample too slowly, you miss signal changes; if you sample too fast, you may overwhelm your firmware or capture noise. Understanding sampling time, reference voltage, and input impedance is essential for trustworthy measurements.
Deep Dive into the concept The STM32F3 ADCs use a sample-and-hold circuit that captures the input voltage onto a capacitor, then converts it to a digital code. The sample time must be long enough for the input to charge the capacitor through the source impedance; if it is too short, readings are low or noisy. Resolution (e.g., 12-bit) defines 2^N discrete levels, so the least significant bit represents Vref / 4096 volts. This means that noise or quantization error of even one LSB can be significant for low-level signals. Sampling rate is constrained by conversion time and the ADC clock. If you increase sampling frequency, you may need to reduce sample time or use multiple ADCs in interleaved mode. Aliasing is a key risk: if your signal contains frequency components above half the sampling rate, those components will appear as false lower-frequency signals. In practice, you mitigate aliasing with analog low-pass filters or by choosing a sufficiently high sampling rate. Calibration matters: STM32 ADCs often support calibration routines to reduce offset and gain error. Performing calibration at startup improves accuracy. For reliable measurements, you must also consider Vref stability. If Vref fluctuates, the ADC readings fluctuate even if the input is constant. Some designs use the internal reference voltage or measure Vrefint to compensate. Finally, logging ADC results can introduce jitter if done inside an interrupt. A better approach is to buffer results (possibly with DMA) and log them in a lower-priority task. This separation ensures that conversion timing remains accurate while still providing readable logs. In an ADC logging project, you will configure a timer trigger, set up the ADC sampling parameters, and validate that the measured voltages match a known reference. This approach builds the habit of treating ADC data as engineering data, not just numbers on a screen.
How this fit on projects In ADC Sensor Sampler + Logging, you build a reliable sampling pipeline that converts raw codes into voltages and logs them with timestamps.
Definitions & key terms
- Vref -> Reference voltage that defines the ADC’s full-scale range.
- LSB -> Least significant bit; the smallest measurable voltage step.
- Sample time -> Duration the ADC input capacitor is connected to the signal.
- Aliasing -> False low-frequency signals caused by undersampling.
- Quantization -> Mapping a continuous voltage to discrete digital levels.
Mental model diagram (ASCII)
Analog Signal -> Sample/Hold -> Quantize -> Digital Code -> Scale to Volts
How it works (step-by-step, with invariants and failure modes)
- Select ADC resolution and sampling time based on signal bandwidth and source impedance.
- Configure a timer trigger to start conversions at a fixed rate.
- Read raw ADC codes and convert to voltages using Vref and resolution.
- Log or buffer results without disturbing conversion timing.
- Invariant: sampling interval remains constant; failure mode: jitter or aliasing from poor trigger design.
Minimal concrete example
// Convert raw 12-bit ADC to volts
float volts = (raw * 3.3f) / 4095.0f;
printf("raw=%u volts=%.3f\n", raw, volts);
Common misconceptions
- Higher resolution always means better accuracy ignores Vref noise and calibration.
- Sampling faster always improves data ignores aliasing and firmware bandwidth.
- ADC codes are already in volts ignores scaling and reference voltage.
Check-your-understanding questions
- How does source impedance affect ADC sample time?
- What happens if you sample a 1 kHz signal at 1 kHz?
- Why might two boards report different ADC codes for the same input?
Check-your-understanding answers
- High source impedance charges the sample capacitor slowly, requiring longer sample time.
- You risk aliasing and will not capture the waveform shape; you need >2 kHz sampling.
- Differences in Vref, calibration, and analog noise cause variation.
Real-world applications
- Sensor acquisition in industrial monitoring.
- Audio sampling and signal analysis.
- Battery voltage measurement and power management.
Where you’ll apply it
- In this project: see Section 3.5 Data Formats / Protocols and Section 6.2 Critical Test Cases.
- Also used in: P08 DMA-Based ADC Ring Buffer.
References
- STM32F3 Reference Manual (ADC chapter).
- ‘The Art of Electronics’ (sampling fundamentals).
- Elecia White, ‘Making Embedded Systems’ (signal acquisition).
Key insights
- ADC data is only trustworthy when sampling time, reference voltage, and timing are engineered, not guessed.
Summary ADC sampling is a pipeline from physics to numbers. Proper sampling time, calibration, and timing validation turn raw codes into reliable measurements.
Homework/Exercises to practice the concept
- Compute LSB size for 12-bit ADC with 3.3 V reference.
- Measure a known voltage divider and compare raw code to expected value.
- Design a sampling plan for a sensor that changes at 5 Hz.
Solutions to the homework/exercises
- LSB = 3.3 / 4096 ~ 0.000805 V.
- Expected code is (Vout / 3.3) * 4095; small error due to tolerance is normal.
- Sampling at 50-100 Hz provides margin to capture changes without aliasing.
Timers, Prescalers, and Interrupt Scheduling
Fundamentals Hardware timers are the MCU’s timekeeping engines. They count ticks derived from a clock source and can generate periodic events without CPU busy-waiting. By configuring a prescaler and auto-reload value, you set a precise interval. Timers can trigger interrupts, toggle pins, or drive DMA. Interrupts allow your firmware to react to timer events with deterministic latency, which is the basis of real-time scheduling. Using timers and interrupts instead of delay loops is the core technique for reliable embedded timing.
Deep Dive into the concept STM32 timers are flexible peripherals that can operate as basic timebases, output compare generators, PWM engines, or input capture units. At their core is a counter clocked by a timer clock derived from the APB bus. The prescaler divides the timer clock, and the auto-reload register sets the period at which the counter resets. Each update event can trigger an interrupt, set a status flag, or generate a DMA request. The advantage over software delays is determinism: the hardware counts regardless of CPU load. However, interrupts introduce latency. The NVIC prioritizes interrupts, and higher-priority ISRs can delay timer handlers. If you rely on timer interrupts for scheduling, you must measure latency and account for jitter. A timer-driven LED sequencer is an ideal training ground: you build a state machine that advances on each timer tick, ensuring that your pattern timing is stable even if the CPU is busy. Advanced timers also support one-pulse mode, dead-time insertion, and complementary outputs, which are critical for motor control but beyond the basics here. For a correct timer configuration, you compute the prescaler as (timer_clock / desired_tick) - 1, and the auto-reload as (tick_rate / desired_interval) - 1. You then verify by toggling a GPIO on each interrupt and measuring the period. Misconfigurations are easy to spot: a factor-of-two error usually means you forgot the APB timer clock doubling rule. Another common failure is forgetting to clear interrupt flags, which results in repeated or missed interrupts. In short, timers are your deterministic clockwork. Mastering them means you can build schedules, measure drift, and guarantee timing, which is the heart of embedded systems.
How this fit on projects In ADC Sensor Sampler + Logging, you design a timer-driven schedule that advances a state machine without delay loops, proving deterministic timing.
Definitions & key terms
- Prescaler -> Divider that slows the timer clock.
- Auto-reload -> Value that sets the timer period.
- Update event -> Timer overflow event that can trigger interrupt or DMA.
- Interrupt latency -> Time between event and ISR execution.
- Jitter -> Variation in interrupt timing due to system load.
Mental model diagram (ASCII)
Timer clock -> prescaler -> counter -> overflow -> ISR -> state machine step
How it works (step-by-step, with invariants and failure modes)
- Select a timer and enable its clock.
- Compute prescaler and auto-reload for desired tick rate.
- Enable update interrupt and configure NVIC priority.
- In ISR, update the state machine and clear interrupt flags.
- Invariant: timer tick period remains stable; failure mode: drift or missed interrupts if flags are not cleared.
Minimal concrete example
// Timer ISR toggles LED state
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
step_led_pattern();
}
}
Common misconceptions
- Delays are fine for scheduling ignores CPU load and jitter.
- Timer interrupts are always on time ignores NVIC priority and blocking ISRs.
- Auto-reload equals milliseconds ignores prescaler and timer clock source.
Check-your-understanding questions
- What causes a factor-of-two error in timer frequency?
- How do you measure jitter in a timer-driven system?
- Why must you clear the update flag in the ISR?
Check-your-understanding answers
- Forgetting that timers may run at twice the APB clock when the prescaler is not 1.
- Toggle a GPIO in the ISR and measure period variation with a logic analyzer.
- If not cleared, the ISR may immediately retrigger or mask new interrupts.
Real-world applications
- LED sequencing and deterministic UI timing.
- Sensor sampling schedules with fixed intervals.
- Control loops in motors or power electronics.
Where you’ll apply it
- In this project: see Section 4.4 Algorithm Overview and Section 5.10 Implementation Phases.
- Also used in: P04 PWM Motor or LED Brightness Controller, P05 ADC Sensor Sampler + Logging.
References
- STM32F3 Reference Manual (timer chapter).
- Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (interrupts).
- Elecia White, ‘Making Embedded Systems’ (timing and scheduling).
Key insights
- Timers create deterministic time; interrupts make that time actionable without blocking the CPU.
Summary Timers and interrupts are the foundation of real-time embedded behavior. They replace blocking delays with precise schedules and make timing measurable.
Homework/Exercises to practice the concept
- Calculate prescaler and auto-reload for a 250 ms timer tick at 72 MHz.
- Toggle a GPIO on each timer interrupt and measure drift over 5 minutes.
- Experiment with different NVIC priorities and observe latency.
Solutions to the homework/exercises
- A 1 kHz tick requires prescaler 71999; a 250 ms interval uses auto-reload 249.
- Drift near zero indicates correct timer configuration; large drift indicates clock misconfig.
- Raising priority reduces latency but can starve lower-priority handlers.
Timebase, SysTick, and Measurement Discipline
Fundamentals A reliable timebase is how you reason about time in firmware. SysTick is a dedicated timer tied to the core clock that can generate periodic interrupts. If you configure it for 1 kHz, you get a millisecond tick that becomes your system heartbeat. This tick drives delays, scheduling, timeouts, and timestamps. But SysTick is only reliable if you know the core clock frequency and if you measure the actual timing. Without measurement, a ‘1 ms tick’ is just a guess. A bring-up project must therefore treat SysTick as a calibration point and validate it with real-world observation.
Deep Dive into the concept SysTick is a 24-bit down-counter integrated into the Cortex-M core. It takes the core clock (or core clock divided by 8) and counts down from a reload value to zero, then sets a flag and optionally fires an interrupt. Because it is in the core, it is unaffected by peripheral bus prescalers, which makes it a good reference for measuring the system clock. To configure SysTick, you load the reload register with (core_clock / desired_tick) - 1, select the clock source, enable the counter, and enable its interrupt. The interrupt handler typically increments a global tick counter. On STM32, HAL_Delay and other drivers are often built on this tick. The main danger is implicit coupling: if SysTick is configured incorrectly, every delay and timeout in your system is wrong. Another hazard is jitter. SysTick interrupts can be delayed by higher-priority interrupts, which means the tick is not a precise real-time clock, but a best-effort scheduler. For measurement, you should use SysTick only as a reference and then validate it with a GPIO toggle or timer output. A robust timebase audit logs the computed reload value, the configured clock source, and the observed period. The audit should also include a drift test: toggle a pin every N ticks for several minutes and measure drift relative to a stopwatch or logic analyzer. If drift is observed, the root cause is often clock source accuracy (HSI vs HSE) or an incorrect reload value due to a wrong core clock assumption. You also need to consider wraparound. A 24-bit counter at 72 MHz will wrap quickly if used without an interrupt, and a 32-bit tick counter will wrap after ~49 days at 1 kHz. In embedded systems, you typically handle wraparound by using unsigned arithmetic and comparing time differences rather than absolute values. Finally, you must decide what ‘good enough’ measurement means. For LED blinking, 1-2% accuracy is acceptable. For UART baud or ADC sampling, you need tighter tolerances. The discipline is to tie every time-dependent feature to a measured reference rather than an assumption.
How this fit on projects In ADC Sensor Sampler + Logging, SysTick is your baseline for clock validation. You compute the reload value, log it, and then validate the resulting tick with physical measurement.
Definitions & key terms
- SysTick -> Core-integrated timer used for periodic interrupts and timekeeping.
- Reload value -> The count value loaded into SysTick before it starts counting down.
- Tick -> A periodic time event, commonly 1 ms in embedded systems.
- Jitter -> Variation in the timing of periodic events due to interrupt latency.
- Drift -> Long-term timing error relative to a reference clock.
Mental model diagram (ASCII)
Core clock -> SysTick down-counter -> interrupt -> tick++
|
v
timing reference
How it works (step-by-step, with invariants and failure modes)
- Compute reload value based on expected core clock and desired tick rate.
- Configure SysTick to use core clock and enable interrupt.
- Increment a global tick counter in the SysTick handler.
- Toggle a GPIO every N ticks and measure the period.
- Invariant: tick increments at configured frequency; failure mode: drift or jitter due to wrong clock or interrupt priority.
Minimal concrete example
void SysTick_Handler(void) {
g_tick_ms++;
}
void delay_ms(uint32_t ms) {
uint32_t start = g_tick_ms;
while ((uint32_t)(g_tick_ms - start) < ms) {
__WFI();
}
}
Common misconceptions
- SysTick gives precise real-time scheduling ignores interrupt latency and jitter.
- If LED blinks, tick is correct ignores the need to measure drift over time.
- Tick counters never overflow ignores wraparound in long-running systems.
Check-your-understanding questions
- Why might SysTick interrupts be delayed even if configured correctly?
- How do you compute reload for 1 kHz at 72 MHz?
- What is the safest way to compare timeouts with wraparound?
Check-your-understanding answers
- Higher-priority interrupts can preempt SysTick, causing jitter.
- Reload = (72,000,000 / 1,000) - 1 = 71,999.
- Use unsigned subtraction: if (now - start) >= timeout.
Real-world applications
- Scheduling periodic sensor sampling.
- Timeouts in communication protocols.
- Measuring CPU load and real-time performance.
Where you’ll apply it
- In this project: see Section 3.4 Example Usage and Section 6.2 Critical Test Cases.
- Also used in: P03 Timer-Driven LED Sequencer, P07 Interrupt Latency Profiler.
References
- ARM Cortex-M4 Technical Reference Manual (SysTick).
- Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (timers and exceptions).
- Elecia White, ‘Making Embedded Systems’ (timing discipline).
Key insights
- A tick is only trustworthy when you can measure it and bound its jitter and drift.
Summary SysTick provides a convenient timebase, but it is only as accurate as your clock configuration and interrupt discipline. Treat it as a calibrated instrument, not a magic delay source.
Homework/Exercises to practice the concept
- Compute the reload value for 2 kHz and verify it in code.
- Measure the drift of a 1 Hz LED blink over 10 minutes.
- Experiment with interrupt priorities and observe SysTick jitter.
Solutions to the homework/exercises
- Reload for 2 kHz at 72 MHz is 35,999.
- A 1 Hz blink should be 600 seconds over 10 minutes; log the error and compute percent drift.
- Setting a higher-priority timer interrupt increases SysTick jitter; adjust priorities if needed.
3. Project Specification
3.1 What You Will Build
A sampling pipeline that captures analog sensor data at a fixed rate, timestamps each sample, and logs min/mean/max statistics.
3.2 Functional Requirements
- Fixed Sample Rate: Sample at a configurable rate (e.g., 100 Hz) driven by a timer.
- Voltage Conversion: Convert raw ADC codes to volts using Vref.
- Logging: Log timestamped samples and summary statistics.
- Calibration Support: Support offset/scale correction for a known sensor.
3.3 Non-Functional Requirements
- Performance: Sampling jitter <1 ms at 100 Hz.
- Reliability: No missed samples over 5-minute run.
- Usability: Log format parsable by a simple script.
3.4 Example Usage / Output
Sample rate: 100 Hz
t=0.00s raw=1834 volts=1.48
t=0.01s raw=1840 volts=1.48
Min=1810 Max=1860 Mean=1838
3.5 Data Formats / Schemas / Protocols
Sample log format: t=<sec> raw=<int> volts=<float>
Summary format: Min=<int> Max=<int> Mean=<int>
3.6 Edge Cases
- Floating ADC input causing noisy readings.
- Incorrect Vref leading to wrong voltage conversion.
- UART logging inside ISR causing sampling jitter.
- Timer misconfiguration causing wrong sample rate.
3.7 Real World Outcome
You will see stable, timestamped sensor logs with correct voltage conversion.
3.7.1 How to Run (Copy/Paste)
$ make flash
$ screen /dev/tty.usbmodem* 115200
3.7.2 Golden Path Demo (Deterministic)
- Use a potentiometer set to mid-scale and log 100 samples.
3.7.3 CLI Transcript (Success)
Sample rate: 100 Hz
t=0.00s raw=2048 volts=1.65
t=0.01s raw=2049 volts=1.65
Min=2040 Max=2055 Mean=2048
RESULT=PASS
# Exit code: 0
3.7.4 Failure Demo (Wrong Vref)
volts=2.10 (expected 1.65)
RESULT=FAIL
# Exit code: 2
4. Solution Architecture
A timer triggers ADC conversions; samples are converted and logged outside the ISR.
4.1 High-Level Design
Timer Trigger -> ADC -> Sample Buffer -> Logger -> UART
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Timer Trigger | Provides fixed sampling interval | Use TIMx trigger output |
| ADC Driver | Starts conversions and reads raw data | Single-channel, 12-bit mode |
| Logger | Timestamps and prints samples | Log in main loop to reduce ISR time |
4.3 Data Structures (No Full Code)
typedef struct {
uint32_t t_ms;
uint16_t raw;
float volts;
} sample_t;
4.4 Algorithm Overview
Sampling Loop
- Timer triggers ADC conversion.
- ISR stores raw sample in buffer.
- Main loop converts and logs samples.
Complexity: O(1) per sample.
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 trust the data coming from an analog sensor?”
5.4 Concepts You Must Understand First
- Sampling rate and aliasing.
- ADC resolution and Vref.
- Timer-triggered sampling.
5.5 Questions to Guide Your Design
- What sample rate is needed for your sensor?
- How will you avoid jitter from logging?
- What is your calibration method?
5.6 Thinking Exercise
Sampling Rate Selection
Signal changes ~1 Hz -> sample at 20-50 Hz
Signal changes ~10 Hz -> sample at 200-500 Hz
5.7 The Interview Questions They’ll Ask
- Explain aliasing in ADC sampling.
- How do you choose a sample rate?
- Why does ADC resolution matter?
5.8 Hints in Layers
Hint 1: Start with a low sample rate to validate timing. Hint 2: Trigger ADC with a timer, not in a loop. Hint 3: Log from main loop to avoid ISR jitter.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Sampling theory | Making Embedded Systems | Ch. 7 |
| C numeric accuracy | Effective C | Ch. 6 |
5.10 Implementation Phases
Phase 1: ADC Bring-Up (2 days)
Read a single channel and print raw values.
Phase 2: Timer Trigger (3-4 days)
Move to timer-triggered conversions.
Phase 3: Logging and Calibration (4 days)
Convert to volts, log statistics, and calibrate.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Trigger source | Timer vs software | Timer | Stable sampling interval |
| Logging | ISR vs main | Main loop | Lower jitter |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Voltage conversion math | Raw to volts checks |
| Integration Tests | Sampling at fixed rate | Timestamp intervals |
| Edge Case Tests | Floating input | Noisy readings |
6.2 Critical Test Cases
- Sample Rate: Average interval matches target rate.
- Voltage Conversion: Known input maps to expected volts.
- Logging Stability: No missing samples over 5 minutes.
6.3 Test Data
Vref=3.3, raw=2048 -> volts=1.65
Sample interval mean=10 ms
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Sampling in main loop | Jittery intervals | Use timer trigger |
| Bad Vref | Wrong voltage conversion | Measure Vref or use Vrefint |
| Logging in ISR | Missed samples | Buffer samples and log later |
7.2 Debugging Strategies
- Toggle a GPIO on each sample to validate interval.
- Compare raw readings with a known voltage source.
- Disable logging to see if jitter disappears.
7.3 Performance Traps
UART logging at high sample rates can overwhelm the system.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a moving average filter for noise reduction.
8.2 Intermediate Extensions
- Implement min/mean/max over rolling windows.
8.3 Advanced Extensions
- Stream samples via DMA to a host PC.
9. Real-World Connections
9.1 Industry Applications
- Instrumentation: Accurate sensor sampling in test equipment.
- Industrial monitoring: Periodic analog sensor logging.
9.2 Related Open Source Projects
- libopencm3 ADC examples: Reference ADC setup patterns.
- OpenOCD: Debugging and measurement support.
9.3 Interview Relevance
- ADC resolution and sampling theory questions.
- Timer-triggered sampling discussion.
10. Resources
10.1 Essential Reading
- Making Embedded Systems by Elecia White - Sampling and measurement.
- Effective C by Robert C. Seacord - Numerical accuracy in C.
10.2 Video Resources
- ADC sampling and aliasing tutorial.
- STM32 ADC configuration walkthrough.
10.3 Tools & Documentation
- STM32CubeIDE: Build and flash.
- Multimeter: Verify input voltage.
10.4 Related Projects in This Series
- P06 DAC Waveform Generator - complementary analog output project.
- P08 DMA-Based ADC Ring Buffer - high-throughput extension.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain sampling rate and aliasing.
- I can convert raw ADC codes to volts.
- I understand how timer triggering removes jitter.
11.2 Implementation
- Sampling rate measured and accurate.
- Log format is consistent and parseable.
- Calibration applied correctly.
11.3 Growth
- I can design sampling plans for new sensors.
- I can explain noise sources in ADC readings.
- I can extend to DMA streaming.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Timer-triggered sampling at fixed rate.
- Raw to volts conversion works.
- Logs produced.
Full Completion:
- Calibration and statistics implemented.
- Jitter measured and documented.
Excellence (Going Above & Beyond):
- DMA-based streaming or advanced filtering added.