STM32 SPI 通信实战|高速通信、片选、数据收发

摘要

SPI 是嵌入式里最常用的高速同步串行总线,Flash、OLED、AD 芯片、射频模块基本都用 SPI。很多人写 SPI 只简单实现读写,忽略片选时序、时钟极性相位 CPOL/CPHA、总线冲突,项目调试时频繁出现读不出数据、丢字节、偶尔通信不稳定问题。本文基于 STM32F103,采用标准库 + HAL 库两套极简代码,结合板载 W25Q64 存储芯片项目实战,讲解 SPI 主机通信、软件片选、数据收发逻辑,同时梳理工程高频踩坑点。

一、SPI 核心原理(工程精简版)

SPI 为 4 线同步总线:SCLK 时钟、MOSI 主机发、MISO 主机收、CS 片选。

  • 主机输出时钟,从机跟随时钟同步收发;SPI 是全双工,发数据的同时也在接收数据,这点和串口单向收发有本质区别
  • CPOL:时钟空闲电平;CPHA:数据采样边沿,两个参数必须和从机芯片手册保持一致,否则通信直接乱码
  • 片选 CS:低电平选中从机,通信前拉低,一帧数据收发完成再拉高,严禁全程拉低

项目选型提醒:低速场景可用 GPIO 软件模拟 SPI,简单好调试;1MHz 以上高速通信,优先硬件 SPI,CPU 占用更低、稳定性更强。本文讲解硬件 SPI 主机。

二、硬件接线与 PCB 设计要点

以 STM32F103 SPI1 为例(APB2 最高 36MHz)

  • PA5: SCK,PA7:MOSI,PA6:MISO,PA4:CS(普通 GPIO 软件片选,不用硬件 NSS)
  1. 硬件 SPI 必须使用对应复用引脚,不能随便分配普通 IO
  2. CS 使用独立 GPIO 软件控制,工程里比硬件 NSS 更灵活,支持多从机扩展
  3. 长走线场景,SCLK、MOSI、MISO 串 100Ω 电阻抑制信号反射;电源引脚就近放置 0.1μF 退耦电容
  4. 所有器件共地;高速 SPI 布线尽量短,SCLK 走线尽量远离模拟信号线,防止干扰

三、标准库精简代码(SPI1 主机,软件片选)

#include "stm32f10x.h"

#define SPI_CS_PIN GPIO_Pin_4

#define SPI_CS_PORT GPIOA

//片选控制宏

#define SPI_CS_L() GPIO_ResetBits(SPI_CS_PORT,SPI_CS_PIN)

#define SPI_CS_H() GPIO_SetBits(SPI_CS_PORT,SPI_CS_PIN)

//SPI1初始化,CPOL=0,CPHA=0,波特率分频4,9M

void SPI1_Init(void)

{

GPIO_InitTypeDef GPIO_InitStruct;

SPI_InitTypeDef SPI_InitStruct;

复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

//SCK MOSI 复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//MISO 浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//CS 普通推挽输出,默认高
GPIO_InitStruct.GPIO_Pin = SPI_CS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
SPI_CS_H();

SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI1,&SPI_InitStruct);
SPI_Cmd(SPI1,ENABLE);

}

//SPI读写1字节:SPI特性,发送同时接收

uint8_t SPI_RW_Byte(uint8_t dat)

{

while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);

SPI_I2S_SendData(SPI1,dat);

while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);

return SPI_I2S_ReceiveData(SPI1);

}

//连续多字节发送

void SPI_Write_Buf(uint8_t *buf,uint16_t len)

{

uint16_t i;

SPI_CS_L();

for(i=0;i<len;i++)

{

SPI_RW_Byte(bufi);

}

SPI_CS_H();

}

//连续多字节读取

void SPI_Read_Buf(uint8_t *buf,uint16_t len)

{

uint16_t i;

SPI_CS_L();

for(i=0;i<len;i++)

{

bufi=SPI_RW_Byte(0xFF); //主机发0xFF,产生时钟,读取从机数据

}

SPI_CS_H();

}

四、HAL 库精简版本

#include "stm32f1xx_hal.h"

SPI_HandleTypeDef hspi1;

#define SPI_CS_L() HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET)

