// books for this phase
Modern C — Jens GustedtLanguage depth: type system, memory model, UB, idioms. Chapters 1–20 in order.
Embedded Software Design — Jacob BeningoArchitecture: HAL design, modularity, state machines, testing. The "professional habits" book.
Understanding and Using C Pointers — Richard Reese (finishing)Complete any remaining chapters on function pointers and pointer arithmetic.
// daily schedule template (mon–fri)
08:00 – 10:00  Book reading (Modern C or Beningo chapter)Take notes on paper. Draw diagrams. Do every worked example.
10:00 – 13:00  Coding exercises — implement chapter concepts on STM32Type code by hand. No copy-paste from book examples.
14:00 – 16:00  Weekly project work — layered driver or architecture taskThis is where the week's reading becomes a real artifact.
16:00 – 17:30  Review + documentationWrite up what you built, why it's designed that way, what you'd change.
Saturday        Weekly review: fix what's broken, push to GitHub, write a short technical summary
// week-by-week

Week 1 — C language depth and memory model

Foundation
integer promotionimplicit conversions pointer arithmeticfunction pointers volatile / constUB traps
Reading
Modern C Ch. 1–6: types, operators, control flow, functionsFocus: integer promotion rules, implicit conversions, signed overflow UB
Finish Understanding C Pointers: function pointers, pointer arithmeticThese appear constantly in driver dispatch tables and HAL code
Environment setup (do first)
Build from CLI: CMake + arm-none-eabi-gcc, no IDEKnowing how to build without an IDE forces real understanding of the toolchain
Set up clangd LSP for code navigationgenerate_compile_commands = ON in CMakeLists.txt
Configure OpenOCD for flash + debug via GDBNo GUI debugger — raw GDB commands this week
Daily coding exercises
MonRewrite memcpy, memset, strcpy from scratch without stdlib. Verify against originals.Forces correct pointer arithmetic and off-by-one awareness
TuePractice volatile and const correctness: write a GPIO register access layer using volatile uint32_t pointers. No HAL library.Every peripheral register pointer should be volatile. Understand why.
WedWrite a function pointer dispatch table: define 5 command handler functions, store in an array, call by index.This exact pattern is used in protocol parsers and bootloaders
ThuInteger promotion traps: write 3 examples where implicit conversion causes a bug. Fix each one with explicit casts.Classic: uint8_t a=200, b=200; uint8_t sum = a+b; — what is sum?
FriUndefined behaviour exploration: signed overflow, null pointer deref, out-of-bounds. Compile at -O0 then -O2. Document the difference.This will surprise you. UB that "works" at -O0 can silently break at -O2.
SatSet up Git repo. Push all week's code with a structured README. Document what each exercise taught you.

Week 2 — Structs, alignment, bit manipulation

Data Structures
struct paddingpacked attribute register mapsbit manipulation circular bufferunion type punning
Reading
Modern C Ch. 7–10: structs, unions, typedef, memory layoutFocus: struct padding, alignment requirements, __attribute__((packed))
Beningo Ch. 1–3: software engineering principles for embeddedSOLID applied to C, cohesion, coupling, single responsibility
Key concepts
Struct padding: compiler inserts padding to align members to their natural alignment. sizeof(struct) may surprise you.Use __attribute__((packed)) only when absolutely necessary — creates unaligned access faults on strict-alignment architectures
Register map as packed struct: maps C struct fields directly to hardware register bits.Verify with objdump that the struct sits at the correct base address
Daily coding exercises
MonImplement a register map as a packed struct for UART. Compare with raw pointer offset access. Verify both produce identical machine code with objdump.Use the STM32 reference manual to map every field
TueBit manipulation drills. Implement SET_BIT, CLEAR_BIT, TOGGLE_BIT, READ_BIT macros. No magic numbers anywhere. Apply to GPIO register control.These macros go in every professional embedded codebase
Wed–ThuBuild a circular (ring) buffer: fixed size (power of two), push, pop, is_full, is_empty. Lock-free for single producer / single consumer.You will reuse this in Phase 2 (UART DMA) and Phase 3 (telemetry)
FriUART interrupt-driven driver using your ring buffer for both TX and RX. No polling.TX: task writes to ring buffer, TXEIE interrupt drains it. RX: RXNEIE fills ring buffer, task reads it.
SatVerify UART driver with a Python script (pyserial): send known patterns, verify echo. Fix any corruption.
Circular buffer — reference implementation
/* Power-of-two size for bitmask wrap (no division) */
#define RING_SIZE   256u          /* must be power of 2 */
#define RING_MASK   (RING_SIZE - 1u)

