🌟 STM32 SPI通信指南:从协议原理到库函数深度实战
SPI(Serial Peripheral Interface)作为嵌入式领域的**“高速数字血管”**,凭借其全双工同步传输特性,成为连接Flash、屏幕、传感器等外设的核心通信协议。本文将深入剖析SPI的四种神秘工作模式,并手把手带你玩转STM32的SPI库函数,最后通过W25Q Flash实战项目打通任督二脉!
🔍 一、SPI协议:隐藏在时钟边沿的数据密码
1. 四线交响曲:SPI物理层解析
- SCK:主设备发出的时钟信号,节奏指挥官(频率可达数十MHz)
- MOSI:主设备输出 → 从设备输入(数据发射通道)
- MISO:从设备输出 → 主设备输入(数据回流通道)
- CS/SS:片选信号(低电平激活从设备,一主多从的关键)
⚡ 冷知识:三线SPI(半双工)可省MISO或MOSI,但牺牲吞吐量!
2. 四大模式:时钟极性与相位的魔法组合
| 模式 | CPOL | CPHA | 时钟空闲状态 | 数据采样时刻 | 应用场景 |
|---|---|---|---|---|---|
| 模式0 | 0 | 0 | 低电平 | SCK上升沿 | SD卡、多数传感器 |
| 模式1 | 0 | 1 | 低电平 | SCK下降沿 | 部分ADC芯片 |
| 模式2 | 1 | 0 | 高电平 | SCK下降沿 | 少见 |
| 模式3 | 1 | 1 | 高电平 | SCK上升沿 | W25Q Flash |
示例:W25Q128 Flash芯片仅支持模式0和模式3,配置错误将导致数据错乱!
⚙️ 二、STM32 SPI库函数完全图鉴(标准外设库版)
1. 初始化:六步搭建SPI高速公路
// 1. 使能时钟(SPI1在APB2,SPI2在APB1)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
// 2. GPIO配置(复用推挽输出)
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; // SCK, MISO, MOSI
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. SPI参数配置(核心!)
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工
SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // 主模式
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 8位数据
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 模式0
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // 72MHz/32=2.25MHz
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 高位优先
SPI_Init(SPI1, &SPI_InitStruct);
// 4. 使能SPI
SPI_Cmd(SPI1, ENABLE);
2. 关键库函数:数据流动的操控术
| 函数名 | 功能说明 | 超时风险 |
|---|---|---|
SPI_I2S_SendData() | 阻塞发送单字节数据 | 高 |
SPI_I2S_ReceiveData() | 阻塞接收单字节数据 | 高 |
SPI_I2S_GetFlagStatus() | 查询状态(TXE/RXNE/BSY等) | 中 |
SPI_DMACmd() | 启用DMA传输(高速场景必备) | 无 |
SPI_ITConfig() | 启用中断(接收完成/发送完成) | 低 |
3. 安全收发模板:避免数据丢帧的黄金法则
// 发送一字节并接收返回数据(全双工)
uint8_t SPI_TransferByte(uint8_t txData) {
// 等待发送缓冲区空(TXE=1)
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, txData); // 写入DR寄存器触发发送
// 等待接收完成(RXNE=1)→ 避免过早拉高CS!
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1); // 读取接收到的数据
}
💡 陷阱提示:STM32主频高,若未等待
RXNE就拉高CS,从设备数据可能未完全发出!
🔥 三、W25Q Flash实战:SPI库函数的终极试炼
1. Flash特性揭秘
- 写入特性:只能将
1→0,需先擦除(整块变0xFF) - 最小擦除单位:扇区(4KB) → 擦除耗时3ms!
- 最大写入单位:页(256字节) → 超页写入会回卷覆盖
2. 关键指令集(SPI驱动基石)
| 指令 | 字节序列 | 功能 |
|---|---|---|
| 写使能 | 0x06 | 开启写入权限 |
| 页编程 | 0x02+地址+数据 | 写入最多256字节 |
| 扇区擦除 | 0x20+地址 | 擦除4KB区域 |
| 读数据 | 0x03+地址 | 连续读取数据 |
3. 页编程代码示例(含安全时序)
void W25Q_WritePage(uint8_t* data, uint32_t addr, uint16_t len) {
// 1. 写使能
CS_LOW();
SPI_TransferByte(0x06); // 发送写使能指令
CS_HIGH();
// 2. 等待写使能生效(检查状态寄存器)
do {
CS_LOW();
SPI_TransferByte(0x05); // 读状态寄存器指令
status = SPI_TransferByte(0x00);
CS_HIGH();
} while(status & 0x01); // 检查BUSY位
// 3. 发送页编程指令
CS_LOW();
SPI_TransferByte(0x02); // 页编程指令
SPI_TransferByte(addr >> 16); // 地址高字节
SPI_TransferByte(addr >> 8); // 地址中字节
SPI_TransferByte(addr & 0xFF); // 地址低字节
// 4. 循环写入数据(不超过256字节)
for(int i=0; i<len; i++) {
SPI_TransferByte(data[i]);
}
CS_HIGH(); // 发送完成,结束帧
}
🚀 四、高级技巧:释放SPI的野兽性能
1. DMA+定时器:零CPU占用的精准传输
// 使用TIM3触发SPI的DMA传输
void SPI_DMA_With_Timer(void) {
// 定时器配置(每1ms触发一次)
TIM_TimeBaseInit(TIM3, ...);
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);
// DMA配置(内存→SPI_DR)
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(SPI1->DR);
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)txBuffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST; // 内存到外设
DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;
DMA_Init(DMA1_Channel3, &DMA_InitStruct);
// 绑定定时器事件到DMA
TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); // 启用SPI的DMA请求
}
性能对比:DMA传输速度可达阻塞方式的5倍以上,且CPU可并行处理其他任务!
2. SPI自环测试:硬件验证的终极手段
// 将SPI1(主)与SPI2(从)内部短接
void SPI_Loopback_Test(void) {
// 配置SPI2为从机(同模式!)
SPI_InitStruct.SPI_Mode = SPI_Mode_Slave;
SPI_Init(SPI2, &SPI_InitStruct);
// 主设备发送 → 从设备接收
SPI_I2S_SendData(SPI1, 0xAA);
while(!SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE));
uint8_t recv = SPI_I2S_ReceiveData(SPI2);
// 验证数据一致性
if(recv == 0xAA)
printf("Loopback test PASS!\n");
}
⚖️ 五、SPI的荣耀与短板(协议选型指南)
| 优点 | 缺点 | 应对方案 |
|---|---|---|
| 全双工高速(>10Mbps) | 无硬件流控 | 增加ACK/NACK软件协议 |
| 无设备地址(简化寻址) | 多从机需多CS → 引脚占用多 | 使用译码器(如74HC138) |
| 低延迟(实时性强) | 无错误重传机制 | CRC校验 + 超时重发 |
| 时序简单(易软件模拟) | 主从时钟需同步 → 距离受限 | 增加缓冲器延长传输距离 |
💎 核心结论:SPI是短距离板级通信的王者,但不适合长距离或多节点复杂网络!
提示:合理分配SPI时钟相位,避免多从机模式冲突!期待在评论区看到你的设计方案~
标签:#STM32实战 #SPI协议精解 #嵌入式通信 #SPI库函数指南 #W25Q开发

531

被折叠的 条评论
为什么被折叠?



