@@ -6,72 +6,99 @@ const time = microzig.drivers.time;
66
77const peripherals = microzig .chip .peripherals ;
88const PFIC = peripherals .PFIC ;
9+ const RCC = peripherals .RCC ;
10+ const TIM2 = peripherals .TIM2 ;
911
1012/// Global tick counter in microseconds.
11- /// Incremented by the SysTick interrupt handler.
13+ /// Incremented by the TIM2 interrupt handler.
1214var ticks_us : u64 = 0 ;
1315
14- /// Interval between SysTick interrupts in microseconds.
16+ /// Interval between TIM2 interrupts in microseconds.
1517/// Configured during init().
1618var tick_interval_us : u64 = 1000 ; // Default 1ms
1719
18- /// Initialize the time system with SysTick .
20+ /// Initialize SysTick as a free-running counter for delays .
1921///
20- /// This configures SysTick to trigger interrupts at regular intervals and maintains
21- /// a microsecond counter.
22+ /// This configures SysTick to run continuously without interrupts or auto-reload.
23+ /// CNTH:CNTL together form a 64-bit counter that increments at HCLK rate .
2224/// NOTE: This must be called AFTER configuring the system clock to the final frequency.
23- pub fn init () void {
24- // Reset configuration
25- PFIC .STK_CTLR .raw = 0 ;
25+ fn init_delay_counter () void {
26+ // Configure SysTick for free-running mode (no interrupts, no auto-reload)
27+ PFIC .STK_CTLR .modify (.{
28+ // Turn on the system counter STK
29+ .STE = 1 ,
30+ // Disable counter interrupt
31+ .STIE = 0 ,
32+ // HCLK for time base (i.e. count 8x faster)
33+ .STCLK = 1 ,
34+ // Free-running (no auto-reload)
35+ .STRE = 0 ,
36+ });
2637
27- // Reset the count register
38+ // Reset the count registers
2839 PFIC .STK_CNTL .raw = 0 ;
40+ PFIC .STK_CNTH .raw = 0 ;
41+ }
2942
30- // Configure SysTick to trigger every 1ms
31- // Compute tick interval using board frequency if available, else CPU default
43+ /// Initialize TIM2 to fire interrupts every 1ms for timekeeping.
44+ ///
45+ /// This configures TIM2 to generate periodic interrupts that maintain the ticks_us counter.
46+ /// NOTE: This must be called AFTER configuring the system clock to the final frequency.
47+ fn init_tick_timer () void {
3248 const freq : u32 = if (microzig .config .has_board and @hasDecl (board , "cpu_frequency" ))
3349 board .cpu_frequency
3450 else
3551 microzig .cpu .cpu_frequency ;
36- const counts_per_ms = freq / 1000 ;
37- tick_interval_us = 1000 ;
3852
39- // Set the compare register
40- PFIC . STK_CMPLR .raw = counts_per_ms - 1 ;
53+ // Enable TIM2 clock (bit 0 of APB1PCENR)
54+ RCC . APB1PCENR .raw |= 1 << 0 ;
4155
42- // Configure SysTick
43- PFIC .STK_CTLR .modify (.{
44- // Turn on the system counter STK
45- .STE = 1 ,
46- // Enable counter interrupt
47- .STIE = 1 ,
48- // HCLK for time base
49- .STCLK = 1 ,
50- // Re-counting from 0 after counting up to the comparison value
51- .STRE = 1 ,
52- });
56+ // Set prescaler and auto-reload for 1ms ticks
57+ // For 48MHz: PSC = 47 (divide by 48), ARR = 999 (count to 1000)
58+ // = 48MHz / 48 / 1000 = 1kHz = 1ms
59+ const prescaler : u16 = @intCast ((freq / 1_000_000 ) - 1 ); // Divide to 1MHz
60+ TIM2 .PSC .modify (.{ .PSC = prescaler });
61+ TIM2 .ATRLR .modify (.{ .ARR = 999 }); // Count 1000 cycles = 1ms at 1MHz
62+
63+ // Enable update interrupt
64+ TIM2 .DMAINTENR .modify (.{ .UIE = 1 });
65+
66+ // Enable the counter
67+ TIM2 .CTLR1 .modify (.{ .CEN = 1 });
5368
54- // Clear the trigger state
55- PFIC .STK_SR .modify (.{ .CNTIF = 0 });
69+ // Enable TIM2 interrupt in NVIC
70+ cpu .interrupt .enable (.TIM2 );
71+ }
72+
73+ /// Initialize the time system with TIM2 and SysTick.
74+ ///
75+ /// - TIM2: Configured to fire interrupts every 1ms, maintaining a microsecond counter
76+ /// - SysTick: Configured as a free-running 64-bit counter for precise delays (no interrupts)
77+ /// NOTE: This must be called AFTER configuring the system clock to the final frequency.
78+ pub fn init () void {
79+ tick_interval_us = 1000 ; // 1ms intervals
80+
81+ // Initialize SysTick as free-running delay counter (no interrupts)
82+ init_delay_counter ();
5683
57- // Enable SysTick interrupt
58- cpu . interrupt . enable ( .SysTick );
84+ // Initialize TIM2 for periodic 1ms interrupts
85+ init_tick_timer ( );
5986}
6087
61- /// SysTick interrupt handler.
88+ /// TIM2 interrupt handler.
6289///
6390/// This should be registered in the chip default_interrupts
6491/// ```zig
6592/// pub const default_interrupts: microzig.cpu.InterruptOptions = .{
66- /// .SysTick = time.systick_handler ,
93+ /// .TIM2 = time.tim2_handler ,
6794/// };
6895/// ```
69- pub fn systick_handler () callconv (cpu .riscv_calling_convention ) void {
96+ pub fn tim2_handler () callconv (cpu .riscv_calling_convention ) void {
7097 // Increment the tick counter
7198 ticks_us +%= tick_interval_us ;
7299
73- // Clear the trigger state for the next interrupt
74- PFIC . STK_SR .modify (.{ .CNTIF = 0 });
100+ // Clear the update interrupt flag
101+ TIM2 . INTFR .modify (.{ .UIF = 0 });
75102}
76103
77104/// Get the current time since boot.
@@ -83,8 +110,12 @@ pub fn get_time_since_boot() time.Absolute {
83110}
84111
85112/// Sleep for the specified number of milliseconds.
113+ /// Uses the interrupt-driven tick counter for accurate timing regardless of interrupt latency.
86114pub fn sleep_ms (time_ms : u32 ) void {
87- sleep_us (@as (u64 , time_ms ) * 1000 );
115+ const deadline = get_time_since_boot ().add_duration (time .Duration .from_ms (time_ms ));
116+ while (deadline .is_reached_by (get_time_since_boot ()) == false ) {
117+ asm volatile ("" ::: .{ .memory = true });
118+ }
88119}
89120
90121/// Sleep for the specified number of microseconds.
@@ -98,62 +129,38 @@ pub fn sleep_us(time_us: u64) void {
98129 }
99130}
100131
101- /// Busy-wait for the specified number of microseconds using the RISC-V cycle counter .
132+ /// Busy-wait for the specified number of microseconds using SysTick .
102133///
103- /// This does not depend on SysTick granularity and provides sub-millisecond resolution.
104- /// It blocks the CPU and may be affected by interrupt latency.
134+ /// Uses SysTick as a free-running 64-bit counter. This provides:
135+ /// - No interrupt conflicts (SysTick runs with no interrupts, TIM2 handles timing)
136+ /// - 64-bit range (practically unlimited delays)
137+ /// - Immune to interrupt latency (counter runs continuously)
138+ /// - Direct hardware access, no wrapping concerns
105139pub fn delay_us (us : u32 ) void {
140+ if (us == 0 ) return ;
141+
106142 const freq : u32 = if (microzig .config .has_board and @hasDecl (board , "cpu_frequency" ))
107143 board .cpu_frequency
108144 else
109145 microzig .cpu .cpu_frequency ;
110146
111- // Guard against very low frequencies
112- if (freq < 1_000_000 or us == 0 ) return ;
113-
114- const cycles_per_us : u32 = freq / 1_000_000 ;
115-
116- // Ensure the cycle counter is running (clear mcountinhibit.CY if present)
117- if (@hasField (cpu .csr , "mcountinhibit" )) {
118- const ci = cpu .csr .mcountinhibit .read ();
119- if ((ci & 0x1 ) != 0 ) cpu .csr .mcountinhibit .write (ci & ~ @as (u32 , 1 ));
120- }
147+ // SysTick counter runs at HCLK
148+ // Calculate ticks needed for the delay
149+ const ticks_per_us : u32 = freq / 1_000_000 ;
150+ const ticks : u64 = @as (u64 , us ) * @as (u64 , ticks_per_us );
121151
122- // Probe whether the cycle counter advances at all; if not, fall back.
123- const probe0 : u32 = cpu .csr .cycle .read ();
124- asm volatile ("" ::: .{ .memory = true });
125- const probe1 : u32 = cpu .csr .cycle .read ();
126- // TODO: Determine if this works on ANY ch32v chips. It does not seem to on CH32V203
127- if (probe0 == probe1 ) {
128- // Fallback: use a dedicated tight loop. We keep a volatile asm barrier in the loop body to
129- // prevent the compiler from removing or merging iterations. In practice this still
130- // generates a tight (addi+bnez) loop on QingKe cores.
131- //
132- // Effective cost ≈ 3 cycles/iter (2 instructions + branch penalty).
133- // If hardware observation shows a ~4/3 slowdown, consider either:
134- // - switching the loop body to an explicit `nop` and using 4 cycles/iter,
135- // - or adding a one-time boot calibration when mcycle is available.
136- const cycles_per_iter : u32 = 3 ;
137- const total_cycles : u32 = us * cycles_per_us ;
138- const iters : u32 = (total_cycles + cycles_per_iter - 1 ) / cycles_per_iter ; // ceil
139- fallback_delay_iters (iters );
140- return ;
141- }
152+ // Read 64-bit counter (CNTH:CNTL)
153+ const start_low : u32 = PFIC .STK_CNTL .raw ;
154+ const start_high : u32 = PFIC .STK_CNTH .raw ;
155+ const start : u64 = (@as (u64 , start_high ) << 32 ) | @as (u64 , start_low );
142156
143- const start : u32 = probe0 ;
144- const wait_cycles : u32 = us * cycles_per_us ;
145- while (@as (u32 , cpu .csr .cycle .read () - start ) < wait_cycles ) {
146- asm volatile ("" ::: .{ .memory = true });
147- }
148- }
157+ // Wait until enough ticks have elapsed
158+ while (true ) {
159+ const current_low : u32 = PFIC .STK_CNTL .raw ;
160+ const current_high : u32 = PFIC .STK_CNTH .raw ;
161+ const current : u64 = (@as (u64 , current_high ) << 32 ) | @as (u64 , current_low );
149162
150- // A dedicated function for the fallback loop to keep the body stable across optimization levels
151- fn fallback_delay_iters (iter : u32 ) callconv (.c ) void {
152- var i = iter ;
153- if (i == 0 ) return ;
154- while (i != 0 ) : (i -= 1 ) {
155- // Don't optimize away. Should be 2 instructions, addi + bne, with a cycle lost on branch
156- // prediction.
163+ if (current - start >= ticks ) break ;
157164 asm volatile ("" ::: .{ .memory = true });
158165 }
159166}
0 commit comments