ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pico ADC底层全解析:RP2040寄存器、DMA与精度调优

Pico ADC底层全解析:RP2040寄存器、DMA与精度调优 我前前后后摸树莓派 Pico 的 ADC 也有一段时间了从最初照着 MicroPython 的machine.ADC直接读值到后来为了做数据采集项目开始啃 RP2040 数据手册才发现底下的门道比想象中多。如果你只是把 ADC 当成一个“读取电压的传感器”来用那确实写几行代码就够了但一旦你关心采样率、精度、多通道切换、DMA 传输、或者在高噪声环境下的稳定性就必须沉到寄存器层面去理解它。这篇文章就站在 RP2040 底层视角把 Pico ADC 的硬件结构、工作模式、关键寄存器、应用避坑一次讲透适合所有想精进嵌入式采集能力的开发者。1. RP2040 的 ADC 硬件架构它不是“一个”ADC而是一套模拟前端系统很多人第一次看 Pico 的引脚图会看到 ADC0、ADC1、ADC2 这些名字想当然地以为这是三个独立的 ADC 外设。实际上 RP2040 内部只有一个 12 位逐次逼近型 ADCSAR ADC外部暴露的 3 个 GPIO 引脚加上第 4 个引脚GPIO26~GPIO29共享这一个转换器。这一点非常关键因为它决定了你使用时必须遵循“分时复用”的规则不能像看待多路独立转换器那样同时读取多个通道。1.1 核心架构与引脚分布RP2040 的 ADC 外设有 5 个输入源其中 4 个可以对外使用输入引脚对应的 GPIO内部连接用途ADC0GPIO26模拟输入通道 0外部电压采样ADC1GPIO27模拟输入通道 1外部电压采样ADC2GPIO28模拟输入通道 2外部电压采样ADC3GPIO29模拟输入通道 3外部电压采样注意此引脚默认接在板载温度传感器上内部通道无温度传感器输出片内温度采集内部通道无带隙参考电压固定电压自校准参考有些资料会告诉你 GPIO29 是“第四个 ADC 引脚”但从 Pico 官方板子的原理图来看GPIO29 往往会被用于连接板载温度传感器或者被用户复用为其他功能。如果你用 ADC3 做外部电压采样要先确认你的板子型号和电路不要把外部信号和温度传感器输出混在一起。在内部架构上RP2040 的 ADC 前端分为三段输入多路选择器MUX、采样保持电路S/H、逐次逼近比较器SAR logic。输入多路选择器负责从 5 个输入源中选出一路采样保持电路负责把该路的电压“定格”在比较器的输入端SAR logic 则通过二分查找的方式逐位逼近出 12 位数字码。整个转换过程是“一次性”的也就是说你在某一时刻只能得到某一路的转换结果切换通道后需要重新采样转换。1.2 转换器原理与采样保持机制逐次逼近型 ADC 的通俗理解就像猜体重先从最大砝码开始试如果偏重就减半如果偏轻就加半逐步逼近真实值。RP2040 内部用了一个电容阵列charge redistribution DAC而不是电阻分压网络这种结构的好处是精度高、功耗低但代价是对输入信号的驱动能力比较敏感。采样保持电路在这个过程里的作用是保证在 SAR 比较的整个周期内进入比较器的电压保持恒定。采样保持分两个阶段采样阶段Sampling phase开关闭合外部信号对内部电容阵列充电这段时间内 ADC 输入引脚呈现的是电容负载需要外部信号源有足够的驱动能力。保持阶段Hold phase开关断开电容上存储的电荷维持电压SAR 逻辑开始逐位比较。所以当你直接用一个高阻抗的分压电阻网络接在 ADC 输入引脚上时采样阶段电容充电速度可能不够导致采到的电压偏低这也就是后面要讲的输入阻抗问题。RP2040 数据手册中给出的 ADC 输入阻抗不是一个固定值而是与采样周期有关简单记就是外部源阻抗越小越好分压电阻总阻值控制在 10kΩ 以下相对稳妥。提示RP2040 的 ADC 参考电压VREF固定为 3.3V也就是 ADC 满量程对应 3.3V12 位数值 4095。它没有外部参考电压引脚也没有内部可编程增益放大器PGA这一点和 STM32 系列不太一样。所以你在精度上能做文章的空间主要在校准和软件处理上。2. 工作模式拆解轮询、中断、自由运行、FIFO 与 DMARP2040 的 ADC 外设提供了几种工作模式但是不像 STM32 的 ADC 有那么多复杂的注入通道、规则通道配置。它的模式少胜在直接。用到后来你会发现真正麻烦的不是模式本身而是模式和中断、DMA、FIFO 之间的配合。2.1 最朴素的轮询模式轮询模式是最直观的用法启动一次转换然后不断检查转换完成标志位标志位置位后读取结果寄存器。整个过程是“阻塞式”的CPU 会停在等待循环里。在寄存器层面一次轮询转换可以拆成四步设置 ADC_CS 寄存器的 INPUTSEL 位段选择输入通道。将 ADC_CS 的 START 位置 1启动一次转换。反复读取 ADC_CS 的 READY 位直到它变为 1。读取 ADC_RESULT 寄存器取出低 12 位即为转换结果。用 C 语言操作寄存器可以这么写#include pico/stdlib.h #include hardware/adc.h uint16_t adc_read_single(uint8_t channel) { // 1. 选择通道 adc_select_input(channel); // 2. 启动单次转换 adc_run(false); adc_run(true); // 3. 等待转换完成READY 置 1 while (!(adc_hw-cs ADC_CS_READY_BITS)) { tight_loop_contents(); } // 4. 读取结果 return (uint16_t)adc_hw-result 0xFFF; }这段代码里的adc_hw是 RP2040 外设寄存器的映射指针ADC_CS_READY_BITS是状态位的掩码。实际开发中我更推荐直接用官方 SDK 的adc_read()函数因为它在启动转换前会做一次通道选择检查并对 READY 标志的处理做了一些优化但搞清楚底层寄存器的操作方式对调试问题很有帮助。轮询模式适合低频采样场景比如每秒采样几次环境温度、电池电压。它的缺点很明显CPU 忙等浪费算力采样间隔不精准取决于代码执行路径。如果你要做音频采样、振动监测这类连续高速采集轮询模式基本不可用。2.2 自由运行模式与定时采样连续将 START 位置 1 是低效的因为每次都要 CPU 参与。RP2040 的 ADC 支持把 START 位写成 1 时进入“自由运行模式free-running”也就是连续不断地进行采样转换。转换速率由时钟分频器决定不需要 CPU 介入。进入自由运行模式的寄存器操作是// 设置通道 adc_select_input(0); // 使能 ADC 并进入自由运行 adc_hw-cs ADC_CS_EN_BITS | ADC_CS_START_ONCE_BITS;需要说明的是START位在自由运行模式下并不是“写 1 就开始一直跑”准确理解是当 START 被写 1 且 ADC 正在运行时ADC 会持续触发转换当你对 START 写 0 时它会停止。Pico SDK 里的adc_run(true)和adc_run(false)就是封装了这个行为。自由运行模式下转换结果会不断更新到 ADC_RESULT 寄存器而 READY 位也会在每次转换完成后短暂置 1。如果你用中断监听 READY 位就可以做到“转换完成一次中断通知一次”CPU 在等待期间可以做其他事情。不过由于 RP2040 的 ADC 只有一个转换完成事件多个通道轮流采样时你要在中断服务函数里手动切换通道不然它会一直在同一个通道上转换。自由运行模式的采样精度受到时钟分频器影响。RP2040 数据手册建议 ADC 时钟频率不要超过 48MHz 的一半也就是 24MHz。Pico SDK 默认把 ADC 时钟设为 48MHz 的整数分频实际上 ADC 内部经过了一个时钟预分频器最终采样转换率可以达到 500ksps千次采样每秒。如果超过这个速率转换精度会明显下降。2.3 FIFO 与 DMA连续采样的黄金搭档自由运行模式虽然解放了 CPU但如果你不快速读取结果下一次转换会把上一次结果覆盖掉数据就丢了。解决方式是引入 FIFO 缓冲。RP2040 的 ADC 外设内置了一个 4 深度的 FIFO每次转换完成后结果自动压入 FIFO你可以用轮询或 DMA 方式把数据搬运出来。FIFO 的工作方式配置 ADC_FCS 寄存器的 EN 位使能 FIFO。配置 THRESH阈值位决定在哪一级触发 DMA 请求。如果开了 DREQ_EN则在 FIFO 达到约定深度时向 DMA 控制器发送请求。DMA 自动从 ADC_FIFO 寄存器读出数据搬移到内存数组。这里有一个容易踩的坑FIFO 中的结果位置是“右对齐的 12 位有效数据”但 FIFO 寄存器本身是 16 位宽的高 4 位默认是 0所以读取后直接用 0xFFF取低 12 位没有问题。真正需要注意的是 FIFO 溢出问题。当 FIFO 已满而你又没有及时读取新的结果不会覆盖旧数据而是会丢弃同时 ERR 位置 1。如果你发现波形有“丢点”先查一下 FCS 的 ERR 位是不是被置起来了。用 DMA 搬数据时我推荐把 FIFO 阈值设为 1也就是每来一个结果就触发一次 DMA 请求。这样 DMA 的传输节奏和 ADC 转换节奏完全一致不容易丢数据。如果你设成 4积满 4 个数据才触发一次 DMA 传输在采样率高的时候反而可能因为 DMA 响应延迟导致 FIFO 溢出。#include hardware/dma.h #include hardware/adc.h void adc_dma_init(uint16_t *buf, size_t len) { // ADC 初始化 adc_init(); adc_gpio_init(26); adc_select_input(0); // 使能 FIFO阈值设为 1 adc_fifo_setup( true, // 使能 FIFO true, // 每个样本触发一次 DREQ 1, // 阈值 1 false, // 不通过 ERR 状态位 false // 不移位处理 ); // 配置 DMA dma_channel dma_claim_unused_channel(true); dma_channel_config cfg dma_channel_get_default_config(dma_channel); channel_config_set_transfer_data_size(cfg, DMA_SIZE_16); channel_config_set_read_increment(cfg, false); // 固定读地址 ADC_FIFO channel_config_set_write_increment(cfg, true); // 写地址递增 channel_config_set_dreq(cfg, DREQ_ADC); // 由 ADC 请求触发 dma_channel_configure(dma_channel, cfg, buf, // 写地址 adc_hw-fifo, // 读地址 len, // 传输数量 true // 立即启动 ); // 启动 ADC 自由运行 adc_run(true); }这段代码的核心思路是让 ADC 连续转换转换结果通过 FIFO 驱动 DMADMA 直接写到内存数组。整个过程基本不需要 CPU 介入适合高频数据采集。Pico 官方示例中有类似的hello_adc_dma工程强烈建议跑一遍然后把 FIFO 阈值、采样率都改一改观察不同参数对数据完整性的影响。3. 寄存器逐位解读直接操作底层到底在操作什么如果你想做底层开发或者想排查一些 SDK 封装后不易理解的问题寄存器操作是躲不过去的。好在 RP2040 的 ADC 寄存器数量很少核心就 4 个比 STM32 的 ADC 寄存器族简洁太多。3.1 寄存器地址映射与指针操作RP2040 的数据手册里ADC 外设挂在 APB 总线上基地址是0x4004C000。在 C 语言里你可以通过一个结构体指针直接操作这些寄存器Pico SDK 里已经定义了对应的结构体typedef struct { io_rw_32 cs; // 0x00 控制和状态 io_rw_32 result; // 0x04 转换结果 io_rw_32 fcs; // 0x08 FIFO 控制和状态 io_rw_32 fifo; // 0x0C FIFO 数据 io_rw_32 div; // 0x10 时钟分频 } adc_hw_t; #define adc_hw ((adc_hw_t *)ADC_BASE)操作寄存器的本质是操作特定内存地址。比如adc_hw-cs 0x01;这条语句对应的机器码最终会变成“向地址 0x4004C000 处写入 0x01”。这也是为什么嵌入式里常说“指针和寄存器密切相关”——寄存器就是内存地址上的特定物理单元你用指针访问它后面是硬件逻辑在响应而不是普通的 RAM 读写。3.2 ADC_CS控制与状态寄存器ADC_CS 的各位定义如下位段名称类型说明[0]ENRWADC 使能位写 1 启动 ADC 内部模拟电路[1]STARTRW触发转换位写 1 启动单次转换或进入自由运行[2]TSRW选择内部温度传感器通道[3]ERR_STICKYR状态位发生过错误后保持置 1直到写 1 清除[4]ERRR当前错误标志位[5]READYR转换完成标志1 表示一次转换完成且结果有效[9:6]保留-保留[11:10]保留-保留[15:12]保留-保留[21:16]保留-保留[25:24]INPUTSELRW输入通道选择0ADC01ADC12ADC23ADC3[31:26]保留-保留注意INPUTSEL是 2 位宽对应 0~3但因为位段偏移是 24实际表达是(通道号 24)。操作时最好用掩码和位清除// 设置输入通道为 0 adc_hw-cs ~(0x3u ADC_CS_INPUTSEL_LSB); adc_hw-cs | (0u ADC_CS_INPUTSEL_LSB);ERR_STICKY是个很有意思的位。它不是我们常见的只读状态位而是“写 1 清除”的 sticky 位。你读取到 1 说明曾经发生过错误比如 FIFO 溢出、通道切换异常排查完问题后要手动写 1 把它清掉否则它会一直保持 1干扰后续判断。很多人在排查 FIFO 溢出问题时就栽在这程序判断if (adc_hw-fcs ADC_FCS_ERR_STICKY_BITS)一直进入错误分支但清除写成了adc_hw-fcs ~ADC_FCS_ERR_STICKY_BITS——这是错的正确写法是直接adc_hw-fcs ADC_FCS_ERR_STICKY_BITS;。3.3 ADC_RESULT、ADC_FCS 与 ADC_FIFOADC_RESULT 寄存器只有低 12 位有效读取时建议直接用掩码result 0xFFF。如果开了移位功能FCS 中的 SHIFT 位结果会左移若干位这时读取时就要看具体配置了。ADC_FCS 是 FIFO 的控制和状态寄存器各位位段名称说明[0]ENFIFO 使能[1]DREQ_EN达到阈值后触发 DMA 请求[2]DREQ_STALL若 DMA 请求未处理是否暂停 ADC 转换[3]SHIFTFIFO 结果左移位数0 或 1[4]ERRFIFO 错误标志溢出[5:8]LEVEL当前 FIFO 中的数据量0~4[9]FULLFIFO 满[10]EMPTYFIFO 空[11:18]THRESHDMA 触发阈值[31:19]保留保留DREQ_STALL位值得多说几句。如果把这一位置 1那么当 DMA 请求发出但 DMA 还没来读取时ADC 会暂停转换等数据被搬走后再继续。这相当于一个反压机制理论上可以避免 FIFO 溢出丢数据但代价是采样率不稳定采样时间间隔会出现抖动。如果你做的是频谱分析或者FFT这类对采样间隔一致性要求高的项目建议关掉这个位通过调大 FIFO 阈值来妥协如果只是做数据记录打开它也无妨。ADC_FIFO 寄存器是 16 位只读寄存器读一次就是弹出一个数据。它没有任何状态位状态都放在 FCS 里。读取 FIFO 时要注意即使 FIFO 是空的读取它也可能返回旧数据所以先判断LEVEL或EMPTY位再决定是否读取是稳妥的写法。uint16_t adc_read_fifo_safe(void) { // 等待 FIFO 非空 while (adc_hw-fcs ADC_FCS_EMPTY_BITS) { tight_loop_contents(); } return (uint16_t)(adc_hw-fifo 0xFFF); }ADC_DIV 寄存器是时钟分频器默认是 0即不做分频。当你把 DIV 设为 96 时ADC 等效于每 96 个时钟周期转换一个样本。这个寄存器在不同场景下的意义我会在下一节详细展开。4. 精度与稳定性为什么你读到的 ADC 值和万用表对不上这是所有做模拟采集的人都会遇到的问题同一节点示波器/万用表量出来是 1.650VPico 的 ADC 读出来却是 1.612V或者显示值在几十毫伏范围内跳动。问题不一定出在 ADC 本身很多时候出在参考电压、输入阻抗、采样保持和布线上。4.1 输入阻抗、参考电压与采样时间RP2040 的 ADC 是电容采样型前面提到过。它对外呈现的等效阻抗在采样阶段约在几 kΩ 到十几 kΩ 之间变化具体取决于采样时钟频率。如果你的信号源阻抗很大比如一个 100kΩ 分压电阻网络信号源和采样电容之间就构成了一个 RC 低通滤波器。在有限采样时间内电容来不及充到稳态电压采到的值必然偏低。解决这个问题有两条路降低源阻抗在信号源和 ADC 之间加一级运算放大器缓冲。运放输出阻抗极低能够快速驱动采样电容。增加采样时间将 ADC_DIV 设大让采样阶段持续更长时间。代价是整体采样率下降。参考电压的问题同样重要。RP2040 的 VREF 是由芯片内部连接到 VDD 引脚3.3V的。这意味着 3.3V 电源的质量直接决定了 ADC 的满量程精度。如果 3.3V 上有 50mV 的纹波那么 ADC 读数的绝对值就会引入接近 62 LSB 的偏差50mV / 3.3V × 4095 ≈ 62。所以 Pico 做高精度采集时3.3V 电源要尽量干净板载开关稳压器的噪声要靠外部 LDO 或者更复杂的电源滤波电路来压低。Pico SDK 里有一个非常有用的校准函数adc_calibrate()。它利用芯片内部的带隙参考电压来校准 ADC 的增益和偏移误差。很多人在做绝对电压测量时忽略了这一步导致 1% 左右的系统偏差。校准后精度可以提升到约 0.1% 级别。4.2 软件处理三板斧均值滤波、过采样、动态校准硬件层面能优化的空间有限实际项目中我更依赖软件手段。第一板斧是均值滤波。最简单的做法是连续采 N 次取平均有效抑制随机噪声。RP2040 的 ADC 白噪声在几 LSB 左右采 16 次取平均可以把噪声降到 1 LSB 以内。但要注意均值滤波不能消除电源纹波带来的周期性噪声因为每次采样误差不完全独立。第二板斧是过采样加抽取。如果 ADC 本身是 12 位但有效位数ENOB只有 10 位左右你可以通过 4 倍过采样 平均提升 1 位有效位数。原理是对随机噪声来说每增加 4 倍样本量平均有效分辨率提升 1 位。比如要 14 位有效分辨率需要 4 的 2 次方16 倍过采样。这个方法在采样率有余量的场景非常实用。第三板斧是软件校准。在系统启动时用高精度万用表测量已知电压比如 1.000V、2.000V记录 ADC 读数得到一组增益和偏移系数之后每次测量都套用线性校正。这个方法是把外部误差吸收到软件里比盲目相信出厂校准可靠得多。// 简单的两点校准 typedef struct { int32_t x1, y1; // 标准电压点1 (mV, ADC读数) int32_t x2, y2; // 标准电压点2 } adc_cal_t; int32_t adc_cal_apply(adc_cal_t *cal, int32_t adc_raw) { return cal-y1 (cal-y2 - cal-y1) * (adc_raw - cal-x1) / (cal-x2 - cal-x1); }除此之外还有两个容易忽略的细节值得提醒未使用的 ADC 输入引脚不要悬空最好接到 GND 或 3.3V否则它们会像天线一样引入噪声通过衬底耦合影响正在采样的通道。ADC 采样期间尽量避免同一时间切换 GPIO 数字输出尤其是在驱动 LED 或电机时。数字信号边沿的电流冲击会在地平面上产生毛刺直接砸进 ADC 参考电压。5. 实操避坑清单从初始化到数据搬移最常见的六个坑底层开发最怕的不是复杂而是“看起来正常但是结果不对”。下面这六个坑是我自己踩过的或者在社区帮别人排查过的整理出来当个速查表。5.1 坑一ADC 初始化顺序不对导致第一个采样值总是异常很多教程写 ADC 初始化是“三步走”adc_init()、adc_gpio_init()、adc_select_input(channel)。但实际上 GPIO 的模拟功能切换和 ADC 外设使能之间有一个顺序要求。如果先使能 ADC 再配置 GPIOPin 上的模拟通路还没完全建立第一次采样会偏小。更稳的写法是先adc_gpio_init()再adc_init()最后adc_select_input()。另外刚使能 ADC 时模拟电路需要一个上电稳定时间不要在初始化后立刻采样加一个几百微秒的延迟再开始转换。5.2 坑二切换通道后没有丢弃第一次转换结果RP2040 的 ADC 在切换输入通道后采样保持电路需要几个时钟周期来稳定。如果切换后立刻启动转换结果可能受残余电荷影响。处理办法是切换通道后启动一次“哑转换”dummy conversion读取其结果并丢弃然后再开始正式采集。Pico SDK 的adc_select_input没有自动丢弃第一次结果这是需要开发者自己补的逻辑。5.3 坑三单片 ADC 引脚接了多个功能GPIO26~GPIO29 在 Pico 上是复用引脚既能当模拟输入也能当数字 GPIO、PWM、I2C、SPI 等。如果你不小心在代码里把 ADC0 对应的 GPIO26 配成了数字输出ADC 转换得到的值就不是正常的模拟电压而可能是驱动电平。排查这类问题时先用gpio_get_function(26)确认引脚功能必要时用gpio_set_function(26, GPIO_FUNC_NULL)解除复用。5.4 坑四FIFO 溢出以后 ERR_STICKY 不清除前面已经提过一次这里再强调ERR_STICKY需要写 1 清除不是写 0。很多从 STM32 转过来的开发者习惯“写 0 清标志”到了 RP2040 上就会踩这个坑。建议在初始化时就把ERR_STICKY位清干净并在运行中周期性地检查它一旦出现ERR就停止采集定位是 DMA 配置问题还是 FIFO 阈值设置问题。5.5 坑五DMA 传输数量配置错误导致半途停止或越界DMA 的传输数量是len它代表要传输的样本总数单位是 16 位DMA_SIZE_16。很多人会在这里惯性思维以为传“字节数”结果缓冲数组只开了长度len/2跑着跑着就越界了。比如你申请uint16_t buf[1000]DMA 的len应该写 1000而不是 2000。安装传输完成后会触发中断记得在中断服务函数里重新配置下一次传输否则采集就是一次性的。5.6 坑六采样时间间隔抖动导致 FFT 频谱泄漏如果你用 DMA 连续采样然后做 FFT 频谱分析会发现即使输入是一个纯正弦波频谱里也有明显的旁瓣。原因不是 FFT 算法问题而是采样时刻不是均匀间隔的也就是抖动。DMA 请求响应的延迟、总线竞争、中断抢占都可能导致采样点之间的时间间隔不完全相等。解决思路是把 ADC 配置成自由运行模式并关闭DREQ_STALL用 FIFO 自然的节奏出数据如果还不行就在硬件上加上一个稳定的采样时钟触发源或者用外部定时器触发 ADC 转换。RP2040 的 ADC 没有内置定时触发通道这是它的一个短板需要靠外部 PWM 脉冲或自建硬件电路来弥补。常见问题可能原因排查方法解决方案读数值整体偏低输入阻抗大采样时间不足增大 ADC_DIV 或测量源阻抗加运放缓冲或降低分压电阻阻值读数值跳动大参考电压噪声、未用通道悬空示波器测 3.3V 纹波检查未用引脚加强电源滤波未用引脚接 GND切换通道后第一点错误通道切换后未丢弃第一个样本打印切换后前几个采样值执行 dummy conversionFIFO 溢出丢数据阈值设置太高DMA 响应慢检查 FCS.ERR_STICKY 是否置 1阈值设为 1开启 DREQ_STALLDMA 传输停止传输数量配满未重新配置检查中断服务函数在传输完成中断中重新初始化 DMA 传输FFT 频谱旁瓣明显采样时间间隔抖动使用逻辑分析仪抓采样完成信号用外部定时器触发 ADC或关闭 DREQ_STALL6. 从寄存器到应用的完整示例连续采集电池电压并软件校准最后用一个实际例子把前面讲的内容串起来用 Pico 的 ADC0 采集一节 18650 锂电池电压通过两路电阻分压把最高电压 4.2V 降到 2.8V 左右再用软件校准输出毫伏值并把数据通过串口发送出来。这个方案在电池供电的便携设备里很常见。硬件准备Pico 开发板一块10kΩ 和 4.7kΩ 分压电阻各一只18650 电池及座分压比计算如果电池满电 4.2V分压后应该低于 3.3V。10k 和 4.7k 分压后约为 4.2 × 4.7 / (10 4.7) ≈ 1.34V在 ADC 输入范围内。电池最低放电截止电压按 3.0V 算分压后约 0.96V能覆盖整个 ADC 线性区间。这样我们在代码里可以根据 ADC 读数反向推算出电池电压V_battery V_adc × (R1 R2) / R2 V_adc × 14.7 / 4.7代码实现#include stdio.h #include pico/stdlib.h #include hardware/adc.h #define ADC_CHANNEL 0 #define R1_OHM 10000.0f #define R2_OHM 4700.0f // 两点校准系数实际测量得到 #define ADC_GAIN (1.0015f) #define ADC_OFFSET (-2.0f) // mV float adc_read_voltage_mv(void) { // 使用 FIFO 模式连续取 8 次平均 adc_fifo_setup(true, false, 1, false, false); adc_run(true); uint32_t sum 0; for (int i 0; i 8; i) { while (adc_hw-fcs ADC_FCS_EMPTY_BITS) { tight_loop_contents(); } sum adc_hw-fifo 0xFFF; } adc_run(false); adc_fifo_drain(); float v_adc (sum / 8.0f) * (3.3f / 4095.0f) * 1000.0f; // mV // 软件校准 v_adc (v_adc - ADC_OFFSET) / ADC_GAIN; // 恢复电池电压 return v_adc * (R1_OHM R2_OHM) / R2_OHM; } int main() { stdio_init_all(); adc_gpio_init(26); adc_init(); sleep_us(500); while (true) { float battery_mv adc_read_voltage_mv(); printf(Battery: %.1f mV (%.2f V)\n, battery_mv, battery_mv / 1000.0f); sleep_ms(500); } }这段代码里我做了几件事adc_fifo_setup启用了 FIFO这样连续取 8 次数据时不需要一直管 READY 位。每次做均值前先清空 FIFOadc_fifo_drain避免残留数据污染。校准公式用的是“先减偏移再除增益”对应实际值 (ADC值 - offset) / gain。这个校准系数怎么来的前面说的两点校准法用万用表实测电池分压点电压再对比 ADC 读数线性拟合出来。你如果不想做校准直接把ADC_GAIN1、ADC_OFFSET0也能用精度差一些而已。7. 最后一个提醒ADC 是个模拟器件别用数字思维去调它其实写到这里我最有感触的一点是ADC 的问题很多时候不是靠堆逻辑能解决的而是靠理解模拟电路的基本原理来解决。我见过有人花了一整天调试 FIFO 丢数据结果是采样率设置太高导致转换不稳定也见过有人反复检查寄存器配置最后发现是 3.3V 供电引脚接触不良。如果你在读这篇文章后打算深入 Pico 的 ADC 开发我建议你手边常备一台示波器采样频率、电源纹波、引脚波形这些问题都有直观答案。寄存器能告诉你状态示波器能告诉你真相。这两者结合起来ADC 开发就再也不会是玄学。
返回列表