DMA Single Block

该示例通过使用 SPI 与外部 flash 进行通信,并使用 DMA 搬运数据。

SPI 通过 DMA 与外部 flash 通信,读取外部 flash 的 JEDEC_ID 信息,并通过 UART 将读到的数据发送至 PC 端串口助手。

示例中 SPI 使用 EEPROM 模式进行通信。

环境需求

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

开发套件

Hardware Platforms

Board Name

RTL8752H HDK

RTL8752H EVB

更多信息请参考 快速入门

硬件连线

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

EVB 外接 FT232 模块,连接 P3_0(UART TX)和 FT232 的 RX,P3_1(UART RX)和 FT232 的 TX。

备注

W25Q128 的模块介绍参考 W25Q128硬件介绍

编译和下载

该示例的工程路径如下:

Project file: board\evb\io_sample\SPI\GDMA_SingleBlock\mdk

Project file: board\evb\io_sample\SPI\GDMA_SingleBlock\gcc

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

  1. 打开工程文件。

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

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

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

  5. 按下 reset 按键,开始运行。

测试验证

准备阶段

启动 PuTTY 或 UartAssist 等 PC 终端,连接到使用的 COM 端口,并进行以下 UART 设置:

  • 波特率: 115200

  • 8 数据位

  • 1 停止位

  • 无校验

  • 无硬件流控

测试阶段

  1. SPI 通过 DMA TX 通道发送读 JEDEC_ID 指令信息。发送完毕后,在 DMA 中断函数内打印发送完成信息。

    [io_gdma]GDMA_TX_Channel_Handler: TX completed!
    
  2. 在 PC 端串口助手上可观察到 JEDEC_ID 信息。

    EF 40 18
    

代码介绍

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

  1. 源码路径

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

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

源码路径

  • 工程路径: sdk\board\evb\io_sample\SPI\GDMA_SingleBlock

  • 源码路径: sdk\src\sample\io_sample\SPI\GDMA_SingleBlock

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

└── Project: gdma_singleblock
    └── secure_only_app
        └── include
            ├── app_define.h
            └── rom_uuid.h
        ├── cmsis                    includes CMSIS header files and startup files
            ├── overlay_mgr.c
            ├── system_rtl876x.c
            └── startup_rtl876x.s
        ├── lib                      includes all binary symbol files that user application is built on
            ├── rtl8752h_sdk.lib
            ├── gap_utils.lib
            └── ROM.lib
        ├── peripheral               includes all peripheral drivers and module code used by the application
            ├── rtl876x_rcc.c
            ├── rtl876x_pinmux.c
            ├── rtl876x_nvic.c
            ├── rtl876x_gdma.c
            ├── rtl876x_uart.c
            └── rtl876x_spi.c
        ├── profile
        └── app                      includes the ble_peripheral user application implementation
            ├── main.c
            ├── ancs.c
            ├── app.c
            ├── app_task.c
            ├── io_spi.c
            ├── io_uart.c
            └── io_gdma.c

初始化

当 EVB 复位启动时,调用 main() 函数,将执行以下流程:

int main(void)
{
    extern uint32_t random_seed_value;
    srand(random_seed_value);
    global_data_init();
    board_init();
    le_gap_init(APP_MAX_LINKS);
    gap_lib_init();
    app_le_gap_init();
    app_le_profile_init();
    pwr_mgr_init();
    task_init();
    os_sched_start();

    return 0;
}

备注

le_gap_init()gap_lib_init()app_le_gap_initapp_le_profile_init 等为 privacy 管理模块相关的初始化,参考 LE Peripheral Privacy 中的初始化流程介绍。

