返回列表 发新帖我要提问本帖赏金: 50.00元(功能说明)

[APM32E1] 玩转APM32的DMA-用SPI的DMA实现NorFlash的读写

[复制链接]
 楼主| shanyuxiang 发表于 2024-7-1 21:19 | 显示全部楼层 |阅读模式
<
本帖最后由 shanyuxiang 于 2024-7-2 20:57 编辑

#申请原创# @21小跑堂
玩转APM32的DMA-用SPI的DMA实现NorFlash的读写

一、前言

1.1 关于NorFlash

NorFlash是一种常见的非易失性存储介质,经常用来保存程序固件或一些数据,其中以SOP-8封装的25Qxx系列为代表。
常规的方式是一个字节一个字节的读写,对于SPI这种接口来说,这种方法不太划算,当需要多个字节读写的操作时可以用DMA去接管,从而省去大量CPU的时间。
这里就以W25Q64为例,介绍一下如何用SPI的DMA实现flash的读写,对于其他容量或其他品牌的NOR FLASH芯片也是类似的方法。

完整的SPI要用到四个信号,这里连接到SPI2,CS使用任意一个IO口即可,Flash与MCU的连接关系如下:

CS  --   PB12
CLK --  PB13(SPI2_SCK)
DO  --  PB14(SPI2_MISO)
DI   --  PB15(SPI2_MOSI)

1-1.png

2.png


Nor Flash的指令挺多,常用的其实只有几条,这里要用DMA实现的指令就只有"读数据"和"页编程",其他指令还是按照传统的方式实现。

Read Data      03h
Page Program 02h

2-5.png


1.2 关于SPI和DMA

通过查阅用户手册“APM32E103xCxE用户手册”的DMA章节可以知道SPI2的对应通道,SPI2接收对应DMA1_Channel4,SPI2发送对应DMA1_Channel5。

Flash的读数据可以用DMA1_Channel4来传输,Flash的页编程可以用DMA1_Channel5来传输。

3.png

二、SPI的DMA读写

2.1 SPI的初始化

这里参考SDK把SPI初始化为主机、全双工模式即可,比较简单,代码如下:

  1. //SPI2 初始化
  2. void spi_flash_init(void)
  3. {
  4.     GPIO_Config_T gpioConfig;
  5.     SPI_Config_T spiConfig;

  6.     /* Enable related Clock */
  7.     RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_SPI2);
  8.     RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOB);

  9.     /* config MISO*/
  10.     gpioConfig.pin =  GPIO_PIN_14 ;
  11.     gpioConfig.mode = GPIO_MODE_AF_PP;
  12.     gpioConfig.speed = GPIO_SPEED_50MHz;
  13.     GPIO_Config(GPIOB, &gpioConfig);

  14.     /** config SCK,MOSI*/
  15.     gpioConfig.pin = GPIO_PIN_13 | GPIO_PIN_15;
  16.     gpioConfig.mode = GPIO_MODE_AF_PP;
  17.     gpioConfig.speed = GPIO_SPEED_50MHz;
  18.     GPIO_Config(GPIOB, &gpioConfig);

  19.     /** config CS */
  20.     gpioConfig.pin = GPIO_PIN_12;
  21.     gpioConfig.mode = GPIO_MODE_OUT_PP;
  22.     gpioConfig.speed = GPIO_SPEED_50MHz;
  23.     GPIO_Config(GPIOB, &gpioConfig);

  24.     GPIO_SetBit(GPIOB, GPIO_PIN_12);
  25.     GPIO_SetBit(GPIOB, GPIO_PIN_13);
  26.     GPIO_SetBit(GPIOB, GPIO_PIN_14);
  27.     GPIO_SetBit(GPIOB, GPIO_PIN_15);

  28.     SPI_ConfigStructInit(&spiConfig);

  29.     spiConfig.length = SPI_DATA_LENGTH_8B;

  30.     spiConfig.baudrateDiv = SPI_BAUDRATE_DIV_64;
  31.     /*  2 line full duplex  */
  32.     spiConfig.direction = SPI_DIRECTION_2LINES_FULLDUPLEX;
  33.     /*  LSB first  */
  34.     spiConfig.firstBit = SPI_FIRSTBIT_MSB;
  35.     /*  Slave mode  */
  36.     spiConfig.mode = SPI_MODE_MASTER;
  37.     /*  Polarity is low  */
  38.     spiConfig.polarity = SPI_CLKPOL_HIGH;
  39.     /*  Software select slave enable  */
  40.     spiConfig.nss = SPI_NSS_SOFT;
  41.     /*  Phase is 1 edge  */
  42.     spiConfig.phase = SPI_CLKPHA_2EDGE;

  43.     spiConfig.crcPolynomial = 7;

  44.     /*  SPI config  */
  45.     SPI_Config(SPI2, &spiConfig);

  46.     SPI_ConfigDataSize(SPI2, SPI_DATA_LENGTH_8B);

  47.     SPI_Enable(SPI2);
  48. }


