Files
STM32F4-Base/test/host/adc_cal/adc_cal_test.c
T
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。

—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。

—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。

—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。

—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。

—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。

产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

552 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:通道标定层(L1.5,adc_cal)主机侧单元测试
* 被测对象:真实源文件 App/algo/adc_cal.c + App/cfg/cfg_file.c(均 #include
* 原样纳入——卡 4 的落盘契约必须用真引擎测,不造轮子替身)。
* 平台依赖桩:dbg_log.h(stub/ 计数宏)、cfg_store_*(本文件内存
* 虚拟盘,同 cfg_file 套件先例)、syslog_post(计数)、
* adc_input_desc()(按 ADC通道映射说明.md 权威表给 V0.1 来源映射)。
*
* 为什么必须主机侧(数据可信 §2.6):板端无标准源,「读数 = 系数 × 码值」
* 这一根本判据在台架上不可验;Q16 舍入、系数翻倍口径、负零点落盘、合法带
* 哨兵漏放,只有离线端能用独立推导的期望值 + 坏文件注入一次性钉死。
*
* 覆盖点(TC-ADCCAL-xxx;属主登记处 = 本目录,族 TC-HOST-ADCCAL):
* TC-ADCCAL-001 init 注册两段:名称/键数/data_size/无回调/默认表指向
* TC-ADCCAL-002 UA(TPAFE)折算:0 / 8192 / 32767 / -32768 对 10V/65536 独立推导
* TC-ADCCAL-003 UB(STM32)折算:0 / 2048 / 4095 对 3.3V/4096
* TC-ADCCAL-004 其余 6 路各 1 点(合计 8 通道 × ≥3 码值点,卡 2 DoD)
* TC-ADCCAL-005 TEMP 透传 + flags/k_uv 恒 0;sem 越界守卫
* TC-ADCCAL-006 默认 flags 三态:VALID 全 0;CLIPPED = UB/UC/IB/IC/IN(0xE6)
* TC-ADCCAL-007 k_uv 精确值:152.587890625 / 805.6640625(Q16 无舍入验证)
* TC-ADCCAL-008 set():合法带边界含端点 / 越界拒绝 RAM 不动 / TEMP 拒绝
* TC-ADCCAL-009 合法带哨兵回退:坏 k/坏 b → ROM 值 + 每通道一次 [ERR] +
* flags 不报 VALID(即便位图置位)+ init 重置观测窗
* TC-ADCCAL-014 零点 b 生效(set 路径):(code+b)×k 逐项可算
* TC-ADCCAL-015 两点标定公式:合成数据还原 k/b + 写回往返 + 参数/斜率/
* 零点越界(-2)守卫(卡 12 的算术核,台架注入待跑)
* TC-ADCCAL-016 rail_ths 门限:TPAFE ±32112 / STM32 4013·81 / TEMP 不监视
* TC-ADCCAL-017 init 幂等:重复调用 RAM 复位默认 + 重新注册
* —— 以下走真实 cfg_file 引擎(卡 4 DoD)——
* TC-ADCCAL-010 文件值生效:k_ua/b_ua 经 load 改变折算结果,VALID 置位
* TC-ADCCAL-011 越界文件值:k=999 / k=-999 被引擎收下后由哨兵兜住:
* 折算 = ROM、VALID 不报(位图置位也不报)、[ERR] 留痕
* TC-ADCCAL-012 段损坏(超长文件)→ 回退 ROM 且 flags 无 VALID(卡 4 DoD)
* TC-ADCCAL-013 段缺失(文件无本段)→ ROM 默认、VALID 全 0
* TC-ADCCAL-018 未标定闭锁单点 adc_cal_prot_blocked()(道F task-16):开机态必须
* 闭锁 / 两路齐第三路缺仍闭锁(全或无)/ 三路齐放行且 UB 仍无 VALID
* (证明它是"三路"而不是 feed_eng 那个"8 路")/ 复位后立刻回闭锁
* (现查、无自有状态可残留)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-25(道A 卡 2/卡 4)
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* 先纳入桩头(DBG_* → 计数),再纳入生产源码 */
#include "dbg_log.h"
/* ===================== 内存虚拟文件系统(cfg_store_* 强符号) ===================== */
/* 与 cfg_file 套件同款的最小实现;生产代码经 cfg_file_store.h 做全部文件/堆操作 */
#include "cfg_file_store.h"
#define VFS_CAP 8192u
static uint8_t s_vfs[VFS_CAP];
static size_t s_vfs_len = 0u;
static uint32_t s_vfs_mtime = 100u;
static int s_vfs_exists = 0;
void *cfg_store_alloc(size_t size) { return malloc(size); }
void cfg_store_free(void *p_buf) { free(p_buf); }
uint32_t cfg_store_heap_free(void) { return 0u; }
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) {
size_t n;
(void)p_path;
if (!s_vfs_exists) {
return -1;
}
n = s_vfs_len;
if (n > cap) {
n = cap;
}
(void)memcpy(p_buf, s_vfs, n);
*p_nread = n;
return 0;
}
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) {
(void)p_path;
if (len > VFS_CAP) {
return -1;
}
(void)memcpy(s_vfs, p_buf, len);
s_vfs_len = len;
s_vfs_exists = 1;
s_vfs_mtime++;
return 0;
}
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
(void)p_path;
if (!s_vfs_exists) {
return -1;
}
p_st->size = (uint32_t)s_vfs_len;
p_st->mtime = s_vfs_mtime;
return 0;
}
int cfg_store_remove(const char *p_path) {
(void)p_path;
s_vfs_exists = 0;
s_vfs_len = 0u;
return 0;
}
const char *cfg_store_config_path(void) { return "/config.ini"; }
const char *cfg_store_tmp_path(void) { return "/config.tmp"; }
/* ---- sys_log 桩:只计数自检留痕 ---- */
#include "sys_log.h"
static int s_syslog_count = 0;
void syslog_post(syslog_id_t id, const char *desc) {
(void)id;
(void)desc;
s_syslog_count++;
}
/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md §3/§5) ---- */
#include "adc_input.h"
static const adc_channel_desc_t s_desc_stub[ADC_SEM_COUNT] = {
{ ADC_SRC_TPAFE, 6u }, /* UA = buf[6] */
{ ADC_SRC_STM32, 14u }, /* UB = CH14(PC4) */
{ ADC_SRC_STM32, 3u }, /* UC = CH3 */
{ ADC_SRC_TPAFE, 5u }, /* UN = buf[5] */
{ ADC_SRC_TPAFE, 2u }, /* IA = buf[2] */
{ ADC_SRC_STM32, 6u }, /* IB = CH6 */
{ ADC_SRC_STM32, 1u }, /* IC = CH1 */
{ ADC_SRC_STM32, 2u }, /* IN = CH2 */
{ ADC_SRC_STM32, 5u }, /* TEMP = CH5 */
};
const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) {
if ((int)ch < 0 || (unsigned)ch >= (unsigned)ADC_SEM_COUNT) {
return NULL;
}
return &s_desc_stub[(unsigned)ch];
}
/* ---- dbg_log 桩观测计数定义(stub/dbg_log.h 声明为 extern) ---- */
int g_caltest_err_count = 0;
int g_caltest_info_count = 0;
int g_caltest_debug_count = 0;
/* ===================== 测试框架(极简,同 cfg_file 套件) ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码 ===================== */
/* 真实引擎 + 真实标定层(顺序:cfg_file.c 先,其 DBG_TAG 宏被 adc_cal.c 重定义
* 仅产生主机侧 C4005 告警——MCU 端两者本就在不同 TU,无此现象) */
#include "cfg_file.c"
#include "adc_cal.c"
/* ===================== 独立推导的期望值 ===================== */
/* 期望一律用 **double 直接除**(10/65536、3.3/4096),不引用被测的 Q16 常量,
* 满足卡 2 DoD「期望值由 LSB 常数独立算得(不是抄实现)」;容差只需盖住
* float32 中间积舍入(相对 ~1e-7)与打印粒度。 */
static double exp_tpafe(int32_t code) { return (double)code * (10.0 / 65536.0); }
static double exp_stm32(int32_t code) { return (double)code * (3.3 / 4096.0); }
static double exp_q16(int32_t code, int32_t b, int32_t k_q16) {
return ((double)code + (double)b) * ((double)k_q16 / 65536.0) / 1.0e6;
}
static int near_d(double got, double want) {
double d = got - want;
if (d < 0.0) { d = -d; }
return (d <= 1.0e-4 + (want < 0.0 ? -want : want) * 1.0e-5) ? 1 : 0;
}
static long my_lround(double x) { return (long)((x >= 0.0) ? (x + 0.5) : (x - 0.5)); }
/* ===================== 辅助 ===================== */
/* 复位「引擎注册表 + 虚拟盘 + 全部观测计数」= 刚上电(白盒代价同 cfg_file 套件) */
static void reset_all(void) {
memset(s_vfs, 0, sizeof(s_vfs));
s_vfs_len = 0u;
s_vfs_mtime = 100u;
s_vfs_exists = 0;
memset((void *)s_sections, 0, sizeof(s_sections));
memset(&s_base, 0, sizeof(s_base));
memset(&s_last, 0, sizeof(s_last));
s_stable = 0u;
s_base_valid = 0u;
g_caltest_err_count = 0;
g_caltest_info_count = 0;
g_caltest_debug_count = 0;
s_syslog_count = 0;
}
static void vfs_put(const char *text) {
size_t n = strlen(text);
if (n > VFS_CAP) { n = VFS_CAP; }
(void)memcpy(s_vfs, text, n);
s_vfs_len = n;
s_vfs_exists = 1;
s_vfs_mtime++;
}
static const char *vfs_get(void) { return (const char *)s_vfs; }
/* ===================== 用例(白盒组) ===================== */
/* TC-ADCCAL-001:init 注册两段(名称/键数/data_size/无回调) */
static void tc_001_init_registers(void) {
printf("[TC-ADCCAL-001] init registers [adc_cal]/[adc_cal_ofs]\n");
reset_all();
adc_cal_init();
CHECK(s_sections[0].used == 1u, "section slot0 used");
CHECK(strcmp(s_sections[0].name, "adc_cal") == 0, "sec0 name = adc_cal");
CHECK(s_sections[0].key_count == 9u, "sec0 keys = 8k + valid");
CHECK(s_sections[0].data_size == 33u, "sec0 data_size = 33 (covers valid byte)");
CHECK(s_sections[0].p_cb == NULL, "sec0 no callback (RAM copy = live values)");
CHECK(s_sections[1].used == 1u && strcmp(s_sections[1].name, "adc_cal_ofs") == 0,
"sec1 name = adc_cal_ofs");
CHECK(s_sections[1].key_count == 8u, "sec1 keys = 8b");
CHECK(s_sections[1].data_size == 32u, "sec1 data_size = 32");
CHECK(s_sections[2].used == 0u, "only two sections registered");
}
/* TC-ADCCAL-002/003/004:8 通道 × ≥3 码值点折算,期望独立推导(卡 2 DoD) */
static void tc_002_004_apply(void) {
printf("[TC-ADCCAL-002] UA (TPAFE) apply\n");
reset_all();
adc_cal_init();
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 0), exp_tpafe(0)), "UA code 0 = 0V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 8192), exp_tpafe(8192)), "UA 8192 = 1.25V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 32767), exp_tpafe(32767)), "UA full scale minus 1 (bipolar top code)");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, -32768), exp_tpafe(-32768)), "UA -FS (bipolar)");
printf("[TC-ADCCAL-003] UB (STM32) apply\n");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 0), exp_stm32(0)), "UB code 0 = 0V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 2048), exp_stm32(2048)), "UB mid = 1.65V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 4095), exp_stm32(4095)), "UB +FS 3.2992V");
printf("[TC-ADCCAL-004] other six channels one mid point each\n");
CHECK(near_d(adc_cal_apply(ADC_SEM_UC, 123), exp_stm32(123)), "UC 123");
CHECK(near_d(adc_cal_apply(ADC_SEM_UN, -10000), exp_tpafe(-10000)), "UN -10000");
CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 11452), exp_tpafe(11452)), "IA 11452 (bench first-frame code)");
CHECK(near_d(adc_cal_apply(ADC_SEM_IB, 4000), exp_stm32(4000)), "IB 4000");
CHECK(near_d(adc_cal_apply(ADC_SEM_IC, 1), exp_stm32(1)), "IC 1");
CHECK(near_d(adc_cal_apply(ADC_SEM_IN, 4094), exp_stm32(4094)), "IN 4094");
}
/* TC-ADCCAL-005:TEMP 透传与守卫 */
static void tc_005_temp(void) {
printf("[TC-ADCCAL-005] TEMP passthrough + guards\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_apply(ADC_SEM_TEMP, 3000) == 3000.0f, "TEMP raw passthrough");
CHECK(adc_cal_flags(ADC_SEM_TEMP) == 0u, "TEMP flags = 0 (both)");
CHECK(adc_cal_k_uv(ADC_SEM_TEMP) == 0.0f, "TEMP k_uv = 0");
CHECK(adc_cal_flags((adc_semantic_t)ADC_SEM_COUNT) == 0u, "flags sem OOB = 0");
CHECK(adc_cal_set((adc_semantic_t)ADC_SEM_COUNT, 10000000L, 0L) == -1, "set sem OOB rejected");
CHECK(adc_cal_apply(ADC_SEM_UA, 0) == 0.0f, "UA still applies after guards");
}
/* TC-ADCCAL-006:默认三态位图 */
static void tc_006_default_flags(void) {
uint32_t vl = 0u, cl = 0u;
uint8_t i;
printf("[TC-ADCCAL-006] default flags: no VALID, CLIPPED=0xE6\n");
reset_all();
adc_cal_init();
for (i = 0u; i < 8u; i++) {
const uint32_t f = adc_cal_flags((adc_semantic_t)i);
if ((f & ADC_CAL_F_VALID) != 0u) { vl |= (1uL << i); }
if ((f & ADC_CAL_F_CLIPPED) != 0u) { cl |= (1uL << i); }
}
CHECK(vl == 0u, "VALID map = 0x00 (uncalibrated default build)");
/* bit1 UB, bit2 UC, bit5 IB, bit6 IC, bit7 IN */
CHECK(cl == ((1uL << 1) | (1uL << 2) | (1uL << 5) | (1uL << 6) | (1uL << 7)),
"CLIPPED map = UB/UC/IB/IC/IN (0xE6)");
}
/* TC-ADCCAL-007:k_uv 精确值(Q16 无舍入口径) */
static void tc_007_k_uv(void) {
printf("[TC-ADCCAL-007] k_uv exact (Q16 no-rounding defaults)\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_k_uv(ADC_SEM_UA) > 152.5877f && adc_cal_k_uv(ADC_SEM_UA) < 152.5881f,
"UA k_uv = 152.587890625");
CHECK(adc_cal_k_uv(ADC_SEM_UB) > 805.6639f && adc_cal_k_uv(ADC_SEM_UB) < 805.6643f,
"UB k_uv = 805.6640625");
}
/* TC-ADCCAL-008:set() 合法带边界与拒绝 */
static void tc_008_set(void) {
float before;
printf("[TC-ADCCAL-008] set band edges inclusive / rejects leave RAM\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_set(ADC_SEM_UA, 10500000L, -12L) == 0, "set in-band ok");
CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, "VALID set after accepted");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_q16(1000, -12, 10500000)),
"apply uses accepted k/b");
CHECK(adc_cal_set(ADC_SEM_UA, 5000000L, 0L) == 0, "ROM/2 boundary accepted");
CHECK(adc_cal_set(ADC_SEM_UA, 20000000L, 0L) == 0, "ROM*2 boundary accepted");
before = adc_cal_apply(ADC_SEM_UA, 100);
CHECK(adc_cal_set(ADC_SEM_UA, 4999999L, 0L) == -1, "below ROM/2 rejected");
CHECK(adc_cal_set(ADC_SEM_UA, 20000001L, 0L) == -1, "above ROM*2 rejected");
CHECK(adc_cal_set(ADC_SEM_UA, 10000000L, 8193L) == -1, "b +8193 rejected");
CHECK(adc_cal_set(ADC_SEM_UA, 10000000L, -8193L) == -1, "b -8193 rejected");
CHECK(adc_cal_set(ADC_SEM_TEMP, 10000000L, 0L) == -1, "TEMP rejected");
CHECK(adc_cal_apply(ADC_SEM_UA, 100) == before, "RAM untouched after all rejects");
}
/* TC-ADCCAL-009:合法带哨兵(白盒直写 RAM 模拟引擎收到坏值) */
static void tc_009_sanitize(void) {
double want;
printf("[TC-ADCCAL-009] band sentinel: ROM fallback + one-shot ERR + no VALID\n");
reset_all();
adc_cal_init();
s_kcfg.k[0] = 30000000L; /* > ROM*2:模拟坏文件/坏标定已写入 RAM */
s_kcfg.valid |= 0x01u; /* 位图也说「已标定」——更要用哨兵兜住 */
want = exp_tpafe(1000);
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), want), "out-of-band k falls back to ROM");
CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u, "VALID suppressed while k out of band");
CHECK(g_caltest_err_count == 1, "first bad observation logs exactly one ERR");
(void)adc_cal_apply(ADC_SEM_UA, 2000);
(void)adc_cal_apply(ADC_SEM_UA, 3000);
CHECK(g_caltest_err_count == 1, "repeat observations do not spam ERR");
/* b 越界同哨兵(k 合法、b 坏) */
s_bcfg.b[1] = 99999L;
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 1000), exp_stm32(1000)), "bad b -> ROM + b=0");
CHECK(g_caltest_err_count == 2, "bad b noted on its own channel");
/* init 重置留痕位 = 新观测窗 */
adc_cal_init();
CHECK(adc_cal_k_uv(ADC_SEM_UA) > 152.5f && s_kcfg.valid == 0u, "init restores ROM + clear valid");
/* stub 计数器是测试侧累计量,init 不清它——用增量断言 */
{
const int e0 = g_caltest_err_count;
s_kcfg.k[0] = 30000000L;
(void)adc_cal_apply(ADC_SEM_UA, 1);
CHECK(g_caltest_err_count == e0 + 1, "ERR window restarts after init (delta 1)");
}
}
/* TC-ADCCAL-014:b 生效(经 set 正规路径) */
static void tc_00a_b_effect(void) {
printf("[TC-ADCCAL-014] zero offset b via set path\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_set(ADC_SEM_IA, 10000000L, -77L) == 0, "set IA b=-77");
CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 1077), exp_q16(1077, -77, 10000000)),
"IA apply = (code-77)*ROMk");
}
/* TC-ADCCAL-015:两点标定公式 + 写回往返 */
static void tc_00b_two_point(void) {
int32_t kq = 0, bb = 0;
const int32_t k0 = 10000000L;
const int32_t b0 = -30L;
int32_t c1 = -250, c2 = 32000;
int32_t v1, v2;
int rc;
printf("[TC-ADCCAL-015] two-point formula recovers synthetic k/b\n");
/* 合成测量:v = (c + b0) * k0/65536 µV(lround 模拟量化读数) */
v1 = (int32_t)my_lround(((double)c1 + (double)b0) * ((double)k0 / 65536.0));
v2 = (int32_t)my_lround(((double)c2 + (double)b0) * ((double)k0 / 65536.0));
rc = adc_cal_two_point(c1, c2, v1, v2, &kq, &bb);
CHECK(rc == 0, "two_point ok");
CHECK(kq > (k0 - 5) && kq < (k0 + 5), "k recovered within +-5 q16");
CHECK(bb > (b0 - 3) && bb < (b0 + 3), "b recovered within +-3 codes");
/* 写回往返:set 后 apply(c2) 应还原 v2(µV 级) */
reset_all();
adc_cal_init();
CHECK(adc_cal_set(ADC_SEM_UA, kq, bb) == 0, "set recovered coeffs");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, c2), (double)v2 / 1.0e6), "roundtrip v(c2) = measured");
printf("[TC-ADCCAL-015] two_point guards\n");
CHECK(adc_cal_two_point(100, 100, 0L, 100000L, &kq, &bb) == -1, "dc=0 rejected");
CHECK(adc_cal_two_point(100, 200, 50000L, 50000L, &kq, &bb) == -1, "dv=0 rejected");
CHECK(adc_cal_two_point(100, 200, 100000L, 50000L, &kq, &bb) == -1, "inverted slope rejected");
CHECK(adc_cal_two_point(100, 200, 0L, 1000L, NULL, &bb) == -1, "NULL k rejected");
CHECK(adc_cal_two_point(100, 200, 0L, 1000L, &kq, NULL) == -1, "NULL b rejected");
/* 真零点 +9000 码 > B_MAX:斜率对但 b 越界 -> -2(先查前端,不用系数凑) */
v1 = (int32_t)my_lround((100.0 + 9000.0) * ((double)k0 / 65536.0));
v2 = (int32_t)my_lround((2000.0 + 9000.0) * ((double)k0 / 65536.0));
CHECK(adc_cal_two_point(100, 2000, v1, v2, &kq, &bb) == -2, "b beyond +-8192 -> -2");
}
/* TC-ADCCAL-016:贴轨门限表 */
static void tc_00c_rail_ths(void) {
int32_t lo = 0, hi = 0;
printf("[TC-ADCCAL-016] rail thresholds by source\n");
CHECK(adc_cal_rail_ths(ADC_SEM_UA, &lo, &hi) == 0 && hi == 32112L && lo == -32112L,
"TPAFE band = +-98% half-FS");
CHECK(adc_cal_rail_ths(ADC_SEM_UB, &lo, &hi) == 0 && hi == 4013L && lo == 81L,
"STM32 band = 98%/2% of 4095");
CHECK(adc_cal_rail_ths(ADC_SEM_TEMP, &lo, &hi) == -1, "TEMP not rail-watched");
CHECK(adc_cal_rail_ths(ADC_SEM_UA, NULL, &hi) == -1, "NULL guard");
}
/* TC-ADCCAL-017:init 幂等 */
static void tc_00d_idempotent(void) {
printf("[TC-ADCCAL-017] re-init restores defaults\n");
reset_all();
adc_cal_init();
(void)adc_cal_set(ADC_SEM_UA, 12345678L, 5L);
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_q16(1000, 5, 12345678)), "pre-check tuned");
adc_cal_init(); /* 幂等重入:RAM 复位 + 再注册 */
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_tpafe(1000)), "re-init back to ROM");
CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "re-init clears VALID too");
}
/* ===================== 用例(引擎集成组,卡 4 DoD) ===================== */
/* TC-ADCCAL-010:文件值经 load 生效 + VALID */
static void tc_010_file_apply(void) {
printf("[TC-ADCCAL-010] file values applied via cfg_file_load\n");
reset_all();
adc_cal_init();
vfs_put("[adc_cal]\r\nk_ua=12000000\r\nvalid=1\r\n"
"[adc_cal_ofs]\r\nb_ua=-40\r\n");
CHECK(cfg_file_load() == 0, "load ok");
CHECK(adc_cal_k_uv(ADC_SEM_UA) > 183.105f && adc_cal_k_uv(ADC_SEM_UA) < 183.106f,
"UA k = 12000000 q16 = 183.10546875 uV");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 6557), exp_q16(6557, -40, 12000000)),
"apply uses file k and negative file b");
CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, "VALID from file bitmap");
/* 未列键保留默认 */
CHECK(adc_cal_k_uv(ADC_SEM_UB) > 805.663f && adc_cal_k_uv(ADC_SEM_UB) < 805.665f,
"UB untouched keeps ROM");
}
/* TC-ADCCAL-011:越界文件值由哨兵兜住(含位图说谎) */
static void tc_011_file_bad(void) {
printf("[TC-ADCCAL-011] out-of-band file k -> sentinel ROM fallback, VALID denied\n");
reset_all();
adc_cal_init();
vfs_put("[adc_cal]\r\nk_ua=999\r\nk_ub=-999\r\nvalid=7\r\n");
CHECK(cfg_file_load() == 0, "load ok (values structurally valid INT)");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 500), exp_tpafe(500)), "UA sanitized to ROM");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 500), exp_stm32(500)), "UB sanitized to ROM");
CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "UA VALID denied despite bitmap bit (k out of band)");
CHECK(adc_cal_flags(ADC_SEM_UB) == ADC_CAL_F_CLIPPED, "UB keeps only CLIPPED");
CHECK(g_caltest_err_count >= 2, "both sanitizations logged");
}
/* TC-ADCCAL-012:段损坏(超长文件)→ 回退默认且 flags 无 VALID(卡 4 DoD 原句) */
static void tc_012_file_corrupt(void) {
char big[CFG_FILE_MAX_LEN + 64];
size_t i;
printf("[TC-ADCCAL-012] oversized corrupt file -> ROM defaults + no VALID\n");
reset_all();
adc_cal_init();
/* 先把有效值 + valid 位图写入 RAM(模拟曾标定过),再用超长文件触发引擎复位 */
(void)adc_cal_set(ADC_SEM_UA, 11000000L, 3L);
for (i = 0u; i < (sizeof(big) - 1u); i++) {
big[i] = 'A';
}
big[sizeof(big) - 1u] = '\0';
vfs_put(big);
CHECK(cfg_file_load() == 0, "load survives corruption");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 4096), exp_tpafe(4096)), "corrupt -> ROM coefficients");
CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "corrupt -> no VALID anywhere (DoD wording)");
CHECK(s_syslog_count >= 1, "corruption left self-test trail");
}
/* TC-ADCCAL-013:段缺失 → 引擎默认(=ROM),VALID 全 0 */
static void tc_013_section_missing(void) {
printf("[TC-ADCCAL-013] missing sections -> factory ROM, vl=0\n");
reset_all();
adc_cal_init();
vfs_put("; only a comment, no managed sections\n");
CHECK(cfg_file_load() == 0, "load ok with unrelated file");
CHECK(near_d(adc_cal_apply(ADC_SEM_IN, 2048), exp_stm32(2048)), "IN still ROM");
CHECK(adc_cal_flags(ADC_SEM_IA) == 0u, "no VALID with missing sections");
}
/* TC-ADCCAL-018:未标定闭锁判定单点(「数据可信」卡 5 / 道F task-16)
* 为什么值得单独钉:这条谓词此前在 App/task/protect_task.c 与
* App/app/ui_data_feed.c 各写一份,**没有任何一份被主机测到**(ui_data 套件只编
* 快照层,不编组装器)。收敛到本层后它跟着被测源文件一起进来,第一次拿到
* 机器证据。
* 注:DTU_BENCH 旁路这一档在主机侧测不到(宿主构建不定义它,编译进来的就是产品
* 路径),它的正确性由"全仓只剩一处 #ifndef"+ 台架目检保证,不假装有覆盖。 */
static void tc_018_prot_blocked(void) {
printf("[TC-ADCCAL-018] adc_cal_prot_blocked: boot blocks / three required / stateless\n");
/* ① 开机首拍前(任何 set 之前)必须判闭锁。**这条最贵**:若实现偷懒回读了
* 某个"上拍态"变量,刚上电那拍就是 0 = 谎称已标定,屏上「未标定」徽标
* 不出现,现场读到的是"改了定值就会动作" */
reset_all();
adc_cal_init();
CHECK(adc_cal_prot_blocked() == 1u, "boot (no calibration at all) -> blocked");
/* ② 全或无:两路齐、第三路缺 → 仍闭锁(不是多数表决,也不是"任一路齐就放行") */
CHECK(adc_cal_set(ADC_SEM_UA, 10500000L, 0L) == 0, "UA in-band accepted");
CHECK(adc_cal_prot_blocked() == 1u, "UA only -> still blocked");
CHECK(adc_cal_set(ADC_SEM_UN, 10000000L, 0L) == 0, "UN in-band accepted");
CHECK(adc_cal_prot_blocked() == 1u, "UA+UN with IA missing -> still blocked");
/* ③ 三路齐 → 放行;同一时刻 UB 仍无 VALID,证明本判定用的是**三路**口径,
* 不是电气 8 路全 VALID(那是 eng 的量纲口径,两者不可互换) */
CHECK(adc_cal_set(ADC_SEM_IA, 10000000L, 0L) == 0, "IA in-band accepted");
CHECK(adc_cal_prot_blocked() == 0u, "UA/UN/IA all VALID -> released");
CHECK((adc_cal_flags(ADC_SEM_UB) & ADC_CAL_F_VALID) == 0u,
"UB still uncalibrated at release (3-channel rule, not the 8-channel eng rule)");
/* ④ 现查、无残留:init 复位(等价于配置被重载/文件损坏回退)后立刻回闭锁 */
adc_cal_init();
CHECK(adc_cal_prot_blocked() == 1u, "re-init -> blocked again (no cached state)");
CHECK(adc_cal_prot_blocked() == 1u, "repeat call stable (idempotent pure read)");
}
/* ===================== main ===================== */
int main(void) {
printf("=== ADC Cal Layer Tests ===\n");
tc_001_init_registers();
tc_002_004_apply();
tc_005_temp();
tc_006_default_flags();
tc_007_k_uv();
tc_008_set();
tc_009_sanitize();
tc_00a_b_effect();
tc_00b_two_point();
tc_00c_rail_ths();
tc_00d_idempotent();
tc_010_file_apply();
tc_011_file_bad();
tc_012_file_corrupt();
tc_013_section_missing();
tc_018_prot_blocked();
printf("\n=== ADC Cal Layer Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}