Project 10: SPI Display or LED Matrix Driver

An SPI-driven display or LED matrix driver with a frame buffer and deterministic refresh.

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 SPI basics, GPIO AF mapping, timers
Key Topics SPI protocol, frame buffers, refresh timing

1. Learning Objectives

By completing this project, you will:

  1. Configure SPI with correct mode and clock.
  2. Build a frame buffer and push it over SPI.
  3. Achieve a stable refresh rate without flicker.
  4. Validate SPI timing and display output.

2. All Theory Needed (Per-Concept Breakdown)

SPI Protocol, Throughput, and Display Driving

Fundamentals SPI is a high-speed, full-duplex serial protocol using separate lines for clock (SCK), data out (MOSI), data in (MISO), and chip select (CS). Unlike I2C, SPI has no addressing; each device uses a dedicated CS line. SPI is often used for displays and LED matrices because it can move data quickly. Understanding clock polarity/phase and data framing is essential.

Deep Dive into the concept SPI operates by shifting data on each clock edge, with four standard modes defined by clock polarity (CPOL) and clock phase (CPHA). A mismatch between MCU and device modes results in corrupted data. Because SPI is full-duplex, data is transmitted and received simultaneously, but many devices ignore MISO during write-only transfers. Throughput is determined by SPI clock frequency and word size. If you are driving a display or LED matrix, you must compute the required bandwidth: how many bytes per frame times frames per second. For example, an 8x8 RGB matrix may require 192 bytes per frame; at 100 frames per second, that is 19.2 kB/s, which is trivial for SPI. But a larger display or higher refresh rate may push SPI bandwidth limits. In STM32, SPI can be driven by DMA for consistent throughput. For display drivers, you often build a frame buffer in RAM, then push it over SPI in blocks. For LED matrices, you may need row scanning: activate a row, send column data, then advance. Timing matters: if row scanning is too slow, flicker appears. This project focuses on building a stable SPI driver and a predictable refresh loop. It also teaches you to build a simple command protocol over SPI, which is how many displays are configured (command vs data mode). Reliable SPI requires disciplined chip select handling: assert CS, transfer bytes, deassert CS. Many devices require minimum CS timing between commands. Finally, verification can be done with logic analyzers or by visual confirmation (display output or LED matrix patterns). This is a great example of turning a high-level visual output into low-level timing and protocol work.

How this fit on projects In SPI Display or LED Matrix Driver, you build an SPI driver and use it to push deterministic pixel or LED patterns at a stable refresh rate.

Definitions & key terms

  • CPOL/CPHA -> Clock polarity and phase defining SPI mode.
  • Chip Select -> Device-specific line that enables SPI communication.
  • Frame buffer -> Memory area storing pixel or LED states for output.
  • Throughput -> Amount of data transferred per second.
  • Row scanning -> Technique of refreshing one row at a time in matrix displays.

Mental model diagram (ASCII)

MCU SPI -> MOSI/SCK/CS -> Display
FrameBuffer -> DMA/SPI -> Visible Pattern

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

  1. Select SPI mode and clock frequency compatible with the device.
  2. Configure GPIO pins for SPI alternate function and CS as GPIO.
  3. Build a frame buffer or pattern generator in RAM.
  4. Transmit data with correct CS timing and optional command/data separation.
  5. Invariant: data bytes are clocked with correct mode; failure mode: wrong CPOL/CPHA or CS timing.

Minimal concrete example

// Send command then data
cs_low();
spi_write(CMD_SET_PAGE);
cs_high();
cs_low();
spi_write_buffer(framebuffer, FB_LEN);
cs_high();

Common misconceptions

  • SPI is simpler so it always works ignores mode mismatches and CS timing.
  • Display flicker means hardware failure’ often means refresh loop timing issues.
  • Any SPI clock is fine ignores device maximum clock specs.

Check-your-understanding questions

  1. Why does SPI need a separate chip select per device?
  2. How do you calculate required SPI bandwidth for a display?
  3. What happens if CPOL/CPHA is wrong?

Check-your-understanding answers

  1. SPI lacks addressing; CS selects which device listens.
  2. Bandwidth = bytes per frame * frames per second; compare to SPI clock capacity.
  3. Data is sampled on the wrong edges, resulting in bit shifts or corruption.

