细说STM32F407单片机DMA方式读写SPI FLASH W25Q16BV
目录
一、 工程配置
1、时钟、DEBUG、GPIO、USART6、Code Generator
2、SPI2
3、NVIC
二、软件设计
1、 FALSH、KEY_LED
2、 spi.h
3、 spi.c
4、main.c
三、下载、运行
SPI接口具有发送和接收两个DMA请求,在大数据量传输时,使用DMA效率更高,比如,一次写入一个扇区的数据。
参考本文作者写的其他文章:细说STM32F407单片机轮询方式读写SPI FLASH W25Q16BV-CSDN博客 https://wenchm.blog.csdn.net/article/details/144587209
本文目的:对照参考文章的需求,把读、写FLASH操作修改为通过DMA方式。 其它不变。
硬件开发板同参考文章。工程的核心用途:
- KeyUp键按下时,调用函数Flash_EraseChip()擦除整个芯片。
- KeyDown键按下时,调用函数Flash_EraseSector()擦除扇区0,一个扇区有16个页。
- KeyLeft键按下时,调用函数Flash_TestWriteDMA()以DMA方式向Page 3写入数据。
- KeyRight键按下时,调用函数Flash_TestReadDMA()以DMA方式从Page 3读取数据。Flash_TestWriteDMA()和Flash_TestReadDMA()是在文件spi.h中新定义的两个函数,用于测试DMA方式的SPI数据读写功能。
一、 工程配置
1、时钟、DEBUG、GPIO、USART6、Code Generator
与参考文章相同。
2、SPI2
MCU的SPI2基本参数和管脚设置与参考文章相同。
SPI2的DMA设置如下图:
为DMA请求SPI2_TX和SPI2_RX分别配置DMA流。SPI2_TX的DMA传输方向是存储器到外设,SPI2_RX的DMA传输方向是外设到存储器。注意,两个DMA流的Mode(工作模式)一定要设置为正常(Normal),因为每次的DMA发送或接收只执行一次,不需要循环执行。外设和存储器的数据宽度(Data Width)都是字节,开启存储器的地址自增(Increment Address)功能。
3、NVIC
DMA中断默认开启的,优先级设置为1,其它中断设置与参考文章相同。
DMA流的中断会自动打开,设置两个DMA流中断的抢占优先级为1,因为DMA流中断的回调函数里会间接用到延时函数HAL_Delay()。不要打开SPI2的全局中断。
二、软件设计
1、 FALSH、KEY_LED
外部文件,与参考文章相同。
FLASH文件夹里的W25Q16驱动程序的底层SPI传输,采用的是阻塞式传输方式,没必要进行改写,对于一般的操作指令,用阻塞式SPI传输实现更容易。DMA方式适用于需要传输大块数据的场合,例如,一次写入或读取几个页的数据。
2、 spi.h
/* USER CODE BEGIN Prototypes */
void Flash_TestReadStatus(void);
void Flash_TestReadDMA(void); //以DMA方式读取1个Page
void Flash_TestWriteDMA(); //以DMA方式写入1个Page
/* USER CODE END Prototypes */
main()函数中调用的3个W25Q16测试函数都在文件spi.h中定义,函数Flash_TestReadStatus()与参考文章完全相同。函数Flash_TestWriteDMA()和Flash_TestReadDMA()需要在文件spi.h中声明函数原型。这两个函数和相关回调函数的代码在文件spi.c中。
3、 spi.c
/* USER CODE BEGIN 0 */
#include "w25flash.h"
#include <stdio.h>
uint8_t bufPageRead[FLASH_PAGE_SIZE]; //接收1个Page数据的缓存区
uint8_t bufPageWrite[FLASH_PAGE_SIZE]; //发送1个Page数据的缓存区
/* USER CODE END 0 */
/* USER CODE BEGIN 1 */
//读取器件ID,状态寄存器 SR1和SR2
void Flash_TestReadStatus(void)
{
//1. 读DeviceID,SR1,SR2
uint16_t devID;
devID = Flash_ReadID(); //读取器件ID
printf("Device ID= %X\r\n",devID);
printf("The chip is: ");
switch (devID)
{
case 0xEF14:
printf("W25Q16\r\n");
break;
case 0xEF16:
printf("W25Q64\r\n");
break;
case 0xEF17:
printf("W25Q128\r\n");
break;
case 0xC817:
printf("GD25Q128\r\n");
break;
case 0x1C17:
printf("EN25Q128\r\n");
break;
case 0x2018:
printf("N25Q128\r\n");
break;
case 0x2017:
printf("XM25QH128\r\n");
break;
case 0xA117:
printf("FM25Q128\r\n");
break;
default:
printf("Unknown type\r\n");
}
uint8_t SR1=Flash_ReadSR1(); //读寄存SR1
printf("Status Reg1= %X\r\n",SR1); //Hex显示
uint8_t SR2=Flash_ReadSR2(); //读寄存器SR2
printf("Status Reg2= %X\r\n",SR2); //Hex显示
}
void Flash_TestWriteDMA() //以DMA方式写入1个Page
{
uint8_t blockNo=0;
uint16_t sectorNo=0;
uint32_t memAddress=0;
for(uint16_t i=0;i<FLASH_PAGE_SIZE;i++)
bufPageWrite[i]=i; //准备数据
uint16_t pageNo=3;
memAddress=Flash_Addr_byBlockSectorPage(blockNo,sectorNo,pageNo);
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(memAddress,&byte2,&byte3,&byte4);
Flash_Write_Enable(); //写能
Flash_Wait_Busy();
__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x02); //Command=0x02: 对一个Page编程写入
SPI_TransmitOneByte(byte2); //Transmit 24bit address
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);
//以DMA方式连续写入256字节
printf("Writing Page3 in DMA mode.\r\n");
HAL_SPI_Transmit_DMA(&hspi2,bufPageWrite,FLASH_PAGE_SIZE);
}
//DMA transmit completion interrupt callback
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
__Deselect_Flash(); //CS=1, 结束SPI传输过程
Flash_Wait_Busy();
printf("DMA Writing complete.\r\n");
printf("** Reselect menu or reset **\r\n");
}
void Flash_TestReadDMA(void) //以DMA方式读取1个Page
{
uint8_t blockNo=0;
uint16_t sectorNo=0;
uint16_t pageNo=3;
uint32_t memAddress;
memAddress=Flash_Addr_byBlockSectorPage(blockNo,sectorNo,pageNo);
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(memAddress,&byte2,&byte3,&byte4); //地址分解
__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x03); //Command=0x03, read data
SPI_TransmitOneByte(byte2); //transmit 24bit address
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);
//DMA方式连续接收256个字��?
printf("Reading Page3 in DMA mode.\r\n");
HAL_SPI_Receive_DMA(&hspi2,bufPageRead,FLASH_PAGE_SIZE);
}
//DMA接收完成中断回调函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
__Deselect_Flash(); //CS=1, 结束SPI传输过程
Flash_Wait_Busy();
printf("DMA reading complete.\r\n");
printf("Page3[26] = %d\r\n",bufPageRead[26]);
printf("DMA reading complete.\r\n");
printf("Page3[205] = %d\r\n",bufPageRead[205]);
printf("** Reselect menu or reset **\r\n");
}
/* USER CODE END 1 */
4、main.c
/* USER CODE BEGIN Includes */
#include "keyled.h"
#include "w25flash.h"
#include <stdio.h>
/* USER CODE END Includes */
/* USER CODE BEGIN 2 */
printf("Test:SPI-DMA Read/Write: \r");
printf("16Mbit Flash Memory;\r\n");
Flash_TestReadStatus(); //读取DeviceID, SR1,SR2
//显示菜单
printf("[S2]KeyUp = Erase Chip;\r\n");
printf("[S3]KeyDown = Erase Sector0;\r\n");
printf("[S4]KeyLeft = Write Page3;\r\n");
printf("[S5]KeyRight= Read Page3;\r\n");
// MCU output low level LED light is on
LED1_OFF();
LED2_OFF();
LED3_OFF();
LED4_OFF();
/* USER CODE END 2 */
定义了两个256字节的数组bufPageRead和bufPageWrite,分别用作接收和发送数据的缓冲区,用于读取或写入一个页的数据。因为要在不同的函数里使用,所以定义为全局变量。函数Flash_TestWriteDMA()以DMA方式写入一个页共256字节的数据。它实际上是重新实现了“页编程”指令,在发送了指令码0x02和3字节的地址数据后,发送后面的256字节的写入数据时,使用了HAL_SPI_Transmit_DMA(),而不是像函数Flash_WriteInPage()里那样,使用SPI阻塞式传输函数HAL_SPI_Transmit()发送数据。
/* USER CODE BEGIN 3 */
KEYS curKey=ScanPressedKey(KEY_WAIT_ALWAYS);
switch(curKey)
{
case KEY_UP: //S2
printf("Erasing chip, about 30sec...\r\n");
Flash_EraseChip(); //擦除整个芯片
printf("Chip is erased.\r\n");
printf("** Reselect menu or reset **\r\n");
LED1_ON();
LED2_OFF();
LED3_OFF();
LED4_OFF();
break;
case KEY_DOWN: //S3
printf("Erasing Sector 0(16 pages)...\r\n");
uint32_t globalAddr=0;
Flash_EraseSector(globalAddr); //擦除扇区0
printf("Sector 0 is erased.\r\n");
printf("** Reselect menu or reset **\r\n");
LED1_OFF();
LED2_ON();
LED3_OFF();
LED4_OFF();
break;
case KEY_LEFT: //S4
Flash_TestWriteDMA(); //测试写入Page 3
LED1_OFF();
LED2_OFF();
LED3_ON();
LED4_OFF();
break;
case KEY_RIGHT://S5
Flash_TestReadDMA(); //测试读取Page 3
LED1_OFF();
LED2_OFF();
LED3_OFF();
LED4_ON();
break;
default:
LED1_OFF();
LED2_OFF();
LED3_OFF();
LED4_OFF();
break;
}
HAL_Delay(500); //延时500,消除按键后抖动
}
/* USER CODE END 3 */
函数Flash_TestWriteDMA()里启动DMA传输后就退出了,由DMA去完成后面的数据传输。传输完成后会产生DMA传输完成事件中断,关联的回调函数是HAL_SPI_TxCpltCallback()。重新实现此回调函数,在此函数里,首先要将W25Q16的片选信号CS置1,以结束SPI通信过程,然后需要等待BUSY位变为0。这就是以DMA方式实现指令0x02的页编程数据写入过程。函数Flash_TestReadDMA()以DMA方式读取一个页的256字节的数据。在发送了指令码0x03和3字节的地址数据后,调用函数HAL_SPI_Receive_DMA()以DMA方式读取后续256字节的数据。
函数Flash_TestReadDMA()启动DMA传输后就退出了,由DMA去完成后面的数据传输。接收完256字节数据后,产生DMA传输完成事件中断,关联的回调函数是HAL_SPI_RxCpltCallback(),在此函数里,首先将W25Q16的片选信号CS置1,以结束SPI通信过程。然后显示了从数据缓冲区内随意两个位置读出的数,以验证写入和读出的数据是否一致。
/* USER CODE BEGIN 4 */
//串口打印
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart6, (uint8_t*)&ch, 1, 0xFFFF);
return ch;
}
/* USER CODE END 4 */
函数Flash_TestWriteDMA()里启动DMA传输后就退出了,由DMA去完成后面的数据传输。传输完成后会产生DMA传输完成事件中断,关联的回调函数是HAL_SPI_TxCpltCallback()。重新实现此回调函数,在此函数里,首先要将W25Q16的片选信号CS置1,以结束SPI通信过程,然后需要等待BUSY位变为0。这就是以DMA方式实现指令0x02的页编程数据写入过程。函数Flash_TestReadDMA()以DMA方式读取一个页的256字节的数据。在发送了指令码0x03和3字节的地址数据后,调用函数HAL_SPI_Receive_DMA()以DMA方式读取后续256字节的数据。
函数Flash_TestReadDMA()启动DMA传输后就退出了,由DMA去完成后面的数据传输。接收完256字节数据后,产生DMA传输完成事件中断,关联的回调函数是HAL_SPI_RxCpltCallback(),在此函数里,首先将W25Q16的片选信号CS置1,以结束SPI通信过程。然后显示了从数据缓冲区内随意两个位置读出的数,以验证写入和读出的数据是否一致。
三、下载、运行
可以先擦除扇区0。如果擦除后读取Page 3的数据,读出的数据都是255,即擦除后的状态。向Page 3写入数据后再读出,会看到读出的数据与写入的数据一致,说明功能正确。