STM32硬件SPI资源不足?混合驱动方案实现精准时序扩展
1. 项目概述:当硬件SPI口不够用时,我们怎么办?
在嵌入式开发,尤其是基于STM32这类MCU的项目里,SPI(串行外设接口)是连接各类传感器、存储芯片、显示屏的绝对主力。但STM32的型号繁多,引脚资源也各不相同。我遇到过不止一次这样的情况:项目板上已经挂了一个SPI Flash存数据,一个SPI接口的OLED屏做显示,这时候客户临时要求再加一个高精度的SPI接口ADC芯片。一翻数据手册,傻眼了——这颗STM32F103,就只有一个硬件SPI1。硬件资源就这么点,需求却摆在那里,项目又不能换芯片,这时候怎么办?
“STM32通过硬件SPI模块软件模拟驱动来进行拓展”这个标题,指向的就是这个非常实际且经典的工程问题。它的核心思路不是去更换硬件,而是在软件层面做文章:利用一个现有的硬件SPI模块作为“种子”,通过GPIO(通用输入输出口)和精准的时序控制,在软件层面“克隆”或“模拟”出额外的SPI通信能力。这听起来有点像“用一个引擎驱动多辆车”,其价值在于,它能以极低的硬件成本(几乎为零)和可接受的软件开销,突破MCU原生外设数量的限制,为项目赢得宝贵的灵活性和扩展空间。
这篇文章,我就结合自己多次“踩坑”和“填坑”的经历,来彻底拆解这个方案。我会从为什么需要这么做开始,讲到具体的设计思路、代码实现中的魔鬼细节,再到如何调试这种“软硬结合”的通信,最后分享几个我总结出来的、能显著提升模拟SPI稳定性和效率的实战技巧。无论你是正在为SPI口不够用而发愁,还是想深入理解SPI协议和MCU底层操作,相信都能从中找到直接的参考。
2. 核心思路与架构设计:并非简单的GPIO翻转
很多人一听到“软件模拟SPI”,第一反应可能就是:那不就是用几个GPIO,按照SPI的时序图,用HAL_GPIO_WritePin和HAL_GPIO_ReadPin函数去模拟时钟SCK、数据线MOSI/MISO的跳变吗?这种做法,通常被称为“Bit-Banging”(位撞击),它完全由CPU通过指令控制GPIO,实现简单,但效率低下,且会严重占用CPU时间。
而我们标题里提到的“通过硬件SPI模块软件模拟驱动”,是一种更高级、更巧妙的混合架构。它的核心思想是**“硬件为主,软件为辅,协同扩展”**。
2.1 混合驱动架构解析
这种架构的精髓在于分层和复用:
- 硬件SPI层:作为核心与基准。我们首先初始化并配置好一个可用的硬件SPI外设(例如SPI1)。这一步的意义在于,我们得到了一个经过芯片厂商严格测试和优化的、时序绝对精确的“时钟源”和“数据发送引擎”。硬件SPI的SCK时钟频率稳定、占空比准确,这是软件模拟很难媲美的。
- 软件模拟扩展层:这是实现拓展的关键。我们不直接使用硬件SPI的MOSI和MISO数据线(或者仅使用其中之一),而是将其“让”给最高优先级或最要求性能的从设备。然后,我们额外定义几组普通的GPIO引脚,分别作为“扩展SPI”的SCK、MOSI、MISO。
- 协同工作机制:
- 时钟同步:扩展SPI的SCK信号,不再由软件延时循环产生,而是严格同步于硬件SPI的SCK时钟。我们可以通过配置硬件SPI工作在“仅输出时钟(Master Output Disabled)”模式,或者简单地将其MOSI/MISO引脚配置为普通推挽输出并固定电平,只“借用”其产生的精准SCK时钟信号。更常见的做法是,在硬件SPI的传输完成中断(TXE/RXNE)或DMA传输回调函数中,去触发和操作我们扩展的GPIO进行数据读写。这样,扩展SPI的每一位数据切换都与硬件SPI的时钟边沿严格对齐。
- 数据独立:扩展SPI的MOSI(主机输出)和MISO(主机输入)数据线,则由我们通过软件直接控制GPIO电平来实现。我们在硬件SPI时钟的节拍下,进行数据的移出和移入操作。
这样做的好处是显而易见的:我们获得了硬件SPI的时序精度和稳定性,同时实现了多个独立SPI通道的扩展。CPU的负担远低于纯Bit-Banging,因为精准的时钟节拍由硬件负责,CPU只需要在正确的时刻“喂数据”和“读数据”即可。
2.2 方案选型与权衡
为什么选择这种混合方案,而不是其他?我们来对比一下:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯软件模拟 (Bit-Banging) | 实现最简单,不依赖特定硬件,引脚任意指定。 | CPU占用率高,时序精度差(受中断、任务调度影响),通信速率很低(通常<1Mbps)。 | 极低速设备(如RC522读卡器),或硬件SPI完全不可用时的应急方案。 |
| 硬件SPI多路复用器 | 通信性能与硬件SPI一致,稳定可靠。 | 需要外部芯片(如74HC4052等),增加BOM成本和PCB面积,需要额外的GPIO控制片选。 | 对性能要求高,且硬件成本不敏感的项目。 |
| 本文的混合扩展方案 | 时序精准(依托硬件时钟),软件开销适中,无需外部硬件,引脚配置灵活。 | 实现复杂度较高,需要深入理解SPI协议和MCU中断/DMA机制。软件逻辑若编写不当,可能引入时序错误。 | 最适用于需要扩展1-2个中低速SPI从设备,且对时序有一定要求的场景。例如,扩展一个SPI接口的传感器(如BME280)或ADC(如ADS1256)。 |
注意:这种方案扩展出的SPI,其最大通信速率受限于两个因素:一是你所“依附”的那个硬件SPI本身的时钟频率;二是你的软件代码在中断服务程序或DMA回调中执行数据搬移操作的速度。通常,它能达到依附的硬件SPI速率的一半或三分之一,就已经是非常理想的结果了。
3. 关键实现细节与驱动设计
理论清晰了,我们进入实战环节。我将以STM32CubeMX和HAL库为例,展示如何一步步构建这个混合扩展SPI驱动。假设我们的主硬件SPI是SPI1,需要扩展出一个“SPI_EX”(扩展SPI)来驱动一个传感器。
3.1 硬件SPI的初始化与“时钟源”配置
首先,我们需要正确初始化作为“心脏”的硬件SPI1。这里有一个关键技巧:我们可能不需要它完整地工作。
// spi.c SPI_HandleTypeDef hspi1; 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; // 时钟极性CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 时钟相位CPHA=0,模式0 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件管理片选 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 波特率预分频,决定SCK频率 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(); } }初始化后,SPI1的SCK引脚(如PA5)就会输出对应频率的时钟信号。但此时,如果我们不启动传输,时钟是静止的。为了让时钟持续运行,我们可以启动一个空的DMA传输或中断传输。例如,我们可以设置一个循环发送缓冲区,里面全是0xFF(无效数据),让SPI1不断地发送,从而持续产生SCK时钟。这是我们方案中“偷”时钟的一种方法。
更优雅的一种方法是,利用SPI的传输事件来驱动我们的扩展逻辑。我们启动一次SPI1的传输(比如发送一个字节),然后在SPI1的“传输完成中断”或“DMA传输完成回调函数”中,进行我们扩展SPI的数据操作,并再次启动SPI1的传输,形成链式反应。
3.2 扩展SPI的GPIO与数据结构定义
接下来,定义我们扩展SPI所用的引脚。假设我们用PB0、PB1、PB2分别作为SPI_EX的SCK、MOSI、MISO。
// spi_ex.h typedef struct { GPIO_TypeDef* sck_port; uint16_t sck_pin; GPIO_TypeDef* mosi_port; uint16_t mosi_pin; GPIO_TypeDef* miso_port; uint16_t miso_pin; GPIO_TypeDef* cs_port; // 片选引脚,每个从设备独立 uint16_t cs_pin; SPI_HandleTypeDef* hw_spi; // 关联的硬件SPI句柄,用于同步时钟 uint8_t mode; // SPI模式 (0,1,2,3) uint8_t data_size; // 数据位宽,如8或16 } SPI_EX_HandleTypeDef; // 初始化一个扩展SPI实例 void SPI_EX_Init(SPI_EX_HandleTypeDef *hspi_ex); // 扩展SPI阻塞式传输函数 HAL_StatusTypeDef SPI_EX_TransmitReceive(SPI_EX_HandleTypeDef *hspi_ex, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size);在初始化函数SPI_EX_Init中,我们需要将定义的GPIO配置为推挽输出(SCK, MOSI)和输入上拉/下拉(MISO)。这里有一个至关重要的细节:SCK引脚的初始化。在混合方案中,扩展SPI的SCK引脚不应该由软件直接翻转,而是作为“时钟输入监测”或干脆不用。真正的时钟同步依赖于软件逻辑与硬件SPI事件的同步。因此,这个SCK GPIO可以初始化为输入模式,用于在调试时测量波形;或者就初始化为输出,但保持固定电平,不起实际作用。时钟同步的本质是代码执行与硬件SPI时钟节拍的同步,而不是物理引脚信号的同步。
3.3 核心传输函数的实现:与硬件时钟同步
这是整个驱动最核心、最精妙的部分。我们以实现阻塞式单字节收发为例,讲解如何与硬件SPI1的时钟同步。
假设我们采用“利用SPI1中断事件同步”的方式。我们首先写一个辅助函数,它会在SPI1的传输事件中被调用。
// spi_ex.c static uint8_t SPI_EX_CurrentBit = 0; static uint8_t SPI_EX_TxByte = 0; static uint8_t SPI_EX_RxByte = 0; static SPI_EX_HandleTypeDef *Current_SPI_EX = NULL; // 这个函数被SPI1的传输完成中断调用 void SPI_EX_ClockSync_Callback(void) { if(Current_SPI_EX == NULL) return; // 判断当前是发送时钟的上升沿还是下降沿?这取决于SPI1的模式(CPHA) // 假设我们使用模式0 (CPOL=0, CPHA=0): 数据在SCK上升沿采样,下降沿变化。 // 那么,在SPI1的“数据寄存器空”或“传输完成”中断时,对应一个时钟边沿。 // 我们需要根据模式,决定在哪个时刻设置MOSI和读取MISO。 // 简化模型:我们约定在SPI1开始传输后,在其每个“位时间”的中间点进行数据操作。 // 更可靠的做法是使用SPI的TXE和RXNE中断。 } // 更实际的做法:基于SPI1的轮询或中断,手动构建位循环 HAL_StatusTypeDef SPI_EX_TransmitReceiveByte(SPI_EX_HandleTypeDef *hspi_ex, uint8_t txByte, uint8_t *rxByte) { uint8_t rx = 0; uint8_t tx = txByte; Current_SPI_EX = hspi_ex; // 1. 拉低片选(如果需要) HAL_GPIO_WritePin(hspi_ex->cs_port, hspi_ex->cs_pin, GPIO_PIN_RESET); // 2. 根据SPI模式(CPHA)进行首次数据设置 if((hspi_ex->mode & 0x01) == 0) { // CPHA = 0: 数据在第一个时钟边沿(SCK从空闲状态第一次跳变)被采样。 // 因此,在时钟跳变前,主机必须先准备好数据(设置MOSI)。 SPI_EX_SetMOSI((tx & 0x80) ? 1 : 0); // 假设MSB先发 tx <<= 1; } // 3. 启动一次硬件SPI1的传输(发送一个无关字节以产生8个时钟脉冲) uint8_t dummy = 0xFF; // 这里我们使用轮询方式,等待SPI1发送完成。在等待期间,我们需要“跟随时钟”处理数据。 // 但轮询无法知道精确的时钟边沿。因此,更好的方法是利用SPI1的TXE/RXNE中断。 // 我们以中断方案为例,重构思路: }由于在HAL库的阻塞式传输中,我们无法插入精确的位操作,因此中断驱动是混合方案更可行的选择。我们需要配置SPI1使其在每个字节传输期间产生中断(通过使能SPI_IT_TXE或SPI_IT_RXNE),然后在中断服务程序中进行扩展SPI的位操作。
重构后的核心逻辑如下:
- 使能SPI1的TXE(发送缓冲区空)中断。
- 当TXE中断触发,意味着SPI1可以加载下一个要发送的数据了。此时,硬件SPI的移位寄存器正在移出上一个数据位,并产生一个SCK时钟边沿。
- 在TXE中断服务程序里: a.读取扩展SPI的MISO引脚电平,拼接到接收字节中。 b.根据要发送的数据,设置扩展SPI的MOSI引脚电平。 c. 为了维持SPI1的时钟持续产生,向SPI1的数据寄存器(DR)写入一个无关的字节(如0xFF)。
- 重复步骤2-3共8次,完成一个字节的收发。
- 在SPI1的传输完成中断里,进行收尾工作(如拉高片选)。
实操心得:这种中断同步方式对代码的执行时间有严格要求。中断服务程序(ISR)必须非常短小精悍,执行时间要远小于一个SPI位的时间。如果ISR执行太慢,可能会错过下一个时钟边沿,导致数据错位。因此,在ISR中只做最必要的位操作和寄存器读写,避免复杂的计算或函数调用。对于高速SPI,甚至需要考虑用汇编来优化关键部分。
4. 实战代码剖析与移植要点
为了让思路更清晰,我提供一个简化但更直观的“延时同步”版本代码。这个版本不依赖SPI1的中断,而是利用其精准的时钟频率,通过计算延时来模拟位时序。它适用于中低速场景,且对时序要求不是极端苛刻的情况。我们假设SPI1被配置为产生1MHz的SCK时钟(周期为1us)。
// spi_ex.c (延时同步版) void SPI_EX_Delay(uint32_t us) { // 实现一个微秒级延时函数,可以使用DWT周期计数器或SysTick。 // 这里假设有一个精准的Delay_us函数可用。 Delay_us(us); } HAL_StatusTypeDef SPI_EX_TransmitReceiveByte_Delay(SPI_EX_HandleTypeDef *hspi_ex, uint8_t txByte, uint8_t *rxByte) { uint8_t rx = 0; uint8_t tx = txByte; uint32_t half_bit_delay = 0; // 半个位时间的延时 // 计算半个SCK时钟周期的延时(单位us) // 例如,SPI1时钟为1MHz,周期1us,半周期0.5us。但软件延时精度有限,可能需要调整。 half_bit_delay = 0.5; // 理论上0.5us,实际可能需要根据测试调整 // 拉低片选 HAL_GPIO_WritePin(hspi_ex->cs_port, hspi_ex->cs_pin, GPIO_PIN_RESET); for(int i = 0; i < 8; i++) { // 设置MOSI (根据是否MSB先行) if(hspi_ex->first_bit == SPI_FIRSTBIT_MSB) { HAL_GPIO_WritePin(hspi_ex->mosi_port, hspi_ex->mosi_pin, (tx & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); tx <<= 1; } else { HAL_GPIO_WritePin(hspi_ex->mosi_port, hspi_ex->mosi_pin, (tx & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); tx >>= 1; } // 等待半个周期(建立时间) SPI_EX_Delay(half_bit_delay); // 产生时钟上升沿/下降沿 (通过控制关联的硬件SPI?不,这里我们模拟) // 注意:在这个版本里,我们只是用延时来对齐时间,并没有物理的SCK信号从扩展引脚输出。 // 真正的时钟同步是“时间概念”上的同步。我们需要在此时启动一次硬件SPI的传输吗? // 更好的方法是:在这个时刻,我们读取硬件SPI的状态,或者利用一个由硬件SPI时钟触发的定时器。 // 这变得复杂了。因此,“延时同步”版更接近于纯软件模拟,只是延时参数参考了硬件SPI的时钟。 // 读取MISO if(HAL_GPIO_ReadPin(hspi_ex->miso_port, hspi_ex->miso_pin) == GPIO_PIN_SET) { if(hspi_ex->first_bit == SPI_FIRSTBIT_MSB) { rx |= (0x80 >> i); } else { rx |= (0x01 << i); } } // 再等待半个周期(保持时间) SPI_EX_Delay(half_bit_delay); // 模拟时钟的另一个边沿... } // 拉高片选 HAL_GPIO_WritePin(hspi_ex->cs_port, hspi_ex->cs_pin, GPIO_PIN_SET); if(rxByte != NULL) { *rxByte = rx; } return HAL_OK; }这段代码揭示了一个关键点:纯粹的“延时同步”版本,实际上已经退化为一种高精度定时模拟SPI,它依赖于一个非常精准的微秒延时函数。它的稳定性受系统中断、其他任务的影响很大。因此,它并不是标题所述方案的最佳实现。
真正的“通过硬件SPI模块驱动”的精髓,在于事件同步,而非延时同步。我们需要一个硬件机制来告知软件:“现在正好是时钟边沿的时刻”。这可以通过以下方式实现:
- SPI中断法:如前所述,利用SPI的TXE/RXNE中断。
- 定时器捕获法:将硬件SPI的SCK引脚连接到一个定时器的输入捕获通道。在SCK的每个上升沿或下降沿产生捕获中断,在中断里进行数据位操作。这种方法硬件连接稍复杂,但软件同步非常直接。
- DMA+PWM法:配置一个定时器产生PWM波(作为扩展SPI的SCK),同时配置DMA将发送数据缓冲区自动搬运到GPIO的位设置寄存器(如BSRR)。接收则可以通过另一个定时器输入捕获或外部中断读取。这种方法性能最高,几乎不占用CPU,但实现也最复杂。
5. 调试技巧与常见问题排查
调试这种软硬结合的通信协议,逻辑分析仪或者示波器几乎是必备的。光靠点灯打印,很难定位时序上的细微错误。
5.1 调试步骤与工具使用
- 先验证硬件SPI本身:单独测试硬件SPI1驱动一个简单的设备(如SPI Flash的ID读取),确保其配置(模式、速率、相位)绝对正确,波形干净。
- 可视化时序波形:将硬件SPI1的SCK、MOSI、MISO,以及你定义的扩展SPI的MOSI_EX、MISO_EX、CS_EX引脚,都连接到逻辑分析仪。同时抓取这些信号。
- 观察点1:扩展SPI的MOSI_EX数据变化,是否严格对齐硬件SPI1的SCK时钟边沿?偏移有多大?
- 观察点2:在CS_EX拉低期间,硬件SPI1的SCK是否持续、等间隔地输出脉冲?这验证了你的“时钟引擎”是否在正常工作。
- 观察点3:从设备返回的MISO_EX数据,是否在正确的SCK边沿被采样?你的软件读取MISO_EX的时机是否恰当?
- 使用调试器与变量观察:在中断服务程序中设置断点,观察发送和接收缓冲区的数据变化。但注意,断点会严重破坏实时性,可能导致通信失败,所以只能用于检查初始状态和最终结果。
5.2 常见问题速查表
| 现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 扩展SPI完全无通信 | 1. 片选CS引脚未正确控制。 2. 扩展SPI的GPIO模式配置错误(输出/输入)。 3. 硬件SPI未成功启动或时钟未产生。 | 1. 用逻辑分析仪确认CS信号。 2. 检查GPIO初始化代码,确认MOSI为输出,MISO为上拉/浮空输入。 3. 测量硬件SPI的SCK引脚是否有波形。检查SPI初始化代码和启动代码。 |
| 数据错位(如0x55收成0xAA) | 1. SPI模式(CPOL/CPHA)不匹配。 2. 数据位顺序(MSB/LSB)不匹配。 3. 软件读写数据的时机与时钟边沿未对齐。 | 1. 核对从设备数据手册和代码中的SPI模式设置。 2. 核对数据位顺序设置。 3.这是最可能的原因。用逻辑分析仪放大看单个位的时序,检查MOSI变化和MISO采样点相对于SCK边沿的位置。调整中断触发点或延时。 |
| 通信不稳定,时好时坏 | 1. 中断服务程序执行时间过长,错过时钟事件。 2. 系统中有更高优先级中断打断了SPI或扩展逻辑。 3. 电源噪声或信号完整性问题。 | 1. 优化ISR代码,移除任何非必要操作(如打印)。考虑使用DMA。 2. 调整中断优先级,确保SPI相关中断有足够高的优先级(但不要是最高,避免阻塞系统)。 3. 检查PCB布线,SCK和MOSI/MISO走线是否过長,是否有并联端接。在信号线上增加串联电阻(如22Ω-100Ω)。 |
| 通信速度远低于预期 | 1. 软件模拟部分开销太大。 2. 使用的硬件SPI本身速率配置不高。 3. 中断或任务调度引入额外延迟。 | 1. 评估代码性能。对于“中断同步法”,ISR本身的耗时必须小于一个SPI位时间。 2. 尝试提高硬件SPI的波特率设置。 3. 如果使用RTOS,确保通信任务优先级足够高,且关中断的临界区尽量短。 |
| 只能发送,不能接收(或接收全为0/1) | 1. MISO引脚配置错误(应为输入)。 2. 从设备未正确输出数据(检查从设备电源、配置)。 3. 软件读取MISO的时机不对,在读的时候引脚电平已变化。 | 1. 确认MISO GPIO初始化为上拉/浮空输入模式。 2. 用逻辑分析仪看MISO_EX引脚上是否有从设备发送的数据波形。 3. 仔细分析SPI模式,确认采样边沿。在读取MISO前,确保时钟边沿已稳定建立。 |
避坑技巧:在项目初期,可以故意降低硬件SPI的通信速率,比如降到100kHz或更低。在这个低速下,软件模拟部分的时序容错空间大,更容易调试成功。等逻辑完全正确后,再逐步提高速率,观察通信的稳定性边界在哪里,从而为项目留下足够的余量。
6. 性能优化与高级应用探讨
当基本功能实现后,我们自然会考虑如何让它更快、更稳定、更省资源。
6.1 提升通信速率的关键
- 精简ISR,接近极限:对于“中断同步法”,ISR里只保留最核心的位操作和寄存器访问。使用位带操作(如果MCU支持)或直接操作寄存器来替代
HAL_GPIO_ReadPin/WritePin函数,后者有函数调用开销。例如:// 假设PB0是MOSI, 使用位带操作快速置位/清零 #define MOSI_PIN_BITBAND (*(__IO uint32_t *)(0x42000000 + (GPIOB_BASE + 0x14 - 0x40000000)*32 + 0*4)) // 在ISR中 if(tx_data & mask) { MOSI_PIN_BITBAND = 1; // 置高,比HAL_GPIO_WritePin快得多 } else { MOSI_PIN_BITBAND = 0; // 置低 } - 转向DMA驱动的事件同步:这是终极优化方案。思路是:
- 配置硬件SPI使用DMA进行数据传输(发送和接收)。
- 配置一个与SPI时钟同步的定时器(可以从SPI的SCK得到触发)。
- 使用这个定时器触发另一个DMA,将你的发送数据缓冲区搬运到扩展MOSI的GPIO寄存器。
- 同时,配置另一个DMA,在定时器的另一个相位,将扩展MISO的GPIO寄存器状态搬运到接收缓冲区。
- 这样,整个扩展SPI的通信完全由DMA硬件完成,CPU零干预,可以达到接近硬件SPI的性能。但实现复杂度极高,需要对STM32的DMA和定时器联动有很深的理解。
6.2 扩展多个SPI从设备
我们的架构天生支持扩展多个设备。只需要为每个扩展的SPI从设备定义独立的CS(片选)、MOSI、MISO引脚组即可。它们可以共享同一个硬件SPI作为时钟基准。在驱动层,你需要管理多个SPI_EX_HandleTypeDef实例。当需要与某个设备通信时,选中对应的实例,操作其对应的GPIO引脚。关键在于,同一时间只能有一个扩展设备被选中(CS为低),否则数据线会产生冲突。
6.3 应对不同的SPI模式
我们的示例代码主要围绕SPI模式0(CPOL=0, CPHA=0)。如果从设备需要模式1、2或3,我们需要调整软件中数据设置和采样的时机。这需要在中断服务程序或位操作循环中,根据hspi_ex->mode进行条件判断。例如,对于CPHA=1的模式,数据是在第二个时钟边沿采样,因此需要在第一个边沿之后才设置MOSI,在第二个边沿之前读取MISO。这要求你的同步机制(中断或捕获)能区分第一个和第二个边沿。
7. 总结与项目启示
回过头看,“STM32通过硬件SPI模块软件模拟驱动来进行拓展”这个项目,远不止是几行GPIO控制代码。它是一个典型的资源受限环境下,通过软硬件协同设计解决实际问题的案例。它考验的是开发者对底层协议(SPI)、硬件外设(SPI、GPIO、中断、DMA)以及系统实时性的综合把握能力。
在实现过程中,我最大的体会是:理解时序比编写代码更重要。在动手写第一行驱动之前,必须反复研读STM32的SPI外设手册和从设备的数据手册,弄清楚每一个时钟边沿和数据变化的关系。逻辑分析仪是你的眼睛,没有它,调试这种时序相关的代码就像在黑暗中摸索。
这个方案的成功实施,能为老旧型号或引脚紧张的STM32项目带来新的生机。但它也不是银弹,它的性能、稳定性和实现复杂度需要根据项目需求仔细权衡。对于超高速(>10MHz)或对时序抖动极其敏感的应用,还是应该优先选择硬件多路复用器或更换更多SPI外设的MCU型号。
最后,分享一个我常用的测试技巧:在编写扩展SPI驱动时,可以先不连接真实的从设备,而是用杜邦线将扩展的MOSI和MISO短接起来,实现“自发自收”的环回测试。这样能最快速地验证你的驱动基本逻辑和时序是否正确,排除了从设备本身故障的干扰,可以让你更专注于驱动代码本身的调试。
