【stm32进阶】定时器+ADC+DMA+乒乓缓冲区

目录

一、简介

1.配置定时器

2.配置ADC

3.配置DMA并使能中断

一些疑问解答

1.使用定时器定时触发ADC转换,那么在ADC两次触发的间隔中,DMA不会重复搬运数据导致缓冲区内出现大量的重复数据吗?

[2.为什么选择使用:定时器定时触发ADC转换,然后使用DMA进行数据转运。 而不是使用:ADC不停采集数据,然后定时器定时触发DMA进行数据转运?](#2.为什么选择使用:定时器定时触发ADC转换,然后使用DMA进行数据转运。 而不是使用:ADC不停采集数据,然后定时器定时触发DMA进行数据转运?)

3.在接收ADC数据时使用乒乓缓冲区有比使用单缓冲区有什么优势?


一、简介

选修单片机设计课程的期末作业是设计并实现一款简易心率测量装置,用到的心率传感器可使用单片机的ADC直接进行电压信号采集。对于这样的一个系统显然需要使用到ADC,其次对于心率信号的采集频率一般为250Hz,所以需要使用到定时器定时触发ADC采集,最后为了减轻CPU负担选择使用DMA进行数据搬运,并使用乒乓缓冲区(双缓冲区)存储数据,确保DMA和CPU不会发生数据访问冲突。

  • 定时器:决定采样率,例如250Hz(4ms一次),足够还原心率波形并留有滤波余量。

  • ADC:单通道采集传感器电压,由定时器触发,每次转换后自动产生DMA请求。

  • DMA :设置为循环模式,搬运ADC数据到数组。开启 半传输完成中断传输完成中断,实现双缓冲效果。

使用 定时器事件(TRGO)触发ADC转换,并结合DMA传输数据。这种方式由硬件自动完成,几乎不占用CPU资源,非常适合需要稳定采样率的应用,比如电机控制和数据采集系统。

定时器每溢出一次,就发出一个触发信号(TRGO)→ ADC收到信号,立即启动一次转换 → 转换完成后,DMA自动将结果搬运到内存数组中。

1.配置定时器

复制代码
void TIM3_Config(void) 
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

    // 1. 使能定时器时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

    // 2. 定时器基础配置
    // 时钟频率: 72MHz / (PSC+1) = 10kHz,即计数器频率10kHz
    // 重装载值: ARR+1 = 40,因此产生一次溢出事件的时间 = 40/10kHz = 4ms
    TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;   // 预分频器(PSC),分频后10kHz
    TIM_TimeBaseStructure.TIM_Period = 40 - 1;        // 自动重装载值(ARR),40个计数 = 4ms
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;   // 时钟分割,与ADC触发无关
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    // 3. 配置TRGO (关键步骤)
    // 将TIM3的更新事件(Update Event)作为TRGO输出,用于触发ADC
    TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);
}

2.配置ADC

复制代码
void ADC1_Config(void) 
{
    ADC_InitTypeDef ADC_InitStructure;

    // 1. 使能ADC1和GPIO时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置ADC输入引脚为模拟输入模式 (以PA0为例)
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. ADC基本配置
    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;          // 独立模式
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;               // 单通道模式
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;         // 禁用连续转换
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // 外部触发源: TIM3_TRGO
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;      // 数据右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 1;                     // 转换通道数
    ADC_Init(ADC1, &ADC_InitStructure);

    // 4. 配置ADC通道的采样时间 (此处设置转换时间约1us)
    // ADC时钟 = 72MHz/6 = 12MHz,采样时间 = 1.5周期,总转换时间约1.17us
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5);

    // 5. 使能ADC
    ADC_Cmd(ADC1, ENABLE);
    // 执行校准
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

3.配置DMA并使能中断

复制代码
// 定义两个缓冲区
#define BUFFER_SIZE 1024
uint16_t ADC_Buffer1[BUFFER_SIZE];
uint16_t ADC_Buffer2[BUFFER_SIZE];
uint8_t current_buffer = 0; // 0: 正在使用Buffer1, 1: 正在使用Buffer2

/* DMA初始化 */
void DMA1_Channel1_Config(void) 
{
    DMA_InitTypeDef DMA_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    /*********************************** NVIC部分 ***********************************/
    // 设置中断优先级分组(整个项目中只需设置一次,通常放在主函数开头)
    // 例如:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    // 1. 使能DMA1通道1的中断
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;         // 响应优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    // 2. 使能DMA传输完成中断(重要!)
    DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
    // 3. 使能DMA通道
    DMA_Cmd(DMA1_Channel1, ENABLE);


    /*********************************** DMA部分 ***********************************/
    // 1. 使能DMA时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // 2. DMA配置
    DMA_DeInit(DMA1_Channel1);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址:ADC数据寄存器
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_Buffer1;    // 内存地址:初始指向Buffer1,存满后后续在DMA中断函数中手动更换为Buffer2
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;               // 传输方向:外设 -> 内存
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;                  // 传输数据量
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;          // 内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度:半字(16位)
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;          // 内存数据宽度:半字(16位)
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                  // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;              // 优先级高
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;                     // 禁用内存到内存模式
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
}

