Administrator
发布于 2026-09-25 / 0 阅读
0
0

温度传感器不准?B 参数公式、Steinhart-Hart 与多点标定的误差实测

一句话结论: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(厂家给的)。 代码里要注意:

  1. 必须用开尔文,算完再减 273.15
  2. logf() 比 log() 快,能省一半时间(STM32F1 无 FPU 时尤其明显)
  3. 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℃

两个结论:

  1. 标定消掉的是"板子本身的系统误差",量级在 10℃ 以上,比换公式重要得多
  2. 段宽直接决定残差。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 clean

include/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 实编译实运行 =====

评论