DMA Single Block

该示例通过使用 SPI 与外部flash以 DMA 搬运数据方式进行数据传输。

SPI通过DMA与外部flash通信,对外部flash进行数据的读取,写入,擦除等操作。

示例中采用全双工的模式进行通信,可以配置宏 CONFIG_SPI_SW_SIM_CS 选择是否使用软件模拟CS线通信。

环境需求

该示例支持以下开发套件:

开发套件

Hardware Platforms

Board Name

RTL87x2G HDK

RTL87x2G EVB

更多信息请参考快速入门

硬件连线

EVB外接W25Q128模块,使用杜邦线连接P4_0(SCK)和CLK,P4_1(MISO)和DI,P4_2(MOSI)和DO,P4_3(CS)和CS#。

硬件介绍

W25Q128是一款SPI接口的NOR Flash芯片,支持标准串行外设接口(SPI)、双线/四线 SPI 以及 2-时钟指令周期四外设接口(QPI)。

此外,该器件还支持JEDEC标准的制造商和器件ID和SFDP寄存器、64位唯一序列号和三个256字节安全寄存器。

其他细节说明可查阅该器件的应用指南。在示例中使用该器件作为slave进行SPI的通信测试。

配置选项

该示例可配置的宏如下:

  1. CONFIG_SPI_SW_SIM_CS :配置该宏可选择是否使用软件模拟CS线进行通信。配置 1 则代表使用软件模拟CS线通信, 0 则代表不使用软件进行模拟。

编译和下载

该示例的工程路径如下:

Project file: samples\peripheral\spi\gdma_singleblock\proj\rtl87x2g\mdk

Project file: samples\peripheral\spi\gdma_singleblock\proj\rtl87x2g\gcc

请按照以下步骤操作构建并运行该示例:

  1. 打开工程文件。

  2. 按照 快速入门编译APP Image 给出的步骤构建目标文件。

  3. 编译成功后,在路径 mdk\bingcc\bin 下会生成 app bin app_MP_xxx.bin 文件。

  4. 按照 快速入门MP Tool 给出的步骤将app bin烧录至EVB内。

  5. 按下复位按键,开始运行。

测试验证

  1. 当DMA TX通道数据传输完成时,会打印log如下

    gdma tx data finish!
    
  2. 当DMA RX通道数据传输完成时,会打印传输完成说明,收到的数据长度和数据内容。

    1. 读取flash id时,打印如下内容:第一个字节为dummy read,后三个字节为JEDEC_ID内容。

      data_len = 4
      dma rx data[0] = 000000ff
      dma rx data[1] = 000000ef
      dma rx data[2] = 00000040
      dma rx data[3] = 00000018
      
    2. 使能写操作时,打印如下内容:第一个字节为dummy read。

      data_len = 1
      dma rx data[0] = 000000ff
      
    3. 执行擦除操作时,打印如下内容:写操作对应的四个字节为dummy read。

      data_len = 4
      dma rx data[0] = 000000ff
      dma rx data[1] = 000000ff
      dma rx data[2] = 000000ff
      dma rx data[3] = 000000ff
      
    4. 擦除完成后,读取该地址下的数据,打印如下内容:由于该地址下数据被擦除,因此读到的数据均为FF,前5个字节为dummy read。

      data_len = 105
      dma rx data[0] = 000000ff
      dma rx data[1] = 000000ff
      ...
      dma rx data[104] = 000000ff
      
    5. 执行数据写入操作,打印如下内容:写操作对应的前4个字节为指令的dummy read,后100个字节为数据的dummy read。

      data_len = 104
      dma rx data[0] = 000000ff
      ...
      dma rx data[103] = 000000ff
      
    6. 写入数据之后再次进行数据读取,打印读取的数据内容:前5个字节为dummy read,后100个字节为读取到的数据。

      data_len = 105
      dma rx data[0] = 000000ff
      dma rx data[1] = 000000ff
      dma rx data[2] = 000000ff
      dma rx data[3] = 000000ff
      dma rx data[4] = 000000ff
      dma rx data[5] = 0000000a
      dma rx data[6] = 0000000b
      dma rx data[7] = 0000000c
      ...
      dma rx data[104] = 0000006d
      

