正点原子 I.MX6ULL MINI 开发板学习笔记:光敏电阻 ADC 采集

分享
I.MX6ULL ADC 模块框图
I.MX6ULL ADC1 模块内部结构

这篇笔记记录在正点原子 I.MX6ULL MINI 开发板上完成的一次小实验——光敏电阻 ADC 采集。内容涉及采样、量化、编码、逐次逼近法等知识点,尽量用图文结合的方式讲清楚。

1. 什么是 ADC?

ADC(Analog-to-Digital Converter,模数转换器)的作用就是把连续的模拟信号转换成离散的数字信号,让 CPU 能够读取和处理。

举个例子:光敏电阻两端的电压会随着光线强弱连续变化,这个电压是模拟量;而 CPU 只认识 0 和 1,所以我们需要 ADC 把这个电压变成一串数字,比如 0~4095 之间的整数。

ADC 转换四步骤
ADC 把模拟信号变成数字信号要经历的四个步骤

2. 我所使用的 ADC 分辨率是多少?

I.MX6ULL 片上集成的 ADC 是 12 位分辨率的。也就是说,转换结果用 12 个二进制位来表示,范围是:

0 ~ 2^12 - 1 = 0 ~ 4095

如果基准电压是 3.3V,那么理论上每一个数字码对应的电压步长(LSB)约为:

LSB = 3.3V / 4095 ≈ 0.806 mV

换句话说,ADC 能分辨的最小电压变化大约是 0.8 毫伏,对光敏电阻这种变化缓慢的信号来说完全够用了。

3. ADC 转换需要经过哪几步?(ADC 工作原理)

ADC 并不是“咔嚓”一下就把电压变成数字的,它内部要分四步走:

  1. 采样(Sample):在某一瞬间把连续的模拟信号“截取”下来,完成时间上的离散化。
  2. 保持(Hold):把采样得到的电压值保持住,让后面的电路有足够时间处理,避免转换过程中输入信号变化。
  3. 量化(Quantize):把保持住的模拟电压映射到离散的数字等级上。12 位 ADC 就有 4096 个等级。
  4. 编码(Encode):把量化后的等级编码成二进制数输出,比如 1010 0110 1100。
ADC 转换四步骤
采样 → 保持 → 量化 → 编码

4. 什么是逐次逼近法?

I.MX6ULL 的 ADC 采用的是逐次逼近型(SAR,Successive Approximation Register)架构。它的核心思想就像“猜数字游戏”里的二分查找:

  • 第一次先猜最大值的一半(12 位就是 2048),把 DAC 输出与输入电压比较;
  • 如果 DAC 输出比输入大,就把这一位清 0;如果小,就置 1;
  • 接着从最高位(MSB)到最低位(LSB)一位一位地确定;
  • 12 位分辨率只需要 12 次比较,就能拿到最终转换结果。
逐次逼近型 ADC 工作原理
SAR + DAC + 比较器 协同完成逐次逼近

这种结构在速度和功耗之间取得了很好的平衡,也是嵌入式 SoC 中最常见的 ADC 类型。

5. SoC 上 ADC 的基准电压是多少 V?(IMX6ULL)

I.MX6ULL 的 ADC 基准电压由 VREFH 和 VREFL 决定。在正点原子 MINI 开发板上,VREFH 通常接 3.3V,VREFL 接地(0V),所以实际基准电压为:

Vref = VREFH - VREFL = 3.3V - 0V = 3.3V

因此数字码和输入电压的换算关系是:

Vin = raw * 3.3 / 4095

6. 光敏电阻 ADC 采集实验

6.1 硬件连接

光敏电阻和一个固定电阻组成一个简单的分压电路:

光敏电阻分压电路
光强越大,光敏电阻 RG 越小,ADC 输入电压越低

在正点原子 I.MX6ULL MINI 开发板上,光敏电阻的输出接到了 ADC1_IN1 通道(对应引脚 GPIO1_IO01)。这一阶段还没有跑 Linux,直接通过配置寄存器来驱动 ADC。

