From 6207849f0f77d8c63ae53715150cda638165e55b Mon Sep 17 00:00:00 2001 From: haydenridd Date: Wed, 18 Dec 2024 18:35:48 -0800 Subject: [PATCH] - Added support for RP2040's RTC peripheral (#317) - Added example showing how the RTC is used Co-authored-by: Hayden Riddiford --- examples/raspberrypi/rp2xxx/build.zig | 1 + .../rp2xxx/src/rp2040_only/rtc.zig | 102 +++++++ port/raspberrypi/rp2xxx/src/hal.zig | 4 + .../rp2xxx/src/hal/clocks/rp2040.zig | 7 +- port/raspberrypi/rp2xxx/src/hal/rtc.zig | 259 ++++++++++++++++++ 5 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 examples/raspberrypi/rp2xxx/src/rp2040_only/rtc.zig create mode 100644 port/raspberrypi/rp2xxx/src/hal/rtc.zig diff --git a/examples/raspberrypi/rp2xxx/build.zig b/examples/raspberrypi/rp2xxx/build.zig index d4adf7c9e..5f467d469 100644 --- a/examples/raspberrypi/rp2xxx/build.zig +++ b/examples/raspberrypi/rp2xxx/build.zig @@ -19,6 +19,7 @@ pub fn build(b: *std.Build) void { .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_i2c-bus-scan", .file = "src/rp2040_only/i2c_bus_scan.zig" }, .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_pwm", .file = "src/rp2040_only/pwm.zig" }, .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_random", .file = "src/rp2040_only/random.zig" }, + .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_rtc", .file = "src/rp2040_only/rtc.zig" }, .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_spi-host", .file = "src/rp2040_only/spi_host.zig" }, .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_uart-echo", .file = "src/rp2040_only/uart_echo.zig" }, .{ .target = mb.ports.rp2xxx.boards.raspberrypi.pico, .name = "pico_uart-log", .file = "src/rp2040_only/uart_log.zig" }, diff --git a/examples/raspberrypi/rp2xxx/src/rp2040_only/rtc.zig b/examples/raspberrypi/rp2xxx/src/rp2040_only/rtc.zig new file mode 100644 index 000000000..13049f900 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/rp2040_only/rtc.zig @@ -0,0 +1,102 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const rp2xxx = microzig.hal; +const time = rp2xxx.time; + +const pin_config = rp2xxx.pins.GlobalConfiguration{ + .GPIO25 = .{ + .name = "led", + .direction = .out, + }, +}; + +pub const microzig_options = .{ + .interrupts = .{ + .RTC_IRQ = .{ .C = &rtc_isr }, + }, +}; + +/// Enables/disables interrupts to prevent data-races with variables +/// shared between ISRs and normal code +/// +/// TODO: microzig/core/src/interrupt.zig might provide a more general implementation +/// for this in the future. +const CriticalSection = struct { + isr_reg_value: usize = 0, + + pub fn enter(self: *CriticalSection) void { + var val: usize = undefined; + asm volatile ( + \\mrs %[val], primask + \\movs r1, #1 + \\msr primask, r1 + : [val] "=r" (val), + : + : "r1", "cc" + ); + self.isr_reg_value = val; + } + + pub fn exit(self: *CriticalSection) void { + const val = self.isr_reg_value; + asm volatile ("msr primask, %[val]" + : + : [val] "r" (val), + ); + } +}; +var critical_section: CriticalSection = .{}; + +var _fast_blink: bool = false; +/// Access underlying _sleep_time via volatile * to prevent reads from being optimized away +var fast_blink_vp: *volatile bool = &_fast_blink; + +fn rtc_isr() callconv(.C) void { + + // Important to disable + re-enable the RTC alarm so that the interrupt doesn't + // continue firing for the entire time the alarm condition is met + // (For example, it would fire the entire second that it matches the desired "second" count) + rp2xxx.rtc.alarm.disable(); + rp2xxx.rtc.alarm.enable(); + + // Every 1 minute, alternate between fast and slow blinking + critical_section.enter(); + fast_blink_vp.* = !fast_blink_vp.*; + critical_section.exit(); +} + +pub fn main() !void { + const pins = pin_config.apply(); + + // Configure initial datetime for RTC + rp2xxx.rtc.set_datetime(.{ + .year = 2024, + .month = 12, + .day = 25, + .day_of_week = .Monday, + .hour = 0, + .minute = 0, + .second = 0, + }); + rp2xxx.rtc.apply(rp2xxx.clock_config); + + // Configure an alarm that goes off once a minute (whenever second == 0) + rp2xxx.rtc.alarm.configure(.{ .second = 0 }); + + // Enable peripheral level alarm + rp2xxx.rtc.irq.enable(); + + // Enable top level NVIC alarm + rp2xxx.irq.enable(.RTC_IRQ); + + while (true) { + + // Disable interrupts during volatile read of fast_blink to prevent data races + critical_section.enter(); + const fast_blink = fast_blink_vp.*; + critical_section.exit(); + + pins.led.toggle(); + time.sleep_ms(if (fast_blink) 500 else 1000); + } +} diff --git a/port/raspberrypi/rp2xxx/src/hal.zig b/port/raspberrypi/rp2xxx/src/hal.zig index 2f45aaab5..3d86844b7 100644 --- a/port/raspberrypi/rp2xxx/src/hal.zig +++ b/port/raspberrypi/rp2xxx/src/hal.zig @@ -15,6 +15,10 @@ pub const pwm = @import("hal/pwm.zig"); pub const rand = @import("hal/random.zig"); pub const resets = @import("hal/resets.zig"); pub const rom = @import("hal/rom.zig"); +pub const rtc = switch (compatibility.cpu) { + .RP2040 => @import("hal/rtc.zig"), + .RP2350 => {}, // No explicit "RTC" module on RP2350 +}; pub const spi = @import("hal/spi.zig"); pub const i2c = @import("hal/i2c.zig"); pub const time = @import("hal/time.zig"); diff --git a/port/raspberrypi/rp2xxx/src/hal/clocks/rp2040.zig b/port/raspberrypi/rp2xxx/src/hal/clocks/rp2040.zig index 9e8d7c453..2c62ea13b 100644 --- a/port/raspberrypi/rp2xxx/src/hal/clocks/rp2040.zig +++ b/port/raspberrypi/rp2xxx/src/hal/clocks/rp2040.zig @@ -300,7 +300,7 @@ pub const config = struct { /// - TODO: Could vary the PLL frequency based on sys_freq input to open up a broader range of SYS frequencies /// - CLK_USB sourced from PLL_USB @ 48 MHz /// - CLK_ADC sourced from PLL_USB @ 48 MHz - /// - CLK_RTC sourced from CLK_SYS + /// - CLK_RTC sourced from PLL_USB @ 48 MHz and divided down to ~65484 Hz /// - CLK_PERI sourced from CLK_SYS pub fn system(sys_freq: u32, ref_freq: u32) Global { var cfg: Global = .{}; @@ -359,14 +359,15 @@ pub const config = struct { .integer_divisor = 1, }; - // RTC gets connected to PLL_USB + // RTC gets connected to PLL_USB, and divided down to ~65484 Hz in order + // to be a suitable clock for 1 second reference. cfg.rtc = .{ .input = .{ .source = .pll_usb, .freq = 48_000_000, }, - .integer_divisor = 1, + .integer_divisor = 733, }; // CLK_PERI is sourced from CLK_SYS, divisor hard coded to 1 on RP2040 diff --git a/port/raspberrypi/rp2xxx/src/hal/rtc.zig b/port/raspberrypi/rp2xxx/src/hal/rtc.zig new file mode 100644 index 000000000..dc1ea732a --- /dev/null +++ b/port/raspberrypi/rp2xxx/src/hal/rtc.zig @@ -0,0 +1,259 @@ +const std = @import("std"); +const microzig = @import("microzig"); + +const RTC = microzig.chip.peripherals.RTC; +const clocks = @import("clocks.zig"); +const hw = @import("hw.zig"); + +pub const DateTime = struct { + year: u12, + month: u4, + day: u5, + day_of_week: enum(u3) { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, + }, + hour: u5, + minute: u6, + second: u6, +}; + +/// Configure and enable the RTC given a clock configuration that specifies what CLK_RTC frequency is +pub fn apply(comptime clock_config: clocks.config.Global) void { + disable(); + + const MAX_RTC_FREQ = 65536; + const rtc_freq = comptime clock_config.rtc.?.frequency(); + comptime std.debug.assert((rtc_freq <= MAX_RTC_FREQ) and (rtc_freq >= 1)); + RTC.CLKDIV_M1.write(.{ .CLKDIV_M1 = @as(u16, @truncate(rtc_freq - 1)), .padding = 0 }); + + enable(); +} + +pub inline fn disable() void { + + // Clear alias to atomically disable RTC + hw.clear_alias(&RTC.CTRL).write(.{ + .RTC_ENABLE = 1, + .RTC_ACTIVE = 0, + .reserved4 = 0, + .LOAD = 0, + .reserved8 = 0, + .FORCE_NOTLEAPYEAR = 0, + .padding = 0, + }); + // Poll until enable bit takes effect, important for the purpose of ensuring state while changing settings + while (RTC.CTRL.read().RTC_ACTIVE != 0) {} +} + +pub inline fn enable() void { + // Set alias to atomically enable RTC + hw.set_alias(&RTC.CTRL).write(.{ + .RTC_ENABLE = 1, + .RTC_ACTIVE = 0, + .reserved4 = 0, + .LOAD = 0, + .reserved8 = 0, + .FORCE_NOTLEAPYEAR = 0, + .padding = 0, + }); + // Poll until enable bit takes effect, important for the purpose of ensuring state while changing settings + while (RTC.CTRL.read().RTC_ACTIVE != 1) {} +} + +pub inline fn set_datetime(datetime: DateTime) void { + RTC.SETUP_0.modify(.{ + .YEAR = datetime.year, + .MONTH = datetime.month, + .DAY = datetime.day, + }); + RTC.SETUP_1.modify(.{ + .DOTW = @intFromEnum(datetime.day_of_week), + .HOUR = datetime.hour, + .MIN = datetime.minute, + .SEC = datetime.second, + }); + // Set alias to atomically load RTC values + hw.set_alias(&RTC.CTRL).write(.{ + .RTC_ENABLE = 0, + .RTC_ACTIVE = 0, + .reserved4 = 0, + .LOAD = 1, + .reserved8 = 0, + .FORCE_NOTLEAPYEAR = 0, + .padding = 0, + }); +} + +pub inline fn get_datetime() DateTime { + // Order is important for these as reading SETUP0 latches + // SETUP1 to prevent changes happening in between register reads. + // + // TODO: I believe the compiler is forbidden from re-ordering these two + // volatile accesses, however this should be confirmed! + const RTC0 = RTC.RTC_0.read(); + const RTC1 = RTC.RTC_1.read(); + + return .{ + .year = RTC1.YEAR, + .month = RTC1.MONTH, + .day = RTC1.DAY, + .day_of_week = @enumFromInt(RTC0.DOTW), + .hour = RTC0.HOUR, + .minute = RTC0.MIN, + .second = RTC0.SEC, + }; +} + +/// For configuring time alarms that will generate interrupts +pub const alarm = struct { + const Config = struct { + year: ?u12 = null, + month: ?u4 = null, + day: ?u5 = null, + day_of_week: ?enum(u3) { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, + } = null, + hour: ?u5 = null, + minute: ?u6 = null, + second: ?u6 = null, + }; + + /// Configure and enable an alarm to fire at values specified by config + /// + /// If a single alarm is desired at a specific datetime, all fields to make a datetime unique should be populated. + /// For example: + /// - The config .{.year = 2024, .month = 12, .day = 25, .hour = 8, .minute = 30, .second = 0} + /// Will fire an alarm at 12/25/2024 08:30:00 + /// + /// For an alarm that fires on a set interval, only some fields should be specified. For example: + /// - An alarm that fires once a minute at second "0" would have the config .{.second = 0} + /// - An alarm that fires once an hour at minute "20" would have the config .{.minute = 20} + /// - An alarm that fires 24 times a day at minute "20" but only on day "10" of the month would have the config .{.day = 10, .minute = 20} + /// and so on... + /// + /// NOTE: To get an interrupt from an alarm, interrupts must also be enabled via irq.enable() + /// + pub fn configure(config: Config) void { + + // Keep alarm disabled while modifying settings + alarm.disable(); + var irq_setup0 = RTC.IRQ_SETUP_0.read(); + var irq_setup1 = RTC.IRQ_SETUP_1.read(); + irq_setup0.MATCH_ENA = 0; + + if (config.year) |year| { + irq_setup0.YEAR = year; + irq_setup0.YEAR_ENA = 1; + } + if (config.month) |month| { + irq_setup0.MONTH = month; + irq_setup0.MONTH_ENA = 1; + } + if (config.day) |day| { + irq_setup0.DAY = day; + irq_setup0.DAY_ENA = 1; + } + + if (config.day_of_week) |day_of_week| { + irq_setup1.DOTW = @intFromEnum(day_of_week); + irq_setup1.DOTW_ENA = 1; + } + if (config.hour) |hour| { + irq_setup1.HOUR = hour; + irq_setup1.HOUR_ENA = 1; + } + if (config.minute) |minute| { + irq_setup1.MIN = minute; + irq_setup1.MIN_ENA = 1; + } + if (config.second) |second| { + irq_setup1.SEC = second; + irq_setup1.SEC_ENA = 1; + } + + RTC.IRQ_SETUP_0.write(irq_setup0); + RTC.IRQ_SETUP_1.write(irq_setup1); + + // Re-enable alarm now that settings have been applied + alarm.enable(); + } + + /// Enable an alarm + /// + /// NOTE: To get an interrupt from an alarm, interrupts must also be enabled via irq.enable() + pub inline fn enable() void { + // Set alias to atomically enable Alarm + hw.set_alias(&RTC.IRQ_SETUP_0).write(.{ + .DAY = 0, + .reserved8 = 0, + .MONTH = 0, + .YEAR = 0, + .DAY_ENA = 0, + .MONTH_ENA = 0, + .YEAR_ENA = 0, + .reserved28 = 0, + .MATCH_ENA = 1, + .MATCH_ACTIVE = 0, + .padding = 0, + }); + // Poll until enable bit takes effect, important for the purpose of ensuring state while changing settings + while (RTC.IRQ_SETUP_0.read().MATCH_ACTIVE != 1) {} + } + + /// Disable an alarm + /// + /// NOTE: This is useful for interval alarms. In order to prevent + /// interrupts from being generated the entire period the interval + /// is active, in the ISR you should call rtc.alarm.disable() then rtc.alarm.enable() + pub inline fn disable() void { + // Clear alias to atomically disable Alarm + hw.clear_alias(&RTC.IRQ_SETUP_0).write(.{ + .DAY = 0, + .reserved8 = 0, + .MONTH = 0, + .YEAR = 0, + .DAY_ENA = 0, + .MONTH_ENA = 0, + .YEAR_ENA = 0, + .reserved28 = 0, + .MATCH_ENA = 1, + .MATCH_ACTIVE = 0, + .padding = 0, + }); + // Poll until enable bit takes effect, important for the purpose of ensuring state while changing settings + while (RTC.IRQ_SETUP_0.read().MATCH_ACTIVE != 0) {} + } +}; + +/// Control whether or not this peripheral's IRQ is enabled +pub const irq = struct { + /// Disable interrupts being generated every time an alarm occurs + pub inline fn disable() void { + // Clear alias to atomically disable IRQ + hw.clear_alias(&RTC.INTE).write(.{ + .RTC = 1, + .padding = 0, + }); + } + + /// Enable interrupts being generated every time an alarm occurs + pub inline fn enable() void { + // Set alias to atomically enable IRQ + hw.set_alias(&RTC.INTE).write(.{ + .RTC = 1, + .padding = 0, + }); + } +};