MLX90640热像仪里的45℃为何总在变色

2026-10-263

MLX90640热像仪里的45℃为何总在变色

盯着一块45℃的区域看,它原来偏黄,画面里进入一个更热的物体后,它却变成了青色。被测区域没有降温,颜色先“降温”了。

这类现象很容易让人去查传感器、电源,甚至怀疑校准。其实还有一个更近的原因——显示程序每帧都把最低温和最高温重新拉满色表。同一种颜色在两帧里,可能已经代表两个温度。

排查板卡发热点、比较散热改动前后,恰好需要跨画面比较。本篇把MLX90640的显示链改成固定温标,并补齐等温帧、无效数值和热点坐标的边界处理。完整C代码及回归用例放在正文,复制为一个文件即可运行。

同一温度在自动与固定温标下的对比

图1为合成温度场,四幅图均调用下文的C模块渲染。白圈内恒为45℃,右侧方块从60℃改为100℃;它不是传感器实拍,也不能用来证明测温精度。

先让颜色有固定含义

每帧自动温标常用下面的映射关系,再将结果限制在0~255。

颜色索引 ≈ round(255 × (T - 本帧最低温) / (本帧最高温 - 本帧最低温))

场景A的范围是20~60℃,45℃映射为159。场景B最高温升到100℃,同一个45℃只映射为80。色表没换,颜色却变了。

固定温标把分母和起点换成设置值。选20~80℃时,两个场景里的45℃都映射为106。这个范围只是本例观察板级温升的显示设置,不能当成传感器的量程或报警阈值。

自动温标适合寻找微弱温差;固定温标适合前后比较。代价也很直接——超出显示范围的点都挤在端点颜色上。因此,最高温必须继续从原始温度数组统计,另外显示“超上限点数”。把100℃夹成红色可以,把数值改成80℃就会掩盖问题。

图1右下角有25个点超过80℃,它们仍参与最高温统计。图中“超上限”指超过当前显示温标,不是传感器故障。

确认读到的是哪一路温度

MLX90640提供32×24个测温像元,共768点。320×240屏幕上的细腻颜色由放大和插值得到,不等于有76800个独立测温点。

实际EDA中的MLX90640接口

图2来自实际EDA工程的REV1.0原理图。U7为MLX90640,R23、R24均为4.7kΩ,上拉到VCC;C28是0.1µF去耦。图中器件引脚定义为1脚SDA、2脚供电、3脚地、4脚SCL,与官方手册引脚表对应。不要按封装照片的朝向猜脚位。

工程的I²C驱动实际使用hi2c1,CubeMX配置及初始化代码对应PB8为SCL、PB9为SDA,AF4、开漏。默认7位地址为0x33,HAL调用处再左移一位;不要在上层先写0x66,底层又移一次。

供电应按3.3V系统核对,不能看到“VCC”就接5V。4.7kΩ也不是任何总线长度和速率都适用的答案:本工程配置了Fast Mode Plus,实际总线还需根据电容和上升时间测量决定速率、上拉及走线。本文不据一个初始化值宣称总线波形已经合格。

实际EDA的PCB顶面

图3为同一工程PCB顶面。右侧可找到R23、R24和C28;它们给“原理图里那两只上拉电阻”提供了实际位置参照。

实际EDA的PCB底面

图4从底面观察,已按底面视角镜像,U7在图左侧。它与图3左右关系不同,不能把两张图都按顶面视角对孔位。两图仅调整图层配色、裁去留白,未改走线。

这次改动限定在温度数组到显示缓冲区之间。EDA工程中的主控符号复用了其他型号的部分复用功能文字,不能拿符号上的长引脚名替代STM32H7B0引脚表;整板制板、充电保护和相机接口不属于本次已验证的硬件复现结论。

显示函数还有两处容易踩空

工程现有显示函数用(tmax-tmin)/255作步长。若768点全是35℃,跨度为零,后续会除零;把结果强制转整数再取模,并不能修复这种输入。