6.2 真实可用的 ADC 驱动代码

下面是实际工程 /home/zhao/code/code_2026.7.27ADC/adc 中验证过的代码,只保留了 ADC 相关核心部分。

adc.h

#ifndef _ADC_H_
#define _ADC_H_

extern void adc1_channel1_init(void);
extern unsigned short adc1_get_origin_value(void);
extern float adc1_get_value(void);

#endif

adc.c

#include "adc.h"
#include "stdio.h"
#include "MCIMX6Y2.h"
#include "core_ca7.h"
#include "fsl_iomuxc.h"

/* ADC 自校准:置位 CAL,等待完成,检查 GS[CALF] */
int adc1_calibration(void)
{
    ADC1->GC |= (1 << 7);
    while ((ADC1->GC & (1 << 7)) != 0);
    if ((ADC1->GS & (1 << 1)) == 0)
        return 1;   /* 校准成功 */
    else
        return 0;   /* 校准失败 */
}

/* ADC1 通道 1 初始化 */
void adc1_channel1_init(void)
{
    /* 1. GPIO1_IO01 复用为 ADC1_IN1 */
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO01_GPIO1_IO01, 0);

    /* 2. 配置引脚电气属性 */
    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. 配置 ADC:软件触发、12-bit 模式 */
    ADC1->HC[0] &= ~(1 << 7);
    ADC1->CFG = 0;
    ADC1->CFG |= (2 << 2) | (3 << 0);
    ADC1->GC = 0;
    ADC1->GC |= (1 << 0);

    /* 4. 校准 */
    printf(adc1_calibration() ?
           "calibration completed normally!\n" :
           "calibration completed failed!\n");
}

/* 读取 ADC 原始值(12-bit) */
unsigned short adc1_get_origin_value(void)
{
    /* 1. 启动 ADC 转换:先清通道号,再写通道 1 */
    ADC1->HC[0] = 0;
    ADC1->HC[0] = 1;

    /* 2. 等待转换完成:HS[COCO0] 置 1 */
    while ((ADC1->HS & (1 << 0)) == 0);

    /* 3. 读取结果寄存器低 12 位 */
    return ADC1->R[0] & 0xFFF;
}

/* 原始值换算成电压 */
float adc1_get_value(void)
{
    return adc1_get_origin_value() / 4095.0 * 3.3;
}

main.c 中的调用

#include "adc.h"

int main(void)
{
    /* 板级启动代码已初始化系统时钟、中断、GPIO、UART 等 */
    clock_init();
    system_interrupt_init();
    led_init();
    beep_init();
    gpt_init();
    uart_init();
    i2c_init();

    /* 初始化 ADC1 通道 1 */
    adc1_channel1_init();

    led_off();
    beep_off();

    while (1)
    {
        /* 读取并打印电压值 */
        float f = adc1_get_value();
        int k = f * 1000;
        int m = k / 1000;
        int n = k % 1000;
        printf("voltage:%d.%03d\n", m, n);
    }

    return 0;
}

6.3 关键点说明

  • 板级初始化:clock_init()、system_interrupt_init()、uart_init() 等由启动代码完成,为 ADC 提供时钟和调试输出。
  • 引脚复用:GPIO1_IO01 通过 IOMUXC_SetPinMux 复用为 ADC1_IN1,再用 IOMUXC_SetPinConfig 配置上拉/保持等电气属性。注意代码里 PKE=1 打开了 Keeper,参考手册建议 ADC 输入引脚关闭 Keeper/上下拉(PKE=0),以免对采样引入误差。
  • ADC 配置:ADC1->CFG 配置时钟和模式;ADC1->GC 的 bit0 是 ADACKEN(异步时钟输出使能),不是 ADC 总使能位——该模块没有单独的总使能,往 HC[0] 写通道号就会启动转换。
  • 校准:启动转换前必须执行 adc1_calibration(),校准失败会影响精度。
  • 启动转换:往 HC[0] 写通道号即可触发一次转换,这里是通道 1。
  • 轮询等待:转换完成时 HS[COCO0] 置 1,然后读 R[0] 的低 12 位。
  • 电压换算:Vin = raw / 4095.0 * 3.3,对应基准电压 3.3V、12-bit 分辨率。

