【DMA】DMA高级应用:DMA+定时器PWM模拟通信时序,解放CPU
目录
本文背景
在使用WT588F02B语音模块,或者DHT11这类以单总线进行通信的器件,往往其通信速率很低,一般情况下都是使用GPIO和delay延时来模拟这些这些通信时序。
以WT588F02B为例,截取其手册中的通信时序图如下:

实现方法对比
GPIO模拟
一般常规实现方法如下:
wt588_driver->spk_gpio_interface->pf_io_write(0);
wt588_driver->sys_interface->pf_delay_ms(5);
// 增加临界区保护
taskENTER_CRITICAL();
for(j = 0; j < 8; j++)
{
if(b_data == 1) // high level
{
wt588_driver->spk_gpio_interface->pf_pfio_write(1);
wt588_driver->sys_interface->pf_delay_us(600);
wt588_driver->spk_gpio_interface->pf_pfio_write(0);
wt588_driver->sys_interface->pf_delay_us(200);
} else {
wt588_driver->spk_gpio_interface->pf_pfio_write(1);
wt588_driver->sys_interface->pf_delay_us(200);
wt588_driver->spk_gpio_interface->pf_pfio_write(0);
wt588_driver->sys_interface->pf_delay_us(600);
}
s_data = s_data >> 1;
b_data = s_data & 0X01;
}
taskEXIT_CRITICAL();
wt588_driver->spk_gpio_interface->pf_io_write(1);
这是最容易想到的,也是实现最简单的一种方法。
但是其弊端很明显,其中用到了阻塞延时,会造成CPU执行效率下降,而且为了保证通信时序的正确性,发送数据时必须关全局中断或者进入临界区,这会造成系统中中断延时增大,甚至丢中断。
上述方法,仅发送一个字节就消耗(0.8us*8=)6.4ms,并且CPU在这6.4ms内什么事情都没法做,只是等待通信完成。
定时器PWM
分析发现,上述通信时序中,每个数据位都为周期800us的方波,数据1的高电平时间为600us,数据0的高电平时间为200us,并且都是高电平在前,低电平在后。
这种波形其实就是一个占空比在变化的PWM波,其具有两种占空比,很容易想到使用定时器的PWM输出来实现,将定时器的周期配置为800us,对于数据1和数据0分别配置不同的CCR比较寄存器数值,来产生不同的占空比。

大概配置方法如下:
-
将定时器周期配置为800us,向上计数,此处假设计数值为800
-
PWM输出配置为PWM1模式,PWM空闲电平为高电平,使能比较通道
-
配置PWM输出引脚,配置定时器的中断NVIC,打开定时器update中断
-
对于一个8位数据,对每一位进行遍历,得到所需要写入CCR寄存器的值,存储在一个数组中。例如:数据1对应600,数据0对应200
-
打开定时器计数
-
每来一次定时器update中断,就从第4步中的数组取出一个数写入到CCR寄存器中,直到该数组被用完,通信完成
PS:这种实现方法,已经比使用GPIO模拟要高效很多了,但是仍然存在一个问题:每发送一个字节,就需要产生8次定时器中断,这样对CPU来说或许太频繁,并且考虑到中断无法被及时响应的问题,生成的PWM波形不一定精准,当然对于WT588F02B模块来说精度是足够使用的。
定时器PWM+DMA
基于上述第二种方法,对其继续改进,可以想到使用DMA来帮我们搬运数组中的数值到CCR寄存器中,这样既不会产生大量中断,也能保证通信时序的绝对精确。
首先对这种方法做以简述,然后在文末给出详细实现步骤:
-
定时器的基本配置同方法二,但是无需打开定时器的中断
-
配置定时器的DMA功能,定时器update事件触发DMA请求,DMA将buffer中的数值搬运到CCR1寄存器中。
-
发出一个波形,首先拉低引脚5ms
-
遍历待发送数据的每个比特,写一个CCR1的数据buffer,例如要发送0x96

5. 遍历每一个bit,得到cmp_buff:
uint8_t data = 0x96;
uint32_t cmp_buff[8] = {0};
for(uint8_t i=0; i<8; ++i){
if((data >> i) & 0x01) // LSB优先
cmp_buff[i] = 600; // 数据1对应的pwm比较值
else
cmp_buff[i] = 200; // 数据0对应的pwm比较值
}
配置DMA的data_length等信息,打开定时器和DMA,定时器的update事件触发DMA请求,打开DMA的TC中断
DMA的TC中断代表一个数据发送完成,关闭DMA和定时器,引脚拉高
整个数据发送过程靠DMA和定时器输出比较来完成,只需要发送前算出一个CCR寄存器所需数值的数组即可,整个过程只触发一次DMA完成中断,时间精确可控。但复杂之处在于用到了定时器输出比较和DMA这些较为复杂的功能。
详细实现步骤
步骤一 CubeMX配置(STM32F411CEU6)
此处使用定时器2,首先选择时钟源和PWM通道:

