查看原文
其他

STM32定时器BURST传输介绍及示例

Miler 茶话MCU 2022-09-10


在STM32定时器应用中,我们有时希望基于某定时器事件同时对定时器的多个寄存器进行读写访问。为此,STM32芯片设计了专门应对定时器的多寄存器访问应用的DMA Burst传输。


高级定时器和部分通用定时器都支持针对定时器寄存器访问的BURST传输。所谓定时器的BURST传输,就是指当产生某定时器事件时,可以产生多个DMA请求,并触发多次DMA传输,访问多个定时器寄存器实现从内存到寄存器或从寄存器到内存的数据传输。这里的定时器事件可以是更新事件、比较匹配事件、换相事件以及触发事件。

我们知道,各定时器的所有寄存器都存放在片内某一固定地址开始的连续空间内。下图是我从STM32G4系列定时器地址分布图中截取的一部分,不同的定时器所拥有的寄存器个数可能有差异,但每个定时器的寄存器地址映射表的第一个寄存器一定是TIMx_CR1,所有寄存器在内存空间以字对齐的方式按顺序依次存放。【后面都以TIM2为例来说】


显然,我们在做定时器的DMA BURST传输时,除了配置基本的源地址、目的地址等DMA传输所需的通用配置信息外,还得告知DMA BURST传输模块每次传输时从哪个寄存器开始,连续访问几个寄存器,比方访问上图中圈出来的从TIMx_CCR1开始的连续4个寄存器。

这里有两个专门用于定时器BURST传输的寄存器,分别是TIM2_DCR和TIM2_DMAR. 其中TIM2_DCR就是用来配置从哪个定时器寄存器开始访问、连续访问几个寄存器的问题。【下面截图来自STM32G4参考手册】


DBA:被访问的第一个定时器寄存器相对于定时器地址映射表中的TIMx_CR1的地址偏移量【偏移量从0开始计算】。

DBL:每组BURST访问的寄存器个数【从0开始计算】。

仍然按照上面所说,访问从TIM2_CCR1开始的连续4个寄存器,可得知TIM2_CCR1位于寄存器地址映射表中的第14号位置,则DBA= 14-1;用于BURST分组访问的寄存器个数为4个,则DBL=4-1。


另外一个寄存器就是TIM2_DMAR。那它是干什么的呢?上面TIM2_DCR寄存器只是配置了被访问的首个定时器寄存器地址相对于TIMx_CR1的地址偏移量和每组要访问的寄存器个数。其中地址偏移量还只是个相对数,DMA访问最终是需要绝对地址的,而TIM2_DMAR就是来解决DMA访问时所需的绝对地址的。


DMA访问DMAR寄存器时,按照如下算式得到绝对地址实现对寄存器的逐个访问。(TIM2_CR1address) + (DBA + DMA index)x 4