与外设相关的初始化流程具体如下:

  1. board_init 中,执行 board_spi_init ,该函数为 SPI 相关引脚的 PAD/PINMUX 设置,包含如下流程:

    1. 配置 PAD:设置引脚,PINMUX 模式,PowerOn,内部上拉,输出高。

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

  2. board_init 中,执行 board_uart_init ,该函数为 UART 相关引脚的 PAD/PINMUX 设置,包含如下流程:

    1. 配置 PAD:设置引脚,PINMUX 模式,PowerOn,内部上拉,输出失能。

    2. 配置 PINMUX:分配引脚分别为 UART0_TX 和 UART0_RX 功能。

  3. 在执行 os_sched_start() 开启任务调度后,在 app_main_task 主任务内,执行 driver_init 对外设驱动进行初始化配置。

  4. driver_init 中执行 driver_uart_init ,该函数为 UART 外设的初始化,包含如下流程:

    1. 使能 RCC 时钟。

    2. 默认设置波特率为 115200,数据位 8 位,停止位 1 位。

    3. 设置 RX FIFO 接收阈值为 16。

    4. 设置空闲时间为 2 Byte,即当前波特率下接收 2 个字节的时间。

    void driver_uart_init(void)
    {
        RCC_PeriphClockCmd(APBPeriph_UART0, APBPeriph_UART0_CLOCK, ENABLE);
    
        /* uart init */
        UART_InitTypeDef UART_InitStruct;
        UART_StructInit(&UART_InitStruct);
    
        UART_InitStruct.UART_Parity         = UART_PARITY_NO_PARTY;
        UART_InitStruct.UART_StopBits       = UART_STOP_BITS_1;
        UART_InitStruct.UART_WordLen        = UART_WORD_LENGTH_8BIT;
        UART_InitStruct.UART_RxThdLevel = 16;                      //1~29
        UART_InitStruct.UART_IdleTime      = UART_RX_IDLE_2BYTE;      //idle interrupt wait time
        UART_Init(UART0, &UART_InitStruct);
    }
    
  5. 在主函数中执行 os_sched_start() ,开启任务调度。当 stack 准备好时,执行 app_handle_dev_state_evt ,在 spi_demo 中执行 driver_spi_gdma_init 对 SPI 和 DMA 外设进行初始化。

    1. 初始化 SPI 外设:

      1. 使能 RCC 时钟。

      2. 设置 SPI 的传输模式为 EEPROM 模式。

      3. 设置数据宽度为 8 位。

      4. 设置时钟分频系数为 100。

      5. 在 EEPROM 模式需要设置 NDF 为 GDMA_READ_SIZE_MAX - 1

      6. 使能 DMA TX 和 RX 功能,设置 waterLevel。

      RCC_PeriphClockCmd(APBPeriph_SPI0, APBPeriph_SPI0_CLOCK, ENABLE);
      
      /*----------------------SPI init---------------------------------*/
      SPI_StructInit(&SPI_InitStructure);
      SPI_InitStructure.SPI_Direction   = SPI_Direction_EEPROM;//SPI_Direction_RxOnly;
      SPI_InitStructure.SPI_Mode        = SPI_Mode_Master;
      SPI_InitStructure.SPI_DataSize    = SPI_DataSize_8b;
      SPI_InitStructure.SPI_CPOL        = SPI_CPOL_High;
      SPI_InitStructure.SPI_CPHA        = SPI_CPHA_2Edge;
      SPI_InitStructure.SPI_BaudRatePrescaler = 100;
      SPI_InitStructure.SPI_FrameFormat = SPI_Frame_Motorola;
      SPI_InitStructure.SPI_NDF         = GDMA_READ_SIZE_MAX - 1;
      SPI_InitStructure.SPI_RxDmaEn     = ENABLE;
      SPI_InitStructure.SPI_TxDmaEn     = ENABLE;
      SPI_InitStructure.SPI_RxWaterlevel = 1;
      SPI_InitStructure.SPI_TxWaterlevel = 1;
      SPI_Init(FLASH_SPI, &SPI_InitStructure);
      
    2. 初始化 DMA TX 外设:

      1. 设置 DMA 传输方向为内存到外设。

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

      3. 设置数据宽度为 8 位,每次 burst 传输一个字节的数据。

      4. 设置源端地址为 GDMA_WriteCmdBuffer,目的端地址为 FLASH_SPI->DR

      5. 配置 DMA 传输完成中断 GDMA_INT_Transfer

      /*---------------------TX GDMA initial------------------------------*/
      GDMA_StructInit(&GDMA_InitStruct);
      GDMA_InitStruct.GDMA_ChannelNum          = GDMA_TX_CHANNEL_NUM;
      GDMA_InitStruct.GDMA_DIR                 = GDMA_DIR_MemoryToPeripheral;
      GDMA_InitStruct.GDMA_BufferSize          = 0;
      GDMA_InitStruct.GDMA_SourceInc           = DMA_SourceInc_Inc;
      GDMA_InitStruct.GDMA_DestinationInc      = DMA_DestinationInc_Fix;
      
      GDMA_InitStruct.GDMA_SourceDataSize      = GDMA_DataSize_Byte;
      GDMA_InitStruct.GDMA_DestinationDataSize = GDMA_DataSize_Byte;
      GDMA_InitStruct.GDMA_SourceMsize         = GDMA_Msize_1;
      GDMA_InitStruct.GDMA_DestinationMsize    = GDMA_Msize_1;
      
      GDMA_InitStruct.GDMA_SourceAddr          = (uint32_t)GDMA_WriteCmdBuffer;
      GDMA_InitStruct.GDMA_DestinationAddr     = (uint32_t)FLASH_SPI->DR;
      GDMA_InitStruct.GDMA_DestHandshake       = GDMA_Handshake_SPI0_TX;
      GDMA_InitStruct.GDMA_ChannelPriority     = 1;
      GDMA_InitStruct.GDMA_Multi_Block_En      = 0;
      
      GDMA_Init(GDMA_TX_Channel, &GDMA_InitStruct);
      
      /*-----------------GDMA IRQ-----------------------------*/
      NVIC_InitTypeDef NVIC_InitStruct;
      NVIC_InitStruct.NVIC_IRQChannel = GDMA_TX_Channel_IRQn;
      NVIC_InitStruct.NVIC_IRQChannelPriority = 3;
      NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
      NVIC_Init(&NVIC_InitStruct);
      
      /* Enable transfer interrupt */
      GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
      
    3. 初始化 DMA RX 外设:

      1. 设置 DMA 的传输方向为外设到内存。

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

      3. 设置数据宽度为 8 位,每次 burst 传输一个字节的数据。

      4. 设置源端地址为 FLASH_SPI->DR,目的端地址为 GDMA_Recv_Buffer

      5. 配置 DMA 传输完成中断 GDMA_INT_Transfer

      GDMA_StructInit(&GDMA_InitStruct);
      GDMA_InitStruct.GDMA_ChannelNum          = GDMA_RX_CHANNEL_NUM;
      GDMA_InitStruct.GDMA_DIR                 = GDMA_DIR_PeripheralToMemory;
      GDMA_InitStruct.GDMA_BufferSize          = GDMA_TRANSFER_SIZE;
      GDMA_InitStruct.GDMA_SourceInc           = DMA_SourceInc_Fix;
      GDMA_InitStruct.GDMA_DestinationInc      = DMA_DestinationInc_Inc;
      
      GDMA_InitStruct.GDMA_SourceDataSize      = GDMA_DataSize_Byte;
      GDMA_InitStruct.GDMA_DestinationDataSize = GDMA_DataSize_Byte;
      GDMA_InitStruct.GDMA_SourceMsize         = GDMA_Msize_1;
      GDMA_InitStruct.GDMA_DestinationMsize    = GDMA_Msize_1;
      
      GDMA_InitStruct.GDMA_SourceAddr          = (uint32_t)FLASH_SPI->DR;
      GDMA_InitStruct.GDMA_DestinationAddr     = (uint32_t)GDMA_Recv_Buffer;
      GDMA_InitStruct.GDMA_SourceHandshake     = GDMA_Handshake_SPI0_RX;
      GDMA_InitStruct.GDMA_ChannelPriority     = 1;
      GDMA_InitStruct.GDMA_Multi_Block_En      = 0;
      
      GDMA_Init(GDMA_RX_Channel, &GDMA_InitStruct);
      
      /*-----------------GDMA IRQ-----------------------------*/
      NVIC_InitStruct.NVIC_IRQChannel = GDMA_RX_Channel_IRQn;
      NVIC_InitStruct.NVIC_IRQChannelPriority = 3;
      NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
      NVIC_Init(&NVIC_InitStruct);
      
      /* Enable transfer interrupt */
      GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
      
    4. 使能 DMA RX 通道,使能 SPI。

    5. 设置 DMA TX 通道的总传输数据量为 1,使能 DMA TX 通道。

    GDMA_Cmd(GDMA_RX_CHANNEL_NUM, ENABLE);
    SPI_Cmd(FLASH_SPI, ENABLE);
    
    /* Send read data command */
    GDMA_SetBufferSize(GDMA_TX_Channel, 1);
    GDMA_SetSourceAddress(GDMA_TX_Channel, (uint32_t)GDMA_WriteCmdBuffer);
    GDMA_Cmd(GDMA_TX_CHANNEL_NUM, ENABLE);
    

