MAX6675与K型热电偶测温实战:SPI时序、冷端补偿及多路采集
2026/9/16 13:08:01 网站建设 项目流程

简介:面向使用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 相位要配置成 CPOL=0、CPHA=1,也就是 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 = 400(0x190),在 16 位帧里就是 0x0C80,右移 3 位后得到 400。下面表格列出各 bit 含义,方便写代码时对照:

Bit 位字段说明
D15未用读回时通常为 0,不参与计算
D14-D3温度数据12 位数据,LSB=0.25°C,范围 0~4095
D2开路标志1 表示热电偶开路,0 表示正常
D1保留位恒为 0
D0三态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,再判断 D2:

unsigned 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 再考虑硬件 SPI

C51 例程里用 GPIO 模拟 SPI,一方面是因为 8051 内核通常不带 SPI 外设,另一方面是为了让代码在不同型号单片机间直接复用。换到 STM32 后,建议启用硬件 SPI,原因后面第 4 章展开。不管哪种方式,SPI 的极性、相位和 CS 控制逻辑相同:CPOL=0,CPHA=1,CS 低有效。如果读出来的温度以 4°C 为步长跳变,多半是 D14-D3 的位序解析错误;如果数值完全不对,优先检查 SCK 相位。

3. 从 C51 例程看 Keil 工程组织与连接控制

3.1 压缩包里哪些文件该留、哪些该删

资料包解压后能看到两类文件:一类是 C51 例程源码,一类是 STM32 工程产物,里面还混着编译生成的中间文件。初次接触 Keil 的人容易把.uvgui_Administrator.bak.axfSTARTUP.A51一窝蜂加进工程,导致重编译时报头文件缺失或地址冲突。下表是典型文件分类:

文件类别处理方式
MAX6675_51例程/main.cC51 源码保留,作为主程序
MAX6675_51例程/spi.cspi.hC51 源码保留,实现 IO 模拟 SPI
STARTUP.A51Keil C51 启动文件工程中保留,是 C51 运行环境一部分
STM32-DEMO.uvprojKeil MDK 工程STM32 例程入口,用 Keil 打开
STM32-DEMO.axfARM 链接输出调试器直接下载用的固件,可忽略
STM32-DEMO.uvgui_Administrator.bak窗口布局删除,不影响工程
*.__i预处理中间文件删除,是编译过程的临时产物
enc28j60.__isimple_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 分频更稳。读字节时因为是全双工,必须先发送哑数据才能产生 SCK:

static 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 驱动本身不用动。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询