ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103 ADC与DMA配置调试全攻略:从原理到实战避坑指南

2026/9/3 8:56:38 拓冰建站 浏览量
STM32F103 ADC与DMA配置调试全攻略:从原理到实战避坑指南 简介本资源是一套面向嵌入式初学者与STM32开发者的ADCDMA协同调试实战工程聚焦STM32F103平台模拟信号高速采集场景解决传统轮询或中断方式导致CPU负载高、采样实时性差等典型问题。压缩包共162个文件含28个C源文件如stm32f10x_adc.c、stm32f10x_usart.c、28个头文件h、28个编译中间文件o/d/crf以及Keil uVision5专属工程配置文件uvprojx、uvoptx、dbgconf等和可执行镜像axf、hex、map总大小5.45MB完整呈现从GPIO复用、ADC参数配置、DMA循环传输到串口实时输出的全流程代码结构。已有369人学习下载配套工程可直接编译运行包含关键中断服务程序、DMA地址映射逻辑及串口数据发送机制特别适合掌握外设协同调试方法、理解寄存器级配置细节与排错路径的实践者。1. 从“点灯”到“数据流”为什么ADCDMA是STM32F103的必修课如果你是从51单片机或者Arduino转战到STM32F103的开发者第一个让你感到“高级”的功能很可能就是ADC模数转换器。在51上你可能需要写一个循环去等待ADC转换完成然后读取一个值。但在STM32上特别是当你需要连续、高速、多通道采集模拟信号时如果还用那种“查询-等待”的老方法你会发现CPU被牢牢绑死在ADC上什么其他事都干不了程序效率低得可怜。这时候DMA直接存储器访问就该登场了。简单来说ADC负责把模拟世界的电压变成数字世界的代码而DMA则是一个“不需要CPU插手”的搬运工。ADC每转换完一个数据DMA就默默地把这个数据从ADC的数据寄存器“搬”到你预先指定好的内存数组里。整个过程CPU只需要在初始化时配置好它们然后就可以去处理其他更重要的任务比如运行控制算法、刷新显示屏或者响应通信请求。等你需要数据时直接去那个内存数组里取就行。这种“解放CPU”的设计是STM32这类现代MCU发挥性能的关键。STM32F103的ADC配合DMA尤其适合一些对实时性有要求的场景。比如用多路ADC循环采集多个传感器的数据温度、压力、电压做一个简易的示波器或者数据记录仪实现音频信号的采集与处理。如果你还在用中断方式一个个地搬数据或者傻傻地轮询ADC状态标志那么调试ADC的DMA用法就是你从“单片机爱好者”迈向“嵌入式系统开发者”必须跨过的一道坎。这篇内容我就以一个调试者的视角带你捋清STM32F103上ADC DMA从配置、调试到问题排查的完整链路分享那些手册上不会写但实际开发中一定会踩的坑。2. 核心配置链路CubeMX可视化配置与代码生成现在STM32开发HAL库配合STM32CubeMX图形化配置工具几乎是标配它能极大减少底层寄存器配置的工作量。但工具用得好是帮手用不好就是“坑”的自动生成器。我们一步步来看。2.1 ADC模块的基础参数设定在CubeMX中选中你的STM32F103芯片首先找到ADC1通常我们使用ADC1。关键的配置选项卡在“Parameter Settings”里。1. 时钟配置与采样时间STM32F103的ADC时钟ADCCLK最大不能超过14MHz它由PCLK2APB2时钟分频而来。在“Clock Configuration”标签页确保APB2的时钟比如72MHz经过适当的分频比如6分频得到12MHz提供给ADC。这是ADC能正常工作的前提时钟不对后续一切调试都是徒劳。回到ADC配置“Clock Prescaler”选择对应的分频系数。“Resolution”选择转换精度12位是最常用的对应0-4095的数值范围。“Data Alignment”通常选择右对齐Right alignment这样读取到的就是一个普通的16位整数高4位为0。2. 扫描模式与连续转换这是多通道DMA采集的核心。“Scan Conversion Mode”必须设置为“Enabled”。这个模式允许ADC按照你设定的通道顺序自动进行一轮转换。“Continuous Conversion Mode”也建议设置为“Enabled”。这样当一轮所有通道转换完成后ADC会自动开始下一轮转换形成一个真正的连续数据流。如果不开启连续转换你需要在每次DMA搬运完成后手动触发ADC再次启动增加了软件的复杂性。3. DMA连续请求在“DMA Settings”标签页点击“Add”添加一个DMA通道。对于ADC1通常使用DMA1的通道1。这里有一个至关重要的选项“Mode”。它有两个选择“Normal”和“Circular”。Normal模式DMA在传输完指定数量的数据后会自动停止需要软件重新配置才能启动下一次传输。这适合单次采集任务。Circular模式DMA在传输完指定数量的数据后会自动回到内存缓冲区开头重新开始传输形成一个循环缓冲区。对于持续不断的ADC采集必须选择“Circular”模式。这是实现“设置一次永久运行”的关键。4. 通道配置与采样顺序在“Rank”标签页下添加你需要采样的ADC通道比如Channel 0, Channel 1…。每个通道需要设置“Sampling Time”采样时间。采样时间决定了ADC内部采样保持电容充电的时间时间太短会导致采样不准确尤其是在信号源阻抗较高时。对于大多数传感器如电位器、温度传感器设置成“239.5 Cycles”是一个比较稳妥的起点。通道的顺序就是ADC扫描转换的顺序也是你DMA数据缓冲区中数据的排列顺序。2.2 DMA的详细参数与内存视图添加DMA后双击它进入详细配置。Direction方向肯定是“Peripheral To Memory”从外设到内存。Increment AddressPeripheral设为“Disable”。因为ADC的数据寄存器地址是固定的我们总是从同一个地方ADC1-DR读数。Memory设为“Enable”。这样DMA每次搬运后目标内存地址会自动递增数据才能依次存放到数组的不同位置。Data WidthPeripheral选择“Word”32位。虽然ADC数据是12位但STM32F103的ADC数据寄存器DR是一个32位寄存器低16位有效以Word为单位访问是最高效的。Memory也选择“Word”。这样内存数组的数据类型应该是uint32_t。如果你选择“Half Word”16位则数组类型应为uint16_t。两者在存储12位数据时高位都是0但选择Word能与外设宽度对齐理论上更可靠。Mode如前所述选“Circular”。配置完成后点击GENERATE CODE生成代码。CubeMX会帮我们初始化ADC和DMA并在main.c中生成MX_ADC1_Init()和MX_DMA_Init()函数。但这仅仅是开始很多关键操作它并没有帮我们做。3. 启动与停止那些CubeMX没生成的关键代码生成的初始化代码搭建了舞台但演员还没上场。你需要手动添加启动和控制的代码。3.1 正确的启动顺序ADC和DMA的启动有严格的顺序弄反了可能导致DMA无法正常工作或者采集不到数据。一个稳健的启动流程如下// 1. 定义数据缓冲区 #define ADC_BUFF_SIZE 100 // 假设每个通道采集100次 #define ADC_CHANNEL_NUM 3 // 假设有3个通道 uint32_t adc_value_buf[ADC_BUFF_SIZE * ADC_CHANNEL_NUM]; // 缓冲区大小 单次采样点数 * 通道数 // 2. 在main函数初始化部分之后启动采集前 // 启动DMA传输 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_value_buf, ADC_BUFF_SIZE * ADC_CHANNEL_NUM); // 注意这里有一个巨大的坑 // HAL_ADC_Start_DMA的第三个参数是“数据长度”指的是“要传输的**数据项**总数”。 // 在我们的设置中ADC是12位DMA宽度是Word32位所以每个ADC转换值算一个“数据项”。 // 因此这个长度应该是 缓冲区总大小数组元素个数即 采样次数 * 通道数。 // 如果你错误地理解为“字节数”或“通道数”DMA的行为会非常诡异。为什么是这个顺序先启动DMA再由HAL_ADC_Start_DMA内部启动ADC。DMA需要先就位准备好接收数据的“篮子”。当ADC转换完成产生请求时DMA才能立刻响应并进行搬运。如果先启动ADC它转换完成的数据可能因为DMA未就绪而丢失虽然会有硬件标志但增加了不确定性。3.2 停止与重新启动的注意事项当你需要暂停采集或者修改参数时停止操作同样需要小心。// 停止ADC的DMA传输 HAL_ADC_Stop_DMA(hadc1);调用这个函数后ADC和对应的DMA传输都会停止。但这里有个细节DMA通道可能没有被完全禁用。如果你需要彻底重置DMA通道比如改变缓冲区地址或大小更安全的做法是调用HAL_ADC_Stop_DMA(hadc1)。调用HAL_DMA_DeInit(hdma_adc1)反初始化DMA。重新调用MX_DMA_Init()或HAL_DMA_Init()初始化DMA。最后再调用HAL_ADC_Start_DMA重新启动。特别是在调试阶段如果你发现停止后再启动数据不再更新很可能就是DMA通道状态没有正确复位。4. 数据在哪里如何验证与读取配置和启动都做了但程序跑起来你怎么知道数据在源源不断地进入缓冲区这是调试的第一步。4.1 利用调试器实时查看内存这是最直接有效的方法。以IAR或Keil MDK为例将程序下载到芯片并开始调试。在代码中设置一个断点或者直接让程序全速运行。打开“Memory”或“Watch”窗口。在地址栏输入你的缓冲区数组名adc_value_buf。你应该能看到一片内存区域里面的数据在动态地、有规律地变化。如果ADC采集的是一个变化的电压比如用手触摸ADC输入引脚引入人体感应信号你看到的数据变化会更明显。如何判断DMA是否在循环观察缓冲区末尾的数据。如果你的缓冲区大小是300100次*3通道当你看到第299个元素下标298也在变化并且变化之后下标0的元素紧接着被新数据覆盖这就清晰地证明了DMA工作在Circular模式缓冲区像一个环一样在被循环填充。4.2 通过串口打印数据虽然打印会影响实时性但在调试初期验证数据正确性非常有用。// 在main循环中定期打印缓冲区前几个数据 printf(ADC Values: %lu, %lu, %lu, %lu, %lu, %lu\r\n, adc_value_buf[0], adc_value_buf[1], adc_value_buf[2], adc_value_buf[3], adc_value_buf[4], adc_value_buf[5]);注意这里打印的是原始数字量0-4095。你需要根据参考电压通常是3.3V将其换算为实际电压值Voltage (ADC_Value / 4095.0) * 3.3。一个常见的调试技巧将ADC通道0接到一个已知电压比如通过电阻分压得到1.65V3.3V的一半。那么理论上采集到的数字量应该在2048左右波动。用这个方法来验证你的ADC基准和采样是否基本准确。4.3 数据缓冲区的结构解析理解缓冲区里数据的排列顺序至关重要。假设我们配置了3个通道Ch0, Ch1, Ch2采样100次ADC_BUFF_SIZE100使用Circular DMA。 那么缓冲区adc_value_buf[300]的数据结构是[Ch0-采样1], [Ch1-采样1], [Ch2-采样1], [Ch0-采样2], [Ch1-采样2], [Ch2-采样2], ..., [Ch0-采样100], [Ch1-采样100], [Ch2-采样100]这是一种“通道交错”的存储方式。当你需要处理某一通道的连续数据时需要以ADC_CHANNEL_NUM此处为3为步长进行读取。// 获取通道1索引为1的所有采样值 uint32_t channel1_samples[ADC_BUFF_SIZE]; for(int i 0; i ADC_BUFF_SIZE; i) { channel1_samples[i] adc_value_buf[i * ADC_CHANNEL_NUM 1]; // 下标 通道索引 }5. 深度调试当数据不对或DMA不工作时如果按照上述步骤操作数据仍然没有或者数据全是0、全是4095、杂乱无章那么就需要进行深度排查。以下是一个系统的排查链路。5.1 排查链第一步硬件与引脚电源与地首先确保MCU供电稳定模拟部分VDDA、VSSA和数字部分VDD、VSS的电源都正确连接且干净。ADC的参考电压VREF 如果独立的话是否接对STM32F103C8T6这类芯片VREF通常内部连接到VDDA。信号通路用万用表测量你试图采集的模拟信号是否真的到达了MCU的ADC输入引脚信号电压是否在0-VDDA之间如果超过可能会损坏IO口或导致ADC饱和始终读出4095。引脚配置在CubeMX中确认ADC使用的GPIO引脚模式被正确设置为“Analog”模拟模式。如果被错误地设置为推挽输出等数字模式ADC将无法正确采样。5.2 排查链第二步软件配置复查时钟树再次确认SystemClock_Config()函数APB2总线时钟PCLK2是否已正确配置并启动ADC的时钟分频是否在14MHz限制内一个简单的验证方法是尝试点灯或者用PWM输出一个固定频率如果这些基于定时器的功能正常说明主时钟大致没问题。ADC初始化序列检查生成的MX_ADC1_Init()函数。确保HAL_ADC_Init(hadc1)被调用。这个函数内部会调用HAL_ADC_MspInit()后者负责配置ADC和DMA的底层GPIO和时钟。确保这个MspInit函数存在且被正确编译链接。DMA中断优先级如果使用如果你开启了DMA传输完成中断或半传输中断需要检查NVIC嵌套向量中断控制器配置。DMA中断的优先级不能是无效的。但更常见的是在纯DMA搬运不中断模式下可以不开启DMA中断这反而能避免因中断配置错误导致的问题。5.3 排查链第三步DMA传输控制寄存器诊断当软件配置看似都正确但DMA就是不搬运时需要直接查看DMA控制寄存器的状态。这需要借助调试器。以DMA1通道1为例其控制寄存器是DMA1_Channel1-CCR。在调试器的“Register”窗口或“Watch”窗口添加这个寄存器观察。EN位Bit 0这是通道使能位。在调用HAL_ADC_Start_DMA后这个位应该被置1。如果为0说明DMA通道根本没有启动。CIRC位Bit 5循环模式位。应该为1。MEM2MEM位Bit 14存储器到存储器模式位。必须为0。如果误设为1DMA会在两个内存地址间搬运完全不理睬ADC的请求。TCIF位传输完成中断标志在DMA1-ISR寄存器中即使你没开中断这个标志位也会在DMA传输完指定长度后置1。在Circular模式下这个标志会周期性置1。观察这个标志是否在变化是判断DMA是否在工作的最直接硬件证据。如果EN位为0回溯检查HAL_ADC_Start_DMA函数是否被成功调用以及其内部调用HAL_DMA_Start_IT是否返回HAL_OK。5.4 排查链第四步ADC状态与触发源软件触发在CubeMX的ADC配置中“External Trigger Conversion Source”通常我们选择“Regular Conversion launched by software”。这意味着转换是由软件触发启动的。而HAL_ADC_Start_DMA函数内部会调用HAL_ADC_Start后者会设置ADON位并执行一次软件触发设置SWSTART位。确保这个流程被执行。ADC状态寄存器查看ADC1-SR状态寄存器。EOC转换结束标志位在每次转换完成后会置1。在连续扫描模式下这个标志会频繁置位。如果这个标志一直为0说明ADC根本没有开始转换问题出在ADC启动或触发环节。如果EOC在变化但DMA不搬运问题就锁定在DMA请求与响应的链路上。DMA请求映射在STM32F103中ADC1的DMA请求是固定映射到DMA1通道1的。这一点在CubeMX中通常是锁定的不容易配错。但如果你是自己写寄存器配置代码务必确认这一点。5.5 典型问题与解决方案速查表现象可能原因排查与解决思路缓冲区数据全为01. DMA未启动或配置错误。2. ADC未启动或未触发转换。3. 缓冲区地址错误如传入HAL_ADC_Start_DMA的指针错误。1. 检查DMA CCR寄存器的EN位。2. 检查ADC SR寄存器的EOC位。3. 在调试模式下观察传入函数的缓冲区地址是否有效。缓冲区数据全为4095或最大值1. ADC输入引脚悬空或接到高电平。2. 输入信号电压超过VDDA。3. ADC采样时间太短采样不充分。1. 用万用表测量引脚电压。2. 确保信号在0-3.3V内。3. 增加ADC采样周期数Sampling Time。数据杂乱无章像随机数1. 缓冲区数据类型与DMA宽度不匹配。2. 内存地址自增Increment Address配置错误。3. 时钟不稳定或噪声干扰大。1. 确认uint32_t数组对应DMA Word宽度。2. 确认Memory端Increment Address为Enable。3. 检查电源纹波在模拟输入引脚加滤波电容如100nF。只有缓冲区前几个数据更新后面不变DMA传输长度NDTR寄存器设置错误可能远小于缓冲区实际大小。检查HAL_ADC_Start_DMA的第三个参数确保是“数据项总数”。在调试器查看DMA1_Channel1-CNDTR寄存器的值。DMA不循环传输一次就停止DMA模式误配置为“Normal”而非“Circular”。在CubeMX中检查并修改DMA Mode为Circular重新生成代码。程序运行一段时间后卡死1. 缓冲区溢出在Circular模式下不会溢出。2. 更可能是发生了HardFault由于数组越界、指针错误等。3. 中断冲突。1. 检查是否有其他代码如错误的中断服务程序修改了DMA或ADC相关寄存器。2. 在Debug模式下使能HardFault中断定位错误地址。6. 进阶优化与稳定性实战心得当基本的ADC DMA采集跑通后接下来要考虑的是如何让它更稳定、更高效。6.1 抗干扰与滤波处理ADC采集尤其是从传感器来的小信号很容易受到数字电路噪声的干扰。你会看到采集到的数值在理论值上下频繁跳动。硬件滤波在ADC输入引脚到地之间并联一个10kΩ电阻和一颗100nF的瓷片电容构成一个简单的低通滤波器可以滤除大部分高频噪声。软件滤波对于变化缓慢的信号如温度可以采用滑动平均滤波。在DMA循环缓冲区的基础上你可以开辟一个处理缓冲区。例如DMA缓冲区存储最新100次采样你的处理程序每次取这100个值做平均得到一个更稳定的结果。对于工频干扰50Hz如果采样频率设置得当可以采用算术平均滤波来抑制。6.2 双缓冲与数据就绪标志在Circular DMA模式下CPU和DMA都在操作同一块内存缓冲区这存在“读写竞争”的风险。虽然DMA是硬件操作速度极快但如果你在处理数据时比如进行复杂的滤波计算DMA正好更新了你正在读取的数据就可能读到“半新半旧”的不一致数据。一个更稳健的模式是使用“双缓冲”Double Buffer或“半传输中断”Half Transfer Interrupt。将DMA缓冲区大小设为实际需要大小的两倍例如需要200个数据就设400。开启DMA的“半传输完成中断”HTIE和“传输完成中断”TCIE。在中断服务程序里当发生半传输中断时意味着DMA已经填满了缓冲区的前一半前200个数据此时可以安全地处理这前一半数据而后一半正在被DMA填充。当发生传输完成中断时意味着DMA已经填满了整个缓冲区后200个数据也满了此时可以安全地处理后一半数据而DMA会回到缓冲区开头重新填充前一半。 这样就实现了CPU和DMA的无冲突协作。HAL库提供了相应的回调函数HAL_ADC_ConvHalfCpltCallback和HAL_ADC_ConvCpltCallback。6.3 动态调整采样率与触发同步有时你需要精确控制采样率或者让ADC采样与其他外设如定时器、PWM同步。定时器触发在CubeMX中可以将ADC的“External Trigger Conversion Source”改为某个定时器的更新事件如TIM2的TRGO。然后配置该定时器以你期望的采样频率产生更新事件。这样ADC的每次转换或每次扫描都由硬件定时器精确触发采样间隔非常均匀适合做波形分析。注入通道对于需要高优先级、打断常规序列采样的信号可以使用ADC的注入通道组。注入组可以由外部事件如另一个定时器或EXTI触发并且转换完成后会产生独立的中断。这在电机控制中用于快速采集电流等关键保护信号非常有用。调试ADC的DMA本质上是在理解STM32外设间一种高效的协作机制。从最初的配置迷茫到看到内存中数据跳动时的欣喜再到为消除最后一个字的抖动而反复调整参数这个过程是嵌入式开发中最有魅力的实践之一。它让你从软件逻辑的层面触摸到了硬件数据流的脉搏。我的经验是永远不要完全相信图形化配置工具生成的就是最终答案它只是给了你一个正确的起点。真正的稳定与可靠来自于你对每一个配置位含义的理解以及对时钟、中断、缓冲区这些核心概念在脑海中建立起的清晰画面。当你能够从容地让ADC、DMA、定时器这几个外设像齿轮一样精密咬合、协同工作时STM32的世界才真正向你敞开。本文还有配套的精品资源点击获取