2.2 DMA的初始化

要实现读写需要配置两个DMA通道,两个DMA通道的配置有些不同,主要是注意传输方向。

DMA1_Channel5 作为发送,方向是从内存到外设;

DMA1_Channel4作为接受,方向是从外设到内存。

外设的地址是SPI的数据寄存地址,通过查看手册可知是 SPI2_BASE + 0x0C。

内存地址和传输长度可以先不配,等后面真正要传输时再配置,两个DMA通道都先不要使能。

  1. DMA_Config_T    DMA_ConfigStruct;
  2. void spi_fullduplex_dma_init(void)
  3. {
  4.     RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);

  5.     DMA_ConfigStruct.peripheralBaseAddr = (uint32_t)(SPI2_BASE + 0x0C) ;
  6.     DMA_ConfigStruct.peripheralInc      = DMA_PERIPHERAL_INC_DISABLE;
  7.     DMA_ConfigStruct.memoryInc          = DMA_MEMORY_INC_ENABLE;
  8.     DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
  9.     DMA_ConfigStruct.memoryDataSize     = DMA_MEMORY_DATA_SIZE_BYTE;
  10.     DMA_ConfigStruct.loopMode = DMA_MODE_NORMAL;
  11.     DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH;
  12.     DMA_ConfigStruct.M2M      = DMA_M2MEN_DISABLE;

  13.     //SPI2_TX DMA_CH5
  14.     DMA_Reset(DMA1_Channel5);

  15.     DMA_ConfigStruct.memoryBaseAddr = (uint32_t) AddressBuffer;
  16.     DMA_ConfigStruct.dir                   = DMA_DIR_PERIPHERAL_DST;
  17.     DMA_ConfigStruct.bufferSize         = 0;
  18.     DMA_Config(DMA1_Channel5, &DMA_ConfigStruct);
  19.     SPI_I2S_EnableDMA(SPI2, SPI_I2S_DMA_REQ_TX);

  20.     //SPI2_RX DMA_CH4
  21.     DMA_Reset(DMA1_Channel4);

  22.     DMA_ConfigStruct.memoryBaseAddr = (uint32_t) AddressBuffer;
  23.     DMA_ConfigStruct.dir                   = DMA_DIR_PERIPHERAL_SRC;
  24.     DMA_ConfigStruct.bufferSize         = 0;
  25.     DMA_Config(DMA1_Channel4, &DMA_ConfigStruct);
  26.     SPI_I2S_EnableDMA(SPI2, SPI_I2S_DMA_REQ_RX);

  27.     //DMA_Enable(DMA1_Channel4);
  28.     //DMA_Enable(DMA1_Channel5;
  29. }

2.3 SPI的DMA发送

要发送多个数据时,设置一下DMA1_Channel5内存地址、发送长度、开启内存地址自增,然后使能DMA的传输即可;
DMA1_Channel4类似,只是地址不需要自增,最后等待传输完成就实现了SPI的DMA发送。

