软件:STM32-F1系列-软硬件SPI介绍(2026/7/10)

软件:STM32-F1系列-软硬件SPI介绍(2026/7/10)
目录SPI高位先行1什么是spi2核心特点3. 四根线4. 工作原理5.四种工作模式模式0模式1模式2模式36.优缺点7.常见应用场景8.与UART和I2C的区别W25Q64芯片介绍1内存分块2写入操作3读取操作SPI协议配置1硬件引脚电平变化层①SPI写CS引脚电平②SPI写SCK引脚电平③SPI写MOSI引脚电平④SPI读MISO引脚电平2初始化配置3协议层①起始信号②停止信号③交换数据软件SPI读取W25Q641W25Q64写使能2W25Q64等待忙3W25Q64页编程4W25Q64扇区擦除5读取厂商ID和设备ID6W25Q64读取数据硬件SPI读取W25Q64模式选择1初始化2交换数据SPI高位先行1什么是spiSPISerial Peripheral Interface串行外设接口是一种高速、全双工、同步时钟的串行通信总线由摩托罗拉公司最早提出。它是目前微控制器MCU连接外部芯片最常用的方式之一广泛应用于嵌入式系统中,简单来说它就像是微控制器主机与各种外设从机之间的一条“高速公路”用于传输数据。2核心特点全双工通信数据可以同时发送和接收两根数据线。高速比 I2C、UART 快得多通常可达几十 MHz甚至上百 MHz。同步传输有时钟线SCK统一指挥收发双方步调一致。主从架构一个主机通常是MCU控制多个从机。硬件连接简单直接相比 UART 不需要复杂的波特率匹配相比 I2C 不需要仲裁机制。3. 四根线标准的 SPI 总线包含 4 根信号线SCK (Serial Clock)时钟线。由主机产生用来同步数据传输。MOSI (Master Out Slave In)主发从收。数据从主机发送到从机。MISO (Master In Slave Out)主收从发。数据从从机发送到主机。CS/SS (Chip Select/Slave Select)片选线。由主机控制拉低表示选中某个从机。每个从机都需要一根独立的 CS/SS/NSS 线。4. 工作原理选片主机拉低对应从机的 CS 引脚告诉它“我要和你说话了”。发时钟主机在 SCK 线上产生时钟脉冲。交换数据主机通过 MOSI 线发送一位数据同时从机通过 MISO 线返回一位数据。每产生一个时钟脉冲双方就交换一位数据。因为是环形总线发送和接收是同时进行的全双工。结束数据传输完成后主机拉高 CS 引脚释放从机。5.四种工作模式时钟极性 (CPOL) 和 时钟相位 (CPHA)这是 SPI 最容易让人困惑的地方。SPI 有 4种工作模式由 CPOL 和 CPHA 决定。重要提示通信前必须查阅从机芯片的数据手册Datasheet确认它支持哪种模式否则无法通信。模式0模式1模式2模式36.优缺点SPI 协议本身非常灵活它没有硬性规定一次通信即一帧数据必须传输多少位Bits。7.常见应用场景凡是需要高速传输数据的外设基本都用 SPI显示屏OLED、TFT LCD 屏幕刷图快。存储卡SD 卡部分模式、Flash 存储芯片W25Qxx。传感器高精度 ADC/DAC、部分 IMU惯性测量单元传感器。无线通信LoRa 模块、NRF24L01 无线模块。实时时钟 (RTC)部分时钟芯片。8.与UART和I2C的区别W25Q64芯片介绍1内存分块最小擦除单位为扇4KB最小写入单位为页256Byte块区64KB2写入操作3读取操作SPI协议配置1硬件引脚电平变化层①SPI写CS引脚电平void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue设置SS引脚的电平 }②SPI写SCK引脚电平void MySPI_W_SCK(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue设置SCK引脚的电平 }③SPI写MOSI引脚电平void MySPI_W_MOSI(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue设置MOSI引脚的电平BitValue要实现非0即1的特性 }④SPI读MISO引脚电平uint8_t MySPI_R_MISO(void) { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回 }2初始化配置void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //将PA4、PA5和PA7引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //将PA6引脚初始化为上拉输入 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 MySPI_W_SCK(0); //SCK默认低电平 }3协议层①起始信号void MySPI_Start(void) { MySPI_W_SS(0); //拉低SS开始时序 }②停止信号void MySPI_Stop(void) { MySPI_W_SS(1); //拉高SS终止时序 }③交换数据uint8_t MySPI_SwapByte(uint8_t ByteSend) { uint8_t i, ByteReceive 0x00; //定义接收的数据并赋初值0x00此处必须赋初值0x00后面会用到 for (i 0; i 8; i ) //循环8次依次交换每一位数据 { /*两个!可以对数据进行两次逻辑取反作用是把非0值统一转换为1即!!(0) 0!!(非0) 1*/ MySPI_W_MOSI(!!(ByteSend (0x80 i))); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线 MySPI_W_SCK(1); //拉高SCK上升沿移出数据 if (MySPI_R_MISO()) { ByteReceive | (0x80 i);//当MISO为1时置变量指定位为1当MISO为0时不做处理指定位为默认的初值0 } //读取MISO数据并存储到Byte变量 MySPI_W_SCK(0); //拉低SCK下降沿移入数据 } return ByteReceive; //返回接收到的一个字节数据 }软件SPI读取W25Q641W25Q64写使能void W25Q64_WriteEnable(void) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 MySPI_Stop(); //SPI终止 }2W25Q64等待忙void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 Timeout 100000; //给定超时计数时间 while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) 0x01) 0x01) //循环等待忙标志位 { Timeout --; //等待时计数值自减 if (Timeout 0) //自减到0后等待超时 { /*超时的错误处理代码可以添加到此处*/ break; //跳出等待不等了 } } MySPI_Stop(); //SPI终止 }3W25Q64页编程void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令 MySPI_SwapByte(Address 16); //交换发送地址23~16位 MySPI_SwapByte(Address 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i 0; i Count; i ) //循环Count次 { MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据 } MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 }4W25Q64扇区擦除void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address 16); //交换发送地址23~16位 MySPI_SwapByte(Address 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 }5读取厂商ID和设备IDvoid W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令 *MID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID通过输出参数返回 *DID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位 *DID 8; //高8位移到高位 *DID | MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位通过输出参数返回 MySPI_Stop(); //SPI终止 }6W25Q64读取数据void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 MySPI_SwapByte(Address 16); //交换发送地址23~16位 MySPI_SwapByte(Address 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i 0; i Count; i ) //循环Count次 { DataArray[i] MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据 } MySPI_Stop(); //SPI终止 }硬件SPI读取W25Q64模式选择连续传输CS一直保持有效低电平一直时钟一直收发。非连续传输每传一次数据CS都会释放拉高一次通信结束。区别在于CS片选是否中途释放。1初始化void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //将PA4引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //将PA5和PA7引脚初始化为复用推挽输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //将PA6引脚初始化为上拉输入 /*SPI初始化*/ SPI_InitTypeDef SPI_InitStructure; //定义结构体变量 SPI_InitStructure.SPI_Mode SPI_Mode_Master; //模式选择为SPI主模式 SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; //方向选择2线全双工 SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; //数据宽度选择为8位 SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; //先行位选择高位先行 SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_128; //波特率分频选择128分频 SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; //SPI极性选择低极性 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; //SPI相位选择第一个时钟边沿采样极性和相位决定选择SPI模式0 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; //NSS选择由软件控制 SPI_InitStructure.SPI_CRCPolynomial 7; //CRC多项式暂时用不到给默认值7 SPI_Init(SPI1, SPI_InitStructure); //将结构体变量交给SPI_Init配置SPI1 /*SPI使能*/ SPI_Cmd(SPI1, ENABLE); //使能SPI1开始运行 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 }2交换数据uint8_t MySPI_SwapByte(uint8_t ByteSend) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) ! SET); //轮询查询数据寄存器空空了再写入数据让它发 SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器开始产生时序 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ! SET); //轮询查询接收数据寄存器非空非空表示数据接受完全 return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回 }