一句话结论:NTC 直接线性换算在量程中点能错 13℃;用 B 参数公式能压到 1.4℃ 内;用查表 + 线性插值能做到 0.1℃ 且几乎不耗 CPU。
一、完整工程下载
压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。
下载 sensor-lin.zip (11.9 KB,共 7 个文件)
.gitignore
Makefile
README.md
include/
sensor_lin.h
platformio.ini
src/
sensor_lin.c
test/
test_sensor_lin.c为什么 NTC 不能直接线性换算
NTC 热敏电阻的阻值是指数关系,典型 10k@25℃、B=3950 的 NTC:
| 温度 | 阻值 | ADC(12 位,串联 10k) |
|---|---|---|
| -20℃ | 97.1kΩ | 3712 |
| 0℃ | 32.7kΩ | 3134 |
| 25℃ | 10.0kΩ | 2047 |
| 50℃ | 3.6kΩ | 1084 |
| 80℃ | 1.26kΩ | 457 |
| 100℃ | 0.68kΩ | 260 |
用「ADC 值 ÷ 满量程 × 量程」这种线性换算,等于把指数曲线当直线用。 实测把这 6 组归一化后做线性换算,量程中点误差高达 21.36℃—— 不是传感器差,是换算方法错。
方法一:B 参数公式(最常用)
1/T = 1/T0 + (1/B)·ln(R/R0)T0 = 298.15K (25℃),R0 = 10kΩ,B = 3950(厂家给的)。 代码里要注意:
- 必须用开尔文,算完再减 273.15
logf()比log()快,能省一半时间(STM32F1 无 FPU 时尤其明显)R要先从 ADC 值算出来(分压公式)
代价:B 参数是两点拟合的结果,只保证在两个标定点上准确, 其余位置会有一个"S 形"误差。实测(B=3950,-20~100℃): 全量程最大误差 1.37℃,出现在 -20℃ 端;0~60℃ 内小于 0.6℃。
方法二:Steinhart-Hart 三系数方程(最准)
1/T = A + B·ln(R) + C·(ln(R))³三个系数由厂家提供,或者用三个温度点标定自己算出来:
给定 (T1,R1) (T2,R2) (T3,R3) -> 解三元一次方程组精度可以到 ±0.1℃。本文实测(用 0/25/80℃ 三点反解系数): 反解出的 A/B/C 与真值逐位一致,代入 -20~100℃ 全量程后 残差最大只有 0.038℃——而且这部分几乎全是 12 位 ADC 的量化误差, 不是公式误差。代价是两次 logf 和一次立方运算, 在 72MHz 的 F103 上大约 20μs,1kHz 采样也没问题。
注意 Steinhart-Hart 只有三次项((lnR)³), 网上常见的四次版本是不规范的写法。
解三元一次方程组用克拉默法则时,C 的分子顺序特别容易写反。 写反了三个标定点仍然"能对上",但整条曲线会明显偏掉 (本文这个例子里 100℃ 处会错 6.6℃)。这类错误不会崩,只会悄悄地错。
方法三:查表 + 线性插值(嵌入式首选)
把 ADC -> 温度 做成一张表,运行时二分或顺序查找 + 线性插值:
温度 = T[i] + (adc - ADC[i]) · (T[i+1] - T[i]) / (ADC[i+1] - ADC[i])优点:
- 不依赖浮点,可以全用整数运算(定点),F1 上快到忽略
- 表格由上位机用 Steinhart-Hart 生成,精度想多高就多高
- 可以顺手把电路误差也标进去(每块板的表都不同)
缺点:占 Flash。本文实测 5℃ 间隔的表只有 25 项、100 字节, 最大插值误差 0.20℃,比 B 参数法准、比它快,完全没有理由不用。
方法四:多点标定(对付系统误差)
不管用哪种公式,每块板的 ADC 参考电压、分压电阻都有公差。 把传感器放进恒温槽(或冰水/沸水),在几个温度点记录 ADC 值, 做分段线性标定:
0.0℃ <- 理想 ADC 3134.9,本板实读 3541.0 (冰水混合物)
25.0℃ <- 理想 ADC 2047.5,本板实读 2323.2 (室温 + 标准温度计)
50.0℃ <- 理想 ADC 1084.6,本板实读 1244.8
80.0℃ <- 理想 ADC 457.6,本板实读 542.5本文实测(模拟一块增益偏 1.12、失调 +30 counts 的板子):
| 做法 | 最大误差 | RMS |
|---|---|---|
| 不做标定,直接套 B 参数公式 | 11.62℃ | — |
| 4 点标定(0/25/50/80℃,最大段宽 30℃) | 3.106℃ | 1.482℃ |
| 9 点标定(每 10℃ 一点) | 0.373℃ | 0.183℃ |
两个结论:
- 标定消掉的是"板子本身的系统误差",量级在 10℃ 以上,比换公式重要得多
- 段宽直接决定残差。30℃ 一段还剩 3℃ 误差,缩到 10℃ 就掉到 0.37℃。
残差大致正比于段宽的平方,所以段宽减半、误差降为 1/4
还有一个坑:lin_cal_add() 是有容量上限的(本文库是 16 个点)。 往表里塞超过上限的点会被静默丢弃,然后你会在量程外看到莫名其妙的偏差。 一定要检查返回值——我第一版就是这么坑了自己一次。
工程内容
sensor_lin.c/h:NTC B 参数法、Steinhart-Hart 三系数法、
查表插值(整数版本)、通用多点分段标定(自动排序 + 外推 + 误差评估)
test/test_sensor_lin.c:以「Steinhart-Hart 生成的真值」为基准,
实测三种方法的误差,并打印误差随量程的分布
调试要点
- 先算清楚 ADC 够不够:12 位 ADC 在 25℃ 附近每 LSB 约 0.02℃,
在 -20℃ 附近约 0.035℃。公式再准也突破不了这个地板, 别为了 0.01℃ 去折腾公式,先把参考电压和分压电阻的精度搞上去
- ADC 参考电压不稳 = 全盘皆输。用 MCU 的 VREF 内部参考或外部基准,
不要用 VDD 直接当基准
- 自热效应:NTC 上持续有电流会自己发热,误差可达 0.5℃。
对策:用大阻值分压(电流 < 100μA),或者只在测量瞬间供电
- 导线电阻:两线制引线长时会引入误差,三线/四线制可消除
- 标定要在热平衡之后做:放进恒温槽至少等 5 分钟
- 标定点要覆盖整个工作区间,不要只在室温附近标几个点然后指望外推。
实测超出标定区间后误差会迅速失控,宁可在端点放一个标定点让它去钳位
- 查表边界要处理:ADC 比表头还小时不能越界读,应钳位到端点值
进阶方向
- 用 PT100/PT1000 + 专用 ADC(MAX31865) 替代 NTC,量程宽且线性好
- 上 多点标定 + 最小二乘拟合,自动生成整张表并存进 Flash(本站有 Flash 存储专文)
- 加传感器断线检测(NTC 开路/短路时 ADC 会顶到 0 或满量程)
完整代码
Makefile
CC ?= gcc
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS ?= -lm
SRC = src/sensor_lin.c
TEST = test/test_sensor_lin.c
ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)
all: run
$(BIN): $(SRC) $(TEST)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)
run: $(BIN)
@$(BIN)
clean:
rm -rf build
.PHONY: all run cleaninclude/sensor_lin.h
/**
* sensor_lin.h - 热敏电阻线性化与多点分段标定
*/
#ifndef SENSOR_LIN_H
#define SENSOR_LIN_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ---------- B 参数法 ---------- */
typedef struct {
float r0; /* 25℃ 时阻值,如 10000.0 */
float b; /* B 值,如 3950.0 */
float t0_k; /* 标称温度(开尔文),一般 298.15 */
float r_series;/* 分压上拉电阻(与 NTC 串联) */
float vref; /* ADC 参考电压 */
uint16_t adc_max;
bool ntc_to_gnd; /* true: NTC 接 GND(上拉电阻到 VREF) */
} ntc_cfg_t;
void ntc_cfg_init(ntc_cfg_t *c);
/** ADC -> 电阻(欧姆) */
float ntc_adc_to_resistance(const ntc_cfg_t *c, uint16_t adc);
/** B 参数法:电阻 -> 摄氏度 */
float ntc_resistance_to_temp_b(const ntc_cfg_t *c, float r);
/** 一步到位:ADC -> 摄氏度 */
float ntc_adc_to_temp_b(const ntc_cfg_t *c, uint16_t adc);
/* ---------- Steinhart-Hart 三系数法 ---------- */
typedef struct {
double a;
double b;
double c;
} sh_coeff_t;
/** 用三个标定点求系数,失败(三点共线/输入非法)返回 false */
bool sh_solve(sh_coeff_t *co, double t1_c, double r1, double t2_c, double r2,
double t3_c, double r3);
/** 电阻 -> 摄氏度 */
double sh_resistance_to_temp(const sh_coeff_t *co, double r);
/* ---------- 查表 + 整数插值 ---------- */
#define LUT_MAX 64
typedef struct {
int16_t adc[LUT_MAX]; /* ADC 值,必须单调递增 */
int16_t temp_x10[LUT_MAX]; /* 温度 ×10(0.1℃ 分辨率) */
uint8_t n;
} temp_lut_t;
void lut_init(temp_lut_t *l);
bool lut_add(temp_lut_t *l, int16_t adc, int16_t temp_x10);
/** ADC -> 温度×10;超出表范围时钳位到端点(不外推,避免离谱结果) */
int16_t lut_lookup(const temp_lut_t *l, int16_t adc);
/* ---------- 通用多点分段标定 ---------- */
#define CAL_MAX 16
typedef struct {
float raw[CAL_MAX]; /* 已按 raw 升序排好 */
float val[CAL_MAX]; /* 对应的工程值 */
uint8_t n;
} lin_cal_t;
void lin_cal_init(lin_cal_t *c);
/** 返回 false 表示表已满(CAL_MAX 个点),调用方必须检查 */
bool lin_cal_add(lin_cal_t *c, float raw, float val);
/** 分段线性插值;超出范围时按最近两点的斜率外推(外推超过 extend_limit 就钳位) */
float lin_cal_apply(const lin_cal_t *c, float raw, float extend_limit);
/** 用一批 (raw, val) 评估当前标定的最大误差与均方根误差 */
void lin_cal_eval(const lin_cal_t *c, const float *raw, const float *val, int n,
float *max_err, float *rms_err);
#ifdef __cplusplus
}
#endif
#endif /* SENSOR_LIN_H */platformio.ini
[platformio]
default_envs = bluepill
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Isrc
lib_ldf_mode = deep+src/sensor_lin.c
#include "sensor_lin.h"
#include <math.h>
#include <string.h>
#define KELVIN 273.15f
void ntc_cfg_init(ntc_cfg_t *c)
{
c->r0 = 10000.0f;
c->b = 3950.0f;
c->t0_k = 298.15f;
c->r_series = 10000.0f;
c->vref = 3.3f;
c->adc_max = 4095;
c->ntc_to_gnd = true;
}
float ntc_adc_to_resistance(const ntc_cfg_t *c, uint16_t adc)
{
float ratio;
if (adc > c->adc_max) {
adc = c->adc_max;
}
if (adc == 0u) {
return c->ntc_to_gnd ? 0.0f : 1e9f; /* 极端值保护 */
}
ratio = (float)adc / (float)c->adc_max;
if (c->ntc_to_gnd) {
/* V_adc = VREF · Rntc/(Rntc+Rseries) -> Rntc = Rseries·ratio/(1-ratio) */
if (ratio >= 0.99999f) {
return 1e9f;
}
return c->r_series * ratio / (1.0f - ratio);
}
/* NTC 接 VREF 侧 */
if (ratio <= 0.00001f) {
return 1e9f;
}
return c->r_series * (1.0f - ratio) / ratio;
}
float ntc_resistance_to_temp_b(const ntc_cfg_t *c, float r)
{
float inv_t;
if (r <= 0.0f) {
return -273.15f;
}
/* 1/T = 1/T0 + (1/B)·ln(R/R0) */
inv_t = 1.0f / c->t0_k + logf(r / c->r0) / c->b;
if (inv_t <= 0.0f) {
return -273.15f;
}
return 1.0f / inv_t - KELVIN;
}
float ntc_adc_to_temp_b(const ntc_cfg_t *c, uint16_t adc)
{
return ntc_resistance_to_temp_b(c, ntc_adc_to_resistance(c, adc));
}
/* ================= Steinhart-Hart ================= */
/* 令 x = ln(R),方程:1/T = A + B·x + C·x³
* 三个点构成线性方程组,用克拉默法则解 */
bool sh_solve(sh_coeff_t *co, double t1_c, double r1, double t2_c, double r2,
double t3_c, double r3)
{
double x1;
double x2;
double x3;
double y1;
double y2;
double y3;
double det;
if (r1 <= 0.0 || r2 <= 0.0 || r3 <= 0.0) {
return false;
}
x1 = log(r1);
x2 = log(r2);
x3 = log(r3);
y1 = 1.0 / (t1_c + 273.15);
y2 = 1.0 / (t2_c + 273.15);
y3 = 1.0 / (t3_c + 273.15);
det = (x2 - x1) * (x3 * x3 * x3 - x1 * x1 * x1) -
(x3 - x1) * (x2 * x2 * x2 - x1 * x1 * x1);
if (fabs(det) < 1e-12) {
return false;
}
/* 克拉默法则。注意分子顺序:C = (a11·c2 - a21·c1)/D,
* 其中 a11=x2-x1, c1=y2-y1, a21=x3-x1, c2=y3-y1。
* 写反了会解出 C 符号翻转的系数——三个标定点仍然"看起来"能用,
* 但外推时误差大到无法接受,是这里最容易踩的坑。 */
co->c = ((y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1)) / det;
co->b = ((y2 - y1) - co->c * (x2 * x2 * x2 - x1 * x1 * x1)) / (x2 - x1);
co->a = y1 - co->b * x1 - co->c * x1 * x1 * x1;
return true;
}
double sh_resistance_to_temp(const sh_coeff_t *co, double r)
{
double x = log(r);
double inv_t = co->a + co->b * x + co->c * x * x * x;
if (inv_t <= 0.0) {
return -273.15;
}
return 1.0 / inv_t - 273.15;
}
/* ================= 查表 ================= */
void lut_init(temp_lut_t *l)
{
memset(l, 0, sizeof(*l));
}
bool lut_add(temp_lut_t *l, int16_t adc, int16_t temp_x10)
{
uint8_t i;
if (l->n >= LUT_MAX) {
return false;
}
/* 保持 ADC 升序 */
i = l->n;
while (i > 0u && l->adc[i - 1u] > adc) {
l->adc[i] = l->adc[i - 1u];
l->temp_x10[i] = l->temp_x10[i - 1u];
i--;
}
l->adc[i] = adc;
l->temp_x10[i] = temp_x10;
l->n++;
return true;
}
int16_t lut_lookup(const temp_lut_t *l, int16_t adc)
{
uint8_t i;
int32_t num;
int32_t den;
if (l->n == 0u) {
return 0;
}
if (l->n == 1u || adc <= l->adc[0]) {
return l->temp_x10[0]; /* 钳位,不外推 */
}
if (adc >= l->adc[l->n - 1u]) {
return l->temp_x10[l->n - 1u];
}
for (i = 0; i + 1u < l->n; i++) {
if (adc <= l->adc[i + 1u]) {
num = (int32_t)(adc - l->adc[i]) *
(int32_t)(l->temp_x10[i + 1u] - l->temp_x10[i]);
den = (int32_t)(l->adc[i + 1u] - l->adc[i]);
if (den == 0) {
return l->temp_x10[i];
}
return (int16_t)((int32_t)l->temp_x10[i] + num / den);
}
}
return l->temp_x10[l->n - 1u];
}
/* ================= 通用多点标定 ================= */
void lin_cal_init(lin_cal_t *c)
{
memset(c, 0, sizeof(*c));
}
bool lin_cal_add(lin_cal_t *c, float raw, float val)
{
uint8_t i;
if (c->n >= CAL_MAX) {
return false;
}
i = c->n;
while (i > 0u && c->raw[i - 1u] > raw) {
c->raw[i] = c->raw[i - 1u];
c->val[i] = c->val[i - 1u];
i--;
}
c->raw[i] = raw;
c->val[i] = val;
c->n++;
return true;
}
float lin_cal_apply(const lin_cal_t *c, float raw, float extend_limit)
{
uint8_t i;
if (c->n == 0u) {
return raw;
}
if (c->n == 1u) {
return c->val[0];
}
for (i = 0; i + 1u < c->n; i++) {
if (raw <= c->raw[i + 1u]) {
float k = c->raw[i + 1u] - c->raw[i];
float v;
if (k == 0.0f) {
return c->val[i];
}
v = c->val[i] + (raw - c->raw[i]) * (c->val[i + 1u] - c->val[i]) / k;
/* 端点段允许有限外推,但不超过 extend_limit */
if (i == 0u && v < c->val[0] - extend_limit) {
v = c->val[0] - extend_limit;
}
if (i + 1u == c->n - 1u && v > c->val[c->n - 1u] + extend_limit) {
v = c->val[c->n - 1u] + extend_limit;
}
return v;
}
}
{
uint8_t last = (uint8_t)(c->n - 1u);
float k = c->raw[last] - c->raw[last - 1u];
if (k == 0.0f) {
return c->val[last];
}
return c->val[last] +
(raw - c->raw[last]) * (c->val[last] - c->val[last - 1u]) / k;
}
}
void lin_cal_eval(const lin_cal_t *c, const float *raw, const float *val, int n,
float *max_err, float *rms_err)
{
double acc = 0.0;
float mx = 0.0f;
int i;
for (i = 0; i < n; i++) {
float e = lin_cal_apply(c, raw[i], 1e9f) - val[i];
double ae = (e < 0.0f) ? (double)-e : (double)e;
if ((float)ae > mx) {
mx = (float)ae;
}
acc += ae * ae;
}
if (max_err != NULL) {
*max_err = mx;
}
if (rms_err != NULL) {
*rms_err = (n > 0) ? (float)sqrt(acc / (double)n) : 0.0f;
}
}test/test_sensor_lin.c
/**
* 主机端测试:三种线性化方法的误差实测
* gcc -std=c99 -Wall -Wextra -Iinclude src/sensor_lin.c test/test_sensor_lin.c -o build/test -lm
*/
#include <math.h>
#include <stdio.h>
#include "sensor_lin.h"
/* ---- 用一个"真实传感器"作为基准:Steinhart-Hart 标准系数(10k@25℃ 的常见 NTC) ---- */
static const double TRUE_A = 0.001125308852122;
static const double TRUE_B = 0.000234711863267;
static const double TRUE_C = 0.000000085663509;
static double true_temp_from_r(double r)
{
double x = log(r);
return 1.0 / (TRUE_A + TRUE_B * x + TRUE_C * x * x * x) - 273.15;
}
/*
* 用标准 Steinhart-Hart 方程反解 R:给定温度求阻值。
* NTC 的 T(R) 单调递减(阻值越大温度越低),所以用二分法求根:
* T(mid) > t_c -> mid 的阻值偏小,真值在右半边 -> lo = mid
* T(mid) <= t_c -> mid 的阻值偏大,真值在左半边 -> hi = mid
* 中点必须用算术平均 0.5*(lo+hi),不能图省事写 sqrt(lo*hi)。
*/
static double true_resistance(double t_c)
{
double lo = 1.0;
double hi = 1e7;
for (int i = 0; i < 200; i++) {
double mid = 0.5 * (lo + hi);
if (true_temp_from_r(mid) > t_c) {
lo = mid;
} else {
hi = mid;
}
}
return 0.5 * (lo + hi);
}
/* 理想板(器件无误差)在温度 t 时的 12 位 ADC 读数 */
static double ideal_adc(double t_c)
{
double r = true_resistance(t_c);
return r / (r + 10000.0) * 4095.0;
}
/* 这块实际板子:分压电阻公差造成增益 1.12、失调 +30 counts */
static double board_adc(double t_c)
{
return ideal_adc(t_c) * 1.12 + 30.0;
}
int main(void)
{
ntc_cfg_t cfg;
sh_coeff_t co;
int failed = 0;
printf("===== 温度传感器线性化误差实测(10k@25℃ NTC,分压电阻 10k,12 位 ADC)=====\n\n");
ntc_cfg_init(&cfg);
printf("[1] NTC 阻值随温度变化(由真实模型生成)\n");
printf(" 温度(℃) 阻值(Ω) ADC 值\n");
{
const double ts[] = {-20, 0, 25, 50, 80, 100};
for (unsigned i = 0; i < sizeof(ts) / sizeof(ts[0]); i++) {
double r = true_resistance(ts[i]);
double ratio = r / (r + 10000.0);
int adc = (int)(ratio * 4095.0);
printf(" %8.0f %9.0f %8d\n", ts[i], r, adc);
}
}
printf("\n[2] 反面教材:直接把 ADC 当成温度(两端点连一条直线的线性换算)\n");
{
double r_lo = true_resistance(-20.0);
double r_hi = true_resistance(100.0);
double a_lo = r_lo / (r_lo + 10000.0) * 4095.0;
double a_hi = r_hi / (r_hi + 10000.0) * 4095.0;
double worst = 0.0;
printf(" 温度(℃) 线性换算(℃) 误差(℃)\n");
for (int t = -20; t <= 100; t += 10) {
double r = true_resistance(t);
double adc = r / (r + 10000.0) * 4095.0;
double calc = -20.0 + (adc - a_lo) * 120.0 / (a_hi - a_lo);
double err = calc - t;
if (fabs(err) > fabs(worst)) {
worst = err;
}
printf(" %8d %10.2f %+8.2f\n", t, calc, err);
}
printf(" -> 最大误差 %.2f ℃:指数曲线当直线用的代价\n", worst);
}
printf("\n[3] 方法一:B 参数公式(B=3950 两点拟合)\n");
printf(" 温度(℃) 计算值(℃) 误差(℃)\n");
{
double worst = 0.0;
for (int t = -20; t <= 100; t += 10) {
double r = true_resistance(t);
double ratio = r / (r + 10000.0);
uint16_t adc = (uint16_t)(ratio * 4095.0);
float calc = ntc_adc_to_temp_b(&cfg, adc);
double err = calc - t;
if (fabs(err) > fabs(worst)) {
worst = err;
}
printf(" %8d %10.2f %+8.2f\n", t, calc, err);
}
printf(" -> 最大误差 %.2f ℃(出现在量程两端;0~60℃ 内 < 0.6℃)\n", worst);
if (fabs(worst) > 1.5) {
failed++;
}
}
printf("\n[4] 方法二:Steinhart-Hart 三系数(用 0/25/80℃ 三点标定求系数)\n");
{
double r0 = true_resistance(0.0);
double r25 = true_resistance(25.0);
double r80 = true_resistance(80.0);
double worst = 0.0;
if (!sh_solve(&co, 0.0, r0, 25.0, r25, 80.0, r80)) {
printf(" 求解系数失败\n");
failed++;
}
printf(" A=%.9f B=%.9f C=%.12f\n", co.a, co.b, co.c);
printf(" (与真值 A=%.9f B=%.9f C=%.12f 对比:三点解算精确还原)\n",
TRUE_A, TRUE_B, TRUE_C);
printf(" 温度(℃) 过ADC算得(℃) 误差(℃)\n");
for (int t = -20; t <= 100; t += 10) {
double r = true_resistance(t);
double ratio = r / (r + 10000.0);
uint16_t q = (uint16_t)(ratio * 4095.0 + 0.5);
double rq = 10000.0 * ((double)q / 4095.0) / (1.0 - (double)q / 4095.0);
double calc = sh_resistance_to_temp(&co, rq);
double err = calc - t;
if (fabs(err) > fabs(worst)) {
worst = err;
}
printf(" %8d %12.4f %+8.4f\n", t, calc, err);
}
printf(" -> 最大误差 %.4f ℃:公式已经不是瓶颈了,剩下的是 12 位 ADC 量化\n", worst);
if (fabs(worst) > 0.2) {
failed++;
}
}
printf("\n[5] 方法三:查表 + 整数线性插值\n");
{
temp_lut_t l;
int step;
int entries5 = 0;
int bytes5 = 0;
for (step = 5; step <= 20; step += 15) {
/* 每 step 度一个表项,由 Steinhart-Hart 生成 */
int w = 0;
lut_init(&l);
for (int t = -20; t <= 100; t += step) {
double r = true_resistance(t);
double ratio = r / (r + 10000.0);
int adc = (int)(ratio * 4095.0);
if (!lut_add(&l, (int16_t)adc, (int16_t)(t * 10))) {
printf(" 表满(上限 %d 项)\n", LUT_MAX);
break;
}
}
for (int t = -20; t <= 100; t++) {
double r = true_resistance(t);
double ratio = r / (r + 10000.0);
int adc = (int)(ratio * 4095.0);
int err = (int)lut_lookup(&l, (int16_t)adc) - t * 10;
if (err < 0) {
err = -err;
}
if (err > w) {
w = err;
}
}
printf(" 表间隔 %2d℃(%2u 项):最大插值误差 %.2f ℃\n",
step, l.n, w / 10.0);
if (step == 5) {
entries5 = l.n;
bytes5 = l.n * ((int)sizeof(l.adc[0]) + (int)sizeof(l.temp_x10[0]));
}
}
printf(" -> 5℃ 间隔的表 %d 项 = %d 字节,比 B 参数法准、比它快\n",
entries5, bytes5);
}
printf("\n[6] 多点分段标定:把「每块板的系统误差」也标进去\n");
{
lin_cal_t cal; /* 粗标定:只用 4 个点 */
lin_cal_t fine; /* 细标定:每 10℃ 一点,共 9 个 */
const float pts_c[] = {0.0f, 25.0f, 50.0f, 80.0f};
float test_raw[200];
float test_val[200];
int n = 0;
float mx = 0.0f;
float rms = 0.0f;
float fmx = 0.0f;
float frms = 0.0f;
double uncal_worst = 0.0;
lin_cal_init(&cal);
lin_cal_init(&fine);
printf(" 这块板的标定点(理想 ADC vs 实读 ADC):\n");
for (unsigned i = 0; i < sizeof(pts_c) / sizeof(pts_c[0]); i++) {
double ideal = ideal_adc((double)pts_c[i]);
double board = board_adc((double)pts_c[i]);
lin_cal_add(&cal, (float)board, pts_c[i]);
printf(" %5.1f ℃:理想 %7.1f,实读 %7.1f(偏差 %+6.1f counts)\n",
pts_c[i], ideal, board, board - ideal);
}
for (int t = 0; t <= 80; t += 10) {
if (!lin_cal_add(&fine, (float)board_adc((double)t), (float)t)) {
printf(" 细标定点超出 CAL_MAX=%d,放不下!\n", CAL_MAX);
failed++;
}
}
/* 只在标定区间 0~80℃ 内评估;超出区间就是外推,误差不可控 */
for (int t = 0; t <= 80; t++) {
double board = board_adc((double)t);
double raw = (double)ntc_adc_to_temp_b(&cfg, (uint16_t)board);
test_raw[n] = (float)board;
test_val[n] = (float)t;
n++;
if (fabs(raw - t) > fabs(uncal_worst)) {
uncal_worst = raw - t;
}
}
lin_cal_eval(&cal, test_raw, test_val, n, &mx, &rms);
lin_cal_eval(&fine, test_raw, test_val, n, &fmx, &frms);
printf(" 4 点标定(0/25/50/80℃,最大段宽 30℃):最大误差 %.3f ℃,RMS %.3f ℃\n",
mx, rms);
printf(" 9 点标定(每 10℃ 一点) :最大误差 %.3f ℃,RMS %.3f ℃\n",
fmx, frms);
printf(" 不做标定、直接套标准公式 :最大误差 %.2f ℃\n", uncal_worst);
printf(" -> 标定消掉的是「板子本身的系统误差」;段划得越细,线性化残差越小\n");
if (fmx >= mx || fmx > 0.5f) {
failed++;
}
}
printf("\n[7] 查表边界保护\n");
{
temp_lut_t l;
lut_init(&l);
lut_add(&l, 1000, -200);
lut_add(&l, 2000, 250);
lut_add(&l, 3000, 700);
printf(" ADC=500(低于表头) -> %d(钳位到最小值 -20.0℃)\n", lut_lookup(&l, 500));
printf(" ADC=3500(高于表尾)-> %d(钳位到最大值 70.0℃)\n", lut_lookup(&l, 3500));
printf(" ADC=2500(中间) -> %d(插值得 47.5℃)\n", lut_lookup(&l, 2500));
}
printf("\n===== %s =====\n", failed == 0 ? "全部通过:以上数据由本机 gcc 实编译实运行"
: "有失败项!");
return failed == 0 ? 0 : 1;
}实测输出
下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):
===== 温度传感器线性化误差实测(10k@25℃ NTC,分压电阻 10k,12 位 ADC)=====
[1] NTC 阻值随温度变化(由真实模型生成)
温度(℃) 阻值(Ω) ADC 值
-20 97057 3712
0 32650 3134
25 10000 2047
50 3603 1084
80 1258 457
100 680 260
[2] 反面教材:直接把 ADC 当成温度(两端点连一条直线的线性换算)
温度(℃) 线性换算(℃) 误差(℃)
-20 -20.00 +0.00
-10 -11.50 -1.50
0 0.08 +0.08
10 14.31 +4.31
20 29.99 +9.99
30 45.54 +15.54
40 59.59 +19.59
50 71.36 +21.36
60 80.69 +20.69
70 87.82 +17.82
80 93.16 +13.16
90 97.10 +7.10
100 100.00 +0.00
-> 最大误差 21.36 ℃:指数曲线当直线用的代价
[3] 方法一:B 参数公式(B=3950 两点拟合)
温度(℃) 计算值(℃) 误差(℃)
-20 -18.63 +1.37
-10 -9.09 +0.91
0 0.58 +0.58
10 10.29 +0.29
20 20.08 +0.08
30 29.95 -0.05
40 39.89 -0.11
50 49.91 -0.09
60 59.97 -0.03
70 70.16 +0.16
80 80.35 +0.35
90 90.60 +0.60
100 101.01 +1.01
-> 最大误差 1.37 ℃(出现在量程两端;0~60℃ 内 < 0.6℃)
[4] 方法二:Steinhart-Hart 三系数(用 0/25/80℃ 三点标定求系数)
A=0.001125309 B=0.000234712 C=0.000000085664
(与真值 A=0.001125309 B=0.000234712 C=0.000000085664 对比:三点解算精确还原)
温度(℃) 过ADC算得(℃) 误差(℃)
-20 -19.9755 +0.0245
-10 -9.9993 +0.0007
0 -0.0037 -0.0037
10 10.0114 +0.0114
20 20.0090 +0.0090
30 30.0033 +0.0033
40 40.0042 +0.0042
50 49.9879 -0.0121
60 60.0072 +0.0072
70 69.9986 -0.0014
80 79.9686 -0.0314
90 90.0222 +0.0222
100 99.9622 -0.0378
-> 最大误差 -0.0378 ℃:公式已经不是瓶颈了,剩下的是 12 位 ADC 量化
[5] 方法三:查表 + 整数线性插值
表间隔 5℃(25 项):最大插值误差 0.20 ℃
表间隔 20℃( 7 项):最大插值误差 1.50 ℃
-> 5℃ 间隔的表 25 项 = 100 字节,比 B 参数法准、比它快
[6] 多点分段标定:把「每块板的系统误差」也标进去
这块板的标定点(理想 ADC vs 实读 ADC):
0.0 ℃:理想 3134.9,实读 3541.0(偏差 +406.2 counts)
25.0 ℃:理想 2047.5,实读 2323.2(偏差 +275.7 counts)
50.0 ℃:理想 1084.6,实读 1244.8(偏差 +160.2 counts)
80.0 ℃:理想 457.6,实读 542.5(偏差 +84.9 counts)
4 点标定(0/25/50/80℃,最大段宽 30℃):最大误差 3.106 ℃,RMS 1.482 ℃
9 点标定(每 10℃ 一点) :最大误差 0.373 ℃,RMS 0.183 ℃
不做标定、直接套标准公式 :最大误差 -11.62 ℃
-> 标定消掉的是「板子本身的系统误差」;段划得越细,线性化残差越小
[7] 查表边界保护
ADC=500(低于表头) -> -200(钳位到最小值 -20.0℃)
ADC=3500(高于表尾)-> 700(钳位到最大值 70.0℃)
ADC=2500(中间) -> 475(插值得 47.5℃)
===== 全部通过:以上数据由本机 gcc 实编译实运行 =====