修改说明:本版本根据反馈补充了“指令/命令解释层”“知识点普及层”和“五星评价体系增强版”,重点增强 ARM 指令、GCC、GPIO、Makefile 与 Linux 驱动部分。

嵌入式 考点总结

覆盖实验:实验1 搭建开发环境 | 实验2 GPIO与ARM裸机开发 | 实验3 中断 | 实验4 Uboot与Linux系统 | 实验5 设备驱动
覆盖课件:CH1 嵌入式系统基础 | CH2 ARM硬件平台 | CH3 交叉开发环境 | CH4 Linux驱动原理 | CH5 蜂鸣器/光传感器 | CH6 温湿度传感器 | CH7 炫彩LED/独立按键
参考资料:LPKT030驱动开发指南 | RK3399 Datasheet | RK3399 TRM Part1/Part2


⭐ 五星评级说明【补强版五星评价体系】

后文所有星级均采用“只显示实心星”的五星评级系统:星越多,考试优先级越高。
但星级不只表示“重要不重要”,还表示要学到什么程度、可能怎么考、复习时要产出什么

星级 定位 典型题型 你要做到什么程度 复习产出
背景了解 选择 / 判断 知道名词含义,不需要展开 看懂即可
⭐⭐ 基础考点 填空 / 选择 / 简答小问 能说出定义、分类、用途 1~2句话解释
⭐⭐⭐ 高频考点 简答 / 计算 / 代码分析 能解释原理,会算地址、权限、位掩码,会判断命令作用 表格 + 例题
⭐⭐⭐⭐ 重点考点 流程题 / 程序分析题 能按步骤写完整流程,能解释每一步为什么做 流程模板 + 易错点
⭐⭐⭐⭐⭐ 核心必考 综合题 / 代码题 / 驱动模板题 能闭卷写模板,能从现象定位错误,能把多个知识点串起来 必背模板 + 手写代码

快速判断法:

问法 通常星级
只问“是什么” ⭐⭐
问“有什么特点 / 有哪些分类” ⭐⭐~⭐⭐⭐
问“为什么这样做 / 为什么不能省略” ⭐⭐⭐
问“怎么算地址 / 权限 / 位掩码” ⭐⭐⭐
问“写完整流程 / 分析一段程序” ⭐⭐⭐⭐
问“闭卷写模板 / 写驱动 / 综合定位错误” ⭐⭐⭐⭐⭐

🧩 指令/命令解释模板【新增】指令/命令解释模板

后文遇到 ARM 指令、Shell 命令、GCC 命令、Makefile 规则、Linux 驱动 API 时,建议不要只背“功能”,而要按下面 5 个问题理解:

项目 说明
它是什么 属于哪一类指令 / 命令 / API
它做什么 执行后 CPU、文件、寄存器或系统状态发生什么变化
为什么要用它 当前场景不用它会出现什么问题
考试怎么问 选择题、填空题、代码分析题还是综合题
易错点 最容易混淆或写错的位置

示例:

1
BL main
项目 解释
它是什么 ARM 跳转指令,Branch with Link
它做什么 跳转到 main,同时把返回地址保存到 LR
为什么要用它 调用函数时必须记住函数执行完后回到哪里
考试怎么问 区分 BBL;解释 LR 的作用
易错点 B 不保存返回地址,不能当普通函数调用用

📗 CH1 嵌入式系统基础

1. 嵌入式系统定义 ⭐⭐

IEEE定义: 嵌入式系统是”用于控制、监视或者辅助操作机器和设备的装置”(devices used to control, monitor, or assist the operation of equipment, machinery or plants)。

通俗定义: 嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可裁剪,适用于应用系统对功能、可靠性、成本、体积功耗有严格要求的专用计算机系统。

嵌入式系统本身是一个相对模糊的定义。一个手持的MP3和一个PC104工控计算机都可以认为是嵌入式系统。


2. 嵌入式系统特点 ⭐⭐⭐

特点 说明
专用性 为固定用途而定制
可裁剪 资源有限,够用即可
可靠性 长期工作,故障率低
实时性 强实时 / 弱实时
低功耗 首要目标不是高性能而是低功耗

3. PC与嵌入式系统对比 ⭐⭐

硬件对比:

对比项 PC 嵌入式系统
CPU Intel 和 AMD ARM、MIPS、Motorola
内存 内存条 内存芯片
存储设备 硬盘 Flash 芯片
输入设备 键盘、鼠标 按键、触摸屏
输出设备 显示器 LCD、无、控制设备
接口 芯片

软件对比:

对比项 PC 嵌入式系统
引导代码 主板的 BIOS BootLoader(要移植)
OS Windows、Linux WindowsCE、VxWorks、Linux(要移植)
驱动程序 OS自带或下载 自己开发或移植
开发环境 本机开发和调试 借助服务器进行交叉编译
仿真器 不需要 需要

4. 嵌入式系统组成结构 ⭐⭐

1
2
3
嵌入式系统 = 硬件系统 + 软件系统
├── 硬件系统 = 嵌入式处理器 + 最小系统 + 接口电路
└── 软件系统 = 嵌入式OS + 应用软件
  • 最小系统: 处理器 + 最少的外围电路(时钟、复位、电源)
  • 简单嵌入式系统: 硬件 + 监控程序(无OS)
  • 复杂嵌入式系统: 硬件 + 嵌入式OS + 驱动 + 应用

5. 嵌入式处理器分类 ⭐⭐⭐

类型 全称 特点 典型代表
MPU Micro Processor Unit 功能强,需外部扩展存储器和I/O ARM系列、MIPS系列、PowerPC
MCU Micro Controller Unit 片内集成CPU+RAM+Flash+I/O 8051系列、STM32系列
DSP Digital Signal Processor 专用于数字信号处理 TI的TMS320系列
SoC System on Chip 系统级芯片,高度集成 RK3399、Exynos4412

MCU基本含义: 在一块芯片上集成了CPU、存储器(RAM/ROM)、定时器/计数器及多种I/O接口的比较完整的系统。


6. 嵌入式操作系统 ⭐⭐

OS 特点
VxWorks 稳定,异常处理强,商业系统
Windows CE 界面漂亮
μC/OS-II 最佳学习型,开源,实时性好
嵌入式Linux 开源,可裁剪,应用广泛
Android / iOS 移动智能终端
HarmonyOS 华为全场景分布式OS

嵌入式Linux(Embedded Linux)是指对Linux经过小型化裁剪后,能够固化在容量为几百KB到几十MB的存储芯片中,应用于特定嵌入式系统的专用Linux操作系统。


7. 交叉开发环境 ⭐⭐⭐

交叉开发(Cross Developing): 在PC机(宿主机/上位机)上完成程序的编辑、编译、链接等工作;程序的运行在嵌入式设备(目标机)上。

环境 组成
宿主机 PC + Linux + 交叉编译工具链
目标机 实验箱 + 引导程序 + 嵌入式操作系统

使用交叉开发的原因: 嵌入式系统的硬件资源有限,编译所需资源大、耗时长,且嵌入式设备编辑不方便。

在线调试方式:

  • JTAG(Joint Test Action Group):IBM和TI公司提出
  • BDM(Background Debugging Method):Motorola公司提出

8. GCC编译过程 ⭐⭐⭐

C源程序 hello.c 编译成可执行文件,要依次经过四个阶段:

1
hello.c →[预处理 -E]→ hello.i →[编译 -S]→ hello.S →[汇编 -c]→ hello.o →[链接]→ a.out
阶段 选项 功能
预处理 -E 处理 #include 头文件、#define 宏替换,生成 .i 文件
编译 -S 语法检查 → 转换为汇编语言,生成 .s 文件
汇编 -c 汇编代码 → 目标代码(机器码),生成 .o 文件
链接 目标代码 + 库文件 → 可执行文件

GCC编译过程演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 预处理 → 编译 → 汇编 → 链接(一步步执行)
gcc -E hello.c -o hello.i # 预处理:展开头文件、替换宏
gcc -S hello.i -o hello.s # 编译:转换为汇编代码
gcc -c hello.s -o hello.o # 汇编:生成目标文件
gcc hello.o -o hello # 链接:生成可执行文件

# 一步到位(默认输出 a.out)
gcc hello.c
./a.out

# 指定输出文件名
gcc hello.c -o hello
./hello

GCC多文件编译实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* exam1.c */
int sum(int n) {
int temp = 0, i;
for(i = 1; i <= n; i++)
temp += i;
return temp;
}

/* exam2.c */
#include <stdio.h>
main() {
int m;
extern int sum(int sum);
scanf("%d", &m);
printf("The sum is %d", sum(m));
}

1
2
gcc exam1.c exam2.c -o exam    # 多个源文件编译为一个可执行文件
./exam

GCC编译选项:

选项 功能 示例
-o FileName 指定输出文件名(默认 a.out gcc test.c -o test
-c 只编译生成目标文件 .o gcc -c test.c
-g 包含标准调试信息 gcc -g test.c -o test
-O 优化编译,提高执行效率 gcc -O test.c -o test
-l 链接库文件(如 -lpthread gcc pthread.c -lpthread -o pthread

GCC优化效果对比:

1
2
3
4
5
6
7
8
9
# 不优化
gcc optimize.c -o op_1
time ./op_1
# real 0m4.203s

# 优化编译
gcc -O optimize.c -o op_2
time ./op_2
# real 0m1.064s ← 速度提升约4倍

GCC链接库示例(多线程程序):

1
2
3
4
5
6
# 错误:未指定线程库
gcc pthread.c -o pthread
# 报错:undefined reference to `pthread_create'

# 正确:使用 -l 指定链接 libpthread 库
gcc pthread.c -lpthread -o pthread

多文件编译: gcc exam1.c exam2.c -o exam

8.1 GCC 命令为什么这样写【新增】GCC 命令为什么这样写 ⭐⭐⭐

GCC 的四个阶段不要只背文件后缀,要理解它是在把“人写的 C 语言”一步步变成“机器能执行的程序”。

1
hello.c → hello.i → hello.s → hello.o → hello
阶段 命令 输出 通俗解释 考试关键词
预处理 gcc -E hello.c -o hello.i .i 展开头文件、替换宏、处理条件编译 #include#define
编译 gcc -S hello.i -o hello.s .s 把 C 语言翻译成汇编语言 语法检查、汇编代码
汇编 gcc -c hello.s -o hello.o .o 把汇编翻译成机器码目标文件 目标文件、机器码
链接 gcc hello.o -o hello 可执行文件 把目标文件和库函数合成最终程序 库文件、符号解析

为什么有时编译通过,链接失败?

例如:

1
undefined reference to `pthread_create'

这说明:
编译器在头文件中看到了 pthread_create 的声明,所以语法检查能过;但链接时找不到函数真正的实现代码,所以报错。

正确写法:

1
gcc pthread.c -lpthread -o pthread

其中 -l pthread 表示链接 libpthread 线程库。

考试答题模板:

GCC 编译 C 程序一般经过预处理、编译、汇编、链接四个阶段。预处理处理宏和头文件;编译把 C 代码转为汇编;汇编生成目标文件;链接把目标文件与库文件合成为可执行文件。



9. Buildroot与SDK层次架构 ⭐

Buildroot: Linux平台上构建嵌入式Linux系统的框架,由大量Makefile脚本和Kconfig配置文件构成。可通过 make menuconfig 配置,编译出完整系统(Uboot + kernel + rootfs + 应用程序)。

SDK层次架构:

1
2
3
4
5
6
7
8
9
┌─────────────────────────┐
│ Applications(应用层) │ 具体产品功能及交互逻辑
├─────────────────────────┤
│ Libraries(中间件层) │ 系统基础库、第三方开源库、API接口
├─────────────────────────┤
│ Kernel(内核层) │ Linux 4.4 内核
├─────────────────────────┤
│ Bootloader(引导层) │ Uboot
└─────────────────────────┘

编译命令:

1
2
3
./build.sh uboot    # 编译Uboot
./build.sh kernel # 编译内核
./build.sh rootfs # 编译根文件系统


10. CH1 高频考点补充:GCCmakeBuildroot 的关系 ⭐⭐⭐

这一章容易把工具链概念混在一起,考试一般不是单纯问定义,而是给一个编译现象或命令,让你判断发生在哪一层

名词 作用 典型命令/文件 高频考法
gcc 编译器,完成预处理、编译、汇编、链接 gcc -E/-S/-c/-o 判断编译阶段与输出文件
make 自动化构建工具,按依赖关系决定是否重新编译 makemake clean 判断默认目标、依赖更新、Tab错误
Makefile 描述目标、依赖、命令的文本文件 target: prereq 写规则、写变量、写自动变量
Kconfig 描述图形化配置菜单 make menuconfig 配置项如何生成 .config
Buildroot 构建嵌入式Linux系统框架 ./build.sh rootfs U-Boot、kernel、rootfs 的关系
交叉编译工具链 在宿主机生成目标机可执行文件 aarch64-linux-gnu-gcc 为什么不能直接用宿主机 gcc

记忆线索: gcc 负责“编一个文件/一组文件”,make 负责“按依赖自动调用 gcc”,Buildroot/SDK 负责“组织 U-Boot、Kernel、Rootfs 等大工程”。


✅ CH1 本章复习题(考研/期末高频题型)

题1:简答题 ⭐⭐

说明嵌入式系统与通用PC系统在硬件、软件、开发方式上的主要区别。

参考答案要点 嵌入式系统以应用为中心,通常资源受限、软硬件可裁剪、强调可靠性/实时性/低功耗;PC系统通用性更强。硬件上,嵌入式常用 ARM/MIPS/SoC、Flash、板载RAM和专用外设;PC常用 x86、硬盘、标准外设。软件上,嵌入式常见 BootLoader + 嵌入式Linux/RTOS + 驱动 + 应用;PC依赖BIOS/UEFI和通用OS。开发方式上,嵌入式多采用宿主机交叉编译、目标机运行调试。

题2:计算题 ⭐⭐

某文件权限显示为 -rwxr-xr--,请写出其数字权限;若执行 chmod 755 app,分别表示所有者、同组用户、其他用户具有什么权限?

参考答案要点 `-rwxr-xr--` 对应所有者 `rwx=7`,同组用户 `r-x=5`,其他用户 `r--=4`,所以数字权限为 `754`。`chmod 755 app` 表示所有者可读写执行,同组用户可读执行,其他用户可读执行。

题3:程序/命令分析题 ⭐⭐⭐

给出如下命令,分别说明输出文件属于 GCC 哪个阶段:

1
2
3
4
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o hello
参考答案要点 `hello.i` 是预处理结果,已展开头文件和宏;`hello.s` 是编译后生成的汇编代码;`hello.o` 是汇编后生成的目标文件;`hello` 是链接后生成的可执行文件。

题4:代码题 ⭐⭐⭐

写出一条命令,将 main.cinput.ccalcu.c 编译并链接为可执行文件 main,要求保留调试信息并开启优化。

参考答案要点
1
gcc -g -O main.c input.c calcu.c -o main

题5:综合题 ⭐⭐⭐

为什么嵌入式Linux开发中常说“宿主机编译,目标机运行”?若把宿主机 gcc 编译出的 x86 程序直接放到 RK3399 开发板上运行,可能出现什么问题?

参考答案要点 因为目标板资源有限,不适合承担大型编译任务;并且目标机CPU架构与宿主机可能不同。宿主机 `gcc` 默认生成 x86/x86_64 指令集程序,RK3399 是 ARM64 架构,直接运行会出现“Exec format error”等架构不匹配问题,应使用 `aarch64-linux-gnu-gcc` 等交叉编译器。

📘 CH2 ARM硬件平台

1. ARM处理器概述 ⭐⭐

ARM处理器特点:

  • 功耗低(以mW计,首要目标不是高性能而是低功耗)
  • 集成丰富的外设接口(尽可能做到SoC)
  • 对实时多任务有很强的支持能力

典型ARM系列:

体系结构 典型内核 特点
ARMv4T ARM7TDMI 3级流水线,无Cache无MMU
ARMv5 ARM920T 5级流水线,16KB Cache,有MMU
ARMv7-A Cortex-A8/A9/A15 有Cache有MMU
ARMv7-M Cortex-M3 高速公路ETC读卡机
ARMv8 Cortex-A53/A72 64位(RK3399使用)

2. ARM处理器工作模式 ⭐⭐⭐

ARM处理器有 7种 工作模式:

模式 缩写 说明
用户模式 usr 正常程序执行模式,不能直接切换到其他模式
快速中断模式 fiq 高速数据传输或通道处理
外部中断模式 irq 通用中断处理
管理模式 svc 操作系统使用的保护模式(复位后默认进入)
中止模式 abt 虚拟内存或存储器保护
未定义指令模式 und 支持硬件协处理器的软件仿真
系统模式 sys 运行具有特权的操作系统任务(ARMv4以上)

其中 usr 为非特权模式,其余6种为特权模式fiq、irq、svc、abt、und 有独立的寄存器组,称为异常模式


3. ARM寄存器 ⭐⭐⭐

通用寄存器: R0~R15

寄存器 别名 用途
R0~R3 参数传递 / 返回值 / 临时变量
R4~R11 局部变量(被调用函数需保存)
R12 ip 过程间临时寄存器(子程序调用时的scratch寄存器)
R13 SP 栈指针(Stack Pointer)
R14 LR 链接寄存器(Link Register),保存函数返回地址
R15 PC 程序计数器(Program Counter)

状态寄存器:

寄存器 用途
CPSR 当前程序状态寄存器,保存条件标志、中断禁止位、当前处理器模式
SPSR 程序状态保存寄存器,异常发生时保存CPSR的副本(5种异常模式各有1个SPSR)

CPSR中关键位:低5位为模式位(如SVC=0x13),bit6为FIQ禁止位,bit7为IRQ禁止位


4. ARM指令分类 ⭐⭐⭐

4.0 ARM 指令理解层【新增】ARM 指令理解层

学习 ARM 指令时,不要只背“指令功能”,而要先抓住三句话:

1
2
3
数据处理指令:主要在寄存器里算东西
跳转指令:本质是改变 PC
LDR/STR:负责寄存器和内存之间搬数据

ARM 属于典型的 Load/Store 架构,CPU 不能直接在内存中完成复杂运算,通常要遵循:

1
内存 → LDR → 寄存器 → 运算 → STR → 内存

所以看到寄存器题、GPIO 寄存器题、裸机汇编题时,要先判断:
这条指令是在算数值改跳转位置,还是在读写内存 / 外设寄存器


数据处理指令

指令 功能 示例
MOV 数据传送 MOV R0, #0xFF
MVN 数据取反传送 MVN R0, #0
ADD 加法 ADD R0, R1, R2
SUB 减法 SUB R0, R1, #5
AND 按位与 AND R0, R0, #0xFF
ORR 按位或 ORR R0, R0, #0x01
BIC 位清零 BIC R0, R0, #0x1F
EOR 按位异或 EOR R0, R0, R0
CMP 比较(设置CPSR标志,不保存结果) CMP R1, #0
MRS 读状态寄存器 MRS R0, CPSR
MSR 写状态寄存器 MSR CPSR_c, R0

跳转指令

指令 功能 示例
B 无条件跳转 B Label
BL 带链接跳转(调用子程序,返回地址存入LR) BL main
BX 跳转到寄存器中的地址(常用于函数返回) BX LR
BEQ 条件跳转(Z=1时跳转) BEQ Label

存储器访问指令(LDR/STR系列)

ARM除了寄存器(R0~R15)外没有别的存储单元。所有外围模块都看作是ARM的不同地址单元。

指令 位数 功能 示例
LDR 32位(字) 从存储器加载到寄存器 LDR R3, [R4]
LDRB 8位(字节) 加载字节,高24位清零 LDRB R3, [R1, #8]
LDRH 16位(半字) 加载半字,高16位清零 LDRH R3, [R1, R2]
STR 32位(字) 从寄存器存储到存储器 STR R3, [R1], #8
STRB 8位(字节) 存储低8位 STRB R3, [R1]
STRH 16位(半字) 存储低16位 STRH R3, [R1]

LDR寻址方式:

方式 语法 说明
零偏移 LDR Rd, [Rn] Rn即为地址
前索引偏移 LDR Rd, [Rn, #0x04]! 先更新地址,再访问(!表示写回Rn)
后索引偏移 LDR Rd, [Rn], #0x04 先访问,再更新地址
程序相对偏移 LDR Rd, label 基于PC的偏移(加载标号地址)

批量加载/存储指令(LDM/STM):

堆栈类型 后缀 说明
满递减堆栈 FD 栈满时地址递减(常用
空递减堆栈 ED 栈空时地址递减
满递增堆栈 FA 栈满时地址递增
空递增堆栈 EA 栈空时地址递增
1
2
STMFD SP!, {R0, R4-R12, LR}   @ 多寄存器入栈(保存现场)
LDMFD SP!, {R0, R4-R12, PC} @ 多寄存器出栈(恢复现场)

PUSH = STMFD SP!POP = LDMFD SP!(PUSH/POP是STMFD/LDMFD的别名)

ARM 指令易混点补充

指令 通俗解释 常见场景 易错点
MOV R0, #1 把立即数 1 放进 R0 初始化寄存器 不是内存复制,只是寄存器赋值
MVN R0, #0 把 0 取反后放进 R0 得到全 1 结果不是 0,而是全 1
ORR R0, R0, #0x04 把 bit2 置 1 打开某功能位 不影响其他位
BIC R0, R0, #0x04 把 bit2 清 0 关闭某功能位 BIC 是 Bit Clear
CMP R0, #0 比较 R0 和 0 配合 BEQ/BNE 不保存结果,只改 CPSR 标志位
BL main 调用 main 并保存返回地址 函数调用 返回地址保存到 LR
BX LR 跳回 LR 中保存的位置 函数返回 常和 BL 配合
LDR R0, [R1] 从 R1 指向的内存读数据到 R0 读内存 / 读寄存器 方向是内存到寄存器
STR R0, [R1] 把 R0 写到 R1 指向的内存 写内存 / 写寄存器 方向是寄存器到内存

数据交换指令:

指令 功能
SWP 字交换:R2↔[R3]
SWPB 字节交换:R2低8位↔[R3]字节

5. RK3399 SoC详解 ⭐⭐⭐

RK3399 架构:

  • Big.Little架构:双核 Cortex-A72(大核)+ 四核 Cortex-A53(小核),64位
  • CPU频率 > 1.8GHz(大核簇)
  • L1 Cache:A72 各48KB I + 32KB D;A53 各32KB I + 32KB D
  • L2 Cache:大核簇1024KB,小核簇512KB
  • 内部SRAM:总计192KB(启动时bootrom使用4KB)

RK3399 GPIO四步设置(以 GPIO0_A2 引脚为例):

步骤 名称 寄存器地址 操作
1 使能时钟 0xFF750000 + 0x0104 PMUCRU_CLKGATE_CON1 置位19,使能GPIO0时钟
2 设置功能模式 0xFF310000 + 0x0000 PMUGRF_GPIO0A_IOMUX,第21:20位设为11(GPIO模式)
3 设置方向 0xFF720000 + 0x0004 GPIO_SWPORTA_DDR,第2位置1=输出,0=输入
4 设置电平 0xFF720000 + 0x0000 GPIO_SWPORTA_DR,第2位置1=高电平,0=低电平

完整ARM汇编代码(四步合一):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.global _start
_start:
bl led_test
ret

halt:
b halt

/*************************************************/
led_test:
/* 第一步:使能GPIO0时钟 */
mov x0, #0xFF750000 @ PMUCRU基地址
mov x1, #0xFF750000
ldr w1, [x1, #0x0104] @ 读取 PMUCRU_CLKGATE_CON1
orr w1, w1, #0x80000 @ 第19位置1,使能GPIO0时钟
str w1, [x0, #0x0104] @ 写回寄存器

/* 第二步:设置GPIO0为GPIO功能模式 */
mov x0, #0xFF310000 @ PMUGRF基地址
mov x1, #0xFF310000
ldr w1, [x1, #0x0000] @ 读取 PMUGRF_GPIO0A_IOMUX
orr w1, w1, #0x300000 @ 第21:20位设为11(GPIO模式)
str w1, [x0, #0x0000] @ 写回寄存器

/* 第三步:设置GPIO0_A2为输出方向 */
mov x0, #0xFF720000 @ GPIO0基地址
mov x1, #0xFF720000
ldr w1, [x1, #0x0004] @ 读取 GPIO_SWPORTA_DDR
orr w1, w1, #0x04 @ 第2位置1(输出)
str w1, [x0, #0x0004] @ 写回寄存器

/* 第四步:设置GPIO0_A2输出高电平 */
mov x0, #0xFF720000
mov x1, #0xFF720000
ldr w1, [x1, #0x0000] @ 读取 GPIO_SWPORTA_DR
orr w1, w1, #0x04 @ 第2位置1(高电平)
str w1, [x0, #0x0000] @ 写回寄存器

b halt

RK3399 C语言LED控制程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "led.h"

static void led_delay(void) {
volatile unsigned long int i, j;
for(i = 10; i > 0; i--)
for(j = 10000; j > 0; j--);
}

int main(void) {
led_int();
led_status(0);
while(1) {
led_status_led0(1); led_delay(); led_delay();
led_status_led0(0); led_delay(); led_delay();
led_status_led1(1); led_delay(); led_delay();
led_status_led1(0); led_delay(); led_delay();
led_status_led2(1); led_delay(); led_delay();
led_status_led2(0); led_delay(); led_delay();
led_status_led3(1); led_delay(); led_delay();
led_status_led3(0); led_delay(); led_delay();
}
return 0;
}

GPIO操作的核心是“读-改-写”:先从寄存器读取当前值(ldr),修改特定位(orr),再写回寄存器(str)。

5.1 GPIO 四步为什么不能省【新增】GPIO 四步为什么不能省 ⭐⭐⭐⭐

GPIO 不是“往地址里随便写 1 就能亮灯”。
一个引脚想要输出高低电平,至少要经过下面四层硬件准备:

1
时钟打开 → 引脚功能选择 → 输入/输出方向 → 写电平
步骤 为什么必须做 不做会怎样
使能 GPIO 时钟 外设模块需要时钟才能工作 写寄存器可能无效
配置 IOMUX 一个引脚可能复用为 UART / I2C / SPI / GPIO 引脚可能还在别的功能上
配置方向 GPIO 必须知道是输入还是输出 输出 LED 时若仍是输入,电平不可控
设置电平 方向正确后才能真正输出 0 / 1 只配置方向不会自动亮灯

速记:

1
CRU 管时钟,GRF 管复用,DDR 管方向,DR 管电平。

读-改-写为什么重要?

1
2
3
ldr w1, [x0, #0x0004]   @ 先读出寄存器原值
orr w1, w1, #0x04 @ 只把 bit2 置 1
str w1, [x0, #0x0004] @ 写回寄存器

如果直接写:

1
2
mov w1, #0x04
str w1, [x0, #0x0004]

虽然 bit2 被置 1,但其他位会被清 0,可能破坏同一个寄存器中其他引脚或功能的配置。


Exynos4412 GPIO寄存器组织方式(对比):

寄存器类型 命名规则 功能
端口控制寄存器 GPA0CON ~ GPZCON 配置引脚复用功能
端口数据寄存器 GPA0DAT ~ GPZDAT 读/写引脚电平
端口上拉寄存器 GPA0PUD ~ GPZPUD 配置上拉/下拉电阻
驱动能力寄存器 GPA0DRV ~ GPZDRV 配置驱动强度

6. CH2 高频考点补充:RK3399 GPIO 位操作 ⭐⭐⭐

RK3399 的 GPIO 题目常考“寄存器地址 + 位掩码 + 读改写”。以 GPIO0_A2 为例,核心不是背完整代码,而是会解释每一步为什么这样写。

步骤 目标 基地址 + 偏移 关键位 常见考法
1 使能 GPIO0 时钟 0xFF750000 + 0x0104 bit19 写出寄存器地址和掩码 0x80000
2 配置复用为 GPIO 0xFF310000 + 0x0000 bit21:20 解释为什么要配置 IOMUX
3 配置方向为输出 0xFF720000 + 0x0004 bit2 1=输出0=输入
4 输出高电平 0xFF720000 + 0x0000 bit2 1=高电平0=低电平

读-改-写模板:

1
2
3
val = readl(reg);
val |= mask;
writel(val, reg);

裸机汇编中常写成 ldr → orr/bic → str;Linux驱动中不能直接访问物理地址,要先 ioremap,再用 readl/writel


✅ CH2 本章复习题(计算题 + 汇编/寄存器题)

题1:简答题 ⭐⭐

说明 ARM 的 RISC 特点,并与 CISC 做简要对比。

参考答案要点 RISC 精简指令集优先保留高频简单指令,指令长度较固定,寻址方式相对简单,大量操作在寄存器中完成,便于流水线执行,低功耗、低成本、高效率;CISC 指令复杂、功能强但硬件实现复杂,指令长度和执行周期不统一。

题2:计算题 ⭐⭐⭐

已知 GPIO0 基地址为 0xFF720000GPIO_SWPORTA_DDR 偏移为 0x0004GPIO_SWPORTA_DR 偏移为 0x0000。求二者物理地址。若要操作 GPIO0_A2,对应位掩码是多少?

参考答案要点 `GPIO_SWPORTA_DDR = 0xFF720000 + 0x0004 = 0xFF720004`。`GPIO_SWPORTA_DR = 0xFF720000 + 0x0000 = 0xFF720000`。`GPIO0_A2` 是第2位,掩码为 `1 << 2 = 0x04`。

题3:汇编代码题 ⭐⭐⭐

补全代码:将 GPIO_SWPORTA_DDR 的 bit2 置1,要求使用读-改-写。

1
2
3
4
mov x0, #0xFF720000
ldr w1, [x0, #0x0004]
/* 在此补一行 */
str w1, [x0, #0x0004]
参考答案要点
1
orr w1, w1, #0x04
如果要清零 bit2,可写:
1
bic w1, w1, #0x04

题4:程序分析题 ⭐⭐⭐

解释下面两条指令的区别:

1
2
B  loop
BL main
参考答案要点 `B loop` 是无条件跳转,不保存返回地址;`BL main` 是带链接跳转,会把返回地址保存到 `LR`,常用于函数调用。函数返回常用 `BX LR`。

题5:综合设计题 ⭐⭐⭐

若一个按键连接到 GPIO1_A0,要求配置为外部中断输入,至少需要考虑哪些步骤?

参考答案要点 需要使能 GPIO 时钟;配置 IOMUX 为 GPIO 或外部中断功能;设置方向为输入;配置中断触发方式(边沿/电平、上升/下降/高/低);打开中断屏蔽;必要时开启消抖;注册中断服务函数;在 ISR 中清除中断标志并完成按键处理。

📙 CH3 交叉开发环境(BootLoader与Linux系统)

1. BootLoader概述 ⭐⭐⭐

BootLoader定义: 系统加电后运行的第一段代码(通常从地址 0x00000000 开始执行),核心任务是启动操作系统。相当于PC中的 BIOS + MBR

BootLoader通用结构:

阶段 语言 功能
Stage1 汇编 硬件设备初始化 → 为Stage2准备RAM → 复制Stage2到RAM → 设置堆栈 → 跳转Stage2入口
Stage2 C语言 初始化硬件 → 检测内存映射 → 将kernel和rootfs从Flash读到RAM → 设置启动参数 → 引导内核

常用BootLoader: U-Boot(Universal BootLoader)、Blob、ARMboot、RedBoot、vivi


2. U-Boot目录结构 ⭐⭐

目录 存放内容
arch 体系结构相关代码(arm、x86等)
board 核心板相关支持文件(samsung、rockchip等)
common U-Boot命令(bootm、tftp等)
configs 各板配置文件(rk3399_defconfig
driver 驱动程序(网卡、串口、USB等)
fs 文件系统支持(ext4、nfs、jffs2等)
include 头文件(硬件平台支持、系统配置等)
net 网络协议栈(bootp、tftp、rarp)
tools 工具(mkimage 生成U-Boot镜像)

配置命令:

1
2
make rk3399_defconfig    # 加载默认配置
make menuconfig # 图形化配置


3. Linux内核启动流程 ⭐⭐

Image的生成过程:

1
vmlinux(原始内核,~23MB)→[objcopy去掉符号表]→ Image(纯内核镜像,~18MB)

Linux启动流程:

1
2
3
4
5
arch/arm64/kernel/head.S(汇编入口)
→ start_kernel()(init/main.c)
→ rest_init()
→ kernel_init()(启动init进程)
→ 用户空间(加载rootfs中的init脚本)

vmlinux.lds 是内核链接脚本,决定了各段在内存中的位置


4. 根文件系统(rootfs) ⭐⭐⭐

rootfs 与 kernel 的关系:

  • 根文件系统和内核是完全独立的两个部分
  • 单独的Linux内核没法正常工作,必须搭配根文件系统
  • rootfs包含:内核模块(.ko)、/etc/fstab(自动挂载列表)、/etc/inittab(init配置)、应用程序、库文件等

Linux关键目录文件:

文件 用途
/etc/fstab 自动挂载的文件系统列表
/etc/inittab init进程的配置文件
/bin 系统必备命令(cp、ls、mount、rm)
/sbin 系统管理命令(insmod、fdisk、reboot)

Linux文件系统格式(补充):

类型 格式
传统硬盘 ext2、ext3、ext4、XFS
闪存 JFFS2、YAFFS
网络 NFS
虚拟/特殊 swap、procfs、sysfs、debugfs、devfs

5. CH3 高频考点补充:U-Boot、Kernel、Rootfs 与配置系统 ⭐⭐⭐

对象 作用 高频命令/文件 易错点
U-Boot 初始化硬件、加载内核、传递启动参数 make rk3399_defconfigmake menuconfigbootm 不是Linux内核本身
Kernel 操作系统核心,管理进程、内存、文件、设备、网络 ./build.sh kernelImagestart_kernel() 单独内核不能正常完成用户交互
Rootfs 内核挂载的第一个文件系统 /etc/fstab/etc/inittab/dev/lib/modules 不是“可有可无的目录包”
.config 保存配置结果 CONFIG_CMD_BOOTM=y defconfig/menuconfig 生成或修改
Kconfig 描述菜单项 source "xxx/Kconfig" 不直接负责编译,负责生成配置选项
Makefile 描述编译规则 obj-yobj-mmake -C .config 配合决定哪些文件参与编译

启动链路速记:

1
BootROM/romcode → BL1 → BL2/U-Boot → Kernel Image → rootfs → init/应用程序

✅ CH3 本章复习题(BootLoader + Kernel + Rootfs)

题1:简答题 ⭐⭐⭐

说明 BootLoader 的 Stage1 和 Stage2 分别完成什么工作,为什么 Stage1 常用汇编实现?

参考答案要点 Stage1 主要做早期硬件初始化、准备RAM、复制Stage2、设置栈并跳转到Stage2入口;Stage2 初始化更多硬件,检测内存映射,把 kernel/rootfs 从存储介质读入RAM,设置启动参数并引导内核。Stage1 常用汇编,因为此时C运行环境、栈、内存初始化可能尚未完成,需要直接控制处理器状态和寄存器。

题2:计算题 ⭐⭐

已知 vmlinux 大小约 23MB,使用 objcopy 去除符号表后生成的 Image 约 18MB。问减少了多少 MB?减少比例约为多少?

参考答案要点 减少 `23 - 18 = 5MB`。减少比例约为 `5 / 23 ≈ 21.7%`。

题3:命令题 ⭐⭐⭐

写出使用 SDK 分别编译 U-Boot、Kernel、Rootfs 的命令,并说明最终生成物的大致作用。

参考答案要点
1
2
3
./build.sh uboot
./build.sh kernel
./build.sh rootfs
U-Boot 生成启动加载相关镜像;Kernel 生成内核镜像如 `Image/kernel.img/boot.img`;Rootfs 生成根文件系统,包含命令、库、配置、模块和应用等。

题4:代码/配置题 ⭐⭐⭐

make menuconfig 保存后 .config 中出现:

1
CONFIG_CMD_BOOTM=y

这说明什么?若该项为 n,可能产生什么影响?

参考答案要点 说明 U-Boot 中 `bootm` 命令被编译进镜像。若为 `n`,对应命令可能不可用,无法通过 `bootm` 启动某些格式的内核镜像,具体影响取决于启动脚本是否依赖该命令。

题5:综合题 ⭐⭐⭐

解释 rootfs 与 kernel 的关系。为什么说“没有 rootfs,Linux 内核无法正常工作”?

参考答案要点 kernel 与 rootfs 是独立部分。kernel 负责核心资源管理,但用户空间程序、库文件、配置文件、设备节点、模块、初始化脚本等在 rootfs 中。没有 rootfs,内核启动后无法挂载第一个文件系统,无法启动 init 或 shell,也无法正常提供用户交互环境。

📗 实验1 搭建开发环境

1. eAIOT实验平台 ⭐

平台组成:

  • 左侧:高性能嵌入式主板(核心板),采用瑞芯微 RK3399(64位ARM SoC),标配 4GB 内存 + 16GB 闪存
  • 右侧:无线传感器网络扩展板(Zigbee、BLE、LORA、NB-IoT、RFID、10+传感器)
  • 核心板以金手指形式插入底板,板载 2×2 MIMO 双天线 WiFi 模组

支持运行的OS: Android 8.1、Ubuntu 18.04、Armbian、Buildroot、Linux

开机流程(按顺序):

  1. 将各类天线、网线正确接入
  2. 将 Type-C 数据线插入 debug 或 download 口
  3. 确保主板电源开关处于靠左关闭状态
  4. 插入白色 12V 电源插头
  5. 向右拨动电源开关启动系统

⚠️ 请严格按照以上步骤开机,减少瞬时电涌对主板接口的损害。Type-C 接口较为脆弱,请勿暴力抽拽。


2. Linux文件系统 ⭐⭐

文件系统类型:

文件系统 特点
ext2 早期文件系统,非日志文件系统,已不推荐
ext3 在 ext2 基础上发展,日志文件系统,完全兼容 ext2
ext4 在 ext3 基础上发展,性能和可靠性更佳,向下兼容 ext3 和 ext2

Linux根目录重要文件夹:

目录 用途
/bin 二进制可执行命令文件
/sbin 系统命令
/root 超级用户 root 的根目录
/home 普通用户默认目录
/boot 系统内核和启动文件
/dev 设备文件
/etc 系统管理配置文件
/lib 系统运行所需库文件
/proc 虚拟目录,保存系统信息和进程信息
/tmp 临时文件,所有用户可读写
/var 变化文件,如日志
/usr 应用程序和库文件
/sys 系统设备和文件层次结构

查询当前文件系统版本:df -T -h


3. 常用Shell命令 ⭐⭐

Shell命令格式: command -options [argument]

基础命令:

命令 语法示例 功能
ls ls -l /home 列出目录内容(-l详细列表,-a显示隐藏文件)
cd cd /home/xuelitec 切换目录(cd ~回用户目录,cd ..返回上级)
pwd pwd 显示当前所在目录的完整路径
uname uname -a 显示系统信息(-a显示全部内核信息)
sudo sudo apt-get install xxx 以超级用户权限执行命令
man man ls 查看命令手册(按 q 退出)
df df -T -h 查看磁盘使用情况(-T显示文件系统类型,-h人性化显示)

文件操作命令:

命令 语法示例 功能
touch touch test1.c 创建空文件 / 更新文件时间戳
mkdir mkdir -p dir/subdir 创建目录(-p递归创建多级目录)
rm rm -rf dir/ 删除文件(-r递归删除目录,-f强制)
rmdir rmdir empty_dir/ 删除空目录(目录非空会报错)
cp cp file1.c /home/backup/ 复制文件
mv mv oldname.c newname.c 移动文件 / 重命名
chmod chmod 755 test1.c 修改文件权限
find find /home -name "*.c" 按条件查找文件
grep grep "include" main.c 在文件中搜索字符串

压缩解压命令:

命令 语法示例 功能
tar tar -zxvf file.tar.gz 解压 .tar.gz-zgzip,-x解压,-v显示过程,-f指定文件)
tar tar -cvzf dir.tar.gz dir/ 压缩为 .tar.gz-c创建)
zip zip archive.zip file1 file2 压缩为 .zip
unzip unzip archive.zip 解压 .zip

文件权限详解(chmod):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查询权限
ls -l test1.c
# 输出:-rwxr-xr-- 1 user group 1024 May 7 10:00 test1.c
# [-][rwx][r-x][r--]
# | | | |
# | | | └─ 其他用户:只读(r--)
# | | └─────── 同组用户:读+执行(r-x)
# | └───────────── 所有者:读+写+执行(rwx)
# └──────────────── 文件类型(-普通文件,d目录)

# 修改权限 - 数字法
chmod 755 test1.c # rwxr-xr-x(7=rwx, 5=r-x, 5=r-x)
chmod 644 test1.c # rw-r--r--(6=rw-, 4=r--, 4=r--)


4. 开发环境搭建 ⭐

Ubuntu和Windows文件互传: FTP服务

NFS服务安装:

1
sudo apt-get install nfs-kernel-server

SSH服务安装:

1
sudo apt-get install openssh-server

交叉编译工具链: arm-linux-gnueabihf-gcc(用于编译裸机程序)/ aarch64-linux-gnu-gcc(用于编译64位程序)


✅ 实验1 本章复习题(Linux命令 + 环境搭建)

题1:命令题 ⭐⭐

写出安装 NFS 服务和 SSH 服务的命令。

参考答案要点
1
2
sudo apt-get install nfs-kernel-server
sudo apt-get install openssh-server

题2:计算题 ⭐⭐

某目录权限需要设置为“所有者读写执行,同组用户读执行,其他用户读执行”,应使用 chmod 的哪个数字?

参考答案要点 所有者 `rwx=7`,同组 `r-x=5`,其他 `r-x=5`,所以:
1
chmod 755 dir_or_file

题3:命令分析题 ⭐⭐

解释下面命令中各选项含义:

1
tar -zxvf rootfs.tar.gz
参考答案要点 `-z` 表示使用 gzip,`-x` 表示解压,`-v` 显示过程,`-f` 指定文件名。

题4:综合题 ⭐⭐⭐

将虚拟机中编译好的 .ko 和测试程序传到目标板,常见方式有哪些?哪种方式适合大文件?

参考答案要点 可用 FTP/SFTP、串口 Z-modem、U盘/TF卡、NFS 等。大文件更推荐网络方式或U盘/TF卡;串口传输速度慢,适合小文件临时调试。

📘 实验2 GPIO与ARM裸机开发

1. ARM处理器与GPIO ⭐⭐⭐

RK3399 SoC 双处理器:

  • Cortex-A53(64位,小核)
  • Cortex-A72(64位,大核)

GPIO操作步骤(裸机):

  1. 使能GPIO时钟
  2. 设置GPIO复用功能(配置MUX寄存器)
  3. 设置GPIO方向(输入/输出)
  4. 设置GPIO输出电平(高低)

RK3399关键寄存器:

  • PMUGRF_GPIO0A_IOMUX(地址 0xFF310000):GPIO复用功能配置
  • GPIO0_SWPORTA_DR:GPIO数据寄存器(写数据)
  • GPIO0_SWPORTA_DDR:GPIO方向寄存器(0=输入,1=输出)

2. ARM启动文件(start.S) ⭐⭐⭐

start.S完成的主要功能:

  1. 设置异常向量表(Exception Vector)
  2. 关闭 IRQ、FIQ,设置 SVC 模式
  3. 关闭 L1 cache,设置 L2 cache,关闭 MMU
  4. 根据 OM 引脚确定启动方式
  5. 在 SoC 内部 SRAM 中设置栈
  6. 执行 lowlevel_init 函数(初始化系统时钟、SDRAM、串口等)
  7. 设置 SDRAM 中的栈

⚠️ 异常向量表必须从 0x000000000xFFFF0000 开始,每条向量占4字节


3. ARM汇编与C语言混合编程 ⭐⭐

关键汇编指令:

指令 语法示例 功能
LDR LDR R0, =0xFF310000 将地址/数据加载到寄存器(=表示伪指令,加载立即数到R0)
STR STR R0, [R1] 将寄存器的值存储到内存地址
MOV MOV R0, #0xFF 将立即数移动到寄存器
BL BL main 带链接的跳转(调用函数,将返回地址存入LR)
B B loop 无条件跳转(不保存返回地址)
BX BX LR 跳转到寄存器中的地址(常用于函数返回,LR存放返回地址)
PUSH PUSH {R0, R1, LR} 将寄存器压栈(保存现场)
POP POP {R0, R1, PC} 从栈中弹出寄存器(恢复现场,PC为程序计数器)
MRS MRS R0, CPSR 读取状态寄存器CPSR到通用寄存器
MSR MSR CPSR_c, R0 写入通用寄存器的值到状态寄存器

汇编中调用C函数: 使用 BL main 跳转到C语言入口点,函数返回时通过 BX LR 回到汇编

实际应用示例(start.S中设置SVC模式并关中断):

1
2
3
4
5
MRS R0, CPSR          @ 读取当前状态寄存器
BIC R0, R0, #0x1F @ 清除低5位(模式位)
ORR R0, R0, #0x13 @ 设置为SVC模式(0x13 = 10011)
ORR R0, R0, #0xC0 @ 关闭IRQ(bit 7)和FIQ(bit 6)
MSR CPSR_c, R0 @ 写回CPSR

栈设置与调用C函数示例:

1
2
3
LDR SP, =0x00200000   @ 设置栈指针SP(SDRAM顶部往下)
BL main @ 调用C语言main函数
B . @ 如果main返回则死循环


4. 交叉编译与烧写 ⭐⭐

编译步骤:

1
2
3
4
5
6
7
8
# 编译 start.S
arm-linux-gnueabihf-gcc -c start.S -o start.o
# 编译 main.c
arm-linux-gnueabihf-gcc -c main.c -o main.o
# 链接
arm-linux-gnueabihf-ld -Ttext 0x00000000 start.o main.o -o led.elf
# 生成bin文件
arm-linux-gnueabihf-objcopy -O binary led.elf led.bin

烧写命令(Uboot下):

1
2
3
4
# 下载到内存
loadx 0x00200000 # 使用xmodem协议
# 执行
go 0x00200000

Makefile编写要点:

  • CROSS_COMPILE = arm-linux-gnueabihf-
  • 使用 -march=armv7-a 指定架构
  • 链接脚本 .ld 指定各段在内存中的位置

5. Makefile 编写 ⭐⭐⭐

Makefile 是工程管理工具,通过依赖关系自动判断哪些文件需要重新编译,大幅提高编译效率。

基本语法

1
2
目标(target): 依赖(prerequisites)
[Tab] 命令(command)

规则:

  • 目标:要生成的文件,或是一个”伪目标”(如 clean
  • 依赖:生成目标所依赖的文件列表
  • 命令:生成目标的具体操作(必须用 Tab 开头

变量与自动变量

Makefile变量分为三类:

变量/符号 含义 示例
$(CC) 编译器 CC = arm-linux-gnueabihf-gcc
$(LD) 链接器 LD = arm-linux-gnueabihf-ld
$(OBJCOPY) 格式转换工具 OBJCOPY = arm-linux-gnueabihf-objcopy
$@ 当前目标名 led.bin: led.elf$@ = led.bin
$< 第一个依赖名 led.o: led.S$< = led.S
$^ 所有依赖名(空格分隔) app: a.o b.o$^ = a.o b.o
?= 若未定义则赋值 CROSS_COMPILE ?= arm-linux-gnueabihf-

Makefile预定义变量(Make自带,可直接引用):

变量 默认值 用途
AR ar 库文件维护程序
AS as 汇编程序
CC cc C编译器
CPP $(CC) -E C预编译器
CXX g++ C++编译器
RM rm -f 文件删除命令
CFLAGS C编译器选项
LDFLAGS 链接器选项

常用编译变量

变量 用途 常用值
CFLAGS C编译选项 -march=armv7-a -mfloat-abi=hard -mfpu=neon
LDFLAGS 链接选项 -Ttext 0x00000000 -Tlink.ld
CROSS_COMPILE 交叉编译前缀 arm-linux-gnueabihf-
TARGET 最终目标文件名 led.bin

伪目标 .PHONY

1
2
3
.PHONY: clean     # 声明 clean 为伪目标,避免目录下有同名文件时 make 不执行
clean:
rm -f *.o *.elf *.bin

为什么需要 .PHONY 若目录下存在一个名为 clean 的文件,make clean 会认为目标已最新而跳过。.PHONY 告诉 make 无论是否有同名文件都执行该规则。

裸机开发完整 Makefile 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 交叉编译工具链前缀
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy
CFLAGS = -march=armv7-a -marm -Wall

TARGET = led
OBJS = start.o main.o

# 默认目标(第一个目标)
all: $(TARGET).bin

# 链接:将 .o 文件链接为 .elf
$(TARGET).elf: $(OBJS)
$(LD) -Ttext 0x00000000 $^ -o $@

# 生成二进制文件
$(TARGET).bin: $(TARGET).elf
$(OBJCOPY) -O binary $< $@

# 编译 .S → .o
%.o: %.S
$(CC) -c $(CFLAGS) $< -o $@

# 编译 .c → .o
%.o: %.c
$(CC) -c $(CFLAGS) $< -o $@

# 伪目标:清理
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET).elf $(TARGET).bin

内核模块 Makefile(实验5 用)

1
2
3
4
5
6
7
8
9
10
# 内核模块 Makefile(放在模块源码同一目录)
KDIR := /lib/modules/$(shell uname -r)/build # 内核源码目录

obj-m += chrdev.o # 指定要编译为模块的目标

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules # 进入内核目录编译模块

clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean # 清理

关键点: 内核模块 Makefile 与普通 Makefile 不同,obj-m 表示编译为模块,-C 指定内核源码目录,M=$(PWD) 指定模块源码目录。

RK3399实际交叉编译 Makefile(课件原版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# RK3399 裸机交叉编译 Makefile(带链接脚本)
PREFIX = aarch64-linux-gnu-
CC = $(PREFIX)gcc
LD = $(PREFIX)ld
AR = $(PREFIX)ar
OBJCOPY = $(PREFIX)objcopy
OBJDUMP = $(PREFIX)objdump

# 添加 GCC 运行时库
PLATFORM_LIBS += -L $(shell dirname `$(CC) $(CFLAGS) -print-libgcc-file-name`) -lgcc

rk3399.bin: start.S
$(CC) -nostdlib -g -c -o start.o start.S
$(LD) -Trk3399.lds -g start.o -o rk3399_elf $(PLATFORM_LIBS)
$(OBJCOPY) -O binary -S rk3399_elf rk3399.bin
$(OBJDUMP) -D -m arm rk3399_elf > rk3399.dis

clean:
rm -f rk3399.dis rk3399.bin rk3399_elf *.o

与简化版Makefile的区别: 使用 -nostdlib 不链接标准库(裸机无OS)、-Trk3399.lds 指定链接脚本控制内存布局、OBJDUMP 生成反汇编文件用于调试

Make 常用命令

命令 功能
make 执行 Makefile 中第一个目标(通常是 all
make clean 执行 clean 目标,清理编译产物
make -j4 4个线程并行编译,加快速度
make V=1 显示详细的编译命令(调试用)

6. Makefile 高频易错点与速记 ⭐⭐⭐⭐⭐

6.1 普通应用 Makefile 与内核模块 Makefile 的区别

对比项 普通应用 Makefile Linux内核模块 Makefile
编译目标 用户态可执行文件 内核态 .ko 模块
编译器 gcc 或交叉编译器 由内核构建系统调用
关键变量 CCCFLAGSOBJSTARGET KERNELDIRCURRENT_PATHobj-m
典型命令 $(CC) $^ -o $@ $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
清理方式 rm -f *.o app $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

6.2 自动变量必背

自动变量 含义 高频例子
$@ 当前目标 mainled.bin
$< 第一个依赖 main.c
$^ 所有依赖,去重 main.o input.o calcu.o
$? 比目标新的依赖 增量编译判断

6.3 变量赋值必背

写法 含义 例子
= 递归展开,使用时再求值 可能受后续变量修改影响
:= 立即展开,定义时求值 更稳定,常用于路径
?= 未定义才赋值 允许命令行覆盖默认值
+= 追加字符串 追加编译选项或目标文件

6.4 missing separator 的高频原因

Makefile 规则命令行必须以 Tab 开头,不能用空格。若看到:

1
Makefile:7: *** missing separator.  Stop.

优先检查命令行缩进是否为 Tab。

6.5 Makefile 为什么这样写【新增】Makefile 为什么这样写 ⭐⭐⭐⭐⭐

Makefile 的核心不是“把命令写进文件”,而是告诉 make

1
2
目标文件依赖哪些文件;
依赖变了以后,应该执行什么命令重新生成目标。

最小规则结构:

1
2
目标: 依赖
命令

其中命令前必须是 Tab,不能是空格。因为 make 依靠 Tab 判断这一行是“要执行的命令”。

自动变量展开示例

1
2
app: main.o input.o calcu.o
$(CC) $^ -o $@

展开后等价于:

1
gcc main.o input.o calcu.o -o app
自动变量 在本例中代表 含义
$@ app 当前目标
$^ main.o input.o calcu.o 所有依赖
$< main.o 第一个依赖

.PHONY 为什么重要?

1
2
3
.PHONY: clean
clean:
rm -f *.o app

如果目录下刚好有一个名为 clean 的文件,而没有写 .PHONYmake clean 可能会认为 clean 目标已经存在,不再执行清理命令。
.PHONY 的作用是告诉 make:clean 不是普通文件名,而是一个“动作名”。



✅ 实验2 本章复习题(Makefile + 裸机代码)

题1:代码题 ⭐⭐⭐

写一个最小 Makefile:把 main.c input.c calcu.c 编译为可执行文件 main,要求使用变量、模式规则、自动变量,并提供 clean 伪目标。

参考答案要点
1
2
3
4
5
6
7
8
9
10
11
12
13
CC := gcc
TARGET := main
OBJS := main.o input.o calcu.o

$(TARGET): $(OBJS)
$(CC) $^ -o $@

%.o: %.c
$(CC) -c $< -o $@

.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)

题2:计算题 ⭐⭐

若某寄存器当前值为 0x00000000,需要将 bit2 和 bit5 置1,不改变其他位,应与哪个掩码做或操作?结果是多少?

参考答案要点 掩码为 `(1 << 2) | (1 << 5) = 0x04 | 0x20 = 0x24`。结果为 `0x00000024`。

题3:汇编题 ⭐⭐⭐

解释下面裸机编译流程中每一步的作用:

1
2
3
4
arm-linux-gnueabihf-gcc -c start.S -o start.o
arm-linux-gnueabihf-gcc -c main.c -o main.o
arm-linux-gnueabihf-ld -Ttext 0x00000000 start.o main.o -o led.elf
arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
参考答案要点 第一行把启动汇编编译成目标文件;第二行把C文件编译成目标文件;第三行把目标文件链接为ELF文件,并指定链接地址为 `0x00000000`;第四行从ELF中提取纯二进制镜像 `led.bin`,用于裸机烧写/运行。

题4:Makefile分析题 ⭐⭐⭐

已知规则:

1
2
app: a.o b.o c.o
$(CC) $^ -o $@

请问 $@$^ 分别是什么?

参考答案要点 `$@` 是当前目标,即 `app`;`$^` 是所有依赖文件,即 `a.o b.o c.o`。

题5:综合题 ⭐⭐⭐

为什么裸机程序链接时常使用链接脚本或 -Ttext 指定地址?如果链接地址与实际运行地址不一致,可能出现什么问题?

参考答案要点 裸机没有操作系统加载器帮程序安排虚拟地址,需要开发者明确代码段、数据段、栈等位置。若链接地址与实际运行地址不一致,涉及绝对地址访问、全局变量、跳转表等代码可能出错,导致程序跑飞、访问错误地址或无法启动。

📙 实验3 中断

1. 中断基本概念 ⭐⭐⭐

中断定义: CPU在执行程序的过程中,当出现某些突发事件时,CPU暂停当前程序,转去处理该事件,处理完毕后再返回原来被中断的位置继续执行。

中断类型:

类型 来源 特点
IRQ 外设中断 普通中断,可屏蔽
FIQ 快速中断 优先级高于IRQ,用于高速数据传输
SWI/SVC 软件中断 用户态切换到内核态
Reset 复位 最高优先级
Undefined Instruction 未定义指令 执行未定义指令时触发
Data Abort 数据访问异常 数据读写出错
Prefetch Abort 指令预取异常 指令读取出错

2. RK3399 GPIO中断实验 ⭐⭐

GPIO中断配置步骤:

  1. 使能GPIO时钟
  2. 设置GPIO复用功能为GPIO模式
  3. 设置GPIO方向为输入
  4. 配置GPIO中断触发方式(上升沿/下降沿/高电平/低电平)
  5. 使能GPIO中断
  6. 注册中断服务函数(ISR)

按键消抖: 软件延时消抖(约10~20ms)


3. ARM异常处理 ⭐⭐

异常向量表:

地址偏移 异常类型
0x00 Reset
0x04 Undefined Instruction
0x08 SWI/SVC
0x0C Prefetch Abort
0x10 Data Abort
0x14 未使用
0x18 IRQ
0x1C FIQ

中断处理流程:

  1. 保存现场(将寄存器压栈)
  2. 执行中断服务程序(ISR)
  3. 恢复现场(寄存器出栈)
  4. 中断返回

4. GIC中断控制器 ⭐⭐

GIC(Generic Interrupt Controller)功能:

  • 中断优先级管理
  • 中断路由(分发到哪个CPU核心)
  • 中断屏蔽与使能
  • 中断状态跟踪(active/pending/inactive)

✅ 实验3 本章复习题(中断 + 异常向量)

题1:简答题 ⭐⭐⭐

说明 IRQ 与 FIQ 的区别。

参考答案要点 IRQ 是普通外设中断,可屏蔽,用于一般中断处理;FIQ 是快速中断,优先级通常更高,拥有更多独立寄存器,适合高速、低延迟场景。

题2:计算题 ⭐⭐

ARM异常向量表中每项占4字节,若 Reset 偏移为 0x00,IRQ 是第7项,FIQ 是第8项,求 IRQ 和 FIQ 的偏移地址。

参考答案要点 从0开始计偏移:IRQ 对应偏移 `0x18`,FIQ 对应偏移 `0x1C`。

题3:代码题 ⭐⭐⭐

补全如下设置:要求进入 SVC 模式并关闭 IRQ/FIQ。

1
2
3
4
5
MRS R0, CPSR
BIC R0, R0, #0x1F
ORR R0, R0, #0x13
/* 在此补一行:关闭 IRQ 和 FIQ */
MSR CPSR_c, R0
参考答案要点
1
ORR R0, R0, #0xC0
`0xC0` 对应 bit7 和 bit6,分别屏蔽 IRQ 和 FIQ。

题4:流程题 ⭐⭐⭐

说明一次外部按键中断从发生到返回的大致处理流程。

参考答案要点 外设产生中断信号;中断控制器记录并分发中断;CPU响应中断并跳转异常向量;保存现场;执行ISR;读取/判断中断源;处理中断事件;清除中断标志;恢复现场;返回被中断程序继续执行。

题5:综合题 ⭐⭐

为什么按键中断通常需要消抖?软件消抖常见方法是什么?

参考答案要点 机械按键在按下或松开瞬间会产生电平抖动,可能触发多次中断。软件消抖常用延时约10~20ms后再次读取按键状态,确认稳定后再处理。

📕 实验4 Uboot与Linux系统

1. RK3399启动过程 ⭐⭐⭐

完整启动流程:

1
上电复位 → romcode → BL1(SRAM)→ BL2(DDR)→ OS Kernel → Rootfs

详细步骤:

  1. RK3399系统上电复位后,Cortex-A53从地址 0xFFFF0000 执行romcode
  2. romcode依次按以下顺序查找介质上的ID BLOCK信息:
    • SPI NOR FLASH → SPI NAND FLASH → eMMC → SD/MMC → USB
  3. romcode功能:查找ID BLOCK → 确定启动方式 → 初始化硬件 → 将BL1加载到内部SRAM
  4. BL1执行DDR初始化,将BL2复制到DDR,跳转到BL2
  5. BL2初始化硬件和软件,将OS复制到DDR,跳转到OS
  6. 启动操作系统

2. Bootloader (Uboot) ⭐⭐⭐

Bootloader定义: 系统启动后、OS内核运行之前运行的一段小程序,用于初始化硬件设备、建立内存映射,为调用OS内核准备好环境。

Bootloader两种操作模式:

模式 说明
启动加载模式(自主模式) 从固态存储设备将OS加载到RAM运行,无需用户介入。是产品发布时的正常工作模式
下载模式 通过串口/网络从主机下载内核映像和根文件系统。用于第一次安装和系统更新,提供命令行接口

BL1阶段(Stage1,汇编实现):

步骤 功能
1 硬件设备初始化
2 为加载Stage2准备RAM空间
3 复制Stage2到RAM空间
4 设置堆栈(为执行C语言代码做准备)
5 跳转到Stage2的C入口点

BL2阶段(Stage2,C语言实现):

步骤 功能
1 初始化本阶段使用的硬件设备
2 检测系统内存映射(memory map)
3 将内核映像和根文件系统从Flash读到RAM
4 为内核设置启动参数
5 调用内核

3. Uboot源码启动流程 ⭐⭐

start.S 文件(BL1阶段):

  1. 指定Uboot入口
  2. 设置异常向量
  3. 关闭IRQ、FIQ,设置SVC模式
  4. 关闭L1 cache,设置L2 cache,关闭MMU
  5. 根据OM引脚确定启动方式
  6. 在SoC内部SRAM中设置栈
  7. 执行 lowlevel_init(初始化系统时钟、SDRAM、串口)
  8. 设置开发板供电锁存
  9. 设置SDRAM中的栈

_main 函数(BL1→BL2过渡):

  1. 将Uboot从存储介质复制到SDRAM(BL2加载到RAM并跳转)
  2. 设置并开启MMU
  3. 在SDRAM中合适位置设置栈
  4. 清除BSS段,BL1阶段完毕

board_init_r 函数(BL2阶段):

  1. 规划Uboot内存使用
  2. 遍历调用 init_sequence 初始化函数数组
  3. 初始化堆管理器
  4. 初始化SD/MMC控制器
  5. 环境变量重定位
  6. 目标机硬件设备初始化
  7. 控制台初始化
  8. 网卡芯片初始化
  9. 进入主循环 main_loop(出现Uboot提示符)

4. Linux内核 ⭐⭐

Linux内核主要功能:

功能 说明
进程管理 进程创建/销毁,调度策略,处理器资源共享,进程间通信
内存管理 虚拟地址空间,地址映射,多进程安全共享内存
文件管理 虚拟文件系统(VFS),支持ext3/ext4等数十种文件系统
设备管理 设备驱动程序,每种外设对应特定驱动代码
网络管理 网络协议栈 + 网络设备驱动程序

Linux内核目录结构:

目录 内容
arch 架构相关代码(ARM、x86等)
block 块设备(SD卡、eMMC、NAND、硬盘等)
drivers 驱动程序(drivers/i2cdrivers/gpio等)
fs 文件系统
init 初始化代码(含 main.cstart_kernel
ipc 进程间通信
kernel 调度器等核心代码
mm 内存管理
net 网络协议栈

Linux内核启动顺序:

1
内核引导 → 运行init → 系统初始化 → 建立终端 → 用户登录系统

内核编译命令:

1
2
3
4
5
6
7
# 编译uboot
sudo ./build.sh uboot
# 生成:rk3399_loader_v1.22.119.bin, trust.img, uboot.img

# 编译内核
sudo ./build.sh kernel
# 生成:kernel.img, resource.img, boot.img

内核配置命令:

1
2
3
# 加载实验平台默认配置
make lpkt030_linux_defconfig # 或 rk3399_defconfig
# 编译后在 arch/arm64/boot/ 目录下生成 Image 镜像


5. 实验4 高频补充:build.sh 与内核目录定位 ⭐⭐⭐

考试常把“命令—目录—生成物”混合考。注意:./build.sh 只是上层脚本,真正会进入对应源码目录调用更底层脚本或 Makefile。

命令 进入/调用对象 常见生成物 高频判断
./build.sh uboot u-bootmake_uboot.sh uboot.imgtrust.img、loader 启动加载相关
./build.sh kernel kernelmake_kernel.sh Imagekernel.imgboot.img 内核相关
./build.sh rootfs Buildroot/rootfs脚本 根文件系统镜像 用户空间与库/命令相关

✅ 实验4 本章复习题(U-Boot + Kernel + Rootfs)

题1:简答题 ⭐⭐⭐

写出 RK3399 从上电复位到操作系统启动的大致流程。

参考答案要点 上电复位后执行 BootROM/romcode;romcode 查找启动介质中的 ID BLOCK;加载 BL1 到内部 SRAM;BL1 初始化 DDR 并加载 BL2;BL2/U-Boot 初始化更多外设、加载内核镜像与设备树;跳转到内核;内核挂载 rootfs 并启动用户空间。

题2:计算题 ⭐⭐

若某镜像从 eMMC 的 0x800000 偏移开始,长度为 0x400000 字节,请换算长度为多少 MB?

参考答案要点 `0x400000 = 4 × 1024 × 1024` 字节,即 `4MB`。

题3:命令题 ⭐⭐⭐

写出进入 U-Boot 图形配置的一般命令顺序,并说明为什么要先执行 defconfig

参考答案要点
1
2
make rk3399_defconfig
make menuconfig
先执行 `defconfig` 是为了加载开发板默认配置,生成基础 `.config`,之后 `menuconfig` 才是在默认配置基础上进行增删修改。

题4:源码流程题 ⭐⭐⭐

U-Boot 倒计时结束自动启动 Linux 内核,与哪些环境变量/函数关系密切?

参考答案要点 与 `bootdelay`、`bootcmd` 关系密切。`main_loop()` 中处理启动倒计时,`bootdelay_process()` 读取环境变量,`autoboot_command()` 判断是否被用户按键打断,若未打断则执行 `bootcmd`,最终加载并跳转内核。

题5:综合题 ⭐⭐⭐

为什么说 U-Boot 的使命在调用内核入口后基本结束?

参考答案要点 U-Boot 的主要任务是准备硬件环境、加载内核/设备树/根文件系统参数并将控制权交给内核。调用内核入口后,CPU执行流进入Linux内核,后续由内核完成初始化和系统管理,U-Boot不再作为常规运行组件参与调度。

📓 CH4 Linux驱动原理(高频考点加强版)

本章根据“Linux驱动原理”课件与 LPKT030 驱动开发指南补强,属于期末/实验问答/代码题最高频部分。重点掌握:驱动与应用的关系、设备分类、字符设备框架、设备号、Makefile、模块加载、用户/内核数据传递、地址映射


1. 什么是驱动程序 ⭐⭐⭐

驱动程序: 指挥硬件工作的软件,是应用程序与硬件之间的中间层,为应用程序屏蔽硬件细节,实现设备无关性

角度 驱动程序的作用
对上 给应用程序提供统一接口,如 open/read/write/ioctl
对下 操作寄存器、中断、DMA、总线、时钟、电源等硬件资源
对内核 按内核框架注册设备、申请资源、释放资源

驱动程序的基本功能:

功能 说明
设备初始化和释放 注册设备、申请中断、初始化寄存器;退出时释放设备号、中断和内存
数据传送 在设备、内核、应用之间传递数据
错误检测和处理 对硬件错误、参数错误、拷贝失败等做处理

2. 驱动程序与应用程序的区别 ⭐⭐⭐

对比项 应用程序 驱动程序
入口 main() 主动执行 module_init() 注册入口,加载时调用
运行空间 用户空间 内核空间
可用库 可用 glibc,如 printf 只能用内核函数,如 printk
硬件访问 不能直接访问硬件 可通过内核接口访问硬件
执行方式 主动完成任务 初始化后等待系统调用或中断触发
风险 崩溃一般影响进程 错误可能导致内核崩溃

典型调用链:

1
2
3
4
5
6
7
8
9
10
11
应用程序 open/read/write/close

C库系统调用封装

系统调用陷入内核

VFS 根据设备文件找到 file_operations

驱动程序 open/read/write/release

硬件寄存器 / 缓冲区 / 中断

3. Linux 对设备的分类 ⭐⭐⭐

类型 访问特点 典型设备 高频考点
字符设备 按字节流顺序访问 LED、按键、I2C、SPI、UART、触摸屏 file_operations、设备号、/dev 节点
块设备 按块访问,支持缓存和随机读写 硬盘、U盘、eMMC、SD卡、NAND 文件系统基础,常与缓冲区相关
网络设备 按数据包收发,不一定映射为 /dev 文件 eth0wlan0lo 通过网络子系统和接口名访问

高频辨析: /dev/xxx 是设备文件/设备节点,是访问驱动的接口;.ko 才是编译后的驱动模块文件。


4. 设备号:主设备号与次设备号 ⭐⭐⭐⭐

Linux 用 dev_t 表示设备号,通常由 主设备号 major次设备号 minor 组成。

项目 含义
主设备号 major 标识驱动程序,同一类设备通常共用同一驱动
次设备号 minor 标识同一驱动下的具体设备实例
设备类型 c 表示字符设备,b 表示块设备

dev_t 高频宏:

1
2
3
4
#define MINORBITS 20
#define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int)((dev) & ((1U << MINORBITS) - 1)))
#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))

设备节点创建:

1
mknod /dev/chrdevbase c 200 0

含义:创建字符设备节点 /dev/chrdevbase,主设备号 200,次设备号 0


5. 字符设备驱动核心:file_operations ⭐⭐⭐⭐⭐

file_operations 是 Linux 字符设备驱动最核心的操作函数集合,定义在 include/linux/fs.h

成员 对应用户态函数 功能
.owner = THIS_MODULE 指明模块所有者,防止模块被错误卸载
.open open() 打开设备,常做初始化/保存私有数据
.read read() 从设备读数据到用户空间
.write write() 从用户空间写数据到设备
.release close() 关闭设备,释放资源
.unlocked_ioctl ioctl() 读写之外的控制命令
.poll poll/select 非阻塞和多路复用查询
.mmap mmap() 将设备内存映射到用户空间

最小模板:

1
2
3
4
5
6
7
static struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_release,
};

6. 字符设备注册:老框架与新框架 ⭐⭐⭐⭐⭐

6.1 老框架:register_chrdev

1
2
register_chrdev(major, name, &fops);
unregister_chrdev(major, name);

优点是简单;缺点是主设备号易冲突、设备节点常需手动 mknod

6.2 新框架:dev_t + cdev + class + device

新字符设备框架更符合现代 Linux 驱动写法:

1
2
3
4
5
6
7
8
9
alloc_chrdev_region/register_chrdev_region

cdev_init

cdev_add

class_create

device_create

卸载时反向释放:

1
2
3
4
5
6
7
device_destroy

class_destroy

cdev_del

unregister_chrdev_region
函数 作用
alloc_chrdev_region(&dev, 0, count, name) 动态申请设备号
register_chrdev_region(dev, count, name) 静态注册指定设备号
cdev_init(&cdev, &fops) 初始化字符设备对象
cdev_add(&cdev, dev, count) 添加字符设备到内核
class_create(THIS_MODULE, name) 创建设备类
device_create(class, NULL, dev, NULL, name) 自动生成 /dev/name

7. 内核空间与用户空间数据传递 ⭐⭐⭐

用户空间和内核空间不能直接互相访问,应使用专用函数。

1
2
copy_to_user(user_buf, kernel_buf, len);      // 内核 → 用户
copy_from_user(kernel_buf, user_buf, len); // 用户 → 内核
函数 方向 常见场景
copy_to_user 内核空间 → 用户空间 驱动 read() 返回数据
copy_from_user 用户空间 → 内核空间 驱动 write() 接收控制命令

高频坑:返回值表示未成功拷贝的字节数。通常 0 表示全部成功,非0表示还有数据未拷贝成功。


8. 地址映射与寄存器访问 ⭐⭐⭐

Linux 开启 MMU 后,驱动不能像裸机一样直接访问物理地址。必须先把寄存器物理地址映射为内核虚拟地址。

1
2
void __iomem *reg;
reg = ioremap(PHYS_ADDR, 4);

卸载时释放映射:

1
iounmap(reg);

映射后推荐用内核 I/O 访问函数:

函数 位宽 方向
readb(addr) 8位
readw(addr) 16位
readl(addr) 32位
writeb(val, addr) 8位
writew(val, addr) 16位
writel(val, addr) 32位

典型寄存器读改写:

1
2
3
4
u32 val;
val = readl(PMUCRU_CLKGATE_CON1);
val |= (1 << 19);
writel(val, PMUCRU_CLKGATE_CON1);

9. 驱动模块 Makefile 必背 ⭐⭐⭐

这是驱动代码题最高频模板。命令行必须以 Tab 开头。

1
2
3
4
5
6
7
8
9
10
11
KERNELDIR := /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
含义
KERNELDIR 内核源码目录
CURRENT_PATH 当前驱动源码目录
obj-m := chrdevbase.o chrdevbase.c 编译成模块 chrdevbase.ko
-C $(KERNELDIR) 切换到内核源码目录执行构建
M=$(CURRENT_PATH) 告诉内核构建系统,外部模块源码在当前目录
modules 编译模块

10. 驱动调试命令速查 ⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 编译驱动模块
make

# 编译用户态测试程序
aarch64-linux-gnu-gcc chrdevbaseApp.c -o chrdevbaseApp

# 加载模块
insmod chrdevbase.ko

# 查看模块
lsmod

# 查看设备号
cat /proc/devices

# 手动创建设备节点
mknod /dev/chrdevbase c 200 0

# 赋予测试程序执行权限
chmod +x chrdevbaseApp

# 读测试
./chrdevbaseApp /dev/chrdevbase 1

# 写测试
./chrdevbaseApp /dev/chrdevbase 2

# 卸载模块
rmmod chrdevbase.ko

✅ CH4 本章复习题(驱动程序高频题)

题1:简答题 ⭐⭐⭐

说明 Linux 驱动程序与应用程序的主要区别。

参考答案要点 应用程序运行在用户空间,有 `main()`,可使用 glibc,如 `printf`;驱动程序运行在内核空间,通过 `module_init/module_exit` 注册入口出口,只能使用内核API,如 `printk`,通过系统调用被动响应应用请求或中断事件。驱动错误可能影响整个内核。

题2:计算题 ⭐⭐⭐

Linux 中 dev_t 高12位为主设备号,低20位为次设备号。若主设备号为 200,次设备号为 3,求 dev_t 的十六进制值。

参考答案要点 `dev = (200 << 20) | 3`。`200 = 0xC8`,所以 `200 << 20 = 0x0C800000`,加上 `3` 得:
1
0x0C800003

题3:代码题 ⭐⭐⭐

写出一个最小字符设备驱动的 file_operations 初始化代码,至少包含 open/read/write/release

参考答案要点
1
2
3
4
5
6
7
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};

题4:代码题 ⭐⭐⭐

写出内核模块入口/出口函数注册模板,并添加 GPL 协议声明。

参考答案要点
1
2
3
4
5
6
7
8
9
10
11
12
static int __init xxx_init(void)
{
return 0;
}

static void __exit xxx_exit(void)
{
}

module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");

题5:代码题 ⭐⭐⭐

在驱动 read() 中,应使用哪个函数把内核缓冲区 kbuflen 字节复制到用户缓冲区 buf?在 write() 中方向相反又该使用哪个函数?

参考答案要点
1
2
copy_to_user(buf, kbuf, len);       // read: 内核 → 用户
copy_from_user(kbuf, buf, len); // write: 用户 → 内核

题6:计算题 ⭐⭐

执行命令:

1
mknod /dev/demo c 100 0

分别说明 /dev/democ1000 的含义。

参考答案要点 `/dev/demo` 是设备节点路径;`c` 表示字符设备;`100` 是主设备号;`0` 是次设备号。

题7:Makefile代码题 ⭐⭐⭐

led.c 编译为内核模块 led.ko,内核源码路径为 /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel,写出 Makefile。

参考答案要点
1
2
3
4
5
6
7
8
9
KERNELDIR := /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel
CURRENT_PATH := $(shell pwd)
obj-m := led.o

all:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

题8:综合设计题 ⭐⭐⭐

把裸机 LED 程序改成 Linux LED 字符设备驱动,核心修改点有哪些?

参考答案要点 不能直接使用物理地址指针操作寄存器,应在入口函数中用 `ioremap` 映射寄存器;用 `readl/writel` 做寄存器读写;按字符设备框架实现 `open/write/release` 等;注册设备号和 `file_operations`;加载模块后创建 `/dev/led` 节点;用户程序通过 `open/write/close` 控制 LED;卸载时 `iounmap` 并注销设备。

📓 实验5 设备驱动(一)

1. 应用程序与驱动的关系 ⭐⭐⭐

调用流程:

1
应用程序(用户空间) → C库函数(open/read/write) → 系统调用 → 内核空间 → 驱动程序

  • 应用程序运行在用户空间
  • 驱动程序运行在内核空间
  • 用户空间不能直接操作内核,必须通过系统调用陷入内核空间
  • open、close、write、read 等函数由 C 库提供

2. file_operations 结构体 ⭐⭐⭐⭐⭐

定义位置: include/linux/fs.h

file_operations 是Linux内核驱动操作函数集合

成员 功能
owner 拥有该结构体的模块指针,设为 THIS_MODULE
read 读取设备文件
write 向设备文件写入数据
open 打开设备文件
release 关闭设备文件(对应应用层 close

开发字符设备驱动最主要的工作就是实现 file_operations 中的函数


3. 驱动模块加载与卸载 ⭐⭐⭐

驱动运行方式:

  • 编译进Linux内核:内核启动时自动运行
  • 编译为模块(.ko):内核启动后使用命令加载(调试时推荐)

模块注册函数:

1
2
module_init(xxx_init);   // 注册模块加载函数,insmod时调用xxx_init
module_exit(xxx_exit); // 注册模块卸载函数,rmmod时调用xxx_exit

模块加载命令:

1
2
3
insmod drv.ko        # 加载指定模块
modprobe drv.ko # 加载模块(自动处理依赖)
rmmod drv.ko # 卸载模块


4. 字符设备注册与注销 ⭐⭐⭐⭐⭐

1
2
3
4
5
6
// 注册字符设备
register_chrdev(major, name, fops);
// major: 主设备号; name: 设备名; fops: file_operations指针

// 注销字符设备
unregister_chrdev(major, name);

设备号:

  • 每个设备有一个设备号,分为主设备号和次设备号
  • 应用程序通过设备号使用设备驱动
  • 主设备号:标识驱动程序(同类驱动共用)
  • 次设备号:标识具体设备实例

创建设备文件:

1
mknod /dev/chrdev c 200 0    # c=字符设备 200=主设备号 0=次设备号


5. 内核空间与用户空间数据传递 ⭐⭐⭐

两个空间不能直接互相访问,必须使用专用函数:

1
2
3
4
5
// 内核空间 → 用户空间
unsigned long copy_to_user(void *to, const void *from, unsigned long count);

// 用户空间 → 内核空间
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
参数 含义
to 传递的目标地址
from 传递的起始地址
count 传递的数据长度
返回值 未成功传递的数据长度

6. 地址映射(MMU) ⭐⭐⭐

MMU(Memory Manage Unit)功能:

  1. 虚拟空间到物理空间的映射
  2. 内存保护,设置访问权限和缓冲特性

关键概念:

  • 虚拟地址(VA):CPU访问的地址
  • 物理地址(PA):实际硬件地址
  • Linux内核启动时初始化MMU,CPU访问的都是虚拟地址

地址映射函数:

1
2
3
4
5
// 物理地址 → 虚拟地址
void __iomem *ioremap(unsigned long phys_addr, size_t size);

// 释放映射
void iounmap(volatile void __iomem *addr);

示例:

1
2
3
4
// 映射
static void __iomem *PMUGRF_GPIO0A_IOMUX = ioremap(PMUGRF_GPIO0A_IOMUX_BASE, 4);
// 卸载时取消映射
iounmap(PMUGRF_GPIO0A_IOMUX);


7. I/O内存访问函数 ⭐⭐

ARM体系下只有I/O内存(无I/O端口概念)

读操作函数:

函数 位数
readb(addr) 8-bit
readw(addr) 16-bit
readl(addr) 32-bit

写操作函数:

函数 位数
writeb(value, addr) 8-bit
writew(value, addr) 16-bit
writel(value, addr) 32-bit

寄存器操作示例:

1
2
3
4
// 读-改-写操作
val = readl(PMUGRF_GPIO0A_IOMUX); // 读取寄存器
val |= (3 << (4+16)) | (0 << 4); // 修改特定位
writel(val, PMUGRF_GPIO0A_IOMUX); // 写回寄存器


8. 字符设备驱动开发完整步骤 ⭐⭐⭐⭐⭐

  1. 编写驱动代码:实现 file_operations 中的 open/release/read/write
  2. 编写Makefile:使用内核构建系统(KDIR指向内核源码目录)
  3. 编译驱动make 生成 .ko 模块文件
  4. 编译测试程序aarch64-linux-gnu-gcc chrdevApp.c -o chrdevApp
  5. 加载模块insmod chrdev.ko
  6. 创建设备文件mknod /dev/chrdev c 200 0
  7. 运行测试./chrdevApp

9. 实验5 高频补充:chrdevbase 到真实 LED 驱动的迁移思路 ⭐⭐⭐

chrdevbase 是虚拟字符设备,重点训练框架;LED 驱动是在框架基础上增加寄存器控制

修改点 chrdevbase LED驱动
设备对象 内核读写缓冲区 RK3399 GPIO寄存器
read 返回 kernel data! 可选:返回LED状态
write 接收用户字符串 接收 0/1 控制亮灭
入口函数 注册字符设备 注册字符设备 + ioremap + 初始化GPIO
出口函数 注销字符设备 注销字符设备 + iounmap
核心API copy_to_user/copy_from_user 加上 readl/writel

LED驱动 write 的典型逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static ssize_t led_write(struct file *filp,
const char __user *buf,
size_t cnt,
loff_t *offt)
{
unsigned char databuf[1];
unsigned int val;

if (copy_from_user(databuf, buf, 1) != 0)
return -EFAULT;

val = readl(GPIO0_SWPORTA_DR);
if (databuf[0])
val |= (1 << 2);
else
val &= ~(1 << 2);
writel(val, GPIO0_SWPORTA_DR);

return 1;
}

✅ 实验5 本章复习题(字符设备驱动综合)

题1:流程题 ⭐⭐⭐

写出从编写 chrdevbase.c 到在目标板测试成功的完整流程。

参考答案要点 编写驱动源文件;编写内核模块 Makefile;执行 `make` 生成 `.ko`;编写并交叉编译测试APP;把 `.ko` 和APP传到目标板;`insmod chrdevbase.ko` 加载模块;`cat /proc/devices` 查看主设备号;`mknod /dev/chrdevbase c 200 0` 创建设备节点;`chmod +x chrdevbaseApp`;运行读写测试;最后 `rmmod chrdevbase.ko` 卸载模块。

题2:代码题 ⭐⭐⭐

补全驱动入口函数中的字符设备注册代码,主设备号为 200,设备名为 chrdevbase,操作函数集合为 chrdevbase_fops

1
2
3
4
5
6
7
8
9
10
static int __init chrdevbase_init(void)
{
int ret = 0;
/* 在此补全 */
if (ret < 0) {
printk("register failed\n");
return ret;
}
return 0;
}
参考答案要点
1
ret = register_chrdev(200, "chrdevbase", &chrdevbase_fops);

题3:计算题 ⭐⭐

一次 read(fd, buf, 50) 期望读取50字节,实际返回值为32,表示什么?若返回0或负数分别表示什么?

参考答案要点 返回32表示成功读取了部分数据,共32字节;返回0通常表示到达文件末尾或没有数据;返回负数表示读取失败,应根据错误码判断原因。

题4:代码题 ⭐⭐⭐

写出模块卸载时应完成的资源释放操作:已知主设备号 200,设备名 chrdevbase

参考答案要点
1
2
3
4
5
6
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(200, "chrdevbase");
}

module_exit(chrdevbase_exit);
若驱动中使用了 `ioremap`、`class_create`、`device_create`、`cdev_add` 等,还应反向调用 `iounmap`、`device_destroy`、`class_destroy`、`cdev_del`、`unregister_chrdev_region`。

题5:综合改错题 ⭐⭐⭐

下面驱动代码有什么问题?

1
2
#define GPIO0_SWPORTA_DR (*(volatile unsigned int *)(0xFF720000))
GPIO0_SWPORTA_DR |= (1 << 2);
参考答案要点 这是裸机式直接访问物理地址的写法,在 Linux 驱动中 MMU 已开启,不能直接把物理地址当指针访问。应使用 `ioremap(0xFF720000, 4)` 得到内核虚拟地址,再通过 `readl/writel` 完成读改写。

题6:Makefile分析题 ⭐⭐⭐

解释下面命令中 -CM= 的作用:

1
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
参考答案要点 `-C $(KERNELDIR)` 表示先切换到内核源码目录执行内核构建系统;`M=$(CURRENT_PATH)` 表示外部模块源码目录是当前目录,内核构建系统会到该目录读取 `obj-m` 并编译模块。

📒 CH5 蜂鸣器与光传感器(I2C驱动高频)

本章对应第5讲“蜂鸣器 光传感器”。重点不是单纯背传感器型号,而是掌握:GPIO输出控制、三极管驱动蜂鸣器、I2C总线特性、Linux I2C驱动框架、i2c_driver / i2c_client / i2c_transfer 的关系


1. 六合一传感器模块 ⭐⭐

六合一传感器模块包含:

序号 模块 典型接口
1 温湿度传感器 单总线
2 蜂鸣器 普通GPIO
3 电位器 ADC
4 三合一环境光传感器 I2C
5 三轴加速度传感器 SPI
6 智能炫彩LED IO模拟双线

考试常考“某外设应该用哪种总线/子系统”。例如:蜂鸣器是普通 GPIO 输出控制;AP3216C 光传感器必须使用 I2C;DHT11 是单总线;SK9822 是双线同步控制。


2. 蜂鸣器工作原理 ⭐⭐⭐

蜂鸣器常分为 压电式蜂鸣器电磁式蜂鸣器

类型 构成 发声原理
压电式 多谐振荡器、压电蜂鸣片、阻抗匹配器、共鸣箱等 通电后振荡器起振,驱动压电蜂鸣片发声
电磁式 振荡器、电磁线圈、磁铁、振动膜片等 电流经过线圈形成磁场,振动膜片周期振动发声

开发板蜂鸣器电路中,通常通过 BEEP_IO 控制三极管导通/截止,从而控制蜂鸣器是否工作。

1
2
3
4
5
6
7
BEEP_IO 输出高/低电平

控制三极管基极

三极管导通或截止

蜂鸣器响或不响

高频点:GPIO 并不是直接“发声”,而是控制三极管;三极管再控制蜂鸣器回路。


3. 蜂鸣器 Linux 驱动思路 ⭐⭐⭐

蜂鸣器本质是一个 GPIO 输出设备,Linux 驱动思路与 LED 类似。

基本流程:

1
2
3
4
5
1. 获取/配置 BEEP_IO 对应引脚
2. 设置引脚复用为 GPIO
3. 设置 GPIO 方向为输出
4. 在 write() 中根据用户数据输出高/低电平
5. 在 exit() 中释放资源

如果使用寄存器方式,需要掌握:

1
2
3
4
5
6
reg = ioremap(PHYS_ADDR, size);
val = readl(reg);
val |= mask; // 置位
val &= ~mask; // 清零
writel(val, reg);
iounmap(reg);

如果使用 GPIO 子系统,则优先使用:

1
2
3
4
gpio_request(gpio, "beep");
gpio_direction_output(gpio, 0);
gpio_set_value(gpio, 1);
gpio_free(gpio);

4. AP3216C 光传感器 ⭐⭐⭐

AP3216C 是三合一环境光传感器,集成:

功能 英文 作用
环境光强度 ALS 获取环境光照强度,如自动调节屏幕亮度
接近检测 PS 判断物体是否靠近
红外强度 IR 采集红外相关数据

AP3216C 必须使用 I2C 总线

信号 作用 复用引脚
SCL I2C 时钟线 GPIO4_C1
SDA I2C 数据线 GPIO4_C0
INT 中断通知CPU有数据 GPIO3_D6,可轮询时忽略

5. I2C 总线接口规范 ⭐⭐⭐⭐

I2C 是一种常见的芯片间串行扩展总线协议,属于 同步、串行、半双工、有线通信协议

特点 说明
两根线 SDA 数据线,SCL 时钟线
一主多从 主机发起通信,从机响应
无片选线 通过设备地址区分不同从设备
半双工 同一时刻通常只进行一个方向的数据传输
同步通信 SCL 提供时钟
双向开漏/开集 常配合上拉电阻使用

速率常考:

模式 速率
标准模式 100 Kbps
快速模式 400 Kbps
高速模式 3.4 Mbps

高频点:I2C 总线上器件数量不只受地址限制,还受总线电容限制。通常总线总电容不超过约 400 pF。


6. Linux I2C 驱动框架 ⭐⭐⭐⭐

Linux 下 I2C 驱动被拆分为三层:

1
2
3
4
5
I2C core

I2C adapter 驱动

I2C client driver 设备驱动
层次 含义 谁来写
I2C core 提供注册、注销、匹配、传输封装等通用代码 Linux内核
I2C adapter SoC 内置 I2C 控制器的软件抽象 芯片/板厂通常已写好
I2C client driver 具体 I2C 设备驱动,如 AP3216C 驱动开发者重点编写

对开发者而言,通常说“写 I2C 驱动”,指的是写 I2C 设备驱动,不是重新写 I2C 主机控制器驱动。


7. i2c_adapteri2c_clienti2c_driver ⭐⭐⭐

结构体 作用 高频理解
i2c_adapter 表示一个 I2C 控制器/适配器 对应 SoC 中的 I2C 主机
i2c_algorithm 表示适配器的通信算法 核心函数常为 master_xfer
i2c_client 表示挂在 I2C 总线上的一个从设备 包含设备地址 client->addr
i2c_driver 表示某类 I2C 设备的驱动 匹配成功后执行 probe()

i2c_driver 典型模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const struct i2c_device_id ap3216c_id[] = {
{"ap3216c", 0},
{}
};

static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.name = "ap3216c",
},
.id_table = ap3216c_id,
};

module_i2c_driver(ap3216c_driver);

8. AP3216C 驱动调用链 ⭐⭐⭐

AP3216C 驱动中,常见读数据流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
App: open/read

驱动 read()

readdata()

ap3216c_read_reg()

ap3216c_read_regs()

i2c_transfer()

I2C adapter 主机控制器驱动

AP3216C 硬件

匹配成功后,在 probe() 中通常要做:

1
2
3
4
1. 保存 i2c_client
2. 初始化 AP3216C 寄存器
3. 注册字符设备或 misc 设备
4. 创建设备节点

常见写法:

1
ap3216cdev.private_data = client;

这样后续 read() 中就能通过私有数据找到当前 I2C 从设备地址和适配器。


9. I2C 读写时序与 i2c_transfer ⭐⭐⭐⭐

I2C 写寄存器一般需要发送:

1
2
3
从设备地址 + 写方向
寄存器地址
待写入数据

I2C 读寄存器通常分两步:

1
2
第一步:写寄存器地址
第二步:重新开始,按读方向读取数据

在 Linux 中常用 struct i2c_msg 描述传输消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct i2c_msg msg[2];

msg[0].addr = client->addr;
msg[0].flags = 0; // 写
msg[0].buf = &reg;
msg[0].len = 1;

msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读
msg[1].buf = val;
msg[1].len = len;

ret = i2c_transfer(client->adapter, msg, 2);

✅ CH5 本章复习题(蜂鸣器 + I2C + AP3216C)

题1:简答题 ⭐⭐

说明蜂鸣器驱动与 LED 驱动在软件控制思路上的相同点和不同点。

参考答案要点 相同点:二者都可以看成 GPIO 输出控制设备,都需要设置引脚复用、方向和输出电平;应用程序都可通过字符设备 `write()` 控制。不同点:LED 通常直接由 GPIO 电平控制亮灭,蜂鸣器常通过 GPIO 控制三极管导通/截止,再由三极管控制蜂鸣器回路。

题2:计算题 ⭐⭐

I2C 快速模式速率为 400 Kbps,若不考虑起始位、应答位等额外开销,理论上传输 100 字节数据至少需要多少毫秒?

参考答案要点 100 字节 = 800 bit。时间为:
1
800 / 400000 = 0.002 s = 2 ms
实际 I2C 传输还包含地址位、读写位、ACK、起止信号等,实际时间会更长。

题3:代码题 ⭐⭐⭐

写出 AP3216C 作为 I2C 设备驱动时,i2c_driver 至少应包含的两个回调函数名称。

参考答案要点
1
2
3
4
static int ap3216c_probe(struct i2c_client *client,
const struct i2c_device_id *id);

static int ap3216c_remove(struct i2c_client *client);
其中 `probe()` 在设备和驱动匹配成功后执行,`remove()` 在设备或驱动移除时执行。

题4:程序分析题 ⭐⭐⭐

解释 client->addri2c_transfer(client->adapter, msgs, num)client->adapter 的含义。

参考答案要点 `client->addr` 是 I2C 从设备地址,用于在同一总线上区分不同设备;`client->adapter` 表示该设备挂接的 I2C 适配器,也就是具体的 I2C 主机控制器。`i2c_transfer` 通过适配器把消息发送到对应地址的从设备。

题5:综合题 ⭐⭐⭐

为什么 Linux 下 I2C 驱动要把“适配器驱动”和“设备驱动”分开?

参考答案要点 不同 SoC 的 I2C 控制器寄存器不同,但 I2C 从设备协议相同。将适配器驱动和设备驱动分离后,SoC 厂商负责适配器驱动,设备驱动开发者只需关注具体芯片寄存器和数据格式。这样同一个 I2C 设备驱动可以复用到不同平台,提高可移植性和代码复用性。

题6:代码题 ⭐⭐⭐

写出使用两个 i2c_msg 读取某寄存器 reg 的核心伪代码。

参考答案要点
1
2
3
4
5
6
7
8
9
10
11
12
13
struct i2c_msg msg[2];

msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].buf = &reg;
msg[0].len = 1;

msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].buf = data;
msg[1].len = len;

ret = i2c_transfer(client->adapter, msg, 2);

📔 CH6 温湿度传感器 DHT11 与 GPIO 子系统

本章对应第6讲“温湿度传感器”。高频主线:DHT11 单总线严格时序 → GPIO方向需要频繁切换 → 不再直接操作寄存器 → 使用 pinctrl/gpioctrl/设备树/misc 设备


1. DHT11 与 SHT20 对比 ⭐⭐⭐⭐

传感器 类型 温度范围 湿度范围 特点
DHT11 单总线数字温湿度传感器 0℃~50℃ 20%~90% 低成本、简单易用、精度较低
SHT20 高精度温湿度传感器 -40℃~125℃ 0%~100% 精度和稳定性更高,低功耗,适合长期部署

2. DHT11 单总线协议 ⭐⭐⭐⭐

DHT11 使用一根数据线 DQ 传输温湿度数字信号,对时序要求非常严格。

一次通信约 4 ms,传输 40 bit:

1
2
3
4
5
8bit 湿度整数
+ 8bit 湿度小数
+ 8bit 温度整数
+ 8bit 温度小数
+ 8bit 校验位

校验规则:

1
校验位 = 湿度整数 + 湿度小数 + 温度整数 + 温度小数 的低8位

3. DHT11 开始信号与响应信号 ⭐⭐⭐⭐

通信过程:

阶段 主机/DHT11动作
空闲 总线为高电平
主机开始 主机拉低总线至少 18 ms
主机释放 主机拉高总线 20~40 us
DHT11响应低电平 DHT11 拉低约 80 us
DHT11响应高电平 DHT11 拉高约 80 us
传输数据 发送 40 bit 数据

如果主机发出开始信号后,检测不到 DHT11 的低电平响应,通常说明初始化失败或线路/引脚配置有问题。


4. DHT11 数据位判定 ⭐⭐⭐⭐

每 1 bit 数据都以约 50 us 低电平开始,之后通过高电平持续时间判断 0 或 1。

数据位 高电平持续时间
0 约 26~28 us
1 约 70 us

常见代码思路:

1
2
3
4
5
6
7
data <<= 1;

udelay(28);
if (gpio_get_value(dht11_gpio))
data |= 1;

while (gpio_get_value(dht11_gpio));

高频点:读每一位时常先左移一位,再把本轮读到的 0/1 放入最低位。


5. 为什么 DHT11 适合用 GPIO 子系统 ⭐⭐⭐⭐

DHT11 数据线需要一会儿输出、一会儿输入:

阶段 GPIO方向
总线空闲 输出高电平
主机开始信号 输出低电平,再输出高电平
读取响应 输入
读取40bit数据 输入

如果继续像 LED/蜂鸣器那样手动操作 GPIO 方向寄存器和数据寄存器,代码会很繁琐。使用 GPIO 子系统后,可以直接调用通用 API。


6. pinctrl 子系统与 gpioctrl 子系统 ⭐⭐⭐

Linux 对引脚管理通常分为两类:

子系统 负责内容 典型作用
pinctrl 引脚复用和电气属性 配置 pinmux、上下拉、驱动能力等
gpioctrl GPIO 输入输出控制 设置方向、读值、写值、释放GPIO

pinctrl 主要负责:

1
2
pinmux:引脚复用,如 GPIO/I2C/SPI/UART
pinconf:电气特性,如上拉、下拉、开漏、驱动能力

gpioctrl 常用 API:

1
2
3
4
5
6
gpio_request(gpio, "dht11");
gpio_direction_output(gpio, 1);
gpio_direction_input(gpio);
gpio_get_value(gpio);
gpio_set_value(gpio, 0);
gpio_free(gpio);

7. 设备树与 GPIO 获取 ⭐⭐⭐

设备树 DTS 用树形结构描述板级硬件信息,例如 CPU、内存、I2C、SPI、GPIO、传感器等。.dts 经 DTC 编译后生成 .dtb

Linux 驱动中常通过设备树节点获取 GPIO 编号:

1
2
3
4
5
struct device_node *node;
int gpio;

node = of_find_node_by_path("/dht11_sensor");
gpio = of_get_named_gpio(node, "dht11-gpio", 0);
函数 作用
of_find_node_by_path 根据路径查找设备树节点
of_get_named_gpio 从设备树节点中获取指定 GPIO 编号

/proc/device-tree 是设备树在运行中根文件系统里的体现,可在目标板中查看设备树节点信息。


8. misc 杂项设备 ⭐⭐⭐

misc 设备适合一些不太好分类的小型设备。特点:

特点 说明
主设备号固定 misc 设备主设备号固定为 10
次设备号区分设备 不同 misc 设备通过 minor 区分
自动创建设备节点 通常不需要手动 mknod
比字符设备框架更简洁 不需要单独 class_create

misc 设备模板:

1
2
3
4
5
6
7
8
static struct miscdevice dht11_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "dht11",
.fops = &dht11_fops,
};

misc_register(&dht11_miscdev);
misc_deregister(&dht11_miscdev);

✅ CH6 本章复习题(DHT11 + GPIO子系统 + 设备树)

题1:简答题 ⭐⭐⭐

DHT11 为什么对时序要求严格?它与 I2C 传感器在时序控制上有什么不同?

参考答案要点 DHT11 使用单总线,只有一根数据线,没有单独时钟线,数据 0/1 依靠高电平持续时间区分,所以主机必须严格按微秒级时序采样。I2C 有 SCL 时钟线,通信时序由主机时钟控制,只需按 I2C 数据帧规则完成传输。

题2:计算题 ⭐⭐⭐

DHT11 一次传输 40 bit 数据,如果每 bit 由 50 us 低电平 + 70 us 高电平组成,粗略估计仅数据阶段最长需要多少毫秒?

参考答案要点 每 bit 最长约 `50 + 70 = 120 us`。40 bit 共:
1
40 × 120 us = 4800 us = 4.8 ms
实际还包括开始信号和响应信号等阶段。

题3:计算题 ⭐⭐

DHT11 读到 5 个字节分别为 45, 0, 26, 0, 71,判断校验是否正确,并写出温湿度值。

参考答案要点 校验和为:
1
45 + 0 + 26 + 0 = 71
与第5字节相同,校验正确。湿度为 45%,温度为 26℃。

题4:代码题 ⭐⭐⭐

写出将 GPIO 设置为输出低电平,再切换为输入读取电平的核心代码。

参考答案要点
1
2
3
4
5
6
7
8
gpio_direction_output(dht11_gpio, 0);
mdelay(18);

gpio_set_value(dht11_gpio, 1);
udelay(30);

gpio_direction_input(dht11_gpio);
val = gpio_get_value(dht11_gpio);

题5:代码题 ⭐⭐⭐

写出从设备树路径 /dht11_sensor 中获取名为 dht11-gpio 的 GPIO 编号的核心代码。

参考答案要点
1
2
3
4
5
6
7
8
9
10
struct device_node *node;
int gpio;

node = of_find_node_by_path("/dht11_sensor");
if (node == NULL)
return -EINVAL;

gpio = of_get_named_gpio(node, "dht11-gpio", 0);
if (gpio < 0)
return gpio;

题6:简答题 ⭐⭐⭐

说明 pinctrl 与 gpioctrl 的区别。

参考答案要点 pinctrl 负责引脚复用和电气属性配置,例如把某个引脚配置为 GPIO/I2C/SPI/UART 功能,以及上下拉、开漏、驱动能力等;gpioctrl 负责 GPIO 的具体输入输出操作,例如申请 GPIO、设置方向、读电平、写电平、释放 GPIO。

题7:综合题 ⭐⭐⭐

为什么 DHT11 驱动可以使用 misc 杂项设备?misc 设备相比普通字符设备框架有什么简化?

参考答案要点 DHT11 是简单传感器设备,不必单独分配完整字符设备类,适合 misc 设备。misc 设备主设备号固定为10,通过次设备号区分设备,注册时通常能自动创建设备节点,不需要手动 `mknod`,也不需要开发者显式 `class_create/device_create`,代码更简洁。

📕 CH7 炫彩LED、PWM 与独立按键输入子系统

本章对应第7讲“炫彩LED灯珠 独立按键”。重点掌握:SK9822 数据帧、PWM 的频率/占空比计算、GPIO按键电路、轮询缺点、input 子系统事件上报思想


1. SK9822 炫彩 LED ⭐⭐⭐

SK9822 是智能外控双线 LED 光源,将 RGB 晶片与控制 IC 集成在一起。

特点 说明
双线同步控制 DATA 数据线 + CLK 时钟线
可级联 多个灯珠可串接并同步工作
低功耗 内置恒流电路与 RC 振荡器
灰度/亮度 RGB 各 8 bit,亮度 5 bit
最大串行输入频率 30 MHz

引脚对应:

信号 作用
LED_DAT 数据线 SDI
LED_CLK 时钟线 CKI

SK9822 在 CKI 上升沿 读取 SDI 上的数据。


2. SK9822 数据帧格式 ⭐⭐⭐

写数据帧格式:

1
帧头 + LED帧 × N + 帧尾
部分 位数 内容
帧头 32 bit 全 0
LED帧 32 bit 亮度 + BGR颜色数据
帧尾 32 bit 全 1

LED帧格式:

1
2
3
4
5
bit[31:29] = 111
bit[28:24] = 5bit亮度,11111 最亮
bit[23:16] = Blue,8bit
bit[15:8] = Green,8bit
bit[7:0] = Red,8bit

例:亮度最亮、红色全亮、绿色蓝色为0,则 LED帧为:

1
2
11111111 00000000 00000000 11111111
= 0xFF0000FF

注意颜色顺序是 B、G、R,不是 RGB。


3. SK9822 GPIO 模拟时序 ⭐⭐⭐

因为 SK9822 使用 DATA + CLK 双线同步控制,所以可以用 GPIO 模拟 SPI 类似的时序。

发送 1 bit 的基本流程:

1
2
3
4
5
gpio_set_value(LED_DAT, bit);
gpio_set_value(LED_CLK, 1);
udelay(1);
gpio_set_value(LED_CLK, 0);
udelay(1);

发送 32 bit 可从高位到低位依次发送:

1
2
3
4
5
for (i = 31; i >= 0; i--) {
gpio_set_value(LED_DAT, (data >> i) & 0x1);
gpio_set_value(LED_CLK, 1);
gpio_set_value(LED_CLK, 0);
}

4. PWM 基本概念 ⭐⭐⭐

PWM 全称 Pulse Width Modulation,即脉冲宽度调制。通过改变脉冲高电平持续时间,等效得到不同的平均输出效果。

概念 公式
周期 T = T_high + T_low
频率 f = 1 / T
占空比 D = T_high / T × 100%

PWM 常用于:

1
2
3
4
电机转速控制
LCD 背光亮度控制
LED 亮度控制
蜂鸣器音调/响度控制

PWM 面积等效原理:冲量相等而形状不同的窄脉冲,加在具有惯性的环节上时,其效果基本相同。这里的冲量可以理解为脉冲面积。


5. SK9822 与 PWM 灰度 ⭐⭐⭐

SK9822 内部根据处理器发送的数据帧输出 PWM 波,改变 LED 晶体灰阶。对蓝、绿、红各 8 bit 灰阶信息:

数值 对应含义
0x00 关闭该颜色通道
0x80 约一半亮度
0xFF 最大亮度

5 bit 全局亮度用于整体亮度调节,范围 0~31,其中 11111 表示最亮。


6. RK3399 PWM 模块 ⭐⭐⭐

RK3399 有多个 PWM 模块。课件中强调:

1
RK3399 有 4 个 PWM 模块,每个 PWM 模块有 4 个通道。

实验箱 LCD 背光可由:

1
PWM0_BL(GPIO4_C2 复用)

控制。

Linux 下推荐使用 PWM 子系统 完成 PWM 配置,而不是直接手写寄存器。


7. 独立按键电路 ⭐⭐⭐

开发板四个按键分别接到:

1
2
3
4
GPIO1_A0
GPIO1_A1
GPIO1_A3
GPIO1_A4

按键电路特点:

状态 IO电平
未按下 上拉电阻作用下为高电平
按下 IO 接地,为低电平

因此判断按键按下时,常见逻辑是:

1
2
3
if (gpio_get_value(key_gpio) == 0) {
// key pressed
}

8. GPIO轮询按键的缺点 ⭐⭐⭐

普通 GPIO 轮询方式写按键驱动,常见问题有两个:

缺点 说明
没有消抖 机械按键抖动会导致一次按下被识别多次
死读/忙等 程序不断读取 GPIO,占用 CPU,效率低

改进方向:

1
2
3
4
GPIO中断 + 定时器消抖
input输入子系统
阻塞/非阻塞IO
poll/select

9. input 子系统 ⭐⭐⭐⭐

input 子系统是 Linux 为输入设备建立的统一框架,常见设备包括:

1
2
3
4
5
按键
键盘
鼠标
触摸屏
遥控器

input 子系统思想:

1
2
3
4
5
6
7
具体输入设备驱动
↓ 上报事件
input core
↓ 统一分发
event 设备节点

用户程序读取输入事件

驱动编写者重点做:

1
2
3
4
5
1. 申请 input_dev
2. 设置支持的事件类型和键值
3. 注册 input_dev
4. 在中断或扫描中上报事件
5. 同步事件

典型上报代码:

1
2
3
4
5
input_report_key(inputdev, KEY_0, 1);  // 按下
input_sync(inputdev);

input_report_key(inputdev, KEY_0, 0); // 松开
input_sync(inputdev);

✅ CH7 本章复习题(SK9822 + PWM + input)

题1:计算题 ⭐⭐⭐

SK9822 一个灯珠需要一个 32 bit LED帧,帧头 32 bit,帧尾 32 bit。若串接 4 个灯珠,一次刷新共需要发送多少 bit?多少字节?

参考答案要点 总位数:
1
帧头32 + 4 × 32 + 帧尾32 = 192 bit
字节数:
1
192 / 8 = 24 字节

题2:计算题 ⭐⭐⭐

PWM 周期为 20 ms,高电平持续 5 ms,求频率和占空比。

参考答案要点 频率:
1
f = 1 / 0.02s = 50 Hz
占空比:
1
D = 5ms / 20ms × 100% = 25%

题3:代码题 ⭐⭐⭐

写出用 GPIO 模拟 SK9822 发送 1 bit 的核心代码。

参考答案要点
1
2
3
4
5
6
7
8
static void sk9822_write_bit(int bit)
{
gpio_set_value(LED_DAT, bit ? 1 : 0);
gpio_set_value(LED_CLK, 1);
udelay(1);
gpio_set_value(LED_CLK, 0);
udelay(1);
}

题4:计算题 ⭐⭐⭐

SK9822 LED帧中,亮度最亮,蓝色 0x12,绿色 0x34,红色 0x56,请写出 32 bit 帧值。

参考答案要点 bit[31:29] 固定为 `111`,bit[28:24] 亮度最亮为 `11111`,所以最高字节为 `0xFF`。颜色顺序为 B、G、R:
1
0xFF123456

题5:简答题 ⭐⭐⭐

为什么普通 GPIO 轮询按键方式不理想?应如何改进?

参考答案要点 轮询方式会不断读取 GPIO,容易形成死读/忙等,占用 CPU;机械按键还会抖动,导致一次按下被识别多次。可改为 GPIO 外部中断触发,配合定时器消抖;或者使用 Linux input 子系统上报按键事件,由用户程序通过 `/dev/input/eventX` 读取。

题6:代码题 ⭐⭐⭐

写出 input 子系统中上报按键按下和松开的核心代码。

参考答案要点
1
2
3
4
5
input_report_key(inputdev, KEY_0, 1);
input_sync(inputdev);

input_report_key(inputdev, KEY_0, 0);
input_sync(inputdev);

题7:综合题 ⭐⭐⭐

比较 GPIO 字符设备按键驱动与 input 子系统按键驱动。

参考答案要点 GPIO 字符设备按键驱动实现简单,通常由用户程序直接读设备节点获取按键值,但容易出现无消抖、死读、接口不统一等问题。input 子系统是 Linux 标准输入设备框架,驱动负责上报事件,input core 统一分发,用户程序读取标准 event 设备,适合键盘、按键、触摸屏等输入设备,通用性和可扩展性更好。

📝 重点题型总结(考前速背版)

简答题:嵌入式系统定义与特点

定义要点: 以应用为中心、计算机技术为基础、软硬件可裁剪、专用计算机系统
特点记忆: 专裁可实低(专用性、可裁剪、可靠性、实时性、低功耗)

简答题:嵌入式处理器分类

MPU(微处理器)需外扩存储器;MCU(微控制器)片上集成CPU+RAM+Flash+I/O;DSP(数字信号处理器)专用于信号处理;SoC(片上系统)高度集成

简答题:ARM处理器工作模式

7种模式: usr(用户)、fiq(快速中断)、irq(外部中断)、svc(管理/复位默认)、abt(中止)、und(未定义指令)、sys(系统)
特权模式: 除usr外的6种;异常模式: fiq、irq、svc、abt、und(各有独立SPSR)

简答题:ARM寄存器

R13=SP(栈指针)、R14=LR(链接寄存器,保存返回地址)、R15=PC(程序计数器)
CPSR(当前状态寄存器)、SPSR(异常模式下保存CPSR副本,5种异常模式各1个)

程序分析题:ARM汇编指令

掌握指令功能识别:MOV(传送)、ADD/SUB(加减)、AND/ORR/BIC/EOR(位运算)、CMP(比较,设标志)、LDR/STR(存储器访问)、B/BL/BX(跳转)、MRS/MSR(状态寄存器访问)、PUSH/POP(栈操作)
LDR寻址辨析: 零偏移[Rn]、前索引[Rn,#off]!、后索引[Rn],#off

简答题:交叉开发环境

宿主机(PC + Linux + 交叉编译工具链)负责编辑编译;目标机(实验箱 + BootLoader + OS)负责运行
在线调试: JTAG(IBM/TI)、BDM(Motorola)

简答题:GCC编译过程

四阶段: 预处理(-E→.i) → 编译(-S→.s) → 汇编(-c→.o) → 链接(→可执行文件)

简答题:rootfs与kernel的关系

完全独立的两个部分。 单独内核无法工作,必须搭配rootfs(包含内核模块、/etc/fstab、/etc/inittab、应用程序、库文件等)

简答题:RK3399启动过程

记忆线索: 上电 → romcode(0xFFFF0000) → 查ID BLOCK(SPI NOR→NAND→eMMC→SD→USB) → BL1(SRAM, DDR初始化) → BL2(DDR, 加载OS) → 启动OS

简答题:Bootloader两种模式

启动加载模式 = 自主模式,从存储设备加载OS,产品发布用;下载模式 = 从主机下载,首次安装和更新用

简答题:BL1/BL2阶段功能

BL1(汇编):硬件初始化 → 准备RAM → 复制BL2 → 设栈 → 跳转C入口
BL2(C):初始化硬件 → 检测内存 → 读内核/根文件系统 → 设启动参数 → 调用内核

简答题:Linux内核主要功能

记忆:进程、内存、文件、设备、网络

简答题:内核空间与用户空间数据传递

copy_to_user = 内核→用户;copy_from_user = 用户→内核;两个空间不能直接访问

简答题:地址映射

ioremap = 物理地址→虚拟地址;iounmap = 释放映射;因为MMU开启后CPU只能访问虚拟地址

简答题:file_operations结构体

是Linux内核驱动操作函数集合,包含 open/read/write/release 等函数指针

简答题:设备号与设备文件

mknod /dev/chrdev c 200 0:c=字符设备,200=主设备号,0=次设备号;应用通过设备号使用驱动


💡 考试提示(Makefile 与驱动程序优先复习):

  • 嵌入式系统定义与特点(专用性、可裁剪、可靠性、实时性、低功耗)可能考填空/简答
  • 嵌入式处理器分类(MPU/MCU/DSP/SoC)需区分
  • ARM工作模式(7种,特别是usr/svc/irq/fiq)可能考简答或选择
  • ARM寄存器(R13=SP, R14=LR, R15=PC, CPSR/SPSR)必考
  • ARM指令(MOV/ADD/SUB/CMP/LDR/STR/B/BL/BX/PUSH/POP/MRS/MSR)必考程序分析
  • LDR寻址方式(零偏移、前索引、后索引)可能考辨析
  • RK3399启动过程(romcode地址、ID BLOCK查找顺序)必考简答
  • RK3399 GPIO四步(CRU→GRF→方向→电平)必考
  • Bootloader两种模式和BL1/BL2功能必考简答
  • GCC编译过程(预处理→编译→汇编→链接)及编译选项(-c/-o/-g/-O/-l)可能考
  • Linux内核五大功能必考简答
  • 字符设备驱动开发步骤(加载/卸载、注册/注销、数据传递、地址映射)为实验5重难点
  • Makefile编写(变量、自动变量、伪目标、内核模块Makefile)可能考简答或写Makefile
  • 根文件系统与内核的独立性可能考简答
  • ARM异常向量表(7种异常)需记忆地址偏移

📌 复习使用建议

  1. 第一轮: 先背每章的“定义 + 流程 + 表格”。
  2. 第二轮: 重点手写两类代码:普通 Makefile、内核模块 Makefile、最小字符设备驱动模板。
  3. 第三轮: 按本文件每章复习题闭卷做一遍,尤其是计算题和代码补全题。
  4. 考前最后30分钟: 只看“重点题型总结”和 CH4/实验5 的驱动模板。

题目说明:资料中未提供完整考研真题库,因此本版按课件、PDF实验指导和常见考研/期末命题方式整理为“真题风格/高频考点改编题”;涉及具体实验平台的题目优先采用 RK3399、LPKT030、chrdevbase、Makefile、Linux驱动程序等资料中的高频内容。

🧠 Linux 驱动零基础理解层【新增】Linux 驱动零基础理解层 ⭐⭐⭐⭐⭐

本节建议放在 CH4 Linux 驱动原理或实验5 设备驱动之前,用来解决“驱动 API 会背但不知道为什么用”的问题。

驱动程序可以理解为:

1
应用程序和硬件之间的翻译层

应用程序不能直接操作硬件寄存器,因为用户态没有权限,也不知道具体硬件地址。
所以应用层通常通过系统调用访问设备:

1
2
3
4
open();
read();
write();
close();

这些系统调用进入内核后,会转到驱动中注册好的函数:

1
2
3
4
.open
.read
.write
.release

对应关系:

应用层函数 驱动层函数 含义
open("/dev/xxx") .open 打开设备
read(fd, buf, len) .read 从设备读取数据
write(fd, buf, len) .write 向设备写入数据
close(fd) .release 关闭设备

为什么要 copy_to_user / copy_from_user? ⭐⭐⭐⭐⭐

Linux 把内存分为用户空间和内核空间。
驱动不能直接拿用户传进来的指针随便访问,否则可能导致安全问题或内核崩溃。

函数 方向 解释
copy_to_user 内核 → 用户 驱动把数据交给应用程序
copy_from_user 用户 → 内核 应用程序把数据交给驱动

速记:

1
2
to_user:给用户
from_user:从用户拿

为什么要 ioremap? ⭐⭐⭐⭐⭐

外设寄存器地址通常是物理地址。
Linux 开启 MMU 后,内核代码不能像裸机程序一样直接把物理地址当指针用,而要先映射成内核虚拟地址:

1
物理地址 → ioremap → 内核虚拟地址 → readl/writel 访问

答题模板:

Linux 开启 MMU 后,内核访问的是虚拟地址空间,不能直接把外设物理地址当作普通指针使用。应先用 ioremap 将外设物理地址映射为内核虚拟地址,再通过 readl/writel 访问寄存器。