Files
STM32F4-Base/test/host/adc_cal/adc_cal_test.c
T
wandering1 593c5d0c84 refactor(编号): 采集链分层 L1/L1.5/L2/L2.5/L3/L4/L5 → 语义名(批次 1)
用户裁定(2026-10-28):L1/L1.5/L2/L2.5/L3 这套层号「谁能看懂」,改用语义名。
映射(同一批定稿,含 L4/L5):
  L1   → 语义取数与有效性
  L1.5 → 标定折算
  L2   → 全周傅氏相量
  L2.5 → 测频与遥测快照
  L3   → 保护判据
  L4   → 出口与记录
  L5   → 动作留痕

实测规模:384 站点 / 62 文件(App/algo、App/task、test/host 六个套件、docs/算法、
产品需求、架构总览、启动顺序、ADC 采样节拍、IEC104 上行、采集短板评测、内存管理、
AGENTS.md、README.md、docs/README.md、.pre-commit-config.yaml 与 scripts 里的注释)。

改法(不是全局替换,逐模式有上下文规则):
- 先吃同位语再落裸词:L1 有效性校验 → 语义取数与有效性校验、L2 相量层 → 全周傅氏
  相量层、L3 保护 → 保护判据、L2.5 遥测快照 → 测频与遥测快照,避免「保护判据保护判据」
  这类重复(第一版就是这么坏掉的,已整体回滚重做)。
- .c/.h 只改注释:先把字符串字面量切出去(引号数为奇数的跨行注释按整行处理),
  C 字符串里的 L2-in ring 之类原样保留——AGENTS 铁律「字符串字面量必须纯 ASCII」。
- 层内条目号(L1-3 / L2-3 这类)**本批不动**,属批次 2;另外三族(UI 菜单层级
  L0~L3、链路延迟分段 L1~L8、CubeMX 防丢失三层)也不动,属批次 3。
- 顺手修的措辞:范围写法 L1~L3 → 「分层(语义取数与有效性 → 保护判据)」、
  CI 段「不含 L1/L2/L3」→「不含采集链分层」、目录地图里同一处并列表述。

验证:@build 0 错 0 警(Code=236456,与改前逐字节同尺寸——纯注释改动);
内存布局配图已按新脚本重生成(自检通过);doc-links --all --sections-strict 通过
(195 文档);mojibake / bom-guard / coding-standard / 新编号门禁自查 全 rc=0;
git diff --check 干净。
2026-10-28 13:04:02 +08:00

622 lines
29 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.
/*
* 模块名称:通道标定层(标定折算,adc_cal)主机侧单元测试
* 被测对象:真实源文件 App/algo/adc_cal.c + App/cfg/cfg_file.c(均 #include
* 原样纳入——标定落盘与整定入口的持久化契约必须用真引擎测,不造轮子替身)。
* 平台依赖桩: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 码值点,标定折算落地 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)守卫(两点标定的算术核,台架注入待跑)
* TC-ADCCAL-016 rail_ths 门限:TPAFE ±32112 / STM32 4013·81 / TEMP 不监视
* TC-ADCCAL-017 init 幂等:重复调用 RAM 复位默认 + 重新注册
* —— 以下走真实 cfg_file 引擎(标定落盘与整定入口 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(标定落盘与整定入口 DoD)
* TC-ADCCAL-013 段缺失(文件无本段)→ ROM 默认、VALID 全 0
* TC-ADCCAL-018 未标定闭锁单点 adc_cal_prot_blocked()(道F task-16):开机态必须
* 闭锁 / 两路齐第三路缺仍闭锁(全或无)/ 三路齐放行且 UB 仍无 VALID
* (证明它是"三路"而不是 feed_eng 那个"8 路")/ 复位后立刻回闭锁
* (现查、无自有状态可残留)
* —— 以下四项是"改了没落盘"的可诊断化(道G task-43 第②项;四态**定义只在
* App/algo/adc_cal.h 一处**,本表只描述断言、不重定义)——
* TC-ADCCAL-019 观测件不得反过来制造故障:get_stats(NULL) 静默返回、随时可读
* TC-ADCCAL-020 一次成功整定 → set_ok +1、水位不动 ⇒ 未落盘差额 = 1(改盘上没跟上,看得见)
* TC-ADCCAL-021 多次整定 + 一次落盘回执 → 差额归 0(水位是**盖写**不是自增:一次 save
* 覆盖整档,若自增会留下"假未落盘"——这条就是那个坑的回归锁)
* TC-ADCCAL-022 被拒整定(非法通道 / 越出合法带)只加 set_reject,不污染 set_ok 与差额
* TC-ADCCAL-023 空转回执不倒挂:水位永不越过 set_ok(uint32 相减一绕回就是巨大的假差额)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-25(道A 标定折算与落盘整定)
*/
#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 常量,
* 满足标定折算落地 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 码值点折算,期望独立推导(标定折算落地 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");
}
/* ===================== 用例(引擎集成组,标定落盘与整定入口 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(标定落盘与整定入口 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:未标定闭锁判定单点(「数据可信」未标定即闭锁 / 道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)");
}
/* ============ 观测计数(改了没落盘 → 可诊断,道G task-43 第②项) ============ */
/*
* 四态定义只在 App/algo/adc_cal.h 一处;本用例**全部按增量断言**——静态计数跨用例
* 累计,任何"绝对值等于几"的写法都会因为前面 TC 调过多少次 adc_cal_set() 而假失败
* (也与 vol_lock 套件同一套写法先例)。
*/
static void tc_019_023_persist_obs(void) {
adc_cal_stats_t s;
uint32_t ok0;
uint32_t rj0;
printf("-- TC-ADCCAL-019..023 persist observability --\n");
/* TC-ADCCAL-019:观测件不得反过来制造故障 */
adc_cal_get_stats(NULL);
CHECK(1, "get_stats(NULL) returns silently (an observer must not create a fault)");
/* 先立基线:把"到目前为止的改写"记为已落盘,后面全部按增量看 */
adc_cal_note_persisted();
adc_cal_get_stats(&s);
CHECK(s.set_ok == s.persisted_set_ok, "after a sync point water catches count (gap == 0)");
/* TC-ADCCAL-020:一次成功整定 → 差额 1,水位不动 */
ok0 = s.set_ok;
rj0 = s.set_reject;
CHECK(adc_cal_set(ADC_SEM_UA, ADC_CAL_K_Q16_TPAFE * 2, 100L) == 0, "in-band set accepted");
adc_cal_get_stats(&s);
CHECK(s.set_ok == ok0 + 1u, "set_ok incremented by exactly one");
CHECK(s.set_reject == rj0, "set_reject untouched by a successful set");
CHECK(s.persisted_set_ok == ok0, "water still at the old level (nothing saved yet)");
CHECK(s.set_ok - s.persisted_set_ok == 1u, "gap == 1: RAM changed, disk not (the diagnosable fact)");
/* TC-ADCCAL-021:多次整定 + 一次落盘回执 → 差额归 0(水位是盖写不是自增) */
CHECK(adc_cal_set(ADC_SEM_UN, ADC_CAL_K_Q16_TPAFE, -50L) == 0, "second channel set accepted");
/* 合法带是 ROM 系数的 [0.5x, 2x](含端点,TC-ADCCAL-008 已钉)——这里必须留在带内,
* 否则第二次改写被拒,差额就不是我声称的 3 了(写用例时踩过一次,改成 2x 上界) */
CHECK(adc_cal_set(ADC_SEM_IA, ADC_CAL_K_Q16_TPAFE * 2, 7L) == 0, "third channel set accepted (band upper end)");
adc_cal_get_stats(&s);
CHECK(s.set_ok - s.persisted_set_ok == 3u, "three pending sets before the single save");
adc_cal_note_persisted(); /* 一次 save 覆盖整档:等价 run_par_store_save() 成功路径 */
adc_cal_get_stats(&s);
CHECK(s.set_ok - s.persisted_set_ok == 0u, "one save clears all three pending sets (water is a snapshot, not a tick)");
/* TC-ADCCAL-022:被拒的两类整定只加 set_reject,不污染差额 */
ok0 = s.set_ok;
rj0 = s.set_reject;
CHECK(adc_cal_set(ADC_SEM_TEMP, ADC_CAL_K_Q16_TPAFE, 0L) == -1, "TEMP rejected");
CHECK(adc_cal_set(ADC_SEM_UA, ADC_CAL_K_Q16_TPAFE * 100, 0L) == -1, "out-of-band k rejected (RAM untouched)");
adc_cal_get_stats(&s);
CHECK(s.set_reject == rj0 + 2u, "set_reject counts both reject reasons");
CHECK(s.set_ok == ok0, "rejected sets never inflate set_ok");
CHECK(s.set_ok == s.persisted_set_ok, "gap stays zero across rejected attempts (no phantom pending write)");
/* TC-ADCCAL-023:空转回执不倒挂 */
adc_cal_note_persisted();
adc_cal_note_persisted();
adc_cal_get_stats(&s);
CHECK(s.persisted_set_ok == s.set_ok, "idle acks keep water exactly at the level (never overrun)");
CHECK(s.set_ok >= s.persisted_set_ok, "subtraction can never wrap (gap stays non-negative)");
}
/* ===================== 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();
tc_019_023_persist_obs();
printf("\n=== ADC Cal Layer Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}