简介:基于MAX6675与STM32的测温例程,为嵌入式开发者提供一套可直接学习与移植的K型热电偶温度采集实现方案。程序涵盖SPI通信初始化、MAX6675驱动配置、温度数据读取与换算等关键环节,适合正在学习STM32外设驱动或需要快速完成热电偶测温功能的初中级开发者。压缩包整体约2.22MB,共146个文件,包含29个C源码、29个头文件、工程编译生成的十六进制文件与调试映射文件,以及Keil工程配置和备份文件,可据此了解完整工程结构与编译流程。已有806人学习下载,例程代码结构清晰,注释与资源文件便于对照排查硬件连接和SPI时序问题,能有效缩短开发者在温度采集模块上的调试时间。
1. MAX6675_STM32例程:一套SPI测温代码到底解决了什么
如果做K型热电偶测温,很多人第一反应是MAX6675。它把热电偶的微弱毫伏信号放大、冷端补偿、12位ADC全部做进一块芯片里,STM32只要用SPI把结果读回来。标题里这个“MAX6675_STM32例程.zip”听起来就是个代码压缩包,但背后把这类项目的三个痛点都覆盖了:SPI时钟怎么配、16位数据怎么拼接、热电偶断线怎么识别。真正动手时,不少人死在数据始终为0或温度乱跳上,而一份能下到板子上直接改的例程,能省掉半天看手册的时间。无论是做烤箱温度监控、电机保护、还是工业采集模块,这套东西都能作为起点。下面从最常见的F103/F407开发板出发,把例程背后SPI、数据格式和移植细节整个拆开。
2. 从例程反查MAX6675的硬件与SPI时序
2.1 MAX6675内部原理与数据格式
先看数据手册里最关键的输出帧。MAX6675一次转换周期大约220ms到250ms,转换完成后,把结果放在一个16位移位寄存器里,等待外部读走。这16位不是每一位都表示温度,表2-1给出了标准的数据位定义。
表2-1 MAX6675输出数据位(16位,从高位到低位)
| 位序号 | 名称 | 含义 |
|---|---|---|
| D15 | 保留 | 始终为0 |
| D14-D3 | 温度数据 | 12位二进制补码,LSB=0.25°C |
| D2 | 热电偶开路 | 1表示热电偶开路 |
| D1 | 设备ID | 始终为0 |
| D0 | 三态 | 低阻态,不用 |
所以温度值不是右移4位,而是右移3位再乘以0.25。很多新手把D0也当成数据位,导致温度偏小或跳动。例程里一旦出现“温度读出来像室温但差很多”的情况,多半是右移位数错了。另外,如果D2为1,温度寄存器内容不可用,避免拿去换算。
芯片内部实际上包含一个斩波放大器和一个冷端补偿二极管,这部分不需要外部电路干预。STM32要做的就是产生16个SCK时钟,并在合适的SPI采样边沿把MISO上的数据锁存回来。下面看STM32侧的硬件连接。
2.2 STM32的接线和SPI参数设定
无论例程是按标准库还是HAL库写的,引脚映射基本一致。以STM32F103的SPI1为例,常用的接法是PA5接SCK、PA6接MISO(SO)、PA4接CS。如果例程里默认用的是SPI2,对应PB13/PB14/PB12。表2-2整理了这两种常见映射。
表2-2 常用STM32 SPI引脚连接
| 序号 | 信号 | SPI1引脚 | SPI2引脚 |
|---|---|---|---|
| 1 | VCC | 3.3V或5V | 3.3V或5V |
| 2 | GND | GND | GND |
| 3 | SCK | PA5 | PB13 |
| 4 | MISO | PA6 | PB14 |
| 5 | CS | PA4 | PB12 |
注意,MAX6675的SDO是推挽输出,但它在CS为高时是高阻态。如果把MISO引脚复用成SPI功能,STM32的外设会在片选未选中时自动忽略输入,所以通常不需要外部上拉。不过有些低功耗设计会加一个10kΩ上拉到3.3V,防止悬空电平导致SPI误采样,例程里一般不体现,但实际项目值得保留。
SPI参数设定是例程里最值得抄的一段。先看HAL库初始化代码。
/* STM32F103 HAL库 SPI1初始化 */ SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // STM32做主机 hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 逐字节读取 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0, 空闲低 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1, 第二个边沿采样 hspi1.Init.NSS = SPI_NSS_SOFT; // 手动控制CS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; // 按SPI1最高36MHz算约1.125MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先出 HAL_SPI_Init(&hspi1); }这段代码里有几个参数需要说明。CPOL=0、CPHA=1这一组合对应SPI Mode 1,是MAX6675数据手册推荐的做法。如果例程里看到CPOL=0、CPHA=0,也不要马上改,先看MISO数据是否在SCK上升沿附近稳定。BaudRatePrescaler=32是把SPI1时钟36MHz分频到1.125MHz,远低于数据手册给出的4.3MHz上限,实际读取250ms一次的数据根本不需要更快,分频大一点还能减少噪声干扰。
一个常见的反直觉点是NSS。MAX6675有真实CS引脚,但STM32的硬件NSS功能会与SPI片选信号互相干扰。例程里普遍把NSS配成软件管理,然后把CS当普通GPIO操作。这样做的好处是读一个温度可以随时拉低CS、再拉高,不依赖SPI事件标志。如果例程用NSS_HARDWARE,会出现CS拉低时间不对,读回来的数据错位。
3. 例程核心代码讲解:读取、换算与异常位
3.1 用HAL库写一个最小读取函数
例程再怎么封装,最终回到读取16位数据这一步。由于SPI数据宽度按8位配置,需要发两条假写命令把数据时钟打出来。
/* 读取MAX6675一次温度值,返回原始16位ADC码 */ uint16_t MAX6675_ReadRaw(void) { uint8_t rx[2] = {0, 0}; uint8_t tx[2] = {0, 0}; // 写什么无所谓,用来产生SCK uint16_t raw = 0; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); raw = ((uint16_t)rx[0] << 8) | rx[1]; return raw; }这条函数的核心是HAL_SPI_TransmitReceive。tx数组写0也可以,因为SPI全双工通信时SCK由主机产生,只要主机在发数据,MISO就在往外移数据。拉低CS之后必须等待至少100ns再开始打时钟,这个时间在HAL库的片选切换代码中几乎不会如此精确;如果总线太长或线缆有分布电容,建议在写CS之后加一个__NOP()循环或延迟1us,避免第一拍采样到不稳定电平。
3.2 温度换算和热电偶开路检测
有了raw后,要按第2章表2-1进行位处理。直接写进一个函数,方便例程直呼返回浮点温度。
/* 解析MAX6675原始数据,返回温度值;-9999表示热电偶开路 */ float MAX6675_ReadTemp(void) { uint16_t raw = MAX6675_ReadRaw(); float temp; if (raw & 0x0004) { return -9999.0f; // 热电偶未接或断线 } raw >>= 3; // 删除D2-D0,留下12位温度位 temp = (float)raw * 0.25f; // 每个LSB代表0.25°C return temp; }这里的掩码判断和右移3位是例程中最重要的两行。如果你看到别人右移4位,是因为把12位数据误当成按16位对齐。MAX6675的温度位是从D14到D3,D3是最低有效位,所以右移3位才能让D3落到bit0。
注意事项有两点。第一,热电偶开路时,D2位会被置1,但MAX6675的SDO在开路后输出可能表现为持续高电平,导致读回0xFFFF,这时要优先判定D2。第二,MAX6675不支持负温度下的线性输出,虽然数据手册标称-200°C到+700°C的宽容限,但0°C以下建议改用其他冷端补偿更好的方案,例程里一般也不会处理负温度。表3-1列几个关键位组合的解读。
表3-1 常见原始值与解析结果
| 原始值(Hex) | 二进制D15-D0 | D2状态 | 温度值 |
|---|---|---|---|
| 0x019A | 0000 0001 1001 1010 | 0 | 51.25°C |
| 0x0FF8 | 0000 1111 1111 1000 | 0 | 510.25°C |
| 0x0004 | 0000 0000 0000 0100 | 1 | 开路 |
3.3 例程里几种SPI读取变体
在市面上各种例程包中,MAX6675的读取不外乎三种写法。第一种是上面这种HAL库的TransmitReceive;第二种用的是标准库,直接操作SPI_I2S_SendData和SPI_I2S_ReceiveData,一次发两个字节;第三种会自己写GPIO模拟SPI,把SCK拉高拉低手动移位。前两种性能足够,第三种只在硬件SPI被别的设备占用时使用。
如果你的例程是用标准库写的,核心代码通常像这样:
/* 标准库读取MAX6675,SPI1配置为模式0或模式1 */ uint16_t MAX6675_Read(void) { uint16_t value = 0; CS_LOW(); SPI_I2S_SendData(SPI1, 0); // 发第1个假字节 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); value = SPI_I2S_ReceiveData(SPI1); value <<= 8; SPI_I2S_SendData(SPI1, 0); // 发第2个假字节 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); value |= SPI_I2S_ReceiveData(SPI1); CS_HIGH(); return value; }标准库写法容易忽略一个坑:发送后必须等待RXNE置位再取数据,否则读到的可能是上一帧的残留值。HAL库内部已经处理这个等待,但标准库例程里经常有人漏掉while等待,导致数据整体错位一个字节。给不同MCU移植时,尤其注意SPI外设的RXNE和BSY标志。这些可见的“反直觉”点就是例程能帮你避雷的地方。
4. 把例程移植到自己的STM32项目
4.1 从标准库例程迁到HAL库/LL库
很多老旧例程压缩包里是标准库写的。而现在新开项目多数用STM32CubeMX生成HAL库工程,所以直接复制原文件会遇到SPI外设句柄、GPIO函数、Delay函数不兼容的问题。
我一般会先把原例程里的MAX6675驱动文件拆成三部分:底层SPI发送接收、CS控制、温度换算。底层SPI发送接收替换成对应HAL接口,CS控制替换成HAL_GPIO_WritePin,温度换算几乎不用改。这份ZIP例程如果包含bsp_max6675.c和bsp_max6675.h,那么里面一定有以上三个功能块。定义下面这个适配层结构,能把不同库的差异隔离出来。
/* 适配层:把SPI发送接收和CS控制统一封起来 */ typedef struct { void (*cs_low)(void); void (*cs_high)(void); void (*spi_transfer)(uint8_t *tx, uint8_t *rx, uint16_t len); } max6675_port_t; max6675_port_t port; port.cs_low = MAX6675_CS_LOW; port.cs_high = MAX6675_CS_HIGH; port.spi_transfer = MAX6675_SPI_Transfer; float temp = max6675_read_temp(&port);这种写法不是例程原有结构,但做产品时我会这么套。好处是以后换SPI2、换引脚甚至换芯片平台,只需要改四个函数,业务层不用动。如果你只是跑通例程,可以跳过,直接在bsp文件里替换。
4.2 修改SPI引脚和时钟,适配不同开发板
例程默认跑在某个开发板上,你的板子很可能不适用。在CubeMX里打开工程,改SPI引脚或重新映射SPI时,有三个地方必须同步检查。
第一,时钟树里SPI的分频系数。F103的SPI1挂在APB2(最高72MHz),SPI2挂在APB1(最高36MHz)。同一份代码里BaudRatePrescaler设为32,在SPI1上约1.125MHz,在SPI2上也是1.125MHz,但二者预算余量不同。如果改用SPI2且把分频设成2,则9MHz,已经超过MAX6675的4.3MHz上限,所以不宜只看例程里的固定分频值,应结合外设总线时钟计算。
第二,CS引脚如果只做软件GPIO,要注意推挽输出的开漏模式。MAX6675的CS是低电平使能,所以GPIO输出寄存器的初始值要置为高,防止上电瞬间拉低片选,造成一次误转换。例程里一般在GPIO初始化时把CS置为高,移植时别漏。
第三,如果你的开发板引脚不够,可以把CS接到任意GPIO,甚至用TIM PWM或DMA控制,但读取逻辑里CS拉低的时序窗口必须覆盖整个16个SCK周期。最常见的错误是CS拉低后执行了几行计算再发SPI,影响不大,但如果CS拉低后立即发SPI且中间有OS延迟,容易出现读回数据全1,因为MAX6675尚未把数据推到移位寄存器输出端。
/* 换到SPI2时,例程里的相应修改 */ #define MAX6675_CS_PORT GPIOB #define MAX6675_CS_PIN GPIO_PIN_12 #define MAX6675_SPI hspi2 HAL_GPIO_WritePin(MAX6675_CS_PORT, MAX6675_CS_PIN, GPIO_PIN_SET); // CS默认高4.3 多通道或更高刷新率怎么调整
一个MAX6675只能接一支热电偶。需要测多路温度时,常见方案是每个通道一个MAX6675,它们共享SCK,但每片MAX6675都有独立SDO,所以MISO不能直接并联。多数例程会改成多个CS分时选择,把各芯片的SDO都接到同一个MISO引脚上,利用CS高时的三态输出完成总线复用。
这时读取函数要增加CS选择参数:
uint16_t MAX6675_ReadRaw_CS(GPIO_TypeDef *cs_port, uint16_t cs_pin) { uint8_t tx[2] = {0, 0}; uint8_t rx[2] = {0, 0}; uint16_t raw = 0; HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_RESET); // 选中目标 HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100); HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET); // 释放总线 raw = ((uint16_t)rx[0] << 8) | rx[1]; return raw; }多路复用后要注意三个事:每个通道转换时间是异步的,读取时读到的是各自最近一次转换结果;如果某一路热电偶开路,该芯片的SDO可能持续为高,占住MISO电平,所以程序上要增加异常标记;最后是采样周期,别在循环里把四个CS快速轮询一遍就结束,每个通道的转换需要250ms,建议启一个定时器,每250ms循环读一遍四路,或每路独立定时读取。
刷新率不仅是SPI读取频率,也受MAX6675内部转换周期限制。MAX6675每秒约可输出4个有效采样。如果你在例程里硬把读取间隔压到100ms,读到的值是旧值,看起来像温度不变。这种“例程没问题,但速度上不去”的现象,很多人都遇到过。
5. 几招验证例程是否正常的小工具手法
5.1 用逻辑分析仪看CS/SCK/MISO时序
拿到例程第一步不是直接接热电偶,而是先接一个逻辑分析仪,把CS、SCK、MISO三根线抓出来。MAX6675正常读取时,CS先拉低,SCK出现16个脉冲,MISO在SCK边沿附近稳定。如果看到CS拉低但SCK没有动作,说明SPI没有使能外设时钟或中断占用;如果MISO一直为高,检查接线和是否选择了SPI_MODE_1。
逻辑分析仪还能帮你确认最后温度值对应的二进制。例子中读回0x019A时,展开16位后应与表3-1中的二进制完全一致。若错位,往往是把SPI数据大小从8位配成了16位,导致读回顺序颠倒。
5.2 用串口打印观察温度跳变
例程包里一般都有串口打印代码。没接线时,通过ST-Link的虚拟串口把温度值以printf输出。如果打印的是很大数字或-9999,先确认D2位是否置位,也就是热电偶是否接好。K型热电偶的极性不能接反,红色线正极接MAX6675的T+,黄色线接T-,反过来会显示负温度。
观察跳变还有个技巧:把手捏住热电偶接头,看温度是否往37度方向接近。这个实验能同时检验冷端补偿是否工作。如果显示固定在约0.25的倍数且毫无变化,可能是数据位偏移,检查代码里的右移3位是否丢了或配置成了右移4位。
5.3 常见读取失败快速定位
最后给一个排查顺序表,适合遇到例程跑不通时逐条对照。
表5-1 MAX6675例程常见故障定位
| 现象 | 大概率原因 | 检查方法 |
|---|---|---|
| 温度一直是0 | CS未拉低或SPI分频过高导致无时钟 | 测SCK引脚波形 |
| 温度跳变且偏小 | 右移位数错 | 打印原始hex值 |
| -9999或开路 | 热电偶未接/接反/D2位被误判 | 检查热电偶极性和接头 |
| 读回0xFFFF | SPI总线无设备响应或模式错误 | 用逻辑分析仪看MISO电平 |
这几招如果能跑通,例程基本就算吃透了。后续在量产板上去掉测试代码时,别把CS的初始高电平去掉,否则上电瞬间会产生一次虚假片选,可能干扰其他SPI设备。
本文还有配套的精品资源,点击获取