SPI(Serial Peripheral Interface,串行外设接口)是由摩托罗拉(Motorola)在1980前后提出的一种全双工同步串行通信接口,它用于MCU与各种外围设备以串行方式进行通信以交换信息,通信速度最高可达25MHz以上。
SPI接口主要应用在EEPROM、FLASH、实时时钟、网络控制器、OLED显示驱动器、AD转换器,数字信号处理器、数字信号解码器等设备之间。
SPI通常由四条线组成,一条主设备输出与从设备输入(Master Output Slave Input,MOSI),一条主设备输入与从设备输出(Master Input Slave Output,MISO),一条时钟信号(Serial Clock,SCLK),一条从设备使能选择(Chip Select,CS)。与I²C类似,协议都比较简单,也可以使用GPIO模拟SPI时序。
SPI和I²C对比如表 21.1.1 所示。SPI可以同时发出和接收数据,因此SPI的理论传输速度比I²C更快。SPI通过片选引脚选择从机,一个片选一个从机,因此在多从机结构中,需要占用较多引脚,而I²C通过设备地址选择从机,只要设备地址不冲突,始终只需要两个引脚。
SPI可以一个主机连接单个或多个从机,每个从机都使用一个引脚进行片选,物理连接示意图如图21.1.1 和 图 21.1.2 所示。
数据交换
在SCK时钟周期的驱动下,MOSI和MISO同时进行,如图 21.1.3 所示,可以看作一个虚拟的环形拓扑结构。
主机和从机都有一个移位寄存器,主机移位寄存器数据经过MOSI将数据写入从机的移位寄存器,此时从机移位寄存器的数据也通过MISO传给了主机,实现了两个移位寄存器的数据交换。无论主机还是从机,发送和接收都是同时进行的,如同一个“环”。
如果主机只对从机进行写操作,主机只需忽略接收的从机数据即可。如果主机要读取从机数据,需要主机发送一个空数据来引发从机发送数据。
传输模式
SPI有四种传输模式,如表 21.1.2 所示,主要差别在于CPOL和CPHA的不同。
CPOL(Clock Polarity,时钟极性)表示SCK在空闲时为高电平还是低电平。当CPOL=0,SCK空闲时为低电平,当CPOL=1,SCK空闲时为高电平。
CPHA(Clock Phase,时钟相位)表示SCK在第几个时钟边缘采样数据。当CPHA=0,在SCK第一个边沿采样数据,当CPHA=1,在SCK第二个边沿采样数据。
如图 21.1.4 所示,CPHA=0时,表示在时钟第一个时钟边沿采样数据。当CPOL=1,即空闲时为高电平,从高电平变为低电平,第一个时钟边沿(下降沿)即进行采样。当CPOL=0,即空闲时为低电平,从低电平变为高电平,第一个时钟边沿(上升沿)即进行采样。
如图 21.1.5 所示,CPHA=1时,表示在时钟第二个时钟边沿采样数据。当CPOL=1,即空闲时为高电平,从高电平变为低电平再变为高电平,第二个时钟边沿(上升沿)即进行采样。当CPOL=0,即空闲时为低电平,从低电平变为高电平再变为低电平,第二个时钟边沿(下降沿)即进行采样。
有了以上基础知识,基本可以想象出如何使用GPIO模拟SPI通信时序。首先主机和从机都选择同一传输模式。然后主机片选拉低,选中从机。接着在时钟的驱动下,MOSI发送数据,同时MISO读取接收数据。最后完成传输,取消片选。
关于Flash,前面EEPROM章节,有过简单介绍。EEPROM和Flash的本质上是一样的,都用于保存数据,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板的W25Q64就是一颗SPI接口的外部Flash。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。
结构组成
Flash类型众多,其中比较常见是W25Qxx系列,从命名上看,W25Qxx中xx的单位是M Bit,如W25Q16,其存储容量为16M Bit。本开发板上的Flash型号为W25Q64,其存储容量为64M Bit。
对于W25Q64,每页大小为256 Byte。如图 21.1.6 所示,W25Q64由128块(Block)组成,每块由16扇 区(Sector)组成,每个扇区由16页(Page)组成,每一页由256个字节(Byte)组成,每个Byte由8位(Bit)组成,Bit为最小存储单位,存放1个0或1。
Flash有个物理特性:只能写0,不能写1。如果把Flash的每个Bit,都看作一张纸,bit=1表示纸没有内容,bit=0表示纸写入了内容。当纸为白纸时(bit=1),这时往纸上写东西是可以的,写完后纸的状态变为bit=0。当纸有内容时(bit=0),这时往纸上写东西只能让数据越乱,也就无法正常写数据。此时需要橡皮檫,进行擦除操作,将有内容的纸(bit=0)变为白纸(bit=1),使得以后可以重新写入数据。
因此,对Flash写数据前,通常需要擦除操作。对于W25Q64,数据擦除可以以Sector为单位也可以以Block为单位。数据写入只能按照Page来写入,也就一次最多只能写256个Byte。
读写、擦除操作
通过SPI向W25Q64发送指令,即可操作W25Q64,完整指令表参考配套资料里的《W25Q64JV.pdf》,部分指令如图 21.1.7 所示。第一列是功能说明,后续几列为SPI收/发的数据。
以读数据为例,时序如图 21.1.8 所示。首先拉低CS片选,在8个CLK时钟周期里,DI(从机Data Input,主机MOSI)发出指令0x03,在随后的24个CLK时钟周期里,发送24 Bit的地址,最后8个CLK里,DO(从机Data Output,主机MISO)收到8 Bit数据。
以页编程(写数据)为例,时序如图 21.1.9 所示。首先拉低CS片选,在8个CLK时钟周期里,DI(从机Data Input,主机MOSI)发出指令0x02,在随后的24个CLK时钟周期里,发送24 Bit的地址,最后256*8个CLK里,发送256 Byte数据。
以扇区擦除为例,时序如图 21.1.10 所示。首先拉低CS片选,在8个CLK时钟周期里,DI(从机Data Input,主机MOSI)发出指令0x20,在随后的24个CLK时钟周期里,发送24 Bit的地址。W25Q64会将所指定扇区的数据全设置为0x01。
理解了这三个指令的时序,其它指令大同小异,根据芯片手册指令表,可对W25Q64进行其它所需操作。
如图 21.2.1 为开发板Flash部分的原理图。U6为W25Q64芯片,1脚CS为片选引脚,拉低有效,6脚为SPI时钟CLK,2脚、3脚、4脚、5脚在不同的SPI工作方式下,具体不同的功能。
W25Q64支持三种SPI工作方式:Standard SPI、Dual SPI、Quad SPI。Standard SPI即标准SPI,在数据传输时,DI/DO分别负责收发,此时为全双工状态;Dual SPI即双线SPI,对于Flash外设,全双工效率反而不高,因此扩展了SPI用法,让其工作在半双工模式,DI/DO作为双向IO,加倍数据传输;Quad SPI即四线SPI,类似双线SPI的工作模式,此时再加两个IO,最高同时四个IO传输数据,再次加倍数据传输。
在标准SPI工作方式,DO用于输出数据,3脚为写保护引脚(Write Protect,WP),拉低则禁止修改W25Q64的寄存器,7脚为设备暂停引脚(HOLD),拉低暂停W25Q64的数据传输;在双线SPI工作方式,DO和DI都用于输出数据,WP和HOLD与标准SPI功能一致;在四线SPI工作方式,DO、DI、WP、HOLD都用于输出数据。不同工作方式下,引脚功能如表 21.2.1 所示。
如图 21.2.1 所示,使用不同的指令对W25Q64进行读,会有不同的效果。①为标准SPI读,8 Bits数据需要8个时钟周期;②为双线SPI读,8 Bits数据只需4个时钟周期;③为四线SPI读,8 Bits数据只需要2个时间周期。
本开发板硬件上只接了DI和DO,HOLD和WP都上拉处理,因此只支持Standard SPI、Dual SPI工作方式。本章重点讲解SPI时序,因此后面软件设计将使用Standard SPI进行讲解。
由原理图可知,SPI的四个引脚分别为PA5(SCK)、PA6(MISO)、PA7(MOSI)、PA4(CS0)。
实验目的:本实验通过GPIO模拟SPI总线时序,对Flash设备W25Q64进行读写操作。
本实验配套代码位于“5_程序源码\13_通信—模拟SPI\”。
代码段 21.3.1 SPI 硬件相关宏定义(driver_spi.h)
/************************* SPI 硬件相关定义 *************************/
#define SPIx SPI1
#define SPIx_CLK_ENABLE() __HAL_RCC_SPI1_CLK_ENABLE()
#define SPIx_SCK_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPIx_MISO_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPIx_MOSI_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define W25_CS_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPIx_FORCE_RESET() __HAL_RCC_SPI1_FORCE_RESET()
#define SPIx_RELEASE_RESET() __HAL_RCC_SPI1_RELEASE_RESET()
#define SPIx_SCK_PIN GPIO_PIN_5
#define SPIx_SCK_GPIO_PORT GPIOA
#define SPIx_MISO_PIN GPIO_PIN_6
#define SPIx_MISO_GPIO_PORT GPIOA
#define SPIx_MOSI_PIN GPIO_PIN_7
#define SPIx_MOSI_GPIO_PORT GPIOA
#define W25_CS_PIN GPIO_PIN_4
#define W25_CS_GPIO_PORT GPIOA
#define SPI_CLK(level) HAL_GPIO_WritePin(SPIx_SCK_GPIO_PORT, SPIx_SCK_PIN, level?GPIO_PIN_SET:GPIO_P
IN_RESET)
#define SPI_MISO() HAL_GPIO_ReadPin(SPIx_MISO_GPIO_PORT, SPIx_MISO_PIN)
#define SPI_MOSI(level) HAL_GPIO_WritePin(SPIx_MOSI_GPIO_PORT, SPIx_MOSI_PIN, level?GPIO_PIN_SET:GPIO
_PIN_RESET)
#define W25_CS(level) HAL_GPIO_WritePin(W25_CS_GPIO_PORT, W25_CS_PIN, level?GPIO_PIN_SET:GPIO_PIN_R
ESET)
/************************* SPI 硬件相关定义结束 *************************/
随后将四个GPIO引脚初始化,使能引脚时钟,设置输入/输出模式。SCK、MOSI、CS引脚,始终为输出模式,MISO引脚为数据输入引脚,始终为输入模式,如代码段所示。
代码段 21.3.2 SPI 硬件初始化(driver_spi.c)
/*
* 函数名:void SPI_Init(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:初始化 SPI 的四根引脚
*/
void SPI_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
SPIx_SCK_GPIO_CLK_ENABLE();
SPIx_MISO_GPIO_CLK_ENABLE();
SPIx_MOSI_GPIO_CLK_ENABLE();
W25_CS_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = SPIx_SCK_PIN | W25_CS_PIN | SPIx_MOSI_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(SPIx_SCK_GPIO_PORT, &GPIO_InitStruct); // SCK CS MOSI 为输出
GPIO_InitStruct.Pin = SPIx_MISO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(SPIx_MISO_GPIO_PORT, &GPIO_InitStruct); // MISO 为输入
W25_CS(1); // CS 初始化高
SPI_CLK(0); // CLK 初始化低
}
代码段 21.3.3 模拟 SPI 读/写一字节数据(driver_spi.c)
/*
* 函数名:void SPI_WriteByte(uint8_t data)
* 输入参数:data -> 要写的数据
* 输出参数:无
* 返回值:无
* 函数作用:模拟 SPI 写一个字节
*/
void SPI_WriteByte(uint8_t data)
{
uint8_t i = 0;
uint8_t temp = 0;
for(i=0; i<8; i++) {
temp = ((data&0x80)==0x80)? 1:0;
data = data<<1;
SPI_CLK(0); //CPOL=0
SPI_MOSI(temp);
SPI_Delay();
SPI_CLK(1); //CPHA=0
SPI_Delay(); }
SPI_CLK(0); }
/*
* 函数名:uint8_t SPI_ReadByte(void)
* 输入参数:
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读一个字节
*/
uint8_t SPI_ReadByte(void) {
uint8_t i = 0;
uint8_t read_data = 0xFF;
for(i=0; i<8; i++) {
read_data = read_data << 1;
SPI_CLK(0);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1) {
read_data = read_data + 1; } }
SPI_CLK(0);
return read_data;
}
前面提到SPI传输可以看作一个虚拟的环形拓扑结构,即输入和输出同时进行。在前面“SPI_WriteByte()”函数里,发送了1 Byte,也应该接收1 Byte,只是代码中忽略了接收引脚MISO的状态;在前面“SPI_ReadByte ()”函数里,接收了1 Byte,也应该发送1 Byte,只是代码中忽略了发送引脚MOSI的内容。有些场景,SPI需要同时读写,因此还需要编写SPI同时读写函数,如代码段 21.3.4 所示。
代码段 21.3.4 模拟 SPI 读写一字节数据(driver_spi.c)
/*
* 函数名:uint8_t SPI_WriteReadByte(uint8_t data)
* 输入参数:data -> 要写的一个字节数据
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读写一个字节
*/
uint8_t SPI_WriteReadByte(uint8_t data)
{
uint8_t i = 0;
uint8_t temp = 0;
uint8_t read_data = 0xFF;
for(i=0;i<8;i++) {
temp = ((data&0x80)==0x80)? 1:0;
data = data<<1;
read_data = read_data<<1;
SPI_CLK(0);
SPI_MOSI(temp);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1) {
read_data = read_data + 1; } }
SPI_CLK(0);
return read_data;
}
获取设备ID W25Q64主要有三个ID:制造商编号ID(Manufacturer)、设备ID(Device ID)、唯一标识ID(Unique ID)。
制造商编号ID表示制造商名字,Winbond生产的,该值MF[7:0]为0xEF。
设备ID包含两部分:ID[7:0]表示存储容量,W25Q64系列都为0x16;ID[15:0]表示芯片类型,为0x4017或0x7017。
唯一标识ID表示该Flash芯片唯一性,常用于加密。
这里使用0xAB指令获取设备ID[7:0],使用0x9F指令获取JEDEC ID(MF[7:0]+ ID[15:0]),如代码段 21.3.6所示。
代码段 21.3.6 获取 W25Q64 ID(driver_w25qxx.c)
// 函数重定义
#define W25_CS_ENABLE() {W25_CS(0); us_timer_delay(10);}
#define W25_CS_DISABLE() {W25_CS(1); us_timer_delay(10);}
#define W25_RW_Byte(data) SPI_WriteReadByte(data)
/*
* 函数名:uint32_t FLASH_ReadDeviceID(void)
* 输入参数:
* 输出参数:无
* 返回值:读到外部 FLASH 的设备 ID
* 函数作用:读外部 FLASH 的设备 ID
*/
uint32_t FLASH_ReadDeviceID(void) {
uint32_t temp[4];
W25_CS_ENABLE();
W25_RW_Byte(W25X_DeviceID);
temp[0] = W25_RW_Byte(Dummy_Byte);
temp[1] = W25_RW_Byte(Dummy_Byte);
temp[2] = W25_RW_Byte(Dummy_Byte);
temp[3] = W25_RW_Byte(Dummy_Byte); //deviceID
W25_CS_DISABLE();
return temp[3];
}
/*
* 函数名:uint32_t Flash_ReadFlashID(void)
* 输入参数:
* 输出参数:无
* 返回值:读到外部 FLASH 的芯片 ID
* 函数作用:读外部 FLASH 的芯片 ID
*/
uint32_t Flash_ReadFlashID(void) {
uint32_t temp[4];
W25_CS_ENABLE();
W25_RW_Byte(W25X_JedecDeviceID);
temp[0] = W25_RW_Byte(Dummy_Byte);
temp[1] = W25_RW_Byte(Dummy_Byte);
temp[2] = W25_RW_Byte(Dummy_Byte);
W25_CS_DISABLE();
temp[3] = (temp[0] << 16) | (temp[1] << 8) | temp[2];
return temp[3];
}
扇区擦除
参考芯片手册,得知先发送0x20指令,再发送24位的扇区地址,即可对该扇区进行擦除,如代码段 21.3.7所示。
代码段 21.3.7 W25Q64 扇区擦除(driver_w25qxx.c)
/*
* 函数名:void FLASH_SectorErase(uint32_t SectorAddr)
* 输入参数:SectorAddr -> 要擦的扇区地址
* 输出参数:无
* 返回值:无
* 函数作用:扇区擦除
*/
void FLASH_SectorErase(uint32_t SectorAddr)
{
Flash_WritenEN();
FLASH_WaitForWriteEnd();
W25_CS_ENABLE();
W25_RW_Byte(W25X_SectorErase);
W25_RW_Byte((SectorAddr & 0xFF0000) >> 16);
W25_RW_Byte((SectorAddr & 0xFF00) >> 8);
W25_RW_Byte(SectorAddr & 0xFF);
W25_CS_DISABLE();
FLASH_WaitForWriteEnd();
}
这里有一个等待Flash写完成函数“FLASH_WaitForWriteEnd()”,其内容如代码段 21.3.8 所示。
W25Q64有三个状态寄存器,其中状态寄存器1的bit[0]为擦除/写入状态标志位,如图 21.3.1 所示。如果该位为1,表示正在擦除或写数据,如果该位为0,表示可以对W25Q64进行擦除/写数据操作。
代码段 21.3.8 W25Q64 等待写完成(driver_w25qxx.c)
/*
* 函数名:static void FLASH_WaitForWriteEnd(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:等待写完成
*/
static void FLASH_WaitForWriteEnd(void) {
uint8_t flash_status = 0;
W25_CS_ENABLE();
W25_RW_Byte(W25X_ReadStatusReg);
do
{
flash_status = W25_RW_Byte(Dummy_Byte); }
while ((flash_status & WIP_Flag) == SET);
W25_CS_DISABLE(); }
页写W25Q64
W25Q64只能按页写数据,每页大小为256字节,因此一次最多写256字节,如代码段 21.3.9 所示。
代码段 21.3.9 页写 W25Q64(driver_w25qxx.c)
/*
* 函数名:void FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
* 输入参数:pBuffer -> 要写的数据指针; WriteAddr -> 要写的 FLASH 初始地址; NumByteToWrite -> 要写的字节个数
* 输出参数:无
* 返回值:无
* 函数作用:页写
*/
void FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
Flash_WritenEN();
W25_CS_ENABLE();
W25_RW_Byte(W25X_PageProgram);
W25_RW_Byte((WriteAddr & 0xFF0000) >> 16);
W25_RW_Byte((WriteAddr & 0xFF00) >> 8);
W25_RW_Byte(WriteAddr & 0xFF);
if(NumByteToWrite > SPI_FLASH_PerWritePageSize) {
NumByteToWrite = SPI_FLASH_PerWritePageSize; }
while (NumByteToWrite--) {
W25_RW_Byte(*pBuffer);
pBuffer++; }
W25_CS_DISABLE();
FLASH_WaitForWriteEnd(); }
读W25Q64
读取W25Q64没有什么限制,发送指令、地址后,正常读取即可,如代码段 21.3.10 所示。
代码段 21.3.10 读 W25Q64(driver_w25qxx.c)
/*
* 函数名:void FLASH_BufferRead(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite))
* 输入参数:pBuffer -> 要读的数据指针; WriteAddr -> 要读的 FLASH 初始地址; NumByteToWrite -> 要读的字节个数
* 输出参数:无
* 返回值:无
* 函数作用:读 N 个字节出来
*/
void FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
W25_CS_ENABLE();
W25_RW_Byte(W25X_ReadData);
W25_RW_Byte((ReadAddr & 0xFF0000) >> 16);
W25_RW_Byte((ReadAddr& 0xFF00) >> 8);
W25_RW_Byte(ReadAddr & 0xFF);
while (NumByteToRead--) {
*pBuffer = W25_RW_Byte(Dummy_Byte);
pBuffer++; }
W25_CS_DISABLE(); }
代码段 21.3.11 主函数控制逻辑(main.c)
// 初始化 SPI
SPI_Init();
// 读取外部 FLASH 的 ID
HAL_Delay(400);
device_id = FLASH_ReadDeviceID();
printf("-->读取的外部设备 ID: 0x%x\n\r", device_id);
flash_id = Flash_ReadFlashID();
printf("-->读取外部 FLASH ID: 0x%x\n\r", flash_id);
printf("-->实际外部 FLASH ID: 0x%x\n\r", FLASH_ID);
printf("\n\r");
if(flash_id != FLASH_ID)
{
printf("设备错误!\n\r");
Error_Handler(); }
while(1) {
if(rw_flag)
{
rw_flag = 0;
FLASH_SectorErase(FLASH_SECTOR_TO_ERASE);
printf("写入的数据:%s\n\r", tx_buffer);
FLASH_PageWrite(tx_buffer, FLASH_WRITEADDR, 256);
FLASH_BufferRead(rx_buffer, FLASH_READADDR, 256);
printf("读出的数据:%s\n\r", rx_buffer); } }
本实验对应配套资料的“5_程序源码\13_通信—模拟SPI\”。打开工程后,编译,下载,按下按键KEY1(KEY_U),即可看到串口如图 21.4.1 所示。
百问网技术论坛:
http://bbs.100ask.net/
百问网嵌入式视频官网:
https://www.100ask.net/index
百问网开发板:
淘宝:https://100ask.taobao.com/
天猫:https://weidongshan.tmall.com/
技术交流群2(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
QQ群:752871361
单片机-嵌入式Linux交流群:
QQ群:536785813