STM32F103中ADC与DMA协同采集

AI助手已提取文章相关产品:

基于STM32F103的ADC与DMA协同采集技术深度解析

在工业控制、环境监测乃至消费电子中,我们常常需要对温度、压力、电流等物理量进行连续而精确的采集。这些信号本质上是模拟的,必须通过模数转换才能被MCU处理。当采样频率提高到几十甚至上百千赫兹时,传统的中断驱动方式很快就会让CPU疲于奔命——每完成一次转换就进入中断读取数据?那意味着每10μs就要打断主程序一次,系统几乎无法响应其他任务。

有没有办法让ADC自己“默默工作”,把数据自动存好,只在合适的时候提醒我们去处理?答案正是: ADC + DMA 协同机制

STM32F103作为一款经典的Cortex-M3微控制器,不仅集成了12位高精度ADC,还配备了强大的DMA控制器。将二者结合使用,可以构建一个近乎“零负载”的高效数据采集引擎。这不仅是嵌入式开发中的常见需求,更是实现高性能实时系统的底层基石。


STM32F103内置的ADC是一个逐次逼近型(SAR)结构,支持最高12位分辨率和多达16个外部输入通道(具体取决于封装)。它的工作流程分为三个阶段:采样、保持和量化。简单来说,就是先用内部电容对输入电压进行“抓取”并保持稳定,然后通过逐位比较的方式逼近真实值,最终输出一个数字结果。

这个过程由ADC时钟(ADCCLK)驱动,通常来源于APB2总线时钟(PCLK2)的分频。为了保证转换精度,ST推荐将ADCCLK控制在不超过14MHz。例如,若系统主频为72MHz,PCLK2也为72MHz,则需设置分频系数为6,得到12MHz的ADC时钟。

单次转换时间 = 采样周期数 + 12.5个固定周期
以最短采样时间1.5周期为例,总耗时约为14周期,在12MHz下约需1.17μs,理论上可支持高达800ksps的采样率(实际受限于架构和配置)。

但真正决定系统效率的,并不是ADC本身的速度,而是 如何获取这些数据

设想一下:如果每次转换完成后都靠CPU手动从 ADC_DR 寄存器读取结果,即使不考虑中断开销,仅执行一条加载指令的时间也可能超过下一个采样的到来时刻。更不用说频繁中断会打乱任何实时任务的执行节奏。

这时候,DMA的价值就凸显出来了。

DMA(Direct Memory Access)允许外设与内存之间直接传输数据,无需CPU参与。STM32F103配备两个DMA控制器,其中ADC1通常连接至DMA1 Channel1。一旦配置完成,每当ADC产生EOC(转换结束)信号,DMA就会自动触发,将 ADC_DR 中的值搬运到指定的内存缓冲区。

整个过程完全由硬件完成,CPU只需在初始化时设定好源地址、目标地址、数据长度和传输模式即可“放手不管”。这种机制带来的好处是颠覆性的:

  • CPU利用率大幅下降 :原本用于轮询或中断处理的代码几乎消失;
  • 数据吞吐能力显著提升 :不再受软件延迟限制;
  • 系统响应更快更稳定 :关键任务不会被高频中断打断;
  • 功耗优化潜力大 :MCU可在低功耗模式下运行,仅靠DMA维持采集。

尤其值得注意的是DMA的 循环模式(Circular Mode) 。启用该模式后,当缓冲区写满一圈,DMA会自动回到起始位置重新填充,形成一个环形缓冲区。这对于持续采集场景极为有利——比如音频流录制、振动监测或电机相电流采样。

配合半传输中断(HT)和全传输完成中断(TC),我们可以轻松实现双缓冲机制:DMA正在写前半段时,后台处理后半段;写后半段时,处理前半段。这样既避免了数据覆盖风险,又实现了真正的流水线操作。

来看一段典型的配置代码(基于标准外设库):

#include "stm32f10x.h"

#define ADC_BUF_LEN  1024
uint16_t adc_buffer[ADC_BUF_LEN];

