# Embedded Development

### Embedded Development

Embedded development means writing software for small computers inside devices.

These devices may be:

```text
microcontrollers
sensors
robots
keyboards
drones
medical devices
industrial controllers
network appliances
small displays
motor controllers
```

An embedded program often runs close to the hardware. It may not have an operating system. It may not have a filesystem. It may not have dynamic memory. It may only have a few kilobytes of RAM.

Zig is a strong fit for this kind of work because it gives direct control over memory, layout, linking, and machine instructions.

#### What Makes Embedded Different

On a normal desktop program, you often assume many things are available:

```text
heap allocation
threads
files
environment variables
networking
standard input and output
large memory
operating system services
```

In embedded programming, many of these may be missing.

A microcontroller program may start directly from a reset vector. It may write to hardware registers. It may never return from `main`.

The machine is smaller, but the rules are stricter.

#### Bare Metal

Bare metal means your program runs without an operating system.

There is no Linux, Windows, or macOS underneath your code.

Your program is responsible for low-level setup, such as:

```text
startup code
stack pointer
interrupt table
memory sections
clock setup
device initialization
```

Zig can target bare-metal systems, but you need to understand the target platform. The compiler can generate code, but it cannot magically know how your board is wired.

#### Memory-Mapped Registers

Embedded programs often control hardware through memory-mapped registers.

That means a hardware device appears at a fixed memory address. Reading or writing that address talks to the device.

Example idea:

```zig
const gpio_addr: usize = 0x4002_0000;
const gpio = @as(*volatile u32, @ptrFromInt(gpio_addr));

gpio.* = 1;
```

The exact address depends on the chip.

The important part is `volatile`.

```zig
*volatile u32
```

This tells Zig that the value can change outside normal program flow. Hardware may change it. The compiler must not optimize the access away.

Without `volatile`, the compiler may assume repeated reads or writes are ordinary memory operations. That assumption is wrong for hardware registers.

#### Register Layouts with Packed Structs

Hardware registers often contain bit fields.

For example, one 32-bit register may contain several flags:

```text
bit 0: enable
bit 1: interrupt enabled
bits 2..3: mode
bits 4..31: reserved
```

Zig can model this with packed structs:

```zig
const ControlRegister = packed struct {
    enable: bool,
    interrupt_enable: bool,
    mode: u2,
    reserved: u28,
};
```

This gives names to individual bits.

A register pointer might look like:

```zig
const control_addr: usize = 0x4002_0000;
const control = @as(*volatile ControlRegister, @ptrFromInt(control_addr));
```

Then code can write:

```zig
control.enable = true;
control.mode = 2;
```

This is clearer than manually shifting and masking integers everywhere.

Still, be careful. Hardware layout must match the chip documentation exactly.

#### No Hidden Allocation

Embedded systems often avoid heap allocation.

A heap requires memory management. On a small device, that can introduce fragmentation, failure cases, and timing uncertainty.

Zig helps because allocation is explicit.

If a function needs an allocator, you see it:

```zig
fn buildMessage(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.alloc(u8, 64);
}
```

In embedded code, you can avoid such functions or use a fixed buffer allocator.

Example:

```zig
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);

const allocator = fba.allocator();
```

This allocator can only use the provided buffer. It never asks an operating system for more memory.

That is useful when memory must be bounded.

#### Fixed Buffers

Fixed buffers are common in embedded code.

Instead of this:

```zig
var list = std.ArrayList(u8).init(allocator);
```

you may use this:

```zig
var buffer: [256]u8 = undefined;
var len: usize = 0;
```

Then you carefully write into the array.

```zig
fn appendByte(buffer: []u8, len: *usize, byte: u8) !void {
    if (len.* >= buffer.len) {
        return error.BufferFull;
    }

    buffer[len.*] = byte;
    len.* += 1;
}
```

This looks manual, but it is predictable. The memory usage is visible and bounded.

#### Interrupts

Many embedded programs respond to interrupts.

An interrupt is a hardware event that temporarily stops normal code and runs a special function.

Examples:

```text
timer fired
button pressed
data arrived on UART
ADC conversion finished
network packet received
```

Interrupt handlers must be small and careful.

They should usually:

```text
read or clear the hardware status
store minimal data
set a flag
return quickly
```

They should not usually:

```text
allocate memory
do long computations
print large logs
block waiting for another event
```

In Zig, interrupt setup depends heavily on the target architecture and board support code.

#### Volatile Is Not Atomic

This distinction matters.

`volatile` tells the compiler not to remove or reorder a memory access in ways that break hardware interaction.

`atomic` is about safe communication between threads or interrupt contexts.

If normal code and an interrupt handler share data, you may need atomics or careful interrupt masking.

Example concept:

```zig
const std = @import("std");

var flag = std.atomic.Value(bool).init(false);
```

Then one context can set the flag and another can read it.

The exact design depends on the target and concurrency model.

#### Linker Scripts

Embedded programs need precise memory layout.

A chip may have:

```text
flash memory at one address
RAM at another address
special boot memory
interrupt vector table location
memory-mapped peripheral regions
```

The linker decides where code and data go.

For embedded work, you often need a linker script.

A linker script describes memory regions and places program sections into them.

Conceptually:

