在嵌入式开发中,模数转换器(ADC)是连接模拟世界与数字世界的桥梁。无论是读取传感器数据、监测电池电压,还是采集音频信号,都离不开 ADC。
本文将以 NXP i.MX6ULL 处理器为例,从 ADC 的核心概念出发,深入解析其工作原理、寄存器配置,并手把手带你写出一套完整的裸机 ADC 驱动代码。
一、 什么是 ADC?
ADC(Analog-to-Digital Converter),即模数转换器。
它的核心作用是将连续变化的模拟信号 (如电压、电流、温度、压力、光强等)转换为离散的数字信号,使 MCU、CPU、DSP 等数字系统能够处理和分析这些物理量。
简单来说:
- 现实世界:模拟的(连续的电压波形)
- 数字系统:离散的(0 和 1)
- ADC:两者之间的翻译官
例如:热敏电阻随温度变化输出电压 → ADC 采样 → MCU 得到数字值 → 软件计算温度。
二、 核心概念:基准电压与分辨率
1. 什么是基准电压(Vref)?
基准电压(Reference Voltage) ,通常记作 Vref,是 ADC 进行模数转换时的"标尺"。
它有两个核心作用:
- 定义量程范围:单极性 ADC 的可测电压范围通常为 0 ~ Vref。
- 决定量化精度:Vref 与分辨率共同决定了 ADC 能分辨的最小电压变化(1 LSB)。
注意事项:
- 输入电压一般不能超过 Vref,否则 ADC 会饱和输出最大值,严重时可能损坏芯片。
- 基准电压越稳定,ADC 转换结果越准确。如果 Vref 本身有波动,即使输入电压不变,ADC 读数也会跳动。
2. 什么是分辨率?
ADC 的分辨率 用位数 N 表示,决定了 ADC 能把量程划分成多少个离散等级。位数越高,能分辨的最小电压变化越小,理论精度越高。
N 位 ADC 的量化等级数为 2^N,最小分辨电压(1 LSB)的计算公式为:
1\\ \\text{LSB} = \\frac{V_{ref}}{2\^N}
常见分辨率如下:
| 分辨率 | 量化等级(2^N) | 数字输出范围 |
|---|---|---|
| 8 位 | 256 | 0 ~ 255 |
| 10 位 | 1,024 | 0 ~ 1,023 |
| 12 位 | 4,096 | 0 ~ 4,095 |
| 16 位 | 65,536 | 0 ~ 65,535 |
以 12 位 ADC、Vref = 3.3V 为例:
1\\ \\text{LSB} = \\frac{3.3V}{4096} \\approx 0.806\\text{mV}
这意味着该 ADC 理论上能分辨的最小电压变化约为 0.806mV。
三、 ADC 的工作原理
ADC 的完整转换过程通常分为四个步骤:采样 → 保持 → 量化 → 编码。
| 步骤 | 说明 |
|---|---|
| 采样 | 按固定时间间隔采集模拟信号的瞬时值,实现时间上的离散化 |
| 保持 | 将采样到的电压暂时保持住,防止转换过程中信号变化影响精度 |
| 量化 | 将连续的模拟幅值映射为有限个离散的电平等级 |
| 编码 | 将量化后的等级转换为二进制数字输出 |
以嵌入式中最常用的逐次逼近型(SAR)ADC 为例,其核心思想是二分查找:
- 先拿输入电压和 Vref/2 比较,确定最高位是 0 还是 1;
- 再根据已确定的位继续比较下一位;
- 从最高位到最低位逐次逼近,最终得到与输入电压最接近的数字编码。
四、 实战:i.MX6ULL ADC 裸机驱动开发
1. i.MX6ULL ADC 硬件特性
i.MX6ULL 内部集成了 2 个独立的 ADC 控制器(ADC1、ADC2),采用逐次逼近型(SAR)架构,核心特性如下:
- 分辨率:可选 8/10/12 位,裸机开发通常使用 12 位模式
- 参考电压:0 ~ 3.3V(开发板通常为 3.3V)
- 最大采样率:1MS/s,单次转换最快仅需 1μs
- 通道配置:每个 ADC 有 10 个通道,共用 GPIO1_IO00 ~ GPIO1_IO09 这 10 个引脚
- 自动校准:内置硬件自动校准功能,无需手动配置校准参数
- 时钟源:支持 IPG Clock、IPG/2、ADACK(内部 20MHz 时钟)
2. 核心寄存器详解
裸机操作 ADC 主要涉及以下寄存器:
| 寄存器 | 名称 | 功能说明 |
|---|---|---|
| ADCx_CFG | 配置寄存器 | 设置分辨率、时钟源、采样周期、硬件平均等 |
| ADCx_GC | 通用控制寄存器 | 使能时钟、启动自动校准、开启连续转换等 |
| ADCx_HC0 | 硬件通道0控制寄存器 | 选择输入通道并触发转换 |
| ADCx_HS | 状态寄存器 | 查询转换完成标志 |
| ADCx_R0 | 数据结果寄存器 | 存储转换完成后的数字值 |
| ADCx_GS | 通用状态寄存器 | 查询校准结果等全局状态 |
3. 完整驱动代码
以下是基于 i.MX6ULL 的 ADC1 裸机驱动代码,包含初始化、自校准、数据采集和电压换算功能:
#include "adc.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "stdio.h"
/**
* @brief ADC1 自校准函数
* @return 1: 校准成功, 0: 校准失败
*/
int Calibration_self(void)
{
// 清除校准失败标志
ADC1->GS |= (1 << 1);
// 启动校准
ADC1->GC |= (1 << 7);
// 等待校准完成
while ((ADC1->GC & (1 << 7)) != 0);
// 返回校准结果
return ((ADC1->GS & (1 << 1)) == 0);
}
/**
* @brief ADC1 初始化函数
* 配置 GPIO1_IO01 为 ADC 输入通道
*/
void adc1_init(void)
{
// 1. 引脚复用:将 GPIO1_IO01 配置为 ADC 功能
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO01_GPIO1_IO01, 0);
// 2. 电气特性配置:使能 Keeper,禁用 Pull Up/Down
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO01_GPIO1_IO01, IOMUXC_SW_PAD_CTL_PAD_PKE(1));
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO01_GPIO1_IO01, IOMUXC_SW_PAD_CTL_PAD_PUE(0));
// 3. 禁用中断
ADC1->HC[0] &= ~(1 << 7);
// 4. 配置 ADC 参数
// MODE=10(12位分辨率), ADICLK=11(ADACK内部时钟)
ADC1->CFG = 0;
ADC1->CFG |= (2 << 2) | (3 << 0);
// 5. 使能 ADC 模块
ADC1->GC = 0;
ADC1->GC |= (1 << 0);
// 6. 执行自校准并打印结果
printf(Calibration_self() == 0 ? " Calibration completed normally.\n" : "Calibration failed.\n");
}
/**
* @brief 获取 ADC 原始值
* @return 12位 ADC 原始值 (0~4095)
*/
unsigned short get_adc_value(void)
{
// 先写入禁用通道,确保触发新转换
ADC1->HC[0] = 0x1F;
// 选择通道1,触发转换
ADC1->HC[0] = 1;
// 等待转换完成
while((ADC1->HS & (1 << 0)) == 0);
// 读取结果并屏蔽高4位,保留12位有效数据
return (unsigned short)(ADC1->R[0] & 0xFFF);
}
/**
* @brief 获取实际电压值
* @return 电压值 (单位: V)
*/
float get_voltage(void)
{
unsigned short temp = get_adc_value();
// 电压换算公式: Vin = (n / 4096) * 3.3V
return (float)temp / 4096.0f * 3.3f;
}
4. 代码解析
初始化流程
- 引脚配置:将 GPIO1_IO01 复用为 ADC 功能,并配置电气特性(使能 Keeper 保持器)。
- 参数配置:设置 ADC 为 12 位分辨率,使用内部 ADACK 时钟源。
- 自校准:启动硬件自动校准,等待完成后检查校准状态。
数据采集流程
- 触发转换 :向
ADC1->HC[0]写入通道号(通道1),立即启动一次转换。 - 等待完成 :轮询
ADC1->HS寄存器的 COCO0 标志位,等待转换完成。 - 读取结果 :从
ADC1->R[0]读取 12 位原始值。
电压换算
根据公式 V_{in} = \\frac{n}{4096} \\times 3.3V,将 ADC 原始值换算为实际电压。
五、 总结
通过本文,我们系统学习了:
- ADC 的基本概念:模拟信号与数字信号的桥梁
- 基准电压与分辨率:决定 ADC 量程和精度的核心参数
- 工作原理:采样、保持、量化、编码四步曲
- i.MX6ULL ADC 特性:双控制器、12 位分辨率、自动校准
- 裸机驱动实战:从寄存器配置到代码实现
理解这些概念是嵌入式开发的基础。后续可以结合具体的传感器(如光敏电阻、NTC 热敏电阻、电位器等)进行实际采集练习,将理论知识转化为项目经验。