代码介绍

该章节分为以下几个部分:

  1. 源码路径

  2. 初始化函数将在 初始化 章节介绍。

  3. 初始化后的功能实现将在 功能实现 章节介绍。

源码路径

  • 工程路径: sdk\samples\peripheral\spi\gdma_singleblock\proj

  • 源码路径: sdk\samples\peripheral\spi\gdma_singleblock\src

该工程的工程文件代码结构如下:

└── Project: gdma_singleblock
    └── secure_only_app
        └── Device                   includes startup code
            ├── startup_rtl.c
            └── system_rtl.c
        ├── CMSIS                    includes CMSIS header files
        ├── CMSE Library             Non-secure callable lib
        ├── Lib                      includes all binary symbol files that user application is built on
            └── rtl87x2g_io.lib
        ├── Peripheral               includes all peripheral drivers and module code used by the application
            ├── rtl_rcc.c
            ├── rtl_pinmux.c
            ├── rtl_nvic.c
            ├── rtl_gdma.c
            └── rtl_spi.c
        └── APP                      includes the ble_peripheral user application implementation
            ├── main_ns.c
            └── io_spi.c

初始化

初始化流程包括了 board_spi_initdriver_spi_initdriver_tx_gdma_initdriver_rx_gdma_init


board_spi_init 中包含了对SPI的PAD与PINMUX设置。

  1. 配置PAD:设置引脚、PINMUX模式、PowerOn、内部上拉。

  2. 配置PINMUX:分配引脚分别为SPI0_CLK_MASTER、SPI0_MO_MASTER、SPI0_MI_MASTER、SPI0_CSN_0_MASTER功能。

  3. 如果开启宏 CONFIG_SPI_SW_SIM_CS ,则需要将对应的CS线引脚设置为SW模式,无需配置PINMUX。


driver_spi_init 包含了对SPI外设的初始化:

  1. 使能PCC时钟。

  2. 设置通信模式为全双工模式。

  3. 其他基础设置参考 SPI Polling 初始化流程。

  4. 使能SPI的DMA发送与接收功能。

  5. 设置RxWaterlevel为1-1,设置TxWaterlevel为FIFO_SIZE - 1。

RCC_PeriphClockCmd(APBPERIPH_SPI, APBPERIPH_SPI_CLOCK, ENABLE);
...
SPI_InitStructure.SPI_Direction   = SPI_Direction_FullDuplex;
...
SPI_InitStructure.SPI_RxDmaEn     = ENABLE;
SPI_InitStructure.SPI_TxDmaEn     = ENABLE;
SPI_InitStructure.SPI_RxWaterlevel = 1 - 1;
SPI_InitStructure.SPI_TxWaterlevel = SPI_TX_FIFO_SIZE - 1;

备注

SPI使用DMA传输时,推荐配置SPI_TxWaterlevel为SPI_TX_FIFO_SIZE - MSize,SPI_RxWaterlevel为MSize - 1。


driver_tx_gdma_init 包含了对DMA TX的初始化:

  1. 使能PCC时钟。

  2. 使用GDMA通道2。

  3. 传输方向为内存到外设传输。

  4. 设置源端地址自增,目的端地址固定。

  5. 设置源端和目的端的MSize为1。

  6. 源端地址为 GDMA_Send_Buffer ,目的端地址为 SPI0->SPI_DR

  7. 配置GDMA TX总传输完成中断 GDMA_INT_Transfer

