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:
- The C language — syntax, semantics, what the code means
- The C standard library — a contract for common functions
- 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.
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 OSprintf()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 syscallprintf()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)
- Heap existence: Only if you create it
- Allocation:
malloc()moves heap pointer - Failure: Stack/heap collision = crash
3. MicroPython (Managed Runtime)
- 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
- On 32-bit ARM: SingleMUL 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
- The C standard defines interfaces, not implementations
- libc is what makes those interfaces real — different environments have different libcs
- Embedded systems are freestanding — most of stdlib is optional
- Same function name ≠ same behavior —
printf()means different things in different contexts - Memory handling differs radically — OS vs bare-metal vs managed runtime
- Hardware constrains what C can do — not all C concepts map to all CPUs
Related Topics
- Tutorial: Robot Unboxing - First hands-on session
- Execution Models - Hosted vs freestanding