/* * 模块名称:通道标定层(标定折算,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 #include #include #include /* 先纳入桩头(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; }