Skip to content

Reference: The C Standard Library in Embedded Systems


The Core Question

Why does printf() work differently on a PC vs a microcontroller?

The answer requires understanding three separate things:

  1. The C language — syntax, semantics, what the code means
  2. The C standard library — a contract for common functions
  3. The implementation — how that contract is fulfilled (or not)
The Key Insight

The C standard library is a contract, not a guarantee.

In embedded systems, you must know which parts of the contract are actually fulfilled.


What IS the C Standard Library?

Formal Definition

The C standard library is a collection of functions and headers defined by the ISO C standard that provide basic, portable functionality, independent of any specific hardware.

Key Properties

  • Defined by the ISO C standard (C99, C11, C17, etc.)
  • Describes interfaces, not implementations
  • May be full, partial, or minimal, depending on environment
  • Implemented by a libc (library implementation)

Standard Headers (from the C standard)

#include <stdint.h>   // Fixed-width integers (uint8_t, int32_t, etc.)
#include <stddef.h>   // size_t, NULL, offsetof
#include <stdbool.h>  // bool, true, false
#include <string.h>   // memcpy, strlen, strcmp
#include <stdlib.h>   // malloc, free, exit, atoi
#include <stdio.h>    // printf, scanf, FILE, fopen
#include <math.h>     // sin, cos, sqrt
#include <time.h>     // time, clock

What the C Standard Library is NOT

It is NOT... Explanation
The operating system It doesn't schedule tasks, manage devices, or handle interrupts
Hardware access No GPIO, UART, ADC, timers, PWM
Guaranteed to exist fully On embedded systems, only parts may be present
Real-time safe by default malloc, printf may block or have unpredictable timing
The C language itself C without stdlib is still C

What is libc?

libc is the implementation of the C standard library — the actual code that makes C programs work.

C Standard (specification)
    libc (implementation)
    Your program

Different libc Implementations

libc Used Where Characteristics
glibc Linux desktop/server Large, full-featured
musl Embedded Linux, Alpine Small, static-friendly
newlib Bare-metal MCUs Portable, configurable
newlib-nano Resource-constrained MCUs Size-optimized
ucrt Windows Microsoft runtime

What libc Contains

libc
├── C standard library functions
│   ├── stdio   → printf, scanf, fopen
│   ├── stdlib  → malloc, free, exit
│   ├── string  → memcpy, strlen
│   └── math    → sin, cos, sqrt
├── Runtime support
│   ├── Program entry (_start)
│   ├── Calling main()
│   └── Stack/heap setup
└── Platform interface
    ├── System call wrappers (on OS)
    └── Low-level hooks (on bare-metal)
In Short

libc is the bridge that turns C source code into a runnable program by connecting the language to the system underneath.


Hosted vs Freestanding Environments

The C standard defines two execution environments:

Hosted Environment (PC, Linux, Windows)

  • Full standard library expected
  • OS provides services (files, memory, time)
  • main() is called by the OS
  • printf() writes to a terminal

Freestanding Environment (Firmware, Kernel, Bootloader)

  • Only minimal headers guaranteed:
  • <stdint.h>
  • <stddef.h>
  • <stdbool.h>
  • <limits.h>
  • <float.h>
  • Everything else is optional
  • You must provide low-level hooks
Key Point

Embedded systems live in a freestanding world.

Headers may compile, but functions may not work without your support.


C Standard Library Availability by Environment

Feature Availability Table

Feature / Header User-space (Linux) RTOS App Bare-metal MCU OS Kernel
<stdint.h>
<stddef.h>
<stdbool.h>
<string.h> ⚠️
<stdio.h> ⚠️ ⚠️
printf() ⚠️ ⚠️
File I/O (fopen)
<stdlib.h> ⚠️ ⚠️
malloc() / free() ⚠️ ⚠️
<math.h> ⚠️ ⚠️
<time.h> ⚠️ ⚠️
Direct HW access ⚠️

Legend: ✅ Available | ⚠️ Limited/optional | ❌ Not available

Why Features Are Missing

Feature Why it exists Why it may NOT exist
printf() Needs output stream No console/UART configured
File I/O OS provides filesystem No filesystem
malloc() Requires heap allocator No heap defined
Floating point Hardware FPU or emulation Code size, speed concerns
time() System clock/RTC No time source

Call Chains: Where Does printf() Go?

User-Space Application (Linux)

Application code
└── printf("Hello")
    └── libc (stdio)
        └── buffering / formatting
            └── write() syscall
                └── OS kernel
                    └── device driver
                        └── hardware

Bare-Metal MCU (Pico, STM32)

Application code
└── printf("Hello")
    └── newlib/newlib-nano
        └── _write()   ← YOU implement this
            └── HAL / driver
                └── UART registers
                    └── hardware

OS Kernel (Linux Kernel)

Kernel code
└── printk("Hello")   ← NOT printf!
    └── kernel logging subsystem
        └── console driver
            └── hardware
Same Function Name ≠ Same Execution Path
  • printf() in Linux → OS syscall
  • printf() on MCU → UART driver (if you implement it)
  • printf() in kernel → does not exist

Memory Handling: The Biggest Confusion

The Three Memory Models

1. C on an OS (Hosted)

Virtual address space (per process)
├── Code (text)
├── Globals (.data / .bss)
├── Heap      ← managed by libc + OS
└── Stack     ← per thread
  • Heap existence: OS provides it
  • Allocation: malloc() asks OS for memory
  • Failure: Returns NULL

2. C on Bare-Metal MCU (Freestanding)

Physical RAM
├── .data / .bss
├── Heap   ← YOU define it (or don't)
└── Stack  ← fixed size
  • Heap existence: Only if you create it
  • Allocation: malloc() moves heap pointer
  • Failure: Stack/heap collision = crash

3. MicroPython (Managed Runtime)

Physical RAM
├── MicroPython runtime
├── GC heap (objects)
└── Stack (VM + C stack)
  • Heap existence: Runtime provides it
  • Allocation: Implicit when creating objects
  • Deallocation: Garbage collector (non-deterministic)

Memory Ownership Comparison

Question C + OS C Bare-Metal MicroPython
Is heap guaranteed? ✅ (GC heap)
Who owns memory? OS + process Application Runtime
Who decides allocation? Programmer Programmer Runtime
Who frees memory? Programmer Programmer GC
Is allocation explicit?
Is timing deterministic? ⚠️

In Short

  • OS C: The OS lends you memory.
  • Bare-metal C: You own the memory.
  • MicroPython: The runtime owns the memory.

Hardware Constraints on C

The C standard assumes certain capabilities that not all hardware provides:

C Concept Assumes But Some MCUs...
Function calls with locals Stack pointer exists Some 8-bit PICs have no stack pointer
Recursion Unlimited stack depth Hardware call stack may be 8 levels deep
32-bit integers Native 32-bit ops 8-bit CPU needs multiple instructions
Floating point FPU or software lib May not exist, or be very slow
Pointers Uniform address space Harvard architecture: code/data separate
Same C Code, Different Reality

int result = a * b;
- On 32-bit ARM: Single MUL instruction - On 8-bit AVR: Library call, multiple cycles - On DSP: May have special MAC instruction


Common Misconceptions

Thing Why It Seems Standard What It Actually Is
printf() output to UART "It prints!" Vendor/BSP/HAL hook
malloc() on MCU "It compiles!" Linker + runtime choice
sleep_ms() "Looks standard" SDK/HAL function
GPIO functions "Written in C" Hardware abstraction layer
RTOS APIs "Used in C code" OS services

MicroPython vs C Comparison

Aspect C (bare-metal) MicroPython
Memory allocation Explicit (malloc/free) Implicit (objects, GC)
Programmer awareness Required Hidden
Heap management Manual or static Automatic + GC
Allocation failure Returns NULL Raises MemoryError
Determinism High (if static) Lower (GC pauses)
Real-time suitability Excellent Limited
Key Distinction

MicroPython does not expose the concept of dynamic memory allocation, even though it performs dynamic allocation internally.


Safe Subset for Embedded C

Always Safe (Use Freely)

#include <stdint.h>   // Fixed-width integers
#include <stddef.h>   // size_t, NULL
#include <stdbool.h>  // bool type
#include <string.h>   // String/memory functions

Use With Care (Understand Implications)

#include <stdlib.h>   // malloc exists? heap configured?
#include <stdio.h>    // printf goes where?
#include <math.h>     // Code size? FPU available?
#include <time.h>     // What is "time" on this platform?

Summary: Environment Comparison

Layer Talks to libc Talks to OS Talks to HW
User-space app
RTOS app ⚠️ ⚠️
Bare-metal firmware ⚠️
OS kernel

Key Takeaways

  1. The C standard defines interfaces, not implementations
  2. libc is what makes those interfaces real — different environments have different libcs
  3. Embedded systems are freestanding — most of stdlib is optional
  4. Same function name ≠ same behaviorprintf() means different things in different contexts
  5. Memory handling differs radically — OS vs bare-metal vs managed runtime
  6. Hardware constrains what C can do — not all C concepts map to all CPUs