typedef struct {
    uint8_t  buf[RING_SIZE];
    volatile uint32_t head;       /* written by producer (ISR) */
    volatile uint32_t tail;       /* written by consumer (task) */
} RingBuf_t;

/* Single producer / single consumer — lock-free on Cortex-M */
static inline bool rb_push(RingBuf_t *rb, uint8_t byte) {
    uint32_t next = (rb->head + 1u) & RING_MASK;
    if (next == rb->tail) return false;   /* full */
    rb->buf[rb->head] = byte;
    rb->head = next;
    return true;
}

static inline bool rb_pop(RingBuf_t *rb, uint8_t *out) {
    if (rb->head == rb->tail) return false;  /* empty */
    *out = rb->buf[rb->tail];
    rb->tail = (rb->tail + 1u) & RING_MASK;
    return true;
}

static inline uint32_t rb_count(const RingBuf_t *rb) {
    return (rb->head - rb->tail) & RING_MASK;
}

Week 3 — Layered architecture and HAL design

Architecture
HAL patternabstraction layers portabilityerror handling API designdriver architecture
Reading
Beningo Ch. 4–6: HAL design, abstraction layers, portability patterns
Modern C Ch. 11–13: pointers to functions, standard library internals
The three-layer driver model
Layer 1 — Hardware Abstraction (HAL): direct register access, MCU-specific.Changing MCU = rewrite only this layer
Layer 2 — Driver logic: protocol, timing, state. MCU-independent.UART framing, SPI clock polarity handling
Layer 3 — Application API: the public interface your application calls.UART_Write(buf, len). Never exposes registers.
Daily coding exercises
Mon–TueRefactor your Week 2 UART driver into 3 explicit layers. The HAL layer is the only file that touches registers. Verify by compiling driver logic tests on your host (no MCU).
WedWrite a GPIO HAL that could be ported to a different MCU by changing only one file. Test by swapping a stub implementation.GPIO_SetPin(port, pin), GPIO_ClearPin, GPIO_ReadPin — no register names leak upward
ThuDesign and implement a proper error handling system. Define error_t typedef (int32_t or enum), error code list, propagation pattern (return error, never ignore).MISRA Rule 17.7: the return value of a non-void function shall be used. Enforce this now.
FriAdd logging abstraction: LOG_DEBUG, LOG_INFO, LOG_ERROR macros that compile away in RELEASE builds. Implement over UART.
SatGitHub milestone: push UART driver with full 3-layer architecture and a README architecture diagram. This is Portfolio Project #1 start.
Error handling pattern
typedef int32_t error_t;

#define ERR_OK           (0)
#define ERR_INVALID_ARG  (-1)
#define ERR_TIMEOUT      (-2)
#define ERR_BUSY         (-3)
#define ERR_OVERFLOW     (-4)

/* Every function that can fail returns error_t.
   Callers must check the return value — always. */
error_t UART_Write(const uint8_t *buf, uint16_t len) {
    if (buf == NULL || len == 0u) {
        return ERR_INVALID_ARG;
    }
    /* ... implementation ... */
    return ERR_OK;
}

/* Usage — never ignore: */
error_t err = UART_Write(data, len);
if (err != ERR_OK) {
    LOG_ERROR("UART write failed: %d", err);
    return err;   /* propagate up */
}

Week 4 — State machines and design patterns

