做嵌入式这些年,ADC采集算是碰得最多的外设之一。早期我习惯用中断方式:每次转换结束进中断、读数据、出中断,单通道这么搞倒也没啥大问题。但一旦要同时采集电压、电流、温度好几个通道,中断频率一上来,主循环里的任务调度就会被频繁打断——LCD刷新开始卡顿、按键响应变迟钝、就连PID控制周期都被拖累。后来我把这套逻辑换成了DMA模式,ADC转换完的数据自己就跑到内存里去了,CPU全程不用操心,整个系统一下子清爽了很多。
今天的实战主题就是 STM32CubeMX + DMA 搞定 ADC 多通道采集,顺便把我趟过坑之后沉淀下来的几种滤波方案一并分享出来。无论你是刚入手 STM32 的新手,还是被 DMA 配置坑过、想搞懂背后原理的工程师,这篇应该都能给你带来点实在的帮助。
1. 为什么我推荐用DMA做ADC多通道采集
1.1 中断方式其实也能用,但成本太高
很多朋友一开始接触多通道 ADC 采集,习惯性用的是“转换完成中断”方案。ADC 每完成一次转换就拉一次中断,CPU 进中断把数据从ADC_DR寄存器读出来,然后再启动下一次转换。听起来没什么问题,但仔细算一笔账就明白了。
假设我用 3 个通道、单次采样时间 55.5 周期,再加上 12.5 个周期的转换时间,一个通道从启动到转换完成大约需要 68 个 ADC 时钟周期。ADC 时钟配置成 12MHz 的话,一次转换耗时大概 5.7 微秒。3 个通道一轮下来大约 17 微秒。如果还要跑连续转换,中断发生频率接近 60kHz。这个频率下,哪怕中断服务函数里只做“读寄存器 + 存数组”这两步操作,CPU 的有效处理时间也被吃掉了一大部分。关键是,中断服务函数里不适合做复杂数据处理,滤波、换算、协议打包这些活儿最终还是得回到主循环里干,数据积压的问题会越来越明显。
还有一个更隐蔽的问题:中断响应存在延迟。如果主程序正在处理某个临界区,或者一个更高优先级中断正在执行,ADC 转换完成中断会排队等待。这一等,下一次采样启动的时间就不固定了,采样间隔抖动直接影响到信号分析的准确性。对采样时序有要求的场景,这就是硬伤。
1.2 DMA的角色:一个不需要CPU盯着的搬运工
DMA 的全称是 Direct Memory Access,直译过来就是“直接存储器访问”。你可以把它想象成一个专职搬运工:ADC 转换完成一个数据,DMA 自动把ADC_DR寄存器里的值搬到内存指定地址;搬完一个继续搬下一个,全程不需要 CPU 插手。
用 DMA 做 ADC 采集时,整个工作流程变成了这样:CPU 启动一次 DMA 传输,然后该干嘛干嘛;ADC 按配置好的顺序扫描各个通道,转换结果被 DMA 源源不断地送到内存缓冲区;等一组数据搬完了,DMA 可以产生一个中断告诉 CPU“数据齐了,你来处理吧”。CPU 要做的只是初始化时动动嘴,数据处理时才真正干活,中间这段采集过程完全不占用任何 CPU 时间。
从系统架构的角度看,这不是简单的“中断换成 DMA”,而是把采集这个重复劳动从 CPU 手里完全剥离了。CPU 的主循环可以稳定地跑显示刷新、通信协议、控制算法,采样频率再高也不会阻塞业务逻辑。
1.3 三种典型场景,建议直接上DMA
结合我自己的项目经验,下面这三种场景基本是“非 DMA 不可”:
- 多通道连续采样,需要周期性刷新数据的场景。比如一个电源管理模块里要同时监控输入电压、输出电压、两路负载电流,DMA 模式一轮一轮刷新数据,CPU 定期取平均值就行。
- 主循环任务繁重,不能让中断反复打断的场景。比如带 GUI 刷屏、跑 Modbus 通信、做 PID 闭环控制,中断频率过高会直接拖垮实时性。
- 对采样时序一致性有要求的场景。像交流信号的过零检测、波形采样这种,每次采样间隔必须均匀,中断模式很难保证这一点,DMA 模式配合固定的 ADC 触发源可以做到严格稳定。
当然,中断模式也不是一无是处。如果你只是偶尔采一次数据、通道少、转换频率低,用中断反而更简单,没必要为了用 DMA 而用 DMA。选型的标准就一条:ADC 在不在你的关键路径上。
2. STM32CubeMX一步步配置:ADC多通道+DMA
2.1 时钟分频先算清楚:别让ADC超频工作
我用 STM32F103C8T6 当示例芯片来走一遍完整配置流程。新建工程选择芯片之后,首先要做的是在 Clock Configuration 页面把时钟树配好。F103 的最高主频是 72MHz,系统时钟走 PLL 倍频上去,这个大家应该比较熟,不多说。关键是看 ADC 的时钟分频。
F103 的 ADC 时钟挂在 APB2 总线上,APB2 的最高频率是 72MHz,但 ADC 模块自己的最高工作时钟是 14MHz,所以必须分频。在 CubeMX 的 Clock Configuration 界面里找到 ADC1 的时钟源,如果把 PCLK2 设为 72MHz,ADC 时钟分频就选 6 分频,得到 12MHz,安全合规。也有人选 4 分频得到 18MHz,超频状态下的转换结果位数会受影响,不建议这么干。
时钟这里我多提醒一句:ADC 时钟频率直接决定了转换时间和最高采样率。12MHz 下最短采样时间 1.5 周期配置时,单个通道一次转换最快需要 1.5 + 12.5 = 14 个 ADC 时钟周期,约 1.17 微秒。但注意,通道的输入源阻抗越高,需要的采样时间越长,否则采样电容充不满,转换结果就会偏小,这个后面滤波那节还会展开讲。
2.2 ADC参数逐项设置:扫描、连续、采样时间
进入 Pinout & Configuration,在 Analog 分类下找到 ADC1,先把需要采集的通道引脚勾选上。比如我接的是 IN0、IN1、IN2 三个通道,就把这三个勾上。
接着看 Parameter Settings 里的关键选项,我挨个说:
- Scan Conversion Mode:置为 Enabled。多通道采集必须开启扫描模式,否则 ADC 只会转换一个通道。
- Continuous Conversion Mode:置为 Enabled。开启连续转换,ADC 转换完一轮自动开始下一轮,配合 DMA 的循环模式,数据会一直刷新。
- Discontinuous Conversion Mode:保持 Disabled。不连续模式一般用于“每次触发只转 N 个通道”的省电场景,我们这里用不上。
- DMA Continuous Requests:置为 Enabled。这个选项翻译过来是“DMA 连续请求”,它决定了 ADC 转换完成后要不要持续向 DMA 发出数据传输请求。对于多通道连续采集,这里必须打开,否则可能只传一轮就停了。
- Number Of Conversion:填 3,因为我们用了三个通道。
- 每个 Rank 的采样时间:我建议至少选 55.5 周期,输入源阻抗比较高的时候直接上 239.5 周期。采样时间越长,采样电容充得越满,结果越接近真实值;代价是转换速度变慢。低速信号根本不在乎多出来的十几微秒,优先保证精度就好。
- ADC 数据对齐方式:右对齐。12 位分辨率下,右对齐后高 4 位是 0,读出来的数值直接用,换算电压也方便。
还有一个容易忽略的地方:End of Conversion Selection 选项。默认的 EOC flag at end of sequence 表示一组序列转换完之后才置位转换结束标志,这适合多通道场景;如果选成 EOC flag at end of conversion,每个通道转换完都会置一次标志,逻辑上有点绕。我们保持默认即可。
2.3 DMA参数设置:模式、数据宽度、优先级
ADC 参数配完,接下来是最关键的 DMA 配置。在 ADC1 的 DMA Settings 选项卡下点 Add,F103 的 ADC1 对应 DMA1 的 Channel1,这个是硬件固定的,CubeMX 会自动带出来。
参数设置我逐项说明一下:
- Mode 选 Circular,循环模式。循环模式的特点是 DMA 搬运完指定数量的数据后,地址自动回到起始位置,继续搬运下一轮。这样 ADC 连续转换,DMA 连续搬运,数据实时更新,CPU 只需要在回调里取数就行。
- Direction 选 Peripheral To Memory,方向是外设到内存。ADC 是外设,内存缓冲区是目的地,这个方向别选反。
- Peripheral Address 和 Memory Address 一般不用手填,CubeMX 根据外设和用户代码里的缓冲区自动处理。Data Width 这里,外设端和内存端都选 Half Word,也就是 16 位。因为 F103 的 ADC 是 12 位分辨率,一个转换结果正好占 16 位,用 Half Word 最省内存,数据也最规整。
- Priority 建议选 High。DMA 有多个通道同时工作时,优先级高的通道优先被服务。ADC 数据有实时性要求,给高优先级合适。
配置完保存工程,点击 Generate Code 生成代码,工具链选 MDK-ARM 或者 STM32CubeIDE 都行。
2.4 生成代码后的目录检查
生成完工程后,我习惯打开看一下目录结构。核心文件是adc.c和dma.c,初始化函数MX_ADC1_Init()和MX_DMA_Init()会自动生成。务必留意一行代码:HAL_ADC_Start_DMA()的真实调用时机。CubeMX 生成的代码里,DMA 初始化在main()函数中MX_DMA_Init()调用,ADC 初始化是MX_ADC1_Init(),整个顺序都不要动,如果先初始化 ADC 再初始化 DMA,某些系列芯片会出问题。
还有个细节:DMA 中断的优先级在stm32f1xx_it.c里默认没有配置,需要你在 NVIC 设置里手动打开 DMA1 Channel1 中断,否则采集完成回调函数根本不会触发。CubeMX 的 NVIC 页面里勾上 DMA1 channel1 global interrupt,重新生成代码即可。这一步经常被漏掉。
3. 代码实现:启动一次采集,剩下的交给DMA
3.1 定义缓冲区并启动DMA采集
打开main.c,在用户代码区定义一个数组用作 DMA 缓冲区:
/* USER CODE BEGIN PV */ #define ADC_CH_NUM 3 uint16_t adc_buf[ADC_CH_NUM]; volatile uint8_t adc_data_ready = 0; /* USER CODE END PV */注意,我定义的是uint16_t类型数组,因为 ADC 结果是 12 位,一个元素占 16 位。DMA 缓冲区的大小等于通道数,这个数组会被 DMA 按顺序填充:adc_buf[0]对应第一个使能的通道,adc_buf[1]对应第二个,以此类推。如果你在 CubeMX 里把通道顺序调整了,数组下标和通道的对应关系也要跟着变。
然后在main()函数的用户代码区域启动采集:
/* USER CODE BEGIN 2 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CH_NUM); /* USER CODE END 2 */这里有个细节容易踩坑:F1 系列 HAL 库中HAL_ADC_Start_DMA的第二个参数类型是uint32_t*,而我们传入的是uint16_t*数组,所以要强转。很多朋友编译报警,其实就是这里类型不匹配,强转一下就好。第三个参数是 DMA 传输的数据个数,这里传ADC_CH_NUM,也就是 3,DMA 每搬运 3 个数据就完成一轮传输。
启动之后,整个采集链路就自动跑起来了。ADC 连续转换,DMA 自动搬运,CPU 在主循环里该干啥干啥。
3.2 回调函数里的数据搬运和标志
DMA 每搬完一轮数据,会触发一次传输完成中断。HAL 库封装好的回调函数是HAL_ADC_ConvCpltCallback,我们需要重写它。直接在main.c的用户代码区添加:
/* USER CODE BEGIN 4 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { adc_data_ready = 1; } } /* USER CODE END 4 */主循环里判断标志再处理数据:
while (1) { if (adc_data_ready) { adc_data_ready = 0; // 到这里,adc_buf[0]~adc_buf[2] 就是一组完整的采集数据 process_adc_data(); } }这里我特别提醒一个时机问题:HAL_ADC_ConvCpltCallback是在中断上下文执行的,函数体里面不要做耗时操作。像电压换算、滑动窗口滤波这种计算,放在主循环里去处理;回调里只置个标志位,最多做一次数组拷贝,把数据快照下来,避免主循环处理期间 DMA 又刷新了缓冲区,导致数据一致性出问题。
想更进一步降低中断频率的话,可以把 DMA 缓冲区放大,比如定义成uint16_t adc_buf[ADC_CH_NUM * 16],DMA 一次搬完 48 个数据才触发一次回调,这样回调频率变成原来的 1/16,对 CPU 的打扰更小。代价是数据实时性变差,适合对实时性要求不高的监控类场景。
3.3 从原始值到真实电压:换算与多通道对齐
ADC 原始值是一个 0~4095 的整数(12 位分辨率下),要把它换算成真实的物理电压,公式很简单:
电压 = 原始值 × 参考电压 ÷ 4096参考电压在 F103 上默认就是 VDDA,通常是 3.3V。写成代码:
float voltage = (float)adc_buf[0] * 3.3f / 4096.0f;有一个非常常见的认知误区:12 位 ADC 的最大数字是 4095,为什么除数是 4096?因为 ADC 的满量程对应参考电压本身,数值范围是 0~4095,一共 4096 个刻度。如果除以 4095,算出来会偏大一丢丢。这种偏差单独看无所谓,但做校准表时能明显感觉到不整齐。统一用 4096 做除数。
多通道对齐的问题,说白了就一句话:数组下标和 CubeMX 里配置的通道扫描顺序一一对应。你在 CubeMX 的 Rank 列表里看到的顺序,就是 DMA 往缓冲区写数据的顺序。如果先配了 Rank 1 是 IN2,那么adc_buf[0]就是 IN2 的数据。很多人数据“串了”,基本都是配置顺序和代码用到的地方没对上。
4. 附赠干货:我在项目里实际用过的几种ADC滤波手段
4.1 滑动窗口平均滤波:实时性与平滑度的平衡
ADC 采集的原始数据通常带一定噪声,尤其是用开关电源供电时,纹波会直接耦合到采样值上。最简单的处理办法是算术平均,攒够 N 个点求平均。但算术平均有个问题:必须等攒齐 N 个点才能输出一个结果,实时性不好。
我在实际项目里用得最多的是滑动窗口平均滤波,也叫环形缓冲平均滤波。它的思路是维护一个固定长度的窗口,每次进来一个新数据,就把窗口里最老的数据踢掉,再对当前窗口里的所有数据求平均。这样每来一个有效采样点,就能立刻得到一个平滑后的输出,实时性和平滑效果兼顾。
#define WIN_SIZE 16 static uint16_t s_window[WIN_SIZE]; static uint8_t s_index = 0; static uint32_t s_sum = 0; uint16_t sliding_window_filter(uint16_t sample) { s_sum -= s_window[s_index]; // 减掉最老的数据 s_sum += sample; // 加上新数据 s_window[s_index] = sample; // 覆盖最老的位置 s_index = (s_index + 1) % WIN_SIZE; return (uint16_t)(s_sum / WIN_SIZE); }这段代码有几个点值得说一下。s_sum用的是 32 位无符号整数,16 个 12 位 ADC 的最大值是 16 × 4095 = 65520,用 16 位整形会溢出,这个坑我踩过一次,窗口一长结果就完全不对了。循环取模操作(s_index + 1) % WIN_SIZE在编译器优化后效率很高,也可以换成位运算(s_index + 1) & (WIN_SIZE - 1),前提是窗口大小是 2 的幂。
滑动窗口的窗口大小怎么定?我的经验是:信号变化慢、噪声频率高的场景,窗口选大一些,比如 32;信号变化快、需要及时响应的场景,窗口选小一些,比如 8 或 16。窗口越大,延迟越大,响应越迟钝,这是物理规律,没法绕过。
4.2 限幅滤波:先剔除“一眼假”的突变值
滑动窗口平均能防随机噪声,但对付不了突然冒出来的超大尖峰。比如电机启动瞬间产生的电磁干扰,可能让 ADC 值瞬间跳变几百甚至上千的数值。这种突变数据如果混进滑动窗口里,即使只有一个点,也会把平均值带偏一大截。
限幅滤波的思路很简单:如果当前采样值和上一次有效值之间的差值超过了一个阈值,就认为这次采样是异常数据,直接用上一次的值替代,或者干脆丢弃。
#define LIMIT_DELTA 50 uint16_t last_valid_value = 0; uint16_t limit_filter(uint16_t sample) { uint16_t delta; if (sample > last_valid_value) delta = sample - last_valid_value; else delta = last_valid_value - sample; if (delta > LIMIT_DELTA) return last_valid_value; // 判定为尖峰,用上次的值顶替 last_valid_value = sample; return sample; }阈值LIMIT_DELTA怎么定?要看信号的物理意义。比如温度传感器,正常变化速率每分钟最多几度,ADC 值每毫秒跳变超过 50 就非常可疑;但如果采的是电流信号,动态响应很快,阈值定 50 可能把正常的变化也当成尖峰给滤掉了。这个阈值没有标准答案,必须实测数据的波动范围再定。
我习惯把限幅滤波放在最前面,先做粗筛,剔除明显异常值,然后再进滑动窗口做平滑。两级串联的效果比任何单一滤波都稳。
4.3 中值+去极值平均:解决随机尖峰
如果数据里的随机尖峰比较多,而且你期望的输出值要更“稳”,可以考虑中值滤波和去极值平均的组合方案,也就是经典的去极值平均滤波。
思路是:采集 N 个点,把这 N 个点排序,去掉一个最大值和一个最小值,剩下的 N-2 个点求平均。这样既不放过真实的信号变化,又能把偶然出现的脉冲干扰剔除掉。
#define MED_SIZE 9 uint16_t median_avg_filter(uint16_t* raw_buf) { uint16_t sort_buf[MED_SIZE]; memcpy(sort_buf, raw_buf, MED_SIZE * sizeof(uint16_t)); // 选择排序,数据量小,效率无所谓 for (uint8_t i = 0; i < MED_SIZE - 1; i++) { uint8_t min_idx = i; for (uint8_t j = i + 1; j < MED_SIZE; j++) { if (sort_buf[j] < sort_buf[min_idx]) min_idx = j; } if (min_idx != i) { uint16_t tmp = sort_buf[i]; sort_buf[i] = sort_buf[min_idx]; sort_buf[min_idx] = tmp; } } uint32_t sum = 0; for (uint8_t i = 1; i < MED_SIZE - 1; i++) sum += sort_buf[i]; return (uint16_t)(sum / (MED_SIZE - 2)); }注意几个点:sort_buf是局部数组,大小为 9 时栈占用大约 18 字节,在 STM32 上毫无压力,但如果你把MED_SIZE改成 100,就得考虑栈空间够不够了。还有,这个函数需要攒够 9 个采样点才输出一个结果,输出频率是采样频率的 1/9,实际使用时要评估实时性能不能接受。
三种滤波方法做个简单对比:
| 滤波方法 | 实时性 | 抗尖峰能力 | 平滑效果 | 计算开销 |
|---|---|---|---|---|
| 滑动窗口平均 | 高 | 弱 | 中等 | 很低 |
| 限幅滤波 | 极高 | 强 | 无 | 极低 |
| 中值+去极值平均 | 低 | 强 | 好 | 中等 |
实际项目里我常用组合:先限幅,再滑动窗口,偶尔数据质量特别差时上中值+去极值。
4.4 别小看硬件滤波:软件解决不了的问题要靠它
软件滤波能处理很多噪声,但它治标不治本。有些问题,比如采样值整体系统性偏低、波动范围很大,软件再怎么算都救不回来。这时候要先回头检查硬件,常见问题有这么几类:
- 采样时间太短:输入源阻抗高时,采样电容没充满就开始转换,结果必然偏小。F103 的采样时间配置到 239.5 周期,能明显改善这个问题。
- 参考电压不稳:VDDA 上如果直接接了一堆负载,参考电压本身就在波动,ADC 结果当然跟着跳。解决办法是把模拟电源和数字电源分开,用磁珠或电感隔离,靠近 VDDA 引脚放一个 100nF 和 4.7uF 的去耦电容。
- 输入信号带宽太宽:如果输入信号本身带高频噪声,ADC 的采样保持电路会把混叠信号采进来。可以在 ADC 引脚前端加一个 RC 低通滤波,比如 100Ω 串 100nF 电容到地,截止频率大约 16kHz,够滤掉大部分开关噪声。
- 地回路问题:模拟地和数字地没有单点连接时,数字电路的开关噪声会通过地平面窜进模拟回路。PCB 布局上要保证模拟信号的回流路径尽量短,ADC 引脚远离高频时钟线和 PWM 输出线。
顺便提一嘴,现在不少新型号的 MCU 已经内置了硬件滤波机制,比如部分 GD32H7 系列自带 ADC 硬件滤波单元,配置好寄存器后 CPU 拿到手的直接就是滤波后的结果,软件压力小很多。选型的时候如果明确知道要用在噪声环境里,优先考虑这类带硬滤支持的型号,能省不少事。
5. 常见问题与排查技巧实录(建议收藏)
5.1 多通道数据错位、对不上号
现象:adc_buf[0]读到的不像是第一个通道的数据,整体好像错了一位。
最常见的原因是 CubeMX 里通道扫描顺序和代码里读取的顺序不一致。比如你在 CubeMX 里按 Rank 1 = IN2、Rank 2 = IN0、Rank 3 = IN1 配置,那么 DMA 填充顺序就是 IN2、IN0、IN1,adc_buf[0]对应 IN2 而不是你脑补的 IN0。排查方法很简单:回到 CubeMX 的 ADC 配置页,对照 Rank 顺序改代码注释,或者在初始化结构体里看Channel字段的排列顺序。
另一个容易被忽略的原因是 DMA 传输还没开始,第一次中断的数据是历史残留。一般启动 DMA 后,第一次回调里的数据可能不完全对位,稳妥的做法是初始化完成后先丢弃前几组数据:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CH_NUM); // 等待几组数据刷新后再开始使用 for (int i = 0; i < 5; i++) { while (!adc_data_ready); adc_data_ready = 0; }5.2 第一次采样值明显偏大或偏小
初次上电后,ADC 内部采样电容还没有完成充电,第一次转换结果往往会偏离真实值。如果连续转换已经开启了,一般只有第一轮数据受影响。
最简单的方案是启动后延时几毫秒再取值,或者干脆丢弃第一次转换结果。代码层面,我在启动 DMA 后加一段空等待:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CH_NUM); HAL_Delay(5); // 等采样电容充电 adc_data_ready = 0; // 清掉可能已经置位的标志这行HAL_Delay(5)不会影响采集链路,但能把第一轮“不干净”的数据挡在门外。
5.3 采样值一直跳,软件滤波救不回来
如果滤波参数已经调得够重了,数据还是在跳,基本就是硬件问题。排查优先级我自己是这样的,先给一个固定电位给 ADC 输入,比如直接用跳线把输入引脚接到 GND 或 VCC,看读值是否稳定。如果固定输入还跳,就是 ADC 配置或电源问题;如果固定输入稳定,那问题出在输入信号链路本身。
接着检查配置:ADC 时钟分频是否正确,是否超频;采样时间是否太短。再查电源:万用表量一下 VDDA 是不是稳定 3.3V,有没有明显纹波。最后查布线:输入信号线有没有和 PWM 线、时钟线平行走线过长。
按这个顺序查下来,绝大多数“软件救不回来”的跳动问题都能定位到根因。
5.4 DMA只进一次中断,之后再也不触发
这个现象非常经典:程序跑起来第一次回调进了,后面就再也没反应。
先说原因,DMA 工作在 Normal 模式而不是 Circular 模式时,搬完指定数量数据就停了,不会再自动搬运。如果 CubeMX 里 DMA Mode 忘记选 Circular,那就会“只跑一轮就罢工”。检查 DMA 配置,改成 Circular,重新生成代码就能解决。
还有一种情况是Continuous Conversion Mode没有打开。ADC 转完最后一通道后不启动新一轮转换,DMA 自然没有新数据可搬。把 Continuous Conversion Mode 置为 Enabled 即可。
还有一种更隐蔽的原因:CubeMX 生成的 DMA 中断没有正确使能。如果你在主函数里手动重开 DMA:
HAL_ADC_Stop_DMA(&hadc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CH_NUM);注意 Stop 和 Start 之间要确保 DMA 中断标志已经清掉,最好在回调里不调用 Stop,保持循环模式一直跑,从根上避掉这个问题。
5.5 快速排查速查表
| 现象 | 优先检查项 | 处理办法 |
|---|---|---|
| 无任何数据 | DMA 中断未使能 | NVIC 中打开 DMA1 Channel1 全局中断 |
| 只进一次中断 | DMA Mode 非 Circular | 修改为 Circular |
| 数据全部为 0 | 缓冲区地址不对/通道未使能 | 检查 DMA 缓冲区是否在 SRAM,引脚是否配置为模拟输入 |
| 数据错位 | 通道顺序和代码读取顺序不符 | 对照 CubeMX Rank 顺序调整代码 |
| 首个值异常 | 采样电容首次充电 | 启动后延时 5ms 或丢弃第一轮数据 |
| 数值整体偏小 | 采样时间过短 | 采样时间提高到 55.5 或 239.5 周期 |
| 数值跳动剧烈 | 硬件电源/地噪声 | 加强滤波电容,分离模拟地和数字地 |
| F1 平台 DMA 不工作 | 缓冲区放在 CCM RAM | 将缓冲区放在普通 SRAM,禁止使用__attribute__((at(0x10000000))) |
最后再分享一个我自己的使用习惯:软件滤波只是兜底手段,不要一上来就把滤波参数调得很重,那样只会掩盖硬件问题。先让硬件指标尽量做好——供电干净、地回路短、采样时间给够,这时候哪怕不滤波,数据质量也基本过得去。滤波只是在这个基础上再压掉最后一点波动而已。
另外,等这套 DMA 采集链路跑顺了,有一个特别值得继续做的扩展:把 DMA 缓冲区开大,配合HAL_ADC_ConvHalfCpltCallback实现“乒乓缓冲”,前半段在填的时候处理后半段,后半段在填的时候处理前半段,这样采样和处理可以真正做到完全并行。当然,这就不是今天这篇能说完的了,等你把 DMA 的基本功练扎实,自然就会发现它还有更多可玩的余地。