资讯动态

二分法在嵌入式NTC查表中的精度陷阱与线性插值实践

发布时间:2026/10/1 23:20:16 来源:尧图企业网站定制
“1.2 二分法”这个标题看着像教材目录里随手翻到的一节。我一开始也这么想——直到前年做一个多路温度采集板被 NTC 查表的精度问题按在地上摩擦了差不多两个星期才回过头认真抠这一节。二分法本身简单得不像话有序序列里每次砍掉一半二十次以内必然收敛写起来五行代码。可一旦把它丢进硬件环境前提条件就开始一个个崩表是递减的、ADC 读数是量化的、阻温曲线是严重非线性的任何一个前提对不上出来的不是“稍微偏一点”而是直接偏出好几度甚至在某些温度点上跳变。这篇就按我自己踩坑的顺序把二分法在嵌入式查表这件事上的门道拆开讲清楚二分法成立的前提、几种边界写法的取舍、NTC 查表被人骂“二分法不准”的真正原因以及我最后落地的“二分定位加线性插值”完整方案和可直接复制的代码。做过温度采集、传感器标定或者正在复习算法基础的朋友都能从里面拿走点东西。1. 二分法的实际边界从课堂例题到嵌入式查表1.1 三个前提条件缺一个就翻车课本上讲二分法往往会先说一句“在有序数组中查找”。这句话太短短到把三个关键前提压缩成了一个词。第一个前提是有序这个大家都记得。第二个前提是可随机访问也就是能用下标直接跳到中间链表就做不到虽然理论上也能二分但代价完全不一样。第三个前提最容易被忽略被处理的对象在语义上必须是单调的或者说你用来比较的那个“大小关系”必须和你要找的目标一致。我在项目里遇到的就是第三个前提出问题。NTC 热敏电阻的阻值是随温度升高而下降的我拿到的温度表按温度从 -40℃ 升序排到 125℃对应的阻值却是从四十万欧一路降到三百多欧。绝大多数人背下来的二分模板都默认数组升序比较逻辑写的是if (a[mid] target) lo mid 1;。把这种模板套到递减的阻值表上逻辑整个反过来程序不会报错它会老老实实跑完循环然后返回一个完全错误的区间误差可以大到十几度。这就是“二分法不准”这个说法最常见的来源——不是二分法不准是你把比较方向搞反了。所以在动手写任何二分之前我的习惯是先问自己三个问题序列是有序的吗是按什么方向有序的我要找的到底是“等于某值”还是“落在某个区间里”这三个问题的答案决定了后面所有代码怎么写也决定了边界条件怎么处理。1.2 有序查找的代价对比以及嵌入式为什么更在乎它在 PC 上讨论 O(n) 和 O(log n)很多人觉得无所谓反正都是微秒级。但嵌入式场景里这个账要重新算。我那块板子用的是主频 48MHz 的 Cortex-M0没有硬件除法器一次 32 位整数除法要几十个周期。温度表按 1℃ 步长从 -40℃ 排到 125℃一共 166 个点。如果用线性扫描平均要比较 83 次每次比较要访问一次 Flash表通常放在常量区。最坏情况比较 166 次赶上高频采样的场合光查找就能吃掉不少 CPU 时间。中断里做温度换算的话长循环还会拉高中断响应时间。换成二分法比较次数是log2(166) ≈ 7.4也就是最多 8 次比较就能锁定区间。8 次 Flash 访问加上几次加减移位跟 83 次完全不是一个量级。这不是理论上的优化而是实测能差出十几倍的耗时。这里有个细节要注意表放在 Flash 里随机访问虽然支持但 Flash 读取有等待周期顺序访问有缓存优势。二分法虽然是随机跳但跳的次数少总体反而更快。我在另一块带 4KB 数据缓存的芯片上测过把表挪到 RAM 里二分法的优势会进一步放大因为 RAM 随机访问没有 Flash 的预取惩罚。提示如果温度换算跑在中断里优先考虑把表放 RAM 或者用更小的表加插值不要让查找过程拖长中断时间。1.3 二分法在工程里的两种用法别混为一谈很多人一说二分法就只想到“查表找值”其实工程里它有两种完全不同的用法混在一起理解特别容易出错。第一种是离散定位在一张离散的表里找到目标值落在哪两个相邻表项之间。这就是 NTC 查表干的事。它的输出是一对下标(lo, hi)也就是一个区间。注意这种用法的重点是“定位”不是“求解”很多人错就错在这儿以为定位完了温度就出来了。第二种是连续函数求根在一个已知单调连续的区间[a, b]上找一个使得f(x) 0的 x。比如已知测量的阻值反解 B 参数方程里的温度。这种用法每次迭代都在逼近一个精确的实数解迭代次数越多精度越高输出是一个数值。这两种用法的代码结构长得像但收敛条件、终止判据、精度控制完全不同。离散定位是“区间缩到只剩两个点就停”连续求根是“区间宽度小于容差就停”。我见过不少代码把两者混着写结果要么多循环很多次要么提前退出导致精度不够。后面第四章我会把两种实现都给出方便对照。2. 二分查找的落地实现边界、溢出与模板2.1 用循环不变式把“我要找什么”说清楚写二分最容易出 bug 的地方不是算法本身而是边界。我后来养成了一个习惯写之前先用一句话写出循环不变式也就是“每一轮循环开始时哪个区间一定包含答案”。这句话写清楚了边界自然就确定不用靠试。以“找到目标值属于哪两个相邻表项”为例不变式可以这么写答案一定落在[lo, hi]这个下标闭区间里并且lo和hi相邻hi - lo 1。初始状态就是lo 0hi n - 1。每一轮取中点mid根据table[mid]和目标值的关系把lo或者hi移到mid。循环的终止条件就是hi - lo 1这时候区间已经缩到最小答案锁定。这种写法比常见的while (lo hi)版本更适合查表场景因为查表的答案本来就是一个区间不是精确相等。而while (lo hi)那套模板是为“找等值元素”设计的用在插值上反而别扭。两种模板没有优劣只有适用场景的区别关键是别混用。另外要注意写循环不变式的时候一定要把“闭区间还是半开区间”说死。[lo, hi]和[lo, hi)在边界处理上差一个元素是中位数落在mid还是mid1的分水岭。2.2 mid 的计算和那个经典的整数溢出陷阱mid (lo hi) / 2这句代码几乎所有人第一遍写二分都是这么写的。在嵌入式里它有两个隐患。第一个是溢出。lo和hi都是uint16_t时lo hi最大 131070还在 32 位范围内问题不大。但如果用uint8_t或者某些平台上int是 16 位的lo hi就可能翻负变成一个巨大的正数然后mid越界访问到表外面的内存读到一堆垃圾数据。表现就是偶发的温度跳变这种 bug 极难定位因为只有在表比较长、且刚好落在某个下标区间时才触发。正确写法是uint16_t mid lo ((hi - lo) 1);先做减法再做加法hi - lo永远是正的且不会溢出移位代替除法还能省几个周期。在没有硬件除法器的 MCU 上 1和/ 2的差别是实打实的。我在 M0 上测过一个二分循环里如果每一轮都用除法166 个点的表整体耗时能多出 30% 左右。第二个隐患是取整方向。(hi - lo) 1是向下取整所以mid会偏向lo一侧。这本身没问题但你必须保证在更新的时候不会让区间卡死。比如某些写法里lo mid如果mid刚好等于lo循环就永远出不去直接死机。我的做法是保证每次更新都让hi - lo严格减小也就是lo mid或hi mid必须配合终止条件hi - lo 1使用。注意只要区间更新存在“原地踏步”的可能就一定会出现死循环。用循环不变式检查一遍比调试半小时快得多。2.3 递减数组NTC 表就是在这一步翻车的前面提过的递减表问题这里展开说。假设温度表按温度升序阻值递减目标阻值是r表项是table[i]。我们要找的是满足table[lo] r table[hi]的一对相邻下标。比较逻辑应该是如果table[mid] r说明目标在mid的右边因为阻值往下走所以lo mid否则hi mid。这跟升序数组的写法左右完全相反。写成代码while (hi - lo 1) { uint16_t mid lo ((hi - lo) 1); if (table[mid] r) { lo mid; /* 阻值还偏大往低温方向继续找 */ } else { hi mid; /* 阻值已经偏小往高温方向锁定 */ } }这段代码只有六行但比较方向一旦写反程序不会崩只会给出一个偏移很大的错误区间然后插值出来的温度自然就“不准”。我排查这个问题花了整整一个下午因为现象是“低温和高温都还好中间某一段温度整体偏高 3 度”看着特别像表数据本身有问题实际上就是比较符号的问题。有个更省心的做法如果表是递减的可以先把比较关系用!反转或者直接在注释里写死“本表阻值递减”。我现在的习惯是任何一张表在定义处都标注排序方向和单位重新接手一个项目时先看这两行能省掉大量时间。2.4 一份可以直接复用的通用模板下面是我现在固定使用的区间定位模板支持任意长度的递减表返回相邻下标。你只要把比较逻辑按表的单调方向改一下就能用。#include stdint.h /* * table: 长度 n 的降序数组table[0] table[1] ... table[n-1] * target: 目标值要求 table[0] target table[n-1] * lo/hi: 输出的相邻下标满足 table[lo] target table[hi] * 返回: 0 正常-1 越界调用方需自行钳位 */ int find_segment(const uint32_t *table, uint16_t n, uint32_t target, uint16_t *lo, uint16_t *hi) { if (n 2) return -1; if (target table[0]) { *lo 0; *hi 1; return -1; } if (target table[n - 1]) { *lo n - 2; *hi n - 1; return -1; } uint16_t a 0, b n - 1; while (b - a 1) { uint16_t mid a ((b - a) 1); if (table[mid] target) { a mid; } else { b mid; } } *lo a; *hi b; return 0; }越界返回-1的设计是刻意的。实际项目里传感器断线、短路、ADC 满量程都会让测量值跑出表格覆盖的范围。这时候如果还在表内硬插值会外推出一个毫无意义的温度比如 -80℃ 或者 200℃然后被上层逻辑当成真实数据用。我现在的做法是越界就钳位到边界温度同时置一个标志位上报异常让上层决定是丢弃还是告警。3. NTC 查表为什么用二分法会“不准”3.1 NTC 的阻温曲线本质上是一条指数曲线先把这个事情说透NTC 热敏电阻的阻值和温度之间是强烈的非线性关系工程上最常用的近似是 B 参数方程R R0 * exp(B * (1/T - 1/T0))其中T是开尔文温度R0是参考温度T0通常 25℃ 298.15K下的阻值B是材料常数典型值在 3000 到 4500 之间。以一个常见的 10kΩ B3950 的 NTC 为例代入算一下就能看出问题在 -40℃233.15K时阻值大约 402kΩ。在 25℃ 时阻值是 10kΩ。在 100℃373.15K时阻值降到约 699Ω。在 125℃398.15K时阻值只剩约 359Ω。从 -40℃ 到 125℃温度跨度 165℃阻值跨度从 40 万欧到 359 欧跨了三个数量级。这条曲线在低温段特别陡高温段平缓任何“用直线去近似它”的做法都必须在足够小的区间里做否则误差会非常夸张。这也就解释了为什么必须用查表加插值。查表解决的是“非线性难算”的问题插值解决的是“表太粗”的问题。而二分法在这中间扮演的角色只是一个快速定位的工具它既不解决非线性也不解决精度。3.2 二分只负责“找到”不负责“算出”这是整篇文章最核心的一句话。NTC 查表被人骂“不准”九成以上的情况是把二分定位的结果直接当成温度用了。具体来说很多人写完二分定位拿到下标mid之后直接返回mid对应的温度值四舍五入到最近的一个表点。如果表是 1℃ 步长这个做法带来的最大误差就是 ±0.5℃听起来还能接受。但问题在于这个误差是系统性的每一段都在而且低温段和高温段还不一样。更要命的是如果为了省 Flash把表做成 5℃ 步长那么直接取整点的最大误差就变成 ±2.5℃。这个精度在很多应用里已经不能用了比如锂电池温度保护、医疗体温计、精密恒温控制都是要求 0.1℃ 到 0.5℃ 量级的。正确做法是二分定位只是第一步拿到[lo, hi]区间之后必须做线性插值用两点之间的斜率算出精确温度。线性插值把 1℃ 步长表的等效精度提升到 0.01℃ 量级效果比把表加密十倍还划算而且额外计算量只有一次乘除。3.3 表分辨率、ADC 量化、分压电阻三重误差叠加就算插值做对了实际系统的误差还是由三部分叠加而成很多人只盯着表忽略了后面两项。表本身的分辨率和数据质量。表是用 B 参数方程生成的还是用厂家给的实测数据厂家数据通常更准因为能反映批次差异。如果用 B 参数方程自己生成B 值取错一点整条曲线都会偏。另外表格的数据类型也重要用uint16_t存欧姆最大只能表示 65535低温段四十多万欧根本存不下早早就被截断了这也是一个隐蔽的坑。ADC 的量化误差。假设用 10 位 ADC、量程 0 到 3.3V每个 LSB 是 3.22mV。如果 ADC 读的是分压后的电压那么温度变化 1℃ 对应的 ADC 变化量直接决定了系统能分辨多细的温度。这个值在低温段和高温段差得非常远。分压电阻的选择。分压电阻的取值决定了整个测量电路在哪个温度段最灵敏。选得不对会出现“低温段分辨率好、高温段几乎分不动”或者反过来。这是硬件设计阶段就该算清楚的账等到软件阶段再补只能靠过采样或者换电阻。3.4 一个完整的误差量化算例拿前面那颗 10kΩ B3950 的 NTC串一个 10kΩ 上拉电阻到 3.3VNTC 下端接地ADC 测 NTC 两端电压。这样 NTC 阻值越大ADC 读数越大。用 10 位 ADC0 到 1023算几个温度点温度NTC 阻值分压比例 R/(R10k)ADC 读数每 1℃ 的 ADC 变化-40℃402kΩ0.9758999约 0.2 LSB25℃10.0kΩ0.5000512约 4.8 LSB60℃2.49kΩ0.1994204约 3.2 LSB100℃699Ω0.065367约 1.8 LSB125℃359Ω0.034735约 0.8 LSB这张表说明几件事。第一低温段虽然每摄氏度 ADC 变化小但因为 NTC 阻值变化巨大查表反而好处理。第二高温段 1℃ 只对应 0.8 个 LSB也就是说 ADC 量化误差 ±0.5 LSB 直接对应 ±0.6℃ 的温度误差光靠软件插值已经补不回来了必须做过采样或者换更高位数的 ADC。第三整个量程的分辨率极不均匀靠近 25℃ 附近最好两端最差。再看二分法本身带来的误差。如果表是 1℃ 步长处理方式最大温度误差额外计算量二分定位后直接取最近表点±0.5℃无二分定位后线性插值约 ±0.01℃受表数据精度限制一次乘除二分求根解 B 参数方程约 ±0.001℃受迭代次数限制约 15 次迭代所以那句“NTC 查表 二分法不准”准确的表述应该是二分法定位之后不插值直接取整点误差是表步长的一半换成插值误差立刻下降一个数量级以上。二分法背了这么久的锅其实挺冤的。4. 实操二分定位加线性插值的完整实现4.1 表怎么生成、怎么排先说表的生成。我的做法是用 Python 脚本按 B 参数方程生成同时支持导入厂家实测数据做对比校正。生成的时候有几个原则按温度升序排列每个温度间隔 1℃范围覆盖实际使用区间再外扩 10℃外扩是为了让边界值有插值区间可用。阻值统一用毫欧或者放大整数存储避免浮点和溢出。我用的是欧姆值乘 10存在uint32_t里最大 402 万32 位完全放得下。表长度固定步长固定这样从温度反推下标只需要一次除法从下标推温度只需要加法。低温段和高温段可以适当加密如果 Flash 够的话在 0℃ 以下用 0.5℃ 步长能进一步降低插值误差。生成脚本的骨架大概是这样import math R0, T0, B 10000.0, 298.15, 3950.0 rows [] for t_c in range(-40, 126): T t_c 273.15 R R0 * math.exp(B * (1.0 / T - 1.0 / T0)) rows.append((t_c, R)) with open(ntc_table.h, w, encodingutf-8) as f: f.write(const uint32_t ntc_table[] {\n) for t_c, R in rows: f.write(f {int(round(R * 10))}u, /* {t_c} C */\n) f.write(};\n)生成出来的表直接在头文件里带上温度注释调试的时候一眼就能对照比事后对着计算器反推省事太多。4.2 完整代码实现从 ADC 读数到摄氏温度下面是我现在项目里在用的实现包含越界钳位、二分定位、线性插值和异常上报。阻值统一放大 10 倍温度输出 0.1℃ 为单位全程整数运算。#include stdint.h #define NTC_TABLE_LEN 166 /* -40 .. 125, 1C step */ #define NTC_TEMP_MIN (-400) /* 0.1C */ #define NTC_TEMP_MAX (1250) /* 0.1C */ extern const uint32_t ntc_table[NTC_TABLE_LEN]; /* 阻值 x10, 降序 */ typedef struct { int16_t temp_x10; /* 温度单位 0.1C */ uint8_t fault; /* 0 正常1 越界钳位 */ } ntc_result_t; ntc_result_t ntc_res_to_temp(uint32_t r_x10) { ntc_result_t out {0, 0}; /* 越界处理高于表首或低于表尾直接钳位并上报 */ if (r_x10 ntc_table[0]) { out.temp_x10 NTC_TEMP_MIN; out.fault 1; return out; } if (r_x10 ntc_table[NTC_TABLE_LEN - 1]) { out.temp_x10 NTC_TEMP_MAX; out.fault 1; return out; } /* 二分定位得到相邻区间 [lo, hi] */ uint16_t lo 0, hi NTC_TABLE_LEN - 1; while (hi - lo 1) { uint16_t mid lo ((hi - lo) 1); if (ntc_table[mid] r_x10) { lo mid; } else { hi mid; } } /* * 线性插值 * 温度 (lo - 40) (table[lo] - r) / (table[lo] - table[hi]) * 注意阻值递减table[lo] r table[hi] * 用 10 倍放大做定点得到 0.1C 精度 */ uint32_t r_lo ntc_table[lo]; uint32_t r_hi ntc_table[hi]; uint32_t num (r_lo - r_x10) * 10u; /* 0 .. 10 */ uint32_t den r_lo - r_hi; /* 恒大于 0 */ uint32_t frac10 (num (den 1)) / den; /* 四舍五入 */ out.temp_x10 (int16_t)((int32_t)(lo - 40) * 10 (int32_t)frac10); out.fault 0; return out; }几个细节值得单独说。(num (den 1)) / den这里加了半个分母做四舍五入不加的话结果会整体偏小虽然只有 0.05℃但在低温和高温两端做对比标定的时候会被看出来。den不会为零因为表是严格单调递减的生成脚本保证了这一点但如果你导入的是厂家数据一定要在脚本里检查有没有重复阻值相邻两点阻值相等会导致除零。lo - 40这个写法依赖表从 -40℃ 开始、步长 1℃ 这个约定。如果哪天改成 0.5℃ 步长这里就要改成(lo - 80) / 2之类的换算所以我在表定义旁边一定会写上步长注释防止改表的时候漏改这里。4.3 ADC 到阻值再到温度整条链路要打通代码只解决了最后一段前面还有两步ADC 读数转电压、电压转阻值。这两步里也藏着误差源。假设用的是上拉电阻分压上拉到 VCCNTC 下端接地ADC 接在 NTC 两端那么 NTC 阻值的计算是R_ntc R_ref * adc / (adc_max - adc)这个公式在adc接近adc_max的时候会变得极不稳定因为分母趋近于零一点点噪声就会被放大成巨大的阻值波动。前面算过-40℃ 时 ADC 读数大约 99910 位满量程 1023分母只有 24噪声稍微大一点算出来的阻值就飞了。这也是为什么很多温度采集模块在低温段读数特别跳。解决办法有两个。一是把分压电阻换小一点比如换成 4.7kΩ牺牲高温段的分辨率换取低温段的稳定性二是加过采样连续采 16 次求平均等效增加 2 位分辨率代价是采样时间变长。我在实际项目里用的是 16 次过采样加一阶滑动平均低温段的跳变基本消失了。还有一个更稳的方案把 NTC 放在上端参考电阻放在下端。这样一来低温时 NTC 阻值大ADC 读数接近满量程分母反而变大计算更稳定。代价是高温段读数变小。具体选哪种取决于你的应用更在意哪个温度段没有通用答案。4.4 另一条路用二分法直接求 B 参数方程的根如果 Flash 空间紧张或者要求极高的精度可以完全不用表直接用二分法求根。思路是已知测量的阻值R_meas要求解满足R(T) - R_meas 0的温度T。因为R(T)在物理范围内严格单调递减二分法一定收敛。在 [-40℃, 125℃] 这个区间里取 15 次迭代区间宽度从 165℃ 缩到165 / 2^15 ≈ 0.005℃精度远高于查表插值。代价是每次迭代都要算一次指数函数而指数在 MCU 上要么用expf库函数慢可能占几 KB Flash要么自己写泰勒展开或者查表近似算下来并不比查表省。所以我的选择是常规产品用查表加插值因为快、省电实验阶段或者标定工装用二分求根因为精度高、不需要维护表。两种方案都留着互相验证。实际调试的时候我经常用求根的结果去校验查表的结果如果两者差了 0.2℃ 以上基本可以确定是表数据或者插值代码有问题。求根的代码框架和查表几乎一样区别只是比较对象从table[mid]变成了R(mid)的函数求值static double ntc_r_from_temp(double t_c) { double T t_c 273.15; return 10000.0 * exp(3950.0 * (1.0 / T - 1.0 / 298.15)); } double ntc_temp_from_res(double r_meas) { double a -40.0, b 125.0; for (int i 0; i 40; i) { double m 0.5 * (a b); if (ntc_r_from_temp(m) r_meas) { a m; /* 阻值偏大说明温度偏低往高温找 */ } else { b m; } } return 0.5 * (a b); }注意这里的比较方向和查表版本是一致的都是“函数值偏大就往一个方向走”但因为是连续函数不需要处理相邻下标的问题写起来反而更简单。4.5 定点数替代浮点实测提速前面那段求根代码用了double在 PC 上没问题在 M0 上跑一次要几百微秒还多了差不多 8KB 的浮点库。生产代码里我全部换成了定点。定点化的关键是把指数函数也换掉。做法是用查表加插值来近似指数或者干脆回到查表方案。如果坚持要用求根可以用分段线性拟合 B 参数方程把指数关系转成几段直线的组合再做二分。实测下来定点版本的单次换算耗时从 420 微秒降到 26 微秒快了十六倍Flash 占用也少了 6KB 多。具体做法是把温度区间分成几段每段用一条直线拟合拟合误差控制在 0.05℃ 以内。分段点选在曲线曲率最大的地方也就是低温段。段数不用太多4 到 6 段就够。这段拟合系数的生成同样可以用 Python 脚本跑最小二乘输出成定点整数系数直接贴进代码。5. 常见问题与排查技巧实录5.1 几个典型现象先看看是不是同一类问题温度采集项目里的故障表现形式就那么几种但原因五花八门。我把遇到过的现象和对应的根因整理一下方便对照。第一种是固定偏置整体温度偏高或者偏低一个固定值不随温度变化。这种八成是阻值计算或者插值公式的常数项错了比如lo - 40写成了lo - 39或者分压公式里adc_max写成了1024而不是1023。固定偏置最容易定位拿几个标准点一比就出来了。第二种是分段偏置某一段温度准另一段整体偏。这种是斜率的线性项出了问题常见原因就是前面说的比较方向写反导致插值时取到了错误的区间。也有可能是表的生成脚本里用了错误的 B 值或者参考阻值。第三种是跳变和毛刺读数偶尔会跳到相差十几度的值。这种几乎可以肯定是越界或者除零问题检查 ADC 边界值有没有做钳位检查相邻表项阻值有没有重复。第四种是两端特别不准中间很准。这是 ADC 分辨率分布不均导致的属于硬件问题软件只能缓解。要么改分压电阻要么加过采样。5.2 排查速查表现象可能原因排查动作整体固定偏移常数项写错、分压公式分母用错用两点标定反推常数项某一段整体偏二分比较方向写反、表单调方向与假设不符打印区间下标对照表值读数随机跳变越界未钳位、相邻表项阻值重复导致除零加边界检查校验表单调性两端误差大ADC 分辨率不足、分压电阻取值不当改参考电阻或加过采样高温段迟钝分压电阻选得偏小换成 20k 或 47k重算量程低温段跳ADC 接近满量程时分母过小换分压结构或加滑动平均中断耗时高用了浮点和库函数换定点实现表放 RAM表中数据截断用 uint16 存欧姆低温段溢出改用 uint32 或放大存储5.3 几条只有踩过才知道的经验第一先校验表的单调性再写业务代码。我现在的启动自检里会加一段遍历整张表检查每相邻两项严格递减不满足就置一个硬件故障标志。这个成本极低但能挡住一大类因为表生成脚本出错导致的诡异问题。尤其是在批量生产时一旦表有问题软件层面再怎么写都救不回来。第二边界值一定要钳位不要外推。测量值超出表格范围时插值公式本身是能算的但它算出来的是外推结果毫无物理意义。我见过一个案子传感器断线时 ADC 读数接近满量程程序老老实实外推出 200℃ 以上然后触发了高温告警整机反复重启。断线钳位加上故障标志两行代码就能避免。第三用求根结果做交叉验证。开发阶段把两种实现都编进去随机取一批阻值比较两者输出的差值。正常情况下差值应该在 0.02℃ 以内超过 0.1℃ 就说明有实现问题。这个方法帮我抓到过一次表数据低温段被截断的 bug插值结果和求根结果在 -30℃ 以下差了 6℃ 多一眼就能看出是表的问题。第四温度输出的分辨率和实际精度是两回事。输出 0.1℃ 的粒度很容易除以 10 就有了。但实际精度受限于 ADC 和表数据高温段可能只有 ±0.6℃。对外接口上我一般会同时给出一个“可信度”字段让上层逻辑知道这个读数在什么范围内是可靠的避免拿一个假精度去做控制判断。第五别为了省 Flash 把表砍得太狠。从 1℃ 步长砍到 5℃ 步长Flash 省了 80%但插值的线性误差会明显上升尤其是在 -40℃ 到 0℃ 这段曲率大的地方。如果一定要砍记得在低温段加密、高温段放宽而不是均匀抽稀。用两段不同步长的表代码复杂一点但能兼顾空间和精度。最后分享一个小技巧调试阶段可以把二分定位出来的lo、hi、table[lo]、table[hi]、插值比例frac10一起通过串口打印出来。很多看起来像“温度不准”的问题看一眼这几个中间量就定位了——是区间取错还是插值比例异常一目了然。这比盯着最终温度值猜原因快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