项目做到中后期,我接手了一块 GD32C231 的板子,负责把几个模拟通道的采样值读上来。代码写得很常规,初始化 ADC,配好通道和采样时间,然后在主循环里轮询 EOC 标志,置位了就立刻读 ADC_RDATA。刚上电跑起来的时候数据看着还行,虽然偶尔跳几个码,但我想着 12 位 ADC 有点噪声也正常,没太当回事。
问题是在我调整了 ADC 时钟分频之后开始变得明显的。当时为了把 ADC 时钟压到手册推荐的范围以内,我把分频系数改大了一档。改完重新烧录,采样值开始出现成片的错误,而且不是那种小幅度的抖动,是直接跳到完全对不上的数值上。有的通道读出来接近满量程,有的通道读出来接近零,跟实际输入的电压没有任何对应关系。我第一反应是硬件问题,拿万用表量了输入端,电压稳稳的,又换了通道,错误依旧。把分频改回去,数据立刻恢复正常。这个现象太规律了,规律到不像是噪声,更像是读到的根本不是当前这次转换的结果。
我开始翻 GD32C231 的芯片使用限制文档。在这类文档里,ADC 相关的条目通常排得很靠前,这次也不例外。文档里写得很直接,标题就是“当 ADC 时钟相较于 PCLK 时钟慢很多时,会出现 ADC 数据采集错误”。往下读具体描述,问题出在 EOC 置位后的读取时机上。如果 ADC 时钟比 PCLK 慢得多,在 EOC 标志刚刚置位的那一刻立刻去读 ADC_RDATA 寄存器,读到的数据是不可靠的。官方给出的解决办法只有一句话:在 EOC 标志置位后,延迟 2 个 ADC 时钟,再读取数据寄存器。
我回头看我原来的代码逻辑,问题一目了然。轮询 while(adc_flag_get(ADC0, ADC_FLAG_EOC) == RESET) 这个循环,一旦标志置位,下一行就是 adc_regular_data_read(ADC0)。从标志置位到读取寄存器之间,只有几个 CPU 指令的间隔,在 48MHz 的主频下可能连一个 ADC 时钟周期都不到。分频小的时候,ADC 时钟快,这中间的时间差相对 ADC 的节奏来说勉强够用,或者至少错得没那么离谱。分频一大,ADC 时钟变慢,EOC 置位后 ADC 内部把转换结果真正搬进 ADC_RDATA 寄存器所需的时间,相对于 CPU 的读取动作来说就变得太长了。我读到的其实是上一个通道残留的旧值,或者是一个还没稳定下来的中间状态。
修复本身很简单,但“等 2 个 ADC 时钟”这个要求不能随便用个 __NOP() 糊弄过去。ADC 时钟频率取决于 APB 分频和 ADC 预分频的配置,不同工程里这个值不一样。我写了一个小函数,根据当前的 ADC 时钟频率算出一个大概的微秒级延时,用 DWT 周期计数器来做,比空循环靠谱。在读取 ADC_RDATA 之前调用一下,问题就消失了。数据重新变得稳定,跟输入端电压线性对应。
这件事给我的教训是,芯片使用限制文档不是摆设。ADC 的 EOC 标志在很多 MCU 上的语义是“转换完成,数据可读”,但 GD32C231 在特定的时钟配置下,这个语义打了折扣。EOC 置位只代表转换序列结束,数据从内部结果寄存器到 ADC_RDATA 这个可被 CPU 访问的寄存器之间,还有一段硬件同步时间。时钟越慢,这段同步时间在 CPU 视角下就越长。如果只按常规的“轮询标志然后读数据”的写法,在分频偏大的场景下就会踩坑。现在我的 ADC 读取函数里固定留了这段延时,虽然损失了一点吞吐,但换来了数据的确定性。对于一块要连续跑几个月的采集板来说,这个代价是值得的。 |
|