SPI通讯协议
1. 项目: SPI读写串行FLASH。
2. SPI协议:SPI协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在ADC、LCD等设备与MCU间,要求通讯速率较高的场合。
3. SPI物理层特点
SS: (NSS, CS) Slave Select, 从设备选择信号线,也称片选信号线。置低即为选中,一般都是软件置低(即GPIO口输出低电平)。
MOSI: Master Output Slave Input,主机输出从机输入。
MISO: Master Input Slave Output,主机输入从机输出。
SCK: Serial Clock,时钟信号线,用于数据同步。决定通讯速率,一般取决于低速设备。
4. 4种SPI模式
SPI 一共有四种通讯模式,它们的主要区别是总线空闲时 SCK 的时钟状态以及数据采样时刻。为方便说明,在此引入“时钟极性 CPOL”和“时钟相位 CPHA”的概念。
5. 协议层
SPI协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。
①处NSS由高变低,是SPI通讯的起始信号。
⑥处NSS由低变高,是SPI通讯的结束信号。
②处SCK上升沿数据变化,为触发时刻。
③处SCK下降沿数据稳定,为采样时刻。
奇数边沿采样时序
- CPOL决定SCK空闲时电平状态,CPOL=0, SCK为低电平;CPOL=1, SCK为高电平。
- CPHA决定MOSI和MISO的采样时刻,CPHA=0, 奇数边沿采样;CPHA=1, 偶数边沿采样。
6. stm32 SPI架构
- 通讯引脚
- 时钟控制逻辑
其中的fpclk频率是指SPI所在的APB总线频率,APB1为fpclk1,APB2为fpckl2。
- 数据控制逻辑
SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源来源于接收缓冲区及发送缓冲区。
通过写SPI的“数据寄存器DR”把数据填充到发送缓冲区中。
通过读“数据寄存器DR”,可以获取接收缓冲区中的内容。
其中数据帧长度可以通过“控制寄存器CR1”的“DFF位”配置成8位及16位模式;
配置“LSBFIRST位”可选择MSB先行还是LSB先行。
- 整体控制逻辑
整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等。
在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,只要读取状态寄存器相关的寄存器位,就可以了解SPI的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。
实际应用中,一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
7. 通讯时序
控制NSS信号线,产生起始信号(图中没有画出);
把要发送的数据写入到“数据寄存器DR”中,该数据会被存储到发送缓冲区;
通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去;MISO则把数据一位一位地存储进接收缓冲区中;
当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置1,表示传输完一帧,接收缓冲区非空;
等待到“TXE标志位”为1时,若还要继续发送数据,则再次往“数据寄存器DR”写入数据即可;等待到“RXNE标志位”为1时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。
假如使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发“数据寄存器DR”中的数据。
8. 代码
- bsp_spi_flash.h
#ifndef bsp_spi_flash_h #define bsp_spi_flash_h #include "stm32f10x.h" #define USE_BD 0 /*********************************SPI参数定义*******************************************/ #define FLASH_SPIx SPI1 /*定义SPI模式宏,以便切换不同SPI模式*/ #define FLASH_SPI_APBxClock_FUN RCC_APB2PeriphClockCmd /*定义通讯总线时钟宏,因为SPI1挂载在APB2总线上,而SPI2/SPI3挂载在APB1总线上的*/ #define FLASH_SPI_CLK RCC_APB2Periph_SPI1 /*定义SPI时钟宏,以方便SPI2/SPI3调用*/ #define FLASH_SPI_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd /*定义GPIO时钟宏,每条信号线挂载在不同GPIO端口上*/ #define FLASH_SPI_SCK_PORT GPIOA #define FLASH_SPI_SCK_PIN GPIO_Pin_5 #define FLASH_SPI_MOSI_PORT GPIOA #define FLASH_SPI_MOSI_PIN GPIO_Pin_7 #define FLASH_SPI_MISO_PORT GPIOA #define FLASH_SPI_MISO_PIN GPIO_Pin_6 #if (USE_BD ==1) /*霸道开发板*/ #define FLASH_SPI_GPIO_CLK RCC_APB2Periph_GPIOA #define FLASH_SPI_CS_PORT GPIOA #define FLASH_SPI_CS_PIN GPIO_Pin_4 #else /*指南者开发板*/ #define FLASH_SPI_GPIO_CLK (RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC) #define FLASH_SPI_CS_PORT GPIOC #define FLASH_SPI_CS_PIN GPIO_Pin_4 #endif //CS引脚配置 #define FLASH_SPI_CS_HIGH GPIO_SetBits(FLASH_SPI_CS_PORT,FLASH_SPI_CS_PIN); #define FLASH_SPI_CS_LOW GPIO_ResetBits(FLASH_SPI_CS_PORT,FLASH_SPI_CS_PIN); //等待超时时间 #define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000) #define SPIT_LONG_TIMEOUT ((uint32_t)(10*SPIT_FLAG_TIMEOUT)) //信息输出 #define FLASH_DEBUG_ON 0 #define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg) #define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg) #define FLASH_DEBUG(fmt,arg...) do{\ if(FLASH_DEBUG_ON)\ printf("<<-FLASH-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\ }while(0) #define DUMMY 0x00 #define READ_JEDEC_ID 0x9f #define ERASE_SECTOR 0x20 #define READ_STATUS 0x05 #define READ_DATA 0x03 #define WRITE_ENABLE 0x06 #define WRITE_DATA 0x02 void SPI_FLASH_Init(void); uint32_t SPI_Read_ID(void); void SPI_Erase_Sector(uint32_t addr); void SPI_Read_Data(uint32_t addr,uint8_t *readBuff,uint32_t numByteToRead); void SPI_Write_Data(uint32_t addr,uint8_t *writeBuff,uint32_t numByteToWrite); void SPI_WaitForWriteEnd(void); #endif /*bsp_spi_flash_h*/
- bsp_spi_flash.c
#include "bsp_spi_flash.h" #include "bsp_usart.h" static __IO uint32_t SPITimeout = SPIT_LONG_TIMEOUT; static uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode); /*初始化SPI的GPIO*/ void SPI_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /*使能与SPI有关的时钟*/ FLASH_SPI_APBxClock_FUN(FLASH_SPI_CLK,ENABLE); /*初始化通讯使用的端口时钟*/ FLASH_SPI_GPIO_APBxClock_FUN(FLASH_SPI_GPIO_CLK,ENABLE); /*MOSI,MISO,SCK*/ GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(FLASH_SPI_SCK_PORT,&GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(FLASH_SPI_MOSI_PORT,&GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(FLASH_SPI_MISO_PORT,&GPIO_InitStructure); /*初始化CS引脚,使用软件控制,所以直接设置成推挽输出*/ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(FLASH_SPI_CS_PORT,&GPIO_InitStructure); FLASH_SPI_CS_HIGH; } /*SPI工作模式配置*/ static void SPI_Mode_config(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CRCPolynomial = 0; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_Init(FLASH_SPIx,&SPI_InitStructure); /*写入配置到寄存器*/ SPI_Cmd(FLASH_SPIx,ENABLE); /*使能SPI*/ } /*SPI初始化*/ void SPI_FLASH_Init(void) { SPI_GPIO_Config(); SPI_Mode_config(); } /*发送一个字节*/ uint8_t SPI_FLASH_Send_Byte(uint8_t data) { SPITimeout = SPIT_FLAG_TIMEOUT; /*检查并等待至TX缓冲区为空*/ while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_TXE) == RESET); { if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0); } /*程序执行到此处,TX缓冲区已空*/ SPI_I2S_SendData(FLASH_SPIx,data); /*检查并等待至RX缓冲区为非空*/ while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_RXNE) == RESET); { if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0); } /*程序执行到此处,说明数据发送完毕,并接受到一字字节*/ return SPI_I2S_ReceiveData(FLASH_SPIx); } /*读取ID号*/ uint32_t SPI_Read_ID(void) { uint32_t flash_id; /*片选使能*/ FLASH_SPI_CS_LOW; SPI_FLASH_Send_Byte(READ_JEDEC_ID); /*0x9f*/ flash_id = SPI_FLASH_Send_Byte(DUMMY); flash_id <<= 8; /*左移8位*/ flash_id |= SPI_FLASH_Send_Byte(DUMMY); flash_id <<= 8; flash_id |= SPI_FLASH_Send_Byte(DUMMY); FLASH_SPI_CS_HIGH; return flash_id; } /*FLASH写入使能*/ void SPI_Write_Enable(void) { /*片选使能*/ FLASH_SPI_CS_LOW; SPI_FLASH_Send_Byte(WRITE_ENABLE); FLASH_SPI_CS_HIGH; } /*擦除FLASH指定扇区*/ void SPI_Erase_Sector(uint32_t addr) { SPI_Write_Enable(); /*片选使能*/ FLASH_SPI_CS_LOW; SPI_FLASH_Send_Byte(ERASE_SECTOR); SPI_FLASH_Send_Byte((addr>>16)&0xff); SPI_FLASH_Send_Byte((addr>>8)&0xff); SPI_FLASH_Send_Byte(addr&0xff); FLASH_SPI_CS_HIGH; SPI_WaitForWriteEnd(); } /*读取FLASH的内容*/ void SPI_Read_Data(uint32_t addr,uint8_t *readBuff, uint32_t numByteToRead) { /*片选使能*/ FLASH_SPI_CS_LOW; SPI_FLASH_Send_Byte(READ_DATA); SPI_FLASH_Send_Byte((addr>>16)&0xff); SPI_FLASH_Send_Byte((addr>>8)&0xff); SPI_FLASH_Send_Byte(addr&0xff); while(numByteToRead--) { *readBuff = SPI_FLASH_Send_Byte(DUMMY); readBuff++; } FLASH_SPI_CS_HIGH; } //向FLASH写入内容 //读取FLASH的内容 void SPI_Write_Data(uint32_t addr,uint8_t *writeBuff,uint32_t numByteToWrite) { SPI_Write_Enable(); //片选使能 FLASH_SPI_CS_LOW; SPI_FLASH_Send_Byte(WRITE_DATA); SPI_FLASH_Send_Byte((addr>>16)&0xff); SPI_FLASH_Send_Byte((addr>>8)&0xff); SPI_FLASH_Send_Byte(addr&0xff); while(numByteToWrite--) { SPI_FLASH_Send_Byte(*writeBuff); writeBuff++; } FLASH_SPI_CS_HIGH; SPI_WaitForWriteEnd(); } //等待FLASH内部时序操作完成 void SPI_WaitForWriteEnd(void) { uint8_t status_reg = 0; //片选使能 FLASH_SPI_CS_LOW; SPI_FLASH_Send_Byte(READ_STATUS); do { status_reg = SPI_FLASH_Send_Byte(DUMMY); } while((status_reg & 0x01) == 1); FLASH_SPI_CS_HIGH; } /*错误代码,可以用来定位是哪个环节出错。返回0,表示SPI读取失败。检测超时提示报错函数*/ static uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode) { /*block communication and all process*/ FLASH_ERROR("SPI等待超时!errorCode = %d",errorCode); return 0; }
读取JEDEC ID时序
-
For compatibility reasons, the W25Q64BV provides several instructions to electronically determine the identity of the device. The Read JEDEC ID instruction is compatible with the JEDEC standard for SPI compatible serial memories that was adopted in 2003. The instruction is initiated by driving the /CS pin low and shifting the instruction code “9Fh”. The JEDEC assigned Manufacturer ID byte for Winbond (EFh) and two Device ID bytes, Memory Type (ID15-ID8) and Capacity (ID7-ID0) are then shifted out on the falling edge of CLK with most significant bit (MSB) first as shown in figure 29. For memory type and capacity values refer to Manufacturer and Device Identification table.
-
DI(MOSI)和DO(MISO)同时进行的,主机发送dummy字节同时读取FLASH(从机)缓冲区里的内容,通过移位寄存器一位一位的读取出来。
- main.c
#include "stm32f10x.h" // 相当于51单片机中的 #include <reg51.h> #include "bsp_led.h" #include "bsp_spi_flash.h" #include "bsp_usart.h" #include <string.h> uint8_t readBuff[4096]; uint8_t writeBuff[4096]; int main(void) { uint32_t id; uint16_t i; LED_GPIO_Config(); /*串口初始化*/ USART_Config(); printf("\r\n这是一个SPI-FLASH读写测试例程\r\n"); SPI_FLASH_Init(); id = SPI_Read_ID(); printf("\r\n id= 0x%x \r\n",id); // SPI_Erase_Sector(0); // for(i=0;i<25;i++) // { // writeBuff[i]=i+25; // } // SPI_Write_Data(0,writeBuff,25); // // SPI_Read_Data(0,readBuff,4096); // // for(i=0;i<4096;i++) // { // printf("0x%x",readBuff[i]); // if(i%10==0) // printf("\r\n"); // } while(1) { } }
9. 执行结果
10. FLASH存储特性
- 写入前必须先擦除
- 擦除时会把数据位全部重置为1
- 擦除时必须按照最小单位来擦除(一般扇区)
norflash可以一个字节写入,nandflash必须以块或扇区为单位进行读写。