ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32寄存器驱动MAX31856热电偶测温:SPI初始化与温度计算

2026/9/15 16:01:23 拓冰建站 浏览量
STM32寄存器驱动MAX31856热电偶测温:SPI初始化与温度计算 简介基于STM32F103ZET6与MAX31856的测温工程以寄存器操作为主线面向嵌入式初学者及工业温度测量开发者专门解决热电偶数据采集、SPI接口配置和驱动移植等问题。资源压缩包约1.77MB整体为STM32工程源码包含主程序、头文件及引脚接线说明文件结构清晰注释详尽便于按需查阅。目前已有1054人学习下载。工程从SPI初始化入手详细实现了MAX31856各寄存器的写读流程包括配置寄存器、状态寄存器、数据寄存器以及CRC校验部分并配有冷端补偿说明使用户能快速理解工业级温度测量的底层机制。作者还特别整理了STM32与MAX31856之间SCK、MISO、MOSI、NSS等引脚的接线注意事项并留有联系邮箱后续使用中遇到问题可直接沟通。通过该项目既可掌握寄存器级驱动编写能力也能为基于热电偶的高精度测温二次开发打下坚实基础。1. 用STM32F103ZET6寄存器方式驱动MAX31856解决热电偶测温的哪些痛点MAX31856是Maxim现ADI推出的热电偶数字转换器内置ADC、冷端补偿和线性化查找表K型热电偶直接接上去MCU通过SPI读出温度值精度做到±0.15%级别。很多工程师第一反应是用HAL库的SPI驱动但遇到项目要移植到RTOS、裸机时间片调度或者做量产校准的时候寄存器写法反而更好控制启动时间确定、占用的RAM和Flash小、每个时钟周期都知道在干什么。特别是STM32F103ZET6这颗芯片寄存器手册在网上随便能翻到配合MAX31856的寄存器表整个测温链路从硬件到协议完全透明。这篇博文按“接线 → SPI寄存器初始化 → MAX31856寄存器读写 → 温度计算 → 调试”的顺序讲照着抄就能用。如果你是做加热台、工业温度变送器、环境监测或者毕业设计这篇文章都能直接对接你的需求。2. MAX31856与STM32F103ZET6的SPI接线说明与GPIO寄存器配置2.1 六线接线表VCC/GND/SCK/MOSI/MISO/CS/FAULT 怎么连MAX31856是标准SPI从设备8脚SOIC封装。供电范围3.0V到3.6VSTM32F103ZET6的片上外设电平是3.3V两者可以直接连接不需要电平转换。唯一要注意的是MAX31856的FAULT引脚是开漏输出外部必须接上拉电阻STM32这边配置成上拉输入即可。典型接线如下表SPI1挂载在APB2总线上主频72MHzSCK最高可达18MHz但MAX31856的SPI时钟上限是5MHz所以分频数要专门算一下后面会讲。MAX31856引脚引脚名接STM32F103ZET6说明1CSPA4SPI1_NSS软件控制禁止硬件NSS2SCKPA5SPI1_SCK3MISOSOPA6SPI1_MISOMAX31856输出4MOSISIPA7SPI1_MOSIMCU输出5GNDGND信号地热电偶负端也要共地6FAULTPA3开漏输出接10kΩ上拉到3.3V7VCC3.3V并联100nF10μF去耦电容8GNDGND模拟地与数字地单点汇合热电偶的两根线直接接到MAX31856的T和T-引脚上注意K型热电偶的红色线是正极接T银色线是负极接T-。接反了温度读出来是负值这是个特别容易踩的坑。如果热电偶线比较长超过1米建议在靠近MAX31856芯片处加一个100pF的共模滤波电容T和T-各对地接一个。2.1.1 FAULT引脚必须处理不能悬空FAULT是开漏结构内部没有上拉悬空时电平不定。工程上通常把它接到MCU的一个外部中断引脚上热电偶开路时FAULT拉低MCU立刻响应。如果只是轮询读取把PA3配置成上拉输入轮询SR寄存器里的开路标志位也是一样的效果。但一定要在MAX31856的CR0寄存器里使能开路检测功能否则FAULT永远不动作后面初始化代码里会体现。2.2 用寄存器函数初始化SPI1不用库STM32F103ZET6的SPI1寄存器基址是0x40013000控制寄存器CR1、CR2状态寄存器SR数据寄存器DR一一对应RM0008参考手册。下面的初始化函数完全绕开标准外设库和HAL库直接操作寄存器地址。#define RCC_APB2ENR (*(volatile uint32_t *)0x40021018) #define GPIOA_CRL (*(volatile uint32_t *)0x40010800) #define GPIOA_CRH (*(volatile uint32_t *)0x40010804) #define GPIOA_ODR (*(volatile uint32_t *)0x4001080C) #define SPI1_CR1 (*(volatile uint32_t *)0x40013000) #define SPI1_CR2 (*(volatile uint32_t *)0x40013004) #define SPI1_SR (*(volatile uint32_t *)0x40013008) #define SPI1_DR (*(volatile uint32_t *)0x4001300C) #define CS_LOW() (GPIOA_ODR ~(1 4)) #define CS_HIGH() (GPIOA_ODR | (1 4)) void SPI1_Init_Reg(void) { // 1. 打开GPIOA和SPI1的时钟APB2ENR的bit2为GPIOAbit12为SPI1 RCC_APB2ENR | (1 2) | (1 12); // 2. 配置PA5(SCK)、PA6(MISO)、PA7(MOSI)为复用推挽输出PA4(CS)为通用推挽输出 // CRL寄存器每4位控制一个引脚MODE[1:0] CNF[1:0] // PA4: MODE11(50MHz)CNF00(通用推挽) // PA5/PA7: MODE11(50MHz)CNF10(复用推挽) // PA6: MODE11(50MHz)CNF10(复用推挽) GPIOA_CRL ~(0xF 16); // 清PA4配置 GPIOA_CRL | (0x3 16); // PA4推挽输出 GPIOA_CRL ~(0xF 20); // 清PA5配置 GPIOA_CRL | (0xB 20); // PA5复用推挽 GPIOA_CRL ~(0xF 24); // 清PA6配置 GPIOA_CRL | (0xB 24); // PA6复用推挽 GPIOA_CRL ~(0xF 28); // 清PA7配置 GPIOA_CRL | (0xB 28); // PA7复用推挽 // 3. 配置PA3为带上拉输入用于读取FAULT引脚 GPIOA_CRL ~(0xF 12); GPIOA_CRL | (0x8 12); // 输入模式CNF10上下拉 GPIOA_ODR | (1 3); // ODR置1 上拉 // 4. SPI1主模式CPOL0 CPHA0(SPI Mode 0)16分频得4.5MHz SPI1_CR1 (1 6) | // SPESPI使能 (1 2) | // MSTR主模式 (1 3) | // BR[0]116分频 (0 4); // BR[1]0 // BR[2:0]010即16分频72MHz/164.5MHz低于MAX31856的5MHz上限 CS_HIGH(); // CS默认拉高SPI从机未选中 }这里容易忽略的是GPIO复用功能的配置。SPI1的SCK、MISO、MOSI必须配成复用推挽输出CNF10而CS因为是用软件控制配成通用推挽就行。PA6虽然是从设备数据输入但芯片手册上要求MISO引脚配置为复用推挽或复用开漏实测复用推挽工作正常。PA3作为FAULT输入CRL里MODE[1:0]00表示输入模式CNF[1:0]10表示上下拉输入再配合ODR置1选择上拉。2.2.1 SPI分频系数怎么选为什么是16分频MAX31856数据手册规定SCK频率最大5MHz。STM32F103的SPI1挂在APB2上时钟72MHz。分频系数可选2、4、8、16、32、64、128、256。72/164.5MHz正好卡在5MHz以内而且传输4字节温度数据只要7.1μs轮询一次的时间开销完全可以忽略。如果选8分频得到9MHz跑起来偶尔通信会错位表现为读出的温度值在大范围内乱跳。如果选32分频2.25MHz也能用但SPI时序余量更大如果PCB布线差、飞线比较长优先选32分频。分频系数的确定原则是先满足从设备时钟上限再留出20%以上余量最后才考虑速度。2.3 时钟使能、引脚复用与波特率参数怎么定RCC_APB2ENR的bit2和bit12分别对应GPIOA和SPI1的时钟门控。STM32F103的GPIO和SPI外设默认都是关闭的寄存器访问前必须先打开时钟否则读到的是总线上的随机值写进去也没反应。这个坑在寄存器版开发里出现频率最高比配置错误更隐蔽因为读寄存器不会触发HardFault只是数据不对。关于SPI_CR1的其他位这里再细化一下。BR[2:0]占CR1的bit5:3上面的代码只有bit3置1即BR[2:0]010对应16分频。CPOL和CPHA都在CR1里CPOL是bit1CPHA是bit0初始化时全部为0对应SPI Mode 0。MAX31856支持Mode 0和Mode 3两种模式都能工作但工程上统一用Mode 0因为大部分SPI从设备默认支持模式0兼容性最好。SPI1_CR2的位不需要配置因为这里不用硬件NSS也不用DMA。3. MAX31856寄存器映射与K型热电偶初始化序列3.1 关键寄存器一览表CR0/CR1/MASK/SR各管什么MAX31856一共有16个8位寄存器地址从0x00到0x0F。驱动这个芯片只需要操作其中一小部分CR0配置转换模式CR1配置热电偶类型和滤波MASK控制哪些故障能触发FAULT引脚SR是状态寄存器LTCJH/LTCJL是热电偶温度数据CJTH/CJTL是冷端温度数据。还有一个CJTO寄存器用来校准冷端偏移精度要求高的场合会用。地址寄存器名bit7-4bit3-0复位值0x00CR0CMODE(7)、1-shot(6)、OCFAULT(5)保留0x000x01CR1TC类型(bit6-4)、CJ禁用(bit3)FAULT模式(bit2)、50/60Hz(bit1-0)0x030x02MASKCJ故障(bit6)、TC故障(bit5)过压(bit4)、欠压(bit3)、开路(bit2)0xFF0x0CLTCJH热电偶温度高字节—0x000x0DLTCJL热电偶温度低字节—0x000x0ECJTH冷端温度高字节—0x000x0FSRCJ范围(bit6)、TC范围(bit5)过压(bit4)、欠压(bit3)、开路(bit2)0x00CR1的bit6-4选择热电偶类型K型对应二进制0011即0x30。bit3是冷端补偿禁用位保持0使能冷端补偿。bit2是FAULT模式0表示比较模式1表示中断模式。bit1-0是工频滤波选择0060Hz0150Hz1050/60Hz。中国电网是50Hz滤波选01也就是CR1最终配置成0x33。MASK寄存器的复位值是0xFF意思是所有故障都会被屏蔽不会反映到FAULT引脚上。如果要用中断方式检测热电偶开路必须把MASK改成0xFB即bit2清0让开路故障不再被屏蔽。注意bit2对应的二进制是00000100取反是11111011即0xFB。3.2 寄存器读写命令格式与读写函数的写法MAX31856的SPI命令字节由7位寄存器地址和1位读写标志组成。读命令是将寄存器地址左移1位后最低位置1写命令则是左移后最低位保持0。也就是说0x0C地址的读命令是0x19写命令是0x18。SPI时序上有个细节读操作是CS拉低后先发送命令字节此时MISO上会同时返回一个无效字节然后MCU再发送一个任意字节的dummy时钟同时从MISO上接收到寄存器数据。// 读单个寄存器 uint8_t MAX31856_ReadReg(uint8_t addr) { uint8_t dummy, val; CS_LOW(); while (SPI1_SR (1 1)); // 等待TXE发送缓冲区空 SPI1_DR (addr 1) | 0x01; // 命令字节bit01表示读 while (!(SPI1_SR (1 0))); // 等待RXNE接收缓冲区非空 dummy SPI1_DR; // 读走无效字节否则RXNE一直置位 while (SPI1_SR (1 1)); SPI1_DR 0x00; // 发送dummy时钟 while (!(SPI1_SR (1 0))); val SPI1_DR; // 这才是真正的寄存器数据 CS_HIGH(); return val; } // 写单个寄存器 void MAX31856_WriteReg(uint8_t addr, uint8_t data) { CS_LOW(); while (SPI1_SR (1 1)); SPI1_DR (addr 1) 0xFE; // 命令字节bit00表示写 while (SPI1_SR (1 1)); SPI1_DR data; // 写入数据 while (!(SPI1_SR (1 0))); // 等待传输完毕 (void)SPI1_DR; // 清掉RXNE CS_HIGH(); }读函数里最容易出错的地方是第一个无效字节。如果不把SPI1_DR读走RXNE标志会一直为1后面循环判断!(SPI1_SR (1 0))直接跳过导致第二个字节读到的是无效数据。写函数末尾为什么要读DR因为SPI是全双工MCU在发送写数据的同时也会收到一个字节这个字节放在接收缓冲区里不清掉的话下一次读写时RXNE为1会让函数时序错乱。这是寄存器版SPI驱动最典型的时序问题。3.3 初始化序列连续转换、K型、50Hz滤波、开放故障MAX31856上电后默认是单次转换模式寄存器CR0默认为0x00CR1默认为0x03。需要一次性完成所有配置否则芯片不工作或者热电偶类型不对。下面给出推荐的初始化序列。void MAX31856_Init(void) { SPI1_Init_Reg(); // 先初始化STM32的SPI外设 // CR0bit71连续转换模式bit51中断模式开路检测 MAX31856_WriteReg(0x00, 0xA0); // CR10x33 0011 0011 // bit6-40011 选择K型热电偶 // bit30 使能冷端补偿 // bit20 故障输出使用比较模式FAULT引脚实时反映故障状态 // bit1-001 选择50Hz工频滤波 MAX31856_WriteReg(0x01, 0x33); // MASK0xFB 1111 1011 // bit20 取消开路故障屏蔽热电偶断开时FAULT引脚拉低 MAX31856_WriteReg(0x02, 0xFB); }CR0配置成0xA0而不是0x80差别在bit5。0x80只有CMODE位使能了连续转换0xA0额外把OCFAULT位bit5置1使能开路故障检测并且开路时FAULT引脚输出低电平并保持锁存。如果这里忘了置1即使MASK寄存器开放了开路故障FAULT引脚也不会动作只能靠轮询SR寄存器判断。另外如果系统对功耗敏感比如电池供电的设备把CR0改成0x20即bit6置1触发一次单次转换读完温度后芯片自动进入低功耗状态。4. 温度读取与故障判断寄存器位如何换算成摄氏温度4.1 连续读LTCJH/LTCJL/CJTH/CJTL四字节并计算温度MAX31856支持一次读多个连续寄存器。当CS拉低后发送读命令字节然后连续发送4个dummy时钟就能依次收到LTCJH、LTCJL、CJTH、CJTL四个字节的数据。相比分四次单字节读取这种方式节省了CS翻转的开销更重要的是保证了温度数据的一致性如果两次读取之间有新转换完成分次读可能读到新旧混合的数据。typedef struct { int16_t tc_raw; // 热电偶温度原始值 uint16_t cj_raw; // 冷端温度原始值 float tc_temp; // 热电偶温度单位摄氏度 float cj_temp; // 冷端温度单位摄氏度 } MAX31856_Temp_t; void MAX31856_ReadTemp(MAX31856_Temp_t *temp) { uint8_t buf[4]; uint8_t i; CS_LOW(); // 发送读0x0C的读命令地址左移1位后bit0置1 while (SPI1_SR (1 1)); SPI1_DR (0x0C 1) | 0x01; // 第一个RXNE对应的是命令字节的无效响应 while (!(SPI1_SR (1 0))); (void)SPI1_DR; // 连续读取4个数据字节 for (i 0; i 4; i) { while (SPI1_SR (1 1)); SPI1_DR 0x00; // dummy时钟驱动从机输出下一字节 while (!(SPI1_SR (1 0))); buf[i] SPI1_DR; } CS_HIGH(); // 热电偶温度LTCJH为高字节LTCJL为低字节有符号数 temp-tc_raw (int16_t)((buf[0] 8) | buf[1]); // 冷端温度CJTH/CJTL为12位无符号数高字节在前 temp-cj_raw (uint16_t)((buf[2] 8) | buf[3]); // LSB换算热电偶温度每LSB0.0078125摄氏度 temp-tc_temp (float)temp-tc_raw * 0.0078125f; // 冷端温度每LSB0.015625摄氏度 temp-cj_temp (float)temp-cj_raw * 0.015625f; }代码里(int16_t)强转是为了让负温度正确显示。K型热电偶在零下温度时LTCJH的bit7是1整个16位数被解释为负数强转成int16_t后乘以0.0078125f得到的就是负的摄氏温度。比如-10°C时LTCJH:LTCJL的值应该是0xFB00附近的数转成int16_t后乘以0.0078125f得到-10.0。如果不做符号扩展0xFB00会被当成正数64256算出来501°C差了十万八千里。4.2 冷端补偿那个寄存器不能忽略室温不准会导致整体偏移MAX31856内部有冷端补偿传感器测量的是芯片引脚附近的环境温度。冷端温度数据在CJTH/CJTL寄存器里精度0.015625°C。如果发现常温下热电偶读数整体偏高或偏低几度先看CJTH/CJTL算出来的室温对不对。如果室温值明显异常检查芯片旁边有没有发热元件。MAX31856紧挨着大功率MOS管或变压器冷端传感器被加热测温系统误差可能到5°C以上。解决方法是PCB布局上让MAX31856远离发热源或者用CJTO寄存器做软件校准。CJTO寄存器0x09是冷端温度偏移校准写入的值会直接加到冷端测量结果上。比如实测冷端温度比实际高2.5°C写入0x80表示加2°C写入0x40表示加0.5°C最小分辨率0.015625°C。注意CJTO的bit7是符号位正偏移和负偏移的补码写法不同具体算法在数据手册的公式里工程上多数情况不需要动这个寄存器。4.3 用SR寄存器判断热电偶开路与过欠压SR寄存器0x0F的bit6是冷端超范围bit5是热电偶温度超范围bit4是过压bit3是欠压bit2是开路。正常工作时SR应该是0x00。只要任何一个故障位置1FAULT引脚就会拉低前提是MASK寄存器对应位没有被屏蔽。uint8_t MAX31856_CheckFault(void) { uint8_t sr MAX31856_ReadReg(0x0F); if (sr 0x04) { // bit2 热电偶开路 return 1; // 返回1表示开路 } if (sr 0x18) { // bit4或bit3 过压或欠压 return 2; } return 0; // 无故障 }一个容易忽略的细节SR寄存器读取后会自动清除故障标志但FAULT引脚在比较模式下只要故障存在就一直为低故障消失自动恢复高电平。如果配置成中断模式故障标志会锁存必须读SR寄存器才能清除。这是MAX31856两种故障模式的本质差异比较模式适合轮询中断模式适合外部中断触发。上面初始化代码配的是比较模式CR1的bit20轮询读取时每次都能反映当前状态不需要额外清标志。5. 调试MAX31856的三个实测技巧与代码发布建议5.1 常见异常值与对应原因速查调试中遇到异常读数按下面这张表定位最快。异常现象可能原因排查方向温度始终为0SPI通信失败读回全0用示波器量SCK是否有时钟检查MISO是否虚焊温度跳变到几百度热电偶接反对调T和T-再测温度按一定周期波动工频滤波频率设置不对中国电网用50HzCR1的bit1-0设为01室温下读数偏离实际几度冷端传感器受热看CJTH/CJTL算出的室温值是否正常读数全部是0x7FFFMISO数据线电平异常检查PA6是否配置成复用推挽FAULT是否干扰插拔热电偶时读数跳变接触不良或接线过长加共模电容检查热电偶端子压接还有一种情况在寄存器版驱动里很常见读寄存器函数第一个无效字节没处理导致所有数据错位一字节。症状是温度值看起来像随机数且每次读都不一样。排查时先读一个固定寄存器比如SR寄存器如果读回来的值和预期不符先查SPI时序再查代码逻辑。5.2 单次转换模式与连续模式的取舍前面初始化用的连续转换模式芯片每隔100ms完成一次转换这个模式下软件读取非常省事。但在电池供电的低功耗场景里连续转换意味着芯片永远在工作电流消耗远远超过休眠状态。MAX31856在关断模式下电流典型值只有1.5μA而连续转换模式下是1.5mA差了三个数量级。做法是待机时把CR0写0x00进入关断模式需要测温时写0x20触发单次转换然后等待100ms读取。注意单次转换完成后芯片自动回到关断模式下次触发前不需要再写CR0。等待时间取决于热电偶类型和滤波设置K型50Hz滤波下典型转换时间是100ms。单次转换模式还有个额外好处多个MAX31856挂在同一SPI总线上时可以逐个触发、逐个读取避免同时转换时总线竞争。5.3 注释规范、接线标注与代码发布建议标题里提到的“注释清晰”和“留有作者联系邮箱”落地到工程里其实就是代码可维护性的问题。寄存器驱动的注释重点不在解释寄存器名字而在于标注“为什么这么配”。比如CR1写0x33时注释里说明每一位的含义而不是只写“配置K型热电偶”。示波器实测波形图也可以截图放进项目文档里SCK频率、CS低电平时间这些数据比任何描述都有说服力。发布这类代码到开源平台或技术社区时建议在文件头部注释块里写清楚硬件平台STM32F103ZET6、传感器型号MAX31856、接线引脚定义PA4/PA5/PA6/PA7、测试环境。联系方式如果不想直接暴露邮箱可以留一个技术交流群号或者GitHub仓库地址属于个人隐私相关的信息建议用临时邮箱。但代码注释里写邮箱有个现实问题爬虫会把邮箱抓走如果确实要留格式上做一下变形比如写成username#example.com并在注释里说明#替换成既保证人眼能识别又不会直接被爬虫收集。5.3.1 完整示例工程的结构参考一个可直接编译的工程建议按下面结构组织文件划分清晰便于后续维护max31856.c和max31856.h封装寄存器读写和温度计算spi_reg.c和spi_reg.h封装SPI1寄存器驱动main.c里初始化并循环读取打印。这样当MAX31856换成了MAX6675只需要改max31856.c一个文件SPI驱动和其他业务代码完全不用动。本文还有配套的精品资源点击获取