如果发送的数据较多的话,等待的时间可能较长,在等待过程中CPU可以去做别的事。

  1. void spi_dam_transmit_buffer(unsigned char *buffer, unsigned int length)
  2. {

  3.     DMA1_Channel4->CHCFG_B.MIMODE = DISABLE;
  4.     DMA1_Channel4->CHMADDR = (uint32_t)AddressBuffer;
  5.     DMA1_Channel4->CHNDATA = length;

  6.     DMA1_Channel5->CHCFG_B.MIMODE = ENABLE ;
  7.     DMA1_Channel5->CHMADDR = (uint32_t)buffer;
  8.     DMA1_Channel5->CHNDATA = length;

  9.     DMA1_Channel4->CHCFG_B.CHEN = ENABLE;
  10.     DMA1_Channel5->CHCFG_B.CHEN = ENABLE;

  11.     while (DMA_ReadStatusFlag(DMA1_FLAG_TC5) == RESET);
  12.     DMA_ClearStatusFlag(DMA1_FLAG_TC5);

  13.     while (DMA_ReadStatusFlag(DMA1_FLAG_TC4) == RESET);
  14.     DMA_ClearStatusFlag(DMA1_FLAG_TC4);

  15.     DMA1_Channel5->CHCFG_B.CHEN = DISABLE;
  16.     DMA1_Channel4->CHCFG_B.CHEN = DISABLE;
  17. }


2.4 SPI的DMA接收

要读取多个数据时,除了要设置DMA1_Channel4,也要设置DMA1_Channel5,因为MCU作为主机要给flash芯片提供时钟,所以读取的同时也要开启发送。

这里的发送也只要发送任意数据就可以,因此发送通道的内存地址不需要递增,地址给一个自定义的小数组即可。

关于接收部分,和前面类似,设置一下内存地址、发送长度、开启内存地址自增,使能DMA1_Channel4的传输,等待两个通道都传输完成就完成了读取。

  1. void spi_dam_receive_buffer(unsigned char *buffer, unsigned int length)
  2. {

  3.     DMA1_Channel5->CHCFG_B.MIMODE = DISABLE;
  4.     DMA1_Channel5->CHMADDR = (uint32_t)AddressBuffer;
  5.     DMA1_Channel5->CHNDATA = length;

  6.     DMA1_Channel4->CHCFG_B.MIMODE = ENABLE ;
  7.     DMA1_Channel4->CHMADDR = (uint32_t)buffer;
  8.     DMA1_Channel4->CHNDATA = length;

  9.     DMA1_Channel4->CHCFG_B.CHEN = ENABLE;
  10.     DMA1_Channel5->CHCFG_B.CHEN = ENABLE;

  11.     while (DMA_ReadStatusFlag(DMA1_FLAG_TC5) == RESET);
  12.     DMA_ClearStatusFlag(DMA1_FLAG_TC5);

  13.     while (DMA_ReadStatusFlag(DMA1_FLAG_TC4) == RESET);
  14.     DMA_ClearStatusFlag(DMA1_FLAG_TC4);

  15.     DMA1_Channel5->CHCFG_B.CHEN = DISABLE;
  16.     DMA1_Channel4->CHCFG_B.CHEN = DISABLE;
  17. }


三、NorFlash的驱动

NorFlash的驱动代码大部分还是和常规SPI的方式一样,只是要修改关于这两条指令的代码:

#define W25XXX_ReadData         0x03
#define W25XXX_PageProgram    0x02

3.1 NorFlash的数据读取
读取多个数据的流程与不用DMA的方式类似,只是原来用for循环读数据的部分用spi_dam_receive_buffer()替换。。
  1. //SPI FLASH的数据读取
  2. void w25qxxx_read(unsigned char *buf, unsigned int addr, unsigned short num)
  3. {
  4.     unsigned short i;
  5.          
  6.     w25qxxx_cs_set(0);                              //使能器件

  7.     w25qxxx_wr_byte(W25XXX_ReadData);               //发送读取命令
  8.     w25qxxx_wr_byte((unsigned char)((addr) >> 16)); //发送24bit地址
  9.     w25qxxx_wr_byte((unsigned char)((addr) >> 8));
  10.     w25qxxx_wr_byte((unsigned char)addr);

  11.     spi_dam_receive_buffer(buf,num);

  12.     w25qxxx_cs_set(1);
  13. }



3.2 NorFlash的页编程

