Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a7e0625
avr: Add AVR25 CPU support
philocalyst May 14, 2026
9249ea8
avr5: Use generated interrupt vector table
philocalyst May 14, 2026
1422fa9
build.zig: Register ATtiny port
philocalyst May 14, 2026
638f0f4
attiny: Add package manifest and README
philocalyst May 14, 2026
8b2a435
attiny: Define chip targets
philocalyst May 14, 2026
8d3fce8
attiny: Add small board definitions
philocalyst May 14, 2026
d7c1c0f
attiny: Add ATtiny84 and ATtiny85 HAL roots
philocalyst May 14, 2026
ae8dbdb
attiny85: Add register and GPIO helpers
philocalyst May 14, 2026
fdb8605
attiny85: Add timer helpers
philocalyst May 14, 2026
98f05e5
attiny85: Add ADC watchdog and sleep helpers
philocalyst May 14, 2026
a38e96f
attiny85: Add PCINT EEPROM and PROGMEM helpers
philocalyst May 14, 2026
b4695ec
attiny1634: Add HAL root and registers
philocalyst May 14, 2026
12e7141
attiny1634: Add GPIO and PWM helpers
philocalyst May 14, 2026
85cfc4b
attiny1634: Add ADC WDT PCINT and EEPROM helpers
philocalyst May 14, 2026
1e2356e
attiny1616: Add HAL root and registers
philocalyst May 14, 2026
d9a1a09
attiny1616: Add GPIO clock and PCINT helpers
philocalyst May 14, 2026
d5f52a6
attiny1616: Add TCA0 and RTC PIT helpers
philocalyst May 14, 2026
93de1ae
attiny1616: Add ADC WDT and EEPROM helpers
philocalyst May 14, 2026
06fb157
examples: Add ATtiny blinky examples
philocalyst May 14, 2026
6aa1258
examples: Add ATtiny1634 PWM ADC example
philocalyst May 14, 2026
55f41fd
examples: Add ATtiny1616 TCA RTC example
philocalyst May 14, 2026
b99de6e
attiny: Use snake case HAL helper names
philocalyst May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ pub const LinkerScript = internals.LinkerScript;
pub const Stack = internals.Stack;
pub const MemoryRegion = internals.MemoryRegion;

const regz = @import("tools/regz");

