1. 多通道ADC采集与DMA读取的工程实现原理
在嵌入式系统中,当需要对多个模拟信号源进行周期性、高精度、低CPU开销的数据采集时,单通道ADC配置已无法满足实际需求。典型的工业传感器阵列、多路环境监测节点或电机控制系统中,往往需同步采集温度、电压、电流、压力等多个物理量。此时,必须采用扫描模式(Scan Mode)配合DMA(Direct Memory Access)控制器构建高效的数据通路。本实验以STM32F103系列MCU为平台,基于ADC1外设,通过PA0–PA5六个GPIO引脚接入六路模拟输入,完整实现多通道连续采集与DMA自动搬运的工程闭环。其核心价值不在于功能罗列,而在于揭示ADC硬件状态机、DMA传输协议与CPU资源调度三者间的协同逻辑。
1.1 扫描模式的本质:硬件级通道轮询调度器
扫描模式并非软件循环调用的简化替代,而是ADC内部状态机的固有工作方式。当
ADC_InitTypeDef.ADC_ScanConvMode
设置为
ENABLE
时,ADC硬件将依据规则组(Regular Group)中预设的通道序列,自动完成通道切换、采样保持、逐次逼近转换(SAR)及结果寄存器更新的全流程。该过程完全脱离CPU干预,仅需一次启动指令(
HAL_ADC_Start()
),后续所有通道的转换均按序自动触发。关键参数
ADC_NbrOfChannel
定义了该序列长度——本实验中设为6,意味着每次ADC启动后,硬件将严格按顺序执行CH0→CH1→CH2→CH3→CH4→CH5共六次转换,形成一个完整的扫描周期(Scan Cycle)。此机制消除了软件延时引入的通道间时间抖动,保证了多路信号在时间轴上的严格对齐性,是实现同步采样的物理基础。
1.2 DMA传输的触发与数据流绑定
DMA在此架构中承担“数据搬运工”角色,其存在意义在于解除CPU对海量采样数据的搬运负担。ADC在每次转换完成(EOC, End of Conversion)时,会向DMA控制器发出一个硬件请求(Request Signal)。DMA接收到该请求后,立即执行一次内存写操作,将
ADC_DR
(Data Register)中的16位转换结果搬移至用户指定的缓冲区地址。本实验中,DMA通道(DMA1_Channel1)被配置为外设到内存(Peripheral to Memory)传输方向,且工作于循环模式(Circular Mode)。循环模式确保DMA在填满整个缓冲区后,自动重置地址指针,持续覆盖写入,避免因缓冲区溢出导致的数据丢失。这种“ADC硬件触发→DMA硬件搬运→内存自动填充”的三级流水线,将CPU从每毫秒数百次的数据读取中断中彻底解放,使其可专注于数据处理、通信协议栈或人机交互等更高价值任务。
1.3 时钟树与采样时间的协同约束
ADC的精度与速度直接受制于其时钟源(
ADCCLK
)与各通道采样时间(
ADC_SampleTime
)的精确配置。STM32F103的ADCCLK由APB2总线时钟(
PCLK2
)经预分频器(
RCC_CFGR
中
ADCPRE
位)分频得到,最大允许频率为14MHz。本实验中,若
PCLK2
=72MHz,则
ADCPRE=01
(二分频)可得
ADCCLK=36MHz
,但此值超限,故必须选择
ADCPRE=11
(八分频),使
ADCCLK=9MHz
,满足ADC规格书要求。采样时间则决定了模拟输入信号在采样电容上建立稳定电压所需的时间。
ADC_SampleTime_239.5Cycles
(239.5个ADC时钟周期)是本实验选用的最大采样时间,对应约26.6μs(239.5/9MHz)。该长采样时间虽牺牲部分吞吐率,却极大提升了对高源阻抗信号(如电位器分压输出)的采集精度,有效抑制了采样电容充电不足引起的量化误差。总转换时间(
Tconv
)由此计算:
Tconv = 采样时间 + 12.5个ADC时钟周期(SAR转换固定开销)
≈ 26.6μs + 1.39μs ≈ 28μs。单次扫描周期(6通道)理论耗时约168μs,远低于常见应用所需的毫秒级采样间隔,为系统留出了充足的处理裕量。
2. 工程化代码实现与关键配置解析
本节将基于HAL库框架,详细剖析多通道ADC+DMA方案的核心驱动代码。所有配置均围绕硬件原理展开,杜绝无意义的参数堆砌,每一行代码均有其明确的工程目的与物理意义。
2.1 GPIO与ADC外设时钟使能
// 使能GPIOA与ADC1时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_ADC1_CLK_ENABLE();
// 配置PA0-PA5为模拟输入模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 |
GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
原理阐释
:GPIO引脚必须显式配置为
GPIO_MODE_ANALOG
,这是ADC正常工作的前提。该模式禁用施密特触发器与上/下拉电阻,使引脚呈现高阻抗状态,最大限度减少对模拟信号源的负载效应。若误配为浮空输入(
GPIO_MODE_INPUT
)或带上拉/下拉,将导致ADC参考电压被拉偏,引入系统性测量误差。时钟使能顺序亦不可颠倒——必须先使能GPIO时钟再初始化GPIO,否则寄存器写入无效。
2.2 ADC初始化:扫描模式与通道序列配置
ADC_HandleTypeDef hadc1;
ADC_ChannelConfTypeDef sConfig = {0};
// ADC基本参数配置
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ENABLE; // 关键:启用扫描模式
hadc1.Init.ContinuousConvMode = ENABLE; // 连续模式,扫描周期自动重复
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 6; // 规则组包含6个通道
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
Error_Handler(); // 初始化失败处理
}
// 通道0配置(CH0 -> PA0)
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1; // 在规则组中排第1位
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 最大采样时间
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
// 通道1配置(CH1 -> PA1)
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2; // 在规则组中排第2位
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
// 依此类推,配置CH2(CH2), CH3(CH3), CH4(CH4), CH5(CH5)
// Rank值依次为3,4,5,6,构成完整的6通道扫描序列
原理阐释
:
NbrOfConversion=6
与六个
HAL_ADC_ConfigChannel()
调用共同定义了扫描序列的拓扑结构。
Rank
参数决定了硬件执行转换的严格顺序——Rank=1的通道最先转换,Rank=6的最后转换。此顺序直接映射到后续DMA搬运的数据在缓冲区中的存储位置。例如,若
Rank=1
对应CH0,则DMA缓冲区索引
[0]
、
[6]
、
[12]
…处的数据即为CH0的连续采样值。该设计为后续按通道分离数据提供了确定性内存布局,是实现通道级数据处理的基础。
2.3 DMA初始化与ADC-DMA绑定
DMA_HandleTypeDef hdma_adc1;
// DMA基本参数配置
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址固定(ADC_DR)
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR; // 关键:循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) {
Error_Handler();
}
// 将DMA句柄关联到ADC句柄
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
// 启用ADC的DMA请求
__HAL_ADC_ENABLE_DMA(&hadc1, ADC_DMA_CONTINUOUS_REQUEST);
原理阐释
:
PeriphInc=DISABLE
确保DMA始终从
ADC_DR
这一固定地址读取数据;
MemInc=ENABLE
则使DMA在每次传输后自动递增内存地址,将6个通道的转换结果依次存入缓冲区。
DMA_CIRCULAR
模式是实现持续采集的关键——当DMA填满整个缓冲区后,自动重置内存地址指针,开始新一轮覆盖写入,避免了因缓冲区满而触发的中断或传输停止。
__HAL_ADC_ENABLE_DMA()
宏最终将ADC的EOC事件与DMA通道1的请求信号在硬件层面连接,完成了“ADC转换完成→DMA自动搬运”的硬连线,此过程无需任何CPU参与。
2.4 数据缓冲区设计与DMA内存布局
// 定义DMA缓冲区:6通道 × 50次采样 = 300个16位数据
#define ADC_CHANNELS 6
#define SAMPLES_PER_CHANNEL 50
#define BUFFER_SIZE (ADC_CHANNELS * SAMPLES_PER_CHANNEL)
uint16_t adc_dma_buffer[BUFFER_SIZE]; // 全局缓冲区,供DMA直接写入
// 启动ADC与DMA
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, BUFFER_SIZE,
ADC_ALIGN_RIGHT, DMA_CIRCULAR);
原理阐释
:缓冲区大小
BUFFER_SIZE=300
的设计蕴含了工程权衡。
SAMPLES_PER_CHANNEL=50
并非随意设定,而是为后续软件滤波(如滑动平均)提供足够样本点。DMA以循环模式向此缓冲区写入,其内存布局遵循严格的通道顺序:
[0]
~
[49]
为CH0的50次采样,
[50]
~
[99]
为CH1的50次,
[100]
~
[149]
为CH2,依此类推。这种线性、可预测的布局,使得在数据处理阶段可通过简单的整数除法(
index / SAMPLES_PER_CHANNEL
)快速定位通道号,通过取模运算(
index % SAMPLES_PER_CHANNEL
)获取该通道内的采样序号,极大简化了数据索引逻辑。
3. 应用层数据处理与显示逻辑
DMA完成数据搬运后,CPU需在适当时机读取缓冲区内容,进行数值计算与用户界面更新。本节重点解析如何高效、准确地从循环缓冲区中提取有效数据,并转化为用户可理解的物理量。
3.1 DMA传输完成中断与数据就绪标志
// 在stm32f1xx_it.c中定义DMA传输完成中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc1);
}
// 在HAL_DMA_IRQHandler回调中,当DMA完成一次完整缓冲区填充时触发
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance == ADC1) {
// 标记DMA数据已就绪,供主循环检查
dma_transfer_complete_flag = 1;
}
}
原理阐释
:
HAL_ADC_ConvCpltCallback()
是HAL库提供的标准回调函数,当DMA完成对整个
BUFFER_SIZE
(300个字)的搬运后自动调用。此处设置全局标志
dma_transfer_complete_flag=1
,是一种轻量级的同步机制。主循环(
while(1)
)通过轮询此标志即可获知新一批50×6个采样数据已准备就绪,可安全读取。此方法避免了在中断服务函数内执行耗时的计算与显示操作,保障了中断响应的实时性。
3.2 通道级数据提取与滑动平均滤波
// 主循环中处理数据
if(dma_transfer_complete_flag) {
dma_transfer_complete_flag = 0; // 清除标志
// 对每个通道(0-5)计算50次采样的平均值
for(uint8_t ch = 0; ch < ADC_CHANNELS; ch++) {
uint32_t sum = 0;
// 累加该通道的50个采样值
for(uint8_t i = 0; i < SAMPLES_PER_CHANNEL; i++) {
uint32_t index = ch * SAMPLES_PER_CHANNEL + i;
sum += adc_dma_buffer[index];
}
// 计算平均值(整数除法)
uint16_t avg_value = sum / SAMPLES_PER_CHANNEL;
// 存储平均结果(例如存入ch_avg_values[ch]数组)
ch_avg_values[ch] = avg_value;
}
}
原理阐释
:滑动平均(Moving Average)是嵌入式系统中最常用、最有效的软件滤波技术之一,能显著抑制随机噪声。本实现中,对每个通道独立累加其在缓冲区中对应的50个连续采样值,再求平均。此过程利用了DMA循环写入的特性:当
dma_transfer_complete_flag
被置位时,缓冲区中
ch * 50
到
ch * 50 + 49
这50个位置的数据,必然是该通道最近连续50次转换的结果。这种确定性的数据时空关系,是滤波算法正确性的根本保障。平均值计算采用整数运算,规避了浮点运算的性能开销与精度损失。
3.3 ADC数值到物理电压的精确换算
// 假设VREF+ = 3.3V,ADC为12位分辨率(4096级)
#define VREF_MV 3300
#define ADC_RESOLUTION 4096
// 将12位ADC平均值转换为毫伏(mV)
uint32_t voltage_mv = (uint32_t)ch_avg_values[ch] * VREF_MV / ADC_RESOLUTION;
// 分离整数与小数部分用于显示(例如显示"3.299V")
uint16_t voltage_int = voltage_mv / 1000; // 整数部分(3)
uint16_t voltage_dec = voltage_mv % 1000; // 小数部分(299)
原理阐释
:换算公式
Voltage = (ADC_Value / 4096) * VREF
是ADC的基本原理。
VREF_MV=3300
代表参考电压为3.3V,
ADC_RESOLUTION=4096
是12位ADC的满量程计数值。为避免浮点运算并保证显示精度,全程采用整数运算:先将ADC值乘以3300(单位为毫伏),再除以4096,得到结果单位为毫伏。随后通过整数除法与取模,将毫伏值分解为整数伏特(
voltage_int
)和三位小数毫伏(
voltage_dec
),为LCD或串口显示提供格式化数据。此方法消除了浮点舍入误差,确保了显示值与理论值的严格一致性。
4. 硬件连接与实测验证要点
理论设计的最终价值体现在真实硬件上的可靠运行。本节总结实验板级连接的关键细节与常见问题排查思路,这些经验源于多次硬件调试的实践沉淀。
4.1 正点原子战舰开发板引脚映射确认
在正点原子战舰V3开发板上,ADC1的通道0至通道5严格对应GPIOA的Pin0至Pin5:
-
ADC1_IN0 → PA0
:位于P1端子排,标号为
ADC_IN0
-
ADC1_IN1 → PA1
:位于P1端子排,标号为
ADC_IN1
-
ADC1_IN2 → PA2
:位于P7端子排,标号为
ADC_IN2
-
ADC1_IN3 → PA3
:位于P7端子排,标号为
ADC_IN3
-
ADC1_IN4 → PA4
:位于P1端子排,标号为
ADC_IN4
-
ADC1_IN5 → PA5
:位于P1端子排,标号为
ADC_IN5
实践要点 :务必使用万用表蜂鸣档,在开发板上实测所选端子与对应PAx引脚的连通性。曾遇到某批次P1端子排焊接虚焊,导致PA4通道读数恒为0,此问题仅通过实测导通性得以快速定位。切勿完全依赖原理图或文档描述。
4.2 电源与参考电压的稳定性验证
ADC的精度直接受
VDDA
(模拟电源)与
VREF+
(参考电压)质量影响。战舰板默认使用
VDDA=3.3V
作为参考,其稳定性至关重要:
- 使用示波器观察
VDDA
引脚,纹波应小于10mVpp。若纹波过大,可在
VDDA
与
VSSA
(模拟地)之间并联一个10μF钽电容与100nF陶瓷电容。
- 测量
PA0
悬空时的ADC读数。理想情况下,由于输入高阻,读数应在0x000–0x00F范围内微小跳变。若读数稳定在某个非零值(如0x100),则表明
VDDA
存在共模噪声或
VSSA
接地不良。
4.3 实测数据解读与异常诊断
在串口终端或LCD上观察到的数据,其行为模式是判断系统健康状态的直观依据:
-
全通道读数恒为0
:检查PAx引脚是否被意外配置为推挽输出或上拉输入;确认
HAL_ADC_Init()
与
HAL_ADC_ConfigChannel()
调用无误;用万用表直流电压档测量PAx对地电压,确认信号源存在。
-
单通道读数恒为满量程(0xFFF)
:该通道引脚可能被短接到
VDDA
;或PCB走线存在与
VDDA
的隐性短路;检查
HAL_GPIO_Init()
中是否误将该引脚设为
GPIO_MODE_OUTPUT_PP
。
-
读数规律性跳变(如0x000, 0xFFF, 0x000, 0xFFF)
:典型DMA地址错位症状。检查
HAL_ADC_Start_DMA()
中传入的缓冲区地址是否为
(uint32_t*)
类型强制转换,而非
(uint16_t*)
;确认
DMA_PeriphDataAlignment
与
DMA_MemDataAlignment
均设为
HALFWORD
。
我在实际项目中曾因
DMA_MemDataAlignment
误设为
BYTE
,导致DMA将16位ADC值拆分为两个字节写入内存,后续读取时高位字节被解释为另一通道数据,引发完全不可预测的乱码。踩过几次坑之后,现在每次配置DMA都会用示波器抓取
DMA1_Channel1
的请求信号与
ADC_DR
读取时序,双重验证数据流的完整性。

215

被折叠的 条评论
为什么被折叠?



