把SPI模块调通,对很多刚接触STM32F1的人来说是一道不大不小的坎,尤其是手头只有一块核心板、没有独立从机芯片的时候。我当时就卡在一个很尴尬的位置:想写一个SPI收发实验,又不想接外挂Flash或者OLED,最后干脆把芯片上自带的两个SPI外设对起来做实验,SPI1当主机、SPI2当从机,不谈协议层面的高深用法,就老老实实把它俩之间的收发流程跑通。这个思路看起来简单,实际动手后才发现,SPI1和SPI2虽然都是STM32F1里的同一种外设,但它们的时钟来源、引脚位置、配置方式,甚至踩坑的表现形式都不太一样。这篇文章就把我的实验过程、完整代码和排查经验整理出来,给准备做STM32F1 SPI收发实验的人一个可以直接照抄的参考。
1. 实验目标与接线:让SPI1和SPI2在同一颗F103里互相对话
1.1 为什么要用SPI1和SPI2做互联
很多人做SPI实验会选择买一块W25Q64 Flash小板,然后拿STM32的SPI1去读Flash的ID。这种实验当然没问题,但它本质上只是“主机读从机”,主机发出时钟,Flash把数据甩回来,整个过程其实是单向观察居多。我想做的“收发实验”更偏向于主从双方同时参与:主机能发、能从机也能回,还能验证回传数据的正确性。
如果手头有两块F103核心板,也可以一块当主机、一块当从机,用杜邦线把两个板子的SPI引脚连起来。但两板方案涉及供电共地、下载器干扰等问题,调试时不容易分清问题到底出在哪边。后来我注意到,STM32F103系列基本都有SPI1和SPI2两个外设,SPI1挂在APB2总线上,SPI2挂在APB1总线上,它们可以同时工作。所以最省事的方案,就是一颗芯片上同时初始化两个SPI,让它们自己跟自己对话。这样既不需要额外买硬件,也把“两个SPI外设”共同收发这件事一次验证清楚。
1.2 引脚选择与接线表
STM32F103的SPI1和SPI2引脚并不都在同一个GPIO端口上,连线之前最好把引脚表打出来贴显示器旁边。SPI1固定在PA5、PA6、PA7,SPI2固定在PB13、PB14、PB15。虽然F1的引脚可以用重映射功能挪位置,但实验阶段用默认引脚最省心,重映射容易引入额外的AFIO配置问题。
| 功能 | SPI1(主机)引脚 | SPI2(从机)引脚 | 连线关系 |
|---|---|---|---|
| SCK | PA5 | PB13 | PA5接PB13 |
| MISO | PA6 | PB14 | PA6接PB14 |
| MOSI | PA7 | PB15 | PA7接PB15 |
| GND | 板子GND | 板子GND | 共地 |
| NSS | PA4(软件NSS,可不接) | PB12(硬件NSS,接GND) | PB12接GND |
这里有个关键点:主机的NSS引脚和从机的NSS引脚,在实际连线时很容易被忽略。如果主机使用软件NSS管理,PA4悬空没关系,主机内部可以忽略NSS引脚电平,只要把SSI位拉高就不会进入模式故障。但从机如果也使用软件NSS,就必须把SSI位配好,否则从机可能一直认为自己是“未选中”状态,SCK来了也不干活。为了避免这个坑,我在实验里让从机使用硬件NSS,把PB12直接接地,这样从机始终处于选中状态,行为非常稳定。你不需要纠结从机有没有被“选中”,只要主机一发时钟,从机必然响应。
GPIO模式也要区分清楚,SPI的输入输出引脚不能全部照抄成复用推挽。主机侧PA5、PA7是输出,配置成复用推挽;PA6是输入,配置成浮空输入。从机侧PB14是输出,配置成复用推挽;PB13(SCK)、PB15(MOSI)是输入,配置成浮空输入;PB12作为硬件NSS输入,也配置成浮空输入。这个配置不是拍脑袋定的,SPI外设的SCK和MOSI线上数据的流入方向完全由主从模式决定,主机往从机方向发,从机就把这些引脚当输入用。
1.3 实验环境和辅助工具
我用的是一块常见的F103ZET6板子,标准外设库版本为3.5,代码用Keil MDK编译下载。串口1用来打印调试信息,波特率设置成115200。如果你用的是C8T6最小系统板,PA5/PA6/PA7和PB13/PB14/PB15这些引脚也都存在,代码可以直接复用,只需留意板载LED、按键等占用了哪些引脚,避开冲突就行。
实验最终要达到的效果很明确:主机的SPI1向外发送一个字节,从机的SPI2在同一时刻收到这个字节,并且从机把接收到的字节取反后回发,主机的SPI1再把这个回发数据读取出来。整个流程用串口打印,屏幕上能看到每一轮“主机发送的数据”和“主机接收到的数据”。如果从机回传的是主机数据的取反值,那就说明主从双方不仅在数据线上通了,而且收发方向、时钟极性和相位全部正确。
2. 先说通SPI的主从原理:谁给时钟,谁发数据,数据什么时候有效
2.1 SPI不是在发数据,是在交换移位寄存器
我第一次接触SPI时很容易把“发送”和“接收”理解成两个独立动作,总觉得发送就是发送、接收就是接收。实际看SPI硬件结构才发现,SPI主机和从机内部各有一个8位移位寄存器,主机SCLK每翻转一个周期,主从两个移位寄存器就同步向左移动一位,主机移出一位给从机,从机同时移出一位给主机。也就是说,每一个时钟周期内,主机发出1位、收回1位,从机发出1位、收回1位,两边真正在做的是“互换”数据。
这个原理直接解释了一个很多人困惑的现象:主机的“发送函数”执行完之后,为什么必须把接收寄存器读出来?如果你只是往SPI_DR寄存器里写Byte,却从不读SPI_DR,接收标志位会一直保持置位,甚至影响下一次发送。因为主机每次发送时钟的同时,必然有数据从MISO线上回到主机移位寄存器,你不把它读走,它就堵在那。这个实验正好用两个SPI把这种“同时交换”的节奏展示得特别明显,主机每发出一个字节,必然也从从机那里拿回一个字节。
2.2 CPOL和CPHA必须主从保持一致
SPI不像USART那样有起始位和停止位,它靠SCK的空闲电平和采样边沿来区分数据。CPOL决定SCK空闲时是低电平还是高电平,CPHA决定数据在第1个边沿采样还是第2个边沿采样。主机和从机如果CPOL、CPHA不一致,就会出现一种很恶心的情况:线路上数据确实在跳变,主机也能发送,但从机收到的数据全是乱的,或者压根进不了接收中断。
实验中我使用的是SPI_CPOL_High、SPI_CPHA_2Edge这种常见组合,也就是模式3,SCK空闲为高,数据在第二个边沿被采样。这个组合在Flash、SD卡、各种传感器里都很常见。当然你也可以改成模式0,只要主从两边初始化保持一致。需要注意的是,主机和从机的初始化结构体里都必须显式写这两个参数,别以为从机是“被动接收”,时钟极性和相位就无所谓。从机的CPOL、CPHA与主机不一致时,从机的移位寄存器会在错误的边沿采样,结果不是预期数据。
2.3 硬件片选与软件片选:实验里该怎么选
串口、I2C、SPI之间区别很大,串口是全双工点对点,靠波特率同步,I2C靠SCL同步且带设备地址,SPI则依靠NSS片选信号来选中总线上的某个从机设备。SPI协议一开始就是为“一主多从”设计的,NSS就是用来区分当前时钟和数据是发给谁的。
但STM32给了工程师两种简化方式:硬件NSS和软件NSS。硬件NSS模式下,NSS引脚由外设自动控制,主机可以通过 SSOE 位让SS输出低电平,从机必须等NSS被拉低才能工作。软件NSS模式下,NSS引脚状态被芯片内部的SSI位替代,你不必接片选线,只需把SSI位配好。对新手来说,软件NSS的概念更容易绕晕,所以我建议第一次做这个实验时,主机用软件NSS、从机用硬件NSS。主机用软件NSS并强制SSI=1,保证自己不会出现模式故障;从机用硬件NSS并把PB12接地,保证从机永远处于选中状态。这相当于把一个复杂问题先降维,让你集中精力观察MISO、MOSI和SCK三根线,等原理吃透了再玩多从机。
3. 初始化代码:SPI1挂在APB2,SPI2挂在APB1,别把时钟开错了
3.1 时钟树、GPIO复用和IO模式的分配
初始化STM32外设离不开三件事:开外设时钟、配GPIO模式、配外设模式。SPI在这三件事上都有一些容易踩的细节。
首先,SPI1挂在APB2总线上,SPI2挂在APB1总线上。开启SPI1时要调用RCC_APB2PeriphClockCmd,开启SPI2时要调用RCC_APB1PeriphClockCmd。如果你把SPI2的时钟开到了APB2组里,编译不会报错,但SPI2永远不能被使能,初始化后再怎么配置都没有反应。同理,GPIOA、GPIOB和AFIO时钟应该开在APB2组里,因为GPIO和AFIO都挂在APB2上。
其次,STM32F1的GPIO不仅分成输入输出,还分成模拟输入、上拉下拉、复用推挽、开漏输出等模式。SPI的SCK和MOSI在主机模式下是输出,所以要配成复用推挽;MISO是输入,配浮空输入。从机模式反过来看,SCK和MOSI这两个引脚上是外部输入信号,所以要配成输入模式,MISO要往外发数据,才需要配置成复用推挽。
最后,AFIO时钟一定记得开。很多人配置了GPIO、配置了SPI,但忘了开RCC_APB2Periph_AFIO,结果有时候能跑,有时候跑不通。对于SPI默认引脚来说,AFIO不一定每次都起作用,但既然库函数和参考手册都建议开启,我建议统一打开,避免后续其他外设重映射时产生隐藏问题。
3.2 完整SPI1主机初始化代码
下面给出我实验中使用的主机初始化函数。这个函数使用SPI1作为全双工主机,时钟极性为高、第二边沿采样,波特率预分频16分频,MSB先行,软件NSS管理。
void SPI1_Master_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI1->CR1 |= SPI_CR1_SSI; SPI_Cmd(SPI1, ENABLE); }这里重点是两行容易被忽略的代码。第一,SPI1->CR1 |= SPI_CR1_SSI;在很多教程里并不显式出现,但标准库的SPI_Init在配置SPI_NSS_Soft时并不会自动帮你把SSI位置1。SSI位在主机模式下保持为1,可以让芯片知道NSS没有被拉低,避免出现模式故障。如果你不写这一行,主机可能跑着跑着就进MODF中断,SPE位被硬件自动清零,SPI直接罢工。第二,SPI_CRCPolynomial虽然这次实验用不到CRC功能,但库函数的结构体里要求必须显式赋值,不赋值有时会使用随机值,导致初始化行为不确定。
3.3 完整SPI2从机初始化代码
从机初始化与主机很像,但有几个地方正好相反。SPI_Mode必须写成SPI_Mode_Slave,从机的NSS选择了硬件NSS,GPIO配置上也有差异。
void SPI2_Slave_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Slave; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Hard; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI2, &SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }从机模式下SPI_BaudRatePrescaler其实不会真正影响通信速率,因为SCK是从外部输入的,但是从机内部的SCLK频率也不能太低。STM32F1手册里要求从机模式下,外部SCK频率必须小于从机内部PCLK频率,实际使用中我仍然会设置一个分频值,反正不碍事。如果你把从机的波特率预分频写得过高,某些边界条件下可能影响移位寄存器的判断,所以老老实实和主机用同一个预分频值反而安心。
需要额外说明的是,PB12作为硬件NSS输入,代码里配置成浮空输入,接线时要确保它被拉低。如果PB12悬空,从机的NSS电平不确定,可能在SCK到来时不被选择,实验表现为从机完全收不到数据。你既可以用杜邦线把PB12直接连到GND,也可以用一块面包板把该引脚和一个普通GPIO输出低电平相连。我个人推荐直接接GND,简单粗暴不漏电。
3.4 波特率分频:先慢后快
SPI1挂在APB2上,PCLK2为72MHz;SPI2挂在APB1上,PCLK1为36MHz。这个时钟差异直接决定了SPI1和SPI2的最高分频选择。主机SPI1使用16分频,就是72MHz除以16,得到4.5MHz的SCK频率。为什么一开始不用最高频率?因为实验用杜邦线连接两个SPI,杜邦线的分布电容和接触电阻在高频下容易让波形变差,一旦出现数据异常,很难判断是软件问题还是信号完整性问题。先跑低频,等程序逻辑确认无误,再逐步降低预分频值,才符合排障的思路。
4. 收发联动:核心代码与运行效果
4.1 主从数据交换的节奏感
一旦理解了SPI是“换数据”之后,收发联动程序就变得好写了。STM32F1的SPI用两个标志判断状态:发送时等待TXE标志,表示发送数据寄存器已空,可以写入下一个数据;接收时等待RXNE标志,表示接收数据寄存器非空,可以读走数据。写入SPI_DR后,外设会立即启动SCK生成和数据移位过程;读SPI_DR时,硬件会清掉RXNE标志。
但要注意,主机发送一个字节和从机收到这个字节几乎是同一个动作,中间并没有延迟。主机的“发送”操作,实际把数据写进发送数据寄存器,硬件自动把它移到移位寄存器,SCK翻转8次,8位数据就变成了从机接收的数据。如果从机此刻也往它自己的DR里写了一个字节,这个字节会同步通过MISO返回给主机。所以在程序层面,主机通常在同一个循环里执行“发送”和“接收”两次操作,第一次发送的真正结果,在第二次发送时才能被读回来。这个“错一位”的节奏很自然,不需要刻意消除,只要串口打印时留心就好。
4.2 主机轮询收发,从机中断响应
为了让接收过程更真实,我在实验里给SPI2开启了RXNE中断,从机一收到数据就进入中断服务函数,读走数据并把取反后的结果写入从机发送数据寄存器,作为下一轮的回复。主机则使用轮询方式,循环发送0x01到0x0A。
void SPI2_IRQHandler(void) { uint8_t rx_data; if (SPI_I2S_GetITStatus(SPI2, SPI_I2S_IT_RXNE) != RESET) { rx_data = SPI_I2S_ReceiveData(SPI2); SPI_I2S_SendData(SPI2, (uint8_t)~rx_data); } }中断服务里没有做大量运算,只是读数据、写数据,这样能保证从机在下一个SCK周期到来前把回复字节准备好。如果你在中断里用printf打印,那通信速率会瞬间垮掉,因为串口打印一个字符的时间足够主机发好几轮数据。这个实验里处理函数很短,即使SPI时钟跑得再高,也不容易在中断里丢失数据。
主机端的循环可以写成下面这样:
int main(void) { uint8_t tx_buf[10] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}; uint8_t rx_buf[10] = {0}; uint8_t i; USART1_Init(115200); SPI1_Master_Init(); SPI2_Slave_Init(); SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_RXNE, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = SPI2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); SPI_I2S_SendData(SPI2, 0x00); while (1) { for (i = 0; i < 10; i++) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, tx_buf[i]); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); rx_buf[i] = SPI_I2S_ReceiveData(SPI1); delay_ms(10); } for (i = 0; i < 10; i++) { printf("tx=0x%02X rx=0x%02X\n", tx_buf[i], rx_buf[i]); } delay_ms(1000); } }为了让第一轮通信就有一个稳定的回复,我在主循环开始前,额外向SPI2的DR里写了一个字节0x00。这样当主机第一次发送时钟时,从机的MISO线上已经有字节0x00在往外移,主机读到的rx_buf[0]会来自这个“预置回复”,而不是未知值。
4.3 串口输出与关键标志位验证
正常运行后,串口会打印类似这样的结果:
tx=0x01 rx=0xFF tx=0x02 rx=0xFE tx=0x03 rx=0xFD tx=0x04 rx=0xFC第一轮的rx=0xFF就是主循环前从机DR里预置的0x00取反值,后面的rx则分别是上一轮主机发送数据的取反值。比如主机第二次发送0x02,从机在SPI2中断里收到0x02后把0xFD写回DR,这个0xFD会在主机第三次发送0x03的时钟周期内经MISO传回给主机。所以第三行打印的rx=0xFD实际上对应第二轮发送的0x02取反,数据链条是连贯的。
如果看到类似的“错一位”现象,不要慌,这恰恰说明SPI的换数据机制是正常的。真正需要担心的是,如果第二行、第三行打印的rx全部固定为0xFF,或者全部为0,那说明从机没有把正确数据放进DR,问题多半出在SPI2初始化或者中断没触发上。这个问题放在下一节讲排查方法。
5. 我踩过的坑:SPI2收不到数据时到底该检查什么
5.1 现象回顾
第一次跑这个实验时,我的串口输出非常稳定,但稳定得让人头疼:主机发送的tx数据一直在变,主机接收到的rx却永远是0xFF。这看起来像主机完全没有读到从机的任何数据,甚至像主机MISO引脚一直是高电平。
当时我把代码翻来覆去看了好几遍,初始化代码好像没有遗漏,GPIO也按表格配的,为什么从机就是没反应呢?后来用万用表量PB12到GND之间的电压,发现竟然不是0V,而是1.2V左右的浮动电平。再一看,原来杜邦线只插了一半,半接触状态导致从机NSS引脚处于不确定电平。把PB12彻底接到GND之后,问题立刻消失。
5.2 逐层排障路径
后来我又故意制造了几个典型错误,总结出了一套从软件到硬件的排查顺序。当你发现SPI2收不到数据,或者收到全0xFF时,不要先怀疑芯片坏了,按照下面这个顺序过一遍:
第一,看SPI2的SPE位是否被置1。如果从机初始化后没有调用SPI_Cmd(SPI2, ENABLE),SPI2外设根本没有使能,收到SCK也没有反应。这个错误很低级,但调试时最容易忽略。
第二,看PB12是否可靠接地。硬件NSS模式下,从机NSS必须为低电平才被选中。如果PB12悬空,从机的输入可能是随机值,数据自然进不来。
第三,看GPIO模式是否配反。从机PB13和PB15是输入引脚,配成浮空输入;PB14是输出引脚,配成复用推挽。如果你把PB13、PB15配成了复用推挽,虽然多数情况下也能工作,但会改变引脚的驱动状态,增加信号冲突风险。严格按照输入输出方向配置,最能减少干扰。
第四,看SPI2的中断是否使能。如果从机的数据确实进来了,但你没有开SPI2的RXNE中断,也没有在中断服务函数里读DR,那么数据会被一直堵在接收寄存器里,后续数据无法进入。轮询方式的话,就要及时读SPI_I2S_ReceiveData。
第五,用示波器或者逻辑分析仪观察PB13上是否有SCK波形。如果主机这边配置成软件NSS且SSI位为0,主机可能进入模式故障,SPI1的SCK根本不输出,那从机当然什么都收不到。这个Bug很容易被忽视,所以我再次强调那句SPI1->CR1 |= SPI_CR1_SSI;的重要性。
5.3 模式故障与NSS悬空的连锁反应
模式故障是STM32F1的SPI特有的一种保护机制,主机模式下,如果硬件检测到NSS引脚为低电平,就会认为总线上有另一主机也要当主机,于是触发MODF错误,同时把SPE位清零,整个SPI停止工作。症状非常像“SPI卡死”。出现这种状况时,用调试器看SPI_SR的MODF位,会发现它被置1了。
避免模式故障最稳妥的手段,就是使用软件NSS并把SSI置1。软件NSS模式下,NSS引脚被内部逻辑忽略,外部不管接不接上拉电阻,都不会触发MODF。但要注意,主机和从机不能都使用硬件NSS却把NSS引脚接反,如果从机的NSS被拉高、主机的NSS被拉低,主机反而会先挂掉。这个问题在双机联调时尤其常见。
我建议所有做SPI实验的人,初始化主机后都主动读一次SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_MODF),如果MODF标志为1,说明配置或者接线里存在问题。把这个检查项写进程序里,可以节省大量时间。
5.4 排障速查表
| 现象 | 最可能原因 | 处理办法 |
|---|---|---|
| 主机发数据,从机SPI2的RXNE一直不置位 | 从机NSS没有被拉低 | PB12接GND |
| 主机收数据全是0xFF | 从机DR里没有数据可发,或MISO连线断 | 主循环前向SPI2写一次0x00 |
| 主机一运行就卡死在发送等待 | SPI1进入MODF模式 | 检查软件NSS配置,SPI1->CR1置SSI=1 |
| 从机能收到数据但中断不进入 | SPI2的NVIC或RXNE中断未使能 | 打开NVIC,调用SPI_I2S_ITConfig |
| SPI2外设不工作 | SPI2时钟开错到了APB2 | 使用RCC_APB1PeriphClockCmd开启SPI2 |
| 数据大部分正确但偶尔错位 | 使用了杜邦线且SCK频率过高 | 降低波特率预分频,检查接线 |
这张表是我这次实验之后整理出来的,基本覆盖了新手在SPI主从实验中容易遇到的90%问题。
5.5 没有示波器时怎么判断波形
很多人手头没有示波器,只有一块开发板和一个STM32调试器。这时候可以用一个间接方法验证SPI线路上有没有数据:把主机的SCK引脚临时配置成普通GPIO输出,程序里翻转该引脚,用万用表测量引脚电压变化。虽然这个方法测不了高频波形,但至少能确认引脚本身没有损坏。
更实用的办法是把SPI1的MOSI(PA7)用杜邦线直接接到SPI1的MISO(PA6),做一个主机自发自收回环。如果回环模式下主机能收到自己发送的数据,说明SPI1内部收发链路没有问题,问题一定出在SPI2这边。这个“分段隔离”技巧在双设备联调时特别好用,能迅速缩小故障范围。
6. 进阶玩法:DMA、高速率、逻辑分析仪与后续想法
6.1 为什么DMA一个就够,两个会怎样
做SPI收发实验时,有人会纠结到底配几个DMA通道。其实SPI全双工通信中,SCK由主机控制,主机的发送和接收是同时进行的。如果只关心“接收大量数据”,一个接收DMA就够,发送部分可以通过在发送DMA上循环发送0xFF来提供时钟。换句话说,主机想要从从机读一长串数据,不一定需要配置发送DMA,用软件连续写0xFF到SPI_DR也能达到目的。两个DMA都用的场景,通常是主机既要连续发送一大块数据,又要同时无间断接收一大块数据,比如驱动SD卡写入时边读边写。
但STMF1的SPI DMA有一个需要注意的地方:SPI发送DMA和接收DMA的触发条件是分开的,发送DMA由TXE触发,接收DMA由RXNE触发。如果只开启接收DMA,却没关发送中断,当主机发送寄存器空时会产生TXE事件,可能导致DMA请求异常。所以实际项目里建议成对使用SPI的TX/RX DMA,而实验阶段没有必要,轮询已经足够教学了。
6.2 把速率拉高之前先看电源和连线
实验初期我用了16分频,SCK为4.5MHz,杜邦线明显可行。后续测试中,把预分频改成8分频,SCK到9MHz,数据依然正常;改成4分频,SCK到18MHz后,短距离杜邦线下偶尔出现数据错位。这不是STM32F1跑不了18MHz,而是杜邦线的寄生电感和引脚间的串扰在高频下被放大了。
SPI速率与主频的关系很直接:主机SPI的SCK频率等于PCLK除以预分频值,PCLK2为72MHz,PCLK1为36MHz。使用SPI1时,最低预分频是2分频,即36MHz,但这是极限值,一般不建议跑满。如果你需要在更高频率下稳定通信,至少要把杜邦线换成飞线或者PCB走线,同时在SPI的SCK线上串联一个33欧姆左右的电阻抑制振铃。像TF卡这类对信号完整性敏感的存储设备,不少电路还会在SPI信号线上加上拉电阻,这就是为什么网上总有人讨论“TF卡SPI需要上拉吗”这个问题。其实上拉主要是为了应对主设备引脚漏极开路或空闲电平不确定的情况,F103的复用推挽输出本身能驱动高电平,上拉不是必须的,但有的人加了上拉后稳定性确实更好,因为这个上拉能改善信号在空闲状态下的电平。
6.3 实验之后我真正想说的三件事
第一,做STM32F1的SPI收发实验,最大的收获不是记住了SPI_InitStructure里的字段,而是搞清楚了两个SPI外设挂在不同的总线上、拥有不同引脚、但通信规则完全一致。SPI1和SPI2的寄存器结构几乎一样,学会了SPI1,SPI2的代码就是复制粘贴改名字的事。第二,这次实验表面上是两个SPI在通信,本质上是用一个芯片内两个外设做互联,遇到问题时比两块板互联更容易控制变量。如果以后接到ESP8266、DHT11这类芯片和模块,不要被它们特殊的“类SPI”或者“单总线”时序吓到,底层逻辑和这次实验并没有本质区别,不过是加个片选时序、加个启动延时、注意数据位宽和采样沿。第三,软件NSS和硬件NSS没有谁一定好,但第一次做实验时优先选择“主机软件NSS+从机硬件NSS接地”是最不容易翻车的组合,等主从通信跑通后,再改成纯软件NSS或者动态片选也不迟。把这个基础打好,后面无论是用SPI驱动Flash、SD卡,还是用CubeMX重新配置工程,都能快速定位问题。