/* 中断处理函数 */
void DMA1_Channel1_IRQHandler(void) 
{
    // 检查传输完成中断标志
    if (DMA_GetITStatus(DMA1_IT_TC1)) 
    {
        // 清除中断标志
        DMA_ClearITPendingBit(DMA1_IT_TC1);

        // 切换缓冲区
        if (current_buffer == 0) 
        {
            // Buffer1已满,切换DMA目标地址到Buffer2
            DMA1_Channel1->CMAR = (uint32_t)ADC_Buffer2;
            current_buffer = 1;
            // 在这里可以设置一个标志位,通知主循环处理 ADC_Buffer1 中的数据
            // 例如: process_buffer1_flag = 1;
        } 
        else 
        {
            // Buffer2已满,切换DMA目标地址到Buffer1
            DMA1_Channel1->CMAR = (uint32_t)ADC_Buffer1;
            current_buffer = 0;
            // 通知主循环处理 ADC_Buffer2
            // process_buffer2_flag = 1;
        }
    }
}

一些疑问解答

1.使用定时器定时触发ADC转换,那么在ADC两次触发的间隔中,DMA不会重复搬运数据导致缓冲区内出现大量的重复数据吗?

DMA传输的触发源是 ADC转换完成事件。也就是说,每次ADC完成一次新的转换,才会产生一个DMA请求,DMA才搬运一次数据。在两次定时器触发之间,ADC处于空闲状态,不会产生新的转换完成事件,因此DMA不会动作。

即:一次定时器触发 → 一次ADC转换 → 一次DMA请求 → 一次数据搬运。不会有多余的搬运。

2.为什么选择使用:定时器定时触发ADC转换,然后使用DMA进行数据转运。 而不是使用:ADC不停采集数据,然后定时器定时触发DMA进行数据转运?

  • 数据重复或丢失

    由于ADC的转换周期是固定的(例如20µs),而DMA触发间隔可能是1ms(远大于20µs)。在两次DMA触发之间,ADC可能已经完成了50次转换,但数据寄存器只保留最后一次转换的结果。当DMA触发时,它搬运的只是"当前时刻"的数据寄存器值,这相当于每50个转换结果中只采样了1个,而且这1个结果对应的转换时刻并不是严格等间隔的(因为DMA触发时刻与ADC转换完成时刻不同步)。如果定时器间隔小于ADC转换周期,DMA会多次搬运同一个未更新的数据,导致大量重复。

  • 无法控制采样时刻的精确性

    DMA搬运的瞬间,ADC可能正处于转换过程中(数据寄存器正在更新),此时读取的数据可能是不稳定的(半个周期更新)。虽然STM32的ADC数据寄存器有缓冲机制,但读取时机不当仍可能导致读取到旧数据或半完成数据。

  • 浪费功耗

    ADC连续不停地转换,即使系统不需要这么高的采样率,ADC也在全速工作,增加功耗。

  • 与硬件设计意图相悖

    STM32的DMA请求源与ADC转换完成事件是硬连接的。定时器触发DMA通常用于搬运定时器自身的捕获/比较数据,或者用于内存到外设的传输,而不是用来"定期读取ADC"。如果要实现"定时器触发DMA读ADC",需要额外配置定时器的DMA请求并手动管理ADC的启动,非常繁琐且容易出错。

特性 方案A:定时器→ADC→DMA 方案B:ADC连续+定时器→DMA
采样间隔 精确由定时器决定 不精确,依赖ADC转换完成时刻
数据重复/丢失 严重(间隔过大丢失,过小重复)
功耗 低(ADC间歇工作) 高(ADC连续工作)
实现复杂度 简单(标准库有示例) 困难(需额外处理同步问题)
硬件支持 原生支持 不支持(需要软件干预)

3.在接收ADC数据时使用乒乓缓冲区有比使用单缓冲区有什么优势?

特性 单缓冲区 乒乓双缓冲区
采集连续性 有间隙(需停止/重启 DMA) 完全连续,无间隙
数据覆盖风险 高(处理慢则覆盖) 无(切换指针隔离)
CPU 可用处理时间 ≤ 单个缓冲区填充时间 ≤ 整个缓冲区填充时间(可延长数倍)
DMA 中断频率 每个缓冲区一次 每个缓冲区一次(相同)
内存占用 1 × 缓冲区大小 2 × 缓冲区大小
实现复杂度 简单 稍复杂(需中断切换地址)
相关推荐
三佛科技-134163842122 小时前
家用电子血压计方案开发MCU控制芯片
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
进击的小头2 小时前
第4篇:嵌入式处理器内核全解析:ARM Cortex-M_R_A系列核心差异与选型指南
arm开发·单片机·嵌入式硬件
Heartache boy13 小时前
野火STM32_HAL库版课程笔记-手动建立工程模板与CubeMX后续用法(重要)
笔记·stm32·单片机·嵌入式硬件
可乐鸡翅好好吃16 小时前
UUID----私有服务与公有服务
嵌入式硬件
Wave84517 小时前
Freertos中PendSV与sysTick
单片机·嵌入式硬件
jghhh0117 小时前
带红外抄板和LCD显示的单相电能表设计
stm32·单片机·嵌入式硬件
wggmrlee18 小时前
GD32 vs STM32
单片机·嵌入式硬件
czhaii18 小时前
STM32 F103 Altium一键下载PCB图
stm32·单片机·嵌入式硬件
雾削木19 小时前
基于STM32F411RET6 + 双路MB85RS2MT的铁电U盘
stm32·单片机·嵌入式硬件