另一个问题藏在最高、最低点坐标。极值先设成第一个像素的温度,坐标却只在遇到严格更大或更小的值时赋值。等温帧没有触发赋值,光标便可能使用未初始化坐标。

下面的实现把三件事分开处理。

  • 温标先验收。非有限端点、零跨度和反向范围一律返回错误。
  • 原始温度单独统计。首个有效点同时初始化数值和坐标;并列极值取显示扫描顺序的第一个点。
  • 显示再映射。范围外饱和到0或255,NaN及正负无穷使用独立标记256,不转成普通温度颜色。

坐标也使用显示坐标系。开启水平镜像后,传感器数组的(0,0)对应屏幕网格(31,0);最高温的十字光标必须一起翻转。代码统计的是768个测温点,不能到插值后的大图里反推“更精确”的温度。

一个文件跑完显示和边界回归

将下面整段保存为thermal_scale.c。不定义THERMAL_TEST时,它是显示模块;定义后带可执行的主机测试。INVALID对应品红色,正式界面应同时显示无效点数,避免与正常热色混淆。

/* Independent display module, v1.0.0, CC0-1.0. C99, no sensor driver. */
#include <stdint.h>
#include <stddef.h>
#if defined(THERMAL_FREESTANDING)
#define FINITE(x) __builtin_isfinite(x) /* GCC / Clang, no libc required */
#else
#include <math.h>
#define FINITE(x) isfinite(x)
#endif

enum { TW = 32, TH = 24, TN = TW * TH, INVALID = 256 };
typedef struct {
    unsigned valid, bad, below, above;
    float min, max;
    uint16_t min_pos, max_pos; /* display coordinates: y*32+x */
} ThermalStats;

/* src and index must not overlap; st must not alias either array.
 * Returns 1 with valid data, 0 for an entirely invalid frame, -1 on bad args.
 * The caller supplies a complete, stable 768-point temperature snapshot. */
int thermal_map(const float *src, float lo, float hi, int mirror,
                uint16_t *index, ThermalStats *st)
{
    unsigned p;
    float span;
    if (!index || !st) return -1;
    *st = (ThermalStats){0};
    for (p = 0; p < TN; ++p) index[p] = INVALID;
    span = hi - lo;
    if (!src || !FINITE(lo) || !FINITE(hi) ||
        !FINITE(span) || span <= 0.0f) return -1;
    for (p = 0; p < TN; ++p) {
        unsigned x = p % TW, y = p / TW;
        unsigned q = y * TW + (mirror ? TW - 1u - x : x);
        float t = src[q];
        if (!FINITE(t)) { ++st->bad; continue; }
        if (st->valid == 0 || t < st->min) {
            st->min = t; st->min_pos = (uint16_t)p;
        }
        if (st->valid == 0 || t > st->max) {
            st->max = t; st->max_pos = (uint16_t)p;
        }
        ++st->valid;
        if (t < lo) { ++st->below; index[p] = 0; }
        else if (t > hi) { ++st->above; index[p] = 255; }
        else {
            float u = (t - lo) / span;
            if (u < 0.0f) u = 0.0f;
            if (u > 1.0f) u = 1.0f;
            index[p] = (uint16_t)(u * 255.0f + 0.5f);
        }
    }
    return st->valid ? 1 : 0;
}

/* RGB565 palette, blue-cyan-yellow-red; magenta indicates invalid input. */
uint16_t thermal_rgb565(uint16_t k)
{
    unsigned r, g, b, a;
    if (k > 255) return 0xF81Fu;
    if (k < 85) { r = 0; g = k * 3u; b = 255; }
    else if (k < 170) {
        a = (k - 85u) * 3u; r = a; g = 255; b = 255u - a;
    } else { a = (k - 170u) * 3u; r = 255; g = 255u - a; b = 0; }
    return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
}

/* Fill existing 320*240 RGB565 framebuffer with 10x nearest-neighbor pixels.
 * This changes display size, not the sensor's 32*24 measurement resolution.
 * The application must publish the buffer only when DMA is not reading it. */
