This series of posts is inspired by the Rust Discovery Book, a great learning resource on how to program microcontrollers using Rust. We’re going to program the SAM E51 Curiosity Nano using the Zig programming language, which is new and not quite production ready so be prepared to encounter some breaking changes.

Introduction

Scope

Eventually, the following topics will be covered:

  • How to write, build and flash an “embedded” (Zig) program using MicroZig, Regz and Microchip Studio (flashing).
  • Functionality (“peripherals”) commonly found in microcontrollers like:
    • digital input and output
    • pulse width modulation (PWM)
    • analog to digital converters (ADC)
    • common communication protocols like:
      • U(S)ART
      • I2C
  • Multitasking concepts:
    • cooperative vs preemptive multitasking
    • interrupts
    • schedulers
  • Cryptography

Approach

  • As much as possible beginner friendly, but because there are no existing resources for the ATSAME51J20A chip we need to set up everything ourselves.

Non-goals

What’s out of scope for this series of posts:

  • Teaching Zig. We’ll focus on programming microcontrollers.
  • Being a comprehensive text about electric circuit theory or electronics.

Note: This book will make exclusive use of the SAM E51 Curiosity Nano. Because we’re going to use Microchip Studio to flash the device, you need a Windows PC. I haven’t found a good tool for Linux yet.

Reporting problems

The source of this book is in this repository. If you encounter any typo or problem with the code, please report it on the issue tracker.

Other embedded Zig resources

The Zig Embedded Group offers more learning material on programming embedded devices and maintains the libraries (MicroZig and Regz) we’re going to use throughout this course.

Background

What’s a microcontroller?

A microcontroller is a system on a chip (SOC). Whereas your computer is made up of several discrete components: a processor, RAM, storage, an Ethernet port, etc.; a microcontroller has all those types of components built into a single chip. This makes it possible to build systems with fewer parts.

Why use Zig and not C or Rust?

Because you’re interested in Zig and microcontrollers. Honestly, there aren’t many reasons at the moment. You’ll encounter breaking changes because the language is still in development and there are almost no Zig frameworks/ tools for programming microcontrollers at the moment. If you want to create the next big thing, you’ll probably want to stay away from Zig at the moment.

Requirements

The primary requirement to read this series of posts is to know some Zig, i.e. to be at least somewhat comfortable with structs, pointers, and arrays.

Also, to follow this material you’ll need the following hardware:

MCU

  • A micro-B USB cable, to connect the Curiosity Nano to your computer. The cable must support data transfer.

FAQ: Can I follow this guide with a different development board?

Probably. While there are many different boards and processors, the general concepts stay similar. If you choose a different board, you’ll need the datasheets for the development board and processor, e.g. to lookup addresses.

Setting up a development environment

Because Zig supports cross-compilation out of the box, i.e. compiling code for an architecture that differs from the one you’re developing on, we don’t need to install any additional toolchains. Nevertheless, we still need some things before we can get started.

Documentation

Without documentation, it’s pretty much impossible to work with microcontrollers. We’ll be referring to the following documents throughout this book:

Tools

We’ll use the tools listed below.

Note: You can execute all stated instructions in a Shell/ PowerShell.

Zig

To install Zig you can follow the official getting started guide.

MicroZig

When it comes to MicroZig we’re left with two options. We can either add it directly as a submodule to our project or fork it and add our fork as a submodule. I’ll use the latter option because I’m going to make some changes to the library.

Regz

To install Regz just clone the project using Git and build it with Zig.

git clone https://github.com/ZigEmbeddedGroup/regz.git
cd regz
zig build

You can consider this a test that you’ve installed Zig correctly. Zig will place the binary in zig-out/bin/regz.

Microchip Studio

Just download the installer from the website and execute it.

Meet your hardware

Example Image

The SAM E51 Curiosity Nano evaluation board contains the following components:

  • A microcontroller (SAME51J20A)
  • Two LEDs (User LED and Power/Status LED)
  • A User Switch (Button)
  • A Debugger
  • One USB port

Of these components, the most important is the microcontroller (MCU), which is the big black square in the middle of the board. The MCU is what runs your code. When we talk about “programming a board”, in reality, we mean programming the MCU that is part of the board. Let’s take a closer look at the MCU.

SAME51J20A

The name of our MCU seems random at first but it contains a lot of information about its characteristics. SAM stands for Smart ARM Microcontroller (the product familiy), E51 tells you that the MCU is a ARM Coretex-M4F (advanced feature set + 2x Controller Area Network (CAN)), J means that the MCU has 64 Pins in total (which we’re going to control soon), 20 tells you that the MCU has 1 MB of flash memory for storing your program and A stands for default variant.

The chip is packed with different “peripherals”, i.e. building blocks that enable the MCU to fulfill different tasks like doing cryptography in hardware or talking to a computer using a serial interface. We’ll look at some of those peripherals in the future.

Set up the project

Before we can start programming, we need to set up our project. I’ll guide you through the steps necessary to add support for a new chip to the MicroZig library. Create a new folder and execute the following command within:

zig init-exe

Your project folder should now contain the following files:

  • build.zig - contains the build script for your project
  • src/main.zig - contains the main function (the entry point of your program)

The MicroZig library offers help when it comes to building the project and generalized interfaces for GPIO and peripherals. Create a new folder called libs and add MicroZig as a submodule.

Note: You may want to fork MicroZig and add your fork as a submodule.

cd libs
git submodule add https://github.com/ZigEmbeddedGroup/microzig.git

MicroZig supports a variety of chips (see microzig/src/modules/chips). To add support for our SAME51J20A chip, we add a new folder to microzig/src/modules/chips called atsame51j20a which will contain the chip-specific Zig code.

Generate Zig code from an SVD file

Vendors often publish files that describe the special function register layout of a microcontroller called System View Description (SVD). The Regz tool can translate an SVD file into Zig code that can be used with libraries like MicroZig to gain access to special function registers, e.g. the Port. Download the ATSAME51J20A.svd file and then use Regz to translate it into Zig code.

regz ATSAME51J20A.svd > microzig/src/modules/atsame51j20a/registers.zig

Note: I found a issue with Regz, where the tool translates addresses into numbers of type comptime_int. If you encounter an error like error: expected pointer, found 'comptime_int' use @intToPtr to cast the numbers to pointers, e.g. modify pub const base_address = 0x41008000 to pub const base_address = @intToPtr([*]u8, 0x41008000).

Update: The issue has been fixed.

Now let’s look at how the generated file is structured.

The file contains one large struct called registers which itself contains a lot of nested structs. Most of those nested structs represent either a peripheral like a port, serial interfaces, or other important memory-mapped registers to control the behavior and functionality of the chip.

The microcontroller we’re working with has 64 Pins split into two groups A and B with 32 Pins each. We can control each pin by manipulating the PORT (I/O Pin Controller) at address 0x41008000 (Port A) and 0x41008000 + 0x80 (Port B).

pub const registers = struct {
    // ...

    /// Port Module
    pub const PORT = struct {
        pub const base_address = @intToPtr([*]u8, 0x41008000);
        pub const version = "U22102.2.0";

        // ...
    }

    // ...
}

Each nested struct starts with a base_address definition which defines the address in memory at which the group of registers that make up the peripheral can be found. To get an overview of the memory mapping check out the datasheet on page 51.

 pub const GROUP = @ptrCast(*volatile [2]packed struct {
    /// Data Direction
    DIR: u32,

    /// Data Direction Clear
    DIRCLR: u32,

    /// Data Direction Set
    DIRSET: u32,

    // ...

}, base_address);

The base address, plus an optional offset, is then cast to a struct that represents one or multiple registers in memory. This makes it easier to work with peripherals because you’re able to reference specific memory regions by name. In the example above the base_address is cast to an array called GROUP of two structs, where the first struct (index 0) represents port group A (PA) and the second represents port group B (PB).

Say you want to set pin 14 of port group A (PA14) as output. To accomplish that you can access the first element of GROUP and write a 1 into bit 14 of the DIRSET register, i.e. registers.PORT.GROUP[0].DIRSET = (1 << 14). This is much better than writing something like *(int*)(41008000 + 8) = (1 << 14) because you can express your intentions more clearly.

One important thing to note is, that the nested structs represent an idealized version of the register layout, i.e. multiple structs can represent the same memory region (share the same address and size in memory), and the decision on which struct to use depends on the context you’re working in.

Defining the Chip

Next, we’ll write some Zig code for the ATSAME51J20A. Create a new file in microzig/src/modules/chips/atsame51j20a called atsame51j20a.zig and paste the following code:

// microzig/src/modules/chips/atsame51j20a/atsame51j20a.zig

pub const cpu = @import("cpu");
pub const micro = @import("microzig");
pub const chip = @import("registers.zig");

const regs = chip.registers;
pub usingnamespace chip;

pub const clock_frequencies = . {
    .cpu = 120_000_000, // Arm Cortex-M4 runs at 120 MHz
};

We import the register file we created in the last section as chip and assign the chip.registers constant to a new constant called regs so we don’t have to write so much. Also, we define the cpu clock frequency to be 120MHz.

The remaining goal of this chapter is to blink the status led (PA14). To make that happen we’re going to use the existing PIN and GPIO interface of microzig. Let’s start with the pin.

The function microzig.Pin takes a string of the form P{port}{number} and returns a anonymous struct that represents the given pin. This pin-wrapper is then used as argument by other functions, e.g. the GPIO functions we’ll implement soon. microzig.Pin expects each chip to implement the parsePin function.

// microzig/src/modules/chips/atsame51j20a/atsame51j20a.zig

/// Get access to the pin specified by `spec`.
///
/// - `spec`: P{port}{pin}
///     - `port`: A, B
///     - `pin`: 0..31
pub fn parsePin(comptime spec: []const u8) type {
    const invalid_format_msg = "The given pin '" ++ spec ++ "' has an invalid format. Pins must follow the format \"P{Port}{Pin}\" scheme.";

    if (spec[0] != 'P')
        @compileError(invalid_format_msg);
    if (spec[1] < 'A' or spec[1] > 'B') // J = 64 Pins; 2 Ports
        @compileError("Unknown port '" ++ spec[1..2] ++ "'. Supported ports: A, B.");

    return struct {
        // Try to parse the given pin number as u5, i.e. a value in '0'..'31'.
        const pin_number: u5 = @import("std").fmt.parseInt(u5, spec[2..], 10) catch @compileError(invalid_format_msg);
        const pin_mask: u32 = (1 << pin_number);
        // Port is either 'A' or 'B'.
        const port_number: usize = if (spec[1] == 'A') 0 else 1;
        const gpio_port = @field(regs.PORT, "GROUP");
    };
}

The parsePin function first checks that the specified port group and pin number are valid, and then returns a anonymous struct containing information about the pin. This includes a reference to the port, the port number, and a mask so we can set some pin-specific registers more easily. Now let’s implement the required I/O functions to toggle the LED.

// microzig/src/modules/chips/atsame51j20a/atsame51j20a.zig

pub const gpio = struct {
    // See SAM D5x/E5x Family Data Sheet page 807.

    /// Configure the given pin as output with input disabled.
    pub fn setOutput(comptime pin: type) void {
        // To use pin Pxy as an output, write bit y of the DIR register to '1'. This
        // can also be done by writing bit y int the DIRSET register to '1' - this
        // will avoid disturbing the configuration of other pins (datasheet p. 803).
        pin.gpio_port[pin.port_number].DIRSET = pin.pin_mask;
        // Disable input for the given pin.
        pin.gpio_port[pin.port_number].PINCFG[pin.pin_number].modify(.{.INEN = 0});
    }

    pub fn write(comptime pin: type, state: micro.gpio.State) void {
        switch (state) {
            .high => pin.gpio_port[pin.port_number].OUTSET = pin.pin_mask,
            .low => pin.gpio_port[pin.port_number].OUTCLR = pin.pin_mask,
        }
    }

    pub fn toggle(comptime pin: type) void {
        pin.gpio_port[pin.port_number].OUTTGL = pin.pin_mask;
    }
};

We define setOutput so we can configure the direction of a pin as output. One can configure the i’th pin of a port group as output by setting bit i of DIRSET to 1. The alternative would be to manipulate DIR directly but then you must be careful to maintain the state of the other pins.

Setting a pin as high/ low is as easy as setting the i’th pin of the OUTSET/ OUTCLR register to 1. Like before you can also write to the OUT register directly. The write function takes a pin and a state (.high or .low) and writes to the associated register.

Now let’s define the chip itself as a public constant named atsame51j20a in microchip/src/modules/chips.zig.

// microzig/src/modules/chips.zig

pub const atsame51j20a = Chip{
    .name = "ATSAME51J20A",
    .path = root_path ++ "chips/atsame51j20a/atsame51j20a.zig",
    .cpu = cpus.cortex_m4,
    .memory_regions = &.{
        // SAM D5x/E5x Family Data Sheet page 53
        MemoryRegion{ .offset = 0x00000000, .length = 1024 * 1024, .kind = .flash },
        MemoryRegion{ .offset = 0x20000000, .length = 256 * 1024, .kind = .ram },
    },
};

The definition includes the name of the chip, as well as the path to our atsame51j20a.zig file. As CPU we select cortex_m4 so that Zig knows for which architecture an ATSAME51J20A program should be compiled. cortex_m4 also defines, among other things, some CPU-specific startup code (clear the .bss section with zeroes, load the program from flash, …). The memory regions are taken from the data sheet of the ATSAME51J20A and tell Zig where the flash (the place where our program is stored on the chip) and ram are located.

Now, the only thing left is our main function.

Blinking a LED

The remaining code to blink a LED is actually quite simple. Let’s take a look and see how it works.

const micro = @import("microzig");

const status_led_pin = micro.Pin("PA14");

pub fn main() void {
    const status_led = micro.Gpio(status_led_pin, .{
        .mode = .output,
        .initial_state = .high,
    });
    status_led.init();

    while (true) {
        busyloop();
        status_led.toggle();
    }
}

fn busyloop() void {
    const limit = 5_000_000;

    var i: u32 = 0;
    while (i < limit) : (i += 1) {
        @import("std").mem.doNotOptimizeAway(i);
    }
}

At the top, we define a new status_led_pin (PA14) which is connected to the status LED of our board. The Pin function calls our parsePin function we defined earlier. Then we pass that pin to the Gpio function together with some options to set the pin as output with an initial state of high. The struct returned from Gpio is a wrapper that connects our status_led_pin to the GPIO functions we defined in atsame51j20a.zig but also offers additional functionality.

To actually initialize our status_led we must call the init function on it which will indirectly call the setOutput and write function we defined.

Note: I’d advice to read through gpio.zig and pin.zig to get a better understanding on what’s going on under the hood.

The rest of the code is trivial. We define the main loop of our program which contains exactly two instructions. Each turn we wait for a short period of time by wasting CPU cycles using the busyloop function. After that we toggle the PIN, i.e. if the pin is high we set it to low and vice versa.

Build it

Building our project is quite easy.

// build.zig

const std = @import("std");
const microzig = @import("libs/microzig/src/main.zig");

pub fn build(b: *std.build.Builder) void {
    const backing = .{
        .chip = microzig.chips.atsame51j20a,
    };

    const exe = microzig.addEmbeddedExecutable(
        b,
        "my-program",
        "src/main.zig",
        backing,
        .{
            // optional slice of packages that can be imported into your app:b
            // .packages = &my_packages,
        }
    );

    exe.inner.setBuildMode(.ReleaseSmall);
    exe.inner.strip = true;
    exe.inner.install();
}

We can leverage the addEmbeddedExecutable function of MicroZig to save us some work. All it takes is a backing (our chip), the name of our program, the path to our main function, and some optional packages to include.

To build the executable just enter zig build on the command line. If everything goes well you can find your executable in zig-out/bin/.

Flash it

Microchip Studio (Windows)

Open Microchip Studio and connect the board to your Computer using a USB cable. Then press Ctrl + Shift + P to open the Device Programmer. Select your device and then press the Apply button. Open the Memories tab and press the [...] button of the Flash section to select your program located in your-project/zig-out/bin. Finally, press Program to flash your device.

Flash program

EDBG (Linux, Mac, Windows)

The CMSIS-DAP programmer (EDBG) works on Linux, Mac and Windows.

Besides the program itself you’ll need a tool like arm-none-eabi-objcopy to extract the binary content from the executable (e.g. ELF) generated by zig build.

On Ubuntu you can run sudo apt install binutils-arm-none-eabi.

Connect the board to your Computer using a USB cable. You can verify that the onboard debugger has been recognized using the following command:

$ edbg -l
Attached debuggers:
  0: MCHP3360011800003314 - Microchip Technology Incorporated nEDBG CMSIS-DAP

Then build your project and extract the binary content.

$ zig build && arm-none-eabi-objcopy -O binary zig-out/bin/your-program kernel.bin

Last but not least use EDBG to upload your program.

$ sudo edbg -b -t same51 -pv -f kernel.bin 
Debugger: Microchip nEDBG CMSIS-DAP MCHP3360011800003314 00.01.0000 (S)
Clock frequency: 16.0 MHz
Target: SAM E51J20A (Rev D)
Programming.... done.
Verification....... done.

After a few seconds, you should see a blinking LED next to the push button.

Flash program