本文主要记录如何使用STMCubeMX快速部署SPI通信。
SPI是一种串行同步高速的全双工通信方式。
SPI的物理层一般由四条数据线组成:片选信号线CS/NSS,时钟信号线SCK,主写从读信号线MOSI,主读从写信号线MISO。
SPI的协议层主要包括起始信号、停止信号、数据有效性、通信模式四个部分。
与I2C通信不同,SPI的起始停止信号由片选信号线控制,而I2C由时钟线为高电平时数据线电平的变化来控制。
数据有效性部分,SPI在时钟信号电平变化时,即上升沿或者下降沿进行数据的触发(数据变化)或者采样(认定此时数据有效),前一个边沿触发,后一个边沿就进行采样,具体采样和触发谁先谁后要依据通信模式的配置;I2C相对要简单一些,在时钟线为高电平时数据有效,在低电平时进行数据电平变换。
对于通信模式,引入CPOL和CPHA的概念,CPOL和CPHA分别被称为时钟极性和时钟相位,CPOL为0表示SPI通信未开始时SCK时钟信号为低电平,为1则是高电平;CPHA为0表示数据采样时刻是SCK的奇数边沿,为1则是SCK的偶数边沿。
CPOL与CPHA两两组合形成4种模式,对于FLASH芯片支持模式0和模式3,即CPOL和CPHA都为0或者都为1的模式。这样一来,模式0就表示片选信号触发通信开始后,时钟线第一个上升沿进行数据采样,之后的下降沿数据触发,第二个上升沿再采样,依次类推。
而SPI没有应答信号的设置,也不需要在通信前先发送从机地址查找从机,用片选信号线就能解决。由于SPI是全双工通信,因此也不需要设置读写位。
FLASH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH存储器容量遍大于 EEPROM,现在基本取代了它的地位。我们生活中常用的 U 盘、SD卡、SSD 固态硬盘以及我们 STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存储器。在存储控制上,最主要的区别是 FLASH 芯片只能一大片一大片地擦写,而 EEPROM 可以单个字节擦写。
STM32f103VET6单片机使用的是型号为W25Q64的FLASH芯片,是一种使用 SPI 通讯协议的 NOR FLASH存储器(NOR Flash是一种非易失闪存技术,是Intel在1988年创建。是市场上两种主要的非易失闪存技术之一),存储容量64Mbit,即8MBytes。它的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制片选信号,对于STM32f103VET6单片机片选信号由PC0引脚控制。
FLASH芯片在接收到主机发来的信号时会解析信号内容,根据指令表执行不同的命令。
本次实验用到的W25Q64指令表如下(详细的可以查阅W25Q64芯片手册获取相关信息):
指令 | 指令编码 | 指令解释 |
---|---|---|
写使能 | 06h | 开启FLASH芯片的写入 |
写禁止 | 04h | 禁止FLASH芯片的写入 |
读状态寄存器 | 05h | 判断FLASH读取工作的状态 |
写状态寄存器 | 01h | 判断FLASH写入工作的状态 |
读 | 03h | 开始读取FLASH中数据 |
页写入 | 02h | 一次最多写入256个字节 |
扇区擦除 | 20h | 擦除一个扇区的数据(4KB) |
厂商设备信息 | 09h | 获取厂商和设备编号 |
1.新建工程,常规配置调试模式、时钟树、项目环境;
2.选择PB0、PB5、PC0配置为GPIO_Out,初始为高电平,PC0为片选信号;
3.选择SPI1,配置模式为全双工主模式,硬件片选关闭,软件片选打开,选择数据大小,MSB/LSB模式。
值得注意的是,分频系数选择4,虽然SPI1的最大波特率能取到36MBits/s,但这里是被限制了。
4.配置好USART1用于串口调试。
5.GENERATE CODE
#define CS_enable HAL_GPIO_WritePin(GPIOC,GPIO_PIN_0,GPIO_PIN_RESET) //片选信号使能
#define CS_disable HAL_GPIO_WritePin(GPIOC,GPIO_PIN_0,GPIO_PIN_SET) //片选信号失能
/***FLASH指令集***/
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_PageProgram 0x02
#define W25X_SectorErase 0x20
#define W25X_ManufactDeviceID 0x90
uint8_t devid[2];
uint8_t sendbuf[8] = {1,36,4,7,8,9,41,2};
uint8_t revbuff[8];
HAL库有自带函数,我们对它们进行封装方便后续使用。
写函数:
HAL_StatusTypeDef SPI_Write(uint8_t* pbuff, uint16_t size)
{
return HAL_SPI_Transmit(&hspi1, pbuff, size, 100);
}
读函数:
HAL_StatusTypeDef SPI_Read(uint8_t* pbuff, uint16_t size)
{
return HAL_SPI_Receive(&hspi1, pbuff, size, 100);
}
同时读写函数:注意这里的size是读写字节数总和,SPI读写可以同时进行,依据时序进行,因此size是时序中读写字节数的总和。
HAL_StatusTypeDef SPI_WriteAndRead(uint8_t *pwritebuff,uint8_t* preceivebuff,uint8_t size)
{
return HAL_SPI_TransmitReceive(&hspi1,pwritebuff,preceivebuff,size,100);
}
FLASH的读写速度有限,如果读写未完成是不会接收MCU的指令的,而读写状态寄存器可以一直访问,因此读取状态寄存器的busy位可以获取FLASH当前的读写状态。
下图分别为FLASH的状态寄存器结构和时序图:
uint8_t FLASH_WriteStatus(void)
{
uint8_t cmd[4] = {W25X_WriteStatusReg,0x00,0x00,0x00};
uint8_t busy;
CS_enable;
SPI_Write(cmd,4);
SPI_Read(&busy,1);
CS_disable;
return busy;
}
uint8_t FLASH_ReadStatus(void)
{
uint8_t cmd[4] = {W25X_ReadStatusReg,0x00,0x00,0x00};
uint8_t busy;
CS_enable;
SPI_Write(cmd,4);
SPI_Read(&busy,1);
CS_disable;
return busy;
}
设计等待函数,判断FLASH是否处于BUSY状态。使用等待函数时,要不要等待就看不同指令在不在一个时序里,每个时序开始前和结束后都检查一下。等待函数的位置很重要,因为等待函数本身也会进行SPI通信,如果不当会导致片选信号线重复开启,提早关闭。
void Wait_WriteBusy(void)
{
while((FLASH_WriteStatus()&0x01) == 0x01);
}
void Wait_ReadBusy(void)
{
while((FLASH_ReadStatus()&0x01) == 0x01);
}
FLASH写入是受保护的,需要输入指令允许写入。下图为写入使能的时序图,失能同理。
void FLASH_WriteEnable(void)
{
uint8_t cmd = W25X_WriteEnable;
Wait_WriteBusy();
CS_enable;
SPI_Write(&cmd,1);
CS_disable;
Wait_WriteBusy();
}
void FLASH_WriteDisable(void)
{
uint8_t cmd = W25X_WriteDisable;
Wait_WriteBusy();
CS_enable;
SPI_Write(&cmd,1);
CS_disable;
Wait_WriteBusy();
}
Flash的数据位可以由1变为0,但不能由0变为1。所以在向 Flash 写数据之前,必须要先进行擦除操作,把该扇区所有的数据变为 0xFF。擦除的最小单位是扇区。
扇区擦除的时序图如下:
void FLASH_SectorErase(uint32_t erase_add)
{
uint8_t cmd[4];
cmd[0] = W25X_SectorErase;
cmd[1] = (uint8_t)(erase_add >> 16);
cmd[2] = (uint8_t)(erase_add >> 8);
cmd[3] = (uint8_t)(erase_add);
FLASH_WriteEnable();
Wait_WriteBusy();
CS_enable;
SPI_Write(cmd,4);
CS_disable;
Wait_WriteBusy();
}
页缓冲区大小为256B,因此页写入最多写入256B,开启写使能后发送页写入指令开始写入。
与I2C类似, 在进行部分写的时候如果超过当前页的地址范围,就会回到该页开头覆盖写。
页写入时序图如下:
void FLASH_PageWrite(uint8_t *pbuff,uint32_t write_add,uint16_t size)
{
uint8_t cmd[4];
cmd[0] = W25X_PageProgram;
cmd[1] = (uint8_t)(write_add >> 16);
cmd[2] = (uint8_t)(write_add >> 8);
cmd[3] = (uint8_t)(write_add);
FLASH_WriteEnable();
Wait_WriteBusy();
CS_enable;
SPI_Write(cmd,4);
SPI_Write(pbuff,size);
CS_disable;
Wait_WriteBusy();
}
FLASH读取可以从指定地址后读取多个字节的数据。读取时序图如下:
void FLASH_Read(uint8_t *pbuff,uint32_t read_add,uint16_t size)
{
uint8_t cmd[4];
cmd[0] = W25X_ReadData;
cmd[1] = (uint8_t)(read_add >> 16);
cmd[2] = (uint8_t)(read_add >> 8);
cmd[3] = (uint8_t)(read_add);
Wait_ReadBusy();
Wait_WriteBusy();
CS_enable;
SPI_Write(cmd,4);
SPI_Read(pbuff,size);
CS_disable;
Wait_ReadBusy();
}
void FLASH_ManufacturerID(uint8_t* devbuf)
{
uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};
Wait_WriteBusy();
Wait_ReadBusy();
CS_enable;
SPI_Write(cmd,4);
SPI_Read(devbuf,2);
CS_disable;
Wait_ReadBusy();
}
为了方便打印输出,对printf进行重定向。注意使用时要勾选上keil里的微库use MicroLib。
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
编写完必要的函数后,设计实验测试STM32的FLASH读写过程。
/*
便于打印的函数
*/
void Byte_print(uint8_t* pbuff)
{
uint8_t i;
for(i=0;i<8;i++)
{
printf("0x%x",pbuff[i]);
printf("\t");
}
printf("\r\n");
}
/*打印厂商设备信息*/
printf("==test begin==\r\n");
FLASH_ManufacturerID(devid);
printf("%x",devid[0]);
printf("%x\r\n",devid[1]);
/*扇区擦除*/
printf("==erase==\r\n");
FLASH_SectorErase(0);
FLASH_Read(revbuff,0,8);
Byte_print(revbuff);
/*输入输出测试*/
printf("==write data==\r\n");
FLASH_PageWrite(sendbuf,0,8);
FLASH_Read(revbuff,0,8);
Byte_print(revbuff);
/*再次擦除*/
printf("==erase==\r\n");
FLASH_SectorErase(0);
FLASH_Read(revbuff,0,8);
Byte_print(revbuff);
总的来说我认为SPI部署有以下几个比较重要的点:
1.FLASH片选信号使用软件控制,要注意与硬件控制加以区别;
2.SPI的启停由片选信号控制,SPI的时钟线更多的起到数据同步的作用,因此实际传输时参考芯片的传输时序很重要,在不同的芯片操作指令之后跟随的内容是不一样的;
3.注意判断状态寄存器,FLASH读写较慢,没有执行完命令不能进行下一个操作;
4.擦除完也需要加入延时,立刻执行写命令写不进去。