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]>
这个提交包含在:
@@ -0,0 +1,272 @@
|
||||
/* 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();
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/* A8 之二:`t_safety` 的一个周期,以及需求 §10.3 的两条验收判据 ——
|
||||
① sim 时钟下周期是 20 ms ±1 ms;
|
||||
② I²C 占用打印出来,与 §10.3 手算的「约 1.5 ms / 约 8 %」对得上。
|
||||
|
||||
⚠️ 这两条能成立,靠的是 sim 的假 I²C **真的把虚拟时钟花掉**
|
||||
(`sim_i2c_set_bus_time`,默认开)。没有那一条,巡检在 sim 里耗时为零,
|
||||
「20 ms 周期」就退化成「测 delay 参数传对了没」—— 那是句空话:
|
||||
真正会出事的是「巡检自己占掉了 1.5 ms,节拍还准不准」。 */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sched.h"
|
||||
#include "sim_hal.h"
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
#include "unity.h"
|
||||
|
||||
#define N PHONERAIL_RAIL_COUNT
|
||||
|
||||
/* 一圈发出去的事务数:6 片 INA226 各 2 次寄存器读 + PCA9555 一次。 */
|
||||
#define TXNS_PER_CYCLE (INA226_COUNT * 2 + 1)
|
||||
|
||||
/* 把假板子搭起来:6 片 INA226 + 1 片 PCA9555,地址全部**查表**取。 */
|
||||
static void board_up(void)
|
||||
{
|
||||
const hal_i2c_cfg_t cfg = { .sda_gpio = 14, .scl_gpio = 15, .freq_hz = 400000 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(&cfg));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
|
||||
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
sim_i2c_dev_t *d = sim_i2c_add(rail_cfg(rail)->ina_addr, 2);
|
||||
d->regs[INA226_REG_BUS_V] = 3120; /* 3.9 V ÷ 1.25 mV */
|
||||
d->regs[INA226_REG_CURRENT] = (uint16_t)(rail * 1000); /* 每路不同,好认 */
|
||||
}
|
||||
sim_i2c_add(INA226_ADDR_INPUT, 2);
|
||||
sim_pca9555_attach(PCA9555_ADDR);
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
sim_hal_reset();
|
||||
board_up();
|
||||
}
|
||||
void tearDown(void) {}
|
||||
|
||||
/* ---- 验收主项一:20 ms 的节拍 -------------------------------------------- */
|
||||
|
||||
/* 500 圈。每一圈都要**正好**落在 20 ms 的格子上,而且真实唤醒时刻
|
||||
(µs 级)对理想时刻的偏差始终 < 1 ms —— 这就是「20 ms ±1 ms」。
|
||||
会漂的实现(干完睡 20)在这里第一圈就出局:那样周期是 21.56 ms,
|
||||
500 圈之后差出 780 ms。 */
|
||||
static void test_safety_holds_a_20ms_beat_for_500_cycles(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms);
|
||||
|
||||
/* 先确认「干活」在 sim 里真的花掉了时间。不然下面 500 圈测的只是
|
||||
「delay 参数传对了没」—— 巡检耗时为零的话,什么实现都能踩准 20 ms。 */
|
||||
const uint64_t t0 = hal_time_now_us();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT64(t_safety_budget_us(), hal_time_now_us() - t0);
|
||||
sched_periodic_wait(&tick);
|
||||
|
||||
uint32_t prev_ms = hal_time_now_ms();
|
||||
for (int i = 1; i < 500; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
sched_periodic_wait(&tick);
|
||||
|
||||
const uint32_t now_ms = hal_time_now_ms();
|
||||
TEST_ASSERT_EQUAL_UINT32(20, now_ms - prev_ms);
|
||||
prev_ms = now_ms;
|
||||
|
||||
/* 理想时刻是 (i+1)×20 ms;实际唤醒的 µs 与它相差不到一个毫秒格。 */
|
||||
const int64_t ideal_us = (int64_t)(i + 1) * 20000;
|
||||
const int64_t err_us = (int64_t)hal_time_now_us() - ideal_us;
|
||||
TEST_ASSERT_TRUE(err_us > -1000 && err_us < 1000);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tick.overruns);
|
||||
TEST_ASSERT_EQUAL_UINT32(500, snap.cycles);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, snap.skipped);
|
||||
}
|
||||
|
||||
/* ---- 验收主项二:I²C 占用 ------------------------------------------------ */
|
||||
|
||||
/* §10.3 手算:一次寄存器读 ≈46 个位周期,12 次 INA + 1 次 PCA ≈1.5 ms / 20 ms ≈8 %。
|
||||
这里按线上真实发生的字节数算,得 48 位/次 × 13 = 624 位 = 1560 µs = 7.8 %。
|
||||
差的两位是 STOP 和一个重复 START,§10.3 没数进去(hal_i2c_acct.h 里记了这件事)。 */
|
||||
static void test_i2c_load_per_cycle_matches_section_10_3(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(48, hal_i2c_txn_bits(1, 2));
|
||||
TEST_ASSERT_EQUAL_UINT32(1560, snap.bus_us);
|
||||
|
||||
const uint32_t pm = t_safety_load_permille(snap.bus_us,
|
||||
task_desc(TASK_SAFETY)->period_ms);
|
||||
TEST_ASSERT_EQUAL_UINT32(78, pm);
|
||||
/* §10.3 的「约 8 %」:7.0 %~8.5 % 都算对得上,超出就是巡检里多读/少读了东西。 */
|
||||
TEST_ASSERT_TRUE(pm >= 70 && pm <= 85);
|
||||
|
||||
char line[128];
|
||||
const int n = t_safety_format(&snap, line, sizeof(line));
|
||||
TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(line));
|
||||
TEST_ASSERT_NOT_NULL(strstr(line, "bus=1560 us/20 ms"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(line, "load=7.8%"));
|
||||
printf(" %s(§10.3 手算:约 1.5 ms / 约 8 %%)\n", line);
|
||||
}
|
||||
|
||||
/* 静态预算与实测必须一模一样。不一样只有两种可能:巡检里多读/少读了东西,
|
||||
或者别的任务在同一圈里插了事务进来 —— 两种都该被这条拦住。
|
||||
(这也是为什么预算函数不是一个写死的常数:它按同一个算式从事务形状来。) */
|
||||
static void test_measured_bus_time_equals_the_static_budget(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), snap.bus_us);
|
||||
}
|
||||
}
|
||||
|
||||
/* 一圈正好 13 次事务,而且每片器件被读的次数是钉死的 ——
|
||||
多读一次寄存器(比如顺手 check 一下 Calibration)会当场变红,
|
||||
然后写的人才会想起来去把预算那一行也改了。 */
|
||||
static void test_one_cycle_is_exactly_thirteen_transactions(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
const uint32_t before = hal_i2c_acct_txns();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(TXNS_PER_CYCLE, hal_i2c_acct_txns() - before);
|
||||
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(rail_cfg(rail)->ina_addr)->n_read);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(INA226_ADDR_INPUT)->n_read);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_dev(PCA9555_ADDR)->n_read);
|
||||
}
|
||||
|
||||
/* 五路读的是**表里的**地址。挂一片在「0x40+ch 算出来」的地址上,
|
||||
它一次也不该被碰到 —— 这是 A7 那条禁令在巡检路径上的回归拦网。 */
|
||||
static void test_rails_are_read_by_table_not_by_formula(void)
|
||||
{
|
||||
sim_i2c_dev_t *wrong = sim_i2c_add(0x46, 2); /* 谁都不是的地址 */
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, wrong->n_read);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, wrong->n_write);
|
||||
|
||||
/* 顺带确认值确实按路对上了(每路电流不同)。 */
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_TRUE(snap.rail_ok[rail - 1]);
|
||||
TEST_ASSERT_EQUAL_INT32(rail * 1000 * INA226_CURRENT_LSB_UA,
|
||||
snap.rail[rail - 1].current_ua);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 加锁纪律 ------------------------------------------------------------- */
|
||||
|
||||
/* 一整圈里的每一次事务都持着 I²C 锁;出了这个函数锁必须已经放掉。 */
|
||||
static void test_every_transaction_happens_under_the_i2c_lock(void)
|
||||
{
|
||||
sim_lock_require(HAL_LOCK_I2C, true);
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); /* 出门放锁 */
|
||||
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C));
|
||||
}
|
||||
|
||||
/* 上一条不是句空话:不拿锁直接读一片,违规计数必须 +1。
|
||||
(检查本身失灵的话,上一条会永远绿。) */
|
||||
static void test_the_lock_check_itself_can_fail(void)
|
||||
{
|
||||
sim_lock_require(HAL_LOCK_I2C, true);
|
||||
ina226_vi_t vi;
|
||||
(void)ina226_read_vi(rail_cfg(1)->ina_addr, &vi);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_lock_violations(HAL_LOCK_I2C)); /* 两次事务 */
|
||||
}
|
||||
|
||||
/* 拿不到锁:**跳过这一圈**,不死等(t_safety.h 规矩 1)。
|
||||
等掉的是 5 ms,还在 20 ms 之内,所以节拍不丢。 */
|
||||
static void test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever(void)
|
||||
{
|
||||
sim_lock_set_task(1); /* 假装 t_link 正在用总线 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
sim_lock_set_task(0);
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
const uint32_t t0 = hal_time_now_ms();
|
||||
const uint32_t txns = hal_i2c_acct_txns();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, snap.skipped);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, snap.cycles);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, hal_i2c_acct_txns() - txns); /* 一次事务也没发 */
|
||||
TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_TIMEOUT_MS, hal_time_elapsed_ms(t0));
|
||||
TEST_ASSERT_TRUE(T_SAFETY_LOCK_TIMEOUT_MS < task_desc(TASK_SAFETY)->period_ms);
|
||||
}
|
||||
|
||||
/* ---- 器件出问题的时候(§10.2:失联 ≠ 断电) ------------------------------ */
|
||||
|
||||
/* 六片里坏一片,剩下五片照读、PCA 照读。中断这一圈的话,
|
||||
现象是「一片 INA226 掉线,五路判据全瞎」。 */
|
||||
static void test_one_dead_device_does_not_stop_the_round(void)
|
||||
{
|
||||
sim_i2c_set_present(rail_cfg(3)->ina_addr, false);
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&snap));
|
||||
|
||||
TEST_ASSERT_FALSE(snap.rail_ok[2]);
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
if (rail != 3) {
|
||||
TEST_ASSERT_TRUE(snap.rail_ok[rail - 1]);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE(snap.input_ok);
|
||||
TEST_ASSERT_TRUE(snap.io_ok);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, snap.i2c_errors);
|
||||
}
|
||||
|
||||
/* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。
|
||||
清零的话,A10 的过压判据会看到一个 0 V 的「正常」电压,
|
||||
而 ok 标志正是让它先看一眼「这数是不是新的」。 */
|
||||
static void test_a_failed_read_keeps_the_stale_value_and_flags_it(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
const int32_t good_mv = snap.rail[0].bus_mv;
|
||||
TEST_ASSERT_EQUAL_INT32(3900, good_mv);
|
||||
|
||||
sim_i2c_set_present(rail_cfg(1)->ina_addr, false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&snap));
|
||||
TEST_ASSERT_FALSE(snap.rail_ok[0]);
|
||||
TEST_ASSERT_EQUAL_INT32(good_mv, snap.rail[0].bus_mv); /* 还是上一圈那个 */
|
||||
}
|
||||
|
||||
/* 器件掉线之后节拍照样是 20 ms(NAK 的事务也占总线时间,但占得更少)。
|
||||
⚠️ 这条测的是「巡检不会因为总线出错就乱了节奏」。 */
|
||||
static void test_the_beat_survives_a_dead_device(void)
|
||||
{
|
||||
sim_i2c_set_present(rail_cfg(2)->ina_addr, false);
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, 20);
|
||||
|
||||
uint32_t prev = hal_time_now_ms();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
(void)t_safety_step(&snap);
|
||||
sched_periodic_wait(&tick);
|
||||
TEST_ASSERT_EQUAL_UINT32(20, hal_time_now_ms() - prev);
|
||||
prev = hal_time_now_ms();
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tick.overruns);
|
||||
TEST_ASSERT_EQUAL_UINT32(50, snap.i2c_errors);
|
||||
}
|
||||
|
||||
/* PCA9555 的输入被读进来了,而且语义判读能用(A10 要拿它做判据)。 */
|
||||
static void test_pca9555_inputs_land_in_the_snapshot(void)
|
||||
{
|
||||
sim_pca9555_drive(0, 0xFF & ~(1u << pca9555_flt_bit(4))); /* CH4 的 FLT# 拉低 */
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
|
||||
TEST_ASSERT_TRUE(snap.io_ok);
|
||||
TEST_ASSERT_TRUE(pca9555_rail_fault(&snap.io, 4));
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&snap.io, 1));
|
||||
}
|
||||
|
||||
/* 第一圈的占用里不该混进启动期间的事务(游标要在 init 时对齐到「现在」)。 */
|
||||
static void test_startup_traffic_is_not_charged_to_the_first_cycle(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(PCA9555_ADDR)); /* 假装启动扫了一下 */
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), snap.bus_us);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_safety_holds_a_20ms_beat_for_500_cycles);
|
||||
RUN_TEST(test_i2c_load_per_cycle_matches_section_10_3);
|
||||
RUN_TEST(test_measured_bus_time_equals_the_static_budget);
|
||||
RUN_TEST(test_one_cycle_is_exactly_thirteen_transactions);
|
||||
RUN_TEST(test_rails_are_read_by_table_not_by_formula);
|
||||
RUN_TEST(test_every_transaction_happens_under_the_i2c_lock);
|
||||
RUN_TEST(test_the_lock_check_itself_can_fail);
|
||||
RUN_TEST(test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever);
|
||||
RUN_TEST(test_one_dead_device_does_not_stop_the_round);
|
||||
RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it);
|
||||
RUN_TEST(test_the_beat_survives_a_dead_device);
|
||||
RUN_TEST(test_pca9555_inputs_land_in_the_snapshot);
|
||||
RUN_TEST(test_startup_traffic_is_not_charged_to_the_first_cycle);
|
||||
return UNITY_END();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户