在嵌入式开发中,ADC(模拟 - 数字转换器)是连接物理世界与数字系统的核心桥梁 ------ 温度、压力、光照、湿度等绝大多数传感器的输出都是模拟电压信号,必须通过 ADC 转换为离散的数字信号,MCU 才能进行处理和计算。本文基于 NXP IMX6ULL 处理器,从 ADC 原理、硬件资源、寄存器配置,到完整的驱动代码实现与滤波优化,拆解 ADC 开发的细节。
一、ADC 核心基础原理
1.1 什么是 ADC
ADC 的全称是 Analog-to-Digital Converter,即模拟 - 数字转换器,它的核心作用是将连续变化的模拟电压信号,转换为数字系统可以识别、处理的离散数字信号。
完整的物理量采集链路如下:
现实世界的温度、压力、光照等物理量,首先通过传感器转换为对应的模拟电压信号,再由 ADC 将模拟电压转换为二进制数字量,最终送入 MCU 等数字系统进行计算、显示、控制等操作。
1.2 ADC 核心参数详解
理解 ADC 的核心参数,是做好驱动开发和硬件选型的前提,这里结合工程实际讲解最关键的 4 个参数:
(1)分辨率
分辨率指 ADC 的转换位数,决定了 ADC 对模拟信号的细分能力,也是我们常说的 8 位、10 位、12 位、16 位 ADC。
- 对于 N 位 ADC,它会将整个量程划分为
2^N个最小刻度(量化等级) - 位数越高,量化等级越多,测量精度越高
- 本文使用的 IMX6ULL ADC 为 12 位分辨率,对应
2^12=4096个量化等级,在 3.3V 基准电压下,最小可识别电压约为3.3V/4096≈0.8mV
(2)基准电压(VREF)
基准电压是 ADC 转换的 "标尺",ADC 的所有转换结果都是相对于基准电压的比值。IMX6ULL 的 ADC 基准电压由ADC_VREFH引脚提供,本文中使用 3.3V 作为基准电压。
核心转换公式(12 位分辨率、3.3V 基准):
plaintext
实际电压(V) = (ADC采样值 / 4096.0) * 基准电压(3.3V)
这个公式是 ADC 采样值转实际电压的核心,后续代码中会反复用到。
(3)量程
量程指 ADC 能够正常测量的输入电压范围,通常由基准电压决定。本文中基准电压为 3.3V,因此 ADC 的量程为0~3.3V。
- 输入电压超过量程上限,会导致采样结果削顶失真,甚至损坏芯片(可以通过分压电阻解决)
- 输入电压远小于量程,会导致采样精度不足,需要先通过运放放大信号
(4)转换原理:逐次逼近型(SAR)ADC
IMX6ULL 内置的 ADC 为逐次逼近型(SAR)ADC,它是嵌入式领域最常用的 ADC 架构,兼顾了转换速度与精度 ------ 速度远快于双积分型 ADC,精度远高于 Flash 型 ADC。
SAR ADC 的核心工作逻辑是 "二分法权重比较",通过内部 DAC 生成参考电压,与待测电压逐位比较,最终得到量化结果,具体过程如下图所示:

