设备刚上电,我盯着串口助手那排数字,发现三相电流波形根本没法看——毛刺多、抖动大,CPU还被ADC中断占得满满的。后来把方案改成定时器触发+DMA双缓冲多通道采集,波形才终于干净了,主循环也从“一直在处理中断”变成了“偶尔出来冒个泡”。如果你也在用STM32做ADC采集,而且是多通道、要固定采样率,这套方案很值得抄作业。
这篇文章会把整个方案从头拆一遍:为什么用定时器触发而不是软件触发,DMA双缓冲到底在解决什么问题,CubeMX里每一步怎么配,代码怎么组织,以及我实测下来踩过的各种坑。适合做电机控制、传感器监测、电池电压采样、信号分析这类需要稳定采样节奏的STM32项目,F103、F407、H7系列都能用。
1. 方案设计:定时器触发与DMA双缓冲解决什么问题
1.1 为什么定时器触发比软件触发更靠谱
ADC采集最核心的需求是采样时刻确定。用软件触发(比如在主循环里调HAL_ADC_Start,或者用定时器中断里调转换)有个致命问题:主循环的执行时间是不确定的,中断也可能被别的更高优先级中断打断,每次采样之间的间隔都在抖。
采样间隔抖动对普通电压监测影响不大,但一到电机电流采集、FFT频谱分析、交流波形还原这些场景就出问题了。抖动会被当作噪声叠加进信号里,你测出来的波形畸变、FFT频谱出现杂散,本质不是ADC精度不够,而是采样时钟不稳定。
定时器触发是硬件级别的同步:定时器计数到设定值,硬件自动拉高触发信号,ADC立即开始转换,整个过程不经过CPU,不受中断优先级、主循环阻塞影响。采样间隔完全由定时器精度决定,这是软件触发做不到的。
这套方案里的定时器我一般选通用定时器TIM2、TIM3、TIM4这些带TRGO输出的,因为ADC的外部触发源基本都接在这些定时器上,用起来最顺手。高级定时器TIM1、TIM8也行,但没必要,资源配置太浪费。
1.2 DMA双缓冲的本质:写不冲突的流水线
多通道扫描一次会产生N个数据。如果不用DMA,你有两个选择:一是阻塞式轮询,CPU一直在等ADC转换,采样率稍高CPU就白等了;二是中断读取,每转完一个通道进一次中断,3通道就是3次中断,假设采样频率是10kHz,每秒就是3万次中断,光进出中断的开销就够喝一壶了。
DMA的活是把ADC转换完成的数据自动搬运到内存,全程不需要CPU干预。单缓冲的问题是:DMA往同一个Buffer里写,CPU也在读这个Buffer。如果CPU处理数据的速度跟不上DMA写入速度,前面还没处理完的数据就被新数据覆盖了。
双缓冲解决的就是这个冲突。它准备两份Buffer,DMA写第一份的时候,CPU处理第二份;DMA写第二份的时候,CPU处理第一份。两份交替使用,读写永远不打架。用一个不恰当的比喻:单缓冲是一个厨师只给一个菜板,备菜的厨师一刀下去把正在装盘的菜都剁了;双缓冲是两个菜板,备菜和装盘互不干扰。
STM32F103这一代DMA没有硬件双缓冲寄存器,但可以通过DMA的半传输中断和传输完成中断,在软件层面实现完全等价的双缓冲逻辑。F4系列的部分DMA Stream支持硬件Double Buffer模式,不过在代码组织上,软件拆半的做法更通用也更好理解,我后面讲的都是这个思路。
1.3 轮询、中断、DMA三种方案怎么选
我做过一个对比,核心结论用一张表讲清楚:
| 采集方式 | 典型CPU占用(3通道@10kHz) | 采样间隔抖动 | 代码复杂度 | 适用场景 |
|---|---|---|---|---|
| 阻塞轮询 | 接近100%,CPU全耗在等待 | 大,受主循环影响 | 低 | 低采样率、单通道、演示代码 |
| 每通道中断读取 | 30%~50%,中断开销大 | 较大,受中断响应影响 | 中 | 通道数少、采样率不高 |
| DMA搬运+中断处理 | 5%以下 | 极小,硬件触发保证 | 较高 | 多通道、高采样率、信号分析 |
我的经验是:只要通道数大于等于2,且单通道采样率超过1kHz,就直接上DMA方案。前期配置多花30分钟,后面能省下数不清的调试时间。
2. CubeMX配置:从时钟树到DMA参数的每一步
2.1 时钟树配置:ADC时钟绝不超14MHz
先解决ADC的时钟来源。以F103为例,ADC挂载在APB2总线上,默认APB2是72MHz。但F103的ADC最高工作频率是14MHz,所以必须分频,分频系数至少是6,也就是72/6=12MHz。
就是在CubeMX的Clock Configuration页面里,把ADC Prescaler设为6分频(PCLK2/6)。很多人配置时忽略了这一项,直接用默认的4分频,ADC跑在18MHz上,超出了规格,左边红色报警也没注意到。超频状态下ADC的转换结果线性度会变差,尤其高速转换时很明显。
ADC时钟值必须记下来,后面算转换时间要用。这里有个看起来很绕的点:ADC的转换时间计算公式里,有固定12.5个周期的转换开销,加上你配置的采样周期,总周期数乘以ADC时钟周期才是单通道转换时间。ADC时钟越小转换时间越长,所以也不是无脑把ADC时钟拉满,要结合采样率和采样周期平衡。
2.2 ADC多通道扫描模式配置要点
在CubeMX里打开ADC1,把你要用的通道(比如IN0、IN1、IN2)勾上。关键配置项:
- Scan Conversion Mode:Enabled。多通道必须开扫描模式,这样一次触发会按Rank顺序把所有通道转一遍。
- Continuous Conversion Mode:Disabled。我们用的是外部定时器触发,每触发一次转一轮,转完就停在最后一个通道。如果开了连续转换,ADC会不停循环转换,配合外部触发会出现触发信号还没到,ADC已经在转了,数据全乱。
- Number of Conversion:填通道数,比如3。
- 每个Rank对应的通道和采样周期:采样周期(Sampling Time)这里藏着一个重要权衡。
采样周期越长,内部采样电容充电越充分,结果越准,但单通道转换时间变长。对普通电压信号源,我建议至少选28.5周期。如果是高内阻的信号源(比如直接接光敏电阻、热敏电阻分压),选55.5周期更稳。如果信号源已经经过运放跟随、内阻很低,才能用1.5周期或7.5周期这种短采样时间冲刺高采样率。
ADC的External Trigger Conversion Source要选定时器的触发事件,这个在下一节讲。
2.3 定时器触发源:TRGO的Update Event
定时器触发ADC,走的是定时器的TRGO事件(Trigger Output)。在CubeMX里配置TIM2,把Trigger Output (TRGO) Parameters设置为Update Event。这样定时器每次计数器溢出更新,就自动产生一个触发脉冲送给ADC。
PSC和ARR怎么算?记住公式:
定时器触发频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)以F103为例,TIM2挂APB1,注意APB1总线频率是36MHz,但定时器时钟是APB1的2倍,也就是72MHz。这个坑我踩过:第一次计算时直接用36MHz去算,结果触发频率比预期低了一半,ADC数据明显变慢。
如果我要10kHz的触发频率,可以设PSC=71、ARR=99:
72MHz / (71+1) / (99+1) = 72MHz / 72 / 100 = 10kHz如果做到1kHz,就设PSC=71、ARR=999。这两个参数直接影响采样率,建议后期用宏定义留出来调。
2.4 DMA参数设置:Circular、半字宽与连续请求
DMA是这套方案的另一半。CubeMX里切到DMA Settings,添加一个ADC1的DMA请求,关键参数:
- Mode:Circular。必须有这个,否则DMA搬运完一轮数据就停了,配置错了的表现是:只有第一次触发后有数据,后面全是0。
- Data Width:Memory和Peripheral都选Half Word(半字)。ADC数据寄存器是16位的,宽度不匹配会导致数据错位,低字节高字节乱掉。
- Memory Address Increment:Enabled。DMA要把数据连续写进数组,地址必须自增。
还有一个容易遗漏的点:在F3/F4系列上,CubeMX DMA配置里有一个DMA Continuous Requests选项,务必设为Enabled。F1的老版本HAL库里没这个选项,默认就是持续请求,但F4如果不打开,DMA照样只响应一次就不动了,表现是采集一轮之后,arr里的数据再也没更新过。这个坑已经有无数人踩过。
Buffer长度怎么定义?假设我要3通道,每个半缓冲区存256组数据(一组3个通道),那么DMA总长度应该是:
FULL_BUF_LEN = 3 * 256 = 768在HAL_ADC_Start_DMA里传的就是768,不是256。很多人这里搞错,传了256,结果DMA搬了三分之一就不搬了,前3通道数据有,后面全空。
2.5 采样率与ADC转换时间预算
定时器触发频率决定了你“多久采一轮”,但ADC自己转换也需要时间。如果一轮扫描还没转完,下一次触发就到了,ADC就会报Overrun错误(数据溢出)。所以必须提前算好时间预算。
F103的ADC转换时间公式:
单通道转换时间 = (采样周期 + 12.5) / ADC时钟注意:12.5个周期是固定开销,加上你配置的采样周期才是总周期。
以ADC时钟12MHz、采样周期28.5为例:
单通道转换时间 = (28.5 + 12.5) / 12MHz = 41 / 12MHz ≈ 3.42μs 3通道一轮总耗时 ≈ 3.42 * 3 = 10.26μs如果定时器触发频率是10kHz,周期100μs,10.26μs的转换时间只占了十分之一,余量非常充足。
但如果你把采样率拉到100kHz,周期只有10μs,而转换一轮需要10.26μs,这就已经超预算了,Overrun必然出现。所以采样率上限不是拍脑袋定的,得按这条链路算出来:
允许的最大触发频率 = 1 / (通道数 × 单通道转换时间)实际工程我建议留2倍以上裕量,因为还有DMA响应延迟、总线竞争这些因素。上面3通道的例子,理论上限约97kHz,实际我会把触发频率控制在50kHz以内。
3. 代码实现:双缓冲数据流与信号处理
3.1 ADC校准与DMA启动
CubeMX生成工程之后,代码里要补三件事:校准、定义Buffer、启动DMA。
F103的ADC有个自校准功能,精度影响挺大。初始化后启动前调用:
HAL_ADCEx_Calibration_Start(&hadc1);F4/H7也有校准接口,函数名一样,但要注意必须在ADC已有供电、还没开始转换时调用一次。我一开始在每次启动DMA前都校准,反而导致启动慢,后来改成初始化时只校准一次。
定义Buffer时记得一个细节:数组元素类型是uint16_t,数据宽度和ADC寄存器匹配;数组长度必须是通道数的整数倍。我用宏定义管理,方便后期调整:
#define ADC_CH_NUM 3 // 通道数量 #define HALF_GROUP 256 // 每个半缓冲区采样组数 #define FULL_LEN (ADC_CH_NUM * HALF_GROUP * 2) // 双缓冲总共长度 __attribute__((aligned(4))) uint16_t adc_buf[FULL_LEN];aligned(4)是让数组4字节对齐,F4/H7上如果开了D-Cache,对齐之后做Cache操作才不出问题。F103不开Cache不影响,但保留这个属性没坏处。
启动采集:
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, FULL_LEN);启动之后,定时器一跑起来,ADC就开始按触发频率自动转换,DMA自动搬运。CPU完全不用管。
3.2 半传输与传输完成回调:双缓冲的交接棒
这是双缓冲方案的灵魂。DMA搬运整个Buffer,当它搬完前半段时,触发一次半传输中断;搬完后半段时,触发一次传输完成中断。利用这两个中断,把Buffer拆成两块使用:
volatile uint8_t half_ready = 0; // 前半段可读标志 volatile uint8_t full_ready = 0; // 后半段可读标志 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { // DMA刚把adc_buf[0] ~ adc_buf[HALF_GROUP*ADC_CH_NUM-1]填完 // 此时CPU可以安全处理前半段,DMA正在填后半段 half_ready = 1; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { // DMA刚把整个Buffer填完,开始回卷重写前半段 // 此时CPU可以安全处理后半段 full_ready = 1; }主循环里这样消费数据:
while (1) { if (half_ready) { half_ready = 0; process_adc_data(&adc_buf[0], HALF_GROUP); } if (full_ready) { full_ready = 0; process_adc_data(&adc_buf[ADC_CH_NUM * HALF_GROUP], HALF_GROUP); } }这个逻辑的精髓在于:DMA正在写前半段时,CPU处理的是后半段;DMA写后半段时,CPU处理的是前半段。两个半段交替使用,CPU读的数据永远是稳定的、不会被DMA覆盖的数据。
有一个红线必须遵守:在中断回调里不要做耗时操作。回调里我只置一个标志位,真正数据处理全部放在主循环。如果你在回调里直接做滤波、算平均值,中断时间一长,DMA数据可能来不及搬,最终会导致半传输、传输完成中断错过,数据逐渐错位。这个纪律比代码本身更重要。
3.3 数据处理:去直流、归一化与滤波
ADC原始值不是最终结果。我做交流信号采集时,会碰到两个绕不开的问题:直流偏置和幅值校准。
去直流:信号如果叠加了直流分量,比如运放输出抬高到1.65V,ADC读到的是偏置后的值。处理办法是先算一个长时间平均值作为直流偏置,然后每个采样点减去这个偏置:
#define SAMPLE_OFFSET (1700) // 实测的直流偏置对应的ADC值,约1.65V/3.3V*4095 int32_t sample_centered = (int32_t)adc_value - SAMPLE_OFFSET;如果偏置不确定,可以在初始化时先用前几百个点求平均再自动校准。
归一化:把ADC原始值映射到0.0~1.0或者实际物理量。最简单的归一化是除以满量程:
float norm = (float)adc_value / 4095.0f; // 12位ADC满量程4095要换算成电压:
float voltage = (float)adc_value * 3.3f / 4095.0f;更严谨的做法是用万用表实测两点电压,做两点标定求出系数和偏移,而不是简单按3.3V理论值折算,因为板子上的Vref实际可能不是标准3.3V。
滤波:ADC数据有随机噪声,简单粗暴的办法是滑动平均:
uint32_t filter_buf[FILTER_N]; uint8_t filter_idx = 0; uint32_t filter_sum = 0; uint32_t moving_average(uint32_t new_value) { filter_sum -= filter_buf[filter_idx]; filter_buf[filter_idx] = new_value; filter_sum += new_value; filter_idx = (filter_idx + 1) % FILTER_N; return filter_sum / FILTER_N; }我对噪声尖峰明显的信号,会先做一次中值滤波再去滑动平均。中值滤波对脉冲噪声的抑制效果比滑动平均好很多,代价是代码多一点点。简单粗暴版:把5个点冒泡排序取中间值,虽然慢,但数据量不大时完全够用。
3.4 配合串口DMA把数据送出去
采集到的数据最终要输出到上位机看波形或者做记录。如果直接用阻塞式的HAL_UART_Transmit,会让CPU卡在等待发送完成上,得不偿失。正确姿势是用串口DMA发送:
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)adc_buf, FULL_LEN * 2);发送完成后,在HAL_UART_TxCpltCallback里做下一轮发送准备。
我用串口接收数据做控制命令时,会配合空闲中断(UART IDLE)来识别一帧结束。HAL库里比较新的版本提供了HAL_UARTEx_ReceiveToIdle_DMA接口,既能DMA接收,又能在空闲时回调,省去自己判断帧尾的麻烦。比如上位机发一个“S”字母,就触发一次数据上传,整条链路就完整了:
定时器触发ADC → DMA搬运到双缓冲 → 主循环滤波处理 → 串口DMA异步上传 → 上位机实时刷新这条链路里CPU只负责轻量的数据处理和协议判断,数据搬运全部交给DMA,整体占用很低。实测3通道10kHz采样+串口波形上传,CPU占用不到10%,这个余量在复杂项目里非常宝贵。
4. 避坑指南:实测中踩过的7个坑
4.1 定时器触发频率太高,ADC疯狂Overrun
症状很典型:调试时发现HAL_ADC_ErrorCallback一直进,错误码是HAL_ADC_ERROR_OVR,采集到的数据也乱。
原因就是前面算的时间预算超了:ADC一轮扫描还没转完,定时器下一个触发就到了。我当时把3通道的触发频率拉到100kHz,单通道21周期采样,ADC时钟12MHz,一轮转换需要(21+12.5)/12MHz×3≈8.4μs,而100kHz的触发周期只有10μs,加上DMA响应延迟,几乎必然溢出。
解决思路按优先级:
- 降低定时器触发频率,让触发周期至少是扫描耗时的2倍以上。
- 缩短ADC采样周期,从55.5改成28.5或者7.5,牺牲一点精度换速度。
- 减少ADC通道数,比如把不需要的通道从扫描序列里去掉。
排查时可以先不跑业务代码,只保留ADC和定时器,用一个固定PSC/ARR的空跑程序看是不是还Overrun,能快速定位是不是时间预算问题。
4.2 数据错位:DMA长度与通道数不匹配
数据错位的表现是:你明明配了IN0、IN1、IN2三个通道,结果串口打印出来,IN0的位置时而出现IN1的值,时而出错乱。
最常见原因是DMA长度没有按照“通道数×组数”来定义。比如我配了3通道,但DMA长度设成了256(而不是256×3),这时候DMA只搬了前256个16位数据就回到起点,后面的IN1、IN2数据搬运到底址之外,数组内容完全错乱。
另一个隐藏原因是CubeMX生成代码后,DMA的Buffer长度被我手工改小过,但通道数没变。所以排查错位问题时,第一件事就是核对DMA配置里的数据长度是否等于ADC转换序列长度的整数倍。
4.3 volatile、优化和中断长阻塞
用Keil或IAR开-O2优化后,标志位失效是很经典的问题。我在回调里置half_ready,主循环里判断它,编译器如果没意识到这是中断修改的变量,会把判断优化成只读一次,结果主循环死等。
解决方式就是在共享变量的声明上加volatile修饰。我上面代码里的volatile不是随手写的,是踩坑换来的教训。
另一个问题是中断回调里做重活。我最早在回调里直接处理数据、做滤波、还调了HAL_UART_Transmit,结果不仅阻塞时间长,还导致后续DMA中断被堆积,最终数据丢失。后来一律改成“回调只置标志位,处理放主循环”,问题立刻消失。
不要小看这个纪律:哪怕你在回调里只放一个HAL_GPIO_TogglePin,如果采样率很高,每秒几万次中断翻转IO,开销也不小。再实用一点,调试时可以在回调里通过一个示波器引脚输出方波,测一下实际中断频率和占空比,心里有底。
4.4 参考电压、电源噪声与输入阻抗
硬件上的坑比软件更隐蔽,而且一踩就是“数据怎么调都不对”级别的。
参考电压:F103的Vref+引脚如果直接接3.3V,而这个3.3V纹波很大,ADC结果必然有随机噪声。我的板子是专门给ADC供电的参考电压芯片,Vref稳定后,同样代码采出来的噪声从±10个LSB降到±2个LSB。条件有限的,至少保证Vref引脚附近有足够的去耦电容——1μF+0.1μF并联,并且尽量靠近引脚。
电源噪声:模拟电路和数字电路共用一个电源平面时,PWM驱动、电机堵转引起的电压跌落会直接串进ADC。我用过最有效的方案是:ADC的模拟电源(VDDA)和参考电压单独滤波,AGND用磁珠或0欧电阻单点连接DGND,模拟走线尽量避开高频数字信号。
输入阻抗:这是个原理性问题。ADC内部是一个采样电容,转换开始时有短暂的充电过程,充电时间由信号源内阻和采样电容决定。如果信号源内阻太大,采样时间又短,电容来不及充满,采样值就会偏低。表现是:电压越大误差越大,或者直流量测还行、交流量波形衰减。
解决方式:一是增大ADC采样周期,给电容足够的充电时间;二是在信号源和ADC之间加一个运放跟随器,输出阻抗几乎为零,这时候用短采样周期也没问题。对于直接用电阻分压测电池电压这种场景,我建议分压电路的总阻值不要超过10kΩ,否则高采样率下会明显拉低测量值。
4.5 调试断点会让DMA“跑飞”
调试多通道采集时,我在半传输回调里设断点,单步调试,结果数据全乱了。原因是:暂停在断点时,定时器没有停止,DMA继续搬运,而CPU停在断点,半传输中断得不到及时处理,中断标志被挂起,后续回调可能错过。
正确做法是:调试时先停定时器,再看buffer。或者在回调里只设一个SWV/串口打印,不要打断运行。另外,在调试状态下观察到的adc_buf内容可能不是实时的,因为DMA一直在写内存,而调试器读取存在缓存,数据看起来“卡住”了,这并不代表程序有问题,先确认DMA enable状态寄存器再说。
4.6 多通道共享一个Buffer的“组”概念
3个通道共享一份DMA buffer时,读取数据的方式是:前3个位置是IN0、IN1、IN2各一组,然后又是IN0、IN1、IN2,以此类推。所以处理代码里要对通道序号求模。
很多人在这里直接写adc_buf[i]当成单通道处理,数据自然会乱。正确方式:
for (int i = 0; i < group_count; i++) { uint16_t ch0 = adc_buf[i * ADC_CH_NUM + 0]; uint16_t ch1 = adc_buf[i * ADC_CH_NUM + 1]; uint16_t ch2 = adc_buf[i * ADC_CH_NUM + 2]; // 处理这一组数据 }如果对单通道的采样组数没有概念,最容易写出越界访问。这里我建议先用一个有界循环测试数据,确认为止。
4.7 F4/H7的Cache一致性
如果你用的是F4系列且开启了D-Cache,DMA往内存写数据后,CPU读到的可能是Cache里的旧数据。这个问题在F1上不存在,但F4以上很常见。
解决方式是在每次读取DMA buffer前,先执行一下Clean和Invalidate:
SCB_CleanDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf)); SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));这个操作要在DMA写入完成、CPU读取前调用。尤其是半传输和传输完成两个回调里,对应半段的地址要单独清。体验过Cache坑的人都知道,数据偶尔是新的、偶尔是旧的,最折磨人。
5. 常见问题速查与调试经验
5.1 用“活数据”监测DMA是否在跑
我调试时有个习惯:在采集的同时,把某个通道的原始值定时经串口打个平均值出来,看这个值是否随着电位器旋转而改变。如果一动不动,先怀疑DMA根本没在跑。
再进一步,可以直接在调试器里看DMA的寄存器状态:DMA_NDTR(剩余传输计数)如果一直在变化,说明DMA正在搬运;如果静止不变,说明DMA已经停了。F103的DMA1寄存器地址在调试器里能直接看到,这个方法比猜数组内容快很多。
还有一个实用技巧:把DMA的回调函数里加一个GPIO翻转,然后示波器测这个引脚的波形频率和占空比。如果频率等于你预期的采样率,说明整条链路是通的。这个方法不需要任何调试器,现场排查特别管用。
5.2 波形可视化:别急着写上位机
不用急着写完整上位机,调试采集链路最快的方式是VOFA+或SerialPlot这类串口波形软件。协议简单,每秒发几十帧文本数据就能实时出波形。
我调试时会把3个通道的原始值滤波后,通过串口DMA发出去,格式就是逗号分隔的文本,上位机按“换行符”解析成多通道曲线。这样能直观看到:采样率是否稳定(波形横轴是否均匀)、数据是否错位(通道间波形是否互换)、噪声水平(波形毛刺大小)。
等波形确认稳定了,再去做正式的通信协议和上位机界面。分两步走能省下大量纠结时间。
5.3 问题速查表
| 现象 | 可能原因 | 检查顺序 |
|---|---|---|
| 采集全是0 | DMA没启动、DMA Continuous Requests未使能、触发频率为0 | 1.定时器是否在跑 2.Star_DMA是否调用 3.NDTR是否变化 |
| 采集全是4095 | 输入引脚悬空、输入电压超过Vref | 1.万用表测引脚电压 2.检查通道映射 |
| 数据错位乱序 | DMA长度不是通道数整数倍、采样周期太短导致阻抗问题 | 1.核对FULL_LEN 2.增大采样周期 |
| 波形毛刺大 | Vref不干净、采样周期过短、信号源内阻大 | 1.测Vref纹波 2.增大采样周期 3.加跟随器 |
| 只有第一轮有数据 | DMA配置成了Normal模式,不是Circular | 1.检查Mode 2.检查Continuous Requests |
| 定时器中断进,但回调不进 | NVIC里DMA中断没使能 | 1.NVIC配置 2.检查DMA中断标志 |
| 数据偶尔更新偶尔不变 | F4开了Cache,Cache污染 | 1.ADC buffer做Cache Clean/Invalidate 2.检查对齐 |
还有一个算不上Bug但容易让人懵的情况:刚启动时,第一轮半缓冲数据可能有异常,因为DMA是从中途开始搬的,前几个数据不可靠。处理办法是启动后先丢弃前几十组数据,等数据流稳定了再开始处理。我一般在初始化时先HAL_Delay(50),然后清一次标志位,把采集链路洗干净再进主循环。
另外建议把通道数、半缓冲组数、定时器ARR/PSC、采样周期这些参数全部用宏定义集中管理。前期调试时我一天要改十几次采样率,如果散落在代码各处,改一个参数要找半天。集中管理之后,调参就是改一行宏定义然后编译下载的事,省下来的时间足够多看几遍波形了。
这套方案我前后在F103、F407还有GD32的同类芯片上移植过,核心逻辑基本不变,区别只在时钟树配置和个别寄存器名字。你如果在移植时遇到某个寄存器编译不过,去芯片参考手册里搜名字,基本都能找到对应物。最后再分享一个实在的建议:第一次调通时别追求极限采样率,先用10kHz这种保守值把链路跑通,验证完波形和稳定性,再逐步往上拉。能把一套低占用、高确定性的采集方案稳定复现,比单纯调出一个高采样率但偶尔掉链子的工程要值钱得多。