文章目录
- 前言
- 介绍SPI外设
- SPI框图
- 简化框图
- 时序
- 主模式全双工连续传输
- 非连续传输
- 软硬件波形对比
- 硬件SPI读写W25Q64
- 接线图
- 代码规划
- 代码实现
前言
本文介绍STM32中自带的SPI外设,在大容量产品和互联型产品上,SPI接口可以配置为支持SPI协议或者支持I2S音频协议。SPI接口默认工作在SPI方式,可以通过软件把功能从SPI模式切换到I2S模式。
在小容量和中容量产品上,不支持I2S音频协议。
串行外设接口(SPI)允许芯片与外部设备以半/全双工、同步、串行方式通信。此接口可以被配置成主模式,并为外部从设备提供通信时钟(SCK)。接口还能以多主配置方式工作。它可用于多种用途,包括使用一条双向数据线的双线单工同步传输,还可使用CRC校验的可靠通信。
软件SPI读写W25Q64、SPI详解可见此专栏。
介绍SPI外设
常用8位数据帧,高位先行。
外设总线APB1、APB2的频率不一样(APB1 36MHz、APB2 72MHz),所以SPI1与SPI2的频率也不一样。SPI1挂载于APB2上,SPI2挂载于APB1上。
注意:选择时钟频率时分频系数是必选的,即SPI1的最大频率等于72MHz/2为36MHz。
I2S为数字音频传输协议,是用于把音乐信号发送到DAC的协议。
SPI框图
此框图移位寄存器是低位先行,移出到MOSI,从MISO移入 。
当此SPI做从机时,就走MOSI、MISO交叉的路线。
NSS用于多主机模型,由多个设备连接NSS,当NSS配置为输出时,输出低电平,这些设备都变为从机,当NSS配置为输入时,主机拉低NSS,此SPI和其他连接NSS的设备就不能变为主机 由SPI_CR2的SSOE控制NSS输出还是输入。
波特发生器用于产生时钟。
主机从地址和数据总线写入数据到发送缓冲区,然后经过移位寄存器完成数据交换,当移位寄存器还没有数据时,发送缓存区传入数据后会置状态寄存器的TXE为1,表示发送寄存器(缓冲区)为空,此时可以提前准备下一个数据的写入,同时移位寄存器也会自动交换一个数据,然后数据进入接收缓冲区,此时状态寄存器的RXNE=1,表示接收寄存器(缓冲区)非空,在RXNE=1时,要尽快把接收缓冲区的数据读出来,反复执行即可完成连续的数据流。
SPI_CR1寄存器描述如下
简化框图
最主要的结构部分,方便更清晰的理解实现过程。
时序
主模式全双工连续传输
传输效率高,提前写入数据。
非连续传输
效率低,没有提前写入,比较好操作,但是频率很快时,由于没有提前写入数据导致的等待时间会较大的影响传输速率。
例如256分频时,看不出字节间的间隙。
128分频时,有较小间隙。
64分频时,间隙逐渐变大。
软硬件波形对比
硬件SPI读写W25Q64
接线图
代码规划
使用ThisSPI.c来用于书写SPI基本时序单元的内容,然后使用ThisW25Q64.c来基于ThisSPI.c来调用这些SPI基本时序单元来完成读写的功能,以下头文件声明省略。
代码实现
ThisSPI.c
#include "stm32f10x.h" // Device header// 封装写入CS位的函数
void ThisSPI_W_CS(uint8_t BitValue){GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}void ThisSPI_Init(void){// 初始化引脚配置RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);// CSGPIO_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);// CLK、MOSIGPIO_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);// MISOGPIO_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);// 初始化SPI外设SPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 分频系数SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 0SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 0SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC校验,暂时不用SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 八位数据帧SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 暂时不使用SPI_Init(SPI1,&SPI_InitStructure);SPI_Cmd(SPI1,ENABLE);ThisSPI_W_CS(1);
}
/*** @brief SPI起始时序单元* @param 无* @retval 无*/
void ThisSPI_Start(void){ThisSPI_W_CS(0);
}
/*** @brief SPI结束时序单元* @param 无* @retval 无*/
void ThisSPI_End(void){ThisSPI_W_CS(1);
}
/*** @brief SPI交换字节* @param Byte 字节内容* @retval 接收的数据*/
uint8_t ThisSPI_SwapByte(uint8_t Byte){// 等待TXE=1while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);// 写入发送寄存器,同时会清除TXE标志SPI_I2S_SendData(SPI1,Byte);// 等待RXNE=1while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);// 读入接收寄存器,同时会清除RXNE标志return SPI_I2S_ReceiveData(SPI1);
}
ThisW25Q64.C
#include "stm32f10x.h" // Device header
#include "ThisSPI.h"#define JEDEC_ID 0X9F
#define Write_Enable 0X06
#define Read_Status_Register_1 0X05
#define Page_Program 0X02
#define Sector_Erase 0X20
#define Read_Data 0X03// 初始化
void ThisW25Q64_Init(void){ThisSPI_Init();}
// 参考指令集来完成SPI时序单元的拼接
/*** @brief 读取产品ID* @param MID 产品ID,DID 设备ID* @retval 无*/
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID){ThisSPI_Start();ThisSPI_SwapByte(JEDEC_ID);*MID = ThisSPI_SwapByte(0XFF);*DID = ThisSPI_SwapByte(0XFF);*DID <<= 8;*DID |= ThisSPI_SwapByte(0XFF);ThisSPI_End();
}
/*** @brief 写使能* @param 无* @retval 无*/
void W25Q64_WriteEnable(void){ThisSPI_Start();ThisSPI_SwapByte(Write_Enable);ThisSPI_End();
}
/*** @brief 等待写入完成(通过状态寄存器1的BUSY位)* @param 无* @retval 无*/
void W25Q64_WaitBUSY(void){uint32_t Timeout = 10000;ThisSPI_Start();ThisSPI_SwapByte(Read_Status_Register_1);while((ThisSPI_SwapByte(0XFF) & 0x01) == 0x01){Timeout--;if(Timeout == 0) break;}ThisSPI_End();
}
/*** @brief 页写入* @param Address,页地址* @param DataArr,写入的数据* @param Count,一次写入的大小* @retval 无*/
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArr,uint16_t Count){uint8_t i;W25Q64_WriteEnable();ThisSPI_Start();ThisSPI_SwapByte(Page_Program);ThisSPI_SwapByte(Address>>16);ThisSPI_SwapByte(Address>>8);ThisSPI_SwapByte(Address);for(i=0;i<Count;i++){ThisSPI_SwapByte(DataArr[i]);}ThisSPI_End();W25Q64_WaitBUSY();
}
/*** @brief 扇区擦除* @param Address,页地址* @retval 无*/
void W25Q64_SectorErase(uint32_t Address){W25Q64_WriteEnable();ThisSPI_Start();ThisSPI_SwapByte(Sector_Erase);ThisSPI_SwapByte(Address>>16);ThisSPI_SwapByte(Address>>8);ThisSPI_SwapByte(Address);ThisSPI_End();W25Q64_WaitBUSY();
}
/*** @brief 读取页内数据* @param Address,页地址* @param DataArr,用于存取读取的数据* @param Count,读取数据的大小* @retval 无*/
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArr,uint32_t Count){uint32_t i;ThisSPI_Start();ThisSPI_SwapByte(Read_Data);ThisSPI_SwapByte(Address>>16);ThisSPI_SwapByte(Address>>8);ThisSPI_SwapByte(Address);for(i=0;i<Count;i++){DataArr[i] = ThisSPI_SwapByte(0XFF);}ThisSPI_End();
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "ThisW25Q64.h"uint8_t MID;
uint16_t DID;
uint8_t ArrWrite[] = {0x11,0x92,0x73,0x24};
uint8_t ArrRead[4];int main(void)
{OLED_Init();OLED_ShowString(1,1,"MID:");OLED_ShowString(1,8,"DID:");ThisW25Q64_Init();W25Q64_ReadID(&MID,&DID);OLED_ShowHexNum(1,5,MID,2);OLED_ShowHexNum(1,12,DID,4);OLED_ShowString(2,1,"W:");OLED_ShowString(3,1,"R:");W25Q64_SectorErase(0X000000);W25Q64_PageProgram(0X000000,ArrWrite,4);W25Q64_ReadData(0X000000,ArrRead,4);OLED_ShowHexNum(2,3,ArrWrite[0],2);OLED_ShowHexNum(2,5,ArrWrite[1],2);OLED_ShowHexNum(2,7,ArrWrite[2],2);OLED_ShowHexNum(2,9,ArrWrite[3],2);OLED_ShowHexNum(3,3,ArrRead[0],2);OLED_ShowHexNum(3,5,ArrRead[1],2);OLED_ShowHexNum(3,7,ArrRead[2],2);OLED_ShowHexNum(3,9,ArrRead[3],2);while (1){}
}