void ADC_DMA_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // 配置PA0为模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 配置DMA1 Channel1
    DMA_DeInit(DMA1_Channel1);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = ADC_BUF_LEN;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    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);

    DMA_Cmd(DMA1_Channel1, ENABLE);

    // 配置ADC1
    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);

    // 启用ADC的DMA请求
    ADC_DMACmd(ADC1, ENABLE);

    // 使能ADC并校准
    ADC_Cmd(ADC1, ENABLE);
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    // 启动软件转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

这段代码完成了从GPIO、DMA到ADC的完整初始化。重点在于:
- 使用右对齐数据格式,方便提取12位结果;
- 设置最长采样时间(239.5周期),适用于高阻抗信号源;
- 启用连续转换模式,确保ADC不停歇;
- 开启DMA循环模式,构建无限采集缓冲。

初始化完成后,只要ADC一启动,数据就会源源不断地流入 adc_buffer ,整个过程无需任何干预。

当然,真正的挑战往往出现在细节之中。

比如,你是否意识到 模拟电源的质量直接影响ADC精度 ?VDDA和VSSA应独立布线,并加磁珠和滤波电容,否则数字噪声可能串入模拟域,导致测量漂移。再如,若信号源阻抗较高(如某些传感器输出),建议增加运放缓冲,否则采样电容充电不足会造成非线性误差。

还有采样率的问题。上面的例子依赖软件触发,属于自由运行模式,实际间隔受ADC自身速度影响,难以做到严格定时。更优的做法是 使用定时器触发ADC (如TIM3_TRGO),实现精准同步采样。这种方式不仅能保证奈奎斯特准则下的无混叠采集,还能与其他外设(如PWM、DAC)保持时间同步,在电机控制中尤为重要。

此外,缓冲区大小的选择也是一门艺术。太小容易溢出,太大则引入延迟。一个经验法则是:根据后续处理能力确定最大可接受延迟。例如,若FFT处理一批数据需要20ms,那么缓冲区最好不要超过这个时间跨度的数据量。

关于中断策略,也有几点值得强调:
- 若使用RTOS,务必注意DMA缓冲区的访问安全,必要时使用信号量或消息队列通知处理线程;
- 在中断服务函数中尽量少做复杂运算,优先通知主任务处理;
- 可结合空闲任务检测DMA当前写入位置,实现无中断的“软触发”采集。

最后提一句校准。虽然很多项目为了节省时间跳过ADC校准步骤,但在温差较大或精度要求较高的场合,忽略校准可能导致±5LSB以上的偏移。初始化时花几毫秒执行复位校准和增益校准,往往是值得的。


回到最初的问题:如何突破高采样率下的CPU瓶颈?

答案已经很清晰: 把搬运工的工作交给DMA,让CPU专注做决策者

在一个典型的应用架构中,信号路径如下:

[模拟传感器]
     ↓
[PA0 / ADC输入]
     ↓
[ADC模块] → 触发DMA请求
           ↓
       [DMA控制器]
           ↓
   [RAM环形缓冲区]
           ↓
[中断/主循环处理数据]
           ↓
[上传UART / 存储SD卡 / 执行控制算法]

在这个链条里,只有最后一个环节需要CPU介入。前面的所有数据流动都是静默且高效的。

举个实际案例:某智能仪表需要以50kHz采集电池电压和电流,同时运行SOC估算算法并与上位机通信。若采用传统中断方式,中断频率达到50k次/秒,系统根本无法正常工作。改用ADC+DMA+定时器触发方案后,中断频率降低至每1024个样本才触发一次(即约48Hz),CPU负载从接近100%降至不足20%,其余资源可用于算法计算和通信协议处理。

这种设计思路早已在工业PLC、无人机飞控、医疗设备中广泛应用。它不仅仅是一种技术组合,更代表了一种嵌入式系统设计哲学: 尽可能利用硬件自动化,释放CPU去完成更高价值的任务

对于追求极致性能与资源平衡的开发者而言,掌握ADC与DMA的协同机制,远不只是学会几个寄存器配置那么简单。它是通向高效、可靠、实时嵌入式系统的必经之路。

而这颗诞生已久的STM32F103,至今仍在用它的稳定表现告诉我们:经典之所以成为经典,是因为它把基础做得足够扎实。

您可能感兴趣的与本文相关内容

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值