news 2026/10/14 1:24:31

STM32F0 ADC+DMA实战:CubeMX配置、多通道错位与采样率优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F0 ADC+DMA实战:CubeMX配置、多通道错位与采样率优化

用STM32F030R8Tx做模拟量采集,绕不开ADC和DMA这个组合。芯片本身是Cortex-M0内核、主频最高48MHz,算力算不上宽裕,如果你的项目要连续采集多路电压、电流或者做音频级的采样,还用轮询等着ADC转换完成,CPU基本就被绑死了。DMA的活就是在外设和内存之间搬数据,搬完再通过中断告诉你一声,CPU中间可以去干别的。这篇文章把F030R8Tx上用HAL库做ADC DMA采集这件事从头到尾拆开讲,包括CubeMX配置、生成代码的用法、多通道数据错位和采样率上不去的排查思路,以及定时器触发、双缓冲、低功耗这类进阶玩法。不管是刚接触F0系列的新手,还是被DMA数据搞到头大的老手,都能找到可以直接抄走的方案。

1. 为什么非要把ADC数据交给DMA搬运

1.1 没有DMA时,一次采样要掉多少性能

先看一个最基础的场景:单路ADC采集,用阻塞式查询。流程大概是这样的:启动一次转换,死等EOC标志,标志置位后读ADC_DR寄存器,然后做滤波、算平均值,再来下一轮。单路、采样率几百赫兹的时候,这种做法没什么问题,代码还简单,CPU占用也不高。

但事情坏在多通道和连续转换上。假设你接了4路传感器,用ADC扫描模式一轮一轮循环采集,轮询代码就变成了:启动转换、查EOC、读DR、存数组,然后等整个序列转换完,再来一轮。每一轮转换期间CPU都在干等,虽然单通道转换时间只有微秒级别,4路加起来也就十几微秒,但频繁的查询和上下文切换会让主循环的实时性变得很差。尤其当主循环里还有按键扫描、显示刷新、通信任务时,ADC采集这个操作就会反复打断正常的程序流程。

换用中断方式会好一点,但也是个坑。每转换完一个通道进一次中断,在中断里读数据,如果采样率拉到每秒几千次甚至上万次,中断频率就会变得非常恐怖。中断服务函数里除了读个寄存器,其他什么都干不了,而且频繁进出中断还会让任务调度的抖动变大。F0这类M0内核的中断响应本来就不算快,中断密集的情况下,主程序的有效指令周期会被压缩得很厉害。实测中很多“程序跑得卡”“按键反应迟钝”的问题,源头往往就是ADC中断太频繁。

1.2 DMA适合什么样的采集场景

DMA要解决的核心问题,就是把“等待ADC转换完成”和“把数据搬进内存”这两件事从CPU手里剥离开。你只需要在启动时告诉DMA三件事:数据从哪里来(ADC的DR寄存器)、搬到哪去(内存数组)、搬多少(传输次数)。设定完成之后,ADC每转换完一个通道,DMA自动把结果搬进数组,搬完N个数据后再触发一次中断。

我按实际工程需求给场景分了类,哪些值得上DMA一目了然:

场景采集特点是否建议用DMA
单通道、低速、偶发采集手动触发一次,读一次没必要,查询或单次中断即可
多通道周期扫描4到10多个通道,持续反复采集强烈建议
高速连续采样采样率高于几千Hz,数据量大必须用DMA
低功耗电池设备希望CPU尽快休眠建议ADC+DMA,配合WFI
数据需要连续记录大批量搬移或存入缓冲必须用DMA

多通道扫描是F0这类芯片最典型的痛点场景。F0的ADC只有一个数据寄存器DR,转换完一路就更新一次DR,如果没有DMA把结果及时搬走,下一次转换就会把它覆盖掉。用DMA的话,每触发一次搬运,自动把DR里的当前值存到对应的数组元素中,通道顺序由硬件保证,CPU完全不用干预。