RCC_PeriphClockCmd(APBPeriph_GDMA, APBPeriph_GDMA_CLOCK, ENABLE);
...
GDMA_InitStruct.GDMA_ChannelNum          = GDMA_TX_CHANNEL_NUM;
GDMA_InitStruct.GDMA_DIR                 = GDMA_DIR_MemoryToPeripheral;
GDMA_InitStruct.GDMA_SourceInc           = DMA_SourceInc_Inc;
GDMA_InitStruct.GDMA_DestinationInc      = DMA_DestinationInc_Fix;
GDMA_InitStruct.GDMA_SourceMsize         = GDMA_Msize_1;
GDMA_InitStruct.GDMA_DestinationMsize    = GDMA_Msize_1;
...
GDMA_InitStruct.GDMA_SourceAddr          = (uint32_t)GDMA_Send_Buffer;
GDMA_InitStruct.GDMA_DestinationAddr     = (uint32_t)SPI0->SPI_DR;
...
GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
...

driver_rx_gdma_init 包含了对DMA TX的初始化:

  1. 使能PCC时钟。

  2. 使用GDMA通道4。

  3. 传输方向为外设到内存传输。

  4. 设置源端地址固定,目的端地址自增。

  5. 设置源端和目的端的MSize为1。

  6. 源端地址为 SPI0->SPI_DR ,目的端地址为 GDMA_Recv_Buffer

  7. 配置GDMA RX总传输完成中断 GDMA_INT_Transfer

RCC_PeriphClockCmd(APBPeriph_GDMA, APBPeriph_GDMA_CLOCK, ENABLE);
...
GDMA_InitStruct.GDMA_ChannelNum          = GDMA_RX_CHANNEL_NUM;
GDMA_InitStruct.GDMA_DIR                 = GDMA_DIR_PeripheralToMemory;
GDMA_InitStruct.GDMA_SourceInc           = DMA_SourceInc_Fix;
GDMA_InitStruct.GDMA_DestinationInc      = DMA_DestinationInc_Inc;
GDMA_InitStruct.GDMA_SourceMsize         = GDMA_Msize_1;
GDMA_InitStruct.GDMA_DestinationMsize    = GDMA_Msize_1;
...
GDMA_InitStruct.GDMA_SourceAddr          = (uint32_t)SPI0->SPI_DR;
GDMA_InitStruct.GDMA_DestinationAddr     = (uint32_t)GDMA_Recv_Buffer;
...
GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);

功能实现

  1. 执行 spi_flash_read_id(),读取外部flash的JEDEC ID信息。

    1. 初始化DMA接收和发送数组的内容信息。

    2. 设置要发送的指令和发送长度。根据W25Q128的硬件信息,若要读取JEDEC ID,需要通讯的总长度为4字节。因此设置DMA的BufferSize为4。

    3. 使能SPI,使能DMA接收通道,使能DMA发送通道。若使用软件模拟CS通讯,则在通讯开始之前,需要将CS线拉低。

    4. 等待DMA发送和接收通道完成后,可以将CS线拉高,继续后续步骤。

DBG_DIRECT("===================read_flash_id==================");
memset(GDMA_Send_Buffer, 0, sizeof(GDMA_Send_Buffer) / sizeof(GDMA_Send_Buffer[0]));
memset(GDMA_Recv_Buffer, 0, sizeof(GDMA_Recv_Buffer) / sizeof(GDMA_Recv_Buffer[0]));

GDMA_Send_Buffer[0] = SPI_FLASH_JEDEC_ID;
GDMA_SetBufferSize(GDMA_TX_CHANNEL, 4);
GDMA_SetBufferSize(GDMA_RX_CHANNEL, 4);

GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);

pull_cs_down(true);

SPI_Cmd(SPI0, ENABLE);
GDMA_Cmd(GDMA_RX_CHANNEL_NUM, ENABLE);
GDMA_Cmd(GDMA_TX_CHANNEL_NUM, ENABLE);

DBG_DIRECT("after dma cmd enable");

//wait for read id finish!
while ((isDMATxDone != true) || (isDMARxDone != true))
{
    DBG_DIRECT("isDMATxDone = %d", isDMATxDone);
    DBG_DIRECT("isDMARxDone = %d", isDMARxDone);
    platform_delay_ms(1000);
}

