DMA + PWM + GPIO

该示例使用 DMA + PWM + GPIO 实现数字信号任意波形输出。

示例中使用 TIM5,定时周期 10us,定时产生 DMA 请求,实现 DMA 搬运数据到 GPIO 寄存器。通过改变 GPIO 输出高低电平时间 (DMA 源数据搬运次数*TIM 周期),模拟动态改变 PWM 波形占空比的功能。

例如:如果定义数据:{0xFFFFFFFF,0xFFFFFFFF,0x0},则 GPIO 输出高电平时间为 2*10us=20us,GPIO 输出低电平时间为 1*10us=10us,PWM 周期为 30us,占空比 2/3。其中 0xFFFFFFFF 表示高电平,0x0 表示低电平。

示例中可以通过配置宏 GPIO_OUT_REPEAT 选择是否重复输出波形。

环境需求

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

开发套件

Hardware Platforms

Board Name

RTL8752H HDK

RTL8752H EVB

更多信息请参考 快速入门

硬件连线

连接 P2_3(PWM 输出)和 P2_5(GPIO 输出)至逻辑分析仪观察波形。

配置选项

该示例可配置的宏如下:

GPIO_OUT_REPEAT :配置该宏可选择是否重复输出波形。

编译和下载

该示例的工程路径如下:

Project file: board\evb\io_sample\TIM\GDMA+PWM+GPIO\mdk

Project file: board\evb\io_sample\TIM\GDMA+PWM+GPIO\gcc

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

  1. 打开工程文件。

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

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

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

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

测试验证

  1. 开启宏 GPIO_OUT_REPEAT 时,可观察到 GPIO 输出的波形占空比设置与 GPIO_Ctl_AutoReload 数组中的设置相同,高电平所占 PWM 波形的周期数分别为 {1,2,3,5,3,2,1},依次循环,重复输出上述波形。

    这里应该是输出pwm的图片

    PWM 输出波形 - 重复输出

  2. 未开启宏 GPIO_OUT_REPEAT 时,可观察到GPIO输出波形占空比设置与 GPIO_Ctl_LLI 数组中的设置相同。将 GPIO_Ctl_LLI 数组中的所有值搬运完成后,GPIO引脚不再输出波形。

    这里应该是输出pwm的图片

    PWM 输出波形 - 非重复输出

代码介绍

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

  1. 源码路径

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

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

源码路径

  • 工程路径: sdk\board\evb\io_sample\TIM\GDMA+PWM+GPIO

  • 源码路径: sdk\src\sample\io_sample\TIM\GDMA+PWM+GPIO

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

└── Project: gdma_pwm_gpio
    └── 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_gpio.c
            └── rtl876x_tim.c
        ├── profile
        └── app                      includes the ble_peripheral user application implementation
            └── main.c

初始化

当 EVB 复位启动时,执行 main 函数,执行以下流程:

int main(void)
{
    extern uint32_t random_seed_value;
    srand(random_seed_value);
    __enable_irq();

    gdma_pwm_gpio_demo();

    while (1)
    {

    }
}

gdma_pwm_gpio_demo 中,包含 PAD/PINMUX 设置,GPIO,TIM 和 DMA 外设的初始化。

void gdma_pwm_gpio_demo(void)
{
    board_gpio_init();
    board_pwm_init();
    driver_gpio_init();
    driver_pwm_init();
    driver_gdma_multiblock_init();
}

board_gpio_init 为 GPIO 相关的 PAD/PINMUX 设置,包含如下流程:

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

  2. 配置 PINMUX:配置引脚为 DWGPIO 功能。

void board_gpio_init(void)
{
    Pad_Config(GPIO_OUTPUT_PIN_0, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_NONE, PAD_OUT_ENABLE, PAD_OUT_HIGH);
    Pinmux_Config(GPIO_OUTPUT_PIN_0, DWGPIO);
}

board_pwm_init 为 PWM 相关的 PAD/PINMUX 设置,包含如下流程:

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

  2. 配置 PINMUX:配置引脚为 TIM_PWM5 功能。

void board_pwm_init(void)
{
    Pad_Config(PWM_OUT_PIN, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_NONE, PAD_OUT_ENABLE, PAD_OUT_HIGH);
    Pinmux_Config(PWM_OUT_PIN, PWM_OUT_PIN_PINMUX);
}

