3、ADC驱动开发:从原理到实战

大家好,我是你们的老朋友。今天我们来聊聊ADC驱动开发。说实话,ADC这玩意儿,我刚开始做嵌入式那会儿,总觉得它就是个“黑盒子”——给个电压,出来个数字,完事儿。直到有一次,我做的温控项目,采集的温度值跳得像心电图,我才意识到,ADC驱动远没那么简单。

这一章,我会把ADC的底裤扒干净。从工作原理到电压换算,从单次转换到DMA传输,咱们一个一个来啃。

3.1 ADC工作原理:它到底在干啥?

ADC,全称模数转换器。说白了,就是把连续的模拟电压,变成离散的数字量。你想想看,MCU只认识0和1,它怎么知道外面的世界是3.3V还是1.8V?全靠ADC这个“翻译官”。

常见的ADC架构有好几种,比如逐次逼近型(SAR)、积分型、流水线型。咱们MCU里用的最多的,就是逐次逼近型ADC。为什么?因为它性价比高,速度也够用。

它的工作原理,我打个比方:就像你猜一个数字,每次问“比这个数大还是小?”,然后不断缩小范围。ADC内部有个比较器,它拿输入电压跟一个内部DAC产生的参考电压比,从最高位开始,一位一位地逼近真实值。

核心流程:

  1. 采样保持电路把当前电压“冻结”住。
  2. 逐次逼近寄存器(SAR)从最高位开始试探。
  3. 比较器判断“试探电压”比输入电压大还是小。
  4. 根据比较结果,决定这一位是1还是0。
  5. 重复步骤2-4,直到所有位都确定。

嗯,这里要注意:采样保持的时间不能太短,否则电容没充饱,采到的值就不准。我在项目中遇到过,有人把采样时间设成1个ADC时钟周期,结果采集出来的值总是偏低,折腾了两天才找到原因。

3.2 分辨率与采样率:鱼和熊掌

这两个参数,是ADC最重要的指标。咱们一个一个说。

分辨率

分辨率决定了ADC能分辨多小的电压变化。比如12位ADC,参考电压3.3V,那它能分辨的最小电压就是:

LSB = Vref / 2^N = 3.3V / 4096 ≈ 0.8mV

说白了,分辨率越高,能看到的细节就越多。但代价是什么?转换时间变长,数据量变大。

分辨率 量化级数 3.3V下的LSB 典型应用
8位 256 12.9mV 电池电压检测
10位 1024 3.2mV 电位器输入
12位 4096 0.8mV 传感器信号采集
16位 65536 50μV 精密测量

采样率

采样率就是ADC每秒能转换多少次。单位是SPS(Samples Per Second)。根据奈奎斯特定理,采样率至少要是信号最高频率的2倍,才能无失真地还原信号。

我的经验:实际项目中,我一般取5-10倍。比如采集50Hz的工频信号,采样率至少设到500SPS以上。为什么?因为实际信号有谐波,留点余量心里踏实。

分辨率和采样率是矛盾的。分辨率越高,每次转换需要的时间就越长,采样率自然就上不去。所以选型的时候,得看你的应用场景。做音频,采样率优先;做称重传感器,分辨率优先。

3.3 单次转换与连续转换

这两种模式,我刚开始也搞混过。其实很简单:

  • 单次转换:你发个启动命令,ADC转换一次,结果存到数据寄存器里,然后停下来等你来读。适合“偶尔测一下”的场景,比如检测按键是否按下。
  • 连续转换:ADC自己不停地转,转完一次马上开始下一次。数据寄存器里的值一直在更新。适合需要实时监控的场景,比如采集音频信号。

代码实现上,区别就在一个寄存器位。以STM32为例:

// 单次转换模式
ADC1->CR2 |= ADC_CR2_ADON;    // 启动ADC
ADC1->CR2 |= ADC_CR2_SWSTART; // 开始转换
while(!(ADC1->SR & ADC_SR_EOC)); // 等待转换完成
value = ADC1->DR;              // 读取结果

// 连续转换模式
ADC1->CR2 |= ADC_CR2_CONT;    // 开启连续模式
ADC1->CR2 |= ADC_CR2_ADON;    // 启动ADC
ADC1->CR2 |= ADC_CR2_SWSTART; // 开始转换
// 之后EOC标志会不断置位,DR值不断更新

避坑指南:我曾经在连续转换模式下,忘了在中断里及时读取数据寄存器。结果下一次转换完成,直接把上一次的结果覆盖了。读到的数据全是乱的。记住:连续模式下,数据寄存器只有一个,你不读,它就没了。

3.4 DMA传输:解放CPU

连续转换模式下,如果每次转换完都让CPU去读数据,那CPU啥也别干了,光伺候ADC了。这时候,DMA(直接存储器访问)就派上用场了。

DMA说白了,就是一个“数据搬运工”。它可以在没有CPU参与的情况下,把ADC的数据寄存器里的值,自动搬到内存数组里。搬完指定数量后,再通知CPU:“活干完了,你来处理吧。”

配置DMA传输,一般需要设置:

  • 源地址:ADC的数据寄存器地址
  • 目标地址:内存数组的地址
  • 传输长度:要采集多少个点
  • 传输模式:循环模式还是单次模式

我习惯用循环模式。比如采集100个点,DMA搬完100个,自动从头开始,覆盖旧数据。这样CPU随时可以拿到最近100个点的数据。

// DMA配置示例(伪代码)
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
DMA_InitStructure.DMA_BufferSize = 100;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_Init(DMA1_Channel1, &DMA_InitStructure);

// 使能ADC的DMA请求
ADC1->CR2 |= ADC_CR2_DMA;

// 启动ADC连续转换
ADC1->CR2 |= ADC_CR2_CONT | ADC_CR2_ADON | ADC_CR2_SWSTART;

小技巧:DMA传输完成中断里,我一般只设一个标志位,不处理复杂逻辑。主循环里检测到标志位,再去做数据处理。这样中断服务程序执行时间短,不会影响下一次DMA传输。

3.5 电压换算公式:从数字到物理量

ADC转换出来的值,是一个数字量。比如12位ADC,范围是0~4095。怎么换算成实际的电压值?公式很简单:

实际电压 = (ADC值 / 最大ADC值) × 参考电压

举个例子:参考电压3.3V,ADC读到的值是2048,那实际电压就是:

V = (2048 / 4095) × 3.3V ≈ 1.65V

但这里有个坑:参考电压不一定是MCU的供电电压。很多MCU有独立的Vref引脚,可以接外部精密参考源。如果你用内部参考,精度一般只有±1%左右。要求高的场合,我建议用外部参考,比如REF3033这种。

另外,如果传感器输出的是电流信号(比如4-20mA),需要先通过采样电阻转成电压,再进ADC。换算公式就要多一步:

电流 = (ADC值 / 最大ADC值) × 参考电压 / 采样电阻

实战经验:我曾经做一个压力传感器项目,传感器输出0.5V~4.5V,但MCU的ADC参考电压是3.3V。直接接上去,超过3.3V的部分就测不到了。解决办法有两个:一是用电阻分压,把4.5V降到3.3V以下;二是用外部参考电压,把Vref设成4.5V。我选了分压方案,因为成本低。但要注意,分压电阻的精度会影响测量精度,得用1%精度的电阻。

好了,ADC驱动开发的核心内容就这些。从工作原理到DMA传输,再到电压换算,每一步都有讲究。下一章,我们会讲定时器驱动开发,到时候再聊。

记住:ADC驱动写得好不好,直接决定了你的传感器数据准不准。别嫌麻烦,该配置的寄存器一个都不能少。