#define SPI_CS_H() HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET)

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;

HAL_SPI_Init(&hspi1);

}

//读写单字节

uint8_t SPI_RW_Byte(uint8_t txDat)

{

uint8_t rxDat;

HAL_SPI_TransmitReceive(&hspi1,&txDat,&rxDat,1,100);

return rxDat;

}

//多字节写

void SPI_Write_Buf(uint8_t *buf,uint16_t len)

{

SPI_CS_L();

HAL_SPI_Transmit(&hspi1,buf,len,100);

SPI_CS_H();

}

//多字节读

void SPI_Read_Buf(uint8_t *buf,uint16_t len)

{

SPI_CS_L();

HAL_SPI_Receive(&hspi1,buf,len,100);

SPI_CS_H();

}

五、项目实战:W25Q64 简单读写调用示例

//读取W25Q64ID,测试SPI通信

uint32_t W25Q_ReadID(void)

{

uint8_t buf3={0x90,0x00,0x00};

uint8_t id2;

SPI_CS_L();

SPI_RW_Byte(0x90);

SPI_RW_Byte(0x00);

SPI_RW_Byte(0x00);

id0=SPI_RW_Byte(0xFF);

id1=SPI_RW_Byte(0xFF);

SPI_CS_H();

return (id0<<8)|id1;

}

六、工程高频坑点(项目实测)

  1. CPOL、CPHA 参数不匹配
    最常见问题,必须严格对照从机 datasheet,参数错直接读乱码。W25Q 系列是 CPOL=0,CPHA=0;部分 OLED 屏是 CPOL=0,CPHA=1。
  2. 片选时序错误
    不要提前拉高 CS,必须等一整包数据收发完成再释放;不要多个从机共用 CS,多设备需要独立 CS 引脚。
  3. 读数据时不知道发送 0xFF
    SPI 全双工,读取从机数据,主机必须发送 0xFF 来输出 SCLK 时钟,没有时钟从机不会输出数据。
  4. 通信偶尔出错
    原因:速率太高,信号反射。降低 SPI 波特率,增加串阻;检查电源、地线,增加退耦电容。
  5. 复用引脚配置错误
    MOSI/SCK 要配置为复用推挽输出,MISO 是浮空输入;配置成普通 GPIO,硬件 SPI 无法工作。

七、总结

  1. SPI 全双工同步总线,硬件 SPI 适合高速场景,软件片选更适合多从机项目;
  2. 通信成败关键点:CPOL/CPHA、片选时序、引脚模式;
  3. 代码模块化,可直接移植到 Flash、OLED、高速 ADC 等外设;
  4. 调试优先降低 SPI 波特率排查,通信正常后再逐步提速。
相关推荐
POMAGTOR1 小时前
【5G设备POGOPIN连接器 高频弹簧针通信连接器件】
嵌入式硬件·5g·机器人
意法半导体STM321 小时前
【官方原创】LAT1709STM32C5硬件CRC值的计算话题
stm32·单片机·嵌入式硬件·mcu·crc·微控制器·stm32c5
新思维软件2 小时前
基于 STM32 的快递柜自动存取系统设计与实现(项目编号 X604041)
stm32·嵌入式硬件·物联网·物联网开发
xx~t2 小时前
嵌入式——ARM——汇编2
linux·汇编·arm开发·嵌入式硬件·启动代码
TST-魚爷2 小时前
会议摄像头电脑连接实操讲解(易视讯)
stm32·电脑·腾讯会议·zoom会议
M78佐菲2 小时前
ARM学习笔记(三)
linux·arm开发·笔记·嵌入式硬件·学习
wuyk5553 小时前
《WiFi 嵌入式物联网开发全套实战》| 第 12 章 Linux 系统 WiFi 常用命令全集:iw/iwlist/ip/wpa_cli 实战
linux·网络·stm32·物联网·tcp/ip
威嵌神州3 小时前
天脉 3 开发环境与 BSP 适配:IDE license、驱动开发、飞腾平台
ide·驱动开发·嵌入式硬件·fpga开发
辰域电子3 小时前
STM32项目开源:智能插座控制系统(代码 + 原理图 + 仿真)
stm32·单片机·嵌入式硬件·开源·51单片机·proteus