来源 | 嵌入式情报局


今天给大家分享一个在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硬件管理,并在传输中递减,回绕后自动恢复。


4、准备波形数据数组:

这是一个关键步骤!数组中的每个元素对应在定时器每次触发时,将要写入GPIO->ODRGPIO->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 ------------


图片

●专栏《嵌入式工具

●专栏《嵌入式开发》

●专栏《Keil教程》

●嵌入式专栏精选教程


关注公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

点击“阅读原文”查看更多分享。