基于单片机定时器+DMA实现CPU低负载、高精度数字波形输出!
- 2025-07-06 12:11:10
来源 | 嵌入式情报局
今天给大家分享一个在STM32微控制器中用于生成精确、低CPU负载波形(如PWM、脉冲序列)的经典的方法和案例技术。
该方法的核心思想: 就是利用定时器的精确计时能力自动触发DMA,再由DMA高效地将预定义好的“波形数据”(GPIO状态值)从内存传输到控制GPIO的寄存器(如 GPIOx->ODR
或 GPIOx->BSRR
)。整个过程几乎不需要CPU干预非常快速且高效。
如下是我梳理的大致示意图:

细化说明下
1、配置GPIO引脚注意
最好是将其配置为 “推挽输出” 模式,以获得足够的驱动能力。 如果是对于高频翻转(如 > 几MHz),通常选择 “高速” (High Speed) 输出模式,以减小引脚转换时间。速率要求不高时可选中速或低速。这个速度配置以前遇到过坑,这里提一下。
2、 配置定时器 (TIM)注意事项:
计数器每次从ARR
回到0(或0到ARR
,取决于计数模式)时,会产生一个 更新事件 。 f_update = TIM_CLK / (ARR + 1)
,后续也就是这个对应的TIM事件节奏去触发DMA进行数据搬运。
3、配置DMA注意事项:
DMA的配置非常重要,而且配置项也比较多,大家留意:
传输方向: 内存 -> 外设 (Peripheral)。
地址增量模式: 内存 (Memory): 递增(数组地址递增)。外设 (Peripheral): 固定(目标寄存器地址不变)。
数据传输量: 通常设置为半字
(16bit) 或字
(32bit),因为GPIO->ODR
/ GPIO->BSRR
通常是16位或32位寄存器。需要和数组元素的数据类型匹配。
循环模式: 记得启用 循环模式。这样当DMA传输完数组最后一个元素后,会自动回到数组开头重新传输,实现连续波形输出。
请求源: 外设请求模式 (Peripheral Flow Controller)。设置触发源 (DMA请求) 为你在第2步选择的定时器对应的DMA请求(例如,TIMx_UP)。这是告诉DMA,由定时器的更新事件来触发每一次数据传输。
外设地址: (uint32_t)&GPIOx->ODR
或 (uint32_t)&GPIOx->BSRR
,推荐使用后者。
内存地址: (uint32_t)waveform_array
(数组的起始地址)。
设置传输数量: DMA_CNDTRx = 数组长度
。对于循环模式,这个值初始化后由DMA硬件管理,并在传输中递减,回绕后自动恢复。
这是一个关键步骤!数组中的每个元素对应在定时器每次触发时,将要写入GPIO->ODR
或GPIO->BSRR
的值,所以你需要输出这样的波形,那么就怎么去设计这个数据波形数组。
如果你使用GPIO->ODR需要改变单个引脚的状态会改变整个端口的状态。如果其他引脚也被用作输出(或你不希望被干扰),操作会比较麻烦且影响效率。数组的值通常是(当前ODR & ~pin_mask) | desired_level
的计算结果,所以用起来比较麻烦,这里就不多说了重点讨论下GPIO->BSRR:
BSRR
寄存器设计用来原子地置位 (Set) 和复位 (Reset) GPIO引脚。写入1
到BSR的低16位(BSx
)置位相应引脚,写入1
到高16位(BRx
)复位相应引脚。写入0
无效。
数组元素应为32位值。低16位用来置位引脚,高16位用来复位引脚。
只需设置你关心的引脚:例如,要让PA0
输出高电平,数组元素值为 (1 << 0) | (0 << 16)
(只需设置BS0
为1)。
要让PA0
输出低电平,数组元素值为 (0 << 0) | (1 << 0)
(只需设置BR0
为1)。注意这里的 (1 << 0)
是设置 BR0
,所以更清晰地写法是 (0x00000000) | (0x0001 << 16)
或 (0 << 0) | (1 << 16)
? 标准做法是 (0) | (1 << (16 + pin_number))
。
实际上最直接的是:
置位PA0: 1 << pin,
复位PA0: 1 << (pin + 16)
波形数组长度决定了整个输出序列的长度(一个周期)。每个元素控制一次翻转(上升或下降沿)。对于简单的周期性波形,只需要2个元素:一个置位(高)、一个复位(低)。对于更复杂的脉冲序列(如PPM编码、WS2812时序),数组需要包含完整的脉冲高低电平序列。数组长度也会影响DMA传输频率分频后的波形频率。
一旦启动,波形输出由硬件完成,CPU基本空闲。如果你需要动态改变输出波形,可以在DMA传输完成中断 (TCIF) 发生时(或使用双缓冲模式)修改数组内容(但注意同步问题)。修改PSC
/ARR
会立即改变触发频率(从而改变输出频率)。修改数组长度需要重新配置DMA的CNDTR
寄存器(操作不当会导致波形错乱或DMA挂起)。
该方法能产生非常精确的波形边缘时序,抖动主要来自DMA和总线仲裁延迟,比用软件翻转精准得多,当然也有一定的限制,最大翻转频率受限于GPIO最大输出速率、TIMER最大时钟频率、DMA速率以及数据准备效率。查阅STM32具体型号的Datasheet了解具体数值。
伪代码示例:
// 定义波形 (BSRR模式) - 50%占空比方波
uint32_t pwm_wave[2];
pwm_wave[0] = (1 << OUTPUT_PIN); // Set pin high (BSx = 1)
pwm_wave[1] = (1 << (OUTPUT_PIN + 16)); // Set pin low (BRx = 1)
// 配置GPIO
GPIO_InitTypeDef gpio_init = { ... OUTPUT_PIN, Mode: GPIO_MODE_OUTPUT_PP, Speed: GPIO_SPEED_FREQ_HIGH };
HAL_GPIO_Init(GPIOx, &gpio_init);
// 配置定时器 (e.g., TIM3)
TIM_HandleTypeDef htim3;
htim3.Instance = TIM3;
htim3.Init.Prescaler = psc_value; // Set to get desired TIM_CLK
htim3.Init.Period = arr_value; // Set ARR for f_update
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
// ... other settings ...
HAL_TIM_Base_Init(&htim3);
// 配置 TIM Trigger Output for Update Event
__HAL_TIM_SET_TRGO(&htim3, TIM_TRGO_UPDATE); // MMS=010 Update Event
// 配置 DMA
DMA_HandleTypeDef hdma;
hdma.Instance = DMA1_StreamX; // Choose correct Stream/Channel for TIM3_UP
hdma.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma.Init.PeriphInc = DMA_PINC_DISABLE;
hdma.Init.MemInc = DMA_MINC_ENABLE;
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // Assuming BSRR is 32bit
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma.Init.Mode = DMA_CIRCULAR; // Circular mode!
hdma.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma.Init.FIFOMode = ...; // Usually disable or threshold
HAL_DMA_Init(&hdma);
// Link DMA to TIM Update event
__HAL_DMA_REMAP(hdma.Instance, DMAMAP_TIM3_UP); // Many modern STM32 use HAL_DMAEx methods for remapping requests
// Or using HAL:
HAL_DMAEx_MultiBufferStart_IT(...); // Or simpler start if remapping is done in Init
// Associate DMA Handle with TIM's update request
__HAL_LINKDMA(&htim3, hdma[TIM_DMA_ID_UPDATE], hdma); // If using HAL DMA linked mechanisms
// Set DMA addresses and start
HAL_DMA_Start(&hdma, (uint32_t)pwm_wave, (uint32_t)&GPIOx->BSRR, 2); // 2 elements
// Start Timer
HAL_TIM_Base_Start(&htim3);
最后小结一下:
使用定时器+DMA+GPIO (BSRR) 的方法是在STM32上实现低CPU负载、高精度数字波形输出的绝佳方案。它非常适合以下场景:
多通道、高精度PWM控制(需要为每个端口/每组引脚分配一个DMA通道)。 生成复杂的脉冲序列(如IR遥控、数字协议)。 驱动LED灯带(如WS2812B, 需要精准的µs级脉宽)。 产生任意波形的基本信号源。 任何需要CPU脱手的周期性GPIO控制任务。
------------ END ------------
关注公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

- 点赞 0
-
分享
微信扫一扫
-
加入群聊
扫码加入群聊