# ARM and Embedded Targets

### ARM and Embedded Targets

ARM is a CPU architecture family used in phones, tablets, laptops, routers, Raspberry Pi boards, microcontrollers, servers, and many embedded devices. When you write Zig for ARM, you are usually writing for a specific machine, not just for “ARM” in general.

For beginners, the first idea is this: a target is more than an operating system. It includes the CPU architecture, the operating system, the ABI, and sometimes the C library.

Examples:

```text
aarch64-linux
arm-linux-gnueabihf
thumb-freestanding
aarch64-macos
aarch64-windows
```

These names tell Zig what kind of machine code to produce.

#### ARM vs AArch64

You will often see two broad ARM families:

```text
arm
aarch64
```

`arm` usually means 32-bit ARM.

`aarch64` means 64-bit ARM.

A Raspberry Pi running a 64-bit Linux system may use:

```text
aarch64-linux
```

An older 32-bit ARM Linux system may use something like:

```text
arm-linux-gnueabihf
```

An Apple Silicon Mac uses:

```text
aarch64-macos
```

The CPU architecture matters because machine code for one architecture cannot run directly on another.

#### Cross-Compiling to ARM

Zig can build ARM binaries from another machine.

For example, on an x86_64 Linux laptop, you can build for 64-bit ARM Linux:

```bash
zig build-exe main.zig -target aarch64-linux
```

You can build for Apple Silicon macOS:

```bash
zig build-exe main.zig -target aarch64-macos
```

You can build for 64-bit ARM Windows:

```bash
zig build-exe main.zig -target aarch64-windows
```

This is useful because embedded devices may be slow, small, or inconvenient to build on directly. You can compile on a powerful development machine and copy the binary to the target device.

#### Native ARM Linux Example

Here is a simple Zig program:

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

pub fn main() void {
    std.debug.print("Hello from ARM!\n", .{});
}
```

Build for 64-bit ARM Linux:

```bash
zig build-exe main.zig -target aarch64-linux
```

Copy it to the ARM device:

```bash
scp main user@device:/home/user/main
```

Run it on the device:

```bash
chmod +x main
./main
```

This workflow is common for Raspberry Pi, small servers, ARM development boards, and edge devices.

#### Embedded Does Not Always Mean Linux

Some embedded devices run Linux. Others do not have an operating system at all.

A Raspberry Pi running Ubuntu or Raspberry Pi OS is an embedded-like ARM Linux system.

A microcontroller such as an ARM Cortex-M chip is usually bare metal. There may be no filesystem, no process model, no virtual memory, and no normal terminal.

That difference is huge.

On ARM Linux, this may work:

```zig
const file = try std.fs.cwd().openFile("data.txt", .{});
```

On a bare-metal microcontroller, there may be no filesystem, so that code has no meaning.

#### Freestanding Targets

For bare-metal and embedded work, you may see targets using `freestanding`:

```text
thumb-freestanding
arm-freestanding
aarch64-freestanding
```

`freestanding` means Zig should not assume a normal operating system.

That affects your program design. You may not have:

```text
files
processes
environment variables
standard input
standard output
dynamic libraries
normal heap allocation
```

Instead, you often work directly with memory-mapped registers, interrupts, startup code, linker scripts, and hardware manuals.

#### Memory-Mapped I/O

Embedded programs often control hardware through memory-mapped I/O.

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

A simplified example:

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

pub fn main() void {
    gpio.* = 1;
}
```

This says: treat address `0x4002_0000` as a pointer to a volatile 32-bit register, then write `1` to it.

The word `volatile` matters. It tells the compiler that reading or writing this memory has effects outside normal program memory. The compiler must not casually remove or reorder it as if it were an ordinary variable.

This example is simplified. Real hardware code depends on the exact chip, register layout, clock setup, and board design.

#### Why `volatile` Matters

Consider this ordinary variable:

```zig
var x: u32 = 0;
x = 1;
x = 2;
```

The compiler may notice that `x = 1` is overwritten and remove it.

But with hardware registers, every write may matter. Writing `1` may turn on a device. Writing `2` may change a mode. The compiler must preserve those operations.

That is why embedded register pointers usually use `volatile`.

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

Both writes are meaningful.

#### Startup Code

On a desktop program, the operating system starts your process and calls into your program.

On bare metal, there may be no operating system. Something still has to happen before your main logic runs.

Startup code may need to:

```text
set the stack pointer
initialize memory
zero the BSS section
copy initialized data into RAM
configure clocks
set up interrupt vectors
call main
```

In a normal beginner Zig program, you do not see this. In embedded Zig, you may need to provide it or use a board support package that provides it.

#### Linker Scripts

A linker script tells the linker where code and data should go in memory.

Embedded devices often have separate memory regions:

```text
flash memory
RAM
memory-mapped peripherals
bootloader region
```

A program may need code placed in flash and variables placed in RAM.

A linker script describes that layout. Without the right memory layout, the binary may build but fail to boot.

This is one reason embedded programming is more hardware-specific than ordinary desktop programming.

#### Allocators in Embedded Programs

On small embedded systems, heap allocation may be limited or avoided.

This means you may prefer:

```text
fixed arrays
static buffers
ring buffers
arena allocators
fixed buffer allocators
```

Instead of:

```text
allocate whenever needed
grow data structures freely
depend on a large heap
```

Zig’s explicit allocator model helps here. If a function needs memory, it usually asks for an allocator. On embedded systems, you can pass a fixed buffer allocator instead of a general-purpose heap.

Example:

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

pub fn main() !void {
    var backing: [1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&backing);
    const allocator = fba.allocator();

    const buffer = try allocator.alloc(u8, 128);
    defer allocator.free(buffer);

    _ = buffer;
}
```

This program allocates from a fixed 1024-byte buffer. It does not need an operating system heap.

#### No Standard Output

On a microcontroller, this may not work:

```zig
std.debug.print("hello\n", .{});
```

There may be no terminal.

Embedded programs often output through:

```text
UART serial
semihosting
JTAG or SWD debugger
LED blinking
logging over USB
custom debug buffers
```

For beginners, the classic embedded “hello world” is often blinking an LED, not printing text.

#### Interrupts

Embedded systems often respond to hardware events through interrupts.

An interrupt can happen when:

```text
a timer fires
a button is pressed
data arrives on UART
an ADC conversion finishes
a network packet arrives
```

Interrupt code must be careful. It may run at unexpected times. It should usually be short, predictable, and avoid heavy work.

Zig can be used for this kind of programming, but interrupts require target-specific setup and hardware knowledge.

#### Endianness

ARM systems are commonly little-endian, but low-level code should still understand endianness.

Endianness describes byte order for multi-byte values.

For example, the 32-bit value:

```text
0x12345678
```

may be stored in memory as:

```text
78 56 34 12
```

on a little-endian system.

This matters when reading binary protocols, hardware registers, files, and network packets.

Use standard library helpers or explicit conversions when byte order matters.

#### Alignment

Some ARM systems care strongly about alignment.

A 32-bit value may need to be read from an address divisible by 4. Misaligned access may be slower, unsupported, or faulting depending on the CPU and configuration.

Zig makes alignment part of pointer types. This helps you express and check memory assumptions.

For embedded code, alignment is not a detail. It can be the difference between a working program and a hard fault.

#### Build Modes

Debug builds are useful while learning, but embedded systems often have limited memory and timing constraints.

You may build with:

```bash
zig build-exe main.zig -target thumb-freestanding -O ReleaseSmall
```

or:

```bash
zig build-exe main.zig -target thumb-freestanding -O ReleaseFast
```

`ReleaseSmall` focuses on smaller code size.

`ReleaseFast` focuses on speed.

For embedded targets, code size can matter as much as performance because flash memory may be limited.

#### Practical Beginner Path

A sensible learning path is:

First, build native Zig programs on your computer.

Then cross-compile a simple program to ARM Linux, such as Raspberry Pi.

Then learn fixed buffers and allocator control.

Then study bare-metal basics: memory maps, startup code, linker scripts, volatile registers, and interrupts.

Then target a specific microcontroller board.

Do not try to learn all embedded topics at once. Embedded Zig is a mix of language knowledge, compiler knowledge, hardware knowledge, and debugging skill.

#### Complete Example: Fixed Buffer Allocation

This example does not depend on Linux, macOS, or Windows behavior. It shows a style that is useful for embedded systems:

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

fn fill(buffer: []u8, value: u8) void {
    for (buffer) |*byte| {
        byte.* = value;
    }
}

pub fn main() !void {
    var memory: [256]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&memory);
    const allocator = fba.allocator();

    const data = try allocator.alloc(u8, 64);
    defer allocator.free(data);

    fill(data, 0xaa);

    std.debug.print("filled {d} bytes\n", .{data.len});
}
```

On a desktop system, this prints a message. On a real bare-metal system, you would replace the print call with board-specific output.

The important part is the memory style. The program uses a known fixed buffer instead of assuming an unlimited heap.

#### The Practical View

ARM support in Zig is useful because Zig makes cross-compilation, memory control, and low-level access part of the normal language experience.

For ARM Linux, Zig feels close to ordinary Linux programming.

For bare-metal embedded targets, Zig becomes a systems language for direct hardware control. You must understand the chip, the board, the memory map, the linker setup, and the startup path.

Start with ARM Linux if you are new. Move to bare metal after you are comfortable with targets, pointers, fixed memory, and the standard library boundaries.

