STM32F103C8T6+MAX6675+K型热电偶测温教程:SPI时序与代码详解
发布时间:2026/10/5 12:47:17 作者:尧图编辑部 阅读量:1,286

做过温度采集的兄弟应该都有体会测温这件事看着简单真想测准了没那么容易。NTC查表麻烦、PT100电路又贵、DS18B20上限才125℃所以当你要测烙铁头、加热台、烤箱内部温度时最省事的方案基本就是K型热电偶配MAX6675再接一块三十块钱的STM32F103C8T6最小系统板。这篇教程我把从接线到代码的完整流程整理出来包含模拟SPI和硬件SPI两套写法顺带把MAX6675的数据格式、时序细节、常见坑全部讲清楚照着抄就能用。1. 为什么选这套方案需求分析与选型思路1.1 测温需求到底卡在哪先说清楚一个问题你手里的温度传感器到底适不适合这个场景NTC热敏电阻是很多人的入门选择成本低、电路简单但是它是非线性器件想测准必须查表或者上Steinhart-Hart方程做拟合而且常见的NTC也就是-50℃到200℃左右的量程想测高温根本不够用。DS18B20用的人多是因为它自带数字接口、单总线只占一个引脚但是它的温度上限只有125℃同样的毛病测不了高温。PT100铂电阻精度高、稳定性好可是需要恒流源或者电桥电路还要做非线性修正和冷端补偿成本上去了调试难度也上去了。K型热电偶就不一样分度号K的热电偶量程能做到-200℃到1300℃左右工业上用得非常多价格也便宜一根不锈钢铠装的K型热电偶也就几块钱到十几块钱。缺点是热电偶输出的是毫伏级的微弱热电动势必须用专用芯片来处理放大、冷端补偿和数字转换否则你拿ADC直采精度惨不忍睹。MAX6675就是专门干这个活的芯片内置放大器、冷端补偿和12位ADCSPI数字接口输出MCU这边几行代码就能读到温度非常省心。1.2 为什么是STM32F103C8T6 MAX6675STM32F103C8T6这颗芯片在国产替代浪潮之前就是学生和工程师玩得最多的ARM Cortex-M3芯片之一。64KB Flash、20KB RAM主频72MHz有SPI、I2C、USART、ADC、PWM这些外设跑个温度采集加上简单的控制逻辑完全绰绰有余。关键是它的资料太多了从标准外设库到HAL库从寄存器操作到CubeMX配置随便一搜就是一堆参考遇到问题不用自己憋着。现在国产的GD32、APM32等芯片在引脚上做了兼容替换很多项目不用改板子就能换方案温度采集这种基础应用几乎不受影响。拿它和MAX6675搭配还有一个现实原因MAX6675的SPI时序并不复杂但很多人第一次调SPI都会卡在“为什么读回来全是0”、“为什么数据是乱的”这些问题上。STM32F103C8T6既支持硬件SPI又能用普通GPIO模拟SPI两套方案可以互相验证。如果你用的是别的MCU比如ESP32、Arduino读法和这里讲的原理完全一致只是寄存器或库函数名字不一样。所以这篇教程的核心其实是在讲清楚“SPI怎么把一个16位数据从传感器里读出来”搞懂这个换任何平台都一样。2. 硬件准备与接线避坑2.1 材料清单这个项目需要的材料很少而且都很容易买到我把清单和大致用途列一下材料规格/说明数量STM32F103C8T6最小系统板蓝色药丸板即可1块MAX6675 K型热电偶模块成品模块板载上拉电阻和接线端子1块K型热电偶标配插头测量端裸露或铠装1根ST-Link / 串口下载器程序下载调试1个杜邦线公对母或母对母长度尽量短若干面包板可选方便快速搭电路1块104瓷片电容可选给MAX6675电源附近做滤波1个MAX6675模块注意买那种标准的8脚芯片小板上面有VCC、GND、SCK、CS、SO这5个引脚还有一个K型热电偶的接线端子。有些模块把CS标成CS或CSSSO就是MISO叫法不同本质一样。2.2 引脚接线我用的引脚是STM32F103C8T6上比较常规的SPI1引脚PA5做SCKPA6做MISOPA4做片选CS。PA7MOSI在这个项目里其实用不上因为MAX6675只是输出数据不吃我们发过去的内容。接线表如下MAX6675模块引脚接STM32F103C8T6引脚说明VCC3.3V给模块供电建议3.3VGNDGND共地SCKPA5SPI时钟CSPA4片选低电平有效SOPA6数据输出接MISO这里有个细节要提醒如果你的MAX6675模块上板载了稳压芯片那你接3.3V或者5V问题不大如果模块上没有稳压板子上的芯片就是直接由VCC供电的那么接5V时SO输出的高电平也是5V直接怼到STM32的PA6引脚上长期使用有风险。所以最稳妥的做法就是一律接3.3V供电这样MAX6675的SO输出高电平也就是3.3V完全匹配STM32的IO电平。2.3 供电、极性与布线注意事项第一个坑就是热电偶的极性。K型热电偶两根线正极通常是红色负极通常是蓝色或者黄色不同厂家颜色可能有差异但绝大多数红色是正极。接反了读出的温度会随真实温度升高而下降而且偏差特别离谱不是简单换算能救回来的。第二个坑是杜邦线的长度。SPI通信在短距离下很稳定但如果你用非常长的杜邦线去接MAX6675干扰会明显上升。我试过用20厘米以上的飞线接温度读数偶尔会跳一个很大的值尤其是附近有继电器或者电机启停的时候。建议杜邦线控制在10厘米以内如果实在要长距离可以降低SPI时钟频率或者改成屏蔽线。第三个坑是电源纹波。MAX6675内部有ADC对电源噪声比较敏感。如果你用的是电脑USB口供电又同时带着大电流负载可能会看到温度值低频跳动。解决办法是在MAX6675模块的VCC和GND之间加一个104瓷片电容靠近模块电源引脚放置效果立竿见影。3. MAX6675的数据格式与SPI时序看得懂才能调得通3.1 MAX6675内部在工作什么MAX6675本质上是一个专门针对K型热电偶的信号链芯片。K型热电偶在测量端和冷端之间由于温差会产生一个微小的热电动势大约每摄氏度变化对应41微伏左右这个信号非常微弱。MAX6675内部先对热电偶的毫伏信号做高增益放大再通过内部温度传感器测出冷端温度将冷端温度对应的电动势叠加进去实现冷端补偿最后用12位ADC把电压转换为数字量。这就是为什么你不需要自己接冷端补偿电路MAX6675帮你把“冷端温度为0℃时显示0℃”这个校准工作做了。每次转换周期大约是220毫秒也就是说MAX6675不是每时每刻都在输出最新结果你读到的数据最快也只能以5Hz左右的频率更新。所以如果你打算用它做PID控温控制周期至少要在200毫秒以上否则你拿到的是上一周期的旧值。数据通过SPI接口传出MAX6675只能读不能写所以主机给它发什么数据都没有意义关键是把16个时钟脉冲打出来然后从SO引脚把数据一位一位收进来。3.2 16位数据的位定义MAX6675每次读取得到的是一个16位数但这16位不是全是温度。从上到下每一位这样划分位序名称含义D15伪校验位恒定为0D14 ~ D3温度数据12位有符号整数1 LSB 0.25℃D2热电偶开路检测1表示热电偶断开或未接D1 ~ D0设备ID位恒定为0所以读回来的原始值先看D2位如果为1说明热电偶没接好或者线路断开了此时温度数据无效。如果D2为0把高12位转成温度也就是把原始值右移3位再乘以0.25得到的就是摄氏温度。举个例子读回原始值0x0121二进制是0000 0001 0010 0001。D2位为0说明热电偶正常。右移3位之后得到0x24也就是十进制的36再乘以0.25得到9.00℃。如果环境温度是9度这个结果就非常合理。3.3 时序上升沿读、下降沿换、CS边界MAX6675的SPI时序和很多SPI从机不太一样这个点很关键。看数据手册时序图当CS被拉低之后等到SO引脚数据稳定主机在SCK上升沿去采样SO电平然后SCK拉低MAX6675在这个下降沿会切换输出下一位数据。简单说就是下降沿换数据上升沿读数据。这对应到SPI模式上就是CPOL0、CPHA1的模式时钟空闲为低电平数据在第二个边沿上升沿被采样。当然这只是严格来说实际中很多人的硬件SPI用CPOL0、CPHA0也能读出数因为MAX6675建立数据的时序窗口比较宽裕但为了规范和稳定还是建议按照数据手册来配置成CPOL0、CPHA1。还有一个容易忽略的点CS拉低之后不能立刻就开始打时钟要给MAX6675一点时间让它把第一位数据准备好。数据手册上CS下降沿到SO数据输出稳定有最小时间要求实际做的时候在拉低CS之后加个几微秒延时或者用HAL_Delay加至少1毫秒稳得很。CS拉高之后芯片开始新一轮转换这个拉高时间也要确保足够否则可能触发无效转换。4. 模拟SPI版本不依赖外设手写时序4.1 为什么还需要模拟SPI有人会问硬件SPI现成的为什么还要用模拟SPI原因有几个第一你的引脚不一定够或者SPI1的引脚要被其他外设占用这时候用任意IO口模拟就灵活得多第二模拟SPI能帮你彻底理解时序万一硬件SPI调不通用模拟SPI先验证传感器和接线没有问题排查问题就快很多第三有些国产MCU的SPI外设行为不完全一样模拟SPI是一个跑不掉的兜底方案。模拟SPI本质上就是用GPIO的置高置低来产生时钟信号用另外一个GPIO读取数据线电平完全靠软件延时来控制时序。MAX6675数据量不大读取一次才16个bit模拟SPI的速度完全够用。4.2 GPIO初始化用标准外设库写的话GPIO初始化长这样void MAX6675_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SCK - PA5 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // CS - PA4 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // MISO - PA6 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // CS 默认拉高不选中从机 GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); // SCK 默认低电平 }PA5和PA4配置成推挽输出PA6配置成浮空输入。为什么PA6不用上拉因为MAX6675的SO引脚是推挽输出本身就具备驱动能力不需要外部上拉。如果你用的模块上带了上拉电阻那也没影响。4.3 读取函数与逐行解释下面是核心读取函数uint16_t MAX6675_ReadRaw(void) { uint16_t raw 0; uint8_t i; CS_LOW(); // 拉低片选选中MAX6675 delay_us(5); // 等待数据线稳定 for (i 0; i 16; i) { SCK_HIGH(); // SCK拉高上升沿附近数据稳定 delay_us(2); // 等待高电平稳定 raw 1; // 左移一位为当前bit腾出最低位 if (MISO_READ()) // 读取SO引脚电平 { raw | 0x0001; } SCK_LOW(); // SCK拉低触发MAX6675更新下一位 delay_us(2); // 等待低电平稳定 } CS_HIGH(); // 拉高片选结束一次读取 return raw; }逐行说几个重点。CS_LOW之后加延时是等MAX6675把第一位的D15准备好。进入循环后先把SCK拉高再读因为按照时序图数据在上升沿时是稳定的这时候读MISO电平最可靠。读完当前位再把SCK拉低此时MAX6675会更新下一位到SO上之后进入下一轮循环。循环16次读回来的bit被依次拼进raw变量。两个延时取值大约2微秒SCK频率大约在几十到一百多kHz远低于MAX6675能接受的极限非常稳。如果读回来的数据全是0xFF或者全无规律可以尝试把延时加大一些排除是时序太快的问题。最后把原始值转换成温度float MAX6675_GetTemp(void) { uint16_t raw MAX6675_ReadRaw(); if (raw 0x0004) // D2位为1热电偶开路 { return -1000.0f; } return (float)(raw 3) * 0.25f; }这里检测D2位为1就说明热电偶断了或者没接返回一个特殊值方便上层处理。模拟SPI版本的好处是每一步都是可控的出问题也好排查在调试硬件SPI之前先用这个版本验证模块是否正常能帮你省掉大量排查时间。5. 硬件SPI版本CubeMX配置与HAL代码5.1 CubeMX配置步骤模拟SPI能用但正式项目里没人愿意拿GPIO去模拟时序毕竟CPU被延时占着很多事情干不了。这时候就要用STM32F103C8T6的硬件SPI1了。我平时习惯用CubeMX生成初始化代码再用HAL库写应用层配置一次后面都好维护。打开CubeMX具体配置步骤新建工程芯片型号选择STM32F103C8Tx。在RCC配置里把High Speed Clock (HSE) 选为Crystal/Ceramic Resonator。如果你的板子上没有外部晶振这一步也可以不配直接用HSI但时钟树配置会有一点差别。在SYS配置里把Debug选为Serial Wire。这一步非常重要不选的话下次程序下载可能会报错因为调试口被占用了。很多人调SPI调着调着突然下载不了程序就是这里没配。进入SPI1配置页面Mode选择Full-Duplex Master。硬件NSS先不用管保持Disable我们要用软件片选。在Parameter Settings里重点配置以下参数Data Size8 BitsPrescaler64Clock Polarity (CPOL)LowClock Phase (CPHA)2 EdgeNSSSoftwareBaud Rate这里分频64之后大约是1.125MHz对MAX6675来说足够而且抗干扰更好。Data Size为什么不选16 Bits很多人以为读16位数据就配16位帧实际上去用16位帧在HAL库里有字节序的坑不同版本的库表现还不一样读写逻辑容易绕晕。用8位帧直接收两个字节自己手动拼成16位反而直白可靠。把PA4配置为GPIO_Output作为MAX6675的CS脚。默认输出电平设置为High也就是初始状态不选中芯片。这里有一点需要注意在GPIO配置页面如果PA4被自动分配为SPI1_NSS功能你要手动改回GPIO_Output否则片选引脚会被SPI外设占用软件片选就失效了。时钟树保持默认即可如果用了外部8MHz晶振可以把HCLK调成72MHz。生成代码。5.2 硬件SPI读取代码CubeMX生成的代码里SPI1的初始化已经有了我们只需要写一个读取MAX6675的函数放到用户代码区。#define MAX6675_CS_GPIO_Port GPIOA #define MAX6675_CS_Pin GPIO_PIN_4 void MAX6675_Init(void) { // CS 默认拉高不选中 HAL_GPIO_WritePin(MAX6675_CS_GPIO_Port, MAX6675_CS_Pin, GPIO_PIN_SET); } uint16_t MAX6675_ReadRaw(void) { uint16_t raw 0; uint8_t rx_buf[2] {0, 0}; HAL_GPIO_WritePin(MAX6675_CS_GPIO_Port, MAX6675_CS_Pin, GPIO_PIN_RESET); HAL_Delay(1); // 拉低片选之后等数据稳定 HAL_SPI_Receive(hspi1, rx_buf, 2, 10); HAL_GPIO_WritePin(MAX6675_CS_GPIO_Port, MAX6675_CS_Pin, GPIO_PIN_SET); raw (rx_buf[0] 8) | rx_buf[1]; return raw; }有几个地方必须说明。HAL_SPI_Receive虽然是“接收”函数但SPI是全双工协议接收时主机必须提供时钟所以HAL库内部会同时发送一个空字节来产生SCK时钟。很多人第一次用会疑惑觉得只收不发为什么还能读到数据其实你发的那个字节内容无所谓MAX6675根本不看MOSI。读取时序上CS拉低后延时至少1毫秒这个延时不能省有些时序敏感的场景下CS拉低后立刻打时钟第一个bit会不稳定。读完两个字节后一定要把CS拉高否则MAX6675会一直处于被选中的状态不会启动新的转换周期同时还会可能导致后续通信异常。温度换算函数跟模拟SPI版本完全一样可以直接复用。5.3 硬件SPI和模拟SPI怎么选对比项模拟SPI硬件SPI占用引脚任意3个GPIOSPI外设固定引脚或重映射引脚CPU占用读取期间被延时阻塞收到DMA或中断后几乎不占CPU调试难度每一步可控易排查配置出错时现象隐蔽速度低约几十kHz可达几MHz足够用典型适用场景临时验证、引脚紧张、平台特殊正式项目、需要低CPU占用我的建议是前期先用模拟SPI版本验证传感器和接线确认没问题之后再切换硬件SPI版本这样出问题的时候你能明确知道是传感器问题还是SPI外设配置问题不会两件事搅在一起。6. 温度换算、精度分析与软件优化6.1 原始值到温度的换算前面提过公式这里再展开说一次。从MAX6675读回来的16位原始值raw先判断D2位是不是1如果是1直接返回错误。然后右移3位去掉底部的D2、D1、D0得到12位温度数据。float MAX6675_GetTemp(void) { uint16_t raw MAX6675_ReadRaw(); if (raw 0x0004) { return -1000.0f; } float temp (float)(raw 3) * 0.25f; return temp; }0.25这个系数就是12位ADC的分辨率。MAX6675的温度范围是0℃到1023.75℃12位ADC对满量程4096等分每个LSB对应0.25℃。这里有个细节有些资料会把MAX6675说成可以测零下但从数据手册看它本身不是针对负温设计的测温范围下限就是0℃左右所以如果你要测-20℃这种低温MAX6675并不是好选择得考虑MAX31855或者其他方案。6.2 精度到底有多少0.25℃是分辨率不是精度。很多人拿到0.25℃的分辨率就以为能测到0.25℃的绝对精度实际差距很远。MAX6675的数据手册给出的系统精度在常温附近大约±2℃到±3℃这已经考虑了冷端补偿、ADC量化、放大电路漂移的综合影响。误差主要来自几个方面。第一是冷端补偿误差MAX6675内部测温传感器测的是芯片封装附近的温度如果你的模块周围有发热元件或者环境温度变化迅速冷端补偿就会不准。第二是热电偶本身的分度偏差K型热电偶根据等级不同有正负1.5℃到2.5℃的误差。第三是ADC的量化误差和电路噪声。所以如果你拿它做烙铁头温度显示正负两三度完全能接受如果你要做精密恒温槽光靠MAX6675是远远不够的需要更高精度的测温链路。6.3 采样滤波和读取节奏由于MAX6675的转换周期是220毫秒左右程序里调用读取函数的间隔不要小于200毫秒。如果读得太频繁你会得到连续多个相同或者跳动很大的值因为芯片还没完成新一次转换。在工业现场应用中我习惯连续读3次取中值或者做滑动平均这样能把偶发的噪声尖峰滤掉。比如这样float MAX6675_GetTempFiltered(void) { float t1 MAX6675_GetTemp(); HAL_Delay(50); float t2 MAX6675_GetTemp(); HAL_Delay(50); float t3 MAX6675_GetTemp(); if (t1 -500 || t2 -500 || t3 -500) { return -1000.0f; } // 三个数排序取中值 float max (t1 t2) ? t1 : t2; float min (t1 t2) ? t1 : t2; if (t3 max) return max; if (t3 min) return min; return t3; }这样做的代价是更新频率更低了大概200毫秒出一个新值。如果做PID控温建议直接用一次滤波后的值作为反馈控制周期拉长到500毫秒左右效果比较稳。如果你想上FreeRTOS这个温度读取完全可以拆成一个独立任务每200毫秒唤醒一次读一次温度丢进消息队列或者共享变量其他地方只管取最新值就行。7. 我踩过的坑常见问题排查实录7.1 现象-原因-解法速查表把我和周围朋友实际遇到过的典型问题整理成一个表按现象排查会快很多现象常见原因解决办法温度读回来全是0xFFFF或数据异常大MISO和SCK接反检查接线SO接PA6SCK接PA5温度始终显示-1000也就是D2置1热电偶断线、没接好、插头松动重新插拔热电偶检查端子是否压紧温度跳动幅度大、无规律杜邦线太长、电源纹波大、时钟太快缩短接线、加104电容、降低SPI时钟模拟SPI正常硬件SPI读不出SPI模式配错或复查CS被硬件NSS占用确认CPOLLow、CPHA2 EdgeCS换成软件GPIO硬件SPI读回的字节顺序反了用的是8位帧两个字节拼反了交换rx_buf[0]和rx_buf[1]的顺序程序下载一次后第二次下载失败没配置Serial WireSWD口被禁用按住复位键点下载之后重新配置SYS的Debug温度值跟着板子温度漂MAX6675芯片附近有发热器件把模块远离功率器件或者单独供电读数始终为0CS引脚初始电平为低芯片没被释放初始化时先拉高CS再开始读取7.2 几个容易忽略的致命细节第一个是CubeMX里PA4被自动复用成SPI1_NSS的问题。你明明在代码里用HAL_GPIO_WritePin拉低CS但片选就是不起作用因为GPIO已经切换到外设复用功能。解决方法是去GPIO配置页面把PA4的模式改回GPIO_Output同时NSS选择Software两者缺一不可。第二个是读取时序中CS拉低后等待数据稳定的延时问题。我见过不少人把CS拉低之后立刻开始读取结果第一位有时候对有时候错数据偶尔冒一个离谱的大值其实就是这个起始等待时间不够。模拟SPI版本里加个5微秒的延时硬件SPI版本里用HAL_Delay(1)都不麻烦但能省不少排查时间。第三个是接线端子压线的问题。MAX6675模块上的K型热电偶插头看起来插进去了其实没插到位或者压线螺丝没拧紧导致偶发接触不良D2位时好时坏。这种情况在排查时特别迷惑人一开始以为是代码问题最后发现是插头松了。所以遇到D2位报开路第一步永远是检查机械连接不要先怀疑时序。第四个是调试顺序问题。我的建议永远是先跑模拟SPI版本确认传感器和基本接线没问题再切硬件SPI。如果你直接用硬件SPI调试一个问题可能同时涉及传感器、接线、SPI配置、CubeMX设置四层因素排查起来会浪费时间。8. 写在最后的几条经验这套方案我自己做过加热台控温、热风枪测温、还有一个小型回流焊预热板累计跑了大半年整体非常稳定。最大的感受是MAX6675这个芯片虽然老但作为一款廉价的数字式热电偶采集方案在0℃以上、精度要求不高的场景里性价比确实无可替代。你要是手头有现成的STM32F103C8T6板子花十几块钱整个MAX6675模块和热电偶一下午就能跑出温度曲线顺便把SPI时序彻底吃透。模拟SPI和硬件SPI双版本代码其实并不是重复劳动调试的时候你会体会到什么叫“双保险”先把模拟SPI跑通确认传感器没坏、极性对了、接线没错然后再上硬件SPI配置有问题时至少知道问题在外设配置层不会一头雾水。另外做完这个基础测温之后可以往两个方向扩展一个是用它做恒温控制配合PID算法和固态继电器控制加热器功率做个简易回流焊台或者恒温箱另一个是接OLED屏幕或者连WiFi模块把温度实时上传到上位机或者网页上。MAX6675的数据就是这么简单的一个16位数后面你怎么玩都行。最后再分享一个小习惯每次读回原始值之后我会顺便在调试串口打印原始值和换算后的温度比如“raw0x0121, temp9.00℃”开发阶段非常方便定位问题。等到正式运行的时候再把这行打印去掉省得串口刷屏影响系统效率。