```text
.text goes into flash
.rodata goes into flash
.data is stored in flash but copied to RAM
.bss is zeroed in RAM
stack goes into RAM
```

Zig can work with linker scripts, but the script must match the chip.

#### Startup Code

Before normal Zig code runs, the system may need startup code.

Startup code may:

```text
set the stack pointer
copy initialized data from flash to RAM
zero the BSS section
configure clocks
install interrupt vectors
call main
```

On a desktop operating system, this is handled for you.

On bare metal, you may need to provide it yourself or use existing board support code.

#### Panic Behavior

On a desktop, a panic may print a stack trace.

On embedded hardware, there may be nowhere to print.

You may define a panic handler that:

```text
turns on an LED
sends a message over UART
halts the CPU
resets the board
enters an infinite loop
```

The simplest panic behavior is often:

```zig
pub fn panic(msg: []const u8, stack_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
    _ = msg;
    _ = stack_trace;
    _ = ret_addr;

    while (true) {}
}
```

The exact signature may vary with Zig version, but the idea is stable: embedded programs often provide their own panic behavior.

#### Cross Compilation

Embedded development usually means cross compilation.

You compile on your laptop, but the program runs on another CPU.

For example:

```text
build machine: x86_64 laptop
target machine: ARM Cortex-M microcontroller
```

Zig is designed to cross-compile. You choose a target when building.

Conceptually:

```bash
zig build-exe src/main.zig -target thumb-freestanding-eabi
```

The exact target triple depends on your chip.

Common embedded targets include ARM, RISC-V, and freestanding variants.

#### Freestanding Targets

A freestanding target means there is no assumed operating system.

In Zig target names, this often appears as:

```text
freestanding
```

A hosted target uses an operating system ABI.

A freestanding target does not.

This affects what parts of the standard library are available. File APIs, process APIs, and networking APIs usually do not make sense on bare metal.

#### The Standard Library in Embedded Code

Zig’s standard library is modular, but not every part applies to embedded systems.

Useful parts may include:

```text
integer utilities
memory utilities
fixed buffer allocators
formatting into buffers
atomics
math helpers
testing on host builds
```

Less relevant parts may include:

```text
filesystem APIs
process APIs
environment variables
OS networking APIs
thread APIs
```

Embedded Zig code often uses a small subset of `std`.

#### Testing Embedded Logic

You should separate hardware-independent logic from hardware-specific code.

Hardware-independent code can be tested on your normal computer.

Example:

```zig
fn checksum(bytes: []const u8) u8 {
    var sum: u8 = 0;

    for (bytes) |byte| {
        sum +%= byte;
    }

    return sum;
}

test "checksum" {
    const data = [_]u8{ 1, 2, 3 };
    try std.testing.expectEqual(@as(u8, 6), checksum(&data));
}
```

This test does not need the microcontroller.

Only the hardware access layer needs the actual device.

This design saves time.

#### Hardware Abstraction

A good embedded program separates:

```text
application logic
driver code
raw register access
board-specific setup
```

For example:

```text
app.zig
drivers/uart.zig
drivers/gpio.zig
boards/my_board.zig
startup.zig
```

The application should not be full of raw addresses.

Instead of this everywhere:

```zig
const reg = @as(*volatile u32, @ptrFromInt(0x4002_0000));
reg.* = 1;
```

prefer a small driver API:

```zig
gpio.setHigh(.led);
```

The raw address still exists, but it is isolated.

#### Timing and Determinism

Embedded programs often care about timing.

A motor controller, sensor reader, or communication protocol may need predictable timing.

Avoid hidden costs:

```text
unexpected allocation
large formatting operations
unbounded loops
blocking I/O
implicit dynamic dispatch
uncontrolled recursion
```

Zig helps by making many costs visible, but you still need discipline.

For real-time systems, you must know the worst-case behavior of critical code.

#### Embedded Logging

Logging is useful, but embedded logging must be controlled.

Possible outputs:

```text
UART
semihosting
RTT
USB serial
memory ring buffer
LED blink codes
```

Do not assume `std.debug.print` is available or appropriate.

A logging system for embedded code should be:

```text
small
optional
bounded
safe in failure paths
safe around interrupts
```

In release builds, you may disable most logs.

#### Common Embedded Mistakes

A common mistake is treating bare metal like a small desktop.

There may be no files, no heap, no console, and no operating system.

Another mistake is ignoring `volatile` for hardware registers.

A third mistake is putting too much work inside interrupt handlers.

A fourth mistake is spreading raw register addresses throughout the codebase.

A fifth mistake is failing to define memory ownership. Even without heap allocation, ownership matters for buffers and shared state.

#### A Practical Embedded Zig Style

For small embedded projects, use this style:

```text
keep memory static or bounded
use fixed buffers
isolate raw registers
make hardware drivers small
test pure logic on the host
avoid allocation in interrupts
avoid complex formatting in critical paths
write explicit startup and panic behavior
version-control linker scripts and board definitions
```

This keeps the system understandable.

#### The Main Idea

Embedded Zig is about control.

You control memory. You control layout. You control startup. You control hardware access. You control what parts of the standard library you use.

That control has a cost: you must understand the target machine.

Zig does not hide the hardware. It gives you a clear language for working with it.