// If more ports are available, the error "error: evaluation exceeded 1000 backwards branches" may occur.
// In such cases, consider increasing the argument value for @setEvalBranchQuota().
const port_list: []const struct {
Expand All @@ -26,6 +24,7 @@ const port_list: []const struct {
.{ .name = "gd32", .dep_name = "port/gigadevice/gd32" },
.{ .name = "samd51", .dep_name = "port/microchip/samd51" },
.{ .name = "atmega", .dep_name = "port/microchip/atmega" },
.{ .name = "attiny", .dep_name = "port/microchip/attiny" },
.{ .name = "nrf5x", .dep_name = "port/nordic/nrf5x" },
.{ .name = "lpc", .dep_name = "port/nxp/lpc" },
.{ .name = "mcx", .dep_name = "port/nxp/mcx" },
Expand All @@ -36,15 +35,6 @@ const port_list: []const struct {
.{ .name = "tm4c", .dep_name = "port/texasinstruments/tm4c" },
};

const exe_targets: []const std.Target.Query = &.{
.{ .cpu_arch = .aarch64, .os_tag = .macos },
.{ .cpu_arch = .aarch64, .os_tag = .linux },
.{ .cpu_arch = .aarch64, .os_tag = .windows },
.{ .cpu_arch = .x86_64, .os_tag = .macos },
.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl },
.{ .cpu_arch = .x86_64, .os_tag = .windows },
};

pub fn build(b: *Build) void {
const optimize = b.standardOptimizeOption(.{});

Expand Down Expand Up @@ -81,6 +71,7 @@ pub const PortSelect = struct {
gd32: bool = false,
samd51: bool = false,
atmega: bool = false,
attiny: bool = false,
nrf5x: bool = false,
lpc: bool = false,
mcx: bool = false,
Expand Down Expand Up @@ -771,6 +762,21 @@ pub fn MicroBuild(port_select: PortSelect) type {
.name = "avr5",
.root_source_file = mb.core_dep.namedLazyPath("cpu_avr5"),
};
} else if (std.mem.eql(u8, target.cpu.model.name, "avr25")) {
return .{
.name = "avr25",
.root_source_file = mb.core_dep.namedLazyPath("cpu_avr25"),
};
} else if (std.mem.eql(u8, target.cpu.model.name, "avr35")) {
return .{
.name = "avr35",
.root_source_file = mb.core_dep.namedLazyPath("cpu_avr5"),
};
} else if (std.mem.eql(u8, target.cpu.model.name, "avrxmega3")) {
return .{
.name = "avrxmega3",
.root_source_file = mb.core_dep.namedLazyPath("cpu_avr5"),
};
} else if (std.mem.startsWith(u8, target.cpu.model.name, "cortex_m")) {
return .{
.name = target.cpu.model.name,
Expand Down
1 change: 1 addition & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
.@"port/espressif/esp" = .{ .path = "port/espressif/esp", .lazy = true },
.@"port/gigadevice/gd32" = .{ .path = "port/gigadevice/gd32", .lazy = true },
.@"port/microchip/atmega" = .{ .path = "port/microchip/atmega", .lazy = true },
.@"port/microchip/attiny" = .{ .path = "port/microchip/attiny", .lazy = true },
.@"port/microchip/samd51" = .{ .path = "port/microchip/samd51", .lazy = true },
.@"port/nordic/nrf5x" = .{ .path = "port/nordic/nrf5x", .lazy = true },
.@"port/nxp/lpc" = .{ .path = "port/nxp/lpc", .lazy = true },
Expand Down
1 change: 1 addition & 0 deletions core/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub fn build(b: *std.Build) !void {
b.addNamedLazyPath("cpu_cortex_m", b.path("src/cpus/cortex_m.zig"));
b.addNamedLazyPath("cpu_riscv32", b.path("src/cpus/riscv32.zig"));
b.addNamedLazyPath("cpu_avr5", b.path("src/cpus/avr5.zig"));
b.addNamedLazyPath("cpu_avr25", b.path("src/cpus/avr25.zig"));
b.addNamedLazyPath("cpu_msp430", b.path("src/cpus/msp430.zig"));
b.addNamedLazyPath("cpu_msp430x", b.path("src/cpus/msp430x.zig"));

Expand Down
165 changes: 165 additions & 0 deletions core/src/cpus/avr25.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const std = @import("std");
const microzig = @import("microzig");

pub const interrupt = struct {
pub fn enable_interrupts() void {
asm volatile ("sei");
}

pub fn disable_interrupts() void {
asm volatile ("cli");
}
};

/// AVR interrupt handler function type.
pub const HandlerFn = *const fn () callconv(.avr_signal) void;

/// Complete list of interrupt values based on the chip's `interrupts` array.
pub const Interrupt = microzig.utilities.GenerateInterruptEnum(i32);

/// Allowable `interrupt` options for microzig.options.
pub const InterruptOptions = microzig.utilities.GenerateInterruptOptions(&.{
.{ .InterruptEnum = Interrupt, .HandlerFn = HandlerFn },
});

pub inline fn sbi(comptime reg: u5, comptime bit: u3) void {
asm volatile ("sbi %[reg], %[bit]"
:
: [reg] "I" (reg),
[bit] "I" (bit),
);
}

pub inline fn cbi(comptime reg: u5, comptime bit: u3) void {
asm volatile ("cbi %[reg], %[bit]"
:
: [reg] "I" (reg),
[bit] "I" (bit),
);
}

pub const vector_table_asm = blk: {
const fields = std.meta.fields(microzig.chip.VectorTable);
std.debug.assert(std.mem.eql(u8, "RESET", fields[0].name));
// avr25 devices use rjmp (2-byte) instead of jmp (4-byte)
var asm_str: []const u8 = "rjmp microzig_start\n";

const interrupt_options = microzig.options.interrupts;

for (fields[1..]) |field| {
const handler = @field(interrupt_options, field.name);
if (handler) |func| {
const isr = make_isr_handler(field.name, func);
asm_str = asm_str ++ "rjmp " ++ isr.exported_name ++ "\n";
} else {
asm_str = asm_str ++ "rjmp microzig_unhandled_vector\n";
}
}

break :blk asm_str;
};

fn vector_table() linksection("microzig_flash_start") callconv(.naked) noreturn {
asm volatile (vector_table_asm);
}

// @breakpoint() on AVR is calling abort, so we export simple function that is calling hang
export fn abort() noreturn {
microzig.hang();
}

pub fn export_startup_logic() void {
_ = startup_logic;
@export(&vector_table, .{
.name = "_start",
});
}

fn make_isr_handler(comptime name: []const u8, comptime func: anytype) type {
const calling_convention = switch (@typeInfo(@TypeOf(func))) {
.@"fn" => |info| info.calling_convention,
.pointer => |info| switch (@typeInfo(info.child)) {
.@"fn" => |fn_info| fn_info.calling_convention,
else => @compileError("Declarations in 'interrupts' namespace must all be functions. '" ++ name ++ "' is not a function"),
},
else => @compileError("Declarations in 'interrupts' namespace must all be functions. '" ++ name ++ "' is not a function"),
};

switch (calling_convention) {
.auto, .avr_signal, .avr_interrupt => {},
else => @compileError("Calling conventions for interrupts must be 'avr_interrupt', 'avr_signal', or unspecified. The avr_signal calling convention leaves global interrupts disabled during the ISR, where avr_interrupt enables global interrupts for nested ISRs."),
}

return struct {
pub const exported_name = "microzig_isr_" ++ name;

comptime {
@export(func, .{ .name = exported_name });
}
};
}

pub const startup_logic = struct {
export fn microzig_unhandled_vector() callconv(.c) noreturn {
@panic("Unhandled interrupt");
}

extern fn microzig_main() noreturn;

export fn microzig_start() callconv(.c) noreturn {
// At startup the stack pointer is at the end of RAM
// so, no need to set it manually!

copy_data_to_ram();
clear_bss();

microzig_main();
}

fn copy_data_to_ram() void {
asm volatile (
\\ ; load Z register with the address of the data in flash
\\ ldi r30, lo8(microzig_data_load_start)
\\ ldi r31, hi8(microzig_data_load_start)
\\ ; load X register with address of the data in ram
\\ ldi r26, lo8(microzig_data_start)
\\ ldi r27, hi8(microzig_data_start)
\\ ; load address of end of the data in ram
\\ ldi r24, lo8(microzig_data_end)
\\ ldi r25, hi8(microzig_data_end)
\\ rjmp .L2
\\
\\.L1:
\\ lpm r18, Z+ ; copy from Z into r18 and increment Z
\\ st X+, r18 ; store r18 at location X and increment X
\\
\\.L2:
\\ cp r26, r24
\\ cpc r27, r25 ; check and branch if we are at the end of data
\\ brne .L1
);
// Probably a good idea to add clobbers here, but compiler doesn't seem to care
}

fn clear_bss() void {
asm volatile (
\\ ; load X register with the beginning of bss section
\\ ldi r26, lo8(microzig_bss_start)
\\ ldi r27, hi8(microzig_bss_start)
\\ ; load end of the bss in registers
\\ ldi r24, lo8(microzig_bss_end)
\\ ldi r25, hi8(microzig_bss_end)
\\ ldi r18, 0x00
\\ rjmp .L4
\\
\\.L3:
\\ st X+, r18
\\
\\.L4:
\\ cp r26, r24
\\ cpc r27, r25 ; check and branch if we are at the end of bss
\\ brne .L3
);
// Probably a good idea to add clobbers here, but compiler doesn't seem to care
}
};
65 changes: 35 additions & 30 deletions core/src/cpus/avr5.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ pub const interrupt = struct {
}
};

/// AVR interrupt handler function type.
pub const HandlerFn = *const fn () callconv(.avr_signal) void;

/// Complete list of interrupt values based on the chip's `interrupts` array.
pub const Interrupt = microzig.utilities.GenerateInterruptEnum(i32);

/// Allowable `interrupt` options for microzig.options.
pub const InterruptOptions = microzig.utilities.GenerateInterruptOptions(&.{
.{ .InterruptEnum = Interrupt, .HandlerFn = HandlerFn },
});

pub inline fn sbi(comptime reg: u5, comptime bit: u3) void {
asm volatile ("sbi %[reg], %[bit]"
:
Expand All @@ -28,31 +39,26 @@ pub inline fn cbi(comptime reg: u5, comptime bit: u3) void {
}

pub const vector_table_asm = blk: {
std.debug.assert(std.mem.eql(u8, "RESET", std.meta.fields(microzig.chip.VectorTable)[0].name));
const asm_str: []const u8 = "jmp microzig_start\n";

//const has_interrupts = @hasDecl(root, "microzig_options");
//for (@typeInfo(root.VectorTableOptions).@"struct".fields) |field| {
// const new_insn = if (has_interrupts) overload: {
// const interrupts = root.microzig_options.interrupts;
// if (@hasDecl(interrupts, field.name)) {
// const handler = @field(interrupts, field.name);

// const isr = make_isr_handler(field.name, handler);

// break :overload "jmp " ++ isr.exported_name;
// } else {
// break :overload "jmp microzig_unhandled_vector";
// }
// } else "jmp microzig_unhandled_vector";

// asm_str = asm_str ++ new_insn ++ "\n";
//}
const fields = std.meta.fields(microzig.chip.VectorTable);
std.debug.assert(std.mem.eql(u8, "RESET", fields[0].name));
var asm_str: []const u8 = "jmp microzig_start\n";

const interrupt_options = microzig.options.interrupts;

for (fields[1..]) |field| {
const handler = @field(interrupt_options, field.name);
if (handler) |func| {
const isr = make_isr_handler(field.name, func);
asm_str = asm_str ++ "jmp " ++ isr.exported_name ++ "\n";
} else {
asm_str = asm_str ++ "jmp microzig_unhandled_vector\n";
}
}

break :blk asm_str;
};

fn vector_table() callconv(.naked) noreturn {
fn vector_table() linksection("microzig_flash_start") callconv(.naked) noreturn {
asm volatile (vector_table_asm);
}

Expand All @@ -70,25 +76,24 @@ pub fn export_startup_logic() void {

fn make_isr_handler(comptime name: []const u8, comptime func: anytype) type {
const calling_convention = switch (@typeInfo(@TypeOf(func))) {
.Fn => |info| info.calling_convention,
.@"fn" => |info| info.calling_convention,
.pointer => |info| switch (@typeInfo(info.child)) {
.@"fn" => |fn_info| fn_info.calling_convention,
else => @compileError("Declarations in 'interrupts' namespace must all be functions. '" ++ name ++ "' is not a function"),
},
else => @compileError("Declarations in 'interrupts' namespace must all be functions. '" ++ name ++ "' is not a function"),
};

switch (calling_convention) {
.Unspecified, .Signal, .Interrupt => {},
else => @compileError("Calling conventions for interrupts must be 'Interrupt', 'Signal', or unspecified. The signal calling convention leaves global interrupts disabled during the ISR, where the interrupt calling conventions enables global interrupts for nested ISRs."),
.auto, .avr_signal, .avr_interrupt => {},
else => @compileError("Calling conventions for interrupts must be 'avr_interrupt', 'avr_signal', or unspecified. The avr_signal calling convention leaves global interrupts disabled during the ISR, where avr_interrupt enables global interrupts for nested ISRs."),
}

return struct {
pub const exported_name = "microzig_isr_" ++ name;

pub fn isr_vector() callconv(.Signal) void {
@call(.always_inline, func, .{});
}

comptime {
const options = .{ .name = exported_name, .linkage = .Strong };
@export(&isr_vector, options);
@export(func, .{ .name = exported_name });
}
};
}
Expand Down
47 changes: 47 additions & 0 deletions examples/microchip/attiny/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const std = @import("std");
const microzig = @import("microzig");

const MicroBuild = microzig.MicroBuild(.{
.attiny = true,
});

pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const maybe_example = b.option([]const u8, "example", "only build matching examples");

const mz_dep = b.dependency("microzig", .{});
const mb = MicroBuild.init(b, mz_dep) orelse return;

const available_examples = [_]Example{
.{ .target = mb.ports.attiny.boards.digispark, .name = "digispark_blinky", .file = "src/blinky.zig" },
.{ .target = mb.ports.attiny.boards.adafruit.trinket, .name = "trinket_blinky", .file = "src/blinky.zig" },
.{ .target = mb.ports.attiny.boards.adafruit.gemma, .name = "gemma_blinky", .file = "src/blinky.zig" },
.{ .target = mb.ports.attiny.chips.attiny85, .name = "attiny85_blinky", .file = "src/blinky.zig" },
.{ .target = mb.ports.attiny.chips.attiny85, .name = "attiny85_blinky_interrupt", .file = "src/blinky_interrupt.zig" },
.{ .target = mb.ports.attiny.chips.attiny84, .name = "attiny84_blinky", .file = "src/blinky84.zig" },
.{ .target = mb.ports.attiny.chips.attiny1634, .name = "attiny1634_pwm_adc", .file = "src/attiny1634_pwm_adc.zig" },
.{ .target = mb.ports.attiny.chips.attiny1616, .name = "attiny1616_tca_rtc", .file = "src/attiny1616_tca_rtc.zig" },
};

for (available_examples) |example| {
if (maybe_example) |selected_example|
if (!std.mem.containsAtLeast(u8, example.name, 1, selected_example))
continue;

const fw = mb.add_firmware(.{
.name = example.name,
.target = example.target,
.optimize = optimize,
.root_source_file = b.path(example.file),
});

mb.install_firmware(fw, .{});
mb.install_firmware(fw, .{ .format = .elf });
}
}

const Example = struct {
target: *const microzig.Target,
name: []const u8,
file: []const u8,
};
Loading
Loading