diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..0f1955c --- /dev/null +++ b/build/README.md @@ -0,0 +1,4 @@ +# MicroZig Build Support + +This package is meant to provide build utilities to create embedded firmware easily. + diff --git a/build/build.zig b/build/build.zig new file mode 100644 index 0000000..748833f --- /dev/null +++ b/build/build.zig @@ -0,0 +1,979 @@ +//! Some words on the build script here: +//! We cannot use a test runner here as we're building for freestanding. +//! This means we need to use addExecutable() instead of using + +const std = @import("std"); +const uf2 = @import("uf2"); + +//////////////////////////////////////// +// MicroZig Gen 2 Interface // +//////////////////////////////////////// + +fn root() []const u8 { + return comptime (std.fs.path.dirname(@src().file) orelse "."); +} +const build_root = root(); + +const MicroZig = @This(); + +b: *std.Build, +self: *std.Build.Dependency, + +/// Creates a new instance of the MicroZig build support. +/// +/// This is necessary as we need to keep track of some internal state to prevent +/// duplicated work per firmware built. +pub fn init(b: *std.Build, dependency_name: []const u8) *MicroZig { + const mz = b.allocator.create(MicroZig) catch @panic("out of memory"); + mz.* = MicroZig{ + .b = b, + .self = b.dependency(dependency_name, .{}), + }; + return mz; +} + +/// This build script validates usage patterns we expect from MicroZig +pub fn build(b: *std.Build) !void { + const uf2_dep = b.dependency("uf2", .{}); + + const build_test = b.addTest(.{ + .root_source_file = .{ .path = "build.zig" }, + }); + + build_test.addAnonymousModule("uf2", .{ + .source_file = .{ .cwd_relative = uf2_dep.builder.pathFromRoot("build.zig") }, + }); + + const install_docs = b.addInstallDirectory(.{ + .source_dir = build_test.getEmittedDocs(), + .install_dir = .prefix, + .install_subdir = "docs", + }); + + b.getInstallStep().dependOn(&install_docs.step); + + // const backings = @import("test/backings.zig"); + // const optimize = b.standardOptimizeOption(.{}); + + // const minimal = addEmbeddedExecutable(b, .{ + // .name = "minimal", + // .source_file = .{ + // .path = comptime root_dir() ++ "/test/programs/minimal.zig", + // }, + // .backing = backings.minimal, + // .optimize = optimize, + // }); + + // const has_hal = addEmbeddedExecutable(b, .{ + // .name = "has_hal", + // .source_file = .{ + // .path = comptime root_dir() ++ "/test/programs/has_hal.zig", + // }, + // .backing = backings.has_hal, + // .optimize = optimize, + // }); + + // const has_board = addEmbeddedExecutable(b, .{ + // .name = "has_board", + // .source_file = .{ + // .path = comptime root_dir() ++ "/test/programs/has_board.zig", + // }, + // .backing = backings.has_board, + // .optimize = optimize, + // }); + + // const core_tests = b.addTest(.{ + // .root_source_file = .{ + // .path = comptime root_dir() ++ "/src/core.zig", + // }, + // .optimize = optimize, + // }); + + // const test_step = b.step("test", "build test programs"); + // test_step.dependOn(&minimal.inner.step); + // test_step.dependOn(&has_hal.inner.step); + // test_step.dependOn(&has_board.inner.step); + // test_step.dependOn(&b.addRunArtifact(core_tests).step); +} + +/// The resulting binary format for the firmware file. +/// A lot of embedded systems don't use plain ELF files, thus we provide means +/// to convert the resulting ELF into other common formats. +pub const BinaryFormat = union(enum) { + /// [Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format), the standard output from the compiler. + elf, + + /// A flat binary, contains only the loaded portions of the firmware with an unspecified base offset. + bin, + + /// The [Intel HEX](https://en.wikipedia.org/wiki/Intel_HEX) format, contains + /// an ASCII description of what memory to load where. + hex, + + /// A [Device Firmware Upgrade](https://www.usb.org/sites/default/files/DFU_1.1.pdf) file. + dfu, + + /// The [USB Flashing Format (UF2)](https://github.com/microsoft/uf2) designed by Microsoft. + uf2: uf2.FamilyId, + + /// The [firmware format](https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/firmware-image-format.html) used by the [esptool](https://github.com/espressif/esptool) bootloader. + esp, + + /// Custom option for non-standard formats. + custom: *Custom, + + /// Returns the standard extension for the resulting binary file. + pub fn getExtension(format: BinaryFormat) []const u8 { + return switch (format) { + .elf => ".elf", + .bin => ".bin", + .hex => ".hex", + .dfu => ".dfu", + .uf2 => ".uf2", + .esp => ".bin", + + .custom => |c| c.extension, + }; + } + + pub const Custom = struct { + /// The standard extension of the format. + extension: []const u8, + + /// A function that will convert a given `elf` file into the custom output format. + /// + /// The `*Custom` format is passed so contextual information can be obtained by using + /// `@fieldParentPtr` to provide access to tooling. + convert: *const fn (*Custom, elf: std.Build.LazyPath) std.Build.LazyPath, + }; + + const Enum = std.meta.Tag(BinaryFormat); + + const Context = struct { + pub fn hash(self: @This(), fmt: BinaryFormat) u32 { + _ = self; + + var hasher = std.hash.XxHash32.init(0x1337_42_21); + + hasher.update(@tagName(fmt)); + + switch (fmt) { + .elf, .bin, .hex, .dfu, .esp => |val| { + if (@TypeOf(val) != void) @compileError("Missing update: Context.hash now requires special care!"); + }, + + .uf2 => |family_id| hasher.update(@tagName(family_id)), + .custom => |custom| hasher.update(std.mem.asBytes(custom)), + } + + return hasher.final(); + } + + pub fn eql(self: @This(), fmt_a: BinaryFormat, fmt_b: BinaryFormat, index: usize) bool { + _ = self; + _ = index; + if (@as(BinaryFormat.Enum, fmt_a) != @as(BinaryFormat.Enum, fmt_b)) + return false; + + return switch (fmt_a) { + .elf, .bin, .hex, .dfu, .esp => |val| { + if (@TypeOf(val) != void) @compileError("Missing update: Context.eql now requires special care!"); + return true; + }, + + .uf2 => |a| (a == fmt_b.uf2), + .custom => |a| (a == fmt_b.custom), + }; + } + }; +}; + +/// The CPU model a target uses. +/// +/// The CPUs usually require special care on how to do interrupts, and getting an entry point. +/// +/// MicroZig officially only supports the CPUs listed here, but other CPUs might be provided +/// via the `custom` field. +pub const CpuModel = union(enum) { + avr5, + cortex_m0, + cortex_m0plus, + cortex_m3, + cortex_m4, + riscv32_imac, + + custom: *const Cpu, + + pub fn getDescriptor(model: CpuModel) *const Cpu { + return switch (@as(std.meta.Tag(CpuModel), model)) { + inline else => |tag| &@field(cpus, @tagName(tag)), + .custom => model.custom, + }; + } +}; + +/// A cpu descriptor. +pub const Cpu = struct { + /// Display name of the CPU. + name: []const u8, + + /// Source file providing startup code and memory initialization routines. + source_file: std.build.LazyPath, + + /// The compiler target we use to compile all the code. + target: std.zig.CrossTarget, +}; + +/// A descriptor for memory regions in a microcontroller. +pub const MemoryRegion = struct { + /// The type of the memory region for generating a proper linker script. + kind: Kind, + offset: u64, + length: u64, + + pub const Kind = union(enum) { + /// This is a (normally) immutable memory region where the code is stored. + flash, + + /// This is a mutable memory region for data storage. + ram, + + /// This is a memory region that maps MMIO devices. + io, + + /// This is a memory region that exists, but is reserved and must not be used. + reserved, + + /// This is a memory region used for internal linking tasks required by the board support package. + private: PrivateRegion, + }; + + pub const PrivateRegion = struct { + /// The name of the memory region. Will not have an automatic numeric counter and must be unique. + name: []const u8, + + /// Is the memory region executable? + executable: bool, + + /// Is the memory region readable? + readable: bool, + + /// Is the memory region writable? + writeable: bool, + }; +}; + +/// Defines a custom microcontroller. +pub const Chip = struct { + /// The display name of the controller. + name: []const u8, + + /// (optional) link to the documentation/vendor page of the controller. + url: ?[]const u8 = null, + + /// The cpu model this controller uses. + cpu: CpuModel, + + /// The provider for register definitions. + register_definition: union(enum) { + /// Use `regz` to create a zig file from a JSON schema. + json: std.Build.LazyPath, + + /// Use `regz` to create a json file from a SVD schema. + svd: std.Build.LazyPath, + + /// Use `regz` to create a zig file from an ATDF schema. + atdf: std.Build.LazyPath, + + /// Use the provided file directly as the chip file. + zig: std.Build.LazyPath, + }, + + /// The memory regions that are present in this chip. + memory_regions: []const MemoryRegion, +}; + +/// Defines a hardware abstraction layer. +pub const HardwareAbstractionLayer = struct { + /// Root source file for this HAL. + source_file: std.Build.LazyPath, +}; + +/// Provides a description of a board. +/// +/// Boards provide additional information to a chip and HAL package. +/// For example, they can list attached peripherials, external crystal frequencies, +/// flash sizes, ... +pub const BoardDefinition = struct { + /// Display name of the board + name: []const u8, + + /// (optional) link to the documentation/vendor page of the board. + url: ?[]const u8 = null, + + /// Provides the root file for the board definition. + source_file: std.Build.LazyPath, +}; + +/// The linker script used to link the firmware. +pub const LinkerScript = union(enum) { + /// Auto-generated linker script derived from the memory regions of the chip. + generated, + + /// Externally defined linker script. + source_file: std.build.LazyPath, +}; + +/// A compilation target for MicroZig. Provides information about the chip, +/// hal, board and so on. +/// +/// This is used instead of `std.zig.CrossTarget` to define a MicroZig Firmware. +pub const Target = struct { + /// The preferred binary format of this MicroZig target. If `null`, the user must + /// explicitly give the `.format` field during a call to `getEmittedBin()` or installation steps. + preferred_format: ?BinaryFormat, + + /// The chip this target uses, + chip: Chip, + + /// Usually, embedded projects are single-threaded and single-core applications. Platforms that + /// support multiple CPUs should set this to `false`. + single_threaded: bool = true, + + /// Determines whether the compiler_rt package is bundled with the application or not. + /// This should always be true except for platforms where compiler_rt cannot be built right now. + bundle_compiler_rt: bool = true, + + /// (optional) Provides a default hardware abstraction layer that is used. + /// If `null`, no `microzig.hal` will be available. + hal: ?HardwareAbstractionLayer = null, + + /// (optional) Provides description of external hardware and connected devices + /// like oscillators and such. + /// + /// This structure isn't used by MicroZig itself, but can be utilized from the HAL + /// if present. + board: ?BoardDefinition = null, + + /// (optional) Provide a custom linker script for the hardware or define a custom generation. + linker_script: LinkerScript = .generated, + + /// (optional) Further configures the created firmware depending on the chip and/or board settings. + /// This can be used to set/change additional properties on the created `*Firmware` object. + configure: ?*const fn (host_build: *std.Build, *Firmware) void = null, + + /// (optional) Post processing step that will patch up and modify the elf file if necessary. + binary_post_process: ?*const fn (host_build: *std.Build, std.Build.LazyPath) std.Build.LazyPath = null, +}; + +/// Options to the `addFirmware` function. +pub const FirmwareOptions = struct { + /// The name of the firmware file. + name: []const u8, + + /// The MicroZig target that the firmware is built for. Either a board or a chip. + target: Target, + + /// The optimization level that should be used. Usually `ReleaseSmall` or `Debug` is a good choice. + /// Also using `std.Build.standardOptimizeOption` is a good idea. + optimize: std.builtin.OptimizeMode, + + /// The root source file for the application. This is your `src/main.zig` file. + source_file: std.Build.LazyPath, + + // Overrides: + + /// If set, overrides the `single_threaded` property of the target. + single_threaded: ?bool = null, + + /// If set, overrides the `bundle_compiler_rt` property of the target. + bundle_compiler_rt: ?bool = null, + + /// If set, overrides the `hal` property of the target. + hal: ?HardwareAbstractionLayer = null, + + /// If set, overrides the `board` property of the target. + board: ?BoardDefinition = null, + + /// If set, overrides the `linker_script` property of the target. + linker_script: ?LinkerScript = null, +}; + +/// Declares a new MicroZig firmware file. +pub fn addFirmware( + /// The MicroZig instance that should be used to create the firmware. + mz: *MicroZig, + /// The instance of the `build.zig` that is calling this function. + host_build: *std.Build, + /// Options that define how the firmware is built. + options: FirmwareOptions, +) *Firmware { + const micro_build = mz.self.builder; + + const chip = &options.target.chip; + const cpu = chip.cpu.getDescriptor(); + const maybe_hal = options.hal orelse options.target.hal; + const maybe_board = options.board orelse options.target.board; + + const linker_script = options.linker_script orelse options.target.linker_script; + + // TODO: let the user override which ram section to use the stack on, + // for now just using the first ram section in the memory region list + const first_ram = blk: { + for (chip.memory_regions) |region| { + if (region.kind == .ram) + break :blk region; + } else @panic("no ram memory region found for setting the end-of-stack address"); + }; + + // On demand, generate chip definitions via regz: + const chip_source = switch (chip.register_definition) { + .json, .atdf, .svd => |file| blk: { + const regz_exe = mz.dependency("regz", .{ .optimize = .ReleaseSafe }).artifact("regz"); + + const regz_gen = host_build.addRunArtifact(regz_exe); + + regz_gen.addArg("--schema"); // Explicitly set schema type, one of: svd, atdf, json + regz_gen.addArg(@tagName(chip.register_definition)); + + regz_gen.addArg("--output_path"); // Write to a file + const zig_file = regz_gen.addOutputFileArg("chip.zig"); + + regz_gen.addFileArg(file); + + break :blk zig_file; + }, + + .zig => |src| src, + }; + + const config = host_build.addOptions(); + config.addOption(bool, "has_hal", (maybe_hal != null)); + config.addOption(bool, "has_board", (maybe_board != null)); + + config.addOption(?[]const u8, "board_name", if (maybe_board) |brd| brd.name else null); + + config.addOption([]const u8, "chip_name", chip.name); + config.addOption([]const u8, "cpu_name", chip.name); + config.addOption(usize, "end_of_stack", first_ram.offset + first_ram.length); + + const fw: *Firmware = host_build.allocator.create(Firmware) catch @panic("out of memory"); + fw.* = Firmware{ + .mz = mz, + .host_build = host_build, + .artifact = host_build.addExecutable(.{ + .name = options.name, + .optimize = options.optimize, + .target = cpu.target, + .linkage = .static, + .root_source_file = .{ .cwd_relative = mz.self.builder.pathFromRoot("src/start.zig") }, + }), + .target = options.target, + .output_files = Firmware.OutputFileMap.init(host_build.allocator), + + .config = config, + + .modules = .{ + .microzig = micro_build.createModule(.{ + .source_file = .{ .cwd_relative = micro_build.pathFromRoot("src/microzig.zig") }, + .dependencies = &.{ + .{ + .name = "config", + .module = micro_build.createModule(.{ .source_file = config.getSource() }), + }, + }, + }), + + .cpu = undefined, + .chip = undefined, + + .board = null, + .hal = null, + + .app = undefined, + }, + }; + errdefer fw.output_files.deinit(); + + fw.modules.chip = micro_build.createModule(.{ + .source_file = chip_source, + .dependencies = &.{ + .{ .name = "microzig", .module = fw.modules.microzig }, + }, + }); + fw.modules.microzig.dependencies.put("chip", fw.modules.chip) catch @panic("out of memory"); + + fw.modules.cpu = micro_build.createModule(.{ + .source_file = cpu.source_file, + .dependencies = &.{ + .{ .name = "microzig", .module = fw.modules.microzig }, + }, + }); + fw.modules.microzig.dependencies.put("cpu", fw.modules.cpu) catch @panic("out of memory"); + + if (maybe_hal) |hal| { + fw.modules.hal = micro_build.createModule(.{ + .source_file = hal.source_file, + .dependencies = &.{ + .{ .name = "microzig", .module = fw.modules.microzig }, + }, + }); + fw.modules.microzig.dependencies.put("hal", fw.modules.hal.?) catch @panic("out of memory"); + } + + if (maybe_board) |brd| { + fw.modules.board = micro_build.createModule(.{ + .source_file = brd.source_file, + .dependencies = &.{ + .{ .name = "microzig", .module = fw.modules.microzig }, + }, + }); + fw.modules.microzig.dependencies.put("board", fw.modules.board.?) catch @panic("out of memory"); + } + + fw.modules.app = host_build.createModule(.{ + .source_file = options.source_file, + .dependencies = &.{ + .{ .name = "microzig", .module = fw.modules.microzig }, + }, + }); + + const umm = mz.dependency("umm-zig", .{}).module("umm"); + fw.modules.microzig.dependencies.put("umm", umm) catch @panic("out of memory"); + + fw.artifact.addModule("app", fw.modules.app); + fw.artifact.addModule("microzig", fw.modules.microzig); + + fw.artifact.strip = false; // we always want debug symbols, stripping brings us no benefit on embedded + fw.artifact.single_threaded = options.single_threaded orelse fw.target.single_threaded; + fw.artifact.bundle_compiler_rt = options.bundle_compiler_rt orelse fw.target.bundle_compiler_rt; + + switch (linker_script) { + .generated => { + fw.artifact.setLinkerScript( + generateLinkerScript(host_build, chip.*) catch @panic("out of memory"), + ); + }, + + .source_file => |source| { + fw.artifact.setLinkerScriptPath(source); + }, + } + + if (options.target.configure) |configure| { + configure(host_build, fw); + } + + return fw; +} + +/// Configuration options for firmware installation. +pub const InstallFirmwareOptions = struct { + /// Overrides the output format for the binary. If not set, the standard preferred file format for the firmware target is used. + format: ?BinaryFormat = null, +}; + +/// Adds a new dependency to the `install` step that will install the `firmware` into the folder `$prefix/firmware`. +pub fn installFirmware( + /// The MicroZig instance that was used to create the firmware. + mz: *MicroZig, + /// The instance of the `build.zig` that should perform installation. + b: *std.Build, + /// The firmware that should be installed. Please make sure that this was created with the same `MicroZig` instance as `mz`. + firmware: *Firmware, + /// Optional configuration of the installation process. Pass `.{}` if you're not sure what to do here. + options: InstallFirmwareOptions, +) void { + std.debug.assert(mz == firmware.mz); + const install_step = addInstallFirmware(mz, b, firmware, options); + b.getInstallStep().dependOn(&install_step.step); +} + +/// Creates a new `std.Build.Step.InstallFile` instance that will install the given firmware to `$prefix/firmware`. +/// +/// **NOTE:** This does not actually install the firmware yet. You have to add the returned step as a dependency to another step. +/// If you want to just install the firmware, use `installFirmware` instead! +pub fn addInstallFirmware( + /// The MicroZig instance that was used to create the firmware. + mz: *MicroZig, + /// The instance of the `build.zig` that should perform installation. + b: *std.Build, + /// The firmware that should be installed. Please make sure that this was created with the same `MicroZig` instance as `mz`. + firmware: *Firmware, + /// Optional configuration of the installation process. Pass `.{}` if you're not sure what to do here. + options: InstallFirmwareOptions, +) *std.Build.Step.InstallFile { + const format = firmware.resolveFormat(options.format); + + const basename = b.fmt("{s}{s}", .{ + firmware.artifact.name, + format.getExtension(), + }); + + _ = mz; + + return b.addInstallFileWithDir(firmware.getEmittedBin(format), .{ .custom = "firmware" }, basename); +} + +/// Declaration of a firmware build. +pub const Firmware = struct { + const OutputFileMap = std.ArrayHashMap(BinaryFormat, std.Build.LazyPath, BinaryFormat.Context, false); + + const Modules = struct { + app: *std.Build.Module, + cpu: *std.Build.Module, + chip: *std.Build.Module, + board: ?*std.Build.Module, + hal: ?*std.Build.Module, + microzig: *std.Build.Module, + }; + + // privates: + mz: *MicroZig, + host_build: *std.Build, + target: Target, + output_files: OutputFileMap, + + // publics: + + /// The artifact that is built by Zig. + artifact: *std.Build.Step.Compile, + + /// The options step that provides `microzig.config`. If you need custom configuration, you can add this here. + config: *std.Build.Step.Options, + + /// Declaration of the MicroZig modules used by this firmware. + modules: Modules, + + /// Path to the emitted elf file, if any. + emitted_elf: ?std.Build.LazyPath = null, + + /// Returns the emitted ELF file for this firmware. This is useful if you need debug information + /// or want to use a debugger like Segger, ST-Link or similar. + /// + /// **NOTE:** This is similar, but not equivalent to `std.Build.Step.Compile.getEmittedBin`. The call on the compile step does + /// not include post processing of the ELF files necessary by certain targets. + pub fn getEmittedElf(firmware: *Firmware) std.Build.LazyPath { + if (firmware.emitted_elf == null) { + const raw_elf = firmware.artifact.getEmittedBin(); + firmware.emitted_elf = if (firmware.target.binary_post_process) |binary_post_process| + binary_post_process(firmware.host_build, raw_elf) + else + raw_elf; + } + return firmware.emitted_elf.?; + } + + /// Returns the emitted binary for this firmware. The file is either in the preferred file format for + /// the target or in `format` if not null. + /// + /// **NOTE:** The file returned here is the same file that will be installed. + pub fn getEmittedBin(firmware: *Firmware, format: ?BinaryFormat) std.Build.LazyPath { + const actual_format = firmware.resolveFormat(format); + + const gop = firmware.output_files.getOrPut(actual_format) catch @panic("out of memory"); + if (!gop.found_existing) { + const elf_file = firmware.getEmittedElf(); + + const basename = firmware.host_build.fmt("{s}{s}", .{ + firmware.artifact.name, + actual_format.getExtension(), + }); + + gop.value_ptr.* = switch (actual_format) { + .elf => elf_file, + + .bin => blk: { + const objcopy = firmware.host_build.addObjCopy(elf_file, .{ + .basename = basename, + .format = .bin, + }); + + break :blk objcopy.getOutput(); + }, + + .hex => blk: { + const objcopy = firmware.host_build.addObjCopy(elf_file, .{ + .basename = basename, + .format = .hex, + }); + + break :blk objcopy.getOutput(); + }, + + .uf2 => |family_id| blk: { + const uf2_exe = firmware.mz.dependency("uf2", .{ .optimize = .ReleaseSafe }).artifact("elf2uf2"); + + const convert = firmware.host_build.addRunArtifact(uf2_exe); + + convert.addArg("--family-id"); + convert.addArg(firmware.host_build.fmt("0x{X:0>4}", .{@intFromEnum(family_id)})); + + convert.addArg("--elf-path"); + convert.addFileArg(elf_file); + + convert.addArg("--output-path"); + break :blk convert.addOutputFileArg(basename); + }, + + .dfu => buildConfigError(firmware.host_build, "DFU is not implemented yet. See https://github.com/ZigEmbeddedGroup/microzig/issues/145 for more details!", .{}), + .esp => buildConfigError(firmware.host_build, "ESP firmware image is not implemented yet. See https://github.com/ZigEmbeddedGroup/microzig/issues/146 for more details!", .{}), + + .custom => |generator| generator.convert(generator, elf_file), + }; + } + return gop.value_ptr.*; + } + + pub const AppDependencyOptions = struct { + depend_on_microzig: bool = false, + }; + + /// Adds a regular dependency to your application. + pub fn addAppDependency(fw: *Firmware, name: []const u8, module: *std.Build.Module, options: AppDependencyOptions) void { + if (options.depend_on_microzig) { + module.dependencies.put("microzig", fw.modules.microzig) catch @panic("OOM"); + } + fw.modules.app.dependencies.put(name, module) catch @panic("OOM"); + } + + pub fn addIncludePath(fw: *Firmware, path: std.Build.LazyPath) void { + fw.artifact.addIncludePath(path); + } + + pub fn addSystemIncludePath(fw: *Firmware, path: std.Build.LazyPath) void { + fw.artifact.addSystemIncludePath(path); + } + + pub fn addCSourceFile(fw: *Firmware, source: std.Build.Step.Compile.CSourceFile) void { + fw.artifact.addCSourceFile(source); + } + + pub fn addOptions(fw: *Firmware, module_name: []const u8, options: *std.Build.OptionsStep) void { + fw.artifact.addOptions(module_name, options); + fw.modules.app.dependencies.put( + module_name, + fw.host_build.createModule(.{ + .source_file = options.getOutput(), + }), + ) catch @panic("OOM"); + } + + pub fn addObjectFile(fw: *Firmware, source: std.Build.LazyPath) void { + fw.artifact.addObjectFile(source); + } + + fn resolveFormat(firmware: *Firmware, format: ?BinaryFormat) BinaryFormat { + if (format) |fmt| return fmt; + + if (firmware.target.preferred_format) |fmt| return fmt; + + buildConfigError(firmware.host_build, "{s} has no preferred output format, please provide one in the `format` option.", .{ + firmware.target.chip.name, + }); + } +}; + +pub const cpus = struct { + pub const avr5 = Cpu{ + .name = "AVR5", + .source_file = .{ .path = build_root ++ "/src/cpus/avr5.zig" }, + .target = std.zig.CrossTarget{ + .cpu_arch = .avr, + .cpu_model = .{ .explicit = &std.Target.avr.cpu.avr5 }, + .os_tag = .freestanding, + .abi = .eabi, + }, + }; + + pub const cortex_m0 = Cpu{ + .name = "ARM Cortex-M0", + .source_file = .{ .path = build_root ++ "/src/cpus/cortex-m.zig" }, + .target = std.zig.CrossTarget{ + .cpu_arch = .thumb, + .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m0 }, + .os_tag = .freestanding, + .abi = .eabi, + }, + }; + + pub const cortex_m0plus = Cpu{ + .name = "ARM Cortex-M0+", + .source_file = .{ .path = build_root ++ "/src/cpus/cortex-m.zig" }, + .target = std.zig.CrossTarget{ + .cpu_arch = .thumb, + .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m0plus }, + .os_tag = .freestanding, + .abi = .eabi, + }, + }; + + pub const cortex_m3 = Cpu{ + .name = "ARM Cortex-M3", + .source_file = .{ .path = build_root ++ "/src/cpus/cortex-m.zig" }, + .target = std.zig.CrossTarget{ + .cpu_arch = .thumb, + .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m3 }, + .os_tag = .freestanding, + .abi = .eabi, + }, + }; + + pub const cortex_m4 = Cpu{ + .name = "ARM Cortex-M4", + .source_file = .{ .path = build_root ++ "/src/cpus/cortex-m.zig" }, + .target = std.zig.CrossTarget{ + .cpu_arch = .thumb, + .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 }, + .os_tag = .freestanding, + .abi = .eabi, + }, + }; + + pub const riscv32_imac = Cpu{ + .name = "RISC-V 32-bit", + .source_file = .{ .path = build_root ++ "/src/cpus/riscv32.zig" }, + .target = std.zig.CrossTarget{ + .cpu_arch = .riscv32, + .cpu_model = .{ .explicit = &std.Target.riscv.cpu.sifive_e21 }, + .os_tag = .freestanding, + .abi = .none, + }, + }; +}; + +fn buildConfigError(b: *std.Build, comptime fmt: []const u8, args: anytype) noreturn { + const msg = b.fmt(fmt, args); + @panic(msg); +} + +fn dependency(mz: *MicroZig, name: []const u8, args: anytype) *std.Build.Dependency { + return mz.self.builder.dependency(name, args); +} + +fn generateLinkerScript(b: *std.Build, chip: Chip) !std.Build.LazyPath { + const cpu = chip.cpu.getDescriptor(); + + var contents = std.ArrayList(u8).init(b.allocator); + const writer = contents.writer(); + try writer.print( + \\/* + \\ * This file was auto-generated by microzig + \\ * + \\ * Target CPU: {[cpu]s} + \\ * Target Chip: {[chip]s} + \\ */ + \\ + // This is not the "true" entry point, but there's no such thing on embedded platforms + // anyways. This is the logical entrypoint that should be invoked when + // stack, .data and .bss are set up and the CPU is ready to be used. + \\ENTRY(microzig_main); + \\ + \\ + , .{ + .cpu = cpu.name, + .chip = chip.name, + }); + + try writer.writeAll("MEMORY\n{\n"); + { + var counters = [4]usize{ 0, 0, 0, 0 }; + for (chip.memory_regions) |region| { + // flash (rx!w) : ORIGIN = 0x00000000, LENGTH = 512k + + switch (region.kind) { + .flash => { + try writer.print(" flash{d} (rx!w)", .{counters[0]}); + counters[0] += 1; + }, + + .ram => { + try writer.print(" ram{d} (rw!x)", .{counters[1]}); + counters[1] += 1; + }, + + .io => { + try writer.print(" io{d} (rw!x)", .{counters[2]}); + counters[2] += 1; + }, + + .reserved => { + try writer.print(" reserved{d} (rw!x)", .{counters[3]}); + counters[3] += 1; + }, + + .private => |custom| { + try writer.print(" {s} (", .{custom.name}); + if (custom.readable) try writer.writeAll("r"); + if (custom.writeable) try writer.writeAll("w"); + if (custom.executable) try writer.writeAll("x"); + + if (!custom.readable or !custom.writeable or !custom.executable) { + try writer.writeAll("!"); + if (!custom.readable) try writer.writeAll("r"); + if (!custom.writeable) try writer.writeAll("w"); + if (!custom.executable) try writer.writeAll("x"); + } + try writer.writeAll(")"); + }, + } + try writer.print(" : ORIGIN = 0x{X:0>8}, LENGTH = 0x{X:0>8}\n", .{ region.offset, region.length }); + } + } + + try writer.writeAll("}\n\nSECTIONS\n{\n"); + { + try writer.writeAll( + \\ .text : + \\ { + \\ KEEP(*(microzig_flash_start)) + \\ *(.text*) + \\ } > flash0 + \\ + \\ + ); + + switch (cpu.target.getCpuArch()) { + .arm, .thumb => try writer.writeAll( + \\ .ARM.exidx : { + \\ *(.ARM.exidx* .gnu.linkonce.armexidx.*) + \\ } >flash0 + \\ + \\ + ), + else => {}, + } + + try writer.writeAll( + \\ .data : + \\ { + \\ microzig_data_start = .; + \\ *(.rodata*) + \\ *(.data*) + \\ microzig_data_end = .; + \\ } > ram0 AT> flash0 + \\ + \\ .bss (NOLOAD) : + \\ { + \\ microzig_bss_start = .; + \\ *(.bss*) + \\ microzig_bss_end = .; + \\ } > ram0 + \\ + \\ microzig_data_load_start = LOADADDR(.data); + \\ + ); + } + try writer.writeAll("}\n"); + + // TODO: Assert that the flash can actually hold all data! + // try writer.writeAll( + // \\ + // \\ ASSERT( (SIZEOF(.text) + SIZEOF(.data) > LENGTH(flash0)), "Error: .text + .data is too large for flash!" ); + // \\ + // ); + + const write = b.addWriteFiles(); + + return write.add("linker.ld", contents.items); +} diff --git a/build/build.zig.zon b/build/build.zig.zon new file mode 100644 index 0000000..437acb5 --- /dev/null +++ b/build/build.zig.zon @@ -0,0 +1,29 @@ +.{ + .name = "microzig", + .version = "0.1.0", + .paths = .{ + "build.zig", + "build.zig.zon", + "design", + "docs", + "LICENSE", + "README.adoc", + "src", + "test", + "thoughts.md", + }, + .dependencies = .{ + .uf2 = .{ + .url = "https://github.com/ZigEmbeddedGroup/uf2/archive/8037b439ccbac862471392b25e94a8995d784e2c.tar.gz", + .hash = "1220cc66563fc1ecefca7990968441dc9d4db717884ffa9a2de657f60ed4bb74a70a", + }, + .regz = .{ + .url = "https://github.com/ZigEmbeddedGroup/regz/archive/d66ffd56f51fc46c071412141b5d0c74dc83c310.tar.gz", + .hash = "122002c5f2e31c11373ede6e8a8dd9a61aabd60d38df667ec33b5f994d1f0b503823", + }, + .@"umm-zig" = .{ + .url = "https://github.com/ZigEmbeddedGroup/umm-zig/archive/99d815adfbc5cc4ad385dd765a6192f85e54179f.tar.gz", + .hash = "12207ef7375ea45e97f4fba9c5dfa74d022902893c4dbf1a0076726b7ec39a02ea3f", + }, + }, +} diff --git a/build/microzig-package.json b/build/microzig-package.json new file mode 100644 index 0000000..92812f0 --- /dev/null +++ b/build/microzig-package.json @@ -0,0 +1,4 @@ +{ + "package_name": "microzig-build", + "package_type": "core" +} diff --git a/examples/modular/README.md b/examples/modular/README.md new file mode 100644 index 0000000..7909f7c --- /dev/null +++ b/examples/modular/README.md @@ -0,0 +1,5 @@ +# Examples for the BSP `nxp-lpc` + +- [Blinky](src/blinky.zig) on [nRF52840 Dongle](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle) + TODO: Implement this! + diff --git a/examples/modular/build.zig b/examples/modular/build.zig new file mode 100644 index 0000000..f58e4a8 --- /dev/null +++ b/examples/modular/build.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const microzig_build = @import("microzig-build"); +const lpc = @import("lpc"); + +pub fn build(b: *std.Build) void { + const microbuild = microzig_build.init( + b, + b.dependency("microzig", .{}), + ); + + const optimize = b.standardOptimizeOption(.{}); + + // `addFirmware` basically works like addExecutable, but takes a + // `microzig.Target` for target instead of a `std.zig.CrossTarget`. + // + // The target will convey all necessary information on the chip, + // cpu and potentially the board as well. + const firmware = microbuild.addFirmware(b, .{ + .name = "blinky", + .target = lpc.boards.mbed.lpc1768, + .optimize = optimize, + .source_file = .{ .path = "src/blinky.zig" }, + }); + + // `installFirmware()` is the MicroZig pendant to `Build.installArtifact()` + // and allows installing the firmware as a typical firmware file. + // + // This will also install into `$prefix/firmware` instead of `$prefix/bin`. + microbuild.installFirmware(b, firmware, .{}); + + // For debugging, we also always install the firmware as an ELF file + microbuild.installFirmware(b, firmware, .{ .format = .elf }); +} diff --git a/examples/modular/build.zig.zon b/examples/modular/build.zig.zon new file mode 100644 index 0000000..aa5a11a --- /dev/null +++ b/examples/modular/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = "microzig-nxp-lpc-examples", + .version = "0.1.0", + .dependencies = .{ + .microzig = .{ + .url = "https://github.com/ZigEmbeddedGroup/microzig/archive/c6c9ec4516f57638e751141085c9d76120990312.tar.gz", + .hash = "1220af58bdaa721b8189f3a7adfda660517dd354463463388e96d69fe4ceccf80b92", + }, + .lpc = .{ + .url = "https://github.com/ZigEmbeddedGroup/nxp-lpc/archive/130a1316c0892415e7da958a5e9548ed87bba54d.tar.gz", + .hash = "1220165879f85a1d51656d35b3963a95f3585dc665fc7414f76aa6aad4e6635536cf", + }, + }, +} diff --git a/examples/modular/src/blinky.zig b/examples/modular/src/blinky.zig new file mode 100644 index 0000000..328d637 --- /dev/null +++ b/examples/modular/src/blinky.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const microzig = @import("microzig"); + +const chip = microzig.chip; + +// LED-1: P1.18 +// LED-2: P1.20 +// LED-3: P1.21 +// LED-4: P1.23 + +const conn = chip.peripherals.PINCONNECT; +const gpio: *volatile [5]PatchedGpio = @ptrCast(@alignCast(chip.peripherals.GPIO)); + +const led_mask = [4]u32{ + (1 << 18), + (1 << 20), + (1 << 21), + (1 << 23), +}; +const all_mask = led_mask[0] | led_mask[1] | led_mask[2] | led_mask[3]; + +pub fn main() !void { + conn.PINSEL3.modify(.{ + .P1_18 = .{ .value = .GPIO_P1 }, + .P1_20 = .{ .value = .GPIO_P1 }, + .P1_21 = .{ .value = .GPIO_P1 }, + .P1_23 = .{ .value = .GPIO_P1 }, + }); + + const p1 = &gpio[1]; + + p1.dir = all_mask; + + while (true) { + for (led_mask) |mask| { + p1.pin_clr = (all_mask & ~mask); + p1.pin_set = mask; + microzig.core.experimental.debug.busy_sleep(100_000); + } + } +} + +const PatchedGpio = extern struct { + dir: u32, // 0x2009 C000 + __padding0: u32, // 0x2009 C004 + __padding1: u32, // 0x2009 C008 + __padding2: u32, // 0x2009 C00C + mask: u32, // 0x2009 C010 + pin: u32, // 0x2009 C014 + pin_set: u32, // 0x2009 C018 + pin_clr: u32, // 0x2009 C01C + + comptime { + std.debug.assert(@sizeOf(PatchedGpio) == 0x20); + } +};