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周期。

// 定时器句柄
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,因为更新事件触发更简单。

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个脉冲后停止。

#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请求。

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,并做后续处理。

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;
    }
}

完整示例代码

将上述代码整合,主函数中调用初始化并启动输出:

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的情况下精确输出任意数量的脉冲,且时序稳定。该方法适用于步进电机控制、脉冲计数等场景。掌握这一技术,能显著提升嵌入式系统的实时性和效率。希望本文能为你提供实用的参考。