MicroZig Internals

Module Structure

The build portion of MicroZig sets up a dependency graph like the following.

Your application lives in app; that’s where main() resides. root contains the entry point and will set up [zero-initialized data] and [uninitialized data]. This is all encapsulated in an EmbeddedExecutable object. It has methods to add dependencies acquired from the package manager.

The microzig module has different namespaces, some are static, but the nodes you see in the diagram above are switched out according to your configured hardware.

Configurable Modules under microzig

The configurable modules, with the exception of config, are able to import microzig. This exists so that one module may access another through microzig. This allows us to have patterns like the hal grabbing the frequency of an external crystal oscillator from board. Information stays where it’s relevant. Circular dependencies of declarations will result in a compile error.

cpu

This module models your specific CPU and is important for initializing memory. Generally, you shouldn’t need to define this yourself, it’s likely that MicroZig will have the definition for you.

Further research is needed for SOCs with multiple, heterogeneous CPUs. Likely it means patching together multiple EmbeddedExecutables.

chip

This module is intended for generated code from (Regz)[https://github.com/ZigEmbeddedGroup/microzig/tree/main/tools/regz].

hal

This module contains hand-written code for interacting with the chip.

Linker Script Generation

Every firmware needs a linker script that places stuff where it belongs in memory. When porting microzig to a new target you must face the challenge of dealing with a linker script. But fear not as microzig has your back (in most cases). Let’s checkout the linker_script field in Target.

linker_script: LinkerScript = .{},
pub const LinkerScript = struct {
    /// Will anything be auto-generated for this linker script?
    generate: GenerateOptions = .{ .memory_regions_and_sections = .{} },
    /// Linker script path. Will be appended after what is auto-generated if it's not null.
    file: ?LazyPath = null,

    pub const GenerateOptions = union(enum) {
        /// Only generates a comment with target info.
        none,
        /// Only generates memory regions.
        memory_regions,
        /// Generates memory regions and default sections based on the provided options.
        memory_regions_and_sections: struct {
            /// Where should rodata go?
            rodata_location: enum {
                /// Place rodata in the first region tagged as flash.
                flash,
                /// Place rodata in the first region tagged as ram.
                ram,
            } = .flash,
        },
    };
};

For an example let’s look at the target definition of rp2040. In this case we need a linker script that should also place the bootrom at the beginning of flash. Fortunately, we can still mostly auto-generate one and just patch it up a bit.

// port/raspberrypi/rp2xxx/build.zig

const chip_rp2040: microzig.Target = .{
    // ...
    .chip = .{
        // ...
        .memory_regions = &.{
            .{ .tag = .flash, .offset = 0x10000000, .length = 2048 * 1024, .access = .rx },
            .{ .tag = .ram, .offset = 0x20000000, .length = 256 * 1024, .access = .rwx },
        },
    },
    .linker_script = .{
        // The `generate` field defaults to `.memory_regions_and_sections`.

        // This will be appended at the end of the auto-generated linker
        // script.
        .file = b.path("ld/rp2040/sections.ld"),
    },
};
/* port/raspberrypi/rp2xxx/ld/rp2040/sections.ld */

SECTIONS
{
  .boot2 : {
    __boot2_start__ = .;
    KEEP (*(.boot2))
    __boot2_end__ = .;
  } > flash0

  ASSERT(__boot2_end__ - __boot2_start__ == 256,
    "ERROR: Pico second stage bootloader must be 256 bytes in size")
}
INSERT BEFORE .flash_start;

This is the generated linker script:

/*
 * Target CPU:  cortex_m0plus
 * Target Chip: RP2040
 */

/*
 * This section was auto-generated by microzig.
 */
MEMORY
{
  flash0 (rx!w) : ORIGIN = 0x10000000, LENGTH = 0x00200000
  ram0 (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00040000
}
SECTIONS
{
  .flash_start :
  {
    KEEP(*(microzig_flash_start))
  } > flash0

  .text :
  {
    *(.text*)
    *(.srodata*)
    *(.rodata*)
  } > flash0

  .ARM.extab : {
    *(.ARM.extab* .gnu.linkonce.armextab.*)
  } > flash0

  .ARM.exidx : {
    *(.ARM.exidx* .gnu.linkonce.armexidx.*)
  } > flash0

  .data :
  {
    microzig_data_start = .;
    *(.sdata*)
    *(.data*)
    KEEP(*(.ram_text))
    microzig_data_end = .;
  } > ram0 AT> flash0

  .bss (NOLOAD) :
  {
    microzig_bss_start = .;
    *(.sbss*)
    *(.bss*)
    microzig_bss_end = .;
  } > ram0

  .flash_end :
  {
    microzig_flash_end = .;
  } > flash0

  microzig_data_load_start = LOADADDR(.data);
}
/*
 * End of auto-generated section.
 */
SECTIONS
{
  .boot2 : {
    __boot2_start__ = .;
    KEEP (*(.boot2))
    __boot2_end__ = .;
  } > flash0

  ASSERT(__boot2_end__ - __boot2_start__ == 256,
    "ERROR: Pico second stage bootloader must be 256 bytes in size")
}
INSERT BEFORE .flash_start;

FYI