Skip to content

Commit 859297c

Browse files
committed
feat: 补上 time 与 env —— 而 time 缺席的那条理由是错的
kal.cpp 与 README 都写着 time 是「刻意缺席」,并给了理由: SBI 可以armed一个定时器中断,那是内核的机制,不是程序能读的时钟。 前半句对,后半句不从它推出来。这个架构**另外**把 `rdtime` 直接暴露给程序 —— `time` CSR,由 Zicntr 定义,固件让低特权级可读 —— 它和 SBI 的定时器扩展是两个 不同的设施,第一个不存在说明不了第二个。 实测(2026-08-23,OpenSBI + QEMU virt,S 模式,底下没有内核): t0=333572 t1=381292 ADVANCES ⭐ 结论会被复查,写在它旁边的理由不会。那条理由在注释里待了多久,推翻它所需的 两分钟就一直可用多久。 ## time 单调计数、精确粒度、以及在同一个计数器上自旋的 sleep —— 在一台只有一个执行 上下文、没有调度器可让的机器上,自旋**就是**睡眠而不是它的模拟,所以 KAL_TIME_PROP_SLEEP_PRECISE 是照实设的。 ⚠️ 没有墙钟。SBI 不定义任何取墙钟的设施,而 6.2 把「有没有墙钟」定成属性而不是 接口 —— 所以 kal_time_wall 在场并返回 0,KAL_TIME_PROP_WALL_AVAILABLE 清零。 ⚠️ 一处新的板级事实:rdtime 的频率架构不固定,写在设备树里,而入口约定不转发 设备树指针。所以它是构建输入 OPENKAL_OPENSBI_TIMEBASE_HZ,和已有的堆大小同一 先例 —— 属于机器的数字由知道是哪台机器的工程声明,而不是由不知道的包假设。 ## env 每个答案都是空的,而这是实现不是桩。6.2 禁止的是「在场但总是失败」;这里没有 失败可报:固件把 hart 号和设备树交给镜像,两者都不是命令行,所以这个环境**有** 环境,只是里面没有东西。枚举零个东西的调用者不需要任何特例,这正是它与 6.2 禁止的那种运行期拒绝的区别。 ## 判据 例子里加两条断言,并且都验证过会红: - clock ok —— 两次读数中间 sleep 一毫秒,后者必须大于前者。把 kal_time_monotonic 改成返回常量重跑,打印 clock stuck。**读得出来和会走 是两件事,只有这条断言能分开它们。** - env empty —— 「返回 0」和「根本没链进来」不问就分不出。
1 parent 5e5acf5 commit 859297c

7 files changed

Lines changed: 284 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ jobs:
7878
mcpp run 2>&1 | tee run.log
7979
grep -q "hello from openkal over SBI" run.log
8080
grep -q "heap ok" run.log
81+
# ⚠️ `clock ok` is the line that distinguishes a counter that reads
82+
# from a counter that MOVES. The example takes two readings with a
83+
# sleep between them; a stuck clock reads fine and prints
84+
# `clock stuck`. Verified to print it, by returning a constant from
85+
# `kal_time_monotonic` and rerunning.
86+
grep -q "clock ok" run.log
87+
grep -q "env empty" run.log
8188
8289
# ---------------------------------------------------------------------------
8390
# The cross-compilation is performed FROM three systems, not only from Linux.

README.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,45 @@ requires firmware beneath it, a board backend does not.
2323

2424
## What is implemented
2525

