STM32F4无中断高精度PWM采样:DMA双缓冲与定时器触发的协同设计
👁 2 阅读 · 2026-08-27 · 嵌入式
在电机控制、电源转换等高实时性应用中,PWM信号的精确采样至关重要。传统中断采样受CPU负载和中断延迟影响,难以保证一致性。本文深入解析STM32F4如何利用定时器触发ADC、DMA双缓冲搬运数据,实现全程无CPU干预的高精度PWM采样。通过原理剖析、寄存器级配置及完整代码示例,助你构建低延迟、零抖动的采样系统,显著提升控制环路性能。
# STM32F4无中断高精度PWM采样:DMA双缓冲与定时器触发的协同设计
## 为什么需要无中断采样?
在嵌入式控制系统中,PWM信号(如电机驱动、开关电源)的占空比和频率是核心反馈量。传统方法通过ADC中断读取采样值,但存在两大痛点:
- **中断延迟抖动**:CPU响应中断的时间受总线仲裁、优先级抢占影响,导致采样时刻不确定,引入相位噪声。
- **CPU占用**:高频采样(如100kHz)时,频繁中断会消耗大量CPU时间,挤占控制算法执行。
STM32F4的DMA双缓冲模式配合定时器触发,可让ADC在精确时刻自动转换,数据由DMA交替存入两个缓冲区,CPU仅在缓冲区切换时获得通知(或完全轮询),实现零中断、零抖动的采样链路。
## 硬件架构与工作原理
### 核心组件协同
- **定时器(如TIM2)**:产生PWM信号,同时输出触发信号(TRGO)到ADC。
- **ADC**:配置为外部触发模式,接收TRGO后启动转换。
- **DMA**:在ADC转换完成后自动搬运数据到内存,双缓冲模式使用两个缓冲区交替接收。
### 双缓冲机制
DMA双缓冲(Double Buffer)模式下,DMA控制器维护两个内存缓冲区(Memory0和Memory1)。当DMA传输完当前缓冲区后,自动切换至另一缓冲区,并触发传输完成中断(或更新标志)。这避免了单缓冲模式下,CPU必须在DMA传输完成前读走数据,否则数据被覆盖的问题。
### 触发时序
1. TIM2产生PWM,并配置更新事件(或比较事件)作为TRGO。
2. TRGO上升沿触发ADC启动转换。
3. ADC转换结束(EOC)触发DMA请求。
4. DMA将结果存入当前缓冲区,计数器递减。
5. 当缓冲区填满(传输次数完成),DMA切换缓冲区并置位传输完成标志。
整个过程CPU零参与,采样周期严格由定时器决定,消除了软件抖动。
## 配置步骤(以STM32F407为例)
### 1. 时钟配置
使用HAL库,启用ADC、DMA、TIM2时钟。
```c
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_DMA2_CLK_ENABLE();
__HAL_RCC_TIM2_CLK_ENABLE();
```
### 2. 定时器PWM与触发配置
TIM2产生20kHz PWM,并设置更新事件为TRGO。
```c
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 84-1; // 84MHz/84=1MHz
htim2.Init.Period = 50-1; // 1MHz/50=20kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
// 配置TRGO为更新事件
TIM_MasterConfigTypeDef sMasterConfig = {0};
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
// 启动PWM通道1(PA0)
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 25; // 50%占空比
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
```
### 3. ADC配置
ADC1采样通道0(PA0),外部触发,DMA请求使能。
```c
ADC_HandleTypeDef hadc1;
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE; // 非连续,由外部触发
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; // TIM2 TRGO
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = ENABLE; // 每次转换后触发DMA
HAL_ADC_Init(&hadc1);
// 配置通道
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
```
### 4. DMA双缓冲配置
使用DMA2 Stream0,双缓冲模式,外设地址固定,内存地址交替。
```c
DMA_HandleTypeDef hdma_adc;
hdma_adc.Instance = DMA2_Stream0;
hdma_adc.Init.Channel = DMA_CHANNEL_0;
hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc.Init.Mode = DMA_CIRCULAR; // 循环模式,双缓冲必须
hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc);
// 关联DMA与ADC
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc);
// 启动双缓冲传输
uint16_t buffer0[100];
uint16_t buffer1[100];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buffer0, 100); // 初始指向buffer0
// 注意:HAL库的Start_DMA不支持直接双缓冲,需手动设置DMA双缓冲寄存器
// 手动配置双缓冲(以寄存器方式)
DMA2_Stream0->M0AR = (uint32_t)buffer0;
DMA2_Stream0->M1AR = (uint32_t)buffer1;
DMA2_Stream0->CR |= DMA_SxCR_DBM; // 使能双缓冲模式
```
### 5. 启动ADC与定时器
```c
HAL_ADC_Start(&hadc1); // 使能ADC,等待外部触发
HAL_TIM_Base_Start(&htim2); // 启动定时器,开始产生PWM和触发信号
```
## 完整代码示例(关键部分)
```c
// 全局缓冲区
#define BUFFER_SIZE 100
uint16_t adc_buf0[BUFFER_SIZE];
uint16_t adc_buf1[BUFFER_SIZE];
volatile uint8_t buffer_index = 0; // 0表示当前使用buf0,1表示buf1
// DMA传输完成中断回调(可选,用于通知CPU处理)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1) {
// 检查当前活跃缓冲区(通过DMA的CR寄存器NDTR或M0AR/M1AR)
if ((DMA2_Stream0->CR & DMA_SxCR_CT) == 0) {
// 当前使用M0AR(buffer0),则刚完成的是buffer1
buffer_index = 1;
// 处理buffer1中的数据
process_samples(adc_buf1, BUFFER_SIZE);
} else {
buffer_index = 0;
process_samples(adc_buf0, BUFFER_SIZE);
}
}
}
void process_samples(uint16_t* buf, uint32_t len)
{
// 计算平均值、占空比等
uint32_t sum = 0;
for (uint32_t i = 0; i < len; i++) {
sum += buf[i];
}
float average = (float)sum / len;
// 根据PWM占空比计算电压等
}
int main(void)
{
HAL_Init();
SystemClock_Config();
// ... 初始化GPIO、ADC、DMA、TIM2(如上配置)
// 启动DMA双缓冲(使用HAL库的Start_DMA后手动改双缓冲)
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf0, BUFFER_SIZE);
// 手动设置双缓冲
DMA2_Stream0->M1AR = (uint32_t)adc_buf1;
DMA2_Stream0->CR |= DMA_SxCR_DBM;
// 启动定时器
HAL_TIM_Base_Start(&htim2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
while (1) {
// 主循环可做其他任务,采样数据在回调中处理
// 或者轮询DMA标志位(无中断模式)
if (DMA2_Stream0->CR & DMA_SxCR_TCIF) {
DMA2_Stream0->CR &= ~DMA_SxCR_TCIF; // 清除标志
// 处理数据...
}
}
}
```
## 注意事项与优化技巧
- **缓冲区大小**:必须与ADC转换次数匹配。若采样率20kHz,缓冲区100点,则每5ms切换一次缓冲区,CPU有足够时间处理。
- **DMA模式**:必须使用`DMA_CIRCULAR`循环模式,否则双缓冲不会自动切换。
- **数据一致性**:在回调中处理数据时,确保当前缓冲区不再被DMA写入。通过检查`CT`位(当前目标)判断哪个缓冲区正在使用。
- **触发源选择**:若需采样PWM中点(如电流采样),可配置定时器比较事件作为TRGO,而非更新事件。
- **ADC采样时间**:根据信号源阻抗调整采样时间,避免采样电容充电不足。
- **中断优先级**:若使用DMA传输完成中断,建议设为最高优先级,确保及时处理。
- **内存对齐**:缓冲区建议4字节对齐,提高DMA访问效率。
- **调试技巧**:利用逻辑分析仪观察TRGO和ADC转换的时序,验证触发同步。
## 总结
通过定时器触发ADC、DMA双缓冲搬运,STM32F4实现了完全无中断的PWM采样。该方法不仅消除了中断延迟抖动,还大幅降低了CPU负载,适用于高实时性控制场景。本文提供的配置方法和代码可直接应用于实际项目,开发者可根据需求调整采样率、缓冲区大小及触发源,构建更复杂的采样系统。