MAX6675与K型热电偶测温实战:SPI时序、冷端补偿及多路采集
简介面向使用K型热电偶做温度采集的单片机开发者和电子爱好者这份资源围绕MAX6675数字转换器提供C51与STM32两套可直接参考的例程源码并收录多篇应用文档、设计与实验文档覆盖测温原理、芯片手册、冷端补偿到具体工程实现的全流程适合毕设、课程设计或工业测温系统开发查阅。压缩包共177个文件约6.6MB以.c/.h工程源码、Keil工程文件、编译生成的.hex/.axf固件及.pdf/.doc技术文档为主同时保留部分备份与中间文件目录结构清晰便于按需取用。目前已有1955人浏览学习。对需要快速搭建MAX6675温度采集系统或理解热电偶数字转换细节的读者这份资料提供了从理论到代码的配套参考可帮助缩短开发调试周期。1. 为什么工业测温场景总是选 MAX6675 加 K 型热电偶一个电热恒温干燥箱温度想控在 180°C传感器放哪儿都别扭DS18B20 这类半导体器件靠近加热管就会漂封装和引线也扛不住持续高温换成 K 型热电偶直接捆在加热管表面又冒出毫伏级信号和冷端补偿的问题。用运放配合电桥搭模拟调理电路调零、调增益、调冷端补偿一套下来至少半天温漂还不一定满意。MAX6675 就是把这一步废掉内部集成仪表放大器、冷端补偿和 12 位 ADC直接输出 SPI 数字帧单片机只要读 16 位数据再右移三位乘 0.25 就是温度。资料包里包含 C51 和 STM32 两套例程以及多篇 MAX6675 应用文档适合做烤箱、加热台、注塑机料筒、电阻炉这类设备的温度采集。下面直接拆 SPI 时序和这两套代码的移植方式。2. MAX6675 的 SPI 时序、数据格式与温度换算2.1 从毫伏信号到 16 位数据帧K 型热电偶在 0°C 到 1000°C 范围内热电势大约从 0mV 升到 41mV每变化 1°C 约 40µV。如果用单片机 ADC 直接采至少要 16 位以上精度才能分辨 0.25°C而 MAX6675 内置放大器把信号放大后再进 12 位 ADC分辨率做到 0.25°C量程 0°C 到 1023.75°C。单片机看到的是一个 16 位 SPI 帧CS 拉低后 SCK 连续给 16 个时钟SO 在 SCK 下降沿更新数据主机在上升沿采样所以 SPI 相位要配置成 CPOL0、CPHA1也就是 Mode 1。如果配成 Mode 0读到的数据会错开一个相位常见表现是温度值完全不对或者在一个值附近乱跳。2.2 12 位温度数据藏在哪里MAX6675 的 16 位输出帧不是直接拿 16 位当温度只有 D14-D3 这 12 位是温度数据低位 D2 是热电偶开路标志D1 和 D0 没有实际意义。D14-D3 以 12 位温度值的格式存放LSB 对应 0.25°C。因此从单片机读到的原始值 raw 要先看 D2 是否为 1若为 1说明热电偶断线或没插牢。若正常就把 raw 右移 3 位再乘以 0.25。例如 100°C 时12 位温度数据是 100 / 0.25 4000x190在 16 位帧里就是 0x0C80右移 3 位后得到 400。下面表格列出各 bit 含义方便写代码时对照Bit 位字段说明D15未用读回时通常为 0不参与计算D14-D3温度数据12 位数据LSB0.25°C范围 0~4095D2开路标志1 表示热电偶开路0 表示正常D1保留位恒为 0D0三态CS 拉低后为数据帧低位不参与温度计算在实际读取函数中直接定义unsigned int raw;接收 16 位返回值。如果只取温度用raw 3可以同时把 D2 标志位去掉。乘法用 0.25 浮点也可以把raw 3的结果乘以 25 再除以 100用整数保留两位小数这对 51 单片机更友好。2.3 C51 软件模拟 SPI 读取C51 例程里最常见的是用普通 IO 口模拟 SPI因为 51 单片机多数没有硬件 SPI 外设或者例程为了移植方便直接操作引脚。下面这段是从资料包例程里提炼出来的读取函数// spi.h #ifndef __SPI_H__ #define __SPI_H__ #include reg52.h sbit MAX6675_CS P1 ^ 2; // 片选低电平有效 sbit MAX6675_SO P1 ^ 3; // 串行数据输出 sbit MAX6675_SCK P1 ^ 4; // 串行时钟 unsigned int MAX6675_Read(void); #endif// spi.c #include spi.h unsigned int MAX6675_Read(void) { unsigned char i; unsigned int value 0; MAX6675_CS 0; // 片选拉低开始传输 MAX6675_SCK 0; for (i 0; i 16; i) { MAX6675_SCK 1; // 上升沿主机读 SO if (MAX6675_SO) { value | 0x0001; } if (i 15) { value 1; // 左移等待下一位 } MAX6675_SCK 0; // 下降沿从机更新 SO } MAX6675_CS 1; // 片选拉高结束传输 return value; }代码逻辑不复杂16 个时钟循环里先让 SCK 拉高读取 SO 的状态并写到 value 的最低位然后左移除了第 16 位外都在读完当前位后左移避免最后一位被移出 16 位。时序上和 MAX6675 数据手册一致SCK 高电平时数据稳定低电平时从机切换下一位。注意sbit定义大多适用 Keil C51如果换到 SDCC 编译器要改成__sbit或直接操作 P1 寄存器这是移植时容易漏掉的一点。使用这段代码时主循环里先调用MAX6675_Read()拿到 raw再判断 D2unsigned int raw; float temperature; raw MAX6675_Read(); if (raw 0x0004) { // 热电偶开路等待下一次采样 } else { temperature (raw 3) * 0.25f; }raw 0x0004对应的就是 D2 位正常为 0开路为 1。读到温度后如果不需要小数可以保存成(raw 3) * 25表示 0.01°C 的整数这样串口或 LCD 显示时取整容易也避免在 51 上引入浮点运算库。2.4 为什么例程多用软件模拟STM32 再考虑硬件 SPIC51 例程里用 GPIO 模拟 SPI一方面是因为 8051 内核通常不带 SPI 外设另一方面是为了让代码在不同型号单片机间直接复用。换到 STM32 后建议启用硬件 SPI原因后面第 4 章展开。不管哪种方式SPI 的极性、相位和 CS 控制逻辑相同CPOL0CPHA1CS 低有效。如果读出来的温度以 4°C 为步长跳变多半是 D14-D3 的位序解析错误如果数值完全不对优先检查 SCK 相位。3. 从 C51 例程看 Keil 工程组织与连接控制3.1 压缩包里哪些文件该留、哪些该删资料包解压后能看到两类文件一类是 C51 例程源码一类是 STM32 工程产物里面还混着编译生成的中间文件。初次接触 Keil 的人容易把.uvgui_Administrator.bak、.axf、STARTUP.A51一窝蜂加进工程导致重编译时报头文件缺失或地址冲突。下表是典型文件分类文件类别处理方式MAX6675_51例程/main.cC51 源码保留作为主程序MAX6675_51例程/spi.c、spi.hC51 源码保留实现 IO 模拟 SPISTARTUP.A51Keil C51 启动文件工程中保留是 C51 运行环境一部分STM32-DEMO.uvprojKeil MDK 工程STM32 例程入口用 Keil 打开STM32-DEMO.axfARM 链接输出调试器直接下载用的固件可忽略STM32-DEMO.uvgui_Administrator.bak窗口布局删除不影响工程*.__i预处理中间文件删除是编译过程的临时产物enc28j60.__i、simple_server.__iKeil 中间文件与 MAX6675 无关属打包残留可删除有一点容易被忽略.uvgui文件不是工程文件它只记录调试器窗口的位置和打开状态。网上下载的资料包带上这些文件往往是因为原作者的 Keil 工作区没有清理。自己建工程时建议只把.uvproj、.uvopt、.c、.h、.a51纳入版本管理其余都放到.gitignore里。3.2 C51 主循环整数温度值、串口输出和 250ms 等待C51 例程的 main.c 整体套路是先初始化串口然后死循环读 MAX6675。我一般会在循环里插入delay_ms(250)而不是连续读原因在 MAX6675 转换时间。资料《MAX6675.pdf》里给出了约 220ms 的典型转换时间这意味着两次读取至少要间隔 250ms 以上否则只是重复取到上一帧数据。下面是可以直接用的主循环骨架#include spi.h #include stdio.h void UART_Init(void) { // STC89C52/AT89S52 串口配置为 9600 8N1 SCON 0x50; TMOD | 0x20; TH1 0xFD; TL1 0xFD; TR1 1; } void delay_ms(unsigned int n) { unsigned int i, j; for (i 0; i n; i) for (j 0; j 125; j); } void main(void) { unsigned int raw; unsigned int temp; UART_Init(); while (1) { raw MAX6675_Read(); if (raw 0x0004) { printf(OPEN\r\n); } else { temp (raw 3) * 25; // 0.01°C 整数格式 printf(%u.%02u C\r\n, temp / 100, temp % 100); } delay_ms(250); } }注意printf在 Keil C51 中要自己提供putchar才能重定向到串口否则默认走串口 0没初始化串口会卡死。更简单的方法是用资料包例程里已有的UART_SendString函数每次只发字符串不走 printf 格式解析代码体积小很多。上面temp (raw 3) * 25保存的是 0.01°C 整数例如读到 1023.75°C 时temp 102375输出时分出整数和小数。这个技巧比 float 更适合 51。3.3 Keil5 打开 C51 例程的几个典型报错第一类安装了 Keil MDK 但没有装 C51 编译器打开.uvproj后提示No Target Device。这是因为 Keil MDK 的 ARM 版默认不包含 C51 工具链需要单独安装 C51 安装包常见版本有 C51 v9.59a 或 v9.61。安装到同一个 Keil 目录后再打开工程就能识别 C51 设备。如果之前已经装过 Keil5 的 ARM 环境注意安装路径要和 MDK 一致官网下载页面一般会提示 C51 和 ARM 可以共存。第二类设备列表里找不到 STC89C52 或 AT89C52。常见做法是安装厂商的 Pack 包或者在 Device 里选兼容型号STC 系列没有官方 Pack 时可以选 Atmel 的 AT89C52 来编译烧录时再用 STC-ISP 按实际型号下载。网上一些教程会直接让用户改头文件里的寄存器位但考虑到 C51 的 SFR 定义基本一致这里不多展开。第三类链接时报UNRESOLVED EXTERNAL多数是启动文件问题。C51 工程里STARTUP.A51承担着初始化 DATA/XDATA 和设置堆栈的任务如果没把它加入工程C 库函数无法正常工作。另外Options for Target → L51 Misc中的链接器控制文件里经常需要指定STACK大小典型的写法是给STACKTOP预留足够 RAM不然函数嵌套一深就复位。资料包里自带STARTUP.A51直接把文件拖进 Keil 工程的 Source Group 即可不要去修改里面的STACKTOP值除非你确定自己的 RAM 布局。4. STM32 标准库工程里如何改造成多路热电偶采集4.1 先确认例程是标准库还是 HAL 库资料包里STM32-DEMO.uvproj这一命名风格是 Keil MDK 早期项目的常见形态再加上调试布局文件带的是用户电脑用户名推断例程多半基于 STM32 标准外设库。打开stm32f10x_spi.h头文件即可确认如果代码里出现HAL_SPI_TransmitReceive则是 HAL 库。两种库的 SPI 驱动接口不同不能直接混用。我遇到过把标准库的 SPI 初始化函数放进 HAL 工程后编译报大量错误的情况原因不是代码逻辑问题而是头文件互相冲突。所以移植前先统一框架标准库和 HAL 不要放在同一个工程里。标准库工程中系统时钟默认由SystemInit()配置一般会放在启动文件里。如果你的板子晶振不是 8MHz需要在system_stm32f10x.c里修改 PLL 参数或者在外部重新配置 RCC。很多例程默认按 72MHz 配置换成 16MHz 晶振的板子时SPI 分频后的实际时钟会不同MAX6675 读取自然不正常这是 SPI 通讯诡异问题的常见来源。4.2 标准库下的 MAX6675 单通道读取代码在 2.4 节说过STM32 上建议用硬件 SPI。下面是标准库下的初始化关键段SPI_InitTypeDef SPI_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // SCK: PA5, MISO: PA6, MOSI: PA7, CS: PA4 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_32; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);SPI_CPHA_2Edge配合SPI_CPOL_Low就是 SPI Mode 1。时钟分频用 32系统时钟 72MHz 时约 2.25MHz对 MAX6675 完全没有压力如果线比较长或者现场干扰大改成 64 或 128 分频更稳。读字节时因为是全双工必须先发送哑数据才能产生 SCKstatic uint8_t SPI1_ReadByte(void) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, 0x00); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); } uint16_t MAX6675_Read(void) { uint16_t value; GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS 拉低 value SPI1_ReadByte() 8; value | SPI1_ReadByte(); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS 拉高 return value; }高位在前、低位在后正好对应value 高字节 8 | 低字节。注意 MOSI 引脚即使不连 MAX6675 也要配置成复用推挽否则 SPI 外设输出状态不确定少数芯片会在 MISO 上产生额外噪声。读完后必须把 CS 拉高等待内部 A/D 转换完成后再开始下一次读取。4.3 多路采集CS 分时选通与 250ms 调度多路热电偶场景在资料包里的论文中反复出现比如基于 MAX6675 的多路温度采集系统。硬件上可以把所有 MAX6675 的 SCK、SO、VCC、GND 并联每路各用一个 IO 控制 CS每次只拉低一路 CS另外几路保持高电平这样它们不会占用 SPI 总线。示例代码#define CS1_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_0) #define CS1_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_0) #define CS2_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_1) #define CS2_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_1) #define CS3_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_2) #define CS3_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_2) uint16_t temperature_x100[3]; void MAX6675_ScanAll(void) { uint16_t raw[3]; CS1_LOW(); raw[0] MAX6675_Read(); CS1_HIGH(); delay_ms(1); // CS 建立时间 CS2_LOW(); raw[1] MAX6675_Read(); CS2_HIGH(); delay_ms(1); CS3_LOW(); raw[2] MAX6675_Read(); CS3_HIGH(); temperature_x100[0] (raw[0] 3) * 25; temperature_x100[1] (raw[1] 3) * 25; temperature_x100[2] (raw[2] 3) * 25; }三次读取本身只占用微秒级时间但 MAX6675 的转换周期在 220ms 量级。如果三路需要实时更新最好把每个通道的读操作放在自己的定时器时隙中例如每 250ms 采样一路一路上一次采样与下一次之间有 750ms 间隔解码后的数据仍近似实时。如果只在一个主循环里读三路然后 sleep 250ms每一路的实际更新间隔会被压缩到很短反而读到同一帧。下面表给出两种调度写法分别适合的场景调度方式更新周期适合场景主循环顺序读 3 路 最后 delay共 3 次 SPI 读随后等待 250ms数据刷新要求不高代码最简单定时器中断每路独立采样每路 250ms 独立更新多路温度控制需要稳定刷新率独立采样时用temperature_x100[i]作为全局变量中断里只写这个变量主循环只做显示或控制避免在中断里做 sprintf。这也是 STM32 例程升级成多路时最容易踩的一个坑数据本身读取很快但控制系统误以为采样周期是 250ms 就参与了 PID 计算结果因为实际更新的数据还是旧值控制量出现周期性抖动。解决方法是给每路维护一个last_sample_tick只有确实读到新数据才更新控制计算的输入。5. 冷端补偿验证、开路检测与高温环境排错5.1 冰点验证法拿到例程后第一步不是接热电偶而是先将 MAX6675 的 T 和 T- 短接这时读数应当是环境温度附近的一个小值例如 25°C 左右。再把 K 型热电偶的探头放进冰水混合物稳定 2 分钟后读数应在 0°C 上下。为什么短接时不是 0°C因为 MAX6675 内部冷端补偿在起作用它测量的是芯片周围环境温度热电偶开路或短接时报告的是冷端温度。如果冰水测试结果偏差超过 1°C优先检查芯片周边是否有发热元件热电偶延长线是否用了补偿导线而不是去调软件偏移。资料《一种简易的高精度测温系统研制.pdf》里也提到类似验证方法重点在保持 MAX6675 芯片附近温度稳定。5.2 开路标志位与接反线的表现读取代码里if (raw 0x0004)能够快速判断热电偶是否断线但有一种情况例外热电偶正负极接反。接反时热电势方向相反MAX6675 输出不会出现负温度而是被钳位到 0°C 附近D2 开路标志不置位。如果测量对象 100°C 但显示 0.25°C 左右先考虑是不是 T/T- 接反。另外 K 型热电偶的红色线通常是正极但不同厂家颜色定义有差异最可靠的方式是用另一块温度表和万用表确认。资料包里提到热电偶电桥补偿电路那是 MAX6675 内部电路的一部分外部只要保证补偿点温度均匀即可。5.3 数字滤波与读取间隔的配合现场排错的最后一步是对读数做软件抗干扰。由于 MAX6675 转换周期约 220ms在每次 CS 拉高后立刻再读拿到的还是上一帧数据所以任何滤波算法都要先保证相邻两次有效读数之间至少间隔 250ms。滤波可用中值滤波或滑动平均N 取 5 已经足够超过 10 个点会拖慢温度响应使 PID 控制迟钝。常见做法是void UpdateTemperature(uint16_t fresh) { static uint8_t idx 0; static uint16_t buf[5]; buf[idx] fresh; idx (idx 1) % 5; // 排序后去掉最大最小再对中间三个值平均 }读取数据放入缓冲区后取中间三个值的平均作为本次温度。MAX6675 的 SO 引脚是推挽输出一般不需要上拉但如果现场干扰大可以在 SO 上加 10kΩ 上拉到 3.3V同时把 SPI 时钟降到 500kHz 左右再配合滤波基本能解决跳字问题。在代码层面把MAX6675_Read()放到定时器中断每 250ms 触发一次读取结果写入环形缓冲区主循环只负责取数和滤波。需要扩展更多路时只需要增加 CS 引脚和环形缓冲区大小SPI 驱动本身不用动。本文还有配套的精品资源点击获取