Project 8: TF Card Image Viewer
Stream BMP/RAW images from a microSD card and display them without exhausting RAM.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (Alternatives: MicroPython) |
| Alternative Programming Languages | MicroPython |
| Coolness Level | Level 3: Impressive |
| Business Potential | 3. The “Product” Level |
| Prerequisites | Project 1 LCD bring-up, basic SPI |
| Key Topics | FAT filesystem, SD card SPI, streaming IO, BMP decoding |
1. Learning Objectives
By completing this project, you will:
- Initialize a TF (microSD) card over SPI.
- Parse a FAT directory and list image files.
- Stream file data in chunks without full buffering.
- Decode BMP headers and convert pixels to RGB565.
- Display images with a progress indicator on the LCD.
2. All Theory Needed (Per-Concept Breakdown)
2.1 FAT Filesystem Structure
Fundamentals
FAT (File Allocation Table) is a simple filesystem widely used on SD cards. It stores a table that links clusters (blocks of data) into files. Each directory entry contains metadata such as filename, size, and starting cluster. To read a file, you follow the chain of clusters using the FAT table. For embedded systems, understanding FAT helps you locate files and stream data in small chunks without loading the entire file into RAM.
Deep Dive into the concept
FAT uses a boot sector that describes key parameters: bytes per sector (typically 512), sectors per cluster, and where the FAT and root directory start. The FAT table is an array where each entry points to the next cluster in a file. For FAT16, each entry is 16 bits; for FAT32, 32 bits with some reserved bits. A file’s directory entry stores the first cluster. To read the file, you compute the sector address for that cluster, read it, then look up the next cluster in the FAT. This continues until an end-of-chain marker is reached.
Parsing FAT in embedded systems often uses a library (like FatFS), but understanding the structure helps debug issues and optimize reads. For example, reading a file sequentially is fast because clusters are typically contiguous; but fragmentation can make reads jump around. Caching FAT sectors can reduce repeated reads. If your buffer is small (e.g., 4 KB), you must handle partial cluster reads. A robust implementation tracks the current cluster, sector offset, and file offset so you can stream line-by-line without exceeding memory.
How this fits on projects
FAT parsing is critical in Section 3.2 and Section 5.10 Phase 2. It also informs Project 9 (logging) and Project 13 (mini OS filesystem). Also used in: Project 9, Project 13.
Definitions & key terms
- Cluster -> Group of sectors forming a file block.
- FAT -> Table linking clusters into files.
- Root directory -> Directory entries for top-level files.
- End-of-chain -> Marker for file end.
Mental model diagram (ASCII)
File A: cluster 5 -> 6 -> 7 -> EOC
FAT table: [5]=6, [6]=7, [7]=EOC
How it works (step-by-step)
- Read boot sector to get FAT parameters.
- Locate FAT and root directory.
- Parse directory entries to find file.
- Follow cluster chain using FAT table.
- Read data sector by sector.
Failure modes:
- Wrong FAT type -> incorrect offsets.
- No caching -> slow reads.
- Fragmented files -> large seek overhead.
Minimal concrete example
uint32_t cluster = dir_entry.first_cluster;
while (cluster != FAT_EOC) {
read_cluster(cluster, buffer);
cluster = fat_table[cluster];
}
Common misconceptions
- “FAT is just a directory list.” -> The FAT table drives data mapping.
- “Files are always contiguous.” -> Fragmentation exists.
Check-your-understanding questions
- What is a cluster in FAT?
- How do you find the next cluster?
- Why cache FAT sectors?
Check-your-understanding answers
- A group of sectors used as a file allocation unit.
- Look up the cluster index in the FAT table.
- To avoid repeated SPI reads of the same FAT sectors.
Real-world applications
- Firmware updates from SD cards
- Camera image storage
Where you’ll apply it
- This project: Section 3.2, Section 5.10 Phase 2
- Also used in: Project 13
References
- FAT specification overview
- FatFS documentation
Key insights
FAT is simple but requires careful cluster navigation.
Summary
Understand FAT so you can stream files efficiently and safely.
Homework/Exercises to practice the concept
- Parse a boot sector and print FAT parameters.
- Walk a cluster chain and count clusters.
- Implement a 2-sector FAT cache.
Solutions to the homework/exercises
- Use bytes per sector, sectors per cluster, FAT offset.
- Follow FAT entries until EOC.
- Keep last FAT sector in RAM and reuse if same.
2.2 Streaming IO and Buffering
Fundamentals
Streaming IO means reading data in small chunks instead of loading the entire file into RAM. This is essential on microcontrollers with limited memory. A streaming image viewer reads a file row by row or chunk by chunk, converts pixels, and writes them to the LCD as it goes. The key is to maintain a fixed-size buffer and handle partial reads correctly.
Deep Dive into the concept
When streaming an image, you must manage two constraints: storage IO speed and LCD output speed. SD card reads over SPI can be slow, so you should read larger blocks (e.g., 4 KB) and then process them internally. The LCD update also has a fixed bandwidth, so you should pipeline reading and display. A common strategy is a line buffer: read one row of pixels, convert to RGB565, send to LCD, repeat. This uses minimal memory but may be limited by per-row overhead. A block buffer (e.g., 16 rows) amortizes overhead but uses more memory.
Streaming also interacts with file formats. BMP files store rows bottom-to-top by default and may pad each row to 4-byte boundaries. Your streaming reader must handle this padding and reverse order if needed. For raw RGB565 files, streaming is simpler: read bytes and send. In all cases, you must avoid blocking the system for too long; show a progress bar on the LCD and allow user input to cancel. Deterministic streaming uses a fixed buffer size and a fixed chunk size so performance is predictable.
How this fits on projects
Streaming is central in Section 3.2 and Section 5.10 Phase 3. It also helps in Project 3 (DMA pipeline) and Project 9 (data logging). Also used in: Project 3, Project 9.
Definitions & key terms
- Line buffer -> Buffer holding one row of pixels.
- Chunked IO -> Reading data in fixed-size blocks.
- Padding -> Extra bytes to align rows.
- Pipeline -> Overlapping IO and processing.
Mental model diagram (ASCII)
SD read -> buffer -> convert -> LCD write -> repeat
How it works (step-by-step)
- Read a chunk of file data into buffer.
- Convert pixels to RGB565 if needed.
- Send to LCD as rows.
- Repeat until file complete.
Failure modes:
- Wrong row order -> flipped image.
- Ignored padding -> skewed image.
- Buffer too small -> excessive overhead.
Minimal concrete example
for (int row = 0; row < height; row++) {
read_row(sd, buffer, row_bytes);
convert_to_rgb565(buffer, line);
lcd_write_line(row, line);
}
Common misconceptions
- “Just read the whole file.” -> Too much RAM.
- “BMP rows are packed.” -> They are padded to 4-byte boundaries.
Check-your-understanding questions
- Why use a line buffer instead of full image?
- What is row padding in BMP?
- How do you handle bottom-to-top storage?
Check-your-understanding answers
- Saves RAM by streaming.
- Rows are padded to 4-byte boundaries.
- Read rows in reverse order or adjust indexing.
Real-world applications
- Image viewers on embedded systems
- Streaming audio/video with limited memory
Where you’ll apply it
- This project: Section 3.2, Section 5.10 Phase 3
- Also used in: Project 3
References
- BMP file format documentation
- SD card SPI mode notes
Key insights
Streaming lets you handle large assets without large RAM.
Summary
Use chunked IO and careful format handling for reliable image streaming.
Homework/Exercises to practice the concept
- Implement a line buffer reader for a raw RGB565 file.
- Add support for BMP padding.
- Measure total time to display a 100 KB image.
Solutions to the homework/exercises
- Read width*2 bytes per row and send to LCD.
- Skip padding bytes at end of row.
- Time should scale with file size and SPI speed.
3. Project Specification
3.1 What You Will Build
An SD card image viewer that lists BMP/RAW files and displays them on the LCD by streaming data in chunks. A progress bar shows load status.
3.2 Functional Requirements
- SD card init in SPI mode.
- FAT file listing for image files.
- BMP decoding for 24-bit or RGB565 BMP.
- Streaming display with line buffer.
- UI with progress bar and file selection.
3.3 Non-Functional Requirements
- Performance: Display a 100 KB image in under 2 seconds.
- Reliability: Handle missing or corrupt files gracefully.
- Usability: Simple menu navigation.
3.4 Example Usage / Output
Images:
1) BEACH.BMP
2) LOGO.RAW
Loading: 65%
3.5 Data Formats / Schemas / Protocols
- FAT16/32
- BMP 24-bit, RGB565 RAW
3.6 Edge Cases
- Corrupt BMP header
- Fragmented files
- SD card not inserted
3.7 Real World Outcome
Insert an SD card with images; select one; the LCD displays it with a progress bar. Invalid files show a clear error message.
3.7.1 How to Run (Copy/Paste)
cd LEARN_RP2350_LCD_DEEP_DIVE/image_viewer
mkdir -p build
cd build
cmake ..
make -j4
cp image_viewer.uf2 /Volumes/RP2350
3.7.2 Golden Path Demo (Deterministic)
- SD contains BEACH.BMP and LOGO.RAW.
- Select BEACH.BMP.
- Progress bar increments to 100% and image appears.
3.7.3 Failure Demo (Deterministic)
- Remove SD card and boot.
- Expected: LCD shows “No SD Card” error.
- Fix: insert card and reboot.
4. Solution Architecture
4.1 High-Level Design
[SD SPI] -> [FAT Parser] -> [Image Decoder] -> [Line Buffer] -> LCD
4.2 Key Components
| Component | Responsibility | Key Decisions | |———–|—————-|—————| | SD driver | SPI block reads | Use 512-byte sectors | | FAT parser | Locate files | Cache FAT sectors | | Decoder | BMP/RAW to RGB565 | Stream line-by-line |
4.3 Data Structures (No Full Code)
typedef struct { char name[13]; uint32_t size; uint32_t cluster; } file_entry_t;
4.4 Algorithm Overview
Key Algorithm: Stream BMP
- Read BMP header and compute row size.
- For each row, read bytes into buffer.
- Convert to RGB565 and send to LCD.
Complexity Analysis:
- Time: O(pixels)
- Space: O(line buffer)
5. Implementation Guide
5.1 Development Environment Setup
# Use FatFS or minimal FAT parser
5.2 Project Structure
image_viewer/
- src/
- sd_spi.c
- fat.c
- bmp.c
- main.c
5.3 The Core Question You’re Answering
“How do I stream large assets from storage without running out of RAM?”
5.4 Concepts You Must Understand First
- FAT cluster chains
- SPI block reads
- BMP headers and row padding
5.5 Questions to Guide Your Design
- What buffer size balances speed and RAM?
- How will you display progress?
- How will you handle corrupt headers?
5.6 Thinking Exercise
If your buffer is 4 KB, how many reads for a 100 KB file?
5.7 The Interview Questions They’ll Ask
- Why is FAT still common on SD cards?
- How do you stream data with low RAM?
- What is BMP row padding?
5.8 Hints in Layers
- Hint 1: Use FatFS first, then optimize.
- Hint 2: Read one row at a time.
- Hint 3: Cache FAT sectors.
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | Filesystems | “Operating Systems: Three Easy Pieces” | Ch. 39 | | Embedded IO | “Making Embedded Systems” | Ch. 8 |
5.10 Implementation Phases
Phase 1: SD + FAT (4-5 days)
Goals: List files. Tasks: Initialize SD, parse root directory. Checkpoint: File list displayed.
Phase 2: BMP Decode (4-5 days)
Goals: Show images. Tasks: Parse BMP header, stream rows. Checkpoint: BMP displays correctly.
Phase 3: UI + Progress (3-4 days)
Goals: Improve usability. Tasks: Add progress bar and error messages. Checkpoint: User can select files.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Buffer size | 512B vs 4KB | 4KB | Fewer SPI reads | | Decoder | On-the-fly vs preconvert | On-the-fly | No extra storage |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | BMP parsing | header validation | | Integration Tests | SD card IO | read known file | | Edge Case Tests | Missing card | error handling |
6.2 Critical Test Cases
- BMP 24-bit: correct colors.
- RAW RGB565: direct display.
- Corrupt header: error message.
6.3 Test Data
Images: 320x172 BMP, 100 KB RAW
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution | |———|———|———-| | Ignored BMP padding | skewed image | Skip padding bytes | | No FAT cache | slow loads | Cache FAT sectors | | Large buffer | RAM overflow | Use line buffer |
7.2 Debugging Strategies
- Print parsed BMP header values.
- Display a simple checksum while streaming.
7.3 Performance Traps
- Small read sizes cause excessive SPI overhead.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add file sorting by name.
8.2 Intermediate Extensions
- Add JPEG decoding with a tiny decoder.
8.3 Advanced Extensions
- Preload thumbnails in a cache.
9. Real-World Connections
9.1 Industry Applications
- Photo frames and kiosks
- Firmware update via SD card
9.2 Related Open Source Projects
- FatFS
- TinyBMP decoders
9.3 Interview Relevance
- Filesystems and streaming IO are key embedded topics.
10. Resources
10.1 Essential Reading
- FAT filesystem overview
- BMP file format documentation
10.2 Video Resources
- SD card SPI tutorials
10.3 Tools & Documentation
- Logic analyzer for SPI troubleshooting
10.4 Related Projects in This Series
- Project 9 for data logging.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain FAT cluster chaining.
- I can parse BMP headers.
11.2 Implementation
- Images display correctly.
- Progress bar updates smoothly.
11.3 Growth
- I can describe streaming IO in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Display at least one BMP from SD card.
Full Completion:
- File browser with progress UI.
Excellence (Going Above & Beyond):
- Thumbnail cache and fast browsing.