GD32F470通过DMA输出PWM

最近在准备电赛,主控打算使用立创的梁山派(这是我手上现有的性能和扩展最强的MCU了),但梁山派网上资料并不多,通过DMA输出PWM折腾了大半个星期才搞定,坑可以说是一个接着一个,写下这篇文档既是为了防止我将来忘记流程,也算是给初试梁山派DMA的朋友们一点微小的帮助吧。

开发板:梁山派(主控GD32F470ZGT6)
开发环境:MDK v5.27
其它:万用表(测PWM输出电压)

一、官方例程解析

#include "gd32f4xx.h"
#include <stdio.h>

#define TIMER0_CH0CV  ((uint32_t)0x040010034)
uint16_t buffer[3]={249,499,749};

void gpio_config(void);
void timer_config(void);
void dma_config(void);

/*!
    \brief      configure the GPIO ports
    \param[in]  none
    \param[out] none
    \retval     none
*/
void gpio_config(void)
{
    rcu_periph_clock_enable(RCU_GPIOA);

    /*configure PA8(TIMER0 CH0) as alternate function*/
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_8);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_8);

    gpio_af_set(GPIOA, GPIO_AF_1, GPIO_PIN_8);
}

/*!
    \brief      configure the DMA peripheral
    \param[in]  none
    \param[out] none
    \retval     none
*/
void dma_config(void)
{
    dma_single_data_parameter_struct dma_init_struct;

    /* enable DMA clock */
    rcu_periph_clock_enable(RCU_DMA1);

    /* initialize DMA channel5 */
    dma_deinit(DMA1,DMA_CH5);

    /* DMA channel5 initialize */
    dma_init_struct.periph_addr = (uint32_t)TIMER0_CH0CV;
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_init_struct.memory0_addr = (uint32_t)buffer;
    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_16BIT;
    dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_ENABLE;
    dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;
    dma_init_struct.number = 3;
    dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;
    dma_single_data_mode_init(DMA1,DMA_CH5,&dma_init_struct);
    dma_channel_subperipheral_select(DMA1,DMA_CH5,DMA_SUBPERI6);

    /* enable DMA channel5 */
    dma_channel_enable(DMA1,DMA_CH5);
}

/*!
    \brief      configure the TIMER peripheral
    \param[in]  none
    \param[out] none
    \retval     none
*/
void timer_config(void)
{
    /* TIMER0 DMA Transfer example -------------------------------------------------
    TIMER0CLK = 120MHz, Prescaler = 120 
    TIMER0 counter clock = systemcoreclock/120 = 1MHz.

    the objective is to configure TIMER0 channel 1 to generate PWM
    signal with a frequency equal to 1KHz and a variable duty cycle(25%,50%,75%) that is 
    changed by the DMA after a specific number of update DMA request.

    the number of this repetitive requests is defined by the TIMER0 repetition counter,
    each 2 update requests, the TIMER0 Channel 0 duty cycle changes to the next new 
    value defined by the buffer . 
    -----------------------------------------------------------------------------*/
    timer_oc_parameter_struct timer_ocintpara;
    timer_parameter_struct timer_initpara;

    rcu_periph_clock_enable(RCU_TIMER0);
    rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);

    timer_deinit(TIMER0);

    /* TIMER0 configuration */
    timer_initpara.prescaler         = 199;
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.period            = 999;
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
    timer_initpara.repetitioncounter = 1;
    timer_init(TIMER0,&timer_initpara);

    /* CH0 configuration in PWM1 mode */
    timer_ocintpara.outputstate  = TIMER_CCX_ENABLE;
    timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;
    timer_ocintpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;
    timer_ocintpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;
    timer_ocintpara.ocidlestate  = TIMER_OC_IDLE_STATE_HIGH;
    timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    timer_channel_output_config(TIMER0,TIMER_CH_0,&timer_ocintpara);

    timer_channel_output_pulse_value_config(TIMER0,TIMER_CH_0,buffer[0]);
    timer_channel_output_mode_config(TIMER0,TIMER_CH_0,TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER0,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);

    /* TIMER0 primary output enable */
    timer_primary_output_config(TIMER0,ENABLE);

    /* TIMER0 update DMA request enable */
    timer_dma_enable(TIMER0,TIMER_DMA_UPD);

    /* auto-reload preload enable */
    timer_auto_reload_shadow_enable(TIMER0);

    /* TIMER0 counter enable */
    timer_enable(TIMER0);
}

/*!
    \brief      main function
    \param[in]  none
    \param[out] none
    \retval     none
*/
int main(void)
{
    gpio_config(); 
    dma_config();
    timer_config();

    while (1);
}

以上是GD32F4官方固件库中的例程,可以发现其由gpio_config、dma_config、timer_config三个部分组成。我将对其进行解析,逐步列举初始化过程中值得注意之处(AKA 坑) 。

1.外设基地址TIMER0_CH0CV

查询GD32F4的用户手册可知,该地址为通道 0 捕获/比较值寄存器地址。但在官方固件库的gd32f4xx_timer.c文件中提供了TIMER_CH0CV(timerx)函数,且TIMER_CH0CV(TIMER0)的返回值在数值上与宏定义中的地址是相等的,那么为什么会存在这么一个宏定义呢?
这个问题的答案其实非常简单。TIMER_CH0CV(timerx)的宏定义如下:

#define TIMER_CH0CV(timerx)              REG32((timerx) + 0x34U)           /*!< TIMER channel 0 capture/compare value register */

可以注意到其返回值并非常规的uint32_t类型,而是REG32。这实际上是GD32中一种特有的位运算,宏定义如下:

#define REG32(addr)                  (*(volatile uint32_t *)(uint32_t)(addr))

因此,函数不认为其是uint32_t类型变量,即使进行强制类型转换,传入periph_addr成员变量中也无法正确初始化DMA。故应直接使用宏定义规定要进行初始化的定时器基地址。

2.DMA数组buffer

本来buffer其实没什么好讲的(至少在DMA输出PWM里如此),但GD32F4的定时器偏偏喜欢给我整狠活,GD32F4的用户手册对L0定时器特性的部分描述如下:

主要特性
总通道数:4;
计数器宽度:16位(TIMER2&3),32位(TIMER1&4);
时钟源可选:内部时钟,内部触发,外部输入,外部触发;
……

注意到没?L0定时器宽度有16位和32位两种,而高级定时器宽度则只有16位,因此,当使用Timer 0,2,3,7时,buffer处可以照抄例程,但在使用定时器1和4时,buffer则一定要对应修改为uint32_t类型,下面DMA初始化结构体中的periph_memory_width变量也要做对应修改。

3.DMA外设通道选择

当我的队友(以及我)初次配置DMA时,都毫不犹豫的选择了TIMER1_CHx作为DMA通道。当时我们想当然的认为,一路PWM对应一路DMA,简直完美对吧?但例程中实际使用的是DMA_CH5和DMA_SUBPERI6,查GD32F4用户手册表10-3可知,DMA输出PWM实际使用的是TIMERx_UP通道而非TIMER_CHx通道!(加粗是因为我被这玩意坑惨了)

二、代码及补充(以Timer7为例)

#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"

#define TIMER7_CH0CV	((uint32_t)0x40010434)
/*需直接使用宏定义规定要进行初始化的定时器基地址*/

uint16_t buffer[3]={249,499,249};/*数据长度需与定时器宽度一致*/

void gpio_config(void);/*注意引脚复用功能是否正确*/
void timer_config(void);
void dma_config(void);/*注意选择的是TIMERx_UP通道*/

void gpio_config(void)
{
    rcu_periph_clock_enable(RCU_GPIOC);

    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_6);

    gpio_af_set(GPIOC, GPIO_AF_3, GPIO_PIN_6);/*复用为Timer7的0通道*/
}

void dma_config(void)
{
    dma_single_data_parameter_struct dma_init_struct;

    rcu_periph_clock_enable(RCU_DMA1);

    dma_deinit(DMA1,DMA_CH1);/*此处选择的是TIMERx_UP通道,即(CH1,SUBPERI7)*/

    dma_init_struct.periph_addr = (uint32_t)TIMER7_CH0CV;
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_init_struct.memory0_addr = (uint32_t)buffer;
    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_16BIT;/*单次传输数据宽度需与定时器宽度一致*/
    dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_ENABLE;
    dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;
    dma_init_struct.number = 3;
    dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;
    dma_single_data_mode_init(DMA1,DMA_CH1,&dma_init_struct);
    dma_channel_subperipheral_select(DMA1,DMA_CH1,DMA_SUBPERI7);

    dma_channel_enable(DMA1,DMA_CH1);
}

void timer_config(void)
{

    timer_oc_parameter_struct timer_ocintpara;
    timer_parameter_struct timer_initpara;

    rcu_periph_clock_enable(RCU_TIMER7);
    rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);

    timer_deinit(TIMER7);

    timer_initpara.prescaler         = 199;
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.period            = 999;
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
    timer_initpara.repetitioncounter = 1;
    timer_init(TIMER7,&timer_initpara);

    timer_ocintpara.outputstate  = TIMER_CCX_ENABLE;
    timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;
    timer_ocintpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;
    timer_ocintpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;
    timer_ocintpara.ocidlestate  = TIMER_OC_IDLE_STATE_HIGH;
    timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    timer_channel_output_config(TIMER7,TIMER_CH_0,&timer_ocintpara);

    timer_channel_output_pulse_value_config(TIMER7,TIMER_CH_0,buffer[0]);
    timer_channel_output_mode_config(TIMER7,TIMER_CH_0,TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER7,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);

    timer_primary_output_config(TIMER7,ENABLE);

    timer_dma_enable(TIMER7,TIMER_DMA_UPD);/*此处选择定时器更新事件*/

    timer_auto_reload_shadow_enable(TIMER7);

    timer_enable(TIMER7);
}

int main(void)
{

    systick_config();
	
	gpio_config();
	timer_config();
	dma_config();
	
    while(1) {
    }
}

三、经验教训

多翻芯片手册和数据手册,以及,不要想当然!不要想当然!不要想当然!


补充

1.关于为什么不能使用TIMER_CH0CV获取地址

这个是我后来在做超声波的时候才知道的。观察REG32源代码,其形式如下:

#define REG32(addr)                  (*(volatile uint32_t *)(uint32_t)(addr))

右边看着比较绕,我们一层一层来捋,首先是变量addr,然后是强转运算(uint32_t),表示将addr强制转换为32位无符号整形,再然后依然是强转运算(volatile uint32_t *),撇开volatile不看,可以发现它将32位无符号整形转换为指向32位无符号整形变量的指针,最后是解引用运算符 *,表示指针指向的地址中存储的变量。
因此,整个过程就是先将addr转换为指向32位无符号整型变量的指针,再获取指针指向的地址中存储数值。
而对GD32F470来说,地址0x40000000~0x5FFFFFFF为寄存器,因此REG32的功能为获取特定寄存器中存储的值
也因此,TIMER0_CH0CV(timerx)获取的也压根不是什么地址,而是定时器timerx的CH0CV寄存器的值。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值