void thermal_expand(const uint16_t *index, uint16_t *rgb)
{
    unsigned y, x;
    for (y = 0; y < 240; ++y)
        for (x = 0; x < 320; ++x)
            rgb[y * 320 + x] = thermal_rgb565(index[(y / 10) * TW + x / 10]);
}

#ifdef THERMAL_TEST
#include <stdio.h>
#undef NDEBUG /* Keep test expressions active even with compiler defaults. */
#include <assert.h>
#include <stdlib.h>

static float a[TN];
static uint16_t idx[TN], fb[320 * 240];
static ThermalStats s;
static void uniform(float t) { unsigned i; for (i=0;i<TN;++i) a[i]=t; }
int main(int argc, char **argv)
{
    unsigned i;
    FILE *f;
    uniform(35.0f);
    assert(thermal_map(a,20,80,1,idx,&s)==1);
    assert(s.valid==768 && s.min_pos==0 && s.max_pos==0);
    for (i=0;i<TN;++i) assert(idx[i]==64);
    puts("PASS isothermal: 768 points index=64, min/max display=(0,0)");
    uniform(45); a[0]=20; a[1]=60;
    assert(thermal_map(a,20,80,0,idx,&s)==1 && idx[2]==106);
    a[1]=100;
    assert(thermal_map(a,20,80,0,idx,&s)==1 && idx[2]==106 && s.above==1);
    assert(s.max==100 && s.max_pos==1);
    puts("PASS scene change: 45 C index=106 in both scenes, 100 C retained");
    uniform(40); a[0]=10; a[1]=90; a[2]=NAN; a[3]=INFINITY;
    a[4]=-INFINITY; a[5]=20; a[6]=80;
    assert(thermal_map(a,20,80,0,idx,&s)==1);
    assert(s.valid==765 && s.bad==3 && s.below==1 && s.above==1);
    assert(idx[0]==0 && idx[1]==255 && idx[2]==INVALID);
    assert(idx[5]==0 && idx[6]==255 && s.min==10 && s.max==90);
    puts("PASS invalid values and saturation: valid=765 bad=3 low=1 high=1");
    uniform(40); a[0]=80;
    assert(thermal_map(a,20,80,1,idx,&s)==1 && s.max_pos==31 && idx[31]==255);
    assert(thermal_map(a,20,80,0,idx,&s)==1 && s.max_pos==0);
    puts("PASS mirror: sensor (0,0) maps to display (31,0)");
    uniform(NAN); assert(thermal_map(a,20,80,0,idx,&s)==0 && s.bad==768);
    assert(thermal_map(a,80,20,0,idx,&s)==-1 && s.valid==0);
    assert(thermal_map(a,20,20,0,idx,&s)==-1);
    assert(thermal_map(a,NAN,80,0,idx,&s)==-1);
    for (i=0;i<TN;++i) assert(idx[i]==INVALID);
    puts("PASS all-invalid and invalid range: no fabricated extrema");
    for (i=0;i<TN;++i) a[i]=20.0f+(float)(i%32)*60.0f/31.0f;
    assert(thermal_map(a,20,80,0,idx,&s)==1);
    for (i=1;i<TN;++i) if(i%32) assert(idx[i]>=idx[i-1]);
    thermal_expand(idx,fb);
    for(i=0;i<320*240;++i)
        assert(fb[i]==thermal_rgb565(idx[(i/320/10)*32+(i%320/10)]));
    assert(thermal_rgb565(0)==0x001F && thermal_rgb565(255)==0xF800);
    assert(thermal_rgb565(INVALID)==0xF81F);
    puts("PASS monotonic mapping and all 76800 framebuffer pixels");
    if(argc==2) {
        f=fopen(argv[1],"wb"); if(!f) return 2;
        if(fwrite(fb,sizeof(fb),1,f)!=1) { fclose(f); return 3; }
        fclose(f); puts("WROTE RGB565 synthetic gradient");
    }
    return 0;
}
#endif

