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

这篇笔记记录在正点原子 I.MX6ULL MINI 开发板上完成的一次小实验——光敏电阻 ADC 采集。内容涉及采样、量化、编码、逐次逼近法等知识点,尽量用图文结合的方式讲清楚。
1. 什么是 ADC?
ADC(Analog-to-Digital Converter,模数转换器)的作用就是把连续的模拟信号转换成离散的数字信号,让 CPU 能够读取和处理。
举个例子:光敏电阻两端的电压会随着光线强弱连续变化,这个电压是模拟量;而 CPU 只认识 0 和 1,所以我们需要 ADC 把这个电压变成一串数字,比如 0~4095 之间的整数。

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 并不是“咔嚓”一下就把电压变成数字的,它内部要分四步走:
- 采样(Sample):在某一瞬间把连续的模拟信号“截取”下来,完成时间上的离散化。
- 保持(Hold):把采样得到的电压值保持住,让后面的电路有足够时间处理,避免转换过程中输入信号变化。
- 量化(Quantize):把保持住的模拟电压映射到离散的数字等级上。12 位 ADC 就有 4096 个等级。
- 编码(Encode):把量化后的等级编码成二进制数输出,比如 1010 0110 1100。

4. 什么是逐次逼近法?
I.MX6ULL 的 ADC 采用的是逐次逼近型(SAR,Successive Approximation Register)架构。它的核心思想就像“猜数字游戏”里的二分查找:
- 第一次先猜最大值的一半(12 位就是 2048),把 DAC 输出与输入电压比较;
- 如果 DAC 输出比输入大,就把这一位清 0;如果小,就置 1;
- 接着从最高位(MSB)到最低位(LSB)一位一位地确定;
- 12 位分辨率只需要 12 次比较,就能拿到最终转换结果。

这种结构在速度和功耗之间取得了很好的平衡,也是嵌入式 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 / 40956. 光敏电阻 ADC 采集实验
6.1 硬件连接
光敏电阻和一个固定电阻组成一个简单的分压电路:

在正点原子 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? | 把模拟信号转换成数字信号的电路/模块 |
| I.MX6ULL ADC 分辨率 | 12 位,输出范围 0~4095 |
| ADC 转换步骤 | 采样 → 保持 → 量化 → 编码 |
| 什么是逐次逼近法 | 从 MSB 到 LSB 逐位二分比较,快速得到数字码 |
| IMX6ULL ADC 基准电压 | 3.3V(VREFH=3.3V,VREFL=0V) |
这次实验让我对 ADC 的寄存器操作有了更直观的认识。裸机阶段虽然要手动配置 ADC 引脚、控制寄存器并做校准,但板级启动代码已经帮忙把系统时钟、中断、UART 等基础设施搭好,所以能把精力放在理解 ADC 本身。后面学到 Linux 驱动时,再把这些寄存器操作封装成字符设备或 sysfs 节点,体会会更深。