Project 4: Logic Analyzer with PIO

Build a multi‑channel digital capture tool using PIO + DMA and stream traces to a host.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 1-2 weeks
Main Programming Language C + PIO assembly
Alternative Programming Languages Rust
Coolness Level Level 5: “I built my own Saleae”
Business Potential 3. Useful for hardware labs and debugging tools
Prerequisites PIO basics, DMA, buffer management, USB/UART streaming
Key Topics PIO sampling, DMA ring buffers, triggers, streaming

1. Learning Objectives

By completing this project, you will:

  1. Sample multiple GPIO channels at a fixed rate using PIO.
  2. Use DMA ring buffers to store high‑rate samples without CPU intervention.
  3. Implement a basic trigger system (edge/level) in PIO or CPU.
  4. Stream capture data to a host and export to a standard format.
  5. Detect and report buffer overruns deterministically.

2. All Theory Needed (Per-Concept Breakdown)

2.1 PIO Sampling and FIFO Packing

Fundamentals

PIO can sample pins at a deterministic rate and push samples into the RX FIFO. You typically pack multiple GPIO inputs into a single word and transfer them via DMA.

Deep Dive into the concept

A PIO state machine can read a group of pins using in pins, N at each cycle. If the PIO clock is 10 MHz, you get 10 million samples per second. Each sample is N bits; if you capture 8 channels, you can pack them into a byte and then pack multiple samples into a 32‑bit word. The RX FIFO can hold a few words; DMA is required to avoid overflow. Sampling fidelity depends on clock stability, pin pad configuration, and the alignment of GPIO pins (contiguous groups are simpler to capture). You also need to think about aliasing: a 10 MHz sample rate can only capture signals up to 5 MHz (Nyquist), and you should document this.

How this fits on projects

This is the core of §3.2 and §5.10 Phase 1. Also used in: P12-pio-based-i2cspi-analyzer.md.

Definitions & key terms

  • Sample rate -> frequency at which signals are captured
  • FIFO -> buffer between PIO and DMA
  • Nyquist -> max signal frequency = sample_rate/2

Mental model diagram (ASCII)

GPIO -> PIO in pins -> RX FIFO -> DMA -> SRAM ring buffer

How it works (step-by-step)

  1. Configure pins as inputs.
  2. PIO reads pins each cycle.
  3. PIO pushes packed samples into FIFO.
  4. DMA drains FIFO into ring buffer.

Minimal concrete example

loop:
  in pins, 8
  push
  jmp loop

Common misconceptions

  • “PIO can sample any arbitrary pins” -> contiguous pin groups are easiest.

Check-your-understanding questions

  1. What happens if FIFO fills faster than DMA?

Check-your-understanding answers

  1. PIO stalls or samples are dropped.

Real-world applications

  • Logic analyzers, protocol sniffers, waveform capture

Where you’ll apply it

  • In this project: §3.2, §5.10 Phase 1

References

  • RP2040 datasheet: PIO RX FIFO

Key insights

  • PIO gives deterministic sampling, DMA makes it sustainable.

Summary

Deterministic sampling only works if the FIFO is drained on time.

Homework/Exercises to practice the concept

  1. Compute buffer size for 1 second at 10 MHz with 8 channels.

Solutions to the homework/exercises

  1. 10e6 samples * 1 byte = 10 MB (needs streaming).

2.2 DMA Ring Buffers and Overrun Detection

Fundamentals

DMA ring buffers allow continuous capture by wrapping a buffer and signaling when chunks are full. You must detect overruns when the consumer (USB/UART) is slower than the producer (PIO).

Deep Dive into the concept

On RP2040/RP2350, DMA channels can be chained or configured with ring buffer addressing. A common approach is to allocate a large buffer and split it into fixed-size blocks. DMA fills blocks, and an interrupt fires after each block. The CPU or second core processes blocks and streams them to the host. If the consumer is too slow, the DMA will wrap and overwrite unread data. Therefore, you need a write index and a read index, and a policy: drop oldest data or stop capture. Overrun detection is critical for reliable analysis; you should surface it in the UI/logs.

How this fits on projects

This is required for §4.2 and §5.10 Phase 2. Also used in: P05-dual-core-audio-synthesizer.md.

Definitions & key terms

  • Ring buffer -> circular buffer with wrap-around
  • Overrun -> producer overwrites unread data

Mental model diagram (ASCII)

[Block0][Block1][Block2][Block3]
   ^read           ^write

How it works (step-by-step)

  1. DMA fills block 0, interrupt fires.
  2. CPU processes block 0 while DMA fills block 1.
  3. If CPU lags, DMA wraps and overwrites old blocks.

Minimal concrete example

if (next_write == read_idx) overrun++;

Common misconceptions

  • “DMA handles overruns” -> it does not; you must detect them.

Check-your-understanding questions

  1. Why is a ring buffer better than a single buffer?

Check-your-understanding answers

  1. It allows continuous capture without stopping DMA.

Real-world applications

  • Audio streaming, data acquisition

Where you’ll apply it

  • In this project: §4.2, §5.10 Phase 2

References

  • RP2040 datasheet: DMA ring modes

Key insights

  • Sustained capture is a producer/consumer problem.

Summary

Ring buffers are simple but must include overrun detection to be reliable.

Homework/Exercises to practice the concept

  1. Design a block size for 10 MHz capture with USB streaming.

Solutions to the homework/exercises

  1. Balance block size to reduce interrupt rate while keeping latency low.

2.3 Triggering and Host Streaming

Fundamentals

A logic analyzer is only useful if you can trigger on events and export data to a host. Triggers can be implemented in PIO (edge detect) or in CPU after capture. Streaming must not block capture.

Deep Dive into the concept

Triggers define the capture window. A simple trigger is a rising edge on a chosen channel. If you implement this in PIO, you can pause sampling until a condition is met. This reduces buffer load and gives precise alignment. Host streaming typically uses USB CDC or UART. USB offers higher throughput, but requires careful scheduling; UART is simpler but slower. You should design a packet format with timestamps and sample blocks. For compatibility, consider exporting to the Sigrok or Saleae formats.

How this fits on projects

This drives §3.2 and §5.10 Phase 3.

Definitions & key terms

  • Trigger -> condition that starts capture
  • Pre-trigger -> buffer stored before event
  • Post-trigger -> buffer after event

Mental model diagram (ASCII)

----pre----|TRIGGER|----post----

Minimal concrete example

struct capture_header { uint32_t t0; uint16_t rate; uint8_t channels; };

Common misconceptions

  • “Trigger in CPU is precise” -> CPU triggers are less precise than PIO.

Check-your-understanding questions

  1. Why is pre-trigger data useful?

Check-your-understanding answers

  1. It shows signal context before the event.

Real-world applications

  • Protocol debugging, hardware bring-up

Where you’ll apply it

  • In this project: §3.2, §5.10 Phase 3

References

  • Sigrok data formats

Key insights

  • A good trigger system is what makes capture actionable.

Summary

Triggers and streaming are the user-facing features of a logic analyzer.

Homework/Exercises to practice the concept

  1. Implement a rising edge trigger on channel 0.

Solutions to the homework/exercises

  1. Compare current and previous samples or use PIO wait.

3. Project Specification

3.1 What You Will Build

A USB/UART logic analyzer that captures 1–8 channels at selectable sample rates (1–20 MHz), supports edge triggering, and exports captured traces to a host.

3.2 Functional Requirements

  1. Multi-channel capture with fixed sample rate.
  2. Trigger on rising/falling edge.
  3. DMA ring buffer with overrun detection.
  4. Host streaming via USB CDC or UART.

3.3 Non-Functional Requirements

  • Performance: 10 MHz capture for 1 second without drops.
  • Reliability: clear overrun reporting.
  • Usability: simple host tool or CSV export.

3.4 Example Usage / Output

[Capture] rate=10MHz, channels=8
[Trigger] rising edge on CH0
[Export] capture.sr (Sigrok)

3.5 Data Formats / Schemas / Protocols

  • Capture block header: timestamp, rate, channels
  • Sample data: packed bitfields

3.6 Edge Cases

  • Host disconnect during streaming.
  • Trigger never fires.
  • Sample rate too high for USB throughput.

3.7 Real World Outcome

You capture a known UART or SPI waveform and view it in Sigrok, with correct timing and trigger alignment.

3.7.1 How to Run (Copy/Paste)

cmake .. && make -j
picotool load logic_analyzer.uf2 -f

3.7.2 Golden Path Demo (Deterministic)

  1. Attach CH0 to a 1 MHz square wave.
  2. Trigger on rising edge.
  3. Export capture and verify 1 µs period.

3.7.3 If CLI: exact terminal transcript

[Analyzer] rate=10MHz, channels=8
[Trigger] CH0 rising
[Buffer] blocks=16, size=4KB
[OK] capture saved to capture.csv

Failure Demo (Expected)

[ERROR] Overrun detected: host too slow
# Exit code: 4 (capture error)

4. Solution Architecture

4.1 High-Level Design

PIO Sampler -> DMA Ring -> Core1 Streamer -> Host

4.2 Key Components

| Component | Responsibility | Key Decisions | |———-|—————–|—————| | PIO sampler | capture pins | choose contiguous GPIO block | | DMA ring | store samples | block size and count | | Trigger engine | start capture | PIO vs CPU trigger | | Host streamer | export data | USB CDC vs UART |

4.3 Data Structures (No Full Code)

struct block_meta { uint32_t t0; uint16_t len; uint8_t flags; };

4.4 Algorithm Overview

  1. Wait for trigger.
  2. Start DMA ring capture.
  3. Stream blocks to host.

5. Implementation Guide

5.1 Development Environment Setup

brew install cmake ninja arm-none-eabi-gcc

5.2 Project Structure

logic-analyzer/
├── src/
│   ├── sampler.pio
│   ├── capture.c
│   ├── dma.c
│   └── stream.c
└── host/
    └── capture_tool.py

5.3 The Core Question You’re Answering

“How do I capture fast digital signals without dropping samples?”

5.4 Concepts You Must Understand First

  1. PIO sampling (see §2.1)
  2. DMA ring buffers (see §2.2)
  3. Triggering and streaming (see §2.3)

5.5 Questions to Guide Your Design

  1. What sample rate fits within USB throughput?
  2. How will you detect overruns?
  3. Should trigger be in PIO or CPU?

5.6 Thinking Exercise

Compute max capture rate if USB provides 1 MB/s and you sample 8 channels.

5.7 The Interview Questions They’ll Ask

  1. Why use DMA over CPU polling?
  2. How do you avoid aliasing?

5.8 Hints in Layers

  • Hint 1: Start with a single channel.
  • Hint 2: Use a known square wave as test input.
  • Hint 3: Add trigger after capture works.
  • Hint 4: Add streaming last.

5.9 Books That Will Help

| Topic | Book | Chapter | |——|——|———| | DMA & buffers | Making Embedded Systems | Ch. 9 | | Signal capture | The Art of Electronics | Ch. 10 |

5.10 Implementation Phases

  • Phase 1: PIO sampling
  • Phase 2: DMA ring buffer
  • Phase 3: Host streaming

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———|———|—————-|———–| | Interface | USB vs UART | USB | Higher throughput | | Trigger | PIO vs CPU | PIO | Deterministic timing |


6. Testing Strategy

6.1 Test Categories

  • Sample accuracy tests
  • Trigger tests
  • Overrun tests

6.2 Critical Test Cases

  1. Capture 1 MHz square wave with correct duty cycle.
  2. Trigger alignment within 1 sample.

7. Common Pitfalls & Debugging

  • FIFO overflow due to slow DMA
  • Overrun silently corrupting data

8. Extensions & Challenges

  • Multi-level triggers
  • Protocol decode (UART/I2C/SPI)

9. Real-World Connections

  • Debugging embedded buses and signals

10. Resources

  • Sigrok / Saleae format docs

11. Self-Assessment Checklist

  • Can capture stable 10 MHz waveform
  • Overrun detection works

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Single-channel capture and export works.

Full Completion:

  • 8-channel capture with trigger and streaming.

Excellence (Going Above & Beyond):

  • Export compatible with Sigrok.