【分享帖】使用 DMA + PWM 实现单计时器伪多路频率输出
本帖最后由 CYBT 于 2024-7-26 19:39 编辑
使用 DMA + PWM 实现单计时器伪多路频率输出
Tips: 阅读前请自动 #include <stdint.h>
Tips2: 本文写自某老灯,希望能为 RM 的发展做出一点微薄的贡献,让大家不要再为一些底层的琐事困扰。
Tips3: 本人水平有限,能力不强,仅作为抛砖引玉,提出一种可能的方案,大佬轻喷。
一、背景
PWM 是非常常用的外设,无论是图传云台的舵机、工程机器人上的各种抓钩等机构都有应用。传统的 PWM 调频方法依赖于使用 ARR 寄存器。然而不幸的是,同一个定时器的四个通道都共享一个 ARR。因而如果按照传统的方法需要输出 n 个不同频率的信号,就需要 n 个定时器。这对于 F1 系列等 MCU 无疑是难顶的。
二、提出
就像是从连续量到离散量的变化一样,很容易想到一种方法:用大量小的波形,能够拼凑出一个大的波形。这就是 DMA + PWM 产生多频率 PWM 的思路。例如我基于 100 KHz 频率的 PWM 波,在 10 ms 中,让前 400 个 脉冲的占空比为 1,后 600 个 脉冲的占空比为 0,那么合起来整个脉冲就是:400/100K = 0.004s = 4ms 的高电平 + 600/100K = 0.006s = 6ms 的低电平,合起来就是一个:周期为 10ms,占空比为 40% 的新脉冲。这样就形成了一个 100 Hz 的新脉冲。同理,我们可以控制不同的占空比分布,从而组成更多的频率和占空比。
![](https://yins-typora.oss-cn-zhangjiakou.aliyuncs.com/public_img/20240726153523.png)
按这种做法,理论只要我们能够有足够高的 PWM 频率(下称为基频),就可以生成任意不高于基频频率的 PWM 波了!当然我们的 CPU 不可能亲自更改每一个脉冲的 CCR 值,因而我们会划定一段时间(下称为调频周期),以调频周期为单位控制最终输出频率。且由于我们的调频周期长度是有限的,周期内的基频脉冲数是离散量而非连续量(总不能生成三分之一个基频吧?),所以要根据实际输出频率的范围,合理调整调频周期的长度以及基频的频率,以达到期望的输出频率分辨率。
很好,那我们现在就能尝试生成各种频率的 PWM 波了!
三、实践
以上面说到基频 100 KHz,调频周期 10 ms为例,一个调频周期中的脉冲数为:1000。我们要做的就是更改这 1000 个脉冲的占空比,并不断输入到 PWM 的 CCR 寄存器中。你肯定已经想到了,为什么标题要说使用 DMA 了(下面默认使用 STM32 带有 DMA 功能的 MCU)。
STM32 的 PWM 会在达到计数值后触发以此 DMA 请求,从而获得一个新的 CCR 数值到寄存器中。因而我们可以像如下一样配置 DMA:
- DMA 方向:显然是内存到外设啦
- Increment Address:CCR 是个寄存器不是缓存区,自然不能勾选
- 模式:选择 Circle 的话也有作用,后面再说。这里先选上 Normal
- Data Width:这里别看 HAL 库代码对 CCR 操作是用的 uint32_t 就选了 word,实际上寄存器是 16 位的,所以我们选 half word 以节省空间。
![](https://yins-typora.oss-cn-zhangjiakou.aliyuncs.com/public_img/20240726155032.png)
然后就可以按自己想要的方式填充 CCR 值,从而生成新脉冲了。以同样的方法,可以生成多个 CCR 序列填充到不同的方式中,这样便实现了使用同一个定时器控制多个频率。
这种方法的缺点是什么呢?显然就是占用大量 DMA 通道和内存了。不过嘛考虑到大多数友队代码(包括我自己的工程)也就串口会用到 DMA,而定时器在 F1 等芯片上相对更加宝贵;同时 MCU 的内存其实对大多数队伍来说都不太值钱,因而此法还是有一定价值的。
四、改进
1. 错位问题
问题的详情可以见连接文章:https://zhuanlan.zhihu.com/p/506458493
我在测试过程(使用F103ZET6)中也遇到类似问题。不过考虑到我们是输出频率,用于传递信息,错位似乎也无所谓?如果要求严格,就把末尾加个 0 吧。
2. 调频周期的控制
需要知道的是,当调频周期较长,同时基频较高时,DMA 的缓冲区是非常大的。这时候需要平衡好内存占用,同时需要注意一点:
我们是使用定时器控制调频周期内的 DMA 缓存,还是用 DMA 输出完成的中断来控制呢?
这里就需要各位自己测试,根据实际情况选择(个人建议用 DMA 中断控制)。
同时还要注意一点:缓存的刷写不是无限快的。实际测试下来,F103 使用 HAL 库(-O0优化)memset 清空一个 1000 个 CCR 值的缓存需要(好像是?)100us。如果你使用的是定时器唤起缓存刷写,且先 memset 清空再载入数据,就会导致 pwm 有一段奇怪的空窗期。所以最好是用 for 循环从头刷写,或者使用分段刷写等办法优化。
3. 输出个数的控制
很显然,这种方法可以自己控制 CCR 的值,也就可以输出指定脉冲个数。
4. 循环模式
当你的输出频率在较长时间内是固定的,就可以考虑使用 DMA 循环模式。循环模式的含义是:这段数据传输完后,如果没有关闭 DMA,则会重新再把这段数据再传以此。所以可以考虑使用循环模式优化内存用量和调频周期控制次数,从而节省资源。
5. 离散数据的梯形规划
提到 PWM 输出指定个数、频率的脉冲信号,大概率就是在用步进电机的。这里顺便把我在使用过程中对梯形规划的一些改进思路放进来,希望能有所帮助。
梯形规划大概作用就是:根据给定最大速度、最大加速度、期望位移值,获得一个限制了最大加速度的速度曲线,从而减少瞬间力矩的冲击。
梯形规划网上到处都是,但是离散化数据的梯形规划似乎不多。为什么要单独搞个离散化的梯形规划呢?因为步进电机的前进角度只能以步进角为单位,导致其速度只能是 step 为单位(即使微步,依然有最小单位)。直接对没有改进过的梯形规划进行速度取值就会涉及到浮点精度丢失的问题。最后大概率走出来的位移值和期望的不同。
这里建议使用如下开源梯形规划,进行离散化改进:https://blog.tqfx.org/posts/Trap ... -Profile-Trajectory (Tips:代码仓库在底下“实现”部分)
首先考虑:为什么会与实际期望位移值有偏差?实际就是对连续的梯形曲线进行采样时,原来可能是浮点的速度,变成了整形形成了偏差。因而最简单的解决方法是什么?就是完成梯形规划后,先模拟整个运动过程进行采样,算出偏差。再把这些偏差分散到整个运动过程中,就是先了对偏差的补偿。
我的改进思路就是:计算出轨迹后,计算补偿值。可以看如下伪代码:
```
int32_t avgComp = ctxImproved->intTrajCompensation / timeSpan; // 平均分散补偿到整个运动期间内
int32_t remainedComp = ctxImproved->intTrajCompensation % timeSpan; // 剩下的就尽量放到加速阶段,从第一位开始每时刻都补偿 1,补偿完为止
int32_t sign;
if (remainedComp != 0)
sign = remainedComp / abs(remainedComp); // 伪 sign 函数取正负一
if (x <= abs(remainedComp))
return round(FloatGetVel(&ctxImproved->ctx, (float) x - 0.5f))+ avgComp + sign;
else
return round(FloatGetVel(&ctxImproved->ctx, (float) x - 0.5f))+ avgComp;
}
```
通过这种方法,就可以基于上面的开源,实现运算量较小的补偿算法。
请问这篇文章对你有用吗?
【分享帖】使用 DMA + PWM 实现单计时器伪多路频率输出