简介:STM32F103驱动MAX6675测温的完整测试例程,面向需要快速实现K型热电偶温度采集的嵌入式开发者,基于标准外设库完成SPI通信与数据解析,可直接在真实工程中使用。MAX6675内部集成冷端补偿和模数转换,工作分辨率达0.5°C,精度±0.5°C;例程遵循其时序协议,先发送读取命令再获取包含符号位与温度值的多比特数据,并完成二进制到十进制温度换算。压缩包内共96个文件,以MAX6675驱动源码、STM32工程配置和中断处理等C/H代码为主体,辅以.s启动文件与Keil项目文件,目录结构清晰,整体仅308KB,便于按模块阅读和移植到其他STM32型号。目前已有3921人学习,在同类资源中具备较好的参考性;例程还提供SPI初始化、GPIO配置等关键函数实现,并预留串口发送温度值的扩展思路,适合作为学习或项目开发的基础模板,可显著降低驱动调试门槛。
1. 一个NTC测温翻车现场,逼我换上了MAX6675
调试一块带加热功能的小板子时,我最初用的是NTC热敏电阻做温度采样。室温下读数还算正常,可温度一过60℃,数据就开始飘,手都摸到烫了,串口打印还在70℃慢慢爬,PID自然也跟着乱来。后来换成K型热电偶加MAX6675,同一块板子不到半天就把温控调完了。MAX6675是SPI接口的数字测温芯片,直接输出0.25℃分辨率的温度值,自带冷端补偿,省掉了运放和校准环节。这个STM32例程解决的就是这件事:接线、初始化SPI、读数据、解析成摄氏温度。适合做加热台、烤箱温控、电机过热保护的开发者直接拿来跑,想搞清楚SPI从设备时序的人也可以顺着代码走一遍。
2. 硬件接线和SPI时序:先把电路和时钟对齐,再谈代码
2.1 MAX6675引脚功能与最小接线
MAX6675本身是SO-8封装的小芯片,市面上常见的模块已经把它和热电偶接线端子做在一起,引脚也引出来了。以STM32F103C8T6为例,典型接线如下:
| MAX6675模块引脚 | 作用 | 接到STM32 |
|---|---|---|
| VCC | 电源,3.3V或5V(以模块丝印为准) | 3.3V |
| GND | 地 | GND |
| SCK | SPI时钟输入 | PA5 |
| CS | 片选,低有效 | PA4(GPIO输出) |
| SO | SPI数据输出(MISO方向) | PA6 |
| T+ | K型热电偶正极(通常红线) | 热电偶探头 |
| T- | K型热电偶负极(通常蓝或黄线) | 热电偶探头 |
这里有个容易忽略的点:MAX6675是只读从设备,整个通信只有主机发时钟、从机吐数据,所以MOSI脚完全不用接。SO是唯一的数据输出脚,对应STM32的MISO功能。如果用的模块带稳压芯片,VCC接5V问题不大;如果是裸芯片或者模块上没有LDO,老老实实接3.3V,别直接上5V。
2.2 SPI通信参数:模式0、数据宽度和时钟速率的选择
MAX6675的数据帧固定16位,MSB先出,SCK空闲时保持低电平。多数例程按SPI Mode0(CPOL=0,CPHA=0)配置就能正常读,原理上芯片在CS拉低后先把最高位放到SO上,之后每个SCK下降沿更新下一位,主机在上升沿采样,正好和Mode0对上。
时钟速率方面,虽然芯片标称SCK可以跑得比较高,实际工程里我一般把预分频设在32左右,让SCK在2MHz附近,传输距离稍长也不容易出错。速率太高时,SO线上的毛刺会被当成有效数据采进去,读回来的温度跳来跳去,排查起来很费劲。数据宽度固定用8位,收两个字节再拼成16位,比直接配16位宽度更直观,也方便后面做字节序调整。
2.3 CS为什么不能用硬件自动控制
STM32的SPI外设有硬件NSS功能,看起来可以让外设自动管理片选,但实际用起来很别扭。硬件NSS在SPI使能期间一直拉低,而MAX6675要求CS拉低、读完16个时钟、再拉高这样一个完整周期。如果靠硬件NSS自动控制,CS释放时机不完全由你掌控,多设备挂在同一条SPI总线上时更是容易互相干扰。
我一般把SPI的NSS配置成软件管理模式,CS引脚用普通GPIO输出,自己拉低、自己读、自己拉高,时序完全透明。这样做还有个好处:以后想接第二片MAX6675,只需多占一个GPIO,SPI总线不用动。
2.4 读取时序:16个时钟周期拿回16位
一次完整读取的时序是这样的:CS拉低后,芯片把bit15放到SO线上,随后主机连发16个SCK时钟,每来一个时钟下降沿SO更新一位数据,上升沿被主机采走。第16个时钟结束后,CS拉高,一次读取完成。
整个过程需要16个时钟周期,所以用8位数据宽度接收时,HAL_SPI_Receive的Size参数填2,表示收两个字节。芯片内部是连续转换模式,常规转换周期大约100到220ms,两次读取之间最好间隔100ms以上,否则读到的还是上一次转换结果。刚上电时也需要等一个转换周期再开始读,这个后面的避坑章节会单独说。
3. 代码拆解:从CubeMX初始化到稳定输出温度
3.1 工程结构与引脚分配
例程基于STM32CubeMX生成的HAL库工程,芯片选STM32F103C8T6。CubeMX里的关键配置项如下:
- RCC:HSE外部晶振
- SYS:SWD调试口
- USART1:PA9/PA10,波特率115200,用于打印温度
- SPI1:PA5/PA6,全双工主机模式,CPOL=Low、CPHA=1Edge
- PA4:GPIO输出,复用为MAX6675的CS引脚
工程里主要关心的文件就这几个:
- Core/Src/main.c:主循环和读取逻辑
- Core/Src/spi.c:CubeMX生成的SPI初始化
- Core/Src/max6675.c:自己加的驱动,封装CS控制和数据解析
- Core/Inc/max6675.h:驱动头文件,导出读取函数
3.2 SPI1初始化:关键参数的现场解释
CubeMX生成SPI初始化代码后,主要参数是这样的:
void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; /* 主机模式,负责产生SCK */ hspi1.Init.Direction = SPI_DIRECTION_2LINES; /* 全双工,虽然只用MISO */ hspi1.Init.DataSize = SPI_DATASIZE_8BIT; /* 8位数据宽度,分两次收 */ hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; /* CPOL=0,SCK空闲低电平 */ hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; /* CPHA=0,第一个边沿采样 */ hspi1.Init.NSS = SPI_NSS_SOFT; /* 软件管理片选,CS交给GPIO */ hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; /* 72MHz/32=2.25MHz */ hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; /* MSB先出,和芯片时序一致 */ HAL_SPI_Init(&hspi1); }这里最值得留意的是CPOL和CPHA的组合。SPI_POLARITY_LOW加上SPI_PHASE_1EDGE就是SPI Mode0,SCK空闲为低,数据在第一个边沿(上升沿)被主机采样。MAX6675的SO在CS拉低后先输出最高位,之后每个下降沿更新下一位,主机在上升沿读取正好对齐。如果板子走线比较乱、信号质量差导致数据错位,可以改成CPHA=1Edge换成Mode1再对比,这两种配置在MAX6675上都有人验证过能跑,但Mode0是默认首选。
预分频32倍在72MHz主频下得到2.25MHz的SCK,低于芯片标称上限。实际用下来,线长超过10cm时这个速率依然稳定,再高就容易出现偶发跳变。
3.3 CS控制和原始数据读取:软件NSS的正确姿势
CS使用PA4作为普通GPIO输出,读写函数封装在max6675.c里:
#include "max6675.h" #include "spi.h" #include "gpio.h" #define MAX6675_CS_PORT GPIOA #define MAX6675_CS_PIN GPIO_PIN_4 /* CS拉低,开始一次采样周期 */ void MAX6675_CS_Low(void) { HAL_GPIO_WritePin(MAX6675_CS_PORT, MAX6675_CS_PIN, GPIO_PIN_RESET); } /* CS拉高,结束一次采样周期 */ void MAX6675_CS_High(void) { HAL_GPIO_WritePin(MAX6675_CS_PORT, MAX6675_CS_PIN, GPIO_PIN_SET); } /* 读取16位原始数据 */ uint16_t MAX6675_ReadRaw(void) { uint8_t rx[2] = {0, 0}; MAX6675_CS_Low(); HAL_Delay(1); /* 保证CS下降沿到SCK启动的建立时间 */ HAL_SPI_Receive(&hspi1, rx, 2, 100); /* 主机产生时钟,读回两个字节 */ MAX6675_CS_High(); return ((uint16_t)rx[0] << 8) | rx[1]; }HAL_SPI_Receive在主机模式下会自动产生SCK时钟,同时从MISO引脚收数据。这里有个细节:虽然MOSI没接,但全双工模式下主机照样会发送0x00字节,MAX6675根本不关心MOSI上的数据,只按SCK节奏往SO上推自己的位流。HAL_Delay(1)是为了满足CS下降沿到时钟启动的最小时间要求,实际芯片要求是纳秒级,延时1ms纯属保险,也方便用示波器观察时序。
rx[0]是第一个8位时钟周期收到的数据,对应bit15到bit8;rx[1]是第二个字节,对应bit7到bit0。拼接顺序不能反,否则温度值会完全对不上。
3.4 温度解析:右移3位的原理
MAX6675的16位数据格式需要仔细说明,这是整个例程最容易写错的地方:
| bit | 含义 |
|---|---|
| bit15 | 固定0 |
| bit14 ~ bit3 | 12位温度数据,bit14为最高位 |
| bit2 | 热电偶开路标志,1表示开路 |
| bit1 ~ bit0 | 固定0 |
温度数据的LSB在bit3位置,对应0.25℃。也就是说,完整16位数据右移3位,去掉bit2的开路标志和bit1、bit0的固定0后,剩下的12位再乘以0.25就是摄氏温度。
/* 读取一次温度,开路时返回 -1000 */ float MAX6675_GetTemperature(void) { uint16_t raw = MAX6675_ReadRaw(); if (raw & 0x0004) { return -1000.0f; /* bit2 = 1,热电偶开路 */ } raw = raw >> 3; /* 去掉低3位,留下12位温度数据 */ return (float)raw * 0.25f; }这里判断开路用的是0x0004,也就是bit2。很多网上例程在这里检查bit15或bit14,其实都不对,bit15恒为0,bit14是温度数据的最高位,正常工作温度超过512℃时它也会变成1。只有bit2才是专门的开路标志。
raw >> 3这一步是整除移位,因为bit1和bit0恒为0,右移不会丢失有效数据。移位后再乘0.25,得到的就是实际摄氏温度。比如原始值读回0x0320,右移3位变成0x64即100,乘以0.25得到25℃,正好对上一杯室温水。
3.5 主循环与串口打印
主循环逻辑很简单,读取温度,判断开路,串口打印:
#include <stdio.h> #include "max6675.h" #include "usart.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); HAL_Delay(200); /* 上电先等一个转换周期,丢弃首次不稳定数据 */ while (1) { float temp = MAX6675_GetTemperature(); if (temp < -500.0f) { printf("TC OPEN, check wiring\r\n"); } else { printf("Temp: %.2f C\r\n", temp); } HAL_Delay(500); /* 大于最大转换周期,确保读到的是新数据 */ } }HAL_Delay(200)在进入主循环前先让芯片完成首次转换,避免读到残缺数据。循环里的500ms延时远大于MAX6675的转换周期,保证每次读取都是新一次转换的结果。串口输出用printf重定向到USART1,具体重定向方式每个工程略有差异,标准做法是在usart.c里实现fputc调用HAL_UART_Transmit。
4. 数据可靠性与标定:让读数从“能用”到“可信”
4.1 冷端补偿:MAX6675为什么不需要外接温度传感器
K型热电偶的原理是两种不同金属在温度差下产生热电动势,但这个电动势是探头温度相对于参考端(冷端)的差值。普通测温方案需要在冷端放一个温度传感器做补偿,计算麻烦。MAX6675内置了冷端补偿电路,芯片上的温度传感器自动检测冷端温度并修正结果,所以外部不需要接任何温感芯片,SPI读出来的直接就是补偿后的绝对温度值。
这个“冷端”指的是热电偶接线端子所在的PCB位置,不是探头那一端。如果热电偶的接线端子离MAX6675很远、中间温差大,补偿就会不准。工程上的做法是让热电偶连接器尽量靠近芯片,保持两者温度一致。MAX6675的测量范围是0℃到1023.75℃,K型热电偶本身能测负温,但MAX6675的输入级不支持负温直接测量,需要测负温就得换MAX31855或者加偏置电路,这个例程不覆盖。
4.2 热电偶开路检测与异常值过滤
热电偶探头断线、端子松脱、焊点脱落,在模拟方案里很难及时察觉,读数会慢慢漂到一个离谱的固定值。MAX6675的bit2开路标志位就是干这个的:热电偶回路阻抗异常时,内部检测电路把bit2置1,数据无效。
代码里raw & 0x0004判断开路,返回-1000作为哨兵值。应用层拿到这个值后,不要试图把它当作真实温度打印出来,应该直接报错。我见过有人不看这个标志位,热电偶断线后显示一个奇怪的几十度,差点把加热系统跑飞。
4.3 软件滤波:滑动平均对付电磁干扰
MAX6675本身是数字输出,抗干扰能力比模拟方案强不少,但工业现场电磁环境恶劣时,SPI线上的毛刺依然会让单次读数跳变。常用做法是连续采样取平均,我用的是环形缓冲区滑动平均,实现简单且内存占用固定:
#define TEMP_FILTER_N 5 float TEMP_FilterBuf[TEMP_FILTER_N]; uint8_t TEMP_FilterIdx = 0; float MAX6675_GetTemperatureFiltered(void) { float sum = 0.0f; uint8_t i; TEMP_FilterBuf[TEMP_FilterIdx] = MAX6675_GetTemperature(); TEMP_FilterIdx = (TEMP_FilterIdx + 1) % TEMP_FILTER_N; for (i = 0; i < TEMP_FILTER_N; i++) { sum += TEMP_FilterBuf[i]; } return sum / TEMP_FILTER_N; }窗口大小取5,兼顾响应速度和平滑效果。窗口太小起不到滤波作用,太大则温度响应变慢,控温系统容易震荡。如果做快速温升监控,可以把窗口缩到3;如果只做长期环境监测,窗口取10也行。
4.4 现场两点标定法
MAX6675出厂精度大约在±2℃,很多场景够用,但如果你想用它做精确控温,两点标定是性价比最高的提升手段。不用复杂设备,一个冰水混合物和一个沸水锅就够了。
先在冰水混合物中稳定后读一个值,记为temp_ice,理论值0℃;再把探头放入沸水中读一个值,记为temp_boil,理论值100℃(海拔高的话要查当地沸点修正)。然后做线性校正:
float gain = (100.0f - 0.0f) / (temp_boil - temp_ice); float offset = 0.0f - temp_ice * gain; float cal_temp = temp_read * gain + offset;gain是斜率修正系数,offset是零位漂移。这套线性校正在MAX6675的整个量程内基本够用,因为芯片内部本身是线性的,主要误差来自冷端补偿的偏移。标定完成后,把gain和offset存到Flash或写在代码宏定义里,以后每次读取都用修正公式过一遍。
5. 避坑记录:五条测温调试的血泪经验
坑一:温度读数剧烈跳动,甚至出现负的几百度
现象:室温下读数在几十度和几百度之间来回跳,偶尔跳到负值,完全没法用。
原因:K型热电偶正负极接反。MAX6675的T+和T-接反后,热电动势方向反了,内部转换结果不稳定,出现异常跳动。这算是K型热电偶最经典的翻车现场。
解决:把T+和T-两根线对调。K型热电偶正极通常是红色,负极是蓝色或黄色。不同厂家的线色有差异,拆开探头保护套或者用万用表量一下热电偶两端再确认,不要只看颜色就下结论。对调后如果读数稳定在正常范围,问题就解决了。
坑二:读数恒定在室温附近,手摸探头没反应
现象:温度读出来一直在25℃左右,用手捏住热电偶探头,读数纹丝不动。
原因:热电偶两根线在探头端短路了,或者端子焊点连锡。热电偶两端短路时没有温差电动势,MAX6675读到的就是冷端补偿后的环境温度,看起来“正常”但实际没在测温。
解决:先检查代码里的开路标志位,如果bit2没有置1,再用万用表电阻档量热电偶。K型热电偶室温下阻抗通常在几欧到十几欧,如果量出来接近0,说明内部短路,换探头。另外检查一下接线端子有没有焊锡连到相邻引脚。
坑三:HAL_SPI_Receive返回HAL_ERROR或HAL_TIMEOUT
现象:程序卡在HAL_SPI_Receive里,或者返回错误码,温度一直打印不出来。
原因:SPI外设配置成了SPI_DIRECTION_2LINES_RXONLY只接收模式后,部分HAL版本在主机模式下调用HAL_SPI_Receive会直接报错,因为这个函数内部状态机对RXONLY的支持不够完善。另一个常见原因是CS没有预先拉低,芯片根本没进入采样状态。
解决:SPI方向老老实实配置成SPI_DIRECTION_2LINES全双工模式,不接MOSI一样能收数据,没必要省那点配置。读取时先MAX6675_CS_Low()再调HAL_SPI_Receive,顺序不要颠倒。
坑四:读到的温度正好是实际值的一半或两倍
现象:实际25℃,读回来12.5℃或50℃,其他温度也按同样比例偏差。
原因:SPI数据错位一拍,通常是SPI模式配置不对,比如把CPOL或CPHA设置反了,导致第一个采到的bit不是bit15而是bit14,整个数据帧整体偏移一位。
解决:核对CubeMX里SPI的模式参数,CPOL=Low、CPHA=1Edge是Mode0。如果确认配置没错还是错位,用示波器看SCK和SO的波形关系:SCK下降沿后SO更新数据、上升沿时SO稳定,那就是Mode0;反过来就是Mode1。这种按倍数偏差的问题,多半是时序配置的小问题,不是芯片坏了。
坑五:上电后第一次读数离谱,之后恢复正常
现象:板子上电后第一行打印的温度是十几度甚至0℃,第二次开始才显示正常室温。
原因:MAX6675上电后需要一段时间完成第一次完整的A/D转换,典型转换周期100到220ms。如果主循环启动后立刻读取,芯片内部的转换还没完成,读出来的数据是无效的。
解决:上电初始化完成后,先延时200ms再进入主循环,或者干脆在循环开始前做一次假读取,把结果丢弃。这个延时成本很低,但如果漏了,往往会在现场调试时浪费半小时找原因。
6. 二次开发:把一个测温例程扩展成多通道采集节点
6.1 多通道分时片选
一个MAX6675只能接一路热电偶,但SPI总线本身可以挂多个从设备。扩展方式很简单,每个MAX6675占一个CS引脚,SCK和SO共用,分时读取:
void MAX6675_SelectChannel(uint8_t ch) { HAL_GPIO_WritePin(CS1_GPIO_Port, CS1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(CS2_GPIO_Port, CS2_Pin, GPIO_PIN_SET); if (ch == 0) { HAL_GPIO_WritePin(CS1_GPIO_Port, CS1_Pin, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(CS2_GPIO_Port, CS2_Pin, GPIO_PIN_RESET); } }选通一个通道后调用MAX6675_GetTemperature,读完再切下一路。注意每次切换后留出转换周期,别在同一时间片里连续读两片。
6.2 数据上行:串口JSON输出
加上printf重定向后,可以输出结构化的JSON字符串,方便上位机直接解析:
printf("{\"temp\":%.2f,\"open\":%d}\r\n", temp, open_flag);这个格式在串口调试助手和Python上位机里都能直接处理。如果再接个ESP8266或以太网模块,同样的字符串可以直接转发到MQTT或者写入数据库。
6.3 验证方法
扩展完功能后,我的习惯是连续采100个点,打印最大值、最小值和平均值,观察离散程度。正常的MAX6675读数在稳定温度下最大最小偏差应该在1℃以内,如果偏差过大,优先查电源纹波和SPI线走线。然后再用冰水和沸水做一次两点标定,把gain和offset写进代码宏定义,整个采集节点就算成型了。
从那以后,我每次给MAX6675接热电偶,都会先空跑一次读取确认开路标志位,再焊线正式上电,等于给自己加了一道防呆工序。希望帮到你。
本文还有配套的精品资源,点击获取