第十三章 LCD控制器
13.1 帧缓冲区(Frame Buffer)本质
scss
┌─────────────────────────────────────────┐
│ 物理内存(SDRAM) │
│ ┌─────────────────────────────────────┐ │
│ │ 帧缓冲区(Frame Buffer) │ │
│ │ 每个像素对应一个内存地址 │ │
│ │ │ │
│ │ 像素(0,0) 像素(1,0) 像素(2,0)... │ │
│ │ [0x30000000] [0x30000002] [...] │ │
│ │ │ │
│ │ 像素(0,1) 像素(1,1) 像素(2,1)... │ │
│ │ [...] [...] [...] │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ ↓ DMA自动传输 │
│ ┌─────────────────────────────────────┐ │
│ │ LCD控制器 │ │
│ │ 按扫描时序读出像素数据 → 显示屏 │ │ VCLK信号:每个时钟周期传输 1 个像素
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────┐
│ 帧缓冲区 = 内存中的一块区域 │
│ • 每个像素对应内存中的若干位 │
│ • LCD控制器通过DMA自动读取显示 │
│ • CPU只需修改内存,无需操作硬件 │
└─────────────────────────────────┘
核心理解 :显示的本质是内存到屏幕的DMA传输,CPU只需填内存,硬件自动刷新
像素格式:
| 格式 | 每像素位数 | 颜色表示 | 现代对应 |
|---|---|---|---|
| 8BPP | 8位 | 调色板索引(256色) | 已淘汰,理解原理 |
| 16BPP | 16位 | RGB565(5红6绿5蓝) | ✅ 仍常用(资源受限设备) |
| 24BPP | 24/32位 | RGB888/ARGB8888 | ✅ 现代主流 |
💡 关键公式 :
帧缓冲区大小 = 分辨率 × 每像素字节数
例:800×480 @ 16BPP = 800×480×2 = 768KB
13.2 显示时序

scss
一帧图像的扫描过程(Z字形):
VSYNC脉冲 ─┐
│ ↑ 垂直同步(帧开始)
▼ │
┌──┴────────────────────────┐
│ 上边框(VSPW+VBPD行无效数据) │
│ │
┌──┬──┬──┬──┬──┬──┬──┬──┐ │
HSYNC→ │ │ │ │ │ │ │ │ │ ... │ ← 有效显示区
脉冲 │ │ │ │ │ │ │ │ │ │ (LINEVAL+1行)
│ │ │ │ │ │ │ │ │ │
└──┴──┴──┴──┴──┴──┴──┴──┘ │
│ 左边框 有效像素 右边框 │
│ (HBPD) (HOZVAL+1) (HFPD) │
│ │
│ 下边框(VFPD行无效数据) │
└─────────────────────────────┘
关键参数(现代DRM驱动中叫"display timing"):
- VSYNC/HSYNC:同步脉冲宽度
- VBP/VFP:垂直前后肩(上下黑边)
- HBP/HFP:水平前后肩(左右黑边)
- CLK:像素时钟频率
13.2.1 一帧是怎么"扫"出来的?
ini
假设分辨率:800×480(宽×高)
🎬 垂直扫描过程(从上到下):
① VSYNC 脉冲(VSPW=2 行)
[无效][无效] ← 上方黑边(部分)
② 后肩等待(VBPD=10 行)
[无效]×10 ← 上方黑边(剩余)
③ 有效数据(LINEVAL=479 → 480 行)✅
[有效行 0] ← 屏幕第 1 行
[有效行 1] ← 屏幕第 2 行
...
[有效行 479]← 屏幕第 480 行
④ 前肩等待(VFPD=20 行)
[无效]×20 ← 下方黑边
⑤ 下一个 VSYNC 脉冲 → 回到①,开始新一帧
🎬 水平扫描过程(每行从左到右):
对每一行重复:
① HSYNC 脉冲(HSPW=4 像素)
[无效]×4 ← 左侧黑边(部分)
② 后肩等待(HBPD=40 像素)
[无效]×40 ← 左侧黑边(剩余)
③ 有效数据(HOZVAL=799 → 800 像素)✅
[像素 0][像素 1]...[像素 799] ← 屏幕显示
④ 前肩等待(HFPD=10 像素)
[无效]×10 ← 右侧黑边
⑤ 下一个 HSYNC → 回到①,开始下一行
13.2.2 水平方向(行)与 垂直方向(帧)的嵌套关系:
水平方向 (Horizontal Timing):刷出一行
当你看到屏幕上的一行文字时,背后发生了这些事:
- HSPW (同步脉冲): 刷子发出"咔哒"一声,准备开始新的一行。
- HBPD (后肩): 刷子挪到纸张边缘,还没开始动笔。
- HOZVAL (有效数据): 刷子疯狂输出,这才是你屏幕上的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 640 640 </math>640 或 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1024 1024 </math>1024 个像素。
- HFPD (前肩): 刷子到了纸张右边缘,停笔,准备下一行动作。
垂直方向 (Vertical Timing):刷出一屏
把上面的"行"看作一个整体。刷出一屏(一帧)的过程:
- VSPW (同步脉冲): 发出信号,准备刷全新的第一帧。
- VBPD (后肩): 刷子挪到屏幕顶端,等待几行的时间。
- LINEVAL (有效行): 刷子一行一行地刷,刷够 <math xmlns="http://www.w3.org/1998/Math/MathML"> 480 480 </math>480 行或 <math xmlns="http://www.w3.org/1998/Math/MathML"> 768 768 </math>768 行。
- VFPD (前肩): 刷到底部,等待一会儿,准备回到顶端刷下一帧。
13.2.3 为什么会有这些"无效"的参数(Porch)?
VSPW、VBPD、VFPD (垂直方向)和 HSPW、HBPD、HFPD (水平方向),统称为 "消隐期" 。
- 历史原因: 以前的电子束扫完一行回到左边(回扫)需要时间。
- 一句话 : "肩" = 同步信号和有效数据之间的"缓冲带" ,确保显示稳定。
13.2.4 时序参数
像素时钟频率计算
ini
VCLK = 水平总像素 × 垂直总行数 × 刷新率
例如:1024×768@60Hz
水平总像素 = HSPW + HBP + 1024 + HFP ≈ 1344(含消隐期)
垂直总行数 = VSPW + VBP + 768 + VFP ≈ 806(含消隐期)
VCLK = 1344 × 806 × 60 ≈ 65 MHz
现代MIPI DSI/LVDS接口速率更高:
4K@60Hz ≈ 594 MHz(需压缩或双通道)
帧率计算
| 参数 | 公式 | 说明 |
|---|---|---|
| 像素时钟 | VCLK = HCLK / [(CLKVAL+1)×2] | S3C2440特定 |
| 帧率 | FrameRate = VCLK / (总像素 × 总行数) | 通用 |
| 总像素 | (HSPW+1)+(HBPD+1)+(HOZVAL+1)+(HFPD+1) | 含消隐 |
| 总行数 | (VSPW+1)+(VBPD+1)+(LINEVAL+1)+(VFPD+1) | 含消隐 |
帧缓冲区大小计算
ini
帧缓冲区大小 = 宽度 × 高度 × 每像素字节数
例1:1920×1080, 32BPP(4字节)
大小 = 1920 × 1080 × 4 = 8,294,400 字节 ≈ 7.9 MB
例2:双缓冲(前台/后台切换)
总需内存 = 7.9 MB × 2 = 15.8 MB
设计原则:
- 先确定分辨率(HOZVAL+1, LINEVAL+1)
- 根据LCD手册设置最小要求的porch/sync值
- 计算所需VCLK = 目标帧率 × 总像素 × 总行数
- 反推CLKVAL,取整后验证实际帧率
- 微调porch使帧率精确到目标值(如60.00Hz)
13.3 # 帧缓冲区像素寻址
ini
// 书中代码(16BPP示例)
uint16_t *addr = (uint16_t*)fb_base_addr + (y * xsize + x);
*addr = color; // 写入像素
一句话定义:这是把屏幕二维坐标
(x, y)翻译成内存一维物理地址的"核心翻译器" 。所有 GUI 库(LVGL/Qt/FreeType)的底层渲染,最终都会落到这行数学计算上。
公式逐段拆解
| 部分 | 含义 | 生活类比 |
|---|---|---|
fb_base |
帧缓冲区起始地址(RAM 中的一块内存) | 🖼️ 画布的左上角起点 |
y * xsize |
跳过上面 y 整行的像素总数 |
📏 先往下走 y 行(每行 xsize 个像素) |
+ x |
在当前行里,向右偏移 x 个像素 |
➡️ 再往右走 x 步 |
* (bpp/8) |
把"第几个像素"转成"第几个字节" | 📦 16位色=2字节/像素,32位色=4字节/像素 |
求像素 (x=100, y=50) 的内存地址:
- 1.y * xsize = 50 * 800 = 40,000 ← 跳过前50行
- 2.+ x = 40,000 + 100 = 40,100 ← 在第50行向右100步
- 3.* (16/8) = 40,100 * 2 = 80,200 ← 转成字节偏移
- 4.+ fb_base = 0x30000000 + 80200 ← 最终物理地址
核心认知
| 认知 | 说明 | 工程意义 |
|---|---|---|
| 内存是线性的 | 屏幕没有"二维数组",只有一块连续 RAM | 理解为什么需要 y*xsize+x |
| bpp 决定步长 | 16位色跳2字节,32位色跳4字节 | 选错步长 = 画面错位/花屏 |
| GUI 库只是封装 | LVGL/Qt 的 lv_draw_rect() 底层还是循环算地址+写内存 |
调试显示问题直接看帧缓冲区 |
第十四章 ADC
14.1 定义
ADC是模拟到数字转换器(Analog-to-Digital Converter)缩写,主要用于将连续传输的模拟信号转换为数字信号,便于数字系统(如中央处理器CPU、微控制器MCU等)对传输信息进行快速处理和分析。
ini
┌─────────────────────────────────────────┐
│ 物理世界(模拟) │
│ 温度、压力、光照、电压... │
│ ↓ 传感器转换 │
│ 0~3.3V 连续变化的电压 │
│ ↓ ADC采样 │
│ ┌─────────────┐ │
│ │ 采样保持 │ ◄── 关键!冻结瞬间值 │
│ │ (S/H) │ │
│ └──────┬──────┘ │
│ ↓ 量化 │
│ 0~1023(10位)离散值 │
│ ↓ 数字输出 │
│ 二进制数据(0x000~0x3FF) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 数字世界(CPU处理) │
│ 电压值 = ADC值 × 参考电压 / 2^位数 │
│ 例:512 × 3.3V / 1024 = 1.65V │
└─────────────────────────────────────────┘
14.2 相关参数
| 参数 | 含义 | 现代对应 |
|---|---|---|
| 分辨率 | 位数(10/12/16/24位) | 决定精度 |
| 采样率 | KSPS/MSPS (必须 ≥ 信号最高频率的 2 倍(奈奎斯特定理)) | 决定能采多快 |
| 参考电压 | Vref(3.3V/5V/2.5V) | 决定量程 |
| INL/DNL | 积分/微分非线性 | 决定精度 |
| SNR | 信噪比 | 决定有效位数 |
量化误差
量化误差是 ADC 用"有限个数字台阶"去逼近"连续模拟电压"时,必然产生的"舍入差值"。
它不是故障,而是数字化的物理定律
scss
ADC 同理:
真实电压:1.653 V
10位ADC只能输出:1.651 V (对应512) 或 1.654 V (对应513)
误差:-0.002 V 或 +0.001 V
- LSB (Least Significant Bit) :ADC 能分辨的最小电压台阶
LSB = Vref / 2^N(N 为 ADC 位数)- 量化误差永远在
[-0.5 LSB, +0.5 LSB]之间
| ADC位数 | 台阶数 (2^N) | LSB 大小 | 最大量化误差 |
|---|---|---|---|
| 10位 | 1024 | 3.3V / 1024 ≈ 3.22 mV | ±1.61 mV |
| 12位 | 4096 | 3.3V / 4096 ≈ 0.81 mV | ±0.40 mV |
| 16位 | 65536 | 3.3V / 65536 ≈ 0.05 mV | ±0.025 mV |
实际开发中如何应对?
| 方法 | 原理 | 适用场景 | 代码/硬件示例 |
|---|---|---|---|
| ① 提高分辨率 | 直接缩小 LSB | 对精度要求高的传感器(如称重、医疗) | 换 24bit ADC(如 HX711) |
| ② 软件滤波 | 平均/中值/卡尔曼打散随机误差 | 常规物联网设备(温湿度、电池电压) | avg = (raw1+raw2+raw3+raw4)/4 |
| ③ 过采样+抽取 | 以 N 倍速率采样,求和后右移 | 想用软件"骗"出更高精度 | 16倍过采样可等效+2bit精度 |
| ④ 加抖动 (Dithering) | 注入微小噪声打散量化台阶 | 音频/高精度测量防"死区" | 硬件加白噪声 / 软件加随机数 |
✅ 嵌入式最常用组合 :
中值滤波(去毛刺) + 滑动平均(压误差) + 合理选ADC位数
14.3 ADC 通道复用与采样率缩水
ADC 通道复用
本质: 芯片为了省面积、降功耗、控成本,只做了 1 个 ADC 核心,前面加了个"电子开关"轮流接通多个引脚。总采样时间固定,分的人多了,每人分到的次数自然变少。
大多数芯片其实内部只有 1 个 ADC 转换器,但有 8~16 个 引脚,它们是轮询工作的。
结构示意
python
引脚 AIN0 ──┐
引脚 AIN1 ──┤
... ├─→ 多路选择器 (MUX) ──→ 唯一的 ADC 核心 ──→ 数字结果
引脚 AIN7 ──┘
| 模块 | 作用 | 为什么这样设计 |
|---|---|---|
| MUX(多路开关) | 用 MOSFET 电子开关轮流接通指定引脚 | 节省芯片面积,8 引脚只需 1 套 ADC |
| ADC 核心 | 执行真正的采样+量化+编码 | 高精度 ADC 占面积大、耗电高、成本高 |
| 切换稳定时间 | 开关切换后,内部电容需充电到稳定电压 | 物理定律,无法消除(通常几百 ns~几 μs) |
采样率"缩水"
scss
实际每通道采样率 ≈ 总采样率 ÷ (通道数 × 切换开销系数)
举例:
- 标称最大:
500 KSPS(总) - 单通道使用:≈
450~500 KSPS - 8 通道轮询:
500K ÷ 8 ≈ 62.5K(实际约40~50K,扣除切换时间)
第十五章移植U-Boot
系统上电之后,需要一段程序来进行初始化:关闭 WATCHDOG、改变系统时钟、初始化存储控制器、将更多的代码复制到内存中等。如果它 能将操作系统内核复制到内存中运行,无论从本地(比如Flash)还是从远端(比如通过网络), 就称这段程序为Bootloader。
可以增强Bootloader 的功能,比如增加网络功能、从 PC 上通过串口或网络下载文件、 烧写文件、将Flash上压缩的文件解压后再运行等,这就是一个功能更为强大的Bootloader, 也称为Monitor。
Bootloader 的三个使命:
- 硬件初始化: 关看门狗、调时钟、初始化内存控制器(这是最关键的,没有内存,C 语言的栈都跑不起来)。
- 加载内核: 把压缩的 Linux 内核镜像从非易失性存储(NAND Flash/SD卡)拷贝到内存中。
- 启动内核: 设置好启动参数(命令号),然后把 CPU 控制权交给 Linux。
15.1 Bootloader 的「两阶段启动模型」
Stage 1(汇编):纯硬件初始化
• 关看门狗、关中断、设 CPU 模式
• 初始化 SDRAM(让内存可用)
• 设置栈指针(SP)
• 把 Stage 2 代码从 Flash 拷贝到 RAM
• 跳转到 Stage 2 的 C 入口
Stage 2(C语言):复杂逻辑与系统交接
• 初始化串口/网络/Flash 驱动
• 检测内存映射
• 加载 Linux 内核 & 根文件系统到 RAM
• 准备启动参数
• 跳转到内核入口
✅ 现代意义:现代 U-Boot 仍保留此架构,只是 Stage 1 极短,大部分工作由 SPL(Secondary Program Loader)和 U-Boot Proper 分担。
15.2 U-Boot分析与移植
15.2.1 常用Bootloader介绍
现在Bootloader种类繁多,它们各有特点,下面列出Linux的开放源代码的Bootloader及其支持的体 系架构,如表15.1所示。

15.2.2 U-Boot
「U-Boot 不是用来从头写的,是用来配置、裁剪和调试的。」
在 2026 年,我们的开发哲学是:基于成熟单板进行"差异化配置"。
1. 配置化思维 (Defconfigs)
思想: 现代 U-Boot 拥有数千个单板的支持。不需要从头写,而是找到一个"相似的模板"。
- 掌握点: 学习如何使用
make menuconfig这种图形化工具,而不是去改.h文件。会看到成百上千个功能开关(比如要不要开启网络功能、要不要支持 USB)。
2. 设备树 (Device Tree - DTS)
- 思想: 过去硬件信息写死在 C 代码里;现在硬件信息写在一个类似 JSON 的文本文件里(
.dts)。 - 掌握点: 即使是 U-Boot,现在也广泛使用设备树来描述硬件。这样改硬件时,不需要重新编译整个 U-Boot,只需要改这个文本文件。
15.2.3 U-Boot 如何把 Linux 内核"叫醒"?
手册中用 tag 结构体(ATAGs)传参,现代已全面替换为设备树(FDT) ,但交互逻辑不变:
| 步骤 | 手册做法(旧) | 现代做法(新) | 关键点 |
|---|---|---|---|
| 加载内核 | nboot/tftp 读到 RAM |
同左,支持 FIT 镜像打包 | 知道内核在 RAM 的哪个地址 |
| 传递参数 | ATAG_CORE/ATAG_MEM/ATAG_CMDLINE |
设备树 Blob (bootm/bootz 自动加载) |
理解 console=ttySAC0 root=/dev/mmcblk0p2 怎么来的 |
| 跳转内核 | theKernel(0, mach_id, atags_addr) |
bootm/booti 自动处理寄存器设置 |
知道 R0=0, R1=板级ID/0, R2=DTB地址 的约定 |
15.2.4 U-Boot 命令行与环境变量
ruby
# ① 网络下载(替代串口,速度快100倍)
tftp 0x30000000 uImage
nfs 0x30000000 192.168.1.10:/rootfs.img
# ② 烧写 Flash
nand erase 0x0 0x400000
nand write 0x30000000 0x0 ${filesize}
# ③ 自动启动配置(产品发布用)
setenv bootcmd 'nand read 0x30000000 0x0 0x400000; bootm 0x30000000'
setenv bootargs 'console=ttySAC0,115200 root=/dev/mtdblock2 rootfstype=yaffs2'
saveenv
# ④ 调试神命令
md 0x30000000 10 # 查内存
mw 0x30000000 0x12 3 # 改内存
go 0x30000000 # 跑裸机程序
15.2.5 编译与配置逻辑
现代 U-Boot 已改用 Kbuild/Kconfig(和 Linux 内核一致)
scss
配置阶段:选择板子 → 生成 .config → 决定编译哪些驱动
编译阶段:链接脚本(.lds) 决定代码在 Flash/RAM 的布局
输出阶段:生成 u-boot.bin (烧录用) / u-boot.imx (含头校验)
15.3 现代 U-Boot 启动流程
上电 → Boot ROM → SPL → U-Boot → Linux → 用户程序
① 上电瞬间:硬件自动动作
markdown
CPU 复位 → 从固定地址 0x00000000 取第一条指令
↓
这个地址映射到芯片内部的 Boot ROM(厂商固化,不可改)
✅ Boot ROM 代码是 NXP 写的,只需要知道它"会找启动设备"。
② Boot ROM:找"第一级引导"
arduino
🔍 Boot ROM 做的事:
1. 检测启动引脚(BOOT_MODE)→ 决定从 SD/eMMC/USB/NAND 启动
2. 读取设备前 4KB(IVT + DCD + SPL)
3. 校验 CRC → 加载 SPL 到内部 SRAM(128KB)
4. 跳转到 SPL 入口
📦 SPL 是什么?
• 全称:Secondary Program Loader
• 大小:< 128KB(必须塞进芯片内部小内存)
• 任务:初始化 DDR + 加载完整 U-Boot
③ SPL:初始化内存 + 加载 U-Boot
arduino
① 初始化 DDR 控制器(让 512MB 内存可用)
② 从 eMMC/SD 读 U-Boot.bin 到 DDR 0x87800000
③ 跳转到 U-Boot 入口
④ U-Boot Proper:我们主要配置的部分
markdown
🎯 U-Boot 的核心任务:
1. 初始化串口 → 让你能看到打印
2. 初始化网络/存储 → 能下载、能读写
3. 解析设备树 → 知道板子上有哪些硬件
4. 加载 Linux 内核 + 设备树 + 启动参数
5. 跳转到 Linux 入口
实际开发中的 U-Boot 操作
yaml
# ① 串口连接开发板(SecureCRT / minicom)
# 波特率:115200,上电后看到:
U-Boot 2023.04 (Apr 11 2024 - 10:20:30 +0800)
CPU: Freescale i.MX6ULL rev1.1 900 MHz
DRAM: 512 MiB
MMC: FSL_SDHC: 0, FSL_SDHC: 1
In: serial
Out: serial
Err: serial
Net: eth0: ethernet@2188000
Hit any key to stop autoboot: 2 1 0 ← 按任意键中断自动启动
=> # 出现这个提示符,说明进入 U-Boot 命令行!
常用 U-Boot 命令(现代版)
ruby
# 🔍 查看环境变量(启动参数都存在这)
=> printenv
# 📋 关键变量解释:
bootargs=console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait
# ↑串口控制台 ↑根文件系统在 eMMC 分区 2
bootcmd=run findfdt; load mmc 1:1 ${fdt_addr_r} ${fdtfile}; \
load mmc 1:1 ${kernel_addr_r} zImage; \
bootz ${kernel_addr_r} - ${fdt_addr_r}
# ↑加载设备树 ↑加载内核 ↑启动内核
# 🌐 网络下载内核(开发时常用)
=> setenv serverip 192.168.1.100 # 主机 IP
=> setenv ipaddr 192.168.1.50 # 开发板 IP
=> tftp ${kernel_addr_r} zImage # 下载内核到内存
=> tftp ${fdt_addr_r} imx6ull-14x14-evk.dtb # 下载设备树
=> bootz ${kernel_addr_r} - ${fdt_addr_r} # 启动
# 💾 烧写到 eMMC(产品发布用)
=> mmc dev 1 # 选择 eMMC
=> mmc write ${kernel_addr_r} 0x400 0x2000 # 烧内核到偏移 0x400
# 🔧 修改启动参数(比如改根文件系统)
=> setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p3 rootwait'
=> saveenv # ⚠️ 必须 saveenv 否则重启失效!
⑤ 跳转到 Linux
scss
// U-Boot 启动内核的核心代码(简化)
void do_bootz(cmd_tbl_t *cmdtp, ...) {
// ① 准备启动参数(设备树 + 命令行)
setup_kernel_args(bd, bootargs);
// ② 关闭缓存、中断,切换到 SVC 模式
prepare_for_linux();
// ③ 跳转到内核入口(0x80008000 是 ARM Linux 约定)
void (*kernel_entry)(int, char**, struct tag*) =
(void *)KERNEL_RAM_BASE;
kernel_entry(0, NULL, (struct tag*)bd->bi_boot_params);
// ↑ ↑ ↑
// R0=0 R1=0 R2=设备树地址(现代用设备树,ATAGs 已废弃)
}
⑥ Linux Kernel 启动
markdown
🐧 Linux 启动后:
1. 解压内核(zImage → Image)
2. 初始化内存管理、进程调度
3. 挂载根文件系统(/dev/mmcblk1p2)
4. 执行 /sbin/init → 启动用户程序
📺 你看到的:
[ 0.000000] Linux version 6.1.55 ...
[ 1.234567] mmc1: new high speed SDHC card at address 1234
[ 2.345678] EXT4-fs (mmcblk1p2): mounted filesystem
[ 3.456789] systemd[1]: Started Update UTMP about System Boot/Shutdown.
# 最后出现登录提示:
imx6ull-evk login: