← Back to Learning Notes

Learning Notes

Embedded C Review Checklist

A systematic guide to data types, bit operations, pointers, volatile, interrupts, state machines, module interfaces and testability in embedded C.

What Makes Embedded C Different

The language is the same, but the environment includes direct register access, tight memory and compute limits, interrupts, deadlines, concurrency and hardware faults. Compilation is only the first check. Width, execution time, object lifetime, hardware ordering and failure states must be explicit.

Fixed-Width Integers and Conversion

Use uint8_t, int16_t and uint32_t for registers, protocols and storage formats. Mixed signed/unsigned operations can turn a negative value into a large unsigned one, and integer promotion can change small operands before arithmetic.

uint16_t adc = 3000U;
uint32_t millivolts = ((uint32_t)adc * 3300U) / 4095U;

Widen before multiplication so the intermediate result cannot overflow.

Overflow, Boundaries and Units

Unsigned overflow wraps modulo its range; do not rely on signed overflow. Names such as timeout_ms, frequency_hz and voltage_mv prevent unit ambiguity. Check intermediate as well as final ranges in sensor formulas.

Bit Operations and Registers

reg |= (1U << bit);        /* set */
reg &= ~(1U << bit);       /* clear */
reg ^= (1U << bit);        /* toggle */
state = (reg >> bit) & 1U; /* read */

Keep bit below the type width and shift unsigned values. Use named masks rather than unexplained hex constants. Read the reference manual before read-modify-write: write-one-to-clear, clear-on-read and reserved-bit rules require special handling.

Pointers and Object Lifetime

Pointers do not carry buffer length or lifetime. Accept data and length together:

bool uart_send(const uint8_t *data, size_t length);

Never return the address of a local object. Long-lived systems often prefer static storage or fixed pools over fragmented dynamic allocation. const uint8_t *p protects pointed data through p; uint8_t * const p fixes the pointer; combining both fixes both.

Arrays, Strings and Buffers

An array parameter decays to a pointer, so pass length explicitly. Verify string termination and never trust external lengths before writing or parsing. Use bounded formatting and check its return value. Binary buffers may contain zero bytes and are not strings.

Endianness, Alignment and Layout

Protocols define byte order explicitly. Do not cast a byte buffer to a structure pointer because alignment, padding and strict aliasing differ by platform.

uint16_t read_u16_be(const uint8_t *data)
{
    return (uint16_t)(((uint16_t)data[0] << 8) | data[1]);
}

What volatile Does and Does Not Do

volatile forces observable reads and writes for hardware registers or data changed asynchronously by an ISR. It does not guarantee atomic multi-byte access, transaction boundaries or task synchronization. Use critical sections, queues, semaphores, mutexes or atomic operations where the data relationship requires them.

Interrupt Service Routines

Keep ISRs short: capture status, move minimal data, clear the source and notify a task or main loop. Verify clear order, shared-data races, priority, maximum duration, nesting and use of RTOS ISR-specific APIs. Ring-buffer producer and consumer ownership must ensure the consumer never sees partially written data.

Prefer State Machines to Blocking Waits

Split peripheral or protocol sequences into observable states with entry, action, exit and timeout:

switch (state) {
case UART_IDLE:
    if (start_requested) {
        start_transfer();
        deadline = now_ms + 100U;
        state = UART_WAIT;
    }
    break;
case UART_WAIT:
    if (transfer_done()) state = UART_IDLE;
    else if (time_reached(now_ms, deadline)) {
        abort_transfer();
        state = UART_ERROR;
    }
    break;
case UART_ERROR:
    report_error();
    state = UART_IDLE;
    break;
}

Compare Time Safely Across Wrap

For durations within the unambiguous half-range of the counter, unsigned subtraction works across one wrap:

bool time_elapsed(uint32_t now, uint32_t start, uint32_t duration)
{
    return (uint32_t)(now - start) >= duration;
}

Layer Module Interfaces

  • Driver: GPIO, UART, SPI and ADC register access.
  • Service: sensors, protocols and buffers.
  • Application: state machines, control policies and user logic.

Headers expose stable interfaces and necessary types. File-private state and helpers remain static. Return values should distinguish success, busy, timeout and invalid parameters where the caller can act differently.

Errors and Assertions

Return errors that callers can recover from. Assertions capture impossible internal conditions during development, with a defined production policy such as fault recording and controlled reset. Do not silently discard I2C NACK, Flash failure or full-buffer results. Keep high-rate logs bounded so diagnostics do not break real-time behavior.

Testability and Verification

Separate pure calculations from hardware effects. An ADC-to-temperature function that accepts and returns ordinary values can be tested on a PC. Cover normal, min/max, empty input, timeout, checksum failure, full buffer, repeated initialization and ISR/main interleavings. Enable meaningful compiler warnings and treat conversion, bounds and ignored-return warnings seriously.

Pre-Commit Checklist

  • Are fixed-format fields represented with fixed-width types?
  • Can intermediate multiplication, shifts or conversions overflow?
  • Do buffer interfaces include length?
  • Is protocol byte order explicit?
  • Is volatile limited to truly asynchronous objects?
  • Does shared data require a critical section, queue or atomic operation?
  • Are ISRs free of blocking, long loops and unnecessary logs?
  • Does every wait have a timeout and error exit?
  • Do state machines cover every state and failure transition?
  • Are hardware access and business logic separated?
  • Are peripheral return values and boundaries verified?
  • Can core calculations be tested without hardware?