
STM32F030上做多通道ADC采集你第一反应用什么方式我见过不少同事直接在主循环里轮询写完就算完事也有人张口就是DMA好像不用DMA就体现不出水平。说句实在话轮询、中断、DMA这三种方式我都踩过坑也都在实际项目里跑过。这篇文章不聊虚的就围绕F030这颗性价比很高的Cortex-M0单片机把多通道ADC的轮询、中断、DMA三种实现方式从原理、代码到坑位全部撸一遍看完你至少能少走几个月的弯路尤其是那些网上搜不到的寄存器细节和工程经验。我要讲的重点不在基础外设初始化而是多通道场景下三种方案的真正区别谁在等、谁在干活、谁会被卡住。我会用STM32F030C8T6 HAL库为例从CubeMX配置开始给出可以直接抄的代码再把我实际调试中遇到的稀奇古怪问题都摆出来。1. 项目背景与方案选型多通道ADC的三种路线1.1 为什么要把轮询、中断、DMA放在一起比很多嵌入式入门的朋友会有一个误解觉得多通道ADC采集轮询是“低级方案”DMA才是“高级方案”中间好像还夹着一个不太常用的中断方式。其实这三种方式不是技术水平的高低差异而是不同场景下的资源交易策略。轮询是用CPU时间去换代码简单DMA是用硬件外设去换CPU空闲中断则处在两者之间适合“不需要CPU一直盯着但也不能完全撒手不管”的场景。我最早做F030多通道ADC的时候是在一个需要同时采集温度和电压的小项目里通道数只有两路当时偷懒直接在while循环里轮询跑了一阵发现主循环里其他任务被拖得很厉害尤其是加了OLED刷新之后采样周期变得非常不稳定。后来切到中断方式主循环好了一些但切换通道的逻辑又让代码变得很难看。最后换成DMA扫描模式才真正舒服起来。所以我才想把这三条路都拆开讲清楚避免大家和我一样来回折腾。再一个原因F030这颗芯片的ADC有一个非常容易被忽略的EOCS位它决定了“转换完成”到底是指单次转换完成还是整个通道序列转换完成。这个位对三种实现方式的影响完全不一样搞不清楚的话代码看着没错数据却总是错的。这种东西你在常规教程里很难看到但实际调试中几乎必踩一次。1.2 STM32F030的ADC和DMA资源到底够不够用先说结论对于绝大多数多通道低速采集场景F030的片内外设是绰绰有余的。它内置一个12位逐次逼近型ADC最高ADC时钟可以到14MHz采样时间可配置在最快配置下能达到约1Msps。外部模拟输入通道以常见的F030C8T6为例PA0到PA7、PB0、PB1这10个引脚都可以做模拟输入日常“几路电压、几路传感器”这种需求完全够用。DMA方面F030只有一个DMA控制器共5个通道没有内存到内存传输没有FIFO也没有F103那种更复杂的请求映射结构相对简化。它的优势是配置门槛低做 ADCDMA 循环搬运绰绰有余。但也要注意正因为DMA资源少如果你同时又要用串口DMA、SPI DMA就需要提前规划好通道分配别把唯一的控制器的几个通道全部占满后面想加功能就麻烦了。还有一个很多人忽略的点F030是Cortex-M0内核没有浮点单元也没有硬件除法指令。ADC采回来的是12位整数你做平均值滤波、百分比换算这些操作时CPU会花不少指令。这也是为什么DMA方式的优势在数据量上来后会非常明显——搬运数据的活儿完全不需要CPU参与主循环只做轻量的数学运算就行。1.3 三种方式的核心区别一句话版轮询CPU发启动命令然后自己盯着Flag寄存器直到转换完成再去读结果循环往复。整个等待过程CPU被占死。中断CPU发启动命令后就去干别的转换完成时硬件触发中断CPU在中断回调里读结果需要的话再启动下一轮。DMAADC转换结果由DMA控制器自动搬到内存数组里搬完之后可以告诉CPU“这批数据好了”也可以完全不通知CPU需要时直接读数组。从“谁在等”这个角度看轮询是CPU等中断是CPU被叫过去干活DMA是CPU完全不用管。理解了这一点后面看代码和硬件配置就会顺很多。2. 工程准备与硬件配置2.1 硬件接线与参考电压处理F030这颗芯片的ADC参考电压值得先拿出来说因为很多翻车现场就是栽在这里。F030C8T6有独立的VREF引脚如果你的板子上把VREF悬空或者接错ADC采集结果会是完全随机或者恒定的错误值。标准做法是把VREF直接接到VDDA如果要求高精度可以在VDDA和VSSA之间加一个高质量的滤波电容让参考电压更干净。模拟输入引脚这一侧我的建议是信号源输出阻抗尽量低如果信号源是高阻抗输出比如直接接一个10K电位器分压那就需要把ADC采样时间调长一些否则内部的采样电容在有限采样窗口内充不满采出来的值会偏低。如果实在没法降低阻抗可以在引脚对地加一个0.1uF电容利用电容存储电荷来稳定采集电压。此外模拟输入引脚的电压范围必须是0V到VREF之间超过VREF或者低于GND都会损坏引脚或采出错误值。如果外部信号可能超过这个范围一定要加钳位二极管或者分压电阻这不是ADC的锅是前端电路设计的问题。2.2 用CubeMX把ADC和DMA先配好我用STM32CubeMX配合HAL库来做说明因为这个流程最通用也最容易复现。时钟方面先把系统时钟配置到48MHzADC时钟来源选择PCLK分频由于F0的ADC最高只能跑14MHz48MHz的时钟需要至少4分频得到12MHz。这个分频值不是随便填的超过14MHz会不稳定。在ADC1配置界面里把PA0和PA1勾选为模拟输入对应ADC_IN0和ADC_IN1。然后设置以下参数Resolution: 12 bitScan Conversion Mode: EnabledContinuous Conversion Mode: EnabledEnd of Conversion Selection: Single Conversion这个后面会重点讲Number Of Conversion: 2接着在Rank列表里把第1个Rank配成ADC_CHANNEL_0第2个Rank配成ADC_CHANNEL_1两个Rank的采样时间都可以选41.5 Cycles。在DMA Settings标签页添加ADC1的DMA请求Direction选Peripheral To MemoryMode选Circular外设数据宽度和内存数据宽度都选Half Word因为ADC是12位一个结果正好是一个16位半字。Priority可以直接选High反正F0的DMA通道不多优先级影响不大。注意F0的DMA带宽很有限ADC的DMA请求频率跟转换速度直接相关。如果你的转换时间非常短同时还有其他DMA设备在跑可能出现DMA请求排队导致采集丢点这种情况就要考虑降低ADC时钟或者把一些低速外设换成普通中断方式。2.3 别小看序列器多通道的“顺序”就靠它F0系列ADC的多通道转换并不是“启动一次就能把几个通道全部读出来”这么简单。它内部有一个规则序列器Regular Sequencer你得告诉它这次要转哪些通道、按照什么顺序转它才会在Scan模式下按顺序完成一组转换。Rank就代表了通道在这个序列里的先后位置。用一个小例子来说你配置了Rank 1是通道0Rank 2是通道1那么Scan模式启动后硬件会先转ADC_IN0再转ADC_IN1转换结果依次写入ADC数据寄存器。如果没有DMA读寄存器时只能看到最近一次转换的结果前几个通道的数据会被覆盖掉。所以多通道模式下要么用DMA把每次结果都搬走要么单通道轮询来切换。采样时间的分配也值得琢磨。不同信号源的输出阻抗不一样比如温度传感器输出阻抗可能很高需要更长的采样时间而运放输出的信号阻抗低可以用短采样时间提高整体转换速率。F0每个Rank都可以单独配置不同的采样时间不要图省事全部用一个值。3. 方式一轮询读取最简单也最容易“堵车”3.1 轮询的原理与适用边界轮询方式是很多教程一开始就教的因为逻辑最直白你想采集某个通道就配置这个通道启动ADC然后反复检查转换结束标志标志置位后读数据。整个流程完全同步代码走到哪里数据就采到哪里。它的缺点也很明显ADC从启动到转换结束需要时间这段时间CPU就在一个while循环里傻等。以12MHz ADC时钟、41.5周期采样时间为例一次转换大约需要4.5us两个通道就是9us。如果主循环里还有其他高实时性任务这9us的等待会明显挤占它们的执行时间而且采集周期会随主循环任务长短波动。所以轮询方式适合的是通道少、转换频率低、对时序抖动不敏感的场景。比如产品里检测电池电压一秒采一次就够那完全没必要折腾DMA。这也是我在实际项目里的选型原则先算需求再选方案不要为了技术而技术。3.2 主循环轮询代码逐通道切换读取用HAL库写轮询多通道我推荐的做法是关闭扫描模式主循环里逐个配置和读取每个通道这样逻辑最清晰也不容易踩到序列器的坑。示例代码如下static void adc_poll_multi_channel(void) { uint32_t channel_list[2] {ADC_CHANNEL_0, ADC_CHANNEL_1}; uint16_t raw_value[2] {0}; uint32_t i; for (i 0; i 2; i) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel_list[i]; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_41CYCLES_5; HAL_ADC_ConfigChannel(hadc, sConfig); HAL_ADC_Start(hadc); if (HAL_ADC_PollForConversion(hadc, 100) HAL_OK) { raw_value[i] HAL_ADC_GetValue(hadc); } HAL_ADC_Stop(hadc); } }每次循环里都调用HAL_ADC_ConfigChannel是因为F0的通道选择寄存器必须重新写入才能切换下一个模拟输入引脚。写完配置后启动转换PollForConversion会等到转换结束所以后续取值是可靠的。有一点容易出现反直觉问题HAL_ADC_PollForConversion的第二个参数是超时时间单位是毫秒如果转换还没结束就超过这个时间它会返回超时错误。平时配置100毫秒没问题因为一次转换最多几十微秒。但如果调试时你在EOC标志上外加了断点超时后建议重新启动一次转换否则下一次Poll可能会拿到上一次残留的标志。3.3 轮询方式常见的两个坑第一个坑是有人在轮询方式里也开了Scan扫描模式然后写了一个HAL_ADC_PollForConversion等标志再调用HAL_ADC_GetValue结果发现读到的永远只有一个通道的值。原因前面说了Scan模式下ADC数据寄存器每次都会被新结果覆盖不用DMA的话前面的数据早就没了。轮询多通道的正确姿势就是单通道逐次切换别开Scan。第二个坑是切换通道时忘了HAL_ADC_Stop。如果你在上一轮读取结束后没有停止ADC紧接着去ConfigChannel往往配置不生效或者ADC状态机处于Busy状态HAL_ADC_ConfigChannel会直接返回HAL_BUSY。这也是我代码里每轮都Stop的原因。虽然频繁启动停止会带来一点点额外开销但能保证配置可靠适合轮询这种低频场景。4. 方式二中断读取主循环解放了但别高兴太早4.1 中断方式的适用场景与配置要点中断方式的出发点是“让CPU在等待ADC转换时去干别的活”。你启动ADC后不再傻等转换结束硬件会产生中断CPU进入回调函数你在回调里读数据、处理数据。相比轮询主循环不会被片外等待阻塞相比DMA代码和应用逻辑更直观适合只采集少量通道、间隔要求不那么苛刻的场景。但中断方式也有它的别扭之处尤其是多通道。如果你用扫描模式加中断中断回调里能拿到的还是ADC数据寄存器的“当前值”也就是序列里最后一个通道的值前面的通道又丢了。所以做多通道纯中断采集我还是建议关闭扫描用单通道轮流切换的方式每个通道转换完成后在回调里配置下一个通道并启动下一轮。4.2 中断回调代码切换通道的完整流程CubeMX配置时保持Scan模式DisableContinuous模式Disable开启ADC全局中断。代码里做一个通道索引每完成一次转换就切换下一个通道#define ADC_CH_NUM 2 static uint32_t adc_channel_list[ADC_CH_NUM] {ADC_CHANNEL_0, ADC_CHANNEL_1}; static volatile uint16_t adc_result[ADC_CH_NUM]; static volatile uint8_t adc_ch_index 0; static volatile uint8_t adc_group_ready 0; void start_adc_interrupt(void) { adc_ch_index 0; adc_group_ready 0; HAL_ADC_Start_IT(hadc); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ! ADC1) { return; } adc_result[adc_ch_index] HAL_ADC_GetValue(hadc); adc_ch_index; if (adc_ch_index ADC_CH_NUM) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel adc_channel_list[adc_ch_index]; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_41CYCLES_5; HAL_ADC_ConfigChannel(hadc, sConfig); HAL_ADC_Start_IT(hadc); } else { adc_ch_index 0; adc_group_ready 1; /* 如果还需要下一轮可以在主循环检测ready后再次启动 */ } }这段逻辑的核心是“每完成一次转换就换下一个通道”。第一轮从通道0开始结束后在回调里配置通道1并启动通道1结束之后整组数据就齐了。主循环里可以只判断adc_group_ready标志然后读取数组里的值。实际使用中要注意HAL_ADC_ConfigChannel在中断回调里执行会占用一定时间。如果你把采样时间调到很短比如用1.5周期转换本身不到1us但回调里切换通道的开销可能比转换时间还长整体吞吐率反而上不去。这就是为什么高频率多通道采集时中断方式并不比DMA有优势。4.3 中断模式的最大坑EOCS和Scan模式F0系列的ADC_CFGR1寄存器里有一个EOCS位决定EOCEnd of Conversion标志是“每次转换完成”置位还是“整个序列转换完成”置位。HAL库里对应的是初始化的EOCSelection参数hadc.Init.EOCSelection ADC_EOC_SINGLE_CONV; // 每次转换完成 hadc.Init.EOCSelection ADC_EOC_SEQ_CONV; // 整个序列完成这个位在中断方式里几乎决定生死。如果你开了Scan模式又保持EOCSelection为Single那么每个通道转换完成都会触发一次中断回调频率成倍增加而且回调里读到的数据永远是刚转完的单个通道你很难说清这到底算哪个通道的结果。反过来如果你想要“序列完成后再通知CPU”又受限于没有DMA数据在序列中间已经被覆盖了。所以纯中断多通道的做法建议保持Scan关闭、EOCSelection为Single老老实实一个一个通道来别贪心。提示如果你在CubeMX里生成的代码默认EOCSelection是SINGLE_CONV而你又开了Scan模式采集结果会变得非常不可控。遇到这种诡异问题优先检查EOCS这一位。5. 方式三DMA搬运数据落地到内存再通知你5.1 为什么DMA才是多通道ADC的“正解”到了这一步才能真正体现“从轮询到DMA”的质变。DMA方式下你配置好ADC的扫描序列和DMA的循环搬运启动一次之后ADC就会按照Rank依次转换每个转换结果由DMA自动写入你指定的内存数组所有过程不需要CPU参与。以两个通道为例DMA缓冲区只需要两个uint16_t变量DMA每搬运完一轮搬完两个半字就会触发一次传输完成中断进入HAL_ADC_ConvCpltCallback。你可以在这里拷贝数据、置标志或者什么也不做主循环随时可以读缓冲区里的最新值。这样的好处是主循环不再被ADC拖住采样周期稳定CPU占用极低。这里要特别说一个细节DMA传输长度必须等于序列通道数。有些教程示例里写HAL_ADC_Start_DMA(hadc, (uint32_t*)buf, 1)这种写法在单通道时没问题多通道就必须传2否则DMA只搬一个数据就触发了完成中断第二个通道的结果会覆盖第一个数据直接错位。我调试时在这个坑里卡过整整一个晚上。5.2 完整DMA配置与代码实现CubeMX配置部分可以参考前面第2节的步骤这里直接给代码视角。static uint16_t adc_dma_buffer[2]; void adc_dma_start(void) { /* 先清空缓冲区避免启动瞬间读到旧数据 */ memset(adc_dma_buffer, 0, sizeof(adc_dma_buffer)); HAL_ADC_Start_DMA(hadc, (uint32_t *)adc_dma_buffer, 2); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { /* 这一轮两个通道的数据都已经搬到缓冲区 */ /* 这里可以做数据后处理或者只是置个标志让主循环处理 */ /* last_ch0 adc_dma_buffer[0]; */ /* last_ch1 adc_dma_buffer[1]; */ } }启动之后DMA在循环模式Circular下会一遍又一遍地把新结果写入同一块缓冲区。每次写完两个值进入一次完成回调。如果你不需要回调做太多事情甚至可以把回调当成一个“时基”信号用来同步其他任务。停止采集时也要注意必须使用HAL_ADC_Stop_DMA而不是HAL_ADC_Stop。这是因为HAL_ADC_Stop只会停止ADC不会关闭DMA通道容易导致DMA还挂在那边等待新请求下次重新启动时状态错乱。我在现场调试时遇到过这种“不能二次启动”的问题最后定位到就是停止函数用错了。5.3 HalfCplt和Cplt回调的分段处理技巧DMA循环模式还有一个非常实用的回调半传输完成回调HAL_ADC_ConvHalfCpltCallback。以两通道、缓冲区长度2为例每次DMA完成一半传输也就是搬完第1个通道数据时会触发HalfCplt搬完第2个通道数据时触发ConvCplt。利用这两个回调你完全可以不用自己去判断当前缓冲区该归属哪个通道了。比如采集两个通道直接在Half回调里读adc_dma_buffer[0]在Cplt回调里读adc_dma_buffer[1]逻辑非常清楚。如果你的缓冲区长度更大比如4个通道还可以做成双缓冲的概念前半段数据由DMA写入时CPU处理后半段数据交替使用既不冲突也不阻塞。这种技巧在波形采样、音频采集里很常见。但要注意回调里处理数据的耗时越短越好不要让CPU在回调里做浮点运算、打印日志这类耗时操作。否则回调还没退出去下一轮转换又完成了DMA会继续往缓冲区写你的处理进程就会跟不上。一般我只在回调里拷贝数据、置标志位真正的业务逻辑放主循环。5.4 循环DMA的数据一致性怎么避免读到一半循环DMA模式下数据一直在被刷新主循环读缓冲区时DMA可能正好写到一半你会读到一个“半新半旧”的值。比如先读了adc_dma_buffer[0]这时候DMA把它更新成了下一轮的新值你再读adc_dma_buffer[1]可能这个值还是上一轮的两路数据就串了时间点。解决方案不外乎两种。第一种在回调里把整组数据拷贝到另一份“快照”缓冲区主循环永远只读快照区static volatile uint16_t adc_snapshot[2]; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_snapshot[0] adc_dma_buffer[0]; adc_snapshot[1] adc_dma_buffer[1]; } }第二种如果不想使用回调也可以在主循环里用临界段保护一下__disable_irq(); ch0 adc_dma_buffer[0]; ch1 adc_dma_buffer[1]; __enable_irq();关闭全局中断的时间非常短只在复制两个变量的几个指令周期内不影响整体实时性。对于数据一致性要求高的场景这个做法简单有效。6. 三种方式实测对比与选型建议6.1 CPU占用、实时性与代码量对比我在一个实际测试项目里用F030C8T6跑48MHzADC时钟12MHz采样时间41.5周期两通道采集分别实现轮询、中断、DMA三种方式简单测了一下几个维度的表现对比项轮询中断单通道轮流DMA扫描循环代码复杂度最低逻辑直接中等需要维护通道切换状态中等偏高但配置一次后很简单CPU占用高采集期间主循环被阻塞较低但每次转换都进中断极低CPU基本只做数据读取采样时序稳定性差受主循环任务影响中中断本身稳定但切换逻辑有开销好硬件自动持续采集多通道扩展成本每加一路都要重复配置读取每加一路要多维护一个索引只改序列长度和缓冲区大小适合场景调试、低频采集少量通道、中低速并发连续采样、波形、多路实时采集实测下来轮询方式一次完整读两组数据大约要十几微秒这期间主循环一动不动中断方式主循环的空闲时间多了不少但清洗采样率推到比较高的级别后中断频繁触发CPU负载反而上升得很明显DMA方式启动后主循环几乎看不出来ADC在跑只有DMA完成中断偶尔打断一下。如果你要的是“稳定、可靠、可扩展”的采集链路DMA是首选。如果只是写个测试程序轮询反而更快上手。不必盲目追求DMA但也不要把轮询一路用到量产产品里拿CPU时间去省配置时间本质上是不划算的。6.2 不同应用场景怎么选这里我按场景给一个比较主观的选型建议。做电池电压检测、环境温度采集、按键电位器这类低频应用更新速率每秒几十次以内轮询就够了代码短排查也容易。需要主循环响应其他任务又有两三路中等频率的模拟量比如电机反馈采样、简单传感器融合中断方式值得用只要控制好回调里的处理时间。需要连续高速采集多路信号比如三相电流采样、音频输入、或者模拟波形记录DMA几乎是唯一选型。因为它能把转换结果的搬运全部交给硬件CPU只负责业务逻辑。F030虽然性能不高但在48MHz下靠DMA 扫描模式跑两路几十kHz的采样还是轻轻松松的。如果你的需求更极端还可以给ADC加定时器触发用定时器TRGO信号控制ADC启动采样再配合DMA循环搬运这样采样率完全由硬件时钟决定和主循环代码逻辑彻底解耦。这是DMA方式的一个进阶玩法很多信号采集项目就是这么干的。7. 常见问题与排查技巧实录7.1 采到的数值不对从硬件到软件逐层排查遇到ADC采集值不对最先别急着改代码先确认硬件。VREF引脚有没有接VDDA模拟输入的信号电压有没有超过参考电压这些属于“电源和参考”层面的问题。我见过一个板子所有通道采出来都是0最后发现是VREF焊盘虚焊补焊之后一切正常。如果硬件看起来没问题再查引脚配置。PA0如果没被初始化成模拟模式而是默认的GPIO复用模式ADC读到的数值会非常不稳定甚至一直是0或4095。CubeMX生成代码时一般不会漏掉IO配置但手写初始化就容易犯这种错。软件层面的第二排查点是采样时间。信号源内阻高、采样时间设得太短采样电容充不满读出来的值总比真实值低。这种问题往往带有“接上电压表量是对的接上单片机就偏低”的特征判断起来也不难把采样时间调长一档再测数值变好就说明问题出在这里。7.2 DMA数据错位、回调不进的排查清单DMA最典型的两个问题一个是数据错位一个是回调不触发。数据错位通常是因为DMA缓冲区长度和ADC序列通道数不一致。检查一下HAL_ADC_Start_DMA的第三个参数是不是等于Rank数量另外数据宽度是不是Half Word如果设置成Byte12位的数据会被截断看起来就会出现“前一半正常、后一半乱跳”。回调不触发就要分两层排查。先看DMA中断是否使能CubeMX创建DMA时通常会自动勾选NVIC中断但如果手写代码很容易漏掉HAL_NVIC_EnableIRQ。再看DMA工作模式如果是Normal模式传输完成一次后DMA就停了缓冲区不会继续更新看起来就像“回调只进了一次就再也不进”。想要持续采集必须使用Circular模式。还有一个小坑是ADC的DMA请求和EOCS位的关系。如果你把EOCS设成了“整个序列完成”DMA搬运节奏会和EOC标志产生微妙的对不齐数据组之间可能出现边界漂移。所以我在DMA模式下习惯保持EOCSelection为Single让每个转换完成都触发DMA请求数据流的节奏更加均匀。7.3 几个值得收藏的实操细节最后分享几个平时很少写进文档但很有用的细节。第一多通道DMA模式启动前最好先把缓冲区清零避免调试时看到一堆莫名奇妙的旧值干扰判断。第二F0系列没有硬件过采样想要提高分辨率或降低噪声可以在DMA回调里做多次采样求和、软件平均也可以在序列里把同一个通道配置成多个Rank用DMA一次性采多个样本效果相近但更省CPU。第三ADC数据右对齐时返回值是0到4095如果要在主循环里做百分比换算用整数运算时注意避免直接除以4095再乘以100这种顺序容易因为整数截断导致精度损失建议先乘后除。第四如果你用了低功耗模式比如STOP模式ADC和DMA都需要重新初始化不要指望从低功耗唤醒后还能自动继续工作。我在实际项目中做完三种方式的对比之后最大的感触是选择方案要把“数据量、实时性、主循环负担”这三个点想清楚而不是凭直觉觉得某一种方式“更高级”。很多人一上来就上DMA结果因为回调、缓冲区、触发条件这些细节没搞明白反而比用轮询花了更多调试时间。反过来明明需要连续采样却硬啃轮询不放也是给自己挖坑。如果这篇文章能让你少踩一次EOCS的坑、少调一个晚上DMA数据错位的问题那我觉得这些内容就值了。这个项目后续我还在扩展同一个采集链路的定时器触发版本如果大家有兴趣可以留言交流等我跑通之后再整理一套定时器触发加DMA双缓冲的完整方案出来。