/* A8 之一:节拍器(sched)、五任务表(tasks)、两把锁(hal_lock)。 这三样都是「写错了也照跑」的东西:周期漂了、优先级反了、锁忘了放, 板子上的现象都是「偶尔慢一点」。所以这里的断言全部对着**判据**, 不对着「跑起来没崩」。 */ #include #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(); }