去年给一套工业循环水系统做温度采集板,我被单片机内置 ADC 直接采热电阻的方案坑得不轻:室温下校得好好的,一到现场跟着变频水泵跑起来,读数能漂出去两三度,而且毫无规律。后来我把前端换成了 TI 的 ADS122U04IPWR,主控继续用那颗老将 ATmega32,整套系统才算真正稳下来。这篇文章不是产品推广,是我从选型、硬件电路、UART 协议到 ATmega32 固件完整走一遍的工程记录,核心就一件事:怎么用一颗带 UART 接口的 24 位 delta-sigma ADC 配 8 位 AVR,把工业和 HVAC 现场的温度测准、测稳。如果你正在做 RTD、热电偶或 NTC 采集板,或者想彻底摆脱 MCU 内置 ADC 的精度瓶颈,这篇文章值得看完。
1. 为什么这组搭配在工业现场能站稳:先看用内置 ADC 测温度的失败前提
很多朋友一上来就问:为什么测个温度要上 24 位 ADC?ATmega32 自带的 10 位 ADC 难道不够用?这个问题我在实验室里也觉得够用,但到了工业现场就不一样了。先把这层窗户纸捅破,后面所有选型理由都顺了。
1.1 单片机内置 ADC 在温度采集上的三个软肋
第一是分辨率不够。10 位 ADC 在 5V 参考下,一个 LSB 大概是 4.88mV。拿 Pt100 来说,0.385Ω/℃ 的变化率,用 1mA 激励时换算成电压只有 0.385mV/℃,也就是说 4.88mV 的量化台阶对应将近 12.7℃。就算你把传感器的信号放大到 10 倍,一个 LSB 也还有 1.2℃ 左右,这还只是量化误差,没算噪声。HVAC 里控温精度要求 ±0.5℃ 的场景都有不少,内置 ADC 很难交代。
第二是参考源和 PGA 太粗糙。MCU 内置 ADC 的参考电压一般直接取自 VCC 或者内部能隙基准,温漂和电源纹波都会直接变成温度误差。工业现场电源线上常年有变频器、接触器带来的毛刺,VCC 本身就不干净,内置 ADC 又很少带可编程增益放大器,小信号直接被噪声淹没。温度传感器输出都是毫伏级别,没有低噪声 PGA 放大,后续分辨率全是空谈。
第三是单端采样抗共模干扰差。内置 ADC 大多是单端输入,RTD 和热电偶在长线传输时引入的共模电压、地电位差,会直接耦合进测量结果。工业现场动辄几十米线缆,电机启停瞬间地电位都能跳好几伏,单端结构在这种环境下基本就是听天由命。
1.2 UART 接口 ADC 为什么比 SPI 更贴合长距离测温
既然要外置 ADC,常见选择是 SPI 接口的 ADS1220 或者 ADS124S08,我却选了 UART 接口的 ADS122U04IPWR。原因很现实:SPI 需要至少四根线(SCLK、MOSI、MISO、CS),而且时钟线对走线长度和信号完整性非常敏感。线一长,SCLK 边沿变缓,数据采样就出错,尤其是单片机那边 GPIO 模拟 SPI 的话,抗干扰更是雪上加霜。
ADS122U04IPWR 走的是 UART,硬件上只需要 TX、RX 两根线,加上 GND 就完事。现场布线时省一半导线不说,UART 本身就是异步串行,不依赖时钟边沿采样,只要波特率误差控制在 ±2% 以内,线缆长度和寄生电容的影响比 SPI 小得多。HVAC 机房里传感器分散在风管、水管、机房各个角落,线缆动不动拉十几米,UART 这种拓扑显然更皮实。而且 ATmega32 自带硬件 USART,底层驱动几乎不占 CPU 时间,这对 8 位 MCU 来说很关键。
2. ADS122U04IPWR 的资源盘点与温度量程计算
芯片选好了,接下来要搞清楚里面到底有什么,以及怎么把量程算明白。这一步我见过太多人直接抄参考设计,连增益和激励电流都不算,测出来的数据自然对不上。
2.1 芯片里到底集成了什么:24 位核心、PGA、双 IDAC 与内置温度传感器
ADS122U04IPWR 的核心是 24 位 delta-sigma ADC,采样速率可以根据需要配置,低速挡能获得更高的有效分辨率。芯片内部集成了可编程增益放大器,增益范围覆盖 1 到 128,配合内部低温漂基准,能把毫伏级别的传感器信号放大到合适的幅度再做模数转换。这一点对热电偶特别重要,因为热电偶输出只有几十微伏每摄氏度,没有 PGA 根本没法看。
它还内置了两路可编程电流源,型号里所谓 IDAC,这是给 RTD 测量准备的。RTD 本身是无源电阻,必须给它一个恒定的激励电流才能把电阻变化转成电压。内部 IDAC 的好处是省掉了外部恒流源电路,而且可以通过配置寄存器精确设定电流大小。另外芯片里还放了一个内部温度传感器,这个在热电偶冷端补偿时非常有用,后面细说。
封装是 TSSOP-16,工业级温度范围,工作环境从 -40℃ 到 125℃ 都能扛住,上到 HVAC 的室外机、下到工业管道旁的小仪表箱都够用。我选它还有一个重要原因是供电范围宽,3.3V 和 5V 系统都能直接接,和 ATmega32 的 5V 逻辑电平打交道时少一层转换。
2.2 Pt100 方案:激励电流与增益计算示例
我用 Pt100 举例把计算过程完整走一遍,这套方法你在任何电阻型传感器上都通用。Pt100 在 0℃ 时电阻为 100Ω,温度每升高 1℃,阻值增加约 0.385Ω。激励电流我选了 500µA,而不是一些人习惯的 1mA,主要考虑是抑制自热效应。500µA 在 138.5Ω(100℃ 时的阻值)上只产生不到 35µW 的功耗,热电阻自身的温升可以忽略不计。如果为了信号幅度用 1mA,自热效应在精密测量里很容易引入额外零点几度的误差。
0℃ 时传感器压降是 100Ω × 500µA = 50mV;100℃ 时是 138.5Ω × 500µA ≈ 69.25mV。这个信号水平需要经过 PGA 放大。我用了 PGA 增益 8,配合内部基准 2.048V,满量程输入范围就是 2.048V / 8 = 256mV。信号最大 69.25mV,只占了满量程的 27% 左右。有人会想那为什么不用增益 16?因为增益越高,输入等效噪声和失调也越明显,而且 24 位 ADC 在低增益下的线性度更好。69.25mV 对 256mV 满量程来说并不算小,20SPS 采样率下有效分辨率能达到微伏级别,对应到温度大约 0.002℃ 的量化潜力,足够任何 HVAC 场景用了。
还有一个重要的细节:要让 IDAC 的激励电流同时流过基准电阻,用外部基准电压做 ADC 参考。这样激励电流的波动会同时影响传感器信号和参考电压,在比率测量里被自动抵消。温度传感器测量最怕电流源漂移,这一招能直接把电流温漂误差消掉一大半。
2.3 Pt1000 和热电偶的变体接法
HVAC 里风管温度、回水温度经常用 Pt1000,因为阻值大,线缆电阻的影响相对小。Pt1000 在 0℃ 时为 1000Ω,我习惯把激励电流降到 100µA,这时 0℃ 压降 100mV,100℃ 时 1385Ω 压降 138.5mV,依然落在 PGA 增益 8、满量程 256mV 的工作区内。每摄氏度电压变化约 0.385mV,分辨率比 Pt100 更高。线阻的影响也更直观:假设现场拉了 100 米两芯屏蔽线,回路电阻 10Ω,折合温度误差只有 10Ω / 3.85Ω/℃ ≈ 2.6℃。如果用力 Pt100 配 500µA,同样 10Ω 线阻误差是 10Ω / 0.385Ω/℃ ≈ 26℃。差距就是这么明显。所以长线测温时,要么用 Pt1000,要么老老实实做三线制或者四线制。
热电偶则是另一套玩法。热电偶输出的是热电势而不是电阻,K 型热电偶大约是 41µV/℃,信号比 RTD 微弱得多。这时 PGA 增益我直接放到 64 或者 128,否则小信号直接被 ADC 的噪声吃掉。冷端补偿必须做,直接用芯片内置温度传感器是最省事的路子。你得保证热电偶的冷端焊在 PCB 接线端子附近,让冷端温度和芯片温度接近,然后用内部温度传感器的读数去修正热电势。这里有一个工程技巧:把热偶的冷端接点和 ADC 芯片尽量贴着放,中间不要有大功率器件,PCB 局部温度就相对均匀,补偿精度自然就上来了。
2.4 与 ATmega32 的连接和电平问题
ADS122U04IPWR 的 UART 是 TTL 电平,ATmega32 的 USART 也是 TTL 电平,两者同电压供电时直接 TX 对 RX、RX 对 TX、地线相连就完事。但如果 ADC 用 3.3V 供电、ATmega32 用 5V 供电,电平匹配就要小心。ATmega32 的 TX 输出是 5V 高电平,直接进 3.3V 器件的 RX 引脚,长期看有超压风险。我的做法是串一个 1kΩ 电阻做限流,同时加一个 3.3V 的钳位二极管,简单可靠。反过来 ADC 的 TX 输出 3.3V,ATmega32 识别高电平的门限大约是 0.6VCC,也就是 3V 左右,3.3V 输出勉强够但裕量不大,最好确认一下 ATmega32 的 VIH 参数,必要时用 MOS 管做电平转换。
3. UART 协议侧的三个坑:时序、波特率与 CRC
UART 接口的 ADC 用起来比 SPI 省线,但协议侧有几个坑,是我调这块板子时真实踩过的。拿出来说说,大家能少走弯路。
3.1 别再拿 SPI 的习惯写 UART 型 ADC 驱动
SPI 是全双工主从结构,主设备拉低 CS 发时钟,从设备同步移位,时序是死的。UART 型 ADC 不一样。它是一问一答式,主机先发命令帧,芯片处理完之后回一帧响应。这个处理时间不是固定的, datasheet 里写着要等多久,不同命令类型可能还不一样。第一次调驱动时,我按照 SPI 的习惯发完命令立刻去读响应,结果读回来的全是垃圾或者超时。后来改成"发命令 → 等待固定延时 → 再读响应"的流程,一次就通了。工程上建议在底层驱动里加一个超时计数器,别裸等、也别傻等,超时就重发。
还有一点:有些 UART 型 ADC 在空闲状态下会持续输出填充字节或者保持高电平。如果你的驱动把每个字节都当成有效数据,就会被这些填充字节冲昏头。我处理的办法是在解析响应帧时先找帧头同步,然后按长度字段截帧,再校验 CRC,三者都过了才认这一帧。这个套路对这类芯片都适用。
3.2 波特率误差控制与 ATmega32 的 URSEL 坑
UART 通信里波特率误差是硬指标。ATmega32 是 8 位 MCU,波特率寄存器 UBRR 是 12 位的,但注意 UCSRC 和 UBRRH 是复用同一个 I/O 地址。ATmega32 有一个叫 URSEL 的选择位,往这个地址写数据时,如果 URSEL 置 1 就写 UCSRC,URSEL 清零则写 UBRRH。我第一次初始化时死活配置不进去,就是忘了置 URSEL,结果波特率寄存器被写成了控制寄存器,整个串口行为完全错乱。下面是正确的初始化代码,我加了注释:
#define F_CPU 16000000UL #define BAUD 9600 void uart_init(void) { uint16_t ubrr = F_CPU / 16 / BAUD - 1; UBRRH = (uint8_t)(ubrr >> 8); UBRRL = (uint8_t)(ubrr & 0xFF); UCSRA = 0x00; UCSRB = (1 << RXEN) | (1 << TXEN); // 使能收发 UCSRC = (1 << URSEL) | (1 << UCSZ1) | (1 << UCSZ0); // 8N1 格式 }这里 16MHz 下选 9600 波特率,UBRR 算出来是 103,误差在 0.2% 以内。工业现场的电磁环境不比实验室,波特率误差一旦超过 2%,偶发错帧概率会明显上升,我建议宁可把波特率设低一点,比如 9600 或 19200,换可靠性和抗干扰能力。温度采集本来就不需要高吞吐,一秒钟更新个几次完全够用。
3.3 CRC 校验与错误重试机制
ADS122U04IPWR 的通信帧里带 CRC 校验,这是好事。但 8 位 MCU 上跑 CRC 要注意计算效率,我推荐用查表法。先在 PC 上用脚本把整张 256 项的表生成出来,再直接拷到 C 数组里,运行时就一个查表循环,8 位 MCU 也跑得动。具体多项式以 datasheet 为准,查表法实现的框架长这样:
uint8_t crc8_update(uint8_t crc, uint8_t byte) { return crc8_table[crc ^ byte]; } uint8_t calc_frame_crc(uint8_t *buf, uint8_t len) { uint8_t crc = 0; for (uint8_t i = 0; i < len; i++) { crc = crc8_update(crc, buf[i]); } return crc; }CRC 通过不代表万事大吉。工业现场总有可能遇到帧被强干扰打到完全变形,光靠 CRC 重发还不够。我的做法是设置一个连续错误计数器,同一帧命令重发三次都失败就主动复位 ADC,再重新初始化寄存器。如果连续失败超过五次,就置一个全局故障标志,上层直接报警。这个机制看起来简单,但真的能把设备从"假死"状态里救回来,比单纯喂狗强多了。
4. ATmega32 固件链路:从裸码到人看的温度
底层协议通了之后,剩下的就是把 ADC 的裸码变成人能读的温度值。这一节我把固件链路完整梳理一遍,重点讲数据解析和换算,因为这块最容易出隐蔽 bug。
4.1 USART 初始化与带超时的底层收发
底层的 UART 收发我封装了两个函数:一个发送命令帧,一个带超时地等待响应帧。发送端很简单,逐字节往 UDR 寄存器写,等 UDRE 标志位清掉再写下一个。接收端要额外处理超时,避免主程序因为一帧坏数据就卡死。我用一个简单的循环等待 TCNT0 计时,超过时间就直接返回错误。核心代码大概这样:
uint8_t uart_send_cmd(uint8_t *buf, uint8_t len) { for (uint8_t i = 0; i < len; i++) { while (!(UCSRA & (1 << UDRE))); UDR = buf[i]; } return 1; } uint8_t uart_wait_resp(uint8_t *buf, uint8_t max_len, uint16_t timeout_ms) { uint16_t timer = 0; uint8_t idx = 0; while (timer < timeout_ms) { if (UCSRA & (1 << RXC)) { buf[idx++] = UDR; if (idx >= max_len) break; } _delay_ms(1); timer++; } return idx; }实际工程里我会对接收到的数据做状态机解析,找帧头、收长度字节、收数据区、收 CRC,全部通过才算一帧完成。这套状态机写起来不复杂,但比直接按固定长度收字节要健壮得多,因为现场总会有多余字节混进来。
4.2 命令封装、数据解析与软件滤波
命令封装就是把寄存器地址、写入数据、CRC 拼成一帧。我把所有 ADC 操作封装成了几个高层接口,比如adc_read_temperature()、adc_config_channel()。这样主循环里代码很干净,可读性也更好。每次读完原始码之后,要把 24 位数据拼成有符号整数,注意字节序,不同芯片的位序定义不一样,建议用联合体或者手动移位,并且加一个已知电压的自检通道验证解析方向是不是对的。这一步如果搞反了,后面温度全是负的,而且你还很难发现。
软件滤波是温度测量必不可少的一环。RTD 和热电偶信号本身很小,现场的 50Hz 工频干扰和气流波动都会叠加在上面。我用了两层滤波:先做一个 16 次滑动平均,把突发毛刺抹平;再做一阶低通滤波,系数取 0.1 到 0.2,把残余高频噪声压下去。代码如下:
int32_t lowpass_filter(int32_t new_sample, int32_t old_value, uint8_t coeff) { return old_value + ((int32_t)(new_sample - old_value) * coeff >> 8); }系数选太大滤波效果差,选太小响应迟钝,HVAC 温度变化本来就慢,我取 32/256,阶跃响应大约几秒内稳定,现场体感很好。
4.3 温度换算:RTD 反查与热电偶查表
RTD 的测温原理是电阻随温度变化,但电阻到温度的关系不是纯线性。Pt100 在 0 到 850℃ 范围可以用 Callendar-Van Dusen 方程描述,反算温度时我通常用查表加线性插值:预先把温度 0℃ 到 200℃ 每 10℃ 一格对应的电阻值存成常量表,然后在运行时代码里二分查找并做线性插值。这样计算量小,精度能满足 0.1℃ 级别,还不会出现解方程溢出这些幺蛾子。
热电偶那边的换算我不是直接用多项式,而是查 NIST 标准分度表的分段折线表。K 型热电偶在 0 到 1000℃ 范围内的非线性用分段线性逼近,20 段左右,每段单独系数,精度做到 ±0.5℃ 没问题,查表耗时对 8 位 MCU 更是可以忽略。冷端补偿读内部温度传感器的值,加到一个等效修正电压里,再参与查表。这个顺序不要搞错:先把冷端温度折成热电势补偿值,再加上测量热电势,最后才反查温度。我见过不少人直接把冷端温度加进结果里,误差放大了好几倍。
5. 实测数据与现场抗干扰的经验清单
软硬件都通了,最后看疗效。这块板子拿到实验室恒温槽、HVAC 机房现场分别跑了一轮,数据和经验都列出来供参考。
5.1 恒温槽与室温环境下的实测表现
恒温槽设 50℃,用 FLUKE 温度计做参照,我这条采集链路连续采集一小时,读数稳定在 49.94℃ 到 50.08℃ 之间,最大波动 0.14℃,标准差大约 0.03℃。这个精度对 HVAC 控制器、工业换热站、冷水机组回水温度监测都绰绰有余。室内常温下测 Pt1000,用高精度电阻箱模拟 1000Ω,换算出来 0.43℃,和标定值误差在 0.05℃ 以内。室温环境下最怕的是空气对流对传感器自热的干扰,所以我把激励电流降到 100µA 后,传感器自身的自热效应已经可以忽略。
有一点要强调:实验室数据再漂亮,现场环境才是照妖镜。我把同样的板子放到工业水泵房连续跑 72 小时,中间变频器频繁启停,数据依然稳定。关键措施就是后面要讲的电源和布线处理。
5.2 电源、地线、布线与看门狗
温度采集板看起来简单,实际上是一个混合信号系统,模拟部分和数字部分必须分开处理。我给模拟电源单独加了一级 LC 滤波,L 取 10µH 的功率电感,C 用 10µF 加 0.1µF 的电容组合。模拟地和数字地在 ADC 芯片底下用 0Ω 电阻单点连接,这样数字噪声不会在模拟回路里形成环路。传感器进来之后,我先接一个差分低通 RC 滤波器,截止频率设在 100Hz 左右,再进入 ADC 输入引脚。这个并联的 1nF 电容能滤掉大部分的射频干扰,是我试过最有效的滤波手段,没有之一。
ATmega32 那边我开了内部看门狗,超时周期 1 秒。主循环里每次成功读完温度并完成换算后喂狗,一旦通信卡死或者状态机跑飞,单片机自动复位,重新初始化 ADC。现场设备维护周期长,没有看门狗的话,一个软件异常可能就让整个系统凉在那里,只能等人工到场断电重启。复位后程序里还加了一个开机自检动作:读一遍 ADC 的寄存器,再回读确认,保证外设真的就位了才进入正常运行状态。
5.3 现场故障排查对照表
这块板子从调试到上线,我也遇到过几个典型故障,整理成一张对照表,大家可以直接对照排查。
| 现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 读数始终为满量程 | 传感器开路、接线端子松动 | 检查 RTD/热偶接线,用万用表量电阻,重新压接端子 |
| 读数始终接近 0 | 传感器短路、激励未打开 | 量输入端是否有正常毫伏电压,检查 IDAC 配置寄存器 |
| 读数跳变、无规律 | 电源纹波过大、输入滤波缺失 | 确认 LC 滤波、差分电容是否焊好,示波器看 VCC 纹波 |
| 通信经常超时 | 波特率误差偏大、TX 串电阻过大 | 用示波器量 TX/RX 波形,确认波特率,重新计算 UBRR |
| 冷端补偿误差大 | 热电偶冷端离芯片太远或有热源 | 冷端强制靠近 ADC 放置,PCB 局部开等温区域 |
| 上电后无响应 | URSEL 位没处理、初始化顺序错 | 检查 UCSRC/UBRRH 写顺序,上电后延时 100ms 再发命令 |
现场排查时先用示波器看串口波形是最快的。UART 波形非常直观,波特率对不对、有没有毛刺、响应帧有没有发出来,一眼就能看出来。比瞎改代码效率高太多。
6. 量产前我会补的几处小设计
板子调通只是第一步,真正要交给产线批量生产时,我还会补几个设计细节,让整套系统更皮实。
首先是传感器开路和短路检测。RTD 开路时输入端会被 IDAC 拉到接近供电电压,读数会冲到满量程;短路时读数接近 0。这两种状态在逻辑上很容易判断,固件里加两个阈值判断就行。开短路超过一秒直接上报故障,比等到系统跑偏了才发现问题强得多。
其次是校准系数的存储。每块板子的 ADC 失调、PGA 增益误差都不完全一样,产线校准时用恒温槽做两点校准,把零点和增益误差系数写进 ATmega32 的 EEPROM。机器烧录程序时顺便写零校准值,或者提供一条串口命令现场校准。板子之间的一致性问题,靠这个才能彻底解决。EEPROM 里我还会存设备地址、波特率、传感器类型这些参数,这样换一块板子不用改代码,现场配置一下就行。
最后是软硬件版本标识。固件里编译时自动生成版本号,EEPROM 里存硬件版本,调试器和上位机都能读出来。设备出问题打电话回来,运维人员报个版本号,我就能知道是哪版代码哪版硬件的问题,省掉了大量沟通成本。这些小设计不复杂,但对一个要长期运行在工业现场的产品来说,每一处都能省下真金白银的维护费用。
这套方案做完之后,我最深的一个体会是:温度测量很少真的难在算法或者芯片选型上,大多数翻车现场都出在信号链路的细节里。激励电流选多少、增益放多大、地线怎么走、波特率误差控不控,这些看起来琐碎的问题,一个接一个处理干净,系统自然就稳了。ADS122U04IPWR 和 ATmega32 这对组合可能不是最新潮的方案,但胜在每一环都通用、可靠、便宜,无论你是在做 HVAC 控制器、工业数据采集器,还是设备改造,这套设计思路都能直接搬过去用。