Real-world applications

  • OLED/LCD displays and LED matrices.
  • High-speed sensors such as ADCs and IMUs.
  • Flash memory devices.

Where you’ll apply it

References

  • SPI specification (Motorola/industry standard).
  • STM32F3 Reference Manual (SPI chapter).
  • Display or LED matrix datasheet for protocol timing.

Key insights

  • SPI bandwidth is easy to compute; reliable display output comes from disciplined timing and buffering.

Summary SPI is a high-speed protocol ideal for displays. By combining correct mode settings, CS timing, and buffering, you can achieve stable visual output without flicker.

Homework/Exercises to practice the concept

  1. Compute SPI clock needed to refresh a 128x64 monochrome display at 60 fps.
  2. Send a checkerboard pattern and verify the output visually.
  3. Experiment with CPOL/CPHA mismatches and observe output corruption.

Solutions to the homework/exercises

  1. 128x64 = 8192 pixels = 1024 bytes; at 60 fps that’s 61.4 kB/s; a 1 MHz SPI clock is sufficient.
  2. A checkerboard pattern alternates bits; use it to verify correct bit ordering.
  3. Wrong CPOL/CPHA typically shifts data by one bit or swaps edges.

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 SPI Display or LED Matrix Driver, 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.

PWM Generation and Output Compare

Fundamentals Pulse Width Modulation (PWM) is a way to create an analog-like output using digital timers. A timer counts up to a period value, and an output compare register defines when the output switches from high to low. The ratio of high time to total period is the duty cycle, which controls average power delivered to a load. PWM is used for LED brightness, motor speed, and power control. Understanding how to compute frequency and duty, and how resolution changes with frequency, is essential.

Deep Dive into the concept PWM on STM32 uses timer output compare units. The timer counts from 0 to ARR (auto-reload), and when the counter matches the CCR value, the output toggles or changes level depending on the mode. In PWM mode 1, the output is high when the counter is below CCR and low otherwise. Duty cycle equals CCR/ARR. Frequency is determined by the timer clock and ARR, so you choose ARR to meet a frequency target and then compute CCR for duty. There is a trade-off: higher frequency requires a smaller ARR, which reduces duty resolution. For LEDs, frequencies above ~200 Hz avoid flicker, while for motors you may target higher frequencies to reduce audible noise. STM32 timers can also operate in center-aligned mode, which reduces harmonic content by toggling at both up and down counts, useful for motor control. In practice, you must consider the load. LEDs are current-driven devices; PWM controls perceived brightness, but current-limiting resistors and LED forward voltage determine actual current. Motors are inductive and respond to average voltage, but at low PWM frequencies you may get torque ripple. You also need to verify the waveform. A logic analyzer or scope can confirm frequency and duty, but you can also measure average voltage with a multimeter as a rough check. Another subtlety is timer channel remapping and pin configuration: the GPIO pin must be set to the correct alternate function, and the timer output must be enabled in the CCER register. If you forget either step, you may see no output despite correct timer math. PWM is thus a perfect example of hardware-software integration: you compute the numbers in firmware, but the output is only real if the pin is configured and the load is connected correctly.

How this fit on projects In SPI Display or LED Matrix Driver, you generate PWM on a timer channel and map duty cycle steps to visible brightness or speed changes.

Definitions & key terms

  • Duty cycle -> Percentage of time the signal is high within one period.
  • ARR -> Auto-reload register that sets PWM period.
  • CCR -> Capture/compare register that sets duty cycle.
  • Center-aligned -> PWM mode that counts up and down for symmetrical waveforms.
  • Resolution -> Number of distinct duty steps, equal to ARR+1.

Mental model diagram (ASCII)

Counter: 0 .... CCR .... ARR
PWM:    ____|''''''''|________ (PWM mode 1)

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

  1. Choose a timer and target PWM frequency.
  2. Compute ARR from timer clock to hit the desired frequency.
  3. Compute CCR values for desired duty cycle steps.
  4. Configure the pin for the correct timer alternate function.
  5. Invariant: duty cycle equals CCR/ARR; failure mode: wrong AF or CCER disables output.

Minimal concrete example

// 1 kHz PWM, 50% duty
TIM1->PSC = 71; // 72 MHz / 72 = 1 MHz
TIM1->ARR = 1000 - 1;
TIM1->CCR1 = 500;