isDMATxDone = false;
isDMARxDone = false;

pull_cs_down(false);
...
  1. 执行 spi_flash_sector_erase() 函数,将指定地址的数据进行擦除操作。

    1. 在执行写操作之前,需要执行 spi_flash_write_enable() 对写操作进行使能。通过DMA将写使能指令发送至外部flash,并等待DMA传输完成。

    2. 设置需要发送的数据内容和数据长度,使能DMA接收和发送通道,等待DMA传输完成。

...
GDMA_Send_Buffer[0] = SPI_FLASH_SECTOR_ERASE;
GDMA_Send_Buffer[1] = (vAddress >> 16) & 0xff;
GDMA_Send_Buffer[2] = (vAddress >> 8) & 0xff;
GDMA_Send_Buffer[3] = vAddress & 0xff;

GDMA_SetBufferSize(GDMA_TX_CHANNEL, 4);
GDMA_SetBufferSize(GDMA_RX_CHANNEL, 4);
...
  1. 执行 spi_flash_read() 函数,读取指定地址下的数据内容。

  2. 执行 spi_flash_page_write() 函数,在指定地址下写入指定长度的数据。

  3. 再次执行 spi_flash_read() 函数,读取指定地址下的数据内容。

void spi_dma_demo(void)
{
    uint8_t write_data[100];
    uint8_t read_data[105] = {0};
    for (uint16_t i = 0; i < 100; i++)
    {
        write_data[i] = (i + 10) & 0xFF;
    }

    //read flash JEDEC id
    spi_flash_read_id();

    //sector erase
    spi_flash_sector_erase(0x001000);

    spi_flash_read(SPI_FLASH_FAST_READ, 0x001000, read_data, 100);

    spi_flash_page_write(0x001000, write_data, 100);

    spi_flash_read(SPI_FLASH_FAST_READ, 0x001000, read_data, 100);

}
  1. 当DMA TX通道搬运数据完成后触发 GDMA_INT_Transfer 中断,进入中断处理函数 GDMA_TX_CHANNEL_HANDLER

    1. 失能GDMA TX通道中断,打印进入中断信息,清除GDMA中断悬挂位。

    2. 将DMA TX完成的标志位置位true。

void GDMA_TX_CHANNEL_HANDLER(void)
{
    GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, DISABLE);
    GDMA_ClearINTPendingBit(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer);

    DBG_DIRECT("gdma tx data finish!");
    isDMATxDone = true;

    GDMA_Cmd(GDMA_TX_CHANNEL_NUM, DISABLE);
}
  1. 当DMA RX通道搬运数据完成后触发 GDMA_INT_Transfer 中断,进入中断处理函数 GDMA_RX_CHANNEL_HANDLER

    1. 失能GDMA RX通道中断,清除GDMA中断悬挂位。

    2. 打印DMA收到的数据长度和全部数据内容。

    3. 将DMA RX完成的标志位置位true。

void GDMA_RX_CHANNEL_HANDLER(void)
{
    GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, DISABLE);
    GDMA_ClearINTPendingBit(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer);

    DBG_DIRECT("gdma rx data finish!");
    uint16_t data_len = GDMA_GetTransferLen(GDMA_RX_CHANNEL);
    DBG_DIRECT("data_len = %d", data_len);
    for (uint16_t i = 0; i < data_len; i++)
    {
        DBG_DIRECT("dma rx data[%d] = %x", i, GDMA_Recv_Buffer[i]);
    }
    isDMARxDone = true;

    GDMA_Cmd(GDMA_RX_CHANNEL_NUM, DISABLE);
}

常见问题

当需要使用的SPI频率过高(大于10MHz)时,推荐使用DMA传输数据。 如果发现传输数据过程中出现CS线被拉高的情况,可以增大GDMA的MSize以提高DMA的传输效率,或使用软件模拟CS通信。