ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于MAX6675的STM32热电偶测温与SPI驱动实现

基于MAX6675的STM32热电偶测温与SPI驱动实现 1. 先把原理说透为什么不能直接拿ADC量热电偶电压如果你手里已经有一块STM32F103C8T6最小系统板第一反应多半是K型热电偶输出的是电压我直接把信号线接到单片机的ADC引脚不就行了我第一次也是这么干的结果读数几乎纹丝不动。这不是ADC坏了而是K型热电偶的输出电压实在太微弱了。K型热电偶基于塞贝克效应两根不同金属镍铬-镍硅在测温端和参考端存在温差时会产生一个与温差相关的小电压。这个电压的系数大约只有41µV/°C。也就是说在0到100°C的范围内满量程输出也只有4mV左右。STM32F103C8T6的ADC是12位的参考电压3.3V下1个LSB大约是0.8mV。粗略一算温度每变化1°C只对应大约0.05个LSB这根本不是“精度差”的问题而是信号完全淹没在量程里ADC根本分辨不出来。更重要的是K型热电偶的输出电压还取决于参考端冷端的温度如果冷端温度漂个5°C等效误差就能达到5°C你连误差来源都找不到。MAX6675就是为这个场景设计的。它内部集成了三样东西一个低噪声放大器把热电偶的毫伏级信号放大到ADC能处理的电平一个冷端补偿电路用内部温度传感器实时测量参考端温度并自动补偿一个SPI接口的12位ADC把温度直接换算成数字量输出。所以它对MCU来说就是一个挂在SPI总线上的温度传感器不需要任何额外运算读出来的原始值经过简单换算就是真实温度。这正是我在这篇文章里选择MAX6675而不是直接用运放加ADC方案的原因——省掉一整套模拟前端设计难度和风险都大幅下降代价仅仅是SPI通信而已。要真正用好MAX6675必须看懂它输出的16位数据格式。每次读取MAX6675会返回一个16位的帧其中D15始终为0标志位D14到D3是12位温度数据二进制补码形式但实际只能表示正温度这个后面细说D2是热电偶开路检测标志1表示热电偶断开或接线异常D1和D0固定为0。温度数值换算公式很简单温度 读到的12位值 × 0.25°C。举个例子读取到的原始数据是0x0F20二进制展开就是0000 1111 0010 0000。砍掉低3位D2、D1、D0剩下的12位是0000 1111 0100也就是十进制的244。244 × 0.25 61°C。所以实际温度就是61°C。这个换算逻辑在模拟SPI和硬件SPI版本里完全一致代码里就是一行右移和一次乘法的事。2. 硬件连接引脚对应、电平匹配与最小系统板的供电坑接线看起来很简单MAX6675一共5个引脚但处理不当会让数据完全读不出来甚至烧坏芯片。我画一下我在F103C8T6最小系统板上实际使用的连接方式。MAX6675引脚功能连接目标STM32F103C8T6说明VCC电源正3.3V见下方供电说明GND电源地GND与单片机共地SCKSPI时钟PB13也可选PA5硬件SPI1时钟CS片选PB12低电平有效SO数据输出PB14主入从出MISO很多教程会告诉你VCC接5V这在Arduino上没问题但用在3.3V供电的STM32F103C8T6上就要谨慎。MAX6675的VCC允许范围是3.0V5.5V在3.3V供电下完全能正常工作而且它的SO引脚输出逻辑高电平就是3.3V直接连STM32的引脚没有任何电平不匹配问题。如果你非要用5V给它供电那么SO引脚输出的高电平也接近5V直接接到3.3V容忍的STM32引脚上就有风险——F103的GPIO虽然号称5V容忍但在某些情况下仍可能通过引脚内部的保护二极管产生漏电或闩锁效应。我的建议非常简单STM32F103C8T6用3.3VMAX6675也用3.3V两边都是同一个电源轨干净利落。供电电源的干净程度直接影响温度读数稳定。MAX6675内部有个精密放大器如果VCC纹波大或者数字噪声多读数就会在12°C范围内乱跳。我在做第一版测试时用了一个便宜的USB转TTL模块给它供电结果温度在室温下显示值波动超过2°C后来换成STM32板子上的3.3V输出波动立刻降到0.25°C左右。所以建议在MAX6675的VCC和GND之间靠近芯片的位置加一个0.1µF去耦电容如果在干扰比较强的环境电机驱动、继电器旁边可以再加一个10µF电解电容并联。这点已经在很多官方参考设计里有标注实际工程里不是可选项而是必选项。接线时还有一个容易被忽视的点热电偶输入的正负极。K型热电偶的两根线通常是一红一黄或一红一蓝其中黄色或蓝色接MAX6675的T红色接T-。如果接反了最典型的现象就是温度读数变成0或者出现开路标志位少数情况下读数会偏高但没有明显规律。另外热电偶线本身很细尽量缩短到MAX6675芯片的距离超过50cm就有明显引入噪声的风险。3. 模拟SPI版本不依赖外设先把时序控制在手里写完硬件连接第一版代码我强烈建议用模拟SPI也就是用普通GPIO手动翻转引脚来模拟SPI通信时序。原因有两个一是MAX6675的SPI时序非常简单原始非常适合理解二是它不依赖芯片的SPI外设配置只要引脚和代码逻辑正确基本一次就能跑通。等你把模拟版本调通再用硬件SPI版本做个对比就能真正理解外设和时序的关系。MAX6675的SPI读时序严格来说是这样的先拉低CS片选信号然后连续产生16个SCK时钟脉冲在每一个SCK的上升沿之前实际上是在上升沿时读取SO引脚的电平因为是高位先行MSB First读到的第一个bit就是D15第二个是D14依此类推。读完16个位后拉高CS一次完整读取结束。有一个细节值得单独说SCK的默认电平空闲电平必须为低。这是SPI模式0CPOL0, CPHA0的标准要求。你如果让SCK空闲时为高数据读出来就完全错乱。代码实现其实很简单我当时用的是标准库加延时函数逐位采集// 引脚定义PB12 CS, PB13 SCK, PB14 SO #define TCS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define TCS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define TSCK_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define TSCK_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_13) #define TSO_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) uint16_t MAX6675_ReadRaw(void) { uint16_t raw 0; uint8_t i 0; TCS_LOW(); // 片选拉低启动一次通信 delay_us(2); for (i 0; i 16; i) { raw 1; // 先把已有数据左移一位 TSCK_LOW(); // 时钟拉低空闲态 delay_us(1); if (TSO_READ()) raw | 0x0001; // 读SO置为当前bit TSCK_HIGH(); // 时钟拉高数据稳定 delay_us(1); } TCS_HIGH(); // 片选拉高结束通信 return raw; } float MAX6675_ReadTemp(void) { uint16_t raw MAX6675_ReadRaw(); if (raw 0x0004) // D2 1 表示热电偶开路 return -999.0f; return (raw 3) * 0.25f; // 去掉低3位换算温度 }这套代码的时序其实和MAX6675数据手册里的时序图是完全对应的。数据手册推荐的是在SCK上升沿读取SO因为MAX6675的数据在SCK下降沿之后更新、在上升沿时稳定所以在SCK拉高之前读是安全的。上面的代码在SCK为低时读取SO然后拉高SCK等效于在上升沿前完成采样实际测试完全没问题。调用方式也简单在主循环里每隔一段时间读一次float temp MAX6675_ReadTemp(); if (temp -900.0f) { printf(当前温度: %.2f °C\r\n, temp); } else { printf(热电偶未接好\r\n); }这里最需要注意的一点是读取间隔。MAX6675每次温度转换大约需要170ms到220ms手册明确建议读取周期不小于250ms。如果你在主循环里持续不断读取MCU反复向它发起SPI通信器件内部状态机可能出现转换被打断或输出不稳定的情况。我在最初调试时用了一个HAL_Delay(300)放循环里一切正常后来为了测试极限把延时去掉结果数据开始出现偶发的跳变这就是采样间隔太短的锅。4. 硬件SPI版本CubeMX一键配置但片选最好还是手动来当模拟SPI跑通之后你可能会觉得“这也没什么难的”这时候再上硬件SPI才能真正感受到两者的差别和取舍。硬件SPI的最大优势是MCU的外设自己产生时钟、自己接收数据CPU只需要发起一次传输然后去干别的事效率比GPIO翻转高好几个数量级。但对于MAX6675这种低速器件速度提升没有实际意义真正的意义在于你学会了CubeMX里SPI外设的正确配置方式后续接OLED、接Flash、接其他SPI传感器时这套经验能直接复用。在STM32CubeMX中配置SPI1的步骤和参数如下打开SPI1模式选择Full-Duplex Master。硬件片选NSS选择Disable原因后面单独讲。参数设置数据大小选8 Bits时钟极性CPOL选Low时钟相位CPHA选1 Edge这组合起来就是SPI Mode 0。传输方向选8 Bits默认的MSB First。预分频器BaudRate Prescaler选32。STM32F103C8T6的SPI1挂载在APB2总线上时钟是72MHz72 / 32 2.25MHz。MAX6675的数据手册标称最大SPI时钟是4.2MHz2.25MHz留了将近一倍的余量稳妥。在GPIO配置中把PB12设为普通推挽输出用作软件片选CS把PA5SCK、PA6MISO保持为SPI复用功能。按照上面配置生成Keil或IAR工程后核心读取代码用HAL库写起来非常简单uint16_t MAX6675_ReadRaw(void) { uint8_t txData[2] {0x00, 0x00}; // 发送两字节全0触发16个SCK uint8_t rxData[2] {0x00, 0x00}; uint16_t raw 0; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); // CS拉低 HAL_SPI_TransmitReceive(hspi1, txData, rxData, 2, 10); // 同步收发2字节 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); // CS拉高 raw (rxData[0] 8) | rxData[1]; return raw; }这段代码和模拟版本的逻辑一一对应片选拉低 → 产生16个SCLK → 从SO读取16位数据 → 片选拉高。因为MAX6675的SO引脚在SCLK下降沿更新数据、上升沿被读取所以SPI模式0数据在第一个边沿采样天然匹配。说回为什么CubeMX里要把硬件NSS disable掉片选用普通GPIO手动控制。硬件SPI的NSS有两种模式一种是NSS Output模式由SPI外设自动拉低片选另一种是NSS Pulse模式只在传输过程中自动产生片选脉冲。看起来很方便但实际用起来有两个问题第一NSS自动控制的时序和MAX6675的要求不完全一致MAX6675要求CS拉低后至少等待一小段时间再开始时钟而硬件NSS在传输结束后立刻释放两次读取之间的间隔控制不住第二HAL库在处理NSS自动拉低时经常出现判断标志位的时序问题初学者排查起来非常头疼。相比之下用普通GPIO手动拉CS时序完全可控代码可读性也更高代价仅仅是MCU多了一条GPIO的指令开销这在低速传感器场景中完全可以忽略。这个经验也适用于大多数SPI从机设备除非你非常确认从机支持硬件NSS的精确时序否则一律软件片选。硬件SPI版本还需要注意HAL库的一个细节HAL_SPI_TransmitReceive是一次阻塞式传输第二个字节传完后函数才返回。因为MAX6675的转换周期是170ms以上MCU阻塞在这里的时间非常短2个字节在2.25MHz下不到1µs所以完全不用担心阻塞影响主循环这也是它能用于实时性不高场景的原因。如果你的系统对实时性有更高要求可以考虑用DMA方式替代阻塞传输但那就是另一个进阶话题了。5. 两个版本怎么选从可靠性、速度、扩展性三个维度对比很多人会纠结一个问题既然硬件SPI配置也不难那模拟SPI是不是多余的我的看法恰恰相反。在MAX6675这个具体场景里模拟SPI不仅不是多余的在某些维度上反而更“稳”。这里我可以给出一组对比数据都是我在同一条线路上实测的结论。对比维度模拟SPIGPIO翻转硬件SPI外设我的评价代码量约20行约15行差不多模拟版略长但直观速度上限取决于代码执行速度实测能到几百kHz可达18MHz对MAX6675意义不大引脚占用任意3个GPIO必须用SPI复用引脚硬件版引脚限制更严格时序可控性完全可控代码即时序受外设计数器影响模拟版更灵活DEBUG难度逻辑清晰容易排查需理解外设寄存器/库函数新手更友好的是模拟版扩展SPI器件每个器件都要重写时序挂总线代码几乎不用变硬件版扩展性碾压抗干扰能力取决于GPIO配置和代码时序外设时钟更稳硬件版略优从实测波形来看模拟SPI在GPIO翻转时会存在一些毫伏级的尖峰因为GPIO切换瞬间有电流突变但MAX6675对SCK的边沿质量不敏感只要高低电平在合理阈值内数据就是正确的。我测过在50cm长的杜邦线连接下模拟SPI仍然能稳定读出温度数据没有出现丢位的情况硬件SPI在高频下表现更好但如果把预分频设得太小比如2.25MHz以上在一些长线连接场景中反而出现偶发通信错误。所以对于这块蓝色Pill板加MAX6675模块的标准配置用1MHz到2MHz的SPI时钟是最舒服的区间。什么时候必须用硬件SPI答案是当你需要同时挂接多个SPI设备时。比如你的系统里除了MAX6675还有一块OLED屏幕、一片Flash存储芯片它们共用SCK和MISO总线只是片选不同。用模拟SPI你得为每个设备各写一套时序代码GPIO也得分配更多用硬件SPISCK和MISO固定复用引脚每个设备只需要一个GPIO做片选HAL库的HAL_SPI_TransmitReceive函数可以直接原样复用代码量少很多而且DMA支持也是现成的。这种情况下硬件SPI的扩展优势是压倒性的。但反过来如果你的项目就只测一路温度且单片机的其他引脚已经被占得差不多了——比如这个F103C8T6最小系统板上同时接了串口屏、按键矩阵、PWM输出——硬件SPI的固定引脚可能不太好布线而模拟SPI可以任意映射到三个空闲引脚灵活性就体现出来了。我见过一个工程把模拟SPI跑在PA1、PA2、PA4三个引脚上旁边密密麻麻全是其他走线功能完全正常。总结我的个人经验首次调通选模拟SPI产品化或扩展选硬件SPI两者都需要会两种代码我下面都会给完整版。6. 排雷实录我在实测中遇到的6个常见问题把两个版本的代码都跑通之后你多半会遇到一些奇怪的现象。这里列出我在实际调试MAX6675测温时踩过的坑和排查过程按出现频率排序。现象可能原因排查方向读数恒为0x7FF0热电偶开路D2标志位为1检查T/T-是否虚接、热偶线是否断裂读数为0x0000片选无效或SCK无时钟用示波器看CS和SCK是否有波形检查引脚配置读数为0xFFFFSO引脚浮空或读取错误检查MISO引脚是否配置为上拉输入确保SO有线信号温度值跳变2°C以上VCC噪声大/采样间隔太短加0.1µF去耦电容读取间隔至少250ms温度比实际低5~10°C冷端补偿误差MAX6675芯片本身靠近热源需要让芯片远离被测物温度始终显示0°C热电偶极性接反交换T和T-接线第一个高频问题就是读数恒为0x7FF0。这个值很有迷惑性因为它不是全0也不是全F而是温度位全部为1、开路标志位也为1的样子。我在第一次接线时因为杜邦线松动热电偶T引脚接触不良读到的就是这个值。判断方法很简单读原始数据检查第2位0x0004是否为1是1就一定是热电偶开路或没接好。在代码里我通常会加一个状态返回直接提示用户检查传感器这样可以避免把错误温度显示到界面上否则用户可能看到400°C这样的离谱温度而误以为系统故障。这一点在写正式项目时务必加进去。第二个让我印象很深的是关于负温度的问题。MAX6675的数据格式虽然采用了补码形式但它实际上只能测量0°C以上的温度范围是0°C到1023.75°C。当输入温度低于0°C时芯片输出会直接显示0°C不会有负数出现。如果你需要测量零下温度必须更换支持负温的芯片比如MAX31855支持K型热电偶量程-270°C到1370°C。这个限制在芯片选型时就要确认清楚否则做到一半才发现整个方案都得推翻。第三个坑是关于冷端补偿的精度的。MAX6675的内部冷端补偿传感器的精度在室温附近大概是±2°C的水平但它补偿的是芯片引脚处的温度不是板子上某个任意位置。如果你把MAX6675模块紧贴在被加热的元件旁边或者放在一个封闭的机箱里芯片自身温度升高冷端补偿会因为测到的是模块局部温度而非真正的热电偶冷端温度而产生额外误差。实测中我把模块放在热风枪旁边30cm处环境温度上升了大约8°C温度读数就偏高了约4°C。所以安装位置也要当作设计的一部分来考虑。还有一个容易被忽视的细节是CS引脚在空闲时必须保持高电平。我用硬件SPI版本调试时曾遇到这样一个问题第一次读取温度正常第二次之后就一直是同一个值不变。排查到最后发现是因为我把CS拉低之后在一次通信结束后忘记把它拉高MAX6675的片选一直被拉低内部状态机卡住后续所有转换都停止了。这个问题的规避方式就是每次读完必须显式拉高CS代码中确保HAL_GPIO_WritePin的时序不要被中断或者异常返回跳过。最后如果温度读数正常但存在轻微跳动±0.25°C到±0.5°C这是MAX6675的量化噪声属于正常现象。因为它的分辨率就是0.25°C所以读数的抖动不可能小于这个量。如果你需要更平滑的显示可以在应用层加一个简单的中值滤波或者滑动平均比如连续读5次取平均跳动基本能压到0.25°C以内。但要注意滤波不改变真实精度只是让显示更稳定。这个处理方式我在最终项目里是加上去的效果直观可见。整套流程走下来从原理、接线、模拟SPI到硬件SPI再到排雷就是一个完整的STM32F103C8T6接MAX6675测温方案了。代码我分成两个版本分别给出测试时优先用模拟版本验证接线和传感器状态再用硬件版本做产品化适配。虽然文章写了不少字实际调试一遍大概只需要一个晚上核心只是理解16位数据格式和250ms采样间隔这两件事——做到了剩下的都是一马平川。
返回列表