IMX6ULL ADC 驱动开发解析:

在嵌入式开发中,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 为例,转换过程如下:

  1. 最高位权重为 2.5V,2.5V < 3.8V,该位记 1,累计值 2.5V
  2. 下一位权重 1.25V,2.5+1.25=3.75V < 3.8V,该位记 1,累计值 3.75V
  3. 下一位权重 0.625V,3.75+0.625=4.375V > 3.8V,该位记 0,累计值保持 3.75V
  4. 以此类推,逐位比较直到最低位,最终得到 8 位量化结果11000010,对应十进制 194
  5. 代入公式计算实际电压: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 核心板与底板原理图,我们先明确硬件资源与引脚映射:

  1. ADC 模块:IMX6ULL 内置 2 个 ADC 控制器,本文使用 ADC1,支持 10 个模拟输入通道
  2. 通道映射:ADC1_IN1 通道对应 GPIO1_IO01 引脚,也是本文使用的采样通道
  3. 参考电压:ADC_VREFH 引脚接 3.3V,作为 ADC 转换的基准电压
  4. 引脚复用: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");
}

这里有两个关键细节:

  1. 模拟输入引脚无需配置上下拉,只需开启保持器,避免上下拉电阻对模拟信号的干扰
  2. 必须先开启 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);
}

算法细节说明:

  1. 采样次数为 1000 次,可根据实际需求调整,采样次数越多,滤波效果越好,但耗时越长
  2. 去掉前 200 个最小值和后 200 个最大值,消除了突发的尖峰脉冲干扰
  3. 取中间 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软件优化方向

本文的代码实现了基础功能,在实际工程中可根据需求做以下优化:

  1. 滤波算法优化:冒泡排序的时间复杂度为 O (n²),1000 次采样效率较低,可替换为中值滤波、滑动平均滤波、卡尔曼滤波,提升运行效率
  2. 中断模式改造:当前使用查询模式,等待转换完成会占用 CPU 资源,可改为中断模式,转换完成后触发中断,在中断服务函数中读取采样值,大幅提升 CPU 利用率
  3. 硬件平均功能:开启 CFG 寄存器的硬件平均功能,由 ADC 硬件自动完成多次采样平均,无需软件干预,减少 CPU 开销
  4. 多通道扫描采样:配置 ADC 的扫描模式,轮询采样多个通道,实现多路传感器数据的同步采集

4.3 常见问题排查

  1. ADC 校准失败
    • 排查方向:ADC 模块是否提前使能、基准电压是否正常、ADC 时钟是否配置正确、引脚是否短路
  2. 采样值始终为 0
    • 排查方向:引脚复用配置是否正确、通道号是否选择正确、ADC 模块是否正常使能、输入引脚是否有电压输入
  3. 采样值跳变严重
    • 排查方向:是否执行了校准、硬件是否加了 RC 滤波、电源是否稳定、是否开启了滤波算法
  4. 采样值与实际电压偏差大
    • 排查方向:基准电压是否准确、分压电阻精度是否达标、是否存在温漂影响、公式中的基准电压是否与硬件一致

五、总结

本文从 ADC 的核心原理出发,完整拆解了 IMX6ULL ADC 的硬件资源、寄存器配置、驱动代码实现,实现了一个可简单的高ADC 驱动。

ADC 是嵌入式开发中最基础也最核心的外设之一,掌握了 ADC 的开发,就可以对接光敏、热敏、压力、湿度等绝大多数模拟传感器,实现环境监测、工业控制、智能硬件等各类项目。本文的代码基于寄存器和 SDK 库,可直接移植到其他 IMX6ULL 的开发板中,也可作为其他 ARM 架构 MCU 的 ADC 开发参考。

相关推荐
2023自学中2 小时前
正点原子 Linux 驱动开发:多点电容触摸屏实验,gt9147 触摸芯片
linux·驱动开发·嵌入式
UTP协同自动化测试2 小时前
智能家居中控屏测试:触摸屏操作 + I2C 读取传感器 + UART 与子设备通信 + GPIO 控制
功能测试·单片机·嵌入式硬件·测试工具·智能家居
【云轩】2 小时前
【拆解系列 一 】拆解手持式泡泡机
嵌入式硬件
charlie1145141912 小时前
嵌入式Linux驱动开发——模块参数与内核调试:让模块“活“起来的魔法
linux·驱动开发·学习·c
Hello World . .2 小时前
ARM裸机学习9——ADC模块详解与应用实践
arm开发·嵌入式硬件
电子科技圈2 小时前
SmartDV展示AI & HPC连接与存储IP解决方案,以解锁下一代算力芯片和节点的“速度密码”
网络·数据库·人工智能·嵌入式硬件·aigc·边缘计算
CET中电技术2 小时前
“事后维护”到“主动预防”排除故障,CET中电技术为电动机的安全运行保驾护航
单片机·嵌入式硬件
辰哥单片机设计3 小时前
STM32智能鞋柜(机智云)
stm32·单片机·嵌入式硬件
xuxie993 小时前
N18 RTC
单片机·嵌入式硬件·实时音视频