26-
`abort`, `stream` and `memory` — openkal's core set. An implementation provides
27-
an interface in whole or not at all, so the absence of `fs`, `process`, `task`,
28-
`env` and `time` is not a deviation; `import openkal.task;` simply does not
29-
resolve.
30-
31-
⚠️ **`time` is absent deliberately, and SBI does have a timer.** SBI can arm a
32-
timer interrupt, which is a mechanism for a kernel rather than a clock a program
33-
can read. Reporting a clock that does not advance would make every timed wait
34-
silently wrong — the specification's own example of a simulation that
35-
disqualifies an interface from being provided at all.
26+
`abort`, `stream` and `memory` — openkal's core set — plus `time` and `env`. An
27+
implementation provides an interface in whole or not at all, so the absence of
28+
`fs`, `process` and `task` is not a deviation; `import openkal.task;` simply
29+
does not resolve. This machine has no storage, no second image to start and no
30+
scheduler, and clause 6.2 says the remedy for an operation that cannot be
31+
provided is that its absence be expressed by its absence rather than by a
32+
run-time refusal.
33+
34+
⚠️ **`time` used to be on that list, with a reason, and the reason was wrong.**
35+
36+
It read: SBI can arm a timer interrupt, which is a mechanism for a kernel rather
37+
than a clock a program can read. The first half is true. The second does not
38+
follow from it — this architecture also exposes `rdtime` directly to the
39+
program, through the `time` CSR, independently of SBI's timer extension. Two
40+
different facilities, and the absence of the first says nothing about the
41+
second.
42+
43+
Measured 2026-08-23 under OpenSBI on QEMU's `virt`, from supervisor mode with no
44+
kernel beneath:
45+
46+
```
47+
t0=333572 t1=381292 ADVANCES
48+
```
49+
50+
⭐ The conclusion got rechecked and the reason beside it did not. The two
51+
minutes that refuted it had been available for as long as the file existed.
52+
53+
`time` is therefore provided: a monotonic count, an exact granularity, and a
54+
sleep that spins on the same counter — which on a machine with one execution
55+
context and nothing to yield to is what sleeping *is*, so
56+
`KAL_TIME_PROP_SLEEP_PRECISE` is set truthfully. **Not** a wall clock: SBI
57+
defines no facility for one, `KAL_TIME_PROP_WALL_AVAILABLE` is clear, and clause
58+
6.2 makes that a property rather than a missing interface.
59+
60+
`env` is provided and every answer is empty. That is an implementation, not a
61+
stub: firmware enters the image with a hart identifier and a device tree, and
62+
neither is a command line, so this environment *has* an environment and it has
63+
nothing in it. A caller enumerating zero things needs no special case, which is
64+
exactly what distinguishes this from the run-time refusal clause 6.2 forbids.
3665

3766
## The heap is a bump allocator, and that is a bound rather than an omission
3867

examples/hello/src/main.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import openkal.stream;
88
import openkal.abort;
99
import openkal.memory;
10+
import openkal.time;
11+
import openkal.env;
1012

1113
namespace {
1214

@@ -25,9 +27,30 @@ extern "C" void kmain() {
2527
say(p ? "heap ok\n" : "heap exhausted\n");
2628
kal_free(p, 64, 16);
2729

30+
// ⚠️ THE ASSERTION IS THAT IT MOVES, NOT THAT IT READS.
31+
//
32+
// A clock that returns a constant reads perfectly well and is worthless,
33+
// and it is the exact failure the comment this interface replaced was
34+
// afraid of. So the sleep is between two readings and the second must
35+
// exceed the first: a stuck counter fails here, and only here.
36+
//
37+
// The figure is a millisecond because it has to be long enough to exceed
38+
// one tick of a granularity this package does not fix, and short enough
39+
// that a spin of it does not matter to anyone.
40+
const kal_duration t0 = kal_time_monotonic();
41+
kal_time_sleep(1000000);
42+
const kal_duration t1 = kal_time_monotonic();
43+
say(t1 > t0 ? "clock ok\n" : "clock stuck\n");
44+
45+
// Empty, and that is the answer rather than a refusal — src/env.cpp records
46+
// why the two are different. Asserted because "returns zero" and "was never
47+
// linked" are indistinguishable without asking.
48+
say(kal_env_arg_count() == 0 && kal_env_var_count() == 0
49+
? "env empty\n" : "env unexpected\n");
50+
2851
// Reaches the host as QEMU's exit status, because SRST is a real shutdown
2952
// rather than a spin.
30-
kal_exit(p ? 0 : 1);
53+
kal_exit(p && t1 > t0 ? 0 : 1);
3154
}
3255

3356
asm(".section .text.entry\n.globl _start\n_start:\n"

mcpp.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,18 @@ flags = [
3636
"-fno-asynchronous-unwind-tables"] },
3737
]
3838