功能实现

  1. SPI 通过 DMA TX 通道发送读 JEDEC_ID 指令给到外部 flash。DMA 传输完毕后,触发中断,进入中断处理函数 GDMA_TX_Channel_Handler,打印相关信息,清除中断标志位。

    void GDMA_TX_Channel_Handler(void)
    {
        GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, DISABLE);
        DBG_DIRECT("[io_gdma]GDMA_TX_Channel_Handler: TX completed!");
        GDMA_ClearINTPendingBit(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer);
        GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
    }
    
  2. flash 向主机回复 JEDEC_ID 信息,主机通过 DMA RX 通道接收数据。数据搬运完成时,触发中断,进入中断处理函数 GDMA_RX_Channel_Handler

    1. 失能 DMA 中断。

    2. 执行 uart_senddata_continuous ,将收到的数据内容发送至 PC 端串口助手。

    3. 清除中断标志位。

    void GDMA_RX_Channel_Handler(void)
    {
        GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, DISABLE);
    
        T_IO_MSG int_gdma_msg;
    
        int_gdma_msg.type = IO_MSG_TYPE_GDMA;
        int_gdma_msg.subtype = 0;
        int_gdma_msg.u.buf = (void *)GDMA_Recv_Buffer;
        if (false == app_send_msg_to_apptask(&int_gdma_msg))
        {
            APP_PRINT_ERROR0("[io_gdma]GDMA_Channel_Handler: Send int_gdma_msg failed!");
            //Add user code here!
            GDMA_ClearINTPendingBit(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer);
            return;
        }
        GDMA_ClearINTPendingBit(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer);
        GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
    }
    
    void io_handle_gdma_msg(T_IO_MSG *io_gdma_msg)
    {
        uint8_t *p_buf = io_gdma_msg->u.buf;
        uint16_t data_len = (GDMA_TRANSFER_SIZE);
        APP_PRINT_INFO1("[io_gdma] io_handle_gdma_msg: read data complete,data_len = %d", data_len);
        uart_senddata_continuous(UART0, p_buf, data_len);
    }