尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

树莓派Pico ADC深度解析:从SAR原理到DMA多通道采集

树莓派Pico ADC深度解析:从SAR原理到DMA多通道采集 很多人拿到树莓派 Pico 第一件事就是玩点灯和串口等真正要用 ADC 采样电压时SDK 里一个adc_read()调完就把板子扔一边了。但用这个函数读到的数字只是表象背后那块 SAR ADC、状态机、寄存器甚至 FIFO 和 DMA 的配合才是决定项目能不能稳健跑起来的真正分水岭。这篇文章就把树莓派 Pico ADC 从硬件架构、工作模式、寄存器到应用避坑逐层拆一遍最后结合我实际踩过的坑给出可以直接抄作业的代码和排查思路。无论你是第一次接触单片机 ADC还是已经会调 Pico SDK、想上 DMA 多通道采集后面这些内容都值得认真过一遍。调试期间遇到问题可以直接翻到第 4 章的速查表对号入座。从我开始用 Pico 做电压采集到现在最深的感受是ADC 这东西看着简单真正把它放到一个真实项目里几乎每个环节都有暗坑。这篇就按“硬件原理 → 工作模式 → 寄存器/SDK 实操 → 避坑”这条线写下来尽量做到不但告诉你怎么写还告诉你为什么要这么写。1. 硬件架构别再把 ADC 当成一个黑盒子1.1 SAR ADC 的原理为什么每次转换要花 96 个时钟周期树莓派 Pico 用的是 RP2040 芯片内置的 ADC 是一颗 12 位逐次逼近型SAR ADC转换器不是积分型也不是 Σ-Δ 型。SAR 这个词很多人见过但未必清楚内部到底怎么工作。它里面有一个比较器、一个内部 DAC、一个逐次逼近寄存器工作过程很像“用手掂重量”先拿一半满量程电压去比看信号比这个大还是小从而确定最高位然后根据结果再把范围缩小一半继续比下一位。这样一个 bit 一个 bit 地逼近12 位分辨率就要比 12 次所以叫逐次逼近。RP2040 的 ADC 时钟基数是固定的 48MHz而完成一次转换至少要花 96 个 ADC 时钟周期。这 96 个周期里既包括了采样保持电容的充电时间也包括了 SAR 逐位比较和内部 DAC 建立的时间。如果分频器设置成 0也就是不分频时最大采样率大约就是 500kS/s。这个 500k 听起来不高但对一个通用 MCU 来说已经够用。很多人在代码里完全不关心采样率以为读得越快越好其实这个 96 周期约束直接决定了你的采样率上限也决定了高阻抗信号源能不能采准。这里要纠正一个常见误区ADC 的分辨率是 12 位不代表它每次读数都能达到 12 位精度。分辨率只是把 3.3V 电压范围切成了 4096 格每一格对应约 0.806mV真实的精度还受参考电压噪声、采样电容充电程度、芯片制造偏差等因素影响实际有效位数可能只有 10 到 11 位。把它当成一个“裸奔的 12 位传感器”来用往往会在项目中吃亏。1.2 输入通道和参考电压4 个外部引脚加 1 路内部温度Pico 的 ADC 通道规划是很多人第一天就会踩坑的地方。RP2040 一共有 5 个可供使用的模拟输入通道其中 4 个是外部引脚1 个是内部温度传感器。通道编号引脚/来源说明ADC0 / AIN0GPIO26外部模拟输入ADC1 / AIN1GPIO27外部模拟输入ADC2 / AIN2GPIO28外部模拟输入ADC3 / AIN3GPIO29外部模拟输入注意部分板卡被占用ADC4 / AIN4内部温度传感器测量芯片温度不是环境温度参考电压方面ADC 的满量程对应 VREF也就是 3.3V。转换结果和输入电压之间是线性关系raw Vin / 3.3 * 4095。反推电压自然就是Vin raw * 3.3 / 4095。这个公式永远要记住看到 raw 先换算成电压再谈分析。1 个 LSB 对应的电压是 3.3V 除以 4096约 0.806mV有的地方写 4095有的写 4096工程上差距极小不必纠结。还有一个关键点Pico 板子上ADC_VREF引脚通常和 3.3V 电源直接连在一起芯片的参考电压就是板子自己的 3V3因此 ADC 精度会被供电质量直接绑架。你就算代码写得再漂亮3.3V 电源纹波大读出来的数据一样会抖。如果需要高精度要么把供电做好要么考虑外接高精度基准电压源但这需要动板级设计新手不建议折腾。另外Pico W 这类带无线的板子GPIO29 往往已经被板载射频或控制功能占用所以默认拿它当 ADC3 用基本是踩坑。带无线版本的板子外部 ADC 通道优先用 GPIO26、GPIO27、GPIO28遇到问题先看官方原理图确认哪个引脚是干净的。1.3 精度瓶颈不在 ADC 本身而在你给的信号源很多人调 ADC 第一反应是怀疑芯片不行其实大多数精度问题出在信号源阻抗上。SAR ADC 内部有一个采样保持电容每次转换前需要对这个电容充电。如果信号源内阻很高充电时间就不够采样到的电压会低于真实值而且会跟着温度、噪声漂移。RP2040 数据手册对源阻抗的要求其实挺严苛量级在百欧姆级别。这不是说接个 1k 电阻就完全不能读而是你会明显看到读数偏低和跳动。我用一个 10k 电位器直接连到 GPIO26 测试时读数从头到尾都在跳加了一级运放跟随器之后立刻稳定下来。如果你的信号是高阻输出比如光电二极管、某些化学传感器最稳妥的办法是先用一个低输出阻抗的运放缓冲再进 ADC。除了源阻抗输入范围也必须在 0 到 3.3V 之间。超过 3.3V 不仅读不出更高值还可能损坏引脚。要测电池这种超过 3.3V 的电压必须用电阻分压。比如要测 5V 信号可以用 10k 和 18k 分压5V * 18 / (1018) ≈ 3.21V刚好卡在安全范围内。分压电阻也不能选太大否则又变成高阻源问题要综合取舍。2. 工作模式与寄存器手动操作背后的状态机2.1 单次转换START_ONCE、DONE 和 AINSELPico 的 ADC 寄存器基地址是 0x4004C000。SDK 封装得很好但了解寄存器才能理解那些函数到底做了什么。单次转换是最常用的模式流程非常简单先把CS寄存器里的AINSEL字段设为要采的通道号然后写START_ONCE触发一次转换接着轮询DONE标志等硬件完成后从RESULT寄存器读出 12 位数据。SDK 的adc_read()底层干的就是这件事只不过帮你把寄存器操作和等待都封装好了。如果你不想用 SDK核心寄存器操作可以精简成这样#define ADC_BASE 0x4004c000 #define ADC_CS (*(volatile uint32_t *)(ADC_BASE 0x00)) #define ADC_RESULT (*(volatile uint32_t *)(ADC_BASE 0x04)) uint16_t adc_read_reg(int ch) { ADC_CS ~(0x7u 9); // 清 AINSEL ADC_CS | ((uint32_t)ch) 9; // 选通道 ADC_CS | (1u 1); // START_ONCE while (!(ADC_CS (1u 3))); // 等 DONE return ADC_RESULT 0xfff; // 低 12 位 }这段代码的前提是已经初始化好外设时钟、GPIO 复用并且CS.EN已经置 1。它给你展示的是 ADC 工作的核心状态机选通道、触发、等待、读结果。理解了这个SDK 里那些封装函数的调用顺序就不会再玄学。CS寄存器里的字段要记清楚EN是总开关START_ONCE是单次触发START_MANY是连续采样开关DONE是转换完成标志RROBIN[4:0]控制轮询通道AINSEL[2:0]控制当前选择的输入通道。了解了这些位后面的所有模式都是在这个基础上叠加。2.2 连续采样与轮询RROBIN 怎么让四个通道自己转圈单次转换之外第二种常用模式是连续采样。把START_MANY置 1ADC 会启动后不停转换不需要一次次手动触发。这个模式下你只需要在需要时读取最新结果。单独跑一个通道的连续采样很简单真正有意思的是轮询模式。RROBIN是一个 5 位的掩码每一位对应一个通道bit0 到 bit3 对应 ADC0 到 ADC3bit4 对应内部温度传感器。哪些位置 1ADC 在连续模式下就会自动在这些通道之间循环采样。adc_set_round_robin((1 0) | (1 1) | (1 2) | (1 3)); adc_run(true);开启轮询后硬件会自动更新AINSEL所以你不需要手动切频道FIFO 里出来的数据顺序固定是 0、1、2、3、0、1、2、3……这样的循环。这里有个经验轮询模式刚启动时前几个样本可能受之前通道残留电荷影响所以多通道采集时我习惯把第一批数据丢掉或者用 DMA 配合循环缓冲区做后期对齐。2.3 FIFO、DREQ 和 DMA数据搬运的正确姿势单通道慢速采集CPU 每次读一个值无所谓。但如果你要四通道连续采样或者每秒钟要几千个点CPU 忙等会浪费大量时间。这时候 FIFO 和 DMA 就派上用场了。Pico 的 ADC 自带一个 8 项深度的 FIFO转换完成后结果可以先暂存到 FIFO 里。FCS寄存器里的THRESH字段用来设置触发阈值当 FIFO 里的数据量达到这个阈值硬件会拉高 DREQ 信号通过 DMA 控制器自动把数据搬到内存。我自己做四通道 DMA 时最直观的经验是DREQ 阈值设在 1 或 2逻辑最直白每次 FIFO 有货就搬走如果采样率很高可以把阈值调到 4 或 8减少 DMA 请求次数但一定要注意别让 FIFO 溢出。FIFO 溢出时FCS里的OVERFLOW标志会置位从标志位就能看出搬运速度跟不上采样速度。这一整套机制的意义是ADC 采样过程中 CPU 基本可以撒手不管DMA 把数据从外设搬到内存数组里你只需要在 DMA 完成中断里处理整包数据。这就是“数据搬运的正确姿势”。2.4 关键寄存器速查表偏移寄存器关键位用途0x00CSEN、START_ONCE、START_MANY、DONE、RROBIN[4:0]、AINSEL[2:0]ADC 控制与状态0x04RESULTRESULT[11:0]单次转换结果0x08FCSTHRESH[2:0]、LEVEL[2:0]、FULL、EMPTY、UNDERFLOW、OVERFLOW、DREQ_EN、DMA_ENFIFO 控制与状态0x0CFIFOFIFO 数据读取 FIFO 结果0x10DIVINT[15:0]、FRAC[7:0]采样分频16.8 定点0x14INTRFIFO_THRESH、FIFO_OVER、FIFO_UNDER、DONE原始中断状态0x18INTE同上中断使能0x1CINTF同上中断强制0x20INTS同上中断状态表格里这些寄存器主要在数据手册第 4.9 章。SDK 用多了再看这张表会非常有画面感。2.5 采样率计算与常用档位采样率的计算式特别重要几乎所有采样相关的问题最后都会回到这个公式采样率 48MHz / ((DIV 1) * 96)DIV 是分频寄存器的值这是个 16.8 定点数也就是整数部分加小数部分。SDK 里adc_set_clkdiv()可以直接传浮点数很方便。我整理几个常用档位DIV 设置采样率0500kHz1250kHz3125kHz950kHz1925kHz995kHz999500Hz空气采样率 500kHz 只是上限不代表每次都要跑满。采样率越低每个采样点的充电时间越长对高阻源越友好。很多传感器本身响应很慢完全没必要跑 500kHz。低速高精度场景把分频调到 100 或 1000 都是常见操作。3. 基于 SDK 的实操从单通道到四通道 DMA3.1 SDK 常用函数一览Pico SDK 把这些寄存器操作封装得比较顺手核心里面就这些函数函数作用adc_init()初始化 ADC 外设使能时钟和内部电源adc_gpio_init(gpio)配置 GPIO26-29 为 ADC 模拟输入adc_select_input(ch)选择当前输入通道 0-4adc_read()执行一次单通道采样返回 12 位结果adc_run(start)启动或停止连续采样adc_set_round_robin(mask)设置轮询通道掩码adc_fifo_setup(en, dreq_en, thresh, err_in_fifo, byte_shift)配置 FIFO 和 DMA 请求adc_fifo_read()从 FIFO 读取一个结果adc_fifo_get_level()获取 FIFO 内数据个数adc_set_clkdiv(div)设置分频adc_set_temp_sensor_enabled(en)使能内部温度传感器这个函数表就是一个地图。后面几节我会把最常写的三种代码贴出来并且解释每一步在干什么。3.2 单通道读电压最基础也是最容易忽略的流程最简单的单通道采样代码网上一搜一大把但很多人直接抄完发现读数为 0 或者满天飞就是因为少了adc_gpio_init或者选错了通道编号。#include stdio.h #include pico/stdlib.h #include hardware/adc.h int main() { stdio_init_all(); adc_init(); adc_gpio_init(26); // GPIO26 配置为模拟输入 adc_select_input(0); // 选择 ADC0 while (true) { uint16_t raw adc_read(); float v raw * 3.3f / 4095.0f; printf(raw%u v%.3f\n, raw, v); sleep_ms(100); } }注意第二行adc_gpio_init(26)和第三行adc_select_input(0)是不同的操作。前者是把 GPIO26 的引脚复用功能切换到模拟输入后者是告诉 ADC 内部多路选择器选通道 0。漏掉任何一个结果都不对。如果你用 GPIO27 采样记住 GPIO27 是 ADC1通道号和引脚号不是一回事。3.3 温度传感器内部通道的正确打开方式读取内部温度传感器是一个很常见的测试例程代码看起来简单但坑也不少。adc_init(); adc_set_temp_sensor_enabled(true); adc_select_input(4); // 通道 4 是温度传感器 uint16_t raw adc_read(); float v raw * 3.3f / 4095.0f; float t 27.0f - (v - 0.706f) / 0.001721f;温度传感器输出一个随温度变化的电压典型参数是 27 摄氏度时输出电压约 0.706V温度系数约 1.721mV/摄氏度。所以芯片温度大约是T 27 - (V - 0.706) / 0.001721。用这个公式时要把电压先算出来然后再转温度。有几个容易忽略的点温度传感器通道必须先用adc_set_temp_sensor_enabled(true)使能否则读数可能为 0测到的是芯片内部温度不是环境温度这个传感器精度有限当成 ±2 到 3 摄氏度的参考值可以真拿去做温度控制容易被坑。出厂校准参数存在个体差异不同批次芯片读数可能会差好几度不要指望它替代数字温度传感器。3.4 四通道轮询 FIFO DMA多通道采集的正确写法多通道高速采集是把这个 ADC 用到极致的地方也是热搜词里出现最频繁的场景。我直接把一个能跑的四通道采样核心片段贴出来先说结论用 DMA 之后CPU 负担极小数据按通道顺序排列在内存数组里处理很方便。#include string.h #include pico/stdlib.h #include hardware/adc.h #include hardware/dma.h #define CHANNELS 4 #define SAMPLES 100 uint16_t data[CHANNELS * SAMPLES]; void adc_dma_sample(void) { adc_init(); adc_gpio_init(26); adc_gpio_init(27); adc_gpio_init(28); adc_gpio_init(29); int ch dma_claim_unused_channel(true); dma_channel_config cfg dma_channel_get_default_config(ch); channel_config_set_transfer_data_size(cfg, DMA_SIZE_16); channel_config_set_read_increment(cfg, false); channel_config_set_write_increment(cfg, true); channel_config_set_dreq(cfg, DMA_DREQ_ADC); dma_channel_configure(ch, cfg, data, // 写入地址内存数组 adc_hw-fifo, // 读取地址ADC FIFO CHANNELS * SAMPLES); adc_fifo_setup(true, true, 1, false, false); adc_set_round_robin((1 0) | (1 1) | (1 2) | (1 3)); adc_run(true); dma_channel_wait_for_finish_blocking(ch); adc_run(false); adc_fifo_drain(); }这段代码的逻辑是DMA 把 ADC FIFO 里的数据源源不断搬进data数组传输总数是CHANNELS * SAMPLES。由于轮询顺序固定数据在数组里的排列是data[0]CH0, data[1]CH1, data[2]CH2, data[3]CH3, data[4]CH0...处理时按索引取整就是对应通道。这里我踩过的坑是dreq_thresh设为 1 虽然逻辑最简单但在 500kHz 满速采样时DMA 请求频率太高CPU 中断反而可能受影响。实际项目里如果发现 FIFO 溢出可以把adc_fifo_setup的第三个参数调大比如 4让硬件凑够 4 个数据再搬一次。代价是数据响应多了一点延迟但对大多数慢速传感器毫无影响。3.5 C 语言 ADC 值滤波函数放对位置才有效采集到原始数据之后不能直接拿起来就用噪声和抖动是家常便饭。我常用三种滤波方式不同场景选不同方案。滑动平均适合平稳信号比如电池电压、温度这类变化慢的量代码简单有效#define FILTER_N 8 static uint32_t buf[FILTER_N]; static uint8_t idx 0; static uint32_t sum 0; static bool full false; uint16_t moving_average(uint16_t x) { if (full) sum - buf[idx]; buf[idx] x; sum x; idx (idx 1) % FILTER_N; if (idx 0) full true; return (uint16_t)(sum / FILTER_N); }中值滤波适合有明显毛刺的场景比如电机启动时把 ADC 信号瞬间拉高或拉低。取 5 个点排序取中间值稳定性和响应速度都比较均衡static int cmp16(const void *a, const void *b) { return (*(uint16_t *)a) - (*(uint16_t *)b); } uint16_t median_filter(uint16_t x) { static uint16_t hist[5]; static uint8_t i 0; uint16_t tmp[5]; hist[i] x; if (i 5) i 0; memcpy(tmp, hist, sizeof(tmp)); qsort(tmp, 5, sizeof(uint16_t), cmp16); return tmp[2]; }一阶低通适合需要实时平滑、又想要快响应的场景代码最轻量static float y 0; static bool inited false; uint16_t lowpass(uint16_t x) { if (!inited) { y x; inited true; return x; } y 0.9f * y 0.1f * x; return (uint16_t)y; }注意滤波函数要放在哪个位置。我在项目里的习惯是每个通道先各取 5 到 10 个点做中值或滑动平均再进入业务逻辑。如果放到 DMA 中断后统一过滤注意数组长度要够别越界。还有一点滤波不是万能的如果源阻抗和电源问题没解决滤波只会掩盖问题治标不治本。4. 应用避坑与排查实录这些坑我全踩过4.1 读数跳得离谱先怀疑悬空和接线ADC 引脚悬空时读数会随机漂移这是正常现象不是芯片坏了。我见过不少新手拿一根杜邦线直接怼到 GPIO26 上另一端什么也不接然后跑来问为什么读数不稳定。ADC 引脚必须有确定的电压信号这是第一课。接线里的坑还包括ADC 引脚不能和数字输出直接并联否则数字引脚的推挽信号会把模拟电压强行拉高拉低GPIO 复用配置错了也可能导致功能冲突。我调试时常做的一件事是拿万用表直接量引脚电压和程序读到的电压对一下。如果万用表显示的是 1.65V程序读出来是 1.65V那 ADC 本身没问题问题多半在信号源或者接线。这个习惯能帮你快速分层定位。4.2 高阻源带来的读值偏低和拖尾高阻抗源是 ADC 精度杀手。我曾经用一个 10k 电位器中间抽头直接接 GPIO26读数跳得完全没法用后来接了一级运放跟随器才稳定。原理很简单ADC 内部采样电容充电需要时间信号源内阻太高电容充不满读数自然偏低。高速采样时这个问题更明显因为留给电容充电的时间更短。解决思路有三个第一降低信号源阻抗最标准的方法是加电压跟随器这是首选第二降低采样率给采样电容更多的充电时间对某些中高阻源有一定改善第三在 ADC 引脚加一个小电容比如 0.1uF 到 GND但加完以后信号响应会变慢要看你的信号频率能不能接受。真实项目里如果信号频率不高我会同时用跟随器和弱滤波电容效果最稳。4.3 多通道切换后数据错位多通道轮询采集最常见的现象是数据串位比如明明采的是通道 0读出来却像是通道 1 的残留值。这是因为切换通道后采样电容里还残留上一通道的电荷需要一点建立时间才能稳定。别指望硬件会帮你自动丢掉无效数据它只是按顺序转。我的处理办法是轮询模式下第一组数据直接丢弃或者 DMA 启动后先把缓冲区填满一次再触发业务处理或者在每个通道切换后多采一个点但不用它。数据错位问题在低速采样时不太明显一旦跑高速通道数变多这个坑就会暴露处理思路要提前设计好。4.4 电源纹波与参考电压精度瓶颈大部分在供电这是最容易被忽略、也最容易翻车的一环。Pico 的 ADC 参考电压直接和 3.3V 绑在一起3.3V 上有 20mV 纹波对应到 ADC 上就是大约 25 个 LSB 的跳动你滤波都未必救得回来。很多嵌入式系统的 3.3V 是从 DC-DC 输出的纹波往往不小。改善方案按性价比排序先把供电做好比如用 LDO 给模拟部分供电然后在 ADC_VREF 引脚附近加合适的去耦电容如果项目对精度要求高就不要用板内 ADC直接上外部 ADC 芯片。这里再提一个软件校准思路如果只是增益和偏置误差可以用两点或三点校准。三点校准一般是采三个已知电压比如接近 0V、中间值 1.65V、接近 3.3V 的满量程点拟合出线性参数或分段补偿表把 raw 映射到更贴近真实电压的值。注意这只能解决系统误差解决不了随机噪声。4.5 常见问题速查表现象可能原因解决办法读数一直 4095输入超量程、通道悬空接高、选错通道检查信号电压确认 AINSEL 和接线读数一直 0输入接地、未使能 ADC、引脚复用错误检查接线先执行 adc_init 和 adc_gpio_init读数乱跳悬空、高阻源、电源纹波、线缆感应加跟随器、降低采样率、改善供电、加滤波多通道数据串位通道切换建立时间不足丢弃前几个样本降低采样率温度通道读数异常未使能温度传感器、直接套用室温先 adc_set_temp_sensor_enabled(true)再按芯片温度公式换算FIFO overflow 置位DMA 搬运速度跟不上采样调大 DREQ 阈值或降低采样率DMA 一直不触发fifo_setup 里 dreq_en 没开、通道配置错误检查 adc_fifo_setup 参数确认 dreq 设置GPIO29 采样异常板载资源占用该引脚如 Pico W换 GPIO26-28查阅官方原理图写到这里我个人在实际操作中最大的体会是用 Pico ADC 之前先花十分钟想清楚两件事——信号源阻抗多大参考电压干不干净。这两个问题想明白了后面的读数和滤波都会轻松很多。最后再分享一个小技巧。调试任何 ADC 问题第一步永远是拿万用表直接量你要采的节点确认硬件电压确实是你预期的数值。这一步看似多余但在排查中能帮你排除一半以上的低级错误省下大量时间。Pico 的 ADC 是一颗“够用但别指望发高精度财”的外设做电池电压检测、温度监控、按键电平判断、慢速传感器采集都没问题要真的做高精度仪表还是老老实实上一颗外部 ADC比在 MCU 内部和外设搏斗舒心得多。
返回列表