39-
# ⚠️ The heap size is a build input because SBI provides no allocator and the
39+
# ⚠️ TWO FIGURES THAT BELONG TO THE MACHINE, DECLARED RATHER THAN ASSUMED.
40+
#
41+
# The heap size is a build input because SBI provides no allocator and the
4042
# region therefore has to come from somewhere. 64 KiB is enough for the
4143
# allocating half of the freestanding standard library on a small program and
4244
# small enough not to matter in an image; a project that needs another figure
4345
# overrides the define rather than editing this package.
44-
defines = ["OPENKAL_OPENSBI_HEAP_BYTES=65536"]
46+
#
47+
# The timebase is a build input because `rdtime` counts at a rate the
48+
# architecture does not fix. It is published in the device tree, which the entry
49+
# contract does not forward, so a project that runs on other hardware states it
50+
# here. The default is QEMU's `virt`. src/time.cpp records why this is an input
51+
# and what a project that leaves it wrong gets.
52+
defines = ["OPENKAL_OPENSBI_HEAP_BYTES=65536",
53+
"OPENKAL_OPENSBI_TIMEBASE_HZ=10000000"]

src/env.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// openkal.env on the RISC-V Supervisor Binary Interface.
2+
//
3+
// ⚠️ EVERY ANSWER HERE IS EMPTY, AND THAT IS AN IMPLEMENTATION RATHER THAN A
4+
// STUB. THE DIFFERENCE IS THE ONE CLAUSE 6.2 TURNS ON.
5+
//
6+
// Clause 6.2 forbids the arrangement this file could be mistaken for: "an
7+
// operation that is present and always fails is a defect; the remedy is that
8+
// its absence be expressed by its absence". So the question that has to be
9+
// answered before writing any of it is whether an image started by firmware
10+
// HAS an environment, and the answer is that it has one and it is empty.
11+
//
12+
// Those are not the same situation, and the distinction is visible in what a
13+
// caller sees. An implementation that refused would have to report a failure,
14+
// and a caller would have to distinguish "this machine cannot tell me" from
15+
// "there are none". Here there is nothing to distinguish: the count is zero,
16+
// and every enumeration of zero things ends immediately. A hosted
17+
// implementation asked for the fourth of three arguments returns exactly what
18+
// this one returns for the first of none, by the same rule, and no caller needs
19+
// a special case for either.
20+
//
21+
// ⚠️ WHY IT IS EMPTY, WHICH IS A FACT ABOUT THE ENTRY CONTRACT AND NOT ABOUT SBI
22+
//
23+
// Firmware enters the image at its load address with a hart identifier and a
24+
// device tree in registers. Neither is a command line. A device tree CAN carry
25+
// one --- `/chosen/bootargs` --- and this package does not read the device
26+
// tree, for the reason time.cpp records: the entry sequence belongs to the
27+
// consumer, and the pointer is gone by the time anything here runs.
28+
//
29+
// So "no arguments" describes this arrangement accurately today. If the entry
30+
// contract later forwards the device tree, this file is where `bootargs` would
31+
// be split, and the interface does not change --- which is the point of
32+
// answering the question rather than declining it.
33+
34+
#include <openkal/env.h>
35+
36+
extern "C" {
37+
38+
kal_uintptr kal_env_arg_count(void) { return 0; }
39+
40+
const char* kal_env_arg(kal_uintptr, kal_uintptr* len) {
41+
if (len) *len = 0;
42+
return nullptr;
43+
}
44+
45+
kal_uintptr kal_env_var_count(void) { return 0; }
46+
47+
const char* kal_env_var(const char*, kal_uintptr, kal_uintptr* value_len) {
48+
if (value_len) *value_len = 0;
49+
return nullptr;
50+
}
51+
52+
const char* kal_env_var_at(kal_uintptr, kal_uintptr* name_len,
53+
const char** value, kal_uintptr* value_len) {
54+
if (name_len) *name_len = 0;
55+
if (value) *value = nullptr;
56+
if (value_len) *value_len = 0;
57+
return nullptr;
58+
}
59+
60+
} // extern "C"

src/kal.cpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
//
1616
// WHAT IS IMPLEMENTED
1717
//
18-
// abort, stream and memory: openkal's core set. An implementation provides an
19-
// interface in whole or not at all, so the absence of fs, process, task, env
20-
// and time is not a deviation. `time` is deliberately absent even though SBI
21-
// has a timer extension: SBI can arm a timer interrupt, which is a mechanism
22-
// for a kernel rather than a clock a program can read, and reporting a clock
23-
// that does not advance would make every timed wait silently wrong — the
24-
// specification's own example of a simulation that disqualifies an interface.
18+
// This file: abort, stream and memory — openkal's core set. Beside it,
19+
// time.cpp and env.cpp. An implementation provides an interface in whole or not
20+
// at all, so the absence of fs, process and task is not a deviation: this
21+
// machine has no storage, no second image to start, and no scheduler, and
22+
// clause 6.2 says the remedy for an operation that cannot be provided is that
23+
// its absence be expressed by its absence rather than by a run-time refusal.
24+
//
25+
// ⚠️ `time` USED TO BE ON THAT LIST, WITH A REASON, AND THE REASON WAS WRONG.
26+
//
27+
// It read: SBI can arm a timer interrupt, which is a mechanism for a kernel
28+
// rather than a clock a program can read. The first half is true; the second
29+
// does not follow, because this architecture exposes `rdtime` to the program
30+
// independently of SBI. Measured under OpenSBI on QEMU's `virt` and it
31+
// advances. time.cpp records the measurement and what it cost to not take it.
2532

2633
#include <openkal/abort.h>
2734
#include <openkal/memory.h>