定时器时基和PWM配置,这里是产生通信波形的关键点:

Pulse:"Pulse" 指的是定时器通道的比较寄存器(Capture Compare Register, CCRx)的初始值 。


为定时器配置DMA,注意DMA Request应该选择update事件,可以参考数据手册的DMA通道表格,所选择的通道必须是该定时器update事件可以触发请求的。DMA传输方向选择内存到外设,因为是将buffer数组中的数值搬运到外设寄存器中。CCR寄存器是32位的,数据宽度选择Word类型。


步骤二 初始化函数
编写WT588F02B初始化函数,涉及到一些底层外设的初始化,DMA中断回调函数的注册,模块上电演示等,实现如下:
/**
* @brief wt588f及底层外设初始化
*/
void wt588f_init()
{
HAL_DMA_RegisterCallback(
htim2.hdma[TIM_DMA_ID_UPDATE], HAL_DMA_XFER_CPLT_CB_ID, wt588f_dma_callback
); // 注册DMA中断回调函数
__HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE); // 定时器Update事件触发DMA请求
HAL_Delay(200); // WT588F02B模块上电延时
}
中断回调函数在后面步骤四中实现。
步骤三 实现数据发送函数
按之前给出的配置要点,写出代码,如下:
/**
* @brief 一线串口控制程序(单字节指令),使用定时器PWM + DMA方式
* @param data 待发送数据
*/
void wt588f_send_byte(uint8_t data)
{
// 根据待发送数据,配置cmp数组
// 数组前7个元素0,用于产生5.6ms低电平
// 数组最后一个元素,用于等待最后一个upd事件
static uint32_t cmp_buff[16] = {0};
cmp_buff[15] = 800;
for (uint8_t i = 7; i < 15; ++i) {
if (data & 0x01)
cmp_buff[i] = 600; // 数据1对应的pwm比较值
else
cmp_buff[i] = 200; // 数据0对应的pwm比较值
data = data >> 1;
}
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 开启PWM输出
HAL_DMA_Start_IT( // 配置并开启DMA
htim2.hdma[TIM_DMA_ID_UPDATE],
(uint32_t)cmp_buff,
(uint32_t)&htim2.Instance->CCR1,
16
);
tx_busy = 1; // 设置数据发送为忙状态
}
这个tx_busy位是一个静态变量,用于指示数据是否发送完成,在发送数据后被置位,在DMA中断函数中被复位。
步骤四 实现DMA中断回调函数
有关DMA的标志位清除、DMA传输Disable这些工作,HAL库的中断函数已经帮我们做好了,此处只需要关闭PWM输出,复位tx_busy变量用于指示数据传输完成。
void wt588f_dma_callback(DMA_HandleTypeDef* _hdma)
{
if (_hdma == htim2.hdma[TIM_DMA_ID_UPDATE]) {
tx_busy = 0; // 设置数据发送为空闲状态
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); // 停止PWM输出
}
}
中断回调函数已经在前面注册,HAL库的中断函数会自动调用该函数,因此无需其他操作。
编写测试函数
根据手册描述,编写一个连码发送的函数,发送数据F3 + 01 + F3 + 02
/*--------------------------------------
;模块名称:wt588f_list_play()
;功 能:一线串口单字节连码示例(F3 + 01 + F3 + 02)
;入 参:
;出 参:
;-------------------------------------*/
void wt588f_list_play()
{
wt588f_send_byte(0xF3);
while (tx_busy) {
}
HAL_Delay(2); // 延时2ms
wt588f_send_byte(0x01);
while (tx_busy) {
}
HAL_Delay(5); // 延时5ms
wt588f_send_byte(0xF3);
while (tx_busy) {
}
HAL_Delay(2); // 延时2ms
wt588f_send_byte(0x02);
while (tx_busy) {
}
HAL_Delay(5); // 延时5ms
}
注意此处测试,是在裸机环境下,使用了阻塞延时和while循环等待标志位。如果是在FreeRTOS环境下,只需要将阻塞延时替换为非阻塞延时,while循环等待替换为信号量或者任务通知即可。
这个测试函数,可以实现的效果为:语音模块先播放第一段语音,播放完成后立马播放第二段,见手册描述:

在main函数中调用初始化函数和测试函数:
wt588f_init();
wt588f_list_play();
下面是测试波形:

其他
使用定时器的DMA Burst传输模式,可以在每次产生DMA请求时,同时更新多个寄存器的值,适用于需要同时改变周期和多个PWM的占空比的情况。通过这种方法,可以产生一些通信时序,例如WT588F02B的单线串口通信时序。具体功能,可在定时器寄存器介绍部分找到:
本文仅使用普通DMA传输即可实现需求,如有需要同时改变周期和占空比的情况,可以考虑使用DMA传输的Burst模式。

更多推荐



所有评论(0)