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时钟。

__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_DMA2_CLK_ENABLE();
__HAL_RCC_TIM2_CLK_ENABLE();

2. 定时器PWM与触发配置

TIM2产生20kHz PWM,并设置更新事件为TRGO。

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请求使能。

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,双缓冲模式,外设地址固定,内存地址交替。

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与定时器

HAL_ADC_Start(&hadc1); // 使能ADC,等待外部触发
HAL_TIM_Base_Start(&htim2); // 启动定时器,开始产生PWM和触发信号

完整代码示例(关键部分)

// 全局缓冲区
#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负载,适用于高实时性控制场景。本文提供的配置方法和代码可直接应用于实际项目,开发者可根据需求调整采样率、缓冲区大小及触发源,构建更复杂的采样系统。