手把手教你用STM32F103的HAL库GPIO模拟SPI驱动MAX31865(附完整代码)
在嵌入式开发中,SPI通信协议因其高速、全双工的特性被广泛应用于传感器、存储设备等外设的连接。然而,当硬件SPI资源紧张或需要更灵活的时序控制时,GPIO模拟SPI便成为了一种实用的替代方案。本文将详细介绍如何使用STM32F103的HAL库通过GPIO模拟SPI驱动MAX31865温度传感器,从硬件连接到软件实现,一步步带你完成整个项目。
1. 硬件准备与连接
1.1 所需材料清单
- STM32F103开发板(如Blue Pill)
- MAX31865模块(支持PT100/PT1000)
- PT1000温度传感器
- 杜邦线若干
- 逻辑分析仪(可选,用于调试)
1.2 硬件连接示意图
MAX31865与STM32F103的连接方式如下:
| MAX31865引脚 | STM32F103引脚 | 功能说明 |
|---|---|---|
| VIN | 3.3V | 电源正极 |
| GND | GND | 地线 |
| CS | PA4 | 片选信号 |
| SCLK | PA5 | 时钟信号 |
| SDI | PA7 | 数据输入 |
| SDO | PA6 | 数据输出 |
提示:PT1000的三根线分别连接到MAX31865的RTD+、RTD-和REF-引脚,具体接线方式取决于使用的2线、3线还是4线制。
2. GPIO模拟SPI原理与实现
2.1 SPI通信基础
SPI协议包含以下关键信号:
- SCLK:时钟信号,由主机产生
- MOSI:主机输出,从机输入
- MISO:主机输入,从机输出
- CS:片选信号,低电平有效
在GPIO模拟SPI时,我们需要通过软件控制GPIO的电平变化来模拟这些信号。
2.2 HAL库GPIO配置
首先配置相关GPIO引脚:
// 在main.c的GPIO初始化部分添加以下代码 GPIO_InitTypeDef GPIO_InitStruct = {0}; // 配置CS、SCLK、MOSI为输出 GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 配置MISO为输入 GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);2.3 模拟SPI时序实现
MAX31865采用SPI模式1(CPOL=0,CPHA=1),即:
- 时钟空闲时为低电平
- 数据在时钟上升沿采样
下面是基本的读写函数实现:
// 向MAX31865写入一个字节 void SPI_WriteByte(uint8_t data) { for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // SCLK低 // 设置MOSI电平 if(data & 0x80) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); } HAL_Delay(1); // 适当延时保证时序 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCLK高 data <<= 1; HAL_Delay(1); } } // 从MAX31865读取一个字节 uint8_t SPI_ReadByte(void) { uint8_t data = 0; for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // SCLK低 HAL_Delay(1); data <<= 1; if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6)) { data |= 0x01; } HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // SCLK高 HAL_Delay(1); } return data; }3. MAX31865驱动开发
3.1 寄存器定义与配置
MAX31865有几个关键寄存器需要配置:
// 寄存器地址定义 #define MAX31865_CONFIG_REG 0x00 #define MAX31865_RTD_MSB_REG 0x01 #define MAX31865_RTD_LSB_REG 0x02 // 配置寄存器位定义 #define MAX31865_CONFIG_BIAS 0x80 #define MAX31865_CONFIG_MODE_AUTO 0x40 #define MAX31865_CONFIG_3WIRE 0x10 #define MAX31865_CONFIG_FILTER_50HZ 0x013.2 初始化函数实现
初始化时需要根据实际硬件连接配置MAX31865:
void MAX31865_Init(void) { uint8_t config = MAX31865_CONFIG_BIAS | // 开启偏置电压 MAX31865_CONFIG_MODE_AUTO | // 自动转换模式 MAX31865_CONFIG_FILTER_50HZ; // 50Hz滤波器 // 根据接线方式选择2/3/4线制 // config |= MAX31865_CONFIG_3WIRE; // 如果是3线制则取消注释 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // CS低 SPI_WriteByte(0x80); // 写入配置寄存器 SPI_WriteByte(config); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS高 }3.3 温度读取与计算
MAX31865返回的是RTD电阻值,需要转换为温度:
float MAX31865_ReadTemp(void) { uint16_t rtd_raw; float resistance, temp; // 读取RTD值 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); SPI_WriteByte(0x01); // 读取RTD MSB uint8_t msb = SPI_ReadByte(); SPI_WriteByte(0x02); // 读取RTD LSB uint8_t lsb = SPI_ReadByte(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); rtd_raw = ((msb << 8) | lsb) >> 1; // 去掉故障位 // 计算电阻值(假设使用PT1000,参考电阻Rref=430Ω) resistance = (float)rtd_raw * 430.0f / 32768.0f; // 将电阻转换为温度(简化计算) temp = (resistance - 1000.0f) / 3.85f; return temp; }4. 调试技巧与常见问题
4.1 使用逻辑分析仪调试
当通信不正常时,逻辑分析仪是强大的调试工具。连接逻辑分析仪后,你应该能看到类似以下的波形:
- CS信号在通信期间保持低电平
- SCLK有规律的时钟脉冲
- MOSI/MISO上有数据变化
4.2 常见问题排查
无数据返回:
- 检查电源和地线连接
- 确认CS信号是否正确拉低
- 验证SPI时序是否符合MAX31865要求
温度值不正确:
- 检查PT1000接线是否正确
- 确认配置寄存器设置(特别是线制选择)
- 验证参考电阻Rref的值是否与实际硬件匹配
通信不稳定:
- 尝试降低SPI时钟速度
- 检查是否有信号干扰
- 确保所有连接线牢固
4.3 性能优化建议
- 根据实际需求调整SPI时钟速度
- 可以去掉HAL_Delay(),改用更精确的定时器控制时序
- 实现DMA传输以提高效率(如果使用硬件SPI)
5. 完整代码示例
以下是整合后的完整驱动代码:
// max31865.h #ifndef __MAX31865_H #define __MAX31865_H #include "stm32f1xx_hal.h" // 引脚定义 #define MAX31865_CS_PIN GPIO_PIN_4 #define MAX31865_SCLK_PIN GPIO_PIN_5 #define MAX31865_MISO_PIN GPIO_PIN_6 #define MAX31865_MOSI_PIN GPIO_PIN_7 #define MAX31865_PORT GPIOA // 函数声明 void MAX31865_Init(void); float MAX31865_ReadTemp(void); #endif// max31865.c #include "max31865.h" // SPI写一个字节 static void SPI_WriteByte(uint8_t data) { for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_SCLK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_MOSI_PIN, (data & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_SCLK_PIN, GPIO_PIN_SET); data <<= 1; } } // SPI读一个字节 static uint8_t SPI_ReadByte(void) { uint8_t data = 0; for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_SCLK_PIN, GPIO_PIN_RESET); data <<= 1; if(HAL_GPIO_ReadPin(MAX31865_PORT, MAX31865_MISO_PIN)) { data |= 0x01; } HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_SCLK_PIN, GPIO_PIN_SET); } return data; } // MAX31865初始化 void MAX31865_Init(void) { uint8_t config = 0x80 | 0x40 | 0x01; // BIAS ON, Auto, 50Hz HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_CS_PIN, GPIO_PIN_RESET); SPI_WriteByte(0x80); // 写配置寄存器 SPI_WriteByte(config); HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_CS_PIN, GPIO_PIN_SET); } // 读取温度 float MAX31865_ReadTemp(void) { uint16_t rtd_raw; HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_CS_PIN, GPIO_PIN_RESET); SPI_WriteByte(0x01); uint8_t msb = SPI_ReadByte(); SPI_WriteByte(0x02); uint8_t lsb = SPI_ReadByte(); HAL_GPIO_WritePin(MAX31865_PORT, MAX31865_CS_PIN, GPIO_PIN_SET); rtd_raw = ((msb << 8) | lsb) >> 1; float resistance = (float)rtd_raw * 430.0f / 32768.0f; return (resistance - 1000.0f) / 3.85f; }在实际项目中,我发现GPIO模拟SPI虽然灵活,但在高频率通信时稳定性不如硬件SPI。当需要更高采样率时,可以考虑使用STM32的硬件SPI外设,或者优化GPIO模拟的实现方式。