diff --git a/riscv-zig-blink/.gitignore b/riscv-zig-blink/.gitignore new file mode 100644 index 00000000..e1f72cd2 --- /dev/null +++ b/riscv-zig-blink/.gitignore @@ -0,0 +1,3 @@ +zig-cache/ +riscv-zig-blink +riscv-zig-blink.bin diff --git a/riscv-zig-blink/README.md b/riscv-zig-blink/README.md new file mode 100644 index 00000000..b9ebe068 --- /dev/null +++ b/riscv-zig-blink/README.md @@ -0,0 +1,14 @@ +# riscv-zig-blink + +Written against zig 0.5.0+518dbd30c + +You can obtain the zig compiler via https://ziglang.org/download/ +e.g. a linux user might run: +``` +curl -L https://ziglang.org/builds/zig-linux-x86_64-0.5.0+518dbd30c.tar.xz | tar -xJf - +alias zig=./zig-linux-x86_64-0.5.0+518dbd30c/zig +``` + +Run `zig build --help` from this directory for usage and options. + +`zig build run -Drelease` will compile the demo and and run it on your connected FOMU. diff --git a/riscv-zig-blink/build.zig b/riscv-zig-blink/build.zig new file mode 100644 index 00000000..f7a1df39 --- /dev/null +++ b/riscv-zig-blink/build.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const Builder = std.build.Builder; + +pub fn build(b: *Builder) void { + b.setPreferredReleaseMode(.ReleaseSmall); + const mode = b.standardReleaseOptions(); + + const elf = b.addExecutable("riscv-zig-blink", "src/main.zig"); + elf.setTheTarget(.{ + .Cross = .{ + .arch = .riscv32, + .os = .freestanding, + .abi = .none, + .cpu_features = .{ + .cpu = &std.Target.riscv.cpu.generic_rv32, + .features = std.Target.Cpu.Feature.Set.empty, + }, + }, + }); + elf.setLinkerScriptPath("ld/linker.ld"); + elf.setBuildMode(mode); + // The ELF file contains debug symbols and can be passed to gdb for remote debugging + if (b.option(bool, "emit-elf", "Should an ELF file be emitted in the current directory?") orelse false) { + elf.setOutputDir("."); + } + + const binary = b.addSystemCommand(&[_][]const u8{ + b.option([]const u8, "objcopy", "objcopy executable to use (defaults to riscv64-unknown-elf-objcopy)") orelse "riscv64-unknown-elf-objcopy", + "-I", + "elf32-littleriscv", + "-O", + "binary", + }); + binary.addArtifactArg(elf); + binary.addArg("riscv-zig-blink.bin"); + b.default_step.dependOn(&binary.step); + + const run_cmd = b.addSystemCommand(&[_][]const u8{ + "dfu-util", + "-D", + "riscv-zig-blink.bin", + }); + run_cmd.step.dependOn(&binary.step); + const run_step = b.step("run", "Upload and run the app on your FOMU"); + run_step.dependOn(&run_cmd.step); +} diff --git a/riscv-zig-blink/ld/linker.ld b/riscv-zig-blink/ld/linker.ld new file mode 100644 index 00000000..46e81a84 --- /dev/null +++ b/riscv-zig-blink/ld/linker.ld @@ -0,0 +1,60 @@ +ENTRY(_start) + +__DYNAMIC = 0; + +MEMORY { + sram : ORIGIN = 0x10000000, LENGTH = 0x00020000 + rom : ORIGIN = 0x20040000, LENGTH = 0x100000 /* 1 MBit */ +} + +SECTIONS +{ + .text : + { + _ftext = .; + *(.text.start) + *(.text .stub .text.* .gnu.linkonce.t.*) + _etext = .; + } > rom + + .rodata : + { + . = ALIGN(4); + _frodata = .; + *(.rodata .rodata.* .gnu.linkonce.r.*) + *(.rodata1) + *(.srodata) + . = ALIGN(4); + _erodata = .; + } > rom + + .data : AT (ADDR(.rodata) + SIZEOF (.rodata)) + { + . = ALIGN(4); + _fdata = .; + *(.data .data.* .gnu.linkonce.d.*) + *(.data1) + *(.ramtext .ramtext.*) + _gp = ALIGN(16); + *(.sdata .sdata.* .gnu.linkonce.s.* .sdata2 .sdata2.*) + _edata = ALIGN(16); /* Make sure _edata is >= _gp. */ + } > sram + + .bss : + { + . = ALIGN(4); + _fbss = .; + *(.dynsbss) + *(.sbss .sbss.* .gnu.linkonce.sb.*) + *(.scommon) + *(.dynbss) + *(.bss .bss.* .gnu.linkonce.b.*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } > sram + + _end = .; +} + +PROVIDE(_fstack = ORIGIN(sram) + LENGTH(sram) - 4); diff --git a/riscv-zig-blink/src/fomu.zig b/riscv-zig-blink/src/fomu.zig new file mode 100644 index 00000000..bd1af096 --- /dev/null +++ b/riscv-zig-blink/src/fomu.zig @@ -0,0 +1,75 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +pub const MESSIBLE = @import("./fomu/messible.zig").MESSIBLE; +pub const REBOOT = @import("./fomu/reboot.zig").REBOOT; +pub const RGB = @import("./fomu/rgb.zig").RGB; +pub const TIMER0 = @import("./fomu/timer0.zig").TIMER0; +pub const TOUCH = @import("./fomu/touch.zig").TOUCH; + +pub const SYSTEM_CLOCK_FREQUENCY = 12000000; + +pub const start = @import("./fomu/start.zig"); +// This forces start.zig file to be imported +comptime { + _ = start; +} + +/// Panic function that sets LED to red and flashing + prints the panic message over messible +pub fn panic(message: []const u8, stack_trace: ?*builtin.StackTrace) noreturn { + @setCold(true); + + // Put LED into non-raw flashing mode + RGB.CTRL.* = .{ + .EXE = true, + .CURREN = true, + .RGBLEDEN = true, + .RRAW = false, + .GRAW = false, + .BRAW = false, + }; + // Set colour to red + RGB.setColour(255, 0, 0); + // Enable the LED driver, and set 250 Hz mode. + RGB.setControlRegister(.{ + .enable = true, + .fr = .@"250", + .quick_stop = false, + .outskew = false, + .output_polarity = .active_high, + .pwm_mode = .linear, + .BRMSBEXT = 0, + }); + + messibleOutstream.print("PANIC: {}\r\n", .{message}) catch void; + + while (true) { + // TODO: Use @breakpoint() once https://reviews.llvm.org/D69390 is available + asm volatile ("ebreak"); + } +} + +const OutStream = std.io.OutStream(error{}); +pub const messibleOutstream = &OutStream{ + .writeFn = struct { + pub fn messibleWrite(self: *const OutStream, bytes: []const u8) error{}!void { + var left: []const u8 = bytes; + while (left.len > 0) { + const bytes_written = MESSIBLE.write(left); + left = left[bytes_written..]; + } + } + }.messibleWrite, +}; + +const InStream = std.io.InStream(error{}); +pub const messibleInstream = &InStream{ + .writeFn = struct { + pub fn messibleRead(self: *const InStream, buffer: []u8) error{}!usize { + while (true) { + const bytes_read = MESSIBLE.read(buffer); + if (bytes_read != 0) return bytes_read; + } + } + }.messibleRead, +}; diff --git a/riscv-zig-blink/src/fomu/messible.zig b/riscv-zig-blink/src/fomu/messible.zig new file mode 100644 index 00000000..d634d60e --- /dev/null +++ b/riscv-zig-blink/src/fomu/messible.zig @@ -0,0 +1,52 @@ +/// Messible: An Ansible for Messages +/// https://rm.fomu.im/messible.html +/// +/// An Ansible is a system for instant communication across vast distances, +/// from a small portable device to a huge terminal far away. A Messible is +/// a message- passing system from embedded devices to a host system. You can +/// use it to get very simple printf()-style support over a debug channel. +/// +/// The Messible assumes the host has a way to peek into the device’s memory +/// space. This is the case with the Wishbone bridge, which allows both the +/// device and the host to access the same memory. +/// +/// At its core, a Messible is a FIFO. As long as the STATUS.FULL bit is 0, +/// the device can write data into the Messible by writing into the IN. +/// However, if this value is 1, you need to decide if you want to wait for it +/// to empty (if the other side is just slow), or if you want to drop the +/// message. From the host side, you need to read STATUS.HAVE to see if there +/// is data in the FIFO. If there is, read OUT to get the most recent byte, +/// which automatically advances the READ pointer. +pub const MESSIBLE = struct { + const base = 0xe0008000; + + /// Write half of the FIFO to send data out the Messible. Writing to this register advances the write pointer automatically. + pub const IN = @intToPtr(*volatile u8, base + 0x0); + + /// Read half of the FIFO to receive data on the Messible. Reading from this register advances the read pointer automatically. + pub const OUT = @intToPtr(*volatile u8, base + 0x4); + + pub const STATUS = @intToPtr(*volatile packed struct { + /// if more data can fit into the IN FIFO. + FULL: bool, + + /// if data can be read from the OUT FIFO. + HAVE: bool, + }, base + 0x8); + + pub fn write(data: []const u8) usize { + for (data) |c, i| { + if (STATUS.*.FULL) return i; + IN.* = c; + } + return data.len; + } + + pub fn read(dst: []u8) usize { + for (dst) |*c, i| { + if (!STATUS.*.HAVE) return i; + c.* = OUT.*; + } + return dst.len; + } +}; diff --git a/riscv-zig-blink/src/fomu/reboot.zig b/riscv-zig-blink/src/fomu/reboot.zig new file mode 100644 index 00000000..944c2827 --- /dev/null +++ b/riscv-zig-blink/src/fomu/reboot.zig @@ -0,0 +1,22 @@ +/// https://rm.fomu.im/reboot.html +pub const REBOOT = struct { + const base = 0xe0006000; + + /// Provides support for rebooting the FPGA. + /// You can select which of the four images to reboot to. + pub const CTRL = @intToPtr(*volatile packed struct { + /// Which image to reboot to. SB_WARMBOOT supports four images that are configured at FPGA startup. + /// The bootloader is image 0, so set these bits to 0 to reboot back into the bootloader. + image: u2, + + /// A reboot key used to prevent accidental reboots when writing to random areas of memory. + /// To initiate a reboot, set this to 0b101011. + key: u6, + }, base + 0x0); + + /// This sets the reset vector for the VexRiscv. + /// This address will be used whenever the CPU is reset, for example + /// through a debug bridge. You should update this address whenever + /// you load a new program, to enable the debugger to run `mon reset` + pub const ADDR = @intToPtr(*volatile u32, base + 0x4); +}; diff --git a/riscv-zig-blink/src/fomu/rgb.zig b/riscv-zig-blink/src/fomu/rgb.zig new file mode 100644 index 00000000..edcab01a --- /dev/null +++ b/riscv-zig-blink/src/fomu/rgb.zig @@ -0,0 +1,140 @@ +/// https://rm.fomu.im/rgb.html +pub const RGB = struct { + const base = 0xe0006800; + + /// This is the value for the SB_LEDDA_IP.DAT register. + /// It is directly written into the SB_LEDDA_IP hardware block, so you + /// should refer to http://www.latticesemi.com/view_document?document_id=50668. + /// The contents of this register are written to the address specified + /// in ADDR immediately upon writing this register. + pub const DAT = @intToPtr(*volatile u8, base + 0x0); + + /// This register is directly connected to SB_LEDDA_IP.ADDR. + /// This register controls the address that is updated whenever DAT is written. + /// Writing to this register has no immediate effect – data isn’t written until the DAT register is written. + pub const ADDR = @intToPtr(*volatile u4, base + 0x4); + + pub const Register = enum { + PWRR = 1, + PWRG = 2, + PWRB = 3, + + BCRR = 5, + BCFR = 6, + + CR0 = 8, + BR = 9, + ONR = 10, + OFR = 11, + }; + + pub fn setRegister(reg: Register, value: u8) void { + ADDR.* = @enumToInt(reg); + DAT.* = value; + } + + const CR0 = packed struct { + BRMSBEXT: u2, + + pwm_mode: enum(u1) { + linear = 0, + + /// The Polynomial for the LFSR is X^(8) + X^(5) + X^3 + X + 1 + LFSR = 1, + }, + + quick_stop: bool, + + outskew: bool, + + /// PWM output polarity + output_polarity: enum(u1) { + active_high = 0, + active_low = 1, + }, + + /// Flick rate for PWM (in Hz) + fr: enum(u1) { + @"125" = 0, + @"250" = 1, + }, + + /// LED Driver enabled? + enable: bool, + }; + + pub fn setControlRegister(value: CR0) void { + setRegister(.CR0, @bitCast(u8, value)); + } + + pub const Breathe = packed struct { + /// Breathe rate is in 128 ms increments + rate: u4, + + _pad: u1 = 0, + + mode: enum(u1) { + fixed = 0, + modulate = 1, + }, + + pwm_range_extend: bool, + + enable: bool, + }; + + pub fn setBreatheRegister(reg: enum { + On, + Off, + }, value: Breathe) void { + setRegister(switch (reg) { + .On => .BCRR, + .Off => .BCFR, + }, @bitCast(u8, value)); + } + + /// Control logic for the RGB LED and LEDDA hardware PWM LED block. + pub const CTRL = @intToPtr(*volatile packed struct { + /// Enable the fading pattern? + /// Connected to `SB_LEDDA_IP.LEDDEXE`. + EXE: bool, + + /// Enable the current source? + /// Connected to `SB_RGBA_DRV.CURREN`. + CURREN: bool, + + /// Enable the RGB PWM control logic? + /// Connected to `SB_RGBA_DRV.RGBLEDEN`. + RGBLEDEN: bool, + + /// Enable raw control of the red LED via the RAW.R register. + RRAW: bool, + + /// Enable raw control of the green LED via the RAW.G register. + GRAW: bool, + + /// Enable raw control of the blue LED via the RAW.B register. + BRAW: bool, + }, base + 0x8); + + /// Normally the hardware SB_LEDDA_IP block controls the brightness of the + /// LED, creating a gentle fading pattern. + /// However, by setting the appropriate bit in CTRL, it is possible to + /// manually control the three individual LEDs. + pub const RAW = @intToPtr(*volatile packed struct { + /// Red + R: bool, + + /// Green + G: bool, + + /// Blue + B: bool, + }, base + 0xc); + + pub fn setColour(r: u8, g: u8, b: u8) void { + setRegister(.PWRR, r); + setRegister(.PWRG, g); + setRegister(.PWRB, b); + } +}; diff --git a/riscv-zig-blink/src/fomu/start.zig b/riscv-zig-blink/src/fomu/start.zig new file mode 100644 index 00000000..5e6e5a4b --- /dev/null +++ b/riscv-zig-blink/src/fomu/start.zig @@ -0,0 +1,63 @@ +const root = @import("root"); +const std = @import("std"); + +extern var _ftext: u8; +extern var _etext: u8; +extern var _frodata: u8 align(4); +extern var _erodata: u8 align(4); +extern var _fdata: u8 align(4); +extern var _gp: u8 align(16); +extern var _edata: u8 align(16); +extern var _fbss: u8 align(4); +extern var _ebss: u8 align(4); +extern var _end: u8; +extern var _fstack: u8; + +export fn _start() linksection(".text.start") callconv(.Naked) noreturn { + asm volatile ( + \\ j over_magic + \\ .word 0xb469075a // Magic value for config flags + \\ .word 0x00000020 // USB_NO_RESET flag so we can attach the debugger + \\over_magic: + ); + + // set the stack pointer to `&_fstack` + asm volatile ( + \\ la sp, _fstack + 4 + ); + + // zero out bss + asm ( + \\ la a0, %[fbss] + \\ la a1, %[ebss] + \\bss_loop: + \\ beq a0, a1, bss_done + \\ sw zero, 0(a0) + \\ add a0, a0,4 + \\ j bss_loop + \\bss_done: + : [ret] "=" (-> void) + : [fbss] "m" (&_fbss), + [ebss] "m" (&_ebss) + ); + + // copy data from data rom (which is after rodata) to data + asm ( + \\ la t0, %[erodata] + \\ la t1, %[fdata] + \\ la t2, %[edata] + \\data_loop: + \\ lw t3, 0(t0) + \\ sw t3, 0(t1) + \\ addi t0, t0, 4 + \\ addi t1, t1, 4 + \\ bltu t1, t2, data_loop + : [ret] "=" (-> void) + : [erodata] "m" (&_erodata), + [fdata] "m" (&_fdata), + [edata] "m" (&_edata) + ); + + // call user's main + root.main(); +} diff --git a/riscv-zig-blink/src/fomu/timer0.zig b/riscv-zig-blink/src/fomu/timer0.zig new file mode 100644 index 00000000..df448745 --- /dev/null +++ b/riscv-zig-blink/src/fomu/timer0.zig @@ -0,0 +1,109 @@ +/// Provides a generic Timer core. +/// +/// The Timer is implemented as a countdown timer that can be used in various modes: +/// - Polling: Returns current countdown value to software. +/// - One-Shot: Loads itself and stops when value reaches 0. +/// - Periodic: (Re-)Loads itself when value reaches 0. +/// +/// *en* register allows the user to enable/disable the Timer. When the Timer +/// is enabled, it is automatically loaded with the value of load register. +/// +/// When the Timer reaches 0, it is automatically reloaded with value of reload register. +/// +/// The user can latch the current countdown value by writing to update_value +/// register, it will update value register with current countdown value. +/// +/// To use the Timer in One-Shot mode, the user needs to: +/// - Disable the timer. +/// - Set the load register to the expected duration. +/// - (Re-)Enable the Timer. +/// +/// To use the Timer in Periodic mode, the user needs to: +/// - Disable the Timer. +/// - Set the load register to 0. +/// - Set the reload register to the expected period. +/// - Enable the Timer. +/// +/// For both modes, the CPU can be advertised by an IRQ that the +/// duration/period has elapsed. (The CPU can also do software polling with +/// update_value and value to know the elapsed duration) +pub const TIMER0 = struct { + const base = 0xe0002800; + + pub const LOAD3 = @intToPtr(*volatile u8, base + 0x0); + pub const LOAD2 = @intToPtr(*volatile u8, base + 0x4); + pub const LOAD1 = @intToPtr(*volatile u8, base + 0x8); + pub const LOAD0 = @intToPtr(*volatile u8, base + 0xc); + + /// Load value when Timer is (re-)enabled. + /// In One-Shot mode, the value written to this register specify the + /// Timer’s duration in clock cycles. + pub fn load(x: u32) void { + LOAD3.* = @truncate(u8, x >> 24); + LOAD2.* = @truncate(u8, x >> 16); + LOAD1.* = @truncate(u8, x >> 8); + LOAD0.* = @truncate(u8, x); + } + + pub const RELOAD3 = @intToPtr(*volatile u8, base + 0x10); + pub const RELOAD2 = @intToPtr(*volatile u8, base + 0x14); + pub const RELOAD1 = @intToPtr(*volatile u8, base + 0x18); + pub const RELOAD0 = @intToPtr(*volatile u8, base + 0x1c); + + /// Reload value when Timer reaches 0. + /// In Periodic mode, the value written to this register specify the + /// Timer’s period in clock cycles. + pub fn reload(x: u32) void { + RELOAD3.* = @truncate(u8, x >> 24); + RELOAD2.* = @truncate(u8, x >> 16); + RELOAD1.* = @truncate(u8, x >> 8); + RELOAD0.* = @truncate(u8, x); + } + + /// Enable of the Timer. + /// Set if to 1 to enable/start the Timer and 0 to disable the Timer + pub const EN = @intToPtr(*volatile bool, base + 0x20); + + pub fn start() void { + EN.* = true; + } + + pub fn stop() void { + EN.* = false; + } + + /// Update of the current countdown value. + /// A write to this register latches the current countdown value to value + /// register. + pub const UPDATE_VALUE = @intToPtr(*volatile bool, base + 0x24); + + pub const VALUE3 = @intToPtr(*volatile u8, base + 0x28); + pub const VALUE2 = @intToPtr(*volatile u8, base + 0x2c); + pub const VALUE1 = @intToPtr(*volatile u8, base + 0x30); + pub const VALUE0 = @intToPtr(*volatile u8, base + 0x34); + + pub fn latchedValue() u32 { + return (@as(u32, VALUE3.*) << 24) | + (@as(u32, VALUE2.*) << 16) | + (@as(u32, VALUE1.*) << 8) | + (@as(u32, VALUE0.*)); + } + + pub fn value() u32 { + UPDATE_VALUE.* = true; + return latchedValue(); + } + + /// This register contains the current raw level of the Event trigger. + /// Writes to this register have no effect. + pub const EV_STATUS = @intToPtr(*volatile bool, base + 0x38); + + /// When an Event occurs, the corresponding bit will be set in this + /// register. To clear the Event, set the corresponding bit in this + /// register. + pub const EV_PENDING = @intToPtr(*volatile bool, base + 0x3c); + + /// This register enables the corresponding Events. Write a 0 to this + /// register to disable individual events. + pub const EV_ENABLE = @intToPtr(*volatile bool, base + 0x40); +}; diff --git a/riscv-zig-blink/src/fomu/touch.zig b/riscv-zig-blink/src/fomu/touch.zig new file mode 100644 index 00000000..2bbcf91b --- /dev/null +++ b/riscv-zig-blink/src/fomu/touch.zig @@ -0,0 +1,13 @@ +/// https://rm.fomu.im/touch.html +pub const TOUCH = struct { + const base = 0xe0005800; + + /// Output values for pads 1-4 + pub const TOUCH_O = @intToPtr(*volatile u4, base + 0x0); + + /// Output enable control for pads 1-4 + pub const TOUCH_OE = @intToPtr(*volatile u4, base + 0x4); + + /// Input value for pads 1-4 + pub const TOUCH_1 = @intToPtr(*volatile u4, base + 0x8); +}; diff --git a/riscv-zig-blink/src/main.zig b/riscv-zig-blink/src/main.zig new file mode 100644 index 00000000..596758c7 --- /dev/null +++ b/riscv-zig-blink/src/main.zig @@ -0,0 +1,113 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const fomu = @import("./fomu.zig"); + +pub fn panic(message: []const u8, stack_trace: ?*builtin.StackTrace) noreturn { + @setCold(true); + fomu.panic(message, stack_trace); +} + +fn rgb_init() void { + // Turn on the RGB block and current enable, as well as enabling led control + fomu.RGB.CTRL.* = .{ + .EXE = true, + .CURREN = true, + .RGBLEDEN = true, + .RRAW = false, + .GRAW = false, + .BRAW = false, + }; + + // Enable the LED driver, and set 250 Hz mode. + // Also set quick stop, which we'll use to switch patterns quickly. + fomu.RGB.setControlRegister(.{ + .enable = true, + .fr = .@"250", + .quick_stop = true, + .outskew = false, + .output_polarity = .active_high, + .pwm_mode = .linear, + .BRMSBEXT = 0, + }); + + // Set clock register to 12 MHz / 64 kHz - 1 + fomu.RGB.setRegister(.BR, (fomu.SYSTEM_CLOCK_FREQUENCY / 64000) - 1); + + // Blink on/off time is in 32 ms increments + fomu.RGB.setRegister(.ONR, 1); // Amount of time to stay "on" + fomu.RGB.setRegister(.OFR, 0); // Amount of time to stay "off" + + fomu.RGB.setBreatheRegister(.On, .{ + .enable = true, + .pwm_range_extend = false, + .mode = .fixed, + .rate = 1, + }); + fomu.RGB.setBreatheRegister(.Off, .{ + .enable = true, + .pwm_range_extend = false, + .mode = .fixed, + .rate = 1, + }); +} + +const Colour = struct { + r: u8, + g: u8, + b: u8, +}; + +/// Input a value 0 to 255 to get a colour value. +/// The colours are a transition r - g - b - back to r. +fn colourWheel(wheelPos: u8) Colour { + var c: Colour = undefined; + var wp = 255 - wheelPos; + switch (wp) { + 0...84 => { + c = .{ + .r = 255 - wp * 3, + .g = 0, + .b = wp * 3, + }; + }, + 85...169 => { + wp -= 85; + c = .{ + .r = 0, + .g = wp * 3, + .b = 255 - wp * 3, + }; + }, + 170...255 => { + wp -= 170; + c = .{ + .r = wp * 3, + .g = 255 - wp * 3, + .b = 0, + }; + }, + } + return c; +} + +fn msleep(ms: usize) void { + fomu.TIMER0.stop(); + fomu.TIMER0.reload(0); + fomu.TIMER0.load(fomu.SYSTEM_CLOCK_FREQUENCY / 1000 * ms); + fomu.TIMER0.start(); + while (fomu.TIMER0.value() != 0) {} +} + +pub fn main() noreturn { + rgb_init(); + + var i: u8 = 0; + while (true) : (i +%= 1) { + const colour = colourWheel(i); + fomu.RGB.setColour(colour.r, colour.g, colour.b); + + msleep(80); + } + + unreachable; +}