Patterns
finite state machinetable-driven FSM function-pointer FSMentry/exit actions protocol parserdefensive coding
Reading
Beningo Ch. 7–9: state machines, design patterns in embedded C
Modern C Ch. 14–16: defensive coding, assertions, static analysis mindset
Two FSM implementations to master
Table-driven: 2D array of [state][event] → next_state + action. O(1) lookup.Best for simple FSMs. Easy to visualise. Hard to add complex guard conditions.
Function-pointer: each state is a function. Events are parameters.Best for complex FSMs with guards. More flexible. More code.
Table-driven FSM — reference implementation
typedef enum { ST_IDLE, ST_RECEIVING, ST_PROCESSING, ST_ERROR, ST_COUNT } State_t;
typedef enum { EV_START, EV_BYTE, EV_END, EV_TIMEOUT, EV_COUNT } Event_t;

typedef void (*ActionFn_t)(void);

typedef struct {
    State_t   next_state;
    ActionFn_t action;
} Transition_t;

/* Transition table: [current_state][event] */
static const Transition_t FSM_TABLE[ST_COUNT][EV_COUNT] = {
    /*            EV_START                EV_BYTE                 EV_END                  EV_TIMEOUT         */
    [ST_IDLE]     = {{ST_RECEIVING, on_start}, {ST_IDLE, NULL},     {ST_IDLE, NULL},        {ST_IDLE, NULL}},
    [ST_RECEIVING]= {{ST_IDLE, on_error},      {ST_RECEIVING,on_byte},{ST_PROCESSING,on_end},{ST_ERROR,on_timeout}},
    [ST_PROCESSING]={{ST_IDLE, on_error},      {ST_IDLE, on_error},  {ST_IDLE, on_done},     {ST_ERROR,on_timeout}},
    [ST_ERROR]    = {{ST_IDLE, on_reset},      {ST_ERROR, NULL},     {ST_IDLE, on_reset},    {ST_ERROR, NULL}},
};

static State_t g_state = ST_IDLE;

void FSM_Dispatch(Event_t event) {
    const Transition_t *t = &FSM_TABLE[g_state][event];
    if (t->action != NULL) {
        t->action();
    }
    g_state = t->next_state;
}
Daily coding exercises
Mon–TueImplement both FSM variants for a simple packet protocol: IDLE → RECEIVING → PROCESSING → IDLE. States driven by UART bytes.
WedBuild a UART command-line interface: receive ASCII commands, parse with your FSM, dispatch to handlers via function pointer table.Commands: "led on", "led off", "status". Parser handles unknown commands gracefully.
ThuAdd assertions everywhere: ASSERT(ptr != NULL), ASSERT(len <= MAX). Configure assert to log + halt in DEBUG, compile away in RELEASE.
FriAdd defensive input validation to every public API function. Test with malformed inputs: NULL pointers, zero lengths, out-of-range values.
SatPush CLI system to GitHub with a README that includes the FSM state diagram (draw it in ASCII art or draw.io).

Week 5 — Memory management and toolchain mastery

Memory
linker script.text .data .bss static allocationmemory pool objdump / nmstartup code
Reading
Modern C Ch. 17–20: dynamic memory, UB, compilation modelFocus: why malloc is dangerous in embedded and static allocation alternatives
Beningo Ch. 10–12: startup code, linker scripts, memory sections
Memory sections to understand
.text — executable code, stored in flash
.rodata — read-only data (const globals), stored in flash
.data — initialised globals, copied from flash to RAM at startup
.bss — zero-initialised globals, zeroed at startup
.stack — grows downward, size set in linker script
Static memory pool — reference implementation
#define POOL_BLOCK_SIZE   64u
#define POOL_BLOCK_COUNT  16u

typedef struct {
    uint8_t  data[POOL_BLOCK_SIZE];
    bool     in_use;
} Block_t;

static Block_t s_pool[POOL_BLOCK_COUNT];   /* lives in .bss */

void* Pool_Alloc(void) {
    for (uint32_t i = 0; i < POOL_BLOCK_COUNT; i++) {
        if (!s_pool[i].in_use) {
            s_pool[i].in_use = true;
            return s_pool[i].data;
        }
    }
    return NULL;   /* pool exhausted — never hang, return NULL */
}

