Project 6: DAC Waveform Generator
A timer-triggered DAC waveform generator that outputs triangle or sine-like signals with verified frequency.
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 4: Hardcore Tech Flex |
| Business Potential | 1. The “Resume Gold” |
| Prerequisites | Timer triggers, DAC basics, lookup tables |
| Key Topics | DAC, waveform synthesis, timer triggering |
1. Learning Objectives
By completing this project, you will:
- Configure DAC output and buffer settings.
- Build a waveform lookup table and schedule updates.
- Compute output frequency from update rate and sample count.
- Validate waveform using ADC or external tools.
2. All Theory Needed (Per-Concept Breakdown)
DAC Output and Waveform Synthesis
Fundamentals A DAC converts digital values into analog voltages. On STM32F3, the DAC has fixed resolution and output buffer settings that affect drive strength and noise. To generate a waveform, you output a sequence of values at a fixed update rate. The update rate and the number of samples per cycle determine the output frequency. Understanding timing and lookup tables is essential for clean, predictable analog signals.
Deep Dive into the concept The DAC is the inverse of the ADC. It takes a digital code and outputs a corresponding voltage based on a reference. The STM32F3 DAC supports 12-bit resolution and can be triggered by timers, which is the preferred method for stable waveforms. A waveform generator typically uses a lookup table (LUT) containing samples of a sine, triangle, or sawtooth wave. A timer triggers the DAC at a fixed rate, stepping through the table. The output frequency is f_out = f_update / N, where N is the number of samples per cycle. If you want a 400 Hz waveform with 64 samples, you need a 25.6 kHz update rate. The DAC output is a stepped waveform; analog filtering (a simple RC low-pass) smooths it. The reconstruction filter’s cutoff must be above the waveform frequency but below the update frequency to reduce stair-step artifacts. Timing jitter in the update rate results in phase noise, so timer-triggered updates are critical; software loops introduce unacceptable jitter. You must also consider output drive: the DAC may not be able to drive low-impedance loads directly, so buffering might be required for external circuits. On the STM32F3DISCOVERY, you can validate output by measuring with a scope or, if unavailable, by sampling the DAC output with the ADC and logging it. Although this reduces analog fidelity, it provides a workable validation method. When building a waveform generator, you will generate the LUT, configure the timer trigger, and verify the frequency and amplitude. This project also teaches you the broader concept of discrete-time synthesis: a continuous waveform is approximated by discrete samples, and your update rate and table resolution define the fidelity.
How this fit on projects In DAC Waveform Generator, you create a stable waveform using a timer-triggered DAC and validate its frequency and amplitude.
Definitions & key terms
- LUT -> Lookup table containing waveform samples.
- Update rate -> Frequency at which DAC values are updated.
- Reconstruction filter -> Low-pass filter used to smooth stepped DAC output.
- Settling time -> Time required for DAC output to reach final value after update.
- Amplitude -> Peak-to-peak voltage of the generated waveform.
Mental model diagram (ASCII)
Timer -> DAC update -> stepped waveform -> RC filter -> smooth waveform
How it works (step-by-step, with invariants and failure modes)
- Generate a waveform lookup table (sine or triangle).
- Configure a timer to trigger DAC updates at a fixed rate.
- On each trigger, output the next sample from the table.
- Measure output frequency and amplitude; adjust update rate as needed.
- Invariant: update interval is constant; failure mode: jitter if updates are software-timed.
Minimal concrete example
// Triangle wave LUT (0-4095)
uint16_t lut[64] = {0,128,256, ... ,4095, ... ,128};
DAC->DHR12R1 = lut[index++ & 63];
Common misconceptions
- More samples always means better output ignores update rate limits.
- DAC output is smooth by default ignores the stepped nature of samples.
- Software loops are fine for DAC timing ignores jitter and CPU load.
Check-your-understanding questions
- How do you compute output frequency from update rate and LUT length?
- Why is a timer trigger better than a delay loop?
- What does an RC filter do to the DAC output?
Check-your-understanding answers
- f_out = f_update / N, where N is samples per cycle.
- Timers provide consistent timing with minimal jitter, unlike software loops.
- It smooths high-frequency steps and reduces staircase artifacts.
Real-world applications
- Signal generators for lab testing.
- Audio tone generation.
- Control systems requiring analog reference signals.
Where you’ll apply it
- In this project: see Section 4.4 Algorithm Overview and Section 5.10 Implementation Phases.
- Also used in: P05 ADC Sensor Sampler + Logging (for validation loops).
References
- STM32F3 Reference Manual (DAC chapter).
- ‘The Scientist and Engineer’s Guide to DSP’ (sampling and reconstruction).
- ST application notes on DAC usage.
Key insights
- A DAC waveform is just timed samples; fidelity depends on timing precision and filtering.
Summary Waveform generation combines digital timing and analog reconstruction. By using timer-triggered updates and validated lookup tables, you can build stable analog outputs.
Homework/Exercises to practice the concept
- Compute update rate for a 1 kHz sine wave with 128 samples per cycle.
- Design a simple RC filter for a 400 Hz waveform with a 25 kHz update rate.
- Compare triangle vs sine LUTs and note harmonic differences.
Solutions to the homework/exercises
- Update rate = 1 kHz * 128 = 128 kHz.
- Choose RC with cutoff around 2-3 kHz; e.g., 1 kOhm and 68 nF gives ~2.3 kHz.
- Triangle waves have stronger odd harmonics; sine is smoother with fewer harmonics.
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 DAC Waveform Generator, 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.
Clock Tree, PLL, and Prescalers (System Timing Backbone)
Fundamentals Every peripheral timing and every delay in your firmware depends on the clock tree. The STM32F3 can run from internal or external oscillators, and uses a PLL to multiply those sources to reach the desired system clock. From that system clock, prescalers divide down to generate the AHB, APB1, and APB2 bus clocks, which in turn feed timers, ADCs, UARTs, and other peripherals. If you misconfigure any divider, you will see subtle failures: UART baud errors, PWM drift, incorrect ADC sampling, and watchdog timeouts. A clock tree audit is the practice of proving, with measurements, that your configured frequencies match reality. It is the single most valuable habit for embedded reliability.
Deep Dive into the concept The STM32F3 clock system is a directed graph of sources, multipliers, and dividers. At the root are the HSI (internal RC oscillator) and HSE (external crystal or clock). The PLL takes one of those sources and applies multiplication factors to produce a higher-frequency output used as the system clock. On STM32 devices, the system clock (SYSCLK) feeds the AHB bus, while APB1 and APB2 buses are derived by additional prescalers. Timers on APB buses often have a ‘x2’ effect when the prescaler is not 1, which means timer clocks can be double the APB frequency. If you forget that rule, PWM frequencies are off by exactly 2x, a classic bring-up trap. The clock tree is also linked to power: higher frequencies increase current draw, and some peripherals require specific clock ranges. USB and ADCs, for example, require precise clocks to meet protocol or sampling accuracy requirements. The RCC (Reset and Clock Control) registers configure all of this. A good audit reads those registers at runtime and recomputes derived clocks, then compares expected timing (based on configuration) to observed timing (based on measurement). Measurement can be as simple as toggling a GPIO at a known rate and timing it with a stopwatch, or as precise as using a logic analyzer to measure a timer output. A thorough audit includes the SysTick frequency, timer tick frequency, and peripheral baud rate verification. There are also failure modes: the external crystal may not start, the PLL may fail to lock, or the clock security system may switch back to HSI without warning. In robust firmware, you detect and log these events. The clock tree is therefore not just a setup routine, but an operational dependency that must be measured. When you develop on the STM32F3DISCOVERY, you should record expected frequencies, read RCC_CFGR to confirm clock sources, and validate at least one timer output. This practice makes you immune to the most common cause of ‘mystery bugs’ in embedded projects: silent timing errors.
How this fit on projects In DAC Waveform Generator, you audit the clock tree by reading RCC configuration, computing derived clocks, and validating time with measured outputs (LEDs, timers, or UART).
Definitions & key terms
- HSI -> Internal high-speed oscillator, convenient but less accurate.
- HSE -> External crystal oscillator, more accurate but requires hardware.
- PLL -> Phase-Locked Loop that multiplies an input frequency.
- SYSCLK -> System clock feeding the CPU core.
- AHB/APB -> Buses that distribute clocks to memory and peripherals.
Mental model diagram (ASCII)
HSI/HSE -> PLL -> SYSCLK -> AHB -> APB1/APB2 -> Peripherals
| | |
prescalers timers UART/ADC
How it works (step-by-step, with invariants and failure modes)
- Select a clock source (HSI or HSE) and enable it.
- Configure PLL multipliers/dividers to reach target SYSCLK.
- Set AHB/APB prescalers to keep peripherals within spec.
- Validate that timers and UARTs use the expected derived clocks.
- Invariant: All configured clocks must be within device limits; failure mode: peripheral timing mismatch or PLL unlock.
Minimal concrete example
// Pseudocode: compute SYSCLK from RCC registers
uint32_t sysclk = rcc_get_sysclk();
uint32_t hclk = sysclk / ahb_prescaler();
uint32_t pclk1 = hclk / apb1_prescaler();
printf("SYSCLK=%lu HCLK=%lu PCLK1=%lu\n", sysclk, hclk, pclk1);
Common misconceptions
- If SYSCLK is correct, peripherals are correct ignores bus prescalers and timer x2 behavior.
- HSI is good enough for all peripherals ignores baud accuracy and ADC sampling constraints.
- PLL always locks ignores startup and clock security failures.
Check-your-understanding questions
- Why can a timer run at twice the APB clock on STM32?
- How would you detect that the HSE crystal failed to start?
- What is the difference between SYSCLK and HCLK?
- Why does UART baud depend on clock prescalers?
Check-your-understanding answers
- When APB prescaler is not 1, timer clocks are doubled to preserve timer resolution.
- Check RCC flags for HSE ready or clock security events and log a fallback to HSI.
- SYSCLK feeds the core; HCLK is SYSCLK after the AHB prescaler.
- UART divider is computed from the peripheral clock; a mismatch shifts baud rate.
Real-world applications
- Motor control where PWM frequency accuracy matters.
- Precision sensing with ADC sampling rate requirements.
- Communication interfaces that need accurate baud rates.
Where you’ll apply it
- In this project: see Section 3.4 Example Usage, Section 5.4 Concepts You Must Understand First.
- Also used in: P03 Timer-Driven LED Sequencer, P04 PWM Motor or LED Brightness Controller.
References
- STM32F3 Reference Manual (RCC chapter).
- Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (system clocking).
- Elecia White, ‘Making Embedded Systems’ (timing and measurement).
Key insights
- If you cannot measure your clock tree, you cannot trust any timing claim in your system.
Summary Clock configuration is a system-wide contract. A small prescaler mistake can propagate into every peripheral. A clock audit makes timing visible and protects you from silent configuration errors.
Homework/Exercises to practice the concept
- Compute expected timer tick rates for APB1 and APB2 given a target SYSCLK.
- Toggle a GPIO every 1000 SysTick ticks and measure the period with a stopwatch.
- Force a fallback to HSI and log the resulting SYSCLK change.
Solutions to the homework/exercises
- Use the RCC_CFGR register to determine prescalers and apply the x2 rule for timers.
- 1,000 ticks at 1 kHz should be 1 second; if not, check SysTick reload value.
- When HSE fails, the system clock source bit will indicate HSI; log the event over UART.
3. Project Specification
3.1 What You Will Build
A waveform generator that outputs a stable triangle or sine waveform using a DAC and timer trigger, with frequency verification.
3.2 Functional Requirements
- Waveform LUT: Generate at least one waveform table (triangle or sine).
- Timer Trigger: Use a hardware timer to update DAC at fixed rate.
- Frequency Control: Allow selection of output frequency within a defined range.
- Validation: Measure output frequency and amplitude.
3.3 Non-Functional Requirements
- Performance: Update jitter <1% of sample period.
- Reliability: Continuous output for 10 minutes without glitches.
- Usability: Waveform selection via UART command.
3.4 Example Usage / Output
Waveform: triangle
Frequency: 400 Hz
Samples per cycle: 64
DAC resolution: 12-bit
3.5 Data Formats / Schemas / Protocols
Control commands: WAVE=TRI|SINE, FREQ_HZ=<int>
Log format: WAVE=<name> FREQ=<int> SAMPLES=<int>
3.6 Edge Cases
- Update rate too low causing visible stair-steps.
- DAC output buffer disabled leading to weak output.
- Timer misconfigured causing wrong frequency.
- LUT index overflow causing discontinuity.
3.7 Real World Outcome
You will produce a stable analog waveform and verify its frequency.
3.7.1 How to Run (Copy/Paste)
$ make flash
$ screen /dev/tty.usbmodem* 115200
3.7.2 Golden Path Demo (Deterministic)
- Select triangle waveform at 400 Hz with 64 samples.
3.7.3 CLI Transcript (Success)
WAVE=TRI FREQ=400 SAMPLES=64
RESULT=PASS
# Exit code: 0
3.7.4 Failure Demo (Software Timing)
- Replace timer trigger with delay loop.
FREQ=400 (measured 320)
RESULT=FAIL
# Exit code: 2
4. Solution Architecture
A timer triggers DAC updates from a waveform lookup table to create a stable analog output.
4.1 High-Level Design
Timer -> DAC Update -> LUT Index -> Analog Output
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| LUT Generator | Creates waveform sample table | Fixed-size table for simplicity |
| Timer Trigger | Schedules DAC updates | Use hardware timer for low jitter |
| DAC Driver | Outputs samples to DAC register | Enable output buffer for stability |
4.3 Data Structures (No Full Code)
typedef struct {
uint16_t* table;
uint16_t length;
uint16_t index;
} wavetable_t;
4.4 Algorithm Overview
Waveform Output
- Timer event triggers DAC update.
- Output table[index] to DAC.
- Increment index with wrap.
Complexity: O(1) per update.
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 discrete samples become a smooth analog waveform?”
5.4 Concepts You Must Understand First
- DAC resolution and output buffering.
- Timer-triggered updates.
- Waveform synthesis with lookup tables.
5.5 Questions to Guide Your Design
- How many samples per cycle do you need?
- What update rate is required for your target frequency?
- How will you validate output without a scope?
5.6 Thinking Exercise
Frequency Calculation
F_out = F_update / N_samples
F_update = 25.6 kHz, N=64 -> F_out=400 Hz
5.7 The Interview Questions They’ll Ask
- How do you generate a waveform with a DAC?
- What determines the output frequency?
- Why is timer triggering preferred?
5.8 Hints in Layers
Hint 1: Start with a triangle wave (easy to generate). Hint 2: Use a timer trigger, not a delay loop. Hint 3: Validate with ADC sampling if you lack a scope.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| DSP basics | Scientist and Engineer’s Guide to DSP | Ch. 3 |
| Embedded timing | Making Embedded Systems | Ch. 4 |
5.10 Implementation Phases
Phase 1: DAC Bring-Up (2 days)
Output a static mid-scale voltage.
Phase 2: LUT Output (4 days)
Output a triangle wave with timer triggers.
Phase 3: Validation (3 days)
Measure frequency and amplitude, refine LUT.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| LUT size | 32 vs 64 vs 128 | 64 | Balance resolution and update rate |
| Trigger | Timer vs loop | Timer | Stable update interval |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | LUT values within range | Check 0-4095 bounds |
| Integration Tests | Output frequency accuracy | Measure via ADC |
| Edge Case Tests | Index wrap | No discontinuities |
6.2 Critical Test Cases
- Frequency: Measured frequency within 2% of target.
- Amplitude: DAC output covers expected range.
- Continuity: No jumps between last and first sample.
6.3 Test Data
LUT min=0 max=4095
Measured F_out=401 Hz
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Software timing | Jittery waveform | Use timer trigger |
| Output buffer disabled | Low amplitude | Enable DAC buffer |
| Wrong update rate | Wrong frequency | Recompute timer settings |
7.2 Debugging Strategies
- Sample DAC output with ADC to validate waveform shape.
- Use a GPIO toggle on each update for timing checks.
- Verify LUT values with a host-side script.
7.3 Performance Traps
Too many samples per cycle can exceed timer/DAC update limits.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add square wave output mode.
8.2 Intermediate Extensions
- Add adjustable amplitude and offset.
8.3 Advanced Extensions
- Implement DMA-driven DAC updates for higher rates.
9. Real-World Connections
9.1 Industry Applications
- Signal generators: Test waveforms for lab equipment.
- Control systems: Analog reference signals for converters.
9.2 Related Open Source Projects
- libopencm3 DAC examples: DAC configuration references.
- DDS projects: Direct digital synthesis patterns.
9.3 Interview Relevance
- Waveform synthesis and timer triggering concepts.
- Trade-offs in sample rate and resolution.
10. Resources
10.1 Essential Reading
- Scientist and Engineer’s Guide to DSP by Steven W. Smith - Sampling and reconstruction basics.
- Making Embedded Systems by Elecia White - Timing and peripheral integration.
10.2 Video Resources
- STM32 DAC waveform generation tutorial.
- Digital synthesis basics.
10.3 Tools & Documentation
- Oscilloscope or ADC loopback: Measure waveform output.
- STM32CubeIDE: Build and flash firmware.
10.4 Related Projects in This Series
- P05 ADC Sensor Sampler + Logging - use ADC for validation.
- P08 DMA-Based ADC Ring Buffer - DMA concepts for high-rate streams.
11. Self-Assessment Checklist
11.1 Understanding
- I can compute output frequency from update rate and samples.
- I understand DAC output buffering.
- I can explain sampling-based waveform synthesis.
11.2 Implementation
- Waveform output is stable and measurable.
- Timer trigger controls update timing.
- LUT values are correct and bounded.
11.3 Growth
- I can add new waveform types.
- I can validate output without a scope.
- I can explain DAC limitations in interviews.
12. Submission / Completion Criteria
Minimum Viable Completion:
- DAC outputs a repeating waveform.
- Frequency is measurable.
- Logs show configuration.
Full Completion:
- Multiple waveforms supported.
- Frequency accuracy documented.
Excellence (Going Above & Beyond):
- DMA-driven output and filtering implemented.