[Index是DMA Burst访问时硬件自动生成的动态索引号,按0~DBL依次实现对多个寄存器的连续访问而完成BUSRT传输】

也就是说,对于定时器DMA BURST传输,外设地址一定是TIM2_DMAR寄存器的地址【或许是源地址,或许是目的地址】,DMA通过访问它,并根据上面算式实现对实际寄存器的访问。所以TIM2_DMAR寄存器又可称之为专门用于定时器DMA Burst传输的虚拟寄存器。


总的来讲,我们在做基于定时器的DMA BURST传输时,除了使用正确的DMAR寄存器地址作为外设地址外,再就是配置好DCR寄存器中的DBA与DBL参数,弄清从哪个寄存器开始访问,访问几个寄存器。其它配置环节跟通用DMA传输配置一样。


下面用个例子来演示相关用法。后面的验证基于STM32G474 Nucleo板。使用TIM2输出4路PWM,根据更新事件同步变化占空比,实现PWM占空比呈规律性的宽窄变化。即每次发生更新事件时,DMA到内存区取走4个对应于4个通道的比较寄存器的值赋给对应的比较寄存器[CCR1/CCR2/CCR3,CCR4],如下图所示,多组数据传输完毕后循环重来。


下面使用STM32CubeMx工具进行基本的初始化配置。

配置TIM2_CH1/CH2/CH3/CH4的PWM输出:

对定时器时基单元进行配置:

对TIM2更新事件的DMA传输做基本配置:

这里配置为循环模式,具体应用时可以根据具有应用来选择模式。将其它时钟、GPIO配置完毕后即可生成初始化工程文件。


在工程里添加用户应用代码。关于定时器BURST传输有专门的库函数可以给我们直接调用。它们分别是:


HAL_TIM_DMABurst_WriteStart()    ----(1)

HAL_TIM_DMABurst_ReadStart()       ----(2)


第一个函数用于将内存数据以DMA 分组模式写入寄存器的功能函数;

第二个用于将多个寄存器内容以DMA 分组模式读取到内存的功能函数;

不过呢,如果我们简单套用这两个函数有些时候可能出问题,或者遇到障碍。我们不妨一起来看看。

显然,我们要用到第一个函数。当我们进一步打开该函数时,发现它只是再调用了另外一个函数。

即它调用了HAL_TIM_DMABurst_MultiWriteStart()函数。这里就该函数用到的几个变量一起看下。

htim:即指向定时器结构体的地址,就不多说了。

BurstBaseAddress:前面提到过的第一个被访问寄存器的地址偏移量,即给到DCR寄存器中DBA的值。这里第一个被访问的是TIM2_CCR1,所在地址偏移量为13.

BurstRequestSrc:即触发DMA Burst传输的定时器事件源。这里是更新事件。

BurstBuffer:这个是存放数据的内存起始地址,如用户定义的数组地址。

BurstLength:就是前面提到的对应于DCR寄存器中DBL的值,即每组Burst传输的数据个数。具体到这里DBL应该是4-1,即3.

上面是固件里对该变量的定义。数据为什么这样定义,整整往左移了8位。看看上面DCR寄存器中DBL段所处位置就明白了。


最后看看紧随其后的另外一个数据量 ((BurstLength) >> 8U) + 1U;结合前面BurstLength的数据,该计算结果就是给到DMA的传输数据个数,数值等于每组  Burst传输的数据个数。具体到这里就是4。换言之,若我们将每组Burst传输的数据个数设为6,则这里的值就是6。这就意味着,如果按照该函数的现有用法,无论发生多少次Busrt传输只能用到一组数据。如果我希望在Burst传输中使用到多组不同数据【可能部分不同或全部不同】,就像上面示例所期望的那样,那怎么办呢?


这时我们可以基于现有库函数,在BURST传输需要用到多组不同数据时,直接使用

HAL_TIM_DMABurst_MultiWriteStart()函数并将其最后一个表示DMA传输长度的那个变量做适当修改。


比方在应用中每组BURST传输m个数据,一轮DMA传输过程中对应n个触发事件,在不同的触发时刻,每组传输的数据内容并不全部相同,这时总的DMA传输数据个数就是m*n。具体到这里,我要用到11组不同的数据,每组传输4个数据,即一轮DMA传输用到4*11个数据。


好,到此基本介绍和分析都差不多了,再看看具体用户代码。代码很简单,基于STM32HAL库的。


下面是用来调整不同时刻各个通道PWM占空比的内存数据,共11组。

要添加的用户参考代码都在下面,几行代码,应该说明白如画。主要是那个关于定时器DMA分组传输的那个函数,上面也已经详细解释了。

最后看看运行后的演示结果。

示波器只接了2个通通,目的就是演示同时修改4个通道的占空比,实现pwm占空比由窄到宽的规律性变化。


到此,关于定时器DMA Burst传输的介绍及示例就聊到这里。稍事小结:

1、从定时器DMA Burst传输原理的理解上讲,稍显小复杂。需要我们对定时器相关原理和DMA基础知识有较好的了解。在阅读STM32参考手册相关章节时,除了看正文部分外,还需细看TIMx_DMAR和TIMx_DCR寄存器的描述。但从实现代码角度看,使用CubeMx和固件库,其功能代码还是很简单的,将相关变量值对应地填进去即可。


2、STM32固件库的有些例程或函数侧重点在演示相应的功能或特性,但它不能包罗万象或保证适用于任何场景。有时我们可以在基于现有函数的前提下适当地做些改写调整,甚至完全重写代码以满足实际需求。


3、在做定时器DMA  Burst传输时,用来被成组访问的定时器寄存器应该是同一定时器的而且是地址连续的寄存器,不可跳跃访问。


4、上面的示例只是个示范,旨在了解该功能的用法和基本特性。实际应用中,往往还要涉及更多细节,比方各个定时器事件的特性、寄存器的预装功能的开或关、DMA相关知识等,最终结合实际需求加以灵活运用。

======================

往期话题阅读链接【点击阅读】:

1、对内部FLASH编程时遇到的ADC异常问题

2、DMA触发请求异常之案例分享

3、基于ARM MDK调试STM32的两个小提醒

4、基于STM32H7 DMA传输的SPI 应用示例

5、一个跟状态位处理有关的应用案例



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存