嵌入式 考点总结
修改说明:本版本根据反馈补充了“指令/命令解释层”“知识点普及层”和“五星评价体系增强版”,重点增强 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 |
| 为什么要用它 | 调用函数时必须记住函数执行完后回到哪里 |
| 考试怎么问 | 区分 B 和 BL;解释 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 | 嵌入式系统 = 硬件系统 + 软件系统 |
- 最小系统: 处理器 + 最少的外围电路(时钟、复位、电源)
- 简单嵌入式系统: 硬件 + 监控程序(无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 */
main() {
int m;
extern int sum(int sum);
scanf("%d", &m);
printf("The sum is %d", sum(m));
}1
2gcc 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 | ┌─────────────────────────┐ |
编译命令:1
2
3./build.sh uboot # 编译Uboot
./build.sh kernel # 编译内核
./build.sh rootfs # 编译根文件系统
10. CH1 高频考点补充:GCC、make、Buildroot 的关系 ⭐⭐⭐
这一章容易把工具链概念混在一起,考试一般不是单纯问定义,而是给一个编译现象或命令,让你判断发生在哪一层。
| 名词 | 作用 | 典型命令/文件 | 高频考法 |
|---|---|---|---|
gcc |
编译器,完成预处理、编译、汇编、链接 | gcc -E/-S/-c/-o |
判断编译阶段与输出文件 |
make |
自动化构建工具,按依赖关系决定是否重新编译 | make、make 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 | gcc -E hello.c -o hello.i |
参考答案要点
`hello.i` 是预处理结果,已展开头文件和宏;`hello.s` 是编译后生成的汇编代码;`hello.o` 是汇编后生成的目标文件;`hello` 是链接后生成的可执行文件。题4:代码题 ⭐⭐⭐
写出一条命令,将 main.c、input.c、calcu.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 | 数据处理指令:主要在寄存器里算东西 |
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 | STMFD SP!, {R0, R4-R12, LR} @ 多寄存器入栈(保存现场) |
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
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 | ldr w1, [x0, #0x0004] @ 先读出寄存器原值 |
如果直接写:
1 | mov w1, #0x04 |
虽然 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 | val = readl(reg); |
裸机汇编中常写成
ldr → orr/bic → str;Linux驱动中不能直接访问物理地址,要先ioremap,再用readl/writel。
✅ CH2 本章复习题(计算题 + 汇编/寄存器题)
题1:简答题 ⭐⭐
说明 ARM 的 RISC 特点,并与 CISC 做简要对比。
参考答案要点
RISC 精简指令集优先保留高频简单指令,指令长度较固定,寻址方式相对简单,大量操作在寄存器中完成,便于流水线执行,低功耗、低成本、高效率;CISC 指令复杂、功能强但硬件实现复杂,指令长度和执行周期不统一。题2:计算题 ⭐⭐⭐
已知 GPIO0 基地址为 0xFF720000,GPIO_SWPORTA_DDR 偏移为 0x0004,GPIO_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 | mov x0, #0xFF720000 |
参考答案要点
1 | orr w1, w1, #0x04 |
1 | bic w1, w1, #0x04 |
题4:程序分析题 ⭐⭐⭐
解释下面两条指令的区别:
1 | B loop |
参考答案要点
`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
2make rk3399_defconfig # 加载默认配置
make menuconfig # 图形化配置
3. Linux内核启动流程 ⭐⭐
Image的生成过程:1
vmlinux(原始内核,~23MB)→[objcopy去掉符号表]→ Image(纯内核镜像,~18MB)
Linux启动流程:1
2
3
4
5arch/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_defconfig、make menuconfig、bootm |
不是Linux内核本身 |
| Kernel | 操作系统核心,管理进程、内存、文件、设备、网络 | ./build.sh kernel、Image、start_kernel() |
单独内核不能正常完成用户交互 |
| Rootfs | 内核挂载的第一个文件系统 | /etc/fstab、/etc/inittab、/dev、/lib/modules |
不是“可有可无的目录包” |
.config |
保存配置结果 | CONFIG_CMD_BOOTM=y |
由 defconfig/menuconfig 生成或修改 |
Kconfig |
描述菜单项 | source "xxx/Kconfig" |
不直接负责编译,负责生成配置选项 |
Makefile |
描述编译规则 | obj-y、obj-m、make -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 | ./build.sh uboot |
题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
开机流程(按顺序):
- 将各类天线、网线正确接入
- 将 Type-C 数据线插入 debug 或 download 口
- 确保主板电源开关处于靠左关闭状态
- 插入白色 12V 电源插头
- 向右拨动电源开关启动系统
⚠️ 请严格按照以上步骤开机,减少瞬时电涌对主板接口的损害。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 | sudo apt-get install nfs-kernel-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操作步骤(裸机):
- 使能GPIO时钟
- 设置GPIO复用功能(配置MUX寄存器)
- 设置GPIO方向(输入/输出)
- 设置GPIO输出电平(高低)
RK3399关键寄存器:
- PMUGRF_GPIO0A_IOMUX(地址
0xFF310000):GPIO复用功能配置 - GPIO0_SWPORTA_DR:GPIO数据寄存器(写数据)
- GPIO0_SWPORTA_DDR:GPIO方向寄存器(0=输入,1=输出)
2. ARM启动文件(start.S) ⭐⭐⭐
start.S完成的主要功能:
- 设置异常向量表(Exception Vector)
- 关闭 IRQ、FIQ,设置 SVC 模式
- 关闭 L1 cache,设置 L2 cache,关闭 MMU
- 根据 OM 引脚确定启动方式
- 在 SoC 内部 SRAM 中设置栈
- 执行
lowlevel_init函数(初始化系统时钟、SDRAM、串口等) - 设置 SDRAM 中的栈
⚠️ 异常向量表必须从
0x00000000或0xFFFF0000开始,每条向量占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
5MRS 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
3LDR 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 | 目标(target): 依赖(prerequisites) |
规则:
目标:要生成的文件,或是一个”伪目标”(如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 |
|
为什么需要
.PHONY? 若目录下存在一个名为clean的文件,make clean会认为目标已最新而跳过。.PHONY告诉 make 无论是否有同名文件都执行该规则。
裸机开发完整 Makefile 示例
1 | # 交叉编译工具链前缀 |
内核模块 Makefile(实验5 用)
1 | # 内核模块 Makefile(放在模块源码同一目录) |
关键点: 内核模块 Makefile 与普通 Makefile 不同,
obj-m表示编译为模块,-C指定内核源码目录,M=$(PWD)指定模块源码目录。
RK3399实际交叉编译 Makefile(课件原版)
1 | # RK3399 裸机交叉编译 Makefile(带链接脚本) |
与简化版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 或交叉编译器 |
由内核构建系统调用 |
| 关键变量 | CC、CFLAGS、OBJS、TARGET |
KERNELDIR、CURRENT_PATH、obj-m |
| 典型命令 | $(CC) $^ -o $@ |
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules |
| 清理方式 | rm -f *.o app |
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean |
6.2 自动变量必背
| 自动变量 | 含义 | 高频例子 |
|---|---|---|
$@ |
当前目标 | main、led.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 | 目标文件依赖哪些文件; |
最小规则结构:
1 | 目标: 依赖 |
其中命令前必须是 Tab,不能是空格。因为 make 依靠 Tab 判断这一行是“要执行的命令”。
自动变量展开示例
1 | app: main.o input.o calcu.o |
展开后等价于:
1 | gcc main.o input.o calcu.o -o app |
| 自动变量 | 在本例中代表 | 含义 |
|---|---|---|
$@ |
app |
当前目标 |
$^ |
main.o input.o calcu.o |
所有依赖 |
$< |
main.o |
第一个依赖 |
.PHONY 为什么重要?
1 |
|
如果目录下刚好有一个名为 clean 的文件,而没有写 .PHONY,make clean 可能会认为 clean 目标已经存在,不再执行清理命令。.PHONY 的作用是告诉 make:clean 不是普通文件名,而是一个“动作名”。
✅ 实验2 本章复习题(Makefile + 裸机代码)
题1:代码题 ⭐⭐⭐
写一个最小 Makefile:把 main.c input.c calcu.c 编译为可执行文件 main,要求使用变量、模式规则、自动变量,并提供 clean 伪目标。
参考答案要点
1 | CC := gcc |
题2:计算题 ⭐⭐
若某寄存器当前值为 0x00000000,需要将 bit2 和 bit5 置1,不改变其他位,应与哪个掩码做或操作?结果是多少?
参考答案要点
掩码为 `(1 << 2) | (1 << 5) = 0x04 | 0x20 = 0x24`。结果为 `0x00000024`。题3:汇编题 ⭐⭐⭐
解释下面裸机编译流程中每一步的作用:
1 | arm-linux-gnueabihf-gcc -c start.S -o start.o |
参考答案要点
第一行把启动汇编编译成目标文件;第二行把C文件编译成目标文件;第三行把目标文件链接为ELF文件,并指定链接地址为 `0x00000000`;第四行从ELF中提取纯二进制镜像 `led.bin`,用于裸机烧写/运行。题4:Makefile分析题 ⭐⭐⭐
已知规则:
1 | app: a.o b.o c.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中断配置步骤:
- 使能GPIO时钟
- 设置GPIO复用功能为GPIO模式
- 设置GPIO方向为输入
- 配置GPIO中断触发方式(上升沿/下降沿/高电平/低电平)
- 使能GPIO中断
- 注册中断服务函数(ISR)
按键消抖: 软件延时消抖(约10~20ms)
3. ARM异常处理 ⭐⭐
异常向量表:
| 地址偏移 | 异常类型 |
|---|---|
| 0x00 | Reset |
| 0x04 | Undefined Instruction |
| 0x08 | SWI/SVC |
| 0x0C | Prefetch Abort |
| 0x10 | Data Abort |
| 0x14 | 未使用 |
| 0x18 | IRQ |
| 0x1C | FIQ |
中断处理流程:
- 保存现场(将寄存器压栈)
- 执行中断服务程序(ISR)
- 恢复现场(寄存器出栈)
- 中断返回
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 | MRS R0, CPSR |
参考答案要点
1 | ORR R0, R0, #0xC0 |
题4:流程题 ⭐⭐⭐
说明一次外部按键中断从发生到返回的大致处理流程。
参考答案要点
外设产生中断信号;中断控制器记录并分发中断;CPU响应中断并跳转异常向量;保存现场;执行ISR;读取/判断中断源;处理中断事件;清除中断标志;恢复现场;返回被中断程序继续执行。题5:综合题 ⭐⭐
为什么按键中断通常需要消抖?软件消抖常见方法是什么?
参考答案要点
机械按键在按下或松开瞬间会产生电平抖动,可能触发多次中断。软件消抖常用延时约10~20ms后再次读取按键状态,确认稳定后再处理。📕 实验4 Uboot与Linux系统
1. RK3399启动过程 ⭐⭐⭐
完整启动流程:
1 | 上电复位 → romcode → BL1(SRAM)→ BL2(DDR)→ OS Kernel → Rootfs |
详细步骤:
- RK3399系统上电复位后,Cortex-A53从地址
0xFFFF0000执行romcode - romcode依次按以下顺序查找介质上的ID BLOCK信息:
- SPI NOR FLASH → SPI NAND FLASH → eMMC → SD/MMC → USB
- romcode功能:查找ID BLOCK → 确定启动方式 → 初始化硬件 → 将BL1加载到内部SRAM
- BL1执行DDR初始化,将BL2复制到DDR,跳转到BL2
- BL2初始化硬件和软件,将OS复制到DDR,跳转到OS
- 启动操作系统
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阶段):
- 指定Uboot入口
- 设置异常向量
- 关闭IRQ、FIQ,设置SVC模式
- 关闭L1 cache,设置L2 cache,关闭MMU
- 根据OM引脚确定启动方式
- 在SoC内部SRAM中设置栈
- 执行
lowlevel_init(初始化系统时钟、SDRAM、串口) - 设置开发板供电锁存
- 设置SDRAM中的栈
_main 函数(BL1→BL2过渡):
- 将Uboot从存储介质复制到SDRAM(BL2加载到RAM并跳转)
- 设置并开启MMU
- 在SDRAM中合适位置设置栈
- 清除BSS段,BL1阶段完毕
board_init_r 函数(BL2阶段):
- 规划Uboot内存使用
- 遍历调用
init_sequence初始化函数数组 - 初始化堆管理器
- 初始化SD/MMC控制器
- 环境变量重定位
- 目标机硬件设备初始化
- 控制台初始化
- 网卡芯片初始化
- 进入主循环
main_loop(出现Uboot提示符)
4. Linux内核 ⭐⭐
Linux内核主要功能:
| 功能 | 说明 |
|---|---|
| 进程管理 | 进程创建/销毁,调度策略,处理器资源共享,进程间通信 |
| 内存管理 | 虚拟地址空间,地址映射,多进程安全共享内存 |
| 文件管理 | 虚拟文件系统(VFS),支持ext3/ext4等数十种文件系统 |
| 设备管理 | 设备驱动程序,每种外设对应特定驱动代码 |
| 网络管理 | 网络协议栈 + 网络设备驱动程序 |
Linux内核目录结构:
| 目录 | 内容 |
|---|---|
arch |
架构相关代码(ARM、x86等) |
block |
块设备(SD卡、eMMC、NAND、硬盘等) |
drivers |
驱动程序(drivers/i2c、drivers/gpio等) |
fs |
文件系统 |
init |
初始化代码(含 main.c 的 start_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-boot 与 make_uboot.sh |
uboot.img、trust.img、loader |
启动加载相关 |
./build.sh kernel |
kernel 与 make_kernel.sh |
Image、kernel.img、boot.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 | make rk3399_defconfig |
题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 | 应用程序 open/read/write/close |
3. Linux 对设备的分类 ⭐⭐⭐
| 类型 | 访问特点 | 典型设备 | 高频考点 |
|---|---|---|---|
| 字符设备 | 按字节流顺序访问 | LED、按键、I2C、SPI、UART、触摸屏 | file_operations、设备号、/dev 节点 |
| 块设备 | 按块访问,支持缓存和随机读写 | 硬盘、U盘、eMMC、SD卡、NAND | 文件系统基础,常与缓冲区相关 |
| 网络设备 | 按数据包收发,不一定映射为 /dev 文件 |
eth0、wlan0、lo |
通过网络子系统和接口名访问 |
高频辨析:
/dev/xxx是设备文件/设备节点,是访问驱动的接口;.ko才是编译后的驱动模块文件。
4. 设备号:主设备号与次设备号 ⭐⭐⭐⭐
Linux 用 dev_t 表示设备号,通常由 主设备号 major 和 次设备号 minor 组成。
| 项目 | 含义 |
|---|---|
| 主设备号 major | 标识驱动程序,同一类设备通常共用同一驱动 |
| 次设备号 minor | 标识同一驱动下的具体设备实例 |
| 设备类型 | c 表示字符设备,b 表示块设备 |
dev_t 高频宏:
1 |
设备节点创建:
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 | static struct file_operations xxx_fops = { |
6. 字符设备注册:老框架与新框架 ⭐⭐⭐⭐⭐
6.1 老框架:register_chrdev
1 | register_chrdev(major, name, &fops); |
优点是简单;缺点是主设备号易冲突、设备节点常需手动 mknod。
6.2 新框架:dev_t + cdev + class + device
新字符设备框架更符合现代 Linux 驱动写法:
1 | alloc_chrdev_region/register_chrdev_region |
卸载时反向释放:
1 | device_destroy |
| 函数 | 作用 |
|---|---|
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 | copy_to_user(user_buf, kernel_buf, len); // 内核 → 用户 |
| 函数 | 方向 | 常见场景 |
|---|---|---|
copy_to_user |
内核空间 → 用户空间 | 驱动 read() 返回数据 |
copy_from_user |
用户空间 → 内核空间 | 驱动 write() 接收控制命令 |
高频坑:返回值表示未成功拷贝的字节数。通常
0表示全部成功,非0表示还有数据未拷贝成功。
8. 地址映射与寄存器访问 ⭐⭐⭐
Linux 开启 MMU 后,驱动不能像裸机一样直接访问物理地址。必须先把寄存器物理地址映射为内核虚拟地址。
1 | void __iomem *reg; |
卸载时释放映射:
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 | u32 val; |
9. 驱动模块 Makefile 必背 ⭐⭐⭐
这是驱动代码题最高频模板。命令行必须以 Tab 开头。
1 | KERNELDIR := /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel |
| 行 | 含义 |
|---|---|
KERNELDIR |
内核源码目录 |
CURRENT_PATH |
当前驱动源码目录 |
obj-m := chrdevbase.o |
把 chrdevbase.c 编译成模块 chrdevbase.ko |
-C $(KERNELDIR) |
切换到内核源码目录执行构建 |
M=$(CURRENT_PATH) |
告诉内核构建系统,外部模块源码在当前目录 |
modules |
编译模块 |
10. 驱动调试命令速查 ⭐⭐⭐
1 | # 编译驱动模块 |
✅ 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 | static struct file_operations chrdev_fops = { |
题4:代码题 ⭐⭐⭐
写出内核模块入口/出口函数注册模板,并添加 GPL 协议声明。
参考答案要点
1 | static int __init xxx_init(void) |
题5:代码题 ⭐⭐⭐
在驱动 read() 中,应使用哪个函数把内核缓冲区 kbuf 的 len 字节复制到用户缓冲区 buf?在 write() 中方向相反又该使用哪个函数?
参考答案要点
1 | copy_to_user(buf, kbuf, len); // read: 内核 → 用户 |
题6:计算题 ⭐⭐
执行命令:
1 | mknod /dev/demo c 100 0 |
分别说明 /dev/demo、c、100、0 的含义。
参考答案要点
`/dev/demo` 是设备节点路径;`c` 表示字符设备;`100` 是主设备号;`0` 是次设备号。题7:Makefile代码题 ⭐⭐⭐
将 led.c 编译为内核模块 led.ko,内核源码路径为 /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel,写出 Makefile。
参考答案要点
1 | KERNELDIR := /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel |
题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
2module_init(xxx_init); // 注册模块加载函数,insmod时调用xxx_init
module_exit(xxx_exit); // 注册模块卸载函数,rmmod时调用xxx_exit
模块加载命令:1
2
3insmod drv.ko # 加载指定模块
modprobe drv.ko # 加载模块(自动处理依赖)
rmmod drv.ko # 卸载模块
4. 字符设备注册与注销 ⭐⭐⭐⭐⭐
1 | // 注册字符设备 |
设备号:
- 每个设备有一个设备号,分为主设备号和次设备号
- 应用程序通过设备号使用设备驱动
- 主设备号:标识驱动程序(同类驱动共用)
- 次设备号:标识具体设备实例
创建设备文件:1
mknod /dev/chrdev c 200 0 # c=字符设备 200=主设备号 0=次设备号
5. 内核空间与用户空间数据传递 ⭐⭐⭐
两个空间不能直接互相访问,必须使用专用函数:
1 | // 内核空间 → 用户空间 |
| 参数 | 含义 |
|---|---|
to |
传递的目标地址 |
from |
传递的起始地址 |
count |
传递的数据长度 |
| 返回值 | 未成功传递的数据长度 |
6. 地址映射(MMU) ⭐⭐⭐
MMU(Memory Manage Unit)功能:
- 虚拟空间到物理空间的映射
- 内存保护,设置访问权限和缓冲特性
关键概念:
- 虚拟地址(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. 字符设备驱动开发完整步骤 ⭐⭐⭐⭐⭐
- 编写驱动代码:实现
file_operations中的 open/release/read/write - 编写Makefile:使用内核构建系统(
KDIR指向内核源码目录) - 编译驱动:
make生成.ko模块文件 - 编译测试程序:
aarch64-linux-gnu-gcc chrdevApp.c -o chrdevApp - 加载模块:
insmod chrdev.ko - 创建设备文件:
mknod /dev/chrdev c 200 0 - 运行测试:
./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 | static ssize_t led_write(struct file *filp, |
✅ 实验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 | static int __init chrdevbase_init(void) |
参考答案要点
1 | ret = register_chrdev(200, "chrdevbase", &chrdevbase_fops); |
题3:计算题 ⭐⭐
一次 read(fd, buf, 50) 期望读取50字节,实际返回值为32,表示什么?若返回0或负数分别表示什么?
参考答案要点
返回32表示成功读取了部分数据,共32字节;返回0通常表示到达文件末尾或没有数据;返回负数表示读取失败,应根据错误码判断原因。题4:代码题 ⭐⭐⭐
写出模块卸载时应完成的资源释放操作:已知主设备号 200,设备名 chrdevbase。
参考答案要点
1 | static void __exit chrdevbase_exit(void) |
题5:综合改错题 ⭐⭐⭐
下面驱动代码有什么问题?
1 |
|
参考答案要点
这是裸机式直接访问物理地址的写法,在 Linux 驱动中 MMU 已开启,不能直接把物理地址当指针访问。应使用 `ioremap(0xFF720000, 4)` 得到内核虚拟地址,再通过 `readl/writel` 完成读改写。题6:Makefile分析题 ⭐⭐⭐
解释下面命令中 -C 和 M= 的作用:
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 | BEEP_IO 输出高/低电平 |
高频点:GPIO 并不是直接“发声”,而是控制三极管;三极管再控制蜂鸣器回路。
3. 蜂鸣器 Linux 驱动思路 ⭐⭐⭐
蜂鸣器本质是一个 GPIO 输出设备,Linux 驱动思路与 LED 类似。
基本流程:
1 | 1. 获取/配置 BEEP_IO 对应引脚 |
如果使用寄存器方式,需要掌握:
1 | reg = ioremap(PHYS_ADDR, size); |
如果使用 GPIO 子系统,则优先使用:
1 | gpio_request(gpio, "beep"); |
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 | I2C core |
| 层次 | 含义 | 谁来写 |
|---|---|---|
| I2C core | 提供注册、注销、匹配、传输封装等通用代码 | Linux内核 |
| I2C adapter | SoC 内置 I2C 控制器的软件抽象 | 芯片/板厂通常已写好 |
| I2C client driver | 具体 I2C 设备驱动,如 AP3216C | 驱动开发者重点编写 |
对开发者而言,通常说“写 I2C 驱动”,指的是写 I2C 设备驱动,不是重新写 I2C 主机控制器驱动。
7. i2c_adapter、i2c_client、i2c_driver ⭐⭐⭐
| 结构体 | 作用 | 高频理解 |
|---|---|---|
i2c_adapter |
表示一个 I2C 控制器/适配器 | 对应 SoC 中的 I2C 主机 |
i2c_algorithm |
表示适配器的通信算法 | 核心函数常为 master_xfer |
i2c_client |
表示挂在 I2C 总线上的一个从设备 | 包含设备地址 client->addr |
i2c_driver |
表示某类 I2C 设备的驱动 | 匹配成功后执行 probe() |
i2c_driver 典型模板:
1 | static const struct i2c_device_id ap3216c_id[] = { |
8. AP3216C 驱动调用链 ⭐⭐⭐
AP3216C 驱动中,常见读数据流程如下:
1 | App: open/read |
匹配成功后,在 probe() 中通常要做:
1 | 1. 保存 i2c_client |
常见写法:
1 | ap3216cdev.private_data = client; |
这样后续 read() 中就能通过私有数据找到当前 I2C 从设备地址和适配器。
9. I2C 读写时序与 i2c_transfer ⭐⭐⭐⭐
I2C 写寄存器一般需要发送:
1 | 从设备地址 + 写方向 |
I2C 读寄存器通常分两步:
1 | 第一步:写寄存器地址 |
在 Linux 中常用 struct i2c_msg 描述传输消息:
1 | struct i2c_msg 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 |
题3:代码题 ⭐⭐⭐
写出 AP3216C 作为 I2C 设备驱动时,i2c_driver 至少应包含的两个回调函数名称。
参考答案要点
1 | static int ap3216c_probe(struct i2c_client *client, |
题4:程序分析题 ⭐⭐⭐
解释 client->addr 与 i2c_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 | struct i2c_msg 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 | 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 | data <<= 1; |
高频点:读每一位时常先左移一位,再把本轮读到的 0/1 放入最低位。
5. 为什么 DHT11 适合用 GPIO 子系统 ⭐⭐⭐⭐
DHT11 数据线需要一会儿输出、一会儿输入:
| 阶段 | GPIO方向 |
|---|---|
| 总线空闲 | 输出高电平 |
| 主机开始信号 | 输出低电平,再输出高电平 |
| 读取响应 | 输入 |
| 读取40bit数据 | 输入 |
如果继续像 LED/蜂鸣器那样手动操作 GPIO 方向寄存器和数据寄存器,代码会很繁琐。使用 GPIO 子系统后,可以直接调用通用 API。
6. pinctrl 子系统与 gpioctrl 子系统 ⭐⭐⭐
Linux 对引脚管理通常分为两类:
| 子系统 | 负责内容 | 典型作用 |
|---|---|---|
| pinctrl | 引脚复用和电气属性 | 配置 pinmux、上下拉、驱动能力等 |
| gpioctrl | GPIO 输入输出控制 | 设置方向、读值、写值、释放GPIO |
pinctrl 主要负责:
1 | pinmux:引脚复用,如 GPIO/I2C/SPI/UART |
gpioctrl 常用 API:
1 | gpio_request(gpio, "dht11"); |
7. 设备树与 GPIO 获取 ⭐⭐⭐
设备树 DTS 用树形结构描述板级硬件信息,例如 CPU、内存、I2C、SPI、GPIO、传感器等。.dts 经 DTC 编译后生成 .dtb。
Linux 驱动中常通过设备树节点获取 GPIO 编号:
1 | struct device_node *node; |
| 函数 | 作用 |
|---|---|
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 | static struct miscdevice 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 |
题4:代码题 ⭐⭐⭐
写出将 GPIO 设置为输出低电平,再切换为输入读取电平的核心代码。
参考答案要点
1 | gpio_direction_output(dht11_gpio, 0); |
题5:代码题 ⭐⭐⭐
写出从设备树路径 /dht11_sensor 中获取名为 dht11-gpio 的 GPIO 编号的核心代码。
参考答案要点
1 | struct device_node *node; |
题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 | bit[31:29] = 111 |
例:亮度最亮、红色全亮、绿色蓝色为0,则 LED帧为:
1 | 11111111 00000000 00000000 11111111 |
注意颜色顺序是 B、G、R,不是 RGB。
3. SK9822 GPIO 模拟时序 ⭐⭐⭐
因为 SK9822 使用 DATA + CLK 双线同步控制,所以可以用 GPIO 模拟 SPI 类似的时序。
发送 1 bit 的基本流程:
1 | gpio_set_value(LED_DAT, bit); |
发送 32 bit 可从高位到低位依次发送:
1 | for (i = 31; i >= 0; i--) { |
4. PWM 基本概念 ⭐⭐⭐
PWM 全称 Pulse Width Modulation,即脉冲宽度调制。通过改变脉冲高电平持续时间,等效得到不同的平均输出效果。
| 概念 | 公式 |
|---|---|
| 周期 | T = T_high + T_low |
| 频率 | f = 1 / T |
| 占空比 | D = T_high / T × 100% |
PWM 常用于:
1 | 电机转速控制 |
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 | GPIO1_A0 |
按键电路特点:
| 状态 | IO电平 |
|---|---|
| 未按下 | 上拉电阻作用下为高电平 |
| 按下 | IO 接地,为低电平 |
因此判断按键按下时,常见逻辑是:
1 | if (gpio_get_value(key_gpio) == 0) { |
8. GPIO轮询按键的缺点 ⭐⭐⭐
普通 GPIO 轮询方式写按键驱动,常见问题有两个:
| 缺点 | 说明 |
|---|---|
| 没有消抖 | 机械按键抖动会导致一次按下被识别多次 |
| 死读/忙等 | 程序不断读取 GPIO,占用 CPU,效率低 |
改进方向:
1 | GPIO中断 + 定时器消抖 |
9. input 子系统 ⭐⭐⭐⭐
input 子系统是 Linux 为输入设备建立的统一框架,常见设备包括:
1 | 按键 |
input 子系统思想:
1 | 具体输入设备驱动 |
驱动编写者重点做:
1 | 1. 申请 input_dev |
典型上报代码:
1 | input_report_key(inputdev, KEY_0, 1); // 按下 |
✅ 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 | static void sk9822_write_bit(int bit) |
题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 | input_report_key(inputdev, KEY_0, 1); |
题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种异常)需记忆地址偏移
📌 复习使用建议
- 第一轮: 先背每章的“定义 + 流程 + 表格”。
- 第二轮: 重点手写两类代码:普通 Makefile、内核模块 Makefile、最小字符设备驱动模板。
- 第三轮: 按本文件每章复习题闭卷做一遍,尤其是计算题和代码补全题。
- 考前最后30分钟: 只看“重点题型总结”和 CH4/实验5 的驱动模板。
题目说明:资料中未提供完整考研真题库,因此本版按课件、PDF实验指导和常见考研/期末命题方式整理为“真题风格/高频考点改编题”;涉及具体实验平台的题目优先采用 RK3399、LPKT030、
chrdevbase、Makefile、Linux驱动程序等资料中的高频内容。
🧠 Linux 驱动零基础理解层【新增】Linux 驱动零基础理解层 ⭐⭐⭐⭐⭐
本节建议放在 CH4 Linux 驱动原理或实验5 设备驱动之前,用来解决“驱动 API 会背但不知道为什么用”的问题。
驱动程序可以理解为:
1 | 应用程序和硬件之间的翻译层 |
应用程序不能直接操作硬件寄存器,因为用户态没有权限,也不知道具体硬件地址。
所以应用层通常通过系统调用访问设备:
1 | open(); |
这些系统调用进入内核后,会转到驱动中注册好的函数:
1 | .open |
对应关系:
| 应用层函数 | 驱动层函数 | 含义 |
|---|---|---|
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 | to_user:给用户 |
为什么要 ioremap? ⭐⭐⭐⭐⭐
外设寄存器地址通常是物理地址。
Linux 开启 MMU 后,内核代码不能像裸机程序一样直接把物理地址当指针用,而要先映射成内核虚拟地址:
1 | 物理地址 → ioremap → 内核虚拟地址 → readl/writel 访问 |
答题模板:
Linux 开启 MMU 后,内核访问的是虚拟地址空间,不能直接把外设物理地址当作普通指针使用。应先用
ioremap将外设物理地址映射为内核虚拟地址,再通过readl/writel访问寄存器。





