文件
phonerail/firmware/test/test_sched.c
T
zhou yang和Claude Opus 5 ec31891429 A8:任务骨架与调度(5 个任务、两把锁)
需求 §10.3 落地。四件事:

1. 五任务表(tasks.c):周期与优先级写在一张 const 表里,开机自检查的是
   相对顺序(safety 唯一最高、log 唯一最低、thermal 与 actuator 同级),
   外加「巡检周期必须长于 INA226 一帧转换 8.8 ms」。不过就 abort。

2. 节拍器(sched.c):记下一次该醒的绝对时刻,回绕安全(int32_t 差判先后)。
   过载时跳过错过的整周期、保住相位,不补跑。

3. 两把锁(hal_lock):I²C 与 74HC595,禁止嵌套持有 —— 这块板上两者没有交集,
   「拿着 A 去拿 B」永远是写错了。ESP 侧用 mutex 取优先级继承。

4. t_safety 的一个周期:拿锁 → 6 片 INA226 的 V+I + PCA9555 两端口 → 放锁。
   不死等锁(5 ms 超时就跳过这圈),一片读失败不中断整圈、保留上一圈的值。

I²C 占用由 HAL 按线上真实字节数记账(两个目标同一份代码),静态预算与实测
必须相等:一次寄存器读 48 位,一圈 13 次事务 = 1560 µs / 20 ms = 7.8 %,
与 §10.3 手算的「约 1.5 ms / 约 8 %」相符(本节的 46 位没数 STOP 和重复 START)。

sim 侧配套两样:假 I²C 事务真的把虚拟时钟花掉(否则时序测试是空话),
以及可强制的加锁纪律检查(配一条反向测试,防止检查本身失灵)。

11 条 ctest(161 例)全绿,idf.py build 通过;16 个注错实验全部变红。

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 07:35:39 +08:00

273 行
11 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* A8 之一:节拍器(sched)、五任务表(tasks)、两把锁(hal_lock)。
这三样都是「写错了也照跑」的东西:周期漂了、优先级反了、锁忘了放,
板子上的现象都是「偶尔慢一点」。所以这里的断言全部对着**判据**,
不对着「跑起来没崩」。 */
#include <string.h>
#include "hal_lock.h"
#include "hal_time.h"
#include "ina226.h"
#include "sched.h"
#include "sim_hal.h"
#include "tasks.h"
#include "unity.h"
void setUp(void) { sim_hal_reset(); }
void tearDown(void) {}
/* ---- 节拍器:周期是「间隔」,不是「延时」 -------------------------------- */
/* 干活花掉 3 ms,节拍仍然是整 20 ms —— 这是 §10.3 的 20 ms 的定义。
写成「干完睡 20」的话这里会变成 23 ms。 */
static void test_period_is_the_interval_not_the_delay(void)
{
sched_periodic_t p;
sched_periodic_init(&p, 20);
uint32_t prev = hal_time_now_ms();
for (int i = 0; i < 100; i++) {
sim_time_advance_ms(3); /* 干活 */
sched_periodic_wait(&p);
const uint32_t now = hal_time_now_ms();
TEST_ASSERT_EQUAL_UINT32(20, now - prev);
prev = now;
}
TEST_ASSERT_EQUAL_UINT32(0, p.overruns);
}
/* 干活时间**每圈都不一样**(1~19 ms),周期照样必须是整 20 ms。
固定耗时测不出漂移:漂移是「上一圈的耗时被加进下一圈」,等长时看不出来。 */
static void test_jittery_work_does_not_move_the_phase(void)
{
sched_periodic_t p;
sched_periodic_init(&p, 20);
uint32_t work = 1;
for (int i = 0; i < 200; i++) {
sim_time_advance_ms(work);
work = (work * 7u) % 19u + 1u; /* 1~19 ms,绕着周期打转 */
sched_periodic_wait(&p);
/* 每一次醒来都落在 20 ms 的格子上 */
TEST_ASSERT_EQUAL_UINT32(0, hal_time_now_ms() % 20u);
}
TEST_ASSERT_EQUAL_UINT32(0, p.overruns);
}
/* 一圈超时(干了 25 ms):计一次过载、不睡,而且**相位不丢** ——
下一次仍然落在 20 ms 的格子上,不是从 25 ms 重新起算。 */
static void test_overrun_keeps_the_phase_and_is_counted(void)
{
sched_periodic_t p;
sched_periodic_init(&p, 20);
sim_time_advance_ms(25);
TEST_ASSERT_EQUAL_UINT32(0, sched_periodic_wait(&p)); /* 不睡 */
TEST_ASSERT_EQUAL_UINT32(1, p.overruns);
TEST_ASSERT_EQUAL_UINT32(5, p.worst_late_ms);
TEST_ASSERT_EQUAL_UINT32(15, sched_periodic_wait(&p)); /* 睡到 t=40 */
TEST_ASSERT_EQUAL_UINT32(40, hal_time_now_ms());
}
/* 卡了 105 ms(错过 5 个周期)之后**不补跑** —— 只跳过去,计一次过载。
补跑的现象是「卡一下之后连着打满总线」,见 sched.h。 */
static void test_a_long_stall_skips_instead_of_catching_up(void)
{
sched_periodic_t p;
sched_periodic_init(&p, 20);
sim_time_advance_ms(105);
TEST_ASSERT_EQUAL_UINT32(0, sched_periodic_wait(&p));
TEST_ASSERT_EQUAL_UINT32(1, p.overruns); /* 一次,不是五次 */
/* 下一拍是 t=120,也就是 15 ms 之后 —— 仍在 20 ms 的格子上。 */
TEST_ASSERT_EQUAL_UINT32(15, sched_periodic_wait(&p));
TEST_ASSERT_EQUAL_UINT32(120, hal_time_now_ms());
}
/* 32 位毫秒在 49.7 天回绕。裸写 `now >= next` 的话,回绕那一刻会被判成
「迟到了 49 天」,然后一口气跳掉两百万个周期。农场是长稳跑的,这一定会发生。 */
static void test_millisecond_wraparound_is_just_another_tick(void)
{
sim_time_set_us(0xFFFFFFF6ull * 1000ull); /* 距回绕 10 ms */
sched_periodic_t p;
sched_periodic_init(&p, 20);
for (int i = 0; i < 5; i++) {
const uint32_t slept = sched_periodic_wait(&p);
TEST_ASSERT_EQUAL_UINT32(20, slept); /* 跨过回绕点也还是 20 */
TEST_ASSERT_EQUAL_UINT32(0, p.overruns);
}
}
/* 纯计算入口要能不依赖时钟单独用(回绕、连续过载用真时钟很难摆出来)。 */
static void test_delay_from_is_pure(void)
{
sched_periodic_t p;
sched_periodic_init(&p, 20); /* now=0 → next=20 */
TEST_ASSERT_EQUAL_UINT32(20, sched_periodic_delay_from(&p, 0));
TEST_ASSERT_EQUAL_UINT32(1, sched_periodic_delay_from(&p, 39));
TEST_ASSERT_EQUAL_UINT32(0, hal_time_now_ms()); /* 一点时间也没花 */
TEST_ASSERT_EQUAL_UINT32(2, p.cycles);
}
/* ---- 五个任务那张表(§10.3) --------------------------------------------- */
static void test_task_table_passes_selftest(void)
{
TEST_ASSERT_TRUE(task_table_ok());
}
static void test_periods_match_section_10_3(void)
{
TEST_ASSERT_EQUAL_UINT32(20, task_desc(TASK_SAFETY)->period_ms);
TEST_ASSERT_EQUAL_UINT32(1000, task_desc(TASK_THERMAL)->period_ms);
/* 三个事件驱动的没有周期 —— 给它们安一个周期就是把 §10.3 读错了。 */
TEST_ASSERT_EQUAL_UINT32(0, task_desc(TASK_ACTUATOR)->period_ms);
TEST_ASSERT_EQUAL_UINT32(0, task_desc(TASK_LINK)->period_ms);
TEST_ASSERT_EQUAL_UINT32(0, task_desc(TASK_LOG)->period_ms);
}
/* §10.3 的优先级列:最高 / 中 / 中 / 低 / 最低。
反了的现象是「总线一忙巡检就被挤后」,没有任何错误码。 */
static void test_priority_order_is_safety_highest_log_lowest(void)
{
const int safety = task_desc(TASK_SAFETY)->prio;
const int thermal = task_desc(TASK_THERMAL)->prio;
const int actuator = task_desc(TASK_ACTUATOR)->prio;
const int link = task_desc(TASK_LINK)->prio;
const int log = task_desc(TASK_LOG)->prio;
TEST_ASSERT_GREATER_THAN_INT(thermal, safety);
TEST_ASSERT_GREATER_THAN_INT(actuator, safety);
TEST_ASSERT_EQUAL_INT(thermal, actuator); /* 两个都是「中」 */
TEST_ASSERT_GREATER_THAN_INT(link, thermal);
TEST_ASSERT_GREATER_THAN_INT(log, link);
}
/* 巡检周期必须长于 INA226 的一帧转换时间(8.8 ms),否则同一帧会被读两次。
现象是「电流特别稳」,不是任何一种错误。 */
static void test_safety_period_outlasts_one_ina226_conversion(void)
{
TEST_ASSERT_GREATER_THAN_UINT32(INA226_CONV_US,
task_desc(TASK_SAFETY)->period_ms * 1000u);
}
static void test_every_task_has_a_name_and_a_real_stack(void)
{
for (int i = 0; i < TASK_COUNT; i++) {
const task_desc_t *d = task_desc((task_id_t)i);
TEST_ASSERT_NOT_NULL(d->name);
TEST_ASSERT_TRUE(d->name[0] != 0);
TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2048, d->stack_bytes);
for (int j = i + 1; j < TASK_COUNT; j++) {
TEST_ASSERT_NOT_EQUAL(0, strcmp(d->name, task_desc((task_id_t)j)->name));
}
}
}
/* ---- 两把锁 --------------------------------------------------------------- */
static void test_take_and_give_round_trip(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
TEST_ASSERT_TRUE(hal_lock_held(HAL_LOCK_I2C));
hal_lock_give(HAL_LOCK_I2C);
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
}
/* 没 init 就拿 —— 真板上那是个空句柄,不拦就是随机崩。 */
static void test_take_before_init_is_refused(void)
{
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_I2C, 10));
}
/* 同一把锁拿两次:这两把都不是递归锁,第二次必须被拒。
悄悄通过的话,第一次 give 就把互斥放掉了,而代码看上去还在临界区里。 */
static void test_double_take_is_refused(void)
{
(void)hal_lock_init();
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_I2C, 10));
hal_lock_give(HAL_LOCK_I2C);
}
/* 拿着一把去拿另一把:这块板上永远是写错了(595 是纯 GPIO,和 I²C 没交集),
所以不定加锁顺序,直接判非法。见 hal_lock.h。 */
static void test_nesting_the_two_locks_is_refused_both_ways(void)
{
(void)hal_lock_init();
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_SR595, 10));
hal_lock_give(HAL_LOCK_I2C);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_I2C, 10));
hal_lock_give(HAL_LOCK_SR595);
}
/* 别的任务持着的时候:等满超时、计一次数、返回 TIMEOUT,
而且**把那段时间真的花掉** —— 调用方跳过这一周期的路径因此测得到。 */
static void test_contention_times_out_and_costs_real_time(void)
{
(void)hal_lock_init();
sim_lock_set_task(1);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
sim_lock_set_task(0);
const uint32_t t0 = hal_time_now_ms();
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, hal_lock_take(HAL_LOCK_I2C, 5));
TEST_ASSERT_EQUAL_UINT32(5, hal_time_elapsed_ms(t0));
TEST_ASSERT_EQUAL_UINT32(1, hal_lock_timeouts(HAL_LOCK_I2C));
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
/* 1 号放掉之后 0 号才拿得到。 */
sim_lock_set_task(1);
hal_lock_give(HAL_LOCK_I2C);
sim_lock_set_task(0);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 5));
hal_lock_give(HAL_LOCK_I2C);
}
/* 两把锁互不相干:拿着 I²C 不影响另一个任务去拿 595。 */
static void test_the_two_locks_are_independent(void)
{
(void)hal_lock_init();
sim_lock_set_task(1);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
sim_lock_set_task(2);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
TEST_ASSERT_EQUAL_INT(1, sim_lock_owner(HAL_LOCK_I2C));
TEST_ASSERT_EQUAL_INT(2, sim_lock_owner(HAL_LOCK_SR595));
hal_lock_give(HAL_LOCK_SR595);
sim_lock_set_task(1);
hal_lock_give(HAL_LOCK_I2C);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_period_is_the_interval_not_the_delay);
RUN_TEST(test_jittery_work_does_not_move_the_phase);
RUN_TEST(test_overrun_keeps_the_phase_and_is_counted);
RUN_TEST(test_a_long_stall_skips_instead_of_catching_up);
RUN_TEST(test_millisecond_wraparound_is_just_another_tick);
RUN_TEST(test_delay_from_is_pure);
RUN_TEST(test_task_table_passes_selftest);
RUN_TEST(test_periods_match_section_10_3);
RUN_TEST(test_priority_order_is_safety_highest_log_lowest);
RUN_TEST(test_safety_period_outlasts_one_ina226_conversion);
RUN_TEST(test_every_task_has_a_name_and_a_real_stack);
RUN_TEST(test_take_and_give_round_trip);
RUN_TEST(test_take_before_init_is_refused);
RUN_TEST(test_double_take_is_refused);
RUN_TEST(test_nesting_the_two_locks_is_refused_both_ways);
RUN_TEST(test_contention_times_out_and_costs_real_time);
RUN_TEST(test_the_two_locks_are_independent);
return UNITY_END();
}