Skip to content

Commit cb7b34d

Browse files
authored
ch32v: Improve timekeeping (#752)
1 parent 1c6f134 commit cb7b34d

File tree

4 files changed

+93
-91
lines changed

4 files changed

+93
-91
lines changed

port/wch/ch32v/src/cpus/main.zig

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,4 @@ pub const csr = struct {
479479
pub const cpmpocr = Csr(0xBC3, u32);
480480
pub const cmcr = Csr(0xBD0, u32);
481481
pub const cinfor = Csr(0xFC0, u32);
482-
483-
// Cycle counters
484-
pub const cycle = riscv32_common.csr.cycle;
485-
pub const cycleh = riscv32_common.csr.cycleh;
486-
pub const mcountinhibit = riscv32_common.csr.mcountinhibit;
487482
};

port/wch/ch32v/src/hals/ch32v20x.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ pub const clocks = @import("clocks.zig");
66
pub const time = @import("time.zig");
77

88
pub const default_interrupts: microzig.cpu.InterruptOptions = .{
9-
// Default SysTick handler provided by the HAL
10-
.SysTick = time.systick_handler,
9+
// Default TIM2 handler provided by the HAL for 1ms timekeeping
10+
.TIM2 = time.tim2_handler,
1111
};
1212

1313
pub fn init() void {

port/wch/ch32v/src/hals/ch32v30x.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ pub const time = @import("time.zig");
66

77
/// Default interrupt handlers provided by the HAL
88
pub const default_interrupts: microzig.cpu.InterruptOptions = .{
9-
.SysTick = time.systick_handler,
9+
.TIM2 = time.tim2_handler,
1010
};
1111

1212
/// Initialize HAL subsystems used by default
1313
pub fn init() void {
14-
// Configure SysTick timing driver
14+
// Configure TIM2 timing driver
1515
time.init();
1616
}

port/wch/ch32v/src/hals/time.zig

Lines changed: 89 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,72 +6,99 @@ const time = microzig.drivers.time;
66

77
const peripherals = microzig.chip.peripherals;
88
const 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.
1214
var ticks_us: u64 = 0;
1315

14-
/// Interval between SysTick interrupts in microseconds.
16+
/// Interval between TIM2 interrupts in microseconds.
1517
/// Configured during init().
1618
var 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.
86114
pub 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
105139
pub 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

Comments
 (0)