Pārlūkot izejas kodu

lint: 按 PDF 原文全面校对嵌入式驱动知识库

- 46 个文件对照正点原子《I.MX6U嵌入式Linux驱动开发指南》原文逐章核对
- 修正寄存器地址/位域(GIC、GPIO 电气属性、CCM 时钟)、API 签名与事实性错误(bootz、ttymxc0、can_id 位等)
- 补全缺失内容(读写锁与顺序锁 _bh API、U-Boot NAND 命令、init_sequence_f/r、serial_rs485 等)
- 修正编造的结构体字段与函数名(can_priv、__ATTR*、iio_attr 等)
- 教材外内容统一标注来源说明
OpenCode 1 dienu atpakaļ
vecāks
revīzija
c19381d7c9
46 mainītis faili ar 6853 papildinājumiem un 1551 dzēšanām
  1. 50 14
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/01-Cortex-A7架构详解.md
  2. 199 48
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/02-ARM汇编基础.md
  3. 145 49
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/03-时钟与电源管理.md
  4. 158 16
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/04-GPIO与中断系统.md
  5. 196 69
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/05-定时器与看门狗.md
  6. 4 1
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/06-UART串口通信.md
  7. 6 2
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/07-BSP工程管理与封装.md
  8. 459 179
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/08-裸机实验总结.md
  9. 61 44
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/面试-ARM架构与裸机.md
  10. 296 34
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/01-内核编译与配置.md
  11. 137 30
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/02-内核模块编程.md
  12. 99 62
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/03-系统调用与VFS.md
  13. 63 40
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/04-进程调度与中断管理.md
  14. 44 23
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/05-内存管理.md
  15. 65 34
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/06-设备模型与Kobject.md
  16. 34 16
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/面试-Linux内核基础.md
  17. 177 15
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/01-字符设备驱动基础.md
  18. 182 25
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/02-设备树语法与实战.md
  19. 446 72
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/03-pinctrl与gpio子系统.md
  20. 784 53
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/04-并发同步与原子操作.md
  21. 230 4
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/05-中断下半部处理.md
  22. 129 4
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/06-阻塞IO与poll机制.md
  23. 118 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/07-platform总线模型.md
  24. 103 3
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/08-misc与input子系统.md
  25. 114 32
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/面试-Linux驱动开发.md
  26. 80 6
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/01-I2C驱动开发.md
  27. 31 6
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/02-SPI驱动开发.md
  28. 165 30
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/03-USB驱动开发.md
  29. 84 4
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/04-CAN驱动开发.md
  30. 93 7
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/05-RS232与485通信.md
  31. 22 13
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/面试-总线接口驱动.md
  32. 70 2
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/01-LED与蜂鸣器驱动.md
  33. 62 14
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/02-按键驱动.md
  34. 148 33
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/03-LCD驱动.md
  35. 95 19
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动.md
  36. 262 26
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/05-音频驱动.md
  37. 71 20
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/06-RTC与PWM驱动.md
  38. 289 74
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/07-ADC与IIO驱动.md
  39. 125 25
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/08-块设备驱动.md
  40. 35 23
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/面试-外设驱动实战.md
  41. 181 28
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/01-U-Boot命令与使用.md
  42. 485 279
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/02-U-Boot启动流程.md
  43. 58 12
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/03-Linux内核裁剪与编译.md
  44. 51 6
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/04-根文件系统构建.md
  45. 79 3
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/05-系统烧写与量产.md
  46. 68 52
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/面试-系统移植.md

+ 50 - 14
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/01-Cortex-A7架构详解.md

@@ -21,7 +21,7 @@ pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第
 | ARMv7-A  | Cortex-A7/A9/A15 | 应用处理器 | MMU、VFP、NEON     |
 | ARMv7-R  | Cortex-R4/R5/R7  | 实时处理器 | MPU、低延迟        |
 | ARMv7-M  | Cortex-M3/M4/M7  | 微控制器   | NVIC、Thumb-2      |
-| ARMv8-A  | Cortex-A53/A72   | 64位应用   | AArch64、.LITTLE |
+| ARMv8-A  | Cortex-A53/A72   | 64位应用   | AArch64、big.LITTLE |
 | ARMv9-A  | Cortex-A510/A710 | 最新一代   | SVE2、CCA          |
 
 ### 1.2 Cortex-A7架构定位
@@ -128,6 +128,15 @@ ARM架构提供了16个32位通用寄存器(R0~R15)供软件使用:
 | R15    | PC   | 程序计数器   | 保存当前执行指令地址+8字节            |
 | CPSR   | -    | 当前程序状态 | 所有模式共享一个物理寄存器            |
 | SPSR   | -    | 备份程序状态 | 异常模式下保存CPSR的备份              |
+| ELR_hyp | -   | 异常链接寄存器 | Hyp 模式独有,保存虚拟化异常返回地址  |
+
+**寄存器总量小结**(原书第六章 6.3 节):
+
+1. **34 个通用寄存器**,含 R15(PC),全部为 32 位(各模式 banked 寄存器分别计数)
+2. **8 个状态寄存器**,含 1 个 CPSR 和 7 个 SPSR
+3. **Hyp 模式下独有一个 ELR_Hyp 寄存器**,用于虚拟化异常返回
+
+> 注:Cortex-A7 共有 9 种运行模式,但只有 7 种异常模式(FIQ/IRQ/SVC/ABT/UND/MON/HYP)配有 SPSR,User 和 Sys 不是异常模式,因此**没有 SPSR**,在这两个模式下访问 SPSR 会导致不可预知的结果。
 
 ### 2.3 各模式下的寄存器映射
 
@@ -151,8 +160,11 @@ R14(LR)      R14     R14     LR_fiq   LR_irq   LR_abt   LR_svc   LR_und   LR_mon
 R15(PC)      R15     R15     R15      R15      R15      R15      R15      R15      R15
 CPSR         CPSR    CPSR    CPSR     CPSR     CPSR     CPSR     CPSR     CPSR     CPSR
 SPSR         -       -       SPSR_fiq SPSR_irq SPSR_abt SPSR_svc SPSR_und SPSR_mon SPSR_hyp
+ELR_hyp      -       -       -        -        -        -        -        -        ELR_hyp
 ```
 
+> 说明:HYP 模式下 R14 不作 bank,返回地址保存在 ELR_hyp 中;该表依据原书图 6.3.2 整理。
+
 **关键观察**:
 
 - R0~R7(低寄存器组)在所有模式下共享同一组物理寄存器
@@ -167,6 +179,7 @@ SPSR         -       -       SPSR_fiq SPSR_irq SPSR_abt SPSR_svc SPSR_und SPSR_m
 
 **R8~R12**:在FIQ模式下有独立的物理寄存器(R8_fiq~R12_fiq),其他模式共享R8~R12。FIQ是快速中断模式,中断处理程序可以使用R8~R12而无需执行保存/恢复指令,从而加速中断执行。
 
+
 **R13(SP,栈指针)**:共8个物理寄存器,User/Sys共享一个,其余7种模式各对应一个。应用程序初始化时需设置各模式的SP指向专用栈地址。
 
 **R14(LR,链接寄存器)**:共7个物理寄存器,User/Sys/Hyp共享一个,其余6种模式各一个。LR的两个主要用途:
@@ -214,6 +227,7 @@ CPSR(Current Program Status Register)是当前程序状态寄存器,所有
 | Q       | bit27    | 累积饱和标志。ARMv5TE架构支持,Q=1表示累积饱和             |
 | IT[1:0] | bit26:25 | IF-THEN指令执行状态,与IT[7:2]组合使用                     |
 | J       | bit24    | Jazelle状态位,与T位组合表示指令集类型                     |
+| 保留    | bit23:20 | 保留位                                                     |
 | GE[3:0] | bit19:16 | SIMD指令有效位,大于或等于                                 |
 | IT[7:2] | bit15:10 | IF-THEN指令执行状态高6位                                   |
 | E       | bit9     | 大小端控制。E=1大端模式,E=0小端模式                       |
@@ -273,18 +287,26 @@ CPSR(Current Program Status Register)是当前程序状态寄存器,所有
 
 ## 3. Cortex-A7存储系统
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。(原书第六章仅讲解处理器模型与寄存器组,MMU/Cache 细节需查阅《Cortex-A7 Technical Reference Manual》与《I.MX6ULL 参考手册》。)
+
 ### 3.1 存储器映射
 
-Cortex-A7使用32位地址线,总寻址空间为4GB(0x00000000~0xFFFFFFFF)。I.MX6ULL的存储器映射如下:
+Cortex-A7使用32位地址线,总寻址空间为4GB(0x00000000~0xFFFFFFFF)。I.MX6ULL的存储器映射(据 I.MX6ULL 参考手册与第十三章链接地址约定)如下:
 
 ```
-地址范围                    用途                    大小
-0x0000_0000 ~ 0x3FFF_FFFF  外部DDR3内存            1GB
-0x4000_0000 ~ 0x7FFF_FFFF  保留                    1GB
-0x8000_0000 ~ 0xBFFF_FFFF  片内外设寄存器          1GB
-0xC000_0000 ~ 0xFFFF_FFFF  保留/片内ROM/IRAM      1GB
+地址范围                    用途                              大小
+0x0000_0000 ~ 0x0001_7FFF  Boot ROM (NXP 内部使用,不开放)   ~96KB
+0x0090_0000 ~ 0x0091_FFFF  片内 RAM (OCRAM)                   128KB
+0x00A0_0000 ~ 0x00A0_7FFF  GIC 中断控制器                     32KB
+0x0200_0000 ~ 0x02FF_FFFF  AIPS1/AIPS2 片内外设寄存器         16MB
+0x021B_0000                MMDC (DDR 控制器)
+0x6000_0000 ~ 0x6FFF_FFFF  QSPI Flash 映射区                  256MB
+0x8000_0000 ~ 0x9FFF_FFFF  外部 DDR (512MB 型号)              512MB
 ```
 
+> 说明:I.MX6U-ALPHA 开发板 DDR 起始地址均为 **0x8000_0000**,512MB 型号终止于 **0x9FFF_FFFF**,256MB 型号终止于 **0x8FFF_FFFF**;裸机程序统一链接到 **0x8780_0000**(与 U-Boot 保持一致)。
+
+
 **LPAE(Large Physical Address Extension)**:支持40位存储地址,最高可访问1TB内存。
 
 ### 3.2 L1 Cache
@@ -348,6 +370,8 @@ MMU使能前后对比:
 
 ## 4. Cortex-A7中断系统
 
+> 📖 **来源说明**:本节内容不属于原书第六章,原书在**第十七章 GPIO 中断实验**中详细讲解 GIC、CP15 与向量表,此处提前展开,便于建立整体认识。
+
 ### 4.1 GIC中断控制器
 
 Cortex-A7使用ARM GIC V2.0(Generic Interrupt Controller)中断控制器,由两部分组成:
@@ -382,13 +406,13 @@ graph LR
 - 负责所有中断源的优先级排序和分发
 - 为每个中断源配置目标CPU
 - 管理中断使能/禁止
-- 基地址:0x0209C000
+- 基地址:0x00A0_1000(GIC 基地址 0x00A0_0000 + 偏移 0x1000)
 
 **CPU Interface(CPU接口)**:
 
 - 每个CPU核心一个接口
 - 负责向CPU核心发送中断请求
-- 基地址:0x02094000
+- 基地址:0x00A0_2000(GIC 基地址 0x00A0_0000 + 偏移 0x2000)
 
 ### 4.2 中断类型
 
@@ -417,19 +441,23 @@ graph LR
 
 ```asm
 ; GIC中断使能(以SPI中断ID 50为例)
-LDR R0, =0x0209C100       ; GICD_ISENABLERn地址
-MOV R1, #(1 << 50)        ; 设置ID50使能位
+; ID50 属于 GICD_ISENABLER1,地址 = 0x00A01000 + 0x100 + 1*4 = 0x00A01104
+; 位号 = 50 % 32 = 18
+LDR R0, =0x00A01104       ; GICD_ISENABLER1 地址
+LDR R1, =0x00040000       ; 1 << 18,设置 ID50 使能位
 STR R1, [R0]
 
-; 读取中断ACK
-LDR R0, =0x0209400C       ; GICC_IAR地址
+; 读取中断ACK(GIC 基地址由 CP15 的 CBAR 取得,见第十七章)
+LDR R0, =0x00A0200C       ; GICC_IAR 地址
 LDR R1, [R0]              ; 读取中断ID
 
 ; 中断完成(EOI)
-LDR R0, =0x02094010       ; GICC_EOIR地址
+LDR R0, =0x00A02010       ; GICC_EOIR 地址
 STR R1, [R0]              ; 写入中断ID完成中断
 ```
 
+> 原书第十七章的实际做法并不硬编码 GIC 基地址,而是通过 `MRC p15, 4, r1, c15, c0, 0` 从 CP15 的 CBAR 寄存器读出 GIC 基地址,再 `ADD r1, r1, #0x2000` 得到 CPU 接口端地址。
+
 ### 4.4 中断控制器寄存器
 
 | 寄存器           | 偏移      | 说明            |
@@ -448,6 +476,8 @@ STR R1, [R0]              ; 写入中断ID完成中断
 
 ## 5. Cortex-A7异常处理
 
+> 📖 **来源说明**:本节不属于原书第六章,异常向量表与异常处理流程在原书**第十七章 GPIO 中断实验**(17.1.2 节)中讲解。
+
 ### 5.1 异常类型
 
 | 异常类型       | 向量偏移 | 说明                       |
@@ -513,6 +543,8 @@ MCR p15, 0, R0, c12, c0, 0 ; 写入VBAR寄存器
 
 ## 6. 跨平台对比:IMX6ULL vs STM32 vs RK3568
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 参数       | IMX6ULL             | STM32F407     | RK3568              |
 | ---------- | ------------------- | ------------- | ------------------- |
 | CPU核心    | Cortex-A7           | Cortex-M4     | Cortex-A55          |
@@ -538,6 +570,8 @@ MCR p15, 0, R0, c12, c0, 0 ; 写入VBAR寄存器
 
 ## 7. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: Cortex-A7的CPSR寄存器中,N、Z、C、V四个标志位分别在什么情况下被置1?
 
 **答:**
@@ -650,3 +684,5 @@ ISB
 - 正点原子《I.MX6U嵌入式Linux驱动开发指南V2.0.1》第六章
 - ARM Cortex-A7 Technical Reference Manual
 - ARM Cortex-A(armV7)编程手册V4.0
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六章 Cortex-A7 MPCore架构(第 6.1~6.3 节,含 9 种运行模式、寄存器组、CPSR/SPSR 位域);第 16/17 章提供寄存器地址与中断系统补充。

+ 199 - 48
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/02-ARM汇编基础.md

@@ -12,6 +12,8 @@ pdf_ref: "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第七章 AR
 
 ## 一、ARM指令集概述
 
+> ⚠️ **来源说明**:本节(指令集对比、ARM 指令编码格式、寻址模式)不属于《I.MX6U嵌入式Linux驱动开发指南》第七章内容,为扩展知识。原书第七章只讲解 GNU 汇编语法与最常用的 Cortex-A7 汇编指令。
+
 ### 1.1 ARM指令集 vs Thumb指令集
 
 | 特性       | ARM指令集          | Thumb指令集   | Thumb-2指令集 |
@@ -54,6 +56,25 @@ ARM 32位指令格式:
 
 ## 二、数据处理指令
 
+### 2.0 处理器内部数据传输指令(MOV / MRS / MSR)
+
+原书第 7.2.1 节重点讲解的三个数据传输指令:
+
+| 指令        | 目的 | 源   | 描述                                       |
+| ----------- | ---- | ---- | ------------------------------------------ |
+| `MOV R0,R1` | R0   | R1   | 将 R1 里面的数据复制到 R0 中                |
+| `MRS R0,CPSR` | R0 | CPSR | 将特殊寄存器 CPSR 里面的数据复制到 R0 中    |
+| `MSR CPSR,R1` | CPSR | R1 | 将 R1 里面的数据复制到特殊寄存器 CPSR 里中  |
+
+```asm
+MOV R0, R1           @ R0 = R1
+MOV R0, #0X12        @ R0 = 0x12,立即数传递到寄存器
+MRS R0, CPSR         @ 读特殊寄存器只能用 MRS:R0 = CPSR
+MSR CPSR, R0         @ 写特殊寄存器只能用 MSR:CPSR = R0
+```
+
+> 要点:**读** CPSR/SPSR 只能用 `MRS`,**写** CPSR/SPSR 只能用 `MSR`,不能使用 MOV。
+
 ### 2.1 数据传输指令
 
 ```asm
@@ -72,17 +93,25 @@ ADD R0, R1, #10      @ R0 = R1 + 10
 ADC R0, R1, R2       @ R0 = R1 + R2 + C(带进位加法)
 SUB R0, R1, R2       @ R0 = R1 - R2
 SBC R0, R1, R2       @ R0 = R1 - R2 - !C(带借位减法)
+MUL R0, R1, R2       @ R0 = R1 * R2(32位乘法)
+UDIV R0, R1, R2      @ R0 = R1 / R2(无符号除法)
+SDIV R0, R1, R2      @ R0 = R1 / R2(有符号除法)
 ```
 
+> 原书第 7.2.5 节列出的算术指令为:ADD、ADC、SUB、SBC、MUL、UDIV、SDIV;其中乘除指令在嵌入式配置寄存器时基本用不到,最常用的是加减。
+
 ### 2.3 逻辑指令
 
 ```asm
 AND R0, R1, #0xFF    @ R0 = R1 & 0xFF(按位与)
 ORR R0, R1, #0xFF    @ R0 = R1 | 0xFF(按位或)
+ORN R0, R1, #0xFF    @ R0 = R1 | (~0xFF)(按位或非)
 EOR R0, R1, #0xFF    @ R0 = R1 ^ 0xFF(按位异或)
 BIC R0, R1, #0xFF    @ R0 = R1 & ~0xFF(位清除)
 ```
 
+> 原书第 7.2.6 节列出的逻辑指令为:AND、ORR、BIC、ORN、EOR。
+
 ### 2.4 比较指令
 
 ```asm
@@ -113,9 +142,13 @@ LDR R0, =0x0209C000  @ 伪指令:加载32位地址
 STR R0, [R1]         @ *R1 = R0
 STR R0, [R1, #4]     @ *(R1+4) = R0
 LDRB R0, [R1]        @ R0 = *(uint8_t*)R1(字节加载)
+STRB R0, [R1]        @ *(uint8_t*)R1 = R0(字节存储)
 LDRH R0, [R1]        @ R0 = *(uint16_t*)R1(半字加载)
+STRH R0, [R1]        @ *(uint16_t*)R1 = R0(半字存储)
 ```
 
+> LDR/STR 默认按字(32位)读写,加后缀 **B** 按字节(LDRB/STRB)、加后缀 **H** 按半字(LDRH/STRH)读写(原书 7.2.2 节)。
+
 ### 3.2 批量寄存器传输
 
 ```asm
@@ -131,27 +164,84 @@ PUSH {R0-R3, LR}        @ 压栈(满递减FD):STMDB SP!, {R0-R3, LR}
 POP {R0-R3, PC}         @ 出栈:LDMIA SP!, {R0-R3, PC}
 ```
 
+**原书第 7.2.3 节**指出,PUSH/POP 的另一种等价写法是 `STMFD SP!` 和 `LDMFD SP!`:
+
+```asm
+STMFD SP!, {R0~R3, R12}   @ R0~R3,R12 入栈
+STMFD SP!, {LR}           @ LR 入栈
+LDMFD SP!, {LR}           @ 先恢复 LR
+LDMFD SP!, {R0~R3, R12}   @ 再恢复 R0~R3,R12
+```
+
+其中 `STM` = 多存储(Store Multiple),`LDM` = 多加载(Load Multiple),`FD` = Full Descending(满递减)。ATPCS 规定 ARM 使用 FD 类型堆栈:SP 指向最后一个入栈的数据,堆栈由高地址向低地址增长。STM/LDM 的寄存器列表中**编号小的对应低地址,编号高的对应高地址**。
+
+**原书堆栈示例**:设当前 SP = 0x80000000,向下增长,执行 `PUSH {R0~R3, R12}` 后:
+
+```
+地址          内容
+0x80000000    (原栈底,SP 初始处)
+0x7FFFFFFC    R12
+0x7FFFFFF8    R3
+0x7FFFFFF4    R2
+0x7FFFFFF0    R1
+0x7FFFFFEC    R0        ← SP = 0x7FFFFFEC
+```
+
+再执行 `PUSH {LR}` 后 SP = 0x7FFFFFE8,LR 存放于 0x7FFFFFE8。出栈时按 `POP {LR}`、`POP {R0~R3,R12}` 顺序从栈顶(SP 当前指向处)地址依次**减小**提取数据。
+
+> 说明:`PUSH`/`POP` 是 `STMFD SP!`/`LDMFD SP!` 的别名,二者编码相同;`STMFD` 与 `STMDB` 亦为同一指令的两种写法。
+
 ### 3.4 批量传输寻址模式
 
+> ⚠️ **来源说明**:本小节不属于原书第七章内容,为扩展知识;原书只讲解了 STM/LDM 与 STMFD/LDMFD。
+
 | 后缀 | 模式   | 用途                |
 | ---- | ------ | ------------------- |
 | IA   | 后递增 | 连续内存读取        |
 | IB   | 前递增 | 连续内存写入        |
 | DA   | 后递减 | 逆序内存读取        |
 | DB   | 前递减 | 堆栈压入            |
-| FD   | 满递减 | 堆栈操作(ARM常用) |
+
+> 堆栈类型后缀(FD/FA/ED/EA)用于描述堆栈模型,其中 **FD(满递减)为 ARM 默认**,对应 `STMFD`/`LDMFD`;不要把 FD 与 IA/IB/DA/DB 混为同类后缀。x86 使用的是空递减(ED)模型。
 
 ## 四、跳转指令
 
 ### 4.1 跳转指令详解
 
 ```asm
-B loop              @ 无条件跳转到loop
-BNE loop            @ 不等于时跳转(Z=0)
-BEQ loop            @ 等于时跳转(Z=1)
+B loop              @ 无条件跳转到loop(跳转范围 ±2KB 时可改用 B.W 32位版本)
+B.W loop            @ 32位版本跳转指令,跳转范围更大
+BX LR               @ 间接跳转,跳转到 Rm 中保存的地址,并切换指令集
 BL function         @ 跳转并保存返回地址到LR(用于函数调用)
-BX LR               @ 返回(可能切换指令集)
-BLX function        @ 跳转、保存返回地址、可能切换指令集
+BLX function        @ 结合BX和BL:跳转到Rm指定地址、保存返回地址、可切换指令集
+```
+
+原书第 7.2.4 节跳转指令表:
+
+| 指令         | 描述                                                                 |
+| ------------ | -------------------------------------------------------------------- |
+| `B <label>`  | 跳转到 label;若跳转范围超过 ±2KB,可用 `B.W <label>` 使用 32 位版本 |
+| `BX <Rm>`    | 间接跳转,跳转到 Rm 中保存的地址处,并且切换指令集                   |
+| `BL <label>` | 跳转到标号地址,并将返回地址保存在 LR 中                             |
+| `BLX <Rm>`   | 结合 BX 和 BL 的特点,跳转到 Rm 指定地址、保存返回地址、切换指令集    |
+
+**原书示例**:汇编初始化 C 运行环境后跳转到 main(B 指令,不返回):
+
+```asm
+_start:
+    ldr sp, =0X80200000   @ 设置栈指针
+    b   main              @ 跳转到 main 函数
+```
+
+汇编调用 C 函数(需要返回,用 BL):
+
+```asm
+    push {r0, r1}              @ 保存 r0,r1
+    cps  #0x13                 @ 进入 SVC 模式,允许其他中断再次进入
+    bl   system_irqhandler     @ 调用 C 语言中断处理函数
+    cps  #0x12                 @ 进入 IRQ 模式
+    pop  {r0, r1}
+    str  r0, [r1, #0X10]       @ 中断执行完成,写 EOIR
 ```
 
 ### 4.2 条件码
@@ -164,6 +254,8 @@ BLX function        @ 跳转、保存返回地址、可能切换指令集
 | 0011   | CC/LO  | 无符号小于     | C=0      |
 | 0100   | MI     | 负数           | N=1      |
 | 0101   | PL     | 非负数         | N=0      |
+| 0110   | VS     | 溢出           | V=1      |
+| 0111   | VC     | 未溢出         | V=0      |
 | 1000   | HI     | 无符号大于     | C=1且Z=0 |
 | 1001   | LS     | 无符号小于等于 | C=0或Z=1 |
 | 1010   | GE     | 有符号大于等于 | N=V      |
@@ -190,44 +282,50 @@ MOVNE R1, #1        @ R0!=10时执行
 
 ### 5.1 数据定义伪指令
 
-```asm
-.word 0x12345678     @ 定义32位数据
-.hword 0x1234        @ 定义16位数据
-.byte 0x12           @ 定义8位数据
-.asciz "Hello"       @ 定义字符串(带\0结束符)
-```
+原书第 7.1 节列出的常用伪操作:
 
-### 5.2 符号定义伪指令
+| 伪操作   | 说明                                                     |
+| -------- | -------------------------------------------------------- |
+| `.byte`  | 定义单字节数据,如 `.byte 0x12`                          |
+| `.short` | 定义双字节数据,如 `.short 0x1234`                       |
+| `.long`  | 定义 4 字节数据,如 `.long 0x12345678`                   |
+| `.equ`   | 赋值语句,格式 `.equ 变量名, 表达式`,如 `.equ num, 0x12` |
+| `.align` | 数据字节对齐,如 `.align 4` 表示 4 字节对齐              |
+| `.end`   | 表示源文件结束                                           |
+| `.global`| 定义一个全局符号,格式 `.global symbol`,如 `.global _start` |
 
 ```asm
-.equ GPIO1_BASE, 0x0209C000    @ 定义常量(推荐)
-.set GPIO1_BASE, 0x0209C000    @ 等同于.equ
+.byte 0x12           @ 定义8位数据
+.short 0x1234        @ 定义16位数据
+.long 0x12345678     @ 定义32位数据(等价于 .word)
+.equ  num, 0x12      @ 定义常量
 ```
 
-### 5.3 段控制伪指令
+> **扩展(GNU as 支持但原书未列)**:`.word`、`.hword`、`.asciz`、`.set`、`.weak`、`.balign`、`.macro` 等均非原书第 7.1 节所列内容。更多伪操作可参考《ARM Cortex-A(armV7)编程手册 V4.0》第 57 页。
+
+### 5.2 段控制与符号定义伪指令
 
 ```asm
+.section .testsection @ 用 .section 自定义一个段
 .text               @ 代码段
 .data               @ 已初始化数据段
 .bss                @ 未初始化数据段
 .rodata             @ 只读数据段
-```
 
-### 5.4 符号可见性伪指令
-
-```asm
 .global _start      @ 全局符号(类似C的extern)
-.weak weak_func     @ 弱符号(可被覆盖)
+.weak weak_func     @ 弱符号(可被覆盖,扩展)
 ```
 
-### 5.5 对齐伪指令
+> 段名说明:`.text` 表示代码段,`.data` 初始化的数据段,`.bss` 未初始化的数据段,`.rodata` 只读数据段。汇编程序的默认入口标号是 `_start`,也可以在链接脚本中用 `ENTRY` 指定其他入口点。
+
+### 5.3 对齐伪指令
 
 ```asm
-.align 4            @ 4字节对齐
-.balign 4           @ 4字节对齐(填充NOP)
+.align 4            @ 4字节对齐(原书列举)
+.balign 4           @ 4字节对齐(填充NOP,扩展
 ```
 
-### 5.6 宏定义伪指令
+### 5.4 宏定义伪指令(扩展)
 
 ```asm
 .macro SET_GDIR reg, bit
@@ -241,6 +339,42 @@ SET_GDIR r0, 0x8    @ 使用宏
 
 ## 六、GNU汇编语法
 
+### 6.0 语句格式与函数定义(原书 7.1 节)
+
+GNU 汇编适用于所有架构,并非 ARM 独享。每行一条语句,每条语句由三个可选部分组成:
+
+```
+label:instruction @ comment
+```
+
+- **label(标号)**:表示地址位置,任何以“:”结尾的标识符都会被识别为标号,可用来表示指令或数据地址。
+- **instruction(指令)**:汇编指令或伪指令。
+- **@ 符号**:表示注释,也可以用 C 语言风格的 `/* ... */`。
+- **comment**:注释内容。
+
+```asm
+add:
+    MOVS R0, #0X12  @设置 R0=0X12
+```
+
+> **注意!** ARM 中的指令、伪指令、伪操作、寄存器名等可以**全部大写**,也可以**全部小写**,但是**不能大小写混用**。
+
+GNU 汇编也支持函数,格式为函数名 + 函数体 + 返回语句,返回语句不是必须的:
+
+```asm
+/* 未定义中断 */
+Undefined_Handler:
+    ldr r0, =Undefined_Handler
+    bx  r0
+
+/* SVC 中断 */
+SVC_Handler:
+    ldr r0, =SVC_Handler
+    bx  r0
+```
+
+其中 `bx` 就是返回指令。
+
 ### 6.1 文件结构
 
 ```asm
@@ -288,10 +422,12 @@ MOV R0, #'A'            @ 字符常量
 
 ## 七、完整示例分析
 
+> 📖 **来源说明**:本节示例来自原书**第八章 汇编 LED 灯实验**与**第十三章 BSP 工程管理实验**(`imx6ul.lds` 链接脚本),不属于第七章内容,此处作为第七章汇编指令的综合应用。
+
 ### 7.1 LED灯汇编驱动
 
 ```asm
-@ LED灯汇编驱动 - IMX6ULL
+@ LED灯汇编驱动 - IMX6ULL(原书第 8.3 节 led.s)
 @ 功能:点亮GPIO1_IO03连接的LED
 @ 文件:led.s
 
@@ -333,14 +469,14 @@ _start:
     @ ========================================
     @ 步骤3:设置GPIO1_IO03的IO属性
     @ ========================================
-    @ bit 16:0 HYS关闭
-    @ bit [15:14]: 00 默认速率
-    @ bit [13]: 0 keeper使
-    @ bit [12]: 1 pull/keeper使能
-    @ bit [11]: 0 关闭开路输出
+    @ bit [16]:  0  HYS关闭
+    @ bit [15:14]: 00 默认下拉(PUS)
+    @ bit [13]: 0  keeper功
+    @ bit [12]: 1  pull/keeper使能
+    @ bit [11]: 0  关闭开路输出
     @ bit [7:6]: 10 速度100Mhz
     @ bit [5:3]: 110 R0/6驱动能力
-    @ bit [0]: 0 低转换率
+    @ bit [0]: 0  低转换率
     LDR R0, =0X020E02F4    @ SW_PAD_GPIO1_IO03_BASE
     LDR R1, =0X10B0        @ IO属性值
     STR R1, [R0]           @ 写入属性寄存器
@@ -349,7 +485,7 @@ _start:
     @ 步骤4:设置GPIO1_IO03为输出
     @ ========================================
     LDR R0, =0X0209C004    @ GPIO1_GDIR寄存器
-    LDR R1, =0X00000008    @ bit3=1, 输出模式
+    LDR R1, =0X0000008     @ bit3=1, 输出模式
     STR R1, [R0]           @ 写入方向寄存器
 
     @ ========================================
@@ -363,6 +499,7 @@ loop:
     B loop                 @ 死循环,保持LED亮
 ```
 
+
 ### 7.2 代码逐行解释
 
 | 行号 | 指令                  | 说明                         |
@@ -384,36 +521,47 @@ loop:
 | 15   | `STR R1, [R0]`        | 输出低电平,点亮LED          |
 | 16   | `B loop`              | 无限循环,保持状态           |
 
-### 7.3 链接脚本
+### 7.3 链接脚本(原书第十三章 imx6ul.lds)
 
 ```lds
 /* 链接脚本 - imx6ul.lds */
 SECTIONS {
-    . = 0x87800000;  /* DDR起始地址 */
+    . = 0X87800000;              /* 链接起始地址(DDR 中,与 U-Boot 一致) */
 
     .text : {
-        *(.text)         /* 代码段 */
+        obj/start.o              /* 必须把 start.o 放在最前面,保证 _start 位于 0x87800000 */
+        *(.text)                 /* 其余代码段 */
     }
 
-    .rodata : {
-        *(.rodata*)      /* 只读数据段 */
-    }
+    .rodata ALIGN(4) : { *(.rodata*) }   /* 只读数据段 */
 
-    .data : {
-        *(.data)         /* 已初始化数据段 */
-    }
+    .data ALIGN(4) : { *(.data) }        /* 已初始化数据段 */
 
-    __bss_start = .;
-    .bss : {
-        *(.bss)          /* 未初始化数据段 */
-        *(COMMON)        /* 通用符号 */
-    }
-    __bss_end = .;
+    __bss_start = .;                     /* bss 段起始符号 */
+    .bss ALIGN(4) : { *(.bss) *(COMMON) }/* 未初始化数据段 + 通用符号 */
+    __bss_end = .;                       /* bss 段结束符号 */
 }
 ```
 
+**关键符号 `_start` / `__bss_start` / `__bss_end`**:
+
+- `_start`:`.global` 声明的全局入口标号,中断向量表就放在它前面,位于链接起始地址 0x87800000。
+- `__bss_start` / `__bss_end`:链接器生成的 bss 段首尾地址符号,C 代码进入前由汇编循环用 0 清零这段区域,保证未初始化全局变量初值为 0。
+- 注意 `.text` 段必须把 `obj/start.o` 放在最前,否则 `_start` 不会落在 0x87800000。
+
+**原书编译命令链**(第八章 8.4.1 节):
+
+```bash
+arm-linux-gnueabihf-gcc -g -c led.s -o led.o
+arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
+arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
+arm-linux-gnueabihf-objdump -D led.elf > led.dis
+```
+
 ## 八、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 8.1 IMX6ULL vs STM32 vs RK3568
 
 | 特性         | IMX6ULL                 | STM32F4                    | RK3568               |
@@ -460,6 +608,8 @@ str w1, [x0]
 
 ## 九、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:ARM汇编中,LDR伪指令和LDR指令有什么区别?
 
 **答案**:
@@ -578,5 +728,6 @@ str w1, [x0]
 
 **相关知识**: [[01-Cortex-A7架构详解]] | [[03-时钟与电源管理]] | [[04-GPIO与中断系统]]
 
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第七章 ARM汇编基础(7.1 GNU 汇编语法、7.2.1~7.2.6 常用汇编指令);第七章综合示例来自第八章、第十三章。
 **参考来源**: 正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1
 **最后更新**: 2026-09-17

+ 145 - 49
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/03-时钟与电源管理.md

@@ -51,11 +51,13 @@ graph TD
 | PLL1 | ARM_PLL    | 可编程 (54~108) | 最高 1.3 GHz | ARM 内核主频                   |
 | PLL2 | System_PLL | 固定 ×22        | 528 MHz      | 系统总线、DDR、NAND/NOR        |
 | PLL3 | USB1_PLL   | 固定 ×20        | 480 MHz      | USB1 PHY + 4 路 PFD            |
-| PLL4 | AUDIO_PLL  | 可编程          | 650~1300 MHz | 音频外设 (ESAI 等)             |
-| PLL5 | VIDEO_PLL  | 可编程          | 650~1300 MHz | 显示外设 (LCD 等)              |
+| PLL4 | AUDIO_PLL  | 可编程          | 650~1300 MHz | 音频外设 (ESAI 等),最终输出可选 1/2/4 分频 |
+| PLL5 | VIDEO_PLL  | 可编程          | 650~1300 MHz | 显示外设 (LCD 等),最终输出可选 1/2/4/8/16 分频 |
 | PLL6 | ENET_PLL   | 固定 ×(20+5/6)  | 500 MHz      | 以太网时钟 (25/50/100/125 MHz) |
 | PLL7 | USB2_PLL   | 固定 ×20        | 480 MHz      | USB2 PHY                       |
 
+> 原书第 16.1.2 节列举的 7 路 PLL 顺序为:① ARM_PLL(PLL1) ② 528_PLL(PLL2) ③ USB1_PLL(PLL3) ④ USB2_PLL(**PLL7**,序号为 4 但实际是 PLL7) ⑤ ENET_PLL(PLL6) ⑥ VIDEO_PLL(PLL5) ⑦ AUDIO_PLL(PLL4)。
+
 ### 1.3 时钟树
 
 I.MX6ULL 的时钟树分为三部分:
@@ -120,16 +122,18 @@ graph LR
 
 ### 2.1 分频器
 
-I.MX6ULL 使用 PODF (Prescale Output Divider) 进行分频,PODF 值与分频比的映射关系:
+I.MX6ULL 使用 PODF (Prescale Output Divider) 进行分频。原书第 16.1.4/16.1.6 节明确:PODF 值 **0~7 分别对应 1~8 分频**(线性关系,即分频比 = PODF 值 + 1):
+
+| PODF 值 | 分频比 | PODF 值 | 分频比 |
+| ------- | ------ | ------- | ------ |
+| 0       | ÷1     | 4       | ÷5     |
+| 1       | ÷2     | 5       | ÷6     |
+| 2       | ÷3     | 6       | ÷7     |
+| 3       | ÷4     | 7       | ÷8     |
 
-| PODF 值 | 分频比 |
-| ------- | ------ |
-| 0       | ÷1     |
-| 1       | ÷2     |
-| 2       | ÷4     |
-| 3       | ÷8     |
+> 💡 **注意**:PODF 是**线性映射**,分频比 = PODF 值 + 1(0~7 依次对应 1~8 分频),不是 0→÷1、1→÷2、2→÷4、3→÷8 的指数映射。例如设置 ARM_PODF=1 时为 2 分频(1056/2=528MHz),与"值+1"关系一致。
 
-关键分频器及其寄存器:
+关键分频器及其寄存器(CCM 基地址 = **0x020C4000**):
 
 | 分频器      | 寄存器           | 位域 | 范围     | 目标频率          |
 | ----------- | ---------------- | ---- | -------- | ----------------- |
@@ -140,14 +144,28 @@ I.MX6ULL 使用 PODF (Prescale Output Divider) 进行分频,PODF 值与分频
 
 ### 2.2 门控时钟
 
-I.MX6ULL 通过 7 组 CCGR 寄存器 (`CCM->CCGR0` ~ `CCM->CCGR6`) 控制外设时钟的使能与关闭。每个外设时钟有 2 位控制:
+I.MX6ULL 通过 7 组 CCGR 寄存器 (`CCM->CCGR0` ~ `CCM->CCGR6`) 控制外设时钟的使能与关闭。每个外设时钟占 **2 个连续位**(CGn),可设置为 4 种状态:
+
+| CCGR 位设置 | 含义                                                         |
+| ----------- | ------------------------------------------------------------ |
+| 00          | 所有模式下都关闭外设时钟                                     |
+| 01          | 只在运行(Run)模式下打开,等待(Wait)和停止(Stop)模式下均关闭  |
+| 10          | 未使用(保留)                                               |
+| 11          | 除停止模式以外,其他所有模式下时钟都打开                     |
+
+CCGR 寄存器地址(CCM 基地址 0x020C4000):
 
-| CCGR 值 | 含义                                 |
-| ------- | ------------------------------------ |
-| 00      | 关闭 (off)                           |
-| 01      | 部分使能 (defined by CCM clock gate) |
-| 10      | 保留                                 |
-| 11      | 运行时始终使能 (on during run)       |
+| 寄存器   | 地址       | 说明                                             |
+| -------- | ---------- | ------------------------------------------------ |
+| CCM_CCGR0 | 0x020C4068 | 如 bit[31:30] 控制 GPIO2 时钟                    |
+| CCM_CCGR1 | 0x020C406C | 如 bit[27:26] 控制 GPIO1 时钟                    |
+| CCM_CCGR2 | 0x020C4070 |                                                  |
+| CCM_CCGR3 | 0x020C4074 |                                                  |
+| CCM_CCGR4 | 0x020C4078 |                                                  |
+| CCM_CCGR5 | 0x020C407C | UART 等相关                                      |
+| CCM_CCGR6 | 0x020C4080 |                                                  |
+
+> 位域规律:CGn 占据 bit[2n+1 : 2n]。例如 CCGR0 的 bit[31:30]=GPIO2,CCGR1 的 bit[27:26]=GPIO1。
 
 ```c
 /* 使能所有外设时钟 (开发阶段) */
@@ -198,20 +216,31 @@ void clk_enable(void)
 
 #### CCM_CBCDR — 总线时钟分频寄存器
 
-| 位域    | 名称            | 功能                                                 |
-| ------- | --------------- | ---------------------------------------------------- |
-| [25]    | PERIPH_CLK_SEL  | periph_clk 源:0=pre_periph_clk, 1=periph_clk2_clock |
-| [19]    | PERIPH2_CLK_SEL | periph2_clk 源:0=PLL2, 1=periph2_clk2_clk           |
-| [12:10] | AHB_PODF        | AHB 分频:0~7 对应 1~8                               |
-| [9:8]   | IPG_PODF        | IPG 分频:0~3 对应 1~4                               |
-| [7:0]   | AXI_PODF        | AXI 分频:0~7 对应 1~8                               |
+| 位域    | 名称             | 功能                                                     |
+| ------- | ---------------- | -------------------------------------------------------- |
+| [29:27] | PERIPH_CLK2_PODF | periph_clk2 时钟分频:0~7 对应 1~8 分频                  |
+| [26]    | PERIPH2_CLK_SEL  | periph2_clk 源:0=PLL2, 1=periph2_clk2_clk(触发 MMDC 握手) |
+| [25]    | PERIPH_CLK_SEL   | periph_clk 源:0=pre_periph_clk, 1=periph_clk2_clock(触发 MMDC 握手) |
+| [21:19] | MMDC_CH0_PODF    | mmdc_ch0 时钟分频:0~7 对应 1~8 分频                     |
+| [18:16] | AXI_PODF         | AXI 分频:0~7 对应 1~8                                   |
+| [12:10] | AHB_PODF         | AHB 分频:0~7 对应 1~8(触发 MMDC 握手)                 |
+| [9:8]   | IPG_PODF         | IPG 分频:0~3 对应 1~4                                   |
+| [7]     | AXI_ALT_CLK_SEL  | axi_alt 时钟选择:0=PLL2_PFD2, 1=PLL3_PFD1               |
+| [6]     | AXI_CLK_SEL      | axi 时钟源选择:0=periph_clk, 1=axi_alt                  |
+| [5:3]   | FABRIC_MMDC_PODF | fabric/mmdc 时钟分频:0~7 对应 1~8(即 mmdc_podf)       |
+| [2:0]   | PERIPH2_CLK2_PODF| periph2_clk2 时钟分频:0~7 对应 1~8                      |
 
 #### CCM_CBCMR — 总线时钟模式寄存器
 
 | 位域    | 名称                | 功能                                                                   |
 | ------- | ------------------- | ---------------------------------------------------------------------- |
+| [25:23] | LCDIF1_PODF         | lcdif1 时钟分频:0~7 对应 1~8 分频                                     |
+| [22:21] | PRE_PERIPH2_CLK_SEL | pre_periph2 源:00=PLL2, 01=PLL2_PFD2, 10=PLL2_PFD0, 11=PLL4           |
+| [20]    | PERIPH2_CLK2_SEL    | periph2_clk2 源:0=pll3_sw_clk, 1=OSC                                 |
 | [19:18] | PRE_PERIPH_CLK_SEL  | pre_periph_clk 源:00=PLL2, 01=PLL2_PFD2, 10=PLL2_PFD0, 11=PLL2_PFD2/2 |
-| [27:26] | PRE_PERIPH2_CLK_SEL | pre_periph2 源:00=PLL2, 01=PLL2_PFD2, 10=PLL2_PFD0, 11=PLL4           |
+| [13:12] | PERIPH_CLK2_SEL     | periph_clk2 源:00=pll3_sw_clk, 01=osc_clk, 10=pll2_bypass_clk         |
+
+> 原书代码 `CCM->CBCMR |= (1 << 18)` 即把 PRE_PERIPH_CLK_SEL 设为 01(PLL2_PFD2),与 bit[19:18] 一致。
 
 #### CCM_CSCMR1 — 时钟源选择寄存器 1
 
@@ -222,6 +251,17 @@ void clk_enable(void)
 
 ### 3.2 CCM_ANALOG 寄存器
 
+CCM_ANALOG(ANATOP)基地址 = **0x020C8000**:
+
+| 寄存器                  | 地址       | 说明                     |
+| ----------------------- | ---------- | ------------------------ |
+| CCM_ANALOG_PLL_ARMn     | 0x020C8000 | PLL1 (ARM PLL) 控制寄存器 |
+| CCM_ANALOG_PLL_528n     | 0x020C8030 | PLL2 (528_PLL)           |
+| CCM_ANALOG_PLL_USB1n    | 0x020C8010 | PLL3 (USB1_PLL)          |
+| CCM_ANALOG_PLL_USB2n    | 0x020C8020 | PLL7 (USB2_PLL)          |
+| CCM_ANALOG_PFD_480n     | 0x020C80F0 | PLL3 PFD 控制            |
+| CCM_ANALOG_PFD_528n     | 0x020C8100 | PLL2 PFD 控制            |
+
 #### CCM_ANALOG_PLL_ARMn — PLL1 (ARM PLL) 控制寄存器
 
 | 位域  | 名称       | 功能                                            |
@@ -231,20 +271,27 @@ void clk_enable(void)
 
 #### CCM_ANALOG_PFD_528n — PLL2 PFD 控制寄存器
 
-| 位域    | 名称         | 功能                                 |
-| ------- | ------------ | ------------------------------------ |
-| [5:0]   | PFD0_FRAC    | PLL2_PFD0 分频因子:528×18/PFD0_FRAC |
-| [6]     | PFD0_CLKGATE | PFD0 门控:1=关闭, 0=使能            |
-| [13:8]  | PFD1_FRAC    | PLL2_PFD1 分频因子                   |
-| [14]    | PFD1_CLKGATE | PFD1 门控                            |
-| [21:16] | PFD2_FRAC    | PLL2_PFD2 分频因子                   |
-| [22]    | PFD2_CLKGATE | PFD2 门控                            |
-| [29:24] | PFD3_FRAC    | PLL2_PFD3 分频因子                   |
-| [30]    | PFD3_CLKGATE | PFD3 门控                            |
+寄存器分为四组(PFD0~PFD3),每组 8 个 bit;FRAC 可设置范围 **12~35**:
+
+| 位域    | 名称         | 功能                                          |
+| ------- | ------------ | --------------------------------------------- |
+| [5:0]   | PFD0_FRAC    | PLL2_PFD0 分频因子:PLL2_PFD0 = 528×18/FRAC   |
+| [6]     | PFD0_STABLE  | 只读,读此位判断 PLL2_PFD0 是否稳定           |
+| [7]     | PFD0_CLKGATE | PFD0 输出使能:1=关闭, 0=使能                 |
+| [13:8]  | PFD1_FRAC    | PLL2_PFD1 分频因子                            |
+| [14]    | PFD1_STABLE  | PLL2_PFD1 稳定标志(只读)                    |
+| [15]    | PFD1_CLKGATE | PFD1 门控:1=关闭, 0=使能                     |
+| [21:16] | PFD2_FRAC    | PLL2_PFD2 分频因子                            |
+| [22]    | PFD2_STABLE  | PLL2_PFD2 稳定标志(只读)                    |
+| [23]    | PFD2_CLKGATE | PFD2 门控                                     |
+| [29:24] | PFD3_FRAC    | PLL2_PFD3 分频因子                            |
+| [30]    | PFD3_STABLE  | PLL2_PFD3 稳定标志(只读)                    |
+| [31]    | PFD3_CLKGATE | PFD3 门控                                     |
+
 
 #### CCM_ANALOG_PFD_480n — PLL3 PFD 控制寄存器
 
-结构与 PFD_528n 相同,频率公式为 `480 × 18 / PFDx_FRAC`。
+结构与 PFD_528n 完全相同,只是频率公式为 `PLL3_PFDx = 480 × 18 / PFDx_FRAC`。
 
 ### 3.3 NXP 推荐 PFD 频率
 
@@ -317,18 +364,19 @@ void imx6u_clkinit(void)
     /* ========================================
      * 步骤 3:设置 PLL3 (USB1) 各 PFD
      * ======================================== */
+    reg = 0;                    /* 清零 */
     reg = CCM_ANALOG->PFD_480;
     reg &= ~(0x3F3F3F3F);
 
-    reg |= 19 << 24;  /* PLL3_PFD3 = 480×18/19 = 454.7 MHz */
-    reg |= 17 << 16;  /* PLL3_PFD2 = 480×18/17 = 508.2 MHz */
+    reg |= 19 << 24;  /* PLL3_PFD3 = 480×18/19 = 454.74 MHz */
+    reg |= 17 << 16;  /* PLL3_PFD2 = 480×18/17 = 508.24 MHz */
     reg |= 16 << 8;   /* PLL3_PFD1 = 480×18/16 = 540 MHz */
     reg |= 12 << 0;   /* PLL3_PFD0 = 480×18/12 = 720 MHz */
 
     CCM_ANALOG->PFD_480 = reg;
 
     /* ========================================
-     * 步骤 4:设置 AHB 时钟 (最大 132 MHz)
+     * 步骤 4:设置 AHB 时钟 (最小 6 MHz,最大 132 MHz)
      * ======================================== */
 
     /* pre_periph_clk = PLL2_PFD2 = 396 MHz */
@@ -339,13 +387,21 @@ void imx6u_clkinit(void)
     CCM->CBCDR &= ~(1 << 25);  /* PERIPH_CLK_SEL = 0 */
     while (CCM->CDHIPR & (1 << 5));  /* 等待握手完成 */
 
-    /* AHB_PODF = 3 分频 → AHB_CLK_ROOT = 396/3 = 132 MHz
-     * 注意:内部 Boot ROM 已设置为 3 分频
-     * 修改 AHB_PODF 需先关闭 AHB_CLK_ROOT 输出
+    /* 修改 AHB_PODF 需要先禁止 AHB_CLK_ROOT 输出,但原书作者没有找到
+     * 关闭 AHB_CLK_ROOT 输出的寄存器,所以下面设置 AHB_PODF 的代码
+     * 仅供学习参考,不能直接使用(用 #if 0 屏蔽)!
+     * 内部 boot rom 已将 AHB_PODF 设置为 3 分频,不设置时
+     * AHB_ROOT_CLK 也等于 396/3 = 132 MHz。
      */
+#if 0
+    /* 要先关闭 AHB_ROOT_CLK 输出,否则时钟设置会出错 */
+    CCM->CBCDR &= ~(7 << 10);       /* CBCDR 的 AHB_PODF 清零 */
+    CCM->CBCDR |= 2 << 10;          /* AHB_PODF 3分频,AHB_CLK_ROOT=132MHz */
+    while (CCM->CDHIPR & (1 << 1)); /* 等待握手完成 */
+#endif
 
     /* ========================================
-     * 步骤 5:设置 IPG 时钟 (最大 66 MHz)
+     * 步骤 5:设置 IPG 时钟 (最小 3 MHz,最大 66 MHz)
      * ======================================== */
     CCM->CBCDR &= ~(3 << 8);   /* IPG_PODF 清零 */
     CCM->CBCDR |= 1 << 8;      /* IPG_PODF = 1 → 2 分频
@@ -402,10 +458,42 @@ flowchart TD
     J --> K["完成"]
 ```
 
+### 4.4 实验步骤(原书 16.2~16.4 节)
+
+**硬件原理**:时钟原理图分析参考 16.1.1 小节(32.768KHz 接 RTC,24MHz 作内核与外设时钟源)。
+
+**例程路径**:开发板光盘 -> 1、裸机例程 -> `8_clk`。本实验在上一章实验 `7_key` 基础上完成,直接修改 `bsp_clk.c`。
+
+**Makefile 修改**:本实验没有新增源文件,只需把变量 `TARGET` 改为 `clk`,链接脚本保持不变:
+
+```makefile
+CROSS_COMPILE ?= arm-linux-gnueabihf-
+TARGET        ?= clk
+```
+
+**编译烧写**:
+
+```bash
+chmod 777 imxdownload            # 给予 imxdownload 可执行权限,一次即可
+./imxdownload clk.bin /dev/sdd   # 烧写到 SD 卡,不能烧到 /dev/sda 或 sda1
+```
+
+**验证现象**:将 SD 卡插入开发板卡槽后复位。实验现象与 `7_key` 一样,但 **LED 闪烁频率更快**——因为 `7_key` 主频为 396MHz,本实验配置为 528MHz,代码执行延时函数更快,闪烁周期缩短。主频提升约 33%。
+
+**时钟修改五步法**(原书 16.1.4 节):
+
+1. 设置 `CCSR` 的 `STEP_SEL` 位,令 step_clk 时钟源为 24M 晶振。
+2. 设置 `CCSR` 的 `PLL1_SW_CLK_SEL` 位,令 pll1_sw_clk 时钟源为 step_clk=24MHz(先把主频降到 24MHz)。
+3. 设置 `CCM_ANALOG_PLL_ARMn`,将 pll1_main_clk(PLL1) 设为 1056MHz。
+4. 设置 `CCSR` 的 `PLL1_SW_CLK_SEL` 位,切回 pll1_main_clk=1056MHz。
+5. 设置 `CCM_CACRR` 的 `ARM_PODF` 为 2 分频,内核主频 = 1056/2 = 528MHz。
+
 ---
 
 ## 5. 电源管理概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第 16 章内容(该章只讲主频和时钟配置),为扩展知识。I.MX6ULL 的低功耗模式请以《I.MX6ULL 参考手册》电源管理章节为准。
+
 ### 5.1 电源域
 
 I.MX6ULL 的电源管理由 CCM (Clock Controller Module) 和 PMU (Power Management Unit) 协同完成。主要电源域:
@@ -421,12 +509,14 @@ I.MX6ULL 的电源管理由 CCM (Clock Controller Module) 和 PMU (Power Managem
 
 I.MX6ULL 支持多种运行模式:
 
-| 模式 | CPU 状态  | 功耗 | 唤醒源       | 典型应用 |
-| ---- | --------- | ---- | ------------ | -------- |
-| Run  | 全速运行  | 最高 | N/A          | 正常工作 |
-| Wait | 时钟停止  | 中   | 中断         | 等待事件 |
-| Stop | PLL 关闭  | 低   | 中断/GPIO    | 深度休眠 |
-| VLLS | SRAM 保持 | 极低 | RESET/WAKEUP | 电池备份 |
+| 模式    | CPU 状态        | 功耗 | 唤醒源            | 典型应用     |
+| ------- | --------------- | ---- | ----------------- | ------------ |
+| RUN     | 全速运行        | 最高 | N/A               | 正常工作     |
+| WAIT    | 内核时钟停止    | 中   | 中断              | 等待事件     |
+| STOP    | PLL/时钟门控    | 低   | 中断/GPIO/RTC     | 深度休眠     |
+| DORMANT | 内核掉电保持    | 极低 | 中断/GPIO         | 超低功耗待机 |
+| SNVS    | 仅 SNVS 域运行  | 最低 | RTC/SNVS 唤醒引脚 | 电池备份/RTC |
+
 
 ### 5.3 DVFS (动态电压频率调节)
 
@@ -454,6 +544,8 @@ DVFS 调节流程:
 
 ## 6. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 6.1 I.MX6ULL vs STM32 vs RK3568 时钟差异
 
 | 特性             | I.MX6ULL           | STM32F4                       | RK3568                     |
@@ -493,6 +585,8 @@ DVFS 调节流程:
 
 ## 7. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: I.MX6ULL 为什么使用 24 MHz 外部晶振而不是更高的?
 
 **答**:
@@ -556,3 +650,5 @@ AHB 总线连接着 MMDC (内存控制器)、GIC (中断控制器) 等关键部
 | **使用场景** | 多外设共享同一 PLL 的不同频点 | 单外设独立配置    |
 
 PFD 设计思想:一个 PLL 输出通过 PFD 矩阵一次性生成多个衍生频率,避免为每个外设单独配置 PLL,降低时钟切换延迟和功耗。STM32 的 PLL 每次修改都需要重新锁定,不适合频繁切换频率的场景。
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第16章 主频和时钟配置实验(16.1 时钟系统详解、16.1.2 七路 PLL、16.1.5 PFD、16.1.6 总线时钟、16.3 bsp_clk.c、16.4 编译下载验证);寄存器位域地址以《I.MX6ULL/I.MX6UL 参考手册》第 18 章 CCM 为准。

+ 158 - 16
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/04-GPIO与中断系统.md

@@ -8,7 +8,7 @@ pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第
 
 # GPIO与中断系统
 
-> 💡 **关联知识**: [[STM32学习笔记/02-GPIO寄存器详解与三步进化法]] | [[STM32学习笔记/04-GPIO输入与NVIC中断系统]] | [[嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/01-ARM架构概述]]
+> 💡 **关联知识**: [[STM32学习笔记/02-GPIO寄存器详解与三步进化法]] | [[STM32学习笔记/04-GPIO输入与NVIC中断系统]] | [[01-Cortex-A7架构详解]]
 
 ---
 
@@ -16,7 +16,8 @@ pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第
 
 ### 1.1 GPIO基本概念
 
-GPIO(General Purpose Input/Output)是通用输入输出接口,是处理器与外部设备交互的基础通道。IMX6ULL芯片提供5组GPIO(GPIO1~GPIO5),每组包含32个IO引脚,总共160个GPIO引脚。
+GPIO(General Purpose Input/Output)是通用输入输出接口,是处理器与外部设备交互的基础通道。IMX6ULL芯片提供5组GPIO(GPIO1~GPIO5),但各组引脚数并不相同:GPIO1 有 32 个 IO,GPIO2 有 22 个,GPIO3 有 29 个,GPIO4 有 29 个,GPIO5 最少只有 12 个,**一共 124 个 GPIO**(原书第 8.1.3 节)。
+
 
 ```mermaid
 flowchart LR
@@ -56,15 +57,17 @@ flowchart LR
 
 ### 1.3 GPIO寄存器基地址
 
+> 原书**第 8.1.5 节**列出 GPIO 一共 8 个寄存器:`DR、GDIR、PSR、ICR1、ICR2、EDGE_SEL、IMR、ISR`(原书参考《I.MX6UL 参考手册》第 26 章 /《I.MX6ULL 参考手册》第 28 章)。GPIO1 的起始地址为 0x0209C000,每组间隔 0x4000。
+
 ```c
-/* GPIO基地址定义 - IMX6ULL参考手册 Section 28 */
+/* GPIO基地址定义 - IMX6ULL参考手册 GPIO 章节 */
 #define GPIO1_BASE          0x0209C000
 #define GPIO2_BASE          0x020A0000
 #define GPIO3_BASE          0x020A4000
 #define GPIO4_BASE          0x020A8000
 #define GPIO5_BASE          0x020AC000
 
-/* GPIO寄存器偏移 - IMX6ULL参考手册 Table 28-3 */
+/* GPIO寄存器偏移 */
 #define GPIO_DR             0x000   /* 数据寄存器 */
 #define GPIO_GDIR           0x004   /* 方向寄存器 */
 #define GPIO_PSR            0x008   /* 状态寄存器 */
@@ -439,9 +442,11 @@ typedef enum IRQn {
 
 ### 3.5 GIC分发器端寄存器
 
+> I.MX6U 的 GIC 属于 ARM 私有外设,GIC 基地址(CBAR)= **0x00A00000**;分发器端相对基地址偏移 **0x1000**,即 GICD_BASE = **0x00A01000**;CPU 接口端偏移 **0x2000**,即 GICC_BASE = **0x00A02000**。运行时可用 `MRC p15, 4, r1, c15, c0, 0` 读取 CBAR 获得 GIC 基地址,再 `ADD r1, r1, #0x1000` / `#0x2000` 分别访问两端。
+
 ```c
-/* GIC分发器端寄存器 - IMX6ULL参考手册 Section 3.2 */
-#define GICD_BASE           0x01880000
+/* GIC分发器端寄存器 */
+#define GICD_BASE           0x00A01000   /* GIC 基地址 0x00A00000 + 0x1000 */
 
 /* 分发器控制寄存器 */
 #define GICD_CTLR           (*(volatile unsigned int *)(GICD_BASE + 0x000))
@@ -460,8 +465,8 @@ typedef enum IRQn {
 ### 3.6 GIC CPU接口端寄存器
 
 ```c
-/* GIC CPU接口端寄存器 - IMX6ULL参考手册 Section 3.2 */
-#define GICC_BASE           0x01840000
+/* GIC CPU接口端寄存器 */
+#define GICC_BASE           0x00A02000   /* GIC 基地址 0x00A00000 + 0x2000 */
 
 /* CPU接口控制寄存器 */
 #define GICC_CTLR           (*(volatile unsigned int *)(GICC_BASE + 0x000))
@@ -514,6 +519,65 @@ void set_vbar(uint32_t vbar_addr)
 set_vbar(0x87800000);
 ```
 
+**CP15 各寄存器用途小结(原书 17.1.4 节)**:
+
+- **c0**:MIDR 主 ID 寄存器,可读取处理器内核信息(ARM=0x41,Cortex-A7 内核版本 0xC07)。
+- **c1**:SCTLR 系统控制寄存器,用于使能/禁止 MMU、I/D Cache 等(bit0=M, bit1=A, bit2=C, bit11=Z, bit12=I, bit13=V)。
+- **c12**:VBAR 向量表基地址寄存器,用于设置中断向量表偏移。
+- **c15**:用 `MRC p15, 4, rN, c15, c0, 0` 读取 CBAR,得到 GIC 基地址。
+
+> SCTLR 关键位:bit13(V) 向量表基地址选择(0=0x00000000 可用 VBAR 重映射,1=0xFFFF0000 不可重映射);bit12(I) I-Cache 使能;bit2(C) D-Cache 与缓存一致性使能;bit0(M) MMU 使能。
+
+### 3.8 中断优先级设置(原书 17.1.6 节)
+
+Cortex-A7 的中断优先级同样分为**抢占优先级**和**子优先级**。GIC 最多支持 256 级优先级,**数字越小优先级越高**;Cortex-A7 选择了 32 级优先级。分三步设置:
+
+1. **设置优先级个数** —— GICC_PMR,只有低 8 位有效:
+
+| GICC_PMR bit[7:0] | 优先级数 |
+| ----------------- | -------- |
+| 11111111          | 256 个   |
+| 11111110          | 128 个   |
+| 11111100          | 64 个    |
+| 11111000          | 32 个    |
+| 11110000          | 16 个    |
+
+I.MX6U 是 Cortex-A7,只支持 32 个优先级,因此 **GICC_PMR = 0b11111000 = 0xF8**。
+
+2. **设置抢占/子优先级位数** —— GICC_BPR,只有低 3 位有效:
+
+| Binary Point | 抢占优先级域 | 子优先级域 | 描述                     |
+| ------------ | ------------ | ---------- | ------------------------ |
+| 0            | [7:1]        | [0]        | 7 级抢占,1 级子         |
+| 1            | [7:2]        | [1:0]      | 6 级抢占,2 级子         |
+| 2            | [7:3]        | [2:0]      | 5 级抢占,3 级子         |
+| 3            | [7:4]        | [3:0]      | 4 级抢占,4 级子         |
+| 4            | [7:5]        | [4:0]      | 3 级抢占,5 级子         |
+| 5            | [7:6]        | [5:0]      | 2 级抢占,6 级子         |
+| 6            | [7:7]        | [6:0]      | 1 级抢占,7 级子         |
+| 7            | 无           | [7:0]      | 0 级抢占,8 级子         |
+
+为简单起见,一般把所有位都配置为抢占优先级:I.MX6U 优先级位数为 5,故 Binary point 设为 **2**。
+
+3. **设置具体中断优先级** —— GICD_IPRIORITYR,每个中断 ID 一个寄存器。若优先级个数为 32,则使用 bit[7:4] 设置,实际优先级需**左移 3 位**:
+
+```c
+GICD_IPRIORITYR[40] = 5 << 3;   /* 设置 ID40 中断优先级为 5 */
+```
+
+### 3.9 总中断使能(原书 17.1.5 节)
+
+中断使能分两部分:**IRQ/FIQ 总开关**和 **ID0~ID1019 中断源使能**。
+
+| 指令      | 描述            |
+| --------- | --------------- |
+| `cpsid i` | 禁止 IRQ 中断   |
+| `cpsie i` | 使能 IRQ 中断   |
+| `cpsid f` | 禁止 FIQ 中断   |
+| `cpsie f` | 使能 FIQ 中断   |
+
+中断源使能由 **GICD_ISENABLERn / GICD_ICENABLERn** 完成。Cortex-A7 内核只使用了 512 个中断 ID,一个 bit 控制一个中断 ID,故需要 512/32 = **16 个** GICD_ISENABLERn(同理 16 个 GICD_ICENABLERn)。其中 `GICD_ISENABLER0` 的 bit[15:0] 对应 ID15~0 的 SGI 中断,bit[31:16] 对应 ID31~16 的 PPI 中断;`GICD_ISENABLER1~15` 控制 SPI 中断。
+
 ---
 
 ## 四、GPIO中断配置
@@ -626,21 +690,37 @@ void int_init(void)
     __set_VBAR((uint32_t)0x87800000);
 }
 
-/* 使能GIC中断 */
+/* 使能GIC中断(等价于 core_ca7.h 中的 GIC_EnableIRQ) */
 void irq_enable(int irq)
 {
-    /* 使能分发器端中断 */
-    GICD_ISENABLER(irq / 32] = 1 << (irq % 32);
+    /* 使能分发器端中断:ISENABLERn,n = irq/32,位号 = irq%32 */
+    GICD_ISENABLER(irq / 32) = 1U << (irq % 32);
 }
 
-/* 禁止GIC中断 */
+/* 禁止GIC中断(等价于 GIC_DisableIRQ) */
 void irq_disable(int irq)
 {
-    /* 禁止分发器端中断 */
-    GICD_ICENABLER(irq / 32] = 1 << (irq % 32);
+    /* 禁止分发器端中断:ICENABLERn */
+    GICD_ICENABLER(irq / 32) = 1U << (irq % 32);
 }
 ```
 
+
+**原书第 17.3.1 节 GIC 相关 API(core_ca7.h,共 10 个)**:
+
+| 函数                      | 描述                       |
+| ------------------------- | -------------------------- |
+| `GIC_Init`                | 初始化 GIC                 |
+| `GIC_EnableIRQ`           | 使能指定的外设中断         |
+| `GIC_DisableIRQ`          | 关闭指定的外设中断         |
+| `GIC_AcknowledgeIRQ`      | 返回中断号                 |
+| `GIC_DeactivateIRQ`       | 无效化指定中断             |
+| `GIC_GetRunningPriority`  | 获取当前正在运行的中断优先级 |
+| `GIC_SetPriorityGrouping` | 设置抢占优先级位数         |
+| `GIC_GetPriorityGrouping` | 获取抢占优先级位数         |
+| `GIC_SetPriority`         | 设置指定中断的优先级       |
+| `GIC_GetPriority`         | 获取指定中断的优先级       |
+
 ---
 
 ## 五、完整源码分析
@@ -678,6 +758,16 @@ Reset_Handler:
     bic r0, r0, #0x1            /* 清除M位,关闭MMU */
     mcr p15, 0, r0, c1, c0, 0  /* 写入CP15的C1寄存器 */
 
+#if 0
+    /* 汇编版本设置中断向量表偏移(原书中用 #if 0 屏蔽,实际在 C 的 int_init 中用 __set_VBAR 完成) */
+    ldr r0, =0X87800000
+    dsb
+    isb
+    mcr p15, 0, r0, c12, c0, 0
+    dsb
+    isb
+#endif
+
     /* 设置各模式栈指针 */
     /* IRQ模式栈 */
     mrs r0, cpsr
@@ -701,9 +791,17 @@ Reset_Handler:
     ldr sp, =0X80200000         /* SVC栈首地址 */
 
     cpsie i                     /* 打开全局中断 */
+#if 0
+    /* 使能IRQ中断(等价于 cpsie i,原书用 #if 0 屏蔽) */
+    mrs r0, cpsr                /* 读取cpsr寄存器值到r0中 */
+    bic r0, r0, #0x80           /* 清零CPSR的I位,允许IRQ中断 */
+    msr cpsr, r0                /* 重新写入cpsr */
+#endif
     b main                      /* 跳转到main函数 */
 ```
 
+> 另注:Reset_Handler 中各模式栈指针为 IRQ=0x80600000、SYS=0x80400000、SVC=0x80200000,每种模式栈大小都是 2MB,堆栈向下增长且必须 4 字节对齐。DDR 范围 0x80000000~0x9FFFFFFF(512MB)或 0x8FFFFFFF(256MB)。
+
 ### 5.3 IRQ中断服务函数
 
 ```asm
@@ -742,9 +840,32 @@ IRQ_Handler:
 ### 5.4 通用中断处理函数
 
 ```c
+/* 中断嵌套计数器 */
+static unsigned int irqNesting;
+
 /* 中断处理函数表 */
 static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
 
+/* 中断初始化函数(原书 bsp_int.c) */
+void int_init(void)
+{
+    GIC_Init();                        /* 初始化GIC */
+    system_irqtable_init();            /* 初始化中断表 */
+    __set_VBAR((uint32_t)0x87800000);  /* 中断向量表偏移 */
+}
+
+/* 初始化中断服务函数表:全部指向默认处理函数 */
+void system_irqtable_init(void)
+{
+    unsigned int i = 0;
+    irqNesting = 0;
+
+    for (i = 0; i < NUMBER_OF_INT_VECTORS; i++)
+    {
+        system_register_irqhandler((IRQn_Type)i, default_irqhandler, NULL);
+    }
+}
+
 /* 默认中断处理函数 */
 void default_irqhandler(unsigned int giccIar, void *userParam)
 {
@@ -863,10 +984,24 @@ int main(void)
 
 ### 6.1 编译方法
 
-```bash
-# 交叉编译工具链
+本实验在第十六章 `8_clk` 基础上修改,例程路径:开发板光盘 -> 1、裸机例程 -> `9_int`。Makefile 需要修改 `TARGET` 为 `int`,并在 `INCDIRS` 和 `SRCDIRS` 中追加 `bsp/exit` 和 `bsp/int`(原书 17.4.1 节):
+
+```makefile
 CROSS_COMPILE ?= arm-linux-gnueabihf-
+TARGET        ?= int
+
+INCDIRS := imx6ul \
+           bsp/clk bsp/led bsp/delay bsp/beep bsp/gpio bsp/key \
+           bsp/exit \        # GPIO 中断驱动
+           bsp/int           # 通用中断驱动
 
+SRCDIRS := project \
+           bsp/clk bsp/led bsp/delay bsp/beep bsp/gpio bsp/key \
+           bsp/exit \
+           bsp/int
+```
+
+```bash
 # 编译命令
 make clean
 make
@@ -910,6 +1045,8 @@ chmod 777 imxdownload
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568
 
 | 特性             | IMX6ULL (Cortex-A7) | STM32F103 (Cortex-M3) | RK3568 (Cortex-A55) |
@@ -959,6 +1096,8 @@ flowchart LR
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:GIC和NVIC有什么区别?
 
 **考察点**: 中断控制器架构理解
@@ -1033,6 +1172,8 @@ flowchart LR
 
 ## 九、常见问题排查
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 问题现象     | 可能原因       | 排查方法            | 解决方案             |
 | ------------ | -------------- | ------------------- | -------------------- |
 | 中断不触发   | GIC未使能      | 检查GICD_CTLR寄存器 | 确保GICD_CTLR[0]=1   |
@@ -1058,4 +1199,5 @@ flowchart LR
 ---
 
 **代码来源**: 裸机例程 9_int, bsp/int/, bsp/exit/
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第十七章 GPIO中断实验(17.1 中断系统详解、17.3 start.S/bsp_int/bsp_exit 源码、17.4 编译下载验证);GPIO 寄存器详解来自第八章 8.1.5/8.1.6 节。
 **最后更新**: 2026-09-17

+ 196 - 69
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/05-定时器与看门狗.md

@@ -6,6 +6,7 @@ updated: 2026-09-17
 pdf_ref:
   - "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第18章 EPIT定时器实验"
   - "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第19章 定时器按键消抖实验"
+  - "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第20章 高精度延时实验"
 ---
 
 # 定时器与看门狗
@@ -82,6 +83,8 @@ EPIT(Enhanced Periodic Interrupt Timer)增强的周期中断定时器,I.MX
 
 ### 1.3 EPIT 寄存器详解
 
+**基地址:** `EPIT1_BASE = 0x020D0000`、`EPIT2_BASE = 0x020D4000`(寄存器偏移见表,基地址取自官方头文件 `MCIMX6Y2.h`)。
+
 #### EPITx_CR — 控制寄存器(偏移 0x000)
 
 | 位域     | 名称      | 功能                                                             |
@@ -95,15 +98,16 @@ EPIT(Enhanced Periodic Interrupt Timer)增强的周期中断定时器,I.MX
 
 ```c
 #define EPIT_CR_EN          (1 << 0)
-#define EPIT_CR_SWR         (1 << 1)    /* 软件复位 */
 #define EPIT_CR_ENMOD       (1 << 1)
 #define EPIT_CR_OCIEN       (1 << 2)
 #define EPIT_CR_RLD         (1 << 3)
 #define EPIT_CR_PRESCALAR(x)(((x) & 0xFFF) << 4)
+#define EPIT_CR_SWR         (1 << 16)   /* 软件复位 */
 #define EPIT_CR_OM          (3 << 22)   /* 输出模式 */
-#define EPIT_CR_CLKSRC      (1 << 24)   /* 时钟源: 01=ipg_clk */
+#define EPIT_CR_CLKSRC(x)   (((x) & 0x3) << 24)   /* 时钟源: 01=ipg_clk */
 ```
 
+
 #### EPITx_SR — 状态寄存器(偏移 0x004)
 
 | 位域 | 名称 | 功能                                                |
@@ -145,17 +149,53 @@ Tout = ((0+1) × 33000000) / 66000000 = 0.5s
 
 ### 1.4 GPT 定时器概述
 
-GPT(General Purpose Timer)通用定时器,比 EPIT 功能更丰富,支持**输出比较**和**输入捕获**,适合 PWM 输出场景。
+GPT(General Purpose Timer)通用定时器,比 EPIT 功能更丰富。教材**第20章「高精度延时实验」正是用 GPT1 实现高精度延时**,本节按第20章 PDF 原文核对。
+
+**GPT 特点(PDF 20.1.1):**
+
+- 可选时钟源的 32 位**向上**计数器(从 0x00000000 开始递增)
+- **12 位分频器**(PRESCALER 0~4095 → 1~4096 分频)
+- 两个输入捕获通道(可设置触发方式)
+- 三个输出比较通道(可设置输出模式)
+- 可产生捕获中断、比较中断和溢出中断
+- 计数器可运行在重新启动(restart)或自由运行(free-run)模式
+
+**五个可选时钟源(PDF 图 20.1.1.1):**
+
+| CLKSRC(bit8:6) | 时钟源                              |
+| -------------- | ----------------------------------- |
+| 0              | 关闭时钟源                          |
+| **1**          | **ipg_clk(=66MHz,本例程选择)**   |
+| 2              | ipg_clk_highfreq                    |
+| 3              | 外部时钟 GPT_CLK                    |
+| 4              | ipg_clk_32k                         |
+| 5              | ipg_clk_24M                         |
+
+**两种工作模式(FRR 位):**
+
+| 模式                  | FRR位 | 行为                                              |
+| --------------------- | ----- | ------------------------------------------------- |
+| **restart(重新启动)** | FRR=0 | 计数值等于比较值时计数器清零,仅比较通道 1 有效   |
+| **free-run(自由运行)** | FRR=1 | 比较事件不清零计数器,直到 0xFFFFFFFF 回滚到 0    |
 
-**GPT 特点:**
+**GPTx_CR 重要位域(PDF 20.1.1):**
 
-- 32位向上计数器
-- 支持3个输出比较通道(OCR1/OCR2/OCR3)
-- 支持2个输入捕获通道(ICR1/ICR2)
-- 可选择时钟源和分频
+| 位域   | 名称    | 功能                                             |
+| ------ | ------- | ------------------------------------------------ |
+| bit15  | SWR     | 复位 GPT 定时器,写 1 复位,完成后自动清零       |
+| bit9   | FRR     | 0=restart 模式,1=free-run 模式                  |
+| bit8:6 | CLKSRC  | 时钟源选择(见上表)                             |
+| bit1   | ENMOD   | 0=关闭时保留计数值,1=关闭时计数器清零           |
+| bit0   | EN      | GPT 使能:1=使能,0=关闭                         |
+
+**GPTx_PR:** PRESCALER(bit11:0),0~4095 → 1~4096 分频。
+**GPTx_SR:** ROV(bit5) 回滚标志、IF2~IF1(bit4:3) 输入捕获标志、OF3~OF1(bit2:0) 输出比较标志。
+**GPTx_CNT:** 当前计数值;**GPTx_OCR1~3:** 三路 32 位输出比较值。
 
 #### GPT 核心寄存器
 
+**基地址:** `GPT1_BASE = 0x02098000`、`GPT2_BASE = 0x020E8000`。
+
 ```c
 #define GPT_CR      0x000   /* 控制寄存器 */
 #define GPT_PR      0x004   /* 预分频寄存器 */
@@ -169,42 +209,40 @@ GPT(General Purpose Timer)通用定时器,比 EPIT 功能更丰富,支
 #define GPT_CNT     0x024   /* 计数器 */
 ```
 
-#### GPT 初始化示例
+#### GPT 初始化示例(高精度延时用,教材第20章例程)
 
 ```c
-void gpt1_init(void)
+void delay_init(void)
 {
-    /* 复位GPT1 */
-    GPT1_CR |= GPT_CR_SWR;
-    while (GPT1_CR & GPT_CR_SWR) {}
+    GPT1->CR = 0;
+    GPT1->CR = 1 << 15;                 /* bit15 SWR 置1,进入软复位 */
+    while ((GPT1->CR >> 15) & 0x01);    /* 等待复位完成 */
 
-    /* 配置GPT1: 选择ipg_clk, 使能GPT1 */
-    GPT1_CR = 0;
-    GPT1_CR |= (1 << 16);              /* 选择ipg_clk */
-    GPT1_CR |= GPT_CR_EN;
+    /* CLKSRC(bit8:6)=001,选择 ipg_clk=66MHz;FRR(bit9)=0,restart 模式 */
+    GPT1->CR = (1 << 6);
 
-    /* 预分频: 66MHz/66 = 1MHz */
-    GPT1_PR = 65;
+    /* 66 分频,GPT1 时钟为 66M/(65+1)=1MHz,计数一次即 1us */
+    GPT1->PR = 65;
 
-    /* 使能OC1比较中断 */
-    GPT1_IR |= (1 << 0);
+    /* 不使用比较中断,OCR1 设为最大值 */
+    GPT1->OCR[0] = 0xFFFFFFFF;
 
-    /* 比较值: 1MHz下1000000=1秒中断 */
-    GPT1_OCR1 = 1000000;
+    GPT1->CR |= 1 << 0;                 /* 使能 GPT1 */
 }
 ```
 
+
 #### EPIT vs GPT 对比
 
-| 特性         | EPIT                          | GPT                   |
-| ------------ | ----------------------------- | --------------------- |
-| 计数方向     | **向下**                      | 向上                  |
-| 核心功能     | 周期性中断                    | PWM/捕获/比较         |
-| 输出比较通道 | 无(仅CMPR)                  | 3个(OCR1/OCR2/OCR3) |
-| 输入捕获     | 无                            | 2个(ICR1/ICR2)      |
-| 工作模式     | set-and-forget / free-running | 多种模式              |
-| 典型用途     | 精确延时、周期中断            | PWM输出、脉冲计数     |
-| 配置复杂度   | **简单**                      | 较复杂                |
+| 特性         | EPIT                          | GPT                        |
+| ------------ | ----------------------------- | -------------------------- |
+| 计数方向     | **向下**                      | 向上                       |
+| 核心功能     | 周期性中断                    | 高精度延时/PWM/捕获/比较   |
+| 输出比较通道 | 无(仅CMPR)                  | 3个(OCR1/OCR2/OCR3)      |
+| 输入捕获     | 无                            | 2个(ICR1/ICR2)           |
+| 工作模式     | set-and-forget / free-running | restart / free-run         |
+| 典型用途     | 周期中断定时、定时器消抖      | 高精度延时、PWM输出、输入捕获 |
+| 配置复杂度   | **简单**                      | 较复杂                     |
 
 > **选型建议:** 简单定时/周期中断用 EPIT,需要 PWM 或输入捕获用 GPT。
 
@@ -325,6 +363,12 @@ int main(void)
 
 ### 1.6 定时器消抖
 
+**教材实现步骤(PDF 19.1):**
+
+1. **配置按键 IO 中断**:配置按键所用 IO 为中断模式(下降沿触发)。
+2. **初始化消抖用的定时器**:使用 EPIT1,定时周期 10ms(可按需调整)。
+3. **编写中断处理函数**:按键 GPIO 中断里只重启 EPIT1;真正的按键处理放在 EPIT1 中断里完成(如开关蜂鸣器)。
+
 #### 消抖原理
 
 按键机械结构在按下/松开瞬间会产生 5~20ms 的电平抖动。传统做法是用软件延时等待抖动结束,但存在两个问题:
@@ -371,6 +415,23 @@ t1~t3 为抖动期(<10ms),每次 GPIO 中断都重启定时器。只有 t3
 
 #### 消抖实验源码
 
+**bsp_keyfilter.h:**
+
+```c
+#ifndef _BSP_KEYFILTER_H
+#define _BSP_KEYFILTER_H
+
+/* 函数声明 */
+void filterkey_init(void);
+void filtertimer_init(unsigned int value);
+void filtertimer_stop(void);
+void filtertimer_restart(unsigned int value);
+void filtertimer_irqhandler(void);
+void gpio1_16_31_irqhandler(void);
+
+#endif
+```
+
 **bsp_keyfilter.c:**
 
 ```c
@@ -470,6 +531,40 @@ void gpio1_16_31_irqhandler(void)
 }
 ```
 
+**main.c:**
+
+```c
+#include "bsp_clk.h"
+#include "bsp_delay.h"
+#include "bsp_led.h"
+#include "bsp_beep.h"
+#include "bsp_key.h"
+#include "bsp_int.h"
+#include "bsp_keyfilter.h"
+
+int main(void)
+{
+    unsigned char state = OFF;
+
+    int_init();                 /* 初始化中断(一定要最先调用!) */
+    imx6u_clkinit();            /* 初始化系统时钟 */
+    clk_enable();               /* 使能所有的时钟 */
+    led_init();                 /* 初始化LED */
+    beep_init();                /* 初始化BEEP */
+    filterkey_init();           /* 带有消抖功能的按键 */
+
+    while (1)
+    {
+        state = !state;
+        led_switch(LED0, state);   /* LED0 约 500ms 闪烁,提示系统运行 */
+        delay(500);
+    }
+    return 0;
+}
+```
+
+> 编译时需把 Makefile 的 `TARGET` 改为 `keyfilter`,并在 `INCDIRS` 与 `SRCDIRS` 中加入 `bsp/keyfilter`。
+
 **消抖关键点总结:**
 
 | 要素       | 说明                                       |
@@ -483,6 +578,9 @@ void gpio1_16_31_irqhandler(void)
 
 ## Part 2: 看门狗
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+> (教材裸机部分(第8~27章)未安排看门狗实验,仅在第8章引脚复用表中出现过 `WDOG3_WDOG_B`;此处寄存器信息依据《I.MX6ULL参考手册》与官方头文件 `MCIMX6Y2.h` 整理。)
+
 ### 2.1 看门狗概述
 
 看门狗(Watchdog Timer)是一种硬件定时器,用于在软件跑飞或死锁时自动复位系统。
@@ -508,19 +606,20 @@ void gpio1_16_31_irqhandler(void)
 
 ### 2.2 看门狗寄存器
 
+**基地址:** `WDOG1_BASE = 0x020BC000`、`WDOG2_BASE = 0x020C0000`(另有 WDOG3,本表未列出)。
+
 #### WDOG_WCR — 看门狗控制寄存器(偏移 0x000)
 
-| 位域    | 名称      | 功能                              |
-| ------- | --------- | --------------------------------- |
-| bit0    | WDZST     | 停止计数:1=在调试/低功耗模式停止 |
-| bit1    | WDBG      | 调试模式:1=调试模式停止计数      |
-| bit2    | WDE       | **看门狗使能**:0=禁止,1=使能    |
-| bit3    | WDT       | 定时器复位使能:1=超时产生复位    |
-| bit4    | SRS       | 软件复位:写0产生复位             |
-| bit5    | WDA       | 软件看门狗:写1暂停看门狗         |
-| bit8    | WDHO      | 超时输出高                        |
-| bit11   | WDTOP     | 超时周期选择                      |
-| bit15:8 | WCR[15:8] | **超时时间配置位**                |
+| 位域    | 名称  | 功能                                           |
+| ------- | ----- | ---------------------------------------------- |
+| bit0    | WDZST | 停止计数:1=在调试/低功耗模式停止计数          |
+| bit1    | WDBG  | 调试模式:1=调试模式停止计数                   |
+| bit2    | WDE   | **看门狗使能**:0=禁止,1=使能                 |
+| bit3    | WDT   | 定时器复位使能:1=超时产生复位                 |
+| bit4    | SRS   | 软件复位:写 0 产生复位                        |
+| bit5    | WDA   | 软件看门狗:写 1 暂停看门狗                    |
+| bit15:8 | WT    | **超时时间配置位**(16 个档位)                |
+
 
 #### WDOG_WSR — 服务寄存器(偏移 0x002)
 
@@ -528,7 +627,15 @@ void gpio1_16_31_irqhandler(void)
 
 #### WDOG_WRSR — 复位状态寄存器(偏移 0x004)
 
-记录复位原因:看门狗超时复位、软件复位等。
+记录复位原因:`SFTW(bit0)` 软件复位、`TOUT(bit1)` 看门狗超时复位、`POR(bit4)` 上电复位等。
+
+#### WDOG_WICR — 中断控制寄存器(偏移 0x006)
+
+看门狗中断控制,用于配置超时中断(而非直接复位)相关行为。
+
+#### WDOG_WMCR — 杂项控制寄存器(偏移 0x008)
+
+看门狗杂项控制寄存器。
 
 ---
 
@@ -539,21 +646,22 @@ void gpio1_16_31_irqhandler(void)
 #define WDOG1_WCR       (*(volatile unsigned short *)(WDOG1_BASE + 0x000))
 #define WDOG1_WSR       (*(volatile unsigned short *)(WDOG1_BASE + 0x002))
 #define WDOG1_WRSR      (*(volatile unsigned short *)(WDOG1_BASE + 0x004))
+#define WDOG1_WICR      (*(volatile unsigned short *)(WDOG1_BASE + 0x006))
+#define WDOG1_WMCR      (*(volatile unsigned short *)(WDOG1_BASE + 0x008))
 
-/* 看门狗初始化 */
+/* 看门狗初始化(超时时间由 WT[15:8] 与 WDOG 时钟源共同决定,公式见参考手册) */
 void wdog_init(void)
 {
-    /* 1、关闭看门狗(先配置再使能) */
-    WDOG1_WCR = 0;
+    /* SRS(bit4)/WDA(bit5) 应保持为 1,避免写 0 误触发软件复位/暂停看门狗 */
+    WDOG1_WCR = (1 << 4) | (1 << 5);
 
-    /* 2、配置超时时间
-     * bit15:8 = WCR[15:8],超时时间 = 2^14 / 66MHz ≈ 0.25秒
-     * bit2 WDE = 1 使能看门狗
-     * bit3 WDT = 1 超时产生复位
+    /* 2、配置超时时间与使能
+     * WT(bit15:8) 超时档位(示例取 5),具体超时值与 WDOG 时钟源有关
+     * WDT(bit3)=1 超时产生复位;WDE(bit2)=1 使能看门狗
      */
-    WDOG1_WCR |= (5 << 8);          /* 超时配置 */
-    WDOG1_WCR |= (1 << 2);          /* 使能看门狗 */
-    WDOG1_WCR |= (1 << 3);          /* 超时复位使能 */
+    WDOG1_WCR |= (5 << 8);
+    WDOG1_WCR |= (1 << 3);
+    WDOG1_WCR |= (1 << 2);
 }
 
 /* 喂狗函数:依次写入0x5555和0xAAAA */
@@ -564,13 +672,14 @@ void wdog_feed(void)
 }
 ```
 
+
 **看门狗配置要点:**
 
 | 操作     | 说明                                             |
 | -------- | ------------------------------------------------ |
-| 初始化   | 先清零WCR,配置超时时间和使能位                  |
+| 初始化   | 配置时保持 `SRS/WDA=1`,再设置超时档位与使能位   |
 | 喂狗     | 必须先写0x5555再写0xAAAA,顺序不能错             |
-| 超时时间 | 由WCR[15:8]位控制,与看门狗时钟源频率相关        |
+| 超时时间 | 由 WT[15:8] 与看门狗时钟源共同决定               |
 | 注意     | 使能后**必须在主循环中定期喂狗**,否则系统会复位 |
 
 ---
@@ -616,26 +725,43 @@ void wdog_feed(void)
 
 **考察点:** 定时器应用
 
-**答案:**
+**答案(教材第20章使用 GPT1 实现,非 EPIT):**
 
-1. 使用 EPIT 配置为 **1MHz**(66分频:66MHz/66=1MHz,每个时钟周期=1μs)
-2. 设置 EPIT 为 **free-running 模式**,LR=0xFFFFFFFF
-3. 读取 `EPIT1_CNR` 获取当前计数值
-4. 循环读取计数值差值,直到达到目标延时
+1. 选用 **GPT1**,时钟源设为 ipg_clk=66MHz;分频寄存器 `GPT1_PR = 65`(66 分频),进入 GPT1 的时钟为 66MHz/(65+1)=**1MHz**,计数一次即 1μs。
+2. 输出比较寄存器 `GPT1->OCR[0] = 0xFFFFFFFF`(不使用比较中断)。
+3. 使能 GPT1(`GPT1->CR |= 1<<0`),计数器从 0 向上递增。
+4. 延时函数读取 `GPT1->CNT` 并累加走过的计数值,达到目标 us 数即结束;**必须处理 0xFFFFFFFF→0 的溢出回滚**。
 
 ```c
-/* 伪代码 */
-void delay_us(unsigned int us) {
-    unsigned int start = EPIT1_CNR;
-    while ((start - EPIT1_CNR) < us) {}  /* 向下计数,差值即μs数 */
+/* 教材第20章 delayus 实现 */
+void delayus(unsigned int usdelay)
+{
+    unsigned long oldcnt, newcnt;
+    unsigned long tcntvalue = 0;      /* 走过的总时间 */
+
+    oldcnt = GPT1->CNT;
+    while (1)
+    {
+        newcnt = GPT1->CNT;
+        if (newcnt != oldcnt)
+        {
+            if (newcnt > oldcnt)                       /* 向上计数且未溢出 */
+                tcntvalue += newcnt - oldcnt;
+            else                                        /* 发生溢出回滚 */
+                tcntvalue += 0xFFFFFFFF - oldcnt + newcnt;
+            oldcnt = newcnt;
+            if (tcntvalue >= usdelay)                   /* 延时时间到 */
+                break;
+        }
+    }
 }
 ```
 
 **关键注意事项:**
 
-- 必须使用硬件定时器,**不能用软件循环**(受编译器优化影响)
-- free-running 模式下需注意**计数器溢出**处理
-- 需要关闭中断以保证延时精度
+- 教材的高精度延时用的是 **GPT**(向上计数),EPIT 是向下计数器、只提供周期中断,不用于本延时实现。
+- GPTx_CNT 为 32 位,1MHz 下最多约 0xFFFFFFFFμs≈4294s≈72min 后回滚,**必须处理溢出**。
+- 主循环中除延时外还有别的代码开销(如 `led_switch`),示波器实测周期会略大于理论值,属正常现象。
 
 ---
 
@@ -697,5 +823,6 @@ Linux 内核的按键驱动正是使用定时器消抖方案。核心思想:GP
 
 ---
 
-**代码来源**: 裸机例程 `10_epit_timer` (EPIT定时器), `11_key_filter` (定时器消抖)
+**代码来源**: 裸机例程 `10_epit_timer` (EPIT定时器), `11_key_filter` (定时器消抖), `12_highpreci_delay` (GPT高精度延时)
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第18章 EPIT定时器实验、第19章 定时器按键消抖实验、第20章 高精度延时实验(看门狗部分为扩展知识)
 **最后更新**: 2026-09-17

+ 4 - 1
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/06-UART串口通信.md

@@ -717,7 +717,7 @@ void raise(int sig_nr)
 | 接收超时中断 | `UCR2[3]` ATEN + UTIM                 | 判断一帧结束(配合老化定时器) |
 | DMA          | `UCR1[8]` RXDMAEN / `UCR1[3]` TXDMAEN | 大数据量、高波特率             |
 
-中断方式下,接收中断号由 GIC 提供,I.MX6ULL 的 UART1~UART8 依次为 **IRQ 58~62、49、71、72**。`uart_softreset`、`uart_enable` 等初始化代码完全复用,只需额外打开中断使能位,并在 `bsp_int.c` 中注册中断服务函数:
+中断方式下,接收中断号由 GIC 提供,I.MX6ULL 的 UART1~UART8 依次为 **IRQ 58~62、49、71、72**(即 `UART1_IRQn=58`、`UART2_IRQn=59` … `UART5_IRQn=62`、`UART6_IRQn=49`、`UART7_IRQn=71`、`UART8_IRQn=72`,与官方头文件 `MCIMX6Y2.h` 一致)。`uart_softreset`、`uart_enable` 等初始化代码完全复用,只需额外打开中断使能位,并在 `bsp_int.c` 中注册中断服务函数:
 
 ```c
 /* 中断方式接收的最小实现框架(对照本章轮询版本) */
@@ -870,6 +870,8 @@ $(COBJS) : obj/%.o : %.c
 
 ### 5.3 重定向原理:fputc / fgetc
 
+> ⚠️ **来源说明**:教材第22章仅说明移植的 `stdio` 源码「抠自 U-Boot」,并未展开 `fputc`/`fgetc` 的实现细节;本节的重定向代码为扩展知识。
+
 标准 `printf` 最终会把每个字符交给 `fputc()` 输出,`scanf` 则通过 `fgetc()` 取字符。因此**只要重写这两个函数,把底层换成串口收发即可**:
 
 ```c
@@ -1125,5 +1127,6 @@ UBIR = 575, UBMR = 3124   →  80e6 × 576 / (16 × 3125) = 921600,误差 0
 ---
 
 **代码来源**: 裸机例程 `13_uart`(`bsp/uart/bsp_uart.c`、`bsp_uart.h`)、`14_printf`(`stdio/include`、`stdio/lib`)
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第21章 UART串口通信实验、第22章 串口格式化函数移植实验
 **寄存器依据**: 《I.MX6ULL 参考手册》第 55 章 Universal Asynchronous Receiver/Transmitter(UART);NXP SDK `MCIMX6Y2.h` / `fsl_uart.h`
 **最后更新**: 2026-09-17

+ 6 - 2
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/07-BSP工程管理与封装.md

@@ -101,6 +101,8 @@ BSP工程管理的核心思想:**按功能模块分目录,按职责分离文
 | `imx6ul/`  | 芯片头文件 | NXP官方SDK头文件和芯片寄存器定义       |
 | `obj/`     | 编译产物   | 存放编译生成的.o文件,保持源码目录整洁 |
 
+> 教材第13章只在 `bsp/` 下新建了 **clk、delay、led** 三个子目录(对应例程 `5_ledc_bsp`)。后续章节会按同样方式继续扩展 `bsp/`:第14章加 `beep`,第15章加 `gpio`、`key`,第17章加 `exit`、`int`,第18章加 `epittimer`,第19章加 `keyfilter`,第21章加 `uart` 等。每个子目录都包含一对 `bsp_xxx.c` / `bsp_xxx.h`。
+
 ### 2.3 文件命名规范
 
 | 文件类型      | 命名规则         | 示例                         |
@@ -233,6 +235,8 @@ SD卡启动
 
 ## 四、工程管理脚本
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第13章只讲解 Makefile 与链接脚本,未提供 build/burn/clean 等脚本。
+
 ### 4.1 一键编译脚本
 
 创建`build.sh`脚本,一键完成编译和清理:
@@ -365,8 +369,6 @@ echo "=== 清理完成 ==="
 #include "imx6ul.h"
 
 #define LED0    0
-#define ON      1
-#define OFF     0
 
 void led_init(void);
 void led_switch(int led, int status);
@@ -374,6 +376,7 @@ void led_switch(int led, int status);
 #endif
 ```
 
+
 #### 5.2.3 bsp_led.c
 
 ```c
@@ -654,4 +657,5 @@ chmod 777 imxdownload
 ---
 
 **代码来源**: 裸机例程 5_ledc_bsp
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第13章 BSP工程管理实验
 **最后更新**: 2026-09-17

+ 459 - 179
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/08-裸机实验总结.md

@@ -6,9 +6,17 @@ updated: 2026-09-17
 pdf_ref:
   - "第八章 汇编LED灯实验.pdf"
   - "第十章 C语言版LED灯实验.pdf"
+  - "第十二章 官方SDK移植实验.pdf"
+  - "第十三章 BSP工程管理实验.pdf"
   - "第十四章 蜂鸣器实验.pdf"
   - "第十五章 按键输入实验.pdf"
+  - "第十六章 主频和时钟配置实验.pdf"
+  - "第十七章 GPIO中断实验.pdf"
+  - "第十八章 EPIT定时器实验.pdf"
+  - "第十九章 定时器按键消抖实验.pdf"
   - "第二十章 高精度延时实验.pdf"
+  - "第二十一章 UART串口通信实验.pdf"
+  - "第二十二章 串口格式化函数移植实验.pdf"
   - "第二十三章 DDR3实验.pdf"
   - "第二十四章 RGBLCD显示实验.pdf"
   - "第二十五章 RTC实时时钟实验.pdf"
@@ -60,13 +68,15 @@ pdf_ref:
 
 I.MX6ULL的GPIO系统由5组控制器组成:
 
-| GPIO组 | IO数量 | 基地址     | 说明                        |
-| ------ | ------ | ---------- | --------------------------- |
-| GPIO1  | 32个   | 0x0209C000 | 仅GPIO1_IO00~IO09为专用GPIO |
-| GPIO2  | 22个   | 0x020A0000 | 其余IO需通过IOMUXC复用      |
-| GPIO3  | 29个   | 0x020A4000 |                             |
-| GPIO4  | 29个   | 0x020A8000 |                             |
-| GPIO5  | 12个   | 0x020AC000 | 包含SNVS域IO                |
+| GPIO组 | IO数量 | 基地址     | 说明                                              |
+| ------ | ------ | ---------- | ------------------------------------------------- |
+| GPIO1  | 32个   | 0x0209C000 | GPIO1_IO00~IO09 在 IOMUXC 中有独立 GPIO 命名      |
+| GPIO2  | 22个   | 0x020A0000 | 其余 IO 需先经 IOMUXC 复用(MUX_MODE=ALT5)为 GPIO |
+| GPIO3  | 29个   | 0x020A4000 |                                                   |
+| GPIO4  | 29个   | 0x020A8000 |                                                   |
+| GPIO5  | 12个   | 0x020AC000 | 包含 SNVS 域 IO(如 SNVS_TAMPER1)                |
+
+> 5 组 GPIO 共 124 个 IO(32+22+29+29+12)。每组都有 8 个寄存器:`DR`(数据)、`GDIR`(方向)、`PSR`(状态)、`ICR1`/`ICR2`(中断配置,每个 IO 2 位)、`IMR`(中断屏蔽)、`ISR`(中断状态,写 1 清零)、`EDGE_SEL`(边沿选择,置 1 为双边沿,覆盖 ICR)。ICR 触发方式:00 低电平、01 高电平、10 上升沿、11 下降沿。
 
 **GPIO初始化三步曲**:
 
@@ -94,11 +104,13 @@ LED0连接到GPIO1_IO03,通过三极管驱动,低电平点亮。
 .global _start
 
 _start:
-    @ 使能GPIO1时钟
-    ldr r0, =0x020C4068     @ CCM_CCGR1寄存器地址
+    @ 使能GPIO1时钟:GPIO1时钟由 CCM_CCGR1 的 bit27:26 控制
+    @ 注意:0x020C4068 是 CCM_CCGR0,CCM_CCGR1 应为 0x020C406C
+    ldr r0, =0x020C406C     @ CCM_CCGR1寄存器地址
     ldr r1, [r0]
-    orr r1, r1, #(3 << 26)  @ 设置CG14位[27:26]=11
+    orr r1, r1, #(3 << 26)  @ CG13 位[27:26]=11,使能GPIO1时钟
     str r1, [r0]
+    @ (教材例程更简单:CCGR0~CCGR6 从 0x020C4068 起每 4 字节一个,全部写 0xFFFFFFFF)
 
     @ 配置GPIO1_IO03复用为GPIO
     ldr r0, =0x020E0068     @ IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
@@ -158,34 +170,52 @@ _start:
     b main                  @ 跳转到main函数
 ```
 
+**main.h(以宏定义寄存器地址)**:
+
+```c
+#define CCM_CCGR0   *((volatile unsigned int *)0x020C4068)   /* 注意 CCGR0=0x020C4068 */
+#define CCM_CCGR1   *((volatile unsigned int *)0x020C406C)   /*      CCGR1=0x020C406C */
+#define CCM_CCGR2   *((volatile unsigned int *)0x020C4070)
+#define CCM_CCGR3   *((volatile unsigned int *)0x020C4074)
+#define CCM_CCGR4   *((volatile unsigned int *)0x020C4078)
+#define CCM_CCGR5   *((volatile unsigned int *)0x020C407C)
+#define CCM_CCGR6   *((volatile unsigned int *)0x020C4080)
+#define SW_MUX_GPIO1_IO03  *((volatile unsigned int *)0x020E0068)
+#define SW_PAD_GPIO1_IO03  *((volatile unsigned int *)0x020E02F4)
+#define GPIO1_DR    *((volatile unsigned int *)0x0209C000)
+#define GPIO1_GDIR  *((volatile unsigned int *)0x0209C004)
+```
+
 **C语言主函数 main.c**:
 
 ```c
 #include "main.h"
 
-int main(void)
+void clk_enable(void)              /* 使能所有外设时钟 */
 {
-    /* 使能GPIO1时钟 */
-    unsigned int *CCM_CCGR1 = (unsigned int *)0x020C4068;
-    *CCM_CCGR1 |= (3 << 26);
-
-    /* 配置GPIO1_IO03复用 */
-    unsigned int *SW_MUX = (unsigned int *)0x020E0068;
-    *SW_MUX = 5;
-
-    /* 配置电气属性 */
-    unsigned int *SW_PAD = (unsigned int *)0x020E02F4;
-    *SW_PAD = 0x10B0;
+    CCM_CCGR0 = 0xffffffff;
+    CCM_CCGR1 = 0xffffffff;
+    CCM_CCGR2 = 0xffffffff;
+    CCM_CCGR3 = 0xffffffff;
+    CCM_CCGR4 = 0xffffffff;
+    CCM_CCGR5 = 0xffffffff;
+    CCM_CCGR6 = 0xffffffff;
+}
 
-    /* 设置为输出 */
-    unsigned int *GPIO1_GDIR = (unsigned int *)0x0209C004;
-    *GPIO1_GDIR |= (1 << 3);
+void led_init(void)                /* 初始化 LED 所用 GPIO */
+{
+    SW_MUX_GPIO1_IO03 = 0x5;       /* 复用为 GPIO1_IO03(ALT5) */
+    SW_PAD_GPIO1_IO03 = 0x10B0;    /* IO 电气属性 */
+    GPIO1_GDIR = 0x8;              /* bit3=1,设置为输出 */
+    GPIO1_DR = 0x0;               /* 输出低电平,打开 LED0 */
+}
 
-    /* 输出低电平点亮LED */
-    unsigned int *GPIO1_DR = (unsigned int *)0x0209C000;
-    *GPIO1_DR &= ~(1 << 3);
+int main(void)
+{
+    clk_enable();                  /* 使能所有的时钟 */
+    led_init();                    /* 初始化 LED */
 
-    while(1);
+    while (1) {}                   /* 死循环 */
 }
 ```
 
@@ -286,31 +316,82 @@ int gpio_pinread(GPIO_Type *base, int pin)
 }
 ```
 
-**按键检测逻辑**:
+**按键驱动 bsp_key.c(教材第15章)**:
 
 ```c
-int main(void)
+void key_init(void)
 {
-    int value = 0;
     gpio_pin_config_t key_config;
 
-    /* 初始化KEY0为输入 */
+    /* 1、初始化 IO 复用,复用为 GPIO1_IO18 */
+    IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
+
+    /* 2、配置 UART1_CTS_B 的 IO 属性:0xF080
+     * bit[15:14]=11 默认 22K 上拉  bit[13]=1 pull  bit[12]=1 pull/keeper 使能
+     * bit[5:3]=000 关闭输出        bit[0]=0 低转换率
+     */
+    IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
+
+    /* 3、初始化 GPIO,GPIO1_IO18 设置为输入 */
     key_config.direction = kGPIO_DigitalInput;
     gpio_init(GPIO1, 18, &key_config);
+}
+
+int key_getvalue(void)          /* 0:无按键,其他值:对应按键值 */
+{
+    int ret = 0;
+    static unsigned char release = 1;              /* 按键已松开标志 */
+
+    if((release == 1) && (gpio_pinread(GPIO1, 18) == 0))   /* KEY0 按下 */
+    {
+        delay(10);                                  /* 延时消抖约 10ms(教材第57行) */
+        release = 0;                                /* 标记按键按下 */
+        if(gpio_pinread(GPIO1, 18) == 0)
+            ret = KEY0_VALUE;
+    }
+    else if(gpio_pinread(GPIO1, 18) == 1)           /* KEY0 未按下 */
+    {
+        ret = 0;
+        release = 1;                                /* 标记按键释放 */
+    }
+    return ret;
+}
+```
+
+**按键检测逻辑(main.c)**:
 
-    while(1) {
-        value = gpio_pinread(GPIO1, 18);  /* 读取按键状态 */
-        if(value == 0) {                   /* 按下 */
-            delay(10);                     /* 消抖 */
-            if(gpio_pinread(GPIO1, 18) == 0) {
-                beep_switch(!beep_status);  /* 翻转蜂鸣器状态 */
+```c
+int main(void)
+{
+    int keyvalue = 0;
+    unsigned char led_state = OFF;
+    unsigned char beep_state = OFF;
+
+    clk_enable();
+    led_init();
+    beep_init();
+    key_init();
+
+    while(1)
+    {
+        keyvalue = key_getvalue();
+        if(keyvalue)
+        {
+            switch (keyvalue)
+            {
+                case KEY0_VALUE:
+                    beep_state = !beep_state;      /* 翻转蜂鸣器 */
+                    beep_switch(beep_state);
+                    break;
             }
-            while(!gpio_pinread(GPIO1, 18)); /* 等待松开 */
         }
+        delay(10);
     }
 }
 ```
 
+> 说明:教材的 `key_init` 把 UART1_CTS 配置为 **22K 上拉**(0xF080),`key_getvalue` 用静态变量 `release` 记录按键是否已释放,并用 `delay(10)` 做约 10ms 的软件消抖——这是第19章「定时器按键消抖」要优化的对象。
+
 ### 5.4 运行效果
 
 按下KEY0切换蜂鸣器开关状态,实现按键控制。
@@ -340,49 +421,63 @@ GPT(General Purpose Timer)是32位向上计数器,特性:
 ```c
 void delay_init(void)
 {
-    GPT1->CR = 0;                        /* 先关闭GPT */
-    GPT1->CR = (1 << 15);               /* 软复位GPT */
-    while(GPT1->CR & (1 << 15));         /* 等待复位完成 */
+    GPT1->CR = 0;                        /* 清零 */
+    GPT1->CR = 1 << 15;                 /* bit15=1 进入软复位 */
+    while((GPT1->CR >> 15) & 0x01);     /* 等待复位完成 */
+
+    /* CLKSRC(bit8:6)=001 选择 ipg_clk=66MHz;FRR(bit9)=0 为 restart 模式 */
+    GPT1->CR = (1 << 6);
 
-    GPT1->CR = (1 << 1) | (1 << 6);     /* 使能+ipg_clk时钟源 */
-    GPT1->PR = 65;                       /* 66分频,1MHz */
-    GPT1->CR |= (1 << 0);               /* 使能GPT计数器 */
+    GPT1->PR = 65;                       /* 66 分频,GPT1 时钟 = 66M/(65+1)=1MHz */
+    GPT1->OCR[0] = 0xFFFFFFFF;           /* 不使用比较中断,设为最大值 */
+    GPT1->CR |= 1 << 0;                  /* 使能 GPT1 */
 }
 
-void delay_us(unsigned int us)
+/* 微秒级延时(教材 delayus) */
+void delayus(unsigned int usdelay)
 {
-    unsigned int cnt_start = GPT1->CNT;
-    unsigned int cnt_end = cnt_start + us;
-
-    /* 处理溢出情况 */
-    if(cnt_end >= cnt_start) {
-        while(GPT1->CNT < cnt_end);
-    } else {
-        while(GPT1->CNT >= cnt_start);   /* 等待溢出 */
-        while(GPT1->CNT < cnt_end);
+    unsigned long oldcnt, newcnt;
+    unsigned long tcntvalue = 0;         /* 走过的总时间 */
+
+    oldcnt = GPT1->CNT;
+    while(1)
+    {
+        newcnt = GPT1->CNT;
+        if(newcnt != oldcnt)
+        {
+            if(newcnt > oldcnt)                       /* 向上计数且未溢出 */
+                tcntvalue += newcnt - oldcnt;
+            else                                      /* 发生溢出回滚 */
+                tcntvalue += 0xFFFFFFFF - oldcnt + newcnt;
+            oldcnt = newcnt;
+            if(tcntvalue >= usdelay)                  /* 延时时间到 */
+                break;
+        }
     }
 }
 
-void delay_ms(unsigned int ms)
+/* 毫秒级延时(教材 delayms) */
+void delayms(unsigned int msdelay)
 {
-    unsigned int i;
-    for(i = 0; i < ms; i++)
-        delay_us(1000);
+    int i = 0;
+    for(i = 0; i < msdelay; i++)
+        delayus(1000);
 }
 ```
 
+
 ### 6.4 运行效果
 
 延时精度达到1μs级别,比循环延时准确得多。
 
 ### 6.5 延时精度对比
 
-| 延时方式   | 精度 | 原理             | 适用场景       |
-| ---------- | ---- | ---------------- | -------------- |
-| 循环延时   | ms级 | 编译器优化影响大 | 仅用于简单等待 |
-| SysTick    | μs级 | Cortex-M专用     | STM32系列      |
-| GPT定时器  | 1μs  | 硬件计数         | I.MX6U通用     |
-| EPIT定时器 | 1μs  | 单次定时+中断    | 需要中断触发   |
+| 延时方式   | 精度 | 原理                      | 适用场景                       |
+| ---------- | ---- | ------------------------- | ------------------------------ |
+| 循环延时   | ms级 | 编译器优化影响大          | 仅用于简单等待                 |
+| SysTick    | μs级 | Cortex-M专用              | STM32系列                      |
+| GPT定时器  | 1μs  | 硬件计数(读 GPTx_CNT)   | I.MX6U 高精度延时(本章选用)  |
+| EPIT定时器 | —    | 32位向下计数 + 比较中断   | 周期中断、定时器消抖,不做延时 |
 
 ---
 
@@ -413,18 +508,35 @@ I.MX6U Boot ROM会读取IVT中的DCD(Data Configuration Data)表,自动完成D
 - 包含DDR控制器寄存器配置和时序参数
 - 用户无需手动编写DDR初始化代码
 
-### 7.4 DDR3初始化步骤(参考)
+### 7.4 MMDC 控制器与 DDR3 初始化流程
 
-```c
-/* DDR3初始化主要参数 */
-1. 设置CCM_CSCMR1选择DDR时钟源(528MHz)
-2. 配置CCM_CACRR设置DDR时钟分频
-3. 设置MMDC类型寄存器(DDR3类型、位宽)
-4. 配置时序参数(tRCD, tRP, tRAS等)
-5. 配置校准寄存器(MPWLDECTRL, MPRDDLCTL等)
-6. 使能MMDC控制器
-7. 执行ZQ校准
-```
+- **MMDC 基地址**:`MMDC = 0x021B0000`。MMDC 包含内核(MMDC_CORE)和 PHY(MMDC_PHY),PHY 负责时序调整与校准,保证数据在 400MHz 被正确采样。
+- **DDR 时钟源(PDF 23.3.3)**:MMDC 时钟为 **400MHz(实际 396MHz)**,来自 **PLL2_PFD2**:
+  1. `CCM_CBCMR.PRE_PERIPH2_CLK_SEL(bit22:21)=01` → 选 PLL2_PFD2 作为 pre_periph2;
+  2. `CCM_CBCDR.PERIPH2_CLK_SEL(bit26)=0` → periph2_clk 选 pll2_main_clk;
+  3. `CCM_CBCDR.FABRIC_MMDC_PODF(bit5:3)=0` → 1 分频,得到 MMDC 时钟 ≈396MHz。
+
+
+**NXP 官方工具流程(教材第23章):**
+
+1. 用 `I.MX6UL_DDR3_Script_Aid_V0.02.xlsx` 填写 DDR3 参数(容量、位宽、行/列地址、tRCD/tRC/tRAS 等),生成 `.inc` 初始化脚本(全是“寄存器地址=寄存器值”);
+2. 用 `ddr_stress_tester`(`DDR_Tester.exe`)经 USB OTG 把脚本下载到开发板;
+3. 先做 **校准(Calibration)**,得到 6 个校准寄存器的值;
+4. 用校准值更新 `.inc` 后重新加载、下载;
+5. 做 **超频测试(Stress Test)**——能稳定超频 10%~15% 即认为 DDR 硬件走线正常。
+
+**校准得到的 6 个寄存器(教材示例代码 23.5.3.1):**
+
+| 寄存器名 | 地址 |
+| -------- | ---- |
+| MMDC_MPWLDECTRL0 | 0x021B080C |
+| MMDC_MPWLDECTRL1 | 0x021B0810 |
+| MPDGCTRL0 PHY0 | 0x021B083C |
+| MPDGCTRL1 PHY0 | 0x021B0840 |
+| MPRDDLCTL PHY0 | 0x021B0848 |
+| MPWRDLCTL PHY0 | 0x021B0850 |
+
+**DDR3 关键时间参数(NT5CC256M16EP-EK):** tRCD=tRP=CL=13.91ns(CL-tRCD-tRP = 13-13-13)、tRC=47.91ns、tRAS=34ns。
 
 ### 7.5 运行效果
 
@@ -462,23 +574,82 @@ DDR3初始化成功后,程序可使用0x80000000~0xA0000000地址范围(512M
 | HSYNC                | 水平同步   |
 | PCLK                 | 像素时钟   |
 
-### 8.3 关键代码
+### 8.3 eLCDIF 关键寄存器(PDF 24.1.2)
+
+eLCDIF 基地址 **`LCDIF = 0x021C8000`**,支持 MPU / VSYNC / DOTCLK 三种接口,驱动 RGB 屏使用 **DOTCLK** 接口。
+
+| 寄存器 | 作用 |
+| ------ | ---- |
+| `LCDIF_CTRL` | 控制:SFTRST(bit31) 软复位、MASTER(bit5) 主模式、RUN(bit0) 使能、DATA_FORMAT_16_BIT(bit3) 选 ARGB555/RGB565、bit11:10 总线宽度、bit9:8 数据宽度 |
+| `LCDIF_CTRL1` | BYTE_PACKING_FORMAT(bit19:16),ARGB8888 下设为 0x7 |
+| `LCDIF_TRANSFER_COUNT` | bit31:16 = 帧高 V_COUNT,bit15:0 = 帧宽 H_COUNT |
+| `LCDIF_VDCTRL0` | VSYNC/DOTCLK 控制:`VSYNC_PULSE_WIDTH(bit17:0)` 设置 VSPW,bit28 使能 ENABLE 输出等 |
+| `LCDIF_VDCTRL1` | VSYNC 总周期 = 屏高 + VSPW + VBP + VFP |
+| `LCDIF_VDCTRL2` | bit31:18 = HSPW;bit17:0 = HSYNC 总周期 = 屏宽 + HSPW + HBP + HFP |
+| `LCDIF_VDCTRL3` | bit27:16 = HBP+HSPW;bit15:0 = VBP+VSPW |
+| `LCDIF_VDCTRL4` | bit18 置 1(使用 VSYNC/HSYNC/DOTCLK);bit17:0 = 屏宽 |
+| `LCDIF_CUR_BUF` / `LCDIF_NEXT_BUF` | 当前/下一帧缓存地址 |
+
+### 8.4 ATK-7016(1024×600)时序参数(教材例程)
+
+| 参数 | 含义 | 值 |
+| ---- | ---- | -- |
+| width / height | 屏宽 / 屏高 | 1024 / 600 |
+| pixsize | 每像素字节数(ARGB8888) | 4 |
+| hspw | HSYNC 信号宽度 | 20 |
+| hbpd | 水平同步后肩 HBP | 140 |
+| hfpd | 水平同步前肩 HFP | 160 |
+| vspw | VSYNC 信号宽度 | 3 |
+| vbpd | 帧同步后肩 VBP | 20 |
+| vfpd | 帧同步前肩 VFP | 12 |
+| framebuffer | 显存首地址 | 0x89000000 |
+
+帧周期:`T = (VSPW+VBP+LINE+VFP) × (HSPW+HBP+HVALID+HFP)`。
+
+### 8.5 关键代码
 
 ```c
-/* LCD初始化 */
+#define LCD_FRAMEBUF_ADDR   (0x89000000)   /* LCD 显存地址 */
+
+struct tftlcd_typedef {
+    unsigned short height, width;
+    unsigned char  pixsize;
+    unsigned short vspw, vbpd, vfpd;
+    unsigned short hspw, hbpd, hfpd;
+    unsigned int   framebuffer, forecolor, backcolor;
+} tftlcd_dev;
+
 void lcd_init(void)
 {
-    /* 1、初始化eLCDIF控制器 */
-    /* 2、配置LCD时序参数 */
-    /* 3、设置帧缓冲地址 */
-    /* 4、使能LCD控制器 */
+    lcdgpio_init();           /* 初始化 IO,复用为 eLCDIF */
+    lcdclk_init(32, 3, 5);    /* 初始化 LCD 时钟 */
+    lcd_reset();  delayms(10);  lcd_noreset();
+
+    tftlcd_dev.height  = 600;
+    tftlcd_dev.width   = 1024;
+    tftlcd_dev.pixsize = 4;               /* ARGB8888,每像素 4 字节 */
+    tftlcd_dev.vspw = 3;    tftlcd_dev.vbpd = 20;  tftlcd_dev.vfpd = 12;
+    tftlcd_dev.hspw = 20;   tftlcd_dev.hbpd = 140; tftlcd_dev.hfpd = 160;
+    tftlcd_dev.framebuffer = LCD_FRAMEBUF_ADDR;
+
+    LCDIF->CTRL |= (1<<19)|(1<<17)|(3<<10)|(3<<8)|(1<<5);  /* DOTCLK、24位总线/数据、主模式 */
+    LCDIF->CTRL1 = 0x7 << 16;                              /* ARGB 模式,A 通道不传输 */
+    LCDIF->TRANSFER_COUNT = (tftlcd_dev.height << 16) | tftlcd_dev.width;
+    LCDIF->VDCTRL0 = (1<<28)|(1<<24)|(1<<21)|(1<<20)|(tftlcd_dev.vspw << 0);
+    LCDIF->VDCTRL1 = tftlcd_dev.height + tftlcd_dev.vspw + tftlcd_dev.vfpd + tftlcd_dev.vbpd;
+    LCDIF->VDCTRL2 = (tftlcd_dev.hspw << 18) |
+                     (tftlcd_dev.width + tftlcd_dev.hspw + tftlcd_dev.hfpd + tftlcd_dev.hbpd);
+    LCDIF->VDCTRL3 = ((tftlcd_dev.hbpd + tftlcd_dev.hspw) << 16) |
+                     (tftlcd_dev.vbpd + tftlcd_dev.vspw);
+    LCDIF->VDCTRL4 = (1 << 18) | tftlcd_dev.width;
+    LCDIF->CUR_BUF = LCDIF->NEXT_BUF = (unsigned int)tftlcd_dev.framebuffer;
+    lcd_enable();             /* LCDIF->CTRL |= (1 << 0) */
 }
 
-/* 绘制像素点 */
+/* 绘制像素点(ARGB8888,每像素 4 字节) */
 void lcd_drawpoint(unsigned int x, unsigned int y, unsigned int color)
 {
-    unsigned int *fb = (unsigned int *)FRAME_BUFFER_ADDR;
-    fb[y * LCD_WIDTH + x] = color;
+    *(unsigned int *)(tftlcd_dev.framebuffer + 4 * (y * tftlcd_dev.width + x)) = color;
 }
 
 /* 填充矩形 */
@@ -486,15 +657,15 @@ void lcd_fill(unsigned int x0, unsigned int y0,
               unsigned int x1, unsigned int y1, unsigned int color)
 {
     unsigned int x, y;
-    for(y = y0; y < y1; y++) {
-        for(x = x0; x < x1; x++) {
+    for(y = y0; y < y1; y++)
+        for(x = x0; x < x1; x++)
             lcd_drawpoint(x, y, color);
-        }
-    }
 }
 ```
 
-### 8.4 运行效果
+> 显存 0x89000000 对 256MB(0x80000000~0x90000000)和 512MB(0x80000000~0xA0000000)两款 DDR 都适用。教材另提供 `bsp_lcdapi.c`,封装 `lcd_show_string`、`lcd_shownum` 等字符/数字显示函数(依赖字库)。
+
+### 8.6 运行效果
 
 LCD屏幕显示彩色测试图案和字符信息。
 
@@ -519,35 +690,53 @@ I.MX6U的RTC位于SNVS(Secure Non-Volatile Storage)模块内:
 
 ### 9.3 关键代码
 
+SNVS 基地址 **`SNVS = 0x020CC000`**,SRTC 位于 SNVS_LP。相关寄存器:`SNVS_HPCOMR`(NPSWA_EN bit31)、`SNVS_LPCR`(SRTC_ENV bit0)、`SNVS_LPSRTCMR`/`SNVS_LPSRTCLR`(秒数)。
+
 ```c
+/* 教材 bsp_rtc.c(I.MX6ULL 的 MCIMX6Y2.h 需手动补齐 SNVS_Type 寄存器) */
 void rtc_init(void)
 {
-    /* 1、使能SNVS时钟 */
-    CCM->CCGR4 |= (3 << 20);
+    /* HPCOMR 的 NPSWA_EN(bit31) 必须置 1,否则非特权软件无法访问 SNVS 寄存器 */
+    SNVS->HPCOMR |= (1 << 31);
 
-    /* 2、配置SNVS_LP SRTC */
-    SNVS->LPCR &= ~(1 << 0);  /* 先关闭SRTC */
-    while(SNVS->LPCR & (1 << 0)); /* 等待关闭 */
+    /* 首次使用可设置时间(教材中此段用 #if 0 屏蔽,实际由 KEY0 触发设置) */
+    rtc_enable();
+}
 
-    /* 3、设置初始时间(2026-09-17 12:00:00) */
-    rtc_settime(2026, 9, 17, 12, 0, 0);
+void rtc_enable(void)
+{
+    SNVS->LPCR |= (1 << 0);              /* SRTC_ENV=1 使能计数器 */
+    while(!(SNVS->LPCR & 0x01));         /* 等待使能完成 */
+}
 
-    /* 4、使能SRTC */
-    SNVS->LPCR |= (1 << 0);
-    while(!(SNVS->LPCR & (1 << 0))); /* 等待使能 */
+void rtc_disable(void)
+{
+    SNVS->LPCR &= ~(1 << 0);             /* 关闭 RTC */
+    while(SNVS->LPCR & 0x01);            /* 等待关闭完成 */
 }
 
-void rtc_settime(int year, int month, int day, int hour, int min, int sec)
+/* 设置日期时间:SRTC 计数器实际为 32 位 */
+void rtc_setdatetime(struct rtc_datetime *datetime)
 {
-    /* 将日期时间转换为秒数 */
-    unsigned int seconds = timestamp_from_ymd(year, month, day, hour, min, sec);
+    unsigned int seconds = rtc_coverdate_to_seconds(datetime);
+    unsigned int tmp = SNVS->LPCR;
+
+    rtc_disable();                                   /* 修改前先关闭 RTC */
+    SNVS->LPSRTCMR = (unsigned int)(seconds >> 17);  /* 高 15 位 */
+    SNVS->LPSRTCLR = (unsigned int)(seconds << 15);  /* 低 17 位(bit31:15) */
+    if(tmp & 0x1) rtc_enable();                      /* 之前开着则重新打开 */
+}
 
-    /* SRTC计数器是32位(非47位) */
-    SNVS->LPSRTCMR = (seconds >> 17) & 0x7FFF;  /* 高15位 */
-    SNVS->LPSRTCLR = (seconds << 15) & 0xFFFF8000; /* 低17位 */
+/* 读取当前秒数 */
+unsigned int rtc_getseconds(void)
+{
+    return (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15);
 }
 ```
 
+**教材 bsp_rtc.c 共 9 个函数**:`rtc_init`、`rtc_enable`、`rtc_disable`、`rtc_isleapyear`、`rtc_coverdate_to_seconds`、`rtc_setdatetime`、`rtc_convertseconds_to_datetime`、`rtc_getseconds`、`rtc_getdatetime`。时间基准为 1970-01-01,年份范围 1970~2099。
+
+
 ### 9.4 寄存器注意事项
 
 NXP参考手册中SNVS_SRTCMR和SNVS_SRTCLR的描述有误:
@@ -583,51 +772,53 @@ LCD显示实时时间,秒数持续递增。
 
 **读时序**: START → 设备地址+W → ACK → 寄存器地址 → ACK → 重复START → 设备地址+R → ACK → 数据 → NACK → STOP
 
-### 10.3 关键代码
+### 10.3 I2C 寄存器(PDF 26.1.2)
 
-```c
-void i2c_write_reg(unsigned char addr, unsigned char reg, unsigned char data)
-{
-    /* 1、发送起始信号 */
-    i2c_start();
+I2C1 基地址 **`I2C1 = 0x021A0000`**。I.MX6U 使用**硬件 I2C 控制器**,关键寄存器:
 
-    /* 2、发送设备地址 + 写位 */
-    i2c_send_byte((addr << 1) | 0);
-    i2c_wait_ack();
+| 寄存器 | 关键位 | 说明 |
+| ------ | ------ | ---- |
+| `I2Cx_IADR` | ADR(bit7:1) | I2C 从设备地址 |
+| `I2Cx_IFDR` | IC(bit5:0) | 波特率分频(档位固定,IC=0x15 为 640 分频) |
+| `I2Cx_I2CR` | IEN(bit7)/IIEN(bit6)/MSTA(bit5)/MTX(bit4)/TXAK(bit3)/RSTA(bit2) | 使能/中断/主从/方向/应答/重复起始 |
+| `I2Cx_I2SR` | ICF(bit7)/IAAS(bit6)/IBB(bit5)/IAL(bit4)/SRW(bit2)/IIF(bit1)/RXAK(bit0) | 状态寄存器 |
+| `I2Cx_I2DR` | 低 8 位 | 收发数据 |
 
-    /* 3、发送寄存器地址 */
-    i2c_send_byte(reg);
-    i2c_wait_ack();
+时钟源为 IPG_CLK_ROOT=66MHz;设 100KHz:IC=0x15(640 分频),66MHz/640=103.125KHz≈100KHz。
 
-    /* 4、发送数据 */
-    i2c_send_byte(data);
-    i2c_wait_ack();
+### 10.4 关键代码(教材 bsp_i2c.c)
 
-    /* 5、发送停止信号 */
-    i2c_stop();
+```c
+void i2c_init(I2C_Type *base)
+{
+    base->I2CR &= ~(1 << 7);        /* 要访问 I2C 寄存器,先关闭 I2C */
+    base->IFDR = 0x15 << 0;         /* 640 分频 → ≈100KHz */
+    base->I2CR |= (1 << 7);         /* 使能 I2C */
 }
 
-unsigned char i2c_read_reg(unsigned char addr, unsigned char reg)
+/* 发送开始信号(顺带发送从设备地址 + 读写位) */
+unsigned char i2c_master_start(I2C_Type *base, unsigned char address,
+                               enum i2c_direction direction)
 {
-    unsigned char data;
+    if(base->I2SR & (1 << 5)) return 1;          /* 总线忙,返回错误 */
+    base->I2CR |= (1 << 5) | (1 << 4);           /* MSTA=1 主模式,MTX=1 发送 */
+    base->I2DR = ((unsigned int)address << 1) |
+                 ((direction == kI2C_Read) ? 1 : 0);
+    return 0;
+}
+```
 
-    i2c_start();
-    i2c_send_byte((addr << 1) | 0);  /* 写 */
-    i2c_wait_ack();
-    i2c_send_byte(reg);
-    i2c_wait_ack();
+> `i2c_master_repeated_start` 通过 `I2CR |= (1<<4)|(1<<2)`(MTX=1、RSTA=1)产生重复起始。教材 `bsp_i2c.c` 共 8 个函数:`i2c_init`、`i2c_master_start`、`i2c_master_repeated_start`、`i2c_check_and_clear_error`、`i2c_master_stop`、`i2c_master_write`、`i2c_master_read`、`i2c_master_transfer`。
 
-    i2c_start();                       /* 重复起始 */
-    i2c_send_byte((addr << 1) | 1);  /* 读 */
-    i2c_wait_ack();
-    data = i2c_read_byte(0);          /* NACK + STOP */
-    i2c_stop();
+### 10.5 AP3216C 驱动(PDF 26.1.3)
 
-    return data;
-}
-```
+- 器件地址 **0x1E**;I2C1 引脚为 **I2C1_SCL=UART4_TXD、I2C1_SDA=UART4_RXD**,复用值 1,电气属性 0x70B0。
+- 寄存器:`0x00` 系统模式(写 0x04 软复位,写 0x03 开 ALS+PS+IR)、`0x0A~0x0F` 数据寄存器(IR/ALS/PS)。
+- AP3216C **没有 ID 寄存器**,教材用“向 0x00 写 0x03 再读回判断是否相等”来检测芯片是否存在。
+- 同时打开 ALS+PS+IR 时,两次读取间隔需 ≥112.5ms。
+- 数据解析:`*ir = (buf[1]<<2)|(buf[0]&0x03)`;`*als = (buf[3]<<8)|buf[2]`;`*ps = ((buf[5]&0x3F)<<4)|(buf[4]&0x0F)`。
 
-### 10.4 运行效果
+### 10.6 运行效果
 
 读取AP3216C的光强度、距离、红外数据,通过LCD显示。
 
@@ -660,51 +851,138 @@ unsigned char i2c_read_reg(unsigned char addr, unsigned char reg)
 | Mode 2 | 1    | 0    | 高       | 下降沿 |
 | Mode 3 | 1    | 1    | 高       | 上升沿 |
 
-### 11.3 关键代码
+### 11.3 ECSPI 寄存器(PDF 27.1.2)
+
+ECSPI3 基地址 **`ECSPI3 = 0x02010000`**。I.MX6U 的 SPI 外设叫 ECSPI,收发各带 32×64 位 FIFO。
+
+| 寄存器 | 关键位 | 说明 |
+| ------ | ------ | ---- |
+| `ECSPIx_CONREG` | BURST_LENGTH(bit31:20)/CHANNEL_SELECT(bit19:18)/DRCTL(bit17:16)/PRE_DIVIDER(bit15:12)/POST_DIVIDER(bit11:8)/CHANNEL_MODE(bit7:4)/SMC(bit3)/XCH(bit2)/EN(bit0) | 控制寄存器 |
+| `ECSPIx_CONFIGREG` | HT_LENGTH(bit28:24)/SCLK_CTL(bit23:20)/DATA_CTL(bit19:16)/SS_POL(bit15:12)/SCLK_POL(bit7:4)/SCLK_PHA(bit3:0) | 通道极性/相位(各 4 位对应通道 3~0) |
+| `ECSPIx_PERIODREG` | CSD_CTL(bit21:16)/CSRC(bit15)/SAMPLE_PERIOD(bit14:0) | 采样周期/片选延时 |
+| `ECSPIx_STATREG` | TC(bit7)/RO(bit6)/RF(bit5)/RDR(bit4)/RR(bit3)/TF(bit2)/TDR(bit1)/TE(bit0) | 状态寄存器 |
+| `ECSPIx_TXDATA` / `ECSPIx_RXDATA` | 32 位 | 发送/接收数据 |
+
+
+### 11.4 关键代码(教材 bsp_spi.c)
 
 ```c
-void spi_init(void)
+void spi_init(ECSPI_Type *base)
 {
-    /* 1、配置ECSPI3为主模式 */
-    ECSPI3->CONREG = 0;                /* 先关闭 */
-    ECSPI3->CONREG |= (7 << 24);      /* 突发长度=8bit */
-    ECSPI3->CONREG |= (0 << 18);      /* 通道0 */
-    ECSPI3->CONREG |= (0 << 16);      /* 不关心SPI_RDY */
-    ECSPI3->CONREG |= (0 << 12);      /* 预分频=1 */
-    ECSPI3->CONREG |= (0 << 8);       /* 分频=1 */
-    ECSPI3->CONREG |= (1 << 4);       /* 通道0主模式 */
-    ECSPI3->CONREG |= (0 << 3);       /* SMC=0 */
-    ECSPI3->CONREG |= (1 << 2);       /* XCH=1,开始突发 */
-    ECSPI3->CONREG |= (1 << 0);       /* 使能SPI */
-
-    /* 2、配置SPI模式(CPOL=1, CPHA=1 → Mode 3) */
-    ECSPI3->CONFIGREG = 0;
-    ECSPI3->CONFIGREG |= (3 << 4);     /* 模式3 */
+    /* 配置 CONREG:EN=1、SMC=1、通道0主模式、突发长度 8bit */
+    base->CONREG = 0;
+    base->CONREG |= (1 << 0) | (1 << 3) | (1 << 4) | (7 << 20);
+    /*   bit0  EN=1 使能
+     *   bit3  SMC=1,向 TXFIFO 写数据即启动 SPI 突发
+     *   bit7:4 CHANNEL_MODE=1,通道0 主模式(ICM-20608 接 SS0)
+     *   bit31:20 BURST_LENGTH=7,表示 8bit */
+
+    /* 配置 CONFIGREG:通道0 PHA=0、POL=0(SPI Mode 0) */
+    base->CONFIGREG = 0;
+
+    /* PERIODREG:采样周期 0x2000;CSRC(bit15)=0 选择 SPI CLK 为采样时钟源 */
+    base->PERIODREG = 0x2000;
+
+    /* SPI CLK = (SourceCLK / (PRE_DIVIDER+1)) / 2^POST_DIVIDER
+     * 源时钟 pll3_sw_clk/8 = 480/8 = 60MHz;PRE_DIVIDER=0x9 → 6MHz */
+    base->CONREG &= ~((0xF << 12) | (0xF << 8));
+    base->CONREG |= (0x9 << 12);
 }
 
-unsigned char spi_transfer(unsigned char data)
+/* 通道0 收发一个字节(SPI 全双工) */
+unsigned char spich0_readwrite_byte(ECSPI_Type *base, unsigned char txdata)
 {
-    unsigned int dummy;
+    uint32_t spirxdata = 0, spitxdata = txdata;
 
-    while(!(ECSPI3->STATUSREG & (1 << 0)));  /* 等待TXFIFO空 */
-    ECSPI3->TXDATA = data;                    /* 发送数据 */
+    base->CONREG &= ~(3 << 18);              /* 选择通道0 */
+    base->CONREG |= (0 << 18);
 
-    while(!(ECSPI3->STATUSREG & (1 << 3)));  /* 等待RXFIFO满 */
-    dummy = ECSPI3->RXDATA;                   /* 读取数据 */
+    while((base->STATREG & (1 << 0)) == 0){} /* 等待 TXFIFO 空(TE=1) */
+    base->TXDATA = spitxdata;
 
-    return (unsigned char)dummy;
+    while((base->STATREG & (1 << 3)) == 0){} /* 等待 RXFIFO 有数据(RR=1) */
+    spirxdata = base->RXDATA;
+
+    return (unsigned char)spirxdata;
 }
 ```
 
-### 11.4 运行效果
+### 11.5 ICM-20608 驱动要点(PDF 27.1.3)
+
+- **软件片选**:CS 接 `UART2_TX_DATA`(GPIO1_IO20),拉低选中;SCLK=`UART2_RXD`、MISO=`UART2_RTS`、MOSI=`UART2_CTS`,电气属性 0x10B1。
+- **寄存器地址最高位为读写标志**:写时 `reg &= ~0x80`,读时 `reg |= 0x80`,低 7 位才是真实地址。
+- ID 寄存器 `0x75` WHO_AM_I:ICM-20608G=0xAF,ICM-20608D=0xAE。
+- 常用寄存器:`0x6B` PWR_MGMT_1(写 0x80 复位、0x01 关睡眠)、`0x1B` GYRO_CONFIG、`0x1C` ACCEL_CONFIG、`0x3B~0x48` 加速度/温度/陀螺仪数据。
+- 分辨率:陀螺仪 FS=±250/±500/±1000/±2000dps → 131/65.5/32.8/16.4;加速度 ±2/±4/±8/±16g → 16384/8192/4096/2048。
+- 使用浮点运算前需 `imx6ul_hardfpu_enable()` 开启 NEON 和硬件 FPU。
+
+### 11.6 运行效果
 
 读取ICM-20608的加速度、陀螺仪、温度数据,通过LCD显示。
 
 ---
 
-## 十二、裸机开发技巧
+## 十二、其余裸机实验速览
+
+> 本节补充教材中、上文未展开的裸机实验要点(官方SDK移植、BSP工程管理、主频时钟、GPIO中断、EPIT定时器、定时器消抖、UART、printf移植)。
+
+### 12.1 官方SDK移植(第12章)
+
+- 把 NXP 官方 SDK 中的 `MCIMX6Y2.h`、`fsl_common.h`、`fsl_iomuxc.h` 与 `cc.h` 移植到 `imx6ul/` 目录,用 `imx6ul.h` 统一包含。
+- 从此不再手写寄存器地址,改用 `IOMUXC_SetPinMux()` / `IOMUXC_SetPinConfig()` 和 `GPIO1->GDIR` 等 SDK 风格接口。
+- `cc.h` 定义 `__I/__O/__IO`、`ON/OFF` 以及 `uint8_t`~`uint64_t` 等类型(第13章的 `LED0` 之外,`ON/OFF` 也来自这里)。
+
+### 12.2 BSP工程管理(第13章)
+
+- 按功能分目录:`bsp/{clk,delay,led,beep,gpio,key,exit,int,epittimer,keyfilter,uart,lcd,rtc,i2c,ap3216c,spi,icm20608}`、`imx6ul/`、`project/`、`obj/`。
+- 通用 Makefile:`INCDIRS`/`SRCDIRS` 配合 `wildcard`/`foreach`/`notdir`/`patsubst` 自动搜集源文件,`.o` 统一输出到 `obj/`。
+- 链接脚本 `imx6ul.lds`:链接地址 `0x87800000`,`.text` 首行放 `obj/start.o`,`__bss_start`/`__bss_end` 标记 BSS 段。
+
+### 12.3 主频和时钟配置(第16章)
+
+- I.MX6U 默认主频 396MHz(Boot ROM 设置),本章提升到 **528MHz**。
+- 核心时钟源为 **PLL1(ARM_PLL)**:先切到 `step_clk`(24MHz osc) 再改 PLL1=1056MHz,最后 `CCM_CACRR.ARM_PODF=2` 分频得 528MHz。
+- 关键寄存器:`CCM_CCSR`(STEP_SEL / PLL1_SW_CLK_SEL)、`CCM_ANALOG_PLL_ARMn`(DIV_SELECT)、`CCM_CACRR`(ARM_PODF)、`CCM_CBCDR`/`CCM_CBCMR`。
+- 其他 PLL:PLL2=528MHz(固定22倍频,分出 PLL2_PFD0~3)、PLL3=480MHz(固定20倍频,USB 用)。
+
+### 12.4 GPIO中断实验(第17章)
+
+- Cortex-A7 只有 7 个异常向量,所有外设中断共用 **IRQ 向量**,需由 **GIC** 分发(`GICD_*` 分发器、`GICC_*` CPU 接口)。
+- 中断服务流程:`int_init()` 初始化 GIC → `GIC_EnableIRQ(IRQn)` 使能中断 → `system_register_irqhandler(IRQn, handler, NULL)` 注册处理函数。
+- GPIO 中断需配置 `ICR1`/`ICR2`(触发方式) 或 `EDGE_SEL`(双边沿)、`IMR`(使能)、`ISR`(状态,写 1 清零);KEY0 用 `GPIO1_Combined_16_31_IRQn`。
+
+### 12.5 EPIT定时器实验(第18章)
+
+- EPIT 是 32 位**向下**计数器,仅做周期中断;关键寄存器 `EPITx_CR/SR/LR/CMPR/CNR`,基地址 `EPIT1=0x020D0000`。
+- 配置:`CR` 的 CLKSRC(bit25:24)=1(ipg_clk=66MHz)、PRESCALAR(bit15:4)、RLD(bit3)=1(set-and-forget)、OCIEN(bit2)=1、ENMOD(bit1)=1;`LR` 设初值,`CMPR=0`。
+- 溢出时间 `Tout = ((frac+1)×value)/Tclk`;例程 `epit1_init(0, 66000000/2)` → 500ms 中断翻转 LED0。
+- 中断号 `EPIT1_IRQn`;`SR` 的 OCIF(bit0) **写 1 清零**。
+
+### 12.6 定时器按键消抖(第19章)
+
+- 思路:按键 GPIO 中断只负责**重启** EPIT1(10ms),EPIT1 中断里再读 GPIO 电平确认有效按键,避免在中断里使用延时。
+- 关键函数:`filterkey_init`/`filtertimer_init`/`filtertimer_restart`/`filtertimer_stop`/`filtertimer_irqhandler`/`gpio1_16_31_irqhandler`;GPIO1_IO18,`filtertimer_init(66000000/100)`=10ms。
+- 这也是 Linux 按键驱动的标准做法。
+
+### 12.7 UART串口通信实验(第21章)
+
+- UART1 基地址 `0x02020000`,寄存器 `UCR1~UCR4`/`UFCR`/`USR1`/`USR2`/`URXD`/`UTXD`/`UBIR`/`UBMR`/`UTS`。
+- 时钟源 `CCM_CSCDR1.UART_CLK_SEL` 选 pll3_80m(80MHz),`UFCR.RFDIV(bit9:7)=101` 为 1 分频。
+- 波特率公式 `Baud = Ref Freq × (UBIR+1) / (16 × (UBMR+1))`;115200 用 UBIR=71、UBMR=3124。
+- 配置顺序:关 UART → 软复位(`UCR2.SRST` 写 0,轮询等待自动置 1)→ 清 `UCR1` → 配 `UCR2`(bit14 IRTS=1、bit5 WS=1、bit2 TXEN、bit1 RXEN)→ `UCR3.bit2` RXDMUXSEL=1 → 写 `UFCR`/`UBIR`/`UBMR`(**先 UBIR 后 UBMR**)→ `UCR1.UARTEN=1`。
+- 关键 API:`uart_init`、`uart_io_init`、`uart_disable`、`uart_enable`、`uart_softreset`、`uart_setbaudrate`、`putc`、`puts`、`getc`、`raise`。
+
+### 12.8 串口格式化函数移植实验(第22章)
+
+- 把从 U-Boot 抠出的 `stdio/{include,lib}` 复制到工程根目录,Makefile 的 `INCDIRS`/`SRCDIRS` 加入 `stdio/include`、`stdio/lib`,并加编译选项 `-Wa,-mimplicit-it=thumb`。
+- 重定向:实现 `fputc(int ch, FILE *f)` → `putc(ch)`、`fgetc(FILE *f)` → `getc()`,即可使用 `printf`/`scanf`。
+- 注意该 `stdio` 不支持浮点 `%f`。
+
+---
+
+## 十三、裸机开发技巧
 
-### 12.1 调试方法
+### 13.1 调试方法
 
 | 方法       | 工具        | 适用场景           |
 | ---------- | ----------- | ------------------ |
@@ -714,7 +992,7 @@ unsigned char spi_transfer(unsigned char data)
 | 寄存器查看 | Memory窗口  | 验证配置是否正确   |
 | 逻辑分析仪 | Saleae等    | 时序分析           |
 
-### 12.2 代码规范
+### 13.2 代码规范
 
 ```c
 /* 1. 寄存器操作使用位域宏 */
@@ -738,7 +1016,7 @@ unsigned char spi_transfer(unsigned char data)
 #endif
 ```
 
-### 12.3 性能优化
+### 13.3 性能优化
 
 - **避免函数调用开销**: 关键路径使用内联函数
 - **减少内存访问**: 使用寄存器缓存变量
@@ -746,7 +1024,7 @@ unsigned char spi_transfer(unsigned char data)
 - **编译优化**: 使用-O2/-O3优化选项
 - **Cache使能**: ARM Cortex-A7支持I-Cache和D-Cache
 
-### 12.4 链接脚本模板
+### 13.4 链接脚本模板
 
 ```ld
 SECTIONS {
@@ -771,9 +1049,9 @@ SECTIONS {
 
 ---
 
-## 十、跨平台对比
+## 十、跨平台对比
 
-### 13.1 I.MX6ULL vs STM32 vs RK3568裸机差异
+### 14.1 I.MX6ULL vs STM32 vs RK3568裸机差异
 
 | 特性           | I.MX6ULL         | STM32F103            | RK3568           |
 | -------------- | ---------------- | -------------------- | ---------------- |
@@ -789,7 +1067,7 @@ SECTIONS {
 | **中断控制器** | GIC              | NVIC                 | GIC-500          |
 | **开发难度**   | 中等             | 简单                 | 较高             |
 
-### 13.2 寄存器操作对比
+### 14.2 寄存器操作对比
 
 **STM32 (HAL库)**:
 
@@ -811,7 +1089,7 @@ GPIO0->PORT_DR |= BIT(0);   /* 输出高电平 */
 GPIO0->PORT_DR &= ~BIT(0);  /* 输出低电平 */
 ```
 
-### 13.3 关键差异总结
+### 14.3 关键差异总结
 
 - **I.MX6U**: GPIO以功能命名,需配置IOMUXC,Boot ROM提供DCD自动初始化DDR
 - **STM32**: GPIO以端口+引脚命名,HAL库封装好,NVIC中断控制器简单
@@ -819,7 +1097,7 @@ GPIO0->PORT_DR &= ~BIT(0);  /* 输出低电平 */
 
 ---
 
-## 十、面试精选
+## 十、面试精选
 
 ### Q1: 请解释I.MX6U的GPIO初始化流程,与STM32有何不同?
 
@@ -902,9 +1180,9 @@ RTC(SRTC)位于SNVS_LP中,需要32.768KHz晶振提供时钟。即使系统主
 
 ---
 
-## 十、实验总结
+## 十、实验总结
 
-### 15.1 实验知识图谱
+### 16.1 实验知识图谱
 
 ```
 汇编LED → C语言LED → 蜂鸣器 → 按键输入
@@ -918,7 +1196,7 @@ DDR3 → LCD → RTC → I2C → SPI
 内存   显示   时间   传感器  传感器
 ```
 
-### 15.2 学习路径建议
+### 16.2 学习路径建议
 
 | 阶段     | 实验                               | 目标               |
 | -------- | ---------------------------------- | ------------------ |
@@ -927,7 +1205,7 @@ DDR3 → LCD → RTC → I2C → SPI
 | **综合** | RTC → I2C → SPI                    | 掌握通信协议       |
 | **实战** | 串联所有实验,构建完整系统         | 集成能力           |
 
-### 15.3 下一步学习方向
+### 16.3 下一步学习方向
 
 1. **中断系统**: GIC中断控制器、GPIO中断、定时器中断
 2. **UART串口**: 串口通信、printf重定向
@@ -944,3 +1222,5 @@ DDR3 → LCD → RTC → I2C → SPI
 > - 《I.MX6ULL参考手册》第14章 GPT Timer
 > - 《I.MX6UL参考手册》第46章 SNVS
 > - 《I.MX6ULL参考手册》第30章 ECSPI
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第8章 汇编LED灯实验、第10章 C语言版LED灯实验、第12章 官方SDK移植实验、第13章 BSP工程管理实验、第14章 蜂鸣器实验、第15章 按键输入实验、第16章 主频和时钟配置实验、第17章 GPIO中断实验、第18章 EPIT定时器实验、第19章 定时器按键消抖实验、第20章 高精度延时实验、第21章 UART串口通信实验、第22章 串口格式化函数移植实验、第23章 DDR3实验、第24章 RGBLCD显示实验、第25章 RTC实时时钟实验、第26章 I2C实验、第27章 SPI实验

+ 61 - 44
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/01-ARM架构与裸机编程/面试-ARM架构与裸机.md

@@ -3,6 +3,12 @@ title: 面试-ARM架构与裸机
 tags: [面试, ARM, Cortex-A7, 裸机开发, IMX6ULL, 汇编]
 created: 2026-09-17
 updated: 2026-09-17
+pdf_ref:
+  - "第六章 Cortex-A7 MPCore架构.pdf"
+  - "第七章 ARM汇编基础.pdf"
+  - "第八章 汇编LED灯实验.pdf"
+  - "第九章 I.MX6U启动方式详解.pdf"
+  - "第十七章 GPIO中断实验.pdf"
 ---
 
 # 面试-ARM架构与裸机
@@ -101,55 +107,51 @@ ARM架构中PC总是指向当前执行指令+8字节(3级流水线)或+8/12
 
 **答案要点**:
 
-1. 上电复位 → Boot ROM执行基本硬件初始化
-2. Boot ROM根据BOOT_CFG引脚选择启动介质
-3. SPL(Secondary Program Loader)初始化DDR和串口
-4. U-Boot加载Linux Kernel
-5. Kernel挂载rootfs并启动init进程
+1. 上电复位 → Boot ROM执行基本硬件初始化(时钟、MMU/Cache)
+2. Boot ROM根据BOOT_MODE[1:0]选启动模式、BOOT_CFG选启动设备
+3. Boot ROM执行DCD完成DDR初始化
+4. Boot ROM按Boot data把镜像加载到链接地址(如0x87800000)并跳转
+5. U-Boot加载Linux Kernel,Kernel挂载rootfs并启动init进程
 
 **详细解答**:
 
-IMX6ULL的启动分为四个阶段
+IMX6ULL(I.MX6U)的启动流程(教材第9章「I.MX6U启动方式详解」)
 
 ```
-上电复位 → Boot ROM → SPL → U-Boot → Linux Kernel → Rootfs
+上电复位 → Boot ROM(读 IVT/Boot data/DCD)→ 初始化 DDR → 加载镜像到链接地址 → 跳转执行
 ```
 
 **阶段1:Boot ROM**
 
-- 片内ROM代码,上电后自动执行
-- 初始化基本硬件(时钟、看门狗)
-- 读取BOOT_CFG[1:0]引脚选择启动介质
-- BOOT_CFG=00: SD/EMMC, BOOT_CFG=10: USB串行下载
+- 片内 ROM 代码,上电后自动执行;先初始化系统时钟(内核默认 396MHz)。
+- 为加快执行会打开 MMU 与 Cache。
+- 读取启动介质上的镜像头信息:烧写的镜像 = `IVT + Boot data + DCD + .bin`(即 `imxdownload` 生成的 `load.imx`)。
+- 由 `BOOT_MODE[1:0]` 选择启动模式,由 `BOOT_CFG1/2/4` 引脚选择启动设备。
 
-**阶段2:SPL(Secondary Program Loader)**
+**阶段2:DCD 配置(初始化 DDR)**
 
-- 从启动介质加载到IRAM(片内RAM)
-- 初始化DDR控制器
-- 初始化串口用于调试输出
-- 加载完整U-Boot到DDR
+- `DCD`(Device Configuration Data)包含 DDR3 控制器寄存器配置与参数。
+- Boot ROM 执行 DCD 完成 **DDR 初始化**,之后 DDR 才可访问。
 
-**阶段3:U-Boot**
+**阶段3:加载并跳转**
 
-- 初始化更多外设(网络、存储等)
-- 从启动介质加载Linux Kernel到DDR
-- 传递启动参数(命令行、设备树地址)给Kernel
+- `Boot data` 指明镜像要拷贝到的地址与大小,镜像入口点写在 `IVT` 中。
+- 以 SD 卡为例:`IVT` 偏移为 1Kbyte,`IVT+Boot data+DCD` 共 3Kbyte,用户代码从链接地址 `0x87800000` 开始。
+- Boot ROM 把镜像拷贝到 DDR 后跳转到入口点执行用户代码。
 
-**阶段4:Linux Kernel**
+**阶段4(运行 Linux 时):U-Boot → Kernel**
 
-- 解压并运行内核
-- 解析设备树(DTB)
-- 挂载rootfs
-- 启动init进程(PID=1)
+- 若镜像为 U-Boot,则由 U-Boot 继续加载 Linux Kernel 与设备树(DTB),最后挂载 rootfs、启动 init(PID=1)。
 
 **追问**:
 
-- 追问1:SPL为什么要在IRAM中运行而不是DDR?
-  - 上电时DDR尚未初始化,IRAM是片内SRAM,不需要初始化即可访问。SPL的职责之一就是初始化DDR,所以在SPL运行阶段DDR还不可用
+- 追问1:裸机 `start.S` 里并没有初始化 DDR,为什么可以直接把 SP 设到 DDR 地址? 
+  - 因为 Boot ROM 已经执行 **DCD** 完成了 DDR 初始化,用户代码运行时 DDR 已经可用(教材第9、10章)。这也解释了为什么 I.MX6U 不需要像 S3C2440 那样在汇编里手写 DDR 初始化
 - 追问2:Boot ROM如何保证启动安全?
   - Boot ROM支持HAB(High Assurance Boot)安全启动,通过数字签名验证加载的镜像是否经过厂商授权,防止恶意代码在启动阶段执行。
-- 追问3:什么是启动介质?IMX6ULL支持哪些?
-  - 启动介质是存储启动代码的外部存储器,IMX6ULL支持SD卡、eMMC、NAND Flash、QSPI NOR Flash等。选择由BOOT_CFG引脚和eFUSE共同决定。
+- 追问3:什么是启动介质?IMX6ULL支持哪些?如何选择?
+  - 启动介质是存储启动代码的外部存储器,IMX6ULL支持 SD卡、eMMC、NAND Flash、QSPI NOR Flash 等。启动模式由 `BOOT_MODE[1:0]` 引脚/eFUSE 选择(00 从 FUSE、01 串行下载、10 内部 BOOT、11 保留),具体启动设备再由 `BOOT_CFG1/2/4` 配置引脚(或 eFUSE)决定。
+
 
 ---
 
@@ -599,14 +601,14 @@ _start:
     bic r0, r0, #0x1f
     orr r0, r0, #0x13     @ SVC模式
     msr cpsr, r0
-    ldr sp, =0x87F00000   @ SVC栈顶
+    ldr sp, =0x80200000   @ SVC栈顶(教材第17章约定,2MB)
 
     @ 设置IRQ模式栈
     mrs r0, cpsr
     bic r0, r0, #0x1f
     orr r0, r0, #0x12     @ IRQ模式
     msr cpsr, r0
-    ldr sp, =0x87D00000   @ IRQ栈顶
+    ldr sp, =0x80600000   @ IRQ栈顶(教材第17章约定,2MB)
 ```
 
 **追问**:
@@ -1093,9 +1095,12 @@ GIC主要组件:
 中断响应流程:
 
 ```asm
-@ 1. 读取GIC_IAR获取中断号
-ldr r0, =0x01C8400C      @ GICC_IAR地址
-ldr r1, [r0]             @ r1 = 中断号 + CPU ID
+@ GIC 基地址由 CP15 的 CBAR 读出(I.MX6ULL 上为 0x00A00000);
+@ CPU 接口端基地址 = CBAR + 0x2000,GICC_IAR = +0x0C,GICC_EOIR = +0x10
+
+@ 1. 读取 GICC_IAR 获取中断号(绝对地址 0x00A0200C)
+ldr r0, =0x00A0200C      @ GICC_IAR地址
+ldr r1, [r0]             @ r1 = 中断号(低10位) + CPU ID
 
 @ 2. 保存现场
 push {r0-r12, lr}
@@ -1106,11 +1111,12 @@ bl system_irqhandler     @ 传入中断号
 @ 4. 恢复现场
 pop {r0-r12, lr}
 
-@ 5. 写入GIC_EOIR确认中断
-ldr r0, =0x01C84010      @ GICC_EOIR地址
+@ 5. 写入 GICC_EOIR 确认中断(绝对地址 0x00A02010)
+ldr r0, =0x00A02010      @ GICC_EOIR地址
 str r1, [r0]             @ 写入中断号确认
 ```
 
+
 GIC中断优先级:
 
 - 每个中断有独立的优先级(0-255,0最高)
@@ -1244,7 +1250,7 @@ irq_handler:
     pop {r0-r12}
 
     @ 6. 确认中断
-    ldr r0, =0x01C84010
+    ldr r0, =0x00A02010    @ GICC_EOIR(CBAR=0x00A00000,CPU接口+0x2000,EOIR=+0x10)
     str r1, [r0]
 
     @ 7. 返回
@@ -1354,23 +1360,32 @@ _start:
     /* 1、设置处理器为SVC模式 */
     mrs r0, cpsr
     bic r0, r0, #0x1f
-    orr r0, r0, #0x13     @ SVC模式
+    orr r0, r0, #0x13     @ SVC模式(10011)
     msr cpsr, r0
 
     /* 2、设置SVC模式栈指针 */
-    ldr sp, =0x87F00000   @ DDR顶部
+    ldr sp, =0x80200000   @ SVC栈顶(DDR起始0x80000000 + 2MB)
 
-    /* 3、设置IRQ模式栈指针 */
+    /* 3、设置SYS模式栈指针(中断实验会用到,教材第17章) */
     mrs r0, cpsr
     bic r0, r0, #0x1f
-    orr r0, r0, #0x12     @ IRQ模式
+    orr r0, r0, #0x1f     @ SYS模式(11111)
     msr cpsr, r0
-    ldr sp, =0x87D00000   @ IRQ栈
+    ldr sp, =0x80400000   @ SYS栈顶(2MB)
 
-    /* 4、跳转到main函数 */
+    /* 4、设置IRQ模式栈指针 */
+    mrs r0, cpsr
+    bic r0, r0, #0x1f
+    orr r0, r0, #0x12     @ IRQ模式(10010)
+    msr cpsr, r0
+    ldr sp, =0x80600000   @ IRQ栈顶(2MB)
+
+    /* 5、跳转到main函数 */
     b main
 ```
 
+> 说明:教材第10章的 `start.S` 只设置 SVC 模式栈指针(`ldr sp, =0x80200000`)后 `b main`;第17章中断实验的 `Reset_Handler` 才额外设置 SYS/IRQ 等模式的栈,且每种模式栈大小均为 **2MB**(SVC=0x80200000、SYS=0x80400000、IRQ=0x80600000)。上面把两者合并演示。
+
 链接脚本(imx6ul.lds):
 
 ```
@@ -1597,10 +1612,11 @@ arm-linux-gnueabihf-ld -T imx6ul.lds led.o -o led.elf
 **步骤3:格式转换**
 
 ```bash
-arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
+arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
 ```
 
 - `-O binary`:输出纯二进制格式
+- `-S`:不复制重定位信息和符号信息;`-g`:不复制调试信息(教材第8章用法)
 - 去除ELF头、段表、符号表等元数据
 - 输出:.bin文件,可以直接烧录到Flash
 
@@ -1706,4 +1722,5 @@ arm-linux-gnueabihf-objdump -D led.elf > led.dis
 ---
 
 **最后更新**: 2026-09-17
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第6章 Cortex-A7 MPCore架构、第7章 ARM汇编基础、第8章 汇编LED灯实验、第9章 I.MX6U启动方式详解、第17章 GPIO中断实验
 **来源**: [[01-Cortex-A7架构精讲]] | [[02-ARM汇编与启动流程]] | [[08-裸机实验总结]]

+ 296 - 34
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/01-内核编译与配置.md

@@ -29,31 +29,67 @@ uname -r
 # 输出示例:4.1.15-gb39c1d0
 ```
 
+**内核源码获取**(教材 35.1):Linux 由 Linux 基金会管理与发布,官网为 `https://www.kernel.org`,最新稳定版可直接在官网下载。NXP 从官网下载某个版本的内核,移植到自己的 CPU 上并测试通过后,再开放给 NXP CPU 的开发者。本章移植使用 NXP 提供的源码,路径为:
+
+```
+开发板光盘->1、例程源码->4、NXP 官方原版 Uboot 和 Linux->
+    linux-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2
+```
+
+正点原子出厂源码路径为:
+
+```
+开发板光盘->1、例程源码->3、正点原子 Uboot 和 Linux 出厂源码->
+    linux-imx-4.1.15-2.1.0-g8a006db.tar.bz2
+```
+
+> 注意:正点原子出厂系统在持续更新,压缩包名字可能不同,以实际为准。
+
 ### 1.2 内核源码结构
 
-Linux内核源码主要目录结构如下:
+编译后的 Linux 源码目录(教材表 35.3.1)主要文件夹与文件如下:
 
 ```
 linux-4.1.15/
-├── arch/           # 架构相关代码(ARM、x86等)
-├── block/          # 块设备驱动
-├── crypto/         # 加密算法
-├── Documentation/  # 内核文档
-├── drivers/        # 设备驱动
-├── fs/             # 文件系统
-├── include/        # 头文件
-├── init/           # 内核初始化
-├── ipc/            # 进程间通信
-├── kernel/         # 内核核心
-├── lib/            # 库函数
-├── mm/             # 内存管理
-├── net/            # 网络协议栈
-├── scripts/        # 构建脚本
-├── security/       # 安全模块
-├── sound/          # 音频驱动
-└── Makefile        # 顶层Makefile
+├── arch/           # 架构相关目录(arm、arm64、x86 等)
+├── block/          # 块设备相关目录(SD卡、EMMC、NAND、硬盘)
+├── crypto/         # 加密相关目录(crc、crc32、md4、md5、hash)
+├── Documentation/  # 文档相关目录
+├── drivers/        # 驱动相关目录(学习重点)
+├── firmware/       # 固件相关目录
+├── fs/             # 文件系统相关目录(ext2/ext4/f2fs 等)
+├── include/        # 头文件相关目录
+├── init/           # 初始化相关目录(内核启动初始化代码)
+├── ipc/            # 进程间通信相关目录
+├── kernel/         # 内核核心相关目录
+├── lib/            # 库函数相关目录
+├── mm/             # 内存管理相关目录
+├── net/            # 网络相关目录
+├── samples/        # 例程相关目录
+├── scripts/        # 脚本相关目录(编译时大量使用)
+├── security/       # 安全相关目录
+├── sound/          # 音频处理相关目录
+├── tools/          # 工具相关目录
+├── usr/            # 与 initramfs 相关的目录
+├── virt/           # 提供虚拟机技术(KVM)
+├── .config         # 最终使用的配置文件(编译生成)
+├── Kbuild          # Makefile 会读取此文件
+├── Kconfig         # 图形化配置界面的配置文件
+├── Makefile        # Linux 顶层 Makefile
+├── README          # 编译说明与源码目录说明(建议仔细阅读)
+├── System.map      # 符号表(编译生成)
+├── vmlinux         # 未压缩的 ELF 格式 Linux 文件(编译生成)
+└── vmlinux.o       # 编译生成的 vmlinux.o
 ```
 
+
+以 `arch/arm` 为例,其下还有 `boot`、`common`、`configs`、`mach-xxx` 等子目录,分别用于控制系统引导、系统调用、动态调频、主频设置等:
+
+- `arch/arm/configs/` — 各平台的默认配置文件 `xxx_defconfig`,其中 `imx_v7_defconfig` 即 I.MX6U-ALPHA 开发板的默认配置;
+- `arch/arm/boot/dts/` — 对应开发平台的设备树文件;
+- `arch/arm/boot/` — 保存编译出来的 `Image` 和 `zImage` 镜像,`zImage` 就是最终要用的 Linux 镜像;
+- `arch/arm/mach-xxx/` — 相应平台的驱动和初始化文件,如 `mach-imx` 就是 I.MX 系列 CPU 的驱动与初始化文件。
+
 ### 1.3 内核配置系统
 
 Linux内核使用Kconfig配置系统,主要文件包括:
@@ -66,6 +102,8 @@ Linux内核使用Kconfig配置系统,主要文件包括:
 
 ## 二、内核配置系统
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 2.1 Kconfig语法
 
 Kconfig是Linux内核的配置描述语言,定义在各子目录的Kconfig文件中。
@@ -217,19 +255,22 @@ make ARCH=arm menuconfig O=out/
 
 ### 3.1 版本信息
 
-顶层Makefile开头定义内核版本号:
+顶层Makefile一开始(第 1~4 行)就定义了 Linux 内核的版本号:
 
 ```makefile
-# 版本定义
 VERSION = 4
 PATCHLEVEL = 1
 SUBLEVEL = 15
 EXTRAVERSION =
+```
+
+可以看出,Linux 内核版本号为 **4.1.15**。教材 35.5 只列出了上面 4 行;`KERNELVERSION` 由这 4 个变量拼接而成,其完整定义(摘自同一份顶层 Makefile)为:
 
-# 自动生成版本信息
-KERNELVERSION = $(VERSION)$(PATCHLEVEL)$(SUBLEVEL)$(EXTRAVERSION)
+```makefile
+KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
 ```
 
+
 ### 3.2 MAKEFLAGS设置
 
 ```makefile
@@ -324,6 +365,7 @@ KBUILD_EXTMOD := $(M)
 endif
 
 # 外部模块时,目标为modules
+PHONY += all
 ifeq ($(KBUILD_EXTMOD),)
 _all: all
 else
@@ -428,21 +470,98 @@ export KBUILD_AFLAGS_KERNEL KBUILD_CFLAGS_KERNEL
 export KBUILD_ARFLAGS
 ```
 
+### 3.13 调用 scripts/Kbuild.include
+
+顶层Makefile会包含文件 `scripts/Kbuild.include`(教材 35.5 第 9 点):
+
+```makefile
+# We need some generic definitions (do not try to remake the file).
+scripts/Kbuild.include: ;
+include scripts/Kbuild.include
+```
+
+其中定义了后面反复使用的 `build`、`if_changed` 等函数,例如:
+
+```makefile
+build := -f $(srctree)/scripts/Makefile.build obj
+```
+
+### 3.14 Makefile.build 脚本分析
+
+`make xxx_defconfig` 配置内核时,实际执行的是下面两条命令,它们都会用到 `scripts/Makefile.build`(教材 35.5.2):
+
+```bash
+@make -f ./scripts/Makefile.build obj=scripts/basic
+@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
+```
+
+**(1)scripts_basic 目标**
+
+```makefile
+scripts_basic:
+	$(Q)$(MAKE) $(build)=scripts/basic
+	$(Q)rm -f .tmp_quiet_recordmcount
+```
+
+展开后即 `@make -f ./scripts/Makefile.build obj=scripts/basic`。`Makefile.build` 读取 `scripts/basic/Makefile`,其默认目标 `__build` 中只有 `always` 有效:
+
+```makefile
+__build: scripts/basic/fixdep scripts/basic/bin2c
+	@:
+```
+
+所以 `scripts_basic` 的作用就是**编译出 `scripts/basic/fixdep` 和 `scripts/basic/bin2c` 两个主机工具**。
+
+**(2)%config 目标**
+
+顶层Makefile中有 `config` 和 `%config` 两个目标:
+
+```makefile
+config: scripts_basic outputmakefile FORCE
+	$(Q)$(MAKE) $(build)=scripts/kconfig $@
+
+%config: scripts_basic outputmakefile FORCE
+	$(Q)$(MAKE) $(build)=scripts/kconfig $@
+```
+
+`make xxx_defconfig` 与 `%config` 匹配。`Makefile.build` 会读取 `scripts/kconfig/Makefile`,其中的规则为:
+
+```makefile
+%_defconfig: $(obj)/conf
+	$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)
+```
+
+展开后为:
+
+```bash
+@scripts/kconfig/conf --defconfig=arch/arm/configs/%_defconfig Kconfig
+```
+
+`%_defconfig` 依赖 `scripts/kconfig/conf`,因此会先编译 `scripts/kconfig/conf.c` 生成 `conf` 工具,再由它将默认配置输出为内核根目录下的 `.config` 文件。
+
 ## 四、内核配置方法
 
 ### 4.1 make xxx_defconfig
 
-首次编译前需要使用`make xxx_defconfig`配置内核:
+首次编译前需要使用`make xxx_defconfig`配置内核。教材提供的 I.MX6U-ALPHA 默认配置文件为 `arch/arm/configs/imx_v7_defconfig`
 
 ```bash
 # 使用默认配置
 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
+```
+
+顶层Makefile中与之匹配的目标(教材示例代码 35.5.1.1):
+
+```makefile
+config: scripts_basic outputmakefile FORCE
+    $(Q)$(MAKE) $(build)=scripts/kconfig $@
 
-# 配置目标匹配
 %config: scripts_basic outputmakefile FORCE
     $(Q)$(MAKE) $(build)=scripts/kconfig $@
 ```
 
+此时 `config-targets=1`、`mixed-targets=0`、`dot-config=1`。`%config` 第 534 行会 `include arch/$(SRCARCH)/Makefile`(`SRCARCH=arm`),这个文件很重要,`zImage`、`uImage` 等镜像就是由它生成的。
+
 **执行流程:**
 
 ```
@@ -455,6 +574,17 @@ scripts/kconfig/conf --defconfig=arch/arm/configs/xxx_defconfig Kconfig
 生成.config文件
 ```
 
+完整的一次编译(教材 35.2)通常先清理再配置,最后编译:
+
+```bash
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
+```
+
+> 注意:`distclean` 会连带删除 `.config` 等配置信息。若已通过图形界面配置了内核但还没保存新的配置文件,则要慎用该脚本,否则配置会被全部删除。
+
 ### 4.2 make menuconfig
 
 图形化配置界面:
@@ -498,6 +628,38 @@ make olddefconfig
 
 ## 五、内核编译
 
+### 5.0 编译准备与初次编译
+
+**安装 lzop**(教材 35.2):编译内核前需要先在 Ubuntu 上安装 lzop 库,否则编译会失败:
+
+```bash
+sudo apt-get install lzop
+```
+
+否则会提示 `recipe for target 'arch/arm/boot/compressed/piggy.lzo' failed`(lzop 未找到),安装后重新编译即可。
+
+**正点原子编译脚本** `mx6ull_alientek_emmc.sh`:
+
+```sh
+#!/bin/sh
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
+```
+
+- 第 2 行 `make distclean`:清理工程,每次运行脚本都会先清理;
+- 第 3 行 `make xxx_defconfig`:配置工程;
+- 第 4 行 `make menuconfig`:打开图形配置界面,不需要每次配置时可删除此行;
+- 第 5 行 `make`:编译 Linux 源码。
+
+图形配置界面和 U-Boot 一样,本实验不需要任何配置,直接按两下 `ESC` 键退出,退出后会自动开始编译。
+
+编译完成后:
+
+- `arch/arm/boot/zImage` — 要用的 Linux 镜像文件;
+- `arch/arm/boot/dts/*.dtb` — 很多设备树文件。
+
 ### 5.1 编译命令
 
 ```bash
@@ -517,23 +679,56 @@ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
 # make all 目标
 all: vmlinux
 
-# BOOT_TARGETS定义
+# BOOT_TARGETS定义(arch/arm/Makefile)
 BOOT_TARGETS = zImage Image xipImage bootpImage uImage
 
 $(BOOT_TARGETS): vmlinux
     $(Q)$(MAKE) $(build)=$(boot) MACHINE=$(MACHINE) $(boot)/$@
 ```
 
+`BOOT_TARGETS` 依赖 `vmlinux`,所以编译 `zImage` 时会先编译出 `vmlinux`。以 `zImage` 为例,第 316 行命令展开后为:
+
+```bash
+@make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE=arch/arm/boot/zImage
+```
+
+即再次借助 `scripts/Makefile.build` 完成从 `vmlinux` 到 `zImage` 的转换。使用 `make`、`make all`、`make zImage` 均可编译出 `zImage`。
+
 ### 5.3 vmlinux生成流程
 
 ```makefile
-# vmlinux依赖
+# 第 905~907 行:导出给 scripts/link-vmlinux.sh 使用
+export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
+export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y)
+export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
+
+# 第 912 行:vmlinux依赖
 vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)
 
-# 链接命令
+# 第 915 行:链接命令
 cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
+
+# 第 920 行:vmlinux目标
+vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
+    +$(call if_changed,link-vmlinux)
 ```
 
+其中:
+
+- `KBUILD_LDS = arch/arm/kernel/vmlinux.lds`,是整个 Linux 的链接脚本;
+- `$(head-y)` 定义在 `arch/arm/Makefile`:`head-y := arch/arm/kernel/head$(MMUEXT).o`,使能 MMU 时 `MMUEXT` 为空,故 `head-y = arch/arm/kernel/head.o`;
+- `init-y/drivers-y/net-y` 由顶层Makefile用 `patsubst` 把目录名替换为 `%/built-in.o`;
+- `libs-y` 的值为 `arch/arm/lib/lib.a lib/lib.a arch/arm/lib/built-in.o lib/built-in.o`;
+- `core-y` 在 `arch/arm/Makefile` 中被追加了 `arch/arm/kernel、mm、common、probes、net、crypto、firmware、mach-imx` 等目录。
+
+**最终链接的实际命令**(`LDFLAGS_vmlinux=-p --no-undefined -X --pic-veneer --build-id`):
+
+```bash
+/bin/bash scripts/link-vmlinux.sh arm-linux-gnueabihf-ld -EL -p --no-undefined -X --pic-veneer --build-id
+```
+
+`scripts/link-vmlinux.sh` 中的 `vmlinux_link()` 以 `${lds}` 为链接脚本、以 `${KBUILD_VMLINUX_INIT}` 和 `${KBUILD_VMLINUX_MAIN}` 为输入文件完成最终链接。
+
 **vmlinux组成部分:**
 
 | 组成      | 说明       | 路径                     |
@@ -560,17 +755,73 @@ cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
 vmlinux → objcopy → Image → gzip → zImage → mkimage → uImage
 ```
 
-### 5.5 增量编译
+教材 35.5.5 的详细说明:
+
+1. **vmlinux**:编译出来的最原始内核文件,**未压缩**,ELF 格式,正点原子源码编译出来约 16MB;
+2. **Image**:Linux 内核镜像文件,仅包含可执行的二进制数据,即用 `objcopy` 取消掉 vmlinux 中的符号表等信息;**没有压缩**,保存在 `arch/arm/boot` 目录下,约 12MB;
+3. **zImage**:经过 gzip 压缩后的 Image,约 6MB;
+4. **uImage**:老版本 U-Boot 专用的镜像文件,是在 zImage 前面加了一个长度为 **64 字节**的“头”,头信息描述了镜像文件的类型、加载位置、生成时间、大小等。新版本 U-Boot 已经支持直接启动 zImage,所以 uImage 已很少使用。
+
+### 5.5 增量编译与清理
 
 ```bash
-# 编译后修改代码,直接执行
+# 编译后修改代码,直接执行(只重编被修改的文件)
 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
 
-# 清理编译产物
+# 清理编译产物(保留 .config)
 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
 
-# 完全清理(包括.config
+# 完全清理(删除所有生成文件 + .config 及备份文件
 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mrproper
+
+# 在 mrproper 基础上再删除编辑器备份与补丁文件(教材编译脚本使用)
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
+```
+
+### 5.6 built-in.o 文件编译生成过程
+
+教材 35.5.4 指出,`vmlinux-deps` 中除链接脚本外,其余都是各子目录下的 `built-in.o`、`.a` 文件。顶层Makefile中:
+
+```makefile
+# 第 937 行:vmlinux-deps 依赖 vmlinux-dirs
+$(sort $(vmlinux-deps)): $(vmlinux-dirs) ;
+
+# 第 889 行:vmlinux-dirs 保存生成 vmlinux 所需的源码目录
+vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
+             $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
+             $(net-y) $(net-m) $(libs-y) $(libs-m)))
+
+# 第 946~947 行:进入每个目录执行编译
+$(vmlinux-dirs): prepare scripts
+	$(Q)$(MAKE) $(build)=$@
+```
+
+`$(vmlinux-dirs)` 规则的命令展开后就是为 `init`、`usr`、`arch/arm/kernel`、`kernel`、`mm`、`fs`、`drivers`、`lib` 等每个目录各执行一次:
+
+```bash
+@make -f ./scripts/Makefile.build obj=<目录名>
+```
+
+每个目录下生成的 `built-in.o` 由 `scripts/Makefile.build` 中的 `builtin-target` 定义:
+
+```makefile
+ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
+builtin-target := $(obj)/built-in.o
+endif
+
+$(builtin-target): $(obj-y) FORCE
+	$(call if_changed,link_o_target)
+```
+
+`cmd_link_o_target` 使用 `$(LD) ... -r -o $@` 把该目录下所有 `obj-y` 指定的 `.o` 文件链接成一个 `built-in.o`。最终所有 `built-in.o` 与 `.a` 文件被链接成 ELF 可执行文件 `vmlinux`。
+
+### 5.7 编译输出目录
+
+使用 `O=xxx` 可把编译产生的过程文件输出到指定目录,源码目录保持干净:
+
+```bash
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=out/linux imx_v7_defconfig
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=out/linux -j16
 ```
 
 ## 六、内核模块编译
@@ -603,6 +854,8 @@ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- M=/path/to/module \
 
 ### 6.3 模块Makefile示例
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第三十五章内容,为扩展知识。教材仅在 35.5 第 7 点讲解 `make M=dir` 的模块编译机制,完整的模块 Makefile 模板见第四十章。
+
 ```makefile
 # 外部模块Makefile
 obj-m := mydriver.o
@@ -639,6 +892,8 @@ lsmod
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 I.MX6ULL vs STM32 vs RK3568内核差异
 
 | 对比项   | I.MX6ULL              | STM32MP1            | RK3568                   |
@@ -675,6 +930,8 @@ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip_linux_defconfig
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: 请描述Linux内核编译的完整流程
 
 **答案:**
@@ -759,15 +1016,16 @@ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
 cp arch/arm/boot/zImage /tftpboot/
 
 # 7. 复制设备树到TFTP目录
-cp arch/arm/boot/dts/imx6ull-14x14-evk.dtb /tftpboot/
+cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /tftpboot/
 
 # 8. 在U-Boot中设置启动参数
-setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait'
-setenv bootcmd 'tftp ${loadaddr} zImage; tftp ${fdt_addr} imx6ull-14x14-evk.dtb; bootz ${loadaddr} - ${fdt_addr}'
+setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
+setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
 saveenv
 run bootcmd
 ```
 
+
 ---
 
 ## 相关知识
@@ -782,3 +1040,7 @@ run bootcmd
 - 《正点原子I.MX6U嵌入式Linux驱动开发指南V2.0.1》
 - Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/
 - Linux Kernel Build System: https://www.kernel.org/doc/html/latest/kbuild/
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第三十五章 Linux内核顶层Makefile详解

+ 137 - 30
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/02-内核模块编程.md

@@ -3,7 +3,7 @@ title: 内核模块编程
 tags: [Linux内核, 模块, insmod, 驱动开发, 嵌入式]
 created: 2026-09-16
 updated: 2026-09-16
-pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十六章 字符设备驱动原理"
+pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第三章 Linux C编程入门 / 第四十章 字符设备驱动开发"
 ---
 
 # 内核模块编程
@@ -102,6 +102,8 @@ MODULE_ALIAS("my-alias");                /* 模块别名 */
 MODULE_INFO(intree, "Y");                /* 标记为内核源码树内模块 */
 ```
 
+> ⚠️ **来源说明**:教材第四十章 40.2.4 只讲解了 `MODULE_LICENSE()` 与 `MODULE_AUTHOR()`(LICENSE 必须添加,否则编译报错;作者信息可选)。`MODULE_DESCRIPTION`、`MODULE_VERSION`、`MODULE_ALIAS`、`MODULE_INFO` 为扩展知识。
+
 **许可证选项**:
 
 | 许可证         | 说明                                 |
@@ -135,6 +137,25 @@ printk(KERN_DEBUG "debug message\n");
 | `KERN_INFO`    | 6    | 信息性       | 模块加载成功   |
 | `KERN_DEBUG`   | 7    | 调试信息     | 开发调试       |
 
+这 8 个级别定义在 `include/linux/kern_levels.h` 中(教材第四十章),数值越小优先级越高(0 最高、7 最低),每个级别由 `KERN_SOH "\001"` 加数字组成:
+
+```c
+#define KERN_SOH      "\001"
+#define KERN_EMERG    KERN_SOH "0"   /* 紧急事件,一般是内核崩溃 */
+#define KERN_ALERT    KERN_SOH "1"   /* 必须立即采取行动 */
+#define KERN_CRIT     KERN_SOH "2"   /* 临界条件,比如严重的软件或硬件错误 */
+#define KERN_ERR      KERN_SOH "3"   /* 错误状态,设备驱动中用 KERN_ERR 报告硬件错误 */
+#define KERN_WARNING  KERN_SOH "4"   /* 警告信息,不会对系统造成严重影响 */
+#define KERN_NOTICE   KERN_SOH "5"   /* 有必要进行提示的一些信息 */
+#define KERN_INFO     KERN_SOH "6"   /* 提示性的信息 */
+#define KERN_DEBUG    KERN_SOH "7"   /* 调试信息 */
+```
+
+**两个默认级别(教材第四十章)**:
+
+- 不显式设置级别时,`printk` 使用默认级别 `MESSAGE_LOGLEVEL_DEFAULT`,其值为 **4**;
+- `include/linux/printk.h` 中的 `CONSOLE_LOGLEVEL_DEFAULT` 默认为 **7**,控制哪些级别的消息能显示在控制台上——只有优先级高于 7 的消息才会显示。默认级别 4 比 7 高,所以直接使用 `printk` 输出的信息可以显示在控制台上。
+
 **printk 注意事项**:
 
 - 必须以 `\n` 结尾,否则输出会混乱
@@ -144,6 +165,8 @@ printk(KERN_DEBUG "debug message\n");
 
 ### 2.4 模块参数
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第三章、第四十章均未涉及 `module_param` 模块参数机制。
+
 ```c
 #include <linux/module.h>
 #include <linux/moduleparam.h>
@@ -208,8 +231,12 @@ static void __exit my_exit(void)
 | `__initdata` | 标记数据为初始化数据 | 初始化阶段使用的全局变量 |
 | `__exitdata` | 标记数据为退出数据   | 退出阶段使用的全局变量   |
 
+> ⚠️ **来源说明**:教材第四十章仅使用 `__init` / `__exit` 修饰驱动入口、出口函数(`__init` 标记的函数执行后释放内存,`__exit` 标记的函数在模块编译为 built-in 时被丢弃);`__initdata` / `__exitdata` 为扩展知识。
+
 ### 2.6 符号导出
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第三章、第四十章均未涉及 `EXPORT_SYMBOL` 符号导出机制。
+
 ```c
 /* 导出函数供其他模块使用(任何模块可用) */
 EXPORT_SYMBOL(my_function);
@@ -265,10 +292,12 @@ sudo modprobe -r mymodule
 
 **特点**:
 
-- 自动处理模块依赖关系
-- 默认在 `/lib/modules/$(uname -r)/` 目录查找模块
+- 自动处理模块依赖关系(分析 `modules.dep`)
+- 默认在 `/lib/modules/$(uname -r)/` 目录查找模块,例如内核 4.1.15 就是 `/lib/modules/4.1.15`
 - 卸载时自动递归卸载未被使用的依赖模块
-- 生产环境推荐使用
+- 提供依赖性分析、错误检查、错误报告等功能,生产环境推荐使用
+
+> 教材提示:一般自己制作的根文件系统中不会有 `/lib/modules/4.1.15` 这个目录,需要**手动创建**,否则 `modprobe` 无法加载驱动模块。`/lib/modules` 是通用的,只有后面的版本号(如 `4.1.15`、`5.14.31`)需按实际内核版本设置。
 
 ### 3.3 rmmod 命令
 
@@ -311,6 +340,8 @@ cat /sys/module/mymodule/parameters/myvalue
 
 ### 3.6 modinfo 命令
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ```bash
 # 查看模块信息
 modinfo mymodule.ko
@@ -337,8 +368,12 @@ modinfo mymodule.ko
 
 ## 四、内核模块编译
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第三章内容,为扩展知识。教材第四十章 40.4.3 给出了交叉编译驱动模块的 Makefile 模板(见 4.2 节)。
+
 ### 4.1 本机编译 Makefile
 
+> ⚠️ **来源说明**:本机编译属于扩展知识,教材只讲解交叉编译。
+
 ```makefile
 # 内核模块 Makefile(放在与 .c 文件同一目录)
 
@@ -375,28 +410,40 @@ make clean
 
 ### 4.2 交叉编译 Makefile
 
+教材第四十章 40.4.3 给出的驱动模块 Makefile 模板(示例代码 40.4.3.1,原文使用 `chrdevbase`):
+
 ```makefile
-# 交叉编译 Makefile
+KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
+CURRENT_PATH := $(shell pwd)
+obj-m := chrdevbase.o
 
-obj-m := mymodule.o
+build: kernel_modules
 
-# 内核源码路径(根据实际路径修改)
-KDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
+kernel_modules:
+	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
+clean:
+	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
+```
 
-# 交叉编译工具链
-CROSS_COMPILE := arm-linux-gnueabihf-
+- `KERNELDIR`:开发板所使用的 Linux 内核源码目录,使用绝对路径,按实际情况填写;
+- `CURRENT_PATH`:当前路径,通过 `pwd` 命令获取;
+- `obj-m`:将 `chrdevbase.c` 编译为 `chrdevbase.ko` 模块;
+- `-C $(KERNELDIR)`:把当前工作目录切换到内核源码目录;
+- `M=$(CURRENT_PATH)`:指定模块源码目录,`make modules` 会到该目录读取源码并编译为 `.ko`。
 
-# 目标架构
-ARCH := arm
+若内核尚未在当前 shell 中配置好,可显式传入架构与工具链(扩展写法):
 
-# 当前目录
-PWD := $(shell pwd)
+```makefile
+KERNELDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
+CROSS_COMPILE := arm-linux-gnueabihf-
+ARCH := arm
+CURRENT_PATH := $(shell pwd)
+obj-m := mymodule.o
 
 all:
-	make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules
-
+	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
 clean:
-	make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean
+	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
 ```
 
 **IMX6ULL 交叉编译示例**:
@@ -406,10 +453,10 @@ clean:
 export CROSS_COMPILE=arm-linux-gnueabihf-
 export ARCH=arm
 
-# 编译模块
+# 编译模块,成功后生成 chrdevbase.ko
 make
 
-# 拷贝到开发板
+# 拷贝到开发板(NFS 根文件系统可直接拷贝到 rootfs 对应目录)
 scp mymodule.ko root@192.168.1.200:/home/root/
 ```
 
@@ -419,8 +466,8 @@ scp mymodule.ko root@192.168.1.200:/home/root/
 | ---------------- | ---------------------- |
 | `obj-m`          | 编译为可加载模块       |
 | `obj-y`          | 编译进内核(built-in) |
-| `-C $(KDIR)`     | 切换到内核源码目录执行 |
-| `M=$(PWD)`       | 指定模块源码目录       |
+| `-C $(KERNELDIR)` | 切换到内核源码目录执行 |
+| `M=$(CURRENT_PATH)` | 指定模块源码目录     |
 | `ARCH=arm`       | 指定目标架构           |
 | `CROSS_COMPILE=` | 指定交叉编译工具链前缀 |
 
@@ -433,8 +480,56 @@ scp mymodule.ko root@192.168.1.200:/home/root/
 | `Module not found` | modprobe 执行 `depmod` | 运行 `depmod -a` 更新模块依赖      |
 | `tainted kernel`   | 缺少 MODULE_LICENSE    | 添加 `MODULE_LICENSE("GPL")`       |
 
+### 4.5 模块加载与卸载实验步骤(教材第四十章 40.4.4)
+
+1. **准备目录**:检查开发板根文件系统中是否存在 `/lib/modules/4.1.15` 目录,没有就自行创建(`modprobe` 会在此目录查找模块)。
+2. **拷贝模块**:把 `.ko` 与测试 APP 复制到该目录,例如
+
+   ```bash
+   sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ -f
+   ```
+
+3. **加载模块**:
+
+   ```bash
+   insmod chrdevbase.ko     # 最简单,不处理依赖
+   # 或
+   modprobe chrdevbase.ko   # 自动处理依赖
+   ```
+
+4. **生成依赖文件**:若 `modprobe` 提示无法打开 `modules.dep`,执行 `depmod` 自动生成 `modules.alias`、`modules.symbols` 和 `modules.dep`(有些根文件系统没有 `depmod`,需重新配置 busybox 使能该命令后重新编译)。
+
+   ```bash
+   depmod
+   ```
+
+5. **查看模块与设备**:
+
+   ```bash
+   lsmod                # 查看已加载模块(等价读取 /proc/modules)
+   cat /proc/devices    # 查看系统中的字符/块设备及主设备号
+   ```
+
+6. **创建设备节点**(教材第四十章使用旧接口 `register_chrdev(200, ...)`,需手动创建设备节点):
+
+   ```bash
+   mknod /dev/chrdevbase c 200 0    # c=字符设备,200=主设备号,0=次设备号
+   ls /dev/chrdevbase -l
+   ```
+
+7. **测试与卸载**:
+
+   ```bash
+   ./chrdevbaseApp /dev/chrdevbase 1    # 读数据
+   ./chrdevbaseApp /dev/chrdevbase 2    # 写数据
+   rmmod chrdevbase.ko                  # 卸载模块
+   lsmod                                # 确认模块已不存在
+   ```
+
 ## 五、完整源码分析
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。5.1、5.2 的 hello/param 模块教材未给出;5.3 使用了 `cdev` / `class_create` / `device_create` 等新字符设备接口,而教材第四十章 40.4 的 `chrdevbase` 示例使用的是旧接口 `register_chrdev(200, ...)`。
+
 ### 5.1 hello 模块示例
 
 ```c
@@ -717,6 +812,8 @@ MODULE_AUTHOR("ALIENTEK");
 
 ### 6.1 内存映射
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第四十章仅提到“应用程序运行在用户空间,驱动运行于内核空间”,未给出 32 位地址空间划分图。
+
 ```mermaid
 flowchart TB
     accTitle: 内核空间与用户空间内存布局
@@ -736,18 +833,20 @@ flowchart TB
 
 ### 6.2 数据传递函数
 
+> 教材第四十章 40.4.1 在 `chrdevbase_read` / `chrdevbase_write` 中讲解了 `copy_to_user` 与 `copy_from_user`:内核空间不能直接操作用户空间的内存,必须借助这两个函数完成数据复制。
+
 ```c
 #include <linux/uaccess.h>
 
-/* 将内核数据复制到用户空间 */
-unsigned long copy_to_user(void __user *to,
-                           const void *from,
-                           unsigned long n);
+/* 将内核数据复制到用户空间(教材原型) */
+static inline long copy_to_user(void __user *to,
+                                const void *from,
+                                unsigned long n);
 
 /* 将用户数据复制到内核空间 */
-unsigned long copy_from_user(void *to,
-                             const void __user *from,
-                             unsigned long n);
+static inline long copy_from_user(void *to,
+                                  const void __user *from,
+                                  unsigned long n);
 ```
 
 **返回值说明**:
@@ -798,8 +897,12 @@ static ssize_t my_write(struct file *filp, const char __user *buf,
 | `put_user()`       | 内核 → 用户 | 写入单个值到用户空间 |
 | `get_user()`       | 用户 → 内核 | 从用户空间读取单个值 |
 
+> ⚠️ **来源说明**:`put_user()` / `get_user()` 为扩展知识,教材第四十章只讲解了 `copy_to_user()` / `copy_from_user()`。
+
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568 模块差异
 
 | 特性       | IMX6ULL              | STM32MP1             | RK3568             |
@@ -816,18 +919,20 @@ static ssize_t my_write(struct file *filp, const char __user *buf,
 
 ```makefile
 # IMX6ULL 交叉编译
-KDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
+KERNELDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
 CROSS_COMPILE := arm-linux-gnueabihf-
 ARCH := arm
 
 # RK3568 交叉编译(64位)
-KDIR := /home/user/kernel-5.10
+KERNELDIR := /home/user/kernel-5.10
 CROSS_COMPILE := aarch64-linux-gnu-
 ARCH := arm64
 ```
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目 1:什么是内核模块?它的优势是什么?
 
 **考察点**:模块概念
@@ -901,4 +1006,6 @@ ARCH := arm64
 
 ---
 
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第三章 Linux C编程入门、第四十章 字符设备驱动开发
+
 **最后更新**: 2026-09-16

+ 99 - 62
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/03-系统调用与VFS.md

@@ -7,6 +7,8 @@ updated: 2026-09-17
 
 # 系统调用与VFS
 
+> ⚠️ **来源说明**:本主题不属于《I.MX6U嵌入式Linux驱动开发指南》的讲授范围,内容基于 Linux 内核源码(Linux 4.1.15)整理,为扩展知识。
+
 > 💡 **关联知识**: [[01-Cortex-A7架构详解|Cortex-A7架构详解]] | [[03-pinctrl与gpio子系统|pinctrl与gpio子系统]] | [[arm-assembly-basics|ARM汇编基础]]
 
 ## 一、系统调用概述
@@ -89,25 +91,30 @@ sequenceDiagram
 
 ### 1.4 系统调用号
 
-每个系统调用都有唯一编号,ARM32 架构定义在 `arch/arm/include/asm/unistd.h`
+每个系统调用都有唯一编号。ARM32(EABI)的编号定义在 `arch/arm/include/uapi/asm/unistd.h`(`arch/arm/include/asm/unistd.h` 只做包含并定义 `__NR_syscalls`)
 
 ```c
-/* ARM32 常用系统调用号 */
-#define __NR_open           5
-#define __NR_read           3
-#define __NR_write          4
-#define __NR_close          6
-#define __NR_ioctl         54
-#define __NR_mmap         192
-#define __NR_munmap        91
-#define __NR_mmap2        192
-#define __NR_stat          196
-#define __NR_fstat         197
-
-/* 系统调用总数 */
-#define __NR_syscalls      400
+/* ARM32 (EABI) 常用系统调用号 — 以 Linux 4.1.x 为准 */
+#define __NR_read            3   /* (__NR_SYSCALL_BASE + 3) */
+#define __NR_write           4
+#define __NR_open            5
+#define __NR_close           6
+#define __NR_ioctl          54
+#define __NR_mmap           90   /* 传统 mmap,偏移以字节为单位 */
+#define __NR_munmap         91
+#define __NR_stat          106   /* ARM 上 stat/lstat/fstat = 106/107/108 */
+#define __NR_fstat         108
+#define __NR_mmap2         192   /* 偏移以页为单位,glibc 实际使用它 */
+#define __NR_stat64        195
+#define __NR_lstat64       196
+#define __NR_fstat64       197
+
+/* 系统调用表长度(Linux 4.1.15 ARM) */
+#define __NR_syscalls      388
 ```
 
+> ⚠️ **易错点**:`__NR_mmap` 是 90 而非 192(192 是 `__NR_mmap2`);`stat`/`fstat` 是 106/108 而非 196/197(196/197 分别是 `__NR_lstat64`/`__NR_fstat64`)。这些编号常与 x86/ARM64 混淆,务必以 `uapi/asm/unistd.h` 为准。
+
 > ⚠️ **注意**: 系统调用号在不同架构间**不通用**。ARM64 使用 `asm-generic/unistd.h`,编号与 ARM32 不同。跨平台驱动开发时需注意平台差异。
 
 ---
@@ -119,11 +126,13 @@ sequenceDiagram
 系统调用表是一个函数指针数组,以系统调用号为索引,存储对应的内核处理函数地址:
 
 ```c
-/* arch/arm/kernel/entry-armv.S */
-.align 5
-.global sys_call_table
-sys_call_table:
-    .long sys_ni_syscall     /* 0  - 未实现 */
+/* arch/arm/kernel/entry-common.S;表项由同目录的 calls.S 生成 */
+.section .rodata, "a", %progbits
+.type   sys_call_table, #object
+ENTRY(sys_call_table)
+#include "calls.S"
+    /* 展开后即为按编号排列的函数指针数组: */
+    .long sys_ni_syscall     /* 0  - restart_syscall */
     .long sys_ni_syscall     /* 1  - 未实现 */
     .long sys_ni_syscall     /* 2  - 未实现 */
     .long sys_read           /* 3  - __NR_read */
@@ -182,11 +191,15 @@ sys_call_table:
 
 **查找过程:**
 
-1. 内核从 `SVC #0` 中断获取系统调用号
+1. 内核从 `SVC #0` 中断获取系统调用号(`entry-common.S` 的 `vector_swi`)
 2. 以调用号为索引查找 `sys_call_table`
 3. 调用对应的内核函数(如 `sys_open`)
 4. 返回结果存入 R0 寄存器
 
+> ⚠️ **来源说明**:以上的 `vector_swi`/`sys_call_table`/`calls.S` 均为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码整理。
+
+> 💡 **ARM 私有系统调用**:ARM32 除普通系统调用外还有一组 `__ARM_NR_*`(基址 `__ARM_NR_BASE = __NR_SYSCALL_BASE + 0x0f0000`,如 `breakpoint`、`cacheflush`、`set_tls`),由 `arch/arm/kernel/traps.c` 中的 `arm_syscall()` 处理,不占用普通编号。
+
 ### 2.2 系统调用参数传递
 
 ARM32 架构通过寄存器 R0-R3 传递前 4 个参数,超出部分通过栈传递:
@@ -239,12 +252,13 @@ ssize_t my_read(struct file *filp, char __user *buf,
 
 ```c
 /* 1. 定义系统调用号 */
-/* arch/arm/include/asm/unistd.h */
-#define __NR_my_syscall    400
-#define __NR_syscalls      401
+/* arch/arm/include/uapi/asm/unistd.h(新增普通系统调用号,追加在末尾)*/
+#define __NR_my_syscall    388
+#undef  __NR_syscalls
+#define __NR_syscalls      389   /* 必须与实际表项数一致,内核会编译期校验 */
 
 /* 2. 实现系统调用函数 */
-/* kernel/my_syscall.c */
+/* kernel/my_syscall.c(并在 kernel/Makefile 中加入 obj-y += my_syscall.o)*/
 #include <linux/syscalls.h>
 
 SYSCALL_DEFINE2(my_syscall, int, arg1, const char __user *, arg2)
@@ -260,11 +274,12 @@ SYSCALL_DEFINE2(my_syscall, int, arg1, const char __user *, arg2)
     return 0;  /* 成功返回0,失败返回负错误码 */
 }
 
-/* 3. 添加到系统调用表 */
-/* arch/arm/kernel/entry-armv.S */
-    .long sys_my_syscall    /* 400 - __NR_my_syscall */
+/* 3. 添加到系统调用表:追加到 arch/arm/kernel/calls.S 末尾 */
+CALL(sys_my_syscall)
 ```
 
+> ⚠️ **注意**:ARM 的私有系统调用位于 `__ARM_NR_BASE`(0x0f0000)以上,不要用 400 之类的编号直接挪用,否则会与私有系统调用冲突。新增号必须紧跟 `__NR_last_syscall`,并同步更新 `__NR_syscalls`。
+
 > 💡 **编译选项**: 新增系统调用后需重新编译内核。用户空间可通过 `syscall()` 函数或内联汇编调用新系统调用。
 
 ---
@@ -412,8 +427,8 @@ classDiagram
         +d_parent: dentry*
         +d_sb: super_block*
         +d_op: dentry_operations*
-        +d_mounted: int
-        +d_child: list_head
+        +d_flags: unsigned int
+        +d_lockref: lockref
     }
 
     class File {
@@ -421,18 +436,20 @@ classDiagram
         +f_mode: fmode_t
         +f_op: file_operations*
         +f_inode: inode*
-        +f_dentry: dentry*
-        +f_private_data: void*
+        +f_path: path
+        +private_data: void*
     }
 
     SuperBlock "1" --> "*" Inode : 包含多个
     Inode "1" --> "1" SuperBlock : 所属
     Dentry "1" --> "1" Inode : 指向
     Dentry "1" --> "*" Dentry : 父子关系
-    File "1" --> "1" Dentry : 打开的文件
+    File "1" --> "1" Dentry : f_path.dentry
     File "1" --> "1" Inode : 对应的inode
 ```
 
+> ⚠️ **来源说明**:本节 VFS 四大对象的结构体字段与操作集为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码(`include/linux/fs.h`、`include/linux/dcache.h`)整理。
+
 ### 4.2 super_block(超级块)
 
 超级块描述**整个文件系统**的元信息,挂载时从磁盘读取并常驻内存:
@@ -513,31 +530,37 @@ struct inode {
         struct pipe_inode_info  *i_pipe;   /* 管道 */
         struct block_device     *i_bdev;   /* 块设备 */
         struct cdev             *i_cdev;   /* 字符设备 */
-        char                    *i_link;   /* 符号链接 */
-        unsigned            i_dir_seq;     /* 目录序列号 */
-    };
+    };                                     /* i_link / i_dir_seq 在 4.7+ 才加入 */
 
     void *i_private;                  /* 文件系统私有数据 */
 };
 
-/* inode 操作 */
+/* inode 操作(Linux 4.1.15) */
 struct inode_operations {
     struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int);
-    int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool);
+    void *(*follow_link)(struct dentry *, struct nameidata *);
+    int (*permission)(struct inode *, int);
+    struct posix_acl *(*get_acl)(struct inode *, int);
+    int (*readlink)(struct dentry *, char __user *, int);
+    void (*put_link)(struct dentry *, struct nameidata *, void *);
+    int (*create)(struct inode *, struct dentry *, umode_t, bool);
     int (*link)(struct dentry *, struct inode *, struct dentry *);
     int (*unlink)(struct inode *, struct dentry *);
-    int (*symlink)(struct mnt_idmap *, struct inode *, struct dentry *, const char *);
-    int (*mkdir)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t);
+    int (*symlink)(struct inode *, struct dentry *, const char *);
+    int (*mkdir)(struct inode *, struct dentry *, umode_t);
     int (*rmdir)(struct inode *, struct dentry *);
-    int (*mknod)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, dev_t);
-    int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
-                  struct inode *, struct dentry *, unsigned int);
-    int (*permission)(struct mnt_idmap *, struct inode *, int);
-    int (*getattr)(struct mnt_idmap *, const struct path *,
-                   struct kstat *, u32, unsigned int);
+    int (*mknod)(struct inode *, struct dentry *, umode_t, dev_t);
+    int (*rename)(struct inode *, struct dentry *, struct inode *, struct dentry *);
+    int (*setattr)(struct dentry *, struct iattr *);
+    int (*getattr)(struct vfsmount *, struct dentry *, struct kstat *);
+    int (*atomic_open)(struct inode *, struct dentry *, struct file *,
+                       unsigned int, umode_t, int *);
+    int (*tmpfile)(struct inode *, struct dentry *, umode_t);
 };
 ```
 
+> ⚠️ **版本差异**:`struct mnt_idmap *` 作为首个参数是 Linux 6.3+ 的写法;4.1.15 中这些回调直接接收 `struct inode *`。网上较新的示例不能照搬到 4.1.15 内核。
+
 > 📖 **inode 与设备驱动**: inode 中的 `i_rdev` 字段存储设备号,通过 `MAJOR(i_rdev)` 和 `MINOR(i_rdev)` 可获取主次设备号,驱动通过此定位具体设备。
 
 ### 4.4 dentry(目录项)
@@ -547,7 +570,7 @@ dentry 建立**文件名到 inode 的映射**,是路径解析的关键:
 ```c
 struct dentry {
     unsigned int        d_flags;      /* 目录项标志 */
-    seqcount_spinlock_t d_seq;        /* 序列锁 */
+    seqcount_t          d_seq;        /* 序列锁(4.1;5.7+ 为 seqcount_spinlock_t)*/
     struct hlist_bl_node d_hash;      /* 哈希表节点(路径查找) */
     struct dentry       *d_parent;    /* 父目录项 */
     struct qstr         d_name;       /* 文件名 */
@@ -562,24 +585,33 @@ struct dentry {
     void                *d_fsdata;    /* 文件系统私有数据 */
 
     struct list_head    d_lru;        /* LRU 链表 */
-    struct list_head    d_child;      /* 子目录链表 */
+    struct list_head    d_child;      /* 挂在父目录 d_subdirs 上 */
     struct list_head    d_subdirs;    /* 子目录链表头 */
-    struct hlist_node   d_sib;        /* 兄弟目录链表 */
+    union {
+        struct hlist_node d_alias;    /* inode 别名链 */
+        struct rcu_head   d_rcu;      /* RCU 释放头 */
+    } d_u;
 };
 
-/* dentry 操作 */
+/* dentry 操作(Linux 4.1.15) */
 struct dentry_operations {
     int (*d_revalidate)(struct dentry *, unsigned int);
+    int (*d_weak_revalidate)(struct dentry *, unsigned int);
     int (*d_hash)(const struct dentry *, struct qstr *);
-    int (*d_compare)(const struct dentry *, unsigned int,
-                     const char *, const struct qstr *);
+    int (*d_compare)(const struct dentry *, const struct dentry *,
+                     unsigned int, const char *, const struct qstr *);
     int (*d_delete)(const struct dentry *);
     void (*d_release)(struct dentry *);
+    void (*d_prune)(struct dentry *);
     void (*d_iput)(struct dentry *, struct inode *);
     char *(*d_dname)(struct dentry *, char *, int);
+    struct vfsmount *(*d_automount)(struct path *);
+    int (*d_manage)(struct dentry *, bool);
 };
 ```
 
+> ⚠️ **版本差异**:`d_compare` 在 4.1.15 是「两个 dentry + 名称」共 5 个参数;`d_sib`、`seqcount_spinlock_t` 等是较新内核的字段名(4.1.15 中分别为 `d_u.d_alias`、`seqcount_t`)。
+
 **dentry 缓存:**
 
 - 内核维护 dentry 缓存(dentry cache),加速路径查找
@@ -620,7 +652,7 @@ struct file_operations {
     ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
     ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);
     ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
-    int (*iopoll)(struct kiocb *, struct io_comp_batch *, unsigned int);
+    int (*iterate)(struct file *, struct dir_context *);
     unsigned int (*poll)(struct file *, struct poll_table_struct *);
     long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
     long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
@@ -645,6 +677,8 @@ struct file_operations {
 
 > ⚠️ **驱动开发重点**: `file_operations` 是字符设备驱动必须实现的结构体。`read`/`write`/`ioctl`/`open`/`release` 是最常用的五个函数指针。
 
+> ⚠️ **版本差异**:以上以 Linux 4.1.15 为准。`iopoll` 字段是 5.x 才加入的;4.1.15 的目录读取回调字段名是 `iterate`(4.7+ 才改名为 `iterate_shared`)。
+
 ---
 
 ## 五、VFS操作
@@ -734,9 +768,8 @@ static const struct file_operations mydevice_fops = {
 inode 操作处理文件系统层面的元数据操作:
 
 ```c
-/* 目录 inode 操作 */
-static int myfs_mkdir(struct mnt_idmap *idmap,
-                      struct inode *dir, struct dentry *dentry, umode_t mode)
+/* 目录 inode 操作(Linux 4.1.15:无 mnt_idmap 参数) */
+static int myfs_mkdir(struct inode *dir, struct dentry *dentry, umode_t mode)
 {
     struct inode *inode;
     struct myfs_info *info = dir->i_sb->s_fs_info;
@@ -824,18 +857,20 @@ static int myfs_readdir(struct file *filp, struct dir_context *ctx)
     return 0;
 }
 
-/* 目录操作结构体 */
+/* 目录操作结构体(4.1.15 使用 .iterate) */
 static const struct file_operations myfs_dir_operations = {
-    .owner      = THIS_MODULE,
-    .llseek     = generic_file_llseek,
-    .read       = generic_read_dir,
-    .iterate_shared = myfs_readdir,
+    .owner   = THIS_MODULE,
+    .llseek  = generic_file_llseek,
+    .read    = generic_read_dir,
+    .iterate = myfs_readdir,
 };
 ```
 
 ### 5.4 mount 操作
 
-挂载操作将文件系统绑定到目录树:
+挂载操作将文件系统绑定到目录树。
+
+> ⚠️ **版本差异**:下面示例使用的是 5.2+ 的 `fs_context` 接口(`init_fs_context`/`get_tree`/`sget_fc`)。**Linux 4.1.15 没有 `fs_context`**,当时 `struct file_system_type` 通过 `.mount` 回调挂载,内存型文件系统通常配合 `mount_nodev()`/`mount_bdev()` 与 `sget()` 使用。移植驱动/文件系统时务必区分。
 
 ```c
 /* 文件系统类型 */
@@ -1013,4 +1048,6 @@ device_create(myclass, NULL, dev, NULL, "mydevice");
 
 ---
 
+**内容来源**:系统调用与 VFS 部分为内核源码级扩展知识,不属于《I.MX6U嵌入式Linux驱动开发指南》讲授范围,基于 Linux 4.1.15 源码(`arch/arm/kernel/entry-common.S`、`arch/arm/kernel/calls.S`、`include/linux/fs.h`、`include/linux/dcache.h`)核对整理。驱动注册、设备节点、设备树相关实践可参考原书「字符设备驱动」「Linux 设备树」等章节。
+
 **最后更新**: 2026-09-17

+ 63 - 40
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/04-进程调度与中断管理.md

@@ -7,6 +7,8 @@ updated: 2026-09-17
 
 # 进程调度与中断管理
 
+> ⚠️ **来源说明**:本主题不属于《I.MX6U嵌入式Linux驱动开发指南》的讲授范围,内容基于 Linux 内核源码(Linux 4.1.15)整理,为扩展知识。
+
 > **关联知识**: [[01-Cortex-A7架构详解|Cortex-A7架构详解]] · [[freertos-task-scheduling|FreeRTOS任务调度]] · [[freertos-interrupt-time|FreeRTOS中断管理]]
 
 ---
@@ -28,7 +30,7 @@ Linux 中进程和线程本质上都由 `task_struct` 描述,区别在于共
 
 ### 1.2 task_struct 结构体
 
-`task_struct` 是 Linux 内核中最大的数据结构之一(约 6KB,视配置而定),定义在 `include/linux/sched.h`。核心字段分组:
+`task_struct` 是 Linux 内核中最大的数据结构之一(32 位默认配置下通常 1~2KB,随内核选项浮动),定义在 `include/linux/sched.h`。核心字段分组:
 
 ```c
 struct task_struct {
@@ -48,8 +50,8 @@ struct task_struct {
     int normal_prio;            // 普通优先级
     unsigned int rt_priority;   // 实时优先级 (0~99)
     unsigned int policy;        // 调度策略 (SCHED_NORMAL / SCHED_FIFO 等)
-    u64 sum_exec_runtime;       // 总执行时间 (纳秒)
-    u64 vruntime;               // 虚拟运行时间 (CFS 核心)
+    struct sched_entity se;     // CFS 调度实体(vruntime 在这里)
+    struct sched_rt_entity rt;  // 实时调度实体
 
     /* --- 内存 --- */
     struct mm_struct *mm;       // 用户空间内存描述符
@@ -70,6 +72,8 @@ struct task_struct {
 };
 ```
 
+> ⚠️ **易错点(内核源码级)**:`task_struct` 本身**没有** `vruntime`、`sum_exec_runtime` 字段——它们属于 `struct sched_entity`(`task_struct.se`):`se.vruntime`、`se.sum_exec_runtime`。`prio`/`static_prio`/`normal_prio`/`rt_priority`/`policy` 才是 `task_struct` 的直接字段。
+
 ### 1.3 进程状态
 
 ```
@@ -125,23 +129,25 @@ struct task_struct {
 
 CFS 是 Linux 默认的普通进程调度器(`SCHED_NORMAL` / `SCHED_BATCH`),核心思想:**用虚拟运行时间 (vruntime) 衡量每个进程的"公平份额",始终调度 vruntime 最小的进程**。
 
+> ⚠️ **来源说明**:以下 CFS 内容为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码(`kernel/sched/fair.c`、`kernel/sched/sched.h`)整理。
+
 #### 2.2.1 虚拟运行时间
 
 ```
-实际运行时间 × (NICE_0_WEIGHT / 该进程权重) = vruntime 增量
+vruntime 增量 = 实际运行时间 × (NICE_0_LOAD / 该进程权重)
 ```
 
 - **nice 值越低 → 权重越高 → vruntime 增长越慢 → 获得更多 CPU 时间**
-- nice -20 的进程权重是 nice 19 的约 88 倍
-- 权重通过 `sched_prio_to_weight[]` 数组映射,共 40 级(对应 nice -20 ~ 19)
+- nice -20 的权重是 nice 19 的约 5900 倍(`88761 / 15`)
+- 权重通过 `prio_to_weight[]` 数组映射,共 40 级(对应 nice -20 ~ 19),列于 `kernel/sched/sched.h`
 
-权重表(部分):
+权重表(部分,Linux 4.1.15):
 
 | nice 值 | 权重  | nice 值 | 权重 |
 | ------- | ----- | ------- | ---- |
 | -20     | 88761 | 0       | 1024 |
-| -10     | 33554 | 10      | 256  |
-| -5      | 52429 | 19      | 15   |
+| -10     |  9548 | 10      |  110 |
+| -5      |  3121 | 19      |   15 |
 
 #### 2.2.2 红黑树组织
 
@@ -157,7 +163,9 @@ CFS 使用**红黑树**按 vruntime 排序所有可运行进程:
 
 - **最左节点**始终是 vruntime 最小的进程,调度器直接取左叶子
 - 插入/删除/查找最左节点均为 O(log n),支持数千进程不退化
-- `vruntime` 使用 `u64` 存储,溢出时通过 `calc_delta_mine()` 重新计算
+- `vruntime` 使用 `u64` 存储,实际运行中不会溢出;`calc_delta_fair()` / `calc_delta_mine()` 负责把实际运行时间按权重换算为 vruntime 增量
+
+> ⚠️ **版本差异**:Linux 4.1.15 的 CFS 运行队列用 `struct rb_root tasks_timeline` + `struct rb_node *rb_leftmost` 两个字段(`kernel/sched/sched.h`);把二者打包成 `rb_root_cached` 是 4.14+ 的改动。
 
 #### 2.2.3 CFS 调度周期
 
@@ -175,14 +183,24 @@ CFS 使用**红黑树**按 vruntime 排序所有可运行进程:
 进程睡眠醒来后,其 vruntime 可能远小于当前树中其他进程的 vruntime,导致它独占 CPU。CFS 的处理:
 
 ```c
-/* kernel/sched/fair.c: place_entity() */
-if (sleeping) {
-    /* 将新唤醒进程的 vruntime 设为树中最小值减去偏移 */
-    se->vruntime = avg_vruntime() - sysctl_sched_latency;
+/* kernel/sched/fair.c: place_entity()(Linux 4.1 实际实现) */
+static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
+{
+    u64 vruntime = cfs_rq->min_vruntime;
+
+    if (initial)                        /* 新建进程 */
+        vruntime += sched_vslice(cfs_rq, se);
+    if (!initial) {                     /* 唤醒进程:适度"追补"睡眠时间 */
+        unsigned long thresh = sysctl_sched_latency;
+        if (sched_feat(GENTLE_FAIR_SLEEPERS))
+            thresh >>= 1;               /* 减半,避免长期睡眠者抢占 CPU */
+        vruntime -= thresh;
+    }
+    se->vruntime = max_vruntime(se->vruntime, vruntime);
 }
 ```
 
-这确保睡眠进程获得合理的时间份额,但不会"抢夺"过多 CPU。
+这确保睡眠进程获得合理的时间份额,但不会"抢夺"过多 CPU。(注意:4.1 使用的是 `cfs_rq->min_vruntime` 与 `max_vruntime()`,并非新版内核重写后的 `avg_vruntime()`。)
 
 ### 2.3 优先级体系
 
@@ -199,11 +217,12 @@ Linux 优先级分三层,数值越小优先级越高:
 优先级换算关系:
 
 ```
-prio (动态优先级) = max(普通优先级, 实时优先级)
-rt_priority (用户态) = 99 - prio (实时进程时)
-nice = prio - 120  (普通进程时)
+实时进程:prio = 99 - rt_priority            (prio 0~99)
+普通进程:prio = normal_prio = 120 + nice    (prio 100~139)
 ```
 
+(内核的 `effective_prio()` 按调度策略在上述两式中选择,并非对两个数值取大。)
+
 `nice` 值是用户态接口,范围 -20(最高)到 +19(最低),每级差 1 个权重等级。
 
 ### 2.4 时间片与调度延迟
@@ -267,15 +286,15 @@ struct sched_class {
 
 > glibc 的 `pthread_create()` 底层调用 `clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...)`。
 
-### 3.2 do_fork / kernel_clone
+### 3.2 do_fork(kernel_clone)
 
-内核统一入口是 `kernel_clone()`(旧版为 `do_fork()`),流程:
+Linux 4.1.15 的统一入口是 **`do_fork()`**(位于 `kernel/fork.c`,`SYSCALL_DEFINE0(fork)`/`clone`/`vfork` 都调用它)。`do_fork()` 在 4.2 被拆分为 `_do_fork()`,5.9 起更名为 `kernel_clone()`——所以新资料里的 `kernel_clone()` 与 4.1.15 的 `do_fork()` 是同一个函数。流程:
 
 ```
 用户态 fork()/clone()
-  kernel_clone()
+  do_fork()                      // 5.9+ 内核中名为 kernel_clone()
         ├── copy_process()          // 核心:复制进程描述符
         │   ├── dup_task_struct()   // 复制 task_struct + 内核栈
@@ -348,24 +367,26 @@ exit() / _exit()
 ARM Cortex-A7 使用**异常向量表**(固定地址 `0xFFFF0000` 或 `0x00000000`,由 VBAR 寄存器配置):
 
 ```asm
-/* arch/arm/kernel/entry-armv.S */
+/* arch/arm/kernel/entry-armv.S(Linux 4.1) */
     .section .vectors, "ax", %progbits
-.Lvectors:
-    W(b)   vector_swi        /* 0x00: SVC 异常 (系统调用) */
+__vectors_start:
+    W(b)   vector_rst        /* 0x00: 复位 Reset */
     W(b)   vector_und        /* 0x04: 未定义指令 */
-    W(nop)                   /* 0x08: 预留 */
+    W(ldr) pc, __vectors_start + 0x1000  /* 0x08: SWI/SVC(跳到 vector_swi 指针)*/
     W(b)   vector_pabt       /* 0x0C: 指令预取中止 */
     W(b)   vector_dabt       /* 0x10: 数据访问中止 */
-    nop                      /* 0x14: 预留 */
+    W(b)   vector_addrexcptn /* 0x14: 保留 */
     W(b)   vector_irq        /* 0x18: IRQ 中断 */
     W(b)   vector_fiq        /* 0x1C: FIQ 快速中断 */
 ```
 
-> Cortex-A7 有 8 个异常入口,IRQ 占一个。中断号通过 GIC (Generic Interrupt Controller) 确定,而非向量表偏移。
+> Cortex-A7 有 8 个异常入口,IRQ 占一个。SVC(系统调用)的向量在偏移 0x08,而 0x00 是复位向量——两者常被混为一谈。中断号通过 GIC (Generic Interrupt Controller) 确定,而非向量表偏移。
+
+> ⚠️ **来源说明**:异常向量表布局为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码整理。
 
 ### 4.2 GIC 中断控制器
 
-GIC (Generic Interrupt Controller) 是 ARM 的标准中断控制器,GIC-400 支持最多 1020 个 SPI 中断
+GIC (Generic Interrupt Controller) 是 ARM 的标准中断控制器,GIC-400 最多支持 1020 个中断 ID(INTID 0~1019),其中 SPI 为 INTID 32~1019
 
 ```
 ┌─────────────────────────────────────────────┐
@@ -374,10 +395,10 @@ GIC (Generic Interrupt Controller) 是 ARM 的标准中断控制器,GIC-400 
 │  │  Distributor  │  │   CPU Interface      │ │
 │  │  (分发器)     │──│   (CPU 接口)         │ │
 │  │              │  │                      │ │
-│  │  GICD_ISENABLER  │  ICC_IAR  (中断确认) │ │
-│  │  GICD_ICENABLER  │  ICC_EOIR (中断结束) │ │
-│  │  GICD_IPRIORITYR  │  ICC_PMR  (优先级) │ │
-│  │  GICD_ITARGETSR   │  ICC_CTLR (控制)   │ │
+│  │  GICD_ISENABLER  │  GICC_IAR  (中断确认) │ │
+│  │  GICD_ICENABLER  │  GICC_EOIR (中断结束) │ │
+│  │  GICD_IPRIORITYR  │  GICC_PMR  (优先级) │ │
+│  │  GICD_ITARGETSR   │  GICC_CTLR (控制)   │ │
 │  │  GICD_ICFGR      │                    │ │
 │  └──────────────┘  └──────────────────────┘ │
 └─────────────────────────────────────────────┘
@@ -392,7 +413,7 @@ GIC (Generic Interrupt Controller) 是 ARM 的标准中断控制器,GIC-400 
 | ---- | -------- | ----------------------- | --------------- |
 | SGI  | 0~15     | 软件触发中断 (核间通信) | IPI             |
 | PPI  | 16~31    | 私有外设中断 (每核独有) | 定时器          |
-| SPI  | 32~1020  | 共享外设中断            | UART、GPIO、DMA |
+| SPI  | 32~1019  | 共享外设中断            | UART、GPIO、DMA |
 
 ### 4.3 中断处理流程
 
@@ -407,7 +428,7 @@ CPU 保存现场 (R0-R15, CPSR → SPSR_irq)
 gic_handle_irq()
-      ├── 读取 ICC_IAR 获取中断号
+      ├── 读取 GICC_IAR 获取中断号
       ├── 查找注册的 irq_desc
@@ -417,7 +438,7 @@ handle_irq_event()           ← 上半部 (Hardirq)
       │   └── 调度下半部 (如需)
-写 ICC_EOIR 通知 GIC 中断结束
+写 GICC_EOIR 通知 GIC 中断结束
 检查 softirq_pending 标志
@@ -448,11 +469,11 @@ int request_irq(unsigned int irq,
 /* 释放中断 — 返回注册时的 handler */
 void *free_irq(unsigned int irq, void *dev);
 
-/* 中断标志 */
-#define IRQF_SHARED        0x00000800   // 共享中断线
+/* 中断标志(Linux 4.1.15 include/linux/interrupt.h) */
+#define IRQF_SHARED        0x00000080   // 共享中断线
 #define IRQF_ONESHOT       0x00002000   // 中断线在下半部完成前保持屏蔽
-#define IRQF_NO_THREAD     0x00008000   // 不创建线程化的中断处理
 #define IRQF_NO_SUSPEND    0x00004000   // 系统挂起时不屏蔽此中断
+#define IRQF_NO_THREAD     0x00010000   // 不创建线程化的中断处理
 
 /* 禁止/使能中断 (本地 CPU) */
 local_irq_disable();    // 关中断 (CPSR.I=1)
@@ -496,10 +517,10 @@ enum {
     NET_TX_SOFTIRQ,         // 网络发送
     NET_RX_SOFTIRQ,         // 网络接收
     BLOCK_SOFTIRQ,          // 块设备
-    IRQ_POLL_SOFTIRQ,       // 中断轮询
+    BLOCK_IOPOLL_SOFTIRQ,   // 块设备轮询 I/O(4.1 为 BLOCK_IOPOLL,4.11+ 改为 IRQ_POLL_SOFTIRQ)
     TASKLET_SOFTIRQ,        // tasklet
     SCHED_SOFTIRQ,          // 调度器负载均衡
-    HRTIMER_SOFTIRQ,        // 高精度定时器 (未使用)
+    HRTIMER_SOFTIRQ,        // 高精度定时器
     RCU_SOFTIRQ,            // RCU 回调
     NR_SOFTIRQS
 };
@@ -762,4 +783,6 @@ Linux 的 fork 使用写时复制 (Copy-On-Write):
 
 ---
 
+**内容来源**:进程调度(`task_struct`/CFS/`sched_class`/`do_fork`)与中断管理(异常向量表/GIC/软中断/`IRQF_*`)部分为内核源码级扩展知识,不属于《I.MX6U嵌入式Linux驱动开发指南》讲授范围,已按 Linux 4.1.15 源码(`include/linux/sched.h`、`kernel/sched/*`、`kernel/fork.c`、`include/linux/interrupt.h`、`arch/arm/kernel/entry-armv.S`)核对。
+
 **最后更新**: 2026-09-17

+ 44 - 23
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/05-内存管理.md

@@ -7,6 +7,8 @@ updated: 2026-09-17
 
 # 内存管理机制
 
+> ⚠️ **来源说明**:本主题不属于《I.MX6U嵌入式Linux驱动开发指南》的讲授范围,内容基于 Linux 内核源码(Linux 4.1.15)整理,为扩展知识。
+
 > 💡 **关联知识**: [[FreeRTOS学习笔记/16-内存管理]] | [[计算机操作系统/内存管理]] | [[02-嵌入式Linux内核基础/06-设备模型与驱动框架]]
 
 ---
@@ -27,9 +29,11 @@ updated: 2026-09-17
 
 ## 二、页表与地址翻译
 
+> ⚠️ **来源说明**:本节页表/MMU/TLB 为内核与硬件源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码与 ARMv7-A 架构手册整理。
+
 ### 2.1 ARM32 两级页表结构
 
-ARMv7-A 的 MMU 使用「短描述符」(short-descriptor)格式,地址翻译由两个基址寄存器参与:`TTBR0` 指向用户空间页表,`TTBR1` 指向内核空间页表,`TTBCR.N` 决定两者各覆盖多少地址。Linux 常用 `TTBCR.N = 2`,即 TTBR0 覆盖低 3GB(用户),TTBR1 覆盖高 1GB(内核)。
+ARMv7-A 的 MMU 使用「短描述符」(short-descriptor)格式,地址翻译由两个基址寄存器参与:`TTBR0` 指向用户空间页表,`TTBR1` 指向内核空间页表。`TTBCR.N` 决定 TTBR0 所覆盖区域的大小(为 `2^(32-N)` 字节,其余地址走 TTBR1);Linux 的 3G/1G 或 2G/2G 划分由 `CONFIG_VMSPLIT_*`(即 `PAGE_OFFSET`)决定,配置时按该划分设置 N,**不要套用固定值**(例如「N=2 覆盖 3GB」是错误的:N=2 时 TTBR0 只有 1GB)。
 
 | 层级 | 名称                | 条目数 | 表大小 | 对齐要求 | 索引位      |
 | ---- | ------------------- | ------ | ------ | -------- | ----------- |
@@ -46,7 +50,7 @@ ARMv7-A 的 MMU 使用「短描述符」(short-descriptor)格式,地址翻
 +------------+----------+--------------+
 ```
 
-L2 描述符的 `bit[4]` 是 **XN(eXecute Never)**,置 1 表示该页不可执行——这是内核对栈和堆做 NX 保护、`mprotect(PROT_EXEC)` 生效的硬件基础,也是阻止栈溢出攻击跳转到 shellcode 的关键。一级描述符中 `bit[1:0]=0b10` 且 `bit[18]=1` 表示 **Supersection(16MB)**,要求 16MB 对齐,用一个大 TLB 条目覆盖大片内存,适合 CMA 这类大块连续区域。
+L2 描述符中小页的 **XN(eXecute Never)是 `bit[0]`**(硬件定义为 `PTE_EXT_XN = 1 << 0`),置 1 表示该页不可执行——这是内核对栈和堆做 NX 保护、`mprotect(PROT_EXEC)` 生效的硬件基础,也是阻止栈溢出攻击跳转到 shellcode 的关键。(`bit[4]` 属于 AP[0],不是 XN。)一级描述符中 `bit[1:0]=0b10` 且 `bit[18]=1` 表示 **Supersection(16MB)**,要求 16MB 对齐,用一个大 TLB 条目覆盖大片内存,适合 CMA 这类大块连续区域。
 
 ### 2.2 页表控制位:AP、Domain、C/B
 
@@ -63,15 +67,19 @@ AP[2] 是只读位:0 表示特权态读写,1 表示特权态只读。Linux 
 
 **Domain(域,`bit[8:5]`)** 是 4 位编号,配合协处理器寄存器 `DACR` 使用,DACR 中每个域占 2 bit:`00` No access(任何访问都异常)、`01` Client(正常执行 AP 检查)、`11` Manager(跳过 AP 检查且不更新访问位),`10` 保留。Linux 把所有页放在 Domain 0 并设为 Client,其余域设为 Manager。
 
-**C/B 与 TEX(缓存属性)**:`bit[3]=C`(Cacheable)、`bit[2]=B`(Bufferable)连同 `TEX[2:0]`(`bit[14:12]`)决定内存类型:
+**C/B 与 TEX(缓存属性)**:`bit[3]=C`(Cacheable)、`bit[2]=B`(Bufferable)连同 `TEX[2:0]` 决定内存类型。TEX 的位位置随描述符类型不同:**Section 为 `bit[14:12]`,小页为 `bit[8:6]`**(见 `arch/arm/include/asm/pgtable-2level-hwdef.h`,`PTE_EXT_TEX(x) = x << 6`)。
+
+| TEX   | C   | B   | 内存类型(ARM ARM 编码)                 | 用途                               |
+| ----- | --- | --- | ---------------------------------------- | ---------------------------------- |
+| `000` | 0   | 0   | Strongly-ordered                         | 外设寄存器,严格顺序,不缓存不重排 |
+| `000` | 0   | 1   | Shareable Device                         | 外设寄存器,写缓冲,可共享         |
+| `000` | 1   | 0   | Normal, Write-Through, no Write Allocate | 透写                               |
+| `000` | 1   | 1   | Normal, Write-Back, no Write Allocate    | 回写,不写分配                     |
+| `001` | 0   | 0   | Normal, Non-cacheable(Outer and Inner) | 需要显式 cache 维护的 DMA 缓冲     |
+| `001` | 1   | 1   | Normal, Write-Back, Write-Allocate       | 普通 DDR 内存                      |
+| `010` | 0   | 0   | Non-shareable Device                     | 不共享的设备内存                   |
 
-| TEX   | C   | B   | 内存类型                           | 用途                               |
-| ----- | --- | --- | ---------------------------------- | ---------------------------------- |
-| `000` | 0   | 0   | Strongly-ordered                   | 外设寄存器,严格顺序,不缓存不重排 |
-| `000` | 0   | 1   | Device / Shareable                 | 外设寄存器,写缓冲                 |
-| `000` | 1   | 0   | Normal, Non-cacheable              | 需要显式 cache 维护的 DMA 缓冲     |
-| `001` | 1   | 1   | Normal, Write-Back Write-Allocate  | 普通 DDR 内存                      |
-| `010` | 0   | 0   | Normal, Non-cacheable(shareable) | 多核共享数据                       |
+(是否共享另由描述符的 S 位决定。)
 
 驱动无需手改这些位,内核通过 `ioremap()` 的 `MT_DEVICE`、内存管理的 `MT_MEMORY` 等 `mem_type` 封装映射属性。
 
@@ -116,6 +124,8 @@ flowchart TD
 
 ## 三、页分配器与伙伴系统
 
+> ⚠️ **来源说明**:本节(伙伴系统、`struct page`、GFP 标志、内存压缩)为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码(`mm/page_alloc.c`、`include/linux/mmzone.h`、`include/linux/mm_types.h`)整理。
+
 ### 3.1 伙伴系统的组织结构
 
 伙伴系统把空闲内存按 2 的幂次组织成 11 个链表(order 0 到 order 10)。每个 order 内部不是单个链表,而是按**迁移类型**分组的数组:
@@ -161,13 +171,16 @@ flowchart TD
 释放是分配的逆操作。由于块本身是 2^order 页对齐的,伙伴的页帧号可以直接用异或算出,空闲且同 zone、同迁移类型就合并成 order+1,再继续向上判断:
 
 ```c
-static inline unsigned long
-__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
+/* Linux 4.1.15 mm/page_alloc.c:函数名为 __find_buddy_index,参数是页索引 */
+static inline unsigned long __find_buddy_index(unsigned long page_idx,
+                                               unsigned int order)
 {
-    return page_pfn ^ (1 << order);
+    return page_idx ^ (1 << order);
 }
 ```
 
+> ⚠️ **版本差异**:`__find_buddy_pfn()` 是较新内核(约 4.13+)重命名/重写后的版本;4.1.15 中是 `__find_buddy_index()`,直接操作 PFN 与页索引等价。
+
 ### 3.2 struct zone 与 struct pglist_data
 
 物理内存组织是三层:节点(Node)→ 区域(Zone)→ 页框(Page)。**`struct pglist_data`**(别名 `pg_data_t`)描述一个 NUMA 节点,全局数组 `node_data[MAX_NUMNODES]` 保存所有节点;嵌入式 ARM 平台通常只有一个节点(`contig_page_data`)。它包含 zone 数组、zonelist(分配失败时的区域回退顺序)、页框管理区 `node_mem_map`、`kswapd` 进程指针。
@@ -182,7 +195,7 @@ __find_buddy_pfn(unsigned long page_pfn, unsigned int order)
 
 `struct zone` 关键字段:`free_area[MAX_ORDER]`(伙伴系统链表)、`watermark[NR_WMARK]`(水位线)、`nr_free_pages`、`managed_pages`/`present_pages`/`spanned_pages`、`lru_lock` 与 `lruvec`、`vm_stat[]`。
 
-**水位线**是内存回收的触发条件:空闲页降到 `WMARK_LOW` 唤醒 `kswapd`;降到 `WMARK_MIN` 进入 direct reclaim,由分配者自己回收;连 `WMARK_MIN` 都保不住且请求带 `ALLOC_NO_WATERMARKS`(如 `PF_MEMALLOC` 的回收线程)则可能触发 OOM Killer;`WMARK_HIGH` 是 kswapd 的停止线
+**水位线**是内存回收的触发条件:空闲页降到 `WMARK_LOW` 唤醒 `kswapd`;降到 `WMARK_MIN` 进入 direct reclaim,由分配者同步回收;回收后仍无法满足、且请求未被豁免时,才调用 OOM Killer 选择进程杀死。`WMARK_HIGH` 是 kswapd 的停止线。注意 `ALLOC_NO_WATERMARKS`(配合 `PF_MEMALLOC`,如回收线程)是「跳过水位线检查」的豁免标志,它本身**不会触发 OOM**
 
 ```bash
 cat /proc/buddyinfo      # 各 zone 各 order 的空闲块数量
@@ -194,7 +207,7 @@ cat /proc/zoneinfo       # 水位线、统计
 
 ### 3.3 struct page
 
-内核为每个页框维护一个 `struct page`( 64 字节),所有 `struct page` 组成全局数组 `mem_map`,以页帧号(PFN)为下标访问。`flags` 常用标志:PG_locked(正在 I/O)、PG_dirty(需回写)、PG_lru(在 LRU 链上)、PG_slab(由 slab 管理)、PG_buddy(伙伴系统空闲页)。
+内核为每个页框维护一个 `struct page`(32 位系统上通常约 32 字节,随内核配置浮动,并非固定 64 字节),所有 `struct page` 组成全局数组 `mem_map`,以页帧号(PFN)为下标访问。`flags` 常用标志:PG_locked(正在 I/O)、PG_dirty(需回写)、PG_lru(在 LRU 链上)、PG_slab(由 slab 管理)、PG_buddy(伙伴系统空闲页)。
 
 `_count` 是引用计数,降到 0 才可回收;`_mapcount` 记录被多少个页表项映射,为 -1 时表示无进程映射。两者区分对理解回收和 COW 至关重要:一个页可能被 `mmap` 到多个进程(`_mapcount > 0`)同时被文件缓存持有(`_count > 0`),只有两者都归零才能释放。SLUB 复用了 `struct page` 的 union 字段存放 slab 元数据(`slab_cache`、`freelist`),这也是 SLUB 比 SLAB 省内存的原因。
 
@@ -216,13 +229,13 @@ cat /proc/zoneinfo       # 水位线、统计
 | `GFP_DMA`    | 需要 DMA 可用内存      | 从 DMA 区域分配,地址低于 16MB     |
 | `GFP_DMA32`  | 需要 32 位地址的内存   | 地址低于 4GB                       |
 
-常用修饰符:`__GFP_ZERO`(填零)、`__GFP_RETRY_MAYFAIL`(尽力尝试,允许失败)、`__GFP_NOFAIL`(绝不失败,慎用)、`__GFP_COMP`(复合页)、`__GFP_MOVABLE`(可迁移)。选择原则:中断或持锁路径必须用 `GFP_ATOMIC`;进程上下文用 `GFP_KERNEL`;若代码本身就在回收路径中(如 `shrink_slab`)必须用 `GFP_NOFS`/`GFP_NOIO` 避免递归。
+常用修饰符:`__GFP_ZERO`(填零)、`__GFP_RETRY_MAYFAIL`(4.13+,尽力尝试且允许失败;4.1.15 中对应的是 `__GFP_REPEAT`/`__GFP_NORETRY`)、`__GFP_NOFAIL`(绝不失败,慎用)、`__GFP_COMP`(复合页)、`__GFP_MOVABLE`(可迁移)。选择原则:中断或持锁路径必须用 `GFP_ATOMIC`;进程上下文用 `GFP_KERNEL`;若代码本身就在回收路径中(如 `shrink_slab`)必须用 `GFP_NOFS`/`GFP_NOIO` 避免递归。
 
 ### 3.5 内存压缩(Compaction)
 
 内存压缩解决「有空间但没有连续空间」的问题:把散落在低地址的**可移动页**搬到其他空闲页上,腾出连续区域。`compact_zone()` 分四步:隔离(从 zone 尾部扫描,把可移动页从 LRU 摘下放入 `migrate_pages` 链表)→ 迁移(分配新位置、拷贝内容、修改所有指向它的 PTE、释放原页)→ 同步(更新映射并刷新 TLB)→ 释放空闲块并触发伙伴合并。
 
-相关参数与观测:`kcompactd` 每 NUMA 节点一个线程,在 kswapd 之后被动触发;`/proc/sys/vm/compaction_proactiveness`(5.16+)控制主动压缩程度;`/proc/sys/vm/extfrag_threshold` 是碎片指数阈值;`/sys/kernel/debug/extfrag/unusable_index` 给出各 order 的不可用指数(越接近 1000 表示按该 order 分配越必然失败)。
+相关参数与观测:`kcompactd` 每 NUMA 节点一个线程(4.7+ 引入),在 kswapd 之后被动触发;`/proc/sys/vm/compaction_proactiveness`(5.16+)控制主动压缩程度;`/proc/sys/vm/extfrag_threshold` 是碎片指数阈值;`/sys/kernel/debug/extfrag/unusable_index` 给出各 order 的不可用指数(越接近 1000 表示按该 order 分配越必然失败)。
 
 压缩的代价是迁移成本,且**不可移动页是硬约束**:`MIGRATE_UNMOVABLE` 的页无法搬运,散落在要合并的区域中就会导致压缩失败,因此内核尽量把不可移动页聚合。只有请求 order `>= PAGE_ALLOC_COSTLY_ORDER`(通常为 3)时才值得压缩,低 order 请求直接回退到其他迁移类型更划算。
 
@@ -230,6 +243,8 @@ cat /proc/zoneinfo       # 水位线、统计
 
 ## 四、slab 分配器
 
+> ⚠️ **来源说明**:本节(SLAB/SLUB/SLOB、`kmem_cache`、对象着色、`kmalloc` size class)为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码(`mm/slub.c`、`include/linux/slab.h`)整理。
+
 ### 4.1 为什么需要 slab
 
 伙伴系统以页为最小单位分配,但内核中大量数据结构只需几十到几百字节。如果每个 `struct task_struct`(约 2KB)或 `struct inode`(约 600 字节)都占一整页,内存浪费严重,且频繁小块分配/释放会造成外部碎片。
@@ -331,7 +346,7 @@ slab 内第 `i` 个对象的地址为 `slab_start + s->colour * s->colour_off +
 
 ### 4.5 kmalloc 与 size class 内部实现
 
-`kmalloc` 内部维护一组 `kmalloc_caches`,按用途分为 `KMALLOC_NORMAL`、`KMALLOC_RECLAIM`(带 `__GFP_RECLAIMABLE`)、`KMALLOC_DMA`、`KMALLOC_CGROUP`,每种类型下再按 size class 组织。分配时把请求大小向上取整到最近的 class(`kmalloc_index()`,节选):
+`kmalloc` 内部维护按 size class 组织的一组 `kmalloc_caches[]`(`GFP_DMA` 请求另走 `kmalloc_dma_caches[]`)。**Linux 4.1.15 中不存在 `KMALLOC_NORMAL`/`KMALLOC_RECLAIM`/`KMALLOC_CGROUP` 这些「用途维度」**——按用途分维(`kmalloc_caches[type][index]`)是 4.6+ 才引入的。分配时把请求大小向上取整到最近的 class(`kmalloc_index()`,节选):
 
 ```c
 if (size <= 8)    return 3;   /* kmalloc-8    */
@@ -348,7 +363,7 @@ if (size <= 2048) return 11;
 if (size <= 4096) return 12;
 ```
 
-`96` 和 `192` 这两个非 2 的幂的 class 是内核统计真实分配分布后加进去的,这两个尺寸请求量极大,专用 cache 能避免明显的内部碎片浪费。所以 `kmalloc(300, GFP_KERNEL)` 实际落在 `kmalloc-512`,占用 512 字节;`kmalloc(100, GFP_KERNEL)` 落在 `kmalloc-128`。`KMALLOC_MAX_CACHE_SIZE` 通常是 8KB(也可是 4KB),超过后走页分配器;`KMALLOC_MAX_SIZE` 一般对应 `MAX_ORDER` 能提供的最大连续块(4MB 量级)。`kmalloc` 保证物理连续,这是它适合 DMA 的原因。
+`96` 和 `192` 这两个非 2 的幂的 class 是内核统计真实分配分布后加进去的,这两个尺寸请求量极大,专用 cache 能避免明显的内部碎片浪费。所以 `kmalloc(300, GFP_KERNEL)` 实际落在 `kmalloc-512`,占用 512 字节;`kmalloc(100, GFP_KERNEL)` 落在 `kmalloc-128`。`KMALLOC_MAX_CACHE_SIZE` 通常是 8KB(也可是 4KB),超过后走页分配器;`KMALLOC_MAX_SIZE` 由 `KMALLOC_SHIFT_MAX` 决定(4.1.15 SLUB 下为 `MAX_ORDER + PAGE_SHIFT = 23`,即 8MB;但受伙伴系统最大 order 限制,实际可用的连续块通常不超过 4MB)。`kmalloc` 保证物理连续,这是它适合 DMA 的原因。
 
 | API                      | 说明                                       |
 | ------------------------ | ------------------------------------------ |
@@ -399,10 +414,12 @@ void  vunmap(const void *addr);
 void __iomem *ioremap(phys_addr_t phys_addr, size_t size);
 void __iomem *ioremap_cache(phys_addr_t phys_addr, size_t size);
 void __iomem *ioremap_wc(phys_addr_t phys_addr, size_t size);
-void __iomem *ioremap_np(phys_addr_t phys_addr, size_t size);  /* non-posted */
+void __iomem *ioremap_np(phys_addr_t phys_addr, size_t size);  /* non-posted,5.8+ 才有 */
 void iounmap(void __iomem *addr);
 ```
 
+> ⚠️ **版本差异**:`ioremap_np` 是 5.8+ 新增的接口,Linux 4.1.15 中不存在。
+
 `ioremap` 建立的是 `MT_DEVICE` 属性映射(Strongly-ordered 或 Device),即不缓存、保持访问顺序。**返回的 `__iomem` 指针不能直接解引用**,必须用 `readl`/`writel`/`readb`/`writeb`;`__iomem` 是 `sparse` 的地址空间注解,直接解引用会被静态检查器报错,也是驱动里最常见的低级错误之一。这些访问器还隐含内存屏障语义,用 `memcpy` 或指针赋值都会破坏访问顺序。
 
 ```c
@@ -410,7 +427,7 @@ static int my_probe(struct platform_device *pdev)
 {
     void __iomem *base;
 
-    /* 一行搞定 resource 提取 + request_mem_region + ioremap */
+    /* 一行搞定 resource 提取 + request_mem_region + ioremap(4.14+ 提供)*/
     base = devm_platform_ioremap_resource(pdev, 0);
     if (IS_ERR(base))
         return PTR_ERR(base);
@@ -431,7 +448,7 @@ static int my_probe(struct platform_device *pdev)
 | 访问方式 | 直接解引用                         | 直接解引用             | `readl`/`writel`       | 直接解引用             |
 | 缓存属性 | 普通可缓存                         | 普通可缓存             | `MT_DEVICE`,不缓存    | 不缓存(coherent)     |
 | 分配速度 | 快                                 | 慢                     | 中(改页表)           | 慢(可能需要迁移页面) |
-| 大小上限 | ~4MB(`KMALLOC_MAX_SIZE`)         | 虚拟地址空间限制       | 任意                   | CMA 区大小限制         |
+| 大小上限 | 4MB~8MB(`KMALLOC_MAX_SIZE`)    | 虚拟地址空间限制       | 任意                   | CMA 区大小限制         |
 | 可睡眠   | `GFP_KERNEL` 可,`GFP_ATOMIC` 不可 | 可(不可在原子上下文) | 可                     | 可                     |
 | 释放     | `kfree`                            | `vfree`                | `iounmap`              | `dma_free_coherent`    |
 | 典型用途 | 结构体、小缓冲区                   | 大块内存、模块加载     | 外设寄存器             | DMA 描述符与缓冲区     |
@@ -852,4 +869,8 @@ TLB 是硬件翻译缓存,缓存「虚拟页号 + ASID → 物理页号 + 权
 4. **看碎片指数**:`/sys/kernel/debug/extfrag/unusable_index`,接近 1000 表示按该 order 分配几乎必然失败。
 5. **看泄漏**:设备长时间运行后 `CmaFree` 单调下降,多半是驱动错误路径漏了 `dma_free_coherent`;注意 CMA 不在 slab 里,别只盯 `/proc/slabinfo`。
 
-修复手段:放大设备树中 `linux,cma` 的 `size`、为特定外设建立专用 `shared-dma-pool` 隔离、启动参数加 `cma=64M`、在启动早期一次性分配好缓冲区、把大缓冲区分配从运行时前置到 `probe`。若平台有 IOMMU(如 RK3568)优先改用 IOMMU 映射,从根上摆脱物理连续约束。另外 `dma_alloc_coherent` 失败不总是返回 NULL,也可能返回 `ERR_PTR`,必须用 `IS_ERR_OR_NULL` 检查。
+修复手段:放大设备树中 `linux,cma` 的 `size`、为特定外设建立专用 `shared-dma-pool` 隔离、启动参数加 `cma=64M`、在启动早期一次性分配好缓冲区、把大缓冲区分配从运行时前置到 `probe`。若平台有 IOMMU(如 RK3568)优先改用 IOMMU 映射,从根上摆脱物理连续约束。另外 `dma_alloc_coherent` 失败时按约定返回 NULL(用 `if (!buf)` 判断即可);个别平台/封装可能返回 `ERR_PTR`,稳妥起见也可用 `IS_ERR_OR_NULL` 检查。
+
+---
+
+**内容来源**:内存管理部分为内核源码级扩展知识,不属于《I.MX6U嵌入式Linux驱动开发指南》讲授范围,已按 Linux 4.1.15 源码(`arch/arm/mm/*`、`mm/page_alloc.c`、`mm/slub.c`、`include/linux/slab.h`、`include/linux/mmzone.h`、`include/linux/dma-mapping.c`)与 ARMv7-A 手册核对。设备树 `reserved-memory`、platform 驱动中的内存使用可参考原书「Linux 设备树」「platform 设备驱动」章节。

+ 65 - 34
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/06-设备模型与Kobject.md

@@ -7,6 +7,8 @@ updated: 2026-09-17
 
 # 设备模型与Kobject
 
+> ⚠️ **来源说明**:本主题不属于《I.MX6U嵌入式Linux驱动开发指南》的讲授范围,内容基于 Linux 内核源码(Linux 4.1.15)整理,为扩展知识。
+
 > 💡 **关联知识**: [[03-Linux驱动开发核心/07-platform驱动模型]] | [[03-Linux驱动开发核心/02-设备树语法与实战]] | [[02-嵌入式Linux内核基础/05-内存管理机制]]
 
 ---
@@ -165,6 +167,8 @@ void kobject_put(struct kobject *kobj);
 
 ### 2.5 引用计数
 
+> ⚠️ **来源说明**:kobject 的生命周期与引用计数规则为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码(`include/linux/kobject.h`、`include/linux/kref.h`)整理。
+
 Kobject使用`kref`进行引用计数管理:
 
 ```c
@@ -355,7 +359,7 @@ struct kset *kset_create_and_add(const char *name,
 void kset_init(struct kset *k);
 
 /* 添加kset到sysfs */
-int kset_add(struct kset *k);
+int kset_register(struct kset *k);
 
 /* 从sysfs移除kset */
 void kset_unregister(struct kset *k);
@@ -369,12 +373,11 @@ struct kobject *kset_find_obj(struct kset *k, const char *name);
 Ktype定义了kobject的属性和操作函数。
 
 ```c
-/* 内核源码: include/linux/kobject.h */
+/* 内核源码: include/linux/kobject.h(Linux 4.1.15) */
 struct kobj_type {
     void (*release)(struct kobject *kobj);  /* 释放函数 */
     const struct sysfs_ops *sysfs_ops;      /* sysfs操作函数 */
     struct attribute **default_attrs;        /* 默认属性 */
-    const struct attribute_group **default_groups; /* 默认属性组 */
     const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
     const void *(*namespace)(struct kobject *kobj);
 };
@@ -386,6 +389,8 @@ struct sysfs_ops {
 };
 ```
 
+> ⚠️ **版本差异**:`default_groups` 字段是 4.9+ 才加入 `kobj_type` 的;Linux 4.1.15 只能用 `default_attrs` 指定默认属性(属性组需在 kobject 添加后另行 `sysfs_create_groups()`)。下文示例已按 4.1.15 调整为 `default_attrs`。
+
 ### 3.4 关系图
 
 ```
@@ -460,7 +465,7 @@ static struct attribute_group my_attr_group = {
 static struct kobj_type my_ktype = {
     .release = my_release,
     .sysfs_ops = &kobj_sysfs_ops,
-    .default_groups = (const struct attribute_group *[]){ &my_attr_group, NULL },
+    .default_attrs = my_attrs,  /* 4.1.15 用 default_attrs;default_groups 为 4.9+ */
 };
 
 static int my_uevent_filter(struct kset *kset, struct kobject *kobj)
@@ -579,17 +584,25 @@ struct device_attribute {
                      const char *buf, size_t count);
 };
 
-/* 宏定义 */
-#define __ATTR(_name, _mode, _show, _store) \
-    struct kobj_attribute kobj_attr_##_name = __ATTR_INITIALIZER(_name, _mode, _show, _store)
+/* 宏定义(Linux 4.1.15,位于 include/linux/sysfs.h,均为聚合初始化器) */
+#define __ATTR(_name, _mode, _show, _store) {                 \
+    .attr  = { .name = __stringify(_name),                    \
+               .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },     \
+    .show  = _show,                                           \
+    .store = _store,                                          \
+}
 
-#define __ATTR_RO(_name) \
-    struct kobj_attribute kobj_attr_##_name = __ATTR_RO_INITIALIZER(_name)
+#define __ATTR_RO(_name) {                                    \
+    .attr  = { .name = __stringify(_name), .mode = S_IRUGO }, \
+    .show  = _name##_show,                                    \
+}
 
-#define __ATTR_RW(_name) \
-    struct kobj_attribute kobj_attr_##_name = __ATTR_RW_INITIALIZER(_name)
+#define __ATTR_RW(_name) __ATTR(_name, (S_IWUSR | S_IRUGO),   \
+                                _name##_show, _name##_store)
 ```
 
+> ⚠️ **易错点**:`__ATTR_INITIALIZER` / `__ATTR_RO_INITIALIZER` 这类宏在内核中并不存在(属编造);`__ATTR*` 直接展开为结构体聚合初始化,见 4.1.15 的 `include/linux/sysfs.h`。
+
 ### 4.3 sysfs文件操作
 
 ```c
@@ -693,7 +706,7 @@ static struct attribute_group led_attr_group = {
 static struct kobj_type led_ktype = {
     .release = led_release,
     .sysfs_ops = &kobj_sysfs_ops,
-    .default_groups = (const struct attribute_group *[]){ &led_attr_group, NULL },
+    .default_attrs = led_attrs, /* 4.1.15 用 default_attrs;default_groups 为 4.9+ */
 };
 
 static void led_release(struct kobject *kobj)
@@ -774,28 +787,35 @@ kset_create_and_add(           /sys/
 ### 5.1 struct bus_type
 
 ```c
-/* 内核源码: include/linux/device/bus.h */
+/* 内核源码: include/linux/device.h(Linux 4.1.15;device/bus.h 是 5.0+ 才拆分出来的)*/
 struct bus_type {
     const char *name;                      /* 总线名称 */
     const char *dev_name;                  /* 设备名称前缀 */
-    struct module *owner;
+    const struct attribute_group **bus_groups;
+    const struct attribute_group **dev_groups;
+    const struct attribute_group **drv_groups;
 
     int (*match)(struct device *dev, struct device_driver *drv);  /* 匹配函数 */
     int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
     int (*probe)(struct device *dev);      /* 探测函数 */
     int (*remove)(struct device *dev);     /* 移除函数 */
     void (*shutdown)(struct device *dev);
-    int (*pm)(struct device *dev, pm_message_t message);  /* 电源管理 */
-    void *p;                               /* 总线私有数据 */
+    int (*suspend)(struct device *dev, pm_message_t state);
+    int (*resume)(struct device *dev);
+    const struct dev_pm_ops *pm;           /* 电源管理操作集(指针,不是函数)*/
+    struct subsys_private *p;              /* 总线私有数据 */
 };
 ```
 
+> ⚠️ **易错点**:`struct bus_type` **没有** `struct module *owner` 字段;`pm` 是 `const struct dev_pm_ops *` 指针;`p` 的真实类型是 `struct subsys_private *`。
+
 ### 5.2 struct device
 
 ```c
-/* 内核源码: include/linux/device.h */
+/* 内核源码: include/linux/device.h(Linux 4.1.15) */
 struct device {
     struct device *parent;                 /* 父设备 */
+    struct device_private *p;
     struct kobject kobj;                   /* 内嵌kobject */
 
     const char *init_name;                 /* 初始名称 */
@@ -809,22 +829,19 @@ struct device {
     struct dev_pm_domain *pm_domain;       /* 电源域 */
 
     struct device_node *of_node;           /* OF设备树节点 */
-    struct acpi_dev_node *acpi_node;
-
-    struct resource *resource;             /* 资源数组 */
-    unsigned int num_resources;
 
-    struct class *class;                   /* 所属类 */
     struct attribute_group **groups;
-
+    struct class *class;                   /* 所属类 */
     void (*release)(struct device *dev);   /* 释放函数 */
 };
 ```
 
+> ⚠️ **易错点**:`struct device` **没有** `resource`/`num_resources` 字段——它们属于 `struct platform_device`;4.1.15 的 `struct device` 也没有 `struct acpi_dev_node *acpi_node` 指针(仅 ARM/ACPI 平台才涉及,且类型不同)。
+
 ### 5.3 struct device_driver
 
 ```c
-/* 内核源码: include/linux/device/driver.h */
+/* 内核源码: include/linux/device.h(Linux 4.1.15;device/driver.h 是 5.0+ 才拆分出来的)*/
 struct device_driver {
     const char *name;                      /* 驱动名称 */
     struct bus_type *bus;                  /* 所属总线 */
@@ -834,14 +851,18 @@ struct device_driver {
     const struct acpi_device_id *acpi_match_table;
 
     int (*probe)(struct device *dev);      /* 探测函数 */
-    void (*remove)(struct device *dev);    /* 移除函数 */
+    int (*remove)(struct device *dev);     /* 移除函数 */
     void (*shutdown)(struct device *dev);
-    int (*pm)(struct device *dev, pm_message_t state);
+    int (*suspend)(struct device *dev, pm_message_t state);
+    int (*resume)(struct device *dev);
+    const struct dev_pm_ops *pm;           /* 电源管理操作集(指针,不是函数)*/
 
     struct driver_private *p;
 };
 ```
 
+> ⚠️ **易错点**:`pm` 在 4.1.15 是 `const struct dev_pm_ops *`,而不是 `int (*pm)(...)` 函数指针。
+
 ### 5.4 匹配机制
 
 ```
@@ -895,6 +916,10 @@ static int platform_match(struct device *dev, struct device_driver *drv)
     struct platform_device *pdev = to_platform_device(dev);
     struct platform_driver *pdrv = to_platform_driver(drv);
 
+    /* 0. driver_override 指定了驱动名时只匹配它(4.1 已存在) */
+    if (pdev->driver_override)
+        return !strcmp(pdev->driver_override, drv->name);
+
     /* 1. 先尝试OF匹配(设备树) */
     if (of_driver_match_device(dev, drv))
         return 1;
@@ -934,6 +959,8 @@ static int i2c_device_match(struct device *dev, struct device_driver *drv)
 }
 ```
 
+> ⚠️ **版本差异**:Linux 4.1.15 的 `i2c_device_match()` 在 id_table 未命中时**直接 `return 0`**(不做名称比较);上例第 4 步的"名称匹配"并非 4.1.15 的行为(`platform_match` 才有名称回退)。
+
 ### 5.6 总线注册与注销
 
 ```c
@@ -971,25 +998,27 @@ int bus_for_each_drv(struct bus_type *bus, struct device_driver *start,
 ### 6.1 platform_device
 
 ```c
-/* 内核源码: include/linux/platform_device.h */
+/* 内核源码: include/linux/platform_device.h(Linux 4.1.15) */
 struct platform_device {
     const char *name;                      /* 设备名称 */
     int id;                                /* 设备ID,-1表示自动分配 */
-    int id_auto;
+    bool id_auto;
     struct device dev;                     /* 内嵌device */
 
+    u32 num_resources;
     struct resource *resource;             /* 资源数组 */
-    unsigned int num_resources;
 
     const struct platform_device_id *id_entry;
+    char *driver_override;                 /* 强制匹配指定驱动 */
 
-    struct device_node *of_node;           /* OF设备树节点 */
-    struct acpi_dev_node *acpi_node;
-
-    void *platform_data;                   /* 平台特定数据 */
-    void *driver_data;
+    struct mfd_cell *mfd_cell;
+    struct pdev_archdata archdata;
 };
+```
 
+> ⚠️ **易错点**:`platform_device` 没有 `of_node`/`acpi_node`/`platform_data`/`driver_data` 字段,它们都属于内嵌的 `dev`(如 `pdev->dev.of_node`、`pdev->dev.platform_data`)。
+
+```c
 /* 资源结构 */
 struct resource {
     resource_size_t start;                 /* 起始地址 */
@@ -1448,4 +1477,6 @@ static int my_probe(struct platform_device *pdev)
 
 ---
 
+**内容来源**:设备模型(`kobject`/`kset`/`kobj_type`/sysfs、`bus_type`/`device`/`device_driver`、platform 总线)为内核源码级扩展知识,不属于《I.MX6U嵌入式Linux驱动开发指南》讲授范围,已按 Linux 4.1.15 源码(`include/linux/kobject.h`、`include/linux/device.h`、`include/linux/platform_device.h`、`include/linux/sysfs.h`)核对。属性文件、platform 驱动实战可参考原书「platform 设备驱动实验」「设备树下的 platform 驱动」等章节。
+
 **最后更新**: 2026-09-17

+ 34 - 16
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/02-嵌入式Linux内核基础/面试-Linux内核基础.md

@@ -18,6 +18,8 @@ updated: 2026-09-17
 
 # 面试-Linux内核基础
 
+> ⚠️ **来源说明**:本文的「系统调用与 VFS」「进程调度」「内存管理」「设备模型」四个主题不属于《I.MX6U嵌入式Linux驱动开发指南》的讲授范围,内容基于 Linux 内核源码(Linux 4.1.15)整理,为扩展知识;「内核配置与编译」「内核模块」以及设备树相关内容可对应原书第三十五章(内核顶层 Makefile 分析)、第三十六章(Linux 内核启动流程)、第三十七章(Linux 内核移植)与第四十三章(Linux 设备树)等章节。
+
 > 本文档涵盖Linux内核基础的综合面试题,覆盖内核配置编译、模块机制、系统调用VFS、进程调度、内存管理、设备模型六大核心领域。
 
 ---
@@ -51,6 +53,8 @@ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
 ```
 
+> 📖 **原书参考**:内核配置与顶层 Makefile 详见《I.MX6U嵌入式Linux驱动开发指南》第三十五章「Linux内核顶层Makefile分析」。
+
 **追问**:
 
 - 追问1:.config文件的作用是什么?如何利用它快速配置内核?
@@ -203,6 +207,8 @@ boot
 bootloader → cmd_line指针 → __setup()注册的处理函数
 ```
 
+> 📖 **原书参考**:bootargs 由 U-Boot 传递、内核在启动早期解析,详见原书第三十六章「Linux内核启动流程」。
+
 **追问**:
 
 - 追问1:如何在运行时查看当前的内核启动参数?
@@ -495,13 +501,15 @@ fd = open("/dev/mydev", O_RDWR);
 **内核空间**:
 
 ```
-SVC指令 → 异常向量表 → el0_svc
+SVC指令 → 异常向量表 → vector_swi(ARM32)/ el0_svc(ARM64)
 → 保存寄存器到pt_regs
 → 根据系统调用号索引sys_call_table
 → 执行对应的sys_xxx函数
 → 恢复寄存器,返回用户空间
 ```
 
+> ⚠️ **注意**:ARM32(如 I.MX6ULL)的入口是 `vector_swi`,系统调用号放在 **R7**;ARM64 的入口是 `el0_svc`,号放在 **x8**。两者的向量表和寄存器约定完全不同,不要混用。
+
 ARM64架构的系统调用约定:
 
 | 寄存器 | 用途       |
@@ -544,7 +552,7 @@ struct super_block {
 ```c
 struct inode {
     struct inode_operations *i_op;  // 操作函数
-    struct file_operations *fops;   // 文件操作
+    struct file_operations *i_fop;  // 文件操作(注意是 i_fop,不是 fops)
     umode_t i_mode;                 // 文件类型和权限
     // ...
 };
@@ -557,7 +565,7 @@ struct dentry {
     struct dentry_operations *d_op;
     struct inode *d_inode;          // 关联的inode
     struct dentry *d_parent;        // 父目录
-    char *d_name;                   // 目录名
+    struct qstr d_name;             // 目录名(是 struct qstr,不是 char *)
     // ...
 };
 ```
@@ -596,7 +604,7 @@ mount系统调用流程:
 用户调用: mount("/dev/sda1", "/mnt", "ext4", 0, NULL)
 → 内核解析参数
 → 查找文件系统类型(find_filesystem)
-→ 调用get_tree_bdev()
+→ 调用mount_bdev()          (Linux 4.1.15;5.2+ 改为 get_tree_bdev()
 → 分配super_block
 → 调用ext4_fill_super()
 → 读取磁盘上的超级块
@@ -864,13 +872,13 @@ sched_setscheduler(pid, SCHED_RR, &param);
 3. 支持多级均衡策略
 
 **详细解答**:
-调度域层级结构:
+调度域层级结构(自顶向下,范围由大到小)
 
 ```
-System (SMT域)
-  └── MC域 (多核)
-      └── DIE域 (同一芯片)
-          └── NUMA域 (跨节点)
+NUMA域 (跨节点)
+  └── DIE域 (同一芯片)
+      └── MC域 (多核 / 多线程)
+          └── SMT域 (超线程)
 ```
 
 负载均衡流程:
@@ -930,10 +938,10 @@ nice值与权重映射表:
 | Nice值 | 权重  | 说明       |
 | ------ | ----- | ---------- |
 | -20    | 88761 | 最高优先级 |
-| -10    | 71755 |            |
-| 0      | 1024  | 基准       |
-| 10     | 135   |            |
-| 19     | 15    | 最低优先级 |
+| -10    |  9548 |            |
+| 0      |  1024 | 基准       |
+| 10     |   110 |            |
+| 19     |    15 | 最低优先级 |
 
 示例:
 
@@ -1341,6 +1349,8 @@ static int my_probe(struct platform_device *pdev)
 }
 ```
 
+> 📖 **原书参考**:设备树语法与实战详见原书第四十三章「Linux设备树」。
+
 **追问**:
 
 - 追问1:设备树overlay的作用是什么?如何使用?
@@ -1447,10 +1457,14 @@ cdev_add(&my_cdev, devno, 1);
 ```c
 #include <linux/blkdev.h>
 
+/* 块设备的 open/release 签名与字符设备不同 */
+static int  my_bopen(struct block_device *bdev, fmode_t mode) { return 0; }
+static void my_brelease(struct gendisk *gd, fmode_t mode) { }
+
 static struct block_device_operations my_bops = {
-    .owner = THIS_MODULE,
-    .open = my_open,
-    .release = my_release,
+    .owner   = THIS_MODULE,
+    .open    = my_bopen,     // int (*)(struct block_device *, fmode_t)
+    .release = my_brelease,  // void (*)(struct gendisk *, fmode_t)
 };
 
 // 注册
@@ -1458,6 +1472,8 @@ register_blkdev(240, "myblock");
 // 添加请求队列和处理函数
 ```
 
+> ⚠️ **易错点**:块设备的 `open`/`release` 原型是 `int (*)(struct block_device *, fmode_t)` 与 `void (*)(struct gendisk *, fmode_t)`,**不能**直接套用字符设备的 `open(struct inode *, struct file *)`。
+
 **追问**:
 
 - 追问1:块设备的请求队列(request_queue)是什么?
@@ -1551,5 +1567,7 @@ write_unlock(&rwlock);
 
 ---
 
+**内容来源**:系统调用/VFS、进程调度、内存管理、设备模型等内核源码级事实与 API 已按 Linux 4.1.15 源码核对;内核配置编译、模块加载、内核启动与设备树部分可对应《I.MX6U嵌入式Linux驱动开发指南》第三十五~三十七章及「Linux 设备树」章节。
+
 **最后更新**: 2026-09-17
 **关联页面**: [[面试-Linux内核核心]]

+ 177 - 15
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/01-字符设备驱动基础.md

@@ -6,6 +6,7 @@ updated: 2026-09-17
 pdf_ref:
   - "46_第四十章 字符设备驱动开发.pdf"
   - "47_第四十一章 嵌入式Linux LED驱动开发实验.pdf"
+  - "48_第四十二章 新字符设备驱动实验.pdf"
 ---
 
 # 字符设备驱动基础
@@ -20,6 +21,8 @@ pdf_ref:
 
 ### 1.2 字符设备 vs 块设备 vs 网络设备
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(原书只定义了字符设备)。
+
 | 特性     | 字符设备         | 块设备                 | 网络设备   |
 | -------- | ---------------- | ---------------------- | ---------- |
 | 访问方式 | 字节流,顺序访问 | 块为单位,随机访问     | 数据包传输 |
@@ -100,6 +103,8 @@ sequenceDiagram
 
 ### 2.1 struct file 详解
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书只说明 `filp->private_data` 的用法,并未展开 `struct file` 的完整定义。
+
 `struct file` 代表一个打开的文件,在内核中每当 `open()` 一个设备文件时就会创建一个 `file` 结构体实例。
 
 ```c
@@ -140,6 +145,8 @@ struct file {
 
 ### 2.2 struct inode 详解
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书未给出 `struct inode` 的完整定义。
+
 `struct inode` 代表一个文件系统中的文件/目录,包含文件的元数据信息。每个文件(设备文件)在内核中都有对应的 `inode`。
 
 ```c
@@ -209,6 +216,7 @@ struct file_operations {
     long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
 
     int (*mmap) (struct file *, struct vm_area_struct *);
+    int (*mremap)(struct file *, struct vm_area_struct *);
 
     int (*open) (struct inode *, struct file *);
     int (*flush) (struct file *, fl_owner_t id);
@@ -235,14 +243,19 @@ struct file_operations {
     ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
                            size_t, unsigned int);
 
-    int (*setlease)(struct file *, long, struct file_lock **, void *);
+    int (*setlease)(struct file *, long, struct file_lock **, void **);
 
     long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
 
     void (*show_fdinfo)(struct seq_file *m, struct file *f);
+#ifndef CONFIG_MMU
+    unsigned (*mmap_capabilities)(struct file *);
+#endif
 };
 ```
 
+> 原书(内核 4.1.15 的 `include/linux/fs.h`)中 `mmap_capabilities` 位于 `#ifndef CONFIG_MMU` 条件编译块内,此处照原书保留。
+
 **各字段详细说明:**
 
 | 字段             | 函数原型                                                            | 说明                                             |
@@ -253,13 +266,15 @@ struct file_operations {
 | `write`          | `ssize_t (*)(struct file *, const char __user *, size_t, loff_t *)` | 从用户空间写入数据到设备                         |
 | `poll`           | `unsigned int (*)(struct file *, struct poll_table_struct *)`       | 轮询函数,查询设备是否可以非阻塞读写             |
 | `unlocked_ioctl` | `long (*)(struct file *, unsigned int, unsigned long)`              | 设备控制功能,对应用户空间的 `ioctl()`           |
-| `compat_ioctl`   | `long (*)(struct file *, unsigned int, unsigned long)`              | 32位系统兼容的 ioctl                             |
-| `mmap`           | `int (*)(struct file *, struct vm_area_struct *)`                   | 将设备内存映射到用户空间                         |
+| `compat_ioctl`   | `long (*)(struct file *, unsigned int, unsigned long)`              | 与 `unlocked_ioctl` 功能相同,区别在于 64 位系统上运行 32 位应用程序时会调用此函数;32 位系统上运行 32 位程序调用的仍是 `unlocked_ioctl` |
+| `mmap`           | `int (*)(struct file *, struct vm_area_struct *)`                   | 将设备内存映射到进程空间(用户空间),帧缓冲类设备常用 |
+| `mremap`         | `int (*)(struct file *, struct vm_area_struct *)`                   | 扩展/收缩已有的内存映射                          |
 | `open`           | `int (*)(struct inode *, struct file *)`                            | 打开设备文件                                     |
 | `release`        | `int (*)(struct inode *, struct file *)`                            | 关闭/释放设备文件,对应 `close()`                |
 | `fsync`          | `int (*)(struct file *, loff_t, loff_t, int)`                       | 将缓冲区数据刷新到设备                           |
 | `fasync`         | `int (*)(int, struct file *, int)`                                  | 异步通知                                         |
 
+
 **字符设备驱动中常用的函数:**
 
 - `open` / `release`:打开和关闭设备(必需)
@@ -321,6 +336,30 @@ void device_number_demo(void)
 | 主设备号 | 0 ~ 4095    | 高12位 |
 | 次设备号 | 0 ~ 1048575 | 低20位 |
 
+### 2.5 printk 与日志级别
+
+内核中没有 `printf`,向控制台输出信息必须使用 `printk`。`printk` 与 `printf` 的最大区别是可以指定消息级别,级别定义在 `include/linux/kern_levels.h`:
+
+```c
+#define KERN_SOH     "\001"
+#define KERN_EMERG   KERN_SOH "0"  /* 紧急事件,一般是内核崩溃          */
+#define KERN_ALERT   KERN_SOH "1"  /* 必须立即采取行动                  */
+#define KERN_CRIT    KERN_SOH "2"  /* 临界条件,比如严重的软件或硬件错误 */
+#define KERN_ERR     KERN_SOH "3"  /* 错误状态,一般设备驱动中用 KERN_ERR 报告硬件错误 */
+#define KERN_WARNING KERN_SOH "4"  /* 警告信息,不会对系统造成严重影响   */
+#define KERN_NOTICE  KERN_SOH "5"  /* 有必要进行提示的一些信息           */
+#define KERN_INFO    KERN_SOH "6"  /* 提示性的信息                      */
+#define KERN_DEBUG   KERN_SOH "7"  /* 调试信息                          */
+```
+
+一共 8 个级别,数字越小优先级越高。用法是在消息前加上对应宏:
+
+```c
+printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
+```
+
+若不显式设置级别,`printk` 采用默认级别 `MESSAGE_LOGLEVEL_DEFAULT`(默认为 4)。`include/linux/printk.h` 中的 `CONSOLE_LOGLEVEL_DEFAULT` 控制哪些级别的消息能显示在控制台上,默认为 7,即优先级高于 7 的消息(0~6)才能打印出来。默认级别 4 高于 7,所以直接使用 `printk` 输出的信息可以显示在控制台上。
+
 ---
 
 ## 3. 核心 API 详解
@@ -461,6 +500,44 @@ module_exit(my_exit);
 MODULE_LICENSE("GPL");
 ```
 
+#### 3.1.3 指定设备号注册(register_chrdev_region)
+
+如果已经明确知道要使用的主设备号/次设备号,也可以不使用 `alloc_chrdev_region`,而是用下列函数直接注册一段设备号:
+
+```c
+int register_chrdev_region(dev_t from, unsigned count, const char *name);
+```
+
+| 参数    | 类型           | 说明                             |
+| ------- | -------------- | -------------------------------- |
+| `from`  | `dev_t`        | 要申请的起始设备号(给定的设备号) |
+| `count` | `unsigned`     | 要申请的设备号数量,一般为一个    |
+| `name`  | `const char *` | 设备名字                          |
+
+**返回值:**
+
+- 成功:返回 0
+- 失败:返回负数错误码
+
+不论是 `alloc_chrdev_region` 还是 `register_chrdev_region` 申请的设备号,卸载时统一使用 `unregister_chrdev_region` 释放。
+
+**新字符设备驱动下的设备号分配模板(原书示例代码 42.1.1.1):**
+
+```c
+int major;              /* 主设备号 */
+int minor;              /* 次设备号 */
+dev_t devid;            /* 设备号   */
+
+if (major) {            /* 定义了主设备号 */
+    devid = MKDEV(major, 0);                      /* 大部分驱动次设备号都选择 0 */
+    register_chrdev_region(devid, 1, "test");
+} else {                /* 没有定义设备号 */
+    alloc_chrdev_region(&devid, 0, 1, "test");    /* 申请设备号 */
+    major = MAJOR(devid);                         /* 获取分配的主设备号 */
+    minor = MINOR(devid);                         /* 获取分配的次设备号 */
+}
+```
+
 ### 3.2 cdev 相关 API
 
 `struct cdev` 是内核中字符设备的抽象表示。
@@ -548,6 +625,8 @@ struct class *class_create(struct module *owner, const char *name);
 
 **功能:** 在 `/sys/class/` 目录下创建一个新的 class 目录。
 
+> 原书指出 `class_create` 本身是一个宏,展开后等价于 `struct class *class_create(struct module *owner, const char *name)`,内部调用 `__class_create(owner, name, &__key)`。
+
 #### 3.3.2 device_create
 
 ```c
@@ -575,7 +654,21 @@ struct device *device_create(struct class *cls,
 
 **功能:** 在 `/dev/` 目录下自动创建设备节点。
 
-#### 3.3.3 完整示例
+#### 3.3.3 删除设备与类
+
+卸载驱动时需要删除创建的设备与类,函数原型如下:
+
+```c
+void device_destroy(struct class *class, dev_t devt);
+void class_destroy(struct class *cls);
+```
+
+| 函数              | 参数                                 | 说明                                       |
+| ----------------- | ------------------------------------ | ------------------------------------------ |
+| `device_destroy`  | `class`:设备所处类;`devt`:设备号   | 删除用 `device_create` 创建的设备          |
+| `class_destroy`   | `cls`:要删除的类                     | 删除用 `class_create` 创建的类             |
+
+#### 3.3.4 完整示例
 
 ```c
 #include <linux/types.h>
@@ -733,6 +826,8 @@ static inline long copy_from_user(void *to, const void __user *from, unsigned lo
 
 **功能:** 将用户空间的数据复制到内核空间。
 
+> 💡 `copy_to_user()` / `copy_from_user()` 返回的是**未能复制的字节数**(成功时为 0)。因此判断是否失败通常写作 `if (copy_to_user(...))` 或 `if (retvalue)`,而不是判断返回值是否为负数。
+
 **使用示例:**
 
 ```c
@@ -792,6 +887,16 @@ chrdevbase 是一个虚拟字符设备,包含读缓冲区和写缓冲区,各
 #include <linux/ide.h>
 #include <linux/init.h>
 #include <linux/module.h>
+/***************************************************************
+Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
+文件名  : chrdevbase.c
+作者    : 左忠凯
+版本    : V1.0
+描述    : chrdevbase 驱动文件。
+其他    : 无
+论坛    : www.openedv.com
+日志    : 初版V1.0 2019/1/30 左忠凯创建
+***************************************************************/
 
 #define CHRDEVBASE_MAJOR 200
 #define CHRDEVBASE_NAME "chrdevbase"
@@ -802,7 +907,7 @@ static char kerneldata[] = {"kernel data!"};
 
 static int chrdevbase_open(struct inode *inode, struct file *filp)
 {
-    printk("chrdevbase open!\r\n");
+    //printk("chrdevbase open!\r\n");
     return 0;
 }
 
@@ -819,6 +924,7 @@ static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
         printk("kernel senddata failed!\r\n");
     }
 
+    //printk("chrdevbase read!\r\n");
     return 0;
 }
 
@@ -835,13 +941,14 @@ static ssize_t chrdevbase_write(struct file *filp,
         printk("kernel recevdata failed!\r\n");
     }
 
+    //printk("chrdevbase write!\r\n");
     return 0;
 }
 
 static int chrdevbase_release(struct inode *inode,
                               struct file *filp)
 {
-    printk("chrdevbase release!\r\n");
+    //printk("chrdevbase release!\r\n");
     return 0;
 }
 
@@ -886,7 +993,7 @@ MODULE_AUTHOR("zuozhongkai");
 ```c
 static int chrdevbase_open(struct inode *inode, struct file *filp)
 {
-    printk("chrdevbase open!\r\n");
+    //printk("chrdevbase open!\r\n");
     return 0;
 }
 ```
@@ -919,6 +1026,7 @@ static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
         printk("kernel senddata failed!\r\n");
     }
 
+    //printk("chrdevbase read!\r\n");
     return 0;
 }
 ```
@@ -953,6 +1061,7 @@ static ssize_t chrdevbase_write(struct file *filp,
         printk("kernel recevdata failed!\r\n");
     }
 
+    //printk("chrdevbase write!\r\n");
     return 0;
 }
 ```
@@ -970,7 +1079,7 @@ static ssize_t chrdevbase_write(struct file *filp,
 static int chrdevbase_release(struct inode *inode,
                               struct file *filp)
 {
-    printk("chrdevbase release!\r\n");
+    //printk("chrdevbase release!\r\n");
     return 0;
 }
 ```
@@ -1031,6 +1140,18 @@ static void __exit chrdevbase_exit(void)
 #include "fcntl.h"
 #include "stdlib.h"
 #include "string.h"
+/***************************************************************
+Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
+文件名     : chrdevbaseApp.c
+作者       : 左忠凯
+版本       : V1.0
+描述       : chrdevbase 驱动测试APP。
+其他       : 使用方法:./chrdevbaseApp /dev/chrdevbase <1>|<2>
+             argv[2] 1:读文件
+             argv[2] 2:写文件
+论坛       : www.openedv.com
+日志       : 初版V1.0 2019/1/30 左忠凯创建
+***************************************************************/
 
 static char usrdata[] = {"usr data!"};
 
@@ -1133,37 +1254,46 @@ arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
 # 1. 复制文件到开发板
 sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/
 
-# 2. 加载驱动模块
+# 2. 首次使用 modprobe 前需要生成 modules.dep 等依赖文件
+depmod
+
+# 3. 加载驱动模块
 insmod chrdevbase.ko
 # 或
 modprobe chrdevbase.ko
 
-# 3. 查看已加载模块
+# 4. 查看已加载模块
 lsmod
 
-# 4. 查看设备号
+# 5. 查看设备号
 cat /proc/devices
 
-# 5. 创建设备节点 (首次使用)
+# 6. 创建设备节点 (首次使用)
 mknod /dev/chrdevbase c 200 0
 
-# 6. 读操作测试
+# 7. 读操作测试
 ./chrdevbaseApp /dev/chrdevbase 1
 # 预期输出: read data:kernel data!
 
-# 7. 写操作测试
+# 8. 写操作测试
 ./chrdevbaseApp /dev/chrdevbase 2
 # 预期输出: kernel recevdata:usr data!
 
-# 8. 卸载驱动模块
+# 9. 卸载驱动模块
 rmmod chrdevbase.ko
 ```
 
+> `modprobe` 默认到 `/lib/modules/<内核版本>` 目录查找模块(本书内核为 4.1.15,即 `/lib/modules/4.1.15`),该目录不存在时需自行创建。`insmod` 不解析依赖、也不要求此目录。
+> 使用出厂系统做 LED 实验时,LED 默认被设为心跳灯会一直闪烁,可执行 `echo none > /sys/class/leds/sys-led/trigger` 关闭心跳功能。
+
 ### 5.4 LED 驱动扩展
 
 在 chrdevbase 基础上,LED 驱动需要额外操作硬件寄存器:
 
 ```c
+#define LED_MAJOR   200      /* 主设备号 */
+#define LED_NAME    "led"    /* 设备名字 */
+
 // 寄存器物理地址
 #define CCM_CCGR1_BASE          (0X020C406C)
 #define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)
@@ -1242,10 +1372,36 @@ static void __exit led_exit(void)
 2. **寄存器操作**:使用 `readl()` / `writel()` 读写寄存器
 3. **资源释放**:在出口函数中使用 `iounmap()` 取消映射
 
+**LED 实验步骤(原书第四十一章):**
+
+```bash
+# 1. 编译驱动与测试程序
+make -j32                                # 生成 led.ko
+arm-linux-gnueabihf-gcc ledApp.c -o ledApp
+
+# 2. 拷贝到 rootfs/lib/modules/4.1.15 后,在开发板上执行
+depmod                                   # 第一次加载驱动时需要
+modprobe led.ko
+
+# 3. 创建设备节点
+mknod /dev/led c 200 0
+
+# 4. 测试
+./ledApp /dev/led 1                      # 打开 LED
+./ledApp /dev/led 0                      # 关闭 LED
+
+# 5. 卸载
+rmmod led.ko
+```
+
+> led_switch() 中 `val &= ~(1 << 3)` 控制 GPIO1_DR 的 bit3:写 0 点亮、写 1 熄灭(低电平点亮)。
+
 ---
 
 ## 6. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 6.1 IMX6ULL vs STM32 vs RK3568 字符设备差异
 
 | 特性         | IMX6ULL       | STM32 (Linux) | RK3568       |
@@ -1302,6 +1458,8 @@ graph TD
 
 ## 7. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(面试题整理)。
+
 ### 7.1 什么是字符设备?与块设备有什么区别?
 
 **答案:**
@@ -1411,3 +1569,7 @@ iounmap(vaddr);
 ```
 
 **注意:** 映射后的地址使用 `readl()`/`writel()` 等专用函数访问,不要直接使用指针解引用。
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第四十章 字符设备驱动开发、第四十一章 嵌入式Linux LED驱动开发实验、第四十二章 新字符设备驱动实验

+ 182 - 25
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/02-设备树语法与实战.md

@@ -28,7 +28,7 @@ pdf_ref:
 | DTSI | `.dtsi`  | 设备树头文件,可被 DTS 包含         |
 | DTB  | `.dtb`   | 编译后的二进制文件,bootloader 加载 |
 | DTC  | —        | 设备树编译器,将 DTS 编译为 DTB     |
-| DTBO | `.dtbo`  | 设备树 Overlay,运行时动态叠加      |
+| DTBO | `.dtbo`  | 设备树 Overlay,运行时动态叠加(扩展知识,原书未涉及) |
 
 ### 1.2 为什么需要设备树
 
@@ -268,6 +268,21 @@ spi4 {
 };
 ```
 
+```dts
+/* 地址占 1 cell,长度也占 1 cell */
+aips3: aips-bus@02200000 {
+    compatible = "fsl,aips-bus", "simple-bus";
+    #address-cells = <1>;
+    #size-cells = <1>;
+
+    dcp: dcp@02280000 {
+        compatible = "fsl,imx6sl-dcp";
+        reg = <0x02280000 0x4000>;  /* address=0x02280000, length=0x4000 */
+    };
+};
+```
+
+
 #### `reg` — 寄存器地址范围
 
 `reg` 属性的格式为 `<address length>` 对,描述设备在父节点地址空间中的寄存器基地址和大小。
@@ -295,10 +310,12 @@ alphaled {
 
 | 值           | 含义                 |
 | ------------ | -------------------- |
-| `"okay"`     | 设备可用             |
-| `"disabled"` | 设备不可用(但存在) |
-| `"reserved"` | 设备被占用           |
-| `"fail"`     | 设备初始化失败       |
+| `"okay"`     | 表明设备是可操作的   |
+| `"disabled"` | 表明设备当前不可操作,但未来可以变为可操作(如热插拔设备插入后);具体含义还要看设备的绑定文档 |
+| `"fail"`     | 表明设备不可操作,设备检测到了一系列错误,而且不大可能变得可操作 |
+| `"fail-sss"` | 含义和 `"fail"` 相同,后面的 sss 部分是检测到的错误内容 |
+
+> ⚠️ 上表取自原书表 43.3.3.1。`"reserved"`(设备被占用、由其他软件组件管理)属设备树规范中的扩展取值,原书未列出。
 
 ```dts
 uart1: serial@02020000 {
@@ -427,6 +444,8 @@ U-Boot 在启动 Linux 之前,通过 `fdt_chosen()` 函数将 `bootargs` 环
 
 ## 三、设备树编译与反编译
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书只介绍了 `make dtbs`/`make all` 与 DTC 工具的位置,未涉及 `dtc` 命令行、反编译及设备树 Overlay。
+
 ### 3.1 dtc 命令
 
 DTC(Device Tree Compiler)是设备树的编译器,将文本格式的 DTS 编译为二进制格式的 DTB。
@@ -512,11 +531,21 @@ struct device_node {
     const char *type;           /* 设备类型 */
     phandle phandle;
     const char *full_name;      /* 节点全名 */
-    struct property *properties;/* 属性链表 */
+    struct fwnode_handle fwnode;
+
+    struct property *properties;/* 属性 */
+    struct property *deadprops; /* removed 属性 */
     struct device_node *parent; /* 父节点 */
     struct device_node *child;  /* 子节点 */
-    struct device_node *sibling;/* 兄弟节点 */
+    struct device_node *sibling;
+    struct kobject kobj;
+    unsigned long _flags;
     void *data;
+#if defined(CONFIG_SPARC)
+    const char *path_component_name;
+    unsigned int unique_id;
+    struct of_irq_controller *irq_trans;
+#endif
 };
 ```
 
@@ -524,10 +553,13 @@ struct device_node {
 
 ```c
 struct property {
-    char *name;         /* 属性名字 */
-    int length;         /* 属性长度 */
-    void *value;        /* 属性值 */
+    char *name;            /* 属性名字 */
+    int length;            /* 属性长度 */
+    void *value;           /* 属性值 */
     struct property *next; /* 下一个属性 */
+    unsigned long _flags;
+    unsigned int unique_id;
+    struct bin_attribute attr;
 };
 ```
 
@@ -612,6 +644,42 @@ if (prop)
     printk("compatible = %s\n", (char *)prop->value);
 ```
 
+#### 统计属性元素数量
+
+`of_property_count_elems_of_size` 用于获取属性中元素的数量。比如 reg 属性值是一个数组,用此函数可以拿到数组大小:
+
+```c
+int of_property_count_elems_of_size(const struct device_node *np,
+                                     const char *propname,
+                                     int elem_size);
+```
+
+| 参数        | 类型                         | 说明                   |
+| ----------- | ---------------------------- | ---------------------- |
+| `np`        | `const struct device_node *` | 设备节点               |
+| `propname`  | `const char *`               | 需要统计元素数量的属性名 |
+| `elem_size` | `int`                        | 元素长度               |
+| 返回值      | `int`                        | 得到的属性元素数量     |
+
+#### 按下标读取 u32 数值
+
+`of_property_read_u32_index` 用于从属性中获取指定标号的 u32 类型(无符号 32 位)数据值:
+
+```c
+int of_property_read_u32_index(const struct device_node *np,
+                                const char *propname,
+                                u32 index,
+                                u32 *out_value);
+```
+
+| 参数        | 类型           | 说明                                                             |
+| ----------- | -------------- | ---------------------------------------------------------------- |
+| `np`        | 设备节点       | 设备节点                                                         |
+| `propname`  | `const char *` | 要读取的属性名字                                                 |
+| `index`     | `u32`          | 要读取的值标号                                                   |
+| `out_value` | `u32 *`        | 读取到的值                                                       |
+| 返回值      | `int`          | 0 读取成功,负值读取失败(-EINVAL 属性不存在,-ENODATA 无数据,-EOVERFLOW 属性值列表太小) |
+
 #### 读取整数属性
 
 ```c
@@ -642,7 +710,37 @@ if (ret < 0)
     printk("reg property read failed!\n");
 ```
 
-类似的还有 `of_property_read_u8`、`of_property_read_u16`、`of_property_read_u64` 等单值读取函数。
+#### 读取整数数组属性
+
+除 `of_property_read_u32_array` 外,内核还提供读取 u8/u16/u64 数组的对应变体。多数 reg 属性都是数组数据,可用这四个函数一次读出全部数据:
+
+```c
+int of_property_read_u8_array(const struct device_node *np,
+                              const char *propname, u8 *out_values, size_t sz);
+int of_property_read_u16_array(const struct device_node *np,
+                               const char *propname, u16 *out_values, size_t sz);
+int of_property_read_u32_array(const struct device_node *np,
+                               const char *propname, u32 *out_values, size_t sz);
+int of_property_read_u64_array(const struct device_node *np,
+                               const char *propname, u64 *out_values, size_t sz);
+```
+
+返回值:0 读取成功;负值读取失败(-EINVAL 属性不存在,-ENODATA 无数据,-EOVERFLOW 属性值列表太小)。
+
+#### 读取单个整数值属性
+
+有些属性只有一个整数值,用下列函数读取 u8/u16/u32/u64:
+
+```c
+int of_property_read_u8(const struct device_node *np,
+                        const char *propname, u8 *out_value);
+int of_property_read_u16(const struct device_node *np,
+                         const char *propname, u16 *out_value);
+int of_property_read_u32(const struct device_node *np,
+                         const char *propname, u32 *out_value);
+int of_property_read_u64(const struct device_node *np,
+                         const char *propname, u64 *out_value);
+```
 
 #### 读取字符串属性
 
@@ -704,7 +802,63 @@ void __iomem *base = of_iomap(nd, 0);  /* 映射 reg 属性第 0 段 */
 | 推荐度   | 设备树驱动首选     | 通用但不推荐用于新驱动 |
 | 适用场景 | 有设备树描述的设备 | 所有场景               |
 
-### 4.6 OF 函数返回值汇总
+### 4.6 其他常用 OF 函数
+
+#### of_device_is_compatible
+
+检查节点的 `compatible` 属性中是否包含 `compat` 指定的字符串:
+
+```c
+int of_device_is_compatible(const struct device_node *device,
+                             const char *compat);
+```
+
+| 参数     | 类型                         | 说明               |
+| -------- | ---------------------------- | ------------------ |
+| `device` | `const struct device_node *` | 设备节点           |
+| `compat` | `const char *`               | 要查看的字符串     |
+| 返回值   | `int`                        | 0 表示不包含;正数表示包含 |
+
+#### of_get_named_gpio — 获取 GPIO 编号
+
+将设备树中形如 `<&gpio5 7 GPIO_ACTIVE_LOW>` 的属性信息转换为对应的 GPIO 编号,内核中所有 GPIO API 都使用 GPIO 编号:
+
+```c
+int of_get_named_gpio(struct device_node *np,
+                       const char *propname,
+                       int index);
+```
+
+| 参数       | 类型                   | 说明                                                         |
+| ---------- | ---------------------- | ------------------------------------------------------------ |
+| `np`       | `struct device_node *` | 设备节点                                                     |
+| `propname` | `const char *`         | 包含要获取 GPIO 信息的属性名(如 `"led-gpio"`)              |
+| `index`    | `int`                  | GPIO 索引;一个属性含多个 GPIO 时指定取哪一个,只有一个时为 0 |
+| 返回值     | `int`                  | 正值:获取到的 GPIO 编号;负值:失败                         |
+
+```c
+/* 第四十五章 gpioled 驱动片段 */
+gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
+if (gpioled.led_gpio < 0) {
+    printk("can't get led-gpio");
+    return -EINVAL;
+}
+```
+
+```dts
+/* 对应的设备树属性 */
+led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
+```
+
+#### GPIO_ACTIVE_LOW 的含义
+
+- `GPIO_ACTIVE_LOW`:**低电平有效**。GPIO 输出低电平时对应"有效/激活"状态(例如点亮 LED)。
+- `GPIO_ACTIVE_HIGH`:**高电平有效**。
+- 两者定义在 `include/dt-bindings/gpio/gpio.h` 中,在 `&gpioX` 引用的 cell 里作为第三个值出现,如 `cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;`。
+
+> ⚠️ **来源说明**:本小节(`of_get_named_gpio`、`GPIO_ACTIVE_LOW`)不属于第四十三章、第四十四章的内容,出自同一教材第四十五章《pinctrl和gpio子系统实验》,为扩展知识。
+
+### 4.7 OF 函数返回值汇总
 
 | 返回值       | 含义               |
 | ------------ | ------------------ |
@@ -860,10 +1014,10 @@ static int __init led_init(void)
     /* 1.1 获取 alphaled 节点 */
     dtsled.nd = of_find_node_by_path("/alphaled");
     if (dtsled.nd == NULL) {
-        printk("alphaled node not found!\r\n");
+        printk("alphaled node can not found!\r\n");
         return -EINVAL;
     } else {
-        printk("alphaled node found!\r\n");
+        printk("alphaled node has been found!\r\n");
     }
 
     /* 1.2 获取 compatible 属性 */
@@ -1022,7 +1176,7 @@ ret = of_property_read_string(dtsled.nd, "status", &str);
 printk("status = %s\r\n", str);
 ```
 
-字符串属性用 `of_property_read_string` 读取,返回值直接就是字符串指针。
+字符串属性用 `of_property_read_string` 读取,字符串指针通过 `out_string` 输出参数返回,函数本身返回 `int`(0 成功,负值失败)
 
 #### 通过 of_property_read_u32_array 获取 reg 属性
 
@@ -1082,7 +1236,7 @@ flowchart TD
 make dtbs
 ```
 
-3. 将生成的 `imx6ull-alientek-emmc.dtb` 拷贝到 SD 卡的 boot 分区
+3. 使用新的 `imx6ull-alientek-emmc.dtb` 启动 Linux 内核(如拷贝到 SD 卡 boot 分区替换旧文件)
 
 ### 6.2 验证设备树节点
 
@@ -1114,19 +1268,19 @@ make -j32
 # 编译测试程序
 arm-linux-gnueabihf-gcc ledApp.c -o ledApp
 
-# 拷贝到开发板
-cp dtsled.ko ledApp /home/root/
+# 拷贝到开发板 rootfs/lib/modules/4.1.15 目录
+cp dtsled.ko ledApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/
 
-# 加载驱动
-depmod              # 首次加载需要
-modprobe dtsled     # 加载驱动
+# 加载驱动(驱动模块存放于 /lib/modules/4.1.15)
+depmod                  # 首次加载需要,生成 modules.dep
+modprobe dtsled.ko      # 加载驱动
 
 # 测试
 ./ledApp /dev/dtsled 1     # 点亮 LED
 ./ledApp /dev/dtsled 0     # 关闭 LED
 
 # 卸载驱动
-rmmod dtsled
+rmmod dtsled.ko
 ```
 
 ### 6.4 预期输出
@@ -1134,19 +1288,20 @@ rmmod dtsled
 驱动加载成功后终端输出:
 
 ```
-alphaled node found!
+alphaled node has been found!
 compatible = atkalpha-led
 status = okay
 reg data:
 0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4
 dtsled major=249,minor=0
-dtsled init!
 ```
 
 ---
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568 设备树差异
 
 | 特性           | IMX6ULL                           | STM32MP1                           | RK3568                                      |
@@ -1171,6 +1326,8 @@ dtsled init!
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(面试题整理)。
+
 ### 题目 1:设备树解决了什么问题?
 
 **考察点**:设备树概念理解
@@ -1221,4 +1378,4 @@ dtsled init!
 
 ---
 
-> 参考资料:正点原子 I.MX6U 嵌入式 Linux 驱动开发指南 第四十三章、第四十四章
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第四十三章 Linux设备树、第四十四章 设备树下的LED驱动实验

+ 446 - 72
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/03-pinctrl与gpio子系统.md

@@ -72,34 +72,54 @@ pinctrl(Pin Control)子系统负责:
 #### 2.2.1 pinctrl节点语法
 
 ```dts
-/* 在iomuxc节点下定义pinctrl配置 */
+/* 在iomuxc节点下定义pinctrl配置(原书示例代码 45.1.2.2 / 45.1.2.3) */
 &iomuxc {
     pinctrl-names = "default";  /* 定义状态名称列表 */
     pinctrl-0 = <&pinctrl_hog_1>;  /* default状态使用的配置组 */
 
     imx6ul-evk {
-        /* 定义一个pinctrl配置组 */
+        /* 定义一个pinctrl配置组:热插拔相关PIN */
         pinctrl_hog_1: hoggrp-1 {
             fsl,pins = <
-                /* 宏定义                    电气属性配置值 */
-                MX6UL_PAD_UART1_RTS_B__GPIO1_IO19    0x17059
-                MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
-                MX6UL_PAD_GPIO1_IO09__GPIO1_IO09     0x17059
+                /* 宏定义                                电气属性配置值 */
+                MX6UL_PAD_UART1_RTS_B__GPIO1_IO19      0x17059  /* SD1 CD */
+                MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT   0x17059
+                MX6UL_PAD_GPIO1_IO09__GPIO1_IO09       0x17059
+                MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID   0x13058
+            >;
+        };
+
+        /* flexcan1 外设所使用的 PIN */
+        pinctrl_flexcan1: flexcan1grp {
+            fsl,pins = <
+                MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX     0x1b020
+                MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX     0x1b020
+            >;
+        };
+
+        /* wdog 外设所使用的 PIN */
+        pinctrl_wdog: wdoggrp {
+            fsl,pins = <
+                MX6UL_PAD_LCD_RESET__WDOG1_WDOG_ANY    0x30b0
             >;
         };
     };
 };
 ```
 
+> 同一个外设的 PIN 都组织在 `iomuxc` 下的一个子节点里:`pinctrl_hog_1` 是热插拔相关 PIN,`pinctrl_flexcan1` 是 flexcan1 的 PIN,`pinctrl_wdog` 是看门狗的 PIN。自定义外设需新建自己的子节点。
+
 #### 2.2.2 宏定义详解
 
 以 `MX6UL_PAD_UART1_RTS_B__GPIO1_IO19` 为例:
 
 ```c
-/* 定义在 imx6ull-pinfunc.h 中 */
+/* 定义在 imx6ull-pinfunc.h(由 imx6ul-pinfunc.h 引用)中 */
 #define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19  0x0090 0x031C 0x0000 0x5 0x0
 ```
 
+> 原书示例代码 45.1.2.4 中,以 `MX6UL_PAD_UART1_RTS_B` 开头的宏一共有 **8 个**,分别对应 UART1_RTS_B 这个 PIN 的 8 种复用 IO(UART1_DCE_RTS、UART1_DTE_CTS、ENET1_TX_ER、USDHC1_CD_B、CSI_DATA05、ENET2_1588_EVENT1_OUT、GPIO1_IO19、USDHC2_CD_B)。
+
 **宏的5个组成部分**:
 
 | 序号 | 字段      | 值     | 含义                                |
@@ -110,6 +130,21 @@ pinctrl(Pin Control)子系统负责:
 | 4    | mux_mode  | 0x5    | 复用模式(5=GPIO功能)              |
 | 5    | input_val | 0x0    | 输入选择值                          |
 
+**关于"6 个整数"**:宏本身只展开为 **5 个 u32**(`mux_reg conf_reg input_reg mux_mode input_val`);而在设备树的 `fsl,pins` 属性中,每个引脚条目总计是 **6 个 u32**——在宏的 5 个值之后,紧跟一个由用户自行设置的 `config`(电气属性)值:
+
+```
+< mux_reg  conf_reg  input_reg  mux_mode  input_val  config >
+```
+
+这正是原书示例代码 45.1.2.6 的注释所说的:`Each pin represented in fsl,pins consists of 5 u32 PIN_FUNC_ID and 1 u32 CONFIG, so 24 types in total for each pin.`(`#define FSL_PIN_SIZE 24`,即 6×4 字节)。
+
+以本章 LED 实验使用的引脚为例(`mux_reg`/`conf_reg` 偏移来自原书第四十四章使用的 `SW_MUX_GPIO1_IO03_BASE = 0x020E0068`、`SW_PAD_GPIO1_IO03_BASE = 0x020E02F4`):
+
+```c
+#define MX6UL_PAD_GPIO1_IO03__GPIO1_IO03  0x0068 0x02F4 0x0000 0x5 0x0
+/*                                        mux_reg conf_reg input  mode input */
+```
+
 **寄存器地址计算**:
 
 ```
@@ -124,32 +159,49 @@ conf_reg地址 = 0x020E0000 + 0x031C = 0x020E031C
 
 #### 2.2.3 电气属性值详解
 
-`0x17059` 是pad配置寄存器的值,每个bit的含义:
+宏后面紧跟的数值(如 `0x17059`、`0x13058`、`0x10B0`)就是 **conf_reg** 的值,最终写入 `IOMUXC_SW_PAD_CTL_PAD_xxx` 寄存器,由用户自行设置,用于配置该 IO 的上/下拉、驱动能力和速度等。原书本章只说明该值的用途,未展开位域。
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
 
 ```c
-/* IOMUXC_SW_PAD_CTL_PAD寄存器位定义 */
-bit 0:     SRE     - 压摆率控制 (0: 低速, 1: 高速)
-bit 1-2:   DSE     - 驱动强度 (00: 输出电阻最大, 11: 输出电阻最小)
-bit 3-4:   SPEED   - 速度等级 (00: 50MHz, 01: 100MHz, 10: 100MHz, 11: 200MHz)
-bit 5:     ODE     - 开漏使能 (0: 关闭, 1: 使能)
-bit 6:     PKE     - 上下拉使能 (0: 关闭, 1: 使能)
-bit 7:     PUE     - 上下拉选择 (0: 下拉, 1: 上拉)
-bit 8-9:   LVS     - 电压等级
-bit 10:    HYS     - 滞后使能 (0: 关闭, 1: 使能)
+/* IOMUXC_SW_PAD_CTL_PAD 寄存器位定义(i.MX6ULL) */
+bit 0      : SRE   - 压摆率控制 (0: 低速, 1: 高速)
+bit 5~3    : DSE   - 驱动强度 (3 位,000~111,值越大驱动越强)
+bit 7~6    : SPEED - 速度等级 (00: 50MHz, 01: 100MHz, 10: 100MHz, 11: 200MHz)
+bit 11     : ODE   - 开漏使能 (0: 关闭, 1: 使能)
+bit 12     : PKE   - 上下拉/保持使能 (0: 关闭, 1: 使能)
+bit 13     : PUE   - 上下拉/保持选择 (1: 上下拉, 0: 保持)
+bit 15~14  : PUS   - 上下拉阻值 (00: 100K下拉, 01: 47K上拉, 10: 100K上拉, 11: 22K上拉)
+bit 16     : HYS   - 滞回比较器使能 (0: 关闭, 1: 使能)
 ```
 
-**0x17059的二进制分解**:
+**0x17059 的二进制分解**:
 
 ```
 0x17059 = 0001 0111 0000 0101 1001
 
-bit[0]   = 1  → SRE=1 (高速压摆率)
-bit[1:2] = 00 → DSE=00 (输出电阻最大)
-bit[3:4] = 10 → SPEED=10 (100MHz)
-bit[5]   = 0  → ODE=0 (推挽输出)
-bit[6]   = 1  → PKE=1 (上下拉使能)
-bit[7]   = 0  → PUE=0 (下拉)
-bit[10]  = 1  → HYS=1 (滞后使能)
+bit0     = 1   → SRE=1
+bit5~3   = 011 → DSE=3
+bit7~6   = 01  → SPEED=1
+bit11    = 0   → ODE=0
+bit12    = 1   → PKE=1
+bit13    = 1   → PUE=1
+bit15~14 = 01  → PUS=1 (47K 上拉)
+bit16    = 1   → HYS=1
+```
+
+**0x10B0(本章 LED 使用的电气属性值,原书示例代码 45.4.1.1)的二进制分解**:
+
+```
+0x10B0 = 0001 0000 1011 0000
+
+bit0     = 0   → SRE=0
+bit5~3   = 110 → DSE=6
+bit7~6   = 10  → SPEED=2
+bit11    = 0   → ODE=0
+bit12    = 1   → PKE=1
+bit13    = 0   → PUE=0 (保持)
+bit16    = 0   → HYS=0
 ```
 
 ### 2.3 设备节点引用pinctrl
@@ -189,6 +241,8 @@ gpioled {
 
 ### 2.4 pinctrl驱动API
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ```c
 #include <linux/pinctrl/consumer.h>
 
@@ -351,6 +405,16 @@ int of_get_named_gpio(struct device_node *np,
                       int index);
 ```
 
+**与 GPIO 相关的 OF 函数(原书 45.2.5 逐项核对)**:
+
+| 函数                  | 原型                                                                 | 说明                                                              |
+| --------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
+| `of_gpio_named_count` | `int of_gpio_named_count(struct device_node *np, const char *propname)` | 统计某个属性里定义了几个 GPIO;**空的 GPIO 也会被统计到**          |
+| `of_gpio_count`       | `int of_gpio_count(struct device_node *np)`                          | 统计 `gpios` 属性里的 GPIO 数量(只能统计 "gpios")               |
+| `of_get_named_gpio`   | `int of_get_named_gpio(struct device_node *np, const char *propname, int index)` | 把 `<&gpio5 7 GPIO_ACTIVE_LOW>` 这类属性值转换为内核 GPIO 编号 |
+
+> 原书说明:`gpio_get_value` / `gpio_set_value` 在书中是宏定义,分别展开为 `int __gpio_get_value(unsigned gpio)` 与 `void __gpio_set_value(unsigned gpio, int value)`,使用时传的是 GPIO 编号。
+
 ### 3.4 GPIO使用示例
 
 #### 方式一:传统方式(已废弃)
@@ -420,6 +484,8 @@ static int led_driver_probe(struct platform_device *pdev)
 
 ### 3.5 新旧API对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性       | 旧API (gpio_*)        | 新API (gpiod_*)  |
 | ---------- | --------------------- | ---------------- |
 | 资源管理   | 手动gpio_request/free | devm自动管理     |
@@ -436,19 +502,27 @@ static int led_driver_probe(struct platform_device *pdev)
 ### 4.1 pinctrl驱动框架
 
 ```c
-/* imx6ull pinctrl驱动入口 */
+/* 原书示例代码 45.1.2.5 pinctrl-imx6ul.c 片段 */
+static struct of_device_id imx6ul_pinctrl_of_match[] = {
+    { .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, },
+    { .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, },
+    { /* sentinel */ }
+};
+
 static int imx6ul_pinctrl_probe(struct platform_device *pdev)
 {
+    const struct of_device_id *match;
     struct imx_pinctrl_soc_info *pinctrl_info;
 
-    /* 获取匹配的私有数据 */
+    match = of_match_device(imx6ul_pinctrl_of_match, &pdev->dev);
+    if (!match)
+        return -ENODEV;
+
     pinctrl_info = (struct imx_pinctrl_soc_info *) match->data;
 
-    /* 调用通用探测函数 */
     return imx_pinctrl_probe(pdev, pinctrl_info);
 }
 
-/* 平台驱动结构体 */
 static struct platform_driver imx6ul_pinctrl_driver = {
     .driver = {
         .name = "imx6ul-pinctrl",
@@ -456,13 +530,33 @@ static struct platform_driver imx6ul_pinctrl_driver = {
         .of_match_table = of_match_ptr(imx6ul_pinctrl_of_match),
     },
     .probe = imx6ul_pinctrl_probe,
+    .remove = imx_pinctrl_remove,
 };
 ```
 
+**函数调用链(原书图 45.1.2.3)**:
+
+```mermaid
+graph TD
+    A[imx6ul_pinctrl_probe] --> B[imx_pinctrl_probe]
+    B --> C[imx_pinctrl_probe_dt]
+    C --> D[imx_pinctrl_parse_functions]
+    D --> E[imx_pinctrl_parse_groups]
+    E --> F["解析设备树PIN信息<br/>mux_reg/conf_reg/input_reg/mux_mode/input_val/config"]
+    B --> G["pinctrl_register()<br/>向内核注册pinctrl"]
+```
+
 ### 4.2 设备树解析流程
 
 ```c
-/* 解析pinctrl groups */
+/* 原书示例代码 45.1.2.6 imx_pinctrl_parse_groups 函数代码段 */
+/*
+ * Each pin represented in fsl,pins consists of 5 u32 PIN_FUNC_ID
+ * and 1 u32 CONFIG, so 24 types in total for each pin.
+ */
+#define FSL_PIN_SIZE 24
+#define SHARE_FSL_PIN_SIZE 20
+
 static int imx_pinctrl_parse_groups(struct device_node *np,
                     struct imx_pin_group *grp,
                     struct imx_pinctrl_soc_info *info,
@@ -472,60 +566,331 @@ static int imx_pinctrl_parse_groups(struct device_node *np,
     const __be32 *list;
     int i;
     u32 config;
-
-    /* 读取fsl,pins属性 */
-    list = of_property_read_var_array(np, "fsl,pins", &size);
-
-    /* 解析每个pin的配置 */
-    for (i = 0; i < size; i += pin_size) {
-        /* 提取mux_reg, conf_reg, input_reg, mux_mode, input_val */
-        grp->pins[i] = be32_to_cpu(list[i]);      /* mux_reg */
-        grp->pins[i+1] = be32_to_cpu(list[i+1]);  /* conf_reg */
-        grp->pins[i+2] = be32_to_cpu(list[i+2]);  /* input_reg */
-        grp->pins[i+3] = be32_to_cpu(list[i+3]);  /* mux_mode */
-        grp->pins[i+4] = be32_to_cpu(list[i+4]);  /* input_val */
-
-        /* 读取config值(电气属性) */
-        config = be32_to_cpu(list[i+5]);
-        grp->configs[i/6] = config;
+    ......
+    for (i = 0; i < grp->npins; i++) {
+        u32 mux_reg = be32_to_cpu(*list++);
+        u32 conf_reg;
+        unsigned int pin_id;
+        struct imx_pin_reg *pin_reg;
+        struct imx_pin *pin = &grp->pins[i];
+        ......
+        pin_id = (mux_reg != -1) ? mux_reg / 4 : conf_reg / 4;
+        pin_reg = &info->pin_regs[pin_id];
+        pin->pin = pin_id;
+        grp->pin_ids[i] = pin_id;
+        pin_reg->mux_reg = mux_reg;
+        pin_reg->conf_reg = conf_reg;
+        pin->input_reg = be32_to_cpu(*list++);
+        pin->mux_mode = be32_to_cpu(*list++);
+        pin->input_val = be32_to_cpu(*list++);
+
+        /* SION bit is in mux register */
+        config = be32_to_cpu(*list++);
+        if (config & IMX_PAD_SION)
+            pin->mux_mode |= IOMUXC_CONFIG_SION;
+        pin->config = config & ~IMX_PAD_SION;
+        ......
     }
 
     return 0;
 }
 ```
 
+> 原书说明:`mux_reg` 与 `conf_reg` 保存在 `info` 参数中(`pin_reg->mux_reg` / `pin_reg->conf_reg`),`input_reg`、`mux_mode`、`input_val` 与 `config` 保存在 `grp` 参数中(`pin->...`)。
+
 ### 4.3 pinctrl注册流程
 
 ```c
-/* 注册pinctrl控制器 */
-int pinctrl_register(struct pinctrl_desc *pctldesc,
-                     struct device *dev,
-                     struct pinctrl_handle *handle)
+/* 注册 pinctrl 控制器(原书给出的原型) */
+struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
+                                     struct device *dev,
+                                     void *driver_data);
+```
+
+`pctldesc` 非常重要,它就是要注册的 PIN 控制器描述符,用于配置 SOC 的 PIN 复用功能和电气特性(原书示例代码 45.1.2.7):
+
+```c
+struct pinctrl_desc {
+    const char *name;
+    struct pinctrl_pin_desc const *pins;
+    unsigned int npins;
+    const struct pinctrl_ops *pctlops;
+    const struct pinmux_ops *pmxops;
+    const struct pinconf_ops *confops;
+    struct module *owner;
+#ifdef CONFIG_GENERIC_PINCONF
+    unsigned int num_custom_params;
+    const struct pinconf_generic_params *custom_params;
+    const struct pin_config_item *custom_conf_items;
+#endif
+};
+```
+
+其中 `pctlops`、`pmxops`、`confops` 三个 `_ops` 结构体是 PIN 控制器的"工具",分别对应 I.MX6ULL 的 `imx_pctrl_ops`、`imx_pmx_ops` 和 `imx_pinconf_ops`(原书示例代码 45.1.2.9),由半导体厂商在内核源码中提供:
+
+```c
+/* 原书示例代码 45.1.2.8 imx_pinctrl_probe 函数代码段 */
+imx_pinctrl_desc->name = dev_name(&pdev->dev);
+imx_pinctrl_desc->pins = info->pins;
+imx_pinctrl_desc->npins = info->npins;
+imx_pinctrl_desc->pctlops = &imx_pctrl_ops;
+imx_pinctrl_desc->pmxops = &imx_pmx_ops;
+imx_pinctrl_desc->confops = &imx_pinconf_ops;
+imx_pinctrl_desc->owner = THIS_MODULE;
+......
+ipctl->pctl = pinctrl_register(imx_pinctrl_desc, &pdev->dev, ipctl);
+```
+
+---
+
+## 五、实验:用pinctrl和gpio子系统驱动LED(原书 45.1.3、45.2.4、45.4、45.5)
+
+本节为原书第四十五章的完整实验流程,例程路径:`开发板光盘-> 2、Linux驱动例程-> 5_gpioled`。
+
+### 5.1 修改设备树
+
+**① 添加 pinctrl 节点**(在 `iomuxc` 的 `imx6ul-evk` 子节点下,原书示例代码 45.4.1.1):
+
+```dts
+pinctrl_led: ledgrp {
+    fsl,pins = <
+        MX6UL_PAD_GPIO1_IO03__GPIO1_IO03    0x10B0 /* LED0 */
+    >;
+};
+```
+
+**② 添加 LED 设备节点**(在根节点 `/` 下,原书示例代码 45.4.1.2):
+
+```dts
+gpioled {
+    #address-cells = <1>;
+    #size-cells = <1>;
+    compatible = "atkalpha-gpioled";
+    pinctrl-names = "default";
+    pinctrl-0 = <&pinctrl_led>;
+    led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
+    status = "okay";
+};
+```
+
+**③ 检查 PIN 是否被其他外设占用**(原书一再强调的重点):
+
+- `pinctrl_tsc` 节点中默认把 GPIO1_IO03 配成了 TSC 的 PIN,需要注释掉:
+
+```dts
+pinctrl_tsc: tscgrp {
+    fsl,pins = <
+        MX6UL_PAD_GPIO1_IO01__GPIO1_IO01    0xb0
+        MX6UL_PAD_GPIO1_IO02__GPIO1_IO02    0xb0
+        MX6UL_PAD_GPIO1_IO03__GPIO1_IO03    0xb0   /* 屏蔽掉 */
+        MX6UL_PAD_GPIO1_IO04__GPIO1_IO04    0xb0
+    >;
+};
+```
+
+- 再搜索 `gpio1 3`,把其他使用该 GPIO 的外设也屏蔽掉(例如 `tsc` 节点的 `xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;`)。
+
+修改完成后 `make dtbs` 重新编译设备树,用新的 `imx6ull-alientek-emmc.dtb` 启动,进入 `/proc/device-tree` 检查 `gpioled` 节点是否存在。
+
+### 5.2 驱动程序 gpioled.c(原书示例代码 45.4.2.1)
+
+```c
+#include <linux/types.h>
+#include <linux/kernel.h>
+#include <linux/delay.h>
+#include <linux/ide.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/gpio.h>
+#include <linux/cdev.h>
+#include <linux/device.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/of_gpio.h>
+#include <asm/mach/map.h>
+#include <asm/uaccess.h>
+#include <asm/io.h>
+
+#define GPIOLED_CNT      1            /* 设备号个数 */
+#define GPIOLED_NAME     "gpioled"   /* 名字 */
+#define LEDOFF           0           /* 关灯 */
+#define LEDON            1           /* 开灯 */
+
+/* gpioled 设备结构体 */
+struct gpioled_dev {
+    dev_t devid;              /* 设备号 */
+    struct cdev cdev;         /* cdev */
+    struct class *class;      /* 类 */
+    struct device *device;    /* 设备 */
+    int major;                /* 主设备号 */
+    int minor;                /* 次设备号 */
+    struct device_node *nd;   /* 设备节点 */
+    int led_gpio;             /* led 所使用的 GPIO 编号 */
+};
+
+struct gpioled_dev gpioled; /* led 设备 */
+
+static int led_open(struct inode *inode, struct file *filp)
+{
+    filp->private_data = &gpioled; /* 设置私有数据 */
+    return 0;
+}
+
+static ssize_t led_read(struct file *filp, char __user *buf,
+                        size_t cnt, loff_t *offt)
+{
+    return 0;
+}
+
+static ssize_t led_write(struct file *filp, const char __user *buf,
+                         size_t cnt, loff_t *offt)
+{
+    int retvalue;
+    unsigned char databuf[1];
+    unsigned char ledstat;
+    struct gpioled_dev *dev = filp->private_data;
+
+    retvalue = copy_from_user(databuf, buf, cnt);
+    if (retvalue < 0) {
+        printk("kernel write failed!\r\n");
+        return -EFAULT;
+    }
+
+    ledstat = databuf[0];           /* 获取状态值 */
+
+    if (ledstat == LEDON) {
+        gpio_set_value(dev->led_gpio, 0);   /* 打开 LED 灯 */
+    } else if (ledstat == LEDOFF) {
+        gpio_set_value(dev->led_gpio, 1);   /* 关闭 LED 灯 */
+    }
+    return 0;
+}
+
+static int led_release(struct inode *inode, struct file *filp)
+{
+    return 0;
+}
+
+/* 设备操作函数 */
+static struct file_operations gpioled_fops = {
+    .owner = THIS_MODULE,
+    .open = led_open,
+    .read = led_read,
+    .write = led_write,
+    .release = led_release,
+};
+
+/* 驱动入口函数 */
+static int __init led_init(void)
 {
-    struct pinctrl_dev *pctldev;
+    int ret = 0;
+
+    /* 1、获取设备节点:gpioled */
+    gpioled.nd = of_find_node_by_path("/gpioled");
+    if (gpioled.nd == NULL) {
+        printk("gpioled node cant not found!\r\n");
+        return -EINVAL;
+    } else {
+        printk("gpioled node has been found!\r\n");
+    }
+
+    /* 2、获取设备树中的 gpio 属性,得到 LED 所使用的 GPIO 编号 */
+    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
+    if (gpioled.led_gpio < 0) {
+        printk("can't get led-gpio");
+        return -EINVAL;
+    }
+    printk("led-gpio num = %d\r\n", gpioled.led_gpio);
+
+    /* 3、设置 GPIO1_IO03 为输出,默认高电平,默认关闭 LED 灯 */
+    ret = gpio_direction_output(gpioled.led_gpio, 1);
+    if (ret < 0) {
+        printk("can't set gpio!\r\n");
+    }
 
-    /* 分配pinctrl_dev结构体 */
-    pctldev = kzalloc(sizeof(*pctldev), GFP_KERNEL);
+    /* 注册字符设备驱动 */
+    /* 1、创建设备号 */
+    if (gpioled.major) {            /* 定义了设备号 */
+        gpioled.devid = MKDEV(gpioled.major, 0);
+        register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
+    } else {                        /* 没有定义设备号 */
+        alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
+        gpioled.major = MAJOR(gpioled.devid);
+        gpioled.minor = MINOR(gpioled.devid);
+    }
+    printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);
 
-    /* 初始化 */
-    pctldev->desc = pctldesc;
-    pctldev->dev = dev;
+    /* 2、初始化 cdev */
+    gpioled.cdev.owner = THIS_MODULE;
+    cdev_init(&gpioled.cdev, &gpioled_fops);
 
-    /* 添加到全局链表 */
-    list_add_tail(&pctldev->node, &pinctrldev_list);
+    /* 3、添加一个 cdev */
+    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
 
-    /* 创建sysfs属性 */
-    ret = device_register(&pctldev->dev);
+    /* 4、创建类 */
+    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
+    if (IS_ERR(gpioled.class)) {
+        return PTR_ERR(gpioled.class);
+    }
 
+    /* 5、创建设备 */
+    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid,
+                                   NULL, GPIOLED_NAME);
+    if (IS_ERR(gpioled.device)) {
+        return PTR_ERR(gpioled.device);
+    }
     return 0;
 }
+
+/* 驱动出口函数 */
+static void __exit led_exit(void)
+{
+    cdev_del(&gpioled.cdev);                              /* 删除 cdev */
+    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); /* 注销 */
+
+    device_destroy(gpioled.class, gpioled.devid);
+    class_destroy(gpioled.class);
+}
+
+module_init(led_init);
+module_exit(led_exit);
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("zuozhongkai");
+```
+
+与原书第四十四章的 `dtsled.c` 相比,这里**取消了直接配置寄存器的过程**,改用 `of_get_named_gpio` + `gpio_direction_output` + `gpio_set_value` 等内核 API。
+
+### 5.3 测试 APP
+
+直接复用第四十二章的 `ledApp.c`,无需修改。
+
+### 5.4 编译与运行测试
+
+```bash
+# 编译驱动模块(Makefile 中 obj-m := gpioled.o)
+make -j32                      # 生成 gpioled.ko
+
+# 编译测试 APP
+arm-linux-gnueabihf-gcc ledApp.c -o ledApp
+
+# 拷贝到开发板 rootfs/lib/modules/4.1.15/ 后,在开发板上:
+depmod                         # 第一次加载驱动时需要执行
+modprobe gpioled.ko            # 加载驱动,打印 led-gpio num = 3
+
+./ledApp /dev/gpioled 1        # 打开 LED 灯
+./ledApp /dev/gpioled 0        # 关闭 LED 灯
+
+rmmod gpioled.ko               # 卸载驱动
 ```
 
 ---
 
-## 五、调试方法
+## 六、调试方法
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
 
-### 5.1 查看GPIO状态
+### 6.1 查看GPIO状态
 
 ```bash
 # 查看所有GPIO状态
@@ -541,7 +906,7 @@ cat /sys/class/gpio/gpio3/direction
 cat /sys/class/gpio/gpio3/value
 ```
 
-### 5.2 查看pinctrl状态
+### 6.2 查看pinctrl状态
 
 ```bash
 # 查看设备的pinctrl状态
@@ -551,7 +916,7 @@ cat /sys/kernel/debug/pinctrl/*/pins
 cat /sys/kernel/debug/pinctrl/pinctrl-maps
 ```
 
-### 5.3 设备树验证
+### 6.3 设备树验证
 
 ```bash
 # 编译设备树
@@ -566,9 +931,11 @@ ls /proc/device-tree/gpioled/
 
 ---
 
-## 、避坑指南
+## 、避坑指南
 
-### 6.1 常见错误及解决方案
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
+### 7.1 常见错误及解决方案
 
 | 错误                              | 原因                   | 解决方案                   |
 | --------------------------------- | ---------------------- | -------------------------- |
@@ -577,7 +944,7 @@ ls /proc/device-tree/gpioled/
 | `of_get_named_gpio: failed`       | 属性名错误或节点不存在 | 检查设备树节点路径和属性名 |
 | GPIO输出电平不对                  | active-low配置错误     | 检查GPIO_ACTIVE_LOW标志    |
 
-### 6.2 引脚冲突排查
+### 7.2 引脚冲突排查
 
 ```bash
 # 方法1:搜索设备树中所有使用该引脚的节点
@@ -590,7 +957,7 @@ cat /proc/device-tree/soc/aips-bus@02000000/iomuxc@020e0000/pinctrl_hog_1/fsl,pi
 devmem2 0x020E0090 w
 ```
 
-### 6.3 调试技巧
+### 7.3 调试技巧
 
 ```c
 /* 在驱动中添加调试信息 */
@@ -616,7 +983,9 @@ static int my_driver_probe(struct platform_device *pdev)
 
 ---
 
-## 七、面试精选
+## 八、面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
 
 ### Q1: pinctrl和gpio子系统的区别是什么?
 
@@ -707,7 +1076,9 @@ led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
 
 ---
 
-## 八、跨平台对比
+## 九、跨平台对比
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
 
 ### IMX6ULL vs STM32 vs RK3568
 
@@ -738,11 +1109,12 @@ pinctrl-0 = <&led_pin>;
 
 ---
 
-## 、参考资料
+## 、参考资料
 
 ### PDF原文
 
-- 第四十五章 pinctrl和gpio子系统实验 (P1162-1194)
+- 第四十五章 pinctrl和gpio子系统实验 (P1162-1193)
+- 第四十四章 设备树下的LED驱动实验 (P1131-1161,`SW_MUX_GPIO1_IO03_BASE` / `SW_PAD_GPIO1_IO03_BASE`)
 
 ### 内核源码
 
@@ -758,3 +1130,5 @@ pinctrl-0 = <&led_pin>;
 ---
 
 > **最后更新**: 2026-09-17 | **版本**: v2.0
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第45章 pinctrl和gpio子系统实验(部分实验流程补充自第44章 设备树下的LED驱动实验)

+ 784 - 53
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/04-并发同步与原子操作.md

@@ -1,16 +1,15 @@
 ---
 title: 并发同步与原子操作
-tags: [Linux驱动, 并发, 锁, 原子操作, 嵌入式, 自旋锁, 信号量, 互斥体]
+tags: [Linux驱动, 并发, 锁, 原子操作, 嵌入式, 自旋锁, 信号量, 互斥体, 读写锁, 顺序锁, RCU]
 created: 2026-09-16
 updated: 2026-09-17
 pdf_ref:
   - "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十七章 Linux并发与竞争"
   - "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十八章 Linux并发与竞争实验"
 ---
-
 # 并发同步与原子操作
 
-> 💡 **关联知识**: [[FreeRTOS学习笔记/07-中断管理与临界段]] | [[03-Linux驱动开发核心/05-中断下半部机制]] | [[03-Linux驱动开发核心/06-定时器与中断]]
+> 💡 **关联知识**: [[FreeRTOS学习笔记/07-中断管理与临界段]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/07-platform总线模型]]
 
 ---
 
@@ -43,8 +42,8 @@ sequenceDiagram
 
 ### 1.2 竞态条件产生场景
 
-| 场景              | 说明                                                 | 危害程度   |
-| ----------------- | ---------------------------------------------------- | ---------- |
+| 场景                    | 说明                                                 | 危害程度   |
+| ----------------------- | ---------------------------------------------------- | ---------- |
 | **中断与进程**    | 进程访问共享数据时被中断,中断处理函数也访问同一数据 | ⭐⭐⭐⭐   |
 | **多进程/多线程** | 多个进程或线程同时读写共享内存                       | ⭐⭐⭐     |
 | **多CPU(SMP)**  | 多核处理器上多个CPU核心同时执行访问同一数据          | ⭐⭐⭐⭐⭐ |
@@ -91,6 +90,23 @@ Linux是多任务操作系统,以下因素导致并发不可避免:
 
 > ⚠️ **注意**:编写驱动程序时必须考虑并发保护,否则会导致难以定位的bug。
 
+### 1.6 Linux并发控制机制总览
+
+Linux内核提供的同步机制从"轻"到"重"排列如下,选择哪一种是驱动开发中最基础的判断:
+
+| 机制               | 典型类型              | 是否可睡眠 | 中断上下文             | 适用场景                   |
+| ------------------ | --------------------- | ---------- | ---------------------- | -------------------------- |
+| **原子操作** | `atomic_t`          | 否         | 可用                   | 单个整数计数、状态标志     |
+| **自旋锁**   | `spinlock_t`        | 否         | 可用                   | 极短临界区、中断上下文     |
+| **读写锁**   | `rwlock_t`          | 否         | 可用                   | 读多写少、临界区短         |
+| **顺序锁**   | `seqlock_t`         | 否         | 可用(写者不可被中断) | 读多写极少、读侧不能加锁   |
+| **RCU**      | `rcu_read_lock`     | 否         | 读侧可用               | 链表遍历为主、读多写极少   |
+| **信号量**   | `struct semaphore`  | 是         | 不可用                 | 长临界区、允许并发数 > 1   |
+| **互斥体**   | `struct mutex`      | 是         | 不可用                 | 长临界区、需要互斥且可睡眠 |
+| **完成量**   | `struct completion` | 是         | 不可用                 | 一个执行单元等待另一个完成 |
+
+> 💡 **选择原则**:能不用锁就不用锁(无锁设计优先,如 per-CPU 变量),必须加锁时按"临界区长度 + 是否可能睡眠 + 读多还是写多"三个维度决策。本文档按此顺序逐个讲解。
+
 ---
 
 ## 二、原子操作
@@ -108,8 +124,8 @@ typedef struct {
 
 ### 2.2 原子变量操作API
 
-| 函数                                          | 说明                   |
-| --------------------------------------------- | ---------------------- |
+| 函数                                            | 说明                   |
+| ----------------------------------------------- | ---------------------- |
 | `ATOMIC_INIT(int i)`                          | 定义原子变量时初始化值 |
 | `int atomic_read(atomic_t *v)`                | 读取原子变量的值       |
 | `void atomic_set(atomic_t *v, int i)`         | 设置原子变量的值       |
@@ -153,8 +169,8 @@ static int led_release(struct inode *inode, struct file *filp)
 
 ### 2.4 原子位操作API
 
-| 函数                                      | 说明                 |
-| ----------------------------------------- | -------------------- |
+| 函数                                        | 说明                 |
+| ------------------------------------------- | -------------------- |
 | `void set_bit(int nr, void *p)`           | 设置第nr位为1        |
 | `void clear_bit(int nr, void *p)`         | 清除第nr位           |
 | `void change_bit(int nr, void *p)`        | 反转第nr位           |
@@ -202,8 +218,8 @@ typedef struct spinlock {
 
 ### 3.3 自旋锁API
 
-| 函数                                    | 说明                  | 使用场景     |
-| --------------------------------------- | --------------------- | ------------ |
+| 函数                                      | 说明                  | 使用场景     |
+| ----------------------------------------- | --------------------- | ------------ |
 | `DEFINE_SPINLOCK(spinlock_t lock)`      | 静态定义并初始化      | 全局锁       |
 | `void spin_lock_init(spinlock_t *lock)` | 动态初始化            | 运行时初始化 |
 | `void spin_lock(spinlock_t *lock)`      | 获取锁(忙等待)      | 进程上下文   |
@@ -213,8 +229,8 @@ typedef struct spinlock {
 
 ### 3.4 中断安全的自旋锁API
 
-| 函数                                                                 | 说明                           |
-| -------------------------------------------------------------------- | ------------------------------ |
+| 函数                                                                   | 说明                           |
+| ---------------------------------------------------------------------- | ------------------------------ |
 | `void spin_lock_irq(spinlock_t *lock)`                               | 禁止本地中断并获取锁           |
 | `void spin_unlock_irq(spinlock_t *lock)`                             | 释放锁并开启本地中断           |
 | `void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)`      | 保存中断状态、禁止中断、获取锁 |
@@ -244,13 +260,16 @@ flowchart TD
     style G fill:#dbeafe,stroke:#2563eb
 ```
 
-### 3.6 自旋锁注意事项
+### 3.6 自旋锁使用注意事项
 
-1. **临界区要尽可能短**:自旋锁会忙等待,占用CPU资源
-2. **不能在持有自旋锁时睡眠**:会导致死锁
-3. **不能递归获取**:同一CPU不能多次获取同一把锁
-4. **考虑中断安全**:如果临界区可能被中断访问,必须使用`spin_lock_irqsave`
-5. **不能在中断处理函数中使用可能睡眠的API**
+综合前面关于自旋锁的信息,我们需要在使用自旋锁的时候要注意以下几点(原书 47.3.4):
+
+1. **锁的持有时间不能太长,一定要短**:因为在等待自旋锁的时候处于"自旋"状态,会降低系统性能。如果临界区比较大、运行时间比较长的话,要选择其他的并发处理方式,比如信号量和互斥体。
+2. **自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数**:否则可能导致死锁。
+3. **不能递归申请自旋锁**:因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须"自旋",等待锁被释放,然而你正处于"自旋"状态,根本没法释放锁。结果就是自己把自己锁死了!
+4. **在编写驱动程序时必须考虑驱动的可移植性**:因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。
+
+> 💡 补充:若临界区可能被中断访问,需使用 `spin_lock_irqsave` / `spin_unlock_irqrestore` 保证中断安全(原书 47.3.2 自旋锁 API 中的 `_irqsave` 变体)。
 
 ---
 
@@ -323,13 +342,13 @@ static ssize_t led_write(struct file *filp, const char __user *buf,
 
 ### 4.4 自旋锁 vs 信号量对比表
 
-| 特性           | 自旋锁             | 信号量              |
-| -------------- | ------------------ | ------------------- |
+| 特性                 | 自旋锁             | 信号量              |
+| -------------------- | ------------------ | ------------------- |
 | **等待方式**   | 忙等待(自旋)     | 睡眠等待            |
 | **CPU占用**    | 占用CPU            | 不占用CPU           |
 | **可睡眠**     | ❌ 不可以          | ✅ 可以             |
 | **中断上下文** | ✅ 可以使用        | ❌ 不可以           |
-| **临界区长度** | 短(μs级)         | 长(ms级)          |
+| **临界区长度** | 短(μs级)        | 长(ms级)          |
 | **性能开销**   | 低(忙等)         | 高(上下文切换)    |
 | **适用场景**   | 中断处理、短临界区 | 长时间操作、I/O操作 |
 
@@ -400,20 +419,662 @@ static ssize_t led_write(struct file *filp, const char __user *buf,
 
 ### 5.4 互斥体 vs 自旋锁对比
 
-| 特性           | 互斥体      | 自旋锁      |
-| -------------- | ----------- | ----------- |
+| 特性                 | 互斥体      | 自旋锁      |
+| -------------------- | ----------- | ----------- |
 | **睡眠**       | ✅ 可以睡眠 | ❌ 不能睡眠 |
 | **持有时长**   | 可以较长    | 必须很短    |
 | **中断上下文** | ❌ 不能使用 | ✅ 可以使用 |
 | **优先级继承** | ✅ 支持     | ❌ 不支持   |
 | **递归锁**     | ❌ 不支持   | ❌ 不支持   |
-| **调试支持**   | ✅ 丰富     | ⚠️ 有限     |
+| **调试支持**   | ✅ 丰富     | ⚠️ 有限   |
+
+---
+
+## 六、读写自旋锁(rwlock)
+
+> 本节内容来自《I.MX6U嵌入式Linux驱动开发指南》**47.3.3 其他类型的锁**。原书指出:这些锁**在驱动中其实用的不多,更多的是在 Linux 内核中使用**。
+
+### 6.1 什么是读写自旋锁
+
+**原书背景**:现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被修改和读取。此表肯定是数据,那么必须要对其进行保护。如果我们现在使用自旋锁对其进行保护,每次只能一个读操作或者写操作。但是,实际上此表是可以**并发读取**的,只需要保证在修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的**读和写不能同时进行,但是可以多人并发的读取此表**。像这样,当某个数据结构符合**读/写**或**生产者/消费者**模型的时候就可以使用读写自旋锁。
+
+**核心规则**:
+
+- **一次只能允许一个写操作**:也就是只能一个线程持有写锁,而且不能进行读操作
+- **没有写操作时允许一个或多个线程持有读锁**:可以进行并发的读操作
+
+```mermaid
+flowchart TD
+    A[读写锁状态] --> B{请求类型}
+    B -->|读锁| C{当前有写者?}
+    B -->|写锁| D{当前有任何持有者?}
+    C -->|无写者| E[✅ 获取成功<br/>可多个读者共存]
+    C -->|有写者| F[⏳ 自旋等待]
+    D -->|无| G[✅ 获取成功<br/>排他]
+    D -->|有读者或写者| H[⏳ 自旋等待]
+
+    style E fill:#dcfce7,stroke:#16a34a
+    style G fill:#dcfce7,stroke:#16a34a
+    style F fill:#fef3c7,stroke:#f59e0b
+    style H fill:#fef3c7,stroke:#f59e0b
+```
+
+> 💡 **一句话总结**:读写锁 = "允许 N 个读者 + 1 个写者,读者和写者互斥"。
+
+### 6.2 读写锁类型定义
+
+Linux 内核使用 `rwlock_t` 结构体表示读写锁,结构体定义如下(原书示例**删除了条件编译**):
+
+```c
+/* 示例代码 47.3.3.1 rwlock_t 结构体 */
+typedef struct {
+    arch_rwlock_t raw_lock;
+} rwlock_t;
+```
+
+完整的内核定义(`include/linux/rwlock_types.h`)在开启调试选项时会附加调试字段:
+
+```c
+typedef struct {
+    arch_rwlock_t raw_lock;
+#ifdef CONFIG_DEBUG_SPINLOCK
+    unsigned int magic, owner_cpu;
+    void *owner;
+#endif
+#ifdef CONFIG_DEBUG_LOCK_ALLOC
+    struct lockdep_map dep_map;
+#endif
+} rwlock_t;
+```
+
+`rwlock_t` 的本质与 `spinlock_t` 相同,都是基于架构相关的原子指令实现(ARM 上使用 `LDREX/STREX`),区别在于内部额外维护了读者计数和写者标志。
+
+### 6.3 读写锁 API
+
+```c
+#include <linux/rwlock.h>
+
+/* 初始化方式一:动态初始化 */
+rwlock_t my_rwlock;
+rwlock_init(&my_rwlock);
+
+/* 初始化方式二:静态初始化 */
+DEFINE_RWLOCK(my_rwlock);
+```
+
+**读锁 API**(共享)——原书表 47.3.3.1:
+
+| 函数                                                                 | 描述                                                   |
+| -------------------------------------------------------------------- | ------------------------------------------------------ |
+| `void read_lock(rwlock_t *lock)`                                   | 获取读锁                                               |
+| `void read_unlock(rwlock_t *lock)`                                 | 释放读锁                                               |
+| `void read_lock_irq(rwlock_t *lock)`                               | 禁止本地中断,并且获取读锁                             |
+| `void read_unlock_irq(rwlock_t *lock)`                             | 打开本地中断,并且释放读锁                             |
+| `void read_lock_irqsave(rwlock_t *lock, unsigned long flags)`      | 保存中断状态,禁止本地中断,并获取读锁                 |
+| `void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags)` | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁 |
+| `void read_lock_bh(rwlock_t *lock)`                                | 关闭下半部,并获取读锁                                 |
+| `void read_unlock_bh(rwlock_t *lock)`                              | 打开下半部,并释放读锁                                 |
+
+**写锁 API**(排他)——原书表 47.3.3.1:
+
+| 函数                                                                  | 描述                                                   |
+| --------------------------------------------------------------------- | ------------------------------------------------------ |
+| `void write_lock(rwlock_t *lock)`                                   | 获取写锁                                               |
+| `void write_unlock(rwlock_t *lock)`                                 | 释放写锁                                               |
+| `void write_lock_irq(rwlock_t *lock)`                               | 禁止本地中断,并且获取写锁                             |
+| `void write_unlock_irq(rwlock_t *lock)`                             | 打开本地中断,并且释放写锁                             |
+| `void write_lock_irqsave(rwlock_t *lock, unsigned long flags)`      | 保存中断状态,禁止本地中断,并获取写锁                 |
+| `void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags)` | 将中断状态恢复到以前的状态,并且激活本地中断,释放写锁 |
+| `void write_lock_bh(rwlock_t *lock)`                                | 关闭下半部,并获取写锁                                 |
+| `void write_unlock_bh(rwlock_t *lock)`                              | 打开下半部,并释放写锁                                 |
+
+> 💡 **补充**:内核另外还提供 `read_trylock()` 和 `write_trylock()`,尝试获取锁,成功返回非 0、失败返回 0 且不等待——原书表格未列出。
+>
+> ⚠️ **命名规则与自旋锁一致**:`irq` 版本禁止本地中断,`irqsave` 版本保存并恢复中断状态,`bh` 版本关闭/打开下半部(软中断)。
+
+### 6.4 读写锁使用示例
+
+以"一个全局配置表"为例,配置项读多写少:
+
+```c
+#include <linux/rwlock.h>
+#include <linux/module.h>
+
+/* 共享的配置数据 */
+struct device_config {
+    int sample_rate;
+    int gain;
+    int channel;
+};
+
+static struct device_config g_config;
+static rwlock_t config_lock;    /* 读写锁 */
+
+/* 读操作:多个进程可并发进入 */
+static int get_sample_rate(void)
+{
+    unsigned long flags;
+    int rate;
+
+    read_lock_irqsave(&config_lock, flags);
+    rate = g_config.sample_rate;        /* 只读,不修改 */
+    read_unlock_irqrestore(&config_lock, flags);
+
+    return rate;
+}
+
+/* 写操作:排他,与所有读写者互斥 */
+static void set_sample_rate(int rate)
+{
+    unsigned long flags;
+
+    write_lock_irqsave(&config_lock, flags);
+    g_config.sample_rate = rate;        /* 修改共享数据 */
+    write_unlock_irqrestore(&config_lock, flags);
+}
+
+static int __init rwlock_demo_init(void)
+{
+    rwlock_init(&config_lock);
+    g_config.sample_rate = 8000;
+    g_config.gain = 10;
+    g_config.channel = 1;
+
+    pr_info("read rate = %d\n", get_sample_rate());
+    set_sample_rate(16000);
+    pr_info("read rate = %d\n", get_sample_rate());
+    return 0;
+}
+
+static void __exit rwlock_demo_exit(void)
+{
+    pr_info("rwlock demo exit\n");
+}
+
+module_init(rwlock_demo_init);
+module_exit(rwlock_demo_exit);
+MODULE_LICENSE("GPL");
+```
+
+### 6.5 读写锁 vs 自旋锁对比
+
+| 特性                 | 读写锁(rwlock_t)              | 自旋锁(spinlock_t) |
+| -------------------- | ------------------------------- | -------------------- |
+| **并发性**     | 读者可并发                      | 完全互斥             |
+| **实现开销**   | 稍大(维护读者计数)            | 更小                 |
+| **写者开销**   | 稍大                            | 小                   |
+| **中断上下文** | ✅ 可用                         | ✅ 可用              |
+| **可睡眠**     | ❌ 不可以                       | ❌ 不可以            |
+| **适用场景**   | 读多写少(读:写 > 10:1)        | 读写均衡或写为主     |
+| **饥饿风险**   | ⚠️ 读者持续涌入时写者可能饥饿 | 无                   |
+| **优先级继承** | ❌ 不支持                       | ❌ 不支持            |
+
+### 6.6 读写锁注意事项
+
+> ⚠️ **避坑指南**:
+>
+> 1. **不要试图升级锁**:持有读锁时调用 `write_lock()` 会**死锁**(自己等自己释放读锁)。
+> 2. **不要试图降级锁**:持有写锁时调用 `read_lock()` 同样**死锁**。需要降级时必须先 `write_unlock()` 再 `read_lock()`,但中间存在"空窗期",另一写者可能插入。
+> 3. **写者饥饿**:Linux 的 rwlock 实现中,新来的读者可以"插队"到等待的写者之前,读压力极大时写者可能长时间得不到锁。若写者不能忍受饥饿,应改用**顺序锁**或 **RCU**。
+> 4. **临界区必须短**:读写锁本质仍是自旋锁,持锁期间**不能睡眠**,也不能调用可能睡眠的函数(如 `kmalloc(GFP_KERNEL)`、`copy_from_user()`)。
+> 5. **中断场景用 irqsave**:读写锁保护的临界区若可能被中断访问,必须使用 `read_lock_irqsave` / `write_lock_irqsave`。
+> 6. **优先考虑 RCU**:如果共享数据是"读多写极少"的链表或指针结构,RCU 的性能远优于读写锁。
+
+---
+
+## 七、顺序锁(seqlock)
+
+### 7.1 什么是顺序锁
+
+**原书表述**:顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。**使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。**
+
+虽然顺序锁的读和写操作可以同时进行,但是**如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性**。
+
+> ⚠️ **原书重点警告**:**顺序锁保护的资源不能是指针**,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如**读取野指针导致系统崩溃**。
+
+**实现机制**:
+
+1. 有一个**序号(sequence number)**,初始为偶数
+2. **写者进入**:序号 +1(变为奇数,表示"正在写")→ 修改数据 → 序号 +1(变回偶数)
+3. **读者**:
+   - 进入前读序号 `s1`,若为奇数说明正在写,等待/重试
+   - 读取数据
+   - 读完再读序号 `s2`,比较 `s1 == s2`,相等说明读取期间没有写者,数据有效;否则重读
+
+```mermaid
+sequenceDiagram
+    participant R as 读者(无锁)
+    participant W as 写者
+    participant S as 序号
+
+    R->>S: s1 = 读序号 (偶数=2)
+    W->>S: 序号+1 (3, 奇数, 开始写)
+    W->>W: 修改数据
+    R->>R: 读取数据 (可能读到半成品)
+    W->>S: 序号+1 (4, 偶数, 写完)
+    R->>S: s2 = 读序号 (4)
+    Note over R: s1(2) != s2(4)<br/>本次读取无效 → 重试
+    R->>S: s1 = 读序号 (4)
+    R->>R: 读取数据 (完整)
+    R->>S: s2 = 读序号 (4)
+    Note over R: s1 == s2 → 数据有效 ✅
+```
+
+> 💡 **本质**:顺序锁牺牲了读者的"确定性延迟"(可能重试),换来了读者**零加锁开销**。所以它适合"**读极多、写极少、且读操作非常快**"的场景。
+
+### 7.2 顺序锁类型与 API
+
+Linux 内核使用 `seqlock_t` 结构体表示顺序锁,结构体定义如下(原书示例代码 47.3.3.2):
+
+```c
+/* 示例代码 47.3.3.2 seqlock_t 结构体 */
+typedef struct {
+    struct seqcount seqcount;
+    spinlock_t lock;
+} seqlock_t;
+```
+
+> 💡 **结构体解读**:顺序锁内部由两部分组成——`seqcount` 是序号计数器(读者用来判断是否被写者打断),`spinlock_t lock` 是写者之间的互斥锁(保证不会并发写)。这也解释了为什么"允许写时读,但不允许并发写"。
+
+初始化方式:
+
+```c
+#include <linux/seqlock.h>
+
+/* 初始化方式一:动态 */
+seqlock_t my_seqlock;
+seqlock_init(&my_seqlock);
+
+/* 初始化方式二:静态 */
+DEFINE_SEQLOCK(my_seqlock);
+```
+
+
+**顺序锁写操作 API**(原书表 47.3.3.2):
+
+| 函数                                                                    | 描述                                                       |
+| ----------------------------------------------------------------------- | ---------------------------------------------------------- |
+| `void write_seqlock(seqlock_t *sl)`                                   | 获取写顺序锁                                               |
+| `void write_sequnlock(seqlock_t *sl)`                                 | 释放写顺序锁                                               |
+| `void write_seqlock_irq(seqlock_t *sl)`                               | 禁止本地中断,并且获取写顺序锁                             |
+| `void write_sequnlock_irq(seqlock_t *sl)`                             | 打开本地中断,并且释放写顺序锁                             |
+| `void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags)`      | 保存中断状态,禁止本地中断,并获取写顺序锁                 |
+| `void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags)` | 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁 |
+| `void write_seqlock_bh(seqlock_t *sl)`                                | 关闭下半部,并获取写顺序锁                                 |
+| `void write_sequnlock_bh(seqlock_t *sl)`                              | 打开下半部,并释放写顺序锁                                 |
+
+**顺序锁读操作 API**(原书表 47.3.3.2):
+
+| 函数                                                            | 描述                                                                           |
+| --------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| `unsigned read_seqbegin(const seqlock_t *sl)`                 | 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号                 |
+| `unsigned read_seqretry(const seqlock_t *sl, unsigned start)` | 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 |
+
+### 7.3 顺序锁使用示例
+
+标准的"读侧重试"写法:
+
+```c
+#include <linux/seqlock.h>
+#include <linux/module.h>
+
+struct sensor_data {
+    int temperature;
+    int pressure;
+    int humidity;
+};
+
+static struct sensor_data g_sensor;
+static seqlock_t sensor_lock;
+
+/* 写侧:中断中更新传感器数据(写极少) */
+static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
+{
+    unsigned long flags;
+
+    write_seqlock_irqsave(&sensor_lock, flags);
+    g_sensor.temperature = read_temp_reg();
+    g_sensor.pressure    = read_press_reg();
+    g_sensor.humidity    = read_humid_reg();
+    write_sequnlock_irqrestore(&sensor_lock, flags);
+
+    return IRQ_HANDLED;
+}
+
+/* 读侧:用户进程高频读取(读极多),无锁 + 重试 */
+static void get_sensor_data(struct sensor_data *out)
+{
+    unsigned int seq;
+
+    do {
+        seq = read_seqbegin(&sensor_lock);
+
+        out->temperature = g_sensor.temperature;
+        out->pressure    = g_sensor.pressure;
+        out->humidity    = g_sensor.humidity;
+
+    } while (read_seqretry(&sensor_lock, seq));   /* 不一致则重读 */
+}
+
+static ssize_t sensor_read(struct file *filp, char __user *buf,
+                           size_t count, loff_t *ppos)
+{
+    struct sensor_data data;
+
+    get_sensor_data(&data);      /* 读到的一定是完整快照 */
+
+    if (copy_to_user(buf, &data, sizeof(data)))
+        return -EFAULT;
+
+    return sizeof(data);
+}
+```
+
+### 7.4 顺序锁 vs 读写锁对比
+
+| 特性                     | 顺序锁(seqlock)                | 读写锁(rwlock)        |
+| ------------------------ | -------------------------------- | ----------------------- |
+| **读者加锁**       | ❌ 完全不加锁                    | ✅ 需要加读锁           |
+| **读者开销**       | 极低(2 次读序号)               | 中(原子操作)          |
+| **读者可能重试**   | ✅ 会被写者打断后重试            | ❌ 不会                 |
+| **写者饥饿**       | ❌ 不会(写者优先)              | ⚠️ 可能               |
+| **可保护的数据**   | 仅限**不含指针**的简单数据 | 任意数据(含链表/指针) |
+| **保护指针的危险** | ⚠️ 读侧可能读到野指针          | ✅ 安全                 |
+| **典型场景**       | jiffies、时间戳、统计计数        | 配置表、缓存            |
+| **是否可睡眠**     | ❌ 写侧不可睡眠                  | ❌ 不可睡眠             |
+
+> ⚠️ **关键限制**:顺序锁的读侧**不能保护指针**。因为读者在无锁读取指针时,写者可能已经释放(free)了指针指向的内存,读者解引用就是**野指针访问**。所以顺序锁只能用于"纯值类型"的数据(整数、结构体内全是整数)。
+
+### 7.5 顺序锁在内核中的典型应用
+
+内核里最著名的顺序锁使用者是 `jiffies`:
+
+```c
+/* 内核中的实际用法:读 jiffies_64 */
+u64 get_jiffies_64(void)
+{
+    unsigned int seq;
+    u64 ret;
+
+    do {
+        seq = read_seqbegin(&jiffies_lock);
+        ret = jiffies_64;
+    } while (read_seqretry(&jiffies_lock, seq));
+
+    return ret;
+}
+```
+
+`jiffies` 被每个时钟中断更新(写少),而被大量内核代码读取(读极多),完美契合顺序锁的适用场景。
 
 ---
 
-## 六、完整源码分析
+## 八、RCU(扩展知识)
+
+> ⚠️ **来源说明**:本节**不属于**《I.MX6U嵌入式Linux驱动开发指南》的内容——第四十七章 47.3.3 只讲到**读写自旋锁**和**顺序锁**这两种"其他类型的锁",没有涉及 RCU 和完成量。此处作为**扩展知识**补充,便于阅读内核源码与面试准备。
+
+### 8.1 什么是 RCU
 
-### 6.1 原子操作实验 - atomic.c
+**RCU** 是 Linux 内核中性能最高的读侧同步机制,全称 **Read-Copy Update(读-复制-更新)**。它把"读"的代价降到了接近零:
+
+> **读者:只是标记进入/退出临界区(在非抢占内核里甚至是一条编译器屏障,无原子操作)。**
+> **写者:不直接修改数据,而是"复制一份 → 修改副本 → 替换指针 → 等待宽限期 → 释放旧数据"。**
+
+```mermaid
+flowchart TD
+    A[写者要修改数据] --> B[1. 复制一份新数据]
+    B --> C[2. 修改副本]
+    C --> D[3. 原子替换指针<br/>rcu_assign_pointer]
+    D --> E[4. 等待宽限期<br/>synchronize_rcu]
+    E --> F[5. 释放旧数据]
+
+    G[读者] --> H[rcu_read_lock<br/>标记进入]
+    H --> I[通过指针访问数据]
+    I --> J[rcu_read_unlock<br/>标记退出]
+
+    style D fill:#dbeafe,stroke:#2563eb
+    style E fill:#fef3c7,stroke:#f59e0b
+```
+
+**宽限期(Grace Period)** 是 RCU 的核心概念:写者替换指针后,必须等到**所有已经进入读侧临界区的读者都退出**,才能安全释放旧数据。此时保证没有任何读者还持有旧数据的引用。
+
+### 8.2 RCU 核心 API
+
+```c
+#include <linux/rcupdate.h>
+
+/* ===== 读侧(极低开销)===== */
+rcu_read_lock();          /* 标记进入读侧临界区 */
+/* 访问数据(注意:rcu_read_lock 不会阻塞写者) */
+rcu_read_unlock();        /* 标记退出读侧临界区 */
+
+/* ===== 写侧 ===== */
+rcu_assign_pointer(ptr, new);      /* 释放语义地替换指针 */
+struct foo *p = rcu_dereference(ptr);   /* 读侧带屏障地解引用 */
+
+synchronize_rcu();                 /* 同步等待一个宽限期(会睡眠) */
+void call_rcu(struct rcu_head *head, rcu_callback_t func);  /* 异步回调 */
+kfree_rcu(ptr, rcu_head_field);    /* 延迟释放 */
+```
+
+**RCU 链表 API**:
+
+| 函数                                           | 说明                                   |
+| ---------------------------------------------- | -------------------------------------- |
+| `list_add_rcu(new, head)`                    | RCU 版链表插入                         |
+| `list_del_rcu(entry)`                        | RCU 版链表删除(不释放内存,仅断链)   |
+| `list_replace_rcu(old, new)`                 | RCU 版节点替换                         |
+| `list_for_each_entry_rcu(pos, head, member)` | RCU 版遍历(需在`rcu_read_lock` 内) |
+
+### 8.3 RCU 使用示例
+
+一个典型的"读多写少链表"场景:
+
+```c
+#include <linux/rcupdate.h>
+#include <linux/list.h>
+#include <linux/slab.h>
+
+struct my_node {
+    struct list_head list;
+    int value;
+    struct rcu_head rcu;        /* RCU 延迟释放用 */
+};
+
+static LIST_HEAD(g_node_list);
+static DEFINE_MUTEX(g_write_lock);  /* 写者之间还需互斥 */
+
+/* 读侧:无锁遍历,多个读者可并发 */
+static int lookup_max_value(void)
+{
+    struct my_node *node;
+    int max = INT_MIN;
+
+    rcu_read_lock();                                    /* 进入 */
+    list_for_each_entry_rcu(node, &g_node_list, list) { /* 无锁遍历 */
+        if (node->value > max)
+            max = node->value;
+    }
+    rcu_read_unlock();                                  /* 退出 */
+
+    return max;
+}
+
+/* 写侧:复制-修改-替换 */
+static void add_node(int value)
+{
+    struct my_node *new_node;
+
+    new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
+    if (!new_node)
+        return;
+    new_node->value = value;
+
+    mutex_lock(&g_write_lock);          /* 写者之间互斥 */
+    list_add_rcu(&new_node->list, &g_node_list);
+    mutex_unlock(&g_write_lock);
+}
+
+/* 写侧:删除节点 */
+static void del_first_node(void)
+{
+    struct my_node *node;
+
+    mutex_lock(&g_write_lock);
+    node = list_first_entry_or_null(&g_node_list, struct my_node, list);
+    if (node) {
+        list_del_rcu(&node->list);      /* 断链,但不释放 */
+        kfree_rcu(node, rcu);           /* 等宽限期后自动释放 */
+    }
+    mutex_unlock(&g_write_lock);
+}
+```
+
+### 8.4 RCU vs 读写锁 vs 顺序锁
+
+| 特性                       | RCU                                | 顺序锁          | 读写锁         |
+| -------------------------- | ---------------------------------- | --------------- | -------------- |
+| **读者开销**         | 极低(近乎零)                     | 极低(读序号)  | 中(原子操作) |
+| **读者会重试**       | ❌ 不会                            | ✅ 会           | ❌ 不会        |
+| **读者是否阻塞写者** | ❌ 完全不阻塞                      | ❌ 不阻塞       | ✅ 阻塞        |
+| **可保护指针/链表**  | ✅ 是主要用途                      | ❌ 不可         | ✅ 可          |
+| **写者开销**         | 高(需等宽限期)                   | 低              | 中             |
+| **写者语义**         | 必须"复制-替换"                    | 原地修改        | 原地修改       |
+| **内存开销**         | 高(新旧副本共存一段时间)         | 无额外          | 无额外         |
+| **典型场景**         | 网络路由表、文件描述符表、模块链表 | jiffies、时间戳 | 配置表、缓存   |
+
+> 💡 **一句话记忆**:
+>
+> - 读多写少 + 保护指针 → **RCU**
+> - 读多写少 + 纯值 + 写者不能被饿死 → **顺序锁**
+> - 读写均衡 → **读写锁** 或 **自旋锁**
+> - 临界区长且可睡眠 → **互斥体**
+
+### 8.5 RCU 注意事项
+
+> ⚠️ **避坑指南**:
+>
+> 1. **`rcu_read_lock()` 不会睡眠也不会阻塞**:它只是标记,所以读侧临界区**不能睡眠**(不能调用 `copy_to_user()`、`kmalloc(GFP_KERNEL)` 等可能睡眠的函数)。
+> 2. **`synchronize_rcu()` 会睡眠**:不能在中断上下文、原子上下文或持有自旋锁时调用。
+> 3. **写者必须"复制-替换"**:绝不能在读侧可能引用它时原地修改数据,否则读者会读到不一致的中间状态。
+> 4. **写者之间仍需互斥**:RCU 只解决"读写"冲突,多个写者同时操作同一链表仍需额外的锁(如互斥体)保护。
+> 5. **读侧指针必须用 `rcu_dereference()` 解引用**:否则编译器和 CPU 可能重排指令,导致读到未初始化的数据。
+> 6. **RCU 不保证实时性**:宽限期可能持续较长时间,写者释放旧数据的时机不确定。
+
+---
+
+## 九、完成量(扩展知识)
+
+> ⚠️ **来源说明**:同第八节,本节也**不属于**正点原子教材内容,作为扩展知识补充。
+
+### 9.1 什么是完成量
+
+**完成量(completion)** 用于解决一种特定的同步需求:**一个执行单元等待另一个执行单元完成某件事**。
+
+它和信号量的区别在于:
+
+- **信号量**可以用于"资源计数"(初值 > 1),也可以用于"互斥"(初值 = 1)
+- **完成量**专门用于"等待某事件发生",是最轻量的"等待-唤醒"原语,语义更清晰
+
+典型场景:
+
+- 驱动初始化时等待硬件就绪(中断到来后 `complete()`)
+- 一个线程等待另一个线程完成 DMA 传输
+- 等待内核线程退出
+
+> 💡 **生活类比**:信号量像"停车场车位"(可以有很多个,谁先来谁停),完成量像"等人到齐"(一个人等另一个人做完某事,只关心"完成了没有")。
+
+### 9.2 完成量 API
+
+```c
+#include <linux/completion.h>
+
+/* 初始化方式一:动态 */
+struct completion my_completion;
+init_completion(&my_completion);
+
+/* 初始化方式二:静态 */
+DECLARE_COMPLETION(my_completion);
+DECLARE_COMPLETION_ONSTACK(my_completion);   /* 栈上定义,用于局部等待 */
+```
+
+| 函数                                                                                       | 说明                                     |
+| ------------------------------------------------------------------------------------------ | ---------------------------------------- |
+| `void init_completion(struct completion *x)`                                             | 初始化完成量(计数=0)                   |
+| `void reinit_completion(struct completion *x)`                                           | 重置完成量(允许重复使用)               |
+| `void wait_for_completion(struct completion *x)`                                         | 等待完成,**不可被信号打断**       |
+| `int wait_for_completion_interruptible(struct completion *x)`                            | 可被信号打断,被打断返回`-ERESTARTSYS` |
+| `int wait_for_completion_killable(struct completion *x)`                                 | 可被致命信号打断                         |
+| `unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout)` | 带超时等待,超时返回 0                   |
+| `void complete(struct completion *x)`                                                    | 唤醒**一个**等待者(计数+1)       |
+| `void complete_all(struct completion *x)`                                                | 唤醒**所有**等待者                 |
+
+### 9.3 完成量使用示例
+
+**场景**:等待一次中断到来(典型的"硬件就绪"同步):
+
+```c
+#include <linux/completion.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+
+static struct completion irq_done;
+static int irq_number;
+
+static irqreturn_t my_irq_handler(int irq, void *dev_id)
+{
+    pr_info("IRQ %d triggered\n", irq);
+    complete(&irq_done);        /* 通知等待者:事件已发生 */
+    return IRQ_HANDLED;
+}
+
+static int __init completion_demo_init(void)
+{
+    int ret;
+
+    init_completion(&irq_done);
+
+    irq_number = gpio_to_irq(/* ... */);
+    ret = request_irq(irq_number, my_irq_handler,
+                      IRQF_TRIGGER_FALLING, "my_irq", NULL);
+    if (ret)
+        return ret;
+
+    pr_info("waiting for interrupt...\n");
+
+    /* 等待中断到来,最长等 1 秒(1000 * 10ms) */
+    if (!wait_for_completion_timeout(&irq_done, msecs_to_jiffies(1000))) {
+        pr_err("timeout: IRQ not received\n");
+        free_irq(irq_number, NULL);
+        return -ETIMEDOUT;
+    }
+
+    pr_info("interrupt received, continue init\n");
+    free_irq(irq_number, NULL);
+    return 0;
+}
+```
+
+### 9.4 完成量 vs 信号量对比
+
+| 特性                     | 完成量(completion)       | 信号量(semaphore)            |
+| ------------------------ | -------------------------- | ------------------------------ |
+| **语义**           | "等待事件完成"             | "资源计数 / 互斥"              |
+| **初始值**         | 固定为 0                   | 可任意(`sema_init(&s, n)`) |
+| **主要用途**       | 一对一/一对多事件通知      | 资源池、互斥                   |
+| **能否重复使用**   | 需`reinit_completion()`  | 天然支持                       |
+| **是否可睡眠**     | ✅ 可以                    | ✅ 可以                        |
+| **中断上下文使用** | ✅ 只能`complete()`      | ✅ 只能`up()`                |
+| **代码可读性**     | 语义更明确                 | 需要看初值才知道意图           |
+| **典型场景**       | 等待硬件就绪、等待线程退出 | 限制并发数、简单互斥           |
+
+> 💡 **选择建议**:如果只是想"等某件事做完",用**完成量**比信号量语义更清晰、更不容易出错;如果需要"限制最多 N 个并发"或"互斥",用信号量或互斥体。
+
+---
+
+## 十、完整源码分析
+
+### 10.1 原子操作实验 - atomic.c
 
 ```c
 #include <linux/types.h>
@@ -617,16 +1278,16 @@ MODULE_LICENSE("GPL");
 MODULE_AUTHOR("zuozhongkai");
 ```
 
-### 6.2 代码逐行解析
+### 10.2 代码逐行解析
 
-| 行号                                 | 代码                   | 说明                 |
-| ------------------------------------ | ---------------------- | -------------------- |
+| 行号                                   | 代码                   | 说明                 |
+| -------------------------------------- | ---------------------- | -------------------- |
 | `atomic_t lock`                      | 设备结构体中的原子变量 | 用于实现设备互斥访问 |
 | `atomic_set(&gpioled.lock, 1)`       | 初始化原子变量为1      | 表示设备可用         |
 | `atomic_dec_and_test(&gpioled.lock)` | 原子减1并测试是否为0   | 如果为0表示获取成功  |
 | `atomic_inc(&gpioled.lock)`          | 原子加1                | 释放设备             |
 
-### 6.3 自旋锁实验 - spinlock.c
+### 10.3 自旋锁实验 - spinlock.c
 
 ```c
 /* 使用自旋锁保护的设备结构体 */
@@ -674,7 +1335,7 @@ static int led_release(struct inode *inode, struct file *filp)
 }
 ```
 
-### 6.4 信号量实验 - semaphore.c
+### 10.4 信号量实验 - semaphore.c
 
 ```c
 struct gpioled_dev {
@@ -708,7 +1369,7 @@ static int led_release(struct inode *inode, struct file *filp)
 }
 ```
 
-### 6.5 互斥体实验 - mutex.c
+### 10.5 互斥体实验 - mutex.c
 
 ```c
 struct gpioled_dev {
@@ -744,9 +1405,9 @@ static int led_release(struct inode *inode, struct file *filp)
 
 ---
 
-## 、实验验证
+## 十一、实验验证
 
-### 7.1 编译方法
+### 11.1 编译方法
 
 ```bash
 # 1. 编译驱动模块
@@ -762,7 +1423,7 @@ make -j32
 arm-linux-gnueabihf-gcc atomicApp.c -o atomicApp
 ```
 
-### 7.2 测试步骤
+### 11.2 测试步骤
 
 ```bash
 # 1. 加载驱动模块
@@ -779,7 +1440,7 @@ modprobe atomic.ko      # 加载驱动
 rmmod atomic.ko
 ```
 
-### 7.3 结果分析
+### 11.3 结果分析
 
 | 测试场景         | 预期结果      | 实际结果 |
 | ---------------- | ------------- | -------- |
@@ -808,22 +1469,26 @@ gantt
 
 ---
 
-## 、跨平台对比
+## 十二、跨平台对比
 
-### 8.1 IMX6ULL vs STM32 vs RK3568并发机制差异
+### 12.1 IMX6ULL vs STM32 vs RK3568并发机制差异
 
-| 特性           | IMX6ULL          | STM32        | RK3568          |
-| -------------- | ---------------- | ------------ | --------------- |
-| **CPU架构**    | Cortex-A7 单核   | Cortex-M4/M7 | Cortex-A55 四核 |
-| **并发模型**   | 单核+中断        | 单核+中断    | 多核SMP+中断    |
-| **自旋锁**     | ✅ 支持          | ❌ 不需要    | ✅ 核间同步     |
-| **原子操作**   | ✅ 支持          | ⚠️ 部分支持  | ✅ 支持         |
-| **互斥体**     | ✅ 支持          | ❌ 不支持    | ✅ 支持         |
-| **信号量**     | ✅ 支持          | ✅ 支持      | ✅ 支持         |
-| **中断优先级** | 32级             | 16级         | 32级            |
-| **典型锁选择** | spinlock_irqsave | 临界区       | mutex/spinlock  |
+| 特性                       | IMX6ULL            | STM32         | RK3568                    |
+| -------------------------- | ------------------ | ------------- | ------------------------- |
+| **CPU架构**          | Cortex-A7 单核     | Cortex-M4/M7  | Cortex-A55 四核           |
+| **并发模型**         | 单核+中断          | 单核+中断     | 多核SMP+中断              |
+| **自旋锁**           | ✅ 支持            | ❌ 不需要     | ✅ 核间同步               |
+| **原子操作**         | ✅ 支持            | ⚠️ 部分支持 | ✅ 支持                   |
+| **互斥体**           | ✅ 支持            | ❌ 不支持     | ✅ 支持                   |
+| **信号量**           | ✅ 支持            | ✅ 支持       | ✅ 支持                   |
+| **读写锁**           | ✅ 支持            | ❌ 不需要     | ✅ 支持                   |
+| **顺序锁**           | ✅ 支持            | ❌ 不需要     | ✅ 支持                   |
+| **RCU**              | ✅ 支持            | ❌ 不需要     | ✅ 支持(重点)           |
+| **中断优先级**       | 32级               | 16级          | 32级                      |
+| **典型锁选择**       | spinlock_irqsave   | 临界区        | mutex/spinlock            |
+| **其他类型锁的使用** | 内核为主,驱动少用 | 无对应机制    | RCU 广泛用于网络/文件系统 |
 
-### 8.2 多核 vs 单核的锁选择
+### 12.2 多核 vs 单核的锁选择
 
 ```mermaid
 flowchart TD
@@ -848,7 +1513,7 @@ flowchart TD
 
 ---
 
-## 、面试精选
+## 十三、面试精选
 
 ### 题目1:自旋锁和信号量有什么区别?
 
@@ -861,7 +1526,7 @@ flowchart TD
 | 等待方式   | 忙等待(占用CPU)  | 睡眠(释放CPU)  |
 | 可睡眠     | ❌ 不可以          | ✅ 可以          |
 | 中断上下文 | ✅ 可以使用        | ❌ 不可以        |
-| 临界区长度 | 短(μs级)         | 长(ms级)       |
+| 临界区长度 | 短(μs级)        | 长(ms级)       |
 | CPU占用    | 高(忙等)         | 低(上下文切换) |
 | 适用场景   | 中断处理、短临界区 | 长时间操作、I/O  |
 
@@ -938,7 +1603,73 @@ static int device_release(struct inode *inode, struct file *filp)
 }
 ```
 
+### 题目6:读写锁和自旋锁有什么区别?什么场景下用读写锁?
+
+**考察点**:读写锁理解(教材 47.3.3)
+
+**参考答案**:
+
+| 特性         | 读写锁(rwlock_t)       | 自旋锁(spinlock_t) |
+| ------------ | ------------------------ | -------------------- |
+| 读操作并发   | ✅ 允许多个读者同时持有  | ❌ 完全互斥          |
+| 写操作       | 排他,与所有读写者互斥   | 排他                 |
+| 读写能否同时 | ❌ 不能                  | ❌ 不能              |
+| 实现开销     | 稍大(维护读者计数)     | 更小                 |
+| 适用场景     | 读多写少,且读操作可并发 | 读写均衡、临界区极短 |
+
+**使用场景**:当数据结构符合**读/写**或**生产者/消费者**模型时使用,例如学生信息表、设备配置表这类"读远多于写"的数据。
+
+**追问**:
+
+- 追问1:读写锁能否把写锁升级为读锁(降级)?→ **不能**,持有写锁时再获取读锁会死锁(自己等自己)。必须先 `write_unlock()` 再 `read_lock()`,但中间存在空窗期。
+- 追问2:读写锁会导致写者饥饿吗?→ **会**。Linux 的实现中读者可以插队到等待的写者之前,读压力极大时写者可能长时间得不到锁。
+
+### 题目7:顺序锁的原理是什么?它为什么不能保护指针?
+
+**考察点**:顺序锁理解(教材 47.3.3)
+
+**参考答案**:
+
+顺序锁**允许写的时候同时读**,但不允许并发写。读者不加锁,而是靠序号判断:
+
+1. 读之前读序号 `s1`
+2. 读取数据
+3. 读之后再读序号 `s2`
+4. 若 `s1 == s2` 说明读取期间没有写者,数据有效;否则**重读**
+
+对应 API:`read_seqbegin()` / `read_seqretry()`,写侧用 `write_seqlock()` / `write_sequnlock()`。
+
+**为什么不能保护指针**:原书明确指出——如果在写操作的时候可能会导致指针无效(例如写者把指针指向的旧内存 `kfree` 掉了),而此时恰巧有读操作访问该指针,就会**读取野指针导致系统崩溃**。所以顺序锁只能保护**纯值类型数据**(如 `jiffies`),不能保护含指针的结构。
+
+**追问**:
+
+- 追问1:顺序锁和读写锁相比,谁的性能更好?→ 读者侧顺序锁更好(无加锁开销),但可能重试;写者侧顺序锁不会饥饿。
+- 追问2:内核中顺序锁的典型使用者?→ `jiffies_64`(时钟中断频繁写、大量代码频繁读)。
+
+### 题目8:RCU 与读写锁的区别?(扩展)
+
+**考察点**:RCU 理解(超出教材范围)
+
+**参考答案**:
+
+| 特性            | RCU                     | 读写锁         |
+| --------------- | ----------------------- | -------------- |
+| 读者开销        | 近乎零(只标记)        | 中(原子操作) |
+| 读者会重试      | ❌ 不会                 | ❌ 不会        |
+| 读者阻塞写者    | ❌ 完全不阻塞           | ✅ 阻塞        |
+| 可保护指针/链表 | ✅ 主要用途             | ✅ 可          |
+| 写者语义        | 复制-替换-等宽限期-释放 | 原地加锁修改   |
+| 内存开销        | 高(新旧副本共存)      | 无额外         |
+
+**核心区别**:RCU 的写者不是"原地加锁修改",而是"复制一份 → 改副本 → 原子替换指针 → 等宽限期 → 释放旧数据"。宽限期保证所有已进入读侧临界区的读者都退出后,旧数据才被释放。
+
+**追问**:
+
+- 追问1:`rcu_read_lock()` 会阻塞写者吗?→ 不会,它只是标记进入临界区,所以读侧临界区**不能睡眠**。
+- 追问2:`synchronize_rcu()` 能在中断里调用吗?→ 不能,它会睡眠。
+
 ---
 
-**代码来源**: Linux驱动例程 07_atomic, 08_spinlock, 09_semaphore, 10_mutex
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第四十七章 Linux并发与竞争(含 47.3.3 其他类型的锁)+ 第四十八章 并发与竞争实验;例程 07_atomic, 08_spinlock, 09_semaphore, 10_mutex
+**扩展内容**: 第八、九节(RCU、完成量)超出教材范围,为补充知识
 **最后更新**: 2026-09-17

+ 230 - 4
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/05-中断下半部处理.md

@@ -47,6 +47,166 @@ graph TD
 | 工作队列(Workqueue) | 进程上下文   | 是     | 耗时操作、需要睡眠 |
 | 线程化中断            | 内核线程     | 是     | 复杂处理、需要互斥 |
 
+### 1.4 中断号与 request_irq / free_irq
+
+每个中断都有一个**中断号**(有的资料也叫中断线),内核用 `int` 变量表示,通过中断号区分不同中断。
+
+**`request_irq` 函数**(原书 51.1.1):用于申请中断。注意它**可能导致睡眠**,因此不能在中断上下文或其他禁止睡眠的代码段中使用;它会自动使能中断,无需手动使能。
+
+```c
+int request_irq(unsigned int irq,
+                irq_handler_t handler,
+                unsigned long flags,
+                const char *name,
+                void *dev);
+```
+
+| 参数      | 含义                                                                                       |
+| --------- | ------------------------------------------------------------------------------------------ |
+| `irq`     | 要申请中断的中断号                                                                         |
+| `handler` | 中断处理函数,中断发生后就执行                                                             |
+| `flags`   | 中断标志,见下表                                                                           |
+| `name`    | 中断名字,设置后可在 `/proc/interrupts` 中看到                                             |
+| `dev`     | 区分共享中断;一般传设备结构体,会作为第二个参数传给中断处理函数                           |
+
+返回值:`0` 申请成功;其他负值失败;返回 `-EBUSY` 表示中断已被申请。
+
+**常用中断标志(原书表 51.1.1.1)**:
+
+| 标志                    | 描述                                                                 |
+| ----------------------- | -------------------------------------------------------------------- |
+| `IRQF_SHARED`           | 多个设备共享一根中断线,共享的所有中断都必须指定此标志;此时 `dev` 是唯一区分标志 |
+| `IRQF_ONESHOT`          | 单次中断,中断执行一次就结束                                          |
+| `IRQF_TRIGGER_NONE`     | 无触发                                                               |
+| `IRQF_TRIGGER_RISING`   | 上升沿触发                                                           |
+| `IRQF_TRIGGER_FALLING`  | 下降沿触发                                                           |
+| `IRQF_TRIGGER_HIGH`     | 高电平触发                                                           |
+| `IRQF_TRIGGER_LOW`      | 低电平触发                                                           |
+
+这些标志可以通过 `|` 组合。例如 I.MX6U-ALPHA 的 KEY0 使用 GPIO1_IO18,按下为低电平,可设为 `IRQF_TRIGGER_FALLING`。
+
+**`free_irq` 函数**(原书 51.1.1):释放中断。若非共享中断,`free_irq` 会删除中断处理函数并禁止中断;共享中断只有在释放最后一个中断处理函数时才被禁止。
+
+```c
+void free_irq(unsigned int irq, void *dev);
+```
+
+### 1.5 中断处理函数与返回值
+
+中断处理函数原型(原书 51.1.1):
+
+```c
+irqreturn_t (*irq_handler_t)(int, void *);
+```
+
+第一个参数是中断号,第二个参数是与 `request_irq` 的 `dev` 保持一致的通用指针。返回值 `irqreturn_t` 是枚举类型,定义在 `include/linux/interrupt.h` 中:
+
+```c
+/* 原书示例代码 51.1.1.1 irqreturn_t 结构 */
+enum irqreturn {
+    IRQ_NONE        = (0 << 0),
+    IRQ_HANDLED     = (1 << 0),
+    IRQ_WAKE_THREAD = (1 << 1),
+};
+typedef enum irqreturn irqreturn_t;
+```
+
+中断服务函数一般用如下形式返回:
+
+```c
+return IRQ_RETVAL(IRQ_HANDLED);
+```
+
+### 1.6 中断使能与禁止
+
+```c
+void enable_irq(unsigned int irq);
+void disable_irq(unsigned int irq);
+void disable_irq_nosync(unsigned int irq);
+```
+
+- `disable_irq` 要等到当前正在执行的中断处理函数执行完才返回,所以使用者需保证不会产生新中断且已开始的中断处理程序全部退出。
+- `disable_irq_nosync` 调用后立即返回,不等待当前中断处理程序执行完毕。
+
+要关闭**当前处理器的整个中断系统**(全局中断):
+
+```c
+local_irq_enable();
+local_irq_disable();
+```
+
+为避免 A 任务关闭全局中断时 B 任务粗暴地把全局中断打开,应使用下面这一对函数,把中断状态恢复到以前的状态:
+
+```c
+local_irq_save(flags);      /* 禁止中断,并把中断状态保存到 flags */
+local_irq_restore(flags);   /* 把中断恢复到 flags 状态 */
+```
+
+### 1.7 设备树中断信息节点
+
+使用设备树时,内核通过设备树中的中断属性信息来配置中断。I.MX6ULL 的中断控制器节点 `intc`(`imx6ull.dtsi`,原书示例代码 51.1.3.1):
+
+```dts
+intc: interrupt-controller@00a01000 {
+    compatible = "arm,cortex-a7-gic";
+    #interrupt-cells = <3>;
+    interrupt-controller;
+    reg = <0x00a01000 0x1000>,
+          <0x00a02000 0x100>;
+};
+```
+
+对 ARM 的 GIC,`interrupts` 属性共 **3 个 cells**:
+
+1. **第一个 cell**:中断类型,`0` 表示 SPI 中断,`1` 表示 PPI 中断。
+2. **第二个 cell**:中断号,SPI 范围为 0~987,PPI 范围为 0~15。
+3. **第三个 cell**:标志,`bit[3:0]` 表示触发类型——`1` 上升沿,`2` 下降沿,`4` 高电平,`8` 低电平;`bit[15:8]` 为 PPI 中断的 CPU 掩码。
+
+GPIO 节点也可以作为中断控制器,例如 `gpio5`(原书示例代码 51.1.3.2):
+
+```dts
+gpio5: gpio@020ac000 {
+    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
+    reg = <0x020ac000 0x4000>;
+    interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
+                 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
+    gpio-controller;
+    #gpio-cells = <2>;
+    interrupt-controller;
+    #interrupt-cells = <2>;
+};
+```
+
+GPIO5 用了两个中断号,`74` 对应 GPIO5_IO00~GPIO5_IO15,`75` 对应 GPIO5_IO16~GPIO5_IO31。作为中断控制器时 `#interrupt-cells` 为 `2`。
+
+与中断有关的设备树属性总结:
+
+1. `#interrupt-cells`:指定中断源信息的 cells 个数。
+2. `interrupt-controller`:表示当前节点为中断控制器。
+3. `interrupts`:指定中断号、触发方式等。
+4. `interrupt-parent`:指定父中断(中断控制器)。
+
+### 1.8 获取中断号
+
+`irq_of_parse_and_map` 从设备树的 `interrupts` 属性中提取中断号:
+
+```c
+unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
+```
+
+- `dev`:设备节点。
+- `index`:索引号,`interrupts` 属性可能包含多条中断信息,通过 `index` 指定要获取的信息。
+- 返回值:中断号。
+
+如果使用 GPIO,可以用 `gpio_to_irq` 获取 GPIO 对应的中断号:
+
+```c
+int gpio_to_irq(unsigned int gpio);
+```
+
+- `gpio`:要获取的 GPIO 编号。
+- 返回值:GPIO 对应的中断号。
+
 ---
 
 ## 二、软中断(Softirq)
@@ -160,7 +320,7 @@ struct tasklet_struct
 {
     struct tasklet_struct *next;    /* 下一个tasklet */
     unsigned long state;            /* tasklet状态 */
-    atomic_t count;                 /* 引用计数,记录tasklet是否被禁止 */
+    atomic_t count;                 /* 计数器,记录对tasklet的引用数 */
     void (*func)(unsigned long);    /* tasklet执行的函数 */
     unsigned long data;             /* 传递给func的参数 */
 };
@@ -270,7 +430,7 @@ struct work_struct {
 这些工作组织成队列,使用`workqueue_struct`结构体表示工作队列:
 
 ```c
-// 示例51.1.2.9 workqueue_struct结构体(简化版
+// 示例51.1.2.9 workqueue_struct结构体(省略条件编译
 struct workqueue_struct {
     struct list_head pwqs;
     struct list_head list;
@@ -389,8 +549,11 @@ static int __init xxxx_init(void)
 }
 ```
 
+
 ### 4.5 创建和销毁工作队列
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 除了使用系统默认的工作队列,也可以自己创建工作队列:
 
 ```c
@@ -423,6 +586,8 @@ void destroy_workqueue(struct workqueue_struct *wq);
 
 ## 五、线程化中断(Threaded IRQ)
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 5.1 线程化中断原理
 
 线程化中断是Linux内核提供的一种中断处理机制,将中断处理分为硬中断上下文和内核线程上下文两部分。线程化中断允许在下半部中睡眠,适合复杂的中断处理场景。
@@ -653,6 +818,10 @@ static int keyio_init(void)
         gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);
         imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(
             imx6uirq.nd, i);
+#if 0
+        imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(
+            imx6uirq.irqkeydesc[i].gpio);
+#endif
         printk("key%d:gpio=%d, irqnum=%d\r\n", i,
                imx6uirq.irqkeydesc[i].gpio,
                imx6uirq.irqkeydesc[i].irqnum);
@@ -681,7 +850,57 @@ static int keyio_init(void)
 }
 ```
 
-#### 6.4.5 驱动入口函数
+#### 6.4.5 open / read 与文件操作集
+
+```c
+// 打开设备
+static int imx6uirq_open(struct inode *inode, struct file *filp)
+{
+    filp->private_data = &imx6uirq; /* 设置私有数据 */
+    return 0;
+}
+
+// 从设备读取数据(向应用返回按键值)
+static ssize_t imx6uirq_read(struct file *filp, char __user *buf,
+                             size_t cnt, loff_t *offt)
+{
+    int ret = 0;
+    unsigned char keyvalue = 0;
+    unsigned char releasekey = 0;
+    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)
+                                filp->private_data;
+
+    keyvalue = atomic_read(&dev->keyvalue);
+    releasekey = atomic_read(&dev->releasekey);
+
+    if (releasekey) {       /* 有按键按下 */
+        if (keyvalue & 0x80) {
+            keyvalue &= ~0x80;
+            ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
+        } else {
+            goto data_error;
+        }
+        atomic_set(&dev->releasekey, 0); /* 按下标志清零 */
+    } else {
+        goto data_error;
+    }
+    return 0;
+
+data_error:
+    return -EINVAL;
+}
+
+/* 设备操作函数 */
+static struct file_operations imx6uirq_fops = {
+    .owner = THIS_MODULE,
+    .open = imx6uirq_open,
+    .read = imx6uirq_read,
+};
+```
+
+> 原书说明:`imx6uirq_read` 先判断 `releasekey` 是否为 1,为 1 表示发生过一次有效按键;再判断按键值最高位是否为 1,为 1 表示按键值有效,清掉最高位后通过 `copy_to_user` 返回真实按键值,并把 `releasekey` 清零,准备下一次按键。
+
+#### 6.4.6 驱动入口函数
 
 ```c
 static int __init imx6uirq_init(void)
@@ -724,7 +943,7 @@ static int __init imx6uirq_init(void)
 }
 ```
 
-#### 6.4.6 驱动出口函数
+#### 6.4.7 驱动出口函数
 
 ```c
 static void __exit imx6uirq_exit(void)
@@ -867,6 +1086,8 @@ rmmod imx6uirq.ko
 
 ## 八、下半部机制对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 8.1 四种机制详细对比
 
 | 特性       | 软中断(Softirq) | Tasklet             | 工作队列   | 线程化中断 |
@@ -908,6 +1129,8 @@ graph TD
 
 ## 九、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 9.1 I.MX6ULL vs STM32 vs RK3568
 
 | 特性       | I.MX6ULL                | STM32           | RK3568                  |
@@ -941,6 +1164,8 @@ graph TD
 
 ## 十、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:中断上半部和下半部有什么区别?为什么要这样设计?
 
 **考察点**: 中断处理机制
@@ -1065,3 +1290,4 @@ interrupts = <18 IRQ_TYPE_EDGE_BOTH>;  /* 中断信息 */
 
 **代码来源**: Linux驱动例程 13_irq
 **最后更新**: 2026-09-17
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第51章 Linux中断实验

+ 129 - 4
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/06-阻塞IO与poll机制.md

@@ -470,6 +470,7 @@ int flags = fcntl(fd, F_GETFL);
 fcntl(fd, F_SETFL, flags | FASYNC);
 ```
 
+
 **信号处理函数示例**:
 
 ```c
@@ -609,7 +610,7 @@ static int keyio_init(void) {
     return 0;
 }
 
-/* 从设备读取数据(阻塞方式) */
+/* 从设备读取数据(阻塞方式,原书示例代码 52.2.2.1) */
 static ssize_t imx6uirq_read(struct file *filp, char __user *buf,
                              size_t cnt, loff_t *offt) {
     int ret = 0;
@@ -617,12 +618,27 @@ static ssize_t imx6uirq_read(struct file *filp, char __user *buf,
     unsigned char releasekey = 0;
     struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
 
-    /* 等待按键按下 */
+#if 0
+    /* 加入等待队列,等待被唤醒,也就是有按键按下 */
     ret = wait_event_interruptible(dev->r_wait,
                                    atomic_read(&dev->releasekey));
     if (ret) {
         goto wait_error;
     }
+#endif
+
+    DECLARE_WAITQUEUE(wait, current);       /* 定义一个等待队列 */
+    if (atomic_read(&dev->releasekey) == 0) {  /* 没有按键按下 */
+        add_wait_queue(&dev->r_wait, &wait);        /* 添加到等待队列头 */
+        __set_current_state(TASK_INTERRUPTIBLE);    /* 设置任务状态 */
+        schedule();                                 /* 进行一次任务切换 */
+        if (signal_pending(current)) {  /* 判断是否为信号引起的唤醒 */
+            ret = -ERESTARTSYS;
+            goto wait_error;
+        }
+        __set_current_state(TASK_RUNNING);          /* 设置为运行状态 */
+        remove_wait_queue(&dev->r_wait, &wait);     /* 将等待队列移除 */
+    }
 
     keyvalue = atomic_read(&dev->keyvalue);
     releasekey = atomic_read(&dev->releasekey);
@@ -640,8 +656,8 @@ static ssize_t imx6uirq_read(struct file *filp, char __user *buf,
     return 0;
 
 wait_error:
-    set_current_state(TASK_RUNNING);
-    remove_wait_queue(&dev->r_wait, &wait);
+    set_current_state(TASK_RUNNING);            /* 设置任务为运行态 */
+    remove_wait_queue(&dev->r_wait, &wait);     /* 将等待队列移除 */
     return ret;
 
 data_error:
@@ -722,6 +738,101 @@ static struct file_operations imx6uirq_fops = {
 };
 ```
 
+> 阻塞实验的测试 APP 直接复用第 51 章的 `imx6uirqApp.c`(重命名为 `blockioApp.c`,内容不修改)。
+
+#### 5.2.1 非阻塞测试APP(noblockioApp.c,原书示例代码 52.3.3.2)
+
+```c
+#include "stdio.h"
+#include "unistd.h"
+#include "sys/types.h"
+#include "sys/stat.h"
+#include "fcntl.h"
+#include "stdlib.h"
+#include "string.h"
+#include "poll.h"
+#include "sys/select.h"
+#include "sys/time.h"
+#include "linux/ioctl.h"
+
+int main(int argc, char *argv[])
+{
+    int fd;
+    int ret = 0;
+    char *filename;
+    struct pollfd fds;
+    fd_set readfds;
+    struct timeval timeout;
+    unsigned char data;
+
+    if (argc != 2) {
+        printf("Error Usage!\r\n");
+        return -1;
+    }
+
+    filename = argv[1];
+    fd = open(filename, O_RDWR | O_NONBLOCK);   /* 非阻塞访问 */
+    if (fd < 0) {
+        printf("Can't open file %s\r\n", filename);
+        return -1;
+    }
+
+#if 0
+    /* 使用 poll 函数实现非阻塞访问 */
+    fds.fd = fd;
+    fds.events = POLLIN;
+    while (1) {
+        ret = poll(&fds, 1, 500);
+        if (ret) {  /* 数据有效 */
+            ret = read(fd, &data, sizeof(data));
+            if (ret < 0) {
+                /* 读取错误 */
+            } else {
+                if (data)
+                    printf("key value = %d \r\n", data);
+            }
+        } else if (ret == 0) {   /* 超时 */
+            /* 用户自定义超时处理 */
+        } else if (ret < 0) {    /* 错误 */
+            /* 用户自定义错误处理 */
+        }
+    }
+#endif
+
+    /* 使用 select 函数实现非阻塞访问(原书默认开启) */
+    while (1) {
+        FD_ZERO(&readfds);
+        FD_SET(fd, &readfds);
+        /* 构造超时时间 */
+        timeout.tv_sec = 0;
+        timeout.tv_usec = 500000; /* 500ms */
+        ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
+        switch (ret) {
+            case 0:     /* 超时 */
+                break;
+            case -1:    /* 错误 */
+                break;
+            default:    /* 可以读取数据 */
+                if (FD_ISSET(fd, &readfds)) {
+                    ret = read(fd, &data, sizeof(data));
+                    if (ret < 0) {
+                        /* 读取错误 */
+                    } else {
+                        if (data)
+                            printf("key value=%d\r\n", data);
+                    }
+                }
+                break;
+        }
+    }
+
+    close(fd);
+    return ret;
+}
+```
+
+> 原书说明:`#if 0` 段用 `poll` 函数轮询,默认开启的 `while` 段用 `select` 函数实现非阻塞访问;想用 poll 时把 `#if 0` 改成 `#if 1` 即可。
+
 ### 5.3 异步通知实验源码(asyncnoti.c)
 
 ```c
@@ -866,8 +977,15 @@ modprobe blockio.ko
 
 # 5. 查看CPU使用率
 top
+# 不加阻塞处理时 imx6uirqApp 的 CPU 占用率高达 99.6%,加入阻塞访问后降到 0.0%
+
+# 6. 关闭后台程序(先用 ps 查看 PID)
+ps
+kill -9 149          # 149 为示例 PID
 ```
 
+**测试对比(原书 52.2.3 / 52.3.3)**:三个实验的设备节点分别为 `/dev/blockio`、`/dev/noblockio`、`/dev/asyncnoti`;前两个驱动加载后用 `./xxxApp /dev/xxx &` 后台运行,按下 KEY0 即打印按键值,CPU 占用率均低至 0.0%。
+
 ### 6.3 非阻塞IO测试
 
 ```bash
@@ -895,6 +1013,8 @@ modprobe asyncnoti.ko
 
 ## 七、IO模型对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 模型       | 机制              | CPU占用 | 响应延迟 | 实现复杂度 | 适用场景     |
 | ---------- | ----------------- | ------- | -------- | ---------- | ------------ |
 | 阻塞IO     | 等待队列          | 低      | 高       | 简单       | 低频设备     |
@@ -913,6 +1033,8 @@ modprobe asyncnoti.ko
 
 ## 八、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性     | IMX6ULL (Linux)   | STM32 (FreeRTOS)   | RK3568 (Linux)    |
 | -------- | ----------------- | ------------------ | ----------------- |
 | 阻塞IO   | 等待队列          | 任务通知/信号量    | 等待队列          |
@@ -931,6 +1053,8 @@ modprobe asyncnoti.ko
 
 ## 九、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:阻塞IO和非阻塞IO有什么区别?
 
 **考察点**:IO模型理解
@@ -1008,3 +1132,4 @@ modprobe asyncnoti.ko
 
 **代码来源**: Linux驱动例程 14_blockio, 15_noblockio, 16_asyncnoti
 **最后更新**: 2026-09-17
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第52章 Linux阻塞和非阻塞IO实验、第53章 异步通知实验

+ 118 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/07-platform总线模型.md

@@ -87,6 +87,77 @@ graph TB
 - **platform_device**: 描述设备信息(寄存器地址、中断号等)
 - **platform_driver**: 实现驱动逻辑,提供probe/remove函数
 
+**总线本质是 `bus_type` 结构体**(原书示例代码 54.2.1.1,定义在 `include/linux/device.h`),其中 `match` 函数负责设备与驱动的匹配:
+
+```c
+/* 文件: include/linux/device.h(节选) */
+struct bus_type {
+    const char      *name;                        /* 总线名字 */
+    const char      *dev_name;
+    struct device   *dev_root;
+    struct device_attribute *dev_attrs;
+    const struct attribute_group **bus_groups;    /* 总线属性 */
+    const struct attribute_group **dev_groups;    /* 设备属性 */
+    const struct attribute_group **drv_groups;    /* 驱动属性 */
+
+    int (*match)(struct device *dev, struct device_driver *drv);
+    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
+    int (*probe)(struct device *dev);
+    int (*remove)(struct device *dev);
+    void (*shutdown)(struct device *dev);
+    int (*online)(struct device *dev);
+    int (*offline)(struct device *dev);
+    int (*suspend)(struct device *dev, pm_message_t state);
+    int (*resume)(struct device *dev);
+    const struct dev_pm_ops *pm;
+    const struct iommu_ops *iommu_ops;
+    struct subsys_private *p;
+    struct lock_class_key lock_key;
+};
+```
+
+`platform_bus_type` 就是 platform 总线的 `bus_type` 实例(原书示例代码 54.2.1.2):
+
+```c
+/* 文件: drivers/base/platform.c */
+struct bus_type platform_bus_type = {
+    .name        = "platform",
+    .dev_groups  = platform_dev_groups,
+    .match       = platform_match,
+    .uevent      = platform_uevent,
+    .pm          = &platform_dev_pm_ops,
+};
+```
+
+匹配函数 `platform_match()` 的完整实现(原书示例代码 54.2.1.3,定义在 `drivers/base/platform.c`):
+
+```c
+static int platform_match(struct device *dev, struct device_driver *drv)
+{
+    struct platform_device *pdev = to_platform_device(dev);
+    struct platform_driver *pdrv = to_platform_driver(drv);
+
+    /* When driver_override is set, only bind to the matching driver */
+    if (pdev->driver_override)
+        return !strcmp(pdev->driver_override, drv->name);
+
+    /* Attempt an OF style match first */
+    if (of_driver_match_device(dev, drv))
+        return 1;
+
+    /* Then try ACPI style match */
+    if (acpi_driver_match_device(dev, drv))
+        return 1;
+
+    /* Then try to match against the id table */
+    if (pdrv->id_table)
+        return platform_match_id(pdrv->id_table, pdev) != NULL;
+
+    /* fall-back to driver name match */
+    return (strcmp(pdev->name, drv->name) == 0);
+}
+```
+
 ### 1.3 platform_device vs platform_driver
 
 | 特性         | platform_device                   | platform_driver                   |
@@ -173,6 +244,16 @@ struct platform_driver {
 | `driver`   | 基类device_driver,包含name、of_match_table等  |
 | `id_table` | 用于传统name匹配方式                           |
 
+`id_table` 是数组,每个元素的类型为 `struct platform_device_id`(原书示例代码 54.2.2.2):
+
+```c
+/* 文件: include/linux/platform_device.h */
+struct platform_device_id {
+    char name[PLATFORM_NAME_SIZE];   /* 设备/驱动名,用于 id_table 匹配 */
+    kernel_ulong_t driver_data;      /* 驱动私有数据 */
+};
+```
+
 ### 2.3 struct device_driver详解
 
 ```c
@@ -548,6 +629,31 @@ MODULE_LICENSE("GPL");
 MODULE_AUTHOR("zuozhongkai");
 ```
 
+### 4.5 核心API原型汇总(原书)
+
+```c
+/* ---------- platform 驱动 ---------- */
+/* 注册 platform 驱动,返回 0 成功,负数失败    —— 54.2.2 */
+int  platform_driver_register(struct platform_driver *driver);
+/* 卸载 platform 驱动,无返回值                  —— 54.2.2 */
+void platform_driver_unregister(struct platform_driver *drv);
+
+/* ---------- platform 设备(无设备树方式) ---------- */
+/* 注册 platform 设备,返回 0 成功,负数失败    —— 54.2.3 */
+int  platform_device_register(struct platform_device *pdev);
+/* 注销 platform 设备,无返回值                 —— 54.2.3 */
+void platform_device_unregister(struct platform_device *pdev);
+
+/* ---------- 驱动 probe 中获取资源 ---------- */
+/* 按类型/序号获取资源,type 可为 IORESOURCE_MEM/IORESOURCE_IRQ */
+struct resource *platform_get_resource(struct platform_device *dev,
+                                       unsigned int type, unsigned int num);
+/* 直接获取中断号,返回负值表示失败 */
+int platform_get_irq(struct platform_device *dev, unsigned int num);
+```
+
+> **原书要点**:对于支持设备树的 Linux 版本,设备驱动一般同时支持设备树和无设备树两种匹配方式。也就是第一种(OF/compatible)匹配方式一般都会存在,第三种(id_table)和第四种(name)只要存在一种即可,用得最多的还是第四种直接比较 name 字段,因为最简单。
+
 ---
 
 ## 五、完整源码分析
@@ -1154,6 +1260,10 @@ make -j32
 arm-linux-gnueabihf-gcc ledApp.c -o ledApp
 ```
 
+> **无设备树方式(第五十四章)的差异**:该实验需要**两个模块**,Makefile 中 `obj-m := leddevice.o leddriver.o`,编译后生成 `leddevice.ko`(设备模块)和 `leddriver.ko`(驱动模块),加载时两个都要 `modprobe`;设备节点为 `/dev/platled`,测试命令为 `./ledApp /dev/platled 1|0`。
+>
+> 设备树方式(第五十五章)只需要一个 `leddriver.c`,`obj-m := leddriver.o`,设备节点为 `/dev/dtsplatled`。
+
 ### 6.3 加载测试
 
 ```bash
@@ -1195,6 +1305,8 @@ rmmod leddriver.ko
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性       | IMX6ULL                           | STM32 (Linux)                 | RK3568                             |
 | ---------- | --------------------------------- | ----------------------------- | ---------------------------------- |
 | 平台总线   | platform虚拟总线                  | platform虚拟总线              | platform虚拟总线                   |
@@ -1215,6 +1327,8 @@ rmmod leddriver.ko
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:platform总线模型的核心思想是什么?为什么需要它?
 
 **考察点**:驱动分离与分层
@@ -1333,3 +1447,7 @@ static int xxx_probe(struct platform_device *dev) {
 ```
 
 **总结**:platform驱动是字符设备驱动的一种封装形式,目的是实现驱动的分离与分层。字符设备驱动的核心逻辑仍然在probe函数中实现。
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第五十四章 platform设备驱动实验、第五十五章 设备树下的platform驱动编写

+ 103 - 3
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/08-misc与input子系统.md

@@ -10,7 +10,7 @@ pdf_ref:
 
 # MISC与INPUT子系统
 
-> 💡 **关联知识**: [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[05-Linux外设驱动实战/02-按键输入驱动]] | [[03-Linux驱动开发核心/04-platform总线驱动]]
+> 💡 **关联知识**: [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[05-Linux外设驱动实战/02-按键输入驱动]] | [[03-Linux驱动开发核心/07-platform总线模型]]
 
 ---
 
@@ -206,7 +206,7 @@ sequenceDiagram
 
 ### 4.1 驱动程序 miscbeep.c
 
-以下代码来自正点原子 I.MX6ULL 开发板例程 `19_misc`,采用 platform + MISC 框架实现蜂鸣器驱动。
+以下代码来自正点原子 I.MX6ULL 开发板例程 `17_misc`(书中工程目录名为 `19_miscbeep`,采用 platform + MISC 框架实现蜂鸣器驱动。
 
 ```c
 #include <linux/types.h>
@@ -433,6 +433,34 @@ int main(int argc, char *argv[])
 
 INPUT子系统是Linux内核专门为输入设备(按键、键盘、鼠标、触摸屏等)设计的框架。输入设备本质上是字符设备,但INPUT子系统在其上封装了**标准化的事件上报机制**,驱动开发者只需关注"上报事件",无需关心用户空间如何处理。
 
+**核心层会向内核注册一个字符设备**(原书示例代码 58.1.2.1,`drivers/input/input.c`),主设备号为 `INPUT_MAJOR`:
+
+```c
+/* 文件: include/uapi/linux/major.h */
+#define INPUT_MAJOR   13
+
+/* 文件: drivers/input/input.c(节选) */
+static int __init input_init(void)
+{
+    int err;
+
+    err = class_register(&input_class);      /* 注册 input 类,/sys/class/input */
+    if (err) { ... }
+
+    err = input_proc_init();
+    if (err) goto fail1;
+
+    err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
+                                 INPUT_MAX_CHAR_DEVICES, "input");
+    if (err) { ... }
+
+    return 0;
+    ...
+}
+```
+
+因此 input 子系统的所有设备主设备号都为 13。使用 input 子系统时**不需要自己注册字符设备**,只需向系统注册一个 `input_dev` 即可。
+
 ### 5.2 INPUT子系统架构图
 
 ```mermaid
@@ -821,6 +849,70 @@ void timer_function(unsigned long arg)
 }
 ```
 
+### 7.5 INPUT子系统常用API原型(原书)
+
+```c
+/* ---------- input_dev 申请/注册 ---------- */
+/* 申请一个 input_dev,返回申请到的 input_dev */
+struct input_dev *input_allocate_device(void);
+/* 释放 input_dev,无返回值 */
+void input_free_device(struct input_dev *dev);
+/* 注册 input_dev,返回 0 成功,负值失败 */
+int  input_register_device(struct input_dev *dev);
+/* 注销 input_dev,无返回值 */
+void input_unregister_device(struct input_dev *dev);
+
+/* ---------- 初始化事件类型/事件码 ---------- */
+/* 设置 input_dev 支持某类事件及事件码(如 input_set_capability(dev, EV_KEY, KEY_0)) */
+void input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code);
+
+/* ---------- 上报事件 ---------- */
+/* 上报指定事件及值,可上报所有事件类型 */
+void input_event(struct input_dev *dev, unsigned int type,
+                 unsigned int code, int value);
+/* 上报按键事件,本质是 input_event(dev, EV_KEY, code, !!value) */
+void input_report_key(struct input_dev *dev, unsigned int code, int value);
+/* 其他专用上报函数,内部都调用 input_event */
+void input_report_rel(struct input_dev *dev, unsigned int code, int value);
+void input_report_abs(struct input_dev *dev, unsigned int code, int value);
+void input_report_ff_status(struct input_dev *dev, unsigned int code, int value);
+void input_report_switch(struct input_dev *dev, unsigned int code, int value);
+void input_mt_sync(struct input_dev *dev);
+/* 上报同步事件(EV_SYN),告诉内核本次上报结束,必须调用 */
+void input_sync(struct input_dev *dev);
+```
+
+> **注意**:`input_report_key()` 的定义是 `input_event(dev, EV_KEY, code, !!value)`,`!!value` 把值归一化为 0/1。按键上报后**必须**调用 `input_sync()` 上报 `EV_SYN` 同步事件,这一步一定得做。
+
+### 7.6 内核自带 gpio_keys 驱动(原书 58.5)
+
+内核自带按键驱动 `drivers/input/keyboard/gpio_keys.c`,采用 platform + input 框架,匹配表为:
+
+```c
+static const struct of_device_id gpio_keys_of_match[] = {
+    { .compatible = "gpio-keys", },
+    { },
+};
+```
+
+使用方式:使能内核配置 `CONFIG_KEYBOARD_GPIO=y`(`Device Drivers -> Input device support -> Keyboards -> GPIO Buttons`),并在设备树中添加 `gpio-keys` 节点:
+
+```dts
+gpio-keys {
+    compatible = "gpio-keys";
+    #address-cells = <1>;
+    #size-cells = <0>;
+    autorepeat;                                  /* 支持连按 */
+    key0 {
+        label = "GPIO Key Enter";
+        linux,code = <KEY_ENTER>;                /* 模拟为回车键 */
+        gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;     /* KEY0 使用 GPIO1_IO18 */
+    };
+};
+```
+
+要点:节点名必须为 `gpio-keys`,`compatible` 必须为 `"gpio-keys"`;所有按键是 `gpio-keys` 的子节点,用 `gpios`、`label`、`linux,code` 描述;支持连按需加 `autorepeat`。
+
 ---
 
 ## 八、实验验证
@@ -848,6 +940,9 @@ arm-linux-gnueabihf-gcc keyinputApp.c -o keyinputApp
 depmod                          # 首次加载需要
 modprobe miscbeep.ko            # 加载模块
 
+# 查看MISC类下的设备(所有misc设备都属于misc类)
+ls /sys/class/misc/             # 会看到名为miscbeep的子目录
+
 # 查看设备节点
 ls /dev/miscbeep -l             # 主设备号10,次设备号144
 
@@ -917,6 +1012,8 @@ hexdump /dev/input/event1
 
 ## 九、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 9.1 IMX6ULL vs STM32 vs RK3568
 
 | 特性             | IMX6ULL                | STM32 HAL              | RK3568                 |
@@ -961,6 +1058,8 @@ flowchart TB
 
 ## 十、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:MISC设备和普通字符设备有什么区别?为什么需要MISC设备?
 
 **考察点**: MISC设备机制
@@ -1031,5 +1130,6 @@ struct input_event {
 
 ---
 
-**代码来源**: Linux驱动例程 `19_miscbeep`(MISC蜂鸣器驱动)、`20_input`(INPUT按键驱动)
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第五十七章 Linux MISC驱动实验、第五十八章 Linux INPUT子系统实验
+**代码来源**: Linux驱动例程 `17_misc`(MISC蜂鸣器驱动,书中工程目录 `19_miscbeep`)、`20_input`(INPUT按键驱动)
 **最后更新**: 2026-09-17

+ 114 - 32
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/03-Linux驱动开发核心/面试-Linux驱动开发.md

@@ -144,6 +144,18 @@ static int __init newchrled_init(void)
 | 内核推荐度    | 已过时                             | 推荐                          |
 | 底层实现      | 内部同样是封装 cdev                | 直接操作 cdev                 |
 
+> **设备号基础**(原书 40.3.1 节):`dev_t` 本质是 `unsigned int`(32 位),**高 12 位为主设备号(范围 0~4095),低 20 位为次设备号**。相关宏定义在 `include/linux/kdev_t.h`:
+>
+> ```c
+> #define MINORBITS     20
+> #define MINORMASK     ((1U << MINORBITS) - 1)
+> #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
+> #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
+> #define MKDEV(ma,mi)  (((ma) << MINORBITS) | (mi))
+> ```
+>
+> 因此选择主设备号时不要超过 4095;`register_chrdev` 会一次性占用一个主设备号下全部 256 个次设备号。
+
 **追问**:
 
 - 追问1:`alloc_chrdev_region` 和 `register_chrdev_region` 该如何选择?动态申请后如何让用户知道主设备号?(`/proc/devices`)
@@ -195,7 +207,7 @@ struct file {
 };
 ```
 
-`struct file_operations`(节选):
+`struct file_operations`(节选,签名以原书第四十章示例代码 40.1.1 为准):
 
 ```c
 struct file_operations {
@@ -203,8 +215,9 @@ struct file_operations {
     loff_t (*llseek)(struct file *, loff_t, int);
     ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
     ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
-    __poll_t (*poll)(struct file *, struct poll_table_struct *);
-    int (*ioctl)(struct file *, unsigned int, unsigned long);
+    unsigned int (*poll)(struct file *, struct poll_table_struct *);
+    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
+    long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
     int (*open)(struct inode *, struct file *);
     int (*release)(struct inode *, struct file *);
     int (*fasync)(int, struct file *, int);
@@ -212,6 +225,8 @@ struct file_operations {
 };
 ```
 
+> 注意:老资料里常见 `int (*ioctl)(...)` 的写法,那是更老内核的成员;本书使用的 4.1.15 内核里对应成员为 `unlocked_ioctl`(32 位应用在 64 位系统上则走 `compat_ioctl`),`poll` 的返回类型为 `unsigned int`。
+
 三者的关系可用下图理解:
 
 ```
@@ -297,6 +312,14 @@ static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
 }
 ```
 
+两个函数的原型(原书第四十章给出 `copy_to_user` 原型,`copy_from_user` 与之对称):
+
+```c
+/* 文件: include/linux/uaccess.h */
+static inline long copy_to_user(void __user *to, const void *from, unsigned long n);
+static inline long copy_from_user(void *to, const void __user *from, unsigned long n);
+```
+
 为什么不能用 memcpy:
 
 | 场景             | memcpy 直接访问用户指针 | copy_*_user                      |
@@ -319,6 +342,8 @@ if (copy_from_user(&data, buf, sizeof(data)))
 
 注意一个正点原子教程里的常见写法问题:`if (retvalue < 0)` 判断是**不严谨的**,因为 `copy_*_user` 返回的是 `unsigned long` 类型的未拷贝字节数,它不会为负。很多老代码沿用这个写法是因为 `int` 接收时行为上恰好能进入分支,但严格判断应该是 `!= 0`。
 
+> **原书表述不准确**:正点原子第四十章原文写的是"如果复制成功,返回值为 0,如果复制失败则返回负数"。按内核实际实现,`copy_to_user/copy_from_user` 返回的是**未拷贝的字节数**(`unsigned long`),失败时该值大于 0 而非负数,正确写法是判断 `!= 0`。
+
 **追问**:
 
 - 追问1:`copy_to_user` 返回值到底是"成功拷贝的字节数"还是"未拷贝的字节数"?如何据此返回正确的系统调用结果?
@@ -904,8 +929,14 @@ int of_property_read_string(const struct device_node *np,
 int of_property_read_u32_array(const struct device_node *np,
                                const char *propname, u32 *out_values,
                                size_t sz);
-int of_property_count_u32_elems(const struct device_node *np,
-                                const char *propname);
+/* 统计属性元素数量(原书 4.1.15 内核用此函数;elem_size 为单个元素字节数) */
+int of_property_count_elems_of_size(const struct device_node *np,
+                                    const char *propname, int elem_size);
+int of_property_read_u32_index(const struct device_node *np,
+                               const char *propname, u32 index,
+                               u32 *out_value);
+int of_n_addr_cells(struct device_node *np);      /* 读 #address-cells */
+int of_n_size_cells(struct device_node *np);      /* 读 #size-cells */
 
 /* GPIO / 中断 */
 int of_get_named_gpio(struct device_node *np, const char *propname, int index);
@@ -936,7 +967,7 @@ base = ioremap(res.start, resource_size(&res));
 
 **追问**:
 
-- 追问1:`of_property_read_u32_array` 返回负值说明什么?如何先用 `of_property_count_u32_elems` 探测长度?
+- 追问1:`of_property_read_u32_array` 返回负值说明什么?如何先用 `of_property_count_elems_of_size` 探测长度?(原书 4.1.15 内核没有 `of_property_count_u32_elems`,该函数是后续内核新增的便利接口)
 - 追问2:`of_find_node_by_path` 里的路径是 `/atkalientek`,为什么不用 `/soc/atkalientek`?(取决于节点实际层级,DT 是树状路径)
 - 追问3:`ioremap` 得到的指针必须用 `readl/writel` 访问,为什么不能直接 `*base = x`?(内存屏障与端序,`__iomem` 标注)
 
@@ -1187,18 +1218,20 @@ mux_reg  = 0x020E0000 + 0x008C = 0x020E008C  (IOMUXC_SW_MUX_CTL_PAD_...)
 conf_reg = 0x020E0000 + 0x0318 = 0x020E0318  (IOMUXC_SW_PAD_CTL_PAD_...)
 ```
 
-电气属性值(`fsl,pins` 每行最后那个值)的位定义:
+电气属性值(`fsl,pins` 每行最后那个值,即 `IOMUXC_SW_PAD_CTL_PAD_xxx` 寄存器值)的位定义(据原书第八章 GPIO 寄存器讲解)
 
 ```c
-bit 0:    SRE    压摆率 (0:低速 1:高速)
-bit 1-2:  DSE    驱动强度
-bit 3-4:  SPEED  速度等级 (11=200MHz)
-bit 5:    ODE    开漏使能
-bit 6:    PKE    上下拉使能
-bit 7:    PUE    上下拉选择 (0:下拉 1:上拉)
-bit 11:   HYS    施密特滞后使能
+bit 0:     SRE          压摆率 (0:低压摆率 1:高压摆率)
+bit 5:3:   DSE          驱动能力 (当IO用作输出时设置驱动能力)
+bit 7:6:   SPEED        速度等级 (2'b11 = 200MHz)
+bit 11:    ODE          开漏输出使能 (0:禁止 1:使能)
+bit 12:    PKE          上下拉/状态保持器使能
+bit 13:    PUE          上下拉选择 (0:下拉 1:上拉/状态保持器)
+bit 15:14: PUS          上下拉电阻值 (4档)
+bit 16:    HYS          迟滞比较器使能 (IO作输入时有用)
 ```
 
+
 多状态 pinctrl(低功耗场景):
 
 ```dts
@@ -1425,7 +1458,8 @@ devmem2 0x0209C000 w      # GPIO1_DR
 devmem2 0x0209C004 w      # GPIO1_GDIR
 
 # 读 MUX 寄存器,确认引脚功能是否被配成 GPIO
-devmem2 0x020E008C w      # 对应 GPIO1_IO03 的 MUX 寄存器
+devmem2 0x020E0068 w      # GPIO1_IO03 的 IOMUXC_SW_MUX_CTL_PAD 寄存器(0x020E0000+0x0068)
+devmem2 0x020E02F4 w      # GPIO1_IO03 的 IOMUXC_SW_PAD_CTL_PAD 寄存器(0x020E0000+0x02F4)
 ```
 
 排查清单:
@@ -1546,9 +1580,14 @@ typedef struct {
 | `atomic_set(v, i)`                      | 设值               |
 | `atomic_inc(v)` / `atomic_dec(v)`       | 自增 / 自减        |
 | `atomic_add(i, v)` / `atomic_sub(i, v)` | 加 / 减            |
+| `atomic_dec_return(v)`                  | 自减并返回 v 的值  |
+| `atomic_inc_return(v)`                  | 自增并返回 v 的值  |
 | `atomic_dec_and_test(v)`                | 自减并测试是否为 0 |
 | `atomic_inc_and_test(v)`                | 自增并测试是否为 0 |
 | `atomic_sub_and_test(i, v)`             | 减并测试是否为 0   |
+| `atomic_add_negative(i, v)`             | 加 i 后结果为负返回真 |
+
+对应 64 位版本为 `atomic64_t`(`typedef struct { long long counter; } atomic64_t;`),API 前缀换为 `atomic64_`、类型换为 `long long`。本书 Cortex-A7 为 32 位架构,只用 32 位原子操作。
 
 设备互斥访问的经典用法:
 
@@ -1586,9 +1625,10 @@ atomic_set(&gpioled.lock, 1);
 void set_bit(int nr, void *p);          /* 置位 */
 void clear_bit(int nr, void *p);        /* 清位 */
 void change_bit(int nr, void *p);       /* 翻转 */
-int  test_bit(int nr, void *p);         /* 测试 */
+int  test_bit(int nr, void *p);         /* 测试,返回该位原值 */
 int  test_and_set_bit(int nr, void *p); /* 测试并置位,返回原值 */
-int  test_and_clear_bit(int nr, void *p);
+int  test_and_clear_bit(int nr, void *p);/* 测试并清位,返回原值 */
+int  test_and_change_bit(int nr, void *p);/* 测试并翻转,返回原值 */
 ```
 
 底层原理:
@@ -1638,27 +1678,37 @@ x86 的 lock 前缀:
 
 ```c
 /* 自旋锁 */
-DEFINE_SPINLOCK(lock);                    /* 静态定义 */
+DEFINE_SPINLOCK(lock);                    /* 静态定义并初始化 */
 spin_lock_init(&lock);                    /* 动态初始化 */
 spin_lock(&lock);
 spin_unlock(&lock);
-spin_lock_irqsave(&lock, flags);          /* 中断安全 */
+spin_trylock(&lock);                      /* 尝试获取,失败返回 0 */
+spin_is_locked(&lock);                    /* 已被持有返回非 0 */
+spin_lock_irq(&lock);                     /* 禁止本地中断 + 加锁 */
+spin_unlock_irq(&lock);
+spin_lock_irqsave(&lock, flags);          /* 保存中断状态 + 关本地中断 + 加锁 */
 spin_unlock_irqrestore(&lock, flags);
+spin_lock_bh(&lock);                      /* 关闭下半部 + 加锁 */
+spin_unlock_bh(&lock);
 
 /* 信号量 */
 struct semaphore sem;
+DEFINE_SEMAPHORE(name);                   /* 定义并初始化为 1 */
 sema_init(&sem, 1);                       /* 计数为 1 即互斥 */
-down(&sem);                               /* 不可断 */
+down(&sem);                               /* 不可被信号打断 */
 down_interruptible(&sem);                 /* 可被信号打断 */
-down_trylock(&sem);                       /* 非阻塞 */
+down_trylock(&sem);                       /* 非阻塞,成功返回 0 */
 up(&sem);
 
 /* 互斥体 */
 struct mutex lock;
+DEFINE_MUTEX(name);                       /* 静态定义并初始化 */
 mutex_init(&lock);
 mutex_lock(&lock);
 mutex_unlock(&lock);
-mutex_trylock(&lock);
+mutex_trylock(&lock);                     /* 成功返回 1,失败返回 0 */
+mutex_is_locked(&lock);                   /* 被持有返回 1 */
+mutex_lock_interruptible(&lock);          /* 可被信号打断 */
 ```
 
 使用示例:
@@ -1706,6 +1756,11 @@ static ssize_t led_write(struct file *filp, const char __user *buf,
 | 临界区时长 | 极短(μs)      | 可长(ms)       | 可长              |
 | 典型场景   | 中断、短临界区  | 计数并发控制     | 通用互斥          |
 
+**其他衍生锁**(原书 47.3.3 节):在自旋锁基础上还衍生出两类锁,多在 Linux 内核内部使用,普通驱动用得不多——
+
+- **读写自旋锁 `rwlock_t`**:适用于"读多写少"或"生产者/消费者"模型。无写操作时允许多个线程同时持有读锁并发读;写锁独占,持有写锁时不能读。API:`read_lock/read_unlock`、`write_lock/write_unlock`(均有 `_irq/_irqsave/_bh` 变体)。
+- **顺序锁 `seqlock_t`**:允许"读和写同时进行",但不允许并发写。读用 `read_seqbegin()` 取序号、读完用 `read_seqretry()` 校验期间是否发生过写,若有则重读。注意:顺序锁保护的资源**不能是指针**,否则写时指针可能失效导致读方崩溃。
+
 选择决策:
 
 ```
@@ -1789,7 +1844,7 @@ spin_unlock_bh(&lock);     /* 与上半部并发保护的场景 */
 **追问**:
 
 - 追问1:`spin_lock_irqsave` 里的 `flags` 变量为什么必须传同一个?(恢复的是加锁前的中断状态)
-- 追问2:在中断处理函数里加锁,还需要 `irqsave` 吗?(中断上下文本身不会被同 CPU 中断打断,但共享锁的进程上下文仍可能,所以要统一用 irqsave
+- 追问2:在中断处理函数里加锁,还需要 `irqsave` 吗?(**原书用法**:线程中使用 `spin_lock_irqsave/spin_unlock_irqrestore`、中断中使用 `spin_lock/spin_unlock`——线程侧持锁时已禁止本地中断,中断侧才不会在本 CPU 上自旋死锁;若线程侧只用 `spin_lock` 而中断也访问同一数据,则两边都必须关中断。在中断里用 `irqsave` 变体同样安全
 - 追问3:什么时候用 `spin_lock_bh`?(与软中断/tasklet 共享数据时)
 
 ---
@@ -2115,7 +2170,7 @@ request_threaded_irq(irq, key_hard, key_thread,
 
 - 追问1:内核为什么"不鼓励"普通驱动直接用软中断?(并发性高、易出错、需自己处理每 CPU 重入)
 - 追问2:卸载模块时忘记 `cancel_work_sync` 会导致什么问题?(work 在模块卸载后执行 → oops)
-- 追问3:`IRQF_ONESHOT` 的作用是什么?(线程化中断期间保持中断线屏蔽,防止重复触发)
+- 追问3:`IRQF_ONESHOT` 的作用是什么?(原书描述为"单次中断,中断执行一次就结束";更准确地说,它让中断线在处理完成前保持屏蔽,线程化中断期间防止重复触发)
 
 ---
 
@@ -2195,13 +2250,16 @@ flags 常用值:
 
 | flag                  | 含义                   |
 | --------------------- | ---------------------- |
+| IRQF_TRIGGER_NONE     | 无触发                 |
 | IRQF_TRIGGER_RISING   | 上升沿触发             |
 | IRQF_TRIGGER_FALLING  | 下降沿触发             |
 | IRQF_TRIGGER_HIGH/LOW | 高/低电平触发          |
 | IRQF_SHARED           | 共享中断线             |
-| IRQF_ONESHOT          | 线程化中断期间保持屏蔽 |
+| IRQF_ONESHOT          | 单次中断(中断线上保持屏蔽,线程化中断常用) |
 | IRQF_NO_SUSPEND       | 休眠期间不关闭         |
 
+> **原书要点**:`request_irq` 可能会导致睡眠,因此不能在中断上下文或其他禁止睡眠的代码段中使用;`request_irq` 会激活(使能)中断,不需要我们手动去使能。返回 `-EBUSY` 表示中断已经被申请。
+
 中断处理函数:
 
 ```c
@@ -2491,15 +2549,21 @@ static void __exit imx6uirq_exit(void)
 - 但 SoC 内部的 GPIO 控制器、UART、SPI 控制器、LCD 控制器等**没有可枚举的物理总线**,需要一种机制把"硬件资源"和"驱动程序"关联起来。
 - platform 总线就是为此设计的"虚拟总线"。
 
-`platform_device` 描述硬件资源:
+`platform_device` 描述硬件资源(原书示例代码 54.2.3.1)
 
 ```c
 struct platform_device {
-    const char      *name;          /* 匹配用(非 DT 方式) */
-    int              id;
+    const char      *name;          /* 设备名,要和驱动的 name 相同(非 DT 方式) */
+    int              id;            /* 设备 ID,-1 表示自动分配 */
+    bool             id_auto;       /* 是否自动分配 ID */
     struct device    dev;           /* 内含 of_node,指向设备树节点 */
     u32              num_resources;
     struct resource *resource;      /* 资源数组:内存、中断 */
+
+    const struct platform_device_id *id_entry;  /* id_table 匹配项 */
+    char *driver_override;          /* 强制绑定的驱动名 */
+    struct mfd_cell *mfd_cell;      /* MFD cell pointer */
+    struct pdev_archdata archdata;  /* arch specific additions */
 };
 
 struct resource {
@@ -2507,19 +2571,24 @@ struct resource {
     resource_size_t end;            /* 结束 */
     const char     *name;
     unsigned long   flags;          /* IORESOURCE_MEM / IORESOURCE_IRQ */
+    struct resource *parent, *sibling, *child;  /* 资源树 */
 };
 ```
 
 设备树方式下,platform_device 由内核从 DT 自动生成,`dev.of_node` 指向对应节点。
 
-`platform_driver` 描述驱动行为:
+`platform_driver` 描述驱动行为(原书示例代码 54.2.2.1)
 
 ```c
 struct platform_driver {
     int  (*probe)(struct platform_device *);
     int  (*remove)(struct platform_device *);
     void (*shutdown)(struct platform_device *);
+    int  (*suspend)(struct platform_device *, pm_message_t state);
+    int  (*resume)(struct platform_device *);
     struct device_driver driver;    /* 内含 name、of_match_table */
+    const struct platform_device_id *id_table;  /* id_table 匹配表 */
+    bool prevent_deferred_probe;
 };
 ```
 
@@ -2546,7 +2615,7 @@ struct platform_driver {
 
 1. 传统匹配:`platform_device->name` 与 `platform_driver->driver.name` 字符串相等
 2. 设备树匹配:`of_device_id.compatible` 与 DTS 节点 `compatible` 相等(现代主流)
-3. 还有 ACPI 匹配、id_table 匹配等,优先级一般为 of > acpi > id > name
+3. 还有 ACPI 匹配、id_table 匹配等,`platform_match()` 中的优先级为 `driver_override` > of > acpi > id_table > name
 4. `of_match_table` 放在 `platform_driver.driver.of_match_table`,配合 `MODULE_DEVICE_TABLE(of, ...)`
 5. `module_platform_driver(x)` 展开为 `module_init(x##_init)` + `module_exit(x##_exit)`,其中 init/exit 内部调用 `platform_driver_register/unregister`
 
@@ -2603,6 +2672,13 @@ gpioled {
 ```c
 static int platform_match(struct device *dev, struct device_driver *drv)
 {
+    struct platform_device *pdev = to_platform_device(dev);
+    struct platform_driver *pdrv = to_platform_driver(drv);
+
+    /* 0. driver_override 强制匹配(设置后只绑定指定驱动) */
+    if (pdev->driver_override)
+        return !strcmp(pdev->driver_override, drv->name);
+
     /* 1. 先按 of_match_table / compatible 匹配 */
     if (of_driver_match_device(dev, drv))
         return 1;
@@ -2612,7 +2688,8 @@ static int platform_match(struct device *dev, struct device_driver *drv)
         return 1;
 
     /* 3. 再按 id_table 匹配 */
-    ...
+    if (pdrv->id_table)
+        return platform_match_id(pdrv->id_table, pdev) != NULL;
 
     /* 4. 最后按 name 匹配 */
     return strcmp(pdev->name, drv->name) == 0;
@@ -3048,6 +3125,7 @@ gpioled {
 ## 七、IO 模型
 
 > 对应笔记:[[03-Linux驱动开发核心/06-阻塞IO与poll机制]]
+> 对应原文:《I.MX6U嵌入式Linux驱动开发指南》第52章 Linux阻塞和非阻塞IO实验、第53章 异步通知实验
 
 ### Q31: Linux 的五种 IO 模型分别是什么?各自的特点和适用场景?
 
@@ -3689,5 +3767,9 @@ static struct file_operations imx6uirq_fops = {
 
 > 💡 **关联笔记复盘**: [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[03-Linux驱动开发核心/02-设备树语法与实战]] | [[03-Linux驱动开发核心/03-pinctrl与gpio子系统]] | [[03-Linux驱动开发核心/04-并发同步与原子操作]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/06-阻塞IO与poll机制]] | [[03-Linux驱动开发核心/07-platform总线模型]] | [[03-Linux驱动开发核心/08-misc与input子系统]] | [[02-嵌入式Linux内核基础/06-设备模型与Kobject]]
 >
-> **资料来源**: 【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 第40/43/45/47/51/54章
+> **资料来源**: 【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 第40/43/45/47/51/52/53/54章
 > **最后更新**: 2026-09-17
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第40章 字符设备驱动开发、第43章 Linux设备树、第45章 pinctrl和gpio子系统实验、第47章 Linux并发与竞争、第51章 Linux中断实验、第52章 Linux阻塞和非阻塞IO实验、第53章 异步通知实验、第54章 platform设备驱动实验

+ 80 - 6
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/01-I2C驱动开发.md

@@ -14,6 +14,8 @@ pdf_ref: "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第六十一
 
 ## 一、I2C协议概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十一章内容(I2C 物理协议在裸机篇第二十六章讲解),为扩展知识。
+
 ### 1.1 I2C总线原理
 
 I2C(Inter-Integrated Circuit)是Philips公司开发的两线式串行通信总线,广泛用于连接低速外设。
@@ -257,6 +259,42 @@ struct i2c_msg {
 | `I2C_M_STOP`         | 0x8000 | 传输后发送停止条件 |
 | `I2C_M_NOSTART`      | 0x4000 | 不发送起始条件     |
 | `I2C_M_REV_DIR_ADDR` | 0x2000 | 反转读写方向       |
+| `I2C_M_IGNORE_NAK`   | 0x1000 | 忽略 NAK           |
+| `I2C_M_NO_RD_ACK`    | 0x0800 | 读操作不发送 ACK   |
+| `I2C_M_RECV_LEN`     | 0x0400 | 首字节为长度       |
+
+> 以上 8 个标志与《I.MX6U嵌入式Linux驱动开发指南》示例代码 61.3.2.1 中 `i2c_msg` 结构体内的定义一致。
+
+#### 2.2.6 i2c_board_info — 非设备树下的I2C设备描述
+
+在未使用设备树时,内核使用 `i2c_board_info` 描述一个具体的 I2C 设备(原书示例代码 61.3.1.1):
+
+```c
+struct i2c_board_info {
+    char        type[I2C_NAME_SIZE];   /* I2C 设备名字 */
+    unsigned short  flags;             /* 标志 */
+    unsigned short  addr;              /* I2C 器件地址 */
+    void        *platform_data;
+    struct dev_archdata *archdata;
+    struct device_node *of_node;
+    struct fwnode_handle *fwnode;
+    int     irq;
+};
+```
+
+其中 `type` 和 `addr` 必须设置。原书用 `I2C_BOARD_INFO` 宏来初始化(示例代码 61.3.1.3):
+
+```c
+#define I2C_BOARD_INFO(dev_type, dev_addr) \
+    .type = dev_type, .addr = (dev_addr)
+
+/* 原书示例:OV2640 摄像头,器件地址 0x30 */
+static struct i2c_board_info mx27_3ds_i2c_camera = {
+    I2C_BOARD_INFO("ov2640", 0x30),
+};
+```
+
+采用设备树后不再使用 `i2c_board_info`,改为在 I2C 控制器节点下创建子节点。
 
 ### 2.3 设备与驱动匹配过程
 
@@ -358,6 +396,8 @@ i2c1: i2c@021a0000 {
 };
 ```
 
+> **重要**:`imx6ull-alientek-emmc.dts` 中 `&i2c1` 节点默认带有 NXP 官方 EVK 的两个子节点 `mag3110@0e`(磁力计,compatible 为 `"fsl,mag3110"`)和 `fxls8471@1e`(加速度计,compatible 为 `"fsl,fxls8471"`)。正点原子 I.MX6U-ALPHA 开发板**没有**这两个器件,必须先将它们删除,再添加 `ap3216c@1e` 子节点(原书示例代码 61.5.1.3)。否则 `fxls8471@1e` 会与 AP3216C 争夺同一地址 0x1e。
+
 **pinctrl配置**:
 
 ```dts
@@ -389,11 +429,25 @@ cat /sys/bus/i2c/devices/0-001e/name
 
 ### 4.1 驱动注册/注销
 
-| 函数                                 | 说明                    |
-| ------------------------------------ | ----------------------- |
-| `i2c_add_driver(driver)`             | 注册I2C设备驱动(常用) |
-| `i2c_del_driver(driver)`             | 注销I2C设备驱动         |
-| `i2c_register_driver(owner, driver)` | 注册驱动(底层函数)    |
+| 函数                                          | 说明                    |
+| --------------------------------------------- | ----------------------- |
+| `i2c_add_driver(driver)`                      | 注册I2C设备驱动(常用) |
+| `i2c_del_driver(driver)`                      | 注销I2C设备驱动         |
+| `i2c_register_driver(owner, driver)`          | 注册驱动(底层函数)    |
+| `i2c_add_adapter(adapter)`                    | 注册适配器(动态总线号)|
+| `i2c_add_numbered_adapter(adap)`              | 注册适配器(静态总线号)|
+| `i2c_del_adapter(adap)`                       | 注销适配器              |
+
+**函数原型**(原书第 61.1.1/61.1.2 节):
+
+```c
+int  i2c_register_driver(struct module *owner, struct i2c_driver *driver); /* 0成功,负值失败 */
+void i2c_del_driver(struct i2c_driver *driver);                            /* 无返回值 */
+
+int  i2c_add_adapter(struct i2c_adapter *adapter);            /* 动态总线号,0成功,负值失败 */
+int  i2c_add_numbered_adapter(struct i2c_adapter *adap);      /* 静态总线号,0成功,负值失败 */
+void i2c_del_adapter(struct i2c_adapter *adap);               /* 无返回值 */
+```
 
 `i2c_add_driver`是宏定义,等价于:
 
@@ -482,6 +536,8 @@ int i2c_master_recv(const struct i2c_client *client,
 
 #### 4.2.3 SMBus API
 
+> ⚠️ **来源说明**:本节 API 列表不属于《I.MX6U嵌入式Linux驱动开发指南》第六十一章内容,为扩展知识(原书仅提到 `i2c_algorithm.smbus_xfer`)。
+
 ```c
 /* 读取单个字节数据 */
 s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
@@ -649,6 +705,12 @@ static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
     u8 data = 0;
     ap3216c_read_regs(dev, reg, &data, 1);
     return data;
+
+#if 0
+    /* 也可以用 SMBus API 读取单个寄存器,效果等价 */
+    struct i2c_client *client = (struct i2c_client *)dev->private_data;
+    return i2c_smbus_read_byte_data(client, reg);
+#endif
 }
 
 /*
@@ -656,7 +718,8 @@ static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
  */
 static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
 {
-    u8 buf = data;
+    u8 buf = 0;
+    buf = data;
     ap3216c_write_regs(dev, reg, &buf, 1);
 }
 
@@ -777,11 +840,17 @@ static int ap3216c_probe(struct i2c_client *client,
 
     /* 3. 创建设备类 */
     ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
+    if (IS_ERR(ap3216cdev.class)) {
+        return PTR_ERR(ap3216cdev.class);
+    }
 
     /* 4. 创建设备节点 */
     ap3216cdev.device = device_create(ap3216cdev.class, NULL,
                                       ap3216cdev.devid, NULL,
                                       AP3216C_NAME);
+    if (IS_ERR(ap3216cdev.device)) {
+        return PTR_ERR(ap3216cdev.device);
+    }
 
     /* 5. 保存i2c_client指针 */
     ap3216cdev.private_data = client;
@@ -929,6 +998,8 @@ rmmod ap3216c
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568
 
 | 特性             | IMX6ULL            | STM32F4            | RK3568                |
@@ -973,6 +1044,8 @@ Linux I2C框架:              STM32 HAL框架:              裸机开发:
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:Linux I2C驱动架构分为哪几层?各层职责是什么?
 
 **考察点**:I2C架构理解
@@ -1039,4 +1112,5 @@ NXP的`i2c-imx.c`就是一个标准的platform驱动,通过`of_device_id`匹
 ---
 
 **代码来源**: 正点原子 I.MX6U驱动开发指南 第六十一章, 例程 21_iic
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十一章 Linux I2C驱动实验
 **最后更新**: 2026-09-17

+ 31 - 6
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/02-SPI驱动开发.md

@@ -12,6 +12,8 @@ pdf_ref: "68_第六十二章 Linux SPI驱动实验"
 
 ## 一、SPI协议概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十二章内容(SPI 物理协议在裸机篇第二十七章讲解),为扩展知识。
+
 ### 1.1 SPI总线原理
 
 SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的通信总线,由摩托罗拉公司提出。SPI采用主从架构,一个主设备可以挂载多个从设备,通过独立的片选信号(CS)选择通信对象。
@@ -166,6 +168,8 @@ struct spi_transfer {
 
     dma_addr_t  tx_dma;         /* DMA发送地址 */
     dma_addr_t  rx_dma;         /* DMA接收地址 */
+    struct sg_table tx_sg;      /* 发送散列表 */
+    struct sg_table rx_sg;      /* 接收散列表 */
 
     unsigned    cs_change:1;    /* 传输完成后是否改变CS状态 */
     unsigned    tx_nbits:3;     /* 发送位宽(1/2/4线) */
@@ -299,8 +303,14 @@ struct spi_master *spi_alloc_master(struct device *dev, unsigned size);
 
 /* 释放spi_master */
 void spi_master_put(struct spi_master *master);
+
+/* spi_bitbang:通用bitbang框架的注册/注销,内部会调用 spi_register_master */
+int  spi_bitbang_start(struct spi_bitbang *bitbang);   /* I.MX6U SPI 主机驱动使用此API注册 */
+void spi_bitbang_stop(struct spi_bitbang *bitbang);    /* 对应注销 */
 ```
 
+> 原书 62.1.1 节说明:`spi_bitbang_start()` 内部通过调用 `spi_register_master()` 完成 `spi_master` 注册;使用它注册时,注销需用 `spi_bitbang_stop()`。
+
 ### 4.2 数据传输API
 
 | 函数                    | 说明               | 阻塞 | 适用场景         |
@@ -379,6 +389,7 @@ static int spi_receive(struct spi_device *spi, u8 *buf, int len)
 }
 ```
 
+
 ## 五、完整源码分析
 
 ### 5.1 ICM20608寄存器定义头文件
@@ -530,10 +541,16 @@ static int icm20608_probe(struct spi_device *spi)
 
     /* 3、创建类 */
     icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
+    if (IS_ERR(icm20608dev.class)) {
+        return PTR_ERR(icm20608dev.class);
+    }
 
     /* 4、创建设备 */
     icm20608dev.device = device_create(icm20608dev.class, NULL,
                     icm20608dev.devid, NULL, ICM20608_NAME);
+    if (IS_ERR(icm20608dev.device)) {
+        return PTR_ERR(icm20608dev.device);
+    }
 
     /* 5、初始化SPI设备 */
     spi->mode = SPI_MODE_0;  /* MODE0: CPOL=0, CPHA=0 */
@@ -809,6 +826,8 @@ MXC_SPI_BUF_TX(u32)  /* 展开为spi_imx_buf_tx_u32 */
 
 ### 6.1 spidev工具测试
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十二章内容,为扩展知识(本章原书使用 icm20608 字符设备测试 APP,未涉及 spidev)。
+
 Linux内核提供了spidev驱动,可以在用户空间直接测试SPI设备:
 
 ```bash
@@ -818,13 +837,14 @@ modprobe spidev
 # 检查SPI设备节点
 ls /dev/spidev*
 
-# 使用spidev测试工具(需要安装spi-tools)
-# 发送8字节数据并接收
-spi-pitray -D /dev/spidev0.0 -s 1000000 -b 8 -c 0 -r 8
+# 编译内核源码树中的 tools/spi/spidev_test.c 得到 spidev_test 工具
+arm-linux-gnueabihf-gcc tools/spi/spidev_test.c -o spidev_test
+
+# 基本收发测试(默认发送 0x00...,打印收发结果)
+./spidev_test -D /dev/spidev0.0 -s 1000000 -b 8 -v
 
-# 读取SPI Flash ID(W25Q64为例)
-# 发送0x90命令读取Manufacturer/Device ID
-spi-pitray -D /dev/spidev0.0 -s 1000000 -b 8 -c 0 -tx 0x90,0x00,0x00,0x00 -rx 4
+# 发送自定义数据(-p 后为十六进制字符串,此例为 W25Q 读ID命令 0x90 0x00 0x00 0x00)
+./spidev_test -D /dev/spidev0.0 -p 90000000 -v
 ```
 
 ### 6.2 ICM20608驱动测试步骤
@@ -866,6 +886,8 @@ act temp = 31.24°C
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性         | Linux SPI    | I2C            | STM32 HAL SPI             | RK3568 SPI      |
 | ------------ | ------------ | -------------- | ------------------------- | --------------- |
 | **总线类型** | 全双工4线    | 半双工2线      | 全双工4线                 | 全双工4线       |
@@ -887,6 +909,8 @@ act temp = 31.24°C
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:SPI和I2C有什么区别?各适用于什么场景?
 
 **考察点**: 总线对比理解
@@ -955,4 +979,5 @@ act temp = 31.24°C
 ---
 
 **代码来源**: Linux驱动例程 22_spi(基于正点原子I.MX6ULL开发板)
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十二章 Linux SPI驱动实验
 **最后更新**: 2026-09-17

+ 165 - 30
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/03-USB驱动开发.md

@@ -10,22 +10,35 @@ pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第
 
 > 关联知识:[[03-USB驱动基础]] | [[计算机网络/USB协议]] | [[STM32学习笔记/15-WiFi通信与ESP32-C3]]
 
+> ⚠️ **来源说明**:原书第六十七章《Linux USB驱动实验》**只讲解 USB 协议简介以及如何使能/使用内核自带的 USB HOST 与 OTG(DEVICE/Gadget) 驱动**,明确说明“并不讲解具体的 USB 开发”,因此本文中 USB 设备驱动编程(`usb_driver`/URB/鼠标驱动等)部分不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识;协议与内核配置部分则依据原书第六十七章整理。
+
 ---
 
 ## 一、USB协议概述
 
 ### 1.1 USB总线原理
 
-USB(Universal Serial Bus,通用串行总线)是一种主从式总线架构。主机(Host)发起所有通信,从机(Device)被动响应。总线上通过 HUB 扩展,形成树状拓扑,最多支持 5 级 HUB、127 个设备。
+USB(Universal Serial Bus,通用串行总线)是一种主从式总线架构。主机(Host)发起所有通信,从机(Device)被动响应。总线上通过 HUB 扩展,形成分层的金字塔形树状拓扑,从 Root Hub 开始一共 **7 层**;一个 USB 主控制器支持 128 个地址,其中地址 0 是默认地址(仅枚举时使用,不分配给任何设备),因此**一个 USB 主控制器最多支持 127 个设备**(原书 67.1.3 节)。
+
+USB 版本演进(原书 67.1.1 节):
+
+| 版本      | 速率      | 说明                                             |
+| --------- | --------- | ------------------------------------------------ |
+| USB 1.0   | 1.5 Mbps  | 1996 年正式发布,低速                          |
+| USB 1.1   | 12 Mbps   | Full-Speed(FS) 全速                              |
+| USB 2.0   | 480 Mbps  | High-Speed(HS),半双工,兼容 USB1.0;另有 FS 12Mbps |
+| USB 3.0   | 5 Gbps    | 引入全双工(2 对差分线发送、2 对接收)           |
+| USB 3.1   | 10 Gbps   | 在 USB3.0 基础上提升                             |
+| USB 3.2   | 20 Gbps   | —                                                |
 
-USB 版本演进:
+> **命名规范**:USB-IF 公布新命名后不再使用 USB3.0/3.1 的旧称,统一为 USB3.2,原 USB3.0、USB3.1、USB3.2 分别称为 **USB3.2 Gen1、USB3.2 Gen2、USB3.2 Gen2x2**。
 
-| 版本    | 速率     | 等效编码速率 | 典型应用    |
-| ------- | -------- | ------------ | ----------- |
-| USB 1.1 | 12 Mbps  | 1.5 MB/s     | 键盘鼠标    |
-| USB 2.0 | 480 Mbps | 60 MB/s      | U盘、摄像头 |
-| USB 3.0 | 5 Gbps   | 500 MB/s     | 高速存储    |
-| USB 3.1 | 10 Gbps  | 1.25 GB/s    | 外置显卡    |
+**USB 电气特性**(原书 67.1.2 节):
+
+- USB 采用差分信号传输数据(D- 和 D+),支持热插拔。
+- USB A 插头 4 根线序:1=VBUS(5V)、2=D-、3=D+、4=GND;其中 1、4 触点较长,先供电/后断数据。
+- Mini USB 插头 5 根线序:1=VCC(5V)、2=D-、3=D+、4=ID、5=GND;多出的 **ID 线用于实现 OTG 主从判断**。
+- USB 支持总线供电和自供电两种模式,USB2.0 下总线供电最大可提供 **500mA** 电流。
 
 ### 1.2 USB设备架构
 
@@ -64,6 +77,35 @@ graph TD
 | Interface Descriptor     | 4      | 接口功能     |
 | Endpoint Descriptor      | 5      | 端点参数     |
 
+**描述符详细结构**(原书表 67.3.1.2 ~ 67.3.1.7):
+
+*设备描述符(18 字节,bDescriptorType=0x01)*:
+
+| 偏移 | 域                | 大小(B) | 描述                     |
+| ---- | ----------------- | ------- | ------------------------ |
+| 0    | bLength           | 1       | 描述符长度,18           |
+| 1    | bDescriptorType   | 1       | 类型,0x01               |
+| 2    | bcdUSB            | 2       | USB 版本号               |
+| 4    | bDeviceClass      | 1       | 设备类                   |
+| 5    | bDeviceSubClass   | 1       | 设备子类                 |
+| 6    | bDeviceProtocol   | 1       | 设备协议                 |
+| 7    | bMaxPacketSize0   | 1       | 端点 0 最大包长度        |
+| 8    | idVendor          | 2       | 厂商 ID (VID)            |
+| 10   | idProduct         | 2       | 产品 ID (PID)            |
+| 12   | bcdDevice         | 2       | 设备版本号               |
+| 14   | iManufacturer     | 1       | 厂商字符串描述符索引     |
+| 15   | iProduct          | 1       | 产品字符串描述符索引     |
+| 16   | iSerialNumber     | 1       | 序列号字符串描述符索引   |
+| 17   | bNumConfigurations| 1       | 配置描述符数目           |
+
+*配置描述符(9 字节,bDescriptorType=0x02)*:关键域 `wTotalLength`(整个配置信息总长度)、`bNumInterfaces`(接口数)、`bConfigurationValue`(配置值)、`bmAttributes`(D7保留/D6自给电源/D5远程唤醒)、`bMaxPower`(总线电流,单位 2mA)。
+
+*接口描述符(9 字节,bDescriptorType=0x04)*:`bInterfaceNumber`(接口编号,从 0 开始)、`bAlternateSetting`(备用编号)、`bNumEndpoints`(端点数量)、`bInterfaceClass`/`bInterfaceSubClass`/`bInterfaceProtocol`(类/子类/协议)、`iInterface`(字符串索引)。
+
+*端点描述符(7 字节,bDescriptorType=0x05)*:`bEndpointAddress`(bit3:0 端点号,bit7 方向:0=OUT 输出端点,1=IN 输入端点)、`bmAttributes`(bit1:0 传输类型:00控制/01同步/10批量/11中断)、`wMaxPacketSize`(最大包长)、`bInterval`(轮询间隔,同步传输须为 1ms,中断传输 1~255ms)。
+
+*字符串描述符(bDescriptorType=0x03)*:首字节 `bLength=N+2`,随后为 `wLANGID[]` 语言标识码数组;主机指定语言与索引再次请求后,设备返回 Unicode 编码字符串。
+
 ### 1.3 USB传输类型
 
 USB 协议定义四种传输类型,由端点描述符的 `bmAttributes` 字段的 bit1:0 指定:
@@ -89,16 +131,27 @@ USB 数据包由同步域(SYNC)、包标识符(PID)、数据域、CRC 
 
 主要包类型:
 
-| PID类型 | PID名字 | PID<3:0> | 用途               |
-| ------- | ------- | -------- | ------------------ |
-| 令牌包  | OUT     | 0001     | 主机通知要输出数据 |
-| 令牌包  | IN      | 1001     | 主机通知要输入数据 |
-| 令牌包  | SETUP   | 1101     | 开始控制传输       |
-| 数据包  | DATA0   | 0011     | 偶数据包           |
-| 数据包  | DATA1   | 1011     | 奇数据包           |
-| 握手包  | ACK     | 0010     | 确认,传输正确     |
-| 握手包  | NAK     | 1010     | 不确认,设备忙     |
-| 握手包  | STALL   | 1110     | 端点挂起           |
+| PID类型 | PID名字 | PID<3:0> | 用途                         |
+| ------- | ------- | -------- | ---------------------------- |
+| 令牌包  | OUT     | 0001     | 主机通知从机将要输出数据     |
+| 令牌包  | IN      | 1001     | 主机通知从机将要输入数据     |
+| 令牌包  | SOF     | 0101     | 帧起始包                     |
+| 令牌包  | SETUP   | 1101     | 开始一个控制传输             |
+| 数据包  | DATA0   | 0011     | 偶数据包                     |
+| 数据包  | DATA1   | 1011     | 奇数据包                     |
+| 数据包  | DATA2   | 0111     | 适用于高速和等时传输的数据包 |
+| 数据包  | MDATA   | 1111     | 用于高速分裂/等时传输        |
+| 握手包  | ACK     | 0010     | 确认包,传输正确             |
+| 握手包  | NAK     | 1010     | 不确认包,设备未准备好       |
+| 握手包  | STALL   | 1110     | 设备不支持该请求或端点挂起   |
+| 握手包  | NYET    | 0110     | 接收未响应,未准备好         |
+| 特殊包  | PRE     | 1100     | 前导,令牌包                 |
+| 特殊包  | ERR     | 1100     | 错误,握手包                 |
+| 特殊包  | SPLIT   | 1000     | 分裂传输(Split),令牌包      |
+| 特殊包  | PING    | 0100     | PING 测试,令牌包            |
+| 特殊包  | Reserved| 0000     | 保留                         |
+
+> **PID 编码规则**(原书 67.3.3 节):PID 共 8 位,低 4 位 PID3~PID0 有效,高 4 位 PID7~PID4 是低 4 位的取反,传输顺序为 PID0→PID7。令牌包 PID1~0=01,数据包=11,握手包=10,特殊包=00。例如 SETUP 包 PID3~0=1101,取反得 PID7~4=0010,按传输顺序排列为 10110100=0xB4。
 
 ### 1.5 USB枚举过程
 
@@ -144,6 +197,8 @@ sequenceDiagram
 
 ## 二、Linux USB架构
 
+> ⚠️ **来源说明**:Linux USB 设备驱动编程(`usb_driver`/URB 等)不属于《I.MX6U嵌入式Linux驱动开发指南》第六十七章内容,为扩展知识;其中的 HCD(OHCI/UHCI/EHCI/xHCI)部分是原书 67.1.5 节内容的整理。
+
 ### 2.1 USB子系统架构
 
 ```mermaid
@@ -189,13 +244,19 @@ Linux USB 子系统分为三层:
 
 Linux 内核支持三种 HCD 实现:
 
-| HCD  | 全称                                 | 适用场景              |
-| ---- | ------------------------------------ | --------------------- |
-| EHCI | Enhanced Host Controller Interface   | USB 2.0 高速设备      |
-| OHCI | Open Host Controller Interface       | USB 1.1 低速/全速设备 |
-| xHCI | eXtensible Host Controller Interface | USB 3.0+ 设备         |
+| HCD  | 全称                                 | 适用场景                          |
+| ---- | ------------------------------------ | --------------------------------- |
+| OHCI | Open Host Controller Interface       | USB 1.1(非 X86,嵌入式 USB 控制器) |
+| UHCI | Universal Host Controller Interface  | USB 1.0/1.1(Intel 主导,X86,硬件成本低) |
+| EHCI | Enhanced Host Controller Interface   | USB 2.0 高速(全速/低速由 OHCI/UHCI 提供) |
+| xHCI | eXtensible Host Controller Interface | USB 3.0+,可支持所有速度,用于取代前三种 |
 
-I.MX6ULL 使用 EHCI 控制器(USB 2.0),同时集成 OTG 控制器支持主机/从机切换。
+I.MX6ULL 内部集成两个独立的 USB2.0 控制器内核(Core0/Core1),分别连接到 **OTG1 和 OTG2**(原书 67.1.5 节):
+
+- 两个控制器都支持 **HS(480Mbps)/FS(12Mbps)/LS(1.5Mbps)**,主机和从机模式均支持。
+- 硬件支持 OTG 信号、会话请求协议(SRP)和主机协商协议(HNP),**每个控制器支持 8 个双向端点**,且**各有一个 DMA**。
+- 每个控制器有正常模式(normal)和低功耗模式(low power);从机模式下端口停止活动 3ms 后进入挂起状态。
+- 两个控制器都兼容 **EHCI** 标准(USB2.0);驱动文件为 `drivers/usb/host/ehci-imx.c` 等。
 
 ### 2.3 核心数据结构
 
@@ -339,6 +400,8 @@ V2.4 版本底板使用 Type-C 接口替代 Mini USB,通过 CC1/CC2 引脚实
 
 ## 四、USB驱动API
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十七章内容,为扩展知识。
+
 ### 4.1 驱动注册与注销
 
 ```c
@@ -389,14 +452,15 @@ int usb_bulk_msg(struct usb_device *usb_dev,
                  int *actual_length,
                  int timeout);
 
-/* 中断同步传输 */
-int usb_int_msg(struct usb_device *usb_dev,
-                unsigned int pipe,
-                void *data, int len,
-                int *actual_length,
-                int timeout);
+/* 中断同步传输(内部即调用 usb_bulk_msg) */
+int usb_interrupt_msg(struct usb_device *usb_dev,
+                      unsigned int pipe,
+                      void *data, int len,
+                      int *actual_length,
+                      int timeout);
 ```
 
+
 ### 4.4 控制传输
 
 ```c
@@ -440,6 +504,8 @@ MODULE_DEVICE_TABLE(usb, my_id_table);
 
 ## 五、完整源码分析
 
+> ⚠️ **来源说明**:USB 鼠标驱动、URB 机制、`cdc_acm` 等驱动源码均不属于《I.MX6U嵌入式Linux驱动开发指南》第六十七章内容,为扩展知识(原书未讲解 USB 设备驱动编写);其中 5.4 节的 USB Gadget 内核配置路径来自原书 67.5 节。
+
 ### 5.1 USB鼠标驱动实例
 
 以下是基于 I.MX6ULL 开发板的 USB 鼠标驱动,展示完整的 USB 驱动框架:
@@ -652,8 +718,72 @@ I.MX6ULL 的 USB OTG 从机功能通过 USB Gadget 框架实现:
 
 ## 六、实验验证
 
+### 6.0 内核配置与使能(原书 67.4/67.5 节)
+
+原书实验的核心是**使能内核自带的 USB 驱动**,NXP 官方内核默认大多已使能,以下为手动配置路径。
+
+**1) USB 鼠标键盘(HID)驱动**:
+
+```
+-> Device Drivers
+   -> HID support
+      -> HID bus support (HID [=y])
+         -> <*> Generic HID driver          // 使能通用HID驱动
+      -> USB HID support
+         -> <*> USB HID transport layer     // CONFIG_USB_HID,键盘/鼠标等
+```
+
+> 注意:`USB HID transport layer` 与 `USB HIDBP Keyboard/Mouse (simple Boot) support` 不能同时使用。
+
+**2) U 盘驱动**(U 盘使用 SCSI 协议):
+
+```
+-> Device Drivers
+   -> SCSI device support
+      -> <*> SCSI disk support
+   -> USB support (USB_SUPPORT [=y])
+      -> Support for Host-side USB (USB [=y])
+         -> <*> USB Mass Storage support
+```
+
+**3) 键盘作为 LCD 控制台**:在根文件系统 `/etc/inittab` 中加入:
+
+```
+tty1::askfirst:-/bin/sh
+```
+
+重启后屏幕作为控制台,按回车即可激活。
+
+**4) USB OTG 主机实验**:需在设备树中把 GPIO1_IO00 复用为 OTG1 ID 并配置为默认下拉(详见第 3.2 节)。
+
+**5) OTG 从机(Gadget)实验**:将相关驱动编译为模块 `[M]`,编译后得到并依次加载:
+
+| 实验        | 生成的 .ko 模块(按加载顺序)                                                                 |
+| ----------- | --------------------------------------------------------------------------------------------- |
+| 模拟 U 盘   | `libcomposite.ko` → `function/usb_f_mass_storage.ko` → `legacy/g_mass_storage.ko`              |
+| USB 声卡    | `libcomposite.ko` → `function/usb_f_uac1.ko` → `legacy/g_audio.ko`(UAC 1.0 Legacy)           |
+
+```bash
+# 模拟U盘:file 指定要导出的分区,removable=1 表示可移动
+depmod
+modprobe libcomposite.ko
+modprobe usb_f_mass_storage.ko
+modprobe g_mass_storage.ko file=/dev/sda1 removable=1
+rmmod g_mass_storage.ko           # 退出
+
+# USB声卡
+depmod
+modprobe libcomposite.ko
+modprobe usb_f_uac1.ko
+modprobe g_audio.ko
+```
+
+> 注意:不要把 EMMC/NAND 作为模拟 U 盘的存储区域,其 EXT4/UBIFS 与 Windows 不兼容。
+
 ### 6.1 USB设备识别
 
+> ⚠️ **来源说明**:6.1~6.3 节(lsusb 等工具)不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ```bash
 # 查看系统USB设备列表
 lsusb
@@ -731,6 +861,8 @@ rmmod g_mass_storage
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性       | I.MX6ULL (Linux) | STM32 (HAL)       | RK3568 (Linux)     |
 | ---------- | ---------------- | ----------------- | ------------------ |
 | USB控制器  | EHCI + OTG       | OTG FS/HS         | xHCI + OTG         |
@@ -759,6 +891,8 @@ rmmod g_mass_storage
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:USB有哪几种传输类型?各自的应用场景是什么?
 
 **考察点**:USB协议基础
@@ -840,4 +974,5 @@ Gadget 架构分为三层:UDC 驱动(硬件层)、Gadget 框架(协议
 
 ---
 
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十七章 Linux USB驱动实验
 **最后更新**: 2026-09-17

+ 84 - 4
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/04-CAN驱动开发.md

@@ -95,10 +95,39 @@ CAN总线一个位分为4个段:
 
 在总线空闲时,最先开始发送的单元获得发送权。多个单元同时发送时,从仲裁段第一位开始逐位比较:发送显性电平(0)的单元在仲裁中优先。连续输出显性电平最多的单元赢得仲裁,失利的单元立刻停止发送转为接收。
 
+### 1.8 FlexCAN 控制器(原书 66.1.5 节)
+
+I.MX6ULL 的 CAN 控制器称为 **FlexCAN**,符合 CAN2.0B 协议,支持标准/扩展格式,**支持 64 个消息缓冲(邮箱)**,最高 1Mbit/S,每个邮箱可独立配置为接收或发送并带独立接收掩码。驱动文件位于 `drivers/net/can/flexcan.c`。
+
+**FlexCAN 支持的 4 种工作模式**:
+
+| 模式                | 使能方式                          | 说明                                                     |
+| ------------------- | --------------------------------- | -------------------------------------------------------- |
+| 正常模式(Normal)    | 默认                              | 正常收发,所有 CAN 协议功能使能                          |
+| 冻结模式(Freeze)    | MCR.FRZ = 1                       | 无法收发,CAN 总线同步丢失                               |
+| 仅监听模式(Listen-Only) | CTRL.LOM = 1                  | 禁止发送,错误计数器冻结,只接收其他单元的 ACK,工作于被动错误模式 |
+| 回环模式(Loop-Back) | CTRL.LPB = 1                      | 内部回环,用于自测,发送数据流直接反馈给内部接收单元     |
+
+另有 2 种低功耗模式:**禁止模式(Disable)** 和 **停止模式(Stop)**。
+
+**位时序寄存器位域**(CTRL 寄存器):
+
+| 位域    | 作用              | 取值范围    | 对应段   |
+| ------- | ----------------- | ----------- | -------- |
+| PRESDIV | 分频值,决定 Tq   | —           | —        |
+| PROPSEG | 传播时间段 PTS    | 0~7 → 1~8 Tq | PTS      |
+| PSEG1   | 相位缓冲段1 PBS1  | 0~7 → 1~8 Tq | PBS1     |
+| PSEG2   | 相位缓冲段2 PBS2  | 1~7 → 2~8 Tq | PBS2     |
+| RJW     | 再同步补偿宽度 SJW | 0~3 → 1~4 Tq | SJW      |
+
+其中 `f_Tq = f_CANCLK / (PRESDIV + 1)`,SYNC_SEG 固定为 1 个 Tq。总 Tq 与波特率关系见 1.6 节。
+
 ---
 
 ## 2. Linux CAN架构
 
+> ⚠️ **来源说明**:原书第六十六章只讲解 FlexCAN 的使能与测试,并明确说明“本教程不讲解应用编程”;本章 SocketCAN 数据结构、API 及 flexcan.c 源码分析等均不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 2.1 SocketCAN架构
 
 Linux通过SocketCAN框架将CAN总线抽象为网络设备,用户空间通过标准socket接口访问CAN:
@@ -149,10 +178,10 @@ struct can_frame {
 
 `can_id` 字段的位定义:
 
-- bit 0~28:CAN ID(标准帧11位,扩展帧29位
-- bit 30:`CAN_EFF_FLAG` — 扩展帧标志
-- bit 31:`CAN_RTR_FLAG` — 远程帧标志
-- bit 29:`CAN_ERR_FLAG` — 错误帧标志
+- bit 0~28:CAN ID(标准帧用 bit0~10,扩展帧用 bit0~28
+- bit 29:`CAN_ERR_FLAG`(0x20000000)— 错误帧标志
+- bit 30:`CAN_RTR_FLAG`(0x40000000)— 远程帧标志
+- bit 31:`CAN_EFF_FLAG`(0x80000000)— 扩展帧标志
 
 #### can_filter(CAN过滤器)
 
@@ -245,6 +274,8 @@ flexcan1: can@02090000 {
 };
 ```
 
+**关闭 FlexCAN2**:I.MX6ULL 有两个 CAN 控制器,NXP 官方 EVK 两个都使能了;但正点原子 I.MX6U-ALPHA 开发板把 FlexCAN2 的 IO 分配给了 **ECSPI3**,因此必须在 `imx6ull-alientek-emmc.dts` 中把名为 `flexcan2` 的节点**屏蔽/删除**,否则 ECSPI3 外设无法使用(原书 66.3.1 节)。
+
 ### 3.3 关键属性说明
 
 | 属性              | 说明                                    |
@@ -260,6 +291,8 @@ flexcan1: can@02090000 {
 
 ## 4. SocketCAN API
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(原书不讲解 CAN 应用编程,但测试中用到了 `ip` 与 can-utils 命令)。
+
 ### 4.1 Socket创建与绑定
 
 ```c
@@ -394,6 +427,8 @@ can0  5A1  [8]  11 22 33 44 55 66 77 88
 
 ## 5. 完整源码分析
 
+> ⚠️ **来源说明**:本节 flexcan.c 源码分析不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(原书仅给出驱动文件路径 `drivers/net/can/flexcan.c`)。
+
 ### 5.1 FlexCAN驱动框架(drivers/net/can/flexcan.c)
 
 Linux内核中的FlexCAN驱动位于 `drivers/net/can/flexcan.c`,驱动入口注册网络设备:
@@ -608,6 +643,42 @@ Tq = 24MHz / (5+1) = 4MHz (250ns)
 
 ## 6. 实验验证
 
+### 6.0 移植 iproute2 与 can-utils(原书 66.4.2/66.4.3 节)
+
+busybox 自带的 `ip` 命令不支持 CAN 操作,需要移植 iproute2(原书使用 4.4.0 版本):
+
+```bash
+tar -vxzf iproute2-4.4.0.tar.gz
+cd iproute2-4.4.0
+# 修改 Makefile:CC := gcc  改为  CC:=arm-linux-gnueabihf-gcc
+make
+# 得到的 ip 命令先拷贝到临时目录验证(切勿直接覆盖!)
+sudo cp ip /home/zuozhongkai/linux/nfs/test_rootfs/lib/modules/4.1.15/ -f
+```
+
+在开发板上验证 `./ip -V` 打印版本号正常后,再替换 busybox 的 `/sbin/ip`:
+
+```bash
+cd lib/modules/4.1.15/
+cp ip /sbin/ip -f      # 建议先备份根文件系统
+rm ip
+reboot
+ip -V                  # 重启后确认版本号
+```
+
+can-utils 移植(原书使用 2020.02.04 版本):
+
+```bash
+tar -vxzf can-utils-2020.02.04.tar.gz
+cd can-utils-2020.02.04
+./autogen.sh
+./configure --target=arm-linux-gnueabihf --host=arm-linux-gnueabihf \
+    --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/can-utils --disable-static --enable-shared
+make
+make install
+sudo cp bin/* /home/zuozhongkai/linux/nfs/rootfs/usr/bin/ -f   # 拷贝到 /usr/bin
+```
+
 ### 6.1 内核配置
 
 使能Linux内核的FlexCAN驱动:
@@ -693,6 +764,8 @@ ifconfig can0 down
 
 ## 7. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568 CAN差异
 
 | 特性       | I.MX6ULL               | STM32F4/F7                          | RK3568                  |
@@ -717,6 +790,8 @@ ifconfig can0 down
 
 ## 8. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: SocketCAN相比传统字符设备CAN驱动有什么优势?
 
 **答**:SocketCAN是Linux下CAN总线的标准访问方式,相比传统字符设备驱动有以下优势:
@@ -773,3 +848,8 @@ ifconfig can0 down
 6. **环回测试**:`ip link set can0 type can loopback on` 排除外部设备干扰
 7. **统计信息**:`ip -s link show can0` 查看错误计数、丢帧统计
 8. **逻辑分析仪**:捕获CAN总线原始信号,分析时序和错误帧
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十六章 Linux CAN驱动实验
+**最后更新**: 2026-09-17

+ 93 - 7
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/05-RS232与485通信.md

@@ -20,6 +20,8 @@ pdf_ref: "69_第六十三章 Linux RS232_485_GPS驱动实验.pdf"
 
 ## 1 串口通信概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十三章内容,为扩展知识(原书本章重点为 UART 驱动框架、设备树配置与 minicom 测试)。
+
 ### 1.1 RS232 vs RS485 vs TTL
 
 | 特性       | TTL电平       | RS232               | RS485            |
@@ -554,21 +556,22 @@ JP1跳线帽设置:将1-3和2-4连接,使UART3通过SP3232芯片输出RS232
 
 ### 5.6 RS485驱动配置(方向控制)
 
-RS485是半双工通信,需要控制收发方向。I.MX6U-ALPHA开发板通过硬件巧妙地将RE和DE连接到同一控制线RS485_RX,省掉了额外的GPIO:
+RS485是半双工通信,需要控制收发方向。I.MX6U-ALPHA开发板通过硬件巧妙地将RE和DE连接到同一控制线,省掉了额外的GPIO。原书 63.3 节说明:SP3485 的 RO 为数据输出端、DI 为数据输入端,RE 是接收使能(低有效)、DE 是发送使能(高有效);原理图中 **RE 和 DE 经过一系列电路最终由 RS485_RX 信号控制**,从而把 RS485 完全当作串口使用
 
 ```
 MCU UART3_TX ──▶ SP3485 DI(数据输入)
 MCU UART3_RX ◀── SP3485 RO(数据输出)
-MCU RTS/GPIO ──┬─▶ SP3485 DE(发送使能,高有效)
-               └─▶ SP3485 /RE(接收使能,低有效)
-                        ▲
-                        │ 硬件电路自动切换方向
-                        │
-                   RS485总线 A/B
+UART3_RX 经外围电路 ─┬─▶ SP3485 DE(发送使能,高有效)
+                     └─▶ SP3485 /RE(接收使能,低有效)
+                              
+                              │ 硬件电路自动切换方向
+                              
+                         RS485总线 A/B
 ```
 
 关键:RE和DE通过电路设计连接到同一信号,发送时DE高电平使能发送,接收时RE低电平使能接收。这种设计将RS485完全当作串口使用,无需软件控制方向。
 
+
 RS485在驱动中的配置:
 
 ```c
@@ -589,6 +592,23 @@ RS485标志位说明:
 | `SER_RS485_RX_DURING_TX`   | 发送期间允许接收           |
 | `SER_RS485_TERMINATE_BUS`  | 发送完成后发送总线终止信号 |
 
+**`struct serial_rs485` 定义**(`include/uapi/linux/serial.h`,Linux 4.1.15):
+
+```c
+struct serial_rs485 {
+    __u32 flags;                     /* RS485 功能标志 */
+#define SER_RS485_ENABLED        (1 << 0)
+#define SER_RS485_RTS_ON_SEND    (1 << 1)
+#define SER_RS485_RTS_AFTER_SEND (1 << 2)
+#define SER_RS485_RX_DURING_TX   (1 << 4)
+    __u32 delay_rts_before_send;     /* 发送前 RTS 延迟(ms) */
+    __u32 delay_rts_after_send;      /* 发送后 RTS 延迟(ms) */
+    __u32 padding[5];                /* 预留,动态分配内存用 */
+};
+```
+
+> 原书示例代码 63.2.5 中,驱动侧设置 `sport->port.rs485.flags = SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX;`。`SER_RS485_TERMINATE_BUS` 等标志在较新内核中才定义。
+
 用户空间可通过`ioctl()`配置RS485参数:
 
 ```c
@@ -610,8 +630,65 @@ ioctl(fd, TIOCGRS485, &rs485conf);
 
 ## 6 实验验证
 
+### 6.0 移植 ncurses 与 minicom(原书 63.5 节)
+
+原书使用 **minicom 2.7.1** 测试串口,它依赖 **ncurses 6.0**。
+
+**1) 移植 ncurses**(`ncurses-6.0.tar.gz`):
+
+```bash
+tar -vxzf ncurses-6.0.tar.gz
+./configure --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/ncurses \
+    --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf \
+    --with-shared --without-profile --disable-stripping \
+    --without-progs --with-manpages --without-tests
+make
+make install
+# 将 include/lib/share 分别拷到根文件系统 /usr/include、/usr/lib、/usr/share
+sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -rfa
+sudo cp share/* /home/zuozhongkai/linux/nfs/rootfs/usr/share/ -rfa
+sudo cp include/* /home/zuozhongkai/linux/nfs/rootfs/usr/include/ -rfa
+```
+
+在根文件系统 `/etc/profile`(没有则新建)中加入:
+
+```sh
+#!/bin/sh
+LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
+export LD_LIBRARY_PATH
+
+export TERM=vt100
+export TERMINFO=/usr/share/terminfo
+```
+
+**2) 移植 minicom**(`minicom-2.7.1.tar.gz`):
+
+```bash
+tar -vxzf minicom-2.7.1.tar.gz
+cd minicom-2.7.1/
+./configure CC=arm-linux-gnueabihf-gcc \
+    --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/minicom \
+    --host=arm-linux-gnueabihf \
+    CPPFLAGS=-I/home/zuozhongkai/linux/IMX6ULL/tool/ncurses/include \
+    LDFLAGS=-L/home/zuozhongkai/linux/IMX6ULL/tool/ncurses/lib \
+    --enable-cfg-dir=/etc/minicom
+make
+make install
+sudo cp bin/* /home/zuozhongkai/linux/nfs/rootfs/usr/bin/
+```
+
+**3) 新建 `/etc/passwd`**:如果执行 `minicom -s` 提示 “Go away”,在根文件系统新建 `/etc/passwd` 并写入下面内容后重启:
+
+```
+root:x:0:0:root:/root:/bin/sh
+```
+
+完成后 `minicom -v` 应显示版本 2.7.1,`minicom -s` 可正常打开配置界面。
+
 ### 6.1 stty命令 — 串口参数配置
 
+> ⚠️ **来源说明**:6.1~6.2 节的 stty/picocom/echo 用法不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(原书使用 minicom 测试)。
+
 ```bash
 # 查看当前串口参数
 stty -F /dev/ttymxc2 -a
@@ -716,6 +793,8 @@ $GPRMC,062219.00,A,3150.7892,N,11711.9538,E,0.05,31.5,170926,,,A*64
 
 ## 7 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568
 
 | 特性          | IMX6ULL                  | STM32F4xx     | RK3568                      |
@@ -755,6 +834,8 @@ $GPRMC,062219.00,A,3150.7892,N,11711.9538,E,0.05,31.5,170926,,,A*64
 
 ## 8 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: Linux串口驱动中uart_driver、uart_port、uart_ops三者的关系?
 
 **答**:三者是"驱动-端口-操作"的分层关系:
@@ -831,3 +912,8 @@ write(fd, data, len);
 ```
 
 驱动层面,`rs485_config`回调函数(如`imx_rs485_config()`)根据用户配置设置UART控制寄存器中的RTS极性、发送/接收延迟等参数。`start_tx()`时驱动自动拉高RTS切换为发送模式,发送完成中断中自动拉低RTS切换为接收模式。
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十三章 Linux RS232/485/GPS驱动实验
+**最后更新**: 2026-09-17

+ 22 - 13
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/面试-总线接口驱动.md

@@ -15,6 +15,8 @@ updated: 2026-09-17
 
 # Linux总线与接口驱动面试精选
 
+> ⚠️ **来源说明**:本文为基于《I.MX6U嵌入式Linux驱动开发指南》第六十六、六十七、六十二、六十一、六十三章整理的扩展面试题,题目本身不属于原书内容;涉及的事实(API、结构体、设备树、寄存器)已按原书相应章节核对。
+
 ## I2C驱动面试题
 
 ### Q1: 请描述Linux I2C驱动框架的分层结构,以及各层的作用
@@ -345,14 +347,14 @@ ecspi3: ecspi@02010000 {
 // SPI设备节点
 &ecspi3 {
     fsl,spi-num-chipselects = <1>;
-    cs-gpios = <&gpio4 9 0>;
+    cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
     pinctrl-names = "default";
     pinctrl-0 = <&pinctrl_ecspi3>;
     status = "okay";
 
     icm20608@0 {
-        compatible = "invensense,icm20608";
-        spi-max-frequency = <1000000>;
+        compatible = "alientek,icm20608";
+        spi-max-frequency = <8000000>;
         reg = <0>;
     };
 };
@@ -662,15 +664,18 @@ USB主机控制器标准:
 └─────────────────────────────────┘
 ```
 
-CAN网络设备结构:
+CAN网络设备结构(Linux 中 CAN 控制器驱动通用的私有数据结构是 `struct can_priv`,定义在 `include/linux/can/dev.h`,由 `alloc_candev()` 分配)
 
 ```c
-struct can_dev {
-    struct net_device *netdev;
-    struct can_device *can;
-    can_tx_function_t tx;
-    can_rx_function_t rx;
-    // ...
+struct can_priv {
+    struct net_device *dev;
+    struct can_device_stats can_stats;      /* 总线错误统计 */
+    const struct can_bittiming_const *bittiming_const;
+    struct can_bittiming bittiming;         /* 位时序参数 */
+    const struct can_clock *clock;
+    u32 ctrlmode;                           /* 控制模式标志 */
+    ...
+    struct napi_struct napi;
 };
 ```
 
@@ -863,7 +868,6 @@ static const struct uart_ops serial_imx_pops = {
     .stop_tx    = imx_stop_tx,
     .start_tx   = imx_start_tx,
     .stop_rx    = imx_stop_rx,
-    .ioctl      = imx_ioctl,
     .break_ctl  = imx_break_ctl,
     .startup    = imx_startup,
     .shutdown   = imx_shutdown,
@@ -1029,7 +1033,7 @@ static void imx_dma_tx(struct uart_port *sport)
 应用场景:
 
 - **UART**:调试串口、GPS模块、蓝牙模块
-- **I2C**:传感器、EEPRTC、实时时钟
+- **I2C**:传感器、EEPROM、实时时钟
 - **SPI**:Flash、LCD、高速ADC
 
 **追问**:
@@ -1201,7 +1205,7 @@ if (can_id & CAN_ERR_FLAG) {
 
 // UART错误处理
 if (lsr & UART_LSR_FE)
-    port->ic_frame_errors++;
+    port->icount.frame++;
 ```
 
 **追问**:
@@ -1248,3 +1252,8 @@ spi_sync(spi, &m);  // 同步传输,等待完成
 
 - 如何评估一个总线的实时性能?
 - 在实时系统中如何选择通信接口?
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十一章 Linux I2C驱动实验、第六十二章 Linux SPI驱动实验、第六十三章 Linux RS232/485/GPS驱动实验、第六十六章 Linux CAN驱动实验、第六十七章 Linux USB驱动实验
+**最后更新**: 2026-09-17

+ 70 - 2
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/01-LED与蜂鸣器驱动.md

@@ -80,6 +80,8 @@ LED驱动(led_fops)
 
 ## 二、LED设备树配置
 
+> 📖 **章节说明**:第四十一章的 LED 驱动采用**直接操作寄存器**的方式,**未使用设备树**;本节设备树版本对应《指南》**第四十五章《设备树下的 LED 驱动实验》**及后续 pinctrl/gpio 子系统章节,属相邻章节补充内容。
+
 ### 2.1 设备树基础概念
 
 设备树(Device Tree)用于描述硬件信息,避免在驱动中硬编码硬件参数。LED设备树配置包含两部分:pinctrl节点和设备节点。
@@ -320,6 +322,7 @@ void led_switch(u8 sta)
 - `val &= ~(1 << 3)`:将bit3清零,GPIO输出低电平,LED点亮(低电平有效)
 - `val |= (1 << 3)`:将bit3置1,GPIO输出高电平,LED熄灭
 
+
 ### 3.3 led_init入口函数分析
 
 驱动加载时执行的初始化流程:
@@ -418,10 +421,13 @@ I.MX6U-ALPHA开发板上的蜂鸣器通过SNVS_TAMPER1引脚控制,复用为GP
 | 项目     | 说明                                  |
 | -------- | ------------------------------------- |
 | 控制引脚 | SNVS_TAMPER1 → GPIO5_IO01             |
-| 有效电平 | 低电平有效(Active High,0=开,1=关) |
+| 有效电平 | **低电平鸣叫**(0=开,1=关)          |
 | 驱动方式 | GPIO推挽输出                          |
 | 设备类型 | 有源蜂鸣器(电平驱动即可鸣叫)        |
 
+> [!note] 关于设备树极性
+> 💡 蜂鸣器的实际有效电平为**低电平**:驱动内 `gpio_set_value(dev->beep_gpio, 0)` 直接输出低电平开启蜂鸣器、输出高电平关闭,并未使用 `gpio_set_value_cansleep` 或极性反转逻辑。
+
 ### 5.2 GPIO控制原理
 
 蜂鸣器控制本质上是GPIO输出高低电平:
@@ -673,6 +679,7 @@ static ssize_t beep_write(struct file *filp, const char __user *buf,
 
 通过`filp->private_data`获取设备结构体指针,使用GPIO子系统API `gpio_set_value`控制电平。
 
+
 ### 7.4 与LED驱动的关键差异
 
 | 对比项       | LED驱动                   | 蜂鸣器驱动                               |
@@ -685,6 +692,61 @@ static ssize_t beep_write(struct file *filp, const char __user *buf,
 
 ---
 
+### 7.5 beepApp.c 测试程序源码(原书)
+
+```c
+#include "stdio.h"
+#include "unistd.h"
+#include "sys/types.h"
+#include "sys/stat.h"
+#include "fcntl.h"
+#include "stdlib.h"
+#include "string.h"
+
+#define BEEPOFF   0
+#define BEEPON    1
+
+int main(int argc, char *argv[])
+{
+    int fd, retvalue;
+    char *filename;
+    unsigned char databuf[1];
+
+    if(argc != 3){
+        printf("Error Usage!\r\n");
+        return -1;
+    }
+
+    filename = argv[1];
+
+    /* 打开beep驱动 */
+    fd = open(filename, O_RDWR);
+    if(fd < 0){
+        printf("file %s open failed!\r\n", argv[1]);
+        return -1;
+    }
+
+    databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */
+
+    /* 向/dev/beep文件写入数据 */
+    retvalue = write(fd, databuf, sizeof(databuf));
+    if(retvalue < 0){
+        printf("BEEP Control Failed!\r\n");
+        close(fd);
+        return -1;
+    }
+
+    retvalue = close(fd); /* 关闭文件 */
+    if(retvalue < 0){
+        printf("file %s close failed!\r\n", argv[1]);
+        return -1;
+    }
+    return 0;
+}
+```
+
+---
+
 ## 八、实验验证
 
 ### 8.1 编译驱动程序
@@ -752,12 +814,15 @@ rmmod beep.ko
 - LED测试:执行`./ledApp /dev/led 1`后红色LED点亮,执行`./ledApp /dev/led 0`后熄灭
 - 蜂鸣器测试:执行`./beepApp /dev/beep 1`后蜂鸣器鸣叫,执行`./beepApp /dev/beep 0`后停止
 - 驱动加载成功后终端输出设备号信息(beep major=xxx,minor=xxx)
-- 通过`/proc/device-tree`可验证设备树节点是否正确加载
+- 原书实测:加载 beep.ko 时终端打印 `beep node find!`,`GPIO5_IO01` 对应的 GPIO 编号为 **129**
+- 通过`/proc/device-tree`可验证设备树节点(`gpioled`/`beep`)是否正确加载
 
 ---
 
 ## 九、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性     | I.MX6ULL (Linux) | STM32 (裸机/HAL)             | RK3568 (Linux)   |
 | -------- | ---------------- | ---------------------------- | ---------------- |
 | GPIO API | `gpio_set_value` | `HAL_GPIO_WritePin`          | `gpio_set_value` |
@@ -779,6 +844,8 @@ rmmod beep.ko
 
 ## 十、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 题目1:ioremap的作用是什么?为什么不能直接操作物理地址?
 
 **考察点**:MMU与地址映射
@@ -823,4 +890,5 @@ Linux开启MMU后,CPU访问的都是虚拟地址,直接操作物理地址会
 ---
 
 **代码来源**:正点原子I.MX6U嵌入式Linux驱动开发指南 V2.0.1(第四十一章、第四十六章)
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第41章 嵌入式Linux LED驱动开发实验、第46章 Linux蜂鸣器实验
 **最后更新**: 2026-09-17

+ 62 - 14
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/02-按键驱动.md

@@ -39,13 +39,13 @@ I.MX6U-ALPHA开发板的KEY0使用 `UART1_CTS_B` 这个PIN,复用为 `GPIO1_IO
 
 **消抖方案**:
 
-1. **硬件消抖**:在GPIO引脚并联电容(100nF~100nF),滤除高频毛刺
+1. **硬件消抖**:在GPIO引脚并联电容(如 100nF,配合上拉电阻构成RC低通),滤除高频毛刺
 2. **软件消抖**:检测到电平变化后延时10~20ms,再次读取GPIO确认状态
 3. **定时器消抖**:使用内核定时器(`timer_list`)或工作队列,在中断触发后延迟处理
 4. **INPUT子系统消抖**:内核INPUT框架内置debounce机制
 
 > [!note] 注意
-> 本章基础示例不包含软件消抖,实际按键驱动应使用INPUT子系统或定时器消抖。无消抖时,按一次按键可能输出多行。
+> 原书第四十九章的示例**只用原子变量保护按键值,未实现消抖,也没有使用定时器**(定时器消抖 + INPUT子系统是原书第五十八章《Linux INPUT子系统实验》的内容)。实际按键驱动应使用INPUT子系统或定时器消抖;无消抖时,按一次按键可能输出多行。
 
 ---
 
@@ -103,6 +103,7 @@ key {
 1. `UART1_CTS_B` 是否被其他pinctrl节点使用,若有则屏蔽
 2. `GPIO1_IO18` 是否被其他外设使用,若有则屏蔽
 
+
 ### 2.6 编译与验证
 
 ```bash
@@ -400,6 +401,8 @@ int main(int argc, char *argv[])
 
 ## 5. INPUT子系统按键驱动
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第四十九章内容(第四十九章仅使用原子变量 + `read` 轮询),为扩展知识。其中的定时器消抖 + INPUT子系统上报方式对应原书**第五十八章《Linux INPUT子系统实验》**。
+>
 > [!tip] 推荐方式
 > 实际项目中按键驱动应使用Linux INPUT子系统,它专门处理输入设备事件。
 
@@ -453,17 +456,21 @@ struct key_input_dev {
     struct device_node *nd;
     int key_gpio;
     int irq;
-    struct timer_list timer;
+    struct timer_list timer;      /* 4.1.15内核使用静态定义的timer_list */
     unsigned char curkey;
+    struct input_dev *input;      /* input子系统设备 */
 };
 
 static struct key_input_dev keyinputdev;
 
-/* 定时器中断处理:消抖后读取GPIO */
-static void key_timer_handler(struct timer_list *t)
+/* 定时器处理:消抖后读取GPIO
+ * 注意:4.1.15内核的定时器回调形参为 unsigned long,
+ *      4.15及以上内核才改为 struct timer_list * + from_timer()。
+ */
+static void key_timer_handler(unsigned long arg)
 {
     unsigned char value;
-    struct key_input_dev *dev = from_timer(dev, t, timer);
+    struct key_input_dev *dev = (struct key_input_dev *)arg;
 
     value = gpio_get_value(dev->key_gpio);
     if (value == 0) {
@@ -505,11 +512,13 @@ static int keyio_init(void)
     gpio_request(keyinputdev.key_gpio, "key0");
     gpio_direction_input(keyinputdev.key_gpio);
 
-    /* 获取中断号 */
-    keyinputdev.irq = gpio_to_irq(keyinputdev.key_gpio);
+    /* 获取中断号(原书使用 irq_of_parse_and_map 从设备树解析) */
+    keyinputdev.irq = irq_of_parse_and_map(keyinputdev.nd, 0);
 
-    /* 初始化定时器 */
-    timer_setup(&keyinputdev.timer, key_timer_handler, 0);
+    /* 初始化定时器(4.1.15内核用init_timer,非timer_setup) */
+    init_timer(&keyinputdev.timer);
+    keyinputdev.timer.function = key_timer_handler;
+    keyinputdev.timer.data = (unsigned long)&keyinputdev;
 
     /* 申请中断 */
     ret = request_irq(keyinputdev.irq, key_irq_handler,
@@ -525,15 +534,17 @@ static int keyio_init(void)
 
 static int __init keyinput_init(void)
 {
-    keyio_init();
+    int ret;
 
-    /* 注册input设备 */
+    /* 先申请并初始化input设备,再初始化GPIO/中断,避免中断先于input就绪 */
     keyinputdev.input = input_allocate_device();
     if (!keyinputdev.input) {
         return -ENOMEM;
     }
 
     keyinputdev.input->name = KEY_NAME;
+    __set_bit(EV_KEY, keyinputdev.input->evbit);
+    __set_bit(EV_REP, keyinputdev.input->evbit);
     input_set_capability(keyinputdev.input, EV_KEY, KEY_0);
 
     ret = input_register_device(keyinputdev.input);
@@ -542,15 +553,20 @@ static int __init keyinput_init(void)
         return ret;
     }
 
+    ret = keyio_init();
+    if (ret)
+        return ret;
+
     return 0;
 }
 
 static void __exit keyinput_exit(void)
 {
-    free_irq(keyinputdev.irq, &keyinputdev);
     del_timer_sync(&keyinputdev.timer);
+    free_irq(keyinputdev.irq, &keyinputdev);
     gpio_free(keyinputdev.key_gpio);
     input_unregister_device(keyinputdev.input);
+    input_free_device(keyinputdev.input);
 }
 
 module_init(keyinput_init);
@@ -591,6 +607,28 @@ input_sync(dev->input);
 
 `input_sync()` 必须在每次事件后调用,它向INPUT核心发送 `EV_SYN` / `SYN_REPORT` 事件,表示一次完整的输入事件已完成。
 
+### 5.6 4.1.15内核 timer_list API(原书第五十章)
+
+原书第五十章《Linux内核定时器实验》给出 4.1.15 内核的定时器接口签名,示例如下:
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `init_timer` | `void init_timer(struct timer_list *timer)` | 初始化定时器(新版内核已改为 `timer_setup`) |
+| `add_timer` | `void add_timer(struct timer_list *timer)` | 向内核注册并启动定时器 |
+| `mod_timer` | `int mod_timer(struct timer_list *timer, unsigned long expires)` | 修改超时时间;未激活时会激活,可用于周期性定时 |
+| `del_timer` | `int del_timer(struct timer_list *timer)` | 删除定时器(可在中断上下文调用) |
+| `del_timer_sync` | `int del_timer_sync(struct timer_list *timer)` | 同步版删除,等待其他CPU上的回调退出,不能在中断上下文调用 |
+
+关键点(4.1.15 与 4.15+ 的差异):
+
+- **回调原型**:4.1.15 为 `void (*function)(unsigned long)`;4.15+ 改为 `void (*function)(struct timer_list *)` 并配合 `from_timer()` 取回私有数据。
+- **私有数据**:4.1.15 通过 `timer.data = (unsigned long)dev;` 传递,回调内强转 `(struct key_input_dev *)arg`。
+- **时间换算**:内核 `CONFIG_HZ=100`(`HZ=100`),使用 `msecs_to_jiffies(ms)`、`jiffies_to_msecs()` 换算;`jiffies + msecs_to_jiffies(15)` 即 15ms 后超时。
+- **静态 vs 动态**:本示例定时器是全局/结构体成员,用 `init_timer()`;动态分配时同样先 `init_timer()` 再赋值 `function`/`data`。
+
+> [!note] 设备树中断
+> 使用 `irq_of_parse_and_map(nd, 0)` 时,设备树节点中必须有 `interrupt-parent` 与 `interrupts` 属性;若节点只有 `key-gpio` 而无 `interrupts`,则需改用 `gpio_to_irq()`。
+
 ---
 
 ## 6. 实验验证
@@ -648,6 +686,8 @@ rmmod key.ko
 
 ## 7. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性     | I.MX6ULL                              | STM32(MP1)         | RK3568                          |
 | -------- | ------------------------------------- | ------------------ | ------------------------------- |
 | GPIO库   | `gpio_request`/`gpio_direction_input` | `HAL_GPIO_ReadPin` | `gpio_request`/`gpio_get_value` |
@@ -667,6 +707,8 @@ rmmod key.ko
 
 ## 8. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: 为什么按键驱动要用原子变量而不是普通int?
 
 **答**:按键值是内核与用户空间的共享资源。驱动写入(`atomic_set`)、应用读取(`copy_to_user`)可能并发发生。使用原子变量保证操作的原子性,避免数据竞争。在SMP系统中,普通变量的读-改-写操作可能被中断或其他CPU打断,导致数据不一致。原子操作(`atomic_set`/`atomic_read`)在ARM架构上使用 `ldrex`/`strex` 指令实现无锁保护。
@@ -685,4 +727,10 @@ rmmod key.ko
 
 ### Q5: 如何在设备树中配置GPIO中断?`IRQF_TRIGGER_FALLING` 和 `IRQF_TRIGGER_RISING` 各表示什么?
 
-**答**:设备树中通过 `interrupts` 属性指定中断号和触发方式。驱动中使用 `gpio_to_irq()` 将GPIO转换为中断号,再用 `request_irq()` 申请中断。`IRQF_TRIGGER_FALLING` 表示下降沿触发(高→低,对应按键按下),`IRQF_TRIGGER_RISING` 表示上升沿触发(低→高,对应按键释放)。两者同时指定则双沿触发,可用于检测按键的按下和释放两个动作。
+**答**:设备树中通过 `interrupts` 属性指定中断号和触发方式。驱动中使用 `irq_of_parse_and_map()`(原书第五十八章做法)或 `gpio_to_irq()` 将节点/GPIO 转换为中断号,再用 `request_irq()` 申请中断。`IRQF_TRIGGER_FALLING` 表示下降沿触发(高→低,对应按键按下),`IRQF_TRIGGER_RISING` 表示上升沿触发(低→高,对应按键释放)。两者同时指定则双沿触发,可用于检测按键的按下和释放两个动作。
+
+---
+
+**代码来源**:正点原子I.MX6U嵌入式Linux驱动开发指南 V2.0.1(第四十九章)
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第49章 Linux按键输入实验
+**最后更新**: 2026-09-17

+ 148 - 33
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/03-LCD驱动.md

@@ -10,6 +10,8 @@ pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第
 
 ## LCD硬件概述
 
+> 📖 **章节说明**:本节 LCD 原理与 RGB 时序内容对应《指南》**第二十四章《RGBLCD显示实验》**(原书第五十九章仅引用之),属相邻章节补充。
+
 ### LCD工作原理
 
 LCD(Liquid Crystal Display)即液晶显示器,通过控制液晶分子的排列来实现显示。LCD面板由背光源、偏光片、液晶层、彩色滤光片和驱动电路组成。每个像素由红(R)、绿(G)、蓝(B)三个子像素组成,通过控制各子像素的透光量来混合出不同颜色。
@@ -89,9 +91,18 @@ static const struct file_operations fb_fops = {
     .read           = fb_read,
     .write          = fb_write,
     .unlocked_ioctl = fb_ioctl,
+#ifdef CONFIG_COMPAT
+    .compat_ioctl   = fb_compat_ioctl,
+#endif
     .mmap           = fb_mmap,
     .open           = fb_open,
     .release        = fb_release,
+#ifdef HAVE_ARCH_FB_UNMAPPED_AREA
+    .get_unmapped_area = get_fb_unmapped_area,
+#endif
+#ifdef CONFIG_FB_DEFERRED_IO
+    .fsync          = fb_deferred_io_fsync,
+#endif
     .llseek         = default_llseek,
 };
 ```
@@ -104,23 +115,37 @@ Linux内核将每个Framebuffer设备抽象为`fb_info`结构体,定义在`inc
 struct fb_info {
     atomic_t count;                          /* 引用计数 */
     int node;                                /* 设备节点号 */
+    int flags;                               /* 标志 */
     struct mutex lock;                       /* 互斥锁 */
+    struct mutex mm_lock;                    /* 互斥锁,用于fb_mmap和smem_*域 */
     struct fb_var_screeninfo var;            /* 当前可变参数(分辨率、色深等) */
     struct fb_fix_screeninfo fix;            /* 当前固定参数(物理地址、行宽等) */
     struct fb_monspecs monspecs;             /* 显示器特性 */
+    struct work_struct queue;                /* 帧缓冲事件队列 */
     struct fb_pixmap pixmap;                 /* 图像硬件映射 */
     struct fb_pixmap sprite;                 /* 光标硬件映射 */
     struct fb_cmap cmap;                     /* 当前调色板 */
+    struct list_head modelist;               /* 当前模式列表 */
     struct fb_videomode *mode;               /* 当前视频模式 */
+#ifdef CONFIG_FB_BACKLIGHT
+    struct backlight_device *bl_dev;         /* 背光设备 */
+    struct mutex bl_curve_mutex;             /* 背光曲线互斥锁 */
+    u8 bl_curve[FB_BACKLIGHT_LEVELS];        /* 背光曲线 */
+#endif
     struct fb_ops *fbops;                    /* 帧缓冲操作函数集 */
     struct device *device;                   /* 父设备 */
     struct device *dev;                      /* 当前fb设备 */
+    int class_flag;                          /* 私有sysfs标志 */
     char __iomem *screen_base;              /* 虚拟内存基地址(显存) */
     unsigned long screen_size;               /* 虚拟内存大小(显存大小) */
     void *pseudo_palette;                    /* 伪16位调色板 */
 };
 ```
 
+> 原书第五十九章重点关注的成员为 `var`、`fix`、`fbops`、`screen_base`、`screen_size`、`pseudo_palette`。
+
+> ⚠️ **来源说明**:下面的 `fb_var_screeninfo`、`fb_fix_screeninfo` 结构体定义取自内核 `include/linux/fb.h`,**原书第五十九章并未逐字段列出**,此处为扩展补充。
+
 `fb_var_screeninfo`存储可变的显示参数:
 
 ```c
@@ -246,8 +271,27 @@ pinctrl_lcdif_ctrl: lcdifctrlgrp {
 
 > **注意**:电气属性值`0x49`表示降低驱动能力至R0/1,用于避免模拟开关影响信号完整性。
 
+### lcdif 控制器节点(imx6ull.dtsi)
+
+由芯片级 `imx6ull.dtsi` 提供,所有 I.MX6ULL 板子共有,`compatible` 决定其由 `mxsfb.c` 驱动:
+
+```dts
+lcdif: lcdif@021c8000 {
+    compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
+    reg = <0x021c8000 0x4000>;
+    interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
+    clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
+             <&clks IMX6UL_CLK_LCDIF_APB>,
+             <&clks IMX6UL_CLK_DUMMY>;
+    clock-names = "pix", "axi", "disp_axi";
+    status = "disabled";
+};
+```
+
 ### 完整lcdif节点示例
 
+> 说明:原书 NXP 官方默认节点为 4.3寸 480×272 屏(`clock-frequency=<9200000>`、`hfront-porch=<8>`、`hback-porch=<4>`、`hsync-len=<41>`、`vback-porch=<2>`、`vfront-porch=<4>`、`vsync-len=<10>`,`bits-per-pixel=<16>`),本教程需替换为 ATK7016(7寸 1024×600)参数。
+
 ```dts
 &lcdif {
     pinctrl-names = "default";
@@ -285,7 +329,24 @@ pinctrl_lcdif_ctrl: lcdifctrlgrp {
 
 ### 背光节点配置
 
+背光控制引脚为 `GPIO1_IO08`,复用为 `PWM1_OUT`,需要三处配置:引脚 pinctrl、`&pwm1` 节点、`backlight` 节点。
+
 ```dts
+/* 1. GPIO1_IO08 引脚复用为 PWM1_OUT */
+pinctrl_pwm1: pwm1grp {
+    fsl,pins = <
+        MX6UL_PAD_GPIO1_IO08__PWM1_OUT   0x110b0
+    >;
+};
+
+/* 2. 使能 pwm1 */
+&pwm1 {
+    pinctrl-names = "default";
+    pinctrl-0 = <&pinctrl_pwm1>;
+    status = "okay";
+};
+
+/* 3. 将 PWM1 与 LCD 背光关联 */
 backlight {
     compatible = "pwm-backlight";
     pwms = <&pwm1 0 5000000>;     /* PWM1, 周期5ms(200Hz) */
@@ -295,6 +356,8 @@ backlight {
 };
 ```
 
+> 原书说明:`brightness-levels` 共 8 级(0~7),对应占空比 0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%;`default-brightness-level = <6>` 即 50.19% 亮度。PWM 背光驱动文件为 `drivers/video/backlight/pwm_bl.c`,设备树绑定参考 `Documentation/devicetree/bindings/video/backlight/pwm-backlight.txt`。
+
 ## LCD驱动API
 
 ### framebuffer注册函数
@@ -428,11 +491,14 @@ static const struct of_device_id mxsfb_dt_ids[] = {
 };
 
 static struct platform_driver mxsfb_driver = {
-    .probe  = mxsfb_probe,
-    .remove = mxsfb_remove,
-    .driver = {
+    .probe    = mxsfb_probe,
+    .remove   = mxsfb_remove,
+    .shutdown = mxsfb_shutdown,
+    .id_table = mxsfb_devtype,
+    .driver   = {
         .name           = DRIVER_NAME,
         .of_match_table = mxsfb_dt_ids,
+        .pm             = &mxsfb_pm_ops,
     },
 };
 
@@ -462,50 +528,86 @@ flowchart TB
 ```c
 static int mxsfb_probe(struct platform_device *pdev)
 {
+    const struct of_device_id *of_id =
+            of_match_device(mxsfb_dt_ids, &pdev->dev);
     struct resource *res;
     struct mxsfb_info *host;
     struct fb_info *fb_info;
-    int irq, ret;
-
-    /* 1. 获取中断号 */
-    irq = platform_get_irq(pdev, 0);
+    struct pinctrl *pinctrl;
+    int irq = platform_get_irq(pdev, 0);
+    int gpio, ret;
 
-    /* 2. 获取寄存器资源首地址(0x021C8000) */
+    /* 1. 获取寄存器资源首地址(0x021C8000) */
     res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+    if (!res)
+        return -ENODEV;
 
-    /* 3. 分配mxsfb_info结构体 */
-    host = devm_kzalloc(&pdev->dev, sizeof(*host), GFP_KERNEL);
+    /* 2. 分配mxsfb_info结构体 */
+    host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL);
+    if (!host)
+        return -ENOMEM;
 
-    /* 4. 分配fb_info结构体 */
+    /* 3. 分配fb_info结构体 */
     fb_info = framebuffer_alloc(sizeof(struct fb_info), &pdev->dev);
-    host->fb_info = fb_info;
-    fb_info->par = host;    /* 关联host和fb_info */
+    if (!fb_info) {
+        devm_kfree(&pdev->dev, host);
+        return -ENOMEM;
+    }
+    host->fb_info = fb_info;    /* host->fb_info */
+    fb_info->par = host;        /* fb_info->par 关联host */
 
-    /* 5. 申请中断 */
-    ret = devm_request_irq(&pdev->dev, irq,
-                           mxsfb_irq_handler, 0,
+    /* 4. 申请中断 */
+    ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0,
                            dev_name(&pdev->dev), host);
+    if (ret)
+        goto fb_release;
 
-    /* 6. 寄存器地址映射 */
+    /* 5. 寄存器地址映射 */
     host->base = devm_ioremap_resource(&pdev->dev, res);
+    if (IS_ERR(host->base)) {
+        ret = PTR_ERR(host->base);
+        goto fb_release;
+    }
 
-    /* 7. 分配pseudo_palette调色板 */
+    /* 6. 分配pseudo_palette调色板 */
     fb_info->pseudo_palette = devm_kzalloc(&pdev->dev,
-                                           sizeof(u32) * 16,
-                                           GFP_KERNEL);
+                                           sizeof(u32) * 16, GFP_KERNEL);
+    if (!fb_info->pseudo_palette) {
+        ret = -ENOMEM;
+        goto fb_release;
+    }
 
-    /* 8. 初始化fb_info (从设备树获取参数) */
-    ret = mxsfb_init_fbinfo(host);
+    INIT_LIST_HEAD(&fb_info->modelist);
+    pm_runtime_enable(&host->pdev->dev);
 
-    /* 9. 配置pinctrl引脚 */
-    pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
+    /* 7. 初始化fb_info (从设备树获取参数,并申请显存) */
+    ret = mxsfb_init_fbinfo(host);
+    if (ret != 0)
+        goto fb_pm_runtime_disable;
+
+    mxsfb_dispdrv_init(pdev, fb_info);
+
+    /* 8. 配置pinctrl引脚 */
+    if (!host->dispdrv) {
+        pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
+        if (IS_ERR(pinctrl)) {
+            ret = PTR_ERR(pinctrl);
+            goto fb_pm_runtime_disable;
+        }
+    }
 
-    /* 10. 初始化LCD控制器寄存器 */
-    mxsfb_set_par(fb_info);
-    mxsfb_enable_controller(fb_info);
+    /* 9. 初始化LCD控制器寄存器 */
+    if (!host->enabled) {
+        writel(0, host->base + LCDC_CTRL);
+        mxsfb_set_par(fb_info);
+        mxsfb_enable_controller(fb_info);
+        pm_runtime_get_sync(&host->pdev->dev);
+    }
 
-    /* 11. 注册framebuffer设备 */
+    /* 10. 注册framebuffer设备 */
     ret = register_framebuffer(fb_info);
+    if (ret != 0)
+        goto fb_destroy;
 
     return ret;
 }
@@ -517,13 +619,17 @@ static int mxsfb_probe(struct platform_device *pdev)
 #define LCDC_CTRL               0x00
 #define LCDC_CTRL1              0x10
 #define LCDC_V4_CTRL2           0x20
+#define LCDC_V3_TRANSFER_COUNT  0x20
 #define LCDC_V4_TRANSFER_COUNT  0x30
 #define LCDC_V4_CUR_BUF         0x40
 #define LCDC_V4_NEXT_BUF        0x44
 #define LCDC_V4_DTDMA           0x180
 #define LCDC_V4_DEBUG0          0x1d0
+#define LCDC_V3_DEBUG0          0x1f0
 ```
 
+> 原书提示:可对照《I.MX6ULL 参考手册》eLCDIF 章节核对这些偏移值。
+
 **mxsfb_ops操作集:**
 
 ```c
@@ -570,6 +676,8 @@ sequenceDiagram
 
 ## 实验验证
 
+> ⚠️ **来源说明**:原书第五十九章使用 `make dtbs`、Linux logo、`/dev/tty1`、背光 sysfs 等方式验证;本节中的 `fbset` 命令不属于原书内容,为扩展知识。
+
 ### fbset命令
 
 `fbset`工具用于查看和修改Framebuffer设备参数:
@@ -649,6 +757,8 @@ int main(void)
 
 ## 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性       | I.MX6ULL              | STM32F7             | RK3568              |
 | ---------- | --------------------- | ------------------- | ------------------- |
 | LCD控制器  | eLCDIF                | LTDC                | VOP                 |
@@ -668,6 +778,8 @@ int main(void)
 
 ## 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: Framebuffer设备的本质是什么?用户空间如何访问?
 
 **答**:Framebuffer是Linux内核对显示设备的字符设备抽象,设备节点为`/dev/fbX`。用户空间通过标准文件操作访问:
@@ -716,11 +828,14 @@ Pixel_Clock = (HBP + Hactive + HFP) × (VBP + Vactive + VFP) × Refresh_Rate
 
 ### Q5: 如何解决LCD自动熄屏问题?有几种方案?
 
-**答**:Linux内核默认10分钟后熄屏(`blankinterval = 10*60`),有三种解决方案:
+**答**:Linux内核默认10分钟后熄屏(`blankinterval = 10*60`)。原书给出三种解决方案:
 
-1. **按键唤醒**:按下回车键唤醒屏幕(最简单)
+1. **按键唤醒**(最简单):原书第五十八章已将开发板 KEY 注册为回车键,按回车即可唤醒屏幕
 2. **修改内核源码**:将`drivers/tty/vt/vt.c`中`blankinterval`改为0,重新编译内核
-3. **应用程序方式**:编写程序向`/dev/tty1`写入`\033[9;0]`转义序列禁用屏幕保护,并设置开机自启动
-4. **修改内核配置**:通过`make menuconfig`禁用DPMS(Display Power Management Signaling)
+3. **应用程序方式**:编写程序向`/dev/tty1`写入`\033[9;0]`转义序列禁用屏幕保护,并加入`/etc/init.d/rcS`实现开机自启动
+
+> 补充(扩展):也可通过 `make menuconfig` 关闭 DPMS(Display Power Management Signaling)。原书推荐方案3,无需重新编译内核、部署灵活。
+
+---
 
-推荐方案3,无需重新编译内核,部署灵活。
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第59章 Linux LCD驱动实验(可参考第24章 RGBLCD显示实验)

+ 95 - 19
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动.md

@@ -19,6 +19,8 @@ pdf_ref: "正点原子 I.MX6U嵌入式Linux驱动开发指南 V2.0.1 - 第六十
 
 ### 1.1 电阻式 vs 电容式触摸屏
 
+> ⚠️ **来源说明**:原书第六十四章仅在 64.1.1 小结中回顾电容触摸原理(并指向第二十八章裸机实验),本节对比表为扩展知识。
+
 | 特性     | 电阻式触摸屏        | 电容式触摸屏          |
 | -------- | ------------------- | --------------------- |
 | 工作原理 | 压力导通两层电阻膜  | 手指感应电场变化      |
@@ -173,7 +175,7 @@ EV_ABS   // 绝对坐标事件(触摸坐标)
 IMX6ULL 的触摸屏中断配置涉及:
 
 - `interrupt-parent`:中断所属 GPIO 控制器
-- `interrupts`:中断号和触发方式(0 = 下降沿触发)
+- `interrupts`:`<GPIO引脚号 触发类型>`。第 2 个 cell `0` 表示 `IRQ_TYPE_NONE`(**不指定触发方式**),实际触发方式由驱动程序 `request` 时的 `IRQF_TRIGGER_FALLING` 决定,即下降沿触发
 - `interrupt-gpios`:中断 GPIO 引脚描述
 
 ### 3.3 pinctrl 配置
@@ -277,10 +279,17 @@ int input_mt_init_slots(struct input_dev *dev,
                         unsigned int flags);
 ```
 
-初始化 Type B 多点触控的 slot 数量。`num_slots` 为最大支持触摸点数,`flags` 常用值
+初始化 Type B 多点触控的 slot 数量。`num_slots` 为最大支持触摸点数,`flags` 可选值(定义于 `include/uapi/linux/input.h`,可用 `|` 组合)
 
-- `INPUT_MT_DIRECT`:直接输入设备(触摸屏)
-- `INPUT_MT_DROP_UNUSED`:丢弃未使用的触摸点
+```c
+#define INPUT_MT_POINTER        0x0001  /* 间接指针设备,如触摸板 */
+#define INPUT_MT_DIRECT         0x0002  /* 直接输入设备,如触摸屏 */
+#define INPUT_MT_DROP_UNUSED    0x0004  /* 丢弃本帧未出现的触摸点 */
+#define INPUT_MT_TRACK          0x0008  /* 使用内核内部tracking */
+#define INPUT_MT_SEMI_MT        0x0010  /* 半MT设备,手指数量手动处理 */
+```
+
+原书调用示例:`input_mt_init_slots(input, MAX_SUPPORT_POINTS, 0);`(`0` 表示不附加任何 flag)。
 
 ### 4.4 input_mt_slot
 
@@ -328,7 +337,10 @@ void input_mt_report_pointer_emulation(struct input_dev *dev,
                                        bool use_count);
 ```
 
-兼容单点触摸,上报 `BTN_TOOL_TAP` 事件供不支持 MT 协议的应用程序使用。
+指针模拟,供不支持 MT 协议的单点应用程序使用。`use_count` 参数:
+
+- `true`:上报**当前有效触摸点数量**(内核自动读取,无需用户指定),同时模拟 `ABS_X`/`ABS_Y`/`BTN_TOUCH` 事件;
+- `false`:**硬件追踪到的触摸点数量多于当前上报数量**,此时驱动应先用 `BTN_TOOL_*TAP` 事件告知用户空间追踪到的触摸点总数,再传 `false`。
 
 ### 4.9 devm_request_threaded_irq
 
@@ -730,6 +742,8 @@ SYN_REPORT          value=0   ← 同步
 
 ## 6. 触摸校准
 
+> ⚠️ **来源说明**:原书第六十四章仅介绍使用 tslib 的 `ts_calibrate` 进行校准,本节中的五点校准算法、最小二乘拟合与 `/etc/pointercal` 格式为扩展知识。
+
 ### 6.1 为什么需要校准
 
 - 电阻式触摸屏:必须校准,存在坐标漂移
@@ -796,22 +810,32 @@ modprobe ft5x06.ko                  # 加载驱动模块
 hexdump /dev/input/event2
 ```
 
-触摸屏幕并观察输出,每行数据格式为:
+单指触摸右上角再抬起,原书实测原始数据(整理后)为:
 
 ```
-tv_sec  tv_usec  type  code  value
-02bb    9459    0003  002f  00000000    ← ABS_MT_SLOT 0
-02bb    9459    0003  0039  00000005    ← ABS_MT_TRACKING_ID 5
-02bb    9459    0003  0035  000003ec    ← ABS_MT_POSITION_X 1004
-02bb    9459    0003  0036  00000017    ← ABS_MT_POSITION_Y 23
-02bb    9459    0001  014a  00000001    ← BTN_TOUCH 按下
-02bb    9459    0000  0000  00000000    ← SYN_REPORT
+/* 编号 */   /* tv_sec */  /* tv_usec */  /* type */  /* code */  /* value */
+0000000     02bb 0000     9459 0007       0003        002f        0000 0000   ← ABS_MT_SLOT 0
+0000010     02bb 0000     9459 0007       0003        0039        0005 0000   ← ABS_MT_TRACKING_ID 5
+0000020     02bb 0000     9459 0007       0003        0035        03ec 0000   ← ABS_MT_POSITION_X 1004
+0000030     02bb 0000     9459 0007       0003        0036        0017 0000   ← ABS_MT_POSITION_Y 23
+0000040     02bb 0000     9459 0007       0001        014a        0001 0000   ← BTN_TOUCH 按下
+0000050     02bb 0000     9459 0007       0003        0000        03ec 0000   ← ABS_X 1004(指针模拟)
+0000060     02bb 0000     9459 0007       0003        0001        0017 0000   ← ABS_Y 23(指针模拟)
+0000070     02bb 0000     9459 0007       0000        0000        0000 0000   ← SYN_REPORT
+0000080     02bb 0000     e5f8 0008       0003        0039        ffff ffff   ← ABS_MT_TRACKING_ID -1(抬起)
+0000090     02bb 0000     e5f8 0008       0001        014a        0000 0000   ← BTN_TOUCH 抬起
+00000a0     02bb 0000     e5f8 0008       0000        0000        0000 0000   ← SYN_REPORT
 ```
 
+其中 `ABS_X`/`ABS_Y`(code 0x0000/0x0001)由 `input_mt_report_pointer_emulation()` 为单点应用模拟上报;`ABS_MT_TRACKING_ID = -1` 表示触摸点离开。
+
+
 ### 7.3 tslib 测试
 
+原书 tslib 环境变量写入 `/etc/profile`,并将驱动编入内核后触摸设备设为 `/dev/input/event1`;若使用模块方式则 event 编号可能为 `event2`,需按实际情况修改 `TSLIB_TSDEVICE`。
+
 ```bash
-# 安装 tslib 后配置环境变量
+# 安装 tslib 后配置环境变量(写入 /etc/profile)
 export TSLIB_TSDEVICE=/dev/input/event2
 export TSLIB_CALIBFILE=/etc/pointercal
 export TSLIB_CONFFILE=/etc/ts.conf
@@ -844,21 +868,67 @@ evtest /dev/input/event2
 
 ### 7.5 将驱动编译到内核
 
+原书对应第 64.4.3 节,步骤如下:
+
 ```bash
-# 1. 复制驱动文件
-cp ft5x06.c (内核源码)/drivers/input/touchscreen/
+# 1. 复制驱动文件到触摸屏驱动目录
+cp ft5x06.c (内核源码)/drivers/input/touchscreen/ -f
 
-# 2. 修改 Makefile
-echo "obj-y += ft5x06.o" >> drivers/input/touchscreen/Makefile
+# 2. 修改 drivers/input/touchscreen/Makefile,末尾添加
+#    obj-y += ft5x06.o
 
-# 3. 重新编译内核
+# 3. 重新编译内核,用新 zImage 启动
 make -j32
 ```
 
+> 原书提醒:编译为内核后触摸屏的 event 编号会变化(模块时为 `event2`,编入内核后变为 `event1`),需重新确认。
+
+### 7.6 使用内核自带的 edt-ft5x06 驱动(原书 64.6 节)
+
+内核自带 `drivers/input/touchscreen/edt-ft5x06.c`,可驱动 FT5206/FT5406/FT5426。使用步骤:
+
+1. **移除自定义驱动**:删除 `drivers/input/touchscreen/Makefile` 中的 `obj-y += ft5x06.o`
+2. **使能内核驱动**:`make menuconfig` → `Device Drivers → Input device support → Touchscreens` → `<*> EDT FocalTech FT5x06 I2C Touchscreen support`
+3. **修改设备树**:内核原生 `edt_ft5x06_of_match` 只支持三个 compatible:
+
+```c
+static const struct of_device_id edt_ft5x06_of_match[] = {
+    { .compatible = "edt,edt-ft5206", },
+    { .compatible = "edt,edt-ft5306", },
+    { .compatible = "edt,edt-ft5406", },
+    { /* sentinel */ }
+};
+```
+
+因此需给 `ft5426` 节点补充一条(三选一或改驱动匹配表):
+
+```dts
+ft5426: ft5426@38 {
+    compatible = "edt,edt-ft5426","edt,edt-ft5406";
+    reg = <0x38>;
+    pinctrl-names = "default";
+    pinctrl-0 = <&pinctrl_tsc>;
+    interrupt-parent = <&gpio1>;
+    interrupts = <9 0>;
+    reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
+    interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
+};
+```
+
+### 7.7 GT9147 / GT911 驱动(原书 64.7、64.8 节)
+
+正点原子 4.3 寸屏(800×480、480×272)触摸 IC 为 **GT9147**;新版 7 寸屏换用 **GT911**。GT9147、GT911、GT1151 三款 IC 几乎一致,可直接用同一驱动文件 `gt9147.c`。
+
+- GT9147/GT911 设备树节点:`compatible = "goodix,gt9147", "goodix,gt9xx";`,`reg = <0x14>`
+- 4.3寸屏 lcdif 时序(原书 64.7.3):480×272 用 `clock-frequency=<9000000>`;800×480 用 `clock-frequency=<31000000>`
+- 原书说明:`gt9147.c` 实现的是**单点触摸**,因为 GT9147 硬件无法检测每个触摸点的按下/抬起,多点上报效果不理想
+
 ---
 
 ## 8. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 8.1 IMX6ULL vs STM32 vs RK3568 触摸驱动差异
 
 | 特性         | IMX6ULL                   | STM32MP1      | RK3568        |
@@ -885,6 +955,8 @@ make -j32
 
 ## 9. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### Q1: Type A 和 Type B 多点触控协议的核心区别是什么?为什么现在主流使用 Type B?
 
 **答**:Type A 协议适用于硬件无法单独跟踪触摸点的设备,每个触摸点上报后用 `SYN_MT_REPORT` 分隔,内核需要根据坐标聚类来确定哪些事件属于同一个触摸点,处理开销大且精度低。Type B 协议通过 `slot` 机制和 `ABS_MT_TRACKING_ID` 为每个触摸点分配唯一标识,硬件可以跟踪并维护触摸点的生命周期,内核直接按 slot 处理,效率高且准确。现代触摸 IC(如 FT5426、GT911)都支持硬件级别的触摸点跟踪,因此 Type B 成为主流。
@@ -904,3 +976,7 @@ make -j32
 ### Q5: 如何将触摸屏驱动从模块编译改为编译进内核?需要注意什么?
 
 **答**:步骤:(1) 将驱动文件复制到 `drivers/input/touchscreen/`;(2) 修改该目录下的 Makefile,添加 `obj-y += ft5x06.o`(而非 `obj-m`);(3) 重新编译内核。需要注意:(a) 驱动编译进内核后,设备树中的 compatible 字符串必须与驱动的 of_match_table 匹配;(b) 确保设备树中触摸屏节点的 IO 配置正确且未被其他外设占用;(c) 编译进内核后,触摸屏设备的 event 编号可能与模块方式不同,需重新确认。
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第64章 Linux 多点电容触摸屏实验(可参考第28章 多点电容触摸屏实验)

+ 262 - 26
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/05-音频驱动.md

@@ -56,10 +56,10 @@ WM8960是Wolfson公司的低功耗立体声音频CODEC,主要特性:
 ├─────────────────────────────────────────────────────────────┤
 │  功能特性:                                                  │
 │  - 3D增强效果                                                │
-│  - 双路D类功放(1W/8Ω)             
-│  - 耳机驱动    
-│  - 数字麦克风接口                                            │
-│  - 支持8K~48K采样率                                     
+│  - 立体声D类功放(8Ω负载下每通道1W)
+│  - 集成耳机接口
+│  - 集成麦克风接口                                            │
+│  - 采样率支持8K/11.025K/12K/16K/22.05K/24K/32K/44.1K/48K
 └─────────────────────────────────────────────────────────────┘
 ```
 
@@ -139,6 +139,8 @@ SAI(Synchronous Audio Interface)是I.MX6ULL的同步音频接口,主要特
 
 ## 2. ALSA架构
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书第六十五章未讲解 ASoC 框架内部结构,本实验直接使能内核自带的 WM8960/ES8388 驱动。
+
 ### 2.1 ALSA子系统架构
 
 ALSA(Advanced Linux Sound Architecture)是Linux内核的标准音频框架:
@@ -247,7 +249,9 @@ I.MX6ULL使用SAI2连接音频Codec:
 
 ### 3.2 Audio Codec节点
 
-#### WM8960节点(I2C1)
+> 说明(原书 65.2/65.3.2):WM8960 所挂 I2C 总线随底板版本不同——V2.4 以前挂 **I2C2**,V2.4 及以后挂 **I2C1**;ES8388 始终挂 I2C1。V2.4/V2.8 及以后的 `sound` 节点通过 `audio-codec = <&codec1 &codec2>` 同时引用两个 WM8960 子节点。
+
+#### WM8960节点(I2C1,V2.4及以后)
 
 ```dts
 &i2c1 {
@@ -261,6 +265,20 @@ I.MX6ULL使用SAI2连接音频Codec:
 };
 ```
 
+#### WM8960节点(I2C2,V2.4以前)
+
+```dts
+&i2c2 {
+    codec2: wm8960@1a {
+        compatible = "wlf,wm8960";
+        reg = <0x1a>;
+        clocks = <&clks IMX6UL_CLK_SAI2>;
+        clock-names = "mclk";
+        wlf,shared-lrclk;
+    };
+};
+```
+
 #### ES8388节点(I2C1)
 
 ```dts
@@ -384,6 +402,8 @@ I.MX6ULL使用SAI2连接音频Codec:
 
 ## 4. 音频驱动API
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书第六十五章仅介绍“使能内核自带音频驱动”,未涉及自己编写 ASoC/ALSA 驱动所需的注册 API。
+
 ### 4.1 声卡注册API
 
 ```c
@@ -421,24 +441,37 @@ int snd_pcm_set_managed_buffer_all(struct snd_pcm *pcm,
 ### 4.3 ASoC注册API
 
 ```c
-// 注册声卡
+// 注册声卡(Machine驱动入口)
 int snd_soc_register_card(struct snd_soc_card *card);
+int devm_snd_soc_register_card(struct device *dev, struct snd_soc_card *card);
 
 // 注册DAI驱动
 int snd_soc_register_dai(struct snd_soc_component *component,
                          struct snd_soc_dai_driver *dai_drv);
 
-// 注册Codec驱动
+// 注册Codec/CPU DAI组件(cmpnt_drv 提供 probe/remove/controls/dapm 等)
 int snd_soc_register_component(struct device *dev,
                                const struct snd_soc_component_driver *cmpnt_drv,
                                struct snd_soc_dai_driver *dai_drv,
                                int num_dai);
+int devm_snd_soc_register_component(struct device *dev,
+                                    const struct snd_soc_component_driver *cmpnt_drv,
+                                    struct snd_soc_dai_driver *dai_drv,
+                                    int num_dai);
 ```
 
+关键结构体:
+
+- `snd_soc_component_driver`:组件(Codec/CPU DAI/Platform)通用回调集,含 `.controls`、`.dapm_widgets`、`.dapm_routes`、`.probe`、`.remove` 等。
+- `snd_soc_dai_ops`:DAI 级操作集,核心回调为 `.hw_params`(配置采样率、位宽、时钟)、`.set_fmt`(设置 I2S 格式与主从)、`.set_sysclk`、`.trigger`、`.startup`、`.shutdown`。
+- `snd_soc_dai_driver` 通过 `.ops` 指向一个 `snd_soc_dai_ops`,通过 `.playback`/`.capture` 描述 `snd_soc_pcm_stream`(声道数、采样率、格式)。
+
 ---
 
 ## 5. 完整源码分析
 
+> ⚠️ **来源说明**:本节大部分内容不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书第六十五章只在 65.5.3 小节摘录了 `wm8960.c` 的 `wm8960_reg_defaults` 数组用于修改单声道 MIC 录音,并未展开分析 `imx-audio-wm8960.c`/`fsl_sai.c` 的源码。下表中 5.3 的寄存器数组片段对应原书内容,其余为扩展。
+
 ### 5.1 Machine驱动框架
 
 Machine驱动是连接CPU DAI和Codec的胶水层:
@@ -494,7 +527,6 @@ static struct snd_soc_dai_driver fsl_sai_dai = {
                    SNDRV_PCM_FMTBIT_S32_LE,
     },
     .ops = &fsl_sai_pcm_dai_ops,
-    .ops = &fsl_sai_pcm_dai_ops,
 };
 ```
 
@@ -503,15 +535,16 @@ static struct snd_soc_dai_driver fsl_sai_dai = {
 WM8960 Codec驱动关键代码:
 
 ```c
-// wm8960.c 寄存器默认值
+// wm8960.c 寄存器默认值(原书示例代码 65.5.3.2,共 R0~R55 计 56 个寄存器)
 static const struct reg_default wm8960_reg_defaults[] = {
-    { 0x0, 0x00a7 },   // 左输入音量
-    { 0x1, 0x00a7 },   // 右输入音量
-    { 0x2, 0x0000 },   // 左输出音量
-    { 0x3, 0x0000 },   // 右输出音量
-    // ... 共58个寄存器
-    { 0x17, 0x01c4 },  // ADC控制 - 左右声道使用左ADC
-    { 0x37, 0x00e9 },  // 类DAC控制
+    { 0x0, 0x00a7 },
+    // ...
+    { 0x16, 0x00c3 },
+    /*{ 0x17, 0x01c0 },*/   // 原默认值:左右声道各自使用本侧 ADC
+    { 0x17, 0x01c4 },      // 修改后:左右声道数据均使用左 ADC
+    { 0x18, 0x0000 },
+    // ...
+    { 0x37, 0x00e9 },      // R55 Class-D Control 2
 };
 
 // DAI驱动
@@ -603,52 +636,143 @@ aplay -D plughw:0,0 test.wav
 aplay -l
 ```
 
-### 6.3 录音测试
+### 6.3 MIC录音测试(WM8960)
+
+在开发板根文件系统的 `/music` 目录下创建 `mic_in_config.sh`(对应原书示例代码 65.5.3.1),完整内容如下:
 
 ```bash
-# MIC录音配置脚本
 #!/bin/sh
+# 正点原子@ALIENTEK
+# 设置捕获的音量
 amixer cset name='Capture Volume' 90,90
+
+# PCM
 amixer sset 'PCM Playback' on
 amixer sset 'Playback' 256
 amixer sset 'Right Output Mixer PCM' on
 amixer sset 'Left Output Mixer PCM' on
+
+# ADC PCM
 amixer sset 'ADC PCM' 200
+
+# 耳机/喇叭(扬声器)设置播放音量,直流/交流
+# Turn on Headphone
+amixer sset 'Headphone Playback ZC' on
+# Set the volume of your headphones(98% volume, 127 is the MaxVolume)
 amixer sset Headphone 125,125
+# Turn on the speaker
+amixer sset 'Speaker Playback ZC' on
+# Set the volume of your Speaker(98% volume, 127 is the MaxVolume)
 amixer sset Speaker 125,125
+# Set the volume of your Speaker AC(80% volume, 100 is the MaxVolume)
 amixer sset 'Speaker AC' 4
+# Set the volume of your Speaker DC(80% volume, 5 is the MaxVolume)
 amixer sset 'Speaker DC' 4
 
-# 录制10秒CD质量音频
-arecord -f cd -d 10 record.wav
+# 音频输入,左声道管理
+# Turn on Left Input Mixer Boost
+amixer sset 'Left Input Mixer Boost' off
+amixer sset 'Left Boost Mixer LINPUT1' off
+amixer sset 'Left Input Boost Mixer LINPUT1' 0
+amixer sset 'Left Boost Mixer LINPUT2' off
+amixer sset 'Left Input Boost Mixer LINPUT2' 0
+# Turn off Left Boost Mixer LINPUT3
+amixer sset 'Left Boost Mixer LINPUT3' off
+amixer sset 'Left Input Boost Mixer LINPUT3' 0
+
+# 音频输入,右声道管理
+# Turn on Right Input Mixer Boost
+amixer sset 'Right Input Mixer Boost' on
+amixer sset 'Right Boost Mixer RINPUT1' off
+amixer sset 'Right Input Boost Mixer RINPUT2' 0
+amixer sset 'Right Boost Mixer RINPUT2' on
+amixer sset 'Right Input Boost Mixer RINPUT2' 127
+amixer sset 'Right Boost Mixer RINPUT3' off
+amixer sset 'Right Input Boost Mixer RINPUT3' 0
+```
 
-# 录制完成后播放
-aplay record.wav
+```bash
+chmod 777 mic_in_config.sh   # 给予可执行权限
+./mic_in_config.sh           # 运行
+arecord -f cd -d 10 record.wav   # 录制10秒CD质量音频
+aplay record.wav                 # 录制完成后播放
 ```
 
-### 6.4 Line IN录音配置
+说明:`-f cd` 表示录音质量为 CD 级别(16bit/44.1kHz/立体声),`-d` 指定录音时间(单位 s)。ALPHA 开发板的 MIC 只接了左声道,因此录出的音频只有左声道有数据。
+
+#### 单声道 MIC 录制立体声音频
+
+要让只接左声道的 MIC 录出双声道,可修改 WM8960 的 R23 寄存器(地址 `0x17`)的 bit3:2,让左右声道 ADC 都使用左 ADC 数据。`wm8960_reg_defaults` 中 `0x17` 默认值为 `0x01C0`,改为 `0x01C4` 即可(详见第 8 节 Q4)。修改后需重新编译内核并启动。
+
+### 6.4 LINE IN录音测试(WM8960)
+
+Line IN 接口不能接话筒,需使用一根 3.5mm 公对公音频线连接手机/电脑与开发板 Line in 接口。
+
+创建 `line_in_config.sh`(对应原书示例代码 65.5.4.2):
 
 ```bash
 #!/bin/sh
+# 正点原子@ALIENTEK
+# 设置捕获的音量
 amixer cset name='Capture Volume' 100,100
+
+# PCM
 amixer sset 'PCM Playback' on
 amixer sset 'Playback' 256
 amixer sset 'Right Output Mixer PCM' on
 amixer sset 'Left Output Mixer PCM' on
+
+# ADC PCM
 amixer sset 'ADC PCM' 200
 
-# Line IN输入配置
+# 耳机/喇叭(扬声器)设置播放音量,直流/交流
+amixer sset 'Headphone Playback ZC' on
+amixer sset Headphone 125,125
+amixer sset 'Speaker Playback ZC' on
+amixer sset Speaker 125,125
+amixer sset 'Speaker AC' 4
+amixer sset 'Speaker DC' 4
+
+# 音频输入,左声道管理
+# Turn off Left Input Mixer Boost
 amixer sset 'Left Input Mixer Boost' on
+# 关闭其他通道输入
+amixer sset 'Left Boost Mixer LINPUT1' off
+amixer sset 'Left Input Boost Mixer LINPUT1' 0
+# 关闭麦克风左声道输入
 amixer sset 'Left Boost Mixer LINPUT2' on
 amixer sset 'Left Input Boost Mixer LINPUT2' 127
+# Line_in 右声道输入关闭
+amixer sset 'Left Boost Mixer LINPUT3' off
+amixer sset 'Left Input Boost Mixer LINPUT3' 0
+
+# 音频输入,右声道管理
+# Turn on Right Input Mixer Boost
 amixer sset 'Right Input Mixer Boost' on
+amixer sset 'Right Boost Mixer RINPUT1' off
+amixer sset 'Right Input Boost Mixer RINPUT1' 0
+amixer sset 'Right Boost Mixer RINPUT2' off
+amixer sset 'Right Input Boost Mixer RINPUT2' 0
+
+# 要想设置成音频输入,请打开 RINPUT3,看原理图可知
+# 其他的声道通过上面的配置可关闭,这样是为了避免干扰,需要的时候就打开
+# RINPUT3 打开(关键点)
 amixer sset 'Right Boost Mixer RINPUT3' on
 amixer sset 'Right Input Boost Mixer RINPUT3' 127
+```
 
-# 录制音频
-arecord -f cd -d 10 line_in.wav
+```bash
+chmod 777 line_in_config.sh
+./line_in_config.sh
+arecord -f cd -d 10 record.wav
 ```
 
+ALPHA 开发板 Line IN 接的是左右双声道,因此录制出来是立体声,不像 MIC 只有左声道。若在 MIC 实验里把 R23 改为 `0x01C4`,做 Line IN 测试前应先改回 `0x01C0`(也可不改,不影响测试)。
+
+### 6.4.1 ES8388 录音测试(V2.8 及以后底板)
+
+出厂文件系统已预置音频测试脚本 `/home/root/shell/audio/atk-audio.sh`,直接在文件系统中执行 `./atk-audio.sh`,按提示即可进行板载 MIC 录音测试和 Line IN 录音测试。
+
 ### 6.5 开机自动配置
 
 ```bash
@@ -683,12 +807,118 @@ fi
                 -> SoC Audio for Freescale CPUs
                     -> <*> Asynchronous Sample Rate Converter (ASRC)
                     -> <*> SoC Audio support for i.MX boards with wm8960
+
+# 使能ES8388驱动(V2.8及以后底板)
+SND_SOC_FSL_ASOC_CARD = y   # i.MX ASoC card 支持
+SND_SOC_ES8388 = y          # ES8388 codec 驱动
 ```
 
+ES8388 使能后重新编译内核与设备树,启动后 ALSA 设备列表中应出现 `es8388-audio`(与设备树 `sound-es8388` 节点 `model` 一致的声卡名)。
+
+### 6.7 alsa-lib / alsa-utils 移植
+
+源码下载地址 `http://www.alsa-project.org/main/index.php/Main_Page`,原书使用 1.2.2 版本(光盘路径:例程源码 → 第三方库源码 → `alsa-lib-1.2.2.tar.bz2`、`alsa-utils-1.2.2.tar.bz2`)。
+
+**关键:配置文件路径一致性**。alsa-lib 编译时会生成含绝对路径的配置文件,因此 Ubuntu 与开发板根文件系统必须创建同名目录:
+
+```bash
+# Ubuntu
+cd /usr/share
+sudo mkdir arm-alsa
+# 开发板根文件系统
+mkdir /usr/share/arm-alsa -p
+```
+
+**移植 alsa-lib**:
+
+```bash
+tar -vxjf alsa-lib-1.2.2.tar.bz2
+cd alsa-lib-1.2.2/
+./configure --host=arm-linux-gnueabihf --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/alsa-lib --with-configdir=/usr/share/arm-alsa
+make
+sudo make install
+```
+
+若 `libatopology.la` 编译失败(sudo 切到 root 后找不到交叉编译器),先 `sudo -s` 切到 root,再 `source /etc/profile`,然后直接 `make install`(此时无需 sudo),完成后 `su zuozhongkai` 回到原用户。
+
+编译结果:`alsa-lib/lib` 下文件拷贝到开发板 `/usr/lib`,`/usr/share/arm-alsa` 下文件拷贝到开发板 `/usr/share/arm-alsa`:
+
+```bash
+cd alsa-lib
+sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/lib/ -af
+cd /usr/share/arm-alsa
+sudo cp * /home/zuozhongkai/linux/nfs/rootfs/usr/share/arm-alsa/ -raf
+```
+
+**移植 alsa-utils**:
+
+```bash
+tar -vxjf alsa-utils-1.2.2.tar.bz2
+cd alsa-utils-1.2.2/
+./configure --host=arm-linux-gnueabihf --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/alsa-utils --with-alsa-inc-prefix=/home/zuozhongkai/linux/IMX6ULL/tool/alsa-lib/include/ --with-alsa-prefix=/home/zuozhongkai/linux/IMX6ULL/tool/alsa-lib/lib/ --disable-alsamixer --disable-xmlto
+make
+sudo make install
+```
+
+> 说明:`--disable-alsamixer` 是因为 alsamixer 依赖 ncurses 库且原书未能成功交叉编译,但可用同功能的命令行工具 `amixer`(alsamixer 的图形化版本)。原书另用 buildroot 编译出 alsamixer 再拷贝进根文件系统。
+
+将编译出的 `bin`、`sbin`、`share` 分别拷贝到开发板 `/bin`、`/sbin`、`/usr/share`,并在 `/etc/profile` 中加入:
+
+```bash
+export ALSA_CONFIG_PATH=/usr/share/arm-alsa/alsa.conf
+```
+
+### 6.8 mplayer 播放器移植与使用
+
+mplayer 依赖 zlib 与 alsa-lib。
+
+1)移植 zlib(`zlib-1.2.11.tar.gz`):
+
+```bash
+tar -vxzf zlib-1.2.11.tar.gz
+cd zlib-1.2.11/
+CC=arm-linux-gnueabihf-gcc LD=arm-linux-gnueabihf-ld AD=arm-linux-gnueabihf-as ./configure --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/zlib
+make
+make install
+sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/lib/ -rfa
+```
+
+2)移植 mplayer(`MPlayer-1.4.tar.gz`):
+
+```bash
+tar -vxzf MPlayer-1.4.tar.gz
+cd MPlayer-1.4/
+./configure --cc=arm-linux-gnueabihf-gcc --host-cc=gcc --target=arm-linux-gnueabihf --disable-ossaudio --enable-alsa --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/mplayer --extra-cflags="-I /home/zuozhongkai/linux/IMX6ULL/tool/zlib/include -I /home/zuozhongkai/linux/IMX6ULL/tool/alsa-lib/include" --extra-ldflags="-L/home/zuozhongkai/linux/IMX6ULL/tool/zlib/lib -lz -L/home/zuozhongkai/linux/IMX6ULL/tool/alsa-lib/lib -lasound" --enable-fbdev --disable-mencoder
+make
+```
+
+
+编译后编辑 `config.mak`,把 `INSTALLSTRIP = -s` 后面的 `-s` 去掉,否则 `make install` 会失败。然后:
+
+```bash
+make install
+sudo cp bin/mplayer /home/zuozhongkai/linux/nfs/rootfs/bin/ -f
+```
+
+使用:
+
+```bash
+mplayer test.flac      # 播放音频
+mplayer test.avi -fs   # 全屏/居中播放视频
+```
+
+播放过程中常用按键:`9` 增加音量、`0` 减小音量、左/右键倒退/快进 10 秒、上/下键倒退/快进 1 分钟、空格暂停/播放。I.MX6ULL 性能有限且无硬件视频解码,视频分辨率建议 640×480 左右。
+
+### 6.9 alsamixer 简介
+
+alsamixer 是 amixer 的图形化版本,直接输入 `alsamixer` 打开。功能键:F1 帮助、F2 系统信息、F3 播放设置、F4 录音设置、F6 选择声卡;左右键选择设置项,上下键调整大小,`MM` 表示静音,按 `M` 切换 `MM`/`OO`。
+
 ---
 
 ## 7. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 | 特性      | I.MX6ULL      | STM32F4/F7     | RK3568        |
 | --------- | ------------- | -------------- | ------------- |
 | 音频接口  | SAI           | SAI/I2S        | I2S           |
@@ -709,6 +939,8 @@ fi
 
 ## 8. 面试精选
 
+> ⚠️ **来源说明**:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识;仅 Q4 涉及的 WM8960 R23 寄存器取值来自原书 65.5.3 小节。
+
 ### Q1: ASoC三驱动架构是什么?为什么要这样设计?
 
 **答**:ASoC将音频驱动分为CPU DAI、Codec和Machine三个独立组件:
@@ -788,3 +1020,7 @@ fi
 - 操作:通过`amixer`工具或`alsa-lib`的`snd_ctl_*`系列函数
 
 两者分工明确:PCM负责数据流,Control负责参数配置。
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十五章 Linux 音频驱动实验

+ 71 - 20
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/06-RTC与PWM驱动.md

@@ -24,6 +24,8 @@ pdf_ref:
 
 ## 1. RTC硬件概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十章内容,为扩展知识。原书指出 RTC 硬件原理已在“裸机篇”第二十五章讲解,本章直接从 Linux 内核 RTC 驱动框架切入。
+
 ### 1.1 RTC工作原理
 
 RTC(Real Time Clock,实时时钟)是一种专用外设,用于在系统断电后继续维持时间计数。其核心是一个低功耗计数器,通常以秒为单位递增。
@@ -185,6 +187,31 @@ static const struct file_operations rtc_dev_fops = {
 };
 ```
 
+应用层通过 `ioctl` 下发 `RTC_RD_TIME`(读取时间)/`RTC_SET_TIME`(设置时间)/`RTC_ALM_READ`(读取闹钟)等命令,最终由 `rtc_dev_ioctl` 逐层调用到 `rtc_class_ops` 中的 `read_time`/`set_time`/`read_alarm`。核心层 `__rtc_read_time` 的调用关系如下(原书示例代码 60.1.5):
+
+```c
+static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
+{
+    int err;
+    if (!rtc->ops)
+        err = -ENODEV;
+    else if (!rtc->ops->read_time)
+        err = -EINVAL;
+    else {
+        memset(tm, 0, sizeof(struct rtc_time));
+        err = rtc->ops->read_time(rtc->dev.parent, tm);
+        if (err < 0) {
+            dev_dbg(&rtc->dev, "read_time: fail to read: %d\n", err);
+            return err;
+        }
+        err = rtc_valid_tm(tm);   /* 校验时间有效性 */
+        if (err < 0)
+            dev_dbg(&rtc->dev, "read_time: rtc_time isn't valid\n");
+    }
+    return err;
+}
+```
+
 ### 2.5 RTC注册与注销API
 
 ```c
@@ -328,11 +355,11 @@ static int snvs_rtc_probe(struct platform_device *pdev)
 
     platform_set_drvdata(pdev, data);
 
-    // 4. 初始化毛刺检测寄存器
+    // 4. 初始化毛刺检测寄存器,SNVS_LPPGDR_INIT = 0x41736166
     regmap_write(data->regmap, data->offset + SNVS_LPPGDR,
                  SNVS_LPPGDR_INIT);
 
-    // 5. 清除中断状态
+    // 5. 清除中断状态(LPSR 写1清零)
     regmap_write(data->regmap, data->offset + SNVS_LPSR, 0xffffffff);
 
     // 6. 使能RTC
@@ -421,12 +448,13 @@ static u32 rtc_read_lp_counter(struct snvs_rtc_data *data)
         read2 |= val;
     } while ((read1 >> CNTR_TO_SECS_SH) != (read2 >> CNTR_TO_SECS_SH));
 
-    // 将47位计数器转换为32位秒数
+    // Convert 47-bit counter to 32-bit raw second count
+    // CNTR_TO_SECS_SH = 15,即右移15位得到秒数
     return (u32)(read1 >> CNTR_TO_SECS_SH);
 }
 ```
 
-读取策略说明:由于RTC是47位计数器,需要分两次读取高低32位。为防止读取过程中计数器更新导致高低位不匹配,采用连续读取两次并比较秒位的策略。
+读取策略说明:由于RTC是47位计数器(SNVS_LPSRTCMR/LPSRTCLR),需要分两次读取高低32位。为防止读取过程中计数器更新导致高低位不匹配,采用连续读取两次并比较秒位(`>> CNTR_TO_SECS_SH`)的策略;代码注释也指出在 CPU/总线低速运行时两次读取可能始终不同,因此只比较第二次的值
 
 ---
 
@@ -489,6 +517,8 @@ date
 
 ### 5.5 RTC sysfs接口
 
+> ⚠️ **来源说明**:本小节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十章内容,为扩展知识(原书只演示了 `date` 与 `hwclock -w`)。
+
 ```bash
 # 查看RTC设备列表
 ls /sys/class/rtc/
@@ -513,6 +543,8 @@ cat /sys/class/rtc/rtc0/since_epoch
 
 ## 6. PWM硬件概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第七十三章内容,为扩展知识。原书指出 PWM 原理与 I.MX6ULL PWM 外设已在“裸机篇”讲解,本章直接分析内核自带的 `pwm-imx.c` 驱动。
+
 ### 6.1 PWM工作原理
 
 PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过数字信号模拟模拟信号的技术。
@@ -624,11 +656,11 @@ struct pwm_ops {
 ### 7.4 PWM内核API
 
 ```c
-// 注册/注销PWM芯片
+// 注册/注销PWM芯片(原书 73.1.2 讲解)
 int pwmchip_add(struct pwm_chip *chip);
 int pwmchip_remove(struct pwm_chip *chip);
 
-// PWM操作API
+// 以下是“消费者(consumer)”侧的传统API,属扩展知识
 struct pwm_device *pwm_request(int pwm_num, const char *label);
 void pwm_free(struct pwm_device *pwm);
 int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
@@ -637,6 +669,8 @@ void pwm_disable(struct pwm_device *pwm);
 int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);
 ```
 
+> ⚠️ **来源说明**:`pwmchip_add`/`pwmchip_remove` 来自原书 73.1.2;`pwm_request`/`pwm_config`/`pwm_enable`/`pwm_disable`/`pwm_set_polarity` 等消费者 API 原书第七十三章未讲解,为扩展知识。较新内核推荐使用 `devm_pwm_get()` + `pwm_apply_state()`。
+
 ---
 
 ## 8. PWM设备树配置
@@ -655,14 +689,16 @@ pwm3: pwm@02088000 {
 };
 ```
 
+说明(原书 73.1.1):I.MX6ULL 共有 8 路 PWM(PWM1~PWM8),对应 8 个控制器节点,同属 AIPS-1 域,但在 `imx6ull.dtsi` 中分为 PWM1~PWM4 与 PWM5~PWM8 两部分,8 个节点除 `reg` 外内容相同。本章实验使用 `GPIO1_IO04`,它对应 PWM3 输出。绑定文档:`Documentation/devicetree/bindings/pwm/imx-pwm.txt`。
+
 ### 8.2 pwm-cells说明
 
-`#pwm-cells = <2>` 表示引用此PWM控制器时需要2个参数:
+`#pwm-cells = <2>` 表示引用此PWM控制器时需要2个参数(以原书 73.4.1.1 的 `pwms = <&pwm1 0 5000000>` 为例)
 
-| 参数  | 说明                                    |
-| ----- | --------------------------------------- |
-| 第1个 | PWM通道索引(I.MX6ULL只有1个通道,填0) |
-| 第2个 | PWM标志位(极性等)                     |
+| 参数  | 说明                                      |
+| ----- | ----------------------------------------- |
+| 第1个 | PWM通道索引(I.MX6ULL每个控制器只有1个通道,填0) |
+| 第2个 | PWM周期,单位ns(如5000000ns即5ms,对应200Hz) |
 
 引用示例:
 
@@ -691,6 +727,8 @@ pinctrl_pwm3: pwm3grp {
 };
 ```
 
+注意(原书 73.2.1):若设备树中其他外设也用到 `GPIO1_IO04`,必须屏蔽掉,且不能只屏蔽 pinctrl 配置,还要搜索 `"gpio1 4"` 检查是否有其他地方引用。PWM1~PWM8 分别对应 `IMX6UL_CLK_PWM1`~`IMX6UL_CLK_PWM8`,默认时钟源可能是 `IMX6UL_CLK_DUMMY`,需改为实际时钟。
+
 ### 8.4 LCD背光PWM节点示例
 
 ```dts
@@ -703,14 +741,17 @@ backlight {
 };
 ```
 
-属性解析:
+属性解析(原书 73.4)
 
-| 属性                       | 说明                    |
-| -------------------------- | ----------------------- |
-| `compatible`               | 必须为`"pwm-backlight"` |
-| `pwms`                     | 指定PWM通道和周期       |
-| `brightness-levels`        | 背光等级数组(0~255)     |
-| `default-brightness-level` | 默认等级索引            |
+| 属性                       | 说明                                                                 |
+| -------------------------- | -------------------------------------------------------------------- |
+| `compatible`               | 必须为`"pwm-backlight"`,匹配驱动 `drivers/video/backlight/pwm_bl.c` |
+| `pwms`                     | 指定PWM通道和周期,`<&pwm1 0 5000000>` 即200Hz                        |
+| `brightness-levels`        | 背光等级数组,范围0~255对应0%~100%,必须从0开始、以255结尾           |
+| `default-brightness-level` | 默认背光**等级索引**(不是数值),这里7对应255,即100%占空比         |
+| `power-supply`             | 支持的电压,可选                                                     |
+
+绑定文档:`Documentation/devicetree/bindings/video/backlight/pwm-backlight.txt`。
 
 ---
 
@@ -990,6 +1031,8 @@ echo "Tests complete"
 
 ## 11. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 11.1 RTC对比
 
 | 特性       | I.MX6ULL (SNVS-RTC)       | STM32F103 (RTC) | RK3568 (RTC)         |
@@ -1026,6 +1069,8 @@ echo "Tests complete"
 
 ## 12. 面试精选
 
+> ⚠️ **来源说明**:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识。
+
 ### Q1: RTC子系统中`rtc_class_ops`和`file_operations`的关系是什么?
 
 **答案**:`file_operations`是字符设备的通用操作集,提供给用户空间的`open/read/write/ioctl`接口。`rtc_class_ops`是RTC硬件的底层操作集,包括`read_time/set_time`等。
@@ -1040,7 +1085,7 @@ RTC驱动采用**两层封装**:用户空间调用`ioctl(RTC_RD_TIME)` → `rt
 
 ### Q3: PWM子系统中`#pwm-cells`属性的含义是什么?如何在设备树中引用PWM?
 
-**答案**:`#pwm-cells`定义了引用PWM控制器时需要的参数个数。例如`#pwm-cells = <2>`表示需要2个参数:第1个是通道索引,第2个是标志位
+**答案**:`#pwm-cells`定义了引用PWM控制器时需要的参数个数。例如`#pwm-cells = <2>`表示需要2个参数:第1个是通道索引,第2个是PWM周期(单位ns)
 
 引用示例:`pwms = <&pwm1 0 5000000>` 表示使用pwm1控制器的通道0,周期为5000000ns。核心层会根据`#pwm-cells`解析参数,调用驱动的`of_xlate`回调转换为具体的PWM设备。
 
@@ -1054,7 +1099,7 @@ RTC驱动采用**两层封装**:用户空间调用`ioctl(RTC_RD_TIME)` → `rt
 
 **答案**:周期(period)决定PWM频率,占空比(duty_cycle)决定高电平时间占比。占空比必须小于等于周期。
 
-寄存器值计算:I.MX6ULL使用IPG时钟,`period_cycles = clk_rate × period_ns / 1e9`,`duty_cycles = period_cycles × duty_ns / period_ns`。由于硬件要求周期值需减2(PWMPR寄存器),最终写入`PWMPR = period_cycles - 2`。预分频器用于将大周期值分频到16位计数器范围内
+寄存器值计算:`imx_pwm_config_v2` 用 `clk_get_rate(imx->clk_per)` 取 per 时钟频率,`period_cycles = clk_rate × period_ns / 1e9`,`prescale = period_cycles / 0x10000 + 1`,`period_cycles /= prescale`,`duty_cycles = period_cycles × duty_ns / period_ns`。由于硬件要求 PWMPR 中存放的值应比实际周期小 2(即实际周期 = PWMPR 值 + 2),最终写入 `PWMPR = period_cycles - 2`(若结果 ≤2 则写 0),占空比写入 `PWMSAR = duty_cycles`
 
 ---
 
@@ -1065,3 +1110,9 @@ RTC驱动采用**两层封装**:用户空间调用`ioctl(RTC_RD_TIME)` → `rt
 - [[08-I2C驱动]]
 - [[09-SPI驱动]]
 - [[10-网络设备驱动]]
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十章 Linux RTC驱动实验
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第七十三章 Linux PWM驱动实验

+ 289 - 74
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/07-ADC与IIO驱动.md

@@ -18,6 +18,8 @@ pdf_ref:
 
 ## 一、ADC硬件概述
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第七十六章内容,为扩展知识。原书指出 ADC 原理已在“裸机篇”《ADC 实验》章节讲解,本章直接分析内核自带的 `vf610_adc.c` 驱动。
+
 ### 1.1 ADC工作原理
 
 ADC(Analog-to-Digital Converter,模数转换器)是将连续的模拟信号转换为离散的数字信号的器件。其工作过程包括:
@@ -47,9 +49,9 @@ ADC(Analog-to-Digital Converter,模数转换器)是将连续的模拟信
 
 **I.MX6ULL ADC参数**:
 
-- 12位分辨率(实际有效10位
-- 参考电压:3.3V
-- 分辨率(scale):`3300mV / 2^12 = 0.805664062 mV`
+- 12位分辨率(`vf610_adc.c` 中 `res_mode` 决定,scale 返回 `IIO_VAL_FRACTIONAL_LOG2`
+- 参考电压:3.3V(由 `vref-supply` 指定的 regulator 提供)
+- 分辨率(scale):`3300mV / 2^12 = 0.805664062 mV`(与原书图 76.4.3.3 中 `in_voltage_scale=0.805664062` 一致)
 
 ---
 
@@ -500,6 +502,8 @@ arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard adcApp.c -o a
 
 ## 五、IIO子系统概述
 
+> ⚠️ **来源说明**:本节架构图与缓冲区/Trigger 分类不属于《I.MX6U嵌入式Linux驱动开发指南》第七十五章内容,为扩展知识。原书 75.1 节只讲解 `iio_dev`、`iio_info`、`iio_chan_spec` 三个核心数据结构,并说明 IIO 专为 ADC 类传感器设计。
+
 ### 5.1 IIO子系统架构
 
 ```mermaid
@@ -612,6 +616,19 @@ struct iio_dev {
 | `INDIO_BUFFER_SOFTWARE`  | 支持软件触发的缓冲区    |
 | `INDIO_BUFFER_HARDWARE`  | 支持硬件缓冲区          |
 
+`iio_dev` 的 `setup_ops` 成员为 `iio_buffer_setup_ops` 类型(原书示例代码 75.1.1.2),在使能/禁用缓冲区时被调用;未指定时默认使用 `iio_triggered_buffer_setup_ops`:
+
+```c
+struct iio_buffer_setup_ops {
+    int (*preenable)(struct iio_dev *);    /* 缓冲区使能之前调用 */
+    int (*postenable)(struct iio_dev *);   /* 缓冲区使能之后调用 */
+    int (*predisable)(struct iio_dev *);   /* 缓冲区禁用之前调用 */
+    int (*postdisable)(struct iio_dev *);  /* 缓冲区禁用之后调用 */
+    bool (*validate_scan_mask)(struct iio_dev *indio_dev,
+                               const unsigned long *scan_mask);
+};
+```
+
 ### 6.2 iio_info结构体
 
 驱动层需要实现的操作函数集:
@@ -726,16 +743,18 @@ enum iio_chan_type {
 
 ### 7.1 SPI传感器设备树(ICM20608)
 
+原书 75.3.2 明确说明本章“设备树不需要做任何修改”,ICM20608 的 SPI 节点已在 SPI 实验章节中配置。其驱动在 `icm20608_probe` 中将 SPI 设为 **MODE0(CPOL=0,CPHA=0)**,因此设备树中不应出现 `spi-cpol`/`spi-cpha`:
+
 ```dts
 icm20608: icm20608@0 {
     compatible = "alientek,icm20608";
-    reg = <0>;  /* SPI片选 */
-    spi-max-frequency = <8000000>;  /* 8MHz */
-    spi-cpol;    /* CPOL=1 */
-    spi-cpha;    /* CPHA=1 */
+    reg = <0>;                       /* SPI片选 */
+    spi-max-frequency = <8000000>;   /* 8MHz */
+    /* 不配置 spi-cpol / spi-cpha,驱动内使用 SPI_MODE_0 */
 };
 ```
 
+
 ### 7.2 IIO绑定文档
 
 参考:`Documentation/devicetree/bindings/iio/adc/vf610-adc.txt`
@@ -756,6 +775,8 @@ icm20608: icm20608@0 {
 
 ## 八、IIO驱动API
 
+> ⚠️ **来源说明**:`iio_device_alloc`/`iio_priv`/`iio_device_free`/`iio_device_register`/`iio_device_unregister` 来自原书 75.1.1 小节;`iio_push_to_buffers`/`iio_triggered_buffer_setup` 原书第七十五章未涉及(ICM20608 未实现缓冲区),为扩展知识。
+
 ### 8.1 iio_device_alloc — 申请IIO设备
 
 ```c
@@ -808,17 +829,18 @@ int iio_push_to_buffers(struct iio_dev *indio_dev, const unsigned char *data)
 
 ```c
 int iio_triggered_buffer_setup(struct iio_dev *indio_dev,
-                               irq_handler_t pollfunc,
+                               irq_handler_t pollfunc,
                                irq_handler_t thread_fn,
                                const struct iio_buffer_setup_ops *buf_ops)
 ```
 
 - **参数**:
-  - `pollfunc`:触发中断处理函数
+  - `pollfunc`:触发中断处理函数
   - `thread_fn`:线程化中断处理函数
   - `buf_ops`:缓冲区操作函数集
 - **说明**:设置触发缓冲区模式
 
+
 ### 8.5 iio_device_unregister — 注销IIO设备
 
 ```c
@@ -863,30 +885,36 @@ static const int accel_scale_icm20608[] = {61035, 122070, 244140, 488281};
 #define ICM20608_TEMP_OFFSET 0
 #define ICM20608_TEMP_SCALE  326800000
 
-/* 扫描索引枚举 */
+/* 扫描索引枚举(原书 75.3.2,注意顺序为 加速度X/Y/Z、温度、陀螺仪X/Y/Z、时间戳) */
 enum inv_icm20608_scan {
+    INV_ICM20608_SCAN_ACCL_X,
+    INV_ICM20608_SCAN_ACCL_Y,
+    INV_ICM20608_SCAN_ACCL_Z,
+    INV_ICM20608_SCAN_TEMP,
     INV_ICM20608_SCAN_GYRO_X,
     INV_ICM20608_SCAN_GYRO_Y,
     INV_ICM20608_SCAN_GYRO_Z,
-    INV_ICM20608_SCAN_ACCEL_X,
-    INV_ICM20608_SCAN_ACCEL_Y,
-    INV_ICM20608_SCAN_ACCEL_Z,
-    INV_ICM20608_SCAN_TEMP,
     INV_ICM20608_SCAN_TIMESTAMP,
 };
 
-/* 通道定义宏 */
-#define ICM20608_CHAN(_type, _channel2, _info_mask_separate, \
-                     _scan_index, _scan_type) \
-{ \
-    .type = _type, \
-    .modified = 1, \
-    .channel2 = _channel2, \
-    .info_mask_separate = _info_mask_separate, \
-    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
-    .scan_index = _scan_index, \
-    .scan_type = _scan_type, \
-}
+/* 通道定义宏(原书为 3 参数形式) */
+#define ICM20608_CHAN(_type, _channel2, _index)  \
+    {                                            \
+        .type = _type,                           \
+        .modified = 1,                           \
+        .channel2 = _channel2,                   \
+        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
+        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |        \
+                              BIT(IIO_CHAN_INFO_CALIBBIAS),  \
+        .scan_index = _index,                    \
+        .scan_type = {                           \
+            .sign = 's',                         \
+            .realbits = 16,                      \
+            .storagebits = 16,                   \
+            .shift = 0,                          \
+            .endianness = IIO_BE,                \
+        },                                       \
+    }
 
 /* ICM20608通道数组(7个通道) */
 static const struct iio_chan_spec icm20608_channels[] = {
@@ -905,45 +933,19 @@ static const struct iio_chan_spec icm20608_channels[] = {
             .endianness = IIO_BE,
         },
     },
-    /* 陀螺仪X轴 */
-    ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_X,
-                  BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_CALIBBIAS),
-                  INV_ICM20608_SCAN_GYRO_X,
-                  { .sign = 's', .realbits = 16, .storagebits = 16,
-                    .endianness = IIO_BE }),
-    /* 陀螺仪Y轴 */
-    ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Y,
-                  BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_CALIBBIAS),
-                  INV_ICM20608_SCAN_GYRO_Y,
-                  { .sign = 's', .realbits = 16, .storagebits = 16,
-                    .endianness = IIO_BE }),
-    /* 陀螺仪Z轴 */
-    ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Z,
-                  BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_CALIBBIAS),
-                  INV_ICM20608_SCAN_GYRO_Z,
-                  { .sign = 's', .realbits = 16, .storagebits = 16,
-                    .endianness = IIO_BE }),
-    /* 加速度计X轴 */
-    ICM20608_CHAN(IIO_ACCEL, IIO_MOD_X,
-                  BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_CALIBBIAS),
-                  INV_ICM20608_SCAN_ACCEL_X,
-                  { .sign = 's', .realbits = 16, .storagebits = 16,
-                    .endianness = IIO_BE }),
-    /* 加速度计Y轴 */
-    ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Y,
-                  BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_CALIBBIAS),
-                  INV_ICM20608_SCAN_ACCEL_Y,
-                  { .sign = 's', .realbits = 16, .storagebits = 16,
-                    .endianness = IIO_BE }),
-    /* 加速度计Z轴 */
-    ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Z,
-                  BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_CALIBBIAS),
-                  INV_ICM20608_SCAN_ACCEL_Z,
-                  { .sign = 's', .realbits = 16, .storagebits = 16,
-                    .endianness = IIO_BE }),
+    /* 陀螺仪X/Y/Z轴 */
+    ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X),
+    ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y),
+    ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z),
+    /* 加速度计:原书顺序为 Y、X、Z */
+    ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y),
+    ICM20608_CHAN(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X),
+    ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z),
 };
 ```
 
+> 分辨率计算:陀螺仪以 ±250°/s 量程为例,`500 / 2^16 ≈ 0.007629`,扩大 1000000 倍为 7629;加速度计以 ±2g 量程为例,`4 / 2^16 ≈ 0.000061035`,扩大 1000000000 倍为 61035。
+
 ### 9.3 iio_info实现
 
 ```c
@@ -1200,11 +1202,158 @@ MODULE_LICENSE("GPL");
 MODULE_AUTHOR("ALIENTEK");
 ```
 
+### 9.9 写通道数据 — icm20608_write_raw(量程与校准值)
+
+原书 75.3.4 实现用户空间往驱动写数据,用于设置陀螺仪/加速度计的量程(分辨率)和校准值。配套函数如下:
+
+```c
+/* 向指定通道寄存器写入数据,用于设置校准值 */
+static int icm20608_sensor_set(struct icm20608_dev *dev, int reg,
+                               int axis, int val)
+{
+    int ind, result;
+    __be16 d = cpu_to_be16(val);
+
+    ind = (axis - IIO_MOD_X) * 2;
+    result = regmap_bulk_write(dev->regmap, reg + ind, (u8 *)&d, 2);
+    if (result)
+        return -EINVAL;
+    return 0;
+}
+
+/* 设置陀螺仪量程:在 gyro_scale_icm20608[] 中查找匹配值,换算成 GYRO_CONFIG 的 FS_SEL */
+static int icm20608_write_gyro_scale(struct icm20608_dev *dev, int val)
+{
+    int result, i;
+    u8 d;
+
+    for (i = 0; i < ARRAY_SIZE(gyro_scale_icm20608); ++i) {
+        if (gyro_scale_icm20608[i] == val) {
+            d = (i << 3);
+            result = regmap_write(dev->regmap, ICM20_GYRO_CONFIG, d);
+            if (result)
+                return result;
+            return 0;
+        }
+    }
+    return -EINVAL;
+}
+
+/* 设置加速度计量程:在 accel_scale_icm20608[] 中查找匹配值,换算成 ACCEL_CONFIG 的 AFS_SEL */
+static int icm20608_write_accel_scale(struct icm20608_dev *dev, int val)
+{
+    int result, i;
+    u8 d;
+
+    for (i = 0; i < ARRAY_SIZE(accel_scale_icm20608); ++i) {
+        if (accel_scale_icm20608[i] == val) {
+            d = (i << 3);
+            result = regmap_write(dev->regmap, ICM20_ACCEL_CONFIG, d);
+            if (result)
+                return result;
+            return 0;
+        }
+    }
+    return -EINVAL;
+}
+
+static int icm20608_write_raw(struct iio_dev *indio_dev,
+                              struct iio_chan_spec const *chan,
+                              int val, int val2, long mask)
+{
+    struct icm20608_dev *dev = iio_priv(indio_dev);
+    int ret = 0;
+
+    iio_device_claim_direct_mode(indio_dev);
+    switch (mask) {
+    case IIO_CHAN_INFO_SCALE:            /* 设置陀螺仪和加速度计的分辨率 */
+        switch (chan->type) {
+        case IIO_ANGL_VEL:
+            mutex_lock(&dev->lock);
+            ret = icm20608_write_gyro_scale(dev, val2);
+            mutex_unlock(&dev->lock);
+            break;
+        case IIO_ACCEL:
+            mutex_lock(&dev->lock);
+            ret = icm20608_write_accel_scale(dev, val2);
+            mutex_unlock(&dev->lock);
+            break;
+        default:
+            ret = -EINVAL;
+            break;
+        }
+        break;
+    case IIO_CHAN_INFO_CALIBBIAS:        /* 设置陀螺仪和加速度计的校准值 */
+        switch (chan->type) {
+        case IIO_ANGL_VEL:
+            mutex_lock(&dev->lock);
+            ret = icm20608_sensor_set(dev, ICM20_XG_OFFS_USRH, chan->channel2, val);
+            mutex_unlock(&dev->lock);
+            break;
+        case IIO_ACCEL:
+            mutex_lock(&dev->lock);
+            ret = icm20608_sensor_set(dev, ICM20_XA_OFFSET_H, chan->channel2, val);
+            mutex_unlock(&dev->lock);
+            break;
+        default:
+            ret = -EINVAL;
+            break;
+        }
+        break;
+    default:
+        ret = -EINVAL;
+        break;
+    }
+    iio_device_release_direct_mode(indio_dev);
+    return ret;
+}
+
+/* 指定用户空间写入的数据格式:陀螺仪扩大1e6倍,其余(加速度计)扩大1e9倍 */
+static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev,
+                                      struct iio_chan_spec const *chan, long mask)
+{
+    switch (mask) {
+    case IIO_CHAN_INFO_SCALE:
+        switch (chan->type) {
+        case IIO_ANGL_VEL:
+            return IIO_VAL_INT_PLUS_MICRO;
+        default:
+            return IIO_VAL_INT_PLUS_NANO;
+        }
+    default:
+        return IIO_VAL_INT_PLUS_MICRO;
+    }
+}
+```
+
+要点:`ICM20_GYRO_CONFIG` 的 bit4:3(`FS_SEL`)和 `ICM20_ACCEL_CONFIG` 的 bit4:3(`AFS_SEL`)决定量程,写值 `i << 3`。`in_accel_scale` 返回 `IIO_VAL_INT_PLUS_NANO`,所以用户空间写 `0.000122070`(±4g)时传入驱动的是 `122070`。
+
 ---
 
 ## 十、IIO实验验证
 
-### 10.1 IIO设备目录结构
+### 10.1 内核配置与驱动加载
+
+内核需使能 IIO 及缓冲区相关选项(原书 75.3.1):
+
+```
+-> Device Drivers
+    -> Industrial I/O support (IIO [=y])
+        -> [*] Enable buffer support within IIO
+        -> <*> Industrial I/O buffering based on kfifo
+```
+
+编译并加载驱动、运行测试 APP(原书 75.3.2 / 75.4.3):
+
+```bash
+depmod                  # 第一次加载驱动时需要
+modprobe icm20608.ko    # 加载驱动模块
+
+arm-linux-gnueabihf-gcc icm20608App.c -o icm20608App   # 编译测试APP
+./icm20608App          # 运行,持续打印陀螺仪/加速度计/温度数据
+```
+
+### 10.2 IIO设备目录结构
 
 ```bash
 # 查看所有IIO设备
@@ -1228,7 +1377,7 @@ ls
 # in_temp_scale          温度比例
 ```
 
-### 10.2 sysfs文件命名规则
+### 10.3 sysfs文件命名规则
 
 文件命名模式:`[direction]_[type]_[index]_[modifier]_[info_mask]`
 
@@ -1247,19 +1396,19 @@ ls
 - `x` → X轴(IIO_MOD_X)
 - `raw` → 原始值(IIO_CHAN_INFO_RAW)
 
-### 10.3 读取测试
+### 10.4 读取测试
 
 ```bash
-# 读取加速度计X轴原始值
-cat in_accel_x_raw
-# 输出: 2074
-
-# 读取加速度计比例
+# 读取加速度计比例(默认量程±16g)
 cat in_accel_scale
 # 输出: 0.000488281
 
+# 读取加速度计Z轴原始值(静止时Z轴约为1g)
+cat in_accel_z_raw
+# 输出: 2074(原书图 75.3.3.2)
+
 # 计算实际加速度
-# 2074 × 0.000488281 ≈ 1.01g(Z轴静止时应接近1g)
+# 2074 × 0.000488281 ≈ 1.01g
 
 # 读取陀螺仪X轴原始值
 cat in_anglvel_x_raw
@@ -1268,7 +1417,8 @@ cat in_anglvel_x_raw
 cat in_temp_raw
 ```
 
-### 10.4 iio_info工具
+
+### 10.5 iio_info工具
 
 ```bash
 # 安装libiio
@@ -1280,14 +1430,71 @@ iio_info -s
 # 读取指定设备
 iio_readdev -u iio:device0 voltage0
 
-# 使用iio_dump查看设备详情
-iio_dump -d iio:device1
+# 查看/设置设备属性
+iio_attr -d iio:device1
 ```
 
+### 10.6 测试APP(icm20608App.c)
+
+IIO 的 sysfs 文件内容都是**字符串**(如 `in_accel_scale` 内容为 `"0.000488281"`),因此读取时要先按字符串取出,再用 `atof`/`atoi` 转换。原书 75.4.1 用到的标准 I/O 文件流 API:
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `fopen` | `FILE *fopen(const char *pathname, const char *mode)` | 打开文件流,`mode` 可取 `r`/`r+`/`w`/`w+`/`a`/`a+` |
+| `fclose` | `int fclose(FILE *stream)` | 关闭文件流,成功返回0,失败返回EOF |
+| `fread` | `size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)` | 从流读取对象 |
+| `fwrite` | `size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)` | 向流写入对象 |
+| `fscanf` | `int fscanf(FILE *stream, const char *format, ...)` | 格式化读取,遇空格/换行结束 |
+
+核心转换逻辑(原书示例代码 75.4.2.1):
+
+```c
+/* 读取文件路径(注意 ICM20608 是 iio:device1) */
+static int sensor_read(struct icm20608_dev *dev)
+{
+    int ret = 0;
+    char str[50];
+
+    /* 1、获取陀螺仪原始数据与比例 */
+    SENSOR_FLOAT_DATA_GET(ret, IN_ANGLVEL_SCALE, str, gyro_scale);
+    SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_X_RAW, str, gyro_x_raw);
+    SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Y_RAW, str, gyro_y_raw);
+    SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Z_RAW, str, gyro_z_raw);
+
+    /* 2、获取加速度计原始数据与比例 */
+    SENSOR_FLOAT_DATA_GET(ret, IN_ACCEL_SCALE, str, accel_scale);
+    SENSOR_INT_DATA_GET(ret, IN_ACCEL_X_RAW, str, accel_x_raw);
+    SENSOR_INT_DATA_GET(ret, IN_ACCEL_Y_RAW, str, accel_y_raw);
+    SENSOR_INT_DATA_GET(ret, IN_ACCEL_Z_RAW, str, accel_z_raw);
+
+    /* 3、获取温度原始值、offset、scale */
+    SENSOR_FLOAT_DATA_GET(ret, IN_TEMP_SCALE, str, temp_scale);
+    SENSOR_INT_DATA_GET(ret, IN_TEMP_OFFSET, str, temp_offset);
+    SENSOR_INT_DATA_GET(ret, IN_TEMP_RAW, str, temp_raw);
+
+    /* 4、换算为实际值 */
+    dev->accel_x_act = dev->accel_x_raw * dev->accel_scale;
+    dev->accel_y_act = dev->accel_y_raw * dev->accel_scale;
+    dev->accel_z_act = dev->accel_z_raw * dev->accel_scale;
+
+    dev->gyro_x_act = dev->gyro_x_raw * dev->gyro_scale;
+    dev->gyro_y_act = dev->gyro_y_raw * dev->gyro_scale;
+    dev->gyro_z_act = dev->gyro_z_raw * dev->gyro_scale;
+
+    dev->temp_act = ((dev->temp_raw - dev->temp_offset) / dev->temp_scale) + 25;
+    return ret;
+}
+```
+
+- `SENSOR_FLOAT_DATA_GET` / `SENSOR_INT_DATA_GET` 是形参化宏,内部依次调用 `file_data_read()` + `atof()` / `atoi()`。
+- 温度换算:`temp_act = (temp_raw - temp_offset) / temp_scale + 25`。
+
 ---
 
 ## 十一、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 11.1 I.MX6ULL vs STM32 vs RK3568 ADC/IIO差异
 
 | 特性             | I.MX6ULL                    | STM32MP1                    | RK3568                     |
@@ -1335,6 +1542,8 @@ iio_dump -d iio:device1
 
 ## 十二、面试精选
 
+> ⚠️ **来源说明**:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识(部分结论可对应原书 75.1 小节)。
+
 ### Q1: IIO子系统与input子系统有什么区别?
 
 **答**:
@@ -1397,3 +1606,9 @@ IIO使用`val`和`val2`两个整数参数编码小数:
 - 第七十六章 Linux ADC驱动实验
 - Linux内核文档:`Documentation/driver-api/iio/index.rst`
 - 内核源码:`drivers/iio/adc/vf610_adc.c`
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第七十五章 Linux IIO驱动实验
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第七十六章 Linux ADC驱动实验

+ 125 - 25
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/08-块设备驱动.md

@@ -18,16 +18,20 @@ pdf_ref: "74_第六十八章 Linux 块设备驱动实验.pdf"
 
 块设备是针对存储设备的,比如SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备驱动其实就是这些存储设备的驱动,块设备驱动相比字符设备驱动的主要区别如下:
 
-| 特性     | 块设备              | 字符设备   |
-| -------- | ------------------- | ---------- |
-| 访问单位 | 块(512字节为单位) | 字节       |
-| 数据传输 | 需要缓冲区          | 不需要缓冲 |
-| 访问方式 | 随机访问            | 顺序访问   |
-| I/O调度  | 需要I/O调度器优化   | 无需调度   |
-| 生命周期 | 有擦写次数限制      | 无限制     |
+| 特性     | 块设备                | 字符设备   |
+| -------- | --------------------- | ---------- |
+| 访问单位 | 块(VFS 基本传输单位,扇区为512字节) | 字节 |
+| 数据传输 | 需要缓冲区            | 不需要缓冲 |
+| 访问方式 | 随机访问              | 顺序访问   |
+| I/O调度  | 需要I/O调度器优化     | 无需调度   |
+| 生命周期 | 有擦写次数限制        | 无限制     |
+
+> 原书 68.1 指出:块设备以块为单位读写,使用缓冲区暂存数据、满足条件后再一次性写入物理设备,以减少擦除次数、提高寿命;字符设备不需要缓冲区,实时按字节访问。因此块设备寿命受 Flash 擦写次数限制。
 
 ### 1.2 块设备应用场景
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十八章内容,为扩展知识。
+
 块设备主要用于数据的持久化存储,典型应用场景包括:
 
 - **系统启动**:内核镜像、根文件系统存储
@@ -220,13 +224,14 @@ int register_blkdev(unsigned int major, const char *name)
 
 参数说明:
 
-- `major`:主设备号。若为0则由系统自动分配
+- `major`:主设备号
 - `name`:块设备名字
 
-返回值:
+返回值(原书 68.2.1):
+
+- 若 `major` 为 1~255,表示自定义主设备号:返回 0 表示注册成功,负值表示失败
+- 若 `major` 为 0,表示由系统自动分配主设备号:返回值为系统分配的主设备号(1~255),负值表示失败
 
-- 成功:返回注册成功的主设备号
-- 失败:返回负值
 
 #### 3.1.2 unregister_blkdev
 
@@ -282,6 +287,51 @@ void set_capacity(struct gendisk *disk, sector_t size)
 set_capacity(gendisk, (2 * 1024 * 1024) / 512);  // 4096个扇区
 ```
 
+#### 3.2.5 引用计数 get_disk / put_disk
+
+内核通过这两个函数调整 `gendisk` 的引用计数:
+
+```c
+struct kobject *get_disk(struct gendisk *disk);  /* 增加引用计数 */
+void put_disk(struct gendisk *disk);             /* 减少引用计数 */
+```
+
+#### 3.2.6 块设备操作集 block_device_operations
+
+和字符设备的 `file_operations` 类似,块设备操作集定义在 `include/linux/blkdev.h`(原书示例代码 68.2.3.1):
+
+```c
+struct block_device_operations {
+    int (*open)(struct block_device *, fmode_t);
+    void (*release)(struct gendisk *, fmode_t);
+    int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
+    int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
+    int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
+    long (*direct_access)(struct block_device *, sector_t,
+                          void **, unsigned long *pfn, long size);
+    unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);
+    int (*media_changed)(struct gendisk *);   /* 已废弃,用 check_events 代替 */
+    void (*unlock_native_capacity)(struct gendisk *);
+    int (*revalidate_disk)(struct gendisk *);
+    int (*getgeo)(struct block_device *, struct hd_geometry *);
+    void (*swap_slot_free_notify)(struct block_device *, unsigned long);
+    struct module *owner;
+};
+```
+
+重点成员:`open`/`release`(打开/关闭块设备)、`ioctl`/`compat_ioctl`(I/O 控制,64 位系统上 32 位程序走 `compat_ioctl`)、`getgeo`(获取磁盘磁头/柱面/扇区信息)、`owner`(一般为 `THIS_MODULE`)。块设备操作集**没有** `read`/`write`,数据读写通过 `request_queue`/`request`/`bio` 完成。
+
+`getgeo` 用到的 `hd_geometry` 结构(原书示例代码 68.3.1.4):
+
+```c
+struct hd_geometry {
+    unsigned char heads;      /* 磁头 */
+    unsigned char sectors;    /* 一个磁道上的扇区数量 */
+    unsigned short cylinders; /* 柱面 */
+    unsigned long start;
+};
+```
+
 ### 3.3 请求队列API
 
 #### 3.3.1 blk_init_queue
@@ -323,15 +373,53 @@ void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
 
 ### 3.4 请求处理API
 
-| 函数                    | 描述                              |
-| ----------------------- | --------------------------------- |
-| `blk_peek_request()`    | 获取请求队列中下一个请求          |
-| `blk_start_request()`   | 开始处理请求                      |
-| `blk_fetch_request()`   | 一次性获取并开启请求              |
-| `blk_end_request()`     | 请求中指定字节数据被处理完成      |
-| `blk_end_request_all()` | 请求中所有数据全部处理完成        |
-| `blk_end_request_cur()` | 当前请求中的chunk                 |
-| `__blk_end_request()`   | 同blk_end_request,但需持有队列锁 |
+| 函数                    | 描述                                    |
+| ----------------------- | --------------------------------------- |
+| `blk_peek_request()`    | 获取请求队列中下一个请求,无则返回NULL  |
+| `blk_start_request()`   | 开始处理指定请求                        |
+| `blk_fetch_request()`   | 一次性获取并开启请求(=peek+start)     |
+| `blk_end_request()`     | 请求中指定字节数据被处理完成            |
+| `blk_end_request_all()` | 请求中所有数据全部处理完成              |
+| `blk_end_request_cur()` | 完成当前请求中的 chunk                  |
+| `blk_end_request_err()` | 处理完请求,直到下一个错误产生          |
+| `__blk_end_request()`   | 同 `blk_end_request`,但需持有队列锁    |
+| `__blk_end_request_all()` | 同 `blk_end_request_all`,需持有队列锁 |
+| `__blk_end_request_cur()` | 同 `blk_end_request_cur`,需持有队列锁 |
+| `__blk_end_request_err()` | 同 `blk_end_request_err`,需持有队列锁 |
+
+关键函数原型(原书 68.2.4):
+
+```c
+request *blk_peek_request(struct request_queue *q);
+void     blk_start_request(struct request *req);
+struct request *blk_fetch_request(struct request_queue *q);  /* peek + start */
+```
+
+#### 3.4.1 遍历与辅助宏
+
+```c
+/* 遍历请求中的所有 bio(原书示例代码 68.2.4.5) */
+#define __rq_for_each_bio(_bio, rq) \
+    if ((rq->bio))                  \
+        for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
+
+/* 遍历 bio 中的所有段(原书示例代码 68.2.4.6) */
+#define bio_for_each_segment(bvl, bio, iter) \
+    __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
+
+void bio_endio(struct bio *bio, int error);  /* 通知 bio 处理结束,error=0 表示成功 */
+```
+
+请求处理中还会用到这些辅助函数:
+
+| 函数 | 说明 |
+| ---- | ---- |
+| `blk_rq_pos(req)` | 获取请求要操作的扇区地址(左移9位转换为字节地址) |
+| `blk_rq_cur_bytes(req)` | 获取当前请求要处理的数据长度 |
+| `rq_data_dir(req)` | 判断请求方向(`READ`/`WRITE`) |
+| `bio_data(req->bio)` | 获取请求中 bio 的数据缓冲区 |
+| `bio_data_dir(bio)` | 判断 bio 方向(`READ`/`WRITE`) |
+| `page_address(bvec.bv_page)` | 获取 bio_vec 中页的虚拟地址 |
 
 ## 4. 完整源码分析
 
@@ -357,9 +445,10 @@ void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
 
 #define RAMDISK_SIZE    (2 * 1024 * 1024)  /* 容量大小为2MB */
 #define RAMDISK_NAME    "ramdisk"           /* 名字 */
-#define RAMDISK_MINOR   3                   /* 表示三个磁盘分区 */
+#define RAMDISK_MINOR   3                   /* 表示三个磁盘分区!不是次设备号为3! */
 ```
 
+
 #### 4.1.2 设备结构体
 
 ```c
@@ -585,11 +674,12 @@ void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
 ### 5.1 编译和加载驱动
 
 ```bash
-# 编译驱动
+# 编译驱动,得到 ramdisk.ko
 make
 
-# 复制到开发板
-scp ramdisk.ko root@192.168.1.200:/lib/modules/4.1.15/
+# 将 ramdisk.ko 拷贝到开发板根文件系统 rootfs/lib/modules/4.1.15 目录中
+# 重启开发板后进入该目录:
+cd /lib/modules/4.1.15
 
 # 加载驱动
 depmod                    # 首次加载需要
@@ -599,10 +689,12 @@ modprobe ramdisk.ko       # 加载驱动模块
 ### 5.2 查看块设备信息
 
 ```bash
-fdisk -l                  # 查看所有磁盘信息
+fdisk -l                  # 查看所有磁盘信息,其中包括 ramdisk
 ls /dev/ramdisk           # 查看设备节点
 ```
 
+驱动加载成功后会在 `/dev/` 下生成名为 `ramdisk` 的设备,`fdisk -l` 会显示其大小为 2MB,同时提示 `/dev/ramdisk` 没有分区表(因为尚未格式化)。
+
 ### 5.3 格式化和挂载
 
 ```bash
@@ -629,6 +721,8 @@ rmmod ramdisk             # 卸载驱动模块
 
 ## 6. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 6.1 IMX6ULL vs STM32 vs RK3568块设备差异
 
 | 特性       | IMX6ULL | STM32MP1 | RK3568       |
@@ -657,6 +751,8 @@ rmmod ramdisk             # 卸载驱动模块
 
 ## 7. 面试精选
 
+> ⚠️ **来源说明**:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识。
+
 ### Q1: 块设备驱动和字符设备驱动有什么区别?
 
 **答**:
@@ -760,3 +856,7 @@ Linux内核支持多种I/O调度算法:
 - [[DMA传输]]
 - [[eMMC驱动]]
 - [[SD卡驱动]]
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十八章 Linux 块设备驱动实验

+ 35 - 23
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/05-Linux外设驱动实战/面试-外设驱动实战.md

@@ -23,6 +23,8 @@ updated: 2026-09-17
 
 > 本模块涵盖嵌入式 Linux 外设驱动开发的核心面试题,覆盖 GPIO/LED、按键输入、LCD/Framebuffer、触摸屏、音频 ALSA、RTC/PWM、ADC/IIO、块设备八大类。每题含答案要点、详细解答和追问。
 
+> ⚠️ **来源说明**:本文件为面试题汇编,不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识。涉及原书章节(音频第65章、RTC第60章、PWM第73章、IIO第75章、ADC第76章、块设备第68章、多点电容触摸屏第64章)的事实已按原书相应章节核对。
+
 ---
 
 ## 一、LED/蜂鸣器驱动
@@ -82,12 +84,14 @@ iounmap(GPIO1_DR); unregister_chrdev(LED_MAJOR, LED_NAME);
 - 无源:需要外部方波信号,频率决定音调
 
 ```c
-buzzer { compatible = "my,buzzer"; pwms = <&pwm1 0 5000000 0>; };
+/* I.MX6ULL PWM 的 #pwm-cells = <2>,即 <phandle channel period_ns> */
+buzzer { compatible = "my,buzzer"; pwms = <&pwm1 0 5000000>; };
 struct pwm_device *pwm = devm_pwm_get(&pdev->dev, NULL);
 pwm_config(pwm, period * duty_cycle / 100, period);
 pwm_enable(pwm);
 ```
 
+
 **追问**:
 
 - PWM 频率和占空比分别影响什么?—— 频率决定音调(Hz),占空比决定音量
@@ -167,12 +171,12 @@ gpioleds { compatible = "my,gpio-leds";
 | Input 子系统 | 中     | 低       | 低       |
 | 设备树+中断  | 中     | 低       | 低       |
 
-`c
+```c
 input_report_key(input_dev, KEY_0, 1);
 input_sync(input_dev);
 input_report_key(input_dev, KEY_0, 0);
 input_sync(input_dev);
-`
+```
 
 **追问**:
 
@@ -191,7 +195,7 @@ input_sync(input_dev);
 3. 定时器消抖 -- 中断中启动定时器,回调中读取状态,最常用
 4. 工作队列消抖 -- 中断中调度 work,work 中读取状态
 
-`c
+```c
 static irqreturn_t key_irq_handler(int irq, void *dev_id) {
     struct key_dev *dev = dev_id;
     mod_timer(&dev->timer, jiffies + msecs_to_jiffies(15));
@@ -203,7 +207,7 @@ static void key_timer_func(struct timer_list *t) {
     if (val == 0) { input_report_key(dev->input, KEY_0, 1); input_sync(dev->input); }
     else { input_report_key(dev->input, KEY_0, 0); input_sync(dev->input); }
 }
-`
+```
 
 **追问**:
 
@@ -222,7 +226,7 @@ static void key_timer_func(struct timer_list *t) {
 3. 定时器超时后仍按下 -> 长按
 4. 松开时停止定时器,根据定时器状态判断
 
-`c
+```c
 static irqreturn_t key_irq_handler(int irq, void *dev_id) {
     struct key_dev *dev = dev_id;
     if (!dev->is_pressed) {
@@ -238,7 +242,7 @@ static void long_press_timer_func(struct timer_list *t) {
     struct key_dev *dev = from_timer(dev, t, long_press_timer);
     input_report_key(dev->input, KEY_0, 1); input_sync(dev->input);
 }
-`
+```
 
 **追问**:
 
@@ -342,8 +346,8 @@ LCD 时序:一行 = HBP + HSYNC active + HFP + 有效像素数据;一帧 = V
 3. 内核 LED 子系统可统一管理背光
 4. backlight_device 是背光设备的内核表示
 
-PWM 背光设备树:pwms = <&pwm1 0 5000000 0>; brightness-levels = <0 4 8 16 32 64 128 255>;
-GPIO 背光设备树:gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>;
+PWM 背光设备树:`pwms = <&pwm1 0 5000000>;`(周期 5000000ns=200Hz)、`brightness-levels = <0 4 8 16 32 64 128 255>;`、`default-brightness-level = <7>;`
+GPIO 背光设备树:`gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>;`
 
 **追问**:
 
@@ -389,7 +393,7 @@ GPIO 背光设备树:gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>;
 | 多点触控   | 不支持       | 支持(最多10点) |
 | 透光率     | ~75%         | ~90%             |
 | 驱动接口   | ADC + GPIO   | I2C/SPI + GPIO   |
-| Linux 驱动 | IIO 或自定义 | goodix、ft5x06   |
+| Linux 驱动 | IIO 或自定义 | ft5x06(FT5426)、goodix |
 
 **追问**:
 
@@ -408,7 +412,8 @@ GPIO 背光设备树:gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>;
 3. 设备树在 I2C/SPI 控制器下添加子节点
 4. 配置中断、复位、分辨率参数
 
-设备树示例:compatible = "goodix,gt9147"; reg = <0x5d>; interrupt-parent/irq-gpios; reset-gpios; goodix,x-max/y-max
+设备树示例(ALPHA 板 FT5426,挂 I2C2,原书示例代码 64.3.1.4):`compatible = "edt,edt-ft5426"; reg = <0x38>;` 并配置 `reset-gpios` 与中断 GPIO(`interrupt-parent`/`interrupts`)。使用 Goodix 方案的板子则为 `compatible = "goodix,gt9147"; reg = <0x5d>;`
+
 
 MT-B 协议:input_mt_slot -> input_mt_report_slot_state -> input_report_abs(X/Y) -> input_mt_sync_frame -> input_sync
 
@@ -432,11 +437,12 @@ MT-B 协议:input_mt_slot -> input_mt_report_slot_state -> input_report_abs(X/
 4. Platform 驱动:CPU 侧 DMA 和 DAI
 5. Codec 驱动:Codec 芯片寄存器和 DAI
 
-| 组件     | 职责                      | 示例           |
-| -------- | ------------------------- | -------------- |
-| Platform | CPU 侧:DMA、CPU DAI      | imx-sai.c      |
-| Codec    | Codec 芯片:DAC/ADC、DAPM | wm8960.c       |
-| Machine  | 绑定 Platform 和 Codec    | imx-sgtl5000.c |
+| 组件     | 职责                      | 示例(ALPHA 板)        |
+| -------- | ------------------------- | ----------------------- |
+| Platform | CPU 侧:DMA、CPU DAI      | fsl_sai.c               |
+| Codec    | Codec 芯片:DAC/ADC、DAPM | wm8960.c / es8388.c     |
+| Machine  | 绑定 Platform 和 Codec    | imx-audio-wm8960.c      |
+
 
 **追问**:
 
@@ -517,8 +523,9 @@ PCM/TDM 时序:SYNC 同步信号区分时隙,每个时隙对应一个通道
 3. pwm_enable/pwm_disable 使能/关闭
 4. 设备树中用 pwms 属性配置 PWM
 
-设备树格式:pwms = <phandle controller channel period_ns polarity>;
-驱动 API:devm_pwm_get -> pwm_config(pwm, duty_ns, period_ns) -> pwm_enable
+设备树格式:`pwms = <&pwm1 0 5000000>;`,即 `<PWM控制器phandle 通道号 周期(ns)>`(I.MX6ULL `#pwm-cells = <2>`)
+驱动 API:devm_pwm_get -> pwm_config(pwm, duty_ns, period_ns) -> pwm_enable -> pwm_disable
+
 
 **追问**:
 
@@ -589,7 +596,7 @@ vf610_adc_probe:devm_iio_device_alloc -> devm_ioremap_resource -> devm_request
 **追问**:
 
 - realbits vs storagebits?-- realbits 是实际有效位数,storagebits 是存储占用位数
-- 如何触发一次 ADC 转换?-- 写入 in_voltage1_raw 或使用 trigger
+- 如何触发一次 ADC 转换?-- 读取 in_voltage1_raw(读取即触发一次转换,见 vf610_read_raw)或使用 trigger
 - ADC 精度受什么影响?-- 参考电压、噪声、量化误差
 
 ---
@@ -668,15 +675,16 @@ vf610_adc_probe:devm_iio_device_alloc -> devm_ioremap_resource -> devm_request
 3. set_capacity 设置容量
 4. add_disk 注册磁盘
 
-步骤:alloc_chrdev_region -> alloc_disk -> blk_init_queue(my_request_fn, &lock) -> 设置 major/first_minor/fops -> set_capacity(disk, SECTORS) -> add_disk
+步骤(基于 blk_init_queue 的传统路径,原书第六十八章):`register_blkdev` -> `alloc_disk` -> `blk_init_queue(my_request_fn, &lock)` -> 设置 `major`/`first_minor`/`fops`/`queue` -> `set_capacity(disk, SECTORS)` -> `add_disk`
 
-请求处理函数:blk_fetch_request 从队列取 request -> 根据 req_data_dir 判断读写 -> 执行操作 -> __blk_mq_end_request 完成
+请求处理函数:`blk_fetch_request` 从队列取 request -> 根据 `rq_data_dir(req)` 判断读写 -> 用 `blk_rq_pos`/`blk_rq_cur_bytes`/`bio_data` 完成传输 -> `__blk_end_request_cur(req, err)` 完成
 
 **追问**:
 
 - blk_init_queue 的两个参数?-- 请求处理函数和自旋锁
 - set_capacity 的单位?-- 扇区数(每扇区 512 字节)
-- __blk_mq_end_request 的作用?-- 标记一个请求处理完成
+- `__blk_end_request_cur` 的作用?-- 标记当前请求的 chunk 处理完成(需持有队列锁)
+
 
 ---
 
@@ -687,7 +695,7 @@ vf610_adc_probe:devm_iio_device_alloc -> devm_ioremap_resource -> devm_request
 1. I/O 请求通过 request_queue 到达驱动
 2. 请求处理函数从队列中取出 request
 3. 根据 request 的方向(读/写)执行相应操作
-4. 处理完毕后调用 __blk_mq_end_request 完成
+4. 处理完毕后调用 `__blk_end_request_cur` 完成(Blk-mq 架构下为 `blk_mq_end_request`)
 
 读写流程:应用层 read() -> VFS -> 文件系统 -> 通用块层(查找 Page Cache,未命中生成 bio)-> I/O 调度器 -> 块设备驱动 -> 硬件操作 -> 完成中断 -> 唤醒等待进程
 
@@ -738,3 +746,7 @@ MMC 子系统架构:用户空间 -> /dev/mmcblkX -> MMC 核心层 -> MMC 主
 - Blk-mq vs 传统 blk?-- Blk-mq 消除全局锁,支持真正并行 I/O
 - 什么时候用 Blk-mq?-- NVMe、多队列 SCSI、现代 SSD
 - I/O 调度器在 Blk-mq 中的位置?-- 可选(mq-deadline/bfq/none),none 表示不调度直接下发
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第47/52/55/60/64/65/68/73/75/76章(并发与竞争、阻塞和非阻塞IO、设备树下的platform驱动、RTC、多点电容触摸屏、音频、块设备、PWM、IIO、ADC驱动实验)

+ 181 - 28
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/01-U-Boot命令与使用.md

@@ -38,7 +38,7 @@ bootloader与Linux内核的关系类似PC上的BIOS与Windows的关系。
 
 ### 1.4 U-Boot版本
 
-当前主流版本为`2016.03`,正点原子提供的版本基于此修改。版本号可通过`version`命令查看:
+U-Boot 官方源码当前最新版本为`2019.04`,但一般不会直接使用官方源码。NXP(半导体厂商)维护的版本为`2016.03`,正点原子在此基础上适配了 I.MX6U-ALPHA 开发板(出厂系统由 NXP 官方编译器`arm-poky-linux-gnueabi-gcc`编译,本教程统一使用`arm-linux-gnueabihf-gcc`)。版本号可通过`version`命令查看:
 
 ```
 U-Boot 2016.03-gd3f0479 (Aug 07 2020 - 20:47:37 +0800)
@@ -83,7 +83,7 @@ printenv
 print
 ```
 
-U-Boot支持TAB键自动补全功能,输入`prin`后按TAB即可自动补全。
+U-Boot支持TAB键自动补全功能,输入`print`后按TAB即可自动补全(直接输入`print`也可以)
 
 #### version — 版本号
 
@@ -168,6 +168,7 @@ nm.l 80000000
 与`nm`类似,但修改后地址会自动递增。
 
 ```bash
+# 以 .l 格式修改 0x80000000 起连续 3 个内存块(3*4=12 字节)
 mm.l 80000000
 # 输入05050505
 # 输入05050505
@@ -175,6 +176,7 @@ mm.l 80000000
 # 输入q退出
 ```
 
+
 #### mw — 填充内存
 
 ```bash
@@ -314,38 +316,134 @@ mmc part
 mmc rescan
 ```
 
+#### mmc erase — 擦除指定块
+
+```bash
+mmc erase blk# cnt
+```
+
+> **警告**:没事不要使用`mmc erase`擦除 MMC 设备!
+
+#### mmc 其他命令(PDF 表 30.4.5.1)
+
+| 命令 | 描述 |
+| ---- | ---- |
+| `mmc hwpartition` | 设置 MMC 设备的分区 |
+| `mmc bootbus` | 设置指定 MMC 设备的 BOOT_BUS_WIDTH 域的值 |
+| `mmc bootpart` | 设置指定 MMC 设备的 boot 和 RPMB 分区的大小 |
+| `mmc partconf` | 设置指定 MMC 设备的 PARTITION_CONFIG 域的值 |
+| `mmc rst` | 复位 MMC 设备 |
+| `mmc setdsr` | 设置 DSR 寄存器的值 |
+
+#### 用 mmc write 更新 U-Boot
+
+SD 卡(742 块,即 0x2E6):
+
+```bash
+mmc dev 0 0                    # 切换到 SD 卡分区 0
+mmc write 80800000 2 2E6       # 从第 2 个扇区开始写 0x2E6 个块
+```
+
+EMMC(需要额外做分区配置):
+
+```bash
+mmc dev 1 0                    # 切换到 EMMC 分区 0
+tftp 80800000 u-boot.imx       # 下载 u-boot.imx 到 DRAM
+mmc write 80800000 2 32E       # 烧写 u-boot.imx 到 EMMC 中
+mmc partconf 1 1 0 0           # 分区配置,EMMC 需要这一步!
+```
+
 ### 2.7 FAT文件系统命令
 
-仅支持FAT格式文件系统。
+相关命令有`fatinfo`、`fatls`、`fstype`、`fatload`和`fatwrite`,**仅支持FAT格式文件系统**
 
 ```bash
-# 查询文件系统信息
+# fatinfo <interface> [<dev[:part]>]  查询文件系统信息
 fatinfo mmc 1:1
 
-# 列出目录文件
+# fatls <interface> [<dev[:part]>] [directory]  列出目录/文件
 fatls mmc 1:1
 
-# 查看分区文件系统格式
+# fstype <interface> <dev>:<part>  查看分区文件系统格式
 fstype mmc 1:0
 fstype mmc 1:1
 fstype mmc 1:2
 
-# 读取文件到DRAM
+# fatload <interface> [<dev[:part]> [<addr> [<filename> [bytes [pos]]]]]
+# bytes 为 0 或省略表示读整个文件,pos 为文件内偏移
 fatload mmc 1:1 80800000 zImage
 
-# 写入数据到FAT分区(需使能CONFIG_FAT_WRITE)
+# fatwrite <interface> <dev[:part]> <addr> <filename> <bytes>
+# 需在板级配置头文件中使能 CONFIG_FAT_WRITE
 fatwrite mmc 1:1 80800000 zImage 6788f8
 ```
 
 ### 2.8 EXT文件系统命令
 
+U-Boot 支持`ext2`和`ext4`两种格式,常用命令有`ext2load`、`ext2ls`、`ext4load`、`ext4ls`和`ext4write`,含义和用法与 FAT 系列一致。EMMC 分区 2 为 ext4 格式,用于存放根文件系统(rootfs)。
+
 ```bash
-# ext4文件系统操作
+# 列出 EMMC 分区 2(ext4)中的文件和目录
 ext4ls mmc 1:2
-ext4load mmc 1:2 83000000 /boot/imx6ull.dtb
+
+# 读取文件到 DRAM(用法同 fatload)
+ext4load mmc 1:2 <addr> <filename>
+
+# 写入(用法同 fatwrite)
+ext4write mmc 1:2 <addr> <filename> <bytes>
+```
+
+### 2.9 NAND操作命令(NAND核心板)
+
+前提是使用 NAND 版本核心板并编译对应的 U-Boot。常用命令有`nand info`、`nand device`、`nand erase`、`nand write`、`nand read`,以及查看/操作设备树的`fdt`命令。
+
+```bash
+# 打印 NAND 信息(页大小、OOB、擦除大小等)
+nand info
+
+# nand erase[.spread] [clean] off size     从 off 开始擦除 size 区域
+nand erase.part [clean] partition          擦除指定分区
+nand erase.chip [clean]                    全片擦除
+
+# nand write addr off size    向 NAND 指定地址写数据
+# nand read  addr off size    从 NAND 指定地址读数据到 DRAM
+```
+
+I.MX6U-ALPHA 出厂系统 NAND 分区(共 6 个分区):
+
+| 分区 | 地址范围 | 大小 | 内容 |
+| ---- | -------- | ---- | ---- |
+| boot | 0x000000~0x3FFFFF | 4MB | uboot |
+| env | 0x400000~0x41FFFF | 128KB | 环境变量 |
+| logo | 0x420000~0x51FFFF | 1MB | 启动图标 |
+| dtb | 0x520000~0x61FFFF | 1MB | 设备树 |
+| kernel | 0x620000~0xE1FFFF | 8MB | Linux 内核 |
+| rootfs | 0xE20000~0x20000000 | 剩余 | 根文件系统 |
+
+烧写 kernel 与 dtb 示例:
+
+```bash
+tftp 0x87800000 zImage
+nand erase 0x620000 0x800000
+nand write 0x87800000 0x620000 0x800000
+
+tftp 0x87800000 imx6ull-14x14-emmc-7-1024x600-c.dtb
+nand erase 0x520000 0x100000
+nand write 0x87800000 0x520000 0x100000
 ```
 
-### 2.9 boot命令
+> **注意**:I.MX6ULL 要求 NAND 版 U-Boot 额外包含 BCB 和 DBBT,直接编译出的`u-boot.imx`不能烧写到 NAND(需 mfgtool 用`kogs-ng`工具添加)。rootfs 太大,也不在 U-Boot 中更新,改用 Mfgtool 烧写。
+
+读取设备树并用`fdt`查看:
+
+```bash
+nand read 0x83000000 0x520000 0x19000
+fdt addr 83000000
+fdt header
+fdt print
+```
+
+### 2.10 boot命令
 
 #### bootz — 启动zImage
 
@@ -354,7 +452,12 @@ bootz [addr [initrd[:size]] [fdt]]
 
 # 示例:从网络启动
 tftp 80800000 zImage
-tftp 83000000 imx6ull.dtb
+tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
+bootz 80800000 - 83000000
+
+# 示例:从 EMMC 分区 1 启动
+fatload mmc 1:1 80800000 zImage
+fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
 bootz 80800000 - 83000000
 ```
 
@@ -372,19 +475,22 @@ bootm [addr [initrd[:size]] [fdt]]
 boot
 ```
 
-### 2.10 其他常用命令
+### 2.11 其他常用命令
 
 ```bash
 # 复位重启
 reset
 
-# 跳转到指定地址执行
-go addr
+# 跳转到指定地址执行(arg 为可选参数)
+go addr [arg ...]
+# 例:tftp 87800000 printf.bin; go 87800000(裸机程序链接首地址为 0x87800000)
+tftp 87800000 printf.bin
+go 87800000
 
 # 运行环境变量中的命令
 run bootcmd
 
-# 内存测试
+# 内存读写测试
 mtest [start [end [pattern [iterations]]]]
 mtest 80000000 80001000
 ```
@@ -407,17 +513,19 @@ saveenv
 - `rootwait`:等待根设备就绪
 - `rw`:以读写方式挂载
 
+> 若不设置`bootargs`,启动内核时会报错:`Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)`,原因是内核找不到根文件系统。
+
 ### 3.2 bootcmd — 启动命令
 
 `bootcmd`保存自动启动时执行的命令集合:
 
 ```bash
 # 从网络启动
-setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull.dtb; bootz 80800000 - 83000000'
+setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000'
 saveenv
 
 # 从EMMC启动
-setenv bootcmd 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull.dtb; bootz 80800000 - 83000000'
+setenv bootcmd 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000'
 saveenv
 ```
 
@@ -435,17 +543,18 @@ saveenv
 
 ```bash
 # 定义不同启动方式
-setenv mybootemmc 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull.dtb; bootz 80800000 - 83000000'
-setenv mybootnet 'tftp 80800000 zImage; tftp 83000000 imx6ull.dtb; bootz 80800000 - 83000000'
+setenv mybootemmc 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000'
 setenv mybootnand 'nand read 80800000 4000000 800000; nand read 83000000 6000000 100000; bootz 80800000 - 83000000'
+setenv mybootnet 'tftp 80800000 zImage; tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000'
 saveenv
 
 # 使用run命令切换启动方式
 run mybootemmc
-run mybootnet
 run mybootnand
+run mybootnet
 ```
 
+
 ### 3.5 bootdelay
 
 控制启动倒计时时间,默认为3秒。修改方法:
@@ -557,6 +666,14 @@ bootz 80800000 - 83000000
 
 ### 5.2 U-Boot编译
 
+U-Boot 源码解压到 `~/linux/uboot/alientek_uboot`:
+
+```bash
+tar -vxjf uboot-imx-2016.03-2.1.0-g8b546e4.tar.bz2
+```
+
+512MB(DDR3)+8GB(EMMC)核心板:
+
 ```bash
 # 清理工程
 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
@@ -568,7 +685,18 @@ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defcon
 make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12
 ```
 
-编译产物:`u-boot.bin`(二进制)和`u-boot.imx`(加头部后的最终镜像)。
+也可写成脚本 `mx6ull_alientek_emmc.sh`:
+
+```bash
+#!/bin/bash
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
+make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
+make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12
+```
+
+256MB(DDR3)+512MB(NAND)核心板配置文件为 `mx6ull_14x14_ddr256_nand_defconfig`,脚本名 `mx6ull_alientek_nand.sh`。
+
+编译产物:`u-boot.bin`(二进制)和`u-boot.imx`(加头部后的最终镜像)。U-Boot 是裸机程序,需在前面加上 IVT、DCD 等头部才能在 I.MX6U 上执行,`u-boot.imx` 就是添加头部后的镜像,也是最终烧写的文件。
 
 ### 5.3 U-Boot烧写
 
@@ -590,12 +718,29 @@ chmod 777 imxdownload
 
 ```
 U-Boot 2016.03-gd3f0479 (Aug 07 2020 - 20:47:37 +0800)
-CPU: Freescale i.MX6ULL rev1.1 792 MHz (running at 396 MHz)
+CPU:   Freescale i.MX6ULL rev1.1 792 MHz (running at 396 MHz)
+CPU:   Industrial temperature grade (-40C to 105C) at 51C
+Reset cause: POR
 Board: I.MX6U ALPHA|MINI
-DRAM: 512 MiB
-MMC: FSL_SDHC: 0, FSL_SDHC: 1
-Net: FEC1
-```
+I2C:   ready
+DRAM:  512 MiB
+MMC:   FSL_SDHC: 0, FSL_SDHC: 1
+Display: ATK-LCD-7-1024x600 (1024x600)
+Video: 1024x600x24
+In:    serial
+Out:   serial
+Err:   serial
+switch to partitions #0, OK
+mmc1(part 0) is current device
+Net:   FEC1
+Error: FEC1 address not set.
+
+Normal Boot
+Hit any key to stop autoboot:  0
+=>
+```
+
+要点:CPU 为飞思卡尔 i.MX6ULL,主频 792MHz(此时运行在 396MHz),工业级结温 -40°C~105°C;`FSL_SDHC:0` 接 SD(TF) 卡,`FSL_SDHC:1` 接 EMMC;EMMC 版本默认以 EMMC 为存储器,故显示 `switch to partitions #0` 与 `mmc1(part 0)`;`FEC1 address not set` 提示网卡 MAC 未设置。NAND 版本无 EMMC/SD 信息,显示 NAND 容量。
 
 ### 5.6 命令验证测试
 
@@ -619,6 +764,8 @@ bootz 80800000 - 83000000
 
 ## 6. 跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 6.1 IMX6ULL vs STM32MP1 vs RK3568 U-Boot差异
 
 | 特性           | IMX6ULL             | STM32MP1               | RK3568              |
@@ -641,6 +788,8 @@ bootz 80800000 - 83000000
 
 ## 7. 面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(答案基于书中事实整理)。
+
 ### Q1: U-Boot的作用是什么?与BIOS有什么区别?
 
 **答**:U-Boot是嵌入式系统的bootloader,主要作用是初始化硬件(DDR、串口等)、加载内核到内存并启动Linux。与PC BIOS的区别:
@@ -671,5 +820,9 @@ bootz 80800000 - 83000000
 
 1. `tftp 80800000 u-boot.imx` — 下载新U-Boot到DRAM
 2. `mmc dev 0 0` — 切换到目标设备
-3. `mmc write 80800000 2 2E6` — 写入(跳过前2个扇区的分区表)
+3. `mmc write 80800000 2 2E6` — 写入(742 块,跳过前2个扇区的分区表;EMMC 用 `mmc write 80800000 2 32E`,还需 `mmc partconf 1 1 0 0`
 4. 重启验证版本号
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第30章 U-Boot使用实验

+ 485 - 279
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/02-U-Boot启动流程.md

@@ -50,16 +50,15 @@ mindmap
 arch/arm/
 +-- cpu/
 |   +-- armv7/
-|   |   +-- start.S        # 启动入口
-|   |   +-- lowlevel_init.S
-|   |   +-- u-boot.lds     # 链接脚本模板
-|   +-- u-boot.lds
+|   |   +-- start.S        # reset、save_boot_params、cpu_init_cp15、cpu_init_crit
+|   |   +-- lowlevel_init.S# 低级初始化(设置 SP、调用 s_init)
+|   +-- u-boot.lds         # 链接脚本模板(编译后生成根目录 u-boot.lds)
 +-- imx-common/            # NXP i.MX系列通用代码
 +-- mach-mx6/              # i.MX6系列SoC支持
 +-- lib/
-|   +-- vectors.S          # 中断向量表
-|   +-- crt0.S             # C运行时初始化
-|   +-- board.c
+|   +-- vectors.S          # _start 与中断向量表
+|   +-- crt0.S             # _main 与 C 运行时初始化
+|   +-- relocate.S         # relocate_code / relocate_vectors
 +-- include/asm/arch-mx6/  # i.MX6寄存器定义
 ```
 
@@ -112,8 +111,8 @@ cmd_u-boot.bin := cp u-boot-nodtb.bin u-boot.bin
 
 # .u-boot-nodtb.bin.cmd - ELF转二进制
 cmd_u-boot-nodtb.bin := arm-linux-gnueabihf-objcopy \
-    --gap-fill=0xff -j .text -j .rodata -j .data \
-    -j .got -j .got.plt -j .u_boot_list -j .rel.dyn \
+    --gap-fill=0xff -j .text -j .secure_text -j .rodata -j .hash \
+    -j .data -j .got -j .got.plt -j .u_boot_list -j .rel.dyn \
     -O binary u-boot u-boot-nodtb.bin
 
 # .u-boot.imx.cmd - 生成NXP专用镜像
@@ -129,23 +128,32 @@ cmd_u-boot.imx := ./tools/mkimage \
 ### 2.1 Makefile 核心变量
 
 ```makefile
-# 版本信息
+# 版本信息(顶层 Makefile 第 5~9 行)
 VERSION = 2016
 PATCHLEVEL = 03
-SUBLEVEL = 00
-EXTRAVERSION = -alientek
+SUBLEVEL =
+EXTRAVERSION =
+NAME =
+# VERSION/PATCHLEVEL/SUBLEVEL 共同构成 2016.03;EXTRAVERSION、NAME 一般不使用
+
+# 交叉编译工具链(第 245~246 行:主机架构==目标架构时 CROSS_COMPILE 为空)
+ifeq ($(HOSTARCH),$(ARCH))
+CROSS_COMPILE ?=
+endif
+# 实际编译时通过 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 传入,
+# 也可直接在顶层 Makefile 中写死 ARCH=arm / CROSS_COMPILE=arm-linux-gnueabihf-
+```
 
-# 交叉编译工具链
-CROSS_COMPILE ?= arm-linux-gnueabihf-
+以下`CONFIG_SYS_*`来自板级配置头文件(经`make xxx_defconfig`写入`.config`),而非顶层 Makefile:
 
-# 架构和CPU配置
-ARCH ?= arm
+```text
 CONFIG_SYS_ARCH="arm"
 CONFIG_SYS_CPU="armv7"
 CONFIG_SYS_SOC="mx6"
 CONFIG_SYS_VENDOR="freescale"
 CONFIG_SYS_BOARD="mx6ull_alientek_emmc"
 CONFIG_SYS_CONFIG_NAME="mx6ull_alientek_emmc"
+CONFIG_SYS_TEXT_BASE=0x87800000
 ```
 
 ### 2.2 主要Makefile目标
@@ -174,32 +182,47 @@ flowchart TD
 ### 2.4 编译规则详解
 
 ```makefile
-u-boot: $(OBJS) $(LIBS) u-boot.lds FORCE
-    $(LD) -pie --gc-sections -Bstatic \
-        -Ttext 0x87800000 \
-        -o u-boot -T u-boot.lds \
-        arch/arm/cpu/armv7/start.o \
-        --start-group $(OBJS) $(LIBS) --end-group
+# 目标依赖关系(顶层 Makefile)
+u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE
+	$(call if_changed,u-boot__)
 
 u-boot-nodtb.bin: u-boot FORCE
-    $(OBJCOPY) --gap-fill=0xff \
-        -j .text -j .rodata -j .data -j .got -j .u_boot_list \
-        -O binary u-boot u-boot-nodtb.bin
+	$(call if_changed,objcopy)
+	$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
+
+u-boot.bin: u-boot-nodtb.bin FORCE
+	$(call if_changed,copy)     # 即 cp u-boot-nodtb.bin u-boot.bin
 
-u-boot.bin: u-boot-nodtb.bin
-    cp u-boot-nodtb.bin u-boot.bin
+# u-boot-init = $(head-y) = arch/arm/cpu/armv7/start.o
+# u-boot-main = $(libs-y) = 所有子目录的 built-in.o
 ```
 
+展开后实际执行的链接命令(来自 `.u-boot.cmd`):
+
+```bash
+arm-linux-gnueabihf-ld.bfd -pie --gc-sections -Bstatic -Ttext 0x87800000 \
+    -o u-boot -T u-boot.lds arch/arm/cpu/armv7/start.o \
+    --start-group arch/arm/cpu/built-in.o arch/arm/cpu/armv7/built-in.o \
+        arch/arm/imx-common/built-in.o arch/arm/lib/built-in.o \
+        board/freescale/common/built-in.o \
+        board/freescale/mx6ull_alientek_emmc/built-in.o \
+        cmd/built-in.o common/built-in.o disk/built-in.o drivers/built-in.o \
+        ... fs/built-in.o lib/built-in.o net/built-in.o ... --end-group \
+    arch/arm/lib/eabi_compat.o -lgcc -Map u-boot.map
+```
+
+`u-boot.lds` 的生成规则:`u-boot.lds: $(LDSCRIPT) prepare FORCE`(`$(LDSCRIPT)` 即 `arch/arm/cpu/u-boot.lds`)。
+
 ### 2.5 链接脚本关键配置
 
 ```text
-OUTPUT_FORMAT("elf32-littlearm")
+OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
 OUTPUT_ARCH(arm)
 ENTRY(_start)
-
 SECTIONS
 {
-    . = 0x87800000;  /* U-Boot加载地址 */
+    . = 0x00000000;          /* 注意:链接脚本里是 0x00000000,不是 0x87800000 */
+    . = ALIGN(4);
     .text :
     {
         *(.__image_copy_start)
@@ -210,35 +233,53 @@ SECTIONS
     .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
     .data : { *(.data*) }
     .u_boot_list : { KEEP(*(SORT(.u_boot_list*))); }
-    .bss : { *(.bss*) }
+    .image_copy_end : { *(.__image_copy_end) }
+    .rel_dyn_start : { *(.__rel_dyn_start) }
+    .rel.dyn : { *(.rel*) }
+    .rel_dyn_end : { *(.__rel_dyn_end) }
+    .end : { *(.__end) }
+    _image_binary_end = .;
+    .bss_start __rel_dyn_start (OVERLAY) : { KEEP(*(.__bss_start)); __bss_base = .; }
+    .bss __bss_base (OVERLAY) : { *(.bss*) . = ALIGN(4); __bss_limit = .; }
+    .bss_end __bss_limit (OVERLAY) : { KEEP(*(.__bss_end)); }
+    ...
 }
 ```
 
-**关键地址变量:**
+> 真正的加载地址由链接时的 `-Ttext 0x87800000` 决定,链接脚本本身从 `0x00000000` 开始。入口点是 `_start`,定义在 `arch/arm/lib/vectors.S` 中,其后紧跟中断向量表(`.vectors` 段)。
 
-| 变量                 | 数值       | 描述               |
-| -------------------- | ---------- | ------------------ |
-| `__image_copy_start` | 0x87800000 | U-Boot拷贝首地址   |
-| `__image_copy_end`   | 0x8785dd54 | U-Boot拷贝结束地址 |
-| `__rel_dyn_start`    | 0x8785dd54 | .rel.dyn段起始     |
-| `__bss_start`        | 0x8785dd54 | BSS段起始          |
-| `__bss_end`          | 0x878a8e74 | BSS段结束          |
+**关键地址变量(示例编译值,以实际编译为准):**
+
+| 变量                 | 数值       | 描述                    |
+| -------------------- | ---------- | ----------------------- |
+| `__image_copy_start` | 0x87800000 | U-Boot拷贝首地址        |
+| `__image_copy_end`   | 0x8785dd54 | U-Boot拷贝结束地址      |
+| `__rel_dyn_start`    | 0x8785dd54 | .rel.dyn段起始          |
+| `__rel_dyn_end`      | 0x878668f4 | .rel.dyn段结束          |
+| `_image_binary_end`  | 0x878668f4 | 镜像结束地址            |
+| `__bss_start`        | 0x8785dd54 | BSS段起始               |
+| `__bss_end`          | 0x878a8e74 | BSS段结束               |
 
 ### 2.6 编译选项详解
 
 ```text
-# 链接选项
--DCONFIG_SYS_TEXT_BASE=0x87800000
--pie
---gc-sections
-
-# 编译选项
--O2
--fpie
-
-# 安全选项
--fstack-protector-strong
--D_FORTIFY_SOURCE=2
+# 链接选项(.u-boot.cmd)
+-pie                 # 位置无关可执行文件
+--gc-sections        # 回收未使用的段
+-Bstatic            # 静态链接
+-Ttext 0x87800000   # U-Boot 链接/加载地址
+
+# 生成 u-boot-nodtb.bin 时(.u-boot-nodtb.bin.cmd)
+--gap-fill=0xff
+$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))   # 修正 .rel.dyn 重定位表
+
+# 交叉工具变量(顶层 Makefile 第 333~347 行)
+CC = $(CROSS_COMPILE)gcc
+LD = $(CROSS_COMPILE)ld.bfd(若 ld.bfd 不可用则回退为 $(CROSS_COMPILE)ld)
+OBJCOPY = $(CROSS_COMPILE)objcopy
+AR = $(CROSS_COMPILE)ar
+NM = $(CROSS_COMPILE)nm
+OBJDUMP = $(CROSS_COMPILE)objdump
 ```
 
 ### 2.7 Kbuild系统
@@ -300,93 +341,80 @@ flowchart TD
 
 ### 3.2 启动阶段划分
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(书中未讲 SPL;此处用于说明 I.MX6U 与常见 SPL 架构的区别)。
+
+I.MX6U 的 U-Boot **不是 SPL 架构**(默认直接使用完整的`u-boot.imx`)。上电后由芯片内部的 Boot ROM 把`u-boot.imx`加载到 DDR 的`0x87800000`处执行,之后 U-Boot 自身再经历“重定位前 → 重定位 → 重定位后”三个阶段:
+
 ```mermaid
 flowchart LR
-    subgraph SPL
-        A1[初始化DDR] --> A2[初始化串口]
-        A2 --> A3[加载U-Boot]
+    subgraph ROM[Boot ROM]
+        A1[解析 u-boot.imx 头部] --> A2[加载到 DDR 0x87800000]
+    end
+    subgraph F[board_init_f(重定位前)]
+        B1[初始化串口/定时器/DDR] --> B2[规划内存并计算重定位地址]
     end
-    subgraph U-Boot
-        B1[初始化外设] --> B2[网络初始化]
-        B2 --> B3[命令初始化]
-        B3 --> B4[环境变量]
+    subgraph R[board_init_r(重定位后)]
+        C1[初始化外设/MMC/网络] --> C2[环境变量/控制台] --> C3[main_loop 命令行]
     end
     subgraph Linux
-        C1[加载内核] --> C2[加载设备树]
-        C2 --> C3[传递参数]
-        C3 --> C4[跳转执行]
+        D1[加载内核与设备树] --> D2[传递参数] --> D3[跳转执行]
     end
-    A3 --> B1
-    B4 --> C1
+    A2 --> B1
+    B2 --> R
+    C3 --> D1
 ```
 
-**三个阶段职责:**
+**阶段职责:**
 
-| 阶段   | 职责                        | 关键函数                           |
-| ------ | --------------------------- | ---------------------------------- |
-| SPL    | 初始化DDR、串口,加载U-Boot | `board_init_f()`, `board_init_r()` |
-| U-Boot | 初始化更多外设,网络,命令  | `board_init_r()`, `main_loop()`    |
-| Linux  | 挂载rootfs,启动init        | `start_kernel()`                   |
+| 阶段 | 职责 | 关键函数 |
+| ---- | ---- | -------- |
+| Boot ROM | 从启动设备加载`u-boot.imx`到 DDR | 芯片固化代码 |
+| board_init_f(重定位前) | 初始化串口/定时器/DDR,`init_sequence_f[]`,计算重定位目的地址 | `board_init_f()` |
+| 重定位 | 将 U-Boot 拷贝到 DDR 高地址,修正`.rel.dyn`,重定位向量表 | `relocate_code()`、`relocate_vectors()` |
+| board_init_r(重定位后) | 初始化 MMC/NAND/网络/环境变量,进入命令行 | `board_init_r()`、`main_loop()` |
+| Linux | 挂载rootfs,启动init | `boot_jump_linux()`、`kernel_entry()` |
 
 ### 3.3 内存布局
 
 ```text
-OCRAM (内部RAM):
-0x00900000 +---------------------+
-           |    OCRAM起始       |
-0x0091FF00 +---------------------+ <- CONFIG_SYS_INIT_SP_ADDR
-           |    256B GD结构体   |
-0x00920000 +---------------------+ OCRAM结束 (128KB)
-
-DRAM (外部DDR):
-0x80000000 +---------------------+
-           |    DRAM起始       |
-0x80800000 +---------------------+ <- Linux内核加载地址
-           |                   |
-0x83000000 +---------------------+ <- 设备树加载地址
-           |                   |
-0x84000000 +---------------------+ <- 根文件系统
-           |                   |
-0x87800000 +---------------------+ <- U-Boot加载地址
+OCRAM (内部RAM, 128KB):
+0x00900000 +---------------------+ <- IRAM_BASE_ADDR
+           |       OCRAM         |
+0x0091FE08 +---------------------+ <- lowlevel_init 中 sp -= GD_SIZE(248)
+0x0091FF00 +---------------------+ <- CONFIG_SYS_INIT_SP_ADDR (sp 初始值)
+0x00920000 +---------------------+ OCRAM结束 (0x20000, 128KB)
+
+DRAM (外部DDR, 512MB):
+0x80000000 +---------------------+ <- DRAM起始 (gd->ram_size=0x20000000)
+0x80800000 +---------------------+ <- Linux内核(zImage)加载地址
+0x83000000 +---------------------+ <- 设备树(dtb)加载地址
+0x87800000 +---------------------+ <- U-Boot链接/初始加载地址
            |    U-Boot代码     |
-0x878a8e74 +---------------------+ <- BSS段结束
-           +---------------------+
+0x878a8e74 +---------------------+ <- BSS段结束 (__bss_end)
+           |         ...       |
+0x9EF44E90 +---------------------+ <- 重定位后最终的 sp (gd->start_addr_sp)
+0x9FF47000 +---------------------+ <- U-Boot重定位后的运行地址 (gd->relocaddr)
+           |         ...       |
+0xA0000000 +---------------------+ <- DRAM最高地址 (gd->ram_top)
 ```
 
-### 3.4 SPL阶段详解
+### 3.4 重定位前阶段(board_init_f)
+
+`_main`中先调用`board_init_f_alloc_reserve`预留早期 malloc 与 gd 区域,再调用`board_init_f`运行`init_sequence_f[]`,主要工作是初始化串口、定时器、DDR 等,并“自顶向下”规划内存,最终算出重定位目的地址。
 
 ```text
-SPL (Secondary Program Loader) 阶段职责:
-+-- 1. 初始化CPU时钟
-+-- 2. 初始化DDR控制器
-+-- 3. 初始化串口控制台
-+-- 4. 从存储介质加载U-Boot
-|   +-- MMC/SD卡
-|   +-- NAND Flash
-|   +-- SPI Flash
-|   +-- USB
-+-- 5. 跳转到U-Boot执行
+board_init_f(重定位前)职责:
++-- 1. setup_mon_len:设置 gd->mon_len = __bss_end - _start
++-- 2. arch_cpu_init / board_early_init_f:CPU 与板级早期初始化(串口 IO)
++-- 3. timer_init / get_clocks:定时器与时钟
++-- 4. env_init / init_baud_rate / serial_init / console_init_f:环境变量、波特率、串口、控制台
++-- 5. dram_init:设置 gd->ram_size(正点原子 EMMC 版为 512MB)
++-- 6. setup_dest_addr:设置 gd->ram_size、gd->ram_top=0xA0000000、gd->relocaddr=0xA0000000
++-- 7. reserve_*:从高地址向下依次预留 MMU TLB、U-Boot、malloc、bd、gd、栈等
++-- 8. setup_reloc:把旧 gd 拷贝到 gd->new_gd
 ```
 
-**SPL启动时序:**
-
-```mermaid
-sequenceDiagram
-    participant ROM as Boot ROM
-    participant SPL as SPL
-    participant DDR as DDR控制器
-    participant UART as 串口
-    participant MMC as MMC/SD卡
-    participant UBoot as U-Boot
-    ROM->>SPL: 加载SPL到OCRAM
-    SPL->>SPL: 初始化CPU时钟
-    SPL->>DDR: 初始化DDR控制器
-    DDR-->>SPL: DDR就绪
-    SPL->>UART: 初始化串口
-    SPL->>MMC: 从MMC读取U-Boot
-    MMC-->>SPL: U-Boot镜像
-    SPL->>UBoot: 跳转到U-Boot入口
-```
+关键中间结果(示例):`gd->relocaddr = 0xA0000000` → `reserve_mmu`后 `0x9FFF0000` → `reserve_uboot`后 `0x9FF47000` → `reserve_malloc`(16MB+8KB)后 `0x9EF45000` → `reserve_board`(80B)→ `reserve_global_data`(248B)→ `reserve_stacks` 后最终 `gd->start_addr_sp = 0x9EF44E90`。
 
 ### 3.5 U-Boot主阶段详解
 
@@ -439,70 +467,106 @@ boot                            // 执行bootcmd
 
 ## 四、关键代码分析
 
-### 4.1 start.S - 启动入口
+### 4.1 vectors.S / start.S - 启动入口
 
-```armasm
-/ * arch/arm/cpu/armv7/start.S * /
+入口`_start`定义在`arch/arm/lib/vectors.S`,其后就是中断向量表(`.vectors` 段),第一个向量`b reset`跳到`arch/arm/cpu/armv7/start.S`中的`reset`:
 
+```armasm
+/ * arch/arm/lib/vectors.S * /
 _start:
-    b   reset              @ 0x00: 跳转到reset向量处理
-    ldr pc, _undefined_instruction @ 0x04: 未定义指令异常
-    ldr pc, _software_interrupt @ 0x08: 软件中断
-    ldr pc, _prefetch_abort   @ 0x0c: 取指令中断
-    ldr pc, _data_abort        @ 0x10: 数据中断
-    ldr pc, _not_used          @ 0x14: 未使用
-    ldr pc, _irq               @ 0x18: IRQ中断
-    ldr pc, _fiq               @ 0x1c: FIQ中断
+    b   reset                     @ 0x00: 复位向量
+    ldr pc, _undefined_instruction @ 0x04
+    ldr pc, _software_interrupt    @ 0x08
+    ldr pc, _prefetch_abort        @ 0x0c
+    ldr pc, _data_abort            @ 0x10
+    ldr pc, _not_used              @ 0x14
+    ldr pc, _irq                   @ 0x18
+    ldr pc, _fiq                   @ 0x1c
 
+/ * arch/arm/cpu/armv7/start.S * /
 reset:
-    mrs   r0, cpsr            @ 读取CPSR寄存器
-    orr   r0, r0, #0x1f       @ 设置模式为SVC模式 (0x13 | 0x1c = 0x1f)
-    msr   cpsr, r0            @ 禁止IRQ和FIQ中断
-
-    bl    save_boot_params     @ 保存启动参数
-    bl    cpu_init_cp15        @ 初始化CP15协处理器
-    bl    cpu_init_crit        @ 初始化关键硬件
-    b     _main                @ 跳转到_main入口
+    b   save_boot_params           @ 允许板子保存重要寄存器
+
+ENTRY(save_boot_params)
+    b   save_boot_params_ret       @ back to my caller
+
+save_boot_params_ret:
+    mrs  r0, cpsr
+    and  r1, r0, #0x1f   @ mask mode bits
+    teq  r1, #0x1a       @ test for HYP mode
+    bicne r0, r0, #0x1f  @ clear all mode bits
+    orrne r0, r0, #0x13  @ set SVC mode
+    orr  r0, r0, #0xc0   @ disable FIQ and IRQ
+    msr  cpsr, r0
+
+    @ 设置向量表重定位:清 SCTLR.V,把 _start 写入 VBAR
+    mrc p15, 0, r0, c1, c0, 0      @ Read CP15 SCTLR
+    bic r0, #CR_V                  @ CR_V = (1 << 13)
+    mcr p15, 0, r0, c1, c0, 0
+    ldr r0, =_start
+    mcr p15, 0, r0, c12, c0, 0     @ Set VBAR
+
+#ifndef CONFIG_SKIP_LOWLEVEL_INIT
+    bl  cpu_init_cp15
+    bl  cpu_init_crit
+#endif
+    bl  _main
 ```
 
+
 ### 4.2 cpu_init_cp15 - 协处理器初始化
 
 ```armasm
-cpu_init_cp15:
-    @ 禁用MMU
-    mcr p15, 0, r0, c8, c7, 0   @ 清零TLB表
-    mcr p15, 0, r0, c7, c5, 0   @ 清零指令缓存
-    mcr p15, 0, r0, c7, c5, 6   @ 清零BP表
-    mrc p15, 0, r0, c1, c0, 0   @ 读取SCTLR
-    bic r0, r0, #0x000d         @ 关闭MMU和数据缓存
-    bic r0, r0, #0x1100         @ 关闭I-Cache和切换缓存
-    mcr p15, 0, r0, c1, c0, 0   @ 写入SCTLR
-    @ 启用I-Cache
-    mrc p15, 0, r0, c1, c0, 0   @ 读取SCTLR
-    orr r0, r0, #0x0004         @ 启用D-Cache作为普通内存
-    mcr p15, 0, r0, c1, c0, 0   @ 写入SCTLR
-    mov pc, lr                  @ 返回
+ENTRY(cpu_init_cp15)
+    /* Invalidate L1 I/D */
+    mov r0, #0
+    mcr p15, 0, r0, c8, c7, 0   @ invalidate TLBs
+    mcr p15, 0, r0, c7, c5, 0   @ invalidate icache
+    mcr p15, 0, r0, c7, c5, 6   @ invalidate BP array
+    mcr p15, 0, r0, c7, c10, 4  @ DSB
+    mcr p15, 0, r0, c7, c5, 4   @ ISB
+
+    /* disable MMU stuff and caches */
+    mrc p15, 0, r0, c1, c0, 0
+    bic r0, r0, #0x00002000     @ clear bit 13 (--V-)
+    bic r0, r0, #0x00000007     @ clear bits 2:0 (-CAM)
+    orr r0, r0, #0x00000002     @ set bit 1 (--A-) Align
+    orr r0, r0, #0x00000800     @ set bit 11 (Z---) BTB
+#ifdef CONFIG_SYS_ICACHE_OFF
+    bic r0, r0, #0x00001000     @ clear bit 12 (I) I-cache
+#else
+    orr r0, r0, #0x00001000     @ set bit 12 (I) I-cache
+#endif
+    mcr p15, 0, r0, c1, c0, 0
+    ...
+    mov pc, r5                  @ back to my caller
+ENDPROC(cpu_init_cp15)
 ```
 
+> 说明:I.MX6U 的 U-Boot 默认**打开 I-Cache**(未定义 `CONFIG_SYS_ICACHE_OFF`),并关闭 MMU、D-Cache。
+
 ### 4.3 lowlevel_init - 低级初始化
 
 ```armasm
-lowlevel_init:
-    @ 检查是否在OCRAM中执行
-    mov r9, lr
-    bl  is_in_ocram
-    cmp r0, #1
-    beq 1f
-    @ 不在OCRAM中,跳转到基杼地址执行
+/ * arch/arm/cpu/armv7/lowlevel_init.S * /
+ENTRY(lowlevel_init)
     ldr sp, =CONFIG_SYS_INIT_SP_ADDR
-    push {r0-r3, r12, lr}
-1:
-    ldr sp, =CONFIG_SYS_INIT_SP_ADDR
-    push {lr}
-    bl  board_early_init_f     @ 板级早期初始化
-    pop {pc}
+    bic sp, sp, #7              @ 8 字节对齐
+#ifdef CONFIG_SPL_BUILD
+    ldr r9, =gdata
+#else
+    sub sp, sp, #GD_SIZE        @ 为 gd 预留空间(GD_SIZE=248)
+    bic sp, sp, #7
+    mov r9, sp                  @ r9 保存 gd 指针
+#endif
+    push {ip, lr}
+    bl  s_init                  @ 早期初始化(I.MX6ULL 为空函数)
+    pop {ip, pc}
+ENDPROC(lowlevel_init)
 ```
 
+`CONFIG_SYS_INIT_SP_ADDR = CONFIG_SYS_INIT_RAM_ADDR + (CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE) = 0x00900000 + (0x20000 - 256) = 0x0091FF00`,再减去 `GD_SIZE`(248) 后为 `0x0091FE08`。
+
 **CPSR模式位对照表:**
 
 | 位置  | 模式 | 值    | 说明                  |
@@ -518,119 +582,248 @@ lowlevel_init:
 ### 4.4 _main - C运行时初始化
 
 ```armasm
-_main:
-    ldr sp, =CONFIG_SYS_INIT_SP_ADDR
-    bl  board_init_f_alloc_reserve  @ 预留GD和栈空间
-    mov sp, r0                      @ sp指向栈顶
-    bl  board_init_f                @ 初始化全局数据结构
-    @ relocate_code重定位完成后返回
-    bl  board_init_r                @ 主阶段初始化
+/ * arch/arm/lib/crt0.S * /
+ENTRY(_main)
+    ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)   @ sp = 0x0091FF00
+    bic sp, sp, #7                        @ 8 字节对齐
+    mov r0, sp
+    bl  board_init_f_alloc_reserve        @ 预留 malloc 与 gd 区域,返回新 top
+    mov sp, r0
+    mov r9, r0                            @ r9 = gd 指针
+    bl  board_init_f_init_reserve         @ 清零 gd 区域
+    mov r0, #0
+    bl  board_init_f                      @ 重定位前初始化
+
+    ldr sp, [r9, #GD_START_ADDR_SP]       @ sp = gd->start_addr_sp
+    bic sp, sp, #7
+    ldr r9, [r9, #GD_BD]                  @ r9 = gd->bd
+    sub r9, r9, #GD_SIZE                  @ 新 gd 位于 bd 之下
+    adr lr, here
+    ldr r0, [r9, #GD_RELOC_OFF]           @ r0 = gd->reloc_off
+    add lr, lr, r0
+    ldr r0, [r9, #GD_RELOCADDR]           @ r0 = gd->relocaddr
+    b   relocate_code                     @ 代码重定位
+here:
+    bl  relocate_vectors                  @ 向量表重定位
+    bl  c_runtime_cpu_setup
+    ldr r0, =__bss_start
+    ...                                   @ 清零 BSS(memset 或 clbss_l 循环)
+    mov r0, r9                            @ r0 = gd_t
+    ldr r1, [r9, #GD_RELOCADDR]           @ r1 = dest_addr
+    ldr pc, =board_init_r                 @ 跳转 board_init_r(不返回)
+ENDPROC(_main)
 ```
 
-**board_init_f_alloc_reserve 内存布局:**
-
-```text
-CONFIG_SYS_INIT_SP_ADDR:
-+---------------------+ <- sp初始值 (0x0091FF00)
-|                     |
-|    栈空间        |
-|    (512B)           |
-+---------------------+
-|    256B GD结构     |
-| (global_data_t)     |
-+---------------------+ <- sp更新后
-|    512B GD->malloc   |
-|    缓冲区         |
-+---------------------+
-```
+`board_init_f_alloc_reserve`自栈顶向下预留早期 malloc 区(`CONFIG_SYS_MALLOC_F_LEN`)与 gd 区并返回新的栈顶,`board_init_f_init_reserve`再把该区域清零并把 gd 指针存入 `r9`。
 
 ### 4.5 board_init_f - 全局数据初始化
 
 ```c
+/* common/board_f.c */
 void board_init_f(ulong boot_flags)
 {
-    gd_t *id;
-    gd = (gd_t *)_text;  // 设置全局数据指针
-
-    // 清零GD结构
-    memset(gd, 0, sizeof(gd_t));
-
-    // 执行编程时初始化回调
-    init_sequence_f[] = {
-        setup_mon_len,           // 设置监控器长度
-        init_fnc_t,             // 初始化函数表
-        console_init_f,         // 控制台早期初始化
-        serial_init,            // 串口初始化
-        dram_init,              // DRAM初始化
-        NULL
-    };
-
-    // 计算重定位后地址
-    fdt_register_boot_chart(gd->fdt_blob);
+    gd_t data;
+
+    gd = &data;
+    gd->flags = boot_flags;
+    gd->have_console = 0;
+
+    /* 通过 initcall_run_list 依次执行 init_sequence_f[] */
+    if (initcall_run_list(init_sequence_f))
+        hang();
 }
 ```
 
+`init_sequence_f[]`(去掉条件编译后的主要函数,按执行顺序):
+
+```c
+static init_fnc_t init_sequence_f[] = {
+    setup_mon_len,        /* gd->mon_len = __bss_end - _start */
+    initf_malloc,         /* gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN = 0x400 */
+    initf_console_record, /* I.MX6ULL 未定义 CONFIG_CONSOLE_RECORD,直接返回 */
+    arch_cpu_init,
+    initf_dm,
+    arch_cpu_init_dm,     /* 未实现 */
+    mark_bootstage,
+    board_early_init_f,   /* 板级早期初始化(I.MX6ULL 初始化串口 IO) */
+    timer_init,           /* 初始化 Cortex-A 内核定时器 */
+    board_postclk_init,   /* I.MX6ULL:设置 VDDSOC 电压 */
+    get_clocks,           /* I.MX6ULL:获取 sdhc_clk */
+    env_init,             /* 设置 gd->env_addr */
+    init_baud_rate,       /* 根据 baudrate 环境变量设置 gd->baudrate */
+    serial_init,
+    console_init_f,       /* gd->have_console = 1 */
+    display_options,
+    display_text_info,
+    print_cpuinfo,
+    show_board_info,
+    INIT_FUNC_WATCHDOG_INIT,
+    INIT_FUNC_WATCHDOG_RESET,
+    init_func_i2c,
+    announce_dram_init,   /* 输出 "DRAM:" */
+    dram_init,            /* 设置 gd->ram_size,EMMC 版为 512MB */
+    post_init_f,
+    INIT_FUNC_WATCHDOG_RESET,
+    testdram,             /* 空函数 */
+    INIT_FUNC_WATCHDOG_RESET,
+    INIT_FUNC_WATCHDOG_RESET,
+    setup_dest_addr,      /* gd->ram_size/ram_top/relocaddr */
+    reserve_round_4k,
+    reserve_mmu,          /* TLB 表,64K 对齐 */
+    reserve_trace,
+    reserve_uboot,        /* 预留 U-Boot 空间(gd->mon_len),4K 对齐 */
+    reserve_malloc,       /* TOTAL_MALLOC_LEN = 16MB + 8KB */
+    reserve_board,        /* bd_t 大小 80B */
+    setup_machine,        /* I.MX6ULL 走设备树,此函数无效 */
+    reserve_global_data,  /* gd_t 大小 248B */
+    reserve_fdt,          /* I.MX6ULL 未用到 */
+    reserve_arch,         /* 空函数 */
+    reserve_stacks,       /* 预留栈空间,16 字节对齐 */
+    setup_dram_config,    /* 设置 gd->bd->bi_dram[0].start/size */
+    show_dram_config,
+    display_new_sp,
+    INIT_FUNC_WATCHDOG_RESET,
+    reloc_fdt,            /* 未用到 */
+    setup_reloc,          /* 拷贝 gd 到 gd->new_gd */
+    NULL,
+};
+```
+
+执行完毕后关键值(示例):`gd->ram_size=0x20000000`、`gd->ram_top=0xA0000000`、`gd->relocaddr=0x9FF47000`、`gd->start_addr_sp=0x9EF44E90`、`gd->reloc_off=0x18747000`、`gd->new_gd=0x9EF44EB8`。
+
 ### 4.6 board_init_r - 主阶段初始化
 
 ```c
-void board_init_r(gd_t *gd, ulong dest_addr)
+/* common/board_r.c */
+void board_init_r(gd_t *new_gd, ulong dest_addr)
 {
-    // 重定位后的初始化
-    // 初始化回调函数表
-    initr_serial,          // 串口初始化
-    initr_console,         // 控制台初始化
-    initr_board,           // 板级外设初始化
-    initr_ide,             // IDE初始化
-    initr_nand,            // NAND初始化
-    initr_mmc,             // MMC初始化
-    initr_net,             // 网络初始化
-    initr_env,             // 环境变量初始化
-    initr_std,             // 标准IO初始化
-    initr_boot,            // 启动脚本初始化
-    NULL
+#ifdef CONFIG_NEEDS_MANUAL_RELOC
+    for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
+        init_sequence_r[i] += gd->reloc_off;
+#endif
+    if (initcall_run_list(init_sequence_r))
+        hang();
+    /* NOTREACHED - run_main_loop() does not return */
+    hang();
 }
 ```
 
+`init_sequence_r[]`(去掉条件编译后的主要函数):
+
+```c
+init_fnc_t init_sequence_r[] = {
+    initr_trace,
+    initr_reloc,               /* 设置 gd->flags,标记重定位完成 */
+    initr_caches,              /* 使能 cache */
+    initr_reloc_global_data,
+    initr_barrier,             /* I.MX6ULL 未用到 */
+    initr_malloc,
+    initr_console_record,      /* 空函数 */
+    bootstage_relocate,
+    initr_bootstage,
+    board_init,                /* 板级初始化:I2C/FEC/USB/QSPI 等 */
+    stdio_init_tables,
+    initr_serial,
+    initr_announce,
+    INIT_FUNC_WATCHDOG_RESET,
+    INIT_FUNC_WATCHDOG_RESET,
+    INIT_FUNC_WATCHDOG_RESET,
+    power_init_board,          /* 正点原子板未用到 */
+    initr_flash,               /* 定义了 CONFIG_SYS_NO_FLASH,无效 */
+    INIT_FUNC_WATCHDOG_RESET,
+    initr_nand,                /* NAND 版本核心板初始化 NAND */
+    initr_mmc,                 /* EMMC 版本核心板初始化 EMMC */
+    initr_env,                 /* 初始化环境变量 */
+    INIT_FUNC_WATCHDOG_RESET,
+    initr_secondary_cpu,       /* 单核,未用 */
+    INIT_FUNC_WATCHDOG_RESET,
+    stdio_add_devices,         /* 初始化 LCD 等设备 */
+    initr_jumptable,
+    console_init_r,            /* 控制台初始化 */
+    INIT_FUNC_WATCHDOG_RESET,
+    interrupt_init,
+    initr_enable_interrupts,
+    initr_ethaddr,             /* 读取环境变量 ethaddr 作为 MAC */
+    board_late_init,           /* 定义在 mx6ull_alientek_emmc.c,切换 MMC 设备 */
+    INIT_FUNC_WATCHDOG_RESET,
+    INIT_FUNC_WATCHDOG_RESET,
+    INIT_FUNC_WATCHDOG_RESET,
+    initr_net,                 /* initr_net -> eth_initialize -> board_eth_init */
+    INIT_FUNC_WATCHDOG_RESET,
+    run_main_loop,             /* 进入主循环 */
+};
+```
+
 ### 4.7 main_loop - 主循环
 
 ```c
+/* common/board_r.c */
+static int run_main_loop(void)
+{
+    /* main_loop() can return to retry autoboot, if so just run it again */
+    for (;;)
+        main_loop();
+    return 0;
+}
+```
+
+```c
+/* common/main.c */
 void main_loop(void)
 {
-    // 处理环境变量
-    process_fdt_options();
+    const char *s;
 
-    // 执行启动命令
-    if (bootdelay >= 0) {
-        autoboot_command();  // 执行bootcmd
-    }
+    bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
+
+#ifdef CONFIG_VERSION_VARIABLE
+    setenv("ver", version_string);   /* 设置 version 环境变量 */
+#endif
 
-    // 启动脚本
-    run_command_list();
+    cli_init();                      /* 初始化 hush shell 相关变量 */
+    run_preboot_environment_command(); /* 执行 preboot 环境变量(一般不用) */
 
-    // 进入交互式命令行
-    cli_loop();
+    s = bootdelay_process();         /* 读取 bootdelay 和 bootcmd,返回 bootcmd */
+    if (cli_process_fdt(&s))
+        cli_secure_boot_cmd(s);
+
+    autoboot_command(s);             /* 倒计时;未打断则运行 bootcmd */
+
+    cli_loop();                      /* 进入命令行 */
 }
 ```
 
+`autoboot_command`核心:`stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)`三条都成立时执行`run_command_list(s, -1, 0)`;否则(按键打断)走`cli_loop()`。
+
 ### 4.8 boot_jump_linux - 跳转内核
 
 ```c
-void boot_jump_linux(bootm_headers_t *images)
+/* arch/arm/lib/bootm.c */
+static void boot_jump_linux(bootm_headers_t *images, int flag)
 {
-    // 获取内核入口函数指针
-    void (*kernel_entry)(int zero, int machid, void *fdt);
-    kernel_entry = (void (*)(int, int, void *))images->ep;
+    unsigned long machid = gd->bd->bi_arch_number;   /* 机器 ID */
+    const char *s;
+    void (*kernel_entry)(int zero, int arch, uint params);
+    unsigned long r2;
+
+    kernel_entry = (void (*)(int, int, uint))images->ep;  /* 内核入口 = images->ep */
 
-    // 打印启动信息
-    printf("Starting kernel ...\n");
+    s = getenv("machid");
+    if (s) {
+        if (strict_strtoul(s, 16, &machid) < 0)
+            return;
+    }
+
+    if (images->ft_len)
+        r2 = (unsigned long)images->ft_addr;   /* 使用设备树:r2 = DTB 地址 */
+    else
+        r2 = gd->bd->bi_boot_params;           /* 不使用设备树:r2 = 参数地址 */
 
-    // 调用内核入口函数
-    kernel_entry(0, machid, r2);
-    // r2指向设备树DTB
-    // machid = MACH_TYPE_MX6ULL
+    kernel_entry(0, machid, r2);               /* 进入 Linux 内核,一去不复返 */
 }
 ```
 
+> 使用设备树后,内核靠 DTB 的 "compatible" 属性匹配机器,`machid` 已不再关键;I.MX6U 实际就使用设备树方式启动。
+
 **调用关系图:**
 
 ```mermaid
@@ -653,33 +846,34 @@ flowchart TD
 ### 4.9 bootz_setup - 内核启动准备
 
 ```c
-int bootz_setup(ulong image, ulong *initrd_start, ulong *initrd_end,
-                ulong *fdt_start)
+/* arch/arm/lib/bootm.c */
+#define LINUX_ARM_ZIMAGE_MAGIC  0x016f2818
+
+int bootz_setup(ulong image, ulong *start, ulong *end)
 {
-    struct zimage_headers *hdr = (struct zimage_headers *)image;
+    struct zimage_header *zi;
 
-    // 验证内核魔数
-    if (hdr->code0 != ZIMAGE_MAGIC) {
-        printf("Bad zImage magic number\n");
-        return -1;
+    zi = (struct zimage_header *)map_sysmem(image, 0);
+    if (zi->zi_magic != LINUX_ARM_ZIMAGE_MAGIC) {
+        puts("Bad Linux ARM zImage magic!\n");
+        return 1;
     }
 
-    // 获取内核起始地址
-    *initrd_start = hdr->start;
+    *start = zi->zi_start;
+    *end = zi->zi_end;
 
-    // 获取DTB地址
-    *fdt_start = hdr->start + hdr->offset;
+    printf("Kernel image @ %#08lx [ %#08lx - %#08lx ]\n",
+           image, *start, *end);
     return 0;
 }
 ```
 
 **关键魔数:**
 
-| 魔数             | 值         | 说明             |
-| ---------------- | ---------- | ---------------- |
-| ZIMAGE_MAGIC     | 0x016f2818 | zImage标识       |
-| MACH_TYPE_MX6ULL | 0xFFFFFFFF | i.MX6ULL机器类型 |
-| DTB_MAGIC        | 0xd00dfeed | 设备树魔数       |
+| 魔数                     | 值         | 说明               |
+| ------------------------ | ---------- | ------------------ |
+| `LINUX_ARM_ZIMAGE_MAGIC` | 0x016f2818 | ARM Linux zImage 标识 |
+| `ZIMAGE_MAGIC`(旧书稿) | 0x016f2818 | 同一魔数,命名不同 |
 
 ---
 
@@ -741,28 +935,34 @@ make mrproper # 彻底清理(含.config)
 
 ### 5.5 烧录流程
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(书中第三十章使用`imxdownload`烧写`u-boot.bin`;此处为通用 dd 方式)。
+
+`u-boot.imx`需烧写到 SD 卡偏移 **1024 字节**处(即跳过前 2 个 512 字节扇区):
+
 ```mermaid
 flowchart LR
     A[获取u-boot.imx] --> B[插入SD卡]
-    B --> C[sudo dd if=u-boot.imx of=/dev/sdb bs=1M seek=2]
-    C --> D[确认录完成]
+    B --> C[sudo dd if=u-boot.imx of=/dev/sdb bs=512 seek=2 conv=fsync]
+    C --> D[确认录完成]
     D --> E[插入开发板启动]
 ```
 
 **dd命令参数说明:**
 
-| 参数            | 说明         |
-| --------------- | ------------ |
-| `if=u-boot.imx` | 输入文件     |
-| `of=/dev/sdb`   | 输出设备     |
-| `bs=1M`         | 块大小       |
-| `seek=2`        | 跳过2个块    |
-| `conv=notrunc`  | 不截断原文件 |
+| 参数            | 说明                                   |
+| --------------- | -------------------------------------- |
+| `if=u-boot.imx` | 输入文件                               |
+| `of=/dev/sdb`   | 输出设备(切勿写系统盘)               |
+| `bs=512`        | 块大小 = 一个扇区                      |
+| `seek=2`        | 跳过 2 个扇区 = 1024 字节偏移           |
+| `conv=fsync`    | 同步写入,确保数据落盘(可选 notrunc) |
 
 ---
 
 ## 六、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 6.1 i.MX6ULL vs STM32MP1 vs RK3568
 
 | 对比项             | i.MX6ULL    | STM32MP1            | RK3568      |
@@ -801,14 +1001,13 @@ flowchart LR
 
 ## 七、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(答案基于书中事实整理)。
+
 ### Q1: 为什么U-Boot加载地址是0x87800000?
 
-> 这是基于i.MX6ULL SoC的设计约束:
+> `0x87800000`是 NXP 为 i.MX6 系列定义的`CONFIG_SYS_TEXT_BASE`(写在板级头文件 mx6ullevk.h / mx6ull_alientek_emmc.h 中),编译时通过`-Ttext 0x87800000`链接,也是上电后 Boot ROM 把 U-Boot 加载到 DDR 的位置。正点原子裸机例程的链接起始地址同样选`0x87800000`,目的是与 U-Boot 保持一致。
 >
-> 1. OCRAM范围是0x00900000-0x00920000(128KB),只能作为启动缓存
-> 2. DRAM起始于0x80000000,需要留出空间给内核和设备树
-> 3. 0x87800000位于DRAM中间位置,既不会冲突内核(0x80800000),
->    也不会冲突设备树(0x83000000)
+> 注意:这是 U-Boot 的**初始加载/链接地址**;重定位后 U-Boot 实际运行在 DDR 高地址(示例为`0x9FF47000`)。
 
 ### Q2: U-Boot重定位的作用是什么?为什么要重定位?
 
@@ -827,21 +1026,24 @@ flowchart LR
 ### Q3: CONFIG_SYS_INIT_SP_ADDR是如何计算的?
 
 > ```text
-> OCRAM结束地址 = 0x00900000 + 128KB = 0x00920000
-> GD大小 = 248字节(对齐后256字节)
-> 留出栈空间 = 512字节
-> SP = 0x00920000 - 256 - 512 = 0x0091FF00
+> CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000
+> CONFIG_SYS_INIT_RAM_SIZE = IRAM_SIZE = 0x00020000 (128KB)
+> GENERATED_GBL_DATA_SIZE = (sizeof(struct global_data) + 15) & ~15 = 256
+> CONFIG_SYS_INIT_SP_OFFSET = 0x20000 - 256 = 0x1FF00
+> CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0x1FF00 = 0x0091FF00
 > ```
+>
+> 随后在`lowlevel_init`中执行`sub sp, sp, #GD_SIZE`(GD_SIZE=248)并 8 字节对齐,得到`0x0091FE08`。
 
 ### Q4: U-Boot如何传递参数给Linux内核?
 
 > 1. **r0 = 0**: 未使用
-> 2. **r1 = machid**: 机器类型ID,如MACH_TYPE_MX6ULL
-> 3. **r2 = dtb地址**: 设备树DTB在DRAM中的地址
-> 4. **bootargs**: 通过bootargs环境变量传递
->    - console=ttyS0,115200 串控台配置
->    - root=/dev/mmcblk0p2 根文件系统
->    - rdinit=/sbin/init 初始化程序
+> 2. **r1 = machid**: 机器类型ID(`gd->bd->bi_arch_number`);使用设备树后内核靠 DTB 的 compatible 匹配,machid 已不再关键
+> 3. **r2 = dtb地址**: 使用设备树时为`images->ft_addr`(DTB  DRAM 中的地址),否则为`gd->bd->bi_boot_params`
+> 4. **bootargs**: 通过 bootargs 环境变量传递,I.MX6ULL 上典型值:
+>    - `console=ttymxc0,115200` 串口控制台配置
+>    - `root=/dev/mmcblk1p2` 根文件系统分区
+>    - `rootwait`、`rw`、`rootfstype=ext4` 等
 
 ### Q5: 如何分析U-Boot启动失败?
 
@@ -861,3 +1063,7 @@ flowchart LR
 > - U-Boot官方文档: https://docs.u-boot.org/
 > - 正点原子STM32MP157开发板教程
 > - 正点原子电子网: https://www.alientek.com/
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第31章 U-Boot顶层Makefile详解、第32章 U-Boot启动流程详解

+ 58 - 12
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/03-Linux内核裁剪与编译.md

@@ -24,8 +24,8 @@ pdf_ref:
 
 | 来源         | 版本 / 文件                                 | 说明                            |
 | ------------ | ------------------------------------------- | ------------------------------- |
-| NXP 官方原版 | `linux-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2` | NXP 移植到 i.MX 系列后的 BSP    |
-| 正点原子出厂 | `linux-imx-4.1.15-2.1.0-g8a006db.tar.bz2`   | 已适配 I.MX6U-ALPHA,含编译脚本 |
+| NXP 官方原版 | `linux-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2` | NXP 移植到 i.MX 系列后的 BSP,解压出目录`linux-imx-rel_imx_4.1.15_2.1.0_ga` |
+| 移植用目录   | `linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek` | 用`mv`重命名,避免与 NXP 官方目录混淆(书中做法) |
 
 为什么用 **4.1.15**:NXP 对 i.MX6ULL 的 BSP 长期维护在这个版本,外设驱动、设备树最完整;4.x 与 5.x 在核心机制上没有本质区别,5.x 更多是新增平台和外设。中途换版本意味着设备树、驱动全部重调,得不偿失。
 
@@ -492,14 +492,14 @@ flowchart LR
 
 ### 5.1 清理命令
 
-| 命令             | 清理范围                                   | 是否删 `.config`        |
-| ---------------- | ------------------------------------------ | ----------------------- |
-| `make clean`     | 大部分编译产物(`.o`、`built-in.o`、镜像) | **会删**(4.1.15 行为) |
-| `make mrproper`  | 所有编译产物 + 配置文件 + 备份             | 会删                    |
-| `make distclean` | `mrproper` + 编辑器备份、补丁文件          | 会删                    |
-| `make defconfig` | 用架构默认配置重新生成 `.config`           | 覆盖                    |
+| 命令             | 清理范围                                   | 是否删 `.config` |
+| ---------------- | ------------------------------------------ | ---------------- |
+| `make clean`     | 大部分编译产物(`.o`、`built-in.o`、镜像) | **不删**         |
+| `make mrproper`  | 所有编译产物 + 配置文件 + 备份             | 会删             |
+| `make distclean` | `mrproper` + 编辑器备份、补丁文件          | 会删             |
+| `make defconfig` | 用架构默认配置重新生成 `.config`           | 覆盖             |
 
-> `distclean` 最彻底。正点原子的编译脚本每次开头都执行 `distclean`,所以**图形界面配置完一定要先 `savedefconfig` 固化**,否则脚本一跑,配置全没。
+> `distclean` 最彻底。正点原子的编译脚本每次开头都执行 `distclean`,所以**图形界面配置完一定要先保存/固化配置**,否则脚本一跑,配置全没。
 
 ### 5.2 默认配置
 
@@ -580,7 +580,7 @@ Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
 | `recipe for target 'arch/arm/boot/compressed/piggy.lzo' failed` | 未安装 lzop                | `sudo apt-get install lzop`       |
 | `arm-linux-gnueabihf-gcc: command not found`                    | 交叉工具链未安装/未加 PATH | 安装工具链并配置环境变量          |
 | 修改配置后配置项“复活”                                          | 直接删了 `.config` 的行    | 改成 `# CONFIG_xxx is not set`    |
-| `make clean` 后配置丢失                                         | `clean` 会删 `.config`     | 先 `savedefconfig` 固化           |
+| `make mrproper`/`distclean` 后配置丢失                          | 这两个目标会删 `.config`   | 改完配置先保存(`savedefconfig` 固化) |
 | 驱动模块加载失败 / `insmod` 报版本不符                          | 内核与模块配置不一致       | 用同一份 `.config` 重编内核与模块 |
 | 编译到一半卡死或内存不足                                        | `-j` 过大                  | 降低并行度,如 `-j4`              |
 
@@ -675,7 +675,47 @@ dtc -I dtb -O dts -o out.dts imx6ull-alientek-emmc.dtb
 
 ---
 
-## 七、跨平台对比
+## 七、内核移植流程(NXP BSP → 正点原子板)
+
+参照第 37 章,把半导体厂商(NXP)的 BSP 移植到自研板,完整步骤如下:
+
+1. **用官方参考板内核先跑一遍**:`make imx_v7_mfg_defconfig`(或 `imx_v7_defconfig`)后 `make`,得到 `zImage` 与 `arch/arm/boot/dts/imx6ull-14x14-evk.dtb`;拷到 tftpboot 后
+   ```bash
+   tftp 80800000 zImage
+   tftp 83000000 imx6ull-14x14-evk.dtb
+   bootz 80800000 - 83000000
+   ```
+   在自研板上验证能否启动。
+2. **新建板级 defconfig**:
+   ```bash
+   cd arch/arm/configs
+   cp imx_v7_mfg_defconfig imx_alientek_emmc_defconfig   # 复制官方 EVK 配置
+   # 删除 CONFIG_ARCH_MULTI_V6=y(I.MX6ULL 用不到 V6)
+   ```
+   之后用 `make imx_alientek_emmc_defconfig` 配置内核。
+3. **新建设备树**:
+   ```bash
+   cd arch/arm/boot/dts
+   cp imx6ull-14x14-evk.dts imx6ull-alientek-emmc.dts   # 以 EVK 为模板
+   # 编辑 Makefile,在 dtb-$(CONFIG_SOC_IMX6ULL) 中加入 imx6ull-alientek-emmc.dtb
+   make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
+   ```
+4. **编译脚本** `imx6ull_alientek_emmc.sh`(distclean → imx_alientek_emmc_defconfig → menuconfig → all -j16)。
+5. **修改差异外设**:主频(改 `arch/arm/boot/dts/imx6ull.dtsi` 的超频节点)、EMMC、网络等。
+6. **网络驱动(重点)**:
+   - 改设备树里 ENET 的复位/时钟引脚(`iomuxc_snvs` 节点新增 `pinctrl_enet1_reset`/`pinctrl_enet2_reset`),并修改 `fec1`/`fec2` 的 `pinctrl-0`;
+   - 改 PHY 地址:V2.4 以前底板 LAN8720A(ENET1=0x0,ENET2=0x1),V2.4 及以后底板 SR8201F(ENET1=0x2,ENET2=0x1);
+   - 加 `phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>` / `<&gpio5 8 GPIO_ACTIVE_LOW>`、`phy-reset-duration = <200>`;
+   - 改 `drivers/net/ethernet/freescale/fec_main.c`(LAN8720A 在 `fec_probe`、SR8201F 在 `fec_reset_phy` 中复位 PHY);
+   - 改 `drivers/net/phy/smsc.c` 的 `smsc_phy_reset`(每次调用都软复位 LAN8720A);
+   - `make menuconfig` → Device Drivers → Network device support → PHY Device support → Drivers for SMSC PHYs,使能 `CONFIG_SMSC_PHY=y`。
+7. **保存配置**:解决完问题后,把 `.config` 另存/覆盖到 `arch/arm/configs/imx_alientek_emmc_defconfig`(图形界面 `<Save>` 或直接 `cp`),使下次 `make imx_alientek_emmc_defconfig` 即生效。
+
+---
+
+## 八、跨平台对比
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
 
 三款常见嵌入式平台的内核差异,移植时思路相通、细节不同:
 
@@ -700,7 +740,9 @@ dtc -I dtb -O dts -o out.dts imx6ull-alientek-emmc.dtb
 
 ---
 
-## 八、面试精选
+## 九、面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(答案基于书中事实整理)。
 
 **Q1:`vmlinux`、`Image`、`zImage`、`uImage` 有什么区别?生成链是怎样的?**
 
@@ -729,3 +771,7 @@ dtc -I dtb -O dts -o out.dts imx6ull-alientek-emmc.dtb
 - `40_第三十五章 Linux内核顶层Makefile详解.pdf`
 - `42_第三十七章 Linux内核移植.pdf`
 - 关联:[[06-系统移植与启动/01-U-Boot命令与使用]] | [[06-系统移植与启动/02-U-Boot启动流程]] | [[06-系统移植与启动/04-根文件系统构建]]
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第35章 Linux内核顶层Makefile详解、第37章 Linux内核移植

+ 51 - 6
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/04-根文件系统构建.md

@@ -39,7 +39,7 @@ Linux 根文件系统采用标准的目录结构,每个目录有特定用途
 | `/sbin` | 系统命令     | 系统管理命令,如 ifconfig、reboot,通常只有管理员使用 |
 | `/lib`  | 共享库       | 系统库文件,供应用程序动态链接使用                    |
 | `/etc`  | 配置文件     | 系统配置文件,如 fstab、inittab、resolv.conf          |
-| `/dev`  | 设备文件     | 设备节点文件,如 /dev/ttyS0、/dev/null                |
+| `/dev`  | 设备文件     | 设备节点文件,如 /dev/ttymxc0(I.MX6ULL 串口0)、/dev/null |
 | `/proc` | 进程信息     | proc 文件系统挂载点,存储系统运行时信息               |
 | `/sys`  | 内核对象     | sysfs 文件系统挂载点,提供内核数据结构信息            |
 | `/usr`  | 用户程序     | Unix Software Resource,用户空间程序和库              |
@@ -305,6 +305,8 @@ mkdir -p dev/pts
 
 ### 3.2 创建设备节点
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。书中第三十八章通过`mdev -s`自动创建设备节点,并未手动`mknod`。
+
 Linux 系统需要基本的设备节点才能正常运行:
 
 ```bash
@@ -317,7 +319,7 @@ sudo mknod console c 5 1
 sudo mknod null c 1 3
 
 # 终端设备
-sudo mknod ttyS0 c 4 64
+sudo mknod ttymxc0 c 4 64
 
 # 随机数设备
 sudo mknod random c 1 8
@@ -336,6 +338,8 @@ cd ..
 
 ### 3.3 /etc 目录配置
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。书中第三十八章只创建了`/etc/init.d/rcS`、`/etc/fstab`、`/etc/inittab`和`/etc/resolv.conf`,未创建`/etc/passwd`、`/etc/group`、`/etc/profile`。
+
 `/etc` 目录包含系统配置文件,是根文件系统中最关键的目录之一。
 
 **创建 /etc/passwd 文件**:
@@ -375,7 +379,7 @@ EOF
 cat > etc/init.d/rcS << 'EOF'
 #!/bin/sh
 
-PATH=/sbin:/bin:/usr/sbin:/usr/bin
+PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
 LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
 export PATH LD_LIBRARY_PATH
 
@@ -485,10 +489,17 @@ cp *so* *.a /home/zuozhongkai/linux/nfs/rootfs/lib/ -d
 # 删除 rootfs/lib 中的符号链接
 rm ld-linux-armhf.so.3
 
-# 从工具链目录拷贝实际文件
+# 从工具链目录拷贝实际文件(注意:不带 -d,复制出的是实体文件)
 cp /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/ld-linux-armhf.so.3 /home/zuozhongkai/linux/nfs/rootfs/lib/
 ```
 
+再进入工具链的 `.../arm-linux-gnueabihf/lib` 目录,把里面的库也拷进 `rootfs/lib`:
+
+```bash
+cd /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/lib
+cp *so* *.a /home/zuozhongkai/linux/nfs/rootfs/lib/ -d
+```
+
 ### 4.3 拷贝 /usr/lib 库文件
 
 ```bash
@@ -517,6 +528,8 @@ du ./lib ./usr/lib/ -sh
 
 ### 5.1 一键构建脚本
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。书中第三十八章给出的是手动分步操作,未提供该一键脚本。
+
 ```bash
 #!/bin/bash
 # rootfs_build.sh - 一键构建根文件系统
@@ -556,7 +569,7 @@ echo "4. 创建设备节点..."
 cd dev
 sudo mknod console c 5 1
 sudo mknod null c 1 3
-sudo mknod ttyS0 c 4 64
+sudo mknod ttymxc0 c 4 64
 sudo mknod random c 1 8
 sudo mknod urandom c 1 9
 cd ..
@@ -564,6 +577,7 @@ cd ..
 # 5. 拷贝库文件
 echo "5. 拷贝库文件..."
 cp ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/libc/lib/*so* ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/libc/lib/*.a $ROOTFS_DIR/lib/ -d
+cp ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/lib/*so* ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/lib/*.a $ROOTFS_DIR/lib/ -d
 cp ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/libc/usr/lib/*so* ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/libc/usr/lib/*.a $ROOTFS_DIR/usr/lib/ -d
 
 # 修复 ld-linux-armhf.so.3
@@ -736,7 +750,32 @@ ifconfig eth0 192.168.1.251
 ping www.baidu.com
 ```
 
-### 6.4 常见问题排查
+### 6.4 开机自启动
+
+在`/etc/init.d/rcS`末尾加上自启动命令即可让程序随系统启动(书中 38.5.3):
+
+```bash
+#!/bin/sh
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
+runlevel=S
+umask 022
+export PATH LD_LIBRARY_PATH runlevel
+
+mount -a
+mkdir /dev/pts
+mount -t devpts devpts /dev/pts
+
+echo /sbin/mdev > /proc/sys/kernel/hotplug
+mdev -s
+
+# 开机自启动
+cd /drivers
+./hello &
+cd /
+```
+
+### 6.5 常见问题排查
 
 **问题 1:can't run '/etc/init.d/rcS': No such file or directory**
 
@@ -760,6 +799,8 @@ ping www.baidu.com
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32 vs RK3568 根文件系统差异
 
 | 特性             | I.MX6ULL             | STM32MP1                       | RK3568             |
@@ -792,6 +833,8 @@ ping www.baidu.com
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(答案基于书中事实整理)。
+
 ### 题目 1:什么是根文件系统?它与普通文件系统有什么区别?
 
 **考察点**:基础概念理解
@@ -909,4 +952,6 @@ sd[a-z]        0:0 0660  */bin/umount /mnt/sdcard
 
 ---
 
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第38章 根文件系统构建
+
 **最后更新**: 2026-09-17

+ 79 - 3
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/05-系统烧写与量产.md

@@ -16,6 +16,8 @@ pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第
 
 系统移植完成后,需要将 U-Boot、Linux Kernel、设备树(`.dtb`)和根文件系统(`rootfs`)四个文件烧写到板载 EMMC、NAND Flash 或 QSPI Flash 等存储设备上,使产品脱离网络独立运行。
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。第 39 章只讲解 **MfgTool + USB OTG** 一种烧写方式;SD 卡 dd、串口 loadb、网络烧写、uuu 等均为扩展内容。
+
 ```mermaid
 flowchart LR
     accTitle: Embedded Linux System Flashing Methods
@@ -45,6 +47,8 @@ flowchart LR
 
 ## 二、SD卡烧写
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。第 39 章未讲解 SD 卡烧写;书中 U-Boot 烧写到 SD 卡使用的是`imxdownload`(见第 30 章)。
+
 ### 2.1 硬件准备
 
 - 一张 MicroSD 卡(建议 8GB 以上 Class 10)
@@ -79,10 +83,10 @@ U-Boot 需要写入 SD 卡的特定偏移位置(IMX6ULL 为扇区 2,即 1KB
 
 ```bash
 # 将 U-Boot 写入 SD 卡(跳过前 1KB)
-sudo dd if=u-boot.imx of=/dev/sdb bs=1k seek=2 conv=fsync
+sudo dd if=u-boot.imx of=/dev/sdb bs=1k seek=1 conv=fsync
 
 # 验证写入
-sudo dd if=/dev/sdb bs=1k skip=2 count=100 2>/dev/null | xxd | head -5
+sudo dd if=/dev/sdb bs=1k skip=1 count=100 2>/dev/null | xxd | head -5
 ```
 
 ### 2.4 拷贝内核与设备树
@@ -146,6 +150,8 @@ ls /               # 确认根文件系统内容
 
 ## 三、串口烧写
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。第 39 章未讲解串口/Ymodem 烧写。
+
 ### 3.1 适用场景
 
 串口烧写主要用于紧急恢复场景——当 EMMC 中无有效系统、网络不可用、USB 也无法使用时,通过 UART 串口将固件传输到板载 DDR,再由 U-Boot 写入存储。
@@ -328,10 +334,72 @@ ifconfig eth0 192.168.1.251 netmask 255.255.255.0
 route add default gw 192.168.1.1
 ```
 
+### 4.8 ucl2.xml 结构与识别原理
+
+`ucl2.xml` 以`<UCL>`开始、`</UCL>`结束:`<CFG>...</CFG>`判断烧写哪款芯片,`<LIST name="...">`针对不同存储介质(SDCard/eMMC/Nor Flash/NAND Flash…)定义烧写命令序列。
+
+`<CFG>`通过 VID/PID 识别芯片(节选):
+
+```xml
+<CFG>
+  <STATE name="BootStrap" dev="MX6SL" vid="15A2" pid="0063"/>
+  <STATE name="BootStrap" dev="MX6UL" vid="15A2" pid="007D"/>
+  <STATE name="BootStrap" dev="MX6ULL" vid="15A2" pid="0080"/>
+  <STATE name="Updater"   dev="MSC"  vid="066F" pid="37FF"/>
+</CFG>
+```
+
+即 VID=0x15A2、PID=0x0080 时判定为 I.MX6ULL。
+
+`mfgtool2-yocto-mx-evk-emmc.vbs` 内容:
+
+```vbscript
+Set wshShell = CreateObject("WScript.shell")
+wshShell.run "mfgtool2.exe -c ""linux"" -l ""eMMC"" -s ""board=sabresd"" -s ""mmc=1"" -s ""6uluboot=14x14evk"" -s ""6uldtb=14x14-evk"""
+Set wshShell = Nothing
+```
+
+参数含义:`board=sabresd`、`mmc=1`、`6uluboot=14x14evk`、`6uldtb=14x14-evk`;文件名`%lite%`取`l`、`%6uluboot%`取`14x14evk`(在`cfg.ini`中定义),故`firmware/u-boot-imx6ul%lite%%6uluboot%_emmc.imx`展开为`u-boot-imx6ull14x14evk_emmc.imx`。第一阶段下载完 uboot/kernel/dtb 后输出`Jumping to OS image.`并跳转启动 Linux。
+
+### 4.9 改造 MfgTool 支持正点原子开发板
+
+书中 39.5 节从三方面改造:
+
+1. **确定系统文件命名**:如`u-boot-alientek-emmc.imx`、`zImage-alientek-emmc`、`imx6ull-alientek-emmc.dtb`、`rootfs-alientek-emmc.tar.bz2`。
+2. **新建 .vbs**:复制`mfgtool2-yocto-mx-evk-emmc.vbs`并重命名为`mfgtool2-alientek-alpha-emmc.vbs`(内容不改)。
+3. **修改 ucl2.xml**:备份原文件后只保留`<LIST name="eMMC">`,把文件路径改为上面的命名。烧写关键命令:用`dd ... of=/dev/mmcblk%mmc%boot0 bs=512 seek=2`写 U-Boot(偏移 1024 字节),`mmc bootpart enable 1 1`使能启动分区,`mkfs.vfat`建 FAT 分区放 zImage/dtb,`mkfs.ext3 -F -E nodiscard`建 rootfs 分区,`pipe tar -jxv -C /mnt/...`解压 rootfs。
+
+改造过后,`firmware/`放 3 个文件(uboot+zImage+dtb),`files/`放 4 个文件(再加 rootfs),双击新`.vbs`即可烧写。**注意**:一旦改造后的工具能正常烧写,`firmware/`目录下的文件以后不要再改,否则可能烧写失败。
+
+### 4.10 解决 “Starting kernel ...” 卡死
+
+现象:烧写自研系统后 U-Boot 输出`Starting kernel ...`便无任何信息。原因是 U-Boot 默认`findfdt`取`imx6ull-14x14-evk.dtb`,而 EMMC 分区 1 中的设备树名为`imx6ull-alientek-emmc.dtb`(`mmc dev 1`+`ls mmc 1:1`可确认)。两种解决:
+
+1. 在 U-Boot 中临时修改:
+
+```bash
+setenv bootcmd 'mmc dev 1;fatload mmc 1:1 80800000 zImage;fatload mmc 1:1 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000'
+setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
+saveenv
+```
+
+2. 永久修改 U-Boot 源码`include/configs/mx6ull_alientek_emmc.h`中`CONFIG_EXTRA_ENV_SETTINGS`的`findfdt`:
+
+```c
+"findfdt="\
+    "if test $fdt_file = undefined; then " \
+        "setenv fdt_file imx6ull-alientek-emmc.dtb; " \
+    "fi;\0" \
+```
+
+改完重新编译 U-Boot 并烧写即可。
+
 ---
 
 ## 五、量产方案
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(uuu、批量脚本、质量检查等书中未涉及)。
+
 ### 5.1 量产工具对比
 
 | 工具                               | 平台    | 速度       | 适用规模 | 特点                   |
@@ -378,7 +446,7 @@ sudo mkfs.vfat -F 32 ${SD_DEV}1
 sudo mkfs.ext4 ${SD_DEV}2
 
 echo "=== 烧写 U-Boot ==="
-sudo dd if=u-boot.imx of=${SD_DEV} bs=1k seek=2 conv=fsync
+sudo dd if=u-boot.imx of=${SD_DEV} bs=1k seek=1 conv=fsync
 
 echo "=== 拷贝内核与设备树 ==="
 sudo mount ${SD_DEV}1 /mnt/sdboot
@@ -461,6 +529,8 @@ echo "✅ 验证通过"
 
 ### 6.1 MfgTool 常见错误
 
+> ⚠️ **来源说明**:本小节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。6.2 节的“Starting kernel 卡死”与改`findfdt`属书中 39.5.3 内容。
+
 | 错误现象                     | 可能原因                   | 解决方案                                 |
 | ---------------------------- | -------------------------- | ---------------------------------------- |
 | 无法识别 HID 设备            | USB 线未连接或拨码开关错误 | 检查 USB 线连接;确认拨码开关在 USB 模式 |
@@ -524,6 +594,8 @@ fdt print /
 
 ## 七、跨平台对比
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
+
 ### 7.1 IMX6ULL vs STM32MP1 vs RK3568 烧写差异
 
 | 特性           | IMX6ULL       | STM32MP1            | RK3568                   |
@@ -564,6 +636,8 @@ flowchart LR
 
 ## 八、面试精选
 
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(答案基于书中事实整理)。
+
 ### 题目 1:嵌入式系统烧写的核心流程是什么?
 
 **考察点**: 系统烧写原理理解
@@ -640,4 +714,6 @@ setenv bootargs 'console=ttymxc0,115200 earlycon=imxuart,0x02020000,115200 ignor
 
 ---
 
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第39章 系统烧写
+
 **最后更新**: 2026-09-17

+ 68 - 52
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/06-系统移植与启动/面试-系统移植.md

@@ -9,6 +9,8 @@ updated: 2026-09-17
 
 本文档涵盖嵌入式Linux系统移植全流程的核心面试题,包括U-Boot、Linux内核、设备树、根文件系统、系统烧写和启动流程六大模块。
 
+> ⚠️ **来源说明**:本文档为面试题汇编,不属于《I.MX6U嵌入式Linux驱动开发指南》原文;答案基于指南第30、31、32、35、37、38、39章的事实整理。凡标注“扩展知识”的题目/小节,均为指南未涉及的内容。
+
 ---
 
 ## 一、U-Boot(5道面试题)
@@ -97,9 +99,10 @@ U-Boot常用命令:
 | `setenv`          | 设置环境变量        | `setenv bootargs console=ttymxc0,115200` |
 | `saveenv`         | 保存环境变量到Flash | `saveenv`                                |
 | `tftp`            | 通过TFTP下载文件    | `tftp 80800000 zImage`                   |
-| `nand read/write` | NAND Flash读写      | `nand read 80800000 kernel`              |
-| `mmc read/write`  | SD卡读写            | `mmc write 80800000 0x400 0x3000`        |
-| `bootm`           | 启动内核            | `bootm 80800000`                         |
+| `nand read/write` | NAND Flash读写      | `nand read 80800000 0x620000 0x800000`   |
+| `mmc read/write`  | SD卡读写            | `mmc write 80800000 2 0x2E6`             |
+| `bootz`           | 启动 zImage 内核    | `bootz 80800000 - 83000000`              |
+| `bootm`           | 启动 uImage 内核    | `bootm 80800000`                         |
 
 **下载和烧写内核的完整流程**:
 
@@ -112,7 +115,7 @@ setenv serverip 192.168.1.101
 tftp 80800000 zImage
 
 # 3. 通过TFTP下载设备树
-tftp 83000000 imx6ull-alientek.dtb
+tftp 83000000 imx6ull-alientek-emmc.dtb
 
 # 4. 烧写内核到SD卡(EMMC)
 mmc write 80800000 0x400 0x3000
@@ -195,7 +198,7 @@ U-Boot环境变量是系统配置的重要组成部分:
 => printenv
 
 bootargs=root=/dev/mmcblk1p2 rootwait rw console=ttymxc0,115200
-bootcmd=tftp 80800000 zImage;tftp 83000000 imx6ull-alientek.dtb;bootm 80800000 - 83000000
+bootcmd=tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000
 ipaddr=192.168.1.100
 serverip=192.168.1.101
 ```
@@ -217,12 +220,12 @@ setenv bootargs "console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw"
 
 ```bash
 # bootcmd是自动启动时执行的命令序列
-setenv bootcmd "tftp 80800000 zImage;tftp 83000000 imx6ull-alientek.dtb;bootm 80800000 - 83000000"
+setenv bootcmd "tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000"
 
 # 命令说明:
 # tftp 80800000 zImage        → 下载内核到DDR 0x80800000地址
-# tftp 83000000 imx6ull-alientek.dtb → 下载设备树
-# bootm 80800000 - 83000000   → 启动内核(内核地址 - ramdisk地址 - 设备树地址)
+# tftp 83000000 imx6ull-alientek-emmc.dtb → 下载设备树
+# bootz 80800000 - 83000000   → 启动内核(内核地址 - ramdisk地址 - 设备树地址)
 ```
 
 **追问**:
@@ -285,6 +288,8 @@ drivers/pinctrl/pinctrl-imx6ull.c
 
 ### Q2: 如何编译Linux内核?常用的编译命令有哪些?
 
+> ⚠️ **来源说明**:本题部分内容(`gconfig`、`modules_install` 等)不属于《I.MX6U嵌入式Linux驱动开发指南》第35/37章内容,为扩展知识。
+
 **答案要点**:
 
 1. 配置:`make menuconfig`或使用默认配置
@@ -368,32 +373,29 @@ I.MX6U-ALPHA开发板Linux内核移植步骤:
 **1. 搭建交叉编译环境**:
 
 ```bash
-# 安装ARM交叉编译工具链
-sudo apt-get install gcc-arm-linux-gnueabihf
-
-# 或使用Linaro工具链
-wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
-tar -xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
-export PATH=$PATH:/path/to/gcc-linaro/bin
+# 使用指南配套的 Linaro 工具链
+# gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf,解压到 /usr/local/arm/
+tar -xf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz -C /usr/local/arm
+export PATH=$PATH:/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin
 ```
 
 **2. 获取NXP官方内核**:
 
 ```bash
-# 下载NXP官方Linux源码
-wget https://www.nxp.com/docs/en/supporting-information/linux/linux-imx-4.1.15-2.1.0-ga.tar.gz
-tar -xf linux-imx-4.1.15-2.1.0-ga.tar.gz
-cd linux-imx-4.1.15-2.1.0-ga
+# 使用光盘提供的 NXP 原版内核
+tar -vxjf linux-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2
+# 解压目录名可能与官方混淆,可重命名
+mv linux-imx-rel_imx_4.1.15_2.1.0_ga linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
 ```
 
 **3. 修改设备树**:
 
 ```bash
 # 复制EVK设备树作为基础
-cp arch/arm/boot/dts/imx6ull-14x14-evk.dts arch/arm/boot/dts/imx6ull-alientek.dts
+cp arch/arm/boot/dts/imx6ull-14x14-evk.dts arch/arm/boot/dts/imx6ull-alientek-emmc.dts
 
 # 修改设备树,适配ALPHA开发板硬件
-vi arch/arm/boot/dts/imx6ull-alientek.dts
+vi arch/arm/boot/dts/imx6ull-alientek-emmc.dts
 ```
 
 **4. 修改内核配置**:
@@ -988,7 +990,7 @@ of_irq_to_resource(np, index, &res);
 /lib/libc.so.6, /lib/libpthread.so.0
 
 # 设备文件
-/dev/null, /dev/ttyS0, /dev/mmcblk0
+/dev/null, /dev/ttymxc0, /dev/mmcblk0
 
 # 进程信息
 /proc/cpuinfo, /proc/meminfo
@@ -1021,12 +1023,13 @@ wget https://busybox.net/downloads/busybox-1.29.0.tar.bz2
 tar -xf busybox-1.29.0.tar.bz2
 cd busybox-1.29.0
 
-# 配置BusyBox
-make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
+# BusyBox 的 ARCH/CROSS_COMPILE 直接写在顶层 Makefile 中,无需在命令行传入
+make defconfig
+make menuconfig          # 配置静态/动态、mdev、Unicode 等
 
 # 编译安装
-make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
-make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- CONFIG_PREFIX=/home/user/rootfs install
+make
+make install CONFIG_PREFIX=/home/user/rootfs
 ```
 
 **2. 创建目录结构**:
@@ -1038,7 +1041,7 @@ mkdir -p /home/user/rootfs/{bin,sbin,etc,lib,usr,var,tmp,dev,proc,sys,mnt,opt,ho
 # 创建设备文件节点
 mknod /home/user/rootfs/dev/null c 1 3
 mknod /home/user/rootfs/dev/console c 5 1
-mknod /home/user/rootfs/dev/ttyS0 c 4 64
+mknod /home/user/rootfs/dev/ttymxc0 c 4 64
 ```
 
 **3. 添加共享库**:
@@ -1057,7 +1060,7 @@ arm-linux-gnueabihf-strip /home/user/rootfs/lib/*.so*
 # 创建inittab配置文件
 cat > /home/user/rootfs/etc/inittab << 'EOF'
 ::sysinit:/etc/init.d/rcS
-console::respawn:-/bin/sh
+console::askfirst:-/bin/sh
 ::ctrlaltdel:/sbin/reboot
 ::shutdown:/bin/umount -a -r
 EOF
@@ -1083,6 +1086,8 @@ chmod +x /home/user/rootfs/etc/init.d/rcS
 
 ### Q3: 根文件系统有哪些类型?NFS、EXT4、YAFFS的特点是什么?
 
+> ⚠️ **来源说明**:本题不属于《I.MX6U嵌入式Linux驱动开发指南》第38章内容,为扩展知识(第38章只构建 BusyBox 根文件系统并用 NFS 挂载测试,未涉及 YAFFS/SquashFS/CRAMFS)。
+
 **答案要点**:
 
 1. NFS:网络文件系统,用于开发调试
@@ -1256,7 +1261,7 @@ int main(int argc, char *argv[])
 
 # 常用配置
 ::sysinit:/etc/init.d/rcS        # 系统初始化
-console::respawn:-/bin/sh         # 控制台登录
+console::askfirst:-/bin/sh         # 控制台登录
 ::ctrlaltdel:/sbin/reboot         # Ctrl+Alt+Del重启
 ::shutdown:/bin/umount -a -r      # 关机时卸载文件系统
 ```
@@ -1356,12 +1361,12 @@ uuu -b emmc u-boot.imx zImage rootfs.ext4
 setenv ipaddr 192.168.1.100
 setenv serverip 192.168.1.101
 
-# 下载并烧写
-tftp 80800000 u-boot.bin
-mmc write 80800000 0x0 0x400
+# 下载并烧写(U-Boot 从第 2 个扇区开始,切勿写前两个扇区)
+tftp 80800000 u-boot.imx
+mmc write 80800000 2 0x32E
 
 tftp 80800000 zImage
-mmc write 80800000 0x400 0x3000
+mmc write 80800000 0x800 0x3000
 ```
 
 **追问**:
@@ -1420,14 +1425,12 @@ sudo dd if=/dev/sdb of=u-boot_backup.bin bs=512 count=1000 skip=2
 **4. EMMC烧写**:
 
 ```bash
-# EMMC分区表
-# 扇区0: MBR
-# 扇区0x800: U-Boot(偏移1MB)
-# 扇区0x2000: 内核
-# 扇区0x4000: 根文件系统
-
-# 烧写U-Boot到EMMC
-mmc write 80800000 0x0 0x400
+# EMMC 的 U-Boot 同样从第 2 个扇区(偏移 1024 字节)开始写
+# 烧写 U-Boot 到 EMMC(0x32E=814 块),并做分区配置
+# (指南第 30 章:mmc partconf 1 1 0 0 是 EMMC 必须的一步)
+mmc dev 1 0
+mmc write 80800000 2 0x32E
+mmc partconf 1 1 0 0
 ```
 
 **追问**:
@@ -1493,7 +1496,7 @@ sudo umount /mnt
 
 sudo mount /dev/sdb2 /mnt
 sudo cp zImage /mnt/
-sudo cp imx6ull-alientek.dtb /mnt/
+sudo cp imx6ull-alientek-emmc.dtb /mnt/
 sudo umount /mnt
 
 sudo mount /dev/sdb3 /mnt
@@ -1505,8 +1508,9 @@ sudo umount /mnt
 
 ```bash
 # 在U-Boot中设置
-setenv bootargs "console=ttymxc0,115200 root=/dev/mmcblk1p3 rootwait rw"
-setenv bootcmd "fatload mmc 0:2 80800000 zImage;fatload mmc 0:2 83000000 imx6ull-alientek.dtb;bootm 80800000 - 83000000"
+# SD 卡启动时 SD(TF)卡对应 mmcblk0,EMMC 对应 mmcblk1
+setenv bootargs "console=ttymxc0,115200 root=/dev/mmcblk0p3 rootwait rw"
+setenv bootcmd "fatload mmc 0:2 80800000 zImage;fatload mmc 0:2 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000"
 saveenv
 ```
 
@@ -1519,6 +1523,8 @@ saveenv
 
 ### Q4: 如何使用NXP的MfgTool或UUU工具烧写系统?
 
+> ⚠️ **来源说明**:《指南》第39章只讲解 MfgTool(Windows 图形化)的烧写原理与用法;本题中的 UUU 工具为扩展知识。
+
 **答案要点**:
 
 1. MfgTool:Windows下的图形化烧写工具
@@ -1536,7 +1542,7 @@ NXP烧写工具使用:
 
 # 2. 准备烧写文件
 # profiles/OS Firmware/files/
-# ├── u-boot-imx6ull-14x14-evk.imx
+# ├── u-boot-imx6ull14x14evk_emmc.imx
 # ├── zImage
 # ├── imx6ull-14x14-evk.dtb
 # └── rootfs.tar.bz2
@@ -1564,7 +1570,7 @@ sudo apt-get install uuu
 # 2. 烧写命令
 # 烧写整个系统
 uuu -b emmc \
-    u-boot-imx6ull-14x14-evk.imx \
+    u-boot-imx6ull14x14evk_emmc.imx \
     zImage \
     imx6ull-14x14-evk.dtb \
     rootfs.img
@@ -1583,7 +1589,7 @@ uuu -ls
 # auto_burn.sh - 自动烧写脚本
 
 # 设置文件路径
-UBOOT="u-boot-imx6ull-14x14-evk.imx"
+UBOOT="u-boot-imx6ull14x14evk_emmc.imx"
 KERNEL="zImage"
 DTB="imx6ull-14x14-evk.dtb"
 ROOTFS="rootfs.img"
@@ -1612,6 +1618,8 @@ fi
 
 ### Q5: 如何制作可启动的USB启动盘?如何从USB启动系统?
 
+> ⚠️ **来源说明**:本题不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(第39章只讲 MfgTool 通过 USB OTG 烧写 EMMC,未讲 USB 启动盘)。
+
 **答案要点**:
 
 1. 制作启动盘:使用`dd`或专用工具
@@ -1638,7 +1646,7 @@ sudo apt-get install etcher
 ```bash
 # 在U-Boot中设置USB启动参数
 setenv bootargs "console=ttymxc0,115200 root=/dev/sda3 rootwait rw"
-setenv bootcmd "usb start;fatload usb 0:1 80800000 zImage;fatload usb 0:1 83000000 imx6ull-alientek.dtb;bootm 80800000 - 83000000"
+setenv bootcmd "usb start;fatload usb 0:1 80800000 zImage;fatload usb 0:1 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000"
 saveenv
 ```
 
@@ -1655,10 +1663,10 @@ usb part
 fatload usb 0:1 80800000 zImage
 
 # 4. 加载设备树
-fatload usb 0:1 83000000 imx6ull-alientek.dtb
+fatload usb 0:1 83000000 imx6ull-alientek-emmc.dtb
 
 # 5. 启动内核
-bootm 80800000 - 83000000
+bootz 80800000 - 83000000
 ```
 
 **4. 自动启动脚本**:
@@ -1668,8 +1676,8 @@ bootm 80800000 - 83000000
 cat > /boot/usb_boot.scr << 'EOF'
 setenv bootargs "console=ttymxc0,115200 root=/dev/sda3 rootwait rw"
 fatload usb 0:1 80800000 zImage
-fatload usb 0:1 83000000 imx6ull-alientek.dtb
-bootm 80800000 - 83000000
+fatload usb 0:1 83000000 imx6ull-alientek-emmc.dtb
+bootz 80800000 - 83000000
 EOF
 
 # 编译脚本
@@ -1778,6 +1786,8 @@ mkimage -A arm -O linux -T script -C none -n "Boot Script" -d /boot/usb_boot.scr
 
 ### Q2: U-Boot如何加载和启动Linux内核?bootm命令的工作原理是什么?
 
+> **辨析**:`bootm` 用于启动 **uImage**(U-Boot 专用镜像,带 64 字节头);启动 **zImage** 要用 `bootz`。指南中启动 zImage 的完整序列为:`tftp 80800000 zImage` → `tftp 83000000 imx6ull-alientek-emmc.dtb` → `bootz 80800000 - 83000000`。
+
 **答案要点**:
 
 1. bootm命令从内存地址加载内核并启动
@@ -1967,6 +1977,8 @@ int of_platform_bus_probe(struct device_node *root,
 
 ### Q4: 如何优化嵌入式Linux系统的启动时间?有哪些方法?
 
+> ⚠️ **来源说明**:本题不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(其中 `systemctl` 等为通用 Linux 内容,嵌入式系统通常使用 BusyBox init)。
+
 **答案要点**:
 
 1. U-Boot优化:减少初始化时间、启用缓存
@@ -2177,3 +2189,7 @@ gdb vmlinux
 6. **启动流程**:从上电到系统就绪的完整过程
 
 掌握这些知识点,能够应对嵌入式Linux系统移植相关的面试和实际开发工作。
+
+---
+
+**内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第30、31、32、35、37、38、39章