driver_gpio_init 为 GPIO 外设的初始化,包含如下流程:

  1. 使能 RCC 时钟。

  2. 设置 GPIO 的引脚为输出模式,失能中断。

  3. 设置 GPIO 的控制模式为 Hardware 模式。

备注

GPIO 设定为 Hardware Mode 后,0x40011200 地址作为 GPIO Data Register(Software Mode 时 GPIO_DR 作为 Data Register),BIT0 ~ BIT31 分别 Mapping 到 GPIO0 ~ GPIO31;

void driver_gpio_init(void)
{
    /* Initialize gpio peripheral */
    RCC_PeriphClockCmd(APBPeriph_GPIO, APBPeriph_GPIO_CLOCK, ENABLE);

    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_StructInit(&GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin    = GPIO_PIN_OUTPUT;
    GPIO_InitStruct.GPIO_Mode   = GPIO_Mode_OUT;
    GPIO_InitStruct.GPIO_ITCmd  = DISABLE;
    GPIO_InitStruct.GPIO_ControlMode = GPIO_HARDWARE_MODE;
    GPIO_Init(&GPIO_InitStruct);

}

driver_pwm_init 为 TIM 外设的初始化,包含如下流程:

  1. 使能 RCC 时钟。

  2. 配置 TIM 为用户自定义模式。

  3. 使能 PWM 输出,配置 PWM 输出周期和占空比。

void driver_pwm_init(void)
{
    /* Initialize tim peripheral */
    RCC_PeriphClockCmd(APBPeriph_TIMER, APBPeriph_TIMER_CLOCK, ENABLE);

    TIM_TimeBaseInitTypeDef TIM_InitStruct;
    TIM_StructInit(&TIM_InitStruct);

    TIM_InitStruct.TIM_PWM_En = PWM_ENABLE;
    TIM_InitStruct.TIM_PWM_High_Count   = PWM_HIGH_COUNT;
    TIM_InitStruct.TIM_PWM_Low_Count    = PWM_LOW_COUNT;
    TIM_InitStruct.TIM_Mode = TIM_Mode_UserDefine;
    TIM_TimeBaseInit(PWM_TIMER_NUM, &TIM_InitStruct);

}

driver_gdma_multiblock_init 为 DMA 外设的初始化,包含如下流程:

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

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

  3. 设置传输数据大小为 32 位,每次 burst 固定传输一个数据。

  4. 设置源端地址为 GPIO_Ctl_AutoReload ,目的端地址为 0x40011200 (GPIO Data Register)。

  5. 配置 Handshake 为 GDMA_Handshake_TIM5 ,即每个 PWM 周期触发一次搬运。

  6. 使能 Multi-Block 传输。

  7. 若开启宏 GPIO_OUT_REPEAT,则设置传输模式为自动加载源端地址( GPIO_Ctl_AutoReload ),否则设置传输模式为自动加载 LLI 结构体中源端地址( GPIO_Ctl_LLI )。

  8. 设置 LLI 结构体的参数信息,详情可见 DMA Multi-Block 中的 初始化 章节。

  9. 配置和使能 DMA 的 IRQ 通道。

  10. 配置 DMA 总传输完成中断 GDMA_INT_Transfer,使能 DMA 传输,使能 TIM。

void driver_gdma_multiblock_init(void)
{
    /* Set gdma with GDMA_Handshake_TIM5, msize=1, transfer width = 32 */
    RCC_PeriphClockCmd(APBPeriph_GDMA, APBPeriph_GDMA_CLOCK, ENABLE);

    GDMA_InitTypeDef GDMA_InitStruct;
    GDMA_StructInit(&GDMA_InitStruct);

    GDMA_InitStruct.GDMA_ChannelNum          = GDMA_CHANNEL_NUM;
    GDMA_InitStruct.GDMA_DIR                 = GDMA_DIR_MemoryToPeripheral;
    GDMA_InitStruct.GDMA_BufferSize          = GDMA_TRANSFER_SIZE;//determine total transfer size
    GDMA_InitStruct.GDMA_SourceInc           = DMA_SourceInc_Inc;
    GDMA_InitStruct.GDMA_DestinationInc      = DMA_DestinationInc_Fix;
    GDMA_InitStruct.GDMA_SourceDataSize      = GDMA_DataSize_Word;
    GDMA_InitStruct.GDMA_DestinationDataSize = GDMA_DataSize_Word;
    GDMA_InitStruct.GDMA_SourceMsize         = GDMA_Msize_1;
    GDMA_InitStruct.GDMA_DestinationMsize    = GDMA_Msize_1;
    GDMA_InitStruct.GDMA_SourceAddr          = (uint32_t)GPIO_Ctl_AutoReload;
    GDMA_InitStruct.GDMA_DestinationAddr     = (uint32_t)(0x40011200);//vendor gpio reg address
    GDMA_InitStruct.GDMA_DestHandshake       = GDMA_Handshake_TIM5;
    GDMA_InitStruct.GDMA_Multi_Block_En     = 1;
    GDMA_InitStruct.GDMA_Multi_Block_Struct = (uint32_t)GDMA_LLIStruct;
#if (GPIO_OUT_REPEAT == 1)
    GDMA_InitStruct.GDMA_Multi_Block_Mode   = LLI_WITH_AUTO_RELOAD_SAR;
#else
    GDMA_InitStruct.GDMA_Multi_Block_Mode   = LLI_TRANSFER;
#endif
    for (int i = 0; i < GDMA_LLI_SIZE; i++)
    {
        GDMA_LLIStruct[i].SAR = (uint32_t)(&(GPIO_Ctl_LLI[i]));
        GDMA_LLIStruct[i].DAR = (uint32_t)(0x40011200);
        if (i == (GDMA_LLI_SIZE - 1))
        {
            GDMA_LLIStruct[i].LLP = 0;  //link back to beginning
            /* Configure low 32 bit of CTL register */
            GDMA_LLIStruct[i].CTL_LOW = BIT(0)
                                        | (GDMA_InitStruct.GDMA_DestinationDataSize << 1)
                                        | (GDMA_InitStruct.GDMA_SourceDataSize << 4)
                                        | (GDMA_InitStruct.GDMA_DestinationInc << 7)
                                        | (GDMA_InitStruct.GDMA_SourceInc << 9)
                                        | (GDMA_InitStruct.GDMA_DestinationMsize << 11)
                                        | (GDMA_InitStruct.GDMA_SourceMsize << 14)
                                        | (GDMA_InitStruct.GDMA_DIR << 20);
            /* Configure high 32 bit of CTL register */
            GDMA_LLIStruct[i].CTL_HIGH = GDMA_InitStruct.GDMA_BufferSize;
        }
        else
        {
            GDMA_LLIStruct[i].LLP = (uint32_t)&GDMA_LLIStruct[i + 1];
            /* Configure low 32 bit of CTL register */
            GDMA_LLIStruct[i].CTL_LOW = BIT(0)
                                        | (GDMA_InitStruct.GDMA_DestinationDataSize << 1)
                                        | (GDMA_InitStruct.GDMA_SourceDataSize << 4)
                                        | (GDMA_InitStruct.GDMA_DestinationInc << 7)
                                        | (GDMA_InitStruct.GDMA_SourceInc << 9)
                                        | (GDMA_InitStruct.GDMA_DestinationMsize << 11)
                                        | (GDMA_InitStruct.GDMA_SourceMsize << 14)
                                        | (GDMA_InitStruct.GDMA_DIR << 20)
                                        | (GDMA_InitStruct.GDMA_Multi_Block_Mode & LLP_SELECTED_BIT);//BIT(28) | BIT(27)
            /* Configure high 32 bit of CTL register */
            GDMA_LLIStruct[i].CTL_HIGH = GDMA_InitStruct.GDMA_BufferSize;
        }
    }

    GDMA_Init(GDMA_Channel, &GDMA_InitStruct);

    /* GDMA irq config */
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel         = GDMA_Channel_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelCmd      = (FunctionalState)ENABLE;
    NVIC_InitStruct.NVIC_IRQChannelPriority = 3;
    NVIC_Init(&NVIC_InitStruct);

    GDMA_INTConfig(GDMA_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);

    GDMA_Cmd(GDMA_CHANNEL_NUM, ENABLE);

    /* Set timer toggle */
    TIM_Cmd(PWM_TIMER_NUM, ENABLE);
}

功能实现

当 PWM 每输出一周期的波形时,触发一次 DMA 搬运,将数组内的数据搬运至 GPIO Data Register 中,并根据数据内容输出高低电平。

当 DMA 总传输完成时,触发 GDMA_INT_Transfer 中断,在中断处理函数内,清除 DMA 的中断挂起位。

void GDMA_Channel_Handler(void)
{
    GDMA_ClearINTPendingBit(GDMA_CHANNEL_NUM, GDMA_INT_Transfer);
}