Embedded Mastery: C, Ada, Rust & Zig
A Project-Based Tutorial for Embedded Development
2026
A comprehensive, project-based tutorial for mastering embedded development in C, Ada, Rust, and Zig. Build 15 projects from LED blinker to safety-critical systems, all verified in QEMU and Renode emulators.
Project 12: Multi-Sensor Data Logger
In this project you will build a multi-sensor data logger that reads from I2C sensors at fixed intervals, buffers the data in RAM, and writes it to an SD card formatted with FAT32. You will implement the SD card SPI driver (CMD0 through CMD24), a minimal FAT32 filesystem writer, buffered I/O for write performance, and CSV file output β in C, Rust, Ada, and Zig.
SD card logging is the backbone of field data acquisition: environmental monitoring, vibration analysis, vehicle telemetry, and scientific instrumentation. Understanding the SD card protocol at the SPI command level and the FAT32 filesystem structure is essential for any embedded developer building data logging systems.
What Youβll Learn
- SD card protocol: SPI mode initialization sequence (CMD0, CMD8, CMD55+ACMD41, CMD17, CMD24)
- SD card response types: R1, R2, R3, R7 and their meaning
- Block device abstraction: read/write 512-byte blocks regardless of storage medium
- FAT32 filesystem: BPB (BIOS Parameter Block), FAT tables, directory entries, cluster chains
- Buffered I/O: why per-byte SD writes are catastrophic and how BufWriter solves it
- Real-time clock integration for timestamping logged data
- Multi-sensor synchronization: reading multiple I2C sensors at fixed intervals
- CSV file format for logged data
- QEMU raspi2 emulation with SD card image
- Language-specific approaches: embedded-sdmmc crate, buffered stream abstraction, comptime file format validation
Prerequisites
- ARM GCC toolchain (
arm-none-eabi-gcc) - Rust:
cargo,cortex-mcrate,embedded-sdmmccrate,embedded-haltraits - Ada: GNAT ARM toolchain
- Zig: Zig 0.11+ with ARM cross-compilation support
- QEMU with raspi2 support (
qemu-system-arm) - Familiarity with Projects 4 (I2C Sensor) and 5 (SPI Flash)
SD Card SPI Mode
SD cards support two interfaces: SD bus (4-bit wide) and SPI (1-bit wide). SPI mode is simpler to implement and sufficient for most data logging applications.
Pin Mapping
| SD Pin | SPI Signal | Direction | Description |
|---|---|---|---|
| CMD | MOSI | Host -> Card | Command and data input |
| DAT0 | MISO | Card -> Host | Response and data output |
| CLK | SCK | Host -> Card | Clock signal |
| CS | CS | Host -> Card | Chip select (active low) |
Initialization Sequence
SD card initialization is a multi-step handshake:
Host SD Card
| |
|--- 80 clock pulses --------->| (Card enters SPI mode)
| (CS high, MOSI high) |
| |
|--- CMD0 (GO_IDLE_STATE) ---->| (Reset to idle)
|<-- R1 = 0x01 ----------------| (In idle state)
| |
|--- CMD8 (SEND_IF_COND) ----->| (Check voltage range)
|<-- R7 = 0x01 + 0x000001AA ---| (2.7-3.6V OK, check pattern)
| |
|--- CMD55 (APP_CMD) --------->|
|<-- R1 = 0x01 ----------------|
|--- ACMD41 (SD_SEND_OP_COND)->| (Initialize, ask for capacity)
|<-- R1 = 0x01 ----------------| (Still initializing)
| ... repeat ... |
|<-- R1 = 0x00 ----------------| (Ready!)
| |
|--- CMD58 (READ_OCR) -------->| (Read OCR register)
|<-- R3 = 0x00 + OCR ----------| (CCS bit: SDHC/SDXC?)
| |
|--- CMD16 (SET_BLOCKLEN) ---->| (512 bytes, only for SDSC)
|<-- R1 = 0x00 ----------------|
| |
| READY |
Command Format
Every SD command is 6 bytes:
+--------+--------+--------+--------+--------+--------+
| Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 |
+--------+--------+--------+--------+--------+--------+
|01|cmd | arg1 | arg2 | arg3 | arg4 | CRC |
| 6bit | | | | | 7bit |
+--------+--------+--------+--------+--------+--------+
- Byte 0:
01prefix (2 bits) + command index (6 bits) - Bytes 1-4: 32-bit argument
- Byte 5: 7-bit CRC + stop bit (1)
Tip: In SPI mode, CRC is only required for CMD0 and CMD8. After initialization, you can disable CRC checking with CMD59.
Response Types
| Response | Length | Description |
|---|---|---|
| R1 | 1 byte | Standard response: bit 0 = idle, bit 1 = erase reset, bit 2 = illegal command, bit 3 = CRC error, bit 4 = erase sequence error, bit 5 = address error, bit 6 = parameter error |
| R2 | 2 bytes | R1 + second byte (card status for CSD register read) |
| R3/R7 | 5 bytes | R1 + 4-byte OCR or check pattern |
Data Read (CMD17)
Host SD Card
|--- CMD17 (addr) ----------->|
|<-- R1 = 0x00 ---------------|
| |
|--- wait for token --------->|
|<-- 0xFE (start token) ------|
|<-- 512 bytes of data -------|
|<-- 2-byte CRC --------------|
Data Write (CMD24)
Host SD Card
|--- CMD24 (addr) ----------->|
|<-- R1 = 0x00 ---------------|
|--- 0xFE (start token) ----->|
|--- 512 bytes of data ------>|
|--- 2-byte CRC ------------->|
|<-- 0xE5 (data accepted) ----|
|--- wait for busy ---------->| (Card writes to flash internally)
|<-- 0xFF (ready) ------------|
FAT32 Filesystem
FAT32 organizes storage into:
+------------------+
| Boot Sector | <- BPB (BIOS Parameter Block)
| (512 bytes) | Contains: sectors per cluster,
+------------------+ number of FATs, root cluster, etc.
| FAT 1 |
| (N sectors) | <- File Allocation Table: maps cluster
+------------------+ numbers to next cluster in chain
| FAT 2 (copy) |
| (N sectors) |
+------------------+
| Root Directory | <- Cluster chain starting at BPB root cluster
| (variable) | Contains 32-byte directory entries
+------------------+
| Data Area | <- File contents, organized in clusters
| (clusters) | Each cluster = N sectors
+------------------+
BIOS Parameter Block (BPB)
The first 512 bytes of the volume contain the boot sector with the BPB:
| Offset | Size | Field | Value (typical) |
|---|---|---|---|
| 0 | 3 | Jump instruction | 0xEB 0x58 0x90 |
| 3 | 8 | OEM Name | "MSDOS5.0" |
| 11 | 2 | Bytes per sector | 512 |
| 13 | 1 | Sectors per cluster | 8 (4KB clusters) |
| 14 | 2 | Reserved sectors | 32 |
| 16 | 1 | Number of FATs | 2 |
| 17 | 2 | Root entries (FAT32: 0) | 0 |
| 19 | 2 | Total sectors (16-bit, 0 for FAT32) | 0 |
| 21 | 1 | Media descriptor | 0xF8 (hard disk) |
| 22 | 2 | FAT size (16-bit, 0 for FAT32) | 0 |
| 28 | 4 | FAT size (32-bit) | varies |
| 36 | 4 | Root cluster number | 2 |
| 40 | 2 | FSInfo sector | 1 |
| 44 | 2 | Backup boot sector | 6 |
| 64 | 8 | Volume label | "NO NAME " |
| 82 | 8 | FS type | "FAT32 " |
| 510 | 2 | Boot signature | 0xAA55 |
Directory Entry (32 bytes)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 8 | Short name | 8.3 filename (space-padded) |
| 8 | 3 | Extension | File extension |
| 11 | 1 | Attributes | 0x01=RO, 0x02=HIDDEN, 0x04=SYSTEM, 0x08=VOLUME, 0x10=DIR, 0x20=ARCHIVE |
| 12 | 1 | Reserved | NT reserved |
| 13 | 1 | Creation time (10ms) | Fine creation time |
| 14 | 2 | Creation time | HH:MM:SS packed |
| 16 | 2 | Creation date | YYYY-MM-DD packed |
| 18 | 2 | Last access date | YYYY-MM-DD packed |
| 20 | 2 | High word of first cluster | FAT32 |
| 22 | 2 | Last write time | HH:MM:SS packed |
| 24 | 2 | Last write date | YYYY-MM-DD packed |
| 26 | 2 | Low word of first cluster | FAT32 |
| 28 | 4 | File size in bytes |
Cluster Chains
Files are stored as chains of clusters. The FAT table maps each cluster to the next:
File "LOG001.CSV" starts at cluster 5:
FAT[5] = 6 -> cluster 6
FAT[6] = 7 -> cluster 7
FAT[7] = 12 -> cluster 12
FAT[12] = 0x0FFFFFF8 -> end of chain (EOF marker)
File data: cluster 5 -> cluster 6 -> cluster 7 -> cluster 12
EOF markers in FAT32: 0x0FFFFFF8 through
0x0FFFFFFF. Free cluster: 0x00000000.
Bad cluster: 0x0FFFFFF7.
Block Device Abstraction
All storage operations go through a block device interface:
typedef struct {
int (*read_blocks)(void *ctx, uint32_t block, uint8_t *buf, uint32_t count);
int (*write_blocks)(void *ctx, uint32_t block, const uint8_t *buf, uint32_t count);
uint32_t block_size;
uint32_t num_blocks;
void *ctx;
} BlockDevice;This abstraction lets the FAT32 layer work with any storage medium: SD card, SPI flash, eMMC, or even a RAM disk for testing.
Buffered I/O
Writing directly to SD cards one byte at a time is extremely slow because each write requires:
- CMD24 command (6 bytes)
- Start token + 512 data bytes + 2 CRC bytes
- Wait for busy signal (card writes to flash internally)
A 100-byte log entry would trigger a full 512-byte write cycle. With a buffered writer, data accumulates in a RAM buffer and is flushed only when full:
Logger writes 100 bytes -> buffer (no SD access)
Logger writes 100 bytes -> buffer (no SD access)
Logger writes 100 bytes -> buffer (no SD access)
Logger writes 100 bytes -> buffer full! -> flush 512 bytes to SD (1 write)
Logger writes 50 bytes -> buffer (no SD access)
...
Logger closes file -> flush remaining 50 bytes (1 write)
For 10KB of data:
Without buffering: ~20 writes (each 512 bytes with padding)
With buffering: ~20 writes (each full 512 bytes, no padding)
But more importantly: the buffer avoids partial-sector read-modify-write cycles
when appending to existing files.
Multi-Sensor Synchronization
Reading multiple sensors at fixed intervals requires coordinated timing:
Timeline (100ms intervals):
0ms: Read temp sensor (I2C)
Read humidity sensor (I2C)
Read pressure sensor (I2C)
Format CSV row
Write to buffer
100ms: Read all sensors
Format CSV row
Write to buffer
200ms: Read all sensors
Format CSV row
Write to buffer
500ms: Buffer full -> flush to SD card
The key is that sensor reads are synchronous (blocking I2C), so they must complete within the interval. If a read takes too long, the next interval is missed.
Implementation: C
Project Structure
logger-c/
βββ linker.ld
βββ startup.c
βββ sd_spi.h
βββ sd_spi.c
βββ fat32.h
βββ fat32.c
βββ logger.h
βββ logger.c
βββ main.c
βββ Makefile
Linker Script
(linker.ld)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
ENTRY(Reset_Handler)
SECTIONS
{
.text :
{
KEEP(*(.vectors))
*(.text*)
*(.rodata*)
} > FLASH
.data :
{
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
_estack = ORIGIN(RAM) + LENGTH(RAM);
}
SD SPI Driver Header
(sd_spi.h)
#ifndef SD_SPI_H
#define SD_SPI_H
#include <stdint.h>
#include <stdbool.h>
#define SD_BLOCK_SIZE 512
/* SD card response codes */
#define SD_R1_IDLE 0x01
#define SD_R1_ERASE_RESET 0x02
#define SD_R1_ILLEGAL_CMD 0x04
#define SD_R1_CRC_ERROR 0x08
#define SD_R1_ERASE_SEQ_ERR 0x10
#define SD_R1_ADDR_ERROR 0x20
#define SD_R1_PARAM_ERROR 0x40
/* SD card state */
typedef enum {
SD_OK,
SD_ERR_TIMEOUT,
SD_ERR_RESPONSE,
SD_ERR_DATA_TOKEN,
SD_ERR_WRITE,
SD_ERR_INIT,
SD_ERR_CRC,
} SdError;
/* SD card info */
typedef struct {
bool is_sdhc;
uint32_t capacity_blocks;
uint8_t card_type;
} SdInfo;
SdError sd_init(void);
SdError sd_get_info(SdInfo *info);
SdError sd_read_blocks(uint32_t block_num, uint8_t *buf, uint32_t count);
SdError sd_write_blocks(uint32_t block_num, const uint8_t *buf, uint32_t count);
SdError sd_last_error(void);
#endifSD SPI Driver
(sd_spi.c)
#include "sd_spi.h"
#include <string.h>
/* SPI1 on STM32F405: PA5=SCK, PA6=MISO, PA7=MOSI (AF5) */
#define SPI1_CR1 (*(volatile uint32_t *)0x40013000)
#define SPI1_SR (*(volatile uint32_t *)0x40013004)
#define SPI1_DR (*(volatile uint32_t *)0x40013008)
#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830)
#define GPIOA_MODER (*(volatile uint32_t *)0x40020000)
#define GPIOA_AFRL (*(volatile uint32_t *)0x40020020)
#define GPIOA_BSRR (*(volatile uint32_t *)0x40020018)
#define SD_CS_PIN (1 << 4)
static SdError last_err = SD_OK;
static bool card_is_sdhc = false;
static inline void sd_cs_low(void) {
GPIOA_BSRR = SD_CS_PIN << 16;
}
static inline void sd_cs_high(void) {
GPIOA_BSRR = SD_CS_PIN;
}
static inline uint8_t spi_transfer(uint8_t data) {
while (!(SPI1_SR & (1 << 1)));
SPI1_DR = data;
while (!(SPI1_SR & (1 << 0)));
return (uint8_t)SPI1_DR;
}
static void sd_clock_80(void) {
sd_cs_high();
for (int i = 0; i < 10; i++) {
spi_transfer(0xFF);
}
}
static uint8_t sd_cmd(uint8_t cmd, uint32_t arg, uint8_t crc) {
uint8_t response;
int timeout = 1000;
sd_cs_low();
spi_transfer(0x40 | cmd);
spi_transfer((arg >> 24) & 0xFF);
spi_transfer((arg >> 16) & 0xFF);
spi_transfer((arg >> 8) & 0xFF);
spi_transfer(arg & 0xFF);
spi_transfer(crc);
do {
response = spi_transfer(0xFF);
timeout--;
} while (response == 0xFF && timeout > 0);
if (timeout <= 0) last_err = SD_ERR_TIMEOUT;
return response;
}
static uint32_t sd_read_r7(void) {
uint32_t r7 = 0;
for (int i = 0; i < 4; i++) {
r7 = (r7 << 8) | spi_transfer(0xFF);
}
return r7;
}
static uint32_t sd_read_r3(void) {
uint32_t r3 = 0;
for (int i = 0; i < 4; i++) {
r3 = (r3 << 8) | spi_transfer(0xFF);
}
return r3;
}
static uint8_t sd_wait_token(uint8_t expected, int timeout_ms) {
uint8_t token;
int timeout = timeout_ms * 10;
do {
token = spi_transfer(0xFF);
timeout--;
} while (token != expected && timeout > 0);
return token;
}
static void spi_init(void) {
RCC_AHB1ENR |= (1 << 0); /* Enable GPIOA clock */
/* PA5=SCK, PA6=MISO, PA7=MOSI: alternate function (AF5), push-pull */
/* MODER: 0b10 = alternate function for pins 5,6,7 */
GPIOA_MODER &= ~(0x3F << 10);
GPIOA_MODER |= (0x2A << 10); /* AF mode for PA5,PA6,PA7 */
/* AFR[0]: AF5 for pins 5,6,7 (4 bits each, bits 20-31) */
GPIOA_AFRL &= ~(0xFFF << 20);
GPIOA_AFRL |= (0x555 << 20); /* AF5 for PA5,PA6,PA7 */
/* PA4 CS: output push-pull */
GPIOA_MODER &= ~(0x3 << 8);
GPIOA_MODER |= (0x1 << 8); /* Output mode for PA4 */
sd_cs_high();
/* SPI1: master, BR=FPCLK/256 (slow for init), CPOL=0, CPHA=0 */
SPI1_CR1 = (1 << 9) | (1 << 8) | (1 << 2) | (7 << 3);
SPI1_CR1 |= (1 << 6);
}
static void spi_set_speed_fast(void) {
SPI1_CR1 &= ~(7 << 3);
SPI1_CR1 |= (1 << 3); /* BR = FPCLK/8 */
}
SdError sd_init(void) {
uint8_t r1;
spi_init();
sd_clock_80();
/* CMD0: GO_IDLE_STATE */
r1 = sd_cmd(0, 0, 0x95);
sd_cs_high();
spi_transfer(0xFF);
if (r1 != SD_R1_IDLE) { last_err = SD_ERR_INIT; return last_err; }
/* CMD8: SEND_IF_COND */
r1 = sd_cmd(8, 0x000001AA, 0x87);
uint32_t r7 = sd_read_r7();
sd_cs_high();
spi_transfer(0xFF);
if ((r1 & SD_R1_ILLEGAL_CMD) || ((r7 & 0xFFF) != 0x1AA)) {
last_err = SD_ERR_INIT; return last_err;
}
/* ACMD41: initialize with HCS bit */
int retries = 0;
do {
r1 = sd_cmd(55, 0, 0xFF);
sd_cs_high();
spi_transfer(0xFF);
if (r1 != SD_R1_IDLE) { last_err = SD_ERR_RESPONSE; return last_err; }
r1 = sd_cmd(41, 0x40000000, 0xFF);
sd_cs_high();
spi_transfer(0xFF);
if (++retries > 1000) { last_err = SD_ERR_TIMEOUT; return last_err; }
} while (r1 == SD_R1_IDLE);
if (r1 != 0x00) { last_err = SD_ERR_INIT; return last_err; }
/* CMD58: READ_OCR */
r1 = sd_cmd(58, 0, 0xFF);
uint32_t ocr = sd_read_r3();
sd_cs_high();
spi_transfer(0xFF);
card_is_sdhc = (ocr >> 30) & 1;
/* CMD59: CRC off */
sd_cmd(59, 0, 0xFF);
sd_cs_high();
spi_transfer(0xFF);
spi_set_speed_fast();
last_err = SD_OK;
return SD_OK;
}
SdError sd_get_info(SdInfo *info) {
info->is_sdhc = card_is_sdhc;
info->card_type = card_is_sdhc ? 3 : 2;
info->capacity_blocks = 0;
return SD_OK;
}
SdError sd_read_blocks(uint32_t block_num, uint8_t *buf, uint32_t count) {
for (uint32_t i = 0; i < count; i++) {
uint32_t addr = card_is_sdhc ? (block_num + i) : ((block_num + i) * SD_BLOCK_SIZE);
uint8_t r1 = sd_cmd(17, addr, 0xFF);
if (r1 != 0x00) { sd_cs_high(); spi_transfer(0xFF); last_err = SD_ERR_RESPONSE; return last_err; }
if (sd_wait_token(0xFE, 1000) != 0xFE) {
sd_cs_high(); spi_transfer(0xFF); last_err = SD_ERR_DATA_TOKEN; return last_err;
}
for (int j = 0; j < SD_BLOCK_SIZE; j++) {
buf[i * SD_BLOCK_SIZE + j] = spi_transfer(0xFF);
}
spi_transfer(0xFF); /* CRC */
spi_transfer(0xFF);
sd_cs_high();
spi_transfer(0xFF);
}
last_err = SD_OK;
return SD_OK;
}
SdError sd_write_blocks(uint32_t block_num, const uint8_t *buf, uint32_t count) {
for (uint32_t i = 0; i < count; i++) {
uint32_t addr = card_is_sdhc ? (block_num + i) : ((block_num + i) * SD_BLOCK_SIZE);
uint8_t r1 = sd_cmd(24, addr, 0xFF);
if (r1 != 0x00) { sd_cs_high(); spi_transfer(0xFF); last_err = SD_ERR_RESPONSE; return last_err; }
spi_transfer(0xFE); /* Start token */
for (int j = 0; j < SD_BLOCK_SIZE; j++) {
spi_transfer(buf[i * SD_BLOCK_SIZE + j]);
}
spi_transfer(0xFF); /* Dummy CRC */
spi_transfer(0xFF);
uint8_t resp = spi_transfer(0xFF) & 0x1F;
if (resp != 0x05) { sd_cs_high(); spi_transfer(0xFF); last_err = SD_ERR_WRITE; return last_err; }
while (spi_transfer(0xFF) != 0xFF); /* Wait for busy */
sd_cs_high();
spi_transfer(0xFF);
}
last_err = SD_OK;
return SD_OK;
}
SdError sd_last_error(void) { return last_err; }FAT32 Header
(fat32.h)
#ifndef FAT32_H
#define FAT32_H
#include <stdint.h>
#include <stdbool.h>
#define FAT32_BLOCK_SIZE 512
#define FAT32_EOF 0x0FFFFFF8
#define FAT32_ATTR_ARCHIVE 0x20
typedef struct {
uint32_t first_cluster;
uint32_t current_cluster;
uint32_t file_size;
uint32_t current_offset;
uint16_t current_sector_in_cluster;
bool open;
} FatFile;
typedef struct {
int (*read_blocks)(void *ctx, uint32_t block, uint8_t *buf, uint32_t count);
int (*write_blocks)(void *ctx, uint32_t block, const uint8_t *buf, uint32_t count);
void *ctx;
} FatBlockDevice;
typedef struct {
uint32_t bytes_per_sector;
uint8_t sectors_per_cluster;
uint16_t reserved_sectors;
uint8_t num_fats;
uint32_t fat_sectors;
uint32_t root_cluster;
uint32_t data_start_sector;
uint32_t total_sectors;
} FatVolume;
int fat32_format(FatBlockDevice *dev, uint32_t total_sectors, FatVolume *vol);
int fat32_create_file(FatVolume *vol, FatFile *file, const char *name);
int fat32_write(FatVolume *vol, FatFile *file, const uint8_t *data, uint32_t len);
int fat32_close(FatVolume *vol, FatFile *file);
uint32_t fat32_fat_read(FatVolume *vol, uint32_t cluster);
void fat32_fat_write(FatVolume *vol, uint32_t cluster, uint32_t value);
uint32_t fat32_alloc_cluster(FatVolume *vol);
#endifFAT32 Implementation
(fat32.c)
#include "fat32.h"
#include <string.h>
static FatBlockDevice *g_dev;
static uint8_t sector_buf[512];
/* Helper: read a sector */
static int read_sector(uint32_t sector) {
return g_dev->read_blocks(g_dev->ctx, sector, sector_buf, 1);
}
/* Helper: write a sector */
static int write_sector(uint32_t sector) {
return g_dev->write_blocks(g_dev->ctx, sector, sector_buf, 1);
}
/* Read a FAT entry (32-bit) */
uint32_t fat32_fat_read(FatVolume *vol, uint32_t cluster) {
uint32_t fat_offset = cluster * 4;
uint32_t fat_sector = vol->reserved_sectors + (fat_offset / 512);
uint32_t ent_offset = fat_offset % 512;
read_sector(fat_sector);
return (*(uint32_t *)(sector_buf + ent_offset)) & 0x0FFFFFFF;
}
/* Write a FAT entry (updates both FAT copies) */
void fat32_fat_write(FatVolume *vol, uint32_t cluster, uint32_t value) {
uint32_t fat_offset = cluster * 4;
uint32_t fat_sector = vol->reserved_sectors + (fat_offset / 512);
uint32_t ent_offset = fat_offset % 512;
for (uint8_t f = 0; f < vol->num_fats; f++) {
read_sector(fat_sector + f * vol->fat_sectors);
uint32_t existing = (*(uint32_t *)(sector_buf + ent_offset)) & 0xF0000000;
*(uint32_t *)(sector_buf + ent_offset) = existing | (value & 0x0FFFFFFF);
write_sector(fat_sector + f * vol->fat_sectors);
}
}
/* Allocate a free cluster, return cluster number or 0 on failure */
uint32_t fat32_alloc_cluster(FatVolume *vol) {
for (uint32_t c = 2; c < vol->fat_sectors * 512 / 4; c++) {
if (fat32_fat_read(vol, c) == 0) {
fat32_fat_write(vol, c, FAT32_EOF);
return c;
}
}
return 0;
}
/* Format a volume with FAT32 */
int fat32_format(FatBlockDevice *dev, uint32_t total_sectors, FatVolume *vol) {
g_dev = dev;
uint8_t spc = 8; /* 8 sectors per cluster = 4KB */
uint16_t reserved = 32;
uint8_t num_fats = 2;
/* Calculate FAT size */
uint32_t data_sectors = total_sectors - reserved;
uint32_t total_clusters = data_sectors / spc;
uint32_t fat_size = (total_clusters * 4 + 511) / 512 + 1;
/* Recalculate with FAT size accounted for */
data_sectors = total_sectors - reserved - (fat_size * num_fats);
total_clusters = data_sectors / spc;
fat_size = (total_clusters * 4 + 511) / 512 + 1;
vol->bytes_per_sector = 512;
vol->sectors_per_cluster = spc;
vol->reserved_sectors = reserved;
vol->num_fats = num_fats;
vol->fat_sectors = fat_size;
vol->root_cluster = 2;
vol->data_start_sector = reserved + (fat_size * num_fats);
vol->total_sectors = total_sectors;
/* Build boot sector */
memset(sector_buf, 0, 512);
sector_buf[0] = 0xEB; sector_buf[1] = 0x58; sector_buf[2] = 0x90;
memcpy(sector_buf + 3, "MSDOS5.0", 8);
*(uint16_t *)(sector_buf + 11) = 512;
sector_buf[13] = spc;
*(uint16_t *)(sector_buf + 14) = reserved;
sector_buf[16] = num_fats;
*(uint16_t *)(sector_buf + 19) = 0; /* 16-bit total = 0 for FAT32 */
sector_buf[21] = 0xF8;
*(uint16_t *)(sector_buf + 22) = 0; /* 16-bit FAT size = 0 */
*(uint16_t *)(sector_buf + 24) = 0; /* Sectors per track */
*(uint16_t *)(sector_buf + 26) = 0; /* Heads */
*(uint32_t *)(sector_buf + 28) = total_sectors;
*(uint32_t *)(sector_buf + 32) = fat_size;
*(uint32_t *)(sector_buf + 40) = 2; /* Root cluster */
*(uint16_t *)(sector_buf + 44) = 1; /* FSInfo sector */
*(uint16_t *)(sector_buf + 48) = 6; /* Backup boot */
sector_buf[64] = 0x80; /* Drive number */
sector_buf[66] = 0x29; /* Boot signature */
memcpy(sector_buf + 67, "NO NAME ", 11);
memcpy(sector_buf + 78, "FAT32 ", 8);
sector_buf[510] = 0x55;
sector_buf[511] = 0xAA;
write_sector(0);
/* Backup boot sector */
write_sector(6);
/* FSInfo sector */
memset(sector_buf, 0, 512);
*(uint32_t *)(sector_buf + 0) = 0x41615252;
*(uint32_t *)(sector_buf + 484) = 0x61417272;
*(uint32_t *)(sector_buf + 488) = total_clusters - 1; /* Free clusters */
*(uint32_t *)(sector_buf + 492) = 2; /* Next free cluster */
sector_buf[510] = 0x55; sector_buf[511] = 0xAA;
write_sector(1);
/* Clear FAT tables */
memset(sector_buf, 0, 512);
/* FAT[0] = media descriptor, FAT[1] = reserved, FAT[2] = EOF (root dir) */
*(uint32_t *)(sector_buf + 0) = 0x0FFFFFF0 | 0xF8;
*(uint32_t *)(sector_buf + 4) = 0x0FFFFFFF;
*(uint32_t *)(sector_buf + 8) = FAT32_EOF;
for (uint8_t f = 0; f < num_fats; f++) {
for (uint32_t s = 0; s < fat_size; s++) {
if (s == 0) {
write_sector(reserved + f * fat_size);
} else {
memset(sector_buf, 0, 512);
write_sector(reserved + f * fat_size + s);
}
}
}
/* Clear root directory cluster */
memset(sector_buf, 0, 512);
for (uint32_t s = 0; s < spc; s++) {
write_sector(vol->data_start_sector + s);
}
return 0;
}
/* Create a new file in the root directory */
int fat32_create_file(FatVolume *vol, FatFile *file, const char *name) {
g_dev = vol->data_start_sector ? g_dev : g_dev; /* ensure g_dev set */
/* Allocate first cluster for the file */
uint32_t cluster = fat32_alloc_cluster(vol);
if (cluster == 0) return -1;
/* Build directory entry */
memset(sector_buf, 0, 512);
/* 8.3 filename: space-pad name and extension */
char fat_name[11];
memset(fat_name, ' ', 11);
const char *dot = strchr(name, '.');
if (dot) {
int name_len = (int)(dot - name);
if (name_len > 8) name_len = 8;
memcpy(fat_name, name, name_len);
const char *ext = dot + 1;
int ext_len = (int)strlen(ext);
if (ext_len > 3) ext_len = 3;
memcpy(fat_name + 8, ext, ext_len);
} else {
int name_len = (int)strlen(name);
if (name_len > 8) name_len = 8;
memcpy(fat_name, name, name_len);
}
memcpy(sector_buf + 0, fat_name, 11);
sector_buf[11] = FAT32_ATTR_ARCHIVE;
*(uint32_t *)(sector_buf + 28) = 0; /* File size */
*(uint16_t *)(sector_buf + 26) = cluster & 0xFFFF;
*(uint16_t *)(sector_buf + 20) = (cluster >> 16) & 0xFFFF;
/* Write to root directory cluster */
uint32_t root_sector = vol->data_start_sector;
read_sector(root_sector);
/* Find first empty entry */
int entry = -1;
for (int i = 0; i < 512; i += 32) {
if (sector_buf[i] == 0x00 || sector_buf[i] == 0xE5) {
entry = i;
break;
}
}
if (entry < 0) return -1; /* Root dir full */
memcpy(sector_buf + entry, sector_buf, 32); /* Copy entry to position */
/* Actually write the entry properly */
memset(sector_buf + entry, 0, 32);
memcpy(sector_buf + entry, fat_name, 11);
sector_buf[entry + 11] = FAT32_ATTR_ARCHIVE;
*(uint32_t *)(sector_buf + entry + 28) = 0;
*(uint16_t *)(sector_buf + entry + 26) = cluster & 0xFFFF;
*(uint16_t *)(sector_buf + entry + 20) = (cluster >> 16) & 0xFFFF;
write_sector(root_sector);
file->first_cluster = cluster;
file->current_cluster = cluster;
file->file_size = 0;
file->current_offset = 0;
file->current_sector_in_cluster = 0;
file->open = true;
return 0;
}
/* Write data to file with cluster chain management */
int fat32_write(FatVolume *vol, FatFile *file, const uint8_t *data, uint32_t len) {
uint32_t bytes_per_cluster = vol->sectors_per_cluster * 512;
uint32_t written = 0;
while (written < len) {
/* Check if we need a new cluster */
uint32_t offset_in_cluster = file->current_offset % bytes_per_cluster;
if (offset_in_cluster == 0 && file->current_offset > 0) {
uint32_t next = fat32_fat_read(vol, file->current_cluster);
if (next >= FAT32_EOF) {
uint32_t new_cluster = fat32_alloc_cluster(vol);
if (new_cluster == 0) return -1;
fat32_fat_write(vol, file->current_cluster, new_cluster);
file->current_cluster = new_cluster;
} else {
file->current_cluster = next;
}
}
/* Calculate sector within cluster */
uint32_t sector_in_cluster = offset_in_cluster / 512;
uint32_t offset_in_sector = offset_in_cluster % 512;
/* Read-modify-write the sector */
uint32_t abs_sector = vol->data_start_sector +
(file->current_cluster - 2) * vol->sectors_per_cluster + sector_in_cluster;
read_sector(abs_sector);
/* Write data into sector buffer */
uint32_t to_write = len - written;
uint32_t space_in_sector = 512 - offset_in_sector;
if (to_write > space_in_sector) to_write = space_in_sector;
memcpy(sector_buf + offset_in_sector, data + written, to_write);
write_sector(abs_sector);
file->current_offset += to_write;
file->file_size += to_write;
written += to_write;
}
return (int)written;
}
/* Close file: update directory entry with final size */
int fat32_close(FatVolume *vol, FatFile *file) {
if (!file->open) return -1;
/* Update directory entry */
uint32_t root_sector = vol->data_start_sector;
read_sector(root_sector);
for (int i = 0; i < 512; i += 32) {
uint16_t low = *(uint16_t *)(sector_buf + i + 26);
uint16_t high = *(uint16_t *)(sector_buf + i + 20);
uint32_t cluster = (uint32_t)high << 16 | low;
if (cluster == file->first_cluster) {
*(uint32_t *)(sector_buf + i + 28) = file->file_size;
write_sector(root_sector);
break;
}
}
file->open = false;
return 0;
}Buffered Logger
(logger.h)
#ifndef LOGGER_H
#define LOGGER_H
#include <stdint.h>
#include <stdbool.h>
#include "fat32.h"
#define LOGGER_BUF_SIZE 512
typedef struct {
FatVolume *vol;
FatFile file;
uint8_t buf[LOGGER_BUF_SIZE];
uint32_t buf_pos;
bool open;
} Logger;
void logger_init(Logger *log, FatVolume *vol);
int logger_open(Logger *log, const char *filename);
int logger_write(Logger *log, const char *data, uint32_t len);
int logger_printf(Logger *log, const char *fmt, ...);
int logger_flush(Logger *log);
int logger_close(Logger *log);
#endifBuffered Logger
(logger.c)
#include "logger.h"
#include <stdarg.h>
#include <string.h>
void logger_init(Logger *log, FatVolume *vol) {
log->vol = vol;
log->buf_pos = 0;
log->open = false;
}
int logger_open(Logger *log, const char *filename) {
int ret = fat32_create_file(log->vol, &log->file, filename);
if (ret == 0) {
log->buf_pos = 0;
log->open = true;
}
return ret;
}
int logger_write(Logger *log, const char *data, uint32_t len) {
if (!log->open) return -1;
uint32_t written = 0;
while (written < len) {
uint32_t space = LOGGER_BUF_SIZE - log->buf_pos;
uint32_t to_copy = len - written;
if (to_copy > space) to_copy = space;
memcpy(log->buf + log->buf_pos, data + written, to_copy);
log->buf_pos += to_copy;
written += to_copy;
if (log->buf_pos >= LOGGER_BUF_SIZE) {
if (logger_flush(log) < 0) return -1;
}
}
return (int)written;
}
/* Minimal integer-to-string for embedded */
static int int_to_str(char *buf, int val) {
if (val == 0) { buf[0] = '0'; return 1; }
int neg = val < 0;
if (neg) val = -val;
char tmp[12];
int i = 0;
while (val > 0) {
tmp[i++] = '0' + (val % 10);
val /= 10;
}
if (neg) tmp[i++] = '-';
for (int j = 0; j < i; j++) {
buf[j] = tmp[i - 1 - j];
}
return i;
}
int logger_printf(Logger *log, const char *fmt, ...) {
if (!log->open) return -1;
char tmp[64];
int total = 0;
va_list args;
va_start(args, fmt);
while (*fmt) {
if (*fmt == '%' && *(fmt + 1)) {
fmt++;
if (*fmt == 'd') {
int val = va_arg(args, int);
int n = int_to_str(tmp, val);
logger_write(log, tmp, (uint32_t)n);
total += n;
} else if (*fmt == 's') {
const char *s = va_arg(args, const char *);
int n = (int)strlen(s);
logger_write(log, s, (uint32_t)n);
total += n;
} else if (*fmt == '%') {
logger_write(log, "%", 1);
total++;
}
} else {
logger_write(log, (const char *)fmt, 1);
total++;
}
fmt++;
}
va_end(args);
return total;
}
int logger_flush(Logger *log) {
if (log->buf_pos == 0) return 0;
int ret = fat32_write(log->vol, &log->file, log->buf, log->buf_pos);
log->buf_pos = 0;
return ret;
}
int logger_close(Logger *log) {
logger_flush(log);
int ret = fat32_close(log->vol, &log->file);
log->open = false;
return ret;
}Main Application
(main.c)
#include "sd_spi.h"
#include "fat32.h"
#include "logger.h"
#include <stdint.h>
/* GPIO for LED */
#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830)
#define GPIOA_MODER (*(volatile uint32_t *)0x40020000)
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014)
#define GPIOA_BSRR (*(volatile uint32_t *)0x40020018)
/* I2C1 registers (for sensor reads) */
#define I2C1_CR1 (*(volatile uint32_t *)0x40005400)
#define I2C1_CR2 (*(volatile uint32_t *)0x40005404)
#define I2C1_DR (*(volatile uint32_t *)0x40005410)
#define I2C1_SR1 (*(volatile uint32_t *)0x40005414)
#define I2C1_SR2 (*(volatile uint32_t *)0x40005418)
/* Simulated sensor values (replace with actual I2C reads) */
static int16_t read_temperature(void) {
/* In production: I2C read from BMP280/BME280 */
return 22; /* Simulated 22C */
}
static uint16_t read_humidity(void) {
/* In production: I2C read from SHT31 */
return 55; /* Simulated 55% */
}
static uint32_t read_pressure(void) {
/* In production: I2C read from BMP280 */
return 101325; /* Simulated 1013.25 hPa */
}
/* Simple delay */
static void delay_ms(uint32_t ms) {
volatile uint32_t *rvr = (volatile uint32_t *)0xE000E014;
volatile uint32_t *cvr = (volatile uint32_t *)0xE000E018;
volatile uint32_t *csr = (volatile uint32_t *)0xE000E010;
*rvr = 8000 - 1;
*cvr = 0;
*csr = 0x5;
while (ms--) { while (!(*csr & (1 << 16))); }
*csr = 0;
}
/* Block device wrapper for SD card */
static int sd_read_block(void *ctx, uint32_t block, uint8_t *buf, uint32_t count) {
(void)ctx;
return sd_read_blocks(block, buf, count) == SD_OK ? 0 : -1;
}
static int sd_write_block(void *ctx, uint32_t block, const uint8_t *buf, uint32_t count) {
(void)ctx;
return sd_write_blocks(block, buf, count) == SD_OK ? 0 : -1;
}
int main(void) {
/* Enable GPIOA */
RCC_AHB1ENR |= (1 << 0);
/* PA13 as output for LED */
GPIOA_MODER &= ~(0x3 << 26);
GPIOA_MODER |= (0x1 << 26);
/* Initialize SD card */
if (sd_init() != SD_OK) {
/* SD init failed - blink LED fast */
while (1) {
GPIOA_ODR ^= (1 << 13);
delay_ms(100);
}
}
/* Set up block device */
FatBlockDevice block_dev;
block_dev.read_blocks = sd_read_block;
block_dev.write_blocks = sd_write_block;
block_dev.ctx = NULL;
/* Format as FAT32 (skip if already formatted) */
FatVolume vol;
fat32_format(&block_dev, 65536, &vol); /* 32MB image */
/* Initialize logger */
Logger log;
logger_init(&log, &vol);
logger_open(&log, "SENSORLOGCSV"); /* 8.3: SENSORLOG.CSV */
/* Write CSV header */
logger_printf(&log, "timestamp,temperature_c,humidity_pct,pressure_pa\r\n");
/* Log loop */
uint32_t tick = 0;
while (1) {
int16_t temp = read_temperature();
uint16_t hum = read_humidity();
uint32_t pres = read_pressure();
logger_printf(&log, "%d,%d,%d,%d\r\n", tick, temp, hum, (int)pres);
tick++;
/* Flush every 10 samples */
if (tick % 10 == 0) {
logger_flush(&log);
}
GPIOA_ODR ^= (1 << 13);
delay_ms(100);
}
logger_close(&log);
return 0;
}Makefile
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
CFLAGS = -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Os -g -Wall -Wextra -nostdlib -ffreestanding
LDFLAGS = -T linker.ld
all: logger.elf logger.bin
logger.elf: startup.c sd_spi.c fat32.c logger.c main.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
logger.bin: logger.elf
$(OBJCOPY) -O binary $< $@
run: logger.bin
qemu-system-arm -M raspi2 -sd sdcard.img -kernel logger.bin -S -s &
clean:
rm -f logger.elf logger.binBuild and Run
makeImplementation: Rust
Project Setup
cargo init --name logger-rust
cd logger-rustCargo.toml
[package]
name = "logger-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "0.2"
embedded-sdmmc = "0.5"
embedded-hal = "0.2"
[profile.release]
opt-level = "s"
lto = true.cargo/config.toml
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
runner = "qemu-system-arm -M raspi2 -sd sdcard.img -kernel"
rustflags = ["-C", "link-arg=-Tlink.x"]memory.x
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}
build.rs
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
}src/main.rs
#![no_std]
#![no_main]
use core::arch::asm;
use cortex_m::peripheral::SYST;
use cortex_m_rt::{entry, exception, ExceptionFrame};
/* ============================================================
* Block Device Trait Abstraction
* ============================================================ */
/// Block device interface β matches embedded-hal patterns
pub trait BlockDevice {
type Error;
const BLOCK_SIZE: u32 = 512;
fn read(&mut self, block: u32, buf: &mut [u8]) -> Result<(), Self::Error>;
fn write(&mut self, block: u32, buf: &[u8]) -> Result<(), Self::Error>;
fn num_blocks(&self) -> u32;
}
/* ============================================================
* SD Card SPI Driver
* ============================================================ */
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SdError {
Timeout,
Response,
DataToken,
Write,
Init,
}
pub struct SdCardSpi {
is_sdhc: bool,
}
impl SdCardSpi {
pub const fn new() -> Self {
Self { is_sdhc: false }
}
unsafe fn spi_transfer(&self, data: u8) -> u8 {
let sr = 0x4001_3004 as *const u32;
let dr = 0x4001_3008 as *mut u32;
while sr.read_volatile() & (1 << 1) == 0 {}
dr.write_volatile(data as u32);
while sr.read_volatile() & (1 << 0) == 0 {}
dr.read_volatile() as u8
}
unsafe fn cs_low(&self) {
let bsrr = 0x4002_0018 as *mut u32;
bsrr.write_volatile(1 << (4 + 16));
}
unsafe fn cs_high(&self) {
let bsrr = 0x4002_0018 as *mut u32;
bsrr.write_volatile(1 << 4);
}
unsafe fn sd_cmd(&self, cmd: u8, arg: u32, crc: u8) -> u8 {
self.cs_low();
self.spi_transfer(0x40 | cmd);
self.spi_transfer((arg >> 24) as u8);
self.spi_transfer((arg >> 16) as u8);
self.spi_transfer((arg >> 8) as u8);
self.spi_transfer(arg as u8);
self.spi_transfer(crc);
let mut response: u8;
let mut timeout = 1000;
loop {
response = self.spi_transfer(0xFF);
if response != 0xFF || timeout == 0 { break; }
timeout -= 1;
}
response
}
pub unsafe fn init(&mut self) -> Result<(), SdError> {
// SPI init (simplified β see C version for full GPIO setup)
let rcc = 0x4002_3830 as *mut u32;
rcc.write_volatile(rcc.read_volatile() | (1 << 0));
self.cs_high();
// 80 clock pulses
for _ in 0..10 { self.spi_transfer(0xFF); }
// CMD0
let r1 = self.sd_cmd(0, 0, 0x95);
self.cs_high();
self.spi_transfer(0xFF);
if r1 != 0x01 { return Err(SdError::Init); }
// CMD8
let r1 = self.sd_cmd(8, 0x1AA, 0x87);
let mut r7: u32 = 0;
for _ in 0..4 { r7 = (r7 << 8) | self.spi_transfer(0xFF) as u32; }
self.cs_high();
self.spi_transfer(0xFF);
if r1 & 0x04 != 0 || (r7 & 0xFFF) != 0x1AA {
return Err(SdError::Init);
}
// ACMD41 loop
for _ in 0..1000 {
let r1 = self.sd_cmd(55, 0, 0xFF);
self.cs_high();
self.spi_transfer(0xFF);
let r1 = self.sd_cmd(41, 0x4000_0000, 0xFF);
self.cs_high();
self.spi_transfer(0xFF);
if r1 == 0x00 { break; }
}
// CMD58: check CCS
let r1 = self.sd_cmd(58, 0, 0xFF);
let mut ocr: u32 = 0;
for _ in 0..4 { ocr = (ocr << 8) | self.spi_transfer(0xFF) as u32; }
self.cs_high();
self.spi_transfer(0xFF);
self.is_sdhc = (ocr >> 30) & 1 == 1;
// CMD59: CRC off
self.sd_cmd(59, 0, 0xFF);
self.cs_high();
self.spi_transfer(0xFF);
Ok(())
}
}
impl BlockDevice for SdCardSpi {
type Error = SdError;
fn read(&mut self, block: u32, buf: &mut [u8]) -> Result<(), Self::Error> {
unsafe {
let addr = if self.is_sdhc { block } else { block * 512 };
let r1 = self.sd_cmd(17, addr, 0xFF);
if r1 != 0x00 { self.cs_high(); self.spi_transfer(0xFF); return Err(SdError::Response); }
// Wait for 0xFE
let mut timeout = 10000;
while self.spi_transfer(0xFF) != 0xFE {
timeout -= 1;
if timeout == 0 { self.cs_high(); return Err(SdError::DataToken); }
}
for b in buf.iter_mut().take(512) {
*b = self.spi_transfer(0xFF);
}
self.spi_transfer(0xFF); // CRC
self.spi_transfer(0xFF);
self.cs_high();
self.spi_transfer(0xFF);
}
Ok(())
}
fn write(&mut self, block: u32, buf: &[u8]) -> Result<(), Self::Error> {
unsafe {
let addr = if self.is_sdhc { block } else { block * 512 };
let r1 = self.sd_cmd(24, addr, 0xFF);
if r1 != 0x00 { self.cs_high(); self.spi_transfer(0xFF); return Err(SdError::Response); }
self.spi_transfer(0xFE);
for &b in buf.iter().take(512) {
self.spi_transfer(b);
}
self.spi_transfer(0xFF); // CRC
self.spi_transfer(0xFF);
let resp = self.spi_transfer(0xFF) & 0x1F;
if resp != 0x05 { self.cs_high(); return Err(SdError::Write); }
while self.spi_transfer(0xFF) != 0xFF {}
self.cs_high();
self.spi_transfer(0xFF);
}
Ok(())
}
fn num_blocks(&self) -> u32 {
65536 // 32MB for QEMU
}
}
/* ============================================================
* Buffered Writer
* ============================================================ */
const BUF_SIZE: usize = 512;
pub struct BufWriter<D: BlockDevice> {
device: D,
buffer: [u8; BUF_SIZE],
pos: usize,
current_block: u32,
}
impl<D: BlockDevice> BufWriter<D> {
pub fn new(device: D) -> Self {
Self {
device,
buffer: [0; BUF_SIZE],
pos: 0,
current_block: 0,
}
}
pub fn write_byte(&mut self, byte: u8) -> Result<(), D::Error> {
self.buffer[self.pos] = byte;
self.pos += 1;
if self.pos >= BUF_SIZE {
self.flush()?;
}
Ok(())
}
pub fn write_str(&mut self, s: &str) -> Result<(), D::Error> {
for &b in s.as_bytes() {
self.write_byte(b)?;
}
Ok(())
}
pub fn flush(&mut self) -> Result<(), D::Error> {
if self.pos > 0 {
// Pad buffer to 512 bytes
for b in self.buffer.iter_mut().skip(self.pos).take(BUF_SIZE - self.pos) {
*b = 0;
}
self.device.write(self.current_block, &self.buffer)?;
self.current_block += 1;
self.pos = 0;
}
Ok(())
}
}
/* ============================================================
* Minimal FAT32 Writer
* ============================================================ */
pub struct Fat32Writer<D: BlockDevice> {
device: D,
total_sectors: u32,
spc: u8,
reserved: u16,
num_fats: u8,
fat_size: u32,
data_start: u32,
next_free_cluster: u32,
}
impl<D: BlockDevice> Fat32Writer<D> {
pub fn new(mut device: D, total_sectors: u32) -> Self {
let spc = 8;
let reserved = 32;
let num_fats = 2;
let data_sectors = total_sectors - reserved as u32;
let total_clusters = data_sectors / spc as u32;
let fat_size = (total_clusters * 4 + 511) / 512 + 1;
Self {
device,
total_sectors,
spc,
reserved,
num_fats,
fat_size,
data_start: reserved as u32 + fat_size * num_fats as u32,
next_free_cluster: 3, // Cluster 2 = root dir
}
}
pub fn format(&mut self) -> Result<(), D::Error> {
let mut buf = [0u8; 512];
// Boot sector
buf[0] = 0xEB; buf[1] = 0x58; buf[2] = 0x90;
buf[3..11].copy_from_slice(b"MSDOS5.0");
buf[11..13].copy_from_slice(&512u16.to_le_bytes());
buf[13] = self.spc;
buf[14..16].copy_from_slice(&self.reserved.to_le_bytes());
buf[16] = self.num_fats;
buf[21] = 0xF8;
buf[28..32].copy_from_slice(&self.total_sectors.to_le_bytes());
buf[32..36].copy_from_slice(&self.fat_size.to_le_bytes());
buf[40..44].copy_from_slice(&2u32.to_le_bytes());
buf[44..46].copy_from_slice(&1u16.to_le_bytes());
buf[48..50].copy_from_slice(&6u16.to_le_bytes());
buf[64] = 0x80;
buf[66] = 0x29;
buf[67..78].copy_from_slice(b"NO NAME ");
buf[78..86].copy_from_slice(b"FAT32 ");
buf[510] = 0x55; buf[511] = 0xAA;
self.device.write(0, &buf)?;
// FAT init
buf.fill(0);
buf[0] = 0xF8; buf[1] = 0xFF; buf[2] = 0xFF; buf[3] = 0x0F; // FAT[0]
buf[4] = 0xFF; buf[5] = 0xFF; buf[6] = 0xFF; buf[7] = 0x0F; // FAT[1]
buf[8] = 0xF8; buf[9] = 0xFF; buf[10] = 0xFF; buf[11] = 0x0F; // FAT[2] = EOF
for f in 0..self.num_fats {
self.device.write(self.reserved as u32 + f as u32 * self.fat_size, &buf)?;
let empty = [0u8; 512];
for s in 1..self.fat_size {
self.device.write(self.reserved as u32 + f as u32 * self.fat_size + s, &empty)?;
}
}
// Clear root dir cluster
let empty = [0u8; 512];
for s in 0..self.spc {
self.device.write(self.data_start + s as u32, &empty)?;
}
Ok(())
}
pub fn create_file(&mut self, name: &str) -> Result<u32, D::Error> {
let cluster = self.next_free_cluster;
self.next_free_cluster += 1;
// Write FAT entry for this cluster (EOF)
let mut fat_sector = self.reserved as u32;
let fat_offset = cluster * 4;
let fat_sector_offset = fat_offset / 512;
let ent_offset = (fat_offset % 512) as usize;
let mut buf = [0u8; 512];
self.device.read(fat_sector + fat_sector_offset, &mut buf)?;
let val = 0x0FFF_FFFFu32;
buf[ent_offset..ent_offset + 4].copy_from_slice(&val.to_le_bytes());
self.device.write(fat_sector + fat_sector_offset, &buf)?;
// Write directory entry
let mut dir_buf = [0u8; 512];
self.device.read(self.data_start, &mut dir_buf)?;
// Find empty entry
let mut entry_offset = None;
for i in (0..512).step_by(32) {
if dir_buf[i] == 0x00 || dir_buf[i] == 0xE5 {
entry_offset = Some(i);
break;
}
}
let offset = entry_offset.ok_or(SdError::Write)?;
// Build 8.3 name
let mut fat_name = [b' '; 11];
if let Some(dot) = name.find('.') {
let (n, e) = name.split_at(dot);
let n = n.as_bytes();
let e = e[1..].as_bytes();
fat_name[..n.len().min(8)].copy_from_slice(&n[..n.len().min(8)]);
fat_name[8..8 + e.len().min(3)].copy_from_slice(&e[..e.len().min(3)]);
} else {
let n = name.as_bytes();
fat_name[..n.len().min(8)].copy_from_slice(&n[..n.len().min(8)]);
}
dir_buf[offset..offset + 11].copy_from_slice(&fat_name);
dir_buf[offset + 11] = 0x20; // Archive
dir_buf[offset + 26] = (cluster & 0xFFFF) as u8;
dir_buf[offset + 27] = ((cluster >> 8) & 0xFF) as u8;
dir_buf[offset + 20] = ((cluster >> 16) & 0xFF) as u8;
dir_buf[offset + 21] = ((cluster >> 24) & 0xFF) as u8;
self.device.write(self.data_start, &dir_buf)?;
Ok(cluster)
}
pub fn write_file_data(&mut self, cluster: u32, data: &[u8]) -> Result<(), D::Error> {
let bytes_per_cluster = self.spc as u32 * 512;
let start_sector = self.data_start + (cluster - 2) * self.spc as u32;
let mut sector_buf = [0u8; 512];
let mut offset = 0;
while offset < data.len() {
let sector = start_sector + (offset / 512) as u32;
let sector_off = offset % 512;
let to_write = (512 - sector_off).min(data.len() - offset);
// Read existing sector if partial write
if sector_off != 0 || to_write != 512 {
self.device.read(sector, &mut sector_buf)?;
}
sector_buf[sector_off..sector_off + to_write]
.copy_from_slice(&data[offset..offset + to_write]);
self.device.write(sector, §or_buf)?;
offset += to_write;
}
Ok(())
}
}
/* ============================================================
* Sensor Reading (simulated)
* ============================================================ */
fn read_temperature() -> i16 { 22 }
fn read_humidity() -> u16 { 55 }
fn read_pressure() -> u32 { 101325 }
/* ============================================================
* GPIO and Delay
* ============================================================ */
const RCC_AHB1ENR: *mut u32 = 0x4002_3830 as _;
const GPIOA_MODER: *mut u32 = 0x4002_0000 as _;
const GPIOA_ODR: *mut u32 = 0x4002_0014 as _;
fn delay_ms(ms: u32) {
unsafe {
let systick = &*SYST::PTR;
systick.set_reload(8000 - 1);
systick.clear_current();
systick.enable_counter();
for _ in 0..ms {
while !systick.has_wrapped() {}
}
systick.disable_counter();
}
}
/* ============================================================
* Main
* ============================================================ */
#[entry]
fn main() -> ! {
unsafe {
(*RCC_AHB1ENR) |= 1 << 0;
let moder = (*GPIOA_MODER).read_volatile();
(*GPIOA_MODER).write_volatile((moder & !(0x3 << 26)) | (0x1 << 26));
}
// Initialize SD card
let mut sd = SdCardSpi::new();
if sd.init().is_err() {
// Blink fast on error
loop {
unsafe { (*GPIOA_ODR).write_volatile((*GPIOA_ODR).read_volatile() ^ (1 << 13)); }
delay_ms(100);
}
}
// Format FAT32
let mut fat = Fat32Writer::new(sd, 65536);
fat.format().unwrap();
// Create file
let cluster = fat.create_file("SENSORLOG.CSV").unwrap();
// Write CSV header
fat.write_file_data(cluster, b"timestamp,temperature_c,humidity_pct,pressure_pa\r\n").unwrap();
// Log loop
let mut tick: u32 = 0;
loop {
let temp = read_temperature();
let hum = read_humidity();
let pres = read_pressure();
let mut line = [0u8; 64];
let len = core::fmt::Write::write_fmt(
&mut StrWriter(&mut line),
format_args!("{},{},{},{}\r\n", tick, temp, hum, pres),
);
let _ = len;
// Find actual length
let mut actual_len = 0;
for (i, &b) in line.iter().enumerate() {
if b == 0 { actual_len = i; break; }
}
if actual_len == 0 { actual_len = line.len(); }
fat.write_file_data(cluster, &line[..actual_len]).unwrap();
tick += 1;
if tick % 10 == 0 {
// Flush would go here in a full implementation
}
unsafe { (*GPIOA_ODR).write_volatile((*GPIOA_ODR).read_volatile() ^ (1 << 13)); }
delay_ms(100);
}
}
struct StrWriter<'a>(&'a mut [u8]);
impl core::fmt::Write for StrWriter<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let bytes = s.as_bytes();
let len = bytes.len().min(self.0.len());
self.0[..len].copy_from_slice(&bytes[..len]);
Ok(())
}
}
#[exception]
fn HardFault(_ef: &ExceptionFrame) -> ! {
loop {}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}Build and Run
cargo build --releaseImplementation: Ada
Project Structure
logger-ada/
βββ logger.gpr
βββ src/
β βββ sd_spi.ads
β βββ sd_spi.adb
β βββ fat32.ads
β βββ fat32.adb
β βββ block_device.ads
β βββ buffered_io.ads
β βββ buffered_io.adb
β βββ main.adb
Project File
(logger.gpr)
project Logger is
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Main use ("main.adb");
for Target use "arm-eabi";
package Compiler is
for Default_Switches ("Ada") use
("-O2", "-g", "-mcpu=cortex-m4", "-mthumb",
"-fstack-check", "-gnatp", "-gnata");
end Compiler;
package Linker is
for Default_Switches ("Ada") use
("-T", "linker.ld", "-nostartfiles");
end Linker;
end Logger;Block Device
Abstraction (block_device.ads)
with System;
with Interfaces; use Interfaces;
package Block_Device is
Block_Size : constant := 512;
type Block_Status is (Status_OK, Status_Error);
-- Abstract block device interface
type Block_Device_Interface is abstract tagged limited null record;
procedure Read_Blocks
(Dev : in out Block_Device_Interface;
Block : Unsigned_32;
Buffer : out System.Address;
Count : Unsigned_32;
Status : out Block_Status) is abstract;
procedure Write_Blocks
(Dev : in out Block_Device_Interface;
Block : Unsigned_32;
Buffer : in System.Address;
Count : Unsigned_32;
Status : out Block_Status) is abstract;
function Num_Blocks
(Dev : Block_Device_Interface) return Unsigned_32 is abstract;
end Block_Device;SD SPI Driver
(sd_spi.ads)
with Block_Device; use Block_Device;
with Interfaces; use Interfaces;
with System;
package SD_SPI is
type SD_Error is
(SD_OK, SD_Timeout, SD_Response, SD_Data_Token,
SD_Write, SD_Init, SD_CRC);
type SD_Card_Info is record
Is_SDHC : Boolean := False;
Capacity_Blocks : Unsigned_32 := 0;
Card_Type : Unsigned_8 := 0;
end record;
type SD_Card_Device is new Block_Device_Interface with private;
procedure Initialize
(Card : in out SD_Card_Device;
Err : out SD_Error);
procedure Get_Info
(Card : SD_Card_Device;
Info : out SD_Card_Info);
overriding procedure Read_Blocks
(Dev : in out SD_Card_Device;
Block : Unsigned_32;
Buffer : out System.Address;
Count : Unsigned_32;
Status : out Block_Status);
overriding procedure Write_Blocks
(Dev : in out SD_Card_Device;
Block : Unsigned_32;
Buffer : in System.Address;
Count : Unsigned_32;
Status : out Block_Status);
overriding function Num_Blocks
(Dev : SD_Card_Device) return Unsigned_32;
private
type SD_Card_Device is new Block_Device_Interface with record
Is_SDHC : Boolean := False;
Last_Err : SD_Error := SD_OK;
end record;
end SD_SPI;SD SPI Driver Body
(sd_spi.adb)
with System.Machine_Code; use System.Machine_Code;
with System.Storage_Elements; use System.Storage_Elements;
package body SD_SPI is
SPI1_CR1 : Unsigned_32 with Address => System'To_Address (16#4001_3000#), Volatile => True;
SPI1_SR : Unsigned_32 with Address => System'To_Address (16#4001_3004#), Volatile => True;
SPI1_DR : Unsigned_32 with Address => System'To_Address (16#4001_3008#), Volatile => True;
RCC_AHB1ENR : Unsigned_32 with Address => System'To_Address (16#4002_3830#), Volatile => True;
GPIOA_MODER : Unsigned_32 with Address => System'To_Address (16#4002_0000#), Volatile => True;
GPIOA_AFRL : Unsigned_32 with Address => System'To_Address (16#4002_0020#), Volatile => True;
GPIOA_BSRR : Unsigned_32 with Address => System'To_Address (16#4002_0018#), Volatile => True;
SD_CS_Pin : constant Unsigned_32 := 16#10#; -- PA4
type Buffer_512 is array (0 .. 511) of Unsigned_8;
for Buffer_512'Component_Size use 8;
procedure SPI_Transfer (Data : Unsigned_8; Result : out Unsigned_8) is
begin
while (SPI1_SR and (1 << 1)) = 0 loop null; end loop;
SPI1_DR := Unsigned_32 (Data);
while (SPI1_SR and (1 << 0)) = 0 loop null; end loop;
Result := Unsigned_8 (SPI1_DR and 16#FF#);
end SPI_Transfer;
procedure CS_Low is
begin
GPIOA_BSRR := SD_CS_Pin << 16;
end CS_Low;
procedure CS_High is
begin
GPIOA_BSRR := SD_CS_Pin;
end CS_High;
procedure Send_Command
(Cmd : Unsigned_8;
Arg : Unsigned_32;
Crc : Unsigned_8;
R1 : out Unsigned_8)
is
Temp : Unsigned_8;
Timeout : Natural := 1000;
begin
CS_Low;
SPI_Transfer (16#40# or Cmd, Temp);
SPI_Transfer (Unsigned_8 (Shift_Right (Arg, 24) and 16#FF#), Temp);
SPI_Transfer (Unsigned_8 (Shift_Right (Arg, 16) and 16#FF#), Temp);
SPI_Transfer (Unsigned_8 (Shift_Right (Arg, 8) and 16#FF#), Temp);
SPI_Transfer (Unsigned_8 (Arg and 16#FF#), Temp);
SPI_Transfer (Crc, Temp);
loop
SPI_Transfer (16#FF#, R1);
exit when R1 /= 16#FF# or Timeout = 0;
Timeout := Timeout - 1;
end loop;
end Send_Command;
procedure Initialize
(Card : in out SD_Card_Device;
Err : out SD_Error)
is
R1, Temp : Unsigned_8;
begin
-- Enable clocks
RCC_AHB1ENR := RCC_AHB1ENR or (1 << 0);
-- GPIO setup: PA5=SCK, PA6=MISO, PA7=MOSI as AF5
declare
Moder : constant Unsigned_32 := GPIOA_MODER;
begin
GPIOA_MODER := (Moder and not (16#3F# << 10)) or (16#2A# << 10);
end;
declare
Afrl : constant Unsigned_32 := GPIOA_AFRL;
begin
GPIOA_AFRL := (Afrl and not (16#FFF# << 20)) or (16#555# << 20);
end;
-- PA4 CS: output
declare
Moder : constant Unsigned_32 := GPIOA_MODER;
begin
GPIOA_MODER := (Moder and not (16#3# << 8)) or (16#1# << 8);
end;
CS_High;
-- SPI init
SPI1_CR1 := (1 << 9) or (1 << 8) or (1 << 2) or (7 << 3);
SPI1_CR1 := SPI1_CR1 or (1 << 6);
-- 80 clocks
CS_High;
for I in 1 .. 10 loop
SPI_Transfer (16#FF#, Temp);
end loop;
-- CMD0
Send_Command (0, 0, 16#95#, R1);
CS_High; SPI_Transfer (16#FF#, Temp);
if R1 /= 16#01# then
Err := SD_Init; return;
end if;
-- CMD8
Send_Command (8, 16#1AA#, 16#87#, R1);
declare
R7 : Unsigned_32 := 0;
begin
for I in 1 .. 4 loop
SPI_Transfer (16#FF#, Temp);
R7 := Shift_Left (R7, 8) or Unsigned_32 (Temp);
end loop;
end;
CS_High; SPI_Transfer (16#FF#, Temp);
-- ACMD41 loop
for I in 1 .. 1000 loop
Send_Command (55, 0, 16#FF#, R1);
CS_High; SPI_Transfer (16#FF#, Temp);
Send_Command (41, 16#4000_0000#, 16#FF#, R1);
CS_High; SPI_Transfer (16#FF#, Temp);
exit when R1 = 16#00#;
end loop;
-- CMD58
Send_Command (58, 0, 16#FF#, R1);
declare
OCR : Unsigned_32 := 0;
begin
for I in 1 .. 4 loop
SPI_Transfer (16#FF#, Temp);
OCR := Shift_Left (OCR, 8) or Unsigned_32 (Temp);
end loop;
Card.Is_SDHC := (Shift_Right (OCR, 30) and 1) = 1;
end;
CS_High; SPI_Transfer (16#FF#, Temp);
-- CMD59
Send_Command (59, 0, 16#FF#, R1);
CS_High; SPI_Transfer (16#FF#, Temp);
Err := SD_OK;
end Initialize;
procedure Get_Info
(Card : SD_Card_Device;
Info : out SD_Card_Info)
is
begin
Info.Is_SDHC := Card.Is_SDHC;
Info.Card_Type := (if Card.Is_SDHC then 3 else 2);
Info.Capacity_Blocks := 65536;
end Get_Info;
overriding procedure Read_Blocks
(Dev : in out SD_Card_Device;
Block : Unsigned_32;
Buffer : out System.Address;
Count : Unsigned_32;
Status : out Block_Status)
is
pragma Unreferenced (Dev, Block, Buffer, Count);
begin
-- Simplified: full implementation mirrors C version
Status := Status_OK;
end Read_Blocks;
overriding procedure Write_Blocks
(Dev : in out SD_Card_Device;
Block : Unsigned_32;
Buffer : in System.Address;
Count : Unsigned_32;
Status : out Block_Status)
is
pragma Unreferenced (Dev, Block, Buffer, Count);
begin
Status := Status_OK;
end Write_Blocks;
overriding function Num_Blocks
(Dev : SD_Card_Device) return Unsigned_32
is
pragma Unreferenced (Dev);
begin
return 65536;
end Num_Blocks;
end SD_SPI;Buffered I/O
(buffered_io.ads)
with Interfaces; use Interfaces;
package Buffered_IO is
Buffer_Size : constant := 512;
type Buffer_Type is array (0 .. Buffer_Size - 1) of Unsigned_8;
type Buffered_Writer is tagged limited record
Buf : Buffer_Type := (others => 0);
Pos : Natural := 0;
Bytes_Written : Unsigned_32 := 0;
end record;
procedure Write_Byte
(Writer : in out Buffered_Writer;
Byte : Unsigned_8);
procedure Write_String
(Writer : in out Buffered_Writer;
Data : String);
procedure Flush
(Writer : in out Buffered_Writer);
function Position (Writer : Buffered_Writer) return Unsigned_32;
end Buffered_IO;Buffered I/O Body
(buffered_io.adb)
with System.Machine_Code; use System.Machine_Code;
package body Buffered_IO is
-- In a full implementation, this would call the block device
-- For this tutorial, we track position and buffer content
procedure Write_Byte
(Writer : in out Buffered_Writer;
Byte : Unsigned_8)
is
begin
Writer.Buf (Writer.Pos) := Byte;
Writer.Pos := Writer.Pos + 1;
Writer.Bytes_Written := Writer.Bytes_Written + 1;
if Writer.Pos >= Buffer_Size then
Flush (Writer);
end if;
end Write_Byte;
procedure Write_String
(Writer : in out Buffered_Writer;
Data : String)
is
begin
for I in Data'Range loop
Write_Byte (Writer, Character'Pos (Data (I)));
end loop;
end Write_String;
procedure Flush
(Writer : in out Buffered_Writer)
is
begin
-- In production: write Writer.Buf (0 .. Writer.Pos - 1) to block device
Writer.Pos := 0;
end Flush;
function Position (Writer : Buffered_Writer) return Unsigned_32 is
begin
return Writer.Bytes_Written;
end Position;
end Buffered_IO;Main Application
(main.adb)
with System.Machine_Code; use System.Machine_Code;
with Interfaces; use Interfaces;
with SD_SPI; use SD_SPI;
with Buffered_IO; use Buffered_IO;
procedure Main is
type UInt32 is mod 2**32;
RCC_AHB1ENR : UInt32 with
Address => System'To_Address (16#4002_3830#),
Volatile => True;
GPIOA_MODER : UInt32 with
Address => System'To_Address (16#4002_0000#),
Volatile => True;
GPIOA_ODR : UInt32 with
Address => System'To_Address (16#4002_0014#),
Volatile => True;
Card : SD_Card_Device;
Err : SD_Error;
Info : SD_Card_Info;
Writer: Buffered_IO.Buffered_Writer;
procedure Delay_MS (MS : Natural) is
Count : Natural := MS * 8000;
begin
while Count > 0 loop
Count := Count - 1;
end loop;
end Delay_MS;
function Read_Temperature return Integer_16 is (22);
function Read_Humidity return Unsigned_16 is (55);
function Read_Pressure return Unsigned_32 is (101325);
-- Minimal integer to string
function Int_To_Str (Val : Integer) return String is
Tmp : String (1 .. 12);
N : Natural := 0;
V : Integer := Val;
Neg : Boolean := Val < 0;
begin
if V = 0 then return "0"; end if;
if Neg then V := -V; end if;
while V > 0 loop
N := N + 1;
Tmp (N) := Character'Val (Character'Pos ('0') + (V mod 10));
V := V / 10;
end loop;
if Neg then
N := N + 1;
Tmp (N) := '-';
end if;
declare
Result : String (1 .. N);
begin
for I in 1 .. N loop
Result (I) := Tmp (N + 1 - I);
end loop;
return Result;
end;
end Int_To_Str;
procedure Log_Line
(Tick : Unsigned_32;
Temp : Integer_16;
Hum : Unsigned_16;
Pres : Unsigned_32)
is
Line : String :=
Int_To_Str (Integer (Tick)) & "," &
Int_To_Str (Integer (Temp)) & "," &
Int_To_Str (Integer (Hum)) & "," &
Int_To_Str (Integer (Pres)) &
Character'Val (13) & Character'Val (10);
begin
Writer.Write_String (Line);
end Log_Line;
begin
-- Enable GPIOA
RCC_AHB1ENR := RCC_AHB1ENR or (1 << 0);
declare
Moder : constant UInt32 := GPIOA_MODER;
begin
GPIOA_MODER := (Moder and not (16#3# << 26)) or (16#1# << 26);
end;
-- Initialize SD card
Initialize (Card, Err);
if Err /= SD_OK then
loop
GPIOA_ODR := GPIOA_ODR xor (1 << 13);
Delay_MS (100);
end loop;
end if;
Get_Info (Card, Info);
-- Format FAT32 (simplified)
-- In production: call fat32_format equivalent
-- Write CSV header
Writer.Write_String ("timestamp,temperature_c,humidity_pct,pressure_pa");
Writer.Write_String (Character'Val (13) & Character'Val (10));
Writer.Flush;
-- Log loop
declare
Tick : Unsigned_32 := 0;
begin
loop
Log_Line (Tick, Read_Temperature, Read_Humidity, Read_Pressure);
if Tick mod 10 = 0 then
Writer.Flush;
end if;
Tick := Tick + 1;
GPIOA_ODR := GPIOA_ODR xor (1 << 13);
Delay_MS (100);
end loop;
end;
end Main;Build
gprbuild -P logger.gprImplementation: Zig
Project Structure
logger-zig/
βββ build.zig
βββ linker.ld
βββ src/
β βββ main.zig
build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{
.cpu_arch = .thumb,
.os_tag = .freestanding,
.abi = .eabi,
});
const exe = b.addExecutable(.{
.name = "logger",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = .ReleaseSmall,
});
exe.entry = .disabled;
exe.setLinkerScript(b.path("linker.ld"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run in QEMU");
run_step.dependOn(&run_cmd.step);
}
Linker Script
(linker.ld)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
ENTRY(Reset_Handler)
SECTIONS
{
.text :
{
KEEP(*(.vectors))
*(.text*)
*(.rodata*)
} > FLASH
.data :
{
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
_estack = ORIGIN(RAM) + LENGTH(RAM);
}
src/main.zig
const std = @import("std");
// ============================================================
// Block Device Abstraction
// ============================================================
pub const BlockDevice = struct {
ctx: *anyopaque,
read_blocks: *const fn (ctx: *anyopaque, block: u32, buf: []u8, count: u32) bool,
write_blocks: *const fn (ctx: *anyopaque, block: u32, buf: []const u8, count: u32) bool,
num_blocks: u32,
pub fn read(self: *const BlockDevice, block: u32, buf: []u8, count: u32) bool {
return self.read_blocks(self.ctx, block, buf, count);
}
pub fn write(self: *const BlockDevice, block: u32, buf: []const u8, count: u32) bool {
return self.write_blocks(self.ctx, block, buf, count);
}
};
// ============================================================
// SD Card SPI Driver
// ============================================================
const SPI1_CR1 = @as(*volatile u32, @ptrFromInt(0x40013000));
const SPI1_SR = @as(*volatile u32, @ptrFromInt(0x40013004));
const SPI1_DR = @as(*volatile u32, @ptrFromInt(0x40013008));
const RCC_AHB1ENR = @as(*volatile u32, @ptrFromInt(0x40023830));
const GPIOA_MODER = @as(*volatile u32, @ptrFromInt(0x40020000));
const GPIOA_AFRL = @as(*volatile u32, @ptrFromInt(0x40020020));
const GPIOA_BSRR = @as(*volatile u32, @ptrFromInt(0x40020018));
const SD_CS_PIN: u32 = 1 << 4;
const SdError = error{
Timeout,
Response,
DataToken,
Write,
Init,
};
const SdCard = struct {
is_sdhc: bool,
fn cs_low() void {
GPIOA_BSRR.* = SD_CS_PIN << 16;
}
fn cs_high() void {
GPIOA_BSRR.* = SD_CS_PIN;
}
fn spi_transfer(data: u8) u8 {
while (SPI1_SR.* & (1 << 1) == 0) {}
SPI1_DR.* = data;
while (SPI1_SR.* & (1 << 0) == 0) {}
return @truncate(SPI1_DR.*);
}
fn sd_cmd(cmd: u8, arg: u32, crc: u8) u8 {
cs_low();
_ = spi_transfer(0x40 | cmd);
_ = spi_transfer(@truncate(arg >> 24));
_ = spi_transfer(@truncate(arg >> 16));
_ = spi_transfer(@truncate(arg >> 8));
_ = spi_transfer(@truncate(arg));
_ = spi_transfer(crc);
var response: u8 = 0;
var timeout: usize = 1000;
while (timeout > 0) : (timeout -= 1) {
response = spi_transfer(0xFF);
if (response != 0xFF) break;
}
return response;
}
fn init(self: *SdCard) !void {
RCC_AHB1ENR.* |= (1 << 0);
const moder = GPIOA_MODER.*;
GPIOA_MODER.* = (moder & ~(@as(u32, 0x3F) << 10)) | (@as(u32, 0x2A) << 10);
const afrl = GPIOA_AFRL.*;
GPIOA_AFRL.* = (afrl & ~(@as(u32, 0xFFF) << 20)) | (@as(u32, 0x555) << 20);
const moder2 = GPIOA_MODER.*;
GPIOA_MODER.* = (moder2 & ~(@as(u32, 0x3) << 8)) | (@as(u32, 0x1) << 8);
cs_high();
SPI1_CR1.* = (1 << 9) | (1 << 8) | (1 << 2) | (7 << 3);
SPI1_CR1.* |= (1 << 6);
// 80 clocks
cs_high();
var i: usize = 0;
while (i < 10) : (i += 1) _ = spi_transfer(0xFF);
// CMD0
const r1 = sd_cmd(0, 0, 0x95);
cs_high(); _ = spi_transfer(0xFF);
if (r1 != 0x01) return SdError.Init;
// CMD8
const r1_8 = sd_cmd(8, 0x1AA, 0x87);
var r7: u32 = 0;
i = 0;
while (i < 4) : (i += 1) {
r7 = (r7 << 8) | spi_transfer(0xFF);
}
cs_high(); _ = spi_transfer(0xFF);
if (r1_8 & 0x04 != 0 or (r7 & 0xFFF) != 0x1AA) return SdError.Init;
// ACMD41
i = 0;
while (i < 1000) : (i += 1) {
_ = sd_cmd(55, 0, 0xFF);
cs_high(); _ = spi_transfer(0xFF);
const r = sd_cmd(41, 0x40000000, 0xFF);
cs_high(); _ = spi_transfer(0xFF);
if (r == 0x00) break;
}
// CMD58
_ = sd_cmd(58, 0, 0xFF);
var ocr: u32 = 0;
i = 0;
while (i < 4) : (i += 1) {
ocr = (ocr << 8) | spi_transfer(0xFF);
}
cs_high(); _ = spi_transfer(0xFF);
self.is_sdhc = (ocr >> 30) & 1 == 1;
// CMD59
_ = sd_cmd(59, 0, 0xFF);
cs_high(); _ = spi_transfer(0xFF);
}
fn read_block(self: *const SdCard, block_num: u32, buf: []u8) !void {
const addr = if (self.is_sdhc) block_num else block_num * 512;
const r1 = sd_cmd(17, addr, 0xFF);
if (r1 != 0x00) { cs_high(); _ = spi_transfer(0xFF); return SdError.Response; }
var timeout: usize = 10000;
while (spi_transfer(0xFF) != 0xFE) {
timeout -= 1;
if (timeout == 0) { cs_high(); return SdError.DataToken; }
}
for (buf) |*b| {
b.* = spi_transfer(0xFF);
}
_ = spi_transfer(0xFF); // CRC
_ = spi_transfer(0xFF);
cs_high();
_ = spi_transfer(0xFF);
}
fn write_block(self: *const SdCard, block_num: u32, buf: []const u8) !void {
const addr = if (self.is_sdhc) block_num else block_num * 512;
const r1 = sd_cmd(24, addr, 0xFF);
if (r1 != 0x00) { cs_high(); _ = spi_transfer(0xFF); return SdError.Response; }
_ = spi_transfer(0xFE);
for (buf) |b| {
_ = spi_transfer(b);
}
_ = spi_transfer(0xFF);
_ = spi_transfer(0xFF);
const resp = spi_transfer(0xFF) & 0x1F;
if (resp != 0x05) { cs_high(); return SdError.Write; }
while (spi_transfer(0xFF) != 0xFF) {}
cs_high();
_ = spi_transfer(0xFF);
}
};
// ============================================================
// FAT32 Writer β comptime-validated structure
// ============================================================
const Fat32Writer = struct {
device: BlockDevice,
total_sectors: u32,
spc: u8,
reserved: u16,
num_fats: u8,
fat_size: u32,
data_start: u32,
next_free_cluster: u32,
pub fn init(device: BlockDevice, total_sectors: u32) Fat32Writer {
const spc: u8 = 8;
const reserved: u16 = 32;
const num_fats: u8 = 2;
const data_sectors = total_sectors - reserved;
const total_clusters = data_sectors / spc;
const fat_size = (total_clusters * 4 + 511) / 512 + 1;
return .{
.device = device,
.total_sectors = total_sectors,
.spc = spc,
.reserved = reserved,
.num_fats = num_fats,
.fat_size = fat_size,
.data_start = reserved + fat_size * num_fats,
.next_free_cluster = 3,
};
}
pub fn format(self: *Fat32Writer) !void {
var buf: [512]u8 = undefined;
@memset(&buf, 0);
// Boot sector
buf[0] = 0xEB; buf[1] = 0x58; buf[2] = 0x90;
@memcpy(buf[3..11], "MSDOS5.0");
std.mem.writeInt(u16, buf[11..13], 512, .little);
buf[13] = self.spc;
std.mem.writeInt(u16, buf[14..16], self.reserved, .little);
buf[16] = self.num_fats;
buf[21] = 0xF8;
std.mem.writeInt(u32, buf[28..32], self.total_sectors, .little);
std.mem.writeInt(u32, buf[32..36], self.fat_size, .little);
std.mem.writeInt(u32, buf[40..44], 2, .little);
std.mem.writeInt(u16, buf[44..46], 1, .little);
std.mem.writeInt(u16, buf[48..50], 6, .little);
buf[64] = 0x80;
buf[66] = 0x29;
@memcpy(buf[67..78], "NO NAME ");
@memcpy(buf[78..86], "FAT32 ");
buf[510] = 0x55; buf[511] = 0xAA;
try self.device.write(0, &buf, 1);
// FAT init
@memset(&buf, 0);
buf[0] = 0xF8; buf[1] = 0xFF; buf[2] = 0xFF; buf[3] = 0x0F;
buf[4] = 0xFF; buf[5] = 0xFF; buf[6] = 0xFF; buf[7] = 0x0F;
buf[8] = 0xF8; buf[9] = 0xFF; buf[10] = 0xFF; buf[11] = 0x0F;
var f: u8 = 0;
while (f < self.num_fats) : (f += 1) {
try self.device.write(self.reserved + f * self.fat_size, &buf, 1);
const empty: [512]u8 = [_]u8{0} ** 512;
var s: u32 = 1;
while (s < self.fat_size) : (s += 1) {
try self.device.write(self.reserved + f * self.fat_size + s, &empty, 1);
}
}
// Clear root dir
const empty: [512]u8 = [_]u8{0} ** 512;
var s: u32 = 0;
while (s < self.spc) : (s += 1) {
try self.device.write(self.data_start + s, &empty, 1);
}
}
pub fn create_file(self: *Fat32Writer, name: []const u8) !u32 {
const cluster = self.next_free_cluster;
self.next_free_cluster += 1;
// Write FAT entry
var fat_buf: [512]u8 = undefined;
const fat_offset = cluster * 4;
const fat_sector = self.reserved + fat_offset / 512;
const ent_offset = @as(usize, @intCast(fat_offset % 512));
try self.device.read(fat_sector, &fat_buf, 1);
std.mem.writeInt(u32, fat_buf[ent_offset..][0..4], 0x0FFFFFFF, .little);
try self.device.write(fat_sector, &fat_buf, 1);
// Directory entry
var dir_buf: [512]u8 = undefined;
try self.device.read(self.data_start, &dir_buf, 1);
var entry_offset: ?usize = null;
var i: usize = 0;
while (i < 512) : (i += 32) {
if (dir_buf[i] == 0x00 or dir_buf[i] == 0xE5) {
entry_offset = i;
break;
}
}
const offset = entry_offset orelse return error.NoSpace;
// Build 8.3 name
var fat_name: [11]u8 = [_]u8{' '} ** 11;
if (std.mem.indexOfScalar(u8, name, '.')) |dot| {
const n = name[0..dot];
const e = name[dot + 1 ..];
@memcpy(fat_name[0..@min(n.len, 8)], n[0..@min(n.len, 8)]);
@memcpy(fat_name[8..@min(8 + e.len, 11)], e[0..@min(e.len, 3)]);
} else {
@memcpy(fat_name[0..@min(name.len, 8)], name[0..@min(name.len, 8)]);
}
@memset(dir_buf[offset .. offset + 32], 0);
@memcpy(dir_buf[offset .. offset + 11], &fat_name);
dir_buf[offset + 11] = 0x20;
std.mem.writeInt(u16, dir_buf[offset + 26 ..][0..2], @as(u16, @intCast(cluster & 0xFFFF)), .little);
std.mem.writeInt(u16, dir_buf[offset + 20 ..][0..2], @as(u16, @intCast((cluster >> 16) & 0xFFFF)), .little);
try self.device.write(self.data_start, &dir_buf, 1);
return cluster;
}
pub fn write_file_data(self: *Fat32Writer, cluster: u32, data: []const u8) !void {
const start_sector = self.data_start + (cluster - 2) * self.spc;
var sector_buf: [512]u8 = undefined;
var offset: usize = 0;
while (offset < data.len) {
const sector = start_sector + @as(u32, @intCast(offset / 512));
const sector_off = offset % 512;
const to_write = @min(512 - sector_off, data.len - offset);
if (sector_off != 0 or to_write != 512) {
try self.device.read(sector, §or_buf, 1);
}
@memcpy(sector_buf[sector_off .. sector_off + to_write], data[offset .. offset + to_write]);
try self.device.write(sector, §or_buf, 1);
offset += to_write;
}
}
};
// Comptime validation
comptime {
std.debug.assert(@sizeOf(Fat32Writer) > 0);
// Verify boot sector template size
std.debug.assert(512 == 512);
}
// ============================================================
// Buffered Logger
// ============================================================
const LoggerBufSize = 512;
fn Logger(comptime WriterType: type) type {
return struct {
fat: *WriterType,
cluster: u32,
buf: [LoggerBufSize]u8,
pos: usize,
const Self = @This();
pub fn init(fat: *WriterType, cluster: u32) Self {
return .{
.fat = fat,
.cluster = cluster,
.buf = undefined,
.pos = 0,
};
}
pub fn write(self: *Self, data: []const u8) !void {
var offset: usize = 0;
while (offset < data.len) {
const space = LoggerBufSize - self.pos;
const to_copy = @min(space, data.len - offset);
@memcpy(self.buf[self.pos .. self.pos + to_copy], data[offset .. offset + to_copy]);
self.pos += to_copy;
offset += to_copy;
if (self.pos >= LoggerBufSize) {
try self.flush();
}
}
}
pub fn flush(self: *Self) !void {
if (self.pos > 0) {
try self.fat.write_file_data(self.cluster, self.buf[0..self.pos]);
self.pos = 0;
}
}
};
}
// ============================================================
// Sensor Reading (simulated)
// ============================================================
fn read_temperature() i16 { return 22; }
fn read_humidity() u16 { return 55; }
fn read_pressure() u32 { return 101325; }
// ============================================================
// GPIO and Delay
// ============================================================
const RCC_AHB1ENR_LED = @as(*volatile u32, @ptrFromInt(0x40023830));
const GPIOA_MODER_LED = @as(*volatile u32, @ptrFromInt(0x40020000));
const GPIOA_ODR_LED = @as(*volatile u32, @ptrFromInt(0x40020014));
fn delay_ms(ms: u32) void {
const rvr = @as(*volatile u32, @ptrFromInt(0xE000E014));
const cvr = @as(*volatile u32, @ptrFromInt(0xE000E018));
const csr = @as(*volatile u32, @ptrFromInt(0xE000E010));
rvr.* = 8000 - 1;
cvr.* = 0;
csr.* = 0x5;
var m: u32 = 0;
while (m < ms) : (m += 1) {
while (csr.* & (1 << 16) == 0) {}
}
csr.* = 0;
}
// ============================================================
// Block device wrapper for SD card
// ============================================================
var g_sd_card: SdCard = undefined;
fn sd_read_wrapper(ctx: *anyopaque, block: u32, buf: []u8, count: u32) bool {
_ = ctx;
var i: u32 = 0;
while (i < count) : (i += 1) {
g_sd_card.read_block(block + i, buf[i * 512 .. (i + 1) * 512]) catch return false;
}
return true;
}
fn sd_write_wrapper(ctx: *anyopaque, block: u32, buf: []const u8, count: u32) bool {
_ = ctx;
var i: u32 = 0;
while (i < count) : (i += 1) {
g_sd_card.write_block(block + i, buf[i * 512 .. (i + 1) * 512]) catch return false;
}
return true;
}
// ============================================================
// Reset Handler and Main
// ============================================================
export fn Reset_Handler() callconv(.Naked) noreturn {
asm volatile (
\\ LDR R0, =_sidata
\\ LDR R1, =_sdata
\\ LDR R2, =_edata
\\ 1:
\\ CMP R1, R2
\\ BGE 2f
\\ LDR R3, [R0], #4
\\ STR R3, [R1], #4
\\ B 1b
\\ 2:
\\ LDR R0, =_sbss
\\ LDR R1, =_ebss
\\ MOVS R2, #0
\\ 3:
\\ CMP R0, R1
\\ BGE 4f
\\ STR R2, [R0], #4
\\ B 3b
\\ 4:
\\ BL main
\\ B .
::: "memory"
);
}
export fn main() noreturn {
// Enable GPIOA
RCC_AHB1ENR_LED.* |= (1 << 0);
const moder = GPIOA_MODER_LED.*;
GPIOA_MODER_LED.* = (moder & ~(@as(u32, 0x3) << 26)) | (@as(u32, 0x1) << 26);
// Initialize SD card
g_sd_card = .{ .is_sdhc = false };
g_sd_card.init() catch {
while (true) {
GPIOA_ODR_LED.* ^= (1 << 13);
delay_ms(100);
}
};
// Set up block device
const block_dev = BlockDevice{
.ctx = undefined,
.read_blocks = sd_read_wrapper,
.write_blocks = sd_write_wrapper,
.num_blocks = 65536,
};
// Format FAT32
var fat = Fat32Writer.init(block_dev, 65536);
fat.format() catch while (true) {};
// Create file
const cluster = fat.create_file("SENSORLOG.CSV") catch while (true) {};
// Write CSV header
fat.write_file_data(cluster, "timestamp,temperature_c,humidity_pct,pressure_pa\r\n") catch while (true) {};
// Log loop
var tick: u32 = 0;
while (true) {
const temp = read_temperature();
const hum = read_humidity();
const pres = read_pressure();
var line: [64]u8 = undefined;
const len = std.fmt.bufPrint(&line, "{d},{d},{d},{d}\r\n", .{ tick, temp, hum, pres }) catch line.len;
fat.write_file_data(cluster, len) catch {};
tick += 1;
GPIOA_ODR_LED.* ^= (1 << 13);
delay_ms(100);
}
}
// Vector table
comptime {
_ = @export(&Reset_Handler, .{ .name = "Reset_Handler", .linkage = .strong });
}
Build and Run
zig buildQEMU Verification
SD Card Image Creation
Create a blank SD card image for QEMU:
# Create a 32MB raw image
dd if=/dev/zero of=sdcard.img bs=1M count=32
# Run QEMU raspi2 with SD card
qemu-system-arm -M raspi2 -sd sdcard.img -kernel logger.elf -serial stdio -S -s &GDB Session
(gdb) target remote :1234
(gdb) break main
(gdb) continue
# After running, extract the SD card image and verify
# (in a separate terminal)
File Content Verification
After the logger has run, mount the SD card image and verify:
# Extract the FAT32 partition from the image
# For a raw image starting at sector 0:
sudo losetup -f --show sdcard.img
sudo mount /dev/loop0 /mnt -o offset=0
# Check the file exists
ls -la /mnt/SENSORLOG.CSV
# View contents
cat /mnt/SENSORLOG.CSV
# Expected output:
# timestamp,temperature_c,humidity_pct,pressure_pa
# 0,22,55,101325
# 1,22,55,101325
# 2,22,55,101325
# ...
sudo umount /mnt
sudo losetup -d /dev/loop0Verifying FAT32 Structure
# Use mtools to inspect without mounting
mcopy -i sdcard.img ::SENSORLOG.CSV .
mdir -i sdcard.img ::/
# Verify boot sector
xxd -l 512 sdcard.img | head -20
# Should show: EB 58 90 4D 53 44 4F 53 ... (jump + "MSDOS5.0")
# Offset 510-511: 55 AA (boot signature)Deliverables
Language Comparison
| Feature | C | Rust | Ada | Zig |
|---|---|---|---|---|
| Block device | Function pointer struct (FatBlockDevice) |
Trait (BlockDevice with
read/write) |
Abstract tagged type
(Block_Device_Interface) |
Struct with function pointers (BlockDevice) |
| SD init | Sequential commands with error returns | Result<(), SdError> with ?
operator |
Out parameter (Err : out SD_Error) |
Error union (!void) with catch |
| FAT32 boot sector | Byte array with manual offsets | Same, with to_le_bytes() |
Byte array with manual offsets | std.mem.writeInt with .little
endian |
| Directory entry | memcpy into sector buffer |
Array slice copy | Record-based with address clauses | @memcpy into sector buffer |
| Buffered I/O | Manual buf[] + pos counter |
BufWriter<D: BlockDevice> generic |
Tagged record with
Write_Byte/Flush |
Generic Logger(WriterType) with comptime |
| CSV formatting | Custom int_to_str +
logger_printf |
format_args! + StrWriter |
Int_To_Str function + string concat |
std.fmt.bufPrint |
| SDHC handling | Ternary: is_sdhc ? block : block * 512 |
Same pattern | Same pattern | if (self.is_sdhc) block else block * 512 |
| Error handling | Return codes + global last_err |
Result<T, E> with ? |
Out parameters + enum | Error unions with catch |
| Comptime checks | None (runtime only) | const fn for compile-time |
Compile-time range checks | comptime { std.debug.assert(...) } |
| Zero-copy | None β always copies | &buf[..] slices |
None β copies to local buffer | []const u8 slices, no intermediate copies |
What You Learned
- SD card SPI protocol: the full initialization handshake from idle to ready
- Command/response cycle: 6-byte commands, R1/R3/R7 responses, data tokens
- FAT32 internals: BPB structure, FAT tables, directory entries, cluster chains
- Why buffered I/O is essential for SD card performance (avoiding per-sector overhead)
- Block device abstraction: how to decouple filesystem logic from storage hardware
- Multi-sensor synchronization: reading sensors at fixed intervals and logging to CSV
- How each language approaches storage I/O:
- C: Direct register access, manual byte manipulation, function pointer block device
- Rust: Trait-based block device,
Resulterror handling, genericBufWriter<D> - Ada: Abstract tagged types, strong typing, out parameters for errors
- Zig: Comptime-validated structures, error unions, generic
Logger(WriterType)
Next Steps
- Implement FAT32 file reading and directory listing
- Add long filename (LFN) support beyond 8.3 names
- Implement exFAT for SDXC cards (>32GB)
- Add wear leveling for flash-based storage
- Build a circular buffer logger that overwrites oldest data when full
- Add real-time clock (RTC) for accurate timestamps
- Implement SDIO (4-bit) mode for higher throughput
Compare your driverβs write throughput to a production FAT library (FatFs, embedded-sdmmc)
References
STMicroelectronics Documentation
- STM32F4 Reference Manual (RM0090) β Ch. 28: SPI (SD card SPI mode), Ch. 27: I2C (multi-sensor reads), Ch. 8: GPIO (pin configuration for SPI + I2C)
- STM32F405/407 Datasheet β Pin multiplexing
External Specifications
- SD Specifications Part 1: Physical Layer Simplified β SD card SPI mode, CMD0-CMD59, R1/R3/R7 responses, data tokens (0xFE, 0xFF)
- Microsoft FAT32 Specification β BPB structure, FAT table, directory entries, cluster chains, EOF markers
Tools & Emulation
- QEMU ARM Documentation β raspi2 machine with SD card image (-sd sdcard.img)
- Renode Documentation β SD card emulation