拿到GD32的ADC外设,第一反应容易觉得“学过STM32就万事大吉”。我最早接触GD32开发板时也这么干过,直接把熟悉的ADC配置流程照搬过来,结果同一个通道采出来的数值忽高忽低,越调越发毛。后来老老实实把GD32的ADC时钟树、采样周期和校准流程单独过了一遍,才发现问题基本都出在细节上:时钟分频对不对、采样时间够不够、校准有没有做、多通道DMA的缓冲顺序是不是被自己搞反了。
这篇笔记打算从最基础的认知讲起,然后给一套可直接参考的单通道、多通道DMA配置方案,再聊数据换算和片内校准,最后把我在实际调试中踩过的坑和滤波经验一并整理出来。适合刚接触GD32、或者正从其他Cortex-M平台迁移过来的开发者,照着排查能少走不少弯路。
1. 上手前必须搞清的GD32 ADC基础认知
1.1 它是一颗什么精度的ADC,时钟从哪来
GD32内置的ADC是逐次逼近型(SAR)结构,常见精度为12位,也就是说转换结果用12个bit来表示输入电压相对于参考电压的比例。内部包含多个模拟输入通道,实际数量与封装和芯片型号强相关,比如部分型号支持10个左右外部通道,加上内部温度和参考电压通道,具体以数据手册引脚定义为准。我第一次用的时候没查引脚复用表,把ADC引脚配成了普通GPIO复用模式,结果读出来永远是零,排查了半天才发现方向错了。
ADC的时钟源来自APB2总线时钟,一般不能直接给到满频,需要经过分频后再作为ADC工作时钟,常见配置为PCLK2的2/4/6/8分频。GD32手册通常会给出ADC时钟上限,不同系列差异很大,有的允许到14MHz,有的系列可以更高,务必以你手上的型号数据手册为准。如果ADC时钟超限,转换结果线性度会明显变差,看起来就是采样值乱跳且毫无规律,这时候先别怀疑传感器,回头查分频配置。
时钟分频不只是为了“不超过上限”,还会直接决定单个转换花费的时间。ADC完成一次转换需要的ADC时钟周期数由采样时间加固定转换周期组成,而采样时间由寄存器配置决定。所以选分频系数时,要考虑最终能得到的采样率能不能满足你的控制周期或信号频带需求。
1.2 一次采样到底经历了什么
逐次逼近型ADC的转换过程可以拆成三个阶段。首先是采样阶段,内部采样开关闭合,采样电容被充电到输入电压,这段时间由ADC_SAMPLETIME_x配置,一般有几档可选,越长的采样时间越有助于电容充分跟踪高阻抗信号源的电压。接着是转换阶段,SAR逻辑逐位比较,经过固定的转换周期后得到数字结果。最后是数据的锁存和标志位置位,应用程序可以读取数据寄存器。
对很多人来说,采样时间只是“随便选一个能用的”,但这里有个关键逻辑:如果信号源内阻很高,比如某个分压电阻网络阻值达到10千欧以上,而采样时间太短,采样电容还没充满就开始比较,结果会偏低且不稳定。我在一次模拟量采集项目里遇到过类似现象,前级传感器输出阻抗高,用最短采样时间读出的值比实际电压低了一截,把采样时间调长一档后立即恢复正常。所以看到数据偏小且有规律,优先检查采样时间配置。
采样时间直接决定了ADC的等效输入阻抗,可以近似把采样电容、开关电阻和输入源串在一起理解。外部源阻抗越大的场合,越需要更长的采样时间。实际项目中,如果传感器输出经过了运放跟随器,输出阻抗很低,那么选短采样时间也没问题。
1.3 不同GD32系列之间有哪些差异
GD32家族里的芯片很多,ADC外设寄存器设计大体沿着Cortex-M MCU的惯例,但不同系列之间存在不少细微差异。比如有的系列ADC校准寄存器名称不同,有的库函数里写adc_calibration_enable,有的则是adc_calibrate_start,如果你的代码是用库函数封装的,跨系列移植时编译报错是常有的事。另外部分新系列增加了更高精度的选项或差分输入通道,这部分能力在旧系列上完全不存在,学习时不要拿着一个系列的寄存器硬套另一个系列。
DMA与ADC的联动也在不同系列上有区别,有些系列需要单独开启DMA请求使能位,有些则在ADC使能后自动请求。还有多通道DMA的数据宽度、当前通道寄存器名称都有差异。建议拿到新板子后,先跑一个最简单的单通道轮询例程,确认采样值和寄存器都能对上,再做多通道和DMA扩展。直接上手抄老代码,往往是浪费时间最久的方式。
另外一个非常实际的差异是参考电压范围。绝大多数GD32芯片的ADC参考电压直接来自VREF引脚或VDDA,如果开发板把VREF接到了3.3V,那么ADC范围就是0到3.3V。少数芯片内部有参考电压选择位,可以在内部参考和外部VREF之间切换,使用前务必备注当前板子的连接方式。这块弄错的话,所有电压换算结果都会成比例漂移。
2. 配置方式选哪种:寄存器、标准库还是HAL风格
2.1 我的建议顺序
我自己的习惯是先用标准库或HAL风格的封装把功能跑通,再去读寄存器确认重要配置的最终值。学习阶段不建议一上来就纯寄存器操作,因为ADC涉及使能顺序、校准和等待标志,代码流程比寄存器名更难记。先用封装好的函数把流程建立起来,调试时再通过调试器查看ADC控制寄存器的实际值,同时可以加深对位域意义的理解。
库函数版本和芯片系列要匹配。工程里用到了adc_channel_length_config这类“通道长度”配置函数,说明这套库函数把常规通道和注入通道区分开了,配置常规通道数量时也要同时考虑注入通道数量是否被清零。有些开发者在这里漏调用,导致常规通道转换触发后不产生正确的结果。个人经验是,每配置一次采样通道组,就把流程拆成“清长度、配通道、配顺序”三步,简单且不容易错。
如果你偏好HAL风格,也完全可以,关键是先确认你使用的代码生成器或库版本包含该系列支持。GD32没有统一标准库,版本差异比想象中明显,不同版本函数名可能完全不一致。拷贝代码前先看头文件里有没有对应原型,这是最基础的检查手段。
2.2 常用库函数的一个最小配置骨架
以典型的标准外设库风格代码为例,单通道轮询方式可以这样写:
rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_ADC0); gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_OSPEED_50MHZ, GPIO_PIN_1); adc_clock_config(ADC0, ADC_CLK_DIV6); adc_channel_length_config(ADC0, ADC_ROUTINE_CHANNEL, 1); adc_routine_channel_config(ADC0, 0, ADC_CHANNEL_1, ADC_SAMPLETIME_55POINT5); adc_calibration_enable(ADC0); adc_enable(ADC0); adc_software_trigger_enable(ADC0, ADC_ROUTINE_CHANNEL); while (!adc_flag_get(ADC0, ADC_FLAG_END)); uint16_t raw = adc_regular_data_read(ADC0);这段代码里我需要特别解释几点。ADC_SAMPLETIME_55POINT5是55.5个ADC时钟周期的采样时间,为什么选它而不是最短值?因为我的信号源是电阻分压网络,输出阻抗不低,长一点采样时间能确保结果稳定。如果你接的是低阻抗链路,可以换成更短时间提高采样率。ADC_CLK_DIV6则取决于APB2频率和芯片ADC时钟上限,假设APB2为72MHz,6分频后ADC时钟12MHz,在多数允许的范围内。如果芯片允许14MHz,8分频只有9MHz,6分频能获得更快转换速度。
那句adc_calibration_enable很多人会漏。GD32的ADC校准模块在上电后会修正常规通道的偏移误差,不做校准也能转换,但零输入时的输出可能不是0,而是几十到上百个LSB的偏移。项目里对精度有要求时,校准基本上必做。注意校准必须在ADC使能之前调用,顺序反了的话,库函数内部会卡在等待标志的地方。
2.3 配置顺序为什么不能乱
ADC外设对配置顺序很敏感,尤其是“校准必须在使能之前”“使能之后才能触发转换”“触发之后等待结束标志”。如果把校准放在adc_enable之后,有些库函数会出现不等待或死锁,最终读到的数据始终是老值。我见过一个现象:程序跑起来只有一个固定采样结果,无论输入电压怎么变结果都不动,就是因为触发配置写在了使能后面,软件触发根本没有真正生效。
正确顺序可以概括为:先开时钟,再配GPIO模拟模式,再配ADC分频与通道,做校准,使能ADC,最后触发并等待。这个顺序背后的原因是:校准模块需要在ADC上电稳定后且未开始转换前介入,而触发转换需要ADC已经完全进入工作状态,提前触发只会把转换请求丢失。遵循这个固定顺序,基本可以避免初学者最容易踩的一批坑。
还有一个容易忽略的细节:常规通道配置函数里,第二个参数是“通道在序列中的位置”,第三个参数是“物理通道号”,最后一个是采样时间。不熟悉的人可能把位置和物理通道搞反,导致触发转换后读到的仍然是默认通道的数据。我建议每次配置完都用调试器看一眼当前序列寄存器,确保你要采集的通道确实被放在第0个位置上。
3. 单通道到多通道DMA的完整配置流程
3.1 单通道轮询是理解ADC机制的试金石
轮询模式适合采样频率不高、时间不敏感的场景。配置完成后,每次需要采集时发起软件触发,然后死等ADC_FLAG_END,等待时间内的CPU只能空转。以12MHz ADC时钟、55.5周期采样加固定转换周期估算,一次转换大约要几十个ADC周期,对应时间大概是几微秒。这个量级在慢速传感器采集、按键电压检测等场合完全够用。
但这并不意味着轮询代码可以随便写。等待结束标志之前,最好先清一次结束标志,否则上一次转换遗留的标志可能导致你读到上一个时刻的数据。常见的正确写法是:触发前清标志,触发后轮询标志,标志置位后读取数据并再次清标志。这样即使后续要在中断里读取,也不会因为标志残留而拿到陈旧结果。
如果你发现轮询读取的数据一直是第一次的值,或者偶尔漏采,多半是标志没有清干净。另外,读取数据寄存器本身有“读后清”的效果,但依赖这个特性不够稳妥,显式清标志才是可移植性更好的方式。我在实际项目里统一封装了一个函数,触发、清标志、等待、读取、再清标志,所有ADC读取都走同一个入口,排查问题时方便很多。
3.2 多通道扫描加DMA,让CPU从等待里解放
当需要采集多个通道时,轮询模式会变得很难受,因为你要不断查询“序列是否转换完成”,CPU占用率上升,且容易漏掉高速信号的变化。更常见的方案是使用扫描模式配合DMA,把所有通道的转换结果自动搬运到内存数组中,CPU只需要在处理周期内读取数组。
以两个通道PA1、PA2为例,典型配置如下:
#define ADC_CH_NUM 2 uint16_t adc_value[ADC_CH_NUM]; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH0); dma_parameter_struct dma_param = {0}; dma_param.periph_addr = (uint32_t)&ADC_RDATA(ADC0); dma_param.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_param.memory_addr = (uint32_t)adc_value; dma_param.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_param.periph_width = DMA_PERIPH_WIDTH_16BIT; dma_param.memory_width = DMA_MEMORY_WIDTH_16BIT; dma_param.direction = DMA_PERIPHERAL_TO_MEMORY; dma_param.number = ADC_CH_NUM; dma_param.priority = DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH0, &dma_param); dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0);然后把ADC配置成扫描模式,并使能DMA请求:
adc_channel_length_config(ADC0, ADC_ROUTINE_CHANNEL, ADC_CH_NUM); adc_routine_channel_config(ADC0, 0, ADC_CHANNEL_1, ADC_SAMPLETIME_55POINT5); adc_routine_channel_config(ADC0, 1, ADC_CHANNEL_2, ADC_SAMPLETIME_55POINT5); adc_scan_mode_enable(ADC0); adc_dma_mode_enable(ADC0); adc_calibration_enable(ADC0); adc_enable(ADC0); adc_software_trigger_enable(ADC0, ADC_ROUTINE_CHANNEL);这段代码里最值得注意的一点是DMA地址必须是ADC_RDATA(ADC0),也就是ADC常规数据寄存器地址。这个寄存器是16位宽,但只有低12位有效,所以DMA用16位宽度传输刚好。如果DMA配成8位宽度,连续搬运两个通道时会得到两个拆开的字节,还原麻烦还容易出错。如果配成32位宽度,虽然读取没问题,但内存数组要求4字节对齐,并且数据排列不如16位直观。
dma_circulation_enable在这里很关键。因为扫描模式下一次转换序列会依次产生多个数据,完成一轮后DMA传输数量归零。如果不开启循环模式,第一轮结束后DMA就停止搬运,你需要重新初始化才能继续采集。开启循环模式后,DMA会持续把后续结果写入同一数组。这样在循环模式里,数组里总是最近一轮的多通道数据。
3.3 采样时间、转换周期与最大采样率的估算
很多人配置ADC时只看“能用”,不关心到底能采多快,直到项目需要控制环路或音频采样时才傻眼。这里给一个估算方法:一次完整转换总周期数 = 采样时间周期数 + 固定转换周期数。固定转换周期通常为12到15个ADC时钟周期,具体看芯片。采样时间由寄存器选择档位决定,比如55.5周期。那么一次转换大约70个ADC时钟周期,ADC时钟12MHz时,单次转换时间约5.8微秒,对应采样率约170kSPS。
这只是单通道极限值,多通道扫描时所有通道共享这个速率,通道越多单通道等效采样率越低。比如4个通道扫描,每个通道的分摊时间就是总转换时间乘以通道数。如果你的应用要求每通道1kSPS以上,4通道扫描模式下总采样率要求4kSPS,这个需求大多数GD32都能轻松满足。但如果要求每通道100kSPS以上,就得认真算清楚分频和采样时间,否则数据会失真。
我建议在配置表里把APB2频率、ADC分频、采样时间、通道数和理论采样率写清楚,方便后续调整。实际项目中我遇到过一次采样率不够导致波形严重畸变的情况,问题不在ADC本身,而是选了过长的采样时间和过低的分频,把理论采样率压到了远低于需求的水平。重新计算后把采样时间调短一档,波形立刻正常。
4. 读取转换结果是起点,数据换算才是真正踩坑区
4.1 左右对齐与12位数据的真实含义
转换结果是12位的,而数据寄存器是16位的,这就有对齐方式的问题。右对齐模式下,12位有效数据落在寄存器的低12位,高4位为0,直接读出的uint16_t就是0到4095的原始值。左对齐模式下,12位数据被放在高12位,低4位为0,适合某些需要直接作为16位有符号数或DAC输入的场景。一般情况下我们都用右对齐,因为在换算电压时右对齐更直观,左对齐反而要移位后才能用。
如果库函数配置里有对齐模式的选项,请先确认默认值。很多库默认右对齐,但个别封装示例可能是左对齐,导致电压换算结果总是变成原先的16倍。这种错误非常隐蔽,因为数据看起来有规律,只是整体偏大。判断方法很简单:输入一个已知电压,比如1.65V,右对齐合理值应该是2048左右,如果读出来是32768附近,说明对齐方式搞错了。
另外要注意的是读取数据寄存器的时机。多通道扫描模式下,DMA会逐个把数据搬到内存,此时数据寄存器里已经是最后一个通道的值。如果你在DMA搬运过程中手动读数据寄存器,结果可能停留在中间状态。所以一旦启用了DMA,就不要再手动读取数据寄存器,而是读取DMA搬运到内存的数组,否则数据时序会乱。
4.2 电压换算公式里的参考电压陷阱
从原始码值换算成电压,最简单的公式是:电压 = 原始值 × 参考电压 / 4096。这里除以4096而不是4095,是因为满量程码值对应的是接近参考电压的模拟输入,细分度就是4096份。不少资料写4095也能用,误差在0.02%级别,但做精密采集时建议统一用4096。
真正的坑是参考电压本身不一定是3.3V。很多开发板用稳压芯片输出3.3V,实际可能只有3.28V甚至3.31V。如果直接用3.3V参与计算,所有通道的电压都会系统性偏高或偏低。解决方法是实测一下板上的VREF引脚电压,再把实测值代入公式。如果硬件上无法方便测量,可以采集内部参考电压通道,用已知的内部参考值反向推算实际VREF,这是高端MCU常见的自校准思路。
电源噪声也会直接进入ADC参考路径,导致换算后的电压值出现抖动。因此对精度要求高的场合,模拟电源引脚和参考引脚需要加滤波电容,尽量减少数字开关噪声耦合。这部分工作虽然属于硬件设计,但嵌入式工程师如果不理解,就会在软件里花大量时间滤波,效果还不尽如人意。软件滤波是必要的,但硬件参考设计更值得优先做好。
4.3 片内校准和温度传感器通道的使用细节
GD32很多型号内置温度传感器,可以用于粗略的片上温度监测。使用方式一般是把温度传感器通道加入常规通道序列,然后按固定公式计算温度。不同型号的公式斜率、偏移都不同,必须要查数据手册,不能拿着另一个系列的公式硬套。温度传感器通道的采样值通常变化非常缓慢,采样时间可以选长一些,以降低噪声影响。
片内校准的细节前面提过,这里再补充一点:校准操作本身需要等待标志位,不同系列库函数返回方式不同。有的adc_calibration_enable是阻塞式,内部一直在等待校准结束;有的则只是发起校准,需要你额外等待。用的库不同,代码习惯也不同。建议在使用前翻一下库函数源码,明确它是不是阻塞实现,避免校准还没完成就开始ADC转换,导致校准值无效。
校准完成后,如果要切换参考电压或改变ADC时钟,部分系列要求重新校准。特别是从低功耗唤醒后,ADC模拟部分可能经历了电源状态变化,不重新校准直接读取的偏移会变大。低功耗应用里,唤醒后重新初始化ADC并校准是一个稳妥且必要的流程。我见过项目因为省了这一步,休眠唤醒后采集值偏差在几十个码值以上。
5. 实际调试中最容易踩的五个坑
5.1 采样值飘、跳变严重,先从参考和采样时间入手
跳变是ADC调试图中最高频的问题。原因嘛,无外乎信号源内阻高、采样时间不足、参考电压不稳、地线噪声以及GPIO配置错误。我调试时先做三件事:第一是确认GPIO确实处于模拟输入模式,第二是确认采样时间不是最短档,第三是把参考电压引脚上的纹波用示波器看一遍。这三项都正常后,跳变才会考虑软件滤波。
如果输入引脚悬空或者接了高阻分压网络,ADC采到的就是浮空电压,读数肯定乱跳。有些开发者抱怨“ADC采样值不稳”,结果发现输入引脚根本没接传感器,纯属悬空状态。这种问题是硬件连接层面的,不是软件能彻底解决的。我的经验是先接一个稳定的直流电压源,比如电池或稳压模块输出,确认ADC本身稳定后,再接真实传感器,这样能把问题范围缩小一半。
还有一点容易被忽略:电源和地平面噪声。ADC对小信号敏感,数字外设频繁翻转时,地弹会通过模拟输入路径进入ADC。如果条件允许,把ADC通道布局在远离高速数字信号线的地方,并在电源入口加磁珠和电容隔离。软件上可以开启硬件均值或过采样功能,但前提是硬件噪声没有大到完全淹没信号。
5.2 多通道数据错位,先查DMA传输数量和通道顺序
多通道扫描配合DMA时,数组里数据位置与通道顺序不一致是常见故障。我遇到过一种现象:把PA1配置成通道0,PA2配置成通道1,结果数组里第一个元素总是PA2的电压。排查后发现是寄存器里通道位置写反了,因为扫描序列按“位置号”从0开始转换,DMA按完成顺序写入数组。如果配置里把物理通道号和位置搞混,数据自然错位。
另一种常见错位原因是DMA传输数量与通道数不匹配。假设你要采集3个通道,DMA的传输数量配置成2,那么第三个通道数据永远不会进入数组,第二个通道数据也可能被第三个通道的结果覆盖。这类问题往往不会立刻报错,而是表现为数据偶尔缺失。建议把通道数和DMA传输数量都用宏定义写好,修改通道时同步修改,省得排查时一头雾水。
在带缓存区的工程里,DMA循环写入同一数组会持续覆盖。如果应用在读数组的同时DMA也写入数组,就会读到半旧半新的一组数据。我采取的方案是引入“双缓冲”,DMA使用两个数组交替搬运,读方拿到完整一组后再处理。虽然增加了内存开销,但数据一致性明显改善。GD32的DMA本身不一定支持硬件双缓冲,但可以用两个普通数组和中断切换来实现。
5.3 DMA和中断同时使用时的优先级问题
ADC转换结束可以触发中断,DMA传输完成也能触发中断。一旦两者同时使能,中断优先级配置不对,可能导致DMA传输完成标志被高优先级的ADC中断淹没,应用无法及时更新数据。我的建议是:只要用了DMA搬运多通道数据,就不必再开ADC转换完成中断。DMA传输完成中断已经足够说明这一轮数据全部就位,开两次中断纯属增加复杂度。
如果确实需要在单通道快速转换时用ADC结束中断,那就在中断里直接读取数据寄存器,不要在这个中断里访问DMA数组,避免时序混乱。不同系列的中断标志位名称也有差异,ADC_FLAG_END和ADC_INT_FLAG_END可能是两个不一样的标志,前者是状态标志,后者是中断标志,读取和清除方式都不一样,使用前对照库函数头文件查清楚。
中断服务程序里还有一个小细节:ADC结束中断标志必须在刚进入中断时就清掉,否则连续触发下会一直进中断,看起来像程序卡死在中断里。清除标志操作本身有延迟要求,等标志位真正确认清除后再退出中断比较稳妥。我在一个项目里就因为清标志位置靠后,导致中断频繁误触发,CPU占用率直线飙升。
5.4 校准标志卡死,大多是库版本和调用位置问题
库函数adc_calibration_enable内部如果采用等待方式,一旦校准硬件没正常启动,程序就会在调用处死等。常见原因是调用前ADC已经处于使能状态,或者APB2时钟没开对。第一次遇到这问题时,我先检查rcu_periph_clock_enable(RCU_ADC0)是否执行,再确认校准调用前没有其他代码把ADC使能了。排查顺序对了,问题基本就解决了。
不同库版本的校准函数名和等待方式也有差异。有的库提供的校准函数不会等待完成,而是启动后立刻返回,这时候你需要在后续操作前再加一个while(!adc_flag_get(...))等待校准结束标志。如果不查源码,直接照抄老工程的调用方式,很可能会在新库上出现“校准未完成就开始转换”的潜在问题。建议每次换库版本后,都花十分钟把用到的那几个函数实现源码快速扫一遍。
还有一个隐蔽问题:某些系列在低功耗模式唤醒后,校准标志可能不会被正确置位。这时候即使调用校准函数,也可能直接跳过等待,导致后续转换没有真正校准。处理办法是在唤醒后先把ADC模块做一次完整复位或失能再使能,再执行校准。虽然麻烦,但比数据偏差来得省心。
5.5 采样率不够时,波形看起来像“台阶”而不是正弦波
如果连续采集正弦波信号,发现波形呈明显阶梯状,说明采样率跟不上信号频率,每个周期只采到几个点。这种问题不是ADC坏了,而是配置层面的采样率不够。我一般先计算理论采样率,再和信号频率做对比,至少保证每个周期20个采样点以上,才能看出大致波形形状。实际测试中,记录几个特征频率下的采样点数,就能快速判断瓶颈在哪里。
要提高采样率,主要途径是提高ADC时钟分频、缩短采样时间、关闭扫描模式中不需要的通道。有些系列还提供快速采样模式,能进一步减少固定转换周期数。但要注意,缩短采样时间是有代价的,输入阻抗稍高时误差会增大。我的原则是:先用保守配置确认硬件没问题,再逐步缩短采样时间,观察数据稳定性,找到一个既能满足采样率、又不明显恶化精度的平衡点。
6. 从实际项目里沉淀下来的降噪与过采样经验
6.1 软件滤波别只会取平均值
很多初学者处理ADC抖动就是连续读几十次取平均,这在低频缓变信号面前有用,但对高频噪声基本无效,还会拖慢响应速度。我实际项目中用过的方案是:滑动平均滤波(适合周期性采样),中值滤波(适合脉冲噪声),以及一阶低通滤波(适合控制系统实时性要求高的场景)。一阶低通形如y = y_prev + alpha * (x - y_prev),alpha越小滤波越强,但响应越慢,需要根据信号带宽来定。
滤波参数不能随便拍脑袋。如果信号本身变化比较快,把alpha设得太小,真实信号也会被拉平,表现为系统响应迟钝。我建议先用串口或调试器把原始波形导出来,观察噪声频率和幅度,再决定用哪种滤波以及滤波深度。用数据说话,远比“多采几次平均一下”更可靠。
如果ADC带有硬件均值模式或多次采样功能,优先使用硬件而不是软件多次采样。硬件均值不会占用CPU,而且内部采样节奏更稳定,效果通常优于同等次数的软件平均。但是要注意,硬件均值相当于降低了有效采样率,因为一次结果由多次采样合成,换算控制周期时要算清楚。
6.2 过采样加求均值,如何高效提升有效分辨率
过采样技术可以用来提升有效分辨率:每多采4倍样本,按理论可以多获得1位有效分辨率,但前提是噪声满足白噪声特性。实际操作中,把同一通道连续采集N次后求和并右移,等效于平均+增益,12位ADC理论上可以通过过采样得到更高的“有效位数”。不过这个提升上限受硬件噪声水平限制,不是无脑多采就能无限提高,一般做到14到15位有效已经不错。
我习惯的做法是:对同一通道连续采集16次或32次,累加后右移位数,得到一组降噪结果。关键是在累加时使用uint32_t变量,防止16位数据累加多次后溢出。右移位数根据采集次数决定,比如采4次右移2位,采16次右移4位。这样的过采样本质上就是平均,但你不必手动维护一堆数组,累加器写法更省内存。
过采样最大的副作用是终端的有效采样率降低。假设ADC实际采样率100kSPS,过采样16次后有效输出只有6.25kSPS,如果你的控制环路需要10kSPS更新率,就满足不了。所以过采样更适合低频高精度测量,不适合高速控制。我在一个温度采集项目里对低频信号做过采样,效果非常明显,原先抖动几个码值,过采样后稳定在一两个码值以内。
6.3 测量低电压小信号时的硬件配合
软件再强,硬件前端太差也白搭。对于0到几十毫伏级别的小信号,GD32的ADC输入范围通常无法直接覆盖,需要先经过放大电路把信号调理到0到3.3V区间。这时候运放的供电、偏置电压和PCB走线都有讲究:反馈电阻要选低温漂的,走线尽量短,模拟地要单点连接主地,避免数字地电流干扰。
在软件侧,可以配合查表法或线性校准来补偿放大电路的增益误差和零点偏移。标准做法是先输入两个已知电压,测量对应的ADC码值,用两点求斜率得到实际换算关系。把这组参数固化在设备校准流程里,生产时每台设备单独校准一次,能显著提高批量一致性。ADC本身的线性度在GD32这类MCU里通常足够好,真正的误差大头往往来自外部调理电路。
还有一个很实用的小技巧:如果输入信号在ADC量程的很小一段,比如0到0.1V,可以直接把参考电压选成1.2V或2.5V档(如果芯片支持),等效于放大了输入信号的分辨率。这样不需要外部运放,也能获得更精细的量化结果。但要注意,参考电压降低后,参考路径上噪声对结果的影响也会变大。
6.4 一个可复用的ADC配置检查清单
项目做到后期,调ADC时我基本不看资料,只对着自己整理的检查清单逐项过一遍。这个清单包括:GPIO是否配置为模拟输入、ADC时钟是否超限、采样时间是否和信号源阻抗匹配、校准是否在使能之前完成、使能后是否触发了转换、等待的标志位是否清干净、DMA传输数量是否等于通道数、DMA宽度是否为16位、数据数组是否被其他任务意外改写、参考电压实测值是否参与换算。
这套检查顺序看起来简单,实际排查时价值很高。遇到过几次同事来求助,都是因为“GPIO忘了配成模拟模式”或者“校准放在使能之后”这种低级问题。只要按顺序逐项确认,很多谜之现象会立刻显出原形。你也可以照着这个逻辑做一份自己的检查清单,后续维护多通道采集代码时能省下不少时间。
如果需要更高精度应用,还可以考虑环境中电磁干扰的影响,比如电机驱动、继电器开关、电源适配器都会对ADC采样造成不小的干扰。这时候采样时机要尽量避开发射源的切换时刻,或在硬件上加入RC低通滤波器。RC滤波器的截止频率要选得比信号最高频率高,同时要远低于ADC采样率,否则信号会被削掉。软件滤波永远替代不了合理的硬件滤波,两者是配合关系,不是二选一。
最后再分享一点个人体会:GD32的ADC外设本身并不神秘,学习它最有效的方式就是拿一块板子,把单通道、多通道DMA、校准和换算全部亲手写一遍,踩过几个坑之后就会形成肌肉记忆。我后来移植到其他芯片平台,也把这套“先算采样率、再配硬件、最后做校准和滤波”的思路带过去了,基本没再因为ADC配置问题翻过车。希望这篇笔记也能帮你少走一点弯路。