6.4 实验现象

下面是一段真实采集的曲线:0~10 点室内光下电压约 1.8V;10~15 点用手遮住光敏电阻,光照变弱、RG 变大,电压逐渐升到约 2.6V;30~35 点用手电筒照射,光照变强、RG 变小,电压迅速降到约 0.9V;45~50 点再次遮挡,电压又回升。实际数据带有噪声和抖动,不会是那种理想的光滑正弦波。

真实光照变化下的 ADC 采样曲线
遮挡时电压升高、强光照射时电压降低,且曲线带有自然噪声

总结

问题答案
什么是 ADC?把模拟信号转换成数字信号的电路/模块
I.MX6ULL ADC 分辨率12 位,输出范围 0~4095
ADC 转换步骤采样 → 保持 → 量化 → 编码
什么是逐次逼近法从 MSB 到 LSB 逐位二分比较,快速得到数字码
IMX6ULL ADC 基准电压3.3V(VREFH=3.3V,VREFL=0V)

这次实验让我对 ADC 的寄存器操作有了更直观的认识。裸机阶段虽然要手动配置 ADC 引脚、控制寄存器并做校准,但板级启动代码已经帮忙把系统时钟、中断、UART 等基础设施搭好,所以能把精力放在理解 ADC 本身。后面学到 Linux 驱动时,再把这些寄存器操作封装成字符设备或 sysfs 节点,体会会更深。

继续阅读

嵌入式开发面试知识手册

嵌入式开发工程师面试知识手册 适用对象:零基础起步、目标是通过嵌入式软件/驱动/BSP 方向面试的读者 编写依据:赵哲简历全部知识点 + 智能车 AGV 控制平台完整项目代码(i.MX6ULL Linux 端 + STM32F407 固件)+ Linux 驱动练习代码包 + embedded-agent 嵌入式开发智能体仓库 + 真实面试手撕代码与追问记录 使用方法:每篇按"通俗解释 → 原理细节 → 命令/API → 代码实例 → 项目结合 → 记忆口诀 → 面试问答"组织;口诀用于快速记忆,问答用于自测。第9篇是讲项目的演练场,第10篇是手撕代码实战题库。 总目录 * 第1篇 C语言与数据结构 * 1.1 编译链接全过程 * 1.2 数据类型与内存

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:Linux 驱动架构详解——编译、树内树外与运行时效果

这是 Linux 驱动小系列的总结篇。前三篇把 LED 驱动从内核模块写到 misc 设备,属于“会用”;这篇把背后的架构讲透:驱动到底怎么编译、内核树内和树外两种方式的区别、以及驱动里每个操作在系统跑起来之后到底产生了什么效果。内容整理自我的学习笔记(note.pdf)和实际工程 code/2026/20260803。 1. 驱动代码到底怎么编译的? 不管哪种方式,最终都靠内核的 kbuild 系统。核心就三个文件的关系: * Kconfig:定义 menuconfig 里的选项(如 CONFIG_MYMODULE),三态 y/m/n——y 编进内核,m 编成模块,n 不编; * .config:menuconfig 的选择结果,保存在源码顶层; * Makefile:

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:misc 设备——字符设备驱动的快捷方式

上两篇分别用手动 mknod 和 class_create+device_create 注册字符设备,调用链一长串。这篇用 misc(混杂)设备 一步搞定。代码来自实际工程 code/2026/20260803/05_misc_led,已验证可用。 1. 完成什么实验? 用 misc_register 重写 LED 驱动:一步完成设备号分配、字符设备注册、/dev/myled0 节点创建,同时保留上一篇的 sysfs 属性 led_status。 2. 什么是 misc 设备? misc(混杂)设备是 Linux 给“不好归类的小字符设备”

作者:azhezhezhe