资讯详情

STM32F103 ADC连续采样:DMA搬运替代中断,从配置到调试全解析

📅 2026/10/28 7:19:19 | 华诺云谱 👁 阅读
STM32F103 ADC连续采样:DMA搬运替代中断,从配置到调试全解析
简介针对STM32F103的ADCDMA调试需求这份实用资料面向嵌入式开发者和电子竞赛学生可有效解决高速多通道模拟信号采集时CPU占用高、数据易丢失等问题。压缩包共162个文件约5.45MB涵盖28个C源码及同名头文件、Keil工程配置、启动汇编文件、链接脚本以及编译生成的hex、axf和map文件方便直接参考与烧录验证。已有370人学习下载。资料提供了可直接复用的完整Keil uVision5工程结构从GPIO复用为ADC通道、初始化采样参数到配置DMA循环传输、使能ADC的DMA请求再到串口发送结果与中断服务程序均可在源码与调试文件中找到对应实现。借助这些工程代码和编译产物初学者也能快速验证配置流程、缩短环境搭建与排错时间适用于实时数据采集、传感器信号处理等典型场景。1. 用DMA扛住ADC连续采样F103上为什么绕不开DMA搬运在STM32F103上调ADC很多人第一步是用中断转换结束进EOC中断读ADC1-DR拿数据。可是采样率一拉高这套方案立刻开始喘不上气。我之前做一套电机电流采样10kHz的转换率每次中断里做滤波和累加CPU占用直接顶满别的任务全部卡顿最后被迫改成ADC的DMA方式才把问题解决。ADC的DMA法核心一句话ADC转换完成后由DMA硬件自动把数据寄存器里的结果搬到内存缓冲区CPU不需要一条条去读。你要做的只是提前配好DMA通道、给一块内存、启动一次然后等着数据自己排队进数组。整个过程的关键不在“配通”而在“怎么确认它真的在搬、搬的顺序对不对、搬完以后怎么处理不出乱子”。这篇就按F103这个具体平台把从配置到调试的完整路径讲透适合正被ADC中断拖累、或者DMA搬运数据“玄学”异常的开发者。2. 为什么选DMA而不是中断一个开销算给你看2.1 中断方式的真实代价一个采样率算给你看先算一笔账。F103的ADC挂在APB2上PCLK2为72MHz时通过ADCPRE分频后给ADC的时钟一般取6分频也就是12MHz这没有超过F103规定的ADC最高14MHz。ADC完成一次转换需要固定的12.5个周期再加上采样时间最短配置是1.5个周期采样于是一次转换总共14个ADC时钟周期折合时间大约1.17微秒。1.17微秒一次转换意味着如果开连续转换并用中断读数据CPU每1.17微秒就要被打断一次。72MHz主频下这相当于每隔大约84个CPU周期就进一次中断。HAL库的ADC中断处理函数本身就要消耗几十个周期再算上进出栈、数据存储、滤波运算CPU占用率几乎被吃满。我当年就是在1kHz采样时毫无感觉把采样率调到10kHz后直接翻车才发现中断方式在高频采样下根本没有余量。所以DMA的价值在于它把“搬运”这件事从CPU手里拿走了。CPU只需要在DMA缓冲区半满或全满的时候被通知一次比如缓冲区长256个字中断频率就降为原来的1/256。边界也很清楚如果你的采样率只有几千赫兹以下、通道只有一两路中断方式完全够用不必为了用DMA而用DMA反而增加调试成本。2.2 Cubemx最小配置ADC1DMA1Circular连续请求用STM32CubeMX配置这套方案是最快也最不容易漏步骤的路径。以STM32F103C8T6为例操作步骤如下新建工程选择芯片先把PA0配置为Analog模式作为ADC1的输入通道IN0。打开ADC1使能通道IN0。单通道采集时把ScanConvMode保持为DisabledContinuousConvMode设为Enable。在ADC1的DMA Settings里点Add DMA Request选择ADC1和DMA1 Channel1方向为Peripheral To Memory。DMA的Mode选CircularMemory地址递增开启外设和内存的数据宽度都设为Word。生成代码在main里确认MX_DMA_Init先于MX_ADC1_Init执行。这一步里最关键的几个配置项对比如下配置项推荐值作用ADC Continuous ConvEnable转换完一通道后自动开始下一轮保证DMA一直有数据可搬DMA ModeCircular循环模式搬满指定长度后自动回到缓冲区头继续搬否则一轮后DMA就停了Continuous RequestsEnable与Circular联动DMA持续响应ADC的请求不会只搬一次DMA Data WidthWord与ADC-DR的32位寄存器对齐取值时用低16位Memory IncrementEnable每次搬运后内存地址自动加4字节数据依次排进数组生成的核心代码大概是这样的。注意这是Cubemx生成后你会看到的形态省去了初始化外设时钟和GPIO的部分static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; /* 单通道不扫描 */ hadc1.Init.ContinuousConvMode ENABLE; /* 连续转换 */ hadc1.Init.NbrOfConversion 1; /* 规则组里1个通道 */ HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_0; /* PA0 */ sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_1CYCLES_5; /* 验证阶段先用最短采样时间 */ HAL_ADC_ConfigChannel(hadc1, sConfig); } static void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); }启动采集的代码放在main里只有一行核心调用#define ADC_BUF_LEN 256 uint32_t adc_buf[ADC_BUF_LEN]; /* 一定要用32位数组 */ HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);这里三个参数的含义要说明白第一个是ADC句柄第二个是DMA搬运的目标内存地址第三个不是字节数而是DMA传输次数也就是多少个Word。F103的ADC1 DMA请求是固定走DMA1通道1的不需要你自己选通道。启动之后PA0的12位ADC结果就会以右对齐方式持续写入adc_buf取值时用adc_buf[0] 0xFFFF就是电压原始值。这里特别强调一下初始化顺序。Cubemx生成的main默认是MX_DMA_Init在前、MX_ADC1_Init在后是因为ADC的HAL_ADC_MspInit里会使用DMA的句柄。如果手动写工程时把顺序搞反ADC初始化时DMA还没初始化整个搬运链路就是断的后面调多久都调不通。3. 多通道扫描与缓冲区设计让DMA搬运始终对得上号3.1 规则组排序决定数组顺序从SQR寄存器说起单通道开DMA只是入门实际项目里很少只采一路。F103的ADC有规则组和注入组DMA方式下常用的是规则组扫描模式。规则组里每一个转换通道都有一个Rank编号扫描模式下ADC按Rank顺序依次转换每次转换结束触发一次DMA搬运于是DMA缓冲区里的数据顺序严格等于规则组Rank的排列顺序而不是按通道号大小排。举个例子如果我配置Rank1是ADC_CHANNEL_2、Rank2是ADC_CHANNEL_0、Rank3是ADC_CHANNEL_1那么一轮转换完成后adc_buf[0]是PA2的结果adc_buf[1]是PA0的结果adc_buf[2]是PA1的结果。这个对应关系可以用表格直观表示数组索引对应Rank源通道adc_buf[0]Rank1ADC_CHANNEL_2 / PA2adc_buf[1]Rank2ADC_CHANNEL_0 / PA0adc_buf[2]Rank3ADC_CHANNEL_1 / PA1多通道配置的代码并不复杂关键是把每个通道的Rank写清楚hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; /* 开启扫描模式 */ hadc1.Init.NbrOfConversion 3; /* 规则组3个通道 */ sConfig.Channel ADC_CHANNEL_2; /* 第一个转换PA2 */ sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_13CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_0; /* 第二个转换PA0 */ sConfig.Rank ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; /* 第三个转换PA1 */ sConfig.Rank ADC_REGULAR_RANK_3; HAL_ADC_ConfigChannel(hadc1, sConfig);网上搜“stm32 adc切换通道”很多人纠结要不要在中断里改通道寄存器。其实开了扫描模式之后规则组会自动按Rank切通道软件不需要干预。DMA搬运和通道切换是联动的每次EOC事件产生DMA请求随之发出数据落进数组下一个位置。你要做的只有一件事就是别搞错Rank和数组下标的对应关系。3.2 缓冲区长度、半传输中断与环形处理缓冲区长度最小也要等于规则组通道数的一个整轮否则一轮数据没搬完就被下一轮覆盖。实际工程里我一般取256或512个Word这样在循环模式下DMA搬满一轮后自动回绕到缓冲区开头继续搬整个缓冲区就像一个环形队列。问题来了CPU怎么知道缓冲区哪些数据是新的DMA循环模式不会在每一轮结束都给一个中断除非你开启中断并利用半传输和传输完成事件。F103的DMA在CNDTR从初始长度递减到一半时触发半传输中断减到0时触发传输完成中断然后自动重载。HAL库里对应两个弱回调函数用户可以自行覆写void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { half_flag 1; /* 前半段准备好 */ } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { half_flag 2; /* 后半段准备好 */ } }回调里绝对不能做耗时的滤波、打印一类操作那会让DMA中断长时间占住CPU。正确做法是只置标志位主循环检测到标志后再去处理对应半区的数据。比如缓冲区512个Wordhalf_flag变成1时处理adc_buf[0]到adc_buf[255]变成2时处理adc_buf[256]到adc_buf[511]。这样就实现了“数据还在往另一半搬运CPU同时处理另一半”的效果。缓冲区长度要能整除2并且最好是规则组通道数的整数倍。如果3个通道却把缓冲区设成500半场在250处会把一组通道数据拦腰截断处理时对不上帧。我一般设成通道数的整数倍比如3通道时用256虽然有一点点浪费但换来回调逻辑的简单可靠。3.3 采样周期怎么定高阻输入和通道串扰采样周期这个参数新手很容易忽略。F103的采样时间可配置为1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。周期越短采样率越高但ADC内部采样电容充电时间越短对外部信号源的阻抗就越敏感。常见踩坑场景是信号源阻抗大比如用100kΩ电阻分压做电压采集采样时间却选了1.5周期结果采集值整体偏小而且两个相邻通道的值会互相串扰。原因是采样电容还没来得及充满电开关就切走了充电不足导致转换结果偏低扫描模式下多通道连续切换残余电荷又会影响下一个通道。我一般按信号源阻抗粗略选采样时间参考对应关系如下信号源阻抗建议采样周期备注 1kΩ1.5 / 7.5 周期运放输出、开关电源分压后缓冲1kΩ - 10kΩ13.5 / 28.5 周期大多数传感器分压电路 10kΩ55.5 / 239.5 周期高阻分压、NTC热敏电阻如果采样率要求不紧直接给55.5周期最省心。以12MHz ADC时钟计算55.5周期采样加12.5周期转换总共68个ADC周期约5.7微秒一次转换。三通道扫描一轮大约17微秒换算采样率约59ksps已经能覆盖大部分传感器场景。只有在真正需要高速采集时才把采样时间往下压并且这时候信号源最好经过运放缓冲把阻抗压到1kΩ以下。4. 调试ADC的DMA搬运5个翻车点与现场解法4.1 数组全零或死数先查启动顺序现象adc_buf数组全部是0但用单次转换的方式去读ADC1-DR却能读出有效值。或者DMA启动后数组从头到尾没有一点变化。原因最常见的有两个。一是启动函数用错比如误调用HAL_ADC_Start而不是HAL_ADC_Start_DMA导致只有ADC在转换DMA根本没启动。二是初始化顺序反了MX_ADC1_Init跑在MX_DMA_Init之前HAL_ADC_MspInit里初始化DMA句柄时DMA外设时钟还没打开或句柄还是空的DMA请求自然挂不上。解决先确认main里初始化顺序是MX_DMA_Init在前然后统一用HAL_ADC_Start_DMA启动HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);定位问题时用调试器看DMA1通道1的CNDTR寄存器。如果这个值一直不变说明DMA没动如果它在递减说明搬运正在发生问题大概率在ADC侧而不是DMA侧。4.2 数据宽度不一致导致的“隔位零”错乱现象缓冲区定义成uint16_t adc_buf[ADC_BUF_LEN]但Cubemx生成的DMA数据宽度是Word。运行后读取数组发现偶数索引有数值奇数索引全是0。多通道时更诡异看起来像两个通道的数据被压进了一个32位字里某一路恒为0。原因ADC的DR寄存器是32位的有效数据在低16位。Cubemx默认把DMA外设和数据宽度都配成Word每次搬运4字节到内存。而uint16_t数组按2字节索引于是一个Word被拆成两个uint16_t位置高16位全是0。解决最简单是使用uint32_t缓冲区取值时再截取低16位uint32_t adc_buf[ADC_BUF_LEN]; uint16_t raw adc_buf[i] 0xFFFFU;如果你确实想省一半内存用uint16_t就把DMA的PeriphDataAlignment和MemDataAlignment都改成HalfWord两边保持一致。但这里有个血泪教训手动改DMA配置格式非常容易漏而Worduint32_t跟HAL库默认配置完全对齐排错最省事。4.3 调试器断点下CNDTR不动别急着判DMA死刑现象全速运行时采样数据正常但一进断点看到CNDTR停在某个数值数组也定格在某个瞬时快照于是怀疑DMA已经卡死。原因调试器暂停内核时CPU不执行指令DMA虽然还在走但CNDTR的刷新也是动态的你在断点看到的只是一个冻结瞬间。F103的DBGMCU并没有提供针对ADC和DMA的独立冻结控制位这类寄存器快照正常情况就是“不动”的。解决不要单靠断点判断DMA死活。改用软件计数器在DMA中断回调里累加一个变量全速运行时让它跑几秒再用串口打印或者调试器看计数值有没有增长volatile uint32_t dma_tick 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) dma_tick; }只要dma_tick在跳链路就是通的。断点只是用来暂停世界观察不适合用来判定实时性。4.4 多通道帧对齐不要依赖“最后一个值”做帧头现象三通道数据在数组里的排列有时是A/B/C有时是B/C/A下游解析协议的通道值对不上而且不是每次启动都错看起来没有规律。原因DMA循环模式启动时ADC可能正转换到规则组的任何一个Rank。如果刚好停在Rank2DMA的第一次搬运就是Rank2的结果整轮数据整体偏移了一个位置而且偏移量不固定。解决第一种做法是把一个已知的固定电压通道放在Rank1作为对齐基准。比如Rank1接一个GND引脚该通道数值接近0处理数据时扫描数组前几个位置找到近似0的位置就是帧头。第二种做法更简单粗暴但很有效每次开始采集前先停止DMA清DMA的使能位再重新启动尽量让起始点落在Rank1。这个办法在大多数情况下够用但不是绝对可靠。工程上最稳妥的还是加一个校验通道每轮数据里检查Rank1对应通道的值是否在预期范围不符合就丢弃这一帧。这个思路能避免“以最后一个值当帧头”造成的整体错位也是我一直推荐的做法。4.5 电压换算误差实测VREF而不是写死3.3现象给PA0输入一个已知的2.5V电压换算出来是2.35V换一块板子误差又不一样。用同一个程序在开发板和贴片板上结果差异明显。原因ADC是比例转换结果等于输入电压除以参考电压VREF再乘以4095。F103的小系统板上VREF通常与VDDA相连而VDDA的实际电压不一定是理想3.3V。很多LDO输出是3.28V或3.29V代码里写死3.3V自然会有偏差。之前说的HAL_ADCEx_Calibration_Start校准的是内部偏移不是参考电压解决不了满量程误差。解决用万用表实测板上的VDDA或VREF引脚把这个实测值作为换算系数#define VREF_MV 3280.0f /* 用万用表实测的参考电压单位mV */ float voltage (float)(adc_buf[i] 0xFFFF) * VREF_MV / 4095.0f;如果要求更高用两点校准输入一个已知0V和一个已知2.5V基准反推出实际满量程值再写进代码。这样能把不同板卡之间的差异消除掉。5. 如何确认DMA在干活寄存器、串口和波形三板斧5.1 寄存器三板斧CNDTR递减、DMA标志位、内存数组刷新DMA到底有没有在搬很多时候是个黑匣子。第一个最直接的观察窗口是CNDTR寄存器它在DMA每次搬运后递减。把下面这行代码放到主循环里周期性读取就能看到递减uint16_t remain DMA1_Channel1-CNDTR; /* remain 不断减小说明DMA正在响应ADC请求搬运数据 */注意CNDTR是16位寄存器初值是启动时传入的传输长度每次DMA传送一个Word就减1。如果这个值保持不变优先怀疑DMA没有收到ADC的请求而不是DMA本身坏了。第二个窗口是DMA的中断状态标志HAL库里可以用__HAL_DMA_GET_FLAG查询半传输和传输完成标志不过实际调试中看CNDTR更快。第三个窗口是内存数组本身。在调试器的Live Watch窗口添加adc_buf全速运行几秒后暂停看数组有没有变化。还有一种纯软件的验证方式每隔一段时间把adc_buf[0]的值复制到另一个变量比较两次是否不同。这个技巧在没有调试器、只有串口的现场排查中特别管用。5.2 串口打印采样值从printf到串口DMA发送确认DMA在搬之后接下来要确认数据对不对。最直接的办法是把采样值通过串口打出来。先在MDK里做printf重定向把fputc指向USART1int fputc(int ch, FILE *f) { while (!(USART1-SR USART_SR_TXE)); USART1-DR ch; return ch; }然后在主循环里按一定节拍打印数据。打印频率一定要限制住115200波特率下每秒最多传约1万字符全速打印会严重拖累主循环。我习惯的做法是打印数组前16个值作为快照static void dump_snapshot(uint32_t *buf) { for (int i 0; i 16; i) { printf(%04X , buf[i] 0xFFFFU); } printf(\r\n); }串口打印还能顺便观察数据毛刺。你在网上搜c语言adc值滤波函数时看到的那些去极值平均、滑动平均代码在这个阶段就可以加在打印之前让曲线更干净。如果是大量数据需要搬出去用串口DMA发送即HAL_UART_Transmit_DMA把数据先放进发送缓冲区由DMA负责发送CPU不阻塞在等待上。这样采集端和发送端都走DMACPU才真正解放。5.3 波形验证信号发生器加电位器把ADC数据画成曲线打印数值只能看散点要确认“波形成像”最好给ADC输入一个已知信号。信号发生器输出1kHz正弦波幅度0到3.3V串一个几百欧姆的电阻接到PA0避免信号过冲。先用示波器测PA0引脚确认波形幅值正常再观察串口打印的数据曲线。如果采回来的数据和示波器看到的波形在幅值、周期上一致说明DMA搬运链路全部正常。没有信号发生器时用电位器从3.3V分压到PA0缓慢转动旋钮串口打印的值应该平滑变化而不是跳变或来回抖动。如果发现曲线有台阶状跳变先怀疑前面说的数据宽度和缓冲区半场截断问题再看采样周期是否太短。这一步是整个调试流程的收尾。它能直观暴露“数据没错位但数值不连续”“数值连续但通道错位”这两类只有看到波形才能发现的问题。等波形验证通过这套ADC的DMA方案才算真正落地。6. 把它做成稳定流水线半缓冲节拍6.1 用半满中断把缓冲区分成两段处理到这一步采集链路已经通了但离稳定运行还差一步数据处理的节拍。很多人的第一版代码喜欢在传输完成回调里直接处理整块缓冲区这在采样率低时没毛病采样率一高就出问题。回调里处理时间一旦超过半场转换时间下一轮DMA已经开始往缓冲区后半段写数据而你还在处理上一轮结果就是数据互相覆盖采集曲线出现周期性跳变。F103的DMA没有F4/F7那种硬件双缓冲机制但半传输中断加传输完成中断的组合可以模拟出双缓冲的效果。把缓冲区切成两半DMA往一半写的时候主循环处理另一半#define ADC_BUF_LEN 512 uint32_t adc_buf[ADC_BUF_LEN]; volatile uint8_t ready_block 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) ready_block 1; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) ready_block 2; } int main(void) { /* ... 初始化 ... */ HAL_ADC_Start_DMA(hadc1, adc_buf, ADC_BUF_LEN); while (1) { if (ready_block 1) { process_block(adc_buf[0], ADC_BUF_LEN / 2); ready_block 0; } else if (ready_block 2) { process_block(adc_buf[ADC_BUF_LEN / 2], ADC_BUF_LEN / 2); ready_block 0; } } }这个结构下主循环的处理时间和DMA搬运时间并行。只要一次半场处理耗时小于半场搬运耗时数据就不会被覆盖。如果处理不过来优先加大缓冲区长度或者降低采样周期而不是在回调里硬扛。这个半缓冲方案是我调试ADC的DMA法时最后一块拼图。早期我图省事把整个缓冲区处理放在TC回调里数据一直呈现阶梯状跳变后来改成半场节拍并在主循环处理问题立刻消失。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