Embedded Mastery: C, Ada, Rust & Zig

Embedded Mastery: C, Ada, Rust & Zig

A Project-Based Tutorial for Embedded Development

Embedded Mastery Project

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

Prerequisites


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  |
  +--------+--------+--------+--------+--------+--------+

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:

  1. CMD24 command (6 bytes)
  2. Start token + 512 data bytes + 2 CRC bytes
  3. 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);

#endif

SD 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);

#endif

FAT32 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);

#endif

Buffered 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.bin

Build and Run

make

Implementation: Rust

Project Setup

cargo init --name logger-rust
cd logger-rust

Cargo.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, &sector_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 --release

Implementation: 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.gpr

Implementation: 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, &sector_buf, 1);
            }
            @memcpy(sector_buf[sector_off .. sector_off + to_write], data[offset .. offset + to_write]);
            try self.device.write(sector, &sector_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 build

QEMU 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/loop0

Verifying 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

Next Steps

References

STMicroelectronics Documentation

External Specifications

Tools & Emulation