void Pool_Free(void *ptr) {
    if (ptr == NULL) return;
    Block_t *b = (Block_t *)((uint8_t *)ptr - offsetof(Block_t, data));
    ASSERT(b >= s_pool && b < s_pool + POOL_BLOCK_COUNT);
    b->in_use = false;
}
Daily coding exercises
MonRead and annotate your STM32 linker script. Locate every section. Understand why .data initialisation happens in the startup assembly.
TueUse objdump -h to print section sizes. Use nm to list all symbols with sizes. Find where your largest variables live. Find any unexpected bloat.
Wed–ThuImplement the static memory pool above. Add to your CLI project: command receive buffers come from the pool, released after processing.
FriAdd memory usage reporting to the CLI: a "mem" command that prints pool usage (blocks used/total) and stack watermark (fill stack with known pattern at startup, scan for pattern at runtime).
SatVerify that your entire project compiles with zero dynamic memory calls: grep for malloc, calloc, realloc, free. There should be none.

Week 6 — Unit testing and Phase 1 capstone

Capstone
Unity test frameworkCMock Ceedlingmocking hardware host-side testingbranch coverage
Reading
Beningo Ch. 13–15: unit testing in embedded C, mocking hardware, testable design
Ceedling + Unity documentation: ceedling.ioRead the getting-started guide before touching any test code
What to test this week
Ring buffer: push/pop, full/empty edge cases, wrap-aroundTarget: 100% branch coverage. This is pure algorithmic — no excuses.
FSM: every state/event transition. Invalid transitions handled.Create a test for each cell in your transition table.
Memory pool: alloc, free, double-free safety, exhaustion handling
Command parser: valid commands, unknown commands, malformed input
Unity test example — ring buffer
/* test/test_ring_buffer.c */
#include "unity.h"
#include "ring_buffer.h"

static RingBuf_t rb;

void setUp(void)    { rb_init(&rb); }
void tearDown(void) { }

void test_RingBuf_InitiallyEmpty(void) {
    TEST_ASSERT_TRUE(rb_empty(&rb));
    TEST_ASSERT_FALSE(rb_full(&rb));
}

void test_RingBuf_PushPop_SingleByte(void) {
    TEST_ASSERT_TRUE(rb_push(&rb, 0xAB));
    uint8_t out = 0;
    TEST_ASSERT_TRUE(rb_pop(&rb, &out));
    TEST_ASSERT_EQUAL_HEX8(0xAB, out);
    TEST_ASSERT_TRUE(rb_empty(&rb));
}

void test_RingBuf_PushOnFull_ReturnsFalse(void) {
    for (uint32_t i = 0; i < RING_SIZE - 1; i++) {
        TEST_ASSERT_TRUE(rb_push(&rb, (uint8_t)i));
    }
    TEST_ASSERT_TRUE(rb_full(&rb));
    TEST_ASSERT_FALSE(rb_push(&rb, 0xFF));  /* must fail */
}

void test_RingBuf_WrapAround_DataIntegrity(void) {
    /* Fill, drain halfway, fill again — verify wrap-around is correct */
    uint8_t in, out;
    for (uint8_t i = 0; i < 128; i++) rb_push(&rb, i);
    for (uint8_t i = 0; i < 128; i++) rb_pop(&rb, &out);
    for (uint8_t i = 0; i < 100; i++) rb_push(&rb, i);
    for (uint8_t i = 0; i < 100; i++) {
        rb_pop(&rb, &out);
        TEST_ASSERT_EQUAL_UINT8(i, out);
    }
}
Phase 1 capstone — deliverable checklist
Layered UART driver: 3 explicit layers (HAL / driver / API), interrupt-driven, ring-buffered TX and RXVerified by Python pyserial test script
Reusable FSM framework: table-driven and function-pointer variants. Used in command parser.State diagram in README
Static memory pool: ISR-safe alloc/free, exhaustion handling, usage reportingUsed for command receive buffers
CLI system: ASCII command interface over UART, function pointer dispatch table, error handlingAt least 5 commands. "help" command lists all.
Unit tests: Unity + Ceedling, ≥90% branch coverage on ring buffer / FSM / memory poolTests run on host (no MCU needed). CI badge in README.
GitHub repo: architecture diagram, build instructions, test results in READMEThis is Portfolio Project #1