diff --git a/build.zig b/build.zig index 06c634afd..8b05770f9 100644 --- a/build.zig +++ b/build.zig @@ -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 { @@ -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" }, @@ -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(.{}); @@ -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, @@ -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, diff --git a/build.zig.zon b/build.zig.zon index 30dde66c6..41e32ae25 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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 }, diff --git a/core/build.zig b/core/build.zig index a1fcce0d6..5a1d297dd 100644 --- a/core/build.zig +++ b/core/build.zig @@ -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")); diff --git a/core/src/cpus/avr25.zig b/core/src/cpus/avr25.zig new file mode 100644 index 000000000..ee79a1498 --- /dev/null +++ b/core/src/cpus/avr25.zig @@ -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 + } +}; diff --git a/core/src/cpus/avr5.zig b/core/src/cpus/avr5.zig index 75850922e..c061c3c9c 100644 --- a/core/src/cpus/avr5.zig +++ b/core/src/cpus/avr5.zig @@ -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]" : @@ -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); } @@ -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 }); } }; } diff --git a/examples/microchip/attiny/build.zig b/examples/microchip/attiny/build.zig new file mode 100644 index 000000000..df2730852 --- /dev/null +++ b/examples/microchip/attiny/build.zig @@ -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, +}; diff --git a/examples/microchip/attiny/build.zig.zon b/examples/microchip/attiny/build.zig.zon new file mode 100644 index 000000000..4225f34bd --- /dev/null +++ b/examples/microchip/attiny/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .examples_microchip_attiny, + .fingerprint = 0x83cbabd71b524269, + .version = "0.0.0", + .dependencies = .{ + .microzig = .{ .path = "../../.." }, + }, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/examples/microchip/attiny/src/attiny1616_tca_rtc.zig b/examples/microchip/attiny/src/attiny1616_tca_rtc.zig new file mode 100644 index 000000000..8d7807d82 --- /dev/null +++ b/examples/microchip/attiny/src/attiny1616_tca_rtc.zig @@ -0,0 +1,55 @@ +const microzig = @import("microzig"); +const hal = microzig.hal; + +const pwm_cool = hal.gpio.pin(.b, 0); +const pwm_warm = hal.gpio.pin(.b, 1); +const aux_led = hal.gpio.pin(.b, 5); +const switch_pin = hal.gpio.pin(.a, 5); +const Ramp = hal.progmem.Table(u8, 4, .{ 1, 4, 16, 64 }); + +pub fn main() void { + hal.clock.use_default20_m_hz_div2(); + + pwm_cool.set_direction(.output); + pwm_warm.set_direction(.output); + aux_led.set_direction(.output); + hal.pcint.configure(switch_pin, true, .both_edges); + + hal.tca0.configure_pwm(.{ + .top = 255, + .compare0 = 48, + .compare1 = 96, + .enable_compare0 = true, + .enable_compare1 = true, + .waveform = .dual_slope_bottom, + .clock = .div1, + }); + + hal.rtc_pit.configure(.cycles512, true); + hal.adc.configure(.{ + .channel = .internal_reference, + .reference = .vdd, + .sample_count = .samples4, + .prescaler = .div16, + .initial_delay = .cycles16, + .sample_capacitance = true, + .freerun = true, + .run_standby = true, + }); + hal.adc.enable_result_ready_interrupt(); + hal.adc.start(); + hal.watchdog.reset(); + + const saved_level = hal.eeprom.read_byte(.from_int(0)); + const ramp_level = Ramp.get(1); + _ = saved_level; + _ = ramp_level; + + while (true) { + nop(); + } +} + +inline fn nop() void { + asm volatile ("nop"); +} diff --git a/examples/microchip/attiny/src/attiny1634_pwm_adc.zig b/examples/microchip/attiny/src/attiny1634_pwm_adc.zig new file mode 100644 index 000000000..6c0c9e3b4 --- /dev/null +++ b/examples/microchip/attiny/src/attiny1634_pwm_adc.zig @@ -0,0 +1,40 @@ +const microzig = @import("microzig"); +const hal = microzig.hal; + +const ch1_pwm = hal.gpio.pin(.b, 3); +const ch2_pwm = hal.gpio.pin(.a, 6); +const fet_pwm = hal.gpio.pin(.c, 0); +const voltage = hal.gpio.pin(.b, 1); +const Gamma = hal.progmem.Table(u8, 4, .{ 0, 8, 32, 255 }); + +pub fn main() void { + ch1_pwm.set_direction(.output); + ch2_pwm.set_direction(.output); + fet_pwm.set_direction(.output); + voltage.set_direction(.input); + + hal.timer1.configure_phase_correct_dynamic(.{ .top = 255, .prescaler = .clk_1 }); + hal.timer1.set_compare_a(96); + hal.timer1.set_compare_b(64); + + hal.timer0.configure_phase_correct_pwm_a(.clk_1); + hal.timer0.set_compare_a(32); + + hal.adc.configure_internal1v1(.adc6, .div64); + hal.adc.start(); + hal.watchdog.reset(); + hal.watchdog.configure(.interrupt, .ms16); + + const saved_level = hal.eeprom.read_byte(hal.eeprom.address(0)); + const gamma_level = Gamma.get(2); + _ = saved_level; + _ = gamma_level; + + while (true) { + nop(); + } +} + +inline fn nop() void { + asm volatile ("nop"); +} diff --git a/examples/microchip/attiny/src/blinky.zig b/examples/microchip/attiny/src/blinky.zig new file mode 100644 index 000000000..f04ea46f6 --- /dev/null +++ b/examples/microchip/attiny/src/blinky.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const gpio = microzig.hal.gpio; + +// ATtiny85: PB1 is the Digispark onboard LED +const led_pin = gpio.pin(.b, 1); + +pub fn main() void { + led_pin.set_direction(.output); + + while (true) { + busy_sleep(20_000); + led_pin.toggle(); + } +} + +pub fn busy_sleep(comptime limit: comptime_int) void { + if (limit <= 0) @compileError("limit must be non-negative!"); + + comptime var bits = 0; + inline while ((1 << bits) <= limit) { + bits += 1; + } + + const I = std.meta.Int(.unsigned, bits); + + var i: I = 0; + while (i < limit) : (i += 1) { + std.mem.doNotOptimizeAway(i); + } +} diff --git a/examples/microchip/attiny/src/blinky84.zig b/examples/microchip/attiny/src/blinky84.zig new file mode 100644 index 000000000..443fba4f0 --- /dev/null +++ b/examples/microchip/attiny/src/blinky84.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const gpio = microzig.hal.gpio; + +// ATtiny84: use PA0 as the LED pin +const led_pin = gpio.pin(.a, 0); + +pub fn main() void { + led_pin.set_direction(.output); + + while (true) { + busy_sleep(20_000); + led_pin.toggle(); + } +} + +pub fn busy_sleep(comptime limit: comptime_int) void { + if (limit <= 0) @compileError("limit must be non-negative!"); + + comptime var bits = 0; + inline while ((1 << bits) <= limit) { + bits += 1; + } + + const I = std.meta.Int(.unsigned, bits); + + var i: I = 0; + while (i < limit) : (i += 1) { + std.mem.doNotOptimizeAway(i); + } +} diff --git a/examples/microchip/attiny/src/blinky_interrupt.zig b/examples/microchip/attiny/src/blinky_interrupt.zig new file mode 100644 index 000000000..7034fff78 --- /dev/null +++ b/examples/microchip/attiny/src/blinky_interrupt.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const gpio = microzig.hal.gpio; + +const led_pin = gpio.pin(.b, 1); + +pub const microzig_options: microzig.Options = .{ + .interrupts = .{ + .INT0 = &my_int0_handler, + }, +}; + +fn my_int0_handler() callconv(.avr_signal) void { + led_pin.toggle(); +} + +pub fn main() void { + led_pin.set_direction(.output); + + while (true) { + std.mem.doNotOptimizeAway({}); + } +} diff --git a/port/microchip/attiny/README.md b/port/microchip/attiny/README.md new file mode 100644 index 000000000..3f7fceaae --- /dev/null +++ b/port/microchip/attiny/README.md @@ -0,0 +1,18 @@ +# Microchip ATtiny Hardware Support Package + +## Supported Chips + +- ATtiny85 +- ATtiny84 +- ATtiny1634 +- ATtiny1616 + +## FYI: LLVM issues + +Currently LLVM is having trouble lowering AVR when this is built in debug mode. + +For now always build in release small: + +``` +zig build -Doptimize=ReleaseSmall +``` diff --git a/port/microchip/attiny/build.zig b/port/microchip/attiny/build.zig new file mode 100644 index 000000000..3998673ef --- /dev/null +++ b/port/microchip/attiny/build.zig @@ -0,0 +1,168 @@ +const std = @import("std"); +const microzig = @import("microzig/build-internals"); + +const Self = @This(); + +chips: struct { + attiny85: *const microzig.Target, + attiny84: *const microzig.Target, + attiny1634: *const microzig.Target, + attiny1616: *const microzig.Target, +}, + +boards: struct { + digispark: *const microzig.Target, + adafruit: struct { + trinket: *const microzig.Target, + gemma: *const microzig.Target, + }, +}, + +pub fn init(dep: *std.Build.Dependency) Self { + const b = dep.builder; + + const atpack = b.dependency("atpack", .{}); + + const avr25_target: std.Target.Query = .{ + .cpu_arch = .avr, + .cpu_model = .{ .explicit = &std.Target.avr.cpu.avr25 }, + .os_tag = .freestanding, + .abi = .eabi, + }; + + const avr35_target: std.Target.Query = .{ + .cpu_arch = .avr, + .cpu_model = .{ .explicit = &std.Target.avr.cpu.avr35 }, + .os_tag = .freestanding, + .abi = .eabi, + }; + + const avrxmega3_target: std.Target.Query = .{ + .cpu_arch = .avr, + .cpu_model = .{ .explicit = &std.Target.avr.cpu.avrxmega3 }, + .os_tag = .freestanding, + .abi = .eabi, + }; + + const chip_attiny85: microzig.Target = .{ + .dep = dep, + .preferred_binary_format = .hex, + .zig_target = avr25_target, + .chip = .{ + .name = "ATtiny85", + .url = "https://www.microchip.com/en-us/product/attiny85", + .register_definition = .{ + .atdf = atpack.path("atdf/ATtiny85.atdf"), + }, + .memory_regions = &.{ + .{ .tag = .flash, .offset = 0x000000, .length = 8 * 1024, .access = .rx }, + .{ .tag = .ram, .offset = 0x800060, .length = 512, .access = .rw }, + }, + }, + .hal = .{ + .root_source_file = b.path("src/hals/ATtiny85.zig"), + }, + .bundle_compiler_rt = false, + }; + + const chip_attiny84: microzig.Target = .{ + .dep = dep, + .preferred_binary_format = .hex, + .zig_target = avr25_target, + .chip = .{ + .name = "ATtiny84", + .url = "https://www.microchip.com/en-us/product/attiny84", + .register_definition = .{ + .atdf = atpack.path("atdf/ATtiny84.atdf"), + }, + .memory_regions = &.{ + .{ .tag = .flash, .offset = 0x000000, .length = 8 * 1024, .access = .rx }, + .{ .tag = .ram, .offset = 0x800060, .length = 512, .access = .rw }, + }, + }, + .hal = .{ + .root_source_file = b.path("src/hals/ATtiny84.zig"), + }, + .bundle_compiler_rt = false, + }; + + const chip_attiny1634: microzig.Target = .{ + .dep = dep, + .preferred_binary_format = .hex, + .zig_target = avr35_target, + .chip = .{ + .name = "ATtiny1634", + .url = "https://www.microchip.com/en-us/product/attiny1634", + .register_definition = .{ + .atdf = atpack.path("atdf/ATtiny1634.atdf"), + }, + .memory_regions = &.{ + .{ .tag = .flash, .offset = 0x000000, .length = 16 * 1024, .access = .rx }, + .{ .tag = .ram, .offset = 0x800100, .length = 1024, .access = .rw }, + }, + }, + .hal = .{ + .root_source_file = b.path("src/hals/ATtiny1634.zig"), + }, + .bundle_compiler_rt = false, + }; + + const chip_attiny1616: microzig.Target = .{ + .dep = dep, + .preferred_binary_format = .hex, + .zig_target = avrxmega3_target, + .chip = .{ + .name = "ATtiny1616", + .url = "https://www.microchip.com/en-us/product/attiny1616", + .register_definition = .{ + .atdf = atpack.path("atdf/ATtiny1616.atdf"), + }, + .memory_regions = &.{ + .{ .tag = .flash, .offset = 0x000000, .length = 16 * 1024, .access = .rx }, + .{ .tag = .ram, .offset = 0x803800, .length = 2048, .access = .rw }, + }, + }, + .hal = .{ + .root_source_file = b.path("src/hals/ATtiny1616.zig"), + }, + .bundle_compiler_rt = false, + }; + + return .{ + .chips = .{ + .attiny85 = chip_attiny85.derive(.{}), + .attiny84 = chip_attiny84.derive(.{}), + .attiny1634 = chip_attiny1634.derive(.{}), + .attiny1616 = chip_attiny1616.derive(.{}), + }, + .boards = .{ + .digispark = chip_attiny85.derive(.{ + .board = .{ + .name = "Digispark", + .url = "http://digistump.com/products/1", + .root_source_file = b.path("src/boards/digispark.zig"), + }, + }), + .adafruit = .{ + .trinket = chip_attiny85.derive(.{ + .board = .{ + .name = "Adafruit Trinket", + .url = "https://www.adafruit.com/product/1501", + .root_source_file = b.path("src/boards/adafruit_trinket.zig"), + }, + }), + .gemma = chip_attiny85.derive(.{ + .board = .{ + .name = "Adafruit Gemma", + .url = "https://www.adafruit.com/product/1222", + .root_source_file = b.path("src/boards/adafruit_gemma.zig"), + }, + }), + }, + }, + }; +} + +pub fn build(b: *std.Build) void { + _ = b.step("test", "Run platform agnostic unit tests"); +} diff --git a/port/microchip/attiny/build.zig.zon b/port/microchip/attiny/build.zig.zon new file mode 100644 index 000000000..1a07fdf96 --- /dev/null +++ b/port/microchip/attiny/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = .mz_port_microchip_attiny, + .fingerprint = 0x6455fe5728af26e9, + .version = "0.0.0", + .dependencies = .{ + .@"microzig/build-internals" = .{ .path = "../../../build-internals" }, + .atpack = .{ + .url = "https://atpack.microzig.tech/Atmel.ATtiny_DFP.2.0.368.atpack", + .hash = "N-V-__8AAChkfwZp4ZmDp0teMCY8Q-2clc9GmzWHeh7xiKnO", + }, + }, + .paths = .{ + "README.md", + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/port/microchip/attiny/src/boards/adafruit_gemma.zig b/port/microchip/attiny/src/boards/adafruit_gemma.zig new file mode 100644 index 000000000..24da7ac8c --- /dev/null +++ b/port/microchip/attiny/src/boards/adafruit_gemma.zig @@ -0,0 +1,13 @@ +pub const chip = @import("chip"); + +pub const clock_frequencies = .{ + .cpu = 8_000_000, +}; + +pub const pin_map = .{ + .D0 = "PB0", + .D1 = "PB1", + .D2 = "PB2", + // Built-in LED on D1 (PB1) + .LED = "PB1", +}; diff --git a/port/microchip/attiny/src/boards/adafruit_trinket.zig b/port/microchip/attiny/src/boards/adafruit_trinket.zig new file mode 100644 index 000000000..afb2c3437 --- /dev/null +++ b/port/microchip/attiny/src/boards/adafruit_trinket.zig @@ -0,0 +1,15 @@ +pub const chip = @import("chip"); + +pub const clock_frequencies = .{ + .cpu = 8_000_000, +}; + +pub const pin_map = .{ + .P0 = "PB0", + .P1 = "PB1", + .P2 = "PB2", + .P3 = "PB3", + .P4 = "PB4", + // Built-in LED on P1 (PB1) + .LED = "PB1", +}; diff --git a/port/microchip/attiny/src/boards/digispark.zig b/port/microchip/attiny/src/boards/digispark.zig new file mode 100644 index 000000000..8245210fc --- /dev/null +++ b/port/microchip/attiny/src/boards/digispark.zig @@ -0,0 +1,17 @@ +pub const chip = @import("chip"); + +pub const clock_frequencies = .{ + .cpu = 16_500_000, +}; + +pub const pin_map = .{ + // Digispark pin numbering maps to PORTB + .P0 = "PB0", + .P1 = "PB1", + .P2 = "PB2", + .P3 = "PB3", + .P4 = "PB4", + .P5 = "PB5", + // Built-in LED on P1 (PB1) + .LED = "PB1", +}; diff --git a/port/microchip/attiny/src/hals/ATtiny1616.zig b/port/microchip/attiny/src/hals/ATtiny1616.zig new file mode 100644 index 000000000..98c918eba --- /dev/null +++ b/port/microchip/attiny/src/hals/ATtiny1616.zig @@ -0,0 +1,16 @@ +pub const registers = @import("attiny1616/registers.zig"); +pub const gpio = @import("attiny1616/gpio.zig"); +pub const clock = @import("attiny1616/clock.zig"); +pub const tca0 = @import("attiny1616/tca0.zig"); +pub const rtc_pit = @import("attiny1616/rtc_pit.zig"); +pub const adc = @import("attiny1616/adc.zig"); +pub const pcint = @import("attiny1616/pcint.zig"); +pub const watchdog = @import("attiny1616/watchdog.zig"); +pub const eeprom = @import("attiny1616/eeprom.zig"); +pub const progmem = @import("attiny85/progmem.zig"); + +pub const memory = struct { + pub const flash_size = 16 * 1024; + pub const eeprom_size = 256; + pub const sram_size = 2048; +}; diff --git a/port/microchip/attiny/src/hals/ATtiny1634.zig b/port/microchip/attiny/src/hals/ATtiny1634.zig new file mode 100644 index 000000000..1dbd497f1 --- /dev/null +++ b/port/microchip/attiny/src/hals/ATtiny1634.zig @@ -0,0 +1,16 @@ +pub const registers = @import("attiny1634/registers.zig"); + +pub const gpio = @import("attiny1634/gpio.zig"); +pub const timer0 = @import("attiny1634/timer0.zig"); +pub const timer1 = @import("attiny1634/timer1.zig"); +pub const adc = @import("attiny1634/adc.zig"); +pub const watchdog = @import("attiny1634/watchdog.zig"); +pub const pcint = @import("attiny1634/pcint.zig"); +pub const eeprom = @import("attiny1634/eeprom.zig"); +pub const progmem = @import("attiny85/progmem.zig"); + +pub const memory = struct { + pub const flash_size = 16 * 1024; + pub const eeprom_size = 256; + pub const sram_size = 1024; +}; diff --git a/port/microchip/attiny/src/hals/ATtiny84.zig b/port/microchip/attiny/src/hals/ATtiny84.zig new file mode 100644 index 000000000..c88226f3e --- /dev/null +++ b/port/microchip/attiny/src/hals/ATtiny84.zig @@ -0,0 +1,71 @@ +const microzig = @import("microzig"); +const cpu = microzig.cpu; + +pub const gpio = struct { + pub const Port = enum(u1) { + a = 0, + b = 1, + + pub const Regs = extern struct { + /// Port Input Pins + PIN: u8, + /// Port Data Direction Register + DDR: u8, + /// Port Data Register + PORT: u8, + }; + + // IO addresses (data address - 0x20) for SBI/CBI instructions. + // PINB=0x16, DDRB=0x17, PORTB=0x18 + // PINA=0x19, DDRA=0x1A, PORTA=0x1B + pub inline fn get_regs(port: Port) *volatile Regs { + return switch (port) { + .b => @ptrFromInt(0x16), + .a => @ptrFromInt(0x19), + }; + } + }; + + pub fn pin(port: Port, num: u3) Pin { + return Pin{ + .port = port, + .num = num, + }; + } + + pub const Direction = enum { + input, + output, + }; + + pub const Pin = packed struct(u4) { + port: Port, + num: u3, + + pub inline fn set_direction(p: Pin, dir: Direction) void { + const dir_addr: *volatile u8 = &p.port.get_regs().DDR; + switch (dir) { + .input => cpu.cbi(@intFromPtr(dir_addr), p.num), + .output => cpu.sbi(@intFromPtr(dir_addr), p.num), + } + } + + pub inline fn read(p: Pin) u1 { + const pin_addr: *volatile u8 = &p.port.get_regs().PIN; + return @truncate(pin_addr.* >> p.num & 0x01); + } + + pub inline fn put(p: Pin, value: u1) void { + const port_addr: *volatile u8 = &p.port.get_regs().PORT; + switch (value) { + 1 => cpu.sbi(@intFromPtr(port_addr), p.num), + 0 => cpu.cbi(@intFromPtr(port_addr), p.num), + } + } + + pub inline fn toggle(p: Pin) void { + const pin_addr: *volatile u8 = &p.port.get_regs().PIN; + cpu.sbi(@intFromPtr(pin_addr), p.num); + } + }; +}; diff --git a/port/microchip/attiny/src/hals/ATtiny85.zig b/port/microchip/attiny/src/hals/ATtiny85.zig new file mode 100644 index 000000000..258271311 --- /dev/null +++ b/port/microchip/attiny/src/hals/ATtiny85.zig @@ -0,0 +1,17 @@ +pub const registers = @import("attiny85/registers.zig"); + +pub const gpio = @import("attiny85/gpio.zig"); +pub const timer0 = @import("attiny85/timer0.zig"); +pub const timer1 = @import("attiny85/timer1.zig"); +pub const adc = @import("attiny85/adc.zig"); +pub const watchdog = @import("attiny85/watchdog.zig"); +pub const sleep = @import("attiny85/sleep.zig"); +pub const pcint = @import("attiny85/pcint.zig"); +pub const eeprom = @import("attiny85/eeprom.zig"); +pub const progmem = @import("attiny85/progmem.zig"); + +pub const memory = struct { + pub const flash_size = 8 * 1024; + pub const eeprom_size = 512; + pub const sram_size = 512; +}; diff --git a/port/microchip/attiny/src/hals/attiny1616/adc.zig b/port/microchip/attiny/src/hals/attiny1616/adc.zig new file mode 100644 index 000000000..9bb5275e7 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/adc.zig @@ -0,0 +1,114 @@ +const regs = @import("registers.zig"); + +pub const Channel = enum(u8) { + ain0 = 0x00, + ain1 = 0x01, + ain2 = 0x02, + ain3 = 0x03, + ain4 = 0x04, + ain5 = 0x05, + ain6 = 0x06, + ain7 = 0x07, + ain8 = 0x08, + ain9 = 0x09, + ain10 = 0x0A, + ain11 = 0x0B, + dac0 = 0x1C, + internal_reference = 0x1D, + temperature = 0x1E, + ground = 0x1F, +}; + +pub const Reference = enum(u8) { + vdd = 0x00, + internal_0v55 = 0x10, + internal_1v1 = 0x20, + internal_2v5 = 0x30, + internal_4v3 = 0x40, + internal_1v5 = 0x50, +}; + +pub const SampleCount = enum(u8) { + samples1 = 0x00, + samples2 = 0x01, + samples4 = 0x02, + samples8 = 0x03, + samples16 = 0x04, + samples32 = 0x05, + samples64 = 0x06, +}; + +pub const Prescaler = enum(u8) { + div2 = 0x00, + div4 = 0x01, + div8 = 0x02, + div16 = 0x03, + div32 = 0x04, + div64 = 0x05, + div128 = 0x06, + div256 = 0x07, +}; + +pub const InitialDelay = enum(u8) { + cycles0 = 0x00, + cycles16 = 0x20, + cycles32 = 0x40, + cycles64 = 0x60, + cycles128 = 0x80, + cycles256 = 0xA0, +}; + +pub const Config = struct { + channel: Channel = .ain0, + reference: Reference = .vdd, + sample_count: SampleCount = .samples1, + prescaler: Prescaler = .div16, + initial_delay: InitialDelay = .cycles0, + sample_capacitance: bool = false, + freerun: bool = false, + run_standby: bool = false, + sample_control: u8 = 0, +}; + +pub fn configure(config: Config) void { + regs.write(regs.adc0_ctrla, 0); + regs.write(regs.vref_ctrla, @intFromEnum(config.reference)); + regs.write(regs.adc0_muxpos, @intFromEnum(config.channel)); + regs.write(regs.adc0_ctrlb, @intFromEnum(config.sample_count)); + regs.write(regs.adc0_ctrlc, @intFromEnum(config.prescaler) | if (config.sample_capacitance) regs.bit(regs.adc_bits.sample_capacitance) else 0); + regs.write(regs.adc0_ctrld, @intFromEnum(config.initial_delay)); + regs.write(regs.adc0_sampctrl, config.sample_control); + + var ctrla = regs.bit(regs.adc_bits.enable); + if (config.freerun) ctrla |= regs.bit(regs.adc_bits.freerun); + if (config.run_standby) ctrla |= regs.bit(regs.adc_bits.runstby); + regs.write(regs.adc0_ctrla, ctrla); +} + +pub fn start() void { + regs.write(regs.adc0_command, regs.bit(regs.adc_bits.start)); +} + +pub fn stop() void { + regs.clear_bits(regs.adc0_ctrla, regs.bit(regs.adc_bits.enable)); +} + +pub fn result_ready() bool { + return (regs.read(regs.adc0_intflags) & regs.bit(regs.adc_bits.resrdy)) != 0; +} + +pub fn clear_result_ready() void { + regs.write(regs.adc0_intflags, regs.bit(regs.adc_bits.resrdy)); +} + +pub fn enable_result_ready_interrupt() void { + regs.set_bits(regs.adc0_intctrl, regs.bit(regs.adc_bits.resrdy)); +} + +pub fn disable_result_ready_interrupt() void { + regs.clear_bits(regs.adc0_intctrl, regs.bit(regs.adc_bits.resrdy)); +} + +pub fn read_raw16() u16 { + return regs.mem16(regs.adc0_res).*; +} diff --git a/port/microchip/attiny/src/hals/attiny1616/clock.zig b/port/microchip/attiny/src/hals/attiny1616/clock.zig new file mode 100644 index 000000000..e169e151b --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/clock.zig @@ -0,0 +1,37 @@ +const regs = @import("registers.zig"); + +pub const Prescaler = enum(u8) { + disabled = 0x00, + div2 = 0x01, + div4 = 0x03, + div6 = 0x11, + div8 = 0x05, + div10 = 0x13, + div12 = 0x15, + div16 = 0x07, + div24 = 0x17, + div32 = 0x09, + div48 = 0x19, + div64 = 0x0B, +}; + +pub fn protected_write(comptime address: u16, value: u8) void { + // ATtiny1614/1616/1617 DS40002204A section 8.5.7.1, page 55: + // protected I/O writes require CPU.CCP=IOREG and the register write + // within four instructions. + // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf + regs.write(regs.ccp, regs.ccp_ioreg_signature); + regs.write(address, value); +} + +pub fn set_prescaler(prescaler: Prescaler) void { + protected_write(regs.clkctrl_mclkctrlb, @intFromEnum(prescaler)); +} + +pub fn use_default20_m_hz_div2() void { + set_prescaler(.div2); +} + +pub fn oscillator_changing() bool { + return (regs.read(regs.clkctrl_mclkstatus) & regs.bit(0)) != 0; +} diff --git a/port/microchip/attiny/src/hals/attiny1616/eeprom.zig b/port/microchip/attiny/src/hals/attiny1616/eeprom.zig new file mode 100644 index 000000000..c721cc755 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/eeprom.zig @@ -0,0 +1,50 @@ +const regs = @import("registers.zig"); + +pub const size = 256; +pub const data_start: u16 = 0x1400; + +const command_page_erase_write = 0x03; + +// Type-safe wrapper for raw EEPROM byte offsets. +pub const Address = enum(u8) { + _, + + pub fn from_int(value: u8) Address { + return @enumFromInt(value); + } +}; + +pub fn read_byte(address: Address) u8 { + return regs.mem8(data_start + @as(u16, @intFromEnum(address))).*; +} + +pub fn write_byte(address: Address, value: u8) void { + busy_wait(); + regs.mem8(data_start + @as(u16, @intFromEnum(address))).* = value; + execute_command(command_page_erase_write); +} + +pub fn update_byte(address: Address, value: u8) void { + if (read_byte(address) != value) write_byte(address, value); +} + +pub fn read_slice(comptime len: usize, start: Address) [len]u8 { + var out: [len]u8 = undefined; + for (&out, 0..) |*byte, offset| { + byte.* = read_byte(@enumFromInt(@intFromEnum(start) + offset)); + } + return out; +} + +pub fn busy_wait() void { + while ((regs.read(regs.nvmctrl_status) & regs.bit(regs.nvm_bits.eebusy)) != 0) {} +} + +fn execute_command(command: u8) void { + // ATtiny1614/1616/1617 DS40002204A section 9.3.2.4, page 63: + // write EEPROM through the mapped page buffer, unlock NVMCTRL.CTRLA with + // CPU.CCP=SPM, then issue the command within four instructions. + // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf + regs.write(regs.ccp, regs.ccp_spm_signature); + regs.write(regs.nvmctrl_ctrla, command); +} diff --git a/port/microchip/attiny/src/hals/attiny1616/gpio.zig b/port/microchip/attiny/src/hals/attiny1616/gpio.zig new file mode 100644 index 000000000..d5c42a25a --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/gpio.zig @@ -0,0 +1,111 @@ +const regs = @import("registers.zig"); + +pub const Port = enum(u2) { + a, + b, + c, +}; + +pub const Direction = enum { + input, + output, +}; + +pub const Sense = enum(u3) { + interrupt_disabled = 0x0, + both_edges = 0x1, + rising = 0x2, + falling = 0x3, + input_disable = 0x4, + level = 0x5, +}; + +pub const Pin = packed struct(u5) { + index: u3, + port: Port, + + pub fn init(comptime port: Port, comptime index: u3) Pin { + return .{ .port = port, .index = index }; + } + + pub inline fn set_direction(p: Pin, direction: Direction) void { + gpio.set_direction(p, direction); + } + + pub inline fn read(p: Pin) bool { + return gpio.read(p); + } + + pub inline fn put(p: Pin, value: bool) void { + gpio.put(p, value); + } + + pub inline fn toggle(p: Pin) void { + gpio.toggle(p); + } +}; + +const gpio = @This(); + +pub fn pin(comptime port: Port, comptime index: u3) Pin { + return Pin.init(port, index); +} + +const PortRegisters = struct { + vdir: u16, + vout: u16, + vin: u16, + vintflags: u16, + pinctrl: u16, +}; + +fn port_registers(port: Port) PortRegisters { + return switch (port) { + .a => .{ .vdir = regs.vporta_dir, .vout = regs.vporta_out, .vin = regs.vporta_in, .vintflags = regs.vporta_intflags, .pinctrl = regs.porta_pinctrl }, + .b => .{ .vdir = regs.vportb_dir, .vout = regs.vportb_out, .vin = regs.vportb_in, .vintflags = regs.vportb_intflags, .pinctrl = regs.portb_pinctrl }, + .c => .{ .vdir = regs.vportc_dir, .vout = regs.vportc_out, .vin = regs.vportc_in, .vintflags = regs.vportc_intflags, .pinctrl = regs.portc_pinctrl }, + }; +} + +pub inline fn mask(gpio_pin: Pin) u8 { + return regs.bit(gpio_pin.index); +} + +pub fn set_direction(gpio_pin: Pin, direction: Direction) void { + const r = port_registers(gpio_pin.port); + switch (direction) { + .input => regs.clear_bits(r.vdir, mask(gpio_pin)), + .output => regs.set_bits(r.vdir, mask(gpio_pin)), + } +} + +pub fn read(gpio_pin: Pin) bool { + return (regs.read(port_registers(gpio_pin.port).vin) & mask(gpio_pin)) != 0; +} + +pub fn put(gpio_pin: Pin, value: bool) void { + const r = port_registers(gpio_pin.port); + if (value) { + regs.set_bits(r.vout, mask(gpio_pin)); + } else { + regs.clear_bits(r.vout, mask(gpio_pin)); + } +} + +pub fn toggle(gpio_pin: Pin) void { + regs.write(port_registers(gpio_pin.port).vout, regs.read(port_registers(gpio_pin.port).vout) ^ mask(gpio_pin)); +} + +pub fn configure_input(gpio_pin: Pin, pullup: bool, sense: Sense) void { + // ATtiny1614/1616/1617 DS40002204A section 16.3.3.1, page 134: + // PORT PINnCTRL selects the per-pin input/sense mode used for pin interrupts. + // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf + set_direction(gpio_pin, .input); + var value: u8 = @intFromEnum(sense); + if (pullup) value |= regs.bit(regs.port_bits.pullupen); + regs.write(port_registers(gpio_pin.port).pinctrl + gpio_pin.index, value); +} + +pub fn clear_interrupt(gpio_pin: Pin) void { + regs.write(port_registers(gpio_pin.port).vintflags, mask(gpio_pin)); +} diff --git a/port/microchip/attiny/src/hals/attiny1616/pcint.zig b/port/microchip/attiny/src/hals/attiny1616/pcint.zig new file mode 100644 index 000000000..52924b28a --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/pcint.zig @@ -0,0 +1,11 @@ +const gpio = @import("gpio.zig"); + +pub const Sense = gpio.Sense; + +pub fn configure(pin: gpio.Pin, pullup: bool, sense: Sense) void { + gpio.configure_input(pin, pullup, sense); +} + +pub fn clear_flag(pin: gpio.Pin) void { + gpio.clear_interrupt(pin); +} diff --git a/port/microchip/attiny/src/hals/attiny1616/registers.zig b/port/microchip/attiny/src/hals/attiny1616/registers.zig new file mode 100644 index 000000000..93a3aea1e --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/registers.zig @@ -0,0 +1,139 @@ +pub inline fn mem8(address: u16) *volatile u8 { + return @ptrFromInt(address); +} + +pub inline fn mem16(address: u16) *volatile u16 { + return @ptrFromInt(address); +} + +pub inline fn read(address: u16) u8 { + return mem8(address).*; +} + +pub inline fn write(address: u16, value: u8) void { + mem8(address).* = value; +} + +pub inline fn set_bits(address: u16, mask: u8) void { + write(address, read(address) | mask); +} + +pub inline fn clear_bits(address: u16, mask: u8) void { + write(address, read(address) & ~mask); +} + +pub inline fn bit(bit_index: u3) u8 { + return @as(u8, 1) << bit_index; +} + +pub const ccp_signature = 0xD8; + +pub const vporta_dir = 0x0000; +pub const vporta_out = 0x0001; +pub const vporta_in = 0x0002; +pub const vporta_intflags = 0x0003; +pub const vportb_dir = 0x0004; +pub const vportb_out = 0x0005; +pub const vportb_in = 0x0006; +pub const vportb_intflags = 0x0007; +pub const vportc_dir = 0x0008; +pub const vportc_out = 0x0009; +pub const vportc_in = 0x000A; +pub const vportc_intflags = 0x000B; + +pub const ccp = 0x0034; +pub const rstctrl_rstfr = 0x0040; +pub const rstctrl_swrr = 0x0041; +pub const clkctrl_mclkctrla = 0x0060; +pub const clkctrl_mclkctrlb = 0x0061; +pub const clkctrl_mclkstatus = 0x0063; +pub const vref_ctrla = 0x00A0; +pub const vref_ctrlb = 0x00A1; +pub const wdt_ctrla = 0x0100; +pub const wdt_status = 0x0101; + +pub const rtc_pitctrla = 0x0150; +pub const rtc_pitstatus = 0x0151; +pub const rtc_pitintctrl = 0x0152; +pub const rtc_pitintflags = 0x0153; + +pub const porta_dir = 0x0400; +pub const porta_out = 0x0404; +pub const porta_in = 0x0408; +pub const porta_intflags = 0x0409; +pub const porta_pinctrl = 0x0410; +pub const portb_dir = 0x0420; +pub const portb_out = 0x0424; +pub const portb_in = 0x0428; +pub const portb_intflags = 0x0429; +pub const portb_pinctrl = 0x0430; +pub const portc_dir = 0x0440; +pub const portc_out = 0x0444; +pub const portc_in = 0x0448; +pub const portc_intflags = 0x0449; +pub const portc_pinctrl = 0x0450; + +pub const adc0_ctrla = 0x0600; +pub const adc0_ctrlb = 0x0601; +pub const adc0_ctrlc = 0x0602; +pub const adc0_ctrld = 0x0603; +pub const adc0_sampctrl = 0x0605; +pub const adc0_muxpos = 0x0606; +pub const adc0_command = 0x0608; +pub const adc0_intctrl = 0x060A; +pub const adc0_intflags = 0x060B; +pub const adc0_res = 0x0610; +pub const dac0_data = 0x06A1; + +pub const tca0_single_ctrla = 0x0A00; +pub const tca0_single_ctrlb = 0x0A01; +pub const tca0_single_intctrl = 0x0A0A; +pub const tca0_single_intflags = 0x0A0B; +pub const tca0_single_cnt = 0x0A20; +pub const tca0_single_per = 0x0A26; +pub const tca0_single_cmp0 = 0x0A28; +pub const tca0_single_cmp1 = 0x0A2A; +pub const tca0_single_perbuf = 0x0A36; +pub const tca0_single_cmp0buf = 0x0A38; +pub const tca0_single_cmp1buf = 0x0A3A; + +pub const nvmctrl_ctrla = 0x1000; +pub const nvmctrl_status = 0x1002; + +pub const ccp_ioreg_signature = 0xD8; +pub const ccp_spm_signature = 0x9D; + +pub const port_bits = struct { + pub const pullupen = 3; +}; + +pub const adc_bits = struct { + pub const enable = 0; + pub const freerun = 1; + pub const runstby = 7; + pub const start = 0; + pub const sample_capacitance = 6; + pub const resrdy = 0; +}; + +pub const rtc_bits = struct { + pub const piten = 0; + pub const pi = 0; + pub const ctrlbusy = 0; +}; + +pub const tca_bits = struct { + pub const enable = 0; + pub const cmp0en = 4; + pub const cmp1en = 5; + pub const ovf = 0; +}; + +pub const wdt_bits = struct { + pub const syncbusy = 0; +}; + +pub const nvm_bits = struct { + pub const fbusy = 0; + pub const eebusy = 1; +}; diff --git a/port/microchip/attiny/src/hals/attiny1616/rtc_pit.zig b/port/microchip/attiny/src/hals/attiny1616/rtc_pit.zig new file mode 100644 index 000000000..a16cbe3e1 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/rtc_pit.zig @@ -0,0 +1,41 @@ +const regs = @import("registers.zig"); + +pub const Period = enum(u8) { + off = 0x00, + cycles4 = 0x08, + cycles8 = 0x10, + cycles16 = 0x18, + cycles32 = 0x20, + cycles64 = 0x28, + cycles128 = 0x30, + cycles256 = 0x38, + cycles512 = 0x40, + cycles1024 = 0x48, + cycles2048 = 0x50, + cycles4096 = 0x58, + cycles8192 = 0x60, + cycles16384 = 0x68, + cycles32768 = 0x70, +}; + +pub fn configure(period: Period, interrupt: bool) void { + // ATtiny1614/1616/1617 DS40002204A section 23.5.1, page 296: + // check PIT synchronization, set PI if needed, select PERIOD, then set PITEN. + // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf + while (busy()) {} + regs.write(regs.rtc_pitctrla, @intFromEnum(period) | regs.bit(regs.rtc_bits.piten)); + regs.write(regs.rtc_pitintctrl, if (interrupt) regs.bit(regs.rtc_bits.pi) else 0); +} + +pub fn stop() void { + while (busy()) {} + regs.write(regs.rtc_pitctrla, 0); +} + +pub fn busy() bool { + return (regs.read(regs.rtc_pitstatus) & regs.bit(regs.rtc_bits.ctrlbusy)) != 0; +} + +pub fn clear_interrupt_flag() void { + regs.write(regs.rtc_pitintflags, regs.bit(regs.rtc_bits.pi)); +} diff --git a/port/microchip/attiny/src/hals/attiny1616/tca0.zig b/port/microchip/attiny/src/hals/attiny1616/tca0.zig new file mode 100644 index 000000000..ca267b040 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/tca0.zig @@ -0,0 +1,77 @@ +const regs = @import("registers.zig"); + +pub const ClockSelect = enum(u8) { + div1 = 0x00, + div2 = 0x02, + div4 = 0x04, + div8 = 0x06, + div16 = 0x08, + div64 = 0x0A, + div256 = 0x0C, + div1024 = 0x0E, +}; + +pub const Waveform = enum(u8) { + normal = 0x0, + frequency = 0x1, + single_slope = 0x3, + dual_slope_top = 0x5, + dual_slope_top_bottom = 0x6, + dual_slope_bottom = 0x7, +}; + +pub const PwmConfig = struct { + top: u16 = 255, + compare0: u16 = 0, + compare1: u16 = 0, + enable_compare0: bool = true, + enable_compare1: bool = true, + waveform: Waveform = .dual_slope_bottom, + clock: ClockSelect = .div1, +}; + +// ATtiny1614/1616/1617 DS40002204A section 20.3.3.4.4, page 183: +// dual-slope PWM uses PER as TOP and CMPn as duty cycle. Buffered PER/CMP +// writes keep active PWM updates clean for dual-channel LED drivers. +// https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf +pub fn configure_pwm(config: PwmConfig) void { + regs.write(regs.tca0_single_ctrla, 0); + set_top(config.top); + set_compare0(config.compare0); + set_compare1(config.compare1); + + var ctrlb: u8 = @intFromEnum(config.waveform); + if (config.enable_compare0) ctrlb |= regs.bit(regs.tca_bits.cmp0en); + if (config.enable_compare1) ctrlb |= regs.bit(regs.tca_bits.cmp1en); + + regs.write(regs.tca0_single_ctrlb, ctrlb); + regs.write(regs.tca0_single_ctrla, @intFromEnum(config.clock) | regs.bit(regs.tca_bits.enable)); +} + +pub fn stop() void { + regs.clear_bits(regs.tca0_single_ctrla, regs.bit(regs.tca_bits.enable)); +} + +pub fn set_top(value: u16) void { + regs.mem16(regs.tca0_single_perbuf).* = value; +} + +pub fn set_compare0(value: u16) void { + regs.mem16(regs.tca0_single_cmp0buf).* = value; +} + +pub fn set_compare1(value: u16) void { + regs.mem16(regs.tca0_single_cmp1buf).* = value; +} + +pub fn set_counter(value: u16) void { + regs.mem16(regs.tca0_single_cnt).* = value; +} + +pub fn enable_overflow_interrupt() void { + regs.set_bits(regs.tca0_single_intctrl, regs.bit(regs.tca_bits.ovf)); +} + +pub fn clear_overflow_flag() void { + regs.write(regs.tca0_single_intflags, regs.bit(regs.tca_bits.ovf)); +} diff --git a/port/microchip/attiny/src/hals/attiny1616/watchdog.zig b/port/microchip/attiny/src/hals/attiny1616/watchdog.zig new file mode 100644 index 000000000..7f9e60eb1 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1616/watchdog.zig @@ -0,0 +1,34 @@ +const regs = @import("registers.zig"); +const clock = @import("clock.zig"); + +pub const Period = enum(u8) { + off = 0x00, + cycles8 = 0x01, + cycles16 = 0x02, + cycles32 = 0x03, + cycles64 = 0x04, + cycles128 = 0x05, + cycles256 = 0x06, + cycles512 = 0x07, + cycles1k = 0x08, + cycles2k = 0x09, + cycles4k = 0x0A, + cycles8k = 0x0B, +}; + +pub fn reset() void { + asm volatile ("wdr"); +} + +pub fn configure(period: Period) void { + while (busy()) {} + clock.protected_write(regs.wdt_ctrla, @intFromEnum(period)); +} + +pub fn stop() void { + configure(.off); +} + +pub fn busy() bool { + return (regs.read(regs.wdt_status) & regs.bit(regs.wdt_bits.syncbusy)) != 0; +} diff --git a/port/microchip/attiny/src/hals/attiny1634/adc.zig b/port/microchip/attiny/src/hals/attiny1634/adc.zig new file mode 100644 index 000000000..bdc153b7e --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/adc.zig @@ -0,0 +1,51 @@ +const regs = @import("registers.zig"); + +pub const Channel = enum(u4) { + adc0 = 0, + adc1 = 1, + adc2 = 2, + adc3 = 3, + adc4 = 4, + adc5 = 5, + adc6 = 6, + adc7 = 7, + adc8 = 8, + adc9 = 9, + adc10 = 10, + adc11 = 11, + vcc_1v1 = 13, + temperature = 14, +}; + +pub const Prescaler = enum(u3) { + div2 = 1, + div4 = 2, + div8 = 3, + div16 = 4, + div32 = 5, + div64 = 6, + div128 = 7, +}; + +pub fn configure_internal1v1(channel: Channel, prescaler: Prescaler) void { + // ATtiny1634 datasheet section 17.13.1, page 177: ADMUX selects reference + // and channel; ADCSRA starts/enables auto-triggered conversions. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + regs.write(regs.ADMUX, 0b1000_0000 | @as(u8, @intFromEnum(channel))); + regs.write(regs.ADCSRB, 1 << 4); + regs.write(regs.ADCSRA, (1 << 7) | (1 << 5) | (1 << 3) | @as(u8, @intFromEnum(prescaler))); +} + +pub inline fn start() void { + regs.set_bits(regs.ADCSRA, 1 << 6); +} + +pub inline fn stop() void { + regs.clear_bits(regs.ADCSRA, 1 << 7); +} + +pub inline fn read_raw16() u16 { + const low = regs.read(regs.ADCL); + const high = regs.read(regs.ADCH); + return (@as(u16, high) << 8) | low; +} diff --git a/port/microchip/attiny/src/hals/attiny1634/eeprom.zig b/port/microchip/attiny/src/hals/attiny1634/eeprom.zig new file mode 100644 index 000000000..cf0f9c0a9 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/eeprom.zig @@ -0,0 +1,33 @@ +const regs = @import("registers.zig"); + +pub const size = 256; +// Type-safe wrapper for raw EEPROM byte offsets. +pub const Address = enum(u8) { _ }; + +pub inline fn address(value: u8) Address { + return @enumFromInt(value); +} + +pub inline fn busy_wait() void { + while ((regs.read(regs.EECR) & regs.bit(regs.eeprom_bits.eepe)) != 0) {} +} + +pub fn read_byte(addr: Address) u8 { + busy_wait(); + regs.write(regs.EEARL, @intFromEnum(addr)); + regs.write(regs.EEARH, 0); + regs.set_bits(regs.EECR, regs.bit(regs.eeprom_bits.eere)); + return regs.read(regs.EEDR); +} + +pub fn update_byte(addr: Address, value: u8) void { + if (read_byte(addr) == value) return; + busy_wait(); + regs.write(regs.EEDR, value); + + // ATtiny1634 datasheet section 5.3.4, page 23: EEMPE must be followed by + // EEPE within four cycles or the byte write is ignored. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + regs.set_bits(regs.EECR, regs.bit(regs.eeprom_bits.eempe)); + regs.set_bits(regs.EECR, regs.bit(regs.eeprom_bits.eepe)); +} diff --git a/port/microchip/attiny/src/hals/attiny1634/gpio.zig b/port/microchip/attiny/src/hals/attiny1634/gpio.zig new file mode 100644 index 000000000..c10cad4a5 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/gpio.zig @@ -0,0 +1,68 @@ +const cpu = @import("microzig").cpu; +const regs = @import("registers.zig"); + +pub const Port = enum(u2) { + a, + b, + c, + + pub const Regs = extern struct { + PIN: u8, + DDR: u8, + PORT: u8, + PUE: u8, + }; + + pub inline fn get_regs(port: Port) *volatile Regs { + return switch (port) { + .c => @ptrFromInt(regs.PINC), + .b => @ptrFromInt(regs.PINB), + .a => @ptrFromInt(regs.PINA), + }; + } +}; + +pub const Direction = enum { input, output }; + +pub fn pin(port: Port, num: u3) Pin { + return .{ .port = port, .num = num }; +} + +pub const Pin = packed struct(u5) { + port: Port, + num: u3, + + pub inline fn set_direction(p: Pin, dir: Direction) void { + const dir_addr: *volatile u8 = &p.port.get_regs().DDR; + switch (dir) { + .input => cpu.cbi(@intFromPtr(dir_addr), p.num), + .output => cpu.sbi(@intFromPtr(dir_addr), p.num), + } + } + + pub inline fn read(p: Pin) u1 { + const pin_addr: *volatile u8 = &p.port.get_regs().PIN; + return @truncate((pin_addr.* >> p.num) & 0x01); + } + + pub inline fn put(p: Pin, value: u1) void { + const port_addr: *volatile u8 = &p.port.get_regs().PORT; + switch (value) { + 1 => cpu.sbi(@intFromPtr(port_addr), p.num), + 0 => cpu.cbi(@intFromPtr(port_addr), p.num), + } + } + + pub inline fn toggle(p: Pin) void { + const pin_addr: *volatile u8 = &p.port.get_regs().PIN; + cpu.sbi(@intFromPtr(pin_addr), p.num); + } + + pub inline fn set_pullup(p: Pin, enabled: bool) void { + const pue_addr: *volatile u8 = &p.port.get_regs().PUE; + switch (enabled) { + true => cpu.sbi(@intFromPtr(pue_addr), p.num), + false => cpu.cbi(@intFromPtr(pue_addr), p.num), + } + } +}; diff --git a/port/microchip/attiny/src/hals/attiny1634/pcint.zig b/port/microchip/attiny/src/hals/attiny1634/pcint.zig new file mode 100644 index 000000000..3d20e41a8 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/pcint.zig @@ -0,0 +1,30 @@ +const regs = @import("registers.zig"); + +pub const Group = enum { + pcint0_7, + pcint8_11, + pcint12_17, +}; + +pub inline fn enable_group(group: Group) void { + regs.set_bits(regs.GIMSK, switch (group) { + .pcint0_7 => 1 << 4, + .pcint8_11 => 1 << 5, + .pcint12_17 => 1 << 6, + }); +} + +pub inline fn enable_pin(group: Group, bit: u3) void { + // ATtiny1634 datasheet section 9.3.6, page 53: set PCMSKn plus the + // corresponding PCIEn group bit in GIMSK to enable a pin-change source. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + regs.set_bits(mask_register(group), regs.bit(bit)); +} + +fn mask_register(group: Group) u16 { + return switch (group) { + .pcint0_7 => regs.PCMSK0, + .pcint8_11 => regs.PCMSK1, + .pcint12_17 => regs.PCMSK2, + }; +} diff --git a/port/microchip/attiny/src/hals/attiny1634/registers.zig b/port/microchip/attiny/src/hals/attiny1634/registers.zig new file mode 100644 index 000000000..91f0fe470 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/registers.zig @@ -0,0 +1,88 @@ +pub inline fn mem8(address: u16) *volatile u8 { + return @ptrFromInt(address); +} + +pub inline fn mem16(address: u16) *volatile u16 { + return @ptrFromInt(address); +} + +pub inline fn read(address: u16) u8 { + return mem8(address).*; +} + +pub inline fn write(address: u16, value: u8) void { + mem8(address).* = value; +} + +pub inline fn set_bits(address: u16, mask: u8) void { + mem8(address).* |= mask; +} + +pub inline fn clear_bits(address: u16, mask: u8) void { + mem8(address).* &= ~mask; +} + +pub inline fn bit(n: u3) u8 { + return @as(u8, 1) << n; +} + +pub const ADCL = 0x00; +pub const ADCH = 0x01; +pub const ADCSRB = 0x02; +pub const ADCSRA = 0x03; +pub const ADMUX = 0x04; +pub const PINC = 0x07; +pub const DDRC = 0x08; +pub const PORTC = 0x09; +pub const PUEC = 0x0A; +pub const PINB = 0x0B; +pub const DDRB = 0x0C; +pub const PORTB = 0x0D; +pub const PUEB = 0x0E; +pub const PINA = 0x0F; +pub const DDRA = 0x10; +pub const PORTA = 0x11; +pub const PUEA = 0x12; +pub const OCR0B = 0x17; +pub const OCR0A = 0x18; +pub const TCNT0 = 0x19; +pub const TCCR0B = 0x1A; +pub const TCCR0A = 0x1B; +pub const EECR = 0x1C; +pub const EEDR = 0x1D; +pub const EEARL = 0x1E; +pub const EEARH = 0x1F; +pub const PCMSK0 = 0x27; +pub const PCMSK1 = 0x28; +pub const PCMSK2 = 0x29; +pub const CCP = 0x2F; +pub const WDTCSR = 0x30; +pub const CLKPR = 0x33; +pub const MCUSR = 0x35; +pub const MCUCR = 0x36; +pub const TIFR = 0x39; +pub const TIMSK = 0x3A; +pub const GIMSK = 0x3C; +pub const ICR1 = 0x68; +pub const OCR1B = 0x6A; +pub const OCR1A = 0x6C; +pub const TCNT1 = 0x6E; +pub const TCCR1C = 0x70; +pub const TCCR1B = 0x71; +pub const TCCR1A = 0x72; + +pub const eeprom_bits = struct { + pub const eempe = 2; + pub const eepe = 1; + pub const eere = 0; +}; + +pub const watchdog_bits = struct { + pub const wdp0 = 0; + pub const wdp1 = 1; + pub const wdp2 = 2; + pub const wde = 3; + pub const wdp3 = 5; + pub const wdie = 6; + pub const wdif = 7; +}; diff --git a/port/microchip/attiny/src/hals/attiny1634/timer0.zig b/port/microchip/attiny/src/hals/attiny1634/timer0.zig new file mode 100644 index 000000000..d583ca287 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/timer0.zig @@ -0,0 +1,21 @@ +const regs = @import("registers.zig"); + +pub const Prescaler = enum(u3) { + stopped = 0b000, + clk_1 = 0b001, + clk_8 = 0b010, + clk_64 = 0b011, + clk_256 = 0b100, + clk_1024 = 0b101, +}; + +pub fn configure_phase_correct_pwm_a(prescaler: Prescaler) void { + // ATtiny1634 datasheet section 11.7.3, page 83: phase-correct PWM on OC0A. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + regs.write(regs.TCCR0A, (1 << 7) | (1 << 0)); + regs.write(regs.TCCR0B, @intFromEnum(prescaler)); +} + +pub inline fn set_compare_a(value: u8) void { + regs.write(regs.OCR0A, value); +} diff --git a/port/microchip/attiny/src/hals/attiny1634/timer1.zig b/port/microchip/attiny/src/hals/attiny1634/timer1.zig new file mode 100644 index 000000000..ebb33c384 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/timer1.zig @@ -0,0 +1,45 @@ +const regs = @import("registers.zig"); + +pub const Prescaler = enum(u3) { + stopped = 0b000, + clk_1 = 0b001, + clk_8 = 0b010, + clk_64 = 0b011, + clk_256 = 0b100, + clk_1024 = 0b101, +}; + +pub const DynamicPwmConfig = struct { + top: u16 = 255, + prescaler: Prescaler = .clk_1, +}; + +pub fn configure_phase_correct_dynamic(config: DynamicPwmConfig) void { + // ATtiny1634 datasheet section 12.9.3 and table 12-5, pages 105 and 113: + // phase-correct PWM with ICR1 TOP gives runtime-adjustable PWM resolution. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + + regs.write(regs.TCCR1A, (1 << 7) | (1 << 5) | (1 << 1)); + regs.write(regs.TCCR1B, (1 << 4) | @as(u8, @intFromEnum(config.prescaler))); + set_top(config.top); +} + +pub inline fn set_top(value: u16) void { + regs.mem16(regs.ICR1).* = value; +} + +pub inline fn set_compare_a(value: u16) void { + regs.mem16(regs.OCR1A).* = value; +} + +pub inline fn set_compare_b(value: u16) void { + regs.mem16(regs.OCR1B).* = value; +} + +pub inline fn set_counter(value: u16) void { + regs.mem16(regs.TCNT1).* = value; +} + +pub inline fn counter() u16 { + return regs.mem16(regs.TCNT1).*; +} diff --git a/port/microchip/attiny/src/hals/attiny1634/watchdog.zig b/port/microchip/attiny/src/hals/attiny1634/watchdog.zig new file mode 100644 index 000000000..6a2169c44 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny1634/watchdog.zig @@ -0,0 +1,59 @@ +const microzig = @import("microzig"); +const regs = @import("registers.zig"); + +pub const Timeout = enum(u4) { + ms16 = 0b0000, + ms32 = 0b0001, + ms64 = 0b0010, + ms125 = 0b0011, + ms250 = 0b0100, + ms500 = 0b0101, + s1 = 0b0110, + s2 = 0b0111, + s4 = 0b1000, + s8 = 0b1001, +}; + +pub const Mode = enum { + interrupt, + reset, + interrupt_then_reset, +}; + +pub inline fn reset() void { + asm volatile ("wdr" ::: .{ .memory = true }); +} + +pub fn configure(mode: Mode, timeout: Timeout) void { + microzig.interrupt.disable_interrupts(); + reset(); + + // ATtiny1634 datasheet section 8.5.2, page 44: WDTCSR changes are + // configuration-change protected and must follow the CCP signature write. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + regs.write(regs.CCP, 0xD8); + regs.write(regs.WDTCSR, control_value(mode, timeout)); + microzig.interrupt.enable_interrupts(); +} + +pub fn stop() void { + microzig.interrupt.disable_interrupts(); + reset(); + // ATtiny1634 datasheet section 8.5.2, page 44: WDTCSR changes are + // configuration-change protected and must follow the CCP signature write. + // https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-8303-8-bit-AVR-Microcontroller-tinyAVR-ATtiny1634_Datasheet.pdf + regs.write(regs.CCP, 0xD8); + regs.write(regs.WDTCSR, 0); + microzig.interrupt.enable_interrupts(); +} + +fn control_value(mode: Mode, timeout: Timeout) u8 { + const raw: u4 = @intFromEnum(timeout); + const prescaler = (@as(u8, raw & 0b0111)) | + ((@as(u8, raw >> 3) & 0x1) << regs.watchdog_bits.wdp3); + return prescaler | switch (mode) { + .interrupt => regs.bit(regs.watchdog_bits.wdie), + .reset => regs.bit(regs.watchdog_bits.wde), + .interrupt_then_reset => regs.bit(regs.watchdog_bits.wdie) | regs.bit(regs.watchdog_bits.wde), + }; +} diff --git a/port/microchip/attiny/src/hals/attiny85/adc.zig b/port/microchip/attiny/src/hals/attiny85/adc.zig new file mode 100644 index 000000000..7779804fb --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/adc.zig @@ -0,0 +1,92 @@ +const regs = @import("registers.zig"); +const sleep = @import("sleep.zig"); + +pub const Reference = enum(u3) { + vcc = 0b000, + internal_1v1 = 0b100, + internal_2v56 = 0b110, +}; + +pub const Channel = enum(u4) { + adc0 = 0b0000, + adc1 = 0b0001, + adc2 = 0b0010, + adc3 = 0b0011, + vcc_1v1 = 0b1100, + temperature = 0b1111, +}; + +pub const Prescaler = enum(u3) { + div2 = 0b001, + div4 = 0b010, + div8 = 0b011, + div16 = 0b100, + div32 = 0b101, + div64 = 0b110, + div128 = 0b111, +}; + +pub const Config = struct { + reference: Reference = .internal_1v1, + channel: Channel, + prescaler: Prescaler = .div128, + left_adjust: bool = true, + auto_trigger: bool = true, + interrupt: bool = false, +}; + +pub fn apply(config: Config) void { + // ATtiny25/45/85 datasheet, section 17.13.1: ADMUX packs reference, + // left-adjust, and channel selection in one register. + regs.write(regs.ADMUX, (@as(u8, @intFromEnum(config.reference)) << 4) | + (@as(u8, @intFromBool(config.left_adjust)) << 5) | + @as(u8, @intFromEnum(config.channel))); + + regs.write(regs.ADCSRA, regs.bit(regs.adc_bits.aden) | + (@as(u8, @intFromBool(config.auto_trigger)) << regs.adc_bits.adate) | + (@as(u8, @intFromBool(config.interrupt)) << regs.adc_bits.adie) | + @as(u8, @intFromEnum(config.prescaler))); +} + +pub inline fn use_adc_noise_reduction_sleep() void { + sleep.set_mode(.adc_noise_reduction); +} + +pub inline fn start() void { + regs.set_bits(regs.ADCSRA, regs.bit(regs.adc_bits.adsc)); +} + +pub inline fn stop() void { + regs.clear_bits(regs.ADCSRA, regs.bit(regs.adc_bits.aden)); +} + +pub inline fn conversion_running() bool { + return (regs.read(regs.ADCSRA) & regs.bit(regs.adc_bits.adsc)) != 0; +} + +pub inline fn enable_digital_input(channel: Channel, enabled: bool) void { + const mask: u8 = switch (channel) { + .adc0 => 1 << 5, + .adc1 => 1 << 2, + .adc2 => 1 << 4, + .adc3 => 1 << 3, + else => 0, + }; + if (enabled) regs.clear_bits(regs.DIDR0, mask) else regs.set_bits(regs.DIDR0, mask); +} + +pub inline fn read_left_adjusted10() u16 { + const low = regs.read(regs.ADCL); + const high = regs.read(regs.ADCH); + return (@as(u16, high) << 2) | (@as(u16, low) >> 6); +} + +pub inline fn read_raw16() u16 { + const low = regs.read(regs.ADCL); + const high = regs.read(regs.ADCH); + return (@as(u16, high) << 8) | low; +} + +pub inline fn clear_interrupt_flag() void { + regs.set_bits(regs.ADCSRA, regs.bit(regs.adc_bits.adif)); +} diff --git a/port/microchip/attiny/src/hals/attiny85/eeprom.zig b/port/microchip/attiny/src/hals/attiny85/eeprom.zig new file mode 100644 index 000000000..be5cba4eb --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/eeprom.zig @@ -0,0 +1,63 @@ +const regs = @import("registers.zig"); + +pub const size = 512; + +// Type-safe wrapper for raw EEPROM byte offsets. +pub const Address = enum(u16) { + _, +}; + +pub inline fn address(value: u16) Address { + return @enumFromInt(value); +} + +pub inline fn is_ready() bool { + return (regs.read(regs.EECR) & regs.bit(regs.eeprom_bits.eepe)) == 0; +} + +pub inline fn busy_wait() void { + while (!is_ready()) {} +} + +pub fn read_byte(addr: Address) u8 { + busy_wait(); + set_address(addr); + regs.set_bits(regs.EECR, regs.bit(regs.eeprom_bits.eere)); + return regs.read(regs.EEDR); +} + +pub fn write_byte(addr: Address, value: u8) void { + busy_wait(); + set_address(addr); + regs.write(regs.EEDR, value); + + // ATtiny25/45/85 datasheet section 5.3.3, page 22: EEMPE must be followed + // by EEPE within four cycles, matching avr-libc's byte-write primitive. + // https://ww1.microchip.com/downloads/en/devicedoc/atmel-2586-avr-8-bit-microcontroller-attiny25-attiny45-attiny85_datasheet.pdf + regs.set_bits(regs.EECR, regs.bit(regs.eeprom_bits.eempe)); + regs.set_bits(regs.EECR, regs.bit(regs.eeprom_bits.eepe)); +} + +pub fn update_byte(addr: Address, value: u8) void { + if (read_byte(addr) != value) write_byte(addr, value); +} + +pub fn read_slice(addr: Address, dest: []u8) void { + const base = @intFromEnum(addr); + for (dest, 0..) |*byte, i| { + byte.* = read_byte(address(base + @as(u16, @intCast(i)))); + } +} + +pub fn update_slice(addr: Address, src: []const u8) void { + const base = @intFromEnum(addr); + for (src, 0..) |byte, i| { + update_byte(address(base + @as(u16, @intCast(i))), byte); + } +} + +fn set_address(addr: Address) void { + const raw: u16 = @intFromEnum(addr); + regs.write(regs.EEARL, @truncate(raw)); + regs.write(regs.EEARH, @truncate(raw >> 8)); +} diff --git a/port/microchip/attiny/src/hals/attiny85/gpio.zig b/port/microchip/attiny/src/hals/attiny85/gpio.zig new file mode 100644 index 000000000..ec576776a --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/gpio.zig @@ -0,0 +1,68 @@ +const cpu = @import("microzig").cpu; +const regs = @import("registers.zig"); + +pub const Port = enum(u1) { + b = 0, + + pub const Regs = extern struct { + /// Port Input Pins. + PIN: u8, + /// Port Data Direction Register. + DDR: u8, + /// Port Data Register. + PORT: u8, + }; + + // ATtiny25/45/85 datasheet section 10.4, page 63: PINB/DDRB/PORTB are + // I/O-space registers, so constant pins can use SBI/CBI. + // https://ww1.microchip.com/downloads/en/devicedoc/atmel-2586-avr-8-bit-microcontroller-attiny25-attiny45-attiny85_datasheet.pdf + pub inline fn get_regs(port: Port) *volatile Regs { + return switch (port) { + .b => @ptrFromInt(regs.PINB), + }; + } +}; + +pub const Direction = enum { + input, + output, +}; + +pub fn pin(port: Port, num: u3) Pin { + return .{ .port = port, .num = num }; +} + +pub const Pin = packed struct(u4) { + port: Port, + num: u3, + + pub inline fn set_direction(p: Pin, dir: Direction) void { + const dir_addr: *volatile u8 = &p.port.get_regs().DDR; + switch (dir) { + .input => cpu.cbi(@intFromPtr(dir_addr), p.num), + .output => cpu.sbi(@intFromPtr(dir_addr), p.num), + } + } + + pub inline fn read(p: Pin) u1 { + const pin_addr: *volatile u8 = &p.port.get_regs().PIN; + return @truncate((pin_addr.* >> p.num) & 0x01); + } + + pub inline fn put(p: Pin, value: u1) void { + const port_addr: *volatile u8 = &p.port.get_regs().PORT; + switch (value) { + 1 => cpu.sbi(@intFromPtr(port_addr), p.num), + 0 => cpu.cbi(@intFromPtr(port_addr), p.num), + } + } + + pub inline fn toggle(p: Pin) void { + const pin_addr: *volatile u8 = &p.port.get_regs().PIN; + cpu.sbi(@intFromPtr(pin_addr), p.num); + } + + pub inline fn set_pullup(p: Pin, enabled: bool) void { + p.put(@intFromBool(enabled)); + } +}; diff --git a/port/microchip/attiny/src/hals/attiny85/pcint.zig b/port/microchip/attiny/src/hals/attiny85/pcint.zig new file mode 100644 index 000000000..94260b798 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/pcint.zig @@ -0,0 +1,30 @@ +const regs = @import("registers.zig"); + +pub const PinChange = enum(u3) { + pcint0 = 0, + pcint1 = 1, + pcint2 = 2, + pcint3 = 3, + pcint4 = 4, + pcint5 = 5, +}; + +pub inline fn enable_pin(pin: PinChange) void { + regs.set_bits(regs.PCMSK, regs.bit(@intFromEnum(pin))); +} + +pub inline fn disable_pin(pin: PinChange) void { + regs.clear_bits(regs.PCMSK, regs.bit(@intFromEnum(pin))); +} + +pub inline fn enable() void { + regs.set_bits(regs.GIMSK, 1 << 5); +} + +pub inline fn disable() void { + regs.clear_bits(regs.GIMSK, 1 << 5); +} + +pub inline fn clear_flag() void { + regs.set_bits(regs.GIFR, 1 << 5); +} diff --git a/port/microchip/attiny/src/hals/attiny85/progmem.zig b/port/microchip/attiny/src/hals/attiny85/progmem.zig new file mode 100644 index 000000000..0e0ba29fb --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/progmem.zig @@ -0,0 +1,21 @@ +pub fn Table(comptime T: type, comptime len: usize, comptime values: [len]T) type { + return struct { + pub const data linksection(".progmem.data") = values; + + pub inline fn get(index: usize) T { + return read(T, &data[index]); + } + }; +} + +pub inline fn read(comptime T: type, ptr: *const T) T { + return ptr.*; +} + +pub inline fn read_byte(ptr: *const u8) u8 { + return read(u8, ptr); +} + +pub inline fn read_word(ptr: *const u16) u16 { + return read(u16, ptr); +} diff --git a/port/microchip/attiny/src/hals/attiny85/registers.zig b/port/microchip/attiny/src/hals/attiny85/registers.zig new file mode 100644 index 000000000..95910326f --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/registers.zig @@ -0,0 +1,92 @@ +pub inline fn io(comptime address: u6) *volatile u8 { + return @ptrFromInt(address); +} + +pub inline fn read(comptime address: u6) u8 { + return io(address).*; +} + +pub inline fn write(comptime address: u6, value: u8) void { + io(address).* = value; +} + +pub inline fn set_bits(comptime address: u6, mask: u8) void { + io(address).* |= mask; +} + +pub inline fn clear_bits(comptime address: u6, mask: u8) void { + io(address).* &= ~mask; +} + +pub inline fn bit(comptime n: u3) u8 { + return @as(u8, 1) << n; +} + +pub const ADCSRB = 0x03; +pub const ADCL = 0x04; +pub const ADCH = 0x05; +pub const ADCSRA = 0x06; +pub const ADMUX = 0x07; +pub const DIDR0 = 0x14; +pub const PCMSK = 0x15; +pub const PINB = 0x16; +pub const DDRB = 0x17; +pub const PORTB = 0x18; +pub const EECR = 0x1C; +pub const EEDR = 0x1D; +pub const EEARL = 0x1E; +pub const EEARH = 0x1F; +pub const WDTCR = 0x21; +pub const CLKPR = 0x26; +pub const OCR0B = 0x28; +pub const OCR0A = 0x29; +pub const TCCR0A = 0x2A; +pub const OCR1B = 0x2B; +pub const GTCCR = 0x2C; +pub const OCR1C = 0x2D; +pub const OCR1A = 0x2E; +pub const TCNT1 = 0x2F; +pub const TCCR1 = 0x30; +pub const TCNT0 = 0x32; +pub const TCCR0B = 0x33; +pub const MCUSR = 0x34; +pub const MCUCR = 0x35; +pub const TIFR = 0x38; +pub const TIMSK = 0x39; +pub const GIFR = 0x3A; +pub const GIMSK = 0x3B; + +pub const adc_bits = struct { + pub const aden = 7; + pub const adsc = 6; + pub const adate = 5; + pub const adif = 4; + pub const adie = 3; +}; + +pub const eeprom_bits = struct { + pub const eepm1 = 5; + pub const eepm0 = 4; + pub const eerie = 3; + pub const eempe = 2; + pub const eepe = 1; + pub const eere = 0; +}; + +pub const watchdog_bits = struct { + pub const wdie = 6; + pub const wdp3 = 5; + pub const wdce = 4; + pub const wde = 3; + pub const wdp2 = 2; + pub const wdp1 = 1; + pub const wdp0 = 0; +}; + +pub const sleep_bits = struct { + pub const bods = 7; + pub const bodse = 2; + pub const se = 5; + pub const sm1 = 4; + pub const sm0 = 3; +}; diff --git a/port/microchip/attiny/src/hals/attiny85/sleep.zig b/port/microchip/attiny/src/hals/attiny85/sleep.zig new file mode 100644 index 000000000..553d1c776 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/sleep.zig @@ -0,0 +1,40 @@ +const regs = @import("registers.zig"); + +pub const Mode = enum(u2) { + idle = 0b00, + adc_noise_reduction = 0b01, + power_down = 0b10, +}; + +pub inline fn set_mode(mode: Mode) void { + const mask = regs.bit(regs.sleep_bits.sm1) | regs.bit(regs.sleep_bits.sm0); + regs.write(regs.MCUCR, (regs.read(regs.MCUCR) & ~mask) | + (@as(u8, @intFromEnum(mode)) << regs.sleep_bits.sm0)); +} + +pub inline fn enable() void { + regs.set_bits(regs.MCUCR, regs.bit(regs.sleep_bits.se)); +} + +pub inline fn disable() void { + regs.clear_bits(regs.MCUCR, regs.bit(regs.sleep_bits.se)); +} + +pub inline fn cpu() void { + asm volatile ("sleep" ::: .{ .memory = true }); +} + +pub inline fn enter(mode: Mode) void { + set_mode(mode); + enable(); + cpu(); + disable(); +} + +pub inline fn bod_disable() void { + // ATtiny25/45/85 datasheet, section 7.2: BODS must be set and BODSE + // cleared in a timed sequence immediately before SLEEP. + const mask = regs.bit(regs.sleep_bits.bods) | regs.bit(regs.sleep_bits.bodse); + regs.set_bits(regs.MCUCR, mask); + regs.write(regs.MCUCR, regs.read(regs.MCUCR) & ~regs.bit(regs.sleep_bits.bodse)); +} diff --git a/port/microchip/attiny/src/hals/attiny85/timer0.zig b/port/microchip/attiny/src/hals/attiny85/timer0.zig new file mode 100644 index 000000000..25e3f9960 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/timer0.zig @@ -0,0 +1,60 @@ +const regs = @import("registers.zig"); + +pub const CompareOutput = enum(u2) { + disconnected = 0b00, + toggle = 0b01, + clear = 0b10, + set = 0b11, +}; + +pub const Waveform = enum(u3) { + normal = 0b000, + phase_correct_pwm_top_0xff = 0b001, + ctc = 0b010, + fast_pwm_top_0xff = 0b011, + phase_correct_pwm_top_ocra = 0b101, + fast_pwm_top_ocra = 0b111, +}; + +pub const Prescaler = enum(u3) { + stopped = 0b000, + clk_1 = 0b001, + clk_8 = 0b010, + clk_64 = 0b011, + clk_256 = 0b100, + clk_1024 = 0b101, + external_falling = 0b110, + external_rising = 0b111, +}; + +pub const Config = struct { + waveform: Waveform = .normal, + compare_a: CompareOutput = .disconnected, + compare_b: CompareOutput = .disconnected, + prescaler: Prescaler = .stopped, +}; + +pub fn apply(config: Config) void { + const wgm: u3 = @intFromEnum(config.waveform); + regs.write(regs.TCCR0A, (@as(u8, @intFromEnum(config.compare_a)) << 6) | + (@as(u8, @intFromEnum(config.compare_b)) << 4) | + (@as(u8, wgm) & 0b011)); + regs.write(regs.TCCR0B, ((@as(u8, wgm) & 0b100) << 1) | + @as(u8, @intFromEnum(config.prescaler))); +} + +pub inline fn set_compare_a(value: u8) void { + regs.write(regs.OCR0A, value); +} + +pub inline fn set_compare_b(value: u8) void { + regs.write(regs.OCR0B, value); +} + +pub inline fn counter() u8 { + return regs.read(regs.TCNT0); +} + +pub inline fn set_counter(value: u8) void { + regs.write(regs.TCNT0, value); +} diff --git a/port/microchip/attiny/src/hals/attiny85/timer1.zig b/port/microchip/attiny/src/hals/attiny85/timer1.zig new file mode 100644 index 000000000..5343d0a8d --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/timer1.zig @@ -0,0 +1,88 @@ +const regs = @import("registers.zig"); + +pub const Prescaler = enum(u4) { + stopped = 0b0000, + pck_1 = 0b0001, + pck_2 = 0b0010, + pck_4 = 0b0011, + pck_8 = 0b0100, + pck_16 = 0b0101, + pck_32 = 0b0110, + pck_64 = 0b0111, + pck_128 = 0b1000, + pck_256 = 0b1001, + pck_512 = 0b1010, + pck_1024 = 0b1011, + pck_2048 = 0b1100, + pck_4096 = 0b1101, + pck_8192 = 0b1110, + pck_16384 = 0b1111, +}; + +pub const CompareOutput = enum(u2) { + disconnected = 0b00, + toggle = 0b01, + clear = 0b10, + set = 0b11, +}; + +pub const PwmOutput = enum { + a, + b, +}; + +pub const Interrupt = enum(u8) { + overflow = 1 << 2, + compare_a = 1 << 6, + compare_b = 1 << 5, +}; + +pub const FastPwmConfig = struct { + output: PwmOutput, + prescaler: Prescaler = .pck_1, + compare: CompareOutput = .clear, + top: u8 = 255, +}; + +pub fn configure_fast_pwm(config: FastPwmConfig) void { + // ATtiny25/45/85 datasheet, sections 12.2 and 12.3: OCR1C is TOP for + // Timer/Counter1 PWM and Timer1 register writes are synchronized. + regs.write(regs.OCR1C, config.top); + + switch (config.output) { + .a => { + regs.write(regs.TCCR1, 0x40 | + (@as(u8, @intFromEnum(config.compare)) << 4) | + @as(u8, @intFromEnum(config.prescaler))); + regs.clear_bits(regs.GTCCR, 0b0111_0000); + }, + .b => { + regs.write(regs.TCCR1, @as(u8, @intFromEnum(config.prescaler))); + regs.write(regs.GTCCR, 0x40 | (@as(u8, @intFromEnum(config.compare)) << 4)); + }, + } +} + +pub inline fn set_compare_a(value: u8) void { + regs.write(regs.OCR1A, value); +} + +pub inline fn set_compare_b(value: u8) void { + regs.write(regs.OCR1B, value); +} + +pub inline fn set_top(value: u8) void { + regs.write(regs.OCR1C, value); +} + +pub inline fn counter() u8 { + return regs.read(regs.TCNT1); +} + +pub inline fn enable_interrupt(interrupt: Interrupt) void { + regs.set_bits(regs.TIMSK, @intFromEnum(interrupt)); +} + +pub inline fn disable_interrupt(interrupt: Interrupt) void { + regs.clear_bits(regs.TIMSK, @intFromEnum(interrupt)); +} diff --git a/port/microchip/attiny/src/hals/attiny85/watchdog.zig b/port/microchip/attiny/src/hals/attiny85/watchdog.zig new file mode 100644 index 000000000..3b7005ca7 --- /dev/null +++ b/port/microchip/attiny/src/hals/attiny85/watchdog.zig @@ -0,0 +1,64 @@ +const microzig = @import("microzig"); +const regs = @import("registers.zig"); + +pub const Timeout = enum(u4) { + ms16 = 0b0000, + ms32 = 0b0001, + ms64 = 0b0010, + ms125 = 0b0011, + ms250 = 0b0100, + ms500 = 0b0101, + s1 = 0b0110, + s2 = 0b0111, + s4 = 0b1000, + s8 = 0b1001, +}; + +pub const Mode = enum { + interrupt, + reset, + interrupt_then_reset, +}; + +pub inline fn reset() void { + asm volatile ("wdr" ::: .{ .memory = true }); +} + +pub fn configure(mode: Mode, timeout: Timeout) void { + // ATtiny25/45/85 datasheet section 8.5.2, page 43: WDTCR writes use the + // WDCE/WDE timed sequence. This mirrors avr-libc's protected update. + // https://ww1.microchip.com/downloads/en/devicedoc/atmel-2586-avr-8-bit-microcontroller-attiny25-attiny45-attiny85_datasheet.pdf + microzig.interrupt.disable_interrupts(); + reset(); + regs.set_bits(regs.WDTCR, regs.bit(regs.watchdog_bits.wdce) | regs.bit(regs.watchdog_bits.wde)); + regs.write(regs.WDTCR, control_value(mode, timeout)); + microzig.interrupt.enable_interrupts(); +} + +pub fn stop() void { + microzig.interrupt.disable_interrupts(); + reset(); + regs.clear_bits(regs.MCUSR, 1 << 3); + regs.set_bits(regs.WDTCR, regs.bit(regs.watchdog_bits.wdce) | regs.bit(regs.watchdog_bits.wde)); + regs.write(regs.WDTCR, 0); + microzig.interrupt.enable_interrupts(); +} + +pub fn force_reset(timeout: Timeout) noreturn { + microzig.interrupt.disable_interrupts(); + regs.write(regs.WDTCR, control_value(.reset, timeout)); + microzig.interrupt.enable_interrupts(); + reset(); + while (true) asm volatile ("" ::: .{ .memory = true }); +} + +fn control_value(mode: Mode, timeout: Timeout) u8 { + const raw: u4 = @intFromEnum(timeout); + const prescaler = (@as(u8, raw & 0b0111)) | + ((@as(u8, raw >> 3) & 0x1) << regs.watchdog_bits.wdp3); + return prescaler | switch (mode) { + .interrupt => regs.bit(regs.watchdog_bits.wdie), + .reset => regs.bit(regs.watchdog_bits.wde), + .interrupt_then_reset => regs.bit(regs.watchdog_bits.wdie) | regs.bit(regs.watchdog_bits.wde), + }; +}