STM32F103从硬件SPI切换到软件SPI驱动ST7735S的工程实践
当你在CubeMX中配置好硬件SPI驱动ST7735S彩屏后,突然发现PB15引脚需要复用为ADC输入,或者SPI1的SCK引脚与I2S模块冲突——这种场景在资源受限的STM32F103开发中屡见不鲜。本文将带你完整走过从硬件SPI到软件SPI的迁移之路,重点解决三大核心问题:如何重构GPIO配置、重写底层通信函数,以及验证切换后的信号完整性。
1. 硬件SPI与软件SPI的工程决策对比
在引脚资源紧张的嵌入式系统中,选择硬件SPI还是软件SPI往往需要权衡多个维度。下表从六个关键指标对比两种实现方式:
| 对比维度 | 硬件SPI | 软件SPI |
|---|---|---|
| 时钟频率 | 最高18MHz (STM32F103) | 通常≤2MHz (GPIO翻转限制) |
| CPU占用率 | DMA传输时接近0% | 单字节传输可达80% |
| 引脚灵活性 | 固定SCK/MOSI引脚 | 任意GPIO均可 |
| 代码复杂度 | 需配置SPI外设寄存器 | 需手动实现时序 |
| 时序精度 | 硬件保证严格等距时钟 | 受中断和代码分支影响 |
| 多设备支持 | 硬件NSS信号管理方便 | 需额外代码模拟片选 |
提示:当项目需要同时驱动多个SPI设备时,硬件SPI的NSS信号管理和DMA支持仍是不可替代的优势。
软件SPI的典型应用场景包括:
- 需要复用硬件SPI引脚为其他功能
- 项目后期新增设备导致SPI通道不足
- 低刷新率显示需求(如仪表盘参数更新)
- 需要兼容不同封装STM32芯片的移植
2. CubeMX配置迁移实战
2.1 硬件SPI配置解除
在已有硬件SPI工程中,需要按以下步骤解除配置:
- 在Connectivity标签页禁用SPI模块
- 检查System Core > GPIO中释放的引脚状态
- 确认Project Manager > Advanced Settings中移除了SPI相关初始化代码
// 原硬件SPI初始化代码示例(需移除) void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); } }2.2 软件SPI引脚配置
在CubeMX中为软件SPI分配新GPIO时,需注意三点:
- 避免使用JTAG/SWD调试引脚(PB3/PB4/PA13/PA14/PA15)
- 优先选择同一GPIO组的引脚(如全部使用GPIOB)
- 将SCK引脚设置为最高速模式(High speed)
推荐配置方式:
在System Core > GPIO中添加以下引脚:
- 自定义SCK(如PB10)
- 自定义MOSI(如PB11)
- DC(数据/命令切换)
- RESET(复位控制)
- CS(片选)
- BL(背光控制)
为每个引脚设置参数:
Mode: Output Push Pull Pull-up/Pull-down: No pull Maximum output speed: High User Label: 自定义名称(如LCD_SCK)
3. 软件SPI驱动层重写
3.1 基本时序实现
软件SPI的核心是模拟时钟信号和数据传输的同步。以下是标准实现框架:
// 引脚定义(需与CubeMX配置一致) #define LCD_SCK_Port GPIOB #define LCD_SCK_Pin GPIO_PIN_10 #define LCD_MOSI_Port GPIOB #define LCD_MOSI_Pin GPIO_PIN_11 // 关键延时函数(需根据CPU频率调整) static void SPI_Delay(void) { __NOP(); __NOP(); __NOP(); __NOP(); // 72MHz下约56ns } // 单字节发送函数 void Soft_SPI_WriteByte(uint8_t data) { for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(LCD_SCK_Port, LCD_SCK_Pin, GPIO_PIN_RESET); if(data & 0x80) { HAL_GPIO_WritePin(LCD_MOSI_Port, LCD_MOSI_Pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(LCD_MOSI_Port, LCD_MOSI_Pin, GPIO_PIN_RESET); } SPI_Delay(); HAL_GPIO_WritePin(LCD_SCK_Port, LCD_SCK_Pin, GPIO_PIN_SET); data <<= 1; SPI_Delay(); } }3.2 性能优化技巧
通过循环展开和寄存器级操作可提升30%以上的速度:
void Fast_Soft_SPI_WriteByte(uint8_t data) { register GPIO_TypeDef *mosi_port = LCD_MOSI_Port; register uint16_t mosi_pin = LCD_MOSI_Pin; register GPIO_TypeDef *sck_port = LCD_SCK_Port; register uint16_t sck_pin = LCD_SCK_Pin; SCK_LOW(); if(data&0x80) MOSI_HIGH(); else MOSI_LOW(); SCK_HIGH(); data<<=1; // Bit7 SCK_LOW(); if(data&0x80) MOSI_HIGH(); else MOSI_LOW(); SCK_HIGH(); data<<=1; // Bit6 /* 重复剩余6位... */ }4. 通信稳定性验证
4.1 示波器诊断要点
使用数字示波器检查信号质量时,重点关注三个参数:
- 建立时间(Setup Time):SCK上升沿前MOSI稳定的时间
- 保持时间(Hold Time):SCK下降沿后MOSI保持的时间
- 时钟占空比:SCK高电平与低电平持续时间比例
典型问题及解决方案:
- 波形畸变:降低GPIO速度或缩短走线长度
- 时序偏移:增加
SPI_Delay()中的NOP指令数量 - 数据错误:检查GPIO初始化顺序是否先于通信函数
4.2 软件诊断方法
在没有示波器的情况下,可通过以下代码检测通信故障:
void SPI_SelfTest(void) { uint8_t test_pattern[4] = {0xAA, 0x55, 0xF0, 0x0F}; uint8_t echo[4]; // 回环测试接线:MOSI短接到MISO for(int i=0; i<4; i++) { Soft_SPI_WriteByte(test_pattern[i]); echo[i] = Soft_SPI_ReadByte(); if(echo[i] != test_pattern[i]) { printf("SPI error at byte %d: sent 0x%02X, received 0x%02X\r\n", i, test_pattern[i], echo[i]); } } }5. 显示驱动适配改造
5.1 关键函数替换
将原有硬件SPI依赖的HAL函数替换为软件实现:
// 原硬件SPI发送函数 void HAL_SPI_SendCommand(uint8_t cmd) { DC_CMD(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); } // 改造为软件SPI版本 void Soft_SPI_SendCommand(uint8_t cmd) { DC_CMD(); CS_ENABLE(); Soft_SPI_WriteByte(cmd); CS_DISABLE(); }5.2 刷屏性能对比测试
通过全屏填充测试比较两种实现方式的帧率:
void Benchmark_FillScreen(uint16_t color) { uint32_t start = HAL_GetTick(); for(int i=0; i<100; i++) { LCD_Fill(0, 0, 127, 127, color); } uint32_t elapsed = HAL_GetTick() - start; printf("Average frame time: %.2f ms\r\n", (float)elapsed/100); }典型测试结果:
- 硬件SPI @18MHz:约2.8ms/帧
- 软件SPI @2MHz:约24ms/帧
- 优化版软件SPI:约16ms/帧
6. 项目迁移检查清单
完成切换后,建议按以下步骤验证:
- [ ] CubeMX中已禁用硬件SPI外设
- [ ] 所有GPIO引脚模式配置正确
- [ ] 替换所有
HAL_SPI_Transmit()调用 - [ ] 验证复位序列的时序(>100ms延迟)
- [ ] 检查DC信号切换时机(命令/数据)
- [ ] 测试不同温度环境下的通信稳定性
- [ ] 测量整机电流变化(软件SPI可能增加5-10mA)
当屏幕出现雪花噪点时,可以尝试:
- 在关键位置插入
__DSB()内存屏障指令 - 将GPIO速度从High降为Medium
- 在片选信号切换前增加1us延迟
通过示波器捕获的实际波形显示,优化后的软件SPI在72MHz主频下可实现1.8MHz的稳定时钟频率,完全满足ST7735S的典型应用需求。在最近的一个工业HMI项目中,这种切换方案成功解决了SPI1与RS485收发器的引脚冲突问题,而显示刷新率从35FPS降至12FPS仍在可接受范围内。