已有C99编译器时,可运行以下命令。测试中主动取消了NDEBUG,防止编译器默认配置把断言表达式连同被测调用一起删掉。

cc -std=c99 -Wall -Wextra -Werror -O1 -DTHERMAL_TEST thermal_scale.c -o thermal_test
./thermal_test

Windows命令行下运行生成的thermal_test.exe。本次使用Zig 0.13.0所带Clang完成主机执行,另以thumb-freestanding-eabi、cortex_m7、THERMAL_FREESTANDING完成目标文件编译。目标编译只验证了该模块,不代表已经链接或烧录整机固件;不要启用会假定不存在NaN/Inf的fast-math选项。

回归用例和目标文件编译结果

图5按真实运行记录排版。等温35℃得到768个索引64;45℃在两个场景里均为106;包含NaN和正负无穷的用例统计到765个有效点、3个无效点。另对76800个放大像素逐点核对,确认其索引来源与最近邻规则一致。

接进热像仪时还要守住两道边界

模块输入必须是一份完整、稳定的温度快照。MLX90640的棋盘模式分成两个子页,温度计算会更新相应像元;不能把一次子页更新直接理解成全画面都已重新测量。官方手册第10.4~10.6节说明了刷新与子页关系。

在现有采集状态机里,应在读数和校验成功后累计子页0、1,收齐后再发布快照。出现I²C错误或切换模式时清掉子页进度;超过应用允许的帧龄也要丢弃,不能把很久以前的半帧凑进来。两个子页是先后采样,仍有时间差,不能写成“同步曝光”。本文模块负责完整数组的显示,不替代采集驱动。

调用时,把索引数组放在静态区,复用已有的显示帧缓冲。不要在栈上再摆一份153600字节的大数组。主控还必须在DMA不读取的缓冲上生成图像,再按现有驱动的换帧机制交付;启用D-cache时另按内存区域及DMA配置处理缓存一致性,不能靠给数组加volatile解决。

/* Place after the complete thermal_scale.c module, or declare its API in a header. */
static uint16_t color_index[TN];
static ThermalStats stats;
/* snapshot[768] is a validated, stable temperature frame.
   backbuffer[320*240] belongs exclusively to the rendering side here. */
int ok = thermal_map(snapshot, 20.0f, 80.0f, 1, color_index, &stats);
if (ok == 1) {
    thermal_expand(color_index, backbuffer);
    /* Draw labels and markers; then publish through the existing LCD driver. */
    /* hottest grid x = stats.max_pos % TW, y = stats.max_pos / TW */
} else {
    /* Show acquisition/range error; do not present old extrema as fresh data. */
}

上面是集成位置示意,snapshot和backbuffer由采集及LCD驱动提供,不是可直接烧录的整机程序。10倍最近邻放大有意保留像元边界;若需要平滑显示,可在显示层插值,极值和报警仍使用原始有效温度。

固定温标让前后截图可比较,但无法修正发射率、反射环境温度、距离和目标尺寸造成的测温误差。工程中写死的发射率0.95及环境温度偏移也不能通用于所有表面。记录截图时,至少同时记录温标范围、色表、发射率设置、测量距离与环境条件,否则看起来同样红,并不代表测量条件相同。

资料与验证范围:硬件参照wesd的“手持红外热成像”,项目页面声明CERN Open Hardware License,未注明细分版本;原理图REV1.0及同工程PCB由实际EDA重新导出。项目及2023年补充软件位于 https://oshwhub.com/wesd/h7b0-re-cheng-xiang 。传感器依据Melexis MLX90640手册Rev.012,https://www.melexis.com/en/documents/documentation/datasheets/datasheet-mlx90640 。工程内Melexis驱动保留Apache-2.0声明,本文未复制发布该驱动。正文独立显示模块v1.0.0按CC0-1.0提供,完整代码已内嵌,无下载附件。验证限于源码与接口核对、合成输入回归及Cortex-M7对象编译,未进行实体测温或整机帧率测试。

技术交流,欢迎关注微信公众号:美男子玩编程。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -