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