STM32F4 DMA+PWM脉冲输出:实现任意数量脉冲的精确时序控制
👁 1 阅读 · 2026-08-27 · 嵌入式
在步进电机控制、精密运动平台或激光测距等应用中,精确输出指定数量的PWM脉冲是核心需求。传统的中断翻转GPIO方法受CPU负载和中断延迟影响,难以保证高频下的时序精度。本文深入讲解如何利用STM32F4的DMA(直接内存访问)配合定时器PWM模式,实现零CPU干预的任意数量脉冲输出,并给出完整的配置代码与工程实践要点,助你轻松掌握这一高效控制技术。
# STM32F4 DMA+PWM脉冲输出:实现任意数量脉冲的精确时序控制
## 为什么需要DMA+PWM方案?
在嵌入式系统中,产生精确的脉冲序列常用于步进电机驱动、脉冲计数或通信协议。传统方法依赖定时器中断,在中断服务函数中翻转GPIO电平,但存在两大痛点:
- **CPU占用高**:高频脉冲(如100kHz)下,中断频繁触发,CPU几乎被占用,无法处理其他任务。
- **时序抖动**:中断响应延迟受其他中断和指令执行影响,导致脉冲周期不稳定,影响控制精度。
DMA(直接内存访问)可以在无需CPU干预的情况下,将内存中的数据搬运到外设寄存器。结合定时器的PWM模式,我们可以通过DMA预先配置定时器的比较寄存器(CCR),从而在硬件层面自动改变PWM占空比或频率,实现精确的脉冲输出。
## 核心原理
STM32F4的定时器(如TIM1、TIM2等)支持PWM模式,输出频率由预分频器(PSC)和自动重载寄存器(ARR)决定,占空比由捕获比较寄存器(CCR)控制。当启用DMA请求后,每次更新事件(计数器溢出)或比较事件时,DMA会自动从内存读取新值写入CCR,从而改变下一个周期的占空比。
**关键点**:
- 若需输出N个脉冲,可预先在内存中准备N个CCR值(例如,前N-1个为正常占空比值,最后一个为0),DMA传输完成后触发中断,停止定时器。
- 或者使用定时器的重复计数功能,但DMA方式更灵活,可支持非均匀脉冲序列。
## 硬件与软件准备
- **硬件**:STM32F407开发板(或其他F4系列),示波器或逻辑分析仪。
- **软件**:STM32CubeIDE(或Keil MDK),STM32F4 HAL库。
## 配置步骤
### 1. 定时器与GPIO初始化
使用TIM2的通道1(PA0)作为PWM输出。配置定时器时钟源为内部时钟,预分频器设置分频系数,ARR决定PWM周期。
```c
// 定时器句柄
TIM_HandleTypeDef htim2;
void MX_TIM2_Init(void) {
TIM_OC_InitTypeDef sConfigOC = {0};
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Instance = TIM2;
htim2.Init.Prescaler = 84 - 1; // 84MHz/84 = 1MHz计数频率
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 100 - 1; // 1MHz/100 = 10kHz PWM频率
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 50; // 初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
// 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
```
### 2. 配置DMA
DMA负责将内存中的CCR值数组传输到TIM2的CCR1寄存器。使用DMA1的Stream5(TIM2_UP)或Stream7(TIM2_CH1),这里选择Stream5,因为更新事件触发更简单。
```c
DMA_HandleTypeDef hdma_tim2_up;
void MX_DMA_Init(void) {
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_tim2_up.Instance = DMA1_Stream5;
hdma_tim2_up.Init.Channel = DMA_CHANNEL_3; // TIM2_UP
hdma_tim2_up.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim2_up.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim2_up.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim2_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_tim2_up.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_tim2_up.Init.Mode = DMA_NORMAL; // 单次传输,传输完成停止
hdma_tim2_up.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_tim2_up);
// 关联DMA到定时器
__HAL_LINKDMA(&htim2, hdma[TIM_DMA_ID_UPDATE], hdma_tim2_up);
// 使能DMA传输完成中断
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}
```
### 3. 准备脉冲数据
假设需要输出100个脉冲,每个脉冲的占空比设为50%(即CCR=50)。但最后一个脉冲的CCR设为0,以便在输出第100个脉冲后停止。
```c
#define PULSE_COUNT 100
uint32_t ccr_values[PULSE_COUNT];
void prepare_pulse_data(void) {
for (int i = 0; i < PULSE_COUNT - 1; i++) {
ccr_values[i] = 50; // 占空比50%
}
ccr_values[PULSE_COUNT - 1] = 0; // 最后一个脉冲后停止
}
```
### 4. 启动脉冲输出
启动PWM,然后启动DMA传输。注意:DMA传输在定时器更新事件时触发,因此需要先使能定时器的DMA请求。
```c
void start_pulse_output(void) {
// 启动PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
// 启动DMA传输
HAL_DMA_Start_IT(&hdma_tim2_up, (uint32_t)ccr_values, (uint32_t)&htim2.Instance->CCR1, PULSE_COUNT);
// 使能定时器更新DMA请求
__HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE);
}
```
### 5. DMA完成中断处理
当DMA传输完所有数据后,会触发中断。在中断服务函数中停止PWM,并做后续处理。
```c
void DMA1_Stream5_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_tim2_up);
}
void HAL_DMA_TxCpltCallback(DMA_HandleTypeDef *hdma) {
if (hdma->Instance == DMA1_Stream5) {
// 停止PWM输出
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
// 可以设置标志位通知主程序
pulse_output_complete = 1;
}
}
```
## 完整示例代码
将上述代码整合,主函数中调用初始化并启动输出:
```c
volatile uint8_t pulse_output_complete = 0;
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM2_Init();
prepare_pulse_data();
start_pulse_output();
while (1) {
if (pulse_output_complete) {
// 处理完成事件
pulse_output_complete = 0;
// 可以重新准备数据再次启动
}
}
}
```
## 注意事项与优化
- **DMA模式**:使用`DMA_NORMAL`模式,传输完成后自动停止,避免循环覆盖。若需连续输出,可改用`DMA_CIRCULAR`模式,但需注意数据更新。
- **时序精度**:DMA传输在定时器更新事件时触发,因此每个CCR值的更新发生在计数器溢出时,不会影响当前周期的PWM输出,确保脉冲周期稳定。
- **内存对齐**:DMA传输要求数据地址对齐,建议将数组定义在4字节对齐的地址,可使用`__attribute__((aligned(4)))`。
- **中断优先级**:DMA完成中断优先级应设置较高,避免丢失。但注意与系统其他关键中断协调。
- **脉冲数量限制**:内存大小决定最大脉冲数,若需大量脉冲,可分段传输或使用循环模式。
- **占空比调整**:若需要不同占空比,只需修改数组中的值。例如,步进电机细分控制时,可生成正弦波占空比序列。
## 总结
通过DMA+PWM方案,STM32F4可以在不占用CPU的情况下精确输出任意数量的脉冲,且时序稳定。该方法适用于步进电机控制、脉冲计数等场景。掌握这一技术,能显著提升嵌入式系统的实时性和效率。希望本文能为你提供实用的参考。