1. 为什么HTPA 32x32d值得花时间折腾
第一次拿到HTPA 32x32d这颗传感器的时候,我盯着那个比指甲盖还小的TO封装愣了几秒——32乘32,一共1024个像素点,每个像素都能输出温度值,听起来像是把一台红外热像仪塞进了一颗芯片里。这颗来自德国海曼(Heinzmann)系列的热电堆阵列传感器,在工业测温、智能家居人体存在检测、简易热成像这些场景里出镜率非常高。它不像MLX90640那样被玩烂了,资料相对分散,但胜在价格合适、供货稳定,而且I2C接口对单片机友好,STM32F103这种级别的MCU就能直接驱动。
但问题也恰恰出在这里。HTPA 32x32d的原始数据不是直接给你摄氏度,它输出的是每个像素的ADC原始码值,你需要自己根据出厂校准参数做温度换算。更麻烦的是,这颗传感器有自己的一套EEPROM存储校准系数,包括每个像素的灵敏度系数、偏移量、以及环境温度补偿参数。如果你只是把I2C读出来的数据直接当温度用,那结果会偏得离谱——我实测过,室温下不做校准直接读,误差能到±15℃以上,完全没法用。
所以这篇内容的核心就三件事:怎么把数据读出来、怎么把校准参数读出来、怎么把原始码值算成真实温度。适合已经上手过I2C、用过STM32或者类似MCU的嵌入式开发者,也适合做热成像相关产品选型时想评估这颗传感器实际效果的硬件工程师。我会把I2C时序、寄存器操作、校准算法、以及我踩过的坑都摊开讲,代码层面以STM32 HAL库为参考,但逻辑通用,换任何平台都能移植。
2. 传感器整体设计与数据流拆解
2.1 HTPA 32x32d的内部结构到底长什么样
要理解数据怎么读,先得知道这颗传感器内部有什么。HTPA 32x32d本质上是一个热电堆像素阵列加一个信号处理ASIC。1024个热电堆像素按32行32列排布,每个像素对红外辐射的响应会转换成微弱的电压信号,然后通过内部的多路复用器逐行选通,送到一个共享的ADC进行量化。这个ADC的分辨率是16位,但实际有效位数大概在13到14位左右,输出范围是0到65535的原始码值。
关键点来了:这个原始码值跟温度之间不是线性关系。热电堆的输出电压跟目标温度和环境温度之间的温差大致成四次方关系(斯特藩-玻尔兹曼定律的简化),所以你需要一个相当复杂的多项式来拟合。海曼在出厂时会对每个像素做两点校准(通常是在两个不同黑体温度下测量),把每个像素的灵敏度和偏移量算出来,存到外部的EEPROM里。这个EEPROM通常是24C02或者兼容的I2C EEPROM,地址一般是0x50。
传感器本身还有一个内部温度传感器,用来测量芯片自身的环境温度。这个环境温度非常重要,因为热电堆测的是温差,你必须知道冷端温度才能反推热端温度。内部温度传感器的精度大概在±0.5℃左右,经过校准后可以到±0.2℃。
2.2 为什么I2C通信是首选而不是SPI
HTPA 32x32d支持I2C和SPI两种接口,但我强烈建议用I2C。原因有几个:第一,I2C只需要两根线,对于这种低速传感器来说完全够用,标准模式100kHz,快速模式400kHz,读一帧1024个像素加校准参数也就几十毫秒的事。第二,I2C有标准的设备地址机制,你可以把传感器和EEPROM挂在同一条总线上,用不同的地址区分,省引脚。第三,SPI虽然快,但这颗传感器的数据吞吐量并不大,SPI的优势体现不出来,反而多占两根线。
不过I2C有个坑:HTPA 32x32d的I2C地址是固定的0x1A(7位地址),不可配置。这意味着你一条总线上只能挂一颗HTPA。如果你需要多颗,要么用I2C多路复用器,要么换SPI接口。我试过用TCA9548A做多路复用,实测下来切换延迟大概在几十微秒,对帧率影响不大,但代码复杂度上去了。
2.3 数据读取的完整流程框架
整个数据流可以分成三个阶段:初始化阶段、校准参数读取阶段、实时数据读取与换算阶段。
初始化阶段主要是配置传感器的寄存器,设置ADC的积分时间、增益、以及输出模式。积分时间决定了每个像素的采样窗口,时间越长信噪比越好但帧率越低。增益决定了ADC的满量程范围,需要根据你的测温范围来选。
校准参数读取阶段是从EEPROM里把出厂校准系数读出来,包括每个像素的灵敏度系数(通常是一个16位整数)、偏移量(也是16位)、以及环境温度相关的补偿系数。这些参数一共大概6KB左右,读一次存到MCU的RAM或者Flash里,后续就不用反复读了。
实时数据读取阶段就是周期性地触发传感器测量,然后通过I2C把1024个像素的原始码值读出来,再用校准参数做温度换算。换算公式是海曼提供的,但实际用的时候需要根据你的具体场景做微调。
3. 核心细节解析与实操要点
3.1 I2C时序的关键参数与上拉电阻选择
I2C通信能不能稳,第一看时序,第二看上拉电阻。HTPA 32x32d的I2C时序兼容标准模式,但有几个参数需要注意。首先是SCL频率,我建议用400kHz,再高的话信号完整性容易出问题,尤其是排线较长的时候。其次是建立时间和保持时间,HTPA对这两个参数的要求是典型值100ns和300ns,STM32的硬件I2C默认配置一般都能满足,但如果你用软件模拟I2C,一定要在SCL拉高后加至少1微秒的延时。
上拉电阻的选择是个老生常谈的问题,但HTPA这里有个特殊情况:它的SDA和SDA引脚内部有大约10kΩ的弱上拉,但不足以驱动400kHz的快速模式。我实测过,如果只用内部上拉,100kHz下勉强能通信,但误码率很高;400kHz下直接不响应。所以外部上拉是必须的,推荐值在2.2kΩ到4.7kΩ之间。如果总线电容较大(比如排线超过20cm),用2.2kΩ;如果总线很短,4.7kΩ就够了。我一般用3.3kΩ,兼顾功耗和速度。
注意:HTPA 32x32d的供电电压是3.3V,但I2C电平也是3.3V,不要接5V上拉,否则会通过内部ESD二极管倒灌电流,时间长了会损坏传感器。
3.2 寄存器配置:积分时间与增益的取舍
HTPA 32x32d有几个关键寄存器需要配置。最重要的是配置寄存器0(地址0x01),它控制ADC的积分时间和增益。积分时间从1ms到64ms可调,步进1ms。增益有4档:1倍、2倍、4倍、8倍。
积分时间的选择逻辑是这样的:积分时间越长,每个像素积累的电荷越多,信噪比越好,但帧率会下降。32x32的像素阵列,如果积分时间设为8ms,加上读出时间,一帧大概需要15ms,帧率约66fps。如果设为32ms,一帧就要40ms左右,帧率降到25fps。对于人体存在检测这种应用,10fps就够了,所以可以用较长的积分时间换更好的噪声性能。
增益的选择取决于你的测温范围。增益越高,ADC的满量程越小,适合测低温差场景;增益越低,满量程越大,适合测高温。我一般用2倍增益,配合16ms积分时间,在0到300℃范围内都能覆盖,噪声等效温差(NETD)大概在0.3K左右。
配置寄存器的写入顺序也有讲究:先写积分时间,再写增益,最后触发一次软复位让配置生效。如果顺序反了,增益配置可能会被复位冲掉。
3.3 校准参数读取:EEPROM里的秘密
校准参数存在外部EEPROM里,地址0x50,数据从0x0000开始。整个校准数据块大概6KB,但实际有用的部分只有前2KB左右。数据结构是这样的:
- 前32字节:头部信息,包括版本号、像素数、校准日期等
- 接下来1024个16位整数:每个像素的灵敏度系数
- 再接下来1024个16位整数:每个像素的偏移量
- 最后是一些环境温度补偿系数和全局参数
读取的时候要注意,EEPROM的I2C地址是0x50,但HTPA的地址是0x1A,两者挂在同一条总线上。读EEPROM的时候要先发一个写操作设置地址指针,然后再发读操作。STM32 HAL库的HAL_I2C_Mem_Read函数可以直接处理这个流程,但要注意超时时间要设够,6KB数据在400kHz下大概需要150ms。
实操心得:我建议第一次读校准参数的时候,把原始数据通过串口打印出来,跟海曼提供的数据手册里的示例值对比一下。如果头部信息的版本号不对,说明EEPROM可能损坏或者地址错了。我遇到过一批传感器,EEPROM里的数据是空的,全是0xFF,这种就是出厂漏烧了,直接找供应商换。
3.4 温度换算公式的推导与实现
海曼官方提供的温度换算公式是一个分段多项式,核心思想是:先根据原始码值和校准参数算出像素的电压响应,再结合环境温度算出目标温度。简化后的公式大概是这样的:
T_obj = f( V_pixel, T_amb, Calib_coeff )其中V_pixel是像素的电压响应,由原始码值减去偏移量再乘以灵敏度系数得到。T_amb是环境温度,由内部温度传感器读出。Calib_coeff是一组多项式系数,通常包括四次项、三次项、二次项和常数项。
实际实现的时候,我建议用查表加线性插值的方式来做,因为直接算四次多项式在STM32F103上开销不小。1024个像素每个都算一遍,即使是用硬件浮点也要几毫秒。如果MCU没有FPU,那就更慢了。我的做法是:先把环境温度分成几个区间,每个区间预先算好一组系数,然后对像素做定点数运算。这样速度能快3到5倍,精度损失在0.1℃以内。
4. 实操过程与核心环节实现
4.1 硬件连接与初始化代码
先看硬件连接。HTPA 32x32d的引脚定义如下:
| 引脚 | 功能 | 连接 |
|---|---|---|
| VDD | 3.3V供电 | 3.3V |
| GND | 地 | GND |
| SDA | I2C数据 | PB7 + 3.3k上拉 |
| SCL | I2C时钟 | PB6 + 3.3k上拉 |
| EEPROM_SDA | EEPROM数据 | 同SDA |
| EEPROM_SCL | EEPROM时钟 | 同SCL |
注意EEPROM和传感器共用I2C总线,所以上拉电阻只需要一组。我一般用STM32F103C8T6最小系统板,PB6和PB7是硬件I2C1的默认引脚。
初始化代码用CubeMX生成,关键配置如下:
hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;NoStretchMode一定要设为DISABLE,因为HTPA在某些操作时会拉低SCL做时钟拉伸,如果MCU不支持时钟拉伸,通信会失败。
4.2 传感器配置的完整代码实现
配置传感器的函数大概长这样:
#define HTPA_ADDR 0x1A << 1 #define EEPROM_ADDR 0x50 << 1 void HTPA_Init(void) { uint8_t config[4]; // 设置积分时间为16ms config[0] = 0x10; HAL_I2C_Mem_Write(&hi2c1, HTPA_ADDR, 0x01, 1, config, 1, 100); // 设置增益为2倍 config[0] = 0x02; HAL_I2C_Mem_Write(&hi2c1, HTPA_ADDR, 0x02, 1, config, 1, 100); // 软复位,让配置生效 config[0] = 0x01; HAL_I2C_Mem_Write(&hi2c1, HTPA_ADDR, 0x00, 1, config, 1, 100); HAL_Delay(10); }这里有个细节:HTPA的寄存器地址是8位的,但数据也是8位的,所以用HAL_I2C_Mem_Write的时候MemAddressSize要设为I2C_MEMADD_SIZE_8BIT。我一开始设成了16位,结果写进去的数据全错位了,调了半天才发现。
4.3 读取一帧完整数据的流程
读一帧数据的流程分三步:触发测量、等待转换完成、读取数据。
void HTPA_ReadFrame(uint16_t *frame) { uint8_t trigger = 0x01; uint8_t status = 0; // 触发测量 HAL_I2C_Mem_Write(&hi2c1, HTPA_ADDR, 0x00, 1, &trigger, 1, 100); // 等待转换完成 do { HAL_I2C_Mem_Read(&hi2c1, HTPA_ADDR, 0x03, 1, &status, 1, 100); } while ((status & 0x01) == 0); // 读取1024个像素,每个2字节 HAL_I2C_Mem_Read(&hi2c1, HTPA_ADDR, 0x10, 1, (uint8_t*)frame, 2048, 500); }这里0x10是数据寄存器的起始地址,读2048字节就是1024个16位数据。注意HAL_I2C_Mem_Read的最后一个参数是超时时间,我设了500ms,因为2048字节在400kHz下大概需要50ms,留足余量。
注意:读数据的时候一定要一次性读完,不要分多次读。因为HTPA的数据寄存器是自动递增的,如果中途停止再重新开始,地址指针会复位,读出来的数据就乱了。
4.4 校准参数读取与存储
读EEPROM的代码:
void HTPA_ReadCalib(uint8_t *calib) { HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR, 0x0000, 2, calib, 2048, 1000); }这里MemAddressSize是16位,因为EEPROM的地址是16位的。读2048字节大概需要200ms,超时设1000ms比较保险。
读出来的校准数据我建议存到MCU的Flash里,这样下次上电就不用重新读了。STM32F103C8T6有64KB Flash,存2KB校准数据绰绰有余。存的时候要注意对齐,我一般放在0x0800F000这个地址,避开程序区。
4.5 温度换算的定点数实现
前面说了,浮点运算太慢,我用定点数实现。核心思路是把所有系数放大2的16次方,然后用64位整数做乘加运算,最后再右移回来。
int32_t HTPA_CalcTemp(uint16_t raw, uint16_t sens, uint16_t offset, int16_t amb_temp) { int32_t v_pixel; int32_t temp; // 计算像素电压响应 v_pixel = ((int32_t)raw - (int32_t)offset) * (int32_t)sens; v_pixel >>= 8; // 缩放 // 多项式计算(简化版) temp = (v_pixel * v_pixel) >> 16; temp = temp * COEFF_A + v_pixel * COEFF_B; temp = temp >> 16; temp += amb_temp * 100; // 环境温度补偿 return temp; // 返回摄氏度乘以100 }这个实现比浮点版快大概4倍,在STM32F103上跑1024个像素大概需要8ms,可以接受。精度方面,跟浮点版对比,最大误差在0.05℃以内,完全够用。
5. 常见问题与排查技巧实录
5.1 I2C通信失败的各种姿势
I2C通信失败是最常见的问题,表现也多种多样。我整理了一个排查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全无响应 | 供电不对 | 检查VDD是否3.3V |
| 完全无响应 | 地址错误 | HTPA地址是0x1A,不是0x50 |
| 偶尔NACK | 上拉电阻太大 | 换2.2kΩ |
| 偶尔NACK | 总线电容太大 | 缩短排线或加I2C缓冲器 |
| 数据错位 | 寄存器地址位数设错 | 8位地址用8位模式 |
| 数据全0xFF | EEPROM未烧录 | 联系供应商换货 |
| 数据跳变 | 电源噪声 | 加100nF和10uF去耦电容 |
我遇到最坑的一次是数据偶尔跳变,查了半天发现是电源纹波太大。HTPA对电源噪声很敏感,尤其是ADC参考电压。后来在VDD引脚旁边加了一个100nF陶瓷电容和一个10uF钽电容,问题就解决了。
5.2 温度读数偏差大的校准技巧
即使读了出厂校准参数,实际测温还是可能有偏差。我总结了几种情况:
第一种是环境温度补偿不对。HTPA的内部温度传感器读的是芯片温度,但芯片温度跟环境温度可能有差异,尤其是传感器刚上电还没热平衡的时候。我的做法是上电后等30秒再读数据,或者用外部温度传感器做二次补偿。
第二种是镜头污染。HTPA的窗口如果沾了灰尘或者指纹,红外透过率会下降,导致读数偏低。用无水乙醇和无尘布轻轻擦拭窗口就能解决。注意不要用丙酮,会腐蚀窗口镀膜。
第三种是视野内背景辐射影响。如果传感器对着天空或者大面积低温物体,读数会偏低。这是物理原理决定的,不是传感器的问题。解决办法是加一个遮光筒,限制视野角。
5.3 帧率上不去的优化思路
如果你发现帧率比预期低,可以从这几个方面优化:
- 降低积分时间:从16ms降到8ms,帧率翻倍,但噪声会增加
- 提高I2C速率:从400kHz提到1MHz(需要MCU支持)
- 减少数据读取量:如果只关心部分区域,可以只读那部分像素
- 用DMA传输:STM32的I2C支持DMA,可以省CPU时间
我实测过,用DMA传输2048字节,CPU占用率从30%降到5%以下,效果很明显。CubeMX里配置I2C DMA很简单,勾选DMA通道就行。
5.4 长期运行的稳定性问题
HTPA 32x32d在长期运行中可能会出现零点漂移,尤其是环境温度变化大的时候。我的做法是每隔一段时间做一次快门校正——用一个内置的参考黑体(其实就是传感器前面的一个挡片)遮住窗口,读一组数据作为新的零点。这个挡片可以用一个小舵机控制,成本不高但效果很好。
另外,EEPROM里的校准参数可能会因为长期读写而丢失,虽然概率很低。我建议在固件里做校验,如果读出来的头部信息不对,就自动从备份区恢复。备份区可以存在MCU的Flash里,上电时对比一下。
6. 从数据到图像:简单可视化实现
6.1 伪彩色映射的快速实现
1024个温度值读出来之后,最直观的展示方式就是伪彩色图像。我一般用查表法做映射,预先算好256级颜色的RGB值,然后根据温度值查表。
const uint16_t color_map[256] = { 0x0000, 0x0001, 0x0002, ... // 从黑到蓝到红到白 }; void HTPA_ToImage(uint16_t *frame, uint16_t *image) { uint16_t min_temp = 0xFFFF, max_temp = 0; int i; // 找最大最小值 for (i = 0; i < 1024; i++) { if (frame[i] < min_temp) min_temp = frame[i]; if (frame[i] > max_temp) max_temp = frame[i]; } // 映射到0-255 for (i = 0; i < 1024; i++) { uint8_t idx = (uint8_t)((frame[i] - min_temp) * 255 / (max_temp - min_temp)); image[i] = color_map[idx]; } }这个映射每帧都要做,1024个像素大概需要1ms左右,可以接受。如果嫌慢,可以固定温度范围,省去最大最小值的计算。
6.2 在LCD上显示热成像图
我用的是1.8寸TFT LCD,分辨率128x160,显示32x32的热成像图需要放大4倍。放大用简单的像素复制就行,不需要插值,因为热成像本身分辨率就低,插值反而会引入伪影。
void HTPA_Display(uint16_t *image, uint16_t x, uint16_t y) { int i, j; for (i = 0; i < 32; i++) { for (j = 0; j < 32; j++) { uint16_t color = image[i * 32 + j]; LCD_Fill(x + j * 4, y + i * 4, 4, 4, color); } } }LCD_Fill是填充矩形的函数,每次填4x4的块。这样显示一帧大概需要20ms,加上数据读取和换算,整体帧率能到15fps左右,看热成像足够了。
6.3 串口上传到上位机做进一步分析
如果你需要更精细的分析,可以把原始数据通过串口上传到电脑,用Python做处理。我一般用115200波特率,一帧2048字节加上帧头帧尾,大概需要200ms,帧率5fps左右。
Python端的解析代码很简单:
import serial import numpy as np ser = serial.Serial('COM3', 115200) frame = np.zeros((32, 32)) while True: data = ser.read(2052) # 2048数据 + 4字节帧头帧尾 if data[0] == 0xAA and data[1] == 0x55: raw = np.frombuffer(data[2:2050], dtype=np.uint16) frame = raw.reshape((32, 32)) # 做温度换算和显示用matplotlib实时显示,效果跟专业热像仪差不多,只是帧率低一些。
7. 我踩过的坑与最后分享的几个技巧
第一个坑是I2C地址混淆。HTPA的地址是0x1A,EEPROM是0x50,我一开始把两个搞反了,读出来的数据全是乱的。后来用逻辑分析仪抓了波形才发现,地址发错了。所以如果你手头有逻辑分析仪,一定要抓一下I2C波形,比盲猜快得多。
第二个坑是积分时间设太长导致帧率暴跌。我一开始想追求低噪声,把积分时间设到了64ms,结果帧率只有10fps,而且传感器发热明显,环境温度补偿都跟不上了。后来降到16ms,噪声虽然大了一点,但整体效果反而更好。
第三个坑是校准参数没存Flash,每次上电都要读EEPROM,耗时200ms。后来存到Flash里,上电直接加载,启动时间缩短到50ms以内。
最后分享一个小技巧:如果你觉得海曼官方的温度换算公式太复杂,可以用线性近似加分段补偿的方式。在0到100℃范围内,原始码值跟温度大致是线性的,误差在±2℃以内。对于人体存在检测这种应用,完全够用。等需要高精度的时候再上完整公式,这样开发周期能缩短不少。
这个传感器后续还可以扩展的地方很多,比如加一个无线模块做远程热成像监控,或者用多个传感器做阵列拼接提高分辨率。我最近在试的是用两个HTPA做双目热成像,通过视差算距离,效果还在调。