Zig NEWS

Cover image for Programming SAM E51 Curiosity Nano with Zig
David Sugar
David Sugar

Posted on • Updated on • Originally published at r4gus.github.io

Programming SAM E51 Curiosity Nano with Zig

EDIT: a more detailed description on how to add a new chip to MicroZig can be found here

This post is about my process of writing a simple program for the SAM E51 Curiosity Nano board. The goal is to turn the User LED (pin PA14) on.

I used MicroZig and Regz to get started.

Code generation using Regz

The board uses the ATSAME51J20A (Arm M4f) processor and because MicroZig doesn't support it yet, the first step was to grap the corresponding SVD File and generate Zig code from it using Regz.

$ regz ATSAME51J20A.svd > registers.zig
Enter fullscreen mode Exit fullscreen mode

Note: I found that the generated code won't
compile because the addresses (e.g. base_address)
used are of type comptime_int. I could fix it by
casting the addresses to pointers using @intToPtr,
e.g. pub const base_address = @intToPtr([*]u8, 0x41008000);.

.\libs\microzig\src\modules\chips\atsame51j20a\registers.zig:10953:12: error: expected pointer, found 'comptime_int'
       }, base_address);
           ^

Chip specific code

Next, I had to define a parsePin function as well as some additional GPIO functions, which MicroZig is going to use to e.g. set the direction of a pin.

// 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
};

/// 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");
    };
}
Enter fullscreen mode Exit fullscreen mode

The parsePin function takes a string like PA14 and extracts the port and pin number from it. The ports are defined in registers.zig (created using Regz) as an array of two packed structs, where A corresponds to index 0 and B to 1.

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

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

            /// Data Direction Clear
            DIRCLR: u32,

            /// Data Direction Set
            DIRSET: u32,

            // ...

        }, base_address);
    };
Enter fullscreen mode Exit fullscreen mode

The functions setOutput and setInput can be used to control the direction of a pin and the functions read and write are for reading from and writing to the specified pin.

// atsame51j20a.zig
pub const gpio = struct {
    pub fn setOutput(comptime pin: type) void {
        pin.gpio_port[pin.port_number].DIRSET |= pin.pin_mask;
    }

    pub fn setInput(comptime pin: type) void {
        pin.gpio_port[pin.port_number].DIRCLR |= pin.pin_mask;
    }

    pub fn read(comptime pin: type) micro.gpio.State {
        _ = pin;
        return micro.gpio.State.low;
    }

    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,
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Then I placed registers.zig and atsame51j20a.zig in a new Folder atsame51j20a under microzig/src/modules/chips, which I added as a submodule to my project.

Define the chip

Last but not least I had to define the chip in microzig/src/chips.zig. The memory regions are from
the SAM D5x/E5x Family Data Sheet page 53.

// 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 },
    },
};
Enter fullscreen mode Exit fullscreen mode

Setup the build script

I followed the instructions of the MicroZig readme to setup the build script.

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,
        "zig-ctap",
        "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.install();
}
Enter fullscreen mode Exit fullscreen mode

A simple main

As already mentioned the goal is to compile a program successfully and use it to turn the User LED (PA14) on.

const micro = @import("microzig");

// Get the pin of the user led; Port A, Pin 14.
// This will call `parsePin` under the hood.
const status_led_pin = micro.Pin("PA14");

pub fn main() !void {
    // setup the status_led_pin as GPIO pin
    const status_led = micro.Gpio(status_led_pin, .{
        .mode = .output,
        .initial_state = .low,
    });
    // this will call `setOutput` and `write(.low)`
    // under the hood.
    status_led.init();

    while (true) {}
}
Enter fullscreen mode Exit fullscreen mode

Flash program

After building the project using zig build I used the Device Programming Ctrl + Shift + P feature of Microchip Studio (Windows) to flash the device.

flashing the device

If you know a program on linux which I can use to flash the device please let me know :)

You can find the code on Github.

Latest comments (1)

Collapse
 
kristoff profile image
Loris Cro

Thank you for sharing! Seems that the Zig Embedded Group is starting to get some traction, nice!