Common misconceptions

  • Duty cycle equals brightness linearly ignores human perception and LED current curves.
  • Higher frequency is always better ignores resolution and driver limits.
  • PWM output appears automatically ignores pin AF and CCER enable bits.

Check-your-understanding questions

  1. How does ARR affect PWM resolution?
  2. Why might a 50% duty cycle not look half as bright?
  3. What is the relationship between PWM frequency and motor audible noise?

Check-your-understanding answers

  1. ARR+1 defines the number of duty steps; smaller ARR means lower resolution.
  2. Perceived brightness is logarithmic, and LED current is nonlinear.
  3. Low PWM frequencies create torque ripple and audible whining; higher frequencies reduce it.

Real-world applications

  • LED dimming in consumer electronics.
  • DC motor control and fan speed regulation.
  • Power converters and battery charging systems.

Where you’ll apply it

References

  • STM32F3 Reference Manual (timer PWM modes).
  • Elecia White, ‘Making Embedded Systems’ (PWM and control).
  • ST application notes on motor control PWM.

Key insights

  • PWM is a precise digital mechanism that controls analog behavior through duty cycle and frequency.

Summary PWM bridges digital timers and analog behavior. By mastering timer configuration, duty mapping, and load effects, you can create stable brightness and speed control.

Homework/Exercises to practice the concept

  1. Compute ARR and CCR values for 500 Hz PWM with 10% duty at 72 MHz.
  2. Measure PWM duty with a multimeter and compare to expected average voltage.
  3. Try 200 Hz vs 1 kHz on an LED and describe perceived differences.

Solutions to the homework/exercises

  1. At 72 MHz, prescale to 1 MHz, ARR=2000-1 for 500 Hz, CCR=200 for 10%.
  2. Average voltage should be duty * Vcc; small errors are expected due to measurement limits.
  3. 200 Hz may show flicker, while 1 kHz appears steady to most observers.

3. Project Specification

3.1 What You Will Build

A driver that renders patterns on a small SPI display or LED matrix with a stable refresh loop and clear timing verification.

3.2 Functional Requirements

  1. SPI Driver: Configure SPI mode and transfer bytes reliably.
  2. Frame Buffer: Maintain a buffer for display or matrix state.
  3. Refresh Loop: Update display at a fixed rate (e.g., 60 Hz).
  4. Pattern Demo: Render at least two patterns (checkerboard, scrolling).

3.3 Non-Functional Requirements

  • Performance: Refresh rate stable within 5% with no visible flicker.
  • Reliability: No corrupted frames over 10 minutes.
  • Usability: Pattern selection via UART command or button.

3.4 Example Usage / Output

Display: 128x64
SPI mode: 0
Refresh: 60 Hz
Pattern: checkerboard

3.5 Data Formats / Schemas / Protocols

Frame buffer layout: row-major bytes, MSB first. Command format: PATTERN=<name>

3.6 Edge Cases

  • Wrong CPOL/CPHA causing scrambled pixels.
  • SPI clock too high causing display errors.
  • Refresh loop too slow causing flicker.
  • CS timing not respected.

3.7 Real World Outcome

You will see clean, stable patterns on the display or LED matrix.

3.7.1 How to Run (Copy/Paste)

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

3.7.2 Golden Path Demo (Deterministic)

  • Render checkerboard at 60 Hz for 5 minutes.

3.7.3 CLI Transcript (Success)

DISPLAY=128x64
SPI_MODE=0
REFRESH=60Hz
PATTERN=checkerboard
RESULT=PASS
# Exit code: 0

3.7.4 Failure Demo (Wrong SPI Mode)

SPI_MODE=3
OUTPUT=corrupted
RESULT=FAIL
# Exit code: 2

4. Solution Architecture

A frame buffer is rendered in RAM and pushed over SPI at a fixed refresh rate.

4.1 High-Level Design

Frame Buffer -> SPI Transfer -> Display -> Visible Pattern

4.2 Key Components

Component Responsibility Key Decisions
SPI Driver Transfers bytes with correct mode Use blocking transfers for simplicity
Frame Buffer Stores pixel/LED states Row-major layout
Refresh Scheduler Triggers updates at fixed rate Use timer or SysTick

4.3 Data Structures (No Full Code)

uint8_t framebuffer[1024]; // example for 128x64 mono

4.4 Algorithm Overview

Refresh Loop

  1. Update frame buffer for current pattern.
  2. Assert CS, send buffer via SPI.
  3. Deassert CS, wait for next refresh tick.

Complexity: O(N) per frame.


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 move enough data over SPI to drive a display without flicker?”

5.4 Concepts You Must Understand First

  1. SPI mode and timing.
  2. Frame buffer layout.
  3. Refresh rate vs bandwidth.

5.5 Questions to Guide Your Design

  1. What SPI clock ensures reliable display updates?
  2. How large is the frame buffer?
  3. How will you detect flicker or dropped frames?

5.6 Thinking Exercise

Bandwidth Calculation

128x64 mono -> 1024 bytes/frame
60 fps -> 61.4 kB/s
SPI clock > 500 kHz is sufficient

5.7 The Interview Questions They’ll Ask

  1. What is CPOL/CPHA and why does it matter?
  2. How do you compute SPI bandwidth requirements?
  3. How would you avoid display flicker?

5.8 Hints in Layers

Hint 1: Start with a small test pattern. Hint 2: Verify SPI mode with a logic analyzer. Hint 3: Keep refresh rate fixed and measure it.

5.9 Books That Will Help

Topic Book Chapter
SPI protocol Embedded Systems: Introduction SPI chapter
Display timing Display datasheet Timing section

5.10 Implementation Phases

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

Send a simple command and verify response.

Phase 2: Frame Buffer (4 days)

Render and send a full frame.

Phase 3: Refresh Optimization (3 days)

Fix refresh rate and test flicker.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Refresh rate 30 vs 60 Hz 60 Hz Avoid flicker
SPI clock 1 MHz vs 8 MHz 1-4 MHz Safe margin for most displays

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Frame buffer index math Pixel set/get
Integration Tests SPI transfers Pattern rendering
Edge Case Tests Wrong mode Corrupted output

6.2 Critical Test Cases

  1. Pattern Render: Checkerboard appears correctly.
  2. Refresh Rate: Measured refresh near target.
  3. SPI Mode: Correct CPOL/CPHA yields stable output.

6.3 Test Data

Frame bytes sent: 1024
Refresh period: 16.7 ms

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Wrong SPI mode Scrambled display Match CPOL/CPHA to datasheet
Slow refresh Flicker Increase SPI clock or reduce frame size
CS timing Commands ignored Respect CS setup/hold times

7.2 Debugging Strategies

  • Use a logic analyzer to confirm SPI timing.
  • Render known patterns to spot bit order errors.
  • Measure refresh period with a GPIO toggle.

7.3 Performance Traps

Large frame buffers can saturate SPI; compute bandwidth early.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a scrolling text pattern.

8.2 Intermediate Extensions

  • Add brightness control using PWM.

8.3 Advanced Extensions

  • Use DMA for SPI transfers.

9. Real-World Connections

9.1 Industry Applications

  • Embedded displays: UI and status displays in devices.
  • Signage: LED matrix drivers for signage.
  • u8g2: Display library with SPI backends.
  • Adafruit GFX: Graphics library for embedded displays.

9.3 Interview Relevance

  • SPI timing and throughput questions.
  • Frame buffer design discussions.

10. Resources

10.1 Essential Reading

  • SPI device datasheet by Vendor - Timing and protocol requirements.
  • Making Embedded Systems by Elecia White - Embedded communication patterns.

10.2 Video Resources

  • SPI display driver tutorial.
  • Logic analyzer SPI decoding demo.

10.3 Tools & Documentation

  • Logic analyzer: Verify SPI signals.
  • STM32CubeIDE: Build and flash.

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain CPOL/CPHA and SPI modes.
  • I can compute SPI bandwidth for a display.
  • I understand frame buffer layout.

11.2 Implementation

  • Display updates with correct patterns.
  • Refresh rate is stable.
  • No flicker or corrupted frames.

11.3 Growth

  • I can drive a different display with this framework.
  • I can optimize throughput when needed.
  • I can explain SPI timing in interviews.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • SPI output works.
  • At least one pattern displayed.
  • Refresh timing measured.

Full Completion:

  • Multiple patterns and stable refresh.
  • Bandwidth calculation documented.

Excellence (Going Above & Beyond):

  • DMA-based transfers and performance analysis.