简介:本资源是一份面向嵌入式初学者与STM32开发者的实战型调试指南,聚焦STM32F103芯片上ADC与DMA协同工作的完整实现方案,解决模拟信号高速采集时CPU负载高、数据丢包及调试困难等典型问题。压缩包含162个文件,总计5.45MB,涵盖28个C源文件(如stm32f10x_adc.c、stm32f10x_usart.c)、28个头文件(h)、28个编译中间文件(o)、27个依赖文件(d)及Keil工程核心配置(uvprojx、uvoptx、axf、hex等),结构完整,支持直接编译下载运行。已有369人学习下载,资源包含可复用的GPIO复用配置、ADC多通道采样、DMA循环传输、串口实时回传及配套中断服务程序,所有代码均基于Keil uVision5环境验证,目录模块清晰对应初始化→配置→启动→调试全流程,便于对照理解寄存器级操作与常见排错逻辑。
1. 项目概述:当ADC遇上DMA,一场效率革命
在嵌入式开发,尤其是基于STM32这类MCU的项目里,ADC(模数转换器)的数据采集是再常见不过的需求。无论是读取电位器的电压、监测电池电量,还是采集传感器的模拟信号,都离不开它。然而,很多新手,甚至一些有经验的开发者,在初次尝试使用DMA(直接存储器访问)来搬运ADC转换结果时,总会遇到各种“玄学”问题:数据对不上、数组里全是零、或者采样率飘忽不定。这背后的核心,往往不是代码逻辑错了,而是对ADC和DMA这两个外设如何协同工作的“潜规则”理解不透彻。
今天,我们就以经典的STM32F103C8T6(也就是常说的“蓝桥杯”或“最小系统板”核心芯片)为例,彻底拆解ADC多通道扫描+DMA循环传输这个经典组合的调试全过程。这个方案的价值在于,它把CPU从枯燥的“数据搬运工”角色中解放了出来。ADC转换完成一个数据,DMA就默默地把这个数据从ADC数据寄存器搬到我们指定的内存数组里,整个过程无需CPU干预。这意味着你可以实现极高频率、连续不断的采样,而CPU可以腾出手来处理更复杂的算法(比如滤波、PID计算)或者进行通信。对于需要实时性的应用,如音频处理、电机电流环采样,这几乎是必选项。
调试这个过程,就像在调试两个精密齿轮的咬合。你需要确保ADC的“节奏”(触发、转换时间)和DMA的“搬运动作”(传输宽度、地址增量)严丝合缝,任何一个参数设置不当,都会导致整个数据流紊乱。接下来,我将带你从原理到配置,从代码到调试,一步步打通这个流程,并分享那些手册上不会写,但实践中一定会踩的坑。
2. 核心原理与设计思路拆解
2.1 为什么是ADC+DMA?场景与优势分析
在深入代码之前,我们必须先想清楚:什么情况下需要用DMA来配合ADC?如果只是偶尔读一下某个通道的电压值,用轮询或者中断方式足矣。但当面临以下场景时,DMA的优势就无可替代:
- 多通道高速巡回采集:例如,需要同时监测系统的输入电压、电流、温度等多个模拟量。ADC的扫描模式可以自动按顺序转换多个通道,配合DMA,每转换完一个通道的数据就自动搬走,效率极高。
- 连续流数据采集:比如语音信号采集、振动传感器波形记录。这类应用要求采样间隔严格均等,不能有大的抖动。如果使用中断,进出中断栈的保存与恢复、可能被更高优先级中断打断,都会引入不可控的延迟。DMA由硬件触发,传输延迟极短且确定。
- 低功耗应用:在采集间隙,CPU可以进入睡眠模式(Sleep或Stop模式)。ADC转换完成由DMA搬运数据,并触发DMA传输完成中断来唤醒CPU进行批量处理,能显著降低系统平均功耗。
对于STM32F103,其ADC是12位的,最多有18个通道(包括内部温度传感器、参考电压等)。使用DMA,特别是循环模式,可以轻松实现一个“乒乓缓冲区”或环形队列,让数据采集和数据处理并行进行,这是实现高效实时系统的关键基石。
2.2 STM32F103的ADC与DMA硬件关联剖析
理解硬件是正确配置的前提。在STM32F103中,ADC和DMA的耦合非常直接。
ADC端的关键点:
- 扫描模式(Scan Mode):此模式开启后,ADC会按照在
SQRx(规则序列寄存器)或JSQRx(注入序列寄存器)中设定的通道顺序,一个接一个地自动进行转换。这是我们实现多通道采集的基础。 - 连续转换模式(Continuous Conversion Mode):此模式开启后,ADC在完成一个序列(单次或扫描)的转换后,会自动重新开始转换,形成连续不断的采集流。通常与DMA循环模式配对使用。
- DMA请求使能:ADC有一个专用的开关
ADC_CR2寄存器中的DMA位。只有打开它,每次ADC转换结束(对于规则通道,是EOCEnd Of Conversion信号)时,才会产生一个DMA请求。
DMA端的关键点:
- 外设地址:对于ADC1,其规则通道数据寄存器的地址是固定的(
ADC1_DR)。这个地址要写入DMA控制器的CPAR(外设地址寄存器)。DMA会从这个固定地址读取数据。 - 存储器地址:这是我们自己定义的数组(如
uint16_t adc_value[10])的首地址。它要写入DMA控制器的CMAR(存储器地址寄存器)。DMA会把数据搬到这里。 - 数据宽度:必须匹配!ADC数据寄存器是32位的,但低16位有效(对于12位ADC,数据右对齐时占用低12位或16位)。因此,通常设置外设数据宽度为半字(16位),存储器数据宽度也为半字。
- 地址增量:外设地址(
ADC1_DR)是固定的,所以外设地址增量应设为“不增量”。存储器地址(我们的数组)每次传输后需要移动到下一个元素,所以存储器地址增量应设为“增量”。 - 循环模式:这是实现连续采集的灵魂。当DMA传输计数器(
CNDTR)减到0后,如果开启了循环模式,计数器会自动重载为初始值,DMA传输永不停歇。这正好对应ADC的连续转换模式。
整个工作流程可以概括为:ADC连续转换 -> 产生EOC信号 -> 触发DMA请求 -> DMA从ADC1_DR搬运数据到数组 -> 数组地址自动增加,DMA传输计数器减1 -> 循环往复。CPU只在初始化时配置一下,之后就可以完全不管数据搬运,只需定期来检查数组里的数据即可。
3. 基于标准外设库的详细配置步骤
我们使用STM32标准外设库(StdPeriph_Lib)进行配置,这是理解寄存器最直观的方式。假设我们要用ADC1的通道0(PA0)、通道1(PA1)和通道8(PB0)进行三通道循环扫描采集。
3.1 硬件与软件环境准备
硬件:
- STM32F103C8T6最小系统板。
- 三个电位器或可调电压源,分别连接到PA0、PA1、PB0。或者用杜邦线将这几个引脚接到3.3V或GND进行简单测试。
- ST-Link/V2调试下载器。
- 串口模块(如CH340,用于打印调试数据,可选但强烈推荐)。
软件:
- Keil MDK-ARM 或 IAR Embedded Workbench。
- STM32F1xx标准外设库。
- 串口调试助手(如SSCOM、Putty)。
工程准备: 在IDE中新建工程,添加对应的库文件(stm32f10x_adc.c,stm32f10x_dma.c,stm32f10x_gpio.c,stm32f10x_rcc.c),并配置好头文件路径。
3.2 GPIO与时钟配置
任何外设使用前,必须先开启其时钟,并配置好引脚。
void ADC_DMA_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; /* 1. 开启时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // DMA1时钟在AHB总线 /* 2. 配置ADC通道对应的GPIO为模拟输入 */ // PA0, PA1 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure); // PB0 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_Init(GPIOB, &GPIO_InitStructure); /* 3. 配置ADC时钟,ADCCLK最大14MHz,由PCLK2分频得到 */ RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 假设系统时钟72MHz,则ADCCLK=12MHz }注意:GPIO必须设置为
GPIO_Mode_AIN,这是ADC的专用模式,可以关闭内部上拉下拉,获得最佳的采样精度。
3.3 DMA控制器配置
这里配置DMA1的通道1,因为ADC1的规则通道转换对应DMA1通道1。
/* 4. 配置DMA */ DMA_DeInit(DMA1_Channel1); // 复位DMA通道1 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); // 外设地址:ADC1数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_value_buf; // 内存地址:自定义数组 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:从外设(ADC)到内存 DMA_InitStructure.DMA_BufferSize = ADC_CHANNEL_NUM * ADC_BUFF_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); DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA通道关键参数解析:
DMA_BufferSize:这是整个循环的“节拍数”。我们计划用adc_value_buf这个数组存储数据,假设它的大小是ADC_CHANNEL_NUM * ADC_BUFF_SIZE。例如,3个通道,希望每个通道保留最近100次采样值,那么数组大小就是300,BufferSize也设为300。DMA会填满整个数组后,又从头部开始覆盖,形成循环。DMA_Mode_Circular:循环模式。与此对应的是DMA_Mode_Normal(正常模式),传输完指定次数后就停止,需要软件重新使能。对于连续采集,必须用循环模式。DMA_M2M_Disable:这个一定要确认!这是DMA的两种触发方式:内存到内存(软件触发),和外设到内存/内存到外设(硬件触发)。我们依赖ADC的EOC信号作为硬件触发源,所以必须禁用M2M模式。
3.4 ADC模块配置
这是最核心也是最容易出错的部分。
/* 5. 配置ADC */ ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 启用扫描模式(多通道必须) ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 启用连续转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 使用软件触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = ADC_CHANNEL_NUM; // 规则通道序列长度,这里是3 ADC_Init(ADC1, &ADC_InitStructure); /* 6. 配置ADC规则通道的转换顺序和采样时间 */ // 规则序列1:通道0,采样时间239.5周期(在12MHz ADCCLK下约20us) ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 规则序列2:通道1 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); // 规则序列3:通道8 ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 3, ADC_SampleTime_239Cycles5); /* 7. 使能ADC的DMA请求 */ ADC_DMACmd(ADC1, ENABLE); /* 8. 使能ADC */ ADC_Cmd(ADC1, ENABLE); /* 9. 复位校准寄存器并执行校准 */ ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); // 等待复位完成 ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成 /* 10. 软件触发启动第一次转换(之后由于连续模式,转换会一直进行) */ ADC_SoftwareStartConvCmd(ADC1, ENABLE); }致命细节与避坑指南:
- 顺序至关重要:必须先配置ADC的扫描和连续模式,并设置好通道序列,最后再使能ADC的DMA请求和ADC本身。如果顺序颠倒,DMA可能在ADC还未准备好时就开始响应请求,导致数据错乱。
- 采样时间计算:
ADC_SampleTime_239Cycles5是采样周期数。总转换时间 = 采样周期数 + 12.5个固定周期。例如,ADCCLK=12MHz,周期为1/12μs。采样时间239.5周期 ≈ 20μs,加上12.5周期,单次转换时间约21μs。三个通道扫描一次约63μs,采样率约15.8kHz。采样时间太短会导致采样不准确,特别是对于高阻抗信号源。 - 校准的必要性:ADC模块存在偏移和增益误差,上电后进行一次校准可以显著提高精度,尤其是测量小电压时。校准过程必须等待完成。
- 软件触发:我们使用了
ADC_ExternalTrigConv_None和ADC_SoftwareStartConvCmd。这意味着ADC的转换是由软件启动第一次,之后依靠“连续转换模式”自动重复。这是最简单可靠的触发方式。如果需要定时精确触发,可以配置定时器触发。
4. 数据读取、处理与调试实战
配置完成后,ADC数据就会源源不断地通过DMA写入我们预设的数组。但如何验证数据正确,并稳定地读取它们呢?
4.1 定义缓冲区与数据索引计算
#define ADC_CHANNEL_NUM 3 // 3个ADC通道 #define ADC_BUFF_SIZE 100 // 每个通道的缓冲区深度 uint16_t adc_value_buf[ADC_CHANNEL_NUM * ADC_BUFF_SIZE]; // DMA目标数组这个一维数组是DMA的搬运目的地。DMA会按顺序填充:[CH0_Sample1, CH1_Sample1, CH8_Sample1, CH0_Sample2, CH1_Sample2, CH8_Sample2, ...]。
为了从循环缓冲区中取出最新一轮(即最近一次完整扫描)的数据,我们需要一个“写索引”。DMA控制器内部有一个寄存器CNDTR(当前数据计数寄存器),它随着传输递减。我们可以通过计算得到当前DMA写到了数组的哪个位置。
// 获取当前DMA的写位置索引(相对数组头部的偏移量) uint32_t dma_current_index = (ADC_CHANNEL_NUM * ADC_BUFF_SIZE) - DMA_GetCurrDataCounter(DMA1_Channel1); // 计算上一轮完整扫描的起始索引 uint32_t last_round_start_index = (dma_current_index + ADC_CHANNEL_NUM) % (ADC_CHANNEL_NUM * ADC_BUFF_SIZE); last_round_start_index = (last_round_start_index == 0) ? 0 : (last_round_start_index - ADC_CHANNEL_NUM); // 读取上一轮三个通道的数据 uint16_t last_ch0_val = adc_value_buf[last_round_start_index]; uint16_t last_ch1_val = adc_value_buf[last_round_start_index + 1]; uint16_t last_ch8_val = adc_value_buf[last_round_start_index + 2];实操心得:直接操作
CNDTR计算索引是最高效的方式,避免了在DMA传输完成中断里更新软件索引的延迟和中断开销。对于实时性要求极高的处理,可以在主循环中定期计算并读取。
4.2 利用串口打印进行数据验证
将读取到的ADC原始值通过串口打印出来,是最直观的调试方法。需要先初始化USART,这里以USART1为例(PA9-TX, PA10-RX)。
void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // USART1_TX -> PA9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); // USART1_RX -> PA10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); } // 重定向printf到串口1(需在工程设置中勾选Use MicroLIB) int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }然后在主循环中,可以定期打印数据:
while(1) { // ... 计算并获取last_ch0_val, last_ch1_val, last_ch8_val ... printf("CH0: %4d, CH1: %4d, CH8: %4d\r\n", last_ch0_val, last_ch1_val, last_ch8_val); Delay_ms(100); // 延时100ms,避免打印太快 }打开串口调试助手,设置好波特率115200,你应该能看到三列不断变化的数字。将PA0接GND,对应值应接近0;接3.3V(VDDA),对应值应接近4095(12位满量程)。通过这个简单的测试,可以快速验证ADC采集和DMA传输的基本功能是否正常。
4.3 进阶:使用DMA传输完成中断进行双缓冲
对于需要处理每一帧数据的应用(如FFT分析),在DMA传输一半和全部完成时产生中断是非常有用的模式,即双缓冲(Ping-Pong Buffer)。
#define ADC_BUF_HALF_SIZE (ADC_CHANNEL_NUM * ADC_BUFF_SIZE / 2) uint16_t adc_ping_buf[ADC_BUF_HALF_SIZE]; uint16_t adc_pong_buf[ADC_BUF_HALF_SIZE]; // 在内存中,Ping和Pong缓冲区是连续的,但逻辑上分开 void DMA1_Channel1_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC1)) { // 传输完成中断(后半部分填满) // 处理Pong缓冲区数据 (adc_pong_buf) Process_ADC_Data(adc_pong_buf, ADC_BUF_HALF_SIZE); DMA_ClearITPendingBit(DMA1_IT_TC1); } if(DMA_GetITStatus(DMA1_IT_HT1)) { // 传输半完成中断(前半部分填满) // 处理Ping缓冲区数据 (adc_ping_buf) Process_ADC_Data(adc_ping_buf, ADC_BUF_HALF_SIZE); DMA_ClearITPendingBit(DMA1_IT_HT1); } }配置要点:在DMA初始化时,将DMA_MemoryBaseAddr设置为adc_ping_buf(即整个缓冲区的首地址),并使能DMA的中断(DMA_IT_TC和DMA_IT_HT)。这样,DMA会在搬运完前半部分数据时产生半传输中断,在搬运完全部数据时产生传输完成中断。你可以在中断服务程序里安全地处理另一半未被DMA当前写入的缓冲区数据,实现了数据采集和处理的并行流水线。
5. 调试过程中常见问题与解决方案实录
即使按照步骤配置,在实际调试中仍会遇到各种问题。下面是我在多个项目中总结的“故障排查清单”。
5.1 问题一:数组数据全为0或固定值
这是最常见的问题。
- 检查DMA是否使能:确认
DMA_Cmd(DMA1_Channel1, ENABLE);已执行。 - 检查ADC的DMA请求是否使能:确认
ADC_DMACmd(ADC1, ENABLE);在ADC使能之前被调用。 - 检查ADC是否真正启动:确认
ADC_SoftwareStartConvCmd(ADC1, ENABLE);已执行。可以用调试器查看ADC1->CR2寄存器的ADON和CONT位是否为1,SWSTART位是否被置位过。 - 检查DMA传输计数器:在调试模式下,观察
DMA1_Channel1->CNDTR寄存器的值。如果配置正确且ADC在转换,这个值应该从初始值(如300)开始递减。如果不变,说明DMA请求未触发。 - 检查GPIO模式:务必是
GPIO_Mode_AIN。如果误配置为浮空输入,读取到的可能是固定值。
5.2 问题二:数据错位或混乱
例如,通道0的数据跑到了通道1的位置。
- 检查DMA存储器地址增量:必须设置为
DMA_MemoryInc_Enable。如果禁用,所有数据都会写到数组的第一个元素。 - 检查ADC通道序列配置顺序:
ADC_RegularChannelConfig函数的第三个参数是转换顺序(1,2,3...),必须与DMA搬运到数组中的顺序预期一致。 - 检查数据宽度:
DMA_PeripheralDataSize和DMA_MemoryDataSize必须匹配。ADC数据寄存器是32位,但有效数据在低16位,所以通常都设为HalfWord。如果设为Byte,会导致高低字节拆分,数据完全错误。
5.3 问题三:采样率不稳定或远低于预期
- 计算总转换时间:单通道转换时间 = (采样周期数 + 12.5) / ADCCLK。扫描多个通道时,总时间 = 单通道时间 * 通道数。确保这个时间能满足你期望的采样间隔。
- 检查DMA传输速度:DMA传输本身很快,通常不是瓶颈。瓶颈多在ADC转换时间。
- 避免在DMA传输期间频繁打断:如果开启了DMA传输完成中断,并在其中进行复杂运算,可能会影响下一轮ADC转换的触发。对于高速连续流,建议使用循环模式而不开中断,在主循环或定时器中断中处理数据。
5.4 问题四:电压值测量不准,跳动大
- 电源与参考电压:STM32F103的ADC参考电压默认是VDDA(通常与VDD相连,3.3V)。确保VDDA和VSSA引脚连接了干净的电源,最好并联一个0.1uF和10uF的电容进行退耦。如果VDD有波动,ADC结果也会波动。
- 采样时间不足:对于高输出阻抗的传感器(如某些热电偶、光敏电阻),ADC内部的采样电容充电需要时间。增加
ADC_SampleTime可以显著改善。可以尝试ADC_SampleTime_239Cycles5或ADC_SampleTime_71Cycles5。 - 软件滤波:硬件上可以并联一个小电容在信号与地之间(如0.1uF)进行简单滤波。软件上可以对读取到的值进行滑动平均滤波、中值滤波等。
- 校准:务必执行ADC校准流程。
5.5 高级调试技巧:使用逻辑分析仪或示波器
如果问题非常诡异,软件调试无法解决,硬件工具是终极手段。
- 探测GPIO模拟触发:可以在ADC转换开始(或DMA传输完成)时,用一个空闲的GPIO口输出一个脉冲。用逻辑分析仪抓取这个脉冲和另一个由软件在读取到特定数据值时触发的脉冲,可以精确测量从ADC转换完成到CPU获知数据的延迟。
- 观察DREQ信号:更专业的方法是使用示波器观察ADC产生的DMA请求信号(对于STM32,这个信号在芯片内部,通常无法直接测量,但可以通过上述GPIO模拟法间接观察时序)。
6. 从标准库过渡到HAL库与CubeMX配置要点
现在很多新项目使用STM32CubeMX和HAL库。虽然配置过程图形化了,但底层原理不变,且有些“坑”被隐藏了,更需要理解。
在CubeMX中配置ADC多通道扫描+DMA的步骤:
- Pinout & Configuration标签页。
- 在
Analog->ADC1中,启用IN0,IN1,IN8等通道。 - 在
Parameter Settings中:Scan Conversion Mode: EnabledContinuous Conversion Mode: EnabledDMA Continuous Requests: Enabled (这个很重要!确保DMA请求连续)End Of Conversion Selection: EOC after each conversion (每转换一个通道产生EOC)Number Of Conversion: 3- 在
Rank中设置每个通道的Channel、Rank(顺序)和Sampling Time。
- 在
DMA Settings点击Add,选择ADC1,模式为Circular,数据宽度为Half Word。
HAL库关键代码差异:
- 启动函数变为
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_value_buf, BUFFER_SIZE)。这个函数一次性完成了使能ADC、使能DMA并启动转换的操作。 - HAL库的大坑:DMA传输完成中断回调函数
HAL_ADC_ConvCpltCallback会在缓冲区半满和全满时各调用一次,天然实现了双缓冲。但你需要清楚当前处理的是哪一半缓冲区。通常通过比较传入的缓冲区地址和预设的缓冲区首地址来判断。
我个人在实际使用HAL库时,依然推荐在初始化后,手动调用一次HAL_ADC_Start_DMA,并在回调函数中设置标志位,在主循环中处理数据,这样程序结构更清晰可控。CubeMX生成的代码是一个很好的起点,但理解其背后的配置,并知道如何根据实际需求调整(比如调整采样时间、DMA缓冲区大小),才是从“会用”到“精通”的关键。
调试ADC的DMA传输,本质上是在理解一套精密的硬件自动化流程。耐心地对照数据手册,用调试器和简单的打印信息验证每一个环节,从时钟、GPIO模式、ADC序列、DMA参数到最终的数据存储与读取,层层递进,问题总能被定位和解决。当屏幕上稳定地显示出你预期的电压数字时,那种对硬件掌控感带来的成就感,正是嵌入式开发的乐趣所在。
本文还有配套的精品资源,点击获取