最近在准备电赛,主控打算使用立创的梁山派(这是我手上现有的性能和扩展最强的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寄存器的值。