1.3 DMA的代价:通道占用和缓冲管理

DMA并不是免费的,它占用的是芯片里实在的硬件资源。F030R8Tx的DMA1一共只有6个通道,ADC用掉一个,串口发送、串口接收、SPI、定时器捕获这些外设再用几个,通道就非常紧张了。所以在设计初期就得想好DMA通道的分配,别等代码写完了才发现SPI想要的那个通道已经被ADC占了。

内存上的代价同样要考虑。DMA采集需要一个固定长度的缓冲区,这个缓冲区会一直常驻RAM。F030R8Tx的SRAM只有8KB,一个4通道、每轮4组、每组2字节的缓冲就要32字节,看似不多,但如果你做了音频采集或者大数据量缓冲,几十KB的数组在这个芯片上是放不下的,必须用环形缓冲加半满中断去滚动处理。

另外,用DMA循环模式时缓冲区会被不断覆盖,CPU读数据不能像普通数组那样随便读,否则可能读到“半个新数据、半个旧数据”的混合体。这个在后面第5章和第6章都会详细展开。

2. F0的ADC和DMA,动手之前先看清这些限制

2.1 ADC结构:只有规则组,没有注入组

很多从F1或F4转到F0的同学,第一件事就会找“注入通道”,结果翻遍参考手册都没有。F0系列的ADC和F1/F4最大的区别就是:没有注入通道,只有规则组。所谓注入通道,在F1上是用来做“高优先级打断”的转换组,可以在规则转换过程中插入一次紧急采样。F0把这一层砍掉了,所有通道统一走规则组,靠序列顺序一个一个转。

这意味着两点。第一,多通道采集只能靠扫描模式,按照你设置的Rank顺序依次转换,不存在“跳过某些通道先取另一个通道”的硬件机制。第二,既然所有转换结果都落在同一个ADC_DR寄存器里,通道和数据的对应关系完全依赖DMA的搬运顺序,一旦DMA配置里的数量、序列长度、数组类型这些参数不一致,数据错位几乎是必然的。后面第5章讲的坑,根子基本都在这里。

F0的ADC还内置了温度传感器和内部参考电压通道,需要采集芯片温度或者用内部基准校准采样值的时候可以直接选对应通道,但具体通道编号各型号有差异,动手前一定翻一下数据手册的ADC通道映射表,不要照搬其他型号的配置。

2.2 DMA请求映射不是固定的

F0的DMA在用法上和F1有个很大的区别:外设请求到DMA通道的映射是可重配的。F1上ADC1固定对应DMA1的Channel1,改不了。F0用了一个DMA请求映射寄存器,同一个外设的DMA请求可以接到任意一个DMA通道上,CubeMX里你也能看到“DMA Request”这一项是可以下拉选择通道的。

这个灵活性对资源紧张的小芯片来说是好事,但也带来了一个隐患:如果你手动写寄存器初始化,而不借助CubeMX,就很容易忘记设置请求映射,结果ADC一直不产生DMA传输,数据纹丝不动。用HAL库的时候,MX_DMA_Init里会自动把映射配好,但你要能看懂生成代码的逻辑,否则排查起来会很被动。

DMA优先级也要留心。6个DMA通道同时有请求时,优先级高的先响应。ADC是持续产生数据的外设,如果它的DMA通道优先级设成了Low,而另一个高优先级的通道搬运又比较频繁,ADC的DMA请求就可能被长时间压制。F0的ADC没有FIFO来缓冲,数据被压住之后要么覆盖要么丢失,表现出来就是缓冲区里偶尔夹杂几个坏数据。我的建议是:ADC的DMA优先级至少设High,如果还有更重要的外设,再单独评估。

2.3 ADCCLK别超过14MHz,采样时间和校准一个都别省

F0的ADC输入时钟有硬性上限:14MHz。F030R8Tx主频48MHz,APB时钟48MHz,ADC预分频如果选2分频,ADCCLK就是24MHz,直接超限,转换结果会乱掉。所以在CubeMX里配时钟,ADCCLK只能选4分频(12MHz)或者8分频(6MHz),这个参数不是想怎么填就怎么填的。

PCLKADC预分频ADCCLK是否合规
48MHz1分频48MHz超限
48MHz2分频24MHz超限
48MHz4分频12MHz合规
48MHz8分频6MHz合规

采样周期也要认真对待。F0的ADC采样周期可以在1.5周期到239.5周期之间选,采样周期越短,等效采样速率越高,但对信号源内阻越敏感。如果传感器输出阻抗比较高,采样电容还没来得及充满就开始转换了,这时候读到的电压就会偏低,而且不同通道之间可能表现出不一样的偏差。这种情况在单通道上很难发现,多通道对比时特别容易露馅。

12位分辨率下,F0的转换时间大约等于采样时间加上12个ADCCLK周期。以12MHz的ADCCLK为例,选1.5周期采样时,单通道转换大约13.5个周期,约1.125微秒;选239.5周期采样时,单通道大约251.5个周期,约20.96微秒。通道一多,这个差别就会明显影响整体吞吐量。后面算采样率时,用这个公式很容易得出理论上限。

还有一个很多人忽略的步骤:校准。F0的ADC上电之后,手册明确要求先做一次校准再开始转换。CubeMX生成的初始化函数里不会自动执行校准,需要自己在启动采集之前调用校准函数,后面第4章会给完整的启动代码。

3. 从CubeMX勾选到跑通,一个可以照抄的配置过程

3.1 时钟与ADC基础参数

用CubeMX打开STM32F030R8Tx工程后,先把时钟树配好:系统主频设48MHz,ADC时钟源选择PCLK除以4。不同版本的CubeMX界面略有差异,有的版本直接在ADC配置里显示“Clock Prescaler”下拉框,有的版本需要去时钟树里单独配置,但最终目的就是把ADCCLK定在12MHz左右。

进入ADC1配置页面后,按照下面这张表设置基础参数:

参数项推荐值说明
Scan Conversion ModeEnabled多通道必须开启扫描
Continuous Conversion ModeEnabled配合DMA循环模式,连续出数
Discontinuous Conversion ModeDisabled不按组间断转换
Resolution12 bitF0最高12位分辨率
Data AlignmentRight alignment右对齐,读寄存器最直观
External Trigger Conversion SourceSoftware Start前期调通用软件触发
DMA Continuous RequestsEnabled让DMA请求持续有效,防止序列结束后断流

这个配置单并不是唯一的,定时器触发时要改触发源,低功耗场景时连续转换模式可能要调整,但作为第一个跑通版本,照这个配最省事。

3.2 通道序列和DMA设置

通道配置在ADC1的通道列表里完成。以4通道为例,把ADC1_IN4、ADC1_IN5、ADC1_IN6、ADC1_IN7依次加入到转换序列中,然后一个一个设置采样时间。这里有个原则:前期调试用稍微长一点的采样周期,比如13.5周期或者28.5周期,让结果更稳定。后续如果发现采样率不够,再逐级往下调,不要一上来就选最短的1.5周期。

DMA配置是在DMA Settings选项卡里添加的。点Add,选择ADC1的DMA请求,然后按下面的值设置:

DMA配置项推荐值原因
DirectionPeripheral To MemoryADC数据流向内存
ModeCircular循环搬运,ADC连续转换时不用反复重启动
Peripheral IncrementDisabled外设地址固定为ADC_DR
Memory IncrementEnabled数据依次写入数组
Peripheral Data SizeHalf WordADC_DR是16位宽
Memory Data SizeHalf Word和源宽度一致
PriorityHigh优先响应ADC请求,减少数据覆盖风险

DMA Mode这里选Circular是特别关键的一步。如果选Normal,DMA搬完一轮之后就会停下来,要么ADC继续转但没人搬数据,要么你必须在回调里再手动启动一次DMA。Circular模式下DMA会在搬完全部数据后自动重新装载计数器,ADC连续转换时数据始终有地方放,CPU全程不用管。

3.3 生成代码后的工程骨架

CubeMX生成代码之后,工程里会出现三个相关的初始化:MX_GPIO_Init、MX_DMA_Init、MX_ADC1_Init。在main函数里它们会依次被调用,其中MX_DMA_Init必须在MX_ADC1_Init之前执行,因为ADC初始化时需要通过链接宏把DMA句柄挂到ADC句柄上,这个顺序CubeMX默认已经排好了,不用改。

实际用户代码只需要补三件事:定义缓冲区、启动ADC DMA、在回调里处理数据。最小可运行框架是这样的:

#define ADC_CH_NUM 4 #define ADC_BUF_LEN 4 static volatile uint16_t adc_buf[ADC_BUF_LEN]; static volatile uint16_t user_data[ADC_BUF_LEN]; void adc_dma_start(void) { for (int i = 0; i < ADC_BUF_LEN; i++) { adc_buf[i] = 0; } if (HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) { Error_Handler(); } HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance != ADC1) { return; } for (int i = 0; i < ADC_BUF_LEN; i++) { user_data[i] = adc_buf[i]; } }

启动函数里先清一遍缓冲区,再校准,最后启动DMA。第一次上电时如果不校准,ADC内部比较器的失调可能会让你的转换结果产生一个固定的偏移,表现为输入接地时读数不是0,或者输入电压为某一固定值时读数始终偏大偏小。校准函数在HAL库里的名称可能随版本略有不同,但F0上基本都有对应接口,找不到时去HAL的ADC扩展文件里翻一下,Cortex-M0芯片上没有校准因子的概念,校准是内部自动完成的,调用接口只是触发一次校准流程。

4. HAL层的使用逻辑:启动、停止和回调

4.1 缓冲区应该用哪种类型

缓冲区类型这一点,我见过太多人在上面栽跟头。F0的ADC是12位的,数据存在16位宽的DR寄存器里,所以DMA搬运时外设数据宽度要配置成Half Word,也就是16位。对应到C语言,缓冲区类型就应该用uint16_t数组。

有些人习惯用uint32_t数组去接数据,想着以后处理方便,但这时如果DMA配置仍然是Half Word,那么DMA搬运来的每个16位数据会依次填进32位数组的低16位和高16位,原本属于通道0的数据可能落在数组第0个元素的低半字,通道1的数据落在第0个元素的高半字,看起来每组数据都错位了。反过来,你把DMA配置改成Word,Peripheral Data Size又得改成Word,可外设寄存器明明只有16位有效数据,这样组合出来的行为在不同HAL版本下还不完全一致,最稳妥的做法就是外设半字、内存半字、缓冲区用uint16_t,三者严格统一。

缓冲区长度也要算对。如果一次要采集4个通道,长度为4就够了,每个元素对应一个通道。但如果之后要做双缓冲或者保留多轮数据,长度就要按“通道数乘以轮数”来算,比如4通道保存4轮,长度就是16。这个长度要和ADC转换序列长度以及DMA传输次数保持一致,否则就会出现第5章要讲的错位问题。

4.2 Start_DMA 到底做了什么

HAL_ADC_Start_DMA这个函数,本质上做了这么几件事:把DMA句柄里配置好的源地址、目的地址、传输次数搬到DMA寄存器里;把ADC的状态切换到转换中;清除相关标志位后启动ADC转换。也就是说,CubeMX生成的MX_ADC1_Init和MX_DMA_Init只是“静态配置”,真正让硬件跑起来的是这个Start函数。

调用方式要注意参数类型:

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);

第二个参数类型是uint32_t指针,但你传进去的实际上是一个uint16_t数组的首地址,在使用前强转一下。这个强制转换看起来有点奇怪,但HAL库内部并不会把缓冲区当成32位数组来访问,它只把这个地址原样交给DMA,DMA的配置决定怎么读写这块内存,所以强转在这里是安全的。第三个参数是DMA传输次数,也就是半字的个数,不是字节数,更不是通道数以外的什么倍数,它是和缓冲区长度一一对应的。

启动之前一定要做校准。校准要放在ADC时钟使能之后、开始转换之前。如果你在回调里看到第一批数据的值明显异常,先检查一下是不是校准没跑,或者校准在启动之前被跳过。

4.3 四个回调的真正触发时机

HAL库把中断回调分成多个层次,ADC DMA场景下你可能会碰到这几个:

回调函数触发时机使用场景
HAL_ADC_ConvCpltCallbackDMA搬完整一轮,即N次传输完成常规数据处理
HAL_ADC_ErrorCallbackADC运行出错时错误排查,打印错误来源
HAL_DMA_RxCpltCallbackDMA接收完成,含义同上底层DMA处理
HAL_DMA_RxHalfCpltCallbackDMA搬了一半时触发双缓冲,半满处理

实际上,HAL库内部会把DMA中断和ADC中断串起来。当DMA完成一组传输后,最终会回调到HAL_ADC_ConvCpltCallback,你绝大多数场景只需要实现这个回调就够了。HAL_ADC_ErrorCallback也不能空着,在调试阶段至少放一个断点或者串口打印,否则ADC发生溢出、DMA传输错误这类故障时你根本不知道。

回调函数里有一个重要的原则:干活要快。回调运行在中断上下文里,你要是在里面做浮点运算、滤波、串口打印这些重操作,就会拖住整个中断响应,尤其采样率高的时候,下一个DMA中断可能已经在排队了,处理不及时数据就可能覆盖。最合理的做法是回调里只负责把数据搬到user_data这种“安全区”,真正的算法和业务逻辑放到主循环里处理。

4.4 停止、重新启动与数据一致性

停止ADC DMA采集用HAL_ADC_Stop_DMA(&hadc1),这个函数会同时停掉ADC转换和DMA传输。停止之后如果重新启动,建议按“清缓冲区、校准、Start_DMA”的顺序再做一遍。很多人直接调用Start,结果发现重启后第一帧数据里带有上一次的旧值,就是因为缓冲区没有清。

连续采集状态下,还有一个数据一致性的问题值得注意。DMA是持续往数组里写的,你在主循环里读数组时,DMA可能在任意时刻更新某个元素。也就是说,从数组第0个元素读到第3个元素的过程中,第1个元素可能已经被新的转换结果覆盖了。最直接的解决办法是:所有数据读取都在转换完成回调里进行,把整份数据一次性搬到另一块缓冲区后,主循环再读那一块。这种方法不是万无一失的,但对大多数控制系统已经够用,真正做到采样快照隔离的方案,在第6章的双缓冲部分会展开讲。

5. 多通道错位、采样率上不去,先把这几个坑排掉

5.1 坑一:ADC序列长度和DMA搬移数量对不上

现象很典型:配置了4个通道采集,DMA缓冲区也开了4个长度,结果跑起来发现只有前两个通道的数据在变化,后面两个通道要么是0,要么是上一次的残留值。

排查链路我建议按顺序走。第一步,先确认CubeMX里ADC的转换序列里到底加了几路。在F0的HAL库里,“Number Of Conversion”这个参数决定了扫描模式的序列长度,它和你在界面上勾选的通道数不一定自动同步,如果你改了通道列表但没重新生成代码,序列长度可能还是旧的。第二步,确认DMA传输次数,也就是Start_DMA的第三个参数,它必须和缓冲区长度、序列长度三者一致。第三步,调运行时读一下DMA的当前传输计数寄存器,方向是对的话,DMA在每次中断后计数会重新加载,从计数变化能判断搬运是否真的在进行。

根因一般就两个:要么序列长度短于实际通道数,导致后面的通道根本没被ADC转换过;要么DMA缓冲区长度少于序列长度,导致序列后段的转换结果没有位置存放,数据被丢弃或者写入越界的地址。解决方法是把三者统一:序列长度、DMA传输次数、缓冲区元素个数全部设为同一个值。如果设计上需要一次保存多轮数据,可以让DMA传输次数大于序列长度,但要保证缓冲区按“序列长度乘轮数”来组织,而且要把轮次边界在回调里手动处理好。

5.2 坑二:缓冲区类型和数据宽度不一致

现象稍微隐蔽一点:数据每个通道都在更新,但通道顺序是乱的,比如输入通道0接1V、通道1接2V,读出来却变成通道1是1V、通道0是2V,有时候还出现间隔性的错位。

问题多数出在DMA外设数据宽度、内存数据宽度和缓冲区类型三者没有统一。前面第4章讲过,F0的ADC_DR是16位寄存器,DMA外设端用Half Word,内存端也必须是Half Word,缓冲区应该是uint16_t数组。如果你用了uint32_t数组,配置却是Half Word,那么每两个半字数据才凑成一个32位元素,从第二个元素开始,通道关系就全部偏了。如果反过来,内存数据宽度配了Word,缓冲区用uint32_t,DMA会按32位为单位搬运,而ADC_DR的高位本来就是0,读出来数值虽然对得上,但效率低,而且F0的DMA内存端按Word递增时,对地址对齐的要求更严格,一不小心就卡在未对齐访问上。

排查方法也很直接:临时把缓冲区打印出来,按16进制看每个16位半字的值,然后和实际输入电压做对比。如果是数据宽度不匹配,你能看出每个半字的有效数据是0X0XXX这样的12位值,而相邻的两个半字才组成一个有效的32位读数。解决方式就是第4章说的:外设半字、内存半字、uint16_t数组,三者严格统一,不要乱配。

5.3 坑三:第一波数据和停止重启后的数据异常

现象集中在启动阶段:刚上电时前几十个采样点全是0或者全是满量程,跑一段时间才正常;或者每回Stop之后重新Start,第一帧数据里总有一个通道的值是明显错误的。

这个坑的原因通常有三层。第一层是缓冲区里的旧数据没有被清空,DMA启动后第一轮搬运开始前,缓冲区保留的是上一次的数据,你把它当新数据用了。解决方法是启动前对缓冲区做一次清零,代码里用一个循环把整个数组置0。第二层是校准没有执行或校准后没有稳定就开始了转换,F0手册要求上电后校准一次,HAL的校准函数会同步等待校准完成,所以在Start_DMA之前调用校准函数,理论上不会有问题,但如果你在非常早的初始化阶段就调用了校准,之后ADC时钟又被重新配置过,那校准结果就是无效的,需要在Start前重新校准。第三层是ADC刚上电时内部模拟电路还没完全稳定,特别在低温场景下,前几组数据偏差可能比较大。

最稳妥的做法是:启动后先执行校准,清空缓冲区,然后丢弃前几轮转换结果。实现上可以在回调里加一个计数器,比如前8次回调只读丢弃不处理,等数据稳定后再真正启用。这个手段在电池供电和传感器场景里很常用,代价是启动后多几十毫秒的建立时间,换来的是干净可靠的数据起点。

5.4 坑四:采样率上不去、采样值毛刺大

先说理论采样率怎么算。假设ADCCLK=12MHz,12位分辨率,采样时间选1.5周期,那么单通道转换时间是13.5个ADCCLK周期,约1.125微秒。4通道一轮大约4.5微秒,整个序列的理论最高扫描频率约222kHz,但这是纯硬件极限。你的程序如果有中断、滤波、串口打印这类操作,实际能稳定处理的采样率会大打折扣。

毛刺和采样率上不去,常见原因有四个。第一,ADCCLK超出14MHz上限,转换结果不稳定,这个在时钟配置阶段就埋下了。第二,采样时间太短,信号源驱动能力不足,导致采样电容充不满,读到的电压偏低且有波动,解决方法是增加采样周期。第三,DMA回调里做了太多耗时操作,特别是浮点和串口打印,直接拖慢了整个采样节拍,解决方法是回调里只搬数据,算法放到主循环。第四,电源和模拟参考电压太脏,ADC的结果本身就包含噪声,这个不是软件能解决的,需要检查VDDA的滤波电容和参考电压的稳定性。

给一个常见配置下的理论计算参考:

ADCCLK采样周期单通道转换周期数单通道转换耗时4通道一轮耗时
12MHz1.5周期13.5周期1.125us4.5us
12MHz13.5周期25.5周期2.125us8.5us
12MHz239.5周期251.5周期20.96us83.8us

从这个表能看出来,单通道采样时间从1.5拉到239.5周期,4通道一轮总耗时从4.5微秒涨到83.8微秒,差了将近19倍。所以如果你的系统对采样率没有硬性要求,采样时间可以大一点,稳定性好;如果采样率紧张,就得在信号质量和吞吐量之间做权衡。

6. 从能用变成好用:定时器触发、双缓冲与低功耗采集

6.1 定时器触发,比软件触发更稳定

软件触发有个天然的毛病:你无法精确控制两次转换的间隔。主循环里任务调度有抖动,期间中断也有抖动,两次软件启动ADC之间的时间间隔就不可能完全均匀。对于传感器慢速采集这无所谓,但对于需要严格等间隔采样的场合,比如电流环、音调采集、振动分析,采样间隔抖动会直接变成数据里的噪声。

解决方法是让定时器来触发ADC。在CubeMX里选一个定时器,比如TIM3,配置它的更新事件作为触发输出。假设采样频率需要10kHz,APB时钟48MHz,定时器不分频的话,PSC=0,ARR=4800-1,每个4800个计数周期产生一次更新事件,对应频率就是48MHz除以4800,等于10kHz。然后在ADC1配置里把External Trigger Conversion Source改为TIM3 Trigger Out,触发沿选上升沿,同时把Continuous Conversion Mode改成Disable。此时每次定时器触发,ADC只执行一轮序列转换,DMA搬完N个数据后在Circular模式下等待下一次触发。这样整个采样节拍完全由硬件维系,CPU只负责在DMA传输完成后取数据。

定时器触发的代码逻辑和软件触发几乎一样,只是初始化时不手动调Start,而是让定时器先启动,ADC在收到第一个触发边沿后自动开始转换。调试时注意一点:定时器更新的第一个事件可能出现在意想不到的时刻,所以数据采集同样建议丢前面几帧。

6.2 半满中断做双缓冲

DMA连续采集模式下,你总是面临一个问题:缓冲区在被后台不断改写,你的业务代码到底什么时候读数据最安全。单缓冲的做法是在转换完成回调里把数组整体拷贝到另一块内存,但如果拷贝期间DMA已经开始写下一轮数据,还是可能读到不一致的快照。

F0没有F4那种硬件双缓冲(Memory0/Memory1自动切换),但可以用DMA的半传输中断来实现类似效果。思路是这样的:把缓冲区扩大为两倍长度,比如2乘N,DMA先填前N个元素,填到一半时触发半传输中断,说明前一半数据已经稳定,这时候CPU可以去处理前一半;DMA继续填后N个元素,填完触发传输完成中断,说明后一半数据已经稳定,CPU再去处理后一半。注意,这期间DMA又会重新装填前一半,但半传输中断和传输完成中断交替触发,正好把CPU的处理节奏和DMA的写缓冲节奏错开。

代码层面需要处理的是DMA层的半传输回调。HAL里对应的是HAL_DMA_RxHalfCpltCallback,而ADC库的HAL_ADC_ConvCpltCallback只对应全部传输完成。半缓冲和全缓冲的中断回调会先后触发,你需要在回调里用一个标志告诉主循环“现在可以处理哪个半区了”。伪代码是这样的:

#define DMA_HALF_LEN 4 #define DMA_FULL_LEN 8 static volatile uint16_t adc_dma_buf[DMA_FULL_LEN]; static volatile uint8_t half_ready = 0; static volatile uint8_t full_ready = 0; void HAL_DMA_RxHalfCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma == &hdma_adc) { half_ready = 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance != ADC1) { return; } full_ready = 1; }

主循环里检测到对应的标志后,就处理相应的半个缓冲区,处理完清标志。这个方案比单缓冲快很多,因为CPU不用等待整个缓冲区填满,只要半区数据稳定就能开始工作,采样数据处理的有效并行度几乎翻倍。但要注意,回调里尽量不要直接开始业务处理,而是和主循环约定好“谁消费哪个半区”,否则两个中断和主循环同时访问同一段缓冲区,一样会打架。

6.3 WFI低功耗采集的注意事项

F0在电池设备里很常见,一个很大的卖点就是功耗低。做ADC采集时,最常见的方法是让CPU进入WFI睡眠模式(等待中断),DMA在后台采集,每完成一轮转换通过中断唤醒CPU做一次处理,然后继续睡。这个模式的省电效果很明显,CPU大部分时间不跑指令。

要注意的是,F0的STOP模式会关闭外设时钟,ADC和DMA都会停摆,所以低功耗采集时不能进STOP,用WFI就够了。WFI只是把CPU核心停下来,外设时钟照常跑,ADC和DMA继续工作,中断到达后CPU自动醒来。实际工程中你还会发现,定时器触发配合WFI是最舒服的组合:定时器保证采样间隔,DMA保证数据搬运,CPU只在采样完成的回调里醒一小会儿。

有一点必须提醒:中断回调里绝对不能做阻塞型操作,尤其是串口打印。串口打印一个字符的时间,可能比DMA半满周期还长,你在中断里打印,DMA已经写完了下一个半区,数据处理就乱套了。我的习惯是:回调里只置标志和搬数据,主循环里检测到标志后再做打印、存储、浮点运算这些事。这样既能保证采样率,又能保持代码清晰。

最后说一个我调F0 ADC DMA时一直沿用的习惯:拿到板子先别急着写应用逻辑,找一个已知稳定的直流电压,接到某一个ADC通道上,把adc_buf里的值和实际电压的对应关系打出来,确认每个通道的读数和预期一致后,再放心地上多通道业务代码。这个习惯可以把上面这些坑和你的应用逻辑彻底隔离开,排查起来会快很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 1:24:16

leetcode_3727 最大平方交替和

1. 题意 给你一个整数数组 nums。你可以以任意顺序 重新排列元素 。 数组 arr 的 交替得分 定义为&#xff1a; scorearr[0]2−arr[1]2arr[2]2−arr[3]2...score arr[0]^2 - arr[1]^2 arr[2]^2 - arr[3]^2 ...scorearr[0]2−arr[1]2arr[2]2−arr[3]2... 在对 nums 重新排列…

作者头像 李华
网站建设 2026/10/14 1:24:15

高分辨率遥感影像土地覆盖分类:可迁移深度模型实战指南

1. 从一张高分影像说起&#xff1a;为什么土地覆盖分类值得认真做第一次接触高分辨率遥感影像分类&#xff0c;是帮一个做城市绿地调查的朋友处理一批亚米级航拍图。他给的需求很朴素&#xff1a;把图里的建筑、道路、水体、植被、裸地分开就行。我原以为拿个现成的分类模型跑一…

作者头像 李华
网站建设 2026/10/14 1:24:04

Nmap端口扫描实战:从协议原理到企业级资产测绘

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:23:48

一维生物面试题:解码信息建模与状态机思维

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:23:47

Flutter Provider状态管理核心原理与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:23:21

Spring Security 多页面登录:多过滤链拆分登录上下文实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华