以 8 位 ADC、5V 基准电压、待测电压 3.8V 为例,转换过程如下:
- 最高位权重为 2.5V,2.5V < 3.8V,该位记 1,累计值 2.5V
- 下一位权重 1.25V,2.5+1.25=3.75V < 3.8V,该位记 1,累计值 3.75V
- 下一位权重 0.625V,3.75+0.625=4.375V > 3.8V,该位记 0,累计值保持 3.75V
- 以此类推,逐位比较直到最低位,最终得到 8 位量化结果
11000010,对应十进制 194 - 代入公式计算实际电压:
194/256 *5V = 3.7890625V,与待测电压 3.8V 基本一致
对应的核心伪代码逻辑如下:
c
运行
unsigned int adc_valu = 0;
unsigned int N = 0;
unsigned int V0 = 基准电压;
for(int i=0; i<位数; i++){
V0 = V0 / 2;
if(N + V0 < 待测电压V1){
N += V0;
adc_valu = (adc_valu << 1) | 1;
}else{
adc_valu = (adc_valu << 1) | 0;
}
}
1.3 IMX6ULL ADC 硬件资源
基于 IMX6ULL 核心板与底板原理图,我们先明确硬件资源与引脚映射:
- ADC 模块:IMX6ULL 内置 2 个 ADC 控制器,本文使用 ADC1,支持 10 个模拟输入通道
- 通道映射:ADC1_IN1 通道对应 GPIO1_IO01 引脚,也是本文使用的采样通道
- 参考电压:ADC_VREFH 引脚接 3.3V,作为 ADC 转换的基准电压
- 引脚复用:GPIO1_IO01 的 ALT0 模式为 ADC1_IN1 模拟输入功能,无需额外上下拉配置
表格
| 信号名 | 功能描述 | 对应引脚 | 方向 |
|---|---|---|---|
| ADC1_IN1 | ADC1 通道 1 模拟输入 | GPIO1_IO01 | 输入 |
| ADC_VREFH | 基准电压高电平 | ADC_VREFH | 输入 |
二、IMX6ULL ADC 寄存器解析
本文的驱动基于寄存器直接开发,因此必须先明确每个核心寄存器的功能与配置逻辑。IMX6ULL ADC 的核心寄存器共 6 个,下面逐一拆解。
2.1 配置寄存器(ADCx_CFG)
CFG 寄存器是 ADC 的核心配置寄存器,用于设置分辨率、时钟源、采样时间、分频系数等核心参数,寄存器位定义如下:
表格
| 位段 | 名称 | 功能说明 | 本文配置值 |
|---|---|---|---|
| [3:2] | MODE | 分辨率选择:00=8 位,01=10 位,10=12 位,11 = 保留 | 10(12 位) |
| [1:0] | ADICLK | 时钟源选择:00=IPG 时钟,01=IPG/2,11 = 异步时钟 ADACK | 11(异步时钟) |
| [6:5] | ADIV | 时钟分频系数:00=1 分频,01=2 分频,10=4 分频,11=8 分频 | 00(1 分频) |
| [15:14] | AVGS | 硬件平均采样次数:00=4 次,01=8 次,10=16 次,11=32 次 | 00(关闭) |
| [12:11] | REFSEL | 参考电压源选择:00=ADC_VREFH | 00(外部基准) |
本文的 CFG 寄存器最终配置值:(2<<2) | (3<<0),即 12 位分辨率、异步 ADACK 时钟源。
2.2 通用控制寄存器(ADCx_GC)
GC 寄存器用于控制 ADC 的全局功能,包括模块使能、校准启动、连续转换、硬件平均等,核心位定义如下:
表格
| 位段 | 名称 | 功能说明 | 本文配置值 |
|---|---|---|---|
| [7] | CAL | 校准启动位:写 1 启动校准,校准完成后硬件自动清 0 | 启动校准置 1 |
| [0] | ADEN | ADC 模块使能位:写 1 开启 ADC 模块,0 关闭 | 1(使能) |
| [6] | ADCO | 连续转换使能:1 连续转换,0 单次转换 | 0(单次转换) |
| [5] | AVGE | 硬件平均使能:1 开启,0 关闭 | 0(关闭) |
2.3 通用状态寄存器(ADCx_GS)
GS 寄存器用于反映 ADC 的全局状态,核心是校准状态标志位:
表格
| 位段 | 名称 | 功能说明 |
|---|---|---|
| [1] | CALF | 校准失败标志位:1 = 校准失败,0 = 校准成功;写 1 可清零该位 |
2.4 通道控制寄存器(ADCx_HCn)
HC 寄存器用于选择 ADC 采样通道、开启转换完成中断,每次向 HCn 寄存器写入通道号,都会触发一次 ADC 转换,核心位定义如下:
表格
| 位段 | 名称 | 功能说明 | 本文配置值 |
|---|---|---|---|
| [4:0] | ADCH | 通道选择位:0~9 对应 ADC1_IN0~IN9,0x1F 关闭所有通道 | 1(通道 1) |
| [7] | AIEN | 转换完成中断使能:1 开启中断,0 关闭 | 0(查询模式) |
2.5 状态寄存器(ADCx_HS)
HS 寄存器用于反映 ADC 转换的完成状态,核心位如下:
表格
| 位段 | 名称 | 功能说明 |
|---|---|---|
| [0] | COCO0 | 转换完成标志位:1 = 转换完成,0 = 转换中;读取结果寄存器后自动清零 |
2.6 数据结果寄存器(ADCx_Rn)
Rn 寄存器用于存放 ADC 转换完成的最终结果,对于 12 位分辨率,有效数据为低 12 位[11:0],高 4 位无效,读取时需要通过& 0xFFF屏蔽无效位。
三、驱动代码逐行详细解析
本文的驱动代码分为 3 个文件:adc.h(头文件接口声明)、adc.c(核心驱动实现)、main.c(主函数测试逻辑),下面逐文件、逐函数拆解代码逻辑。
3.1 头文件 adc.h
头文件的核心作用是对外暴露驱动接口,实现模块化封装,屏蔽内部实现细节,代码如下:
c
运行
#ifndef __ADC_H__
#define __ADC_H__
// ADC初始化函数
extern void adc_init(void);
// 获取单次ADC原始采样值
extern unsigned short adc_get_value(void);
// 获取单次转换的实际电压值
extern float adc_get_voltage(void);
// 获取去极值滤波后的平均电压值
extern float adc_get_average_voltage(void);
#endif // !__ADC_H__
这里声明了 4 个核心接口,覆盖了从初始化、单次采样、电压转换到滤波优化的全流程,其他文件只需包含该头文件,即可调用 ADC 相关功能。
3.2 核心驱动实现 adc.c
adc.c是 ADC 驱动的核心,包含了校准、初始化、采样、电压转换、滤波 5 个核心函数,下面逐函数拆解。
(1)ADC 校准函数 adc_Calibration
SAR 型 ADC 由于芯片制造工艺的偏差,内部比较器、DAC 会存在固有偏移误差,必须通过校准消除,否则采样精度会严重下降。校准必须在 ADC 模块使能后、正式采样前执行。
c
运行
int adc_Calibration(void)
{
// 步骤1:写1清零校准失败标志位CALF
ADC1->GS |= (1 << 1);
// 步骤2:置位CAL位,启动ADC自动校准
ADC1->GC |= (1 << 7);
// 步骤3:循环等待校准完成,硬件会自动清零CAL位
while ((ADC1->GC & (1 << 7)) != 0);
// 步骤4:返回校准结果,CALF位为0表示校准成功,1表示失败
return ((ADC1->GS & (1 << 1)) == 0);
}
函数返回值为 1 表示校准成功,0 表示校准失败,初始化时会通过串口打印校准结果,方便调试。
(2)ADC 初始化函数 adc_init
初始化函数分为两大核心部分:引脚复用配置 + ADC 寄存器配置,是驱动正常运行的基础。
c
运行
void adc_init(void)
{
// ========== 第一部分:引脚复用与PAD属性配置 ==========
// 配置GPIO1_IO01引脚复用模式,SION=1开启软件输入使能
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO01_GPIO1_IO01, 1);
// 配置PAD属性:开启保持器,关闭上下拉,适配模拟输入场景
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));
// ========== 第二部分:ADC核心寄存器配置 ==========
// 先清零CFG寄存器,避免默认值干扰
ADC1->CFG = 0;
unsigned int t = ADC1->CFG;
t |= (2 << 2); // MODE位设为10,配置12位分辨率
t |= (3 << 0); // ADICLK位设为11,选择异步ADACK时钟源
ADC1->CFG = t;
// 清零GC寄存器,关闭默认开启的功能
ADC1->GC = 0;
ADC1->GC |= (1 << 0); // 置位ADEN位,开启ADC1模块
// 执行ADC校准,并通过串口打印校准结果
printf(adc_Calibration() ? "adc calibration success\n" : "adc calibration failed\n");
}
这里有两个关键细节:
- 模拟输入引脚无需配置上下拉,只需开启保持器,避免上下拉电阻对模拟信号的干扰
- 必须先开启 ADC 模块(置位 ADEN 位),再执行校准,否则校准会失败
(3)单次采样函数 adc_get_value
该函数用于触发一次 ADC 单次转换,等待转换完成后,返回 12 位原始采样值。
c
运行
unsigned short adc_get_value(void)
{
// 先关闭所有通道,再选择通道1,确保触发一次全新的转换
ADC1->HC[0] = 0x1F;
ADC1->HC[0] = 1;
// 循环等待转换完成,COCO0位置1表示转换完成
while ((ADC1->HS & (1 << 0)) == 0);
// 读取结果寄存器,屏蔽高4位,返回12位有效采样值
return (unsigned short)(ADC1->R[0] & 0xFFF);
}
核心逻辑:每次向 HC [0] 寄存器写入通道号,都会触发一次单次转换;通过查询 COCO0 标志位判断转换是否完成,确保读取到的是最新的转换结果。
(4)电压转换函数 adc_get_voltage
该函数基于转换公式,将 12 位原始采样值转换为实际电压值(单位:V)。
c
运行
float adc_get_voltage(void)
{
// 获取单次原始采样值
unsigned short adc_value = adc_get_value();
// 核心公式:(采样值/4096) * 基准电压3.3V,转换为实际电压
return (adc_value / 4096.0f) * 3.3;
}
这里必须使用4096.0f浮点数运算,避免整数除法导致的精度丢失;如果基准电压不是 3.3V,只需修改公式中的基准电压值即可。
(5)去极值平均滤波函数 adc_get_average_voltage
实际工程中,ADC 采样会受到电源纹波、电磁干扰、传感器噪声的影响,原始采样值会出现随机跳变,必须通过滤波算法消除干扰。
本文采用冒泡排序 + 掐头去尾去极值平均滤波,核心逻辑是:多次采样后排序,去掉最高和最低的极值,取中间稳定数据的平均值,能有效消除突发脉冲干扰,是工业场景最常用的滤波算法之一。
c
运行
float adc_get_average_voltage(void)
{
float sum = 0;
int i = 0;
int j = 0;
float temp = 0;
// 定义数组,存放1000次采样的电压值
float volt[1000] = {0};
// 步骤1:连续采样1000次,存入数组
for (i = 0; i < 1000; i++)
{
volt[i] = adc_get_voltage();
}
// 步骤2:冒泡排序,将1000个采样值从小到大排序
for(i = 0;i < 1000; i++)
{
for(j = 0; j < 1000 - i - 1; j++)
{
if(volt[j] > volt[j + 1])
{
temp = volt[j];
volt[j] = volt[j + 1];
volt[j + 1] = temp;
}
}
}
// 步骤3:去掉前20%和后20%的极值,累加中间60%的稳定数据
for (i = 200; i < 800; i++)
{
sum += volt[i];
}
// 步骤4:计算平均值并返回
return (sum / 600.0f);
}
算法细节说明:
- 采样次数为 1000 次,可根据实际需求调整,采样次数越多,滤波效果越好,但耗时越长
- 去掉前 200 个最小值和后 200 个最大值,消除了突发的尖峰脉冲干扰
- 取中间 600 个稳定数据的平均值,兼顾了采样精度与响应速度
3.3 主函数测试逻辑 main.c
主函数完成系统初始化后,在主循环中持续采集滤波后的电压值,并通过串口打印输出。
c
运行
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "uart.h"
#include "stdio.h"
#include "adc.h"
int main(void)
{
// 系统基础初始化
clock_init(); // 系统时钟配置
system_interrupt_init(); // 系统中断初始化
uart1_init(); // 串口1初始化,用于打印日志
adc_init(); // ADC初始化与校准
// 变量定义:volt存滤波后的电压,zheng存整数部分,xiaoshu存小数部分
volatile float volt = 0;
volatile int zheng = 0;
volatile int xiaoshu = 0;
// 主循环:持续采样并打印电压值
while (1)
{
// 获取去极值滤波后的平均电压
volt = adc_get_average_voltage();
// 拆分电压的整数部分和小数部分,保留3位小数
zheng = ((int )(volt * 1000)) / 1000;
xiaoshu = ((int)(volt * 1000)) % 1000;
// 串口打印电压值,格式为x.xxx V
printf("volt: %d.%03d\n", zheng, xiaoshu);
}
return 0;
}
这里有一个嵌入式开发的常用技巧:拆分整数与小数部分打印。很多嵌入式编译器的 printf 默认不支持浮点数打印,开启浮点数支持会大幅增加固件体积,因此将电压值放大 1000 倍,拆分出整数部分和 3 位小数部分,通过整型打印实现浮点数的显示效果,兼顾了代码体积与显示精度。
四、常见问题排查
4.1软件优化方向
本文的代码实现了基础功能,在实际工程中可根据需求做以下优化:
- 滤波算法优化:冒泡排序的时间复杂度为 O (n²),1000 次采样效率较低,可替换为中值滤波、滑动平均滤波、卡尔曼滤波,提升运行效率
- 中断模式改造:当前使用查询模式,等待转换完成会占用 CPU 资源,可改为中断模式,转换完成后触发中断,在中断服务函数中读取采样值,大幅提升 CPU 利用率
- 硬件平均功能:开启 CFG 寄存器的硬件平均功能,由 ADC 硬件自动完成多次采样平均,无需软件干预,减少 CPU 开销
- 多通道扫描采样:配置 ADC 的扫描模式,轮询采样多个通道,实现多路传感器数据的同步采集
4.3 常见问题排查
- ADC 校准失败
- 排查方向:ADC 模块是否提前使能、基准电压是否正常、ADC 时钟是否配置正确、引脚是否短路
- 采样值始终为 0
- 排查方向:引脚复用配置是否正确、通道号是否选择正确、ADC 模块是否正常使能、输入引脚是否有电压输入
- 采样值跳变严重
- 排查方向:是否执行了校准、硬件是否加了 RC 滤波、电源是否稳定、是否开启了滤波算法
- 采样值与实际电压偏差大
- 排查方向:基准电压是否准确、分压电阻精度是否达标、是否存在温漂影响、公式中的基准电压是否与硬件一致
五、总结
本文从 ADC 的核心原理出发,完整拆解了 IMX6ULL ADC 的硬件资源、寄存器配置、驱动代码实现,实现了一个可简单的高ADC 驱动。
ADC 是嵌入式开发中最基础也最核心的外设之一,掌握了 ADC 的开发,就可以对接光敏、热敏、压力、湿度等绝大多数模拟传感器,实现环境监测、工业控制、智能硬件等各类项目。本文的代码基于寄存器和 SDK 库,可直接移植到其他 IMX6ULL 的开发板中,也可作为其他 ARM 架构 MCU 的 ADC 开发参考。