src/time.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// openkal.time on the RISC-V Supervisor Binary Interface.
2+
//
3+
// ⚠️ THIS FILE EXISTS BECAUSE THE REASON RECORDED FOR ITS ABSENCE WAS WRONG.
4+
//
5+
// kal.cpp said, and said it as a design decision rather than as a note:
6+
//
7+
// `time` is deliberately absent even though SBI has a timer extension:
8+
// SBI can arm a timer interrupt, which is a mechanism for a kernel rather
9+
// than a clock a program can read, and reporting a clock that does not
10+
// advance would make every timed wait silently wrong.
11+
//
12+
// The first half is true and the second does not follow from it. SBI's timer
13+
// extension does arm an interrupt, and it is indeed a kernel's mechanism. But
14+
// this architecture ALSO exposes a counter directly to the program: `rdtime`
15+
// reads the `time` CSR, which the Zicntr extension defines and which firmware
16+
// makes readable below its own privilege level. Those are two different
17+
// facilities, and the absence of the first says nothing about the second.
18+
//
19+
// Measured 2026-08-23 under OpenSBI on QEMU's `virt`, from a program in
20+
// supervisor mode with no kernel beneath it:
21+
//
22+
// t0=333572 t1=381292 ADVANCES
23+
//
24+
// ⭐ The lesson is the one the repository keeps relearning: a conclusion gets
25+
// rechecked and the reason written beside it does not. The reason above sat in
26+
// a comment for as long as the file existed, and the two minutes that refuted
27+
// it were available the whole time.
28+
//
29+
// WHAT IS AND IS NOT AVAILABLE HERE
30+
//
31+
// A monotonic count, precisely; a granularity, exactly; a sleep, by spinning on
32+
// the same counter, which on a machine with one execution context and no
33+
// scheduler is what sleeping is rather than a simulation of it.
34+
//
35+
// ⚠️ NOT a wall clock. SBI defines no facility for one, and no board fact would
36+
// supply it either --- a real-time clock is a device, and reading it is what a
37+
// board backend does. `KAL_TIME_PROP_WALL_AVAILABLE` is left clear, which is
38+
// the specification's own way of saying so: clause 6.2 makes availability of a
39+
// wall clock a PROPERTY rather than an interface, so `kal_time_wall` is present
40+
// and reports zero, and a program reads the word before it reads the clock.
41+
42+
#include <openkal/time.h>
43+
44+
#include "sbi.h"
45+
46+
namespace {
47+
48+
// ⚠️ THE ONE BOARD FACT THIS PACKAGE TAKES, AND IT TAKES IT AS AN INPUT.
49+
//
50+
// `rdtime` counts at a rate the architecture does not fix. The rate is
51+
// published in the device tree as `/cpus/timebase-frequency`, and this package
52+
// does not read one: the device tree is handed to the image in a register at
53+
// entry, the entry sequence belongs to the consumer rather than to this
54+
// package, and requiring every consumer to forward it would change a contract
55+
// that today is "jump here with a stack".
56+
//
57+
// So the rate is a build input, exactly as the heap size already is, and for
58+
// the same reason --- a figure that belongs to the machine is declared by the
59+
// project that knows which machine, rather than assumed by a package that does
60+
// not. The default is QEMU's `virt`, which is what the example runs on.
61+
//
62+
// ⚠️ A project on other hardware that leaves the default in place gets a clock
63+
// that advances at the wrong rate. That is a stated bound and not a hidden one:
64+
// the figure has a name, the name appears in the manifest, and this comment is
65+
// what a reader finds when they look for it.
66+
#ifndef OPENKAL_OPENSBI_TIMEBASE_HZ
67+
# define OPENKAL_OPENSBI_TIMEBASE_HZ 10000000
68+
#endif
69+
70+
constexpr kal_u64 kHz = OPENKAL_OPENSBI_TIMEBASE_HZ;
71+
constexpr kal_u64 kNano = 1000000000ULL;
72+
73+
inline kal_u64 ticks() {
74+
kal_u64 v;
75+
__asm__ __volatile__("rdtime %0" : "=r"(v));
76+
return v;
77+
}
78+
79+
// Split rather than `t * kNano / kHz`, which overflows a 64-bit product after
80+
// about eighteen seconds at ten megahertz. The split form is exact for every
81+
// value the counter can hold.
82+
inline kal_u64 to_ns(kal_u64 t) {
83+
return (t / kHz) * kNano + (t % kHz) * kNano / kHz;
84+
}
85+
86+
inline kal_u64 to_ticks(kal_u64 ns) {
87+
return (ns / kNano) * kHz + (ns % kNano) * kHz / kNano;
88+
}
89+
90+
} // namespace
91+
92+
extern "C" {
93+
94+
kal_duration kal_time_monotonic(void) { return to_ns(ticks()); }
95+
96+
// Present and reporting zero, which clause 6.2 provides for: the availability
97+
// of a wall clock is a property of the implementation, the property word says
98+
// this one has none, and a program that reads the word does not reach here.
99+
kal_duration kal_time_wall(void) { return 0; }
100+
101+
// One tick, in nanoseconds, rounded up so that the figure is never reported as
102+
// finer than it is. At ten megahertz this is 100.
103+
kal_duration kal_time_monotonic_granularity(void) {
104+
const kal_u64 g = kNano / kHz;
105+
return g ? g : 1;
106+
}
107+
108+
// ⚠️ Spinning, and that is the accurate implementation rather than a stand-in.
109+
//
110+
// Sleeping means giving the machine to something else until a time arrives. On
111+
// a machine with one execution context and nothing to give it to, the time
112+
// still has to arrive, and waiting for it on the same counter the caller would
113+
// read is precise to a tick. `KAL_TIME_PROP_SLEEP_PRECISE` is therefore set,
114+
// and it is set truthfully: there is no scheduler to overshoot.
115+
//
116+
// A `wfi` between polls would lower power, and it is not used, because it
117+
// requires an interrupt to be pending to wake from --- which requires SBI's
118+
// timer extension, which requires a trap handler, which this arrangement does
119+
// not have. The comment kal.cpp got wrong was about that extension; this is
120+
// where it would have been right.
121+
void kal_time_sleep(kal_duration ns) {
122+
if (ns == 0) return;
123+
const kal_u64 deadline = ticks() + to_ticks(ns);
124+
while (ticks() < deadline) {}
125+
}
126+
127+
const kal_uintptr kal_time_props = KAL_TIME_PROP_SLEEP_PRECISE;
128+
129+
} // extern "C"

0 commit comments

Comments
 (0)