写入多个数据的流程与以往类似,只是原来用for循环写数据的部分用spi_dam_transmit_buffer()替换
  1. //SPI FLASH的页编程
  2. void w25qxxx_write_page(unsigned char *buf, unsigned int addr, unsigned short num)
  3. {
  4.     unsigned short i;
  5.     w25qxxx_write_enable();                      // SET WEL
  6.     w25qxxx_cs_set(0);                              //使能器件

  7.     w25qxxx_wr_byte(W25XXX_PageProgram);            //发送写页命令
  8.     w25qxxx_wr_byte((unsigned char)((addr) >> 16)); //发送24bit地址
  9.     w25qxxx_wr_byte((unsigned char)((addr) >> 8));
  10.     w25qxxx_wr_byte((unsigned char)addr);

  11.     spi_dam_transmit_buffer(buf, num);

  12.     w25qxxx_cs_set(1);           //取消片选
  13.     w25qxxx_wait_busy();      //等待写入结束
  14. }



四、测试验证

最后我们验证上面的代码是否可行,先初始化SPI和对应的DMA。
  1. //初始化
  2. void w25qxxx_init()
  3. {
  4.   spi_flash_init();
  5.   spi_fullduplex_dma_init();
  6. }

然后读一下芯片的ID,这步主要是确认一下硬件是否正常。
  1. FlashChipID = w25qxxx_read_id();

接下来就往任意一个扇区写入从0x00-0xFF的数据,擦除扇区后,写入该扇区,再读出该扇区的数据,把读出的数据和写入的原始数据作对比。
  1. //flash测试
  2. unsigned short FlashChipID;
  3. unsigned char flash_test_write_buffer[4096];
  4. unsigned char flash_test_read_buffer[4096];
  5. unsigned int AddrSector=0;
  6. void w25qxxx_test()
  7. {
  8.   unsigned int i;
  9.         for(i=0;i<4096;i++)
  10.         {
  11.           flash_test_write_buffer[i]=i%256;
  12.         }
  13.         
  14.         w25qxxx_erase_sector(AddrSector);

  15.         w25qxxx_write_sector(flash_test_write_buffer,AddrSector);
  16.         
  17.         w25qxxx_read(flash_test_read_buffer,AddrSector*4096,4096);
  18.         
  19.         if(memcmp(flash_test_read_buffer,flash_test_write_buffer,4096)==0)
  20.         {
  21.            printf("flash write success.");
  22.         }
  23.         else
  24.         {
  25.            printf("flash write fail!!!");
  26.         }
  27. }

下载程序并执行,查看输出的日志,可以看到芯片的DeviceID=0xEF16,说明硬件正常;写入数据和读出数据完全一致,读写正常。
4.png


游客,如果您要查看本帖隐藏内容请回复

打赏榜单

21小跑堂 打赏了 50.00 元 2024-07-18
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

在APM32上使用SPI+DMA的方式高效读写NorFlash,在DMA的加持下,MCU可以在大量数据的读写时留出更多的时间处理其他事件。文章思路清晰,结构完整,适合借鉴学习  发表于 2024-7-18 14:53
caigang13 发表于 2024-7-2 08:01 来自手机 | 显示全部楼层
用DMA能够大大提升CPU的利用效率。
Clearhu 发表于 2024-7-7 20:50 | 显示全部楼层
感谢分享
Bobbyxzh 发表于 2024-9-10 16:15 | 显示全部楼层
学习一下
nnqtdf 发表于 2024-9-22 09:37 | 显示全部楼层
感谢分享
xiaogu666 发表于 2024-10-15 13:16 | 显示全部楼层
看看  正好用到
295433181 发表于 2025-8-6 17:48 | 显示全部楼层
2846878626 发表于 2025-8-13 18:32 | 显示全部楼层
感谢大佬
maidilong 发表于 2025-8-21 16:23 | 显示全部楼层
while (DMA_ReadStatusFlag(DMA1_FLAG_TC5) == RESET);这个等待还是阻塞了,CPU无法干别的,只能干等,还是用中断比较好些
 楼主| shanyuxiang 发表于 2025-8-22 15:16 | 显示全部楼层
maidilong 发表于 2025-8-21 16:23
while (DMA_ReadStatusFlag(DMA1_FLAG_TC5) == RESET);这个等待还是阻塞了,CPU无法干别的,只能干等,还是 ...

用RTOS或状态机的话,可以把这部分等待时间释放出来做其他事,也可以在下次操作flash时再等待。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

6

主题

42

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部

6

主题

42

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部