Просмотр исходного кода

ingest: 03-外设与高级IO编程(7篇)

OpenCode 12 часов назад
Родитель
Сommit
5e6c39e32a

+ 1288 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/01-高级IO.md

@@ -0,0 +1,1288 @@
+---
+title: 高级IO
+tags: [嵌入式Linux, Linux应用编程, 高级IO, 非阻塞IO, IO多路复用, select, poll, epoll, 异步IO, SIGIO, mmap, 存储映射IO, 文件锁, flock, fcntl, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I/O"
+---
+
+# 高级IO
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/02-GPIO与LED应用编程]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/20. 共享内存与mmap]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/6. 信号机制]]
+
+前面章节讨论的文件 I/O 都是"打开→read/write→close"的直线流程。本篇汇总文件 I/O 的五类高级用法,用来应对不同应用场景:**非阻塞 I/O**、**I/O 多路复用**、**异步 I/O**、**存储映射 I/O**、**文件锁**。它们是后面提高篇(LCD、输入设备、网络)反复用到的底层工具。
+
+> 约定:本篇出现的设备路径 `/dev/input/event3` 是正点原子教程所用 Ubuntu 主机上鼠标对应的设备文件,在自己的机器上要先用 `sudo od -x /dev/input/eventX` 确认,不要照抄编号。
+
+---
+
+## 1. 非阻塞 I/O
+
+### 1.1 阻塞与非阻塞
+
+"阻塞"就是**进入了休眠状态,交出了 CPU 控制权**。前面学过的 `wait()`、`pause()`、`sleep()` 都会阻塞。I/O 的阻塞则特指读写操作:
+
+- **阻塞式 I/O**:对文件发起读操作时,若数据未准备好、当前无数据可读,调用者会被挂起,直到有数据可读才被唤醒。
+- **非阻塞式 I/O**:即使没有数据可读,也不会被阻塞,而是**立即返回错误**(`errno = EAGAIN`,即 "Resource temporarily unavailable")。
+
+一个关键前提:**普通文件的读写永远不会阻塞**,不管读写多少字节,`read()`/`write()` 一定会在有限时间内返回。这是普通文件本质决定的。只有**管道文件、网络设备文件、字符设备文件**等才区分阻塞/非阻塞。
+
+### 1.2 两种打开方式
+
+| 方式 | 做法 | 说明 |
+| ---- | ---- | ---- |
+| 默认(阻塞) | `open(path, O_RDONLY)` | 不指定 `O_NONBLOCK` 即为阻塞 |
+| 非阻塞 | `open(path, O_RDONLY \| O_NONBLOCK)` | 后续 I/O 以非阻塞方式进行 |
+| 改造已有 fd | `fcntl(fd, F_GETFL)` → `flag \|= O_NONBLOCK` → `fcntl(fd, F_SETFL, flag)` | 用于别人打开、自己继承来的 fd(如标准输入 0) |
+
+`open` 时无法指定 `O_ASYNC`,但 `O_NONBLOCK` 可以在 `open` 时直接指定。普通文件设置与否没有影响。
+
+### 1.3 阻塞 I/O 的困境
+
+程序若先 `read` 鼠标、再 `read` 键盘,只要鼠标没数据,进程就卡在鼠标的 `read` 上,后面的键盘读取永远得不到执行——**无法实现并发读取**。用多线程/多进程可以绕开,但不是本节重点。非阻塞 + 轮询能解决"卡死",却带来新问题。
+
+### 1.4 阻塞 vs 非阻塞对比
+
+| 维度 | 阻塞 I/O | 非阻塞 I/O |
+| ---- | -------- | ---------- |
+| 无数据时 | 挂起休眠,交出 CPU | 立即返回错误 |
+| CPU 占用 | 几乎为 0 | 轮询时接近 100% |
+| 并发读取 | 单线程做不到(会被第一个 I/O 卡住) | 轮询可以做到 |
+| 适用场景 | 单设备、愿意等待 | 多设备非阻塞 + 配合多路复用 |
+
+```mermaid
+flowchart LR
+    subgraph B[阻塞 I/O]
+        B1[read 鼠标] --> B2{有数据?}
+        B2 -- 否 --> B3[进程休眠 交出CPU]
+        B3 --> B2
+        B2 -- 是 --> B4[返回数据]
+    end
+    subgraph N[非阻塞 I/O]
+        N1[read 鼠标] --> N2{有数据?}
+        N2 -- 否 --> N3[立即返回 EAGAIN]
+        N3 --> N1
+        N2 -- 是 --> N4[返回数据]
+    end
+```
+
+### 1.5 完整例程:非阻塞方式同时读取鼠标与键盘
+
+这是把"阻塞困境"改造成"轮询并发"的完整程序。键盘是标准输入 fd=0,需要 `fcntl` 手动改成非阻塞。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#define MOUSE       "/dev/input/event3"
+
+int main(void)
+{
+    char buf[100];
+    int fd, ret, flag;
+
+    /* 打开鼠标设备文件 */
+    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 将键盘(标准输入 fd=0)设置为非阻塞方式 */
+    flag = fcntl(0, F_GETFL);   //先获取原来的 flag
+    flag |= O_NONBLOCK;         //将 O_NONBLOCK 标志添加到 flag
+    fcntl(0, F_SETFL, flag);    //重新设置 flag
+
+    for ( ; ; ) {
+        /* 读鼠标 */
+        ret = read(fd, buf, sizeof(buf));
+        if (0 < ret)
+            printf("鼠标: 成功读取<%d>个字节数据\n", ret);
+
+        /* 读键盘 */
+        ret = read(0, buf, sizeof(buf));
+        if (0 < ret)
+            printf("键盘: 成功读取<%d>个字节数据\n", ret);
+    }
+
+    /* 关闭文件 */
+    close(fd);
+    exit(0);
+}
+```
+
+逐段说明:
+- `O_NONBLOCK` 让鼠标 `read` 无数据时立刻返回,循环不会被卡死。
+- 键盘 fd 是继承来的,只能用 `F_GETFL`/`F_SETFL` 两步加标志,不能重新 `open`。
+- 循环里虽然"并发"了,但空转轮询导致 **CPU 占用率极高**——这正是引出 I/O 多路复用的动机。
+
+---
+
+## 2. I/O 多路复用
+
+### 2.1 概念
+
+I/O 多路复用(IO multiplexing)用一个机制**同时监视多个文件描述符**,一旦其中某个可以执行 I/O,就通知应用程序去读写。它解决"进程阻塞在某个特定 I/O 系统调用上"的问题,是**外部阻塞、内部监视多路 I/O**:调用本身会阻塞,但阻塞期间内核替你盯着多个 fd。两个功能几乎相同的系统调用是 `select()` 和 `poll()`。
+
+```mermaid
+flowchart TD
+    A[用户进程] --> B{select/poll 阻塞等待}
+    B -->|鼠标就绪| C[返回, 可用集合中标记鼠标]
+    B -->|键盘就绪| D[返回, 可用集合中标记键盘]
+    B -->|超时/无就绪| E[返回 0 或错误]
+    C --> F[对就绪fd执行 read/write 清除就绪态]
+    D --> F
+    F --> B
+```
+
+### 2.2 select() 函数
+
+```c
+#include <sys/select.h>
+
+int select(int nfds, fd_set *readfds, fd_set *writefds,
+           fd_set *exceptfds, struct timeval *timeout);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `nfds` | 三个集合中**最大文件描述符编号值 + 1** |
+| `readfds` | 关心"是否可读"的 fd 集合,不关心传 `NULL` |
+| `writefds` | 关心"是否可写"的 fd 集合,不关心传 `NULL` |
+| `exceptfds` | 关心"异常情况"的 fd 集合(不是出错),不关心传 `NULL` |
+| `timeout` | `NULL`:一直阻塞;`0`(两成员为 0):不阻塞只轮询一次;否则为等待上限 |
+
+`fd_set` 是位掩码集合,全部操作由四个宏完成:
+
+| 宏 | 作用 |
+| -- | ---- |
+| `FD_ZERO(fd_set *set)` | 将集合初始化为空 |
+| `FD_SET(int fd, fd_set *set)` | 把 fd 加入集合 |
+| `FD_CLR(int fd, fd_set *set)` | 把 fd 从集合移除 |
+| `FD_ISSET(int fd, fd_set *set)` | fd 是集合成员则返回真 |
+
+集合容量由 `FD_SETSIZE` 决定,Linux 下为 **1024**。使用前必须 `FD_ZERO` 初始化。
+
+**返回值**(三种情况):
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| `-1` | 出错,设置 `errno`(`EBADF` fd 非法、`EINTR` 被信号打断等) |
+| `0` | 超时,没有任何 fd 就绪,三个集合被清空 |
+| `> 0` | 处于就绪态的 fd 总数(同一 fd 在多个集合就绪会被重复统计) |
+
+**关键特性(值-结果参数)**:`select()` 返回时会**修改**传入的集合,使其只保留就绪的 fd。因此若在循环中重复调用,**每次调用前都必须重新 `FD_ZERO` 并重新 `FD_SET`**。
+
+### 2.3 poll() 函数
+
+```c
+#include <poll.h>
+
+int poll(struct pollfd *fds, nfds_t nfds, int timeout);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `fds` | `struct pollfd` 数组,每个元素指定一个 fd 及关心的事件 |
+| `nfds` | 数组元素个数(`nfds_t` 实际为无符号整型) |
+| `timeout` | `-1`:一直阻塞;`0`:不阻塞只检查一次;`> 0`:最多阻塞 timeout 毫秒 |
+
+```c
+struct pollfd {
+    int   fd;      /* file descriptor */
+    short events;  /* requested events  (输入,调用者填写) */
+    short revents; /* returned events   (输出,内核填写) */
+};
+```
+
+`events` 是调用者填写(关心的条件),`revents` 由内核在返回时填写,`poll()` 不会改动 `events`。常用事件标志:
+
+| 标志 | events 可填 | revents 可返回 | 说明 |
+| ---- | :---------: | :------------: | ---- |
+| `POLLIN` | ● | ● | 有数据可读 |
+| `POLLRDNORM` | ● | ● | 等价于 `POLLIN` |
+| `POLLRDBAND` | ● | ● | 可读优先级数据(Linux 通常不用) |
+| `POLLPRI` | ● | ● | 可读高优先级数据(GPIO 中断用它) |
+| `POLLRDHUP` | ● | ● | 对端套接字关闭 |
+| `POLLOUT` | ● | ● | 可写入数据 |
+| `POLLWRNORM` | ● | ● | 等价于 `POLLOUT` |
+| `POLLWRBAND` | ● | ● | 优先级数据可写 |
+| `POLLERR` | — | ● | 有错误发生 |
+| `POLLHUP` | — | ● | 出现挂断 |
+| `POLLNVAL` | — | ● | 文件描述符未打开 |
+
+不关心某 fd 时,可把 `events` 设为 0,或把 `fd` 设为负数(`-fd`),后者会让该元素被忽略且 `revents` 恒为 0。实际编程中最常用的是 `POLLIN`、`POLLOUT`;GPIO 中断场景用 `POLLPRI`。
+
+**返回值**与 `select()` 一致:`-1` 出错;`0` 超时;`> 0` 表示 `revents` 非 0 的元素个数。
+
+### 2.4 完整例程:select 实现同时读取键盘和鼠标
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/select.h>
+
+#define MOUSE       "/dev/input/event3"
+
+int main(void)
+{
+    char buf[100];
+    int fd, ret = 0, flag;
+    fd_set rdfds;
+    int loops = 5;
+
+    /* 打开鼠标设备文件,并设置为非阻塞 */
+    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 将键盘设置为非阻塞方式 */
+    flag = fcntl(0, F_GETFL);
+    flag |= O_NONBLOCK;
+    fcntl(0, F_SETFL, flag);
+
+    /* 同时读取键盘和鼠标 */
+    while (loops--) {
+        FD_ZERO(&rdfds);
+        FD_SET(0, &rdfds);  //添加键盘
+        FD_SET(fd, &rdfds); //添加鼠标
+
+        ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
+        if (0 > ret) {
+            perror("select error");
+            goto out;
+        }
+        else if (0 == ret) {
+            fprintf(stderr, "select timeout.\n");
+            continue;
+        }
+
+        /* 检查键盘是否为就绪态 */
+        if (FD_ISSET(0, &rdfds)) {
+            ret = read(0, buf, sizeof(buf));
+            if (0 < ret)
+                printf("键盘: 成功读取<%d>个字节数据\n", ret);
+        }
+
+        /* 检查鼠标是否为就绪态 */
+        if (FD_ISSET(fd, &rdfds)) {
+            ret = read(fd, buf, sizeof(buf));
+            if (0 < ret)
+                printf("鼠标: 成功读取<%d>个字节数据\n", ret);
+        }
+    }
+
+out:
+    close(fd);
+    exit(ret);
+}
+```
+
+编译运行(主机上,非开发板):`gcc select_test.c -o select_test && sudo ./select_test`。先动鼠标或先按键都能正确读取。
+
+### 2.5 完整例程:poll 实现同时读取键盘和鼠标
+
+把 `select` 换成 `poll`,逻辑完全一致:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <poll.h>
+
+#define MOUSE       "/dev/input/event3"
+
+int main(void)
+{
+    char buf[100];
+    int fd, ret = 0, flag;
+    int loops = 5;
+    struct pollfd fds[2];
+
+    /* 打开鼠标设备文件,并设置为非阻塞 */
+    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 将键盘设置为非阻塞方式 */
+    flag = fcntl(0, F_GETFL);
+    flag |= O_NONBLOCK;
+    fcntl(0, F_SETFL, flag);
+
+    /* 填充 pollfd 数组:只关心数据可读 */
+    fds[0].fd = 0;
+    fds[0].events = POLLIN;
+    fds[0].revents = 0;
+    fds[1].fd = fd;
+    fds[1].events = POLLIN;
+    fds[1].revents = 0;
+
+    while (loops--) {
+        ret = poll(fds, 2, -1);
+        if (0 > ret) {
+            perror("poll error");
+            goto out;
+        }
+        else if (0 == ret) {
+            fprintf(stderr, "poll timeout.\n");
+            continue;
+        }
+
+        /* 检查键盘是否为就绪态 */
+        if (fds[0].revents & POLLIN) {
+            ret = read(0, buf, sizeof(buf));
+            if (0 < ret)
+                printf("键盘: 成功读取<%d>个字节数据\n", ret);
+        }
+
+        /* 检查鼠标是否为就绪态 */
+        if (fds[1].revents & POLLIN) {
+            ret = read(fd, buf, sizeof(buf));
+            if (0 < ret)
+                printf("鼠标: 成功读取<%d>个字节数据\n", ret);
+        }
+    }
+
+out:
+    close(fd);
+    exit(ret);
+}
+```
+
+**使用 select/poll 的共同注意事项**:监测到某个 fd 就绪后,**必须对该 fd 执行 I/O 以清除就绪状态**,否则该状态会一直存在,下一次调用会立刻返回。
+
+### 2.6 select / poll / epoll 对比
+
+> ⚠️ **来源说明**:epoll 部分不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书明确说明"本书并不会介绍 epoll 相关内容",仅提示当需要检查大量文件描述符时可用 epoll 解决 select/poll 性能低的问题,且 epoll 在性能上与异步 I/O 相似、并有一些胜过异步 I/O 的优点。
+
+| 维度 | select | poll | epoll |
+| ---- | ------ | ---- | ----- |
+| 数据结构 | `fd_set` 位图 | `pollfd` 数组 | 内核红黑树 + 就绪链表 |
+| fd 上限 | `FD_SETSIZE`=1024 | 无硬上限(受内存限制) | 无硬上限 |
+| 每次调用开销 | O(n),且需重建/拷贝集合 | O(n) 轮询 | O(1) 取就绪链表(`epoll_wait`) |
+| 是否修改入参 | 是(值-结果),需重复初始化 | 否(`revents` 与 `events` 分离) | 否 |
+| 触发模式 | 只有水平触发 | 只有水平触发 | 支持 LT 与 ET |
+| 大量 fd 场景 | 差 | 差 | 优 |
+| 少量 fd 场景 | 够用、可移植性好 | 够用 | 略重 |
+
+epoll 三个核心函数:
+
+```c
+#include <sys/epoll.h>
+
+int epoll_create(int size);                 // 创建 epoll 实例,返回 epoll fd
+int epoll_create1(int flags);               // 推荐,flags 可传 EPOLL_CLOEXEC
+int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
+int epoll_wait(int epfd, struct epoll_event *events,
+               int maxevents, int timeout);
+```
+
+`epoll_ctl` 的 `op` 取值:`EPOLL_CTL_ADD`(注册)、`EPOLL_CTL_MOD`(修改)、`EPOLL_CTL_DEL`(注销)。`struct epoll_event` 中 `events` 常用 `EPOLLIN` / `EPOLLOUT`,并可或上 `EPOLLET`(边沿触发)、`EPOLLONESHOT`(只通知一次)。`epoll_wait` 返回就绪事件个数,`timeout` 单位毫秒(`-1` 永久阻塞,`0` 立即返回)。
+
+**LT(水平触发,默认)**:只要 fd 上还有数据可读,每次 `epoll_wait` 都会通知。编程简单,不易丢事件。
+**ET(边沿触发)**:仅在状态**发生变化**时通知一次。若一次没把数据读干净,后续不再通知,事件就丢了。ET 必须把 fd 设为**非阻塞**,并用循环 `read` 到返回 `EAGAIN` 为止。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/epoll.h>
+
+#define MAX_EVENTS 10
+
+int main(int argc, char *argv[])
+{
+    int epfd, nfds, fd, i;
+    struct epoll_event ev, events[MAX_EVENTS];
+
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 以非阻塞方式打开目标文件/设备 */
+    fd = open(argv[1], O_RDONLY | O_NONBLOCK);
+    if (0 > fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 创建 epoll 实例 */
+    epfd = epoll_create1(0);
+    if (0 > epfd) {
+        perror("epoll_create1 error");
+        exit(-1);
+    }
+
+    /* 注册可读事件;若要边沿触发,改成 EPOLLIN | EPOLLET */
+    ev.events = EPOLLIN;
+    ev.data.fd = fd;
+    if (0 > epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev)) {
+        perror("epoll_ctl error");
+        exit(-1);
+    }
+
+    for ( ; ; ) {
+        nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
+        if (0 > nfds) {
+            perror("epoll_wait error");
+            exit(-1);
+        }
+
+        for (i = 0; i < nfds; i++) {
+            if (events[i].events & EPOLLIN) {
+                char buf[100];
+                /* LT:读一次即可;ET:必须循环读到 EAGAIN */
+                int ret = read(events[i].data.fd, buf, sizeof(buf));
+                if (0 < ret)
+                    printf("读到 %d 字节\n", ret);
+            }
+        }
+    }
+
+    close(fd);
+    close(epfd);
+    return 0;
+}
+```
+
+> ⚠️ **来源说明**:以下为 POSIX 异步 I/O 扩展知识,原书未涉及。原书的"异步 I/O"指 `O_ASYNC` + `SIGIO` 的**信号驱动 I/O**(见第 3 节)。
+
+POSIX 异步 I/O(`<aio.h>`)把"发起 I/O"与"等结果"真正分离,可在数据就绪或完成时通过信号/回调通知:
+
+```c
+#include <aio.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+int main(void)
+{
+    int fd = open("test.txt", O_RDONLY);
+    char buf[256] = {0};
+    struct aiocb cb;
+    struct sigevent sev;
+
+    memset(&cb, 0, sizeof(cb));
+    cb.aio_fildes = fd;
+    cb.aio_buf    = buf;
+    cb.aio_nbytes = sizeof(buf);
+    cb.aio_offset = 0;
+
+    /* 用信号通知:内核在 I/O 完成时发送 SIGRTMIN + 1 */
+    sev.sigev_notify = SIGEV_SIGNAL;
+    sev.sigev_signo  = SIGRTMIN + 1;
+    cb.aio_sigevent  = sev;
+
+    aio_read(&cb);                       // 异步发起,立即返回
+    /* ... 主线程可继续做别的事 ... */
+    while (aio_error(&cb) == EINPROGRESS)
+        ;                                // 演示用忙等;实战应 aio_suspend 或等信号
+    int n = aio_return(&cb);             // 取真正的返回字节数
+    (void)n;
+
+    close(fd);
+    return 0;
+}
+```
+
+要点:`aio_read`/`aio_write` 只负责**提交请求**;结果用 `aio_error()`(`EINPROGRESS` 表示未完成)与 `aio_return()` 获取;通知方式由 `aiocb.aio_sigevent` 决定——`SIGEV_SIGNAL` 发信号,`SIGEV_THREAD` 由线程回调。部分环境下需链接 `-lrt`(老 glibc)。
+
+---
+
+## 3. 异步 I/O(信号驱动 I/O)
+
+### 3.1 原理
+
+I/O 多路复用是进程**主动查询**"谁能读写了";异步 I/O 反过来:**文件描述符可以执行 I/O 时,内核主动给进程发一个信号**。进程可以先做别的任务,收到信号再处理。因为绑定信号,异步 I/O 也常被称为**信号驱动 I/O**。
+
+使用步骤:
+
+1. 通过 `O_NONBLOCK` 使能非阻塞 I/O;
+2. 通过 `O_ASYNC` 使能异步 I/O(`open` 时无法指定,必须用 `fcntl`);
+3. 设置异步 I/O 事件的接收进程(`F_SETOWN`);
+4. 为通知信号(默认 `SIGIO`)注册信号处理函数;
+5. 进程执行其它任务,I/O 就绪时内核发 `SIGIO`,在信号处理函数里做 I/O。
+
+```c
+int flag = fcntl(fd, F_GETFL);
+flag |= O_ASYNC;            // 使能异步 I/O
+fcntl(fd, F_SETFL, flag);
+
+fcntl(fd, F_SETOWN, getpid());  // 把当前进程设为异步 I/O 所有者
+```
+
+### 3.2 完整例程:以异步 I/O 方式读取鼠标
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <signal.h>
+
+#define MOUSE       "/dev/input/event3"
+static int fd;
+
+static void sigio_handler(int sig)
+{
+    static int loops = 5;
+    char buf[100] = {0};
+    int ret;
+
+    if (SIGIO != sig)
+        return;
+
+    ret = read(fd, buf, sizeof(buf));   // 在信号处理函数中读取
+    if (0 < ret)
+        printf("鼠标: 成功读取<%d>个字节数据\n", ret);
+
+    loops--;
+    if (0 >= loops) {
+        close(fd);
+        exit(0);
+    }
+}
+
+int main(void)
+{
+    int flag;
+
+    /* 打开鼠标设备文件<使能非阻塞 I/O> */
+    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 使能异步 I/O */
+    flag = fcntl(fd, F_GETFL);
+    flag |= O_ASYNC;
+    fcntl(fd, F_SETFL, flag);
+
+    /* 设置异步 I/O 的所有者 */
+    fcntl(fd, F_SETOWN, getpid());
+
+    /* 为 SIGIO 信号注册信号处理函数 */
+    signal(SIGIO, sigio_handler);
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+### 3.3 异步 I/O 的两个缺陷与优化
+
+| 缺陷 | 说明 |
+| ---- | ---- |
+| 信号可能丢失 | `SIGIO` 是标准(非实时、不可靠)信号,不支持排队。处理函数执行期间又来了多次 `SIGIO`,只会传递一次,其余丢失 |
+| 无法得知发生了什么事件 | 处理函数里直接 `read()`,并不知道 fd 到底可读、可写还是异常 |
+
+优化方法:
+
+- **用实时信号替换 SIGIO**:`fcntl(fd, F_SETSIG, SIGRTMIN);`(`arg=0` 则恢复默认 `SIGIO`)。使用 `F_SETSIG` 需要先定义 `_GNU_SOURCE` 宏。
+- **用 `sigaction()` + `SA_SIGINFO` 注册处理函数**,通过 `siginfo_t` 获取更多信息:
+
+| 字段 | 含义 |
+| ---- | ---- |
+| `si_signo` | 引发处理函数的信号值 |
+| `si_fd` | 发生异步 I/O 事件的 fd |
+| `si_code` | 发生了什么事件(见下表) |
+| `si_band` | 位掩码,值与 `poll()` 的 `revents` 相同 |
+
+| `si_code` | `si_band` 掩码值 | 说明 |
+| --------- | ---------------- | ---- |
+| `POLL_IN` | `POLLIN \| POLLRDNORM` | 可读取数据 |
+| `POLL_OUT` | `POLLOUT \| POLLWRNORM \| POLLWRBAND` | 可写入数据 |
+| `POLL_MSG` | `POLLIN \| POLLRDNORM \| POLLMSG` | 不使用 |
+| `POLL_ERR` | `POLLERR` | I/O 错误 |
+| `POLL_PRI` | `POLLPRI \| POLLRDNORM` | 可读高优先级数据 |
+| `POLL_HUP` | `POLLHUP \| POLLERR` | 出现宕机 |
+
+### 3.4 完整例程:优化后的异步 I/O
+
+```c
+#define _GNU_SOURCE             //使用 F_SETSIG 需要定义该宏
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <signal.h>
+
+#define MOUSE       "/dev/input/event3"
+static int fd;
+
+static void io_handler(int sig, siginfo_t *info, void *context)
+{
+    static int loops = 5;
+    char buf[100] = {0};
+    int ret;
+
+    if (SIGRTMIN != sig)
+        return;
+
+    /* 判断鼠标是否可读 */
+    if (POLL_IN == info->si_code) {
+        ret = read(fd, buf, sizeof(buf));
+        if (0 < ret)
+            printf("鼠标: 成功读取<%d>个字节数据\n", ret);
+
+        loops--;
+        if (0 >= loops) {
+            close(fd);
+            exit(0);
+        }
+    }
+}
+
+int main(void)
+{
+    struct sigaction act;
+    int flag;
+
+    /* 打开鼠标设备文件<使能非阻塞 I/O> */
+    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 使能异步 I/O */
+    flag = fcntl(fd, F_GETFL);
+    flag |= O_ASYNC;
+    fcntl(fd, F_SETFL, flag);
+
+    /* 设置异步 I/O 的所有者 */
+    fcntl(fd, F_SETOWN, getpid());
+
+    /* 指定实时信号 SIGRTMIN 作为异步 I/O 通知信号 */
+    fcntl(fd, F_SETSIG, SIGRTMIN);
+
+    /* 为实时信号 SIGRTMIN 注册信号处理函数 */
+    act.sa_sigaction = io_handler;
+    act.sa_flags = SA_SIGINFO;
+    sigemptyset(&act.sa_mask);
+    sigaction(SIGRTMIN, &act, NULL);
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+### 3.5 异步 I/O 与 epoll 的适用性
+
+异步 I/O 与 epoll 在需要**同时检查大量(如数千个)文件描述符**时,性能相比 select/poll 有显著优势。原因:异步 I/O 中内核"记住"要检查的 fd,仅在可 I/O 时才发信号;select/poll 内部却是轮询检查所有 fd。fd 数量不多时,select/poll 依然是简单可靠的方案。
+
+---
+
+## 4. 存储映射 I/O
+
+### 4.1 原理
+
+存储映射 I/O(memory-mapped I/O)把一个**文件映射到进程地址空间的一块内存**:读这块内存等于读文件,写这块内存等于写文件,无需调用 `read()`/`write()`。其实质是**共享**——磁盘文件直接与映射区关联,映射区可看作应用层与内核层之间的共享内存,与 IPC 的共享内存很相似。
+
+```mermaid
+flowchart LR
+    A[普通 I/O] --> A1[磁盘文件] -->|read 拷贝| A2[内核缓冲区] -->|拷贝| A3[用户缓冲区]
+    A3 -->|write 拷贝| A2 -->|拷贝| A1
+    B[存储映射 I/O] --> B1[磁盘文件] -->|mmap 建立映射| B2[进程映射区]
+    B2 -->|直接读写, 无需read/write| B1
+```
+
+### 4.2 mmap() 与 munmap()
+
+```c
+#include <sys/mman.h>
+
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
+int   munmap(void *addr, size_t length);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `addr` | 映射区起始地址,通常传 `NULL` 由系统选择 |
+| `length` | 映射长度(字节),`length` 不能大于文件大小 |
+| `prot` | 保护要求:`PROT_EXEC`/`PROT_READ`/`PROT_WRITE`/`PROT_NONE`,可位或组合 |
+| `flags` | 必须二选一:`MAP_SHARED`/`MAP_PRIVATE`,可再或其它标志 |
+| `fd` | 要映射的文件描述符 |
+| `offset` | 文件映射偏移量,通常为 0,必须为系统页大小整数倍 |
+
+`flags` 的关键取值:
+
+| 标志 | 说明 |
+| ---- | ---- |
+| `MAP_SHARED` | 写入映射区会更新到文件,并允许其它进程共享 |
+| `MAP_PRIVATE` | 写时复制一份私有副本,对映射区的修改不写回文件;解除映射后修改丢弃 |
+| `MAP_FIXED` | 强制使用 `addr` 指定的地址,失败才放弃,不利于移植,通常不用 |
+| `MAP_ANONYMOUS` | 匿名映射,忽略 `fd` 和 `offset`,不涉及文件,无法与其它进程共享 |
+| `MAP_LOCKED` | 对映射区上锁 |
+
+**返回值**:成功返回映射区起始地址;失败返回 `(void *)-1`,即 `MAP_FAILED`,并设置 `errno`。
+
+**页对齐**:`addr` 和 `offset` 通常要求是系统页大小整数倍,可用 `sysconf(_SC_PAGE_SIZE)` 或 `sysconf(_SC_PAGESIZE)` 获取页大小。`length` 无对齐要求:映射区实际大小是页大小的整数倍,尾部多余字节被置 0,可访问但不反映到文件;访问映射区末尾之后的内存会触发 `SIGBUS`。
+
+**与映射区相关的两个信号**:
+
+| 信号 | 触发条件 |
+| ---- | -------- |
+| `SIGSEGV` | 映射区被指定为只读,进程却试图写入 |
+| `SIGBUS` | 映射区某部分在访问时已不存在(如另一进程把文件截断) |
+
+**解除映射**:`munmap(addr, length)`,`addr` 必须是页大小整数倍。进程终止时自动解除映射,但**`close()` 关闭文件不会解除映射**。
+
+### 4.3 mprotect() 与 msync()
+
+```c
+int mprotect(void *addr, size_t len, int prot);      // 修改已有映射区的保护要求
+int msync(void *addr, size_t length, int flags);     // 把映射区数据刷回磁盘
+```
+
+`msync` 的 `flags`:
+
+| 标志 | 说明 |
+| ---- | ---- |
+| `MS_ASYNC` | 异步同步,调用后不等数据写完磁盘即返回 |
+| `MS_SYNC` | 同步同步,等数据全部写入磁盘后才返回 |
+| `MS_INVALIDATE` | 可选,使同一文件的其它映射失效 |
+
+注意:`munmap()` **不会**把映射区内容写到磁盘;`MAP_SHARED` 下由内核在某个时刻自动更新文件;`MAP_PRIVATE` 下解除映射后修改全部丢弃。
+
+### 4.4 完整例程:用存储映射 I/O 复制文件
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <string.h>
+
+int main(int argc, char *argv[])
+{
+    int srcfd, dstfd;
+    void *srcaddr;
+    void *dstaddr;
+    int ret;
+    struct stat sbuf;
+
+    if (3 != argc) {
+        fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开源文件 */
+    srcfd = open(argv[1], O_RDONLY);
+    if (-1 == srcfd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 打开目标文件 */
+    dstfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);
+    if (-1 == dstfd) {
+        perror("open error");
+        ret = -1;
+        goto out1;
+    }
+
+    /* 获取源文件的大小,并把目标文件设为相同大小 */
+    fstat(srcfd, &sbuf);
+    ftruncate(dstfd, sbuf.st_size);
+
+    /* 将源文件映射到内存区域中 */
+    srcaddr = mmap(NULL, sbuf.st_size, PROT_READ, MAP_SHARED, srcfd, 0);
+    if (MAP_FAILED == srcaddr) {
+        perror("mmap error");
+        ret = -1;
+        goto out2;
+    }
+
+    /* 将目标文件映射到内存区域中 */
+    dstaddr = mmap(NULL, sbuf.st_size, PROT_WRITE, MAP_SHARED, dstfd, 0);
+    if (MAP_FAILED == dstaddr) {
+        perror("mmap error");
+        ret = -1;
+        goto out3;
+    }
+
+    /* 直接操作映射区完成复制 */
+    memcpy(dstaddr, srcaddr, sbuf.st_size);
+
+    /* 清理工作 */
+out4:
+    munmap(dstaddr, sbuf.st_size);   //解除目标文件映射
+out3:
+    munmap(srcaddr, sbuf.st_size);   //解除源文件映射
+out2:
+    close(dstfd);                    //关闭目标文件
+out1:
+    close(srcfd);                    //关闭源文件并退出
+    exit(ret);
+}
+```
+
+测试:`echo "hello" > srcfile && ./testApp srcfile dstfile && cat dstfile`,内容与源文件一致。
+
+### 4.5 匿名映射与共享内存
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书在 `mmap` 的 `flags` 中介绍了 `MAP_ANONYMOUS` 标志("不涉及文件,映射区域无法和其它进程共享"),本节在此基础上补充其典型用法。这里说的"无法共享"指**无亲缘关系的进程**无法访问匿名映射(没有文件/路径可供其它进程引用);但 `MAP_SHARED | MAP_ANONYMOUS` 建立的内存会随 `fork()` 被子进程继承,因此**父子进程之间可以共享**。
+
+不涉及文件时使用 `MAP_ANONYMOUS`(与 `MAP_SHARED` 搭配,用于 fork 出的父子进程共享内存):
+
+```c
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+int main(void)
+{
+    /* 匿名映射一块 4096 字节、父子进程共享的内存,忽略 fd,fd 传 -1 */
+    char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
+                   MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+    if (MAP_FAILED == p)
+        return -1;
+
+    pid_t pid = fork();
+    if (0 == pid) {                 // 子进程
+        strcpy(p, "hello from child");
+    }
+    else if (0 < pid) {             // 父进程
+        wait(NULL);
+        /* 读到的正是子进程写入的数据,说明映射区被共享 */
+        write(STDOUT_FILENO, p, strlen(p));
+    }
+
+    munmap(p, 4096);
+    return 0;
+}
+```
+
+### 4.6 普通 I/O 与存储映射 I/O 对比
+
+| 维度 | 普通 I/O(read/write) | 存储映射 I/O(mmap) |
+| ---- | ---------------------- | -------------------- |
+| 数据拷贝 | 用户缓冲区↔内核缓冲区多次拷贝 | 直接操作映射区,减少拷贝 |
+| 函数调用链 | 层层调用,开销较大 | 直接内存访问 |
+| 大数据量 | 效率低 | 效率高 |
+| 小数据量 | 方便、划算 | 不划算(页对齐、映射开销) |
+| 文件大小 | 无限制 | 映射时长度固定,且不能大于文件 |
+| 典型场景 | 小文件、普通读写 | 视频/图像处理、Framebuffer(LCD)编程 |
+
+---
+
+## 5. 文件锁
+
+### 5.1 概念与分类
+
+多个进程同时操作同一文件容易产生竞争,导致内容混乱。文件锁是内核提供的锁机制,用来对"文件"这一共享资源加锁保护。它与互斥锁/自旋锁/读写锁的区别是:后者主要用在多线程环境做线程同步,文件锁用在对同一文件的多个进程间。
+
+| 类型 | 特点 |
+| ---- | ---- |
+| 建议性锁 | 本质是协议。程序访问前先加锁,成功了再访问;但不加锁也能直接访问,对不遵守协议的程序没有约束力 |
+| 强制性锁 | 内核会检查每次 I/O,非锁拥有者无法访问。对性能影响大,一般不建议使用 |
+
+Linux 提供 `flock()`、`fcntl()`、`lockf()` 三个加锁函数。
+
+### 5.2 flock() 函数
+
+```c
+#include <sys/file.h>
+
+int flock(int fd, int operation);
+```
+
+| 参数/标志 | 说明 |
+| --------- | ---- |
+| `LOCK_SH` | 共享锁,可被多个进程同时拥有 |
+| `LOCK_EX` | 排它锁(互斥锁),同时只能被一个进程拥有 |
+| `LOCK_UN` | 解锁 |
+| `LOCK_NB` | 非阻塞方式获取锁,无法获取立即返回,`errno=EWOULDBLOCK`(可与上面位或组合) |
+
+返回值:成功 0;失败 -1 并设置 `errno`。同一文件不会同时具有共享锁和互斥锁。
+
+**flock 规则**:
+
+- 同一进程对文件多次加锁不会死锁,新锁替换旧锁;
+- 文件关闭时自动解锁;进程终止时其建立的锁全部释放;
+- 一个进程不能解另一个进程持有的锁;
+- `fork()` 创建的子进程**不继承**父进程的锁;
+- 用 `dup()`/`dup2()`/`F_DUPFD` 复制出的 fd 共享同一把锁,用其中任意一个都能解锁;但若不显式解锁,只有**所有** fd 都关闭后锁才释放。
+
+### 5.3 完整例程:flock 加锁/解锁(建议性锁)
+
+程序 1:持有排它锁,收到 `SIGINT` 后解锁退出。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/file.h>
+#include <signal.h>
+
+static int fd = -1; //文件描述符
+
+/* 信号处理函数 */
+static void sigint_handler(int sig)
+{
+    if (SIGINT != sig)
+        return;
+
+    flock(fd, LOCK_UN);     //解锁
+    close(fd);
+    printf("进程 1: 文件已解锁!\n");
+}
+
+int main(int argc, char *argv[])
+{
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开文件 */
+    fd = open(argv[1], O_WRONLY);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 以非阻塞方式对文件加锁(排它锁) */
+    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
+        perror("进程 1: 文件加锁失败");
+        exit(-1);
+    }
+
+    printf("进程 1: 文件加锁成功!\n");
+
+    /* 为 SIGINT 信号注册处理函数 */
+    signal(SIGINT, sigint_handler);
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+程序 2:加锁失败也继续读写,用来验证"建议性锁"不阻止 I/O。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/file.h>
+#include <string.h>
+
+int main(int argc, char *argv[])
+{
+    char buf[100] = "Hello World!";
+    int fd;
+    int len;
+
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开文件 */
+    fd = open(argv[1], O_RDWR);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 以非阻塞方式对文件加锁(排它锁) */
+    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
+        perror("进程 2: 文件加锁失败");
+    else
+        printf("进程 2: 文件加锁成功!\n");
+
+    /* 写文件 */
+    len = strlen(buf);
+    if (0 > write(fd, buf, len)) {
+        perror("write error");
+        exit(-1);
+    }
+    printf("进程 2: 写入到文件的字符串<%s>\n", buf);
+
+    /* 将文件读写位置移动到文件头 */
+    if (0 > lseek(fd, 0x0, SEEK_SET)) {
+        perror("lseek error");
+        exit(-1);
+    }
+
+    /* 读文件 */
+    memset(buf, 0x0, sizeof(buf));  //清理 buf
+    if (0 > read(fd, buf, len)) {
+        perror("read error");
+        exit(-1);
+    }
+    printf("进程 2: 从文件读取的字符串<%s>\n", buf);
+
+    /* 解锁、退出 */
+    flock(fd, LOCK_UN);
+    close(fd);
+    exit(0);
+}
+```
+
+测试:`touch infile`,先后台跑 `testApp1 infile &`(打印加锁成功并一直持锁),再跑 `testApp2 infile`,会看到进程 2 加锁失败但读写仍然成功——这就是建议性锁。`kill -2 <pid>` 给进程 1 发 `SIGINT` 解锁后,再跑进程 2 就能加锁成功。
+
+### 5.4 fcntl() 函数加锁
+
+```c
+#include <unistd.h>
+#include <fcntl.h>
+
+int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
+```
+
+与锁相关的 `cmd`:
+
+| cmd | 作用 |
+| --- | ---- |
+| `F_GETLK` | 测试某把锁能否加上;若会被现有锁阻塞,则把现有锁信息写回 `flockptr`,否则把 `l_type` 改为 `F_UNLCK` |
+| `F_SETLK` | 加锁/解锁,失败立即返回,`errno` 为 `EACCES` 或 `EAGAIN` |
+| `F_SETLKW` | `F_SETLK` 的阻塞版本(W=wait),等待锁可用 |
+
+```c
+struct flock {
+    short l_type;    /* F_RDLCK / F_WRLCK / F_UNLCK */
+    short l_whence;  /* SEEK_SET / SEEK_CUR / SEEK_END */
+    off_t l_start;   /* 加锁区域起始偏移 */
+    off_t l_len;     /* 加锁区域长度 */
+    pid_t l_pid;     /* 阻塞当前进程的锁的持有者PID,F_GETLK 时有效 */
+};
+```
+
+| `l_type` | 含义 |
+| -------- | ---- |
+| `F_RDLCK` | 共享读锁 |
+| `F_WRLCK` | 独占写锁 |
+| `F_UNLCK` | 解锁一个区域 |
+
+**fcntl 与 flock 的两大区别**:`flock` 只能锁整个文件,`fcntl` 可锁文件的某个区域(精确到字节);`flock` 只支持建议性锁,`fcntl` 支持建议性与强制性锁。
+
+**区域规则**:
+- 锁区域可从文件末尾或越过末尾开始,但不能在文件起始位置之前开始;
+- `l_len = 0` 表示锁区域从起始位置一直延伸到文件最大偏移处,且是动态的(后续追加的数据也在锁范围内);
+- 锁整个文件:`l_whence = SEEK_SET`、`l_start = 0`、`l_len = 0`。
+
+**读锁/写锁兼容性**(与线程读写锁类似):多个进程可在同一字节上持有共享读锁;同一字节上只能有一个写锁。已有读锁的字节上加不了写锁;已有写锁的字节上加不了任何锁。
+
+| 当前锁 \ 请求锁 | 读锁 F_RDLCK | 写锁 F_WRLCK |
+| --------------- | :----------: | :----------: |
+| 无锁 | 可 | 可 |
+| 读锁 | 可(共享) | 不可(阻塞/失败) |
+| 写锁 | 不可(阻塞/失败) | 不可(阻塞/失败) |
+
+加锁权限要求:加读锁需对文件有读权限,加写锁需有写权限。
+
+### 5.5 完整例程:fcntl 对文件加锁/解锁
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+
+int main(int argc, char *argv[])
+{
+    struct flock lock = {0};
+    int fd = -1;
+    char buf[] = "Hello World!";
+
+    /* 校验传参 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开文件 */
+    fd = open(argv[1], O_WRONLY);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 对整个文件加独占性写锁 */
+    lock.l_type = F_WRLCK;      //独占性写锁
+    lock.l_whence = SEEK_SET;   //文件头部
+    lock.l_start = 0;           //偏移量为 0
+    lock.l_len = 0;             //长度 0 表示到文件末尾
+    if (-1 == fcntl(fd, F_SETLK, &lock)) {
+        perror("加锁失败");
+        exit(-1);
+    }
+
+    printf("对文件加锁成功!\n");
+
+    /* 对文件进行写操作 */
+    if (0 > write(fd, buf, strlen(buf))) {
+        perror("write error");
+        exit(-1);
+    }
+
+    /* 解锁 */
+    lock.l_type = F_UNLCK;
+    fcntl(fd, F_SETLK, &lock);
+
+    close(fd);
+    exit(0);
+}
+```
+
+同一进程也可对**不同区域**分别加锁,例如对 100~200 字节加写锁、400~500 字节加读锁,只要区域不重叠:
+
+```c
+struct flock wr_lock = {0}, rd_lock = {0};
+ftruncate(fd, 1024);
+
+wr_lock.l_type = F_WRLCK; wr_lock.l_whence = SEEK_SET;
+wr_lock.l_start = 100;    wr_lock.l_len = 100;
+fcntl(fd, F_SETLK, &wr_lock);   //对 100~200 加写锁
+
+rd_lock.l_type = F_RDLCK; rd_lock.l_whence = SEEK_SET;
+rd_lock.l_start = 400;    rd_lock.l_len = 100;
+fcntl(fd, F_SETLK, &rd_lock);   //对 400~500 加读锁
+```
+
+若两个区域重叠,新锁会替换旧锁涉及的公共部分。
+
+### 5.6 建议性锁与强制性锁
+
+开启强制性锁机制:设置文件的 **Set-Group-ID(`S_ISGID`)位为 1,并清除组执行权限(`S_IXGRP`)**:
+
+```c
+fstat(fd, &sbuf);
+fchmod(fd, (sbuf.st_mode & ~S_IXGRP) | S_ISGID);
+```
+
+并非所有发行版都支持强制性锁,可写测试程序验证:父进程加写锁,子进程对被写的文件加读锁——若系统支持强制性锁,子进程 `read()` 会失败;若不支持,`read()` 会成功。需要注意:在强制性锁机制下,文件被加写锁会阻塞其它进程的读写,因此测试时应把文件显式设为**非阻塞**。
+
+### 5.7 lockf()
+
+`lockf()` 是一个库函数,内部基于 `fcntl()` 实现,是对 fcntl 锁的封装,用法不再展开。
+
+---
+
+## 6. 编译与实验方法
+
+所有示例在主机上即可验证,不需要开发板:
+
+```bash
+# 编译(主机 gcc)
+gcc select_test.c -o select_test
+gcc poll_test.c   -o poll_test
+gcc async_test.c  -o async_test
+gcc mmap_copy.c   -o mmap_copy
+gcc flock1.c      -o testApp1
+gcc flock2.c      -o testApp2
+gcc fcntl_lock.c  -o fcntl_lock
+gcc epoll_demo.c  -o epoll_demo
+
+# 读设备文件需要 root 权限
+sudo ./select_test
+```
+
+若要在 ALPHA/Mini I.MX6U 开发板上运行,先设置交叉编译工具环境,再用 `arm-linux-gnueabihf-gcc` 编译:
+
+```bash
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+arm-linux-gnueabihf-gcc select_test.c -o testApp
+file testApp          # 应是 32 位 ARM 可执行文件
+scp testApp root@<开发板IP>:/home/root/
+```
+
+**调试要点**:
+- 非阻塞 `read` 返回 -1 且 `errno=EAGAIN` 属正常,用 `perror` 会打印 "Resource temporarily unavailable"。
+- 找鼠标设备文件:`sudo od -x /dev/input/eventX`,移动/点击鼠标有输出即为目标设备。
+- `select` 每次循环都要重新 `FD_ZERO`/`FD_SET`,否则第二次就绪集合会残留。
+- `poll` 判断就绪用 `revents & POLLIN`,不要用 `==`。
+- `mmap` 失败要拿返回值与 `MAP_FAILED` 比较,不能与 `NULL` 比较。
+
+---
+
+## 7. 跨平台对比(IMX6ULL vs STM32 vs RK3568)
+
+| 维度 | IMX6ULL(Cortex-A7 + Linux) | STM32(Cortex-M) | RK3568(Cortex-A55 + Linux) |
+| ---- | ---------------------------- | ----------------- | ---------------------------- |
+| 运行环境 | 完整 Linux,进程/文件抽象 | 裸机或 RTOS,无文件系统抽象 | 完整 Linux,性能更强 |
+| I/O 模型 | select/poll/epoll、信号驱动、mmap、文件锁 | 靠中断 + DMA + 状态机轮询 | 同 IMX6ULL,且 fd 数量/吞吐更高 |
+| "文件"概念 | 一切皆文件,设备即 `/dev` 节点 | 无(寄存器直接读写) | 一切皆文件 |
+| 并发读取多设备 | I/O 多路复用,单线程即可 | 多中断/DMA,或 RTOS 多任务 | 多路复用 + 多线程 |
+| 大数据量传输 | mmap/Framebuffer 直接映射显存 | DMA 搬运 | mmap/DMA,带宽更高 |
+| 多进程互斥 | flock/fcntl 文件锁 | RTOS 信号量/互斥量(无文件锁) | 同 IMX6ULL |
+| 适用 | 工业 HMI、网关、中低端嵌入式 Linux | 实时控制、低成本节点 | 边缘计算、多路视频、AI 网关 |
+
+结论:Linux 应用层的高级 I/O 能力是"Cortex-A + Linux"相对于 MCU 的核心差异之一;同一套 select/poll/epoll/mmap 代码在 IMX6ULL 与 RK3568 上基本可移植,仅性能与 fd 规模不同。
+
+---
+
+## 8. 深度面试题
+
+### Q1:select、poll、epoll 三者的区别是什么?各自适用什么场景?
+
+**答**:三者的目标都是"同时监视多个 fd",但实现和复杂度不同。
+
+- `select` 用 `fd_set` 位图,最大 fd 受 `FD_SETSIZE`(1024)限制;每次调用都要把集合从用户态拷到内核态,返回时还会**修改集合**,所以循环中必须重新初始化;内核内部轮询检查,复杂度 O(n)。
+- `poll` 用 `struct pollfd` 数组,没有 1024 上限,`events` 与 `revents` 分离,不用重建集合,但内核仍是轮询,O(n)。
+- `epoll` 在内核维护红黑树 + 就绪链表,`epoll_ctl` 注册一次,`epoll_wait` 直接取就绪链表,复杂度 O(1)(就绪数),无 fd 上限,且支持 LT/ET。
+
+适用:fd 少且要求可移植——select/poll 足够;fd 多(成百上千,如高并发服务器)——epoll 明显更优。三者本质都是"同步 I/O 多路复用",仍需自己读写数据。
+
+### Q2:为什么 select 在循环中每次都要重新 FD_ZERO / FD_SET?
+
+**答**:因为 `readfds`/`writefds`/`exceptfds` 是**值-结果参数**。调用前,集合表示"我关心哪些 fd";返回后,内核把它们**改写**成"哪些 fd 已就绪",未就绪的位被清除。如果下次不重新初始化,集合里残留的只是上一轮的就绪 fd,会漏掉其它 fd、语义也错误。`poll` 不存在这个问题,因为它把输入 `events` 和输出 `revents` 分开存放。
+
+### Q3:epoll 的 LT 和 ET 有什么区别?ET 模式编程要注意什么?
+
+**答**:**LT(水平触发,默认)**:只要 fd 上还有未处理的数据,每次 `epoll_wait` 都会通知,直到数据被读完。编程简单,不容易丢事件。**ET(边沿触发,`EPOLLET`)**:仅在 fd 状态**发生变化**(由不可读变可读、可写变可写)时通知一次;如果这次没把数据读干净,内核不会再通知,剩余数据可能长期得不到处理。
+
+ET 编程要点:① 必须把 fd 设为**非阻塞**,否则循环读到没数据时会阻塞住;② 收到事件后要**循环 `read`/`write` 直到返回 `EAGAIN`/`EWOULDBLOCK`**,确保把内核缓冲区读空;③ 通常配合 `EPOLLONESHOT` 避免多线程重复处理同一个 fd。ET 的好处是减少无效唤醒、提升高并发性能。
+
+### Q4:mmap 相比 read/write 有什么优势和陷阱?
+
+**答**:优势:① 减少数据在内核缓冲区与用户缓冲区之间的拷贝,直接访问映射区;② 省去层层函数调用,大数据量下效率高;③ 可用于进程间共享(`MAP_SHARED`)、映射设备显存(Framebuffer)。陷阱:① 映射长度固定,且 `length` 不能大于文件大小;② 映射区大小是页大小整数倍,尾部多余字节写不进文件;③ `addr`/`offset` 要求页对齐;④ 写入只读映射区触发 `SIGSEGV`,文件被截断后再访问映射区触发 `SIGBUS`;⑤ `munmap` 不会主动写回磁盘,需要时用 `msync`;⑥ 小数据量下不如 read/write 划算。
+
+### Q5:flock 和 fcntl 文件锁有什么区别?fork/dup 时锁如何表现?
+
+**答**:区别:① 粒度——`flock` 只能锁整个文件,`fcntl` 可锁文件的任意字节区域;② 类型——`flock` 只能建议性锁,`fcntl` 支持建议性与强制性锁(强制性锁需文件设置 `S_ISGID` 且清除 `S_IXGRP`);③ `fcntl` 提供 `F_GETLK` 测试、`F_SETLKW` 阻塞等待,功能更丰富。
+
+fork/dup 表现:① `fork()` 出的子进程**不继承**父进程的文件锁(否则父子可同时写同一文件,锁就失去意义);② `dup()`/`dup2()` 复制出的 fd 与原 fd 引用**同一把锁**,用任意一个都能解锁;③ 锁在**文件描述符关闭时自动释放**,且进程终止时全部释放;④ `flock` 下要所有引用该锁的 fd 都关闭才释放,而 `fcntl` 下**任一**相关 fd 关闭即可释放。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十三章 高级I/O

+ 899 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/02-GPIO与LED应用编程.md

@@ -0,0 +1,899 @@
+---
+title: GPIO与LED应用编程
+tags: [嵌入式Linux, Linux应用编程, sysfs, GPIO, LED, LED子系统, trigger, brightness, export, direction, edge, poll, POLLPRI, ioctl, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十五章 点亮LED、第十六章 GPIO应用编程"
+---
+
+# GPIO与LED应用编程
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/01-高级IO]];延伸阅读:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/03-pinctrl与gpio子系统]]
+
+LED 是每块开发板都绕不开的第一个外设,而它背后就是 GPIO。本篇讲清应用层**如何不写驱动就控制 GPIO/LED**:先建立"设备节点 vs sysfs 属性文件"两种操控方式的认知,再分别用 LED 子系统(`/sys/class/leds`)和 GPIO sysfs(`/sys/class/gpio`)完成输出、输入、中断三个完整程序。最后补充 `/dev/led` + `ioctl` 这类自定义字符设备接口,以及完整的交叉编译与板端测试步骤。
+
+ALPHA/Mini I.MX6U 开发板上共有 3 颗 LED,只有 1 颗能被用户控制(其余为电源指示灯)。用户 LED 由 GPIO 控制,出厂系统用 Linux 标准 **LED 驱动框架**注册,**没有 `/dev` 设备节点**,只能通过 sysfs 控制。
+
+---
+
+## 1. 应用层操控硬件的两种方式
+
+Linux 下"一切皆文件",应用层操控硬件同样通过文件 I/O 实现,但有两条路:
+
+| 方式 | 位置 | 特点 | 典型设备 |
+| ---- | ---- | ---- | -------- |
+| 设备节点 | `/dev/` 下 | 驱动自定义接口,常配合 `ioctl`;接口未必标准化 | LCD、触摸屏、摄像头、串口 |
+| sysfs 属性文件 | `/sys/` 下 | 内核把设备属性导出成文件,读写文件即读写属性;简单设备常用 | LED、GPIO、PWM |
+
+`/dev` 下的文件称为**设备节点**,是硬件设备向应用层提供的接口。而 sysfs 则是另一条途径:把内核对象(硬件设备)导出成**目录**、对象属性导出成**文件**、对象关系导出成**链接文件**。
+
+**标准接口与非标准接口**:Linux 为常见设备类(LED、输入、FrameBuffer、video、PWM 等)设计了**设备驱动框架**,框架为应用层提供统一接口,同时降低驱动开发难度。使用了 LED 框架注册的 LED 就是"标准设备";不用框架自己写,接口就是非标准的。杂散/杂项类设备(misc device)无法归入任何标准分类,接口通常非标准,只有驱动工程师知道怎么用——很多嵌入式外设驱动都是定制的。
+
+```mermaid
+flowchart TD
+    A[应用程序] --> B{操控方式}
+    B -->|设备节点 /dev/xxx| C[驱动自定义接口]
+    C --> C1[ioctl / read / write]
+    B -->|sysfs 属性文件 /sys/...| D[内核导出属性]
+    D --> D1[echo / cat]
+    D --> D2[应用层 write / read]
+    C1 --> E[硬件 GPIO/LED]
+    D2 --> E
+```
+
+---
+
+## 2. sysfs 文件系统与 /sys 目录
+
+sysfs 是**基于内存的虚拟文件系统**,与 proc 类似,作用是把内核信息以文件形式提供给应用层。它把系统上的设备和总线组织成分级文件,展示设备驱动模型中各组件的层次关系。挂载点是 `/sys`。
+
+| 内核要素 | sysfs 中的表现 |
+| -------- | -------------- |
+| 内核对象(如硬件设备) | 目录 |
+| 对象属性(如设备属性) | 文件 |
+| 对象关系 | 链接文件 |
+
+/sys 下的主要子目录:
+
+| 子目录 | 说明 |
+| ------ | ---- |
+| `/sys/devices` | 系统中所有设备存放处,是 sysfs 最重要的目录结构 |
+| `/sys/block` | 块设备目录(过时接口,多链接到 `/sys/devices`) |
+| `/sys/bus` | 按总线类型分类(如 I2C 设备挂在 I2C 总线下),多为链接 |
+| `/sys/class` | 按**功能**分类(如 `/sys/class/leds`、`/sys/class/input`),多为链接 |
+| `/sys/dev` | 按`主设备号:次设备号`命名,链接到 `/sys/devices` |
+| `/sys/firmware` | 内核固件描述 |
+| `/sys/fs` | 系统所有文件系统及挂载点 |
+| `/sys/kernel` | 内核可调参数 |
+| `/sys/module` | 所有模块信息 |
+| `/sys/power` | 电源选项,可控制系统电源状态 |
+
+读属性文件 = 读设备属性;写属性文件 = 设置属性、控制设备。
+
+---
+
+## 3. LED 子系统
+
+### 3.1 sys-led 设备目录
+
+进入 `/sys/class/leds`,可以看到 `sys-led` 文件夹,这就是底板上的用户 LED。进入其中,主要关注三个属性文件:
+
+| 属性文件 | 读写 | 含义 |
+| -------- | ---- | ---- |
+| `brightness` | 可读可写 | 设置/获取亮度等级。`0` 表示灭,正整数表示亮,值越大越亮(PWM 控制的 LED 有亮度等级;GPIO 控制的只有亮/灭两态) |
+| `max_brightness` | 只读 | 最大亮度等级 |
+| `trigger` | 可读可写 | 触发模式,读为当前模式,写为设置模式 |
+
+> 注意:对本文这块 GPIO 控制的 LED,只有亮和灭两种状态,不存在连续亮度等级。
+
+### 3.2 trigger 触发模式
+
+`cat trigger` 会列出所有支持的触发模式,方括号 `[...]` 括起来的是当前模式:
+
+| 模式 | 说明 |
+| ---- | ---- |
+| `none` | 无触发,LED 由 brightness 手动控制 |
+| `mmc0` | 对 mmc0 设备读写时 LED 闪烁 |
+| `timer` | 由定时器控制,有规律地一明一灭 |
+| `heartbeat` | 心跳呼吸模式,模仿心跳亮灭变化 |
+
+系统启动后通常把某颗 LED 设为 `heartbeat` 作为系统运行指示灯。
+
+### 3.3 echo 命令控制
+
+```bash
+echo timer > trigger       # 将 LED 触发模式设置为 timer
+echo none > trigger        # 将 LED 触发模式设置为 none
+echo 1 > brightness        # 点亮 LED
+echo 0 > brightness        # 熄灭 LED
+```
+
+> Tips:`cat` 读取和 `echo` 写入属性文件的都是**字符串**。应用层用 `write()` 写属性文件时同样要写字符串;`read()` 读到的也是 ASCII 字符串。
+
+```mermaid
+flowchart TD
+    A[写 trigger=none] --> B[关闭自动触发]
+    B --> C[写 brightness=1]
+    C --> D[LED 常亮]
+    B --> E[写 brightness=0]
+    E --> F[LED 熄灭]
+    G[写 trigger=heartbeat] --> H[LED 自动心跳闪烁]
+```
+
+---
+
+## 4. 完整例程:LED 应用程序(led.c)
+
+例程路径:开发板光盘 → `11、Linux C 应用编程例程源码` → `15_led` → `led.c`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+
+#define  LED_TRIGGER    "/sys/class/leds/sys-led/trigger"
+#define  LED_BRIGHTNESS "/sys/class/leds/sys-led/brightness"
+#define  USAGE()    fprintf(stderr, "usage:\n"  \
+                "    %s <on|off>\n"   \
+                "    %s <trigger> <type>\n", argv[0], argv[0])
+
+int main(int argc, char *argv[])
+{
+    int fd1, fd2;
+
+    /* 校验传参 */
+    if (2 > argc) {
+        USAGE();
+        exit(-1);
+    }
+
+    /* 打开文件 */
+    fd1 = open(LED_TRIGGER, O_RDWR);
+    if (0 > fd1) {
+        perror("open error");
+        exit(-1);
+    }
+
+    fd2 = open(LED_BRIGHTNESS, O_RDWR);
+    if (0 > fd2) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 根据传参控制LED */
+    if (!strcmp(argv[1], "on")) {
+        write(fd1, "none", 4); 	//先将触发模式设置为none
+        write(fd2, "1", 1); 		//点亮LED
+    }
+    else if (!strcmp(argv[1], "off")) {
+        write(fd1, "none", 4); 	//先将触发模式设置为none
+        write(fd2, "0", 1); 		//LED灭
+    }
+    else if (!strcmp(argv[1], "trigger")) {
+        if (3 != argc) {
+            USAGE();
+            exit(-1);
+        }
+
+        if (0 > write(fd1, argv[2], strlen(argv[2])))
+            perror("write error");
+    }
+    else
+        USAGE();
+
+    exit(0);
+}
+```
+
+逐段解释:
+
+- 两个宏分别指向 `trigger` 和 `brightness` 属性文件;`USAGE()` 用于打印用法。
+- `open` 两个属性文件,拿到两个 fd(`O_RDWR` 读写)。
+- 参数为 `on`:先把 `"none"` 写入 `trigger` 关闭自动触发(否则 `heartbeat` 等模式会覆盖 brightness 的效果),再写 `"1"` 点亮。
+- 参数为 `off`:同样先设 `none`,再写 `"0"` 熄灭。
+- 参数为 `trigger`:把第二个参数(如 `heartbeat`)写入 `trigger`,长度用 `strlen(argv[2])`,不能写死。
+
+交叉编译:
+
+```bash
+arm-linux-gnueabihf-gcc led.c -o testApp
+file testApp          # 应为 32 位 ARM 可执行文件
+```
+
+---
+
+## 5. GPIO sysfs 接口
+
+### 5.1 /sys/class/gpio 目录概览
+
+进入 `/sys/class/gpio`,可以看到两个文件和 5 个 `gpiochipX` 文件夹:
+
+| 条目 | 说明 |
+| ---- | ---- |
+| `gpiochip0/32/64/96/128` | I.MX6UL/I.MX6ULL 的 5 个 GPIO 控制器,分别对应 GPIO1~GPIO5 |
+| `export` | 只写。把指定编号的 GPIO 引脚导出,导出的引脚才能使用;写入后在 `/sys/class/gpio` 下生成 `gpioX` 文件夹 |
+| `unexport` | 只写。删除已导出的 GPIO 引脚,写入后对应 `gpioX` 文件夹消失 |
+
+### 5.2 gpiochipX 与引脚编号
+
+每个 `gpiochipX` 目录下关注三个只读属性文件:
+
+| 属性文件 | 说明 |
+| -------- | ---- |
+| `base` | 该控制器管理的引脚组中最小的编号,与 `gpiochipX` 中的 X 相同 |
+| `label` | 该组 GPIO 的名字 |
+| `ngpio` | 该控制器管理的引脚数量(编号范围 `base ~ base+ngpio-1`) |
+
+I.MX6ULL 的映射关系:
+
+| 控制器 | gpiochip | base |
+| ------ | -------- | ---- |
+| GPIO1 | gpiochip0 | 0 |
+| GPIO2 | gpiochip32 | 32 |
+| GPIO3 | gpiochip64 | 64 |
+| GPIO4 | gpiochip96 | 96 |
+| GPIO5 | gpiochip128 | 128 |
+
+**编号计算公式**:`sysfs 编号 = 该组 base + IO 序号`。
+
+例:`GPIO4_IO16` → GPIO4 对应 gpiochip96,96 + 16 = **112**;`GPIO3_IO20` → 64 + 20 = **84**。
+
+导出/删除:
+
+```bash
+echo 0 > export      # 导出编号 0 的 GPIO(GPIO1_IO0)
+echo 0 > unexport    # 删除已导出的编号 0 的 GPIO
+```
+
+> 注意:并不是所有 GPIO 都能成功导出。如果该引脚已被内核(某个驱动)使用,导出会失败并打印错误信息,需要换一个未被占用的引脚。
+>
+> **只有在输入模式下**,才能用 `cat value` 获取引脚输入电平;输出模式下 `cat value` 得不到电平,需要用万用表或外接 LED 检测。
+
+```mermaid
+flowchart LR
+    A[GPIO 编号] --> B{查 gpiochip base}
+    B --> C[base + IO序号 = sysfs编号]
+    C --> D[写入 /sys/class/gpio/export]
+    D --> E[生成 /sys/class/gpio/gpioX/]
+    E --> F[direction / value / active_low / edge]
+```
+
+### 5.3 gpioX 属性文件
+
+导出成功后生成的 `gpioX` 目录下,重点关注四个属性文件:
+
+| 属性文件 | 读写 | 取值 | 说明 |
+| -------- | ---- | ---- | ---- |
+| `direction` | 可读可写 | `"out"` / `"in"` | 配置为输出或输入模式 |
+| `value` | 可读可写 | `"0"` / `"1"` | 输出模式:写 0 输出低、写 1 输出高;输入模式:读获取当前电平 |
+| `active_low` | 可读可写 | `"0"` / `"1"` | 极性控制,默认 0 |
+| `edge` | 可读可写 | `"none"`/`"rising"`/`"falling"`/`"both"` | 中断触发方式(配置前需先设输入模式) |
+
+`active_low` 的极性作用:
+
+```bash
+# active_low 等于 0 时
+echo "0" > active_low
+echo "out" > direction
+echo "1" > value   # 输出高
+echo "0" > value   # 输出低
+
+# active_low 等于 1 时
+echo "1" > active_low
+echo "out" > direction
+echo "1" > value   # 输出低
+echo "0" > value   # 输出高
+```
+
+| `edge` 取值 | 触发方式 |
+| ----------- | -------- |
+| `none` | 非中断引脚 |
+| `rising` | 上升沿触发 |
+| `falling` | 下降沿触发 |
+| `both` | 边沿触发(上升沿和下降沿) |
+
+配置中断后可用 `poll()` 监听引脚电平变化。
+
+---
+
+## 6. 完整例程:GPIO 输出(gpio_out.c)
+
+例程路径:`11、Linux C 应用编程例程源码` → `16_gpio` → `gpio_out.c`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+
+static char gpio_path[100];
+
+static int gpio_config(const char *attr, const char *val)
+{
+    char file_path[100];
+    int len;
+    int fd;
+
+    sprintf(file_path, "%s/%s", gpio_path, attr);
+    if (0 > (fd = open(file_path, O_WRONLY))) {
+        perror("open error");
+        return fd;
+    }
+
+    len = strlen(val);
+    if (len != write(fd, val, len)) {
+        perror("write error");
+        close(fd);
+        return -1;
+    }
+
+    close(fd);  //关闭文件
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    /* 校验传参 */
+    if (3 != argc) {
+        fprintf(stderr, "usage: %s <gpio> <value>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 判断指定编号的GPIO是否导出 */
+    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
+
+    if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
+
+        int fd;
+        int len;
+
+        if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
+            perror("open error");
+            exit(-1);
+        }
+
+        len = strlen(argv[1]);
+        if (len != write(fd, argv[1], len)) {//导出gpio
+            perror("write error");
+            close(fd);
+            exit(-1);
+        }
+
+        close(fd);  //关闭文件
+    }
+
+    /* 配置为输出模式 */
+    if (gpio_config("direction", "out"))
+        exit(-1);
+
+    /* 极性设置 */
+    if (gpio_config("active_low", "0"))
+        exit(-1);
+
+    /* 控制GPIO输出高低电平 */
+    if (gpio_config("value", argv[2]))
+        exit(-1);
+
+    /* 退出程序 */
+    exit(0);
+}
+```
+
+逐段解释:
+
+- 全局 `gpio_path` 保存当前 GPIO 的目录路径 `/sys/class/gpio/gpioX`。
+- `gpio_config(attr, val)` 是通用封装:拼出 `gpio_path/attr`,以只写打开,把字符串 `val` 写入,检查写入长度是否等于 `strlen(val)`,最后关闭。这样 `direction`/`active_low`/`value` 都能复用。
+- `main` 需要两个参数:`argv[1]` 是 GPIO 编号,`argv[2]` 是输出电平。
+- 先用 `access(gpio_path, F_OK)` 判断是否已导出,目录不存在才写 `export` 导出。
+- 然后依次配置为输出模式、极性为 0,最后写 `value` 输出目标电平。
+- 用法示例:`./testApp 1 1` 让 GPIO1_IO01 输出高,`./testApp 1 0` 输出低。
+
+---
+
+## 7. 完整例程:GPIO 输入(gpio_in.c)
+
+例程路径:`16_gpio` → `gpio_in.c`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+
+static char gpio_path[100];
+
+static int gpio_config(const char *attr, const char *val)
+{
+    char file_path[100];
+    int len;
+    int fd;
+
+    sprintf(file_path, "%s/%s", gpio_path, attr);
+    if (0 > (fd = open(file_path, O_WRONLY))) {
+        perror("open error");
+        return fd;
+    }
+
+    len = strlen(val);
+    if (len != write(fd, val, len)) {
+        perror("write error");
+        close(fd);
+        return -1;
+    }
+
+    close(fd);  //关闭文件
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    char file_path[100];
+    char val;
+    int fd;
+
+    /* 校验传参 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 判断指定编号的GPIO是否导出 */
+    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
+
+    if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
+
+        int len;
+
+        if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
+            perror("open error");
+            exit(-1);
+        }
+
+        len = strlen(argv[1]);
+        if (len != write(fd, argv[1], len)) {//导出gpio
+            perror("write error");
+            close(fd);
+            exit(-1);
+        }
+
+        close(fd);  //关闭文件
+    }
+
+    /* 配置为输入模式 */
+    if (gpio_config("direction", "in"))
+        exit(-1);
+
+    /* 极性设置 */
+    if (gpio_config("active_low", "0"))
+        exit(-1);
+
+    /* 配置为非中断方式 */
+    if (gpio_config("edge", "none"))
+        exit(-1);
+
+    /* 读取GPIO电平状态 */
+    sprintf(file_path, "%s/%s", gpio_path, "value");
+
+    if (0 > (fd = open(file_path, O_RDONLY))) {
+        perror("open error");
+        exit(-1);
+    }
+
+    if (0 > read(fd, &val, 1)) {
+        perror("read error");
+        close(fd);
+        exit(-1);
+    }
+
+    printf("value: %c\n", val);
+
+    /* 退出程序 */
+    close(fd);
+    exit(0);
+}
+```
+
+逐段解释:
+
+- 传参只需一个:`argv[1]` 是 GPIO 编号。
+- 与输出程序一样,先判断是否导出,未导出则写 `export`。
+- 配置流程改成:`direction = "in"`、`active_low = "0"`、`edge = "none"`(非中断)。
+- 打开 `value` 文件(只读),`read` 一个字节,`printf("value: %c\n", val)` 打印 `'0'` 或 `'1'`——读到的是 ASCII 字符,直接当字符打印。
+- 用法示例:`./testApp 1` 读取 GPIO1_IO01 当前电平。
+
+---
+
+## 8. 完整例程:GPIO 中断(gpio_intr.c)
+
+例程路径:`16_gpio` → `gpio_intr.c`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <poll.h>
+
+static char gpio_path[100];
+
+static int gpio_config(const char *attr, const char *val)
+{
+    char file_path[100];
+    int len;
+    int fd;
+
+    sprintf(file_path, "%s/%s", gpio_path, attr);
+    if (0 > (fd = open(file_path, O_WRONLY))) {
+        perror("open error");
+        return fd;
+    }
+
+    len = strlen(val);
+    if (len != write(fd, val, len)) {
+        perror("write error");
+        return -1;
+    }
+
+    close(fd);  //关闭文件
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    struct pollfd pfd;
+    char file_path[100];
+    int ret;
+    char val;
+
+    /* 校验传参 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 判断指定编号的GPIO是否导出 */
+    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
+
+    if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
+
+        int len;
+        int fd;
+
+        if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
+            perror("open error");
+            exit(-1);
+        }
+
+        len = strlen(argv[1]);
+        if (len != write(fd, argv[1], len)) {//导出gpio
+            perror("write error");
+            exit(-1);
+        }
+
+        close(fd);  //关闭文件
+    }
+
+    /* 配置为输入模式 */
+    if (gpio_config("direction", "in"))
+        exit(-1);
+
+    /* 极性设置 */
+    if (gpio_config("active_low", "0"))
+        exit(-1);
+
+    /* 配置中断触发方式: 上升沿和下降沿 */
+    if (gpio_config("edge", "both"))
+        exit(-1);
+
+    /* 打开value属性文件 */
+    sprintf(file_path, "%s/%s", gpio_path, "value");
+
+    if (0 > (pfd.fd = open(file_path, O_RDONLY))) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 调用poll */
+    pfd.events = POLLPRI; //只关心高优先级数据可读(中断)
+
+    read(pfd.fd, &val, 1);//先读取一次清除状态
+    for ( ; ; ) {
+
+        ret = poll(&pfd, 1, -1);    //调用poll
+        if (0 > ret) {
+            perror("poll error");
+            exit(-1);
+        }
+        else if (0 == ret) {
+            fprintf(stderr, "poll timeout.\n");
+            continue;
+        }
+
+        /* 校验高优先级数据是否可读 */
+        if(pfd.revents & POLLPRI) {
+            if (0 > lseek(pfd.fd, 0, SEEK_SET)) {//将读位置移动到头部
+                perror("lseek error");
+                exit(-1);
+            }
+
+            if (0 > read(pfd.fd, &val, 1)) {
+                perror("read error");
+                exit(-1);
+            }
+
+            printf("GPIO中断触发<value=%c>\n", val);
+        }
+    }
+
+    /* 退出程序 */
+    exit(0);
+}
+```
+
+逐段解释与关键点:
+
+- 配置 `edge = "both"`,即上升沿和下降沿都触发中断。
+- **为什么用 `poll` 而不是直接读?** 因为直接 `read` 只有在中断发生(`value` 文件高优先级可读)时才有意义,且无法一次性等待多个事件源。`poll` 可以监视文件描述符上的 I/O 状态变化,`POLLPRI` 正表示"有高优先级数据可读取",**中断就是一种高优先级事件**。GPIO 中断场景要用 `POLLPRI`,不是 `POLLIN`。
+- **为什么先 `read` 一次?** 打开的 `value` 文件可能带有上一次残留的状态,先读一次清除,之后 `poll` 才代表真正的新中断。
+- `poll(&pfd, 1, -1)` 中 `-1` 表示一直阻塞等待,直到有中断触发或捕获信号。
+- 触发后用 `lseek(pfd.fd, 0, SEEK_SET)` 把读位置移回文件头,再 `read` 取出当前 `value`,然后打印。
+- 退出方式:`Ctrl+C`。
+
+中断也完全可以用第 3 节的**异步 I/O**(`O_ASYNC` + `SIGIO`/实时信号)来监视,原理相同。
+
+```mermaid
+sequenceDiagram
+    participant App as 应用程序
+    participant Kernel as 内核/GPIO驱动
+    App->>Kernel: 配置 edge=both
+    App->>Kernel: poll(value, POLLPRI, -1) 阻塞
+    Kernel-->>App: 引脚电平跳变(高低切换)
+    Kernel->>App: POLLPRI 就绪
+    App->>Kernel: lseek + read value
+    Kernel-->>App: 返回 '0' 或 '1'
+    App->>App: 打印"GPIO中断触发"
+```
+
+---
+
+## 9. 设备节点 /dev/led 与 ioctl 控制
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书第 15 章明确说明开发板出厂系统的用户 LED 由 LED 驱动框架注册、**没有 `/dev` 设备节点**,使用 sysfs 方式控制。以下是另一类常见做法——用**杂项字符设备**提供 `/dev/led` 节点,并用 `ioctl` 控制,属于自定义非标准接口。
+
+当 LED 驱动不使用 LED 框架、而是注册为杂项字符设备(misc device)时,应用层就通过 `/dev/led` 设备节点操控,接口形式通常是 `open` + `ioctl`(也可用 `write`)。
+
+**内核侧提供的能力(示意)**:
+
+```c
+#include <linux/ioctl.h>
+
+#define LED_IOCTL_MAGIC     'L'
+#define LED_ON              _IO(LED_IOCTL_MAGIC, 0)
+#define LED_OFF             _IO(LED_IOCTL_MAGIC, 1)
+#define LED_SET_BLINK       _IOW(LED_IOCTL_MAGIC, 2, int)  //参数为闪烁周期ms
+```
+
+| 宏 | 方向 | 用途 |
+| -- | ---- | ---- |
+| `_IO(type, nr)` | 无参数 | 纯命令,如开/关 |
+| `_IOW(type, nr, type)` | 用户→内核 | 下发一个整型等参数 |
+| `_IOR(type, nr, type)` | 内核→用户 | 读取内核返回的数据 |
+| `_IOWR(type, nr, type)` | 双向 | 既下发又回读 |
+
+**应用侧完整例程**:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/ioctl.h>
+
+#define LED_DEV_PATH        "/dev/led"
+
+#define LED_IOCTL_MAGIC     'L'
+#define LED_ON              _IO(LED_IOCTL_MAGIC, 0)
+#define LED_OFF             _IO(LED_IOCTL_MAGIC, 1)
+#define LED_SET_BLINK       _IOW(LED_IOCTL_MAGIC, 2, int)
+
+int main(int argc, char *argv[])
+{
+    int fd;
+
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <on|off|blink>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开 LED 设备节点 */
+    fd = open(LED_DEV_PATH, O_RDWR);
+    if (0 > fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    if (!strcmp(argv[1], "on")) {
+        if (0 > ioctl(fd, LED_ON))
+            perror("ioctl LED_ON error");
+    }
+    else if (!strcmp(argv[1], "off")) {
+        if (0 > ioctl(fd, LED_OFF))
+            perror("ioctl LED_OFF error");
+    }
+    else if (!strcmp(argv[1], "blink")) {
+        int period = 500;   //闪烁周期 500ms
+        if (0 > ioctl(fd, LED_SET_BLINK, &period))
+            perror("ioctl LED_SET_BLINK error");
+    }
+    else {
+        fprintf(stderr, "usage: %s <on|off|blink>\n", argv[0]);
+    }
+
+    close(fd);
+    return 0;
+}
+```
+
+`ioctl` 原型与要点:
+
+```c
+#include <sys/ioctl.h>
+int ioctl(int fd, unsigned long request, ... /* arg */);
+```
+
+| 项 | 说明 |
+| -- | ---- |
+| `fd` | 设备节点打开得到的文件描述符 |
+| `request` | 命令码,由 `_IO`/`_IOW`/`_IOR`/`_IOWR` 生成,包含方向、类型、序号、数据大小 |
+| `arg` | 可选参数,通常是整型或无类型指针;`_IOW` 表示的参数按指针传递 |
+| 返回值 | 通常成功返回 0,失败返回 -1 并设置 `errno` |
+
+设备节点出现的前提是内核驱动已注册并创建了该节点(或由 `mdev`/`udev` 根据设备号创建)。没有对应驱动时,`open("/dev/led")` 会报 `No such file or directory`。
+
+---
+
+## 10. 实验步骤与调试方法
+
+### 10.1 交叉编译
+
+```bash
+# 方式一:直接使用交叉编译器
+arm-linux-gnueabihf-gcc led.c       -o testApp_led
+arm-linux-gnueabihf-gcc gpio_out.c  -o testApp_out
+arm-linux-gnueabihf-gcc gpio_in.c   -o testApp_in
+arm-linux-gnueabihf-gcc gpio_intr.c -o testApp_intr
+
+# 方式二:正点原子光盘工具链(先 source 环境脚本,再使用 $CC)
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+$CC led.c -o testApp_led
+
+# 查看文件类型,确认是 ARM 可执行文件
+file testApp_led
+```
+
+### 10.2 拷贝到开发板
+
+```bash
+scp testApp_led testApp_out testApp_in testApp_intr root@<开发板IP>:/home/root/
+```
+
+进入开发板 Linux 系统,在 `~/` 下执行。
+
+### 10.3 运行与验证
+
+**LED(15_led)**:
+
+```bash
+./testApp_led on                 # 点亮 LED
+./testApp_led off                # 熄灭 LED
+./testApp_led trigger heartbeat  # 设置为心跳模式
+./testApp_led trigger timer      # 设置为定时器闪烁
+```
+
+观察板上用户 LED 状态是否与命令一致。
+
+**GPIO 输出(16_gpio,以 GPIO1_IO01 为例,sysfs 编号 = 0 + 1 = 1)**:
+
+```bash
+./testApp_out 1 1    # 控制 GPIO1_IO01 输出高电平
+./testApp_out 1 0    # 控制 GPIO1_IO01 输出低电平
+```
+
+用万用表测量或用 LED 小灯(串限流电阻)连接引脚验证。
+
+**GPIO 输入**:
+
+```bash
+# 用杜邦线把 GPIO1_IO01 接 3.3V,读取
+./testApp_in 1       # 期望打印 value: 1(高电平)
+
+# 再改接到 GND,读取
+./testApp_in 1       # 期望打印 value: 0(低电平)
+```
+
+**GPIO 中断**:
+
+```bash
+./testApp_intr 1     # 监测 GPIO1_IO01 中断
+```
+
+运行后用杜邦线在 GND 与 3.3V 之间来回切换,使引脚电平发生由高到低或由低到高的变化,终端会打印 `GPIO中断触发<value=x>`。测试完毕按 `Ctrl+C` 退出。
+
+### 10.4 常见问题排查
+
+| 现象 | 原因/处理 |
+| ---- | --------- |
+| `echo N > export` 报错 | 该 GPIO 已被内核/驱动占用;换未被占用的引脚 |
+| 输出模式下 `cat value` 无意义 | 输出模式不能用 `cat value` 读电平,需外接 LED 或万用表 |
+| 写 `brightness` 后 LED 仍闪烁 | 没有先把 `trigger` 设为 `none`,触发模式覆盖了亮度控制 |
+| 中断只触发一次 | 未 `lseek` 回文件头或未先 `read` 清状态 |
+| `open /dev/led` 失败 | 内核未注册对应 misc 驱动,没有创建设备节点 |
+
+---
+
+## 11. 跨平台对比(IMX6ULL vs STM32 vs RK3568)
+
+| 维度 | IMX6ULL(Cortex-A7 + Linux) | STM32(Cortex-M) | RK3568(Cortex-A55 + Linux) |
+| ---- | ---------------------------- | ----------------- | ---------------------------- |
+| GPIO 控制方式 | 应用层 sysfs(`/sys/class/gpio`、`/sys/class/leds`)或 `/dev` 节点 | 直接读写寄存器 / HAL 库(`HAL_GPIO_WritePin`) | 同 IMX6ULL,也常用 pinctrl/gpio 子系统 |
+| 抽象层次 | 内核 pinctrl/gpio 子系统 + 设备驱动框架 | 寄存器/厂商库,无统一框架 | 内核 pinctrl/gpio 子系统 |
+| LED 控制 | LED 框架导出 `/sys/class/leds/*/brightness`、`trigger` | 普通 GPIO,无 LED 子系统(自行实现) | LED 框架,同 IMX6ULL |
+| 中断 | 配置 `edge` 后 `poll(POLLPRI)` 或异步 I/O | EXTI 外部中断 + 中断服务函数 | 同 IMX6ULL,内核中断子系统 |
+| 性能 | 低配 Linux,适合工业 HMI | 实时性强、成本低,适合实时控制节点 | 高性能 Linux,适合边缘计算/视频 |
+| 开发门槛 | 需懂 sysfs/驱动模型 | 需懂寄存器/外设 | 需懂 sysfs/设备树/pinctrl |
+
+结论:STM32 是"直接操作硬件",Cortex-A 平台是"通过内核子系统和文件接口间接操作硬件"。同一份 sysfs GPIO 代码在 IMX6ULL 与 RK3568 上基本通用,差别只在编号映射与设备树配置。
+
+---
+
+## 12. 深度面试题
+
+### Q1:应用层操控硬件,`/dev` 设备节点和 `/sys` 属性文件有什么区别?什么时候用哪种?
+
+**答**:`/dev` 下的设备节点由驱动注册,提供 `open/read/write/ioctl` 等接口,接口往往由驱动自定义,能力丰富(如 LCD、摄像头这种复杂设备需要大量 ioctl 命令和缓冲区);`/sys` 是 sysfs 虚拟文件系统,内核把设备对象导出成目录、把设备属性导出成文件,应用层用普通文件 I/O 读写属性即可。
+
+选择取决于设备和驱动的实现:结构简单的设备(LED、GPIO、部分 PWM)常用 sysfs,驱动把属性直接导出,接口标准化;复杂设备(LCD、触摸屏、摄像头、音频)通常用 `/dev` 节点加 ioctl。标准设备驱动框架(如 LED 框架)会为应用层提供统一接口,降低使用和移植成本;非框架的杂项设备接口就是定制的。
+
+### Q2:GPIO4_IO16 在 sysfs 中的编号是多少?为什么有时 `export` 会失败?
+
+**答**:I.MX6ULL 有 5 个 GPIO 控制器,分别对应 gpiochip0/32/64/96/128(base 为 0/32/64/96/128)。GPIO4 对应 base=96,所以编号 = 96 + 16 = **112**。
+
+`export` 失败通常是因为该 GPIO 引脚**已经被内核中的某个驱动占用**(被其它设备驱动申请了)。一个 GPIO 一旦被内核使用,就不能再通过 sysfs 导出。解决办法是确认引脚复用情况、换一个空闲引脚,或修改设备树释放该引脚。
+
+### Q3:`gpio_intr.c` 里为什么用 `POLLPRI` 而不是 `POLLIN`?为什么在 `poll` 前要先 `read` 一次?
+
+**答**:`POLLIN` 表示普通优先级数据可读,`POLLPRI` 表示**有高优先级数据可读**。GPIO 中断在 sysfs 中被建模成"高优先级事件",触发时 `value` 文件上产生的是 `POLLPRI` 就绪,因此要监视 `POLLPRI`;用 `POLLIN` 无法正确捕捉。
+
+`poll` 前先 `read(pfd.fd, &val, 1)` 是为了**清除上一次残留的就绪状态**。文件描述符可能带着历史状态,若不先读掉,第一次 `poll` 可能立即返回一个"假中断"。读一次后,后续 `poll` 返回才代表真正发生了新的边沿跳变。
+
+### Q4:LED 的 `trigger` 和 `brightness` 是什么关系?为什么控制亮灭前常要先把 `trigger` 写成 `none`?
+
+**答**:`brightness` 直接控制 LED 的亮/灭(或亮度等级),但只在该 LED 处于手动模式时有效;`trigger` 是自动触发模式,启用后由内核根据事件(定时器、mmc 读写、心跳)自动改写 LED 状态,会覆盖应用层对 `brightness` 的设置。
+
+所以想让 `write(brightness)` 稳定生效,必须先把 `trigger` 写成 `none` 关闭自动触发,再写 `brightness`。否则 LED 会继续按 `heartbeat`/`timer` 的节奏闪烁,看起来"命令没生效"。反过来,若想要呼吸灯或系统状态指示,就直接设置 `trigger` 为 `heartbeat`。
+
+### Q5:应用层直接操作 sysfs GPIO 和在内核驱动里操作 GPIO,各有什么优劣?
+
+**答**:**应用层 sysfs**:无需写/编译内核驱动,开发和调试快,改动灵活(脚本即可控制);但每次读写都要经过文件系统层、用户态/内核态切换,**性能与实时性差**,不适合高频翻转(软件 PWM、高速时序);且引脚被占用或未导出时无法操作,权限/并发控制也弱。
+
+**内核驱动**:直接操作 pinctrl/gpio 子系统,能配置复用功能、上下拉、驱动能力、中断,速度快、时序可控,可提供稳定标准接口并做资源管理;但需要懂驱动模型、设备树,开发与调试成本高,改动要重编内核/模块。
+
+工程取舍:调试、低频控制用 sysfs 快速验证;产品化、高频或需要精确时序(PWM、编码器、高速中断)时写成内核驱动或在驱动里实现。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十五章 点亮LED、第十六章 GPIO应用编程 + 例程路径:开发板光盘 `11、Linux C 应用编程例程源码\15_led`、`16_gpio`

+ 798 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/03-输入设备与tslib.md

@@ -0,0 +1,798 @@
+---
+title: 输入设备与tslib
+tags: [嵌入式Linux, Linux应用编程, input子系统, 输入设备, 触摸屏, tslib, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十七章 输入设备应用编程、第十八章 使用tslib库"
+---
+
+# 输入设备与tslib
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]];延伸阅读:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/08-misc与input子系统]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]]
+
+输入设备(鼠标、键盘、触摸屏、按键……)把用户动作变成数据交给系统。Linux 用 **input 子系统**统一了它们的接口:应用层只要 `open` 一个 `/dev/input/eventX`,再 `read` 出 `struct input_event`,就得到原始输入数据。本篇先讲清 input 子系统的数据格式与解析方法(按键、单点触摸、多点触摸),再引入 **tslib** —— 把"读原始 event + 去噪 + 坐标变换"封装好的触摸屏应用层函数库。
+
+---
+
+## 1. 输入设备与 input 子系统
+
+**输入设备**(input 设备)指能够产生输入事件的设备:鼠标、键盘、触摸屏、遥控器、画图板等。设备种类繁多、上报数据类型各异,Linux 为统一管理实现了 **input 子系统**:
+
+- 驱动人员基于它开发驱动,它屏蔽硬件差异、向应用层提供统一接口;注册成功的设备在 `/dev/input` 下生成 `eventX` 节点,应用层读取节点即可获取数据。
+
+```mermaid
+flowchart TB
+    subgraph 用户态
+        APP["应用程序 open/read /dev/input/eventX"]
+    end
+    subgraph 内核态
+        CORE["input 子系统核心"]
+        DRV1["按键驱动"]
+        DRV2["触摸屏驱动"]
+        DRV3["USB 键盘/鼠标驱动"]
+    end
+    HW["硬件 GPIO 按键 / 触摸 IC / USB 设备"]
+    APP -->|"read struct input_event"| CORE
+    CORE <--> DRV1
+    CORE <--> DRV2
+    CORE <--> DRV3
+    DRV1 --> HW
+    DRV2 --> HW
+    DRV3 --> HW
+```
+
+读取流程(以触摸屏 `/dev/input/event0` 为例):
+
+1. 应用打开设备文件;2. 发起 `read`,无数据可读时在阻塞 I/O 下休眠;3. 有数据时被唤醒、读操作返回;4. 应用解析数据。
+
+> Tips:设备文件不同于普通文件,读写设备文件之前**无需设置读写位置偏移量**。
+
+---
+
+## 2. struct input_event:应用层看到的数据单元
+
+应用层每次 `read` 得到一个 `struct input_event`,定义在 `<linux/input.h>`:
+
+```c
+struct input_event {
+    struct timeval time;
+    __u16 type;
+    __u16 code;
+    __s32 value;
+};
+```
+
+`time` 是事件发生时间(通常不是重点),重点是其馀三个成员:
+
+| 成员 | 含义 |
+| ---- | ---- |
+| `type` | 事件类型(哪一大类事件) |
+| `code` | 事件代码(该类中的哪一个具体事件) |
+| `value` | 事件值,解释随 `code` 变化 |
+事件类型宏(`<linux/input.h>`):
+
+```c
+
+#define EV_SYN   0x00  //同步类事件
+#define EV_KEY   0x01  //按键类事件
+#define EV_REL   0x02  //相对位移类事件(譬如鼠标)
+#define EV_ABS   0x03  //绝对位移类事件(譬如触摸屏)
+#define EV_MSC   0x04  //其它杂类事件
+/* 还有 EV_SW(0x05)、EV_LED(0x11)、EV_SND(0x12)、EV_REP(0x14) 等 */
+
+#define EV_MAX   0x1f
+#define EV_CNT   (EV_MAX+1)
+
+```
+常见 `code`:
+
+```c
+/* 按键类 */
+
+#define KEY_1    2    //数字 1 键
+#define KEY_A    30   //字母 A 键
+#define KEY_VOLUMEDOWN 114  //出厂系统 KEY0 使用的键值
+/* 相对位移类 */
+
+#define REL_X    0x00 //X 轴
+#define REL_Y    0x01 //Y 轴
+/* 绝对位移类(触摸屏) */
+
+#define ABS_X              0x00 //X 轴
+#define ABS_Y              0x01 //Y 轴
+#define ABS_MT_SLOT        0x2f //当前更新的触摸点 slot
+#define ABS_MT_POSITION_X  0x35 //触摸点 X 坐标
+#define ABS_MT_POSITION_Y  0x36 //触摸点 Y 坐标
+#define ABS_MT_TRACKING_ID 0x39 //触摸点 ID
+#define BTN_TOUCH          330  //触摸按下/松开
+
+```
+`value` 的解释随 `code` 变化:按键事件中 `value=1` 按下、`0` 松开、`2` 长按;绝对位移事件中 `code=ABS_X` 时 `value` 就是 X 坐标,`code=ABS_Y` 时就是 Y 坐标。这些宏定义在 `input-event-codes.h`,被 `<linux/input.h>` 包含。
+
+---
+
+## 3. 数据同步:EV_SYN 与 SYN_REPORT
+
+一次 `read` 只能读一个 event,而一个触摸点含 X、Y 等多项,需多次 `read` 才读全。内核把本轮数据全部上报后,会再上报一个**同步事件**告知"本轮数据已完整":
+
+```c
+#define SYN_REPORT     0    // 本轮数据完整
+#define SYN_DROPPED    3    // 数据丢失
+```
+
+所有输入设备都需上报同步事件,通常是 `SYN_REPORT`、`value` 通常为 0。
+
+---
+
+## 4. 读取 struct input_event 数据(read_input.c)
+
+例程 `read_input.c` 是最基础的读取框架:传参为设备节点路径,用 `open(argv[1], O_RDONLY)` 只读打开,在死循环中每次 `read` 恰好读出一个 event(阻塞 I/O,无数据时在此休眠),再把 `in_ev.type`、`in_ev.code`、`in_ev.value` 打印出来供手工分析。完整程序含 `#include`、传参校验与错误处理,见例程 `17_input/read_input.c`。
+
+```bash
+arm-linux-gnueabihf-gcc -o testApp read_input.c
+scp testApp root@192.168.1.10:/home/root/
+```
+
+### 4.1 在开发板上验证按键 KEY0
+
+ALPHA/Mini 都有一个用户按键 **KEY0**,出厂系统中其驱动基于 input 子系统。用 `cat /proc/bus/input/devices` 确定设备节点后运行程序,示例中 KEY0 对应 `/dev/input/event2`。按下、松开 KEY0 的输出为:
+
+```text
+type:1 code:114 value:1      // EV_KEY, code=114=KEY_VOLUMEDOWN, value=1 按下
+type:0 code:0 value:0        // EV_SYN/SYN_REPORT,本轮数据完整
+type:1 code:114 value:0      // 松开
+type:0 code:0 value:0        // 同步
+```
+
+长按(按住不放)时 `value=2`。
+
+---
+
+## 5. 按键应用编程(read_key.c)
+
+按键上报流程为 `KEY_A` → `SYN_REPORT`,`value` 1/0/2 分别表示按下/松开/长按:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <linux/input.h>
+int main(int argc, char *argv[])
+{
+    struct input_event in_ev = {0};
+    int fd = -1;
+    int value = -1;
+    /* 校验传参 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
+        exit(-1);
+    }
+    /* 打开文件 */
+    if (0 > (fd = open(argv[1], O_RDONLY))) {
+        perror("open error");
+        exit(-1);
+    }
+    for ( ; ; ) {
+        /* 循环读取数据 */
+        if (sizeof(struct input_event) !=
+            read(fd, &in_ev, sizeof(struct input_event))) {
+            perror("read error");
+            exit(-1);
+        }
+        if (EV_KEY == in_ev.type) { //按键事件
+            switch (in_ev.value) {
+            case 0:
+                printf("code<%d>: 松开\n", in_ev.code);
+                break;
+            case 1:
+                printf("code<%d>: 按下\n", in_ev.code);
+                break;
+            case 2:
+                printf("code<%d>: 长按\n", in_ev.code);
+                break;
+            }
+        }
+    }
+}
+```
+
+拿到数据后先判断 `EV_KEY`,再按 `value` 区分松开/按下/长按。
+
+```bash
+arm-linux-gnueabihf-gcc -o testApp read_key.c
+scp testApp root@192.168.1.10:/home/root/
+./testApp /dev/input/event2      # 测试 KEY0
+```
+
+把 USB 键盘插到开发板 USB HOST 口,也可按同样方法在 `/proc/bus/input/devices` 找到 `eventX` 测试;根据 `code` 查 `input-event-codes.h` 即知按键,如 `code=30` 对应 A 键、`code=48` 对应 B 键。
+
+**读取多个输入设备**:一个进程一次只能阻塞读取一个设备节点。若要同时读取多个设备(如按键 + 触摸屏),可用 `poll()`/`select()` 同时监听多个 fd(见 [[03-外设与高级IO编程/01-高级IO]]),或为每个设备各开一个线程/进程;`input_event.time` 时间戳可用于还原不同设备事件的先后顺序。
+
+---
+
+## 6. 触摸屏:事件类型与上报流程
+
+触摸屏是**绝对位移设备**,上报 `EV_ABS`:
+
+| 类型 | 特点 | 承载事件 |
+| ---- | ---- | -------- |
+| 单点触摸 | 一轮完整数据只含一个触摸点 | `ABS_X`、`ABS_Y` 等 `ABS_XXX` |
+| 多点触摸 | 一轮完整数据可含多个触摸点 | `ABS_MT_POSITION_X/Y` 等 `ABS_MT_XXX` |
+
+除位移事件外还会上报:
+
+- **同步事件**:几乎每个输入设备都上报,告知本轮数据是否完整;
+- **按键事件 `BTN_TOUCH`**(`code=0x14a`,即 330):点击触摸屏或手指离开时上报,滑动时不上报。它不支持长按,`value` 不会等于 2;对多点设备,只有第一个点按下时 `value=1`、最后一个点离开时 `value=0`。
+
+单点设备上报顺序:
+
+```text
+# 点击        # 滑动        # 松开
+BTN_TOUCH     ABS_X         BTN_TOUCH
+ABS_X         ABS_Y         SYN_REPORT
+ABS_Y         SYN_REPORT
+SYN_REPORT
+```
+
+不同设备信息量不同(有的仅 X/Y,有的带按压力、接触面积),这些数据都会在 `SYN_REPORT` 之前上报。
+
+### 6.1 多点触摸与 MT Type B 协议
+
+Linux 用多点触摸(MT)协议上报各触摸点数据,分 Type A 与 Type B。**Type A 使用很少、几乎淘汰**,开发板配套触摸屏都属于 **Type B**。
+
+Type B 适用于能追踪并区分触摸点的设备,重点是用 `ABS_MT_SLOT` 上报各触摸点信息的更新:
+
+- **slot**:硬件概念,触摸点的编号,通常按触碰时间先后分配、从 0 开始;`ABS_MT_SLOT` 的 `value` 告知当前正在更新哪个 slot;
+- **tracking_id**:软件概念,由 `ABS_MT_TRACKING_ID` 上报,用于触摸点的创建、替换和销毁——`>=0` 为有效触摸点,`-1` 表示被移除,以前不存在的 ID 表示新触摸点;
+- Type B **只上报发生变更的数据**:若只改变 X 坐标,内核只发新的 `ABS_MT_POSITION_X`。
+
+```text
+ABS_MT_SLOT 0
+ABS_MT_TRACKING_ID 10
+ABS_MT_POSITION_X
+ABS_MT_POSITION_Y
+ABS_MT_SLOT 1
+ABS_MT_TRACKING_ID 11
+ABS_MT_POSITION_X
+ABS_MT_POSITION_Y
+SYN_REPORT
+```
+
+| 概念 | 层级 | 含义 |
+| ---- | ---- | ---- |
+| slot | 硬件 | 触摸点编号,按触碰先后分配 |
+| ID | 软件 | 触摸点生命周期的标识;手离开后销毁,再触碰即为新 ID |
+
+同一手指触碰、离开、再触碰,两次都是 slot=0,但生命周期不同,因此 ID 不同。
+
+### 6.2 上报数据实例
+
+一个手指点击触摸屏不松开,`read_input.c` 打印:
+
+要点:`ABS_MT_TRACKING_ID` 非负表示新建触摸点(按下);多点设备也通过 `ABS_X/ABS_Y` 上报坐标,但通常只有触摸点 0 支持,故可当单点用;**有的屏幕因触摸芯片不同没有 `EV_KEY`,也就没有 `BTN_TOUCH`**。增加第二个触摸点时会先上报 `ABS_MT_SLOT`(`code=47`)`value=1`;手指松开时上报 `ABS_MT_TRACKING_ID` 且 `value=-1`。
+
+---
+
+## 7. 获取触摸屏信息:ioctl 与 input_absinfo
+
+```c
+#include <sys/ioctl.h>
+int ioctl(int fd, unsigned long request, ...);
+```
+
+- `fd` 文件描述符;`request` 请求指令;可变参数随 `request` 决定。
+
+input 设备的请求指令宏(`input.h`):
+
+```c
+#define EVIOCGNAME(len)   _IOC(_IOC_READ, 'E', 0x06, len)   /* get device name */
+#define EVIOCGKEY(len)    _IOC(_IOC_READ, 'E', 0x18, len)   /* get global key state */
+#define EVIOCGABS(abs)    _IOR('E', 0x40 + (abs), struct input_absinfo) /* get abs value/limits */
+```
+
+`EVIOCG`(get)获取信息,`EVIOCS`(set)设置。重点 `EVIOCGABS(abs)` 可获取某 `ABS_XXX` 事件的取值范围,第三个参数为 `struct input_absinfo *`:
+
+```c
+struct input_absinfo {
+    __s32 value;        //最新的报告值
+    __s32 minimum;      //最小值
+    __s32 maximum;      //最大值
+    __s32 fuzz;
+    __s32 flat;
+    __s32 resolution;
+};
+
+```
+例程 `17_input/read_slot.c` 打开设备后执行:
+
+```c
+struct input_absinfo info;
+if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info))
+    perror("ioctl error");
+max_slots = info.maximum + 1 - info.minimum;
+printf("max_slots: %d\n", max_slots);
+
+```
+结果为 `max_slots: 5` 时说明是 5 点触摸屏,该值正是下面多点程序申请 `struct ts_mt` 数组的依据。
+
+## 8. 单点触摸应用程序(read_ts.c)
+
+把多点触摸屏当单点用:用 x/y 保存坐标,down 记录状态(1 按下、0 松开、-1 滑动),valid 标记本轮关注信息是否更新。解析逻辑为:EV_ABS 事件中,ABS_MT_TRACKING_ID 的 value 判断按下(0)/松开(-1),ABS_MT_POSITION_X/Y 更新坐标并置 valid;收到 SYN_REPORT 时数据完整,按 down 打印"按下/松开/移动"并重置标志。完整程序见例程 17_input/read_ts.c。
+
+```bash
+arm-linux-gnueabihf-gcc -o testApp read_ts.c
+scp testApp root@192.168.1.10:/home/root/
+./testApp /dev/input/event1
+```
+
+## 9. 多点触摸应用程序(read_mt.c)
+按 slot 分别维护每个触摸点的坐标与 ID:
+
+```c
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <string.h>
+#include <linux/input.h>
+
+/* 用于描述MT多点触摸每一个触摸点的信息 */
+struct ts_mt {
+    int x;      	//X坐标
+    int y;      	//Y坐标
+    int id;     	//对应ABS_MT_TRACKING_ID
+    int valid;		//数据有效标志位(=1表示触摸点信息发生更新)
+};
+
+/* 一个触摸点的x坐标和y坐标 */
+struct tp_xy {
+    int x;
+    int y;
+};
+
+static int ts_read(const int fd, const int max_slots,
+        struct ts_mt *mt)
+{
+    struct input_event in_ev;
+    static int slot = 0;//用于保存上一个slot
+    static struct tp_xy xy[12] = {0};//用于保存上一次的x和y坐标值,假设触摸屏支持的最大触摸点数不会超过12
+    int i;
+
+    /* 对缓冲区初始化操作 */
+    memset(mt, 0x0, max_slots * sizeof(struct ts_mt));  //清零
+    for (i = 0; i < max_slots; i++)
+        mt[i].id = -2;//将id初始化为-2, id=-1表示触摸点删除, id>=0表示创建
+
+    for ( ; ; ) {
+
+        if (sizeof(struct input_event) !=
+            read(fd, &in_ev, sizeof(struct input_event))) {
+            perror("read error");
+            return -1;
+        }
+
+        switch (in_ev.type) {
+        case EV_ABS:
+            switch (in_ev.code) {
+            case ABS_MT_SLOT:
+                slot = in_ev.value;
+                break;
+            case ABS_MT_POSITION_X:
+                xy[slot].x = in_ev.value;
+                mt[slot].valid = 1;
+                break;
+            case ABS_MT_POSITION_Y:
+                xy[slot].y = in_ev.value;
+                mt[slot].valid = 1;
+                break;
+            case ABS_MT_TRACKING_ID:
+                mt[slot].id = in_ev.value;
+                mt[slot].valid = 1;
+                break;
+            }
+            break;
+        //case EV_KEY://按键事件对单点触摸应用比较有用
+        //  break;
+        case EV_SYN:
+            if (SYN_REPORT == in_ev.code) {
+                for (i = 0; i < max_slots; i++) {
+                    mt[i].x = xy[i].x;
+                    mt[i].y = xy[i].y;
+                }
+            }
+            return 0;
+        }
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    struct input_absinfo slot;
+    struct ts_mt *mt = NULL;
+    int max_slots;
+    int fd;
+    int i;
+
+    /* 参数校验 */
+    if (2 != argc) {
+        fprintf(stderr,"usage: %s <input_dev>\n", argv[0]);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 打开文件 */
+    fd = open(argv[1], O_RDONLY);
+    if (0 > fd) {
+        perror("open error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 获取触摸屏支持的最大触摸点数 */
+    if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)) {
+        perror("ioctl error");
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    max_slots = slot.maximum + 1 - slot.minimum;
+    printf("max_slots: %d\n", max_slots);
+
+    /* 申请内存空间并清零 */
+    mt = calloc(max_slots, sizeof(struct ts_mt));
+
+    /* 读数据 */
+    for ( ; ; ) {
+
+        if (0 > ts_read(fd, max_slots, mt))
+            break;
+
+        for (i = 0; i < max_slots; i++) {
+            if (mt[i].valid) {//判断每一个触摸点信息是否发生更新(关注的信息发生更新)
+
+                if (0 <= mt[i].id)
+                    printf("slot<%d>, 按下(%d, %d)\n", i, mt[i].x, mt[i].y);
+                else if (-1 == mt[i].id)
+                    printf("slot<%d>, 松开\n", i);
+                else
+                    printf("slot<%d>, 移动(%d, %d)\n", i, mt[i].x, mt[i].y);
+            }
+        }
+    }
+
+    /* 关闭设备、退出 */
+    close(fd);
+    free(mt);
+    exit(EXIT_FAILURE);
+}
+
+```
+- `main` 先用 `ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)` 求最大触摸点数,再按 `max_slots` 申请 `struct ts_mt` 数组:`mt[0]` 对应 slot 0,依次类推;
+- `ts_read()` 把一轮数据填入数组:`ABS_MT_SLOT` 更新当前 slot,后续 X/Y、TRACKING_ID 都写入该 slot;`SYN_REPORT` 时同步坐标并返回;
+- `id` 初值 `-2` 表示未上报,`-1` 表示删除(松开),`>=0` 表示创建(按下)。单点靠 `BTN_TOUCH` 判断动作,多点靠 **ID** 判断各手指动作。
+
+```bash
+arm-linux-gnueabihf-gcc -o testApp read_mt.c
+scp testApp root@192.168.1.10:/home/root/
+./testApp /dev/input/event1
+
+```
+
+## 10. tslib 库
+tslib 是专门为触摸屏开发的 Linux **应用层函数库**,开源。它是触摸屏驱动与应用层之间的适配层:
+- 把应用层读取 `struct input_event` 并解析的过程**封装**,向上提供 API;
+- 从触摸屏获得原始坐标,经**去噪、去抖、坐标变换**,转换为屏幕坐标;
+- 通过配置文件 `ts.conf` 提供配置参数;
+- 可作为 **Qt 的触摸屏输入插件**(也可用其它插件,但多数选择 tslib)。
+
+```mermaid
+flowchart LR
+    HW["触摸屏硬件"] --> DRV["内核 input 驱动"]
+    DRV -->|"/dev/input/eventX 原始 input_event"| TS["tslib 去噪/去抖/坐标变换"]
+    TS -->|"封装 API"| APP["触摸屏应用程序"]
+    TS -->|"ts.conf / pointercal"| CFG["配置与校准文件"]
+
+```
+
+### 10.1 tslib 移植
+进入 git 仓库 `https://github.com/libts/tslib/releases` 下载源码。出厂系统已移植 **1.16**,为统一版本下载 `tslib-1.16.tar.gz`。编译分三步:配置 → 编译 → 安装。
+
+```bash
+tar -xzf tslib-1.16.tar.gz
+mkdir -p ~/tools/tslib
+
+# 设置交叉编译 SDK 环境
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+
+cd tslib-1.16
+./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/tslib/
+make
+make install
+
+```
+- `--host`:库文件运行的平台,通常设为交叉编译器名称前缀(`arm-poky-linux-gnueabi-gcc` 的前缀即 `arm-poky-linux-gnueabi`);
+- `--prefix`:安装路径;`./configure --help` 可看全部选项。
+安装目录:`bin/`(测试工具)、`etc/ts.conf`(配置)、`include/tslib.h`(头文件)、`lib/`(库文件与 `ts/` 插件库)、`share/`(可忽略)。
+`ts.conf` 常用配置:
+
+| 配置 | 作用 |
+| ---- | ---- |
+| `module_raw input` | 取消注释后使能支持 input 输入事件 |
+| `module pthres pmin=1` | 支持按压力测试时启用,`pmin` 调节灵敏度 |
+| `module dejitter delta=100` | 去噪插件,过滤噪声样本 |
+| `module linear` | 坐标变换(X/Y 互换、旋转等) |
+
+部署到自己做的根文件系统时:`bin/` 下可执行文件拷到 `/usr/bin`,`ts.conf` 拷到 `/etc`,`lib/` 下库文件拷到 `/usr/lib`。运行时依赖以下环境变量(出厂系统已在 `/etc/profile` 中配置):
+
+| 变量 | 含义 |
+| ---- | ---- |
+| `TSLIB_CONSOLEDEVICE` | 控制台设备名,直接为 `none` |
+| `TSLIB_FBDEVICE` | 显示设备节点(画线测试要在 LCD 显示) |
+| `TSLIB_TSDEVICE` | 触摸屏设备节点,按实际配置 |
+| `TSLIB_CONFFILE` | `ts.conf` 路径 |
+| `TSLIB_PLUGINDIR` | 插件路径 |
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+>
+> 校准相关变量 `TSLIB_CALIBFILE` 用于指定校准数据文件(默认 `/etc/pointercal`)。工具 `ts_calibrate` 会在屏幕**四角与中心**共五个点提示点击,用采集到的原始坐标与理论屏幕坐标做线性变换拟合,求出参数写入 `pointercal`,运行时 `module linear` 插件读取它完成坐标变换。若设备坐标已稳定,也可不生成该校准文件。
+
+测试工具:`ts_print`/`ts_test`(单点,后者可在 LCD 画线)、`ts_print_mt`/`ts_test_mt`(多点)。执行后在触摸屏上按下、滑动、松开,终端会打印信息或画线。查看设备与版本可用 `ts_finddev`。这些工具源码位于 tslib 源码目录 `tests/` 下,内部最终仍落实到前面的 input event 解析。
+
+### 10.2 tslib 库函数
+
+使用需包含 `#include <tslib.h>`,步骤为:打开设备 → 配置设备 → 读取数据。
+
+| 函数 | 说明 |
+| ---- | ---- |
+| `ts_open` | 打开触摸屏设备;成功返回句柄,失败返回 `NULL` |
+| `ts_setup` | 打开**并配置**设备;`dev_name` 传 `NULL` 时读取 `TSLIB_TSDEVICE` 环境变量 |
+| `ts_config` | 解析 `ts.conf`、加载插件;成功 0、失败 -1 |
+| `ts_close` | 关闭设备 |
+| `ts_read` | 读单点触摸数据 |
+| `ts_read_mt` | 读多点触摸数据;`max_slots` 为最大触摸点数 |
+
+`nonblock`:0 阻塞、非 0 非阻塞。`nr` 为对一个触摸点的采样数,设为 1 即可;出错时 `ts_read` 返回负数。
+单点样本结构体:
+
+```c
+struct ts_sample {
+    int              x;           //X 坐标
+    int              y;           //Y 坐标
+    unsigned int     pressure;    //按压力大小
+    struct timeval   tv;          //时间
+};
+
+```
+多点样本结构体 `struct ts_sample_mt` 常用字段:
+
+| 字段 | 含义 |
+| ---- | ---- |
+| `x` / `y` | X / Y 坐标 |
+| `pressure` | 按压力大小 |
+| `slot` | 触摸点 slot |
+| `tracking_id` | 触摸点 ID |
+| `tv` | 时间 |
+| `pen_down` | `BTN_TOUCH` 状态 |
+| `valid` | 本次样本是否有效(数据是否更新) |
+其余字段(`tool_type`、`tool_x/y`、`touch_major/minor`、`width_major/minor`、`orientation`、`distance`、`blob_id`)为更细的描述,一般可忽略。
+
+## 11. 基于 tslib 的单点触摸程序(ts_read.c)
+
+```c
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <tslib.h>      //包含tslib.h头文件
+
+int main(int argc, char *argv[])
+{
+    struct tsdev *ts = NULL;
+    struct ts_sample samp;
+    int pressure = 0;//用于保存上一次的按压力,初始为0,表示松开
+
+    /* 打开并配置触摸屏设备 */
+    ts = ts_setup(NULL, 0);
+    if (NULL == ts) {
+        fprintf(stderr, "ts_setup error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 读数据 */
+    for ( ; ; ) {
+
+        if (0 > ts_read(ts, &samp, 1)) {
+            fprintf(stderr, "ts_read error");
+            ts_close(ts);
+            exit(EXIT_FAILURE);
+        }
+
+        if (samp.pressure) {//按压力>0
+            if (pressure)   //若上一次的按压力>0
+                printf("移动(%d, %d)\n", samp.x, samp.y);
+            else
+                printf("按下(%d, %d)\n", samp.x, samp.y);
+        }
+        else
+            printf("松开\n");//打印坐标
+
+        pressure = samp.pressure;
+    }
+
+    ts_close(ts);
+    exit(EXIT_SUCCESS);
+}
+
+```
+`ts_setup(NULL, 0)` 内部读取 `TSLIB_TSDEVICE` 得到设备节点并完成配置;通过**按压力**判断状态:`pressure=0` 表示松开,大于 0 时再根据上一次按压力判断"按下/移动"。
+
+```bash
+arm-linux-gnueabihf-gcc -I /home/dt/tools/tslib/include -L /home/dt/tools/tslib/lib -lts -o testApp ts_read.c
+scp testApp root@192.168.1.10:/home/root/
+./testApp
+```
+
+| 选项 | 含义 |
+| ---- | ---- |
+| `-I` | 头文件路径(tslib 的 `include`) |
+| `-L` | 库文件路径(tslib 的 `lib`) |
+| `-lts` | 链接 `libts.so`(Linux 动态库命名 `lib`+名字+`.so`) |
+
+---
+
+## 12. 基于 tslib 的多点触摸程序(ts_read_mt.c)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <linux/input.h>
+#include <tslib.h>
+int main(int argc, char *argv[])
+{
+    struct tsdev *ts = NULL;
+    struct ts_sample_mt *mt_ptr = NULL;
+    struct input_absinfo slot;
+    int max_slots;
+    unsigned int pressure[12] = {0};   //用于保存每一个触摸点上一次的按压力,初始为0,表示松开
+    int i;
+    /* 打开并配置触摸屏设备 */
+    ts = ts_setup(NULL, 0);
+    if (NULL == ts) {
+        fprintf(stderr, "ts_setup error");
+        exit(EXIT_FAILURE);
+    }
+    /* 获取触摸屏支持的最大触摸点数 */
+    if (0 > ioctl(ts_fd(ts), EVIOCGABS(ABS_MT_SLOT), &slot)) {
+        perror("ioctl error");
+        ts_close(ts);
+        exit(EXIT_FAILURE);
+    }
+    max_slots = slot.maximum + 1 - slot.minimum;
+    printf("max_slots: %d\n", max_slots);
+    /* 内存分配 */
+    mt_ptr = calloc(max_slots, sizeof(struct ts_sample_mt));
+    /* 读数据 */
+    for ( ; ; ) {
+        if (0 > ts_read_mt(ts, &mt_ptr, max_slots, 1)) {
+            perror("ts_read_mt error");
+            ts_close(ts);
+            free(mt_ptr);
+            exit(EXIT_FAILURE);
+        }
+        for (i = 0; i < max_slots; i++) {
+            if (mt_ptr[i].valid) {//有效表示有更新!
+                if (mt_ptr[i].pressure) { //如果按压力>0
+                    if (pressure[mt_ptr[i].slot])//如果上一次的按压力>0
+                        printf("slot<%d>, 移动(%d, %d)\n", mt_ptr[i].slot, mt_ptr[i].x, mt_ptr[i].y);
+                    else
+                        printf("slot<%d>, 按下(%d, %d)\n", mt_ptr[i].slot, mt_ptr[i].x, mt_ptr[i].y);
+                }
+                else
+                    printf("slot<%d>, 松开\n", mt_ptr[i].slot);
+                pressure[mt_ptr[i].slot] = mt_ptr[i].pressure;
+            }
+        }
+    }
+    /* 关闭设备、释放内存、退出 */
+    ts_close(ts);
+    free(mt_ptr);
+    exit(EXIT_SUCCESS);
+}
+```
+
+- 用 `ts_fd(ts)` 从 tslib 句柄取出底层 fd,再 `ioctl` 获取最大触摸点数;
+- 按 `max_slots` 申请 `struct ts_sample_mt` 数组;`ts_read_mt(ts, &mt_ptr, max_slots, 1)` 读一轮数据;
+- 只处理 `valid` 为真的触摸点;每个 slot 用 `pressure[]` 记录上一次按压力,区分"按下/移动"。
+
+```bash
+arm-linux-gnueabihf-gcc -I /home/dt/tools/tslib/include -L /home/dt/tools/tslib/lib -lts -o testApp ts_read_mt.c
+```
+
+---
+
+## 13. 实验步骤与调试方法
+
+### 13.1 实验步骤
+
+1. 将 LCD 屏连接到开发板 LCD 接口,上电启动出厂系统;点击屏幕进入设置页面,点击退出按钮退出出厂系统 GUI 应用;
+3. `cat /proc/bus/input/devices` 确认触摸屏设备节点(如 `goodix-ts`);
+4. 用 `read_input.c` 打印原始数据,对照第 6.2 节分析事件规则;
+5. 用 `read_slot.c` 查看最大触摸点数,再运行 `read_ts.c`、`read_mt.c`;
+6. 运行 `ts_print`/`ts_print_mt`/`ts_test`/`ts_test_mt` 验证 tslib;
+7. 交叉编译 `ts_read.c`/`ts_read_mt.c`(带 `-I`、`-L`、`-lts`),拷到开发板运行。
+
+### 13.2 调试方法
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| `open error: No such file or directory` | 设备节点写错 | `cat /proc/bus/input/devices` 确认 `eventX` |
+| `open error: Permission denied` | 权限不足 | 用 `root` 或检查节点权限 |
+| `read` 阻塞无输出 | 该设备无事件(正常) | 触发按键/触摸;或改用非阻塞 |
+| 坐标与屏幕不符 | 未做坐标变换/校准 | 用 tslib `module linear` 或校准 |
+| 编译找不到 `tslib.h` | 未指定头文件路径 | 加 `-I <tslib>/include` |
+| 链接找不到 `libts` | 未指定库路径或库名 | 加 `-L <tslib>/lib -lts` |
+
+```bash
+cat /proc/bus/input/devices      # 查看所有输入设备及 eventX
+hexdump /dev/input/event2        # 十六进制查看原始事件流(需触发事件)
+ts_finddev                       # 查看 tslib 设备与版本
+```
+
+---
+
+## 14. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+| 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 |
+| ---- | ------------------ | ------------------ | ------ |
+| 输入框架 | Linux input 子系统,`/dev/input/eventX` | 无统一框架,直接读寄存器或自写驱动 | Linux input 子系统,接口一致 |
+| 数据单元 | `struct input_event` | 自定义结构/寄存器值 | `struct input_event` |
+| 多点触摸 | MT Type B,slot + tracking_id | 触摸 IC 自定义报文 | MT 协议,接口一致 |
+| 用户态库 | tslib(去噪/校准/坐标变换,Qt 插件) | 通常无,需自行滤波校准 | tslib 同样可用 |
+| 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc` | `aarch64-linux-gnu-gcc` |
+
+I.MX6ULL 与 RK3568 同属 Linux 应用编程,input 子系统与 tslib 用法基本通用;STM32 裸机没有设备节点与 `input_event`,不能套用本篇模型。
+
+---
+
+## 15. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:什么是 input 子系统?它对应用层提供了什么?
+
+**答**:input 子系统是 Linux 为统一管理各种输入设备(键盘、鼠标、触摸屏、按键等)实现的兼容框架。驱动人员基于它开发输入设备驱动,它屏蔽硬件差异、向应用层提供统一接口。注册成功的设备在 `/dev/input` 下生成 `eventX` 节点,应用层 `open` 后 `read`,每次得到一个 `struct input_event`(time、type、code、value)。
+
+### Q2:`struct input_event` 的 type、code、value 分别是什么?如何判断按键按下?
+
+**答**:`type` 描述事件大类(`EV_KEY` 按键、`EV_REL` 相对位移、`EV_ABS` 绝对位移、`EV_SYN` 同步等);`code` 指明该类中的具体事件;`value` 是事件值,解释随 `code` 变化。对按键事件,`code` 为键值,`value=1` 按下、`0` 松开、`2` 长按;先判断 `type==EV_KEY`,再读 `value` 即可判断状态,`code` 用于区分具体按键。
+
+### Q3:为什么需要 EV_SYN / SYN_REPORT?一次 read 能读全一个触摸点吗?
+
+**答**:一次 `read` 只能读一个 `struct input_event`,而一个触摸点含 X、Y 等多项,需多次 `read` 才读全。内核把本轮数据全部上报后,再上报 `SYN_REPORT`(`type=EV_SYN`、`code=0`)告知本轮数据完整。应用层收到它时才认为信息到齐、可以解析。
+
+### Q4:多点触摸中 slot 与 tracking_id 有什么区别?
+
+**答**:slot 是硬件层面概念,代表触摸点编号,按触碰时间先后分配、从 0 开始,`ABS_MT_SLOT` 告知当前更新哪个 slot。tracking_id 是软件层面概念,标识触摸点生命周期,由 `ABS_MT_TRACKING_ID` 上报:`>=0` 为有效触摸点,`-1` 表示被移除。同一手指触碰、离开、再触碰,两次都是 slot=0,但属于不同生命周期,tracking_id 不同。
+
+### Q5:tslib 是什么?它做了什么,如何使用?
+
+**答**:tslib 是开源的 Linux 触摸屏应用层函数库,是驱动与应用层之间的适配层。它把"读取原始 `input_event` 并解析"封装起来,经去噪、去抖、坐标变换,把原始触摸坐标转换为屏幕坐标,并通过 `ts.conf` 配置和校准文件完成变换;还可作为 Qt 的触摸屏输入插件。使用步骤:`ts_setup`/`ts_open` 打开、`ts_config` 配置(`ts_setup` 已含)、`ts_read`/`ts_read_mt` 读取、`ts_close` 关闭。需设置 `TSLIB_TSDEVICE`、`TSLIB_CONFFILE`、`TSLIB_PLUGINDIR` 等环境变量,编译时用 `-I`、`-L`、`-lts`。
+
+---
+
+## 延伸阅读
+
+- 驱动侧原理:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/08-misc与input子系统]]
+- 触摸屏驱动实现:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]]
+- 本库相关:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]]
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十七章 输入设备应用编程、第十八章 使用tslib库;例程源码 `17_input`(read_input.c、read_key.c、read_slot.c、read_ts.c、read_mt.c)、`18_tslib`(ts_read.c、ts_read_mt.c)

+ 2153 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/04-FrameBuffer与LCD应用编程.md

@@ -0,0 +1,2153 @@
+---
+title: FrameBuffer 与 LCD 应用编程
+tags: [嵌入式Linux, Linux应用编程, FrameBuffer, LCD, fb0, mmap, RGB565, libjpeg, libpng, FreeType, 竖屏, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十九章 FrameBuffer应用编程;第二十章 在LCD上显示jpeg图像;第二十一章 在LCD上显示png图片;第二十二章 LCD横屏切换为竖屏;第二十三章 在LCD上显示字符"
+---
+
+# FrameBuffer 与 LCD 应用编程
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/03-输入设备与tslib]]、[[03-外设与高级IO编程/01-高级IO]];延伸阅读:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/03-LCD驱动]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]]
+
+在 Linux 里,“显示”这件事被抽象成了一块内存:**FrameBuffer(帧缓冲)**。应用层不接触 LCD 控制器、时序、寄存器,只要拿到 `/dev/fb0`、用 `ioctl` 问清楚屏幕参数、再把这块内存 `mmap` 到用户空间,就能像写普通数组一样在屏幕上打点、画线、贴图片、显示文字。
+
+本篇把《I.MX6U 嵌入式 Linux C 应用编程指南》第五篇(第十九~二十三章)从头到尾串起来:**裸 FrameBuffer 编程 → 显示 BMP → libjpeg 显示 JPEG → libpng 显示 PNG → 横屏转竖屏 → FreeType 显示字符**,所有例程完整收录,可直接交叉编译运行。
+
+---
+
+## 1. FrameBuffer 是什么
+
+### 1.1 帧缓冲与设备节点
+
+- `Frame` = 帧,`buffer` = 缓冲,`Framebuffer` = 帧缓冲 = **保存着一帧图像的一块内存**。
+- 它是 Linux 的**显示驱动接口**:屏蔽不同显示硬件的实现,对应用层抽象为一块**显示内存(显存)**。应用直接读写这块缓冲区,物理显存的位置等细节由 FrameBuffer 驱动处理。
+- 显示设备在 Linux 中就叫 **FrameBuffer 设备**,所以 LCD 就是 FrameBuffer 设备。
+- 设备文件为 `/dev/fbX`(X 为数字)。Linux 最多支持 32 个,即 `/dev/fb0` ~ `/dev/fb31`;正点原子出厂系统里 `/dev/fb0` 就是 LCD 屏。
+
+显存大小由分辨率与像素深度决定。例如 800×480、RGB888(24 位/像素):
+
+```text
+800 × 480 × 24 / 8 = 1152000 字节
+```
+
+因此可以用 `dd` 直接把显存清成黑色(假设设备节点 `/dev/fb0`、分辨率 800×480、RGB888):
+
+```bash
+dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125
+```
+
+该命令把 1125×1024 个字节日均写入 LCD 显存,内容都是 `0x0`。
+
+### 1.2 显示通路
+
+```mermaid
+flowchart LR
+    App["应用程序"] -->|"open / dev/fb0"| FBDev["FrameBuffer 设备"]
+    App -->|"ioctl 取参数"| FBDev
+    App -->|"mmap 映射"| Mem["显存<br/>显示缓冲区"]
+    FBDev -.->|"驱动管理"| Mem
+    Mem -->|"LCD 控制器循环扫描"| Panel["LCD 液晶面板"]
+    App -->|"munmap / close"| FBDev
+```
+
+### 1.3 操作 /dev/fbX 的五个步骤
+
+1. 打开 `/dev/fbX` 设备文件,得到文件描述符 `fd`。
+2. 用 `ioctl()` 获取当前显示设备的参数信息(分辨率、像素格式),据此计算显存大小。
+3. 用**存储映射 I/O**(`mmap`)把屏幕显存映射到用户空间。
+4. 映射成功后直接读写显存,进行绘图、图片显示等操作。
+5. 完成后 `munmap()` 取消映射,`close()` 关闭设备文件。
+
+### 1.4 为什么用 mmap 而不是 read/write
+
+普通 I/O(`read`/`write`)也能操作显存,但**数据量大时效率低**。举例:1920×1080、ARGB8888,刷一帧就是
+
+```text
+1920 × 1080 × 32 / 8 = 8294400 字节 ≈ 8MB
+```
+
+显示画面还在动态更新,数据量庞大,普通 I/O 必然效率低下,所以采用存储映射 I/O:映射一次,之后就是内存访问。
+
+---
+
+## 2. 屏幕参数:两个结构体与三个 ioctl
+
+FrameBuffer 应用编程绕不开 `<linux/fb.h>` 里的两个结构体:可变参数 `struct fb_var_screeninfo` 与固定参数 `struct fb_fix_screeninfo`。
+
+### 2.1 ioctl request 速查
+
+| request 宏 | 值 | 作用 | 第三参数 |
+| ---------- | -- | ---- | -------- |
+| `FBIOGET_VSCREENINFO` | `0x4600` | 获取**可变**参数信息 | `struct fb_var_screeninfo *` |
+| `FBIOPUT_VSCREENINFO` | `0x4601` | 设置**可变**参数信息(驱动支持时才能改) | `struct fb_var_screeninfo *` |
+| `FBIOGET_FSCREENINFO` | `0x4602` | 获取**固定**参数信息(应用不可改) | `struct fb_fix_screeninfo *` |
+
+```c
+struct fb_var_screeninfo fb_var;
+struct fb_fix_screeninfo fb_fix;
+
+ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+```
+
+### 2.2 struct fb_var_screeninfo
+
+```c
+struct fb_var_screeninfo {
+    __u32 xres;             /* 可视区域,一行有多少个像素点,X 分辨率 */
+    __u32 yres;             /* 可视区域,一列有多少个像素点,Y 分辨率 */
+    __u32 xres_virtual;     /* 虚拟区域,一行有多少个像素点 */
+    __u32 yres_virtual;     /* 虚拟区域,一列有多少个像素点 */
+    __u32 xoffset;          /* 虚拟到可见屏幕之间的行偏移 */
+    __u32 yoffset;          /* 虚拟到可见屏幕之间的列偏移 */
+
+    __u32 bits_per_pixel;   /* 每个像素点使用多少个 bit 来描述,即像素深度 bpp */
+    __u32 grayscale;        /* =0 彩色, =1 灰度, >1 FOURCC 颜色 */
+
+    /* 描述 R、G、B 三分量各用多少位及偏移量 */
+    struct fb_bitfield red;     /* Red 颜色分量色域偏移 */
+    struct fb_bitfield green;   /* Green 颜色分量色域偏移 */
+    struct fb_bitfield blue;    /* Blue 颜色分量色域偏移 */
+    struct fb_bitfield transp;  /* 透明度分量色域偏移 */
+
+    __u32 nonstd;           /* 0 表示标准像素格式;非 0 表示非标准像素格式 */
+    __u32 activate;
+
+    __u32 height;           /* LCD 显示图像的高度(毫米) */
+    __u32 width;            /* LCD 显示图像的宽度(毫米) */
+
+    __u32 accel_flags;
+
+    /* 以下表示时序参数 */
+    __u32 pixclock;         /* pixel clock in ps (pico seconds) */
+    __u32 left_margin;      /* time from sync to picture */
+    __u32 right_margin;     /* time from picture to sync */
+    __u32 upper_margin;     /* time from sync to picture */
+    __u32 lower_margin;
+    __u32 hsync_len;        /* length of horizontal sync */
+    __u32 vsync_len;        /* length of vertical sync */
+    __u32 sync;             /* see FB_SYNC_* */
+    __u32 vmode;            /* see FB_VMODE_* */
+    __u32 rotate;           /* angle we rotate counter clockwise */
+    __u32 colorspace;       /* colorspace for FOURCC-based modes */
+    __u32 reserved[4];      /* Reserved for future compatibility */
+};
+```
+
+**关键字段理解:**
+
+- `xres` / `yres`:屏幕水平/垂直分辨率(可视区域)。`xres * yres * bits_per_pixel / 8` 即整个显示缓冲区大小。
+- `xres_virtual` / `yres_virtual`:**虚拟分辨率**,是显存里实际一行/一列能容纳的像素数,可以大于可视区域。它配合 `xoffset` / `yoffset` 实现**平移(panning)**:虚拟分辨率更大时显存像一块“大画布”,可视区域是从 `(xoffset, yoffset)` 开始的一个窗口。显存布局与一行字节数由固定参数 `line_length` 描述,所以绘图定位用 `line_length` 比用 `xres` 更稳妥(两者在无虚拟扩展时通常相等)。
+- `bits_per_pixel`:像素深度 bpp,每个像素用多少 bit 描述颜色。
+- `red` / `green` / `blue`:三个颜色通道各占多少 bit、偏移多少,用来判断 RGB888 / RGB565 / BGR888 / BGR565 等。
+- `height` / `width`:LCD 物理尺寸(毫米),与分辨率无关。
+
+### 2.3 struct fb_bitfield
+
+```c
+struct fb_bitfield {
+    __u32 offset;       /* 偏移量 */
+    __u32 length;       /* 长度 */
+    __u32 msb_right;    /* != 0 : Most significant bit is right */
+};
+```
+
+打印 `red.offset/length`、`green.offset/length`、`blue.offset/length` 就能确定像素格式。例如正点原子 7 寸 800×480 屏打印出 `R<11 5> G<5 6> B<0 5>`,表示:
+
+```text
+16bit 颜色值:高 5 位 = R,中间 6 位 = G,低 5 位 = B  →  RGB565
+```
+
+### 2.4 struct fb_fix_screeninfo
+
+```c
+struct fb_fix_screeninfo {
+    char id[16];                /* 字符串形式的标识符 */
+    unsigned long smem_start;   /* 显存的起始地址(物理地址) */
+
+    __u32 smem_len;             /* 显存的长度 */
+    __u32 type;
+    __u32 type_aux;
+    __u32 visual;
+    __u16 xpanstep;
+    __u16 ypanstep;
+    __u16 ywrapstep;
+    __u32 line_length;          /* 一行的字节数 */
+    unsigned long mmio_start;   /* Start of Memory Mapped I/O(physical address) */
+    __u32 mmio_len;             /* Length of Memory Mapped I/O */
+    __u32 accel;                /* Indicate to driver which specific chip/card we have */
+    __u16 capabilities;
+    __u16 reserved[2];
+};
+```
+
+- `smem_start`:显存起始物理地址,应用层无法直接使用,需靠 `mmap`。
+- `smem_len`:显存长度,不一定等于 LCD 实际显存大小。
+- `line_length`:屏幕一行像素点占用的字节数。通常用 `line_length * yres` 得到显示缓冲区大小。
+
+### 2.5 字段速查表
+
+| 结构体 | 字段 | 含义 | 典型用途 |
+| ------ | ---- | ---- | -------- |
+| fb_var | `xres` / `yres` | 可视分辨率 | 计算 `width`、`height` |
+| fb_var | `xres_virtual` / `yres_virtual` | 虚拟分辨率 | 双缓冲 / panning |
+| fb_var | `xoffset` / `yoffset` | 可视窗口在虚拟显存中的偏移 | 平移显示 |
+| fb_var | `bits_per_pixel` | 像素深度 bpp | 一行有效字节数 = `width * bpp / 8` |
+| fb_var | `red/green/blue` | 通道位域 | 判定 RGB565 / RGB888 |
+| fb_fix | `line_length` | 一行字节数 | 显存大小 = `line_length * yres` |
+| fb_fix | `smem_start` / `smem_len` | 显存物理地址/长度 | 物理信息,应用不可直接用 |
+
+### 2.6 像素格式与 RGB565
+
+一个像素点的字节数 = `bpp / 8`。RGB888 用 3 字节,RGB565 用 2 字节。开发板出厂系统把 LCD 实现为 **RGB565** 显示设备,所以例程里普遍使用 `unsigned short` 指针访问显存。
+
+RGB565 的位域掩码为 `0xF800`、`0x07E0`、`0x001F`。从 ARGB8888 的颜色值转 RGB565 的宏:
+
+```c
+#define argb8888_to_rgb565(color)   ({ \
+            unsigned int temp = (color); \
+            ((temp & 0xF80000UL) >> 8) | \
+            ((temp & 0xFC00UL) >> 5) | \
+            ((temp & 0xF8UL) >> 3); \
+            })
+```
+---
+
+## 3. 例程一:读取屏幕参数 lcd_info.c
+
+对应例程:`19_lcd/lcd_info.c`。打开 `/dev/fb0`,用两个 `ioctl` 取参数并打印。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <linux/fb.h>
+
+int main(int argc, char *argv[])
+{
+    struct fb_fix_screeninfo fb_fix;
+    struct fb_var_screeninfo fb_var;
+    int fd;
+
+    /* 打开 framebuffer 设备 */
+    if (0 > (fd = open("/dev/fb0", O_WRONLY))) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 获取参数信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+    printf("分辨率: %d*%d\n"
+        "像素深度 bpp: %d\n"
+        "一行的字节数: %d\n"
+        "像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
+        fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
+        fb_fix.line_length,
+        fb_var.red.offset, fb_var.red.length,
+        fb_var.green.offset, fb_var.green.length,
+        fb_var.blue.offset, fb_var.blue.length);
+
+    /* 关闭设备文件退出程序 */
+    close(fd);
+    exit(0);
+}
+```
+
+**逐段说明:**
+
+- `open("/dev/fb0", O_WRONLY)`:只读参数用 `O_WRONLY` 即可;后面要写显存时用 `O_RDWR`。
+- `FBIOGET_VSCREENINFO` 得到可变参数,`FBIOGET_FSCREENINFO` 得到固定参数。
+- 打印 `red/green/blue` 的 `offset` 与 `length`,就能按第 2.3 节的方法判断像素格式。
+
+7 寸 800×480 屏实测:分辨率 `800*480`、bpp `16`、一行 `1600` 字节、像素格式 `R<11 5> G<5 6> B<0 5>`,即 RGB565。`800*16/8 = 1600`,与 `line_length` 一致。
+
+> Tips:正点原子的 RGB LCD(4.3 寸 800×480、4.3 寸 480×272、7 寸 800×480、7 寸 1024×600、10.1 寸 1280×800)硬件上均支持 RGB888,但 ALPHA/Mini I.MX6U 出厂系统的 LCD 驱动把它实现为 RGB565 设备;可改设备树支持 RGB888,或通过 `ioctl` 修改。不建议随意用 `FBIOPUT_VSCREENINFO` 改参数,驱动支持不完善时可能出问题。
+
+---
+
+## 4. 例程二:LCD 基本操作(画点/画线/画矩形/填充)lcd_test.c
+
+对应例程:`19_lcd/lcd_test.c`。这是 FrameBuffer 编程的核心范例:`open → ioctl → mmap → 直接写显存`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <linux/fb.h>
+
+#define argb8888_to_rgb565(color)   ({ \
+            unsigned int temp = (color); \
+            ((temp & 0xF80000UL) >> 8) | \
+            ((temp & 0xFC00UL) >> 5) | \
+            ((temp & 0xF8UL) >> 3); \
+            })
+
+static int width;                               /* LCD X 分辨率 */
+static int height;                              /* LCD Y 分辨率 */
+static unsigned short *screen_base = NULL;      /* 映射后的显存基地址 */
+
+/********************************************************************
+ * 函数名称: lcd_draw_point
+ * 功能描述: 打点
+ * 输入参数: x, y, color
+ ********************************************************************/
+static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+
+    /* 对传入参数的校验 */
+    if (x >= width)
+        x = width - 1;
+    if (y >= height)
+        y = height - 1;
+
+    /* 填充颜色 */
+    screen_base[y * width + x] = rgb565_color;
+}
+
+/********************************************************************
+ * 函数名称: lcd_draw_line
+ * 功能描述: 画线(水平或垂直线)
+ * 输入参数: x, y, dir, length, color
+ ********************************************************************/
+static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
+            unsigned int length, unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+    unsigned int end;
+    unsigned long temp;
+
+    /* 对传入参数的校验 */
+    if (x >= width)
+        x = width - 1;
+    if (y >= height)
+        y = height - 1;
+
+    /* 填充颜色 */
+    temp = y * width + x;       /* 定位到起点 */
+    if (dir) {                  /* 水平线 */
+        end = x + length - 1;
+        if (end >= width)
+            end = width - 1;
+
+        for ( ; x <= end; x++, temp++)
+            screen_base[temp] = rgb565_color;
+    }
+    else {                      /* 垂直线 */
+        end = y + length - 1;
+        if (end >= height)
+            end = height - 1;
+
+        for ( ; y <= end; y++, temp += width)
+            screen_base[temp] = rgb565_color;
+    }
+}
+
+/********************************************************************
+ * 函数名称: lcd_draw_rectangle
+ * 功能描述: 画矩形
+ * 输入参数: start_x, end_x, start_y, end_y, color
+ ********************************************************************/
+static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
+            unsigned int start_y, unsigned int end_y,
+            unsigned int color)
+{
+    int x_len = end_x - start_x + 1;
+    int y_len = end_y - start_y - 1;
+
+    lcd_draw_line(start_x, start_y, 1, x_len, color);       /* 上边 */
+    lcd_draw_line(start_x, end_y, 1, x_len, color);         /* 下边 */
+    lcd_draw_line(start_x, start_y + 1, 0, y_len, color);   /* 左边 */
+    lcd_draw_line(end_x, start_y + 1, 0, y_len, color);     /* 右边 */
+}
+
+/********************************************************************
+ * 函数名称: lcd_fill
+ * 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
+ * 输入参数: start_x, end_x, start_y, end_y, color
+ ********************************************************************/
+static void lcd_fill(unsigned int start_x, unsigned int end_x,
+            unsigned int start_y, unsigned int end_y,
+            unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+    unsigned long temp;
+    unsigned int x;
+
+    /* 对传入参数的校验 */
+    if (end_x >= width)
+        end_x = width - 1;
+    if (end_y >= height)
+        end_y = height - 1;
+
+    /* 填充颜色 */
+    temp = start_y * width;     /* 定位到起点行首 */
+    for ( ; start_y <= end_y; start_y++, temp += width) {
+
+        for (x = start_x; x <= end_x; x++)
+            screen_base[temp + x] = rgb565_color;
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    struct fb_fix_screeninfo fb_fix;
+    struct fb_var_screeninfo fb_var;
+    unsigned int screen_size;
+    int fd;
+
+    /* 打开 framebuffer 设备 */
+    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
+        perror("open error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 获取参数信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    screen_size = fb_fix.line_length * fb_var.yres;
+    width = fb_var.xres;
+    height = fb_var.yres;
+
+    /* 将显示缓冲区映射到进程地址空间 */
+    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 画正方形方块 */
+    int w = height * 0.25;      /* 方块的宽度为 1/4 屏幕高度 */
+    lcd_fill(0, width-1, 0, height-1, 0x0);                 /* 清屏(屏幕显示黑色) */
+    lcd_fill(0, w, 0, w, 0xFF0000);                         /* 红色方块 */
+    lcd_fill(width-w, width-1, 0, w, 0xFF00);               /* 绿色方块 */
+    lcd_fill(0, w, height-w, height-1, 0xFF);               /* 蓝色方块 */
+    lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00);/* 黄色方块 */
+
+    /* 画线: 十字交叉线 */
+    lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF);     /* 白色线 */
+    lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF);     /* 白色线 */
+
+    /* 画矩形 */
+    unsigned int s_x, s_y, e_x, e_y;
+    s_x = 0.25 * width;
+    s_y = w;
+    e_x = width - s_x;
+    e_y = height - s_y;
+
+    for ( ; (s_x <= e_x) && (s_y <= e_y);
+            s_x+=5, s_y+=5, e_x-=5, e_y-=5)
+        lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
+
+    /* 退出 */
+    munmap(screen_base, screen_size);   /* 取消映射 */
+    close(fd);                          /* 关闭文件 */
+    exit(EXIT_SUCCESS);                 /* 退出进程 */
+}
+```
+
+### 4.1 显存定位公式
+
+以显存基地址 `screen_base`(`unsigned short *`,RGB565)为例,像素 `(x, y)` 对应:
+
+```text
+screen_base[y * width + x]
+```
+
+若用字节指针 `(unsigned char *)base`,则:
+
+```text
+base + (y * width + x) * pix_bytes
+```
+
+其中 `pix_bytes` 是一个像素占用的字节数。**一行跨度为 `width` 个 `unsigned short`**,所以换行时指针 `+width` 即可。
+
+### 4.2 四个函数职责
+
+| 函数 | 作用 | 关键参数 |
+| ---- | ---- | -------- |
+| `lcd_draw_point` | 打点 | `x, y, color` |
+| `lcd_draw_line` | 画水平/垂直线(不支持斜线) | `dir != 0` 水平,`dir == 0` 垂直;`length` 像素长度 |
+| `lcd_draw_rectangle` | 画矩形边框(4 条线拼成) | 左上 `(start_x,start_y)`、右下 `(end_x,end_y)` |
+| `lcd_fill` | 矩形区域填充 | 同上 |
+
+`lcd_draw_rectangle` 的内边长度 `y_len = end_y - start_y - 1`,左右两条竖线各画 `y_len` 个点;`lcd_fill` 用双层循环逐行填充。
+
+### 4.3 主函数流程
+
+1. `open` 打开 LCD,得到 `fd`。
+2. `ioctl` 取可变/固定参数,算出 `screen_size = line_length * yres`、`width = xres`、`height = yres`。
+3. `mmap` 建立映射。注意 `MAP_FAILED` 判断要先把指针转成 `void *` 再比较。
+4. 直接操作 `screen_base` 画方块、十字线、同心矩形。
+5. `munmap` + `close` 退出。
+
+### 4.4 画斜线说明
+
+本组函数只支持水平/垂直线。画斜线需要 Bresenham 之类的算法,不属于本章要点。
+
+---
+
+## 5. 例程三:在 LCD 上显示 BMP 图片 bmp_show.c
+
+对应例程:`19_lcd/bmp_show.c`。BMP 未压缩、解析简单,是理解“图像文件 → 显存”的最好入口。
+
+### 5.1 BMP 文件由四部分组成
+
+| 数据段 | 大小(Byte) | 说明 |
+| ------ | ------------ | ---- |
+| BMP 文件头(bmp file header) | 14 | 文件格式、大小、到位图数据的偏移量 |
+| 位图信息头(bitmap information) | 通常 40 或 56 | 头大小、图像尺寸、图像大小、位平面数、压缩方式、颜色索引 |
+| 调色板(color palette) | 由颜色索引数决定 | 可选;索引色图像才有 |
+| 位图数据(bitmap data) | 由图像尺寸决定 | 图像数据本体 |
+
+16 位(R5/G6/B5)、24 位(R8/G8/B8)真彩色图像**不需要调色板**,位图信息头后紧跟位图数据。
+
+### 5.2 BMP 文件头成员
+
+| 变量名 | 地址偏移 | 大小 | 作用 |
+| ------ | -------- | ---- | ---- |
+| `bfType` | 00H | 2 bytes | 文件类型,`BM` 表示 Windows 位图 |
+| `bfSize` | 02H | 4 bytes | 文件大小 |
+| `bfReserved1` | 06H | 2 bytes | 保留,必须为 0 |
+| `bfReserved2` | 08H | 2 bytes | 保留,必须为 0 |
+| `bfOffBits` | 0AH | 4 bytes | 从文件头到图像数据的字节偏移量,用它快速定位图像数据 |
+
+### 5.3 位图信息头成员
+
+| 变量名 | 地址偏移 | 大小 | 作用 |
+| ------ | -------- | ---- | ---- |
+| `biSize` | 0EH | 4 bytes | 位图信息头大小 |
+| `biWidth` | 12H | 4 bytes | 图像宽度(像素) |
+| `biHeight` | 16H | 4 bytes | 图像高度(像素);**正数 = 倒向位图,负数 = 正向位图** |
+| `biPlanes` | 1AH | 2 bytes | 色彩平面数,总为 1 |
+| `biBitCount` | 1CH | 2 bytes | 像素深度,可为 1、4、8、16、24、32 |
+| `biCompression` | 1EH | 4 bytes | 压缩方式:0=RGB,1=8bpp RLE,2=4bpp RLE,3=Bit-fields,4/5=打印机 |
+| `biSizeImage` | 22H | 4 bytes | 图像数据大小(BI_RGB 时可设为 0) |
+| `biXPelsPerMeter` | 26H | 4 bytes | 水平分辨率(像素/米) |
+| `biYPelsPerMeter` | 2AH | 4 bytes | 垂直分辨率(像素/米) |
+| `biClrUsed` | 2EH | 4 bytes | 实际使用的调色板颜色索引数 |
+| `biClrImportant` | 32H | 4 bytes | 重要颜色索引数,0 表示都重要 |
+
+只有压缩方式为 Bit-fields(0x3)时,位图信息头才是 56 字节,否则为 40 字节;多出的 16 字节是 R、G、B、A 四个 32bit 位域掩码。RGB565 的位域掩码是 `0xF800`、`0x07E0`、`0x001F`。
+
+### 5.4 正向位图与倒向位图
+
+- **正向位图**(`biHeight < 0`):从左上角到右下角排列,水平从左到右、垂直从上到下。
+- **倒向位图**(`biHeight > 0`):从左下角到右上角排列,水平从左到右、垂直从下到上。一般 BMP 都是倒向位图。
+
+### 5.5 完整源码
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <string.h>
+#include <linux/fb.h>
+#include <sys/mman.h>
+
+/**** BMP 文件头数据结构 ****/
+typedef struct {
+    unsigned char type[2];      /* 文件类型 */
+    unsigned int size;          /* 文件大小 */
+    unsigned short reserved1;   /* 保留字段 1 */
+    unsigned short reserved2;   /* 保留字段 2 */
+    unsigned int offset;        /* 到位图数据的偏移量 */
+} __attribute__ ((packed)) bmp_file_header;
+
+/**** 位图信息头数据结构 ****/
+typedef struct {
+    unsigned int size;          /* 位图信息头大小 */
+    int width;                  /* 图像宽度 */
+    int height;                 /* 图像高度 */
+    unsigned short planes;      /* 位面数 */
+    unsigned short bpp;         /* 像素深度 */
+    unsigned int compression;   /* 压缩方式 */
+    unsigned int image_size;    /* 图像大小 */
+    int x_pels_per_meter;       /* 像素/米 */
+    int y_pels_per_meter;       /* 像素/米 */
+    unsigned int clr_used;
+    unsigned int clr_omportant;
+} __attribute__ ((packed)) bmp_info_header;
+
+/**** 静态全局变量 ****/
+static int width;                               /* LCD X 分辨率 */
+static int height;                              /* LCD Y 分辨率 */
+static unsigned short *screen_base = NULL;      /* 映射后的显存基地址 */
+static unsigned long line_length;               /* LCD 一行的长度(字节为单位) */
+
+/********************************************************************
+ * 函数名称: show_bmp_image
+ * 功能描述: 在 LCD 上显示指定的 BMP 图片
+ * 输入参数: 文件路径
+ * 返 回 值: 成功返回 0, 失败返回 -1
+ ********************************************************************/
+static int show_bmp_image(const char *path)
+{
+    bmp_file_header file_h;
+    bmp_info_header info_h;
+    unsigned short *line_buf = NULL;    /* 行缓冲区 */
+    unsigned long line_bytes;           /* BMP 图像一行的字节大小 */
+    unsigned int min_h, min_bytes;
+    int fd = -1;
+    int j;
+
+    /* 打开文件 */
+    if (0 > (fd = open(path, O_RDONLY))) {
+        perror("open error");
+        return -1;
+    }
+
+    /* 读取 BMP 文件头 */
+    if (sizeof(bmp_file_header) !=
+        read(fd, &file_h, sizeof(bmp_file_header))) {
+        perror("read error");
+        close(fd);
+        return -1;
+    }
+
+    if (0 != memcmp(file_h.type, "BM", 2)) {
+        fprintf(stderr, "it's not a BMP file\n");
+        close(fd);
+        return -1;
+    }
+
+    /* 读取位图信息头 */
+    if (sizeof(bmp_info_header) !=
+        read(fd, &info_h, sizeof(bmp_info_header))) {
+        perror("read error");
+        close(fd);
+        return -1;
+    }
+
+    /* 打印信息 */
+    printf("文件大小: %d\n"
+         "位图数据的偏移量: %d\n"
+         "位图信息头大小: %d\n"
+         "图像分辨率: %d*%d\n"
+         "像素深度: %d\n", file_h.size, file_h.offset,
+         info_h.size, info_h.width, info_h.height,
+         info_h.bpp);
+
+    /* 将文件读写位置移动到图像数据开始处 */
+    if (-1 == lseek(fd, file_h.offset, SEEK_SET)) {
+        perror("lseek error");
+        close(fd);
+        return -1;
+    }
+
+    /* 申请一个 buf、暂存 bmp 图像的一行数据 */
+    line_bytes = info_h.width * info_h.bpp / 8;
+    line_buf = malloc(line_bytes);
+    if (NULL == line_buf) {
+        fprintf(stderr, "malloc error\n");
+        close(fd);
+        return -1;
+    }
+
+    if (line_length > line_bytes)
+        min_bytes = line_bytes;
+    else
+        min_bytes = line_length;
+
+    /**** 读取图像数据显示到 LCD ****/
+    /* 本示例默认传入的 bmp 图像是 RGB565 格式 */
+    if (0 < info_h.height) {        /* 倒向位图 */
+        if (info_h.height > height) {
+            min_h = height;
+            lseek(fd, (info_h.height - height) * line_bytes, SEEK_CUR);
+            screen_base += width * (height - 1);    /* 定位到屏幕左下角位置 */
+        }
+        else {
+            min_h = info_h.height;
+            screen_base += width * (info_h.height - 1);
+        }
+
+        for (j = min_h; j > 0; screen_base -= width, j--) {
+            read(fd, line_buf, line_bytes);         /* 读取出图像数据 */
+            memcpy(screen_base, line_buf, min_bytes);/* 刷入 LCD 显存 */
+        }
+    }
+    else {                          /* 正向位图 */
+        int temp = 0 - info_h.height;   /* 负数转成正数 */
+        if (temp > height)
+            min_h = height;
+        else
+            min_h = temp;
+
+        for (j = 0; j < min_h; j++, screen_base += width) {
+            read(fd, line_buf, line_bytes);
+            memcpy(screen_base, line_buf, min_bytes);
+        }
+    }
+
+    /* 关闭文件、函数返回 */
+    close(fd);
+    free(line_buf);
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    struct fb_fix_screeninfo fb_fix;
+    struct fb_var_screeninfo fb_var;
+    unsigned int screen_size;
+    int fd;
+
+    /* 传参校验 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <bmp_file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开 framebuffer 设备 */
+    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
+        perror("open error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 获取参数信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    screen_size = fb_fix.line_length * fb_var.yres;
+    line_length = fb_fix.line_length;
+    width = fb_var.xres;
+    height = fb_var.yres;
+
+    /* 将显示缓冲区映射到进程地址空间 */
+    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 显示 BMP 图片 */
+    memset(screen_base, 0xFF, screen_size);
+    show_bmp_image(argv[1]);
+
+    /* 退出 */
+    munmap(screen_base, screen_size);   /* 取消映射 */
+    close(fd);                          /* 关闭文件 */
+    exit(EXIT_SUCCESS);                 /* 退出进程 */
+}
+```
+
+### 5.6 关键点
+
+- 两个结构体用 `__attribute__ ((packed))`,保证与文件中的字节布局严格对齐。
+- 读取文件头后先用 `memcmp(file_h.type, "BM", 2)` 校验文件类型;再用 `lseek(fd, file_h.offset, SEEK_SET)` 跳到图像数据。
+- 倒向位图从屏幕左下角往上刷,所以 `screen_base` 先定位到 `width * (height - 1)`,每刷一行 `screen_base -= width`。
+- 正向位图从上往下刷,每刷一行 `screen_base += width`。
+- 该例程不做 565/888 兼容判断,默认传入 RGB565 的 BMP;可用 Photoshop“存储为 → BMP → 16 位色 → 高级模式 → RGB565”得到 16 位色 BMP。
+
+---
+
+## 6. 例程四:在 LCD 上显示 JPEG(libjpeg)
+
+对应例程:`20_libjpeg/show_jpeg_image.c`。
+
+### 6.1 JPEG 与 libjpeg
+
+- **JPEG**(Joint Photographic Experts Group):国际数字图像压缩标准,支持**有损压缩**,压缩比高。它损失的是人眼不敏感的高频信息,后缀 `.jpg` / `.jpeg`。
+- **libjpeg**:完全用 C 语言编写的函数库,包含 JPEG 解码(解压缩)、编码(生成压缩文件)及其他功能。用它解码即可得到原始 RGB 数据。
+
+### 6.2 libjpeg 解码流程
+
+```mermaid
+flowchart TD
+    A["jpeg_create_decompress<br/>创建解码对象"] --> B["jpeg_stdio_src<br/>指定数据源"]
+    B --> C["jpeg_read_header<br/>读取头信息"]
+    C --> D["设置解码参数<br/>out_color_space / scale"]
+    D --> E["jpeg_start_decompress<br/>开始解码"]
+    E --> F{"jpeg_read_scanlines<br/>逐行读取,每次 1 行"}
+    F -->|"output_scanline < min_h"| F
+    F -->|"读取完成"| G["jpeg_finish_decompress<br/>结束解码"]
+    G --> H["jpeg_destroy_decompress<br/>销毁对象"]
+```
+
+### 6.3 核心数据结构
+
+| 结构体 | 说明 |
+| ------ | ---- |
+| `struct jpeg_decompress_struct cinfo` | 解码对象,记录 JPEG 详细信息与解码输出信息 |
+| `struct jpeg_error_mgr jerr` | 错误处理对象 |
+
+调用 `jpeg_read_header()` 后可读到的信息:
+
+```c
+cinfo.image_width        /* jpeg 图像宽度 */
+cinfo.image_height       /* jpeg 图像高度 */
+cinfo.num_components     /* 颜色通道数 */
+cinfo.jpeg_color_space   /* jpeg 图像的颜色空间 */
+```
+
+`jpeg_start_decompress()` 后填充的输出信息:
+
+```c
+cinfo.output_width        /* 输出图像宽度 */
+cinfo.output_height       /* 输出图像高度 */
+cinfo.output_components   /* 每像素颜色通道数(灰度 1,RGB888 为 3) */
+cinfo.output_scanline     /* 接下来要读取的行索引 */
+```
+
+颜色空间枚举 `J_COLOR_SPACE`:`JCS_UNKNOWN`、`JCS_GRAYSCALE`、`JCS_RGB`、`JCS_YCbCr`、`JCS_CMYK`、`JCS_YCCK`、`JCS_BG_RGB`、`JCS_BG_YCC`。
+
+### 6.4 API 速查
+
+| 函数 | 作用 |
+| ---- | ---- |
+| `jpeg_std_error(&jerr)` | 绑定 libjpeg 默认错误处理 |
+| `jpeg_create_decompress(&cinfo)` | 创建解码对象 |
+| `jpeg_stdio_src(&cinfo, fp)` | 以标准 I/O 文件流作为数据源 |
+| `jpeg_read_header(&cinfo, TRUE)` | 读取 JPEG 头信息(约定必须调用) |
+| `jpeg_calc_output_dimensions(&cinfo)` | 在 start 前提前计算输出尺寸 |
+| `jpeg_start_decompress(&cinfo)` | 开始解码 |
+| `jpeg_read_scanlines(&cinfo, buf, 1)` | 逐行读取解码数据(当前一次只支持 1 行) |
+| `jpeg_finish_decompress(&cinfo)` | 完成解码 |
+| `jpeg_destroy_decompress(&cinfo)` | 销毁解码对象、释放资源 |
+
+### 6.5 错误处理
+
+`struct jpeg_error_mgr` 中的 `error_exit` 是错误处理函数指针。`jpeg_std_error()` 将错误处理设为默认方式:内存不足、文件格式不对等错误发生时,默认处理函数会调用 `exit()` 结束整个进程。
+
+```c
+/* 初始化错误处理对象、并将其与解压对象绑定 */
+cinfo.err = jpeg_std_error(&jerr);
+```
+
+也可以注册自定义错误处理:
+
+```c
+void my_error_exit(struct jpeg_decompress_struct *cinfo)
+{
+    /* ... */
+}
+
+cinfo.err.error_exit = my_error_exit;
+```
+
+### 6.6 移植 libjpeg 到开发板
+
+1. 下载源码(http://www.ijg.org/files/ ),本书以 `jpegsrc.v9b.tar.gz` 为例,解压得 `jpeg-9b`。
+2. 初始化交叉编译环境:
+
+```bash
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+```
+
+3. 配置、编译、安装:
+
+```bash
+./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/jpeg/
+make
+make install
+```
+
+`--host` 指定目标平台(通常取交叉编译器名称前缀),`--prefix` 指定安装目录。安装目录含 `bin`(测试工具)、`include`(头文件,应用只需包含 `jpeglib.h`)、`lib`(动态库,`libjpeg.so` → `libjpeg.so.9.2.0`)。
+
+4. 移植到开发板:`bin` 下工具拷到 `/usr/bin`,`lib` 下库文件拷到 `/usr/lib`,注意保持符号链接(可先打包再解压)。移植前删除出厂旧库:
+
+```bash
+rm -rf /usr/lib/libjpeg.*
+```
+
+5. 验证:执行 `djpeg --help` 能打印帮助即成功。
+
+> Tips:删除出厂 `libjpeg` 后,出厂 Qt GUI 程序对 jpeg 图片解码会出问题(原图位置变空白)。新库版本不同也救不回来,知道即可。
+
+### 6.7 完整源码
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <string.h>
+#include <linux/fb.h>
+#include <sys/mman.h>
+#include <jpeglib.h>
+
+typedef struct bgr888_color {
+    unsigned char red;
+    unsigned char green;
+    unsigned char blue;
+} __attribute__ ((packed)) bgr888_t;
+
+static int width;                               /* LCD X 分辨率 */
+static int height;                              /* LCD Y 分辨率 */
+static unsigned short *screen_base = NULL;      /* 映射后的显存基地址 */
+static unsigned long line_length;               /* LCD 一行的长度(字节为单位) */
+static unsigned int bpp;                        /* 像素深度 bpp */
+
+static int show_jpeg_image(const char *path)
+{
+    struct jpeg_decompress_struct cinfo;
+    struct jpeg_error_mgr jerr;
+    FILE *jpeg_file = NULL;
+    bgr888_t *jpeg_line_buf = NULL;     /* 行缓冲区:存储从 jpeg 解压出来的一行图像数据 */
+    unsigned short *fb_line_buf = NULL; /* 行缓冲区:存储写入到 LCD 显存的一行数据 */
+    unsigned int min_h, min_w;
+    unsigned int valid_bytes;
+    int i;
+
+    /* 绑定默认错误处理函数 */
+    cinfo.err = jpeg_std_error(&jerr);
+
+    /* 打开.jpeg/.jpg 图像文件 */
+    jpeg_file = fopen(path, "r");       /* 只读方式打开 */
+    if (NULL == jpeg_file) {
+        perror("fopen error");
+        return -1;
+    }
+
+    /* 创建 JPEG 解码对象 */
+    jpeg_create_decompress(&cinfo);
+
+    /* 指定图像文件 */
+    jpeg_stdio_src(&cinfo, jpeg_file);
+
+    /* 读取图像信息 */
+    jpeg_read_header(&cinfo, TRUE);
+    printf("jpeg 图像大小: %d*%d\n", cinfo.image_width, cinfo.image_height);
+
+    /* 设置解码参数 */
+    cinfo.out_color_space = JCS_RGB;    /* 默认就是 JCS_RGB */
+    /* cinfo.scale_num = 1; */
+    /* cinfo.scale_denom = 2; */
+
+    /* 开始解码图像 */
+    jpeg_start_decompress(&cinfo);
+
+    /* 为缓冲区分配内存空间 */
+    jpeg_line_buf = malloc(cinfo.output_components * cinfo.output_width);
+    fb_line_buf = malloc(line_length);
+
+    /* 判断图像和 LCD 屏哪个的分辨率更低 */
+    if (cinfo.output_width > width)
+        min_w = width;
+    else
+        min_w = cinfo.output_width;
+
+    if (cinfo.output_height > height)
+        min_h = height;
+    else
+        min_h = cinfo.output_height;
+
+    /* 读取数据 */
+    valid_bytes = min_w * bpp / 8;      /* 真正写入到 LCD 显存的一行数据大小 */
+    while (cinfo.output_scanline < min_h) {
+
+        jpeg_read_scanlines(&cinfo, (unsigned char **)&jpeg_line_buf, 1);/* 每次读取一行 */
+
+        /* 将读取到的 BGR888 数据转为 RGB565 */
+        for (i = 0; i < min_w; i++)
+            fb_line_buf[i] = ((jpeg_line_buf[i].red & 0xF8) << 8) |
+                    ((jpeg_line_buf[i].green & 0xFC) << 3) |
+                    ((jpeg_line_buf[i].blue & 0xF8) >> 3);
+
+        memcpy(screen_base, fb_line_buf, valid_bytes);
+        screen_base += width;           /* +width 定位到 LCD 下一行显存起点 */
+    }
+
+    /* 解码完成 */
+    jpeg_finish_decompress(&cinfo);     /* 完成解码 */
+    jpeg_destroy_decompress(&cinfo);    /* 销毁 JPEG 解码对象、释放资源 */
+
+    /* 关闭文件、释放内存 */
+    fclose(jpeg_file);
+    free(fb_line_buf);
+    free(jpeg_line_buf);
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    struct fb_fix_screeninfo fb_fix;
+    struct fb_var_screeninfo fb_var;
+    unsigned int screen_size;
+    int fd;
+
+    /* 传参校验 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <jpeg_file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开 framebuffer 设备 */
+    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
+        perror("open error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 获取参数信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    line_length = fb_fix.line_length;
+    bpp = fb_var.bits_per_pixel;
+    screen_size = line_length * fb_var.yres;
+    width = fb_var.xres;
+    height = fb_var.yres;
+
+    /* 将显示缓冲区映射到进程地址空间 */
+    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 显示图片 */
+    memset(screen_base, 0xFF, screen_size);
+    show_jpeg_image(argv[1]);
+
+    /* 退出 */
+    munmap(screen_base, screen_size);   /* 取消映射 */
+    close(fd);                          /* 关闭文件 */
+    exit(EXIT_SUCCESS);                 /* 退出进程 */
+}
+```
+
+### 6.8 逐段解释
+
+- **数据格式**:libjpeg 默认解码得到 **BGR888**,即内存里每像素依次是 B、G、R 三个字节(结构体 `bgr888_t` 的成员顺序是 `red/green/blue`,但读取时示例按该顺序访问,形成 BGR888 排布)。示例中 `jpeg_line_buf[i].red/green/blue` 分别对应转换需要的分量。
+- **`jpeg_read_scanlines` 参数**:第二个参数是 `unsigned char **` 类型,示例用 `(unsigned char **)&jpeg_line_buf` 强转。libjpeg 当前只支持一次读 1 行。
+- **循环条件**:`cinfo.output_scanline < min_h`,每读一行 `output_scanline` 自增 1。
+- **缩放**:`cinfo.scale_num` / `cinfo.scale_denom` 可缩放输出,解出图像大小为 `scale_num/scale_denom`;JPEG 仅支持 1/1、1/2、1/4、1/8。默认 1/1。示例中注释掉,需要时打开。
+- **裁剪**:`min_w/min_h` 取图像与屏幕分辨率的较小值,超过屏幕的部分不显示。
+- **RGB888 → RGB565**:`((R & 0xF8) << 8) | ((G & 0xFC) << 3) | ((B & 0xF8) >> 3)`。
+
+---
+
+## 7. 例程五:在 LCD 上显示 PNG(libpng)
+
+对应例程:`21_libpng/show_png_image.c`(另有 `setjmp.c` 演示错误跳转)。
+
+### 7.1 PNG 与 libpng / zlib
+
+- **PNG**(Portable Network Graphic Format):采用**无损压缩**(LZ77 派生算法),压缩比高、体积小、支持 256 级透明效果,用于替代 GIF/TIFF。
+- **libpng**:免费开源的 C 语言函数库,支持 PNG 解码/编码。
+- **zlib**:数据压缩函数库,**libpng 依赖 zlib**,所以移植 libpng 前必须先移植 zlib。
+
+### 7.2 两个核心数据结构
+
+| 结构体 | 说明 |
+| ------ | ---- |
+| `png_struct`(句柄 `png_structp png_ptr`) | libpng 内部使用,几乎所有库函数的第一个参数;`png_create_read_struct()` 创建 |
+| `png_info`(句柄 `png_infop info_ptr`) | 描述 PNG 图像信息;新版本通过 `png_get_XXX` / `png_set_XXX` 访问成员 |
+
+### 7.3 setjmp / longjmp 与错误返回点
+
+libpng 默认错误处理会调用 `longjmp()` 跳转到错误返回点,以便程序执行销毁、释放等清理工作。
+
+```c
+#include <setjmp.h>
+
+int setjmp(jmp_buf env);        /* 设置跳转点,初次返回 0 */
+void longjmp(jmp_buf env, int val);  /* 跳回跳转点,val 作为 setjmp 的"伪"返回值 */
+```
+
+用 `png_jmpbuf(png_ptr)` 取出 `png_struct` 中的 `jmp_buf`:
+
+```c
+/* 设置错误返回点 */
+if (setjmp(png_jmpbuf(png_ptr))) {
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+    return -1;
+}
+```
+
+`val` 不能设为 0,否则无法区分初次返回与“伪”返回。示例 `setjmp.c`:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <setjmp.h>
+
+static jmp_buf buf;
+
+static void hello(void)
+{
+    printf("hello world!\n");
+    longjmp(buf,1);
+    printf("Nice to meet you!\n");
+}
+
+int main(void)
+{
+    if(0 == setjmp(buf)) {
+        printf("First return\n");
+        hello();
+    }
+    else
+        printf("Second return\n");
+
+    exit(0);
+}
+```
+
+运行结果:先打印 `First return`、`hello world!`,`longjmp` 跳回后打印 `Second return`,`Nice to meet you!` 不会打印。
+
+### 7.4 high-level 与 low-level 接口
+
+| 方式 | 函数 | 特点 | 适用条件 |
+| ---- | ---- | ---- | -------- |
+| high-level | `png_read_png()` | 一个函数一次性解码全部数据,内部自动分配缓冲区 | 内存足够大、且输出格式限定为 libpng 预定义转换 |
+| low-level | `png_read_info` + `png_set_xxx` + `png_read_update_info` + `png_read_image`/`png_read_rows` | 灵活,需用户分配缓冲区 | 需要自定义转换或逐行处理 |
+
+libpng 预定义数据转换类型(可 `|` 组合,作为 `png_read_png` 第三参数):
+
+| 转换宏 | 说明 |
+| ------ | ---- |
+| `PNG_TRANSFORM_IDENTITY` | No transformation |
+| `PNG_TRANSFORM_STRIP_16` | Strip 16-bit samples to 8 bits |
+| `PNG_TRANSFORM_STRIP_ALPHA` | Discard the alpha channel |
+| `PNG_TRANSFORM_PACKING` | Expand 1, 2 and 4-bit samples to bytes |
+| `PNG_TRANSFORM_PACKSWAP` | Change order of packed pixels to LSB first |
+| `PNG_TRANSFORM_EXPAND` | Perform set_expand() |
+| `PNG_TRANSFORM_INVERT_MONO` | Invert monochrome images |
+| `PNG_TRANSFORM_SHIFT` | Normalize pixels to the sBIT depth |
+| `PNG_TRANSFORM_BGR` | Flip RGB to BGR, RGBA to BGRA |
+| `PNG_TRANSFORM_SWAP_ALPHA` | Flip RGBA to ARGB or GA to AG |
+| `PNG_TRANSFORM_INVERT_ALPHA` | Change alpha from opacity to transparency |
+| `PNG_TRANSFORM_SWAP_ENDIAN` | Byte-swap 16-bit samples |
+| `PNG_TRANSFORM_GRAY_TO_RGB` | Expand grayscale samples to RGB |
+
+`png_read_png()` 等价于依次执行:`png_read_info` → 按 `transforms` 设置转换 → `png_read_image` → `png_read_end`。
+
+颜色类型宏:
+
+```c
+#define PNG_COLOR_TYPE_GRAY        0
+#define PNG_COLOR_TYPE_PALETTE     (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE)
+#define PNG_COLOR_TYPE_RGB         (PNG_COLOR_MASK_COLOR)
+#define PNG_COLOR_TYPE_RGB_ALPHA   (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_ALPHA)
+#define PNG_COLOR_TYPE_GRAY_ALPHA  (PNG_COLOR_MASK_ALPHA)
+```
+
+### 7.5 API 速查
+
+| 函数 | 作用 |
+| ---- | ---- |
+| `png_create_read_struct(PNG_LIBPNG_VER_STRING, ...)` | 创建解码 `png_struct`,失败返回 NULL |
+| `png_create_info_struct(png_ptr)` | 创建 `png_info`,失败返回 NULL |
+| `png_jmpbuf(png_ptr)` | 获取错误跳转点 `jmp_buf` |
+| `png_init_io(png_ptr, fp)` | 以标准 I/O 文件流指定数据源 |
+| `png_read_png(png_ptr, info_ptr, transforms, NULL)` | high-level 一次性解码 |
+| `png_read_info` / `png_read_update_info` / `png_read_end` | low-level 读取信息 / 更新信息 / 结束 |
+| `png_read_image(png_ptr, row_pointers)` | low-level 一次性读全部数据 |
+| `png_read_rows(png_ptr, &row_buf, NULL, 1)` | 每次读 1 行或多行 |
+| `png_get_image_width` / `png_get_image_height` | 获取宽 / 高 |
+| `png_get_bit_depth` / `png_get_color_type` | 获取位深 / 颜色类型 |
+| `png_get_rows(png_ptr, info_ptr)` | 获取指向每一行数据缓冲区的指针数组(high-level 内部缓冲区) |
+| `png_get_rowbytes` | 每行数据字节数 |
+| `png_set_strip_16` / `png_set_expand` / `png_set_gray_to_rgb` | 16→8 位 / 低位深扩展 / 灰度转 RGB |
+| `png_malloc(png_ptr, size)` | libpng 提供的内存分配(等价 malloc) |
+| `png_destroy_read_struct(&png_ptr, &info_ptr, NULL)` | 销毁 `png_struct`(及其关联资源) |
+
+### 7.6 libpng 应用编程完整源码
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <string.h>
+#include <linux/fb.h>
+#include <sys/mman.h>
+#include <png.h>
+
+static int width;                               /* LCD X 分辨率 */
+static int height;                              /* LCD Y 分辨率 */
+static unsigned short *screen_base = NULL;      /* 映射后的显存基地址 */
+static unsigned long line_length;               /* LCD 一行的长度(字节为单位) */
+static unsigned int bpp;                        /* 像素深度 bpp */
+
+static int show_png_image(const char *path)
+{
+    png_structp png_ptr = NULL;
+    png_infop info_ptr = NULL;
+    FILE *png_file = NULL;
+    unsigned short *fb_line_buf = NULL; /* 行缓冲区:存储写入到 LCD 显存的一行数据 */
+    unsigned int min_h, min_w;
+    unsigned int valid_bytes;
+    unsigned int image_h, image_w;
+    png_bytepp row_pointers = NULL;
+    int i, j, k;
+
+    /* 打开 png 文件 */
+    png_file = fopen(path, "r");        /* 以只读方式打开 */
+    if (NULL == png_file) {
+        perror("fopen error");
+        return -1;
+    }
+
+    /* 分配和初始化 png_ptr、info_ptr */
+    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+    if (!png_ptr) {
+        fclose(png_file);
+        return -1;
+    }
+
+    info_ptr = png_create_info_struct(png_ptr);
+    if (!info_ptr) {
+        png_destroy_read_struct(&png_ptr, NULL, NULL);
+        fclose(png_file);
+        return -1;
+    }
+
+    /* 设置错误返回点 */
+    if (setjmp(png_jmpbuf(png_ptr))) {
+        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+        fclose(png_file);
+        return -1;
+    }
+
+    /* 指定数据源 */
+    png_init_io(png_ptr, png_file);
+
+    /* 读取 png 文件 */
+    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);
+    image_h = png_get_image_height(png_ptr, info_ptr);
+    image_w = png_get_image_width(png_ptr, info_ptr);
+    printf("分辨率: %d*%d\n", image_w, image_h);
+
+    /* 判断是不是 RGB888 */
+    if ((8 != png_get_bit_depth(png_ptr, info_ptr)) &&
+        (PNG_COLOR_TYPE_RGB != png_get_color_type(png_ptr, info_ptr))) {
+        printf("Error: Not 8bit depth or not RGB color");
+        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+        fclose(png_file);
+        return -1;
+    }
+
+    /* 判断图像和 LCD 屏哪个的分辨率更低 */
+    if (image_w > width)
+        min_w = width;
+    else
+        min_w = image_w;
+
+    if (image_h > height)
+        min_h = height;
+    else
+        min_h = image_h;
+
+    valid_bytes = min_w * bpp / 8;
+
+    /* 读取解码后的数据 */
+    fb_line_buf = malloc(valid_bytes);
+    row_pointers = png_get_rows(png_ptr, info_ptr);     /* 获取数据 */
+
+    unsigned int temp = min_w * 3;      /* RGB888 一个像素 3 个字节 */
+    for (i = 0; i < min_h; i++) {
+
+        /* RGB888 转为 RGB565 */
+        for (j = k = 0; j < temp; j += 3, k++)
+            fb_line_buf[k] = ((row_pointers[i][j] & 0xF8) << 8) |
+                ((row_pointers[i][j+1] & 0xFC) << 3) |
+                ((row_pointers[i][j+2] & 0xF8) >> 3);
+
+        memcpy(screen_base, fb_line_buf, valid_bytes);  /* 将一行数据刷入显存 */
+        screen_base += width;                           /* 定位到显存下一行 */
+    }
+
+    /* 结束、销毁/释放内存 */
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+    free(fb_line_buf);
+    fclose(png_file);
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    struct fb_fix_screeninfo fb_fix;
+    struct fb_var_screeninfo fb_var;
+    unsigned int screen_size;
+    int fd;
+
+    /* 传参校验 */
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <png_file>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* 打开 framebuffer 设备 */
+    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
+        perror("open error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 获取参数信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    line_length = fb_fix.line_length;
+    bpp = fb_var.bits_per_pixel;
+    screen_size = line_length * fb_var.yres;
+    width = fb_var.xres;
+    height = fb_var.yres;
+
+    /* 将显示缓冲区映射到进程地址空间 */
+    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 显示图片 */
+    memset(screen_base, 0xFF, screen_size);     /* 屏幕刷白 */
+    show_png_image(argv[1]);
+
+    /* 退出 */
+    munmap(screen_base, screen_size);   /* 取消映射 */
+    close(fd);                          /* 关闭文件 */
+    exit(EXIT_SUCCESS);                 /* 退出进程 */
+}
+```
+
+### 7.7 逐段解释与移植
+
+- 示例使用 **high-level 接口**:直接 `png_read_png()` 一次性解码;`PNG_TRANSFORM_STRIP_ALPHA` 丢掉 alpha 通道,得到 RGB888。
+- `row_pointers` 由 `png_get_rows()` 取得,指向 libpng 内部为每一行分配的缓冲区;销毁 `png_struct` 时该缓冲区自动释放。
+- 转换时每行按 3 字节遍历:`R=row[i][j]`、`G=row[i][j+1]`、`B=row[i][j+2]`,转 RGB565。
+- 新版本 libpng 检查更严,运行时会打印一些警告,不影响使用。
+
+**移植顺序:先 zlib 后 libpng。**
+
+zlib:
+
+```bash
+tar -xzf zlib-1.2.10.tar.gz
+./configure --prefix=/home/dt/tools/zlib/
+make
+make install
+```
+
+libpng(需先导出 zlib 路径):
+
+```bash
+export LDFLAGS="${LDFLAGS} -L/home/dt/tools/zlib/lib"
+export CFLAGS="${CFLAGS} -I/home/dt/tools/zlib/include"
+export CPPFLAGS="${CPPFLAGS} -I/home/dt/tools/zlib/include"
+./configure --prefix=/home/dt/tools/png --host=arm-poky-linux-gnueabi
+make
+make install
+```
+
+移植到开发板前删除出厂库并保持符号链接:
+
+```bash
+rm -rf /usr/lib/libz.* /lib/libz.*
+rm -rf /lib/libpng* /usr/lib/libpng*
+```
+
+### 7.8 libpng 与 libjpeg 对比
+
+| 维度 | libjpeg | libpng |
+| ---- | ------- | ------ |
+| 压缩 | 有损 | 无损(LZ77 派生) |
+| 依赖 | 无 | 依赖 zlib |
+| 主要对象 | `jpeg_decompress_struct` + `jpeg_error_mgr` | `png_struct` + `png_info` |
+| 读取方式 | 逐行 `jpeg_read_scanlines` | high-level 一次性 `png_read_png` 或 low-level 逐行 |
+| 输出格式 | 默认 BGR888 | 由转换参数决定,`STRIP_ALPHA` 后为 RGB888 |
+| 错误处理 | `jpeg_std_error` 默认 `exit`,可自定义 `error_exit` | 默认 `longjmp`,用 `setjmp(png_jmpbuf())` 设返回点 |
+| 结束 | `jpeg_finish_decompress` + `jpeg_destroy_decompress` | `png_destroy_read_struct` |
+
+---
+
+## 8. 例程六:LCD 横屏切换为竖屏
+
+对应例程:`22_lcd_vertical_display/lcd_vertical_display.c`。
+
+**核心结论:横屏/竖屏切换与驱动程序无关,是应用层要解决的问题。**
+
+### 8.1 硬件坐标是固定的
+
+以 800×480 为例,屏幕正向放置时(横屏):
+
+```text
+左上角 (0, 0)        右上角 (800-1, 0)
+左下角 (0, 480-1)    右下角 (800-1, 480-1)
+```
+
+像素排列从左到右、从上到下,这是硬件固定属性,无法配置。像素 `(x, y)` 的显存地址(字节基地址 `base`):
+
+```text
+base + (y * width + x) * pix_bytes
+```
+
+### 8.2 竖屏只是应用程序“假装”的坐标
+
+如果应用层想把**左下角**当原点 `(0, 0)`(一种常见的竖屏坐标分布),则应用坐标与物理坐标的对应为:
+
+```text
+左上角 (480-1, 0)    右上角 (480-1, 800-1)
+左下角 (0, 0)        右下角 (0, 800-1)
+```
+
+此时应用坐标 `(x, y)` 不能再用 `base + (y * width + x)`,而应通过物理坐标 `(y, height-1-x)` 计算:
+
+```text
+竖屏 (x, y) → 显存地址 = base + ((height - 1 - x) * width + y) * pix_bytes
+```
+
+在 RGB565 的 `unsigned short *screen_base` 视角下(`lcd_max_y = lcd_height - 1`):
+
+```text
+screen_base[(lcd_max_y - x) * lcd_width + y]
+```
+
+```mermaid
+flowchart LR
+    A["竖屏逻辑坐标 (x, y)"] -->|"lcd_max_y - x 得到物理行<br/>y 得到物理列"| B["物理像素 (y, lcd_max_y - x)"]
+    B --> C["screen_base[(lcd_max_y - x) * lcd_width + y]"]
+```
+
+> 说明:该公式只适用于上面这种“左下角为原点”的竖屏分布。把上图旋转 180° 也是竖屏,但公式不同;坐标变换推导本身很简单,按同样的方法推即可。
+
+### 8.3 坐标变化对绘图函数的影响
+
+- 横屏下:`(x, y)` 中 x 沿宽方向、y 沿高方向,水平线 x 递增地址 `+1`,垂直线 y 递增地址 `+width`。
+- 竖屏下:x 变成了“物理行号”(从下往上),所以竖屏的**水平线**每走一步地址 `-lcd_width`,**垂直线**每走一步地址 `+1`。
+
+### 8.4 完整源码
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <linux/fb.h>
+
+#define argb8888_to_rgb565(color)   ({ \
+            unsigned int temp = (color); \
+            ((temp & 0xF80000UL) >> 8) | \
+            ((temp & 0xFC00UL) >> 5) | \
+            ((temp & 0xF8UL) >> 3); \
+            })
+
+static int lcd_width;                       /* LCD X 分辨率 */
+static int lcd_height;                      /* LCD Y 分辨率 */
+static int lcd_max_y;                       /* LCD Y 坐标最大值 */
+static int user_width;                      /* 竖屏模式下 X 分辨率 */
+static int user_height;                     /* 竖屏模式下 Y 分辨率 */
+static unsigned short *screen_base = NULL;  /* 映射后的显存基地址 */
+
+/********************************************************************
+ * 函数名称: lcd_draw_point
+ * 功能描述: 打点
+ ********************************************************************/
+static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+
+    /* 对传入参数的校验 */
+    if (x >= user_width)
+        x = user_width - 1;
+    if (y >= user_height)
+        y = user_height - 1;
+
+    /* 填充颜色 */
+    screen_base[(lcd_max_y-x) * lcd_width + y] = rgb565_color;
+}
+
+/********************************************************************
+ * 函数名称: lcd_draw_line
+ * 功能描述: 画线(水平或垂直线)
+ ********************************************************************/
+static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
+            unsigned int length, unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+    unsigned int end;
+    unsigned long temp;
+
+    /* 对传入参数的校验 */
+    if (x >= user_width)
+        x = user_width - 1;
+    if (y >= user_height)
+        y = user_height - 1;
+
+    /* 填充颜色 */
+    temp = (lcd_max_y-x) * lcd_width + y;
+    if (dir) {                  /* 水平线 */
+        end = x + length - 1;
+        if (end >= user_width)
+            end = user_width - 1;
+
+        for ( ; x <= end; x++, temp -= lcd_width)
+            screen_base[temp] = rgb565_color;
+    }
+    else {                      /* 垂直线 */
+        end = y + length - 1;
+        if (end >= user_height)
+            end = user_height - 1;
+
+        for ( ; y <= end; y++, temp++)
+            screen_base[temp] = rgb565_color;
+    }
+}
+
+/********************************************************************
+ * 函数名称: lcd_draw_rectangle
+ * 功能描述: 画矩形
+ ********************************************************************/
+static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
+            unsigned int start_y, unsigned int end_y,
+            unsigned int color)
+{
+    int x_len = end_x - start_x + 1;
+    int y_len = end_y - start_y - 1;
+
+    lcd_draw_line(start_x, start_y, 1, x_len, color);       /* 上边 */
+    lcd_draw_line(start_x, end_y, 1, x_len, color);         /* 下边 */
+    lcd_draw_line(start_x, start_y + 1, 0, y_len, color);   /* 左边 */
+    lcd_draw_line(end_x, start_y + 1, 0, y_len, color);     /* 右边 */
+}
+
+/********************************************************************
+ * 函数名称: lcd_fill
+ * 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
+ ********************************************************************/
+static void lcd_fill(unsigned int start_x, unsigned int end_x,
+            unsigned int start_y, unsigned int end_y,
+            unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+    unsigned long temp;
+    unsigned long step_size_count;
+    int x;
+
+    /* 对传入参数的校验 */
+    if (end_x >= user_width)
+        end_x = user_width - 1;
+    if (end_y >= user_height)
+        end_y = user_height - 1;
+
+    /* 填充颜色 */
+    temp = (lcd_max_y-start_x) * lcd_width + start_y;
+    for ( ; start_y <= end_y; start_y++, temp++) {
+
+        step_size_count = 0;
+        for (x = start_x; x <= end_x; x++, step_size_count += lcd_width)
+            screen_base[temp - step_size_count] = rgb565_color;
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    struct fb_fix_screeninfo fb_fix;
+    struct fb_var_screeninfo fb_var;
+    unsigned int screen_size;
+    int fd;
+
+    /* 打开 framebuffer 设备 */
+    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
+        perror("open error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 获取参数信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    screen_size = fb_fix.line_length * fb_var.yres;
+    lcd_width = fb_var.xres;
+    lcd_height = fb_var.yres;
+    lcd_max_y = lcd_height - 1;
+    user_width = fb_var.yres;       /* 竖屏 X = 原 Y */
+    user_height = fb_var.xres;      /* 竖屏 Y = 原 X */
+
+    /* 将显示缓冲区映射到进程地址空间 */
+    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 画正方形方块 */
+    int w = user_height * 0.25;     /* 方块的宽度为 1/4 屏幕高度 */
+    lcd_fill(0, user_width-1, 0, user_height-1, 0x0);       /* 清屏 */
+    lcd_fill(0, w, 0, w, 0xFF0000);                         /* 红色方块 */
+    lcd_fill(user_width-w, user_width-1, 0, w, 0xFF00);     /* 绿色方块 */
+    lcd_fill(0, w, user_height-w, user_height-1, 0xFF);     /* 蓝色方块 */
+    lcd_fill(user_width-w, user_width-1, user_height-w, user_height-1, 0xFFFF00);/* 黄色方块 */
+
+    /* 画线: 十字交叉线 */
+    lcd_draw_line(0, user_height * 0.5, 1, user_width, 0xFFFFFF);   /* 白色水平线 */
+    lcd_draw_line(user_width * 0.5, 0, 0, user_height, 0xFFFFFF);   /* 白色垂直线 */
+
+    /* 画矩形 */
+    unsigned int s_x, s_y, e_x, e_y;
+    s_x = 0.25 * user_width;
+    s_y = w;
+    e_x = user_width - s_x;
+    e_y = user_height - s_y;
+
+    for ( ; (s_x <= e_x) && (s_y <= e_y);
+            s_x+=5, s_y+=5, e_x-=5, e_y-=5)
+        lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
+
+    /* 退出 */
+    munmap(screen_base, screen_size);
+    close(fd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+### 8.5 说明
+
+- 自定义的 4 个函数都基于**竖屏坐标**定义:应用传入竖屏坐标,函数内部转成实际物理坐标再算显存地址。
+- `user_width = fb_var.yres`(原 Y 变竖屏 X),`user_height = fb_var.xres`(原 X 变竖屏 Y)。
+- 与 `lcd_test.c`(图 19.4.3 横屏效果)对比,本例程显示的画面变成竖向(图 22.2.1)。
+- **缩放**:本例程只做坐标旋转,不做分辨率缩放。如果在竖屏坐标下需要将内容按比例显示,只需在坐标转换前对逻辑坐标乘以缩放系数(例如按屏幕短边取整),再套用同一地址公式即可。
+
+---
+
+## 9. 例程七:在 LCD 上显示字符(FreeType)
+
+对应例程:`23_freetype/show_char.c`(取模方式)与 `23_freetype/freetype_test.c`(FreeType 方式)。
+
+### 9.1 两种方式
+
+| 方式 | 原理 | 优点 | 缺点 |
+| ---- | ---- | ---- | ---- |
+| 取模显示 | 用取模软件生成字符点阵二维数组,逐 bit 打点 | 简单、无需库 | 只能显示固定几个字符,很“low” |
+| 字体引擎 | 解析 `.ttf`/`.otf`/`.ttc` 等字体文件,读取字形位图 | 通用、支持任意字符与字号 | 需移植 FreeType 等库 |
+
+取模原理:字符点阵中每个小方块对应一个 bit,填充用 1、不填充用 0。例如“正”是 64×86 点阵,用 `unsigned char arr[86][8]` 存储(宽 64 点 = 8 字节/行)。取模时选择“从左到右、从上到下”。
+
+### 9.2 FreeType 简介与移植
+
+**FreeType** 是完全免费(开源)的软件字体引擎,设计小巧、高效、可定制、可移植,提供统一接口访问多种字体格式(`FreeType 2` 已取代废弃的 `FreeType 1`)。
+
+移植步骤(本书选择 2.8 版本):
+
+1. 下载:https://download.savannah.gnu.org/releases/freetype/ ,取 `freetype-2.8.tar.gz`。
+2. 在 `tools` 下建安装目录 `freetype`,解压后进入源码目录。
+3. 打开 `include/freetype/config/ftoption.h`,去掉这两个宏的注释以启用系统 zlib 与 PNG 位图支持:
+
+```c
+#define FT_CONFIG_OPTION_SYSTEM_ZLIB
+#define FT_CONFIG_OPTION_USE_PNG
+```
+
+4. 配置、编译、安装:
+
+```bash
+./configure --prefix=/home/dt/tools/freetype/ --host=arm-poky-linux-gnueabi \
+  --with-zlib=yes --with-bzip2=no --with-png=yes --with-harfbuzz=no \
+  ZLIB_CFLAGS="-I/home/dt/tools/zlib/include -L/home/dt/tools/zlib/lib" ZLIB_LIBS=-lz \
+  LIBPNG_CFLAGS="-I/home/dt/tools/png/include -L/home/dt/tools/png/lib" LIBPNG_LIBS=-lpng
+make
+make install
+```
+
+5. 应用需包含两个头文件(后者是用宏定义的头文件):
+
+```c
+#include <ft2build.h>
+#include FT_FREETYPE_H
+```
+
+6. 移植到开发板前删除旧库,再拷贝新库并保持符号链接:
+
+```bash
+rm -rf /usr/lib/libfreetype.*
+```
+
+### 9.3 核心概念
+
+- **字形(glyph)**:字符图像。同一字符可有多种字形(宋体“国”与微软雅黑“国”)。
+- **字形索引(glyph index)**:字体文件中通过索引找到字形;索引由字符编码(ASCII、GB2312、Unicode 等)转换而来。
+- **点(point)**:数字印刷中的物理单位,1 点 = 1/72 英寸(1 英寸 = 25.4 毫米)。
+- **dpi**:每英寸像素点数。换算公式:`像素点数 = 点数 * dpi / 72`。
+
+字形布局参数(水平布局为例):
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `origin` / 基准线 | 水平基线与垂直基线用于定位与对齐字形 |
+| `width` / `height` | 字形轮廓最左到最右、最上到最下的距离 |
+| `bearingX`(`bitmap_left`) | 垂直基线到字形轮廓最左边的距离;水平布局为正 |
+| `bearingY`(`bitmap_top`) | 水平基线到字形轮廓最上边的距离;轮廓在基线上方为正 |
+| `xMin/xMax`、`yMin/yMax` | 字形轮廓四边位置,构成边界框(bbox) |
+| `advance`(`advance.x`) | 步进宽度/字间距,相邻两原点距离;26.6 固定浮点格式 |
+| `bitmap.buffer` | 字形位图,每点 1 字节;值为 0 不填充,大于 0 填充 |
+| `bitmap.rows` / `bitmap.width` | 位图行数 / 列数 |
+
+对齐:水平基线负责上下对齐,垂直基线负责左右对齐。画字形先定位左上角:若原点为 `(100,100)`,则左上角为 `(100+bearingX, 100-bearingY)`。
+
+> ⚠️ **来源说明**:`FT_Load_Glyph` + `FT_Render_Glyph` 的两步加载/渲染方式、以及 `FT_Get_Char_Index` 的完整用法,本节按 FreeType 通用接口补充,教材对应段落以图示为主。
+
+### 9.4 API 速查
+
+| 函数 | 作用 |
+| ---- | ---- |
+| `FT_Init_FreeType(&library)` | 初始化 FreeType 库对象,成功返回 0 |
+| `FT_New_Face(library, path, face_index, &face)` | 加载字体文件,创建 face;`face_index` 通常为 0,`face->num_faces` 指示文件内 face 数 |
+| `FT_Set_Pixel_Sizes(face, w, h)` | 以像素为单位设置字体宽高;某一位为 0 则等于另一位 |
+| `FT_Set_Char_Size(face, w, h, hdpi, vdpi)` | 以 1/64 点为单位的宽高 + dpi 设置字号 |
+| `FT_Set_Transform(face, &matrix, &pen)` | 设置 2×2 变换矩阵与原点,实现旋转/斜体 |
+| `FT_Get_Char_Index(face, code)` | 字符编码转字形索引 |
+| `FT_Load_Char(face, char, flags)` | 加载字符字形;`FT_LOAD_RENDER` 直接渲染为位图 |
+| `FT_Load_Glyph(face, index, flags)` | 按字形索引加载 |
+| `FT_Render_Glyph(slot, mode)` | 把已加载的字形轮廓渲染为位图 |
+| `FT_Done_Face(face)` / `FT_Done_FreeType(library)` | 释放 face / 库 |
+
+### 9.5 FreeType 显示字符完整源码
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/mman.h>
+#include <linux/fb.h>
+#include <math.h>       /* 数学库函数头文件 */
+
+#include <wchar.h>
+#include <ft2build.h>
+#include FT_FREETYPE_H
+
+#define FB_DEV "/dev/fb0"       /* LCD 设备节点 */
+
+#define argb8888_to_rgb565(color) (    \
+    {                                  \
+        unsigned int temp = (color);   \
+        ((temp & 0xF80000UL) >> 8) |   \
+            ((temp & 0xFC00UL) >> 5) | \
+            ((temp & 0xF8UL) >> 3);    \
+    })
+
+static unsigned int width;                  /* LCD 宽度 */
+static unsigned int height;                 /* LCD 高度 */
+static unsigned short *screen_base = NULL;  /* LCD 显存基地址 RGB565 */
+static unsigned long screen_size;
+static int fd = -1;
+
+static FT_Library library;
+static FT_Face face;
+
+static int fb_dev_init(void)
+{
+    struct fb_var_screeninfo fb_var = {0};
+    struct fb_fix_screeninfo fb_fix = {0};
+
+    /* 打开 framebuffer 设备 */
+    fd = open(FB_DEV, O_RDWR);
+    if (0 > fd) {
+        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
+        return -1;
+    }
+
+    /* 获取 framebuffer 设备信息 */
+    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    screen_size = fb_fix.line_length * fb_var.yres;
+    width = fb_var.xres;
+    height = fb_var.yres;
+
+    /* 内存映射 */
+    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fd);
+        return -1;
+    }
+
+    /* LCD 背景刷成黑色 */
+    memset(screen_base, 0xFF, screen_size);
+    return 0;
+}
+
+static int freetype_init(const char *font, int angle)
+{
+    FT_Error error;
+    FT_Vector pen;
+    FT_Matrix matrix;
+    float rad;      /* 旋转角度 */
+
+    /* FreeType 初始化 */
+    FT_Init_FreeType(&library);
+
+    /* 加载 face 对象 */
+    error = FT_New_Face(library, font, 0, &face);
+    if (error) {
+        fprintf(stderr, "FT_New_Face error: %d\n", error);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 原点坐标 */
+    pen.x = 0 * 64;
+    pen.y = 0 * 64;     /* 原点设置为(0, 0) */
+
+    /* 2x2 矩阵初始化 */
+    rad = (1.0 * angle / 180) * M_PI;   /* 角度转换为弧度,M_PI 是圆周率 */
+#if 0                                   /* 非水平方向 */
+    matrix.xx = (FT_Fixed)( cos(rad) * 0x10000L);
+    matrix.xy = (FT_Fixed)(-sin(rad) * 0x10000L);
+    matrix.yx = (FT_Fixed)( sin(rad) * 0x10000L);
+    matrix.yy = (FT_Fixed)( cos(rad) * 0x10000L);
+#endif
+
+#if 1 /* 斜体  水平方向显示的 */
+    matrix.xx = (FT_Fixed)(cos(rad) * 0x10000L);
+    matrix.xy = (FT_Fixed)(sin(rad) * 0x10000L);
+    matrix.yx = (FT_Fixed)(0 * 0x10000L);
+    matrix.yy = (FT_Fixed)(1 * 0x10000L);
+#endif
+
+    /* 设置 */
+    FT_Set_Transform(face, &matrix, &pen);
+    FT_Set_Pixel_Sizes(face, 40, 0); /* 设置字体大小 */
+
+    return 0;
+}
+
+static void lcd_draw_character(int x, int y,
+                               const wchar_t *str, unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+    FT_GlyphSlot slot = face->glyph;
+    size_t len = wcslen(str);   /* 计算字符的个数 */
+    long int temp;
+    int n;
+    int i, j, p, q;
+    int max_x, max_y, start_y, start_x;
+
+    /* 循环加载各个字符 */
+    for (n = 0; n < len; n++) {
+
+        /* 加载字形、转换得到位图数据 */
+        if (FT_Load_Char(face, str[n], FT_LOAD_RENDER))
+            continue;
+
+        start_y = y - slot->bitmap_top; /* 字形轮廓上边 y 坐标起点,注意减去 bitmap_top */
+        if (0 > start_y) {              /* 如果为负数 */
+            q = -start_y;
+            temp = 0;
+            j = 0;
+        }
+        else {                          /* 正数 */
+            q = 0;
+            temp = width * start_y;
+            j = start_y;
+        }
+
+        max_y = start_y + slot->bitmap.rows;    /* 字形轮廓下边 y 坐标结束位置 */
+        if (max_y > (int)height)
+            max_y = height;
+
+        for (; j < max_y; j++, q++, temp += width) {
+
+            start_x = x + slot->bitmap_left;    /* 起点要加上左边空余部分长度 */
+            if (0 > start_x) {
+                p = -start_x;
+                i = 0;
+            }
+            else {
+                p = 0;
+                i = start_x;
+            }
+
+            max_x = start_x + slot->bitmap.width;
+            if (max_x > (int)width)
+                max_x = width;
+
+            for (; i < max_x; i++, p++) {
+
+                /* 如果数据不为 0,则表示需要填充颜色 */
+                if (slot->bitmap.buffer[q * slot->bitmap.width + p])
+                    screen_base[temp + i] = rgb565_color;
+            }
+        }
+
+        /* 调整到下一个字形的原点 */
+        x += slot->advance.x / 64;  /* 26.6 固定浮点格式 */
+        y -= slot->advance.y / 64;
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    /* LCD 初始化 */
+    if (fb_dev_init())
+        exit(EXIT_FAILURE);
+
+    /* freetype 初始化 */
+    if (freetype_init(argv[1], atoi(argv[2])))
+        exit(EXIT_FAILURE);
+
+    /* 在 LCD 上显示中文 */
+    int y = height * 0.25;
+    lcd_draw_character(30, 80, L"路漫漫其修远兮,吾将上下而求索", 0x000000);
+    lcd_draw_character(30, y + 80, L"莫愁前路无知己,天下谁人不识君", 0x9900FF);
+    lcd_draw_character(30, 2 * y + 80, L"君不见黄河之水天上来,奔流到海不复回", 0xFF0099);
+    lcd_draw_character(30, 3 * y + 80, L"君不见高堂明镜悲白发,朝如青丝暮成雪", 0x9932CC);
+
+    /* 退出程序 */
+    FT_Done_Face(face);
+    FT_Done_FreeType(library);
+    munmap(screen_base, screen_size);
+    close(fd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+**逐段说明:**
+
+- `fb_dev_init()`:完整的 FrameBuffer 初始化模板(open → ioctl → mmap → memset)。
+- `freetype_init()`:初始化库、加载字体、设置变换矩阵与字号。`0x10000L` 是 FreeType 的 16.16 定点;`FT_Set_Transform` 配合 `pen` 原点实现旋转/斜体;`FT_Set_Pixel_Sizes(face, 40, 0)` 设置字号 40 像素,高度 0 表示与宽度相等。
+- `lcd_draw_character()`:用 `wcslen` 计算宽字符个数,逐个 `FT_Load_Char(..., FT_LOAD_RENDER)` 直接得到位图。`bitmap_top`/`bitmap_left` 用于把字形左上角对齐到 `(x, y)`;`bitmap.buffer` 中非 0 的点才填充颜色。
+- 每画完一个字符,`x += advance.x / 64`(26.6 定点右移 6 位)、`y -= advance.y / 64`,移动到下一个字形原点。
+- 运行参数:第一个是字体文件,第二个是旋转角度(0 表示不旋转)。
+
+### 9.6 取模方式显示字符(show_char.c)
+
+取模方式不依赖 FreeType,直接用字符点阵数组打点。核心函数:
+
+```c
+#define argb8888_to_rgb565(color)   ({ \
+            unsigned int temp = (color); \
+            ((temp & 0xF80000UL) >> 8) | \
+            ((temp & 0xFC00UL) >> 5) | \
+            ((temp & 0xF8UL) >> 3); \
+            })
+
+/********************************************************************
+ * 函数名称: lcd_draw_character
+ * 功能描述: 在 LCD 屏指定位置处(x, y)画字符;指针 ch 指向字符子模数组,
+ *           参数 w、h 分别表示字符的宽度和高度
+ ********************************************************************/
+static void lcd_draw_character(unsigned int x, unsigned int y,
+            const unsigned char *ch, unsigned int w,
+            unsigned int h, unsigned int color)
+{
+    unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
+    unsigned long temp;
+    unsigned int end_x, end_y;
+    int j;
+    int columns;
+
+    /*
+     * 计算二维数组有多少列。参数 w 是字符宽度,1 个宽度对应 1 个 bit 位,
+     * 并不是一个字节;若宽度不是 8 的整数倍通常会补零。
+     */
+    columns = w / 8;        /* 1byte = 8bit */
+    if (0 != w % 8) columns++;
+
+    /* 对参数进行限定 */
+    if (w < 1 || h < 1) return;
+    if (x >= width || y >= height) return;
+
+    /* 计算出结束坐标位置 */
+    end_x = x + w - 1;
+    end_y = y + h - 1;
+
+    /* 对结束坐标位置进行限定 */
+    if (end_x >= width)
+        end_x = width - 1;
+    if (end_y >= height)
+        end_y = height - 1;
+
+    /* 计算有效宽度 */
+    h = end_y - y + 1;
+    w = end_x - x + 1;
+
+    /* 打点 */
+    temp = y * width + x;   /* 定位到起点 */
+    for (y = 0; y < h; y++, temp += width) {
+
+        for (x = 0, j = 0; x < w; ) {
+
+            if (*(ch + y * columns + j) & (0x1 << (x % 8)))
+                screen_base[temp + x] = rgb565_color;
+            x++;
+            if (0 == x % 8) j++;
+        }
+    }
+}
+```
+
+字体数组形如 `static unsigned char ch_char1[86][8] = { {0x00,...}, ... }`,四个数组分别对应“正点原子”,调用时一个个居中绘制:
+
+```c
+int x = width * 0.5 - 128;
+int y = height * 0.5 - 43;
+lcd_draw_character(x,       y, (unsigned char *)ch_char1, 64, 86, 0xFF00FF);
+lcd_draw_character(x + 64,  y, (unsigned char *)ch_char2, 64, 86, 0xFF00FF);
+lcd_draw_character(x + 128, y, (unsigned char *)ch_char3, 64, 86, 0xFF00FF);
+lcd_draw_character(x + 192, y, (unsigned char *)ch_char4, 64, 86, 0xFF00FF);
+```
+
+**要点:** `columns = w / 8`(向上取整)得到二维数组列数;每个字节从左到右对应 8 个 bit;`0x1 << (x % 8)` 逐 bit 判断是否填充。
+
+### 9.7 字体文件
+
+Windows 的字体放在 `C:\Windows\Fonts`,Linux 通常放在 `/usr/share/fonts`,格式有 `otf`、`ttf`、`ttc` 等。移植的 FreeType 与开发板自带字体可能不匹配,可从 Windows 拷贝一个字体(如宋体 `SIMSUN.TTC`)到开发板使用。
+
+---
+
+## 10. 编译与运行命令汇总
+
+先初始化交叉编译环境(每个新终端一次):
+
+```bash
+export CC=arm-poky-linux-gnueabi-gcc     # 或用环境初始化脚本导出的 ${CC}
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+```
+
+| 例程 | 依赖库 | 交叉编译命令 |
+| ---- | ------ | ------------ |
+| `lcd_info.c` / `lcd_test.c` / `bmp_show.c` / `lcd_vertical_display.c` | 无 | `${CC} -o testApp xxx.c` |
+| `show_jpeg_image.c` | libjpeg | `${CC} -o testApp show_jpeg_image.c -I /home/dt/tools/jpeg/include -L /home/dt/tools/jpeg/lib -ljpeg` |
+| `show_png_image.c` | libpng + zlib | `${CC} -o testApp show_png_image.c -I/home/dt/tools/png/include -L/home/dt/tools/png/lib -L/home/dt/tools/zlib/lib -lpng -lz` |
+| `freetype_test.c` | freetype + zlib + libpng + libm | `${CC} -o testApp freetype_test.c -I/home/dt/tools/freetype/include/freetype2 -L/home/dt/tools/freetype/lib -lfreetype -L/home/dt/tools/zlib/lib -lz -L/home/dt/tools/png/lib -lpng -lm` |
+
+编译选项说明:
+
+- `-I`:头文件搜索路径(libpng 不需要指定 zlib 头文件)。
+- `-L`:库文件搜索路径(可重复多次)。
+- `-l`:链接的库,`jpeg`→`libjpeg.so`、`png`→`libpng.so`、`z`→`libz.so`、`freetype`→`libfreetype.so`、`m`→`libm.so`。
+- FreeType 头文件在 `include/freetype2` 目录下,所以 `-I` 要指到 `freetype2`。
+
+---
+
+## 11. 实验步骤与调试
+
+### 11.1 实验步骤
+
+1. **连接 LCD**:断电状态下用软排线把 LCD 接到开发板,再上电。
+2. **退出出厂 Qt GUI**:测试前先退出出厂系统对应的 Qt GUI 应用程序,避免抢占显存。
+3. **交叉编译**:按上表编译对应例程,得到 `testApp`。
+4. **拷贝到开发板**:把可执行文件(图片/字体例程还要带上 `.jpg/.png/.ttc` 文件)拷到用户家目录,例如 `/home/root`。
+5. **运行**:
+
+```bash
+./testApp                       # lcd_info / lcd_test
+./testApp image.bmp             # bmp_show
+./testApp image.jpg             # libjpeg
+./testApp image.png             # libpng
+./testApp                       # 竖屏
+./testApp SIMSUN.TTC 0          # freetype,参数:字体文件 + 旋转角度
+```
+
+6. **验证库移植**:`djpeg --help`(libjpeg)能打印帮助即成功。
+
+### 11.2 常见问题与调试
+
+| 现象 | 可能原因 | 处理 |
+| ---- | -------- | ---- |
+| `open error` | 设备节点不存在 / 权限不足 | 确认 `/dev/fb0` 存在;用 root 或加权限 |
+| `mmap error` | `screen_size` 计算错误或参数非法 | 检查 `line_length * yres`;先跑 `lcd_info` 确认参数 |
+| 花屏 / 颜色错乱 | 把 RGB565 当 RGB888(或反之) | 用 `lcd_info` 打印像素格式,按实际位域转换 |
+| 图片上下颠倒 | BMP 正/倒向位图处理错 | 按 `biHeight` 正负分别处理 |
+| 竖屏画面方向不对 | 坐标变换公式与预期布局不一致 | 确认原点定义(左下角/左上角),套用对应公式 |
+| 屏幕内容被 Qt 覆盖 | 出厂 Qt GUI 未退出 | 先退出 Qt 程序再运行 |
+| 运行报缺库 | 目标板 `/usr/lib` 无对应 `.so` | 拷贝库文件并保持符号链接 |
+| FreeType 显示乱码 | 字体文件与程序不匹配 | 换一个 `.ttf/.ttc` 字体(如宋体) |
+| 删除旧库后 Qt 图片空白 | 出厂 Qt 依赖旧版 libjpeg/libpng 等 | 属预期现象,学习阶段知道即可 |
+| 编译报缺 `-lz/-lpng` | 未链接依赖库 | 按第 10 节补全 `-L`/`-l` |
+
+**常用辅助命令:**
+
+```bash
+dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125   # 清屏为黑
+djpeg --help                                      # 验证 libjpeg
+cat /proc/fb                                       # 查看已注册的 fb 设备
+```
+
+---
+
+## 12. 跨平台对比
+
+| 平台 / 场景 | 显示抽象 | 上屏方式 | 特点 |
+| ----------- | -------- | -------- | ---- |
+| 嵌入式 Linux(FrameBuffer) | `/dev/fb0` + 显存 | `mmap` 后直接写显存 | 接口简单、稳定,本书主线 |
+| 嵌入式 Linux(DRM/KMS) | `/dev/dri/card0` + plane | `drmMode*` 提交 framebuffer | 现代方案,支持多图层/硬件合成 |
+| Windows | GDI / DIB | 位图 API、绘图函数 | 图形子系统封装,不直接暴露显存 |
+| 裸机 MCU(如 STM32 + 并口屏) | LCD 控制器寄存器 + FSMC/SDRAM | 写帧缓存地址,靠 DMA/控制器扫描 | 无操作系统,需自己配时序 |
+| Android | SurfaceFlinger + Gralloc | Surface/Canvas,合成后送显示 | 多层合成,应用不碰显存 |
+
+> ⚠️ **来源说明**:DRM/KMS、Android SurfaceFlinger、Windows GDI 的对比属于教材之外的扩展知识,用于建立全局认知;教材只讲解 Linux FrameBuffer。
+
+本知识库内的驱动侧视角见 [[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/03-LCD驱动]],触摸部分见 [[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]],输入设备与 tslib 见 [[03-外设与高级IO编程/03-输入设备与tslib]]。
+
+---
+
+## 13. 面试题(含答案)
+
+### 1. FrameBuffer 是什么?为什么显示编程通常用 mmap 而不是 read/write?
+
+FrameBuffer 是 Linux 的显示驱动接口,把显示设备抽象成一块保存一帧图像的显存,设备节点为 `/dev/fbX`,读写它相当于读写显示缓冲区。用 `mmap` 是因为一帧数据量很大(如 1920×1080 ARGB8888 约 8MB)且画面频繁更新,普通 `read/write` 需要内核与用户空间之间反复拷贝,效率低;`mmap` 把显存映射到用户地址空间后可直接内存访问,一次映射、反复读写。
+
+### 2. 如何判断 FrameBuffer 的像素格式?RGB888 怎么转 RGB565?
+
+用 `ioctl(fd, FBIOGET_VSCREENINFO, &fb_var)` 取 `fb_var`,看 `bits_per_pixel` 和 `red/green/blue` 的 `offset`、`length`。例如 `R<11 5> G<5 6> B<0 5>` 且 bpp=16 就是 RGB565。RGB888→RGB565:
+
+```c
+fb = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
+```
+
+### 3. 简述 libjpeg 的解码流程,为什么读取到的数据是 BGR888?
+
+流程:`jpeg_create_decompress` → `jpeg_stdio_src` → `jpeg_read_header` → 设置 `out_color_space`(默认 `JCS_RGB`)/`scale` → `jpeg_start_decompress` → 循环 `jpeg_read_scanlines` 每次读 1 行 → `jpeg_finish_decompress` → `jpeg_destroy_decompress`。libjpeg 默认输出 BGR888(R 在低 8 位、B 在高 8 位),所以按 B、G、R 的内存顺序取分量再转 RGB565。
+
+### 4. libpng 中 setjmp/longjmp 的作用?high-level 与 low-level 接口有何区别?
+
+libpng 默认错误处理会调用 `longjmp()` 跳转,需要用 `setjmp(png_jmpbuf(png_ptr))` 设置错误返回点,跳回后执行 `png_destroy_read_struct` 等清理,避免内存泄漏;`longjmp` 的 `val` 不能为 0。high-level 用 `png_read_png()` 一次性解码全部数据并自动分配缓冲区,简单但不灵活、输出格式受预定义转换限制;low-level 用 `png_read_info` + `png_set_xxx` + `png_read_update_info` + `png_read_image`/`png_read_rows`,需自己分配缓冲区但灵活。high-level 获取数据用 `png_get_rows`。
+
+### 5. 横屏转竖屏的显存地址公式怎么来的?FreeType 里 bearingY 和 advance 有什么作用?
+
+竖屏把应用坐标重新定义:例如以左下角为原点,应用 `(x, y)` 对应物理坐标 `(y, height-1-x)`,代入横屏公式 `base + (y*width + x)*pix_bytes`,得到 `base + ((height-1-x)*width + y)*pix_bytes`(RGB565 下即 `screen_base[(lcd_max_y-x)*lcd_width + y]`)。该公式只对应一种竖屏布局,旋转 180° 的竖屏公式不同。FreeType 中 `bitmap_top`(bearingY)是水平基线到字形轮廓上边的距离,用来把字形左上角对齐到绘制点 `(x, y-bearingY)`;`advance.x` 是步进宽度(26.6 定点),画完一个字符后 `x += advance.x/64` 定位到下一个字符原点,保证字符间距与对齐。
+
+---
+
+**内容来源**
+
+- 正点原子《I.MX6U 嵌入式 Linux C 应用编程指南 V1.6》
+  - 第十九章 FrameBuffer 应用编程
+  - 第二十章 在 LCD 上显示 jpeg 图像
+  - 第二十一章 在 LCD 上显示 png 图片
+  - 第二十二章 LCD 横屏切换为竖屏
+  - 第二十三章 在 LCD 上显示字符
+- 例程源码:`11、Linux C 应用编程例程源码` → `19_lcd`、`20_libjpeg`、`21_libpng`、`22_lcd_vertical_display`、`23_freetype`
+- 开发板:正点原子 ALPHA / Mini I.MX6U(7 寸 800×480 RGB LCD,出厂系统 RGB565)
+

+ 698 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/05-PWM与看门狗.md

@@ -0,0 +1,698 @@
+---
+title: PWM与看门狗
+tags: [嵌入式Linux, Linux应用编程, PWM, sysfs, 看门狗, watchdog, ioctl, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十四章 PWM应用编程、第二十七章 看门狗应用编程"
+---
+
+# PWM与看门狗
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]];延伸阅读:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/06-RTC与PWM驱动]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/01-LED与蜂鸣器驱动]]
+
+本篇讲两个外设:**PWM**(脉冲宽度调制,用于背光调光、蜂鸣器发声、电机调速)和**看门狗**(Watchdog,用于系统异常时自动复位)。二者在应用层的操作方式很不一样——PWM 通过 **sysfs 属性文件**读写,看门狗通过 **ioctl** 控制,但都属于"把内核驱动暴露的接口用好"这一类应用编程。
+
+---
+
+## 第一部分:PWM 应用编程
+
+## 1. PWM 基本原理
+
+PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过**数字信号占空比**来模拟不同输出的技术。一个 PWM 波形由三个要素描述:
+
+| 要素 | 含义 | 单位 |
+| ---- | ---- | ---- |
+| 周期 `period` | 一个完整波形的时间长度 | ns(纳秒) |
+| 频率 `f` | 周期的倒数,`f = 1 / period` | Hz |
+| 占空比 `duty_cycle` | 一个周期内高电平的时间长度 | ns(纳秒) |
+| 极性 `polarity` | 高电平有效(normal)或低电平有效(inversed) | — |
+
+占空比比例 = `duty_cycle / period`。例如周期 `500000 ns = 500 us`,对应频率 `1 / 500us = 2 kHz`;占空比 `250000 ns` 占周期一半,即 50%。
+
+```mermaid
+flowchart LR
+    subgraph 一个周期 period
+        H["高电平 duty_cycle"] --> L["低电平 period - duty_cycle"]
+    end
+    P["period = 500000ns (500us)"] --> F["频率 f = 1/period = 2kHz"]
+    H --> D["占空比 = duty_cycle/period"]
+```
+
+PWM 的典型用途:LCD 背光调光(改变占空比即改变平均亮度)、无源蜂鸣器发声(改变频率即改变音调)、电机调速等。I.MX6U 共有 **8 个 PWM 控制器(PWM1~PWM8)**,每个控制器只有 1 路 PWM 输出。
+
+---
+
+## 2. 应用层如何操控 PWM:sysfs
+
+与 LED 一样,PWM 通过 **sysfs** 操控。进入 `/sys/class/pwm` 目录,可以看到 8 个以 `pwmchipX`(X 为 0~7)命名的文件夹,分别对应 I.MX6U 的 8 个 PWM 控制器:
+
+```bash
+ls /sys/class/pwm
+# pwmchip0  pwmchip1  ...  pwmchip7
+```
+
+进入某个控制器目录(如 `pwmchip0`),重点关注 `npwm`、`export`、`unexport`:
+
+| 属性文件 | 读写 | 说明 |
+| -------- | ---- | ---- |
+| `npwm` | 只读 | 该 PWM 控制器下共有几路 PWM 输出 |
+| `export` | 只写 | 导出 PWM,写入编号(从 0 开始) |
+| `unexport` | 只写 | 删除已导出的 PWM,写入编号与 export 对应 |
+
+I.MX6U 每个 PWM 控制器只有 1 路输出,因此 `npwm` 读出来是 1,导出时编号都用 0。
+
+```bash
+echo 0 > /sys/class/pwm/pwmchip0/export   # 导出 PWM1
+echo 0 > /sys/class/pwm/pwmchip1/export   # 导出 PWM2
+# ... pwmchip2~pwmchip5 同理,编号都是 0
+```
+
+导出成功后会生成一个 `pwm0` 目录。
+
+> 注意:开发板出厂系统中,**PWM1 已被用作 LCD 背光控制**,应用层不能直接控制;其它 PWM 因 I/O 资源不够也均不能使用。若要用 PWM1 测试,需要先禁用 LCD 与 backlight(见第 4 节)。
+
+### 2.1 控制 PWM:pwm0 目录下的属性
+
+```bash
+ls /sys/class/pwm/pwmchip0/pwm0
+# duty_cycle  enable  period  polarity
+```
+
+| 属性文件 | 读写 | 说明 |
+| -------- | ---- | ---- |
+| `enable` | 可读写 | 写 `1` 使能 PWM 输出,写 `0` 禁止;读可获取当前状态 |
+| `polarity` | 可读写 | `normal` 普通极性、`inversed` 反转极性 |
+| `period` | 可读写 | 配置 PWM 周期,单位 ns |
+| `duty_cycle` | 可读写 | 配置 PWM 占空比,单位 ns |
+
+```bash
+echo 0 > enable           # 禁止 PWM 输出
+echo 1 > enable           # 使能 PWM 输出(通常配置好后再使能)
+echo normal > polarity    # 默认极性
+echo inversed > polarity  # 极性反转
+echo 10000 > period       # PWM 周期设置为 10us(10 * 1000ns)
+echo 5000  > duty_cycle   # 占空比设置为 5us
+```
+
+> 很多 SoC 的 PWM 外设在硬件上并不支持极性配置,对应驱动未实现该接口,应用层自然无法通过 `polarity` 配置。ALPHA/Mini I.MX6U 出厂系统便如此。
+
+---
+
+## 3. 编写 PWM 应用程序(pwm.c)
+
+程序接收 3 个参数:PWM 编号、周期(ns)、占空比(ns)。先确保 PWM 已导出,再依次配置周期、占空比并使能。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+
+static char pwm_path[100];
+
+static int pwm_config(const char *attr, const char *val)
+{
+    char file_path[100];
+    int len;
+    int fd;
+
+    sprintf(file_path, "%s/%s", pwm_path, attr);
+    if (0 > (fd = open(file_path, O_WRONLY))) {
+        perror("open error");
+        return fd;
+    }
+
+    len = strlen(val);
+    if (len != write(fd, val, len)) {
+        perror("write error");
+        close(fd);
+        return -1;
+    }
+
+    close(fd);  //关闭文件
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    /* 校验传参 */
+    if (4 != argc) {
+        fprintf(stderr, "usage: %s <id> <period> <duty>\n",
+                argv[0]);
+        exit(-1);
+    }
+
+    /* 打印配置信息 */
+    printf("PWM config: id<%s>, period<%s>, duty<%s>\n",
+            argv[1], argv[2],
+            argv[3]);
+
+    /* 导出pwm */
+    sprintf(pwm_path, "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
+
+    if (access(pwm_path, F_OK)) {//如果pwm0目录不存在, 则导出
+
+        char temp[100];
+        int fd;
+
+        sprintf(temp, "/sys/class/pwm/pwmchip%s/export", argv[1]);
+        if (0 > (fd = open(temp, O_WRONLY))) {
+            perror("open error");
+            exit(-1);
+        }
+
+        if (1 != write(fd, "0", 1)) {//导出pwm
+            perror("write error");
+            close(fd);
+            exit(-1);
+        }
+
+        close(fd);  //关闭文件
+    }
+
+    /* 配置PWM周期 */
+    if (pwm_config("period", argv[2]))
+        exit(-1);
+
+    /* 配置占空比 */
+    if (pwm_config("duty_cycle", argv[3]))
+        exit(-1);
+
+    /* 使能pwm */
+    pwm_config("enable", "1");
+
+    /* 退出程序 */
+    exit(0);
+}
+```
+
+逐段解释:
+
+| 位置 | 代码 | 说明 |
+| ---- | ---- | ---- |
+| `pwm_config()` | 通用属性写函数 | 拼接 `pwm_path/attr` 路径,以只写方式打开并写入值,是操作 sysfs 属性的标准写法 |
+| 传参校验 | `4 != argc` | 需传入 `id`、`period`、`duty` 三个参数 |
+| `sprintf(pwm_path, ...)` | 构造 `pwm0` 路径 | 如 `id=0` 时为 `/sys/class/pwm/pwmchip0/pwm0` |
+| `access(pwm_path, F_OK)` | 判断是否已导出 | 目录不存在说明未导出,需要写 `export` |
+| 写 `export` | `write(fd, "0", 1)` | 导出编号 0 的 PWM |
+| `pwm_config("period", ...)` | 配置周期 | 单位 ns |
+| `pwm_config("duty_cycle", ...)` | 配置占空比 | 单位 ns |
+| `pwm_config("enable", "1")` | 使能输出 | 配置完成后最后一步 |
+
+编译:
+
+```bash
+arm-linux-gnueabihf-gcc -o testApp pwm.c
+scp testApp root@192.168.1.10:/home/root/
+```
+
+---
+
+## 4. 在开发板上测试
+
+### 4.1 先把 PWM1 腾出来
+
+出厂系统无法直接使用 PWM,因为 PWM1 被 LCD 背光占用。要想测试,需要对设备树做配置、禁用 LCD 和 backlight 设备(`status` 属性设置为 `disabled`),再重新编译设备树、用生成的 dtb 替换开发板启动文件中的 dtb。也可以参考《I.MX6U 嵌入式 Linux 驱动开发指南》自行配置 PWM。
+
+这里介绍一个**不用重新编译设备树**的简单方法:重启开发板进入 u-boot 命令行模式,u-boot 启动时会把内核设备树(dtb)拷贝到内存,我们直接查看、修改内存中的那份设备树,把 LCD 和 backlight 的 `status` 改为 `disabled`。
+
+SD/eMMC 启动方式,依次执行:
+
+```text
+setenv disable_lcd 'fdt addr ${fdt_addr}; fdt set /backlight status disable; fdt set /soc/aips-bus@02100000/lcdif@021c8000 status disable'
+setenv mmcboot 'echo Booting from mmc ...; run mmcargs; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if run loadfdt; then run disable_lcd; bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;'
+```
+
+NAND 启动方式:
+
+```text
+setenv disable_lcd 'fdt addr ${fdt_addr}; fdt set /backlight status disable; fdt set /soc/aips-bus@02100000/lcdif@021c8000 status disable'
+setenv bootcmd 'nand read ${loadaddr} 0x620000 0x800000;nand read ${fdt_addr} ${fdt_offset} 0x20000; run disable_lcd; bootz ${loadaddr} - ${fdt_addr}'
+```
+
+然后执行 `boot` 启动内核。系统启动后 PWM1 就腾出来了,此时 LCD 被禁用。
+
+> Tips:这种修改方式**只对本次启动生效**,因为我们改的是内存中的设备树;下一次重启会恢复原状。
+
+### 4.2 运行并验证波形
+
+开发板出厂系统已将 PWM1 输出绑定到 **GPIO1_IO08** 引脚(即 LCD 背光引脚),该引脚已通过扩展口引出,可用示波器测量。
+
+```bash
+./testApp 0 500000 250000
+# PWM config: id<0>, period<500000>, duty<250000>
+```
+
+本实验测试 PWM1。用示波器检测 GPIO1_IO08 引脚,应看到周期 `500us`(即 `500000ns`)、对应频率 `2kHz`、占空比 50% 的 PWM 波形,与配置一致。
+
+### 4.3 调试方法
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| `open error: No such file or directory` | 没进 u-boot 禁用 LCD/backlight,或路径/编号写错 | 确认 `ls /sys/class/pwm/pwmchip0/pwm0` |
+| 写属性报错 | 未先导出、或写入格式不对 | 先 `echo 0 > export`,写入的值不带单位 |
+| 引脚无波形 | PWM 未使能 / 被其它功能占用 | `cat enable` 应为 1;确认 PWM1 已腾出 |
+| 占空比大于周期 | 参数不合法 | `duty_cycle` 必须不超过 `period` |
+| 无 `pwm0` 目录 | 未导出 | 程序会自动导出,也可手动 `echo 0 > export` |
+
+### 4.4 PWM 蜂鸣器/背光实验
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+- **背光调光**:PWM1 原本就驱动 LCD 背光,改变 `duty_cycle / period` 即可改变平均亮度。运行 `./testApp 0 500000 100000`(20% 占空比)与 `./testApp 0 500000 400000`(80% 占空比),可对比亮度差异。
+- **蜂鸣器发声**:无源蜂鸣器需要外部方波驱动,**频率决定音调、占空比影响响度**。可把 PWM 输出经扩展口接到无源蜂鸣器,用 `pwm.c` 改变 `period` 产生不同音调,例如 `period=1000000ns` 对应 1kHz。有源蜂鸣器自带振荡电路,只需电平驱动即可,不需要 PWM。
+- 注意:实验前同样需要按第 4.1 节腾出 PWM 通道,并确认引脚连接正确,避免与 LCD 背光功能冲突。
+
+---
+
+## 第二部分:看门狗应用编程
+
+## 5. 看门狗原理
+
+在产品化的嵌入式系统中,为使系统在异常情况下能自动复位,一般都需要引入看门狗。看门狗其实就是一个**可以在一定时间内被复位(重置)的计数器**(看门狗计时器/定时器):
+
+- 看门狗启动后,计数器开始自动计数;
+- 经过一定时间若没有被复位,计数器溢出就会对 CPU 产生一个**复位信号使系统重启**(俗称"被狗咬");
+- 系统正常运行时,需在看门狗允许的时间间隔内对计数器清零(俗称"喂狗"),不让复位信号产生;
+- 若程序跑飞、没有按时喂狗,系统就"被咬"复位。
+
+有些看门狗也可以只产生中断信号而不使系统复位。
+
+```mermaid
+flowchart LR
+    START["看门狗启动/喂狗<br/>计数器清零"] --> COUNT["计数器递减/递增"]
+    COUNT --> Q{"计时期内<br/>是否喂狗?"}
+    Q -->|"是"| START
+    Q -->|"否(超时溢出)"| RST["产生复位信号<br/>系统重启"]
+```
+
+I.MX6UL/I.MX6ULL SoC 集成了两个看门狗定时器(WDOG):**WDOG1 和 WDOG2**。其中 WDOG2 用于安全目的,WDOG1 则是一个普通看门狗,支持产生中断信号以及复位 CPU。
+
+---
+
+## 6. 看门狗设备节点
+
+Linux 系统中注册的看门狗外设,都会在 `/dev/` 下生成设备节点,名称通常为 `watchdogX`(X 为编号 0、1、2…),如 `/dev/watchdog0`、`/dev/watchdog1`。
+
+```bash
+ls /dev/watchdog*
+# /dev/watchdog  /dev/watchdog0
+```
+
+`watchdog0` 就是 I.MX6U 的 WDOG1 对应的设备节点。而 `/dev/watchdog` 名称不带数字编号,它代表**系统默认的看门狗设备**,通常指的就是 `watchdog0`——即这两个节点代表同一个硬件外设。
+
+---
+
+## 7. 通过 ioctl 控制看门狗
+
+应用层控制看门狗通过 `ioctl()` 实现。程序中需要包含头文件:
+
+```c
+
+#include <linux/watchdog.h>
+
+```
+
+该头文件定义了各种 `ioctl` 指令宏,每个指令表示向设备请求不同操作:
+
+```c
+
+#define WDIOC_GETSUPPORT    _IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info)
+#define WDIOC_GETSTATUS     _IOR(WATCHDOG_IOCTL_BASE, 1, int)
+#define WDIOC_GETBOOTSTATUS _IOR(WATCHDOG_IOCTL_BASE, 2, int)
+#define WDIOC_GETTEMP       _IOR(WATCHDOG_IOCTL_BASE, 3, int)
+#define WDIOC_SETOPTIONS    _IOR(WATCHDOG_IOCTL_BASE, 4, int)
+#define WDIOC_KEEPALIVE     _IOR(WATCHDOG_IOCTL_BASE, 5, int)
+#define WDIOC_SETTIMEOUT    _IOWR(WATCHDOG_IOCTL_BASE, 6, int)
+#define WDIOC_GETTIMEOUT    _IOR(WATCHDOG_IOCTL_BASE, 7, int)
+#define WDIOC_SETPRETIMEOUT _IOWR(WATCHDOG_IOCTL_BASE, 8, int)
+#define WDIOC_GETPRETIMEOUT _IOR(WATCHDOG_IOCTL_BASE, 9, int)
+#define WDIOC_GETTIMELEFT   _IOR(WATCHDOG_IOCTL_BASE, 10, int)
+
+```
+
+常用指令:
+
+| ioctl 指令 | 作用 |
+| ---------- | ---- |
+| `WDIOC_GETSUPPORT` | 获取看门狗支持哪些功能 |
+| `WDIOC_SETOPTIONS` | 开启或关闭看门狗 |
+| `WDIOC_KEEPALIVE` | 喂狗操作 |
+| `WDIOC_SETTIMEOUT` | 设置看门狗超时时间 |
+| `WDIOC_GETTIMEOUT` | 获取看门狗超时时间 |
+
+### 7.1 打开设备
+
+```c
+int fd;
+fd = open("/dev/watchdog", O_RDWR);
+if (0 > fd)
+    fprintf(stderr, "open error: %s: %s\n", "/dev/watchdog", strerror(errno));
+
+```
+
+### 7.2 获取设备支持的功能:WDIOC_GETSUPPORT
+
+```c
+ioctl(int fd, WDIOC_GETSUPPORT, struct watchdog_info *info);
+
+```
+
+需要传入一个 `struct watchdog_info *`,`ioctl` 会把获取到的数据写入该对象:
+
+```c
+struct watchdog_info {
+    __u32 options;          /* Options the card/driver supports */
+    __u32 firmware_version; /* Firmware version of the card */
+    __u8  identity[32];     /* Identity of the board */
+};
+
+```
+
+- `options`:设备支持哪些功能或选项;
+- `firmware_version`:固件版本号;
+- `identity`:描述性字符串。
+
+`options` 字段可取以下任意一个值或多值的位或:
+
+```c
+#define WDIOF_OVERHEAT     0x0001 /* Reset due to CPU overheat */
+#define WDIOF_FANFAULT     0x0002
+#define WDIOF_EXTERN1      0x0004
+#define WDIOF_POWERUNDER   0x0010
+#define WDIOF_CARDRESET    0x0020
+#define WDIOF_POWEROVER    0x0040
+#define WDIOF_SETTIMEOUT   0x0080 /* Set timeout (in seconds) */
+#define WDIOF_MAGICCLOSE   0x0100 /* Supports magic close char */
+#define WDIOF_PRETIMEOUT   0x0200 /* Pretimeout (in seconds), get/set */
+#define WDIOF_ALARMONLY    0x0400 /* Watchdog triggers alarm, not a reboot */
+#define WDIOF_KEEPALIVEPING 0x8000 /* Keep alive ping reply */
+```
+
+常见的值有 `WDIOF_SETTIMEOUT`(设备支持设置超时时间)与 `WDIOF_KEEPALIVEPING`(设备支持"喂狗",即重置计时器)。
+
+```c
+struct watchdog_info info;
+if (0 > ioctl(fd, WDIOC_GETSUPPORT, &info)) {
+    fprintf(stderr, "ioctl error: WDIOC_GETSUPPORT: %s\n", strerror(errno));
+    return -1;
+}
+printf("identity: %s\n", info.identity);
+printf("version: %u\n", info.firmware_version);
+if (0 == (WDIOF_KEEPALIVEPING & info.options))
+    printf("设备不支持喂狗操作\n");
+if (0 == (WDIOF_SETTIMEOUT & info.options))
+    printf("设备不支持设置超时时间\n");
+```
+
+### 7.3 获取/设置超时时间
+
+```c
+ioctl(int fd, WDIOC_GETTIMEOUT, int *timeout);   // 获取
+ioctl(int fd, WDIOC_SETTIMEOUT, int *timeout);   // 设置
+```
+
+超时时间以**秒**为单位。设置时不可超过其最大值,否则 `ioctl` 调用会失败。
+
+```c
+int timeout;
+if (0 > ioctl(fd, WDIOC_GETTIMEOUT, &timeout)) {
+    fprintf(stderr, "ioctl error: WDIOC_GETTIMEOUT: %s\n", strerror(errno));
+    return -1;
+}
+printf("current timeout: %ds\n", timeout);
+
+timeout = 10;   //10 秒钟
+if (0 > ioctl(fd, WDIOC_SETTIMEOUT, &timeout)) {
+    fprintf(stderr, "ioctl error: WDIOC_SETTIMEOUT: %s\n", strerror(errno));
+    return -1;
+}
+```
+
+### 7.4 开启/关闭看门狗:WDIOC_SETOPTIONS
+
+```c
+ioctl(int fd, WDIOC_SETOPTIONS, int *option);
+```
+
+`option` 指向一个 `int` 变量,可取:
+
+```c
+
+#define WDIOS_DISABLECARD 0x0001 /* Turn off the watchdog timer */
+#define WDIOS_ENABLECARD  0x0002 /* Turn on the watchdog timer */
+
+```
+
+```c
+int option = WDIOS_ENABLECARD;  //开启
+//int option = WDIOS_DISABLECARD; //停止
+
+if (0 > ioctl(fd, WDIOC_SETOPTIONS, &option)) {
+    fprintf(stderr, "ioctl error: WDIOC_SETOPTIONS: %s\n", strerror(errno));
+    return -1;
+}
+
+```
+
+> **重要**:当调用 `open()` 打开看门狗设备时,即使程序中没有开启看门狗计时器,`close()` 关闭设备时看门狗也会**自动启动**。所以打开设备后,需要先用 `WDIOC_SETOPTIONS` 指令(`WDIOS_DISABLECARD`)停止计时,等所有设置完成之后再开启。
+
+### 7.5 喂狗:WDIOC_KEEPALIVE
+
+```c
+ioctl(int fd, WDIOC_KEEPALIVE, NULL);
+
+```
+
+计时器启动后,必须在超时之前"喂狗",否则计时器溢出将导致系统复位或产生中断:
+
+```c
+if (0 > ioctl(fd, WDIOC_KEEPALIVE, NULL)) {
+    fprintf(stderr, "ioctl error: WDIOC_KEEPALIVE: %s\n", strerror(errno));
+}
+
+```
+
+---
+
+## 8. 看门狗应用程序实战(watchdog_test.c)
+
+程序打开看门狗后先停止计时,设置超时时间,再开启计时并在超时前不断喂狗。
+
+```c
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <errno.h>
+#include <string.h>
+#include <linux/watchdog.h>
+
+#define  WDOG_DEV   "/dev/watchdog"
+
+int main(int argc, char *argv[])
+{
+    struct watchdog_info info;
+    int timeout;
+    int time;
+    int fd;
+    int op;
+
+    if (2 != argc) {
+        fprintf(stderr, "usage: %s <timeout>\n", argv[0]);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 打开看门狗 */
+    fd = open(WDOG_DEV, O_RDWR);
+    if (0 > fd) {
+        fprintf(stderr, "open error: %s: %s\n", WDOG_DEV, strerror(errno));
+        exit(EXIT_FAILURE);
+    }
+
+    /* 打开之后看门狗计时器会开启、先停止它 */
+    op = WDIOS_DISABLECARD;
+    if (0 > ioctl(fd, WDIOC_SETOPTIONS, &op)) {
+        fprintf(stderr, "ioctl error: WDIOC_SETOPTIONS: %s\n", strerror(errno));
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    timeout = atoi(argv[1]);
+    if (1 > timeout)
+        timeout = 1;
+
+    /* 设置超时时间 */
+    printf("timeout: %ds\n", timeout);
+    if (0 > ioctl(fd, WDIOC_SETTIMEOUT, &timeout)) {
+        fprintf(stderr, "ioctl error: WDIOC_SETTIMEOUT: %s\n", strerror(errno));
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 开启看门狗计时器 */
+    op = WDIOS_ENABLECARD;
+    if (0 > ioctl(fd, WDIOC_SETOPTIONS, &op)) {
+        fprintf(stderr, "ioctl error: WDIOC_SETOPTIONS: %s\n", strerror(errno));
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 喂狗 */
+    time = (timeout * 1000 - 100) * 1000;//喂狗时间设置us微秒、在超时时间到来前100ms喂狗
+    for ( ; ; ) {
+
+        usleep(time);
+        ioctl(fd, WDIOC_KEEPALIVE, NULL);
+    }
+}
+
+```
+
+逐段解释:
+
+| 位置 | 代码 | 说明 |
+| ---- | ---- | ---- |
+| 参数校验 | `2 != argc` | 需传入看门狗超时时间(秒) |
+| `open(WDOG_DEV, O_RDWR)` | 打开看门狗 | 以读写方式打开 `/dev/watchdog` |
+| `WDIOC_SETOPTIONS` + `WDIOS_DISABLECARD` | 先停止计时 | 因为打开设备后计时器会开启 |
+| `atoi(argv[1])` | 解析超时时间 | 小于 1 秒则强制为 1 |
+| `WDIOC_SETTIMEOUT` | 设置超时 | 单位秒,不能超过最大值 |
+| `WDIOC_SETOPTIONS` + `WDIOS_ENABLECARD` | 开启计时 | 开始看门狗工作 |
+| `time = (timeout*1000 - 100)*1000` | 计算喂狗间隔 | `usleep` 以微秒为单位,在超时前 100ms 喂狗 |
+| `WDIOC_KEEPALIVE` | 循环喂狗 | 不断重置计时器,避免系统复位 |
+
+编译:
+
+```bash
+arm-linux-gnueabihf-gcc -o testApp watchdog_test.c
+scp testApp root@192.168.1.10:/home/root/
+
+```
+
+---
+
+## 9. 在开发板上测试看门狗
+
+把可执行文件拷贝到开发板 `/home/root`,执行测试程序,例如启动看门狗、设置超时时间为 2 秒:
+
+```bash
+./testApp 2
+
+```
+
+### 9.1 解决 "Device or resource busy"
+
+第一次执行可能会报错:
+
+```text
+open error: /dev/watchdog: Device or resource busy
+
+```
+
+这是因为**看门狗默认已被其他功能(出厂系统的看门狗脚本)使用**。解决方法是手动关闭它:打开 `/etc/init.d/watchdog.sh` 文件,在开头加一行 `exit 0`,把整个文件(后续逻辑)跳过。
+
+```bash
+vi /etc/init.d/watchdog.sh
+
+# 在文件开头加入:
+# exit 0
+
+```
+
+然后重启开发板,再执行程序,看门狗计时器就已启动,程序会不断喂狗、重置计时器,保证不重启:
+
+```bash
+./testApp 2
+
+# timeout: 2s
+
+```
+
+### 9.2 验证不喂狗会复位
+
+程序运行中按 **Ctrl + C** 结束程序。结束程序意味着**停止喂狗了,但看门狗计时器并没有停止**,于是计时器会溢出、发生复位重启:
+
+```text
+./testApp 2
+timeout: 2s
+^C
+watchdog watchdog0: watchdog did not stop!     <- 内核打印:计时器仍在计时、未停止
+(随后系统复位重启)
+
+```
+
+内核打印的 `watchdog watchdog0: watchdog did not stop!` 表示看门狗计时器还正在计时、未被停止。
+
+测试完毕后,把 `/etc/init.d/watchdog.sh` 文件中的 `exit 0` 去掉,让系统重新使用看门狗。
+
+### 9.3 调试方法
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| `Device or resource busy` | 看门狗已被系统占用 | 在 `/etc/init.d/watchdog.sh` 开头加 `exit 0` 后重启 |
+| 设置超时失败 | 超过设备支持的最大值 | 用 `WDIOC_GETTIMEOUT` 读取,或改小参数 |
+| 程序退出后系统立即复位 | `close` 会自动启动看门狗,且退出后无人喂狗 | 正常现象,用于验证;测试完恢复 `watchdog.sh` |
+| 设备不支持喂狗/设置超时 | 硬件/驱动不支持 | 用 `WDIOC_GETSUPPORT` 检查 `options` 位 |
+| `open` 失败无权限 | 非 root 运行 | 用 `root` 执行 |
+
+---
+
+## 10. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+| 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 |
+| ---- | ------------------ | ------------------ | ------ |
+| PWM 接口 | sysfs `/sys/class/pwm/pwmchipN` | 定时器寄存器 / HAL_TIM_PWM | sysfs,接口一致 |
+| PWM 配置 | 写 `period`/`duty_cycle`/`enable`(ns) | 配置 ARR/PSC/CCR 寄存器 | 写 `period`/`duty_cycle`/`enable`(ns) |
+| 看门狗接口 | `/dev/watchdog` + `ioctl` | IWDG/WWDG 寄存器直接喂狗 | `/dev/watchdog` + `ioctl`,接口一致 |
+| 看门狗喂狗 | `WDIOC_KEEPALIVE` | 写 IWDG_KR 重装载键 | `WDIOC_KEEPALIVE` |
+| 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc` | `aarch64-linux-gnu-gcc` |
+
+I.MX6ULL 与 RK3568 的 sysfs PWM 与 ioctl 看门狗用法基本通用;STM32 裸机没有 sysfs 与设备节点,需直接操作寄存器。
+
+---
+
+## 11. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:PWM 的周期、频率、占空比是什么关系?如何用 sysfs 配置?
+
+**答**:周期是波形一个完整循环的时间长度,频率是周期的倒数(`f = 1/period`),占空比是一个周期内高电平的时间长度,占空比比例 = `duty_cycle / period`。在 sysfs 中,先 `echo 0 > /sys/class/pwm/pwmchipN/export` 导出,再向 `pwm0/period` 与 `pwm0/duty_cycle` 写入纳秒值,最后 `echo 1 > pwm0/enable` 使能。例如 `period=500000`、`duty_cycle=250000` 表示 2kHz、50%。
+
+### Q2:sysfs 操作 PWM 的完整流程是怎样的?
+
+**答**:进入 `/sys/class/pwm/pwmchipN`,用 `npwm` 查看路数;向 `export` 写入编号(I.MX6U 为 0)导出,生成 `pwm0` 目录;在 `pwm0` 下配置 `period`、`duty_cycle`(纳秒),可选配置 `polarity`;最后写 `enable=1` 使能;用完向 `unexport` 写入编号删除。程序中通常先 `access()` 判断是否已导出,未导出则先写 `export`。
+
+### Q3:看门狗的工作原理是什么?为什么 open 之后要先关闭它?
+
+**答**:看门狗是一个可在一定时间内被复位的计数器,启动后自动计数;若在超时前未被复位(喂狗),计数器溢出就会对 CPU 产生复位信号使系统重启。系统正常时程序按时喂狗,程序跑飞则不喂狗、系统自动复位,从而提高可靠性。至于 `open` 之后要先关闭:因为打开看门狗设备后即使程序没开启计时器,`close()` 关闭设备时看门狗也会自动启动,所以打开后应先用 `WDIOC_SETOPTIONS` + `WDIOS_DISABLECARD` 停止计时,待一切设置完成后再开启,避免在配置过程中意外触发。
+
+### Q4:看门狗常用的 ioctl 指令有哪些,各起什么作用?
+
+**答**:`WDIOC_GETSUPPORT` 获取设备支持的功能(如是否支持设置超时、是否支持喂狗);`WDIOC_SETOPTIONS` 配合 `WDIOS_ENABLECARD`/`WDIOS_DISABLECARD` 开启或关闭看门狗计时器;`WDIOC_SETTIMEOUT`/`WDIOC_GETTIMEOUT` 设置/获取超时时间(秒);`WDIOC_KEEPALIVE` 执行喂狗、重置计时器。这些指令都需要先用 `open("/dev/watchdog", O_RDWR)` 得到文件描述符。
+
+### Q5:如何验证看门狗确实会复位系统?为什么执行程序会提示 `Device or resource busy`?
+
+**答**:启动看门狗并设置超时(如 2 秒),程序正常运行时会不断喂狗。按 Ctrl+C 结束程序,即停止喂狗,但看门狗计时器仍在计时,超时后就会复位系统;内核会打印 `watchdog watchdog0: watchdog did not stop!`。提示 `Device or resource busy` 是因为看门狗默认已被系统功能(`/etc/init.d/watchdog.sh`)占用,可在该脚本开头加 `exit 0` 并重启开发板后再测试,测试完记得去掉。
+
+---
+
+## 延伸阅读
+
+- PWM 驱动原理:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/06-RTC与PWM驱动]]
+- 蜂鸣器/LED 驱动:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/01-LED与蜂鸣器驱动]]
+- 本库相关:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]]
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第二十四章 PWM应用编程、第二十七章 看门狗应用编程;例程源码 `24_pwm/pwm.c`、`27_watchdog/watchdog_test.c`

+ 2328 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/06-摄像头串口与音频.md

@@ -0,0 +1,2328 @@
+---
+title: 摄像头、串口与音频应用编程
+tags: [嵌入式Linux, Linux应用编程, V4L2, 摄像头, 视频采集, 串口, UART, termios, ALSA, alsa-lib, PCM, WAV, 音频, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十五章 V4L2摄像头应用编程、第二十六章 串口应用编程、第二十八章 音频应用编程"
+---
+
+# 摄像头、串口与音频应用编程
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/01-高级IO]]、[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]];延伸阅读:[[嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/05-RS232与485通信]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/05-音频驱动]]。
+
+本篇覆盖三类"数据流"外设的用户态应用编程:**摄像头**(V4L2 视频采集)、**串口**(termios 终端编程)、**音频**(ALSA 播放与录音)。它们的共同点是——应用层不直接碰寄存器,而是通过 `/dev` 设备节点与 `/sys` 属性文件,借助统一的驱动框架接口完成数据收发。掌握这三者,就具备了嵌入式 Linux 多媒体与外设通信的主力技能。
+
+---
+
+# 第一部分 V4L2 摄像头应用编程
+
+## 1.1 V4L2 是什么
+
+V4L2 是 **Video for Linux Two** 的简称,是 Linux 内核中**视频类设备**的一套驱动框架,为视频设备驱动开发和应用层提供统一的接口规范。最典型的视频类设备就是**视频采集设备**(各种摄像头)。
+
+使用 V4L2 驱动框架注册的设备,会在 `/dev` 目录下生成设备节点,名称通常为 `videoX`(X 为数字编号,0、1、2……),每个 `videoX` 代表一个视频类设备。应用程序通过对 `videoX` 设备文件进行 I/O 操作来配置、使用设备。
+
+> 开发板出厂系统对 ov5640、ov2640、ov7725 三款摄像头都支持,默认使能 ov5640(不能同时生效);也可直接插入 UVC USB 摄像头。USB 摄像头通常不支持 RGB565,多为 YUYV 格式。
+
+## 1.2 摄像头编程流程
+
+V4L2 摄像头应用编程有一套固定的流程,几乎所有操作都通过 `ioctl()` 完成,搭配不同的 V4L2 指令(request 参数)请求不同操作:
+
+```mermaid
+flowchart TB
+    accTitle: V4L2 摄像头视频采集流程
+    accDescr: 从打开设备、查询能力、枚举并设置格式,到申请帧缓冲、内存映射、入队、开启采集,最后循环出队处理再入队。
+    A["open(/dev/videoX, O_RDWR)"] --> B["VIDIOC_QUERYCAP 查询设备能力"]
+    B --> C{"capabilities 含 V4L2_CAP_VIDEO_CAPTURE?"}
+    C -- 否 --> X["不是采集设备,退出"]
+    C -- 是 --> D["VIDIOC_ENUM_FMT 枚举像素格式"]
+    D --> E["VIDIOC_ENUM_FRAMESIZES / FRAMEINTERVALS 枚举分辨率与帧率"]
+    E --> F["VIDIOC_S_FMT 设置帧格式"]
+    F --> G["VIDIOC_REQBUFS 申请帧缓冲"]
+    G --> H["VIDIOC_QUERYBUF + mmap 内存映射"]
+    H --> I["VIDIOC_QBUF 帧缓冲入队"]
+    I --> J["VIDIOC_STREAMON 开启采集"]
+    J --> K["VIDIOC_DQBUF 出队取一帧"]
+    K --> L["处理数据(显示/保存/编码)"]
+    L --> M["VIDIOC_QBUF 重新入队"]
+    M --> K
+    K --> N["VIDIOC_STREAMOFF 结束采集"]
+```
+
+## 1.3 常用 ioctl 指令
+
+所有指令定义在头文件 `<linux/videodev2.h>` 中,以宏 `VIDIOC_XXX` 形式提供,每个宏还携带一个 `struct` 类型——即调用 `ioctl()` 时第三个参数的类型,传入该类型变量的指针。
+
+| V4L2 指令 | 描述 |
+| --------- | ---- |
+| `VIDIOC_QUERYCAP` | 查询设备的属性/能力/功能 |
+| `VIDIOC_ENUM_FMT` | 枚举设备支持的像素格式 |
+| `VIDIOC_G_FMT` | 获取设备当前的帧格式信息 |
+| `VIDIOC_S_FMT` | 设置帧格式信息 |
+| `VIDIOC_REQBUFS` | 申请帧缓冲 |
+| `VIDIOC_QUERYBUF` | 查询帧缓冲 |
+| `VIDIOC_QBUF` | 帧缓冲入队操作 |
+| `VIDIOC_DQBUF` | 帧缓冲出队操作 |
+| `VIDIOC_STREAMON` | 开启视频采集 |
+| `VIDIOC_STREAMOFF` | 关闭视频采集 |
+| `VIDIOC_G_PARM` | 获取设备的流类型参数 |
+| `VIDIOC_S_PARM` | 设置流类型参数(帧率) |
+| `VIDIOC_TRY_FMT` | 尝试设置帧格式,用于判断设备是否支持该格式 |
+| `VIDIOC_ENUM_FRAMESIZES` | 枚举设备支持的视频采集分辨率 |
+| `VIDIOC_ENUM_FRAMEINTERVALS` | 枚举设备支持的视频采集帧率 |
+
+## 1.4 关键数据结构
+
+### 1.4.1 struct v4l2_capability
+
+```c
+struct v4l2_capability {
+    __u8    driver[16];     /* 驱动的名字 */
+    __u8    card[32];       /* 设备的名字 */
+    __u8    bus_info[32];   /* 总线的名字 */
+    __u32   version;        /* 版本信息 */
+    __u32   capabilities;   /* 设备拥有的能力 */
+    __u32   device_caps;
+    __u32   reserved[3];    /* 保留字段 */
+};
+```
+
+重点是 `capabilities`,它描述设备能力,可为多个值的位或。摄像头设备必须包含 `V4L2_CAP_VIDEO_CAPTURE`(0x00000001,视频采集)。其它常用值:
+
+| 能力宏 | 值 | 含义 |
+| ------ | -- | ---- |
+| `V4L2_CAP_VIDEO_CAPTURE` | 0x00000001 | 是视频采集设备 |
+| `V4L2_CAP_VIDEO_OUTPUT` | 0x00000002 | 是视频输出设备 |
+| `V4L2_CAP_VIDEO_OVERLAY` | 0x00000004 | 支持视频叠加 |
+| `V4L2_CAP_READWRITE` | 0x01000000 | 支持 read/write 方式读写 |
+| `V4L2_CAP_STREAMING` | 0x04000000 | 支持 streaming I/O 方式 |
+
+### 1.4.2 struct v4l2_fmtdesc / frmsizeenum / frmivalenum
+
+```c
+struct v4l2_fmtdesc {
+    __u32 index;              /* Format number,枚举前设为 0,每次 +1 */
+    __u32 type;               /* enum v4l2_buf_type */
+    __u32 flags;
+    __u8  description[32];    /* Description string,格式描述 */
+    __u32 pixelformat;        /* Format fourcc,像素格式 */
+    __u32 reserved[4];
+};
+
+struct v4l2_frmsizeenum {
+    __u32 index;              /* Frame size number */
+    __u32 pixel_format;       /* 像素格式 */
+    __u32 type;               /* type */
+    union {
+        struct v4l2_frmsize_discrete discrete;
+        struct v4l2_frmsize_stepwise stepwise;
+    };
+    __u32 reserved[2];
+};
+
+struct v4l2_frmsize_discrete {
+    __u32 width;              /* Frame width [pixel] */
+    __u32 height;             /* Frame height [pixel] */
+};
+
+struct v4l2_frmivalenum {
+    __u32 index;              /* Frame format index */
+    __u32 pixel_format;       /* Pixel format */
+    __u32 width;              /* Frame width */
+    __u32 height;             /* Frame height */
+    __u32 type;               /* type */
+    union {
+        struct v4l2_fract discrete;
+        struct v4l2_frmival_stepwise stepwise;
+    };
+    __u32 reserved[2];
+};
+
+struct v4l2_fract {
+    __u32 numerator;          /* 分子 */
+    __u32 denominator;        /* 分母 */
+};
+```
+
+- `index`:编号,枚举前设为 0,每次 `ioctl()` 后加 1,直到调用失败表示枚举完。
+- `pixel_format`/`width`/`height`:调用前需设置,指定枚举"哪种格式、哪个分辨率"。
+- 当 `type = V4L2_BUF_TYPE_VIDEO_CAPTURE` 时 `discrete` 生效。帧率 = `denominator / numerator`。
+
+常用像素格式(由 `v4l2_fourcc` 宏合成 32 位数据):
+
+| 像素格式宏 | fourcc | 含义 |
+| ---------- | ------ | ---- |
+| `V4L2_PIX_FMT_RGB565` | 'RGBP' | 16 位 RGB 5-6-5 |
+| `V4L2_PIX_FMT_RGB555` | 'RGBO' | 16 位 RGB 5-5-5 |
+| `V4L2_PIX_FMT_YUYV` | 'YUYV' | 16 位 YUV 4:2:2 |
+| `V4L2_PIX_FMT_MJPEG` | 'MJPG' | Motion-JPEG |
+| `V4L2_PIX_FMT_GREY` | 'GREY' | 8 位灰度 |
+
+`v4l2_buf_type` 常用取值:
+
+| 取值 | 含义 |
+| ---- | ---- |
+| `V4L2_BUF_TYPE_VIDEO_CAPTURE`(1) | 视频采集 |
+| `V4L2_BUF_TYPE_VIDEO_OUTPUT`(2) | 视频输出 |
+| `V4L2_BUF_TYPE_VIDEO_OVERLAY`(3) | 视频叠加 |
+| `V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE`(9) | 多平面视频采集 |
+
+### 1.4.3 struct v4l2_format / v4l2_pix_format
+
+```c
+struct v4l2_format {
+    __u32 type;
+    union {
+        struct v4l2_pix_format        pix;     /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
+        struct v4l2_pix_format_mplane pix_mp;  /* ..._MPLANE */
+        struct v4l2_window            win;     /* ..._OVERLAY */
+        struct v4l2_vbi_format        vbi;     /* ..._VBI_CAPTURE */
+        __u8                          raw_data[200];
+    } fmt;
+};
+
+struct v4l2_pix_format {
+    __u32 width;          /* 视频帧宽度(像素) */
+    __u32 height;         /* 视频帧高度(像素) */
+    __u32 pixelformat;    /* 像素格式 */
+    __u32 field;          /* enum v4l2_field */
+    __u32 bytesperline;   /* for padding, zero if unused */
+    __u32 sizeimage;
+    __u32 colorspace;     /* enum v4l2_colorspace */
+    __u32 priv;
+    __u32 flags;
+    union { __u32 ycbcr_enc; __u32 hsv_enc; };
+    __u32 quantization;
+    __u32 xfer_func;
+};
+```
+
+### 1.4.4 struct v4l2_streamparm / v4l2_captureparm
+
+```c
+struct v4l2_streamparm {
+    __u32 type;               /* enum v4l2_buf_type */
+    union {
+        struct v4l2_captureparm capture;
+        struct v4l2_outputparm  output;
+        __u8 raw_data[200];
+    } parm;
+};
+
+struct v4l2_captureparm {
+    __u32 capability;         /* Supported modes */
+    __u32 capturemode;        /* Current mode */
+    struct v4l2_fract timeperframe; /* Time per frame in seconds */
+    __u32 extendedmode;
+    __u32 readbuffers;
+    __u32 reserved[4];
+};
+```
+
+`capability` 字段标志:
+
+| 标志 | 值 | 含义 |
+| ---- | -- | ---- |
+| `V4L2_MODE_HIGHQUALITY` | 0x0001 | 高品质成像模式 |
+| `V4L2_CAP_TIMEPERFRAME` | 0x1000 | 支持设置 `timeperframe` 字段 |
+
+只有 `capability` 包含 `V4L2_CAP_TIMEPERFRAME` 时,应用层才能通过 `VIDIOC_S_PARM` 设置帧率。
+
+### 1.4.5 struct v4l2_requestbuffers / v4l2_buffer
+
+```c
+struct v4l2_requestbuffers {
+    __u32 count;       /* 申请帧缓冲的数量 */
+    __u32 type;        /* enum v4l2_buf_type */
+    __u32 memory;      /* enum v4l2_memory */
+    __u32 reserved[2];
+};
+
+struct v4l2_buffer {
+    __u32 index;       /* buffer 的编号 */
+    __u32 type;
+    __u32 bytesused;
+    __u32 flags;
+    __u32 field;
+    struct timeval timestamp;
+    struct v4l2_timecode timecode;
+    __u32 sequence;
+    __u32 memory;      /* memory location */
+    union {
+        __u32 offset;         /* 偏移量 */
+        unsigned long userptr;
+        struct v4l2_plane *planes;
+        __s32 fd;
+    } m;
+    __u32 length;      /* buffer 的长度 */
+    __u32 reserved2;
+    __u32 reserved;
+};
+```
+
+`enum v4l2_memory`:
+
+| 取值 | 值 | 含义 |
+| ---- | -- | ---- |
+| `V4L2_MEMORY_MMAP` | 1 | 内存映射(最常用) |
+| `V4L2_MEMORY_USERPTR` | 2 | 用户指针 |
+| `V4L2_MEMORY_OVERLAY` | 3 | 叠加 |
+| `V4L2_MEMORY_DMABUF` | 4 | DMA 缓冲 |
+
+`length` 是帧缓冲长度,`m.offset` 是帧缓冲在内核申请的那块大内存中的偏移量;`VIDIOC_REQBUFS` 时内核申请"数量 × 单个大小"的连续内存,每个帧缓冲对应其中一段。
+
+## 1.5 帧缓冲队列与采集方式
+
+V4L2 读取数据有两种方式:**read 方式**(`capabilities` 含 `V4L2_CAP_READWRITE`)和 **streaming 方式**(含 `V4L2_CAP_STREAMING`)。绝大多数设备支持 streaming I/O:内核维护一个帧缓冲队列,驱动不断把采集数据填入队列中的帧缓冲;应用取走一帧叫**出队**,处理完再放回队列叫**入队**。
+
+```mermaid
+flowchart LR
+    accTitle: 帧缓冲队列的入队出队
+    accDescr: 内核维护帧缓冲队列,驱动向空闲缓冲写入采集数据,应用出队取走处理后重新入队,形成循环。
+    Q["内核帧缓冲队列"]
+    D["摄像头驱动<br/>写入一帧"] -->|填入空闲缓冲| Q
+    Q -->|VIDIOC_DQBUF 出队| A["应用程序处理"]
+    A -->|VIDIOC_QBUF 入队| Q
+```
+
+- 申请:`VIDIOC_REQBUFS`,设置 `count`、`type`、`memory=V4L2_MEMORY_MMAP`;
+- 映射:对每个缓冲 `VIDIOC_QUERYBUF` 拿到 `length`/`m.offset`,再 `mmap()` 映射到用户空间;
+- 入队:对每个缓冲 `VIDIOC_QBUF` 放入内核队列;开启后用 `VIDIOC_DQBUF` 循环取帧,处理完再 `VIDIOC_QBUF`。
+- 帧缓冲数量不要太多,嵌入式系统内存吃紧;太少又可能丢帧,例程取 3 个。
+
+## 1.6 完整源码:v4l2_camera.c
+
+> 源码路径:`11、Linux C应用编程例程源码/25_v4l2_camera/v4l2_camera.c`。功能:在 LCD 上实时显示摄像头采集图像(要求摄像头支持 RGB565)。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/mman.h>
+#include <linux/videodev2.h>
+#include <linux/fb.h>
+
+#define FB_DEV              "/dev/fb0"      //LCD设备节点
+#define FRAMEBUFFER_COUNT   3               //帧缓冲数量
+
+/*** 摄像头像素格式及其描述信息 ***/
+typedef struct camera_format {
+    unsigned char description[32];  //字符串描述信息
+    unsigned int pixelformat;       //像素格式
+} cam_fmt;
+
+/*** 描述一个帧缓冲的信息 ***/
+typedef struct cam_buf_info {
+    unsigned short *start;      //帧缓冲起始地址
+    unsigned long length;       //帧缓冲长度
+} cam_buf_info;
+
+static int width;                       //LCD宽度
+static int height;                      //LCD高度
+static unsigned short *screen_base = NULL;//LCD显存基地址
+static int fb_fd = -1;                  //LCD设备文件描述符
+static int v4l2_fd = -1;                //摄像头设备文件描述符
+static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
+static cam_fmt cam_fmts[10];
+static int frm_width, frm_height;   //视频帧宽度和高度
+
+static int fb_dev_init(void)
+{
+    struct fb_var_screeninfo fb_var = {0};
+    struct fb_fix_screeninfo fb_fix = {0};
+    unsigned long screen_size;
+
+    /* 打开framebuffer设备 */
+    fb_fd = open(FB_DEV, O_RDWR);
+    if (0 > fb_fd) {
+        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
+        return -1;
+    }
+
+    /* 获取framebuffer设备信息 */
+    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
+    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);
+
+    screen_size = fb_fix.line_length * fb_var.yres;
+    width = fb_var.xres;
+    height = fb_var.yres;
+
+    /* 内存映射 */
+    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
+    if (MAP_FAILED == (void *)screen_base) {
+        perror("mmap error");
+        close(fb_fd);
+        return -1;
+    }
+
+    /* LCD背景刷白 */
+    memset(screen_base, 0xFF, screen_size);
+    return 0;
+}
+
+static int v4l2_dev_init(const char *device)
+{
+    struct v4l2_capability cap = {0};
+
+    /* 打开摄像头 */
+    v4l2_fd = open(device, O_RDWR);
+    if (0 > v4l2_fd) {
+        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
+        return -1;
+    }
+
+    /* 查询设备功能 */
+    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);
+
+    /* 判断是否是视频采集设备 */
+    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {
+        fprintf(stderr, "Error: %s: No capture video device!\n", device);
+        close(v4l2_fd);
+        return -1;
+    }
+
+    return 0;
+}
+
+static void v4l2_enum_formats(void)
+{
+    struct v4l2_fmtdesc fmtdesc = {0};
+
+    /* 枚举摄像头所支持的所有像素格式以及描述信息 */
+    fmtdesc.index = 0;
+    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
+
+        // 将枚举出来的格式以及描述信息存放在数组中
+        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
+        strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
+        fmtdesc.index++;
+    }
+}
+
+static void v4l2_print_formats(void)
+{
+    struct v4l2_frmsizeenum frmsize = {0};
+    struct v4l2_frmivalenum frmival = {0};
+    int i;
+
+    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    for (i = 0; cam_fmts[i].pixelformat; i++) {
+
+        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
+                    cam_fmts[i].description);
+
+        /* 枚举出摄像头所支持的所有视频采集分辨率 */
+        frmsize.index = 0;
+        frmsize.pixel_format = cam_fmts[i].pixelformat;
+        frmival.pixel_format = cam_fmts[i].pixelformat;
+        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
+
+            printf("size<%d*%d> ",
+                    frmsize.discrete.width,
+                    frmsize.discrete.height);
+            frmsize.index++;
+
+            /* 获取摄像头视频采集帧率 */
+            frmival.index = 0;
+            frmival.width = frmsize.discrete.width;
+            frmival.height = frmsize.discrete.height;
+            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {
+
+                printf("<%dfps>", frmival.discrete.denominator /
+                        frmival.discrete.numerator);
+                frmival.index++;
+            }
+            printf("\n");
+        }
+        printf("\n");
+    }
+}
+
+static int v4l2_set_format(void)
+{
+    struct v4l2_format fmt = {0};
+    struct v4l2_streamparm streamparm = {0};
+
+    /* 设置帧格式 */
+    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//type类型
+    fmt.fmt.pix.width = width;  //视频帧宽度
+    fmt.fmt.pix.height = height;//视频帧高度
+    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;  //像素格式
+    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
+        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
+        return -1;
+    }
+
+    /*** 判断是否已经设置为我们要求的RGB565像素格式
+    如果没有设置成功表示该设备不支持RGB565像素格式 */
+    if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
+        fprintf(stderr, "Error: the device does not support RGB565 format!\n");
+        return -1;
+    }
+
+    frm_width = fmt.fmt.pix.width;  //获取实际的帧宽度
+    frm_height = fmt.fmt.pix.height;//获取实际的帧高度
+    printf("视频帧大小<%d * %d>\n", frm_width, frm_height);
+
+    /* 获取streamparm */
+    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
+
+    /** 判断是否支持帧率设置 **/
+    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
+        streamparm.parm.capture.timeperframe.numerator = 1;
+        streamparm.parm.capture.timeperframe.denominator = 30;//30fps
+        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
+            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+static int v4l2_init_buffer(void)
+{
+    struct v4l2_requestbuffers reqbuf = {0};
+    struct v4l2_buffer buf = {0};
+
+    /* 申请帧缓冲 */
+    reqbuf.count = FRAMEBUFFER_COUNT;       //帧缓冲的数量
+    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    reqbuf.memory = V4L2_MEMORY_MMAP;
+    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {
+        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
+        return -1;
+    }
+
+    /* 建立内存映射 */
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
+
+        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
+        buf_infos[buf.index].length = buf.length;
+        buf_infos[buf.index].start = mmap(NULL, buf.length,
+                PROT_READ | PROT_WRITE, MAP_SHARED,
+                v4l2_fd, buf.m.offset);
+        if (MAP_FAILED == buf_infos[buf.index].start) {
+            perror("mmap error");
+            return -1;
+        }
+    }
+
+    /* 入队 */
+    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
+
+        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
+            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+static int v4l2_stream_on(void)
+{
+    /* 打开摄像头、摄像头开始采集数据 */
+    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {
+        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
+        return -1;
+    }
+
+    return 0;
+}
+
+static void v4l2_read_data(void)
+{
+    struct v4l2_buffer buf = {0};
+    unsigned short *base;
+    unsigned short *start;
+    int min_w, min_h;
+    int j;
+
+    if (width > frm_width)
+        min_w = frm_width;
+    else
+        min_w = width;
+    if (height > frm_height)
+        min_h = frm_height;
+    else
+        min_h = height;
+
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    for ( ; ; ) {
+
+        for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
+
+            ioctl(v4l2_fd, VIDIOC_DQBUF, &buf);     //出队
+            for (j = 0, base=screen_base, start=buf_infos[buf.index].start;
+                        j < min_h; j++) {
+
+                memcpy(base, start, min_w * 2); //RGB565 一个像素占2个字节
+                base += width;  //LCD显示指向下一行
+                start += frm_width;//指向下一行数据
+            }
+
+            // 数据处理完之后、再入队、往复
+            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
+        }
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    if (2 != argc) {
+        fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 初始化LCD */
+    if (fb_dev_init())
+        exit(EXIT_FAILURE);
+
+    /* 初始化摄像头 */
+    if (v4l2_dev_init(argv[1]))
+        exit(EXIT_FAILURE);
+
+    /* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
+    v4l2_enum_formats();
+    v4l2_print_formats();
+
+    /* 设置格式 */
+    if (v4l2_set_format())
+        exit(EXIT_FAILURE);
+
+    /* 初始化帧缓冲:申请、内存映射、入队 */
+    if (v4l2_init_buffer())
+        exit(EXIT_FAILURE);
+
+    /* 开启视频采集 */
+    if (v4l2_stream_on())
+        exit(EXIT_FAILURE);
+
+    /* 读取数据:出队 */
+    v4l2_read_data();       //在函数内循环采集数据、将其显示到LCD屏
+
+    exit(EXIT_SUCCESS);
+}
+```
+
+**逐段解释**
+
+- `fb_dev_init()`:为显示服务。打开 `/dev/fb0`,用 `FBIOGET_VSCREENINFO`/`FBIOGET_FSCREENINFO` 取分辨率、一行字节数,`mmap` 显存,刷白背景。LCD 是 RGB565 显示设备。
+- `v4l2_dev_init()`:打开 `/dev/videoX` 并用 `VIDIOC_QUERYCAP` 校验采集能力。
+- `v4l2_enum_formats()`:循环 `VIDIOC_ENUM_FMT`,把每个格式的 `pixelformat` 与 `description` 存入 `cam_fmts[]`。
+- `v4l2_print_formats()`:对每种格式再枚举分辨率和帧率,便于选型调试(部分摄像头驱动未实现该功能,只打印格式属正常)。
+- `v4l2_set_format()`:把帧格式设为与 LCD 同分辨率的 **RGB565**;若驱动不支持 RGB565 则退出(USB 摄像头常不支持)。
+- `v4l2_init_buffer()`:`VIDIOC_REQBUFS` 申请 3 个 MMAP 缓冲 → 逐个 `VIDIOC_QUERYBUF`+`mmap` → 逐个 `VIDIOC_QBUF` 入队。
+- `v4l2_read_data()`:无限循环"出队→逐行 `memcpy` 到显存→再入队"。`min_w/min_h` 取 LCD 与视频帧的较小值,避免越界;RGB565 每像素 2 字节,所以 `memcpy` 长度是 `min_w * 2`。
+
+## 1.7 交叉编译与实验步骤
+
+```bash
+# 1. 设置交叉编译工具环境(路径按实际安装调整)
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+
+# 2. 编译
+${CC} -o testApp v4l2_camera.c
+
+# 3. 确认是 ARM 可执行文件
+file testApp      # 应显示 32-bit ARM
+```
+
+实验步骤:
+
+1. **先装摄像头再上电**(正点原子摄像头必须启动前安装;USB 摄像头可热插拔)。
+2. 用 `scp` 把 `testApp` 拷到开发板家目录。
+3. 运行:`./testApp /dev/video0`(USB 摄像头可能是 `/dev/video1`,可用 `ls /dev/video*` 确认)。
+4. LCD 上应实时显示采集画面,终端打印格式/分辨率/帧率信息。
+5. 若提示不支持 RGB565,说明该摄像头(多为 USB UVC)输出 YUYV,需要做格式转换。
+6. `Ctrl+C` 结束程序。
+
+## 1.8 扩展:YUYV → RGB565 转换
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+USB 摄像头常输出 YUYV(YUV 4:2:2),每 4 字节表示两个像素:`Y0 U Y1 V`。YUV→RGB 的常用整数公式:
+
+```c
+/* YUYV(4:2:2) -> RGB565,每 4 字节产出 2 个像素 */
+static inline unsigned short yuv2rgb565(int y, int u, int v)
+{
+    int r, g, b;
+
+    /* 归一化到 0~255 */
+    y = y - 16;  if (y < 0)  y = 0;
+    u = u - 128;
+    v = v - 128;
+
+    r = (298 * y + 409 * v + 128) >> 8;
+    g = (298 * y - 100 * u - 208 * v + 128) >> 8;
+    b = (298 * y + 516 * u + 128) >> 8;
+
+    if (r < 0) r = 0; else if (r > 255) r = 255;
+    if (g < 0) g = 0; else if (g > 255) g = 255;
+    if (b < 0) b = 0; else if (b > 255) b = 255;
+
+    return (unsigned short)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
+}
+
+/* 用法:把一帧 YUYV 数据转成 RGB565 后写入映射区 */
+static void yuyv_to_rgb565(const unsigned char *src, unsigned short *dst, int pixels)
+{
+    int i;
+    for (i = 0; i < pixels; i += 2) {
+        int y0 = src[0], u = src[1], y1 = src[2], v = src[3];
+        dst[0] = yuv2rgb565(y0, u, v);
+        dst[1] = yuv2rgb565(y1, u, v);
+        src += 4;
+        dst += 2;
+    }
+}
+```
+
+---
+
+# 第二部分 串口应用编程
+
+## 2.1 串口与终端
+
+串口全称**串行接口**,数据一个接一个按顺序传输,两条线即可双向通信(一发一收)。串口通信距离远、速度相对低,是常用的工业接口。在嵌入式 Linux 中,串口常作为系统的标准输入、输出设备:系统打印信息通过串口输出,用户通过串口与系统交互。
+
+串口在 Linux 中就是一个**终端(Terminal)**。终端是"处理主机输入、输出的一套设备",能接受输入、能显示输出就是终端。终端分类:
+
+| 类型 | 说明 |
+| ---- | ---- |
+| 本地终端 | 连接本机键盘显示器,如 `/dev/tty1`~`/dev/tty63` |
+| 串口远程终端 | 开发板最常见,通过串口线连接 PC,运行 putty/MobaXterm/SecureCRT |
+| 网络远程终端(伪终端) | ssh/Telnet 登录,节点在 `/dev/pts/X` |
+
+设备节点:
+
+- `/dev/ttyX`:本地终端,X 为 0~63;
+- `/dev/pts/X`:伪终端;
+- `/dev/ttymxcX`:I.MX6U 串口终端。ALPHA/Mini 板有 UART1、UART3 两个串口,对应 `/dev/ttymxc0`、`/dev/ttymxc2`(为什么是 0 和 2?因为 I.MX6U 支持 8 个串口,出厂系统只注册 UART1 和 UART3,编号即 0 和 2)。
+
+> `mxc` 这个名字与驱动/硬件平台有关,换个平台可能变成 `/dev/ttyPSX` 等,但前缀都是 `tty`。可用 `who` 命令查看系统当前连接了哪些终端。
+
+## 2.2 termios API 与 struct termios
+
+串口应用编程可以简单理解为对终端进行"配置 + 读写"。Linux 把底层 `ioctl()` 封装成一套标准 API,称为 **termios API**——它面向**所有终端设备**(串口、本地键盘鼠标、伪终端),不只是串口。使用时需包含头文件 `<termios.h>`。
+
+描述终端配置的数据结构是 `struct termios`:
+
+```c
+struct termios {
+    tcflag_t c_iflag;    /* input mode flags  输入模式 */
+    tcflag_t c_oflag;    /* output mode flags 输出模式 */
+    tcflag_t c_cflag;    /* control mode flags 控制模式 */
+    tcflag_t c_lflag;    /* local mode flags  本地模式 */
+    cc_t     c_line;     /* line discipline   线路规程 */
+    cc_t     c_cc[NCCS]; /* control characters 特殊控制字符 */
+    speed_t  c_ispeed;   /* input speed  输入速率 */
+    speed_t  c_ospeed;   /* output speed 输出速率 */
+};
+```
+
+> 对这些成员**不要直接整体初始化**,而应用"按位与/或"添加或清除标志。另外,很多标志并非对所有终端都有效——本地键盘显示器没有波特率、数据位这些硬件概念。
+
+### 2.2.1 输入模式 c_iflag
+
+控制输入数据(驱动从串口/键盘收到的字符)在交给应用前的处理方式。
+
+| 标志 | 含义 |
+| ---- | ---- |
+| `IGNBRK` | 忽略输入终止条件 |
+| `BRKINT` | 检测到输入终止条件时发送 `SIGINT` 信号 |
+| `IGNPAR` | 忽略帧错误和奇偶校验错误 |
+| `PARMRK` | 对奇偶校验错误做出标记 |
+| `INPCK` | 对接收到的数据执行奇偶校验 |
+| `ISTRIP` | 将所有接收数据裁剪为 7 比特位(去掉第八位) |
+| `INLCR` | 将接收到的 NL(换行符)转换为 CR(回车符) |
+| `IGNCR` | 忽略接收到的 CR(回车符) |
+| `ICRNL` | 将接收到的 CR(回车符)转换为 NL(换行符) |
+| `IUCLC` | 将接收到的大写字符映射为小写字符 |
+| `IXON` | 启动输出软件流控 |
+| `IXOFF` | 启动输入软件流控 |
+
+### 2.2.2 输出模式 c_oflag
+
+控制输出字符在传递到串口/屏幕前的处理方式。
+
+| 标志 | 含义 |
+| ---- | ---- |
+| `OPOST` | 启用输出处理功能;不设置该标志则其它标志都被忽略 |
+| `OLCUC` | 将输出字符中的大写字符转换成小写字符 |
+| `ONLCR` | 将输出中的换行符 NL(`\n`)转换成回车符 CR(`\r`) |
+| `OCRNL` | 将输出中的回车符 CR(`\r`)转换成换行符 NL(`\n`) |
+| `ONOCR` | 在第 0 列不输出回车符 CR |
+| `ONLRET` | 不输出回车符 |
+| `OFILL` | 发送填充字符以提供延时 |
+| `OFDEL` | 若设置该标志,填充字符为 DEL 字符,否则为 NULL 字符 |
+
+### 2.2.3 控制模式 c_cflag
+
+控制终端硬件特性,对串口最重要:波特率、数据位、校验位、停止位等。
+
+| 标志 | 含义 |
+| ---- | ---- |
+| `CBAUD` | 波特率的位掩码 |
+| `B0`/`B1200`/`B1800`/`B2400`/`B4800`/`B9600`/`B19200`/`B38400`/`B57600`/`B115200`/`B230400`/`B460800`/`B500000`/`B576000`/`B921600`/`B1000000`/`B1152000`/`B1500000`/`B2000000`/`B2500000`/`B3000000` | 对应波特率 |
+| `CSIZE` | 数据位的位掩码 |
+| `CS5`/`CS6`/`CS7`/`CS8` | 5/6/7/8 个数据位 |
+| `CSTOPB` | 2 个停止位;不设置则默认 1 个停止位 |
+| `CREAD` | 接收使能 |
+| `PARENB` | 使能奇偶校验 |
+| `PARODD` | 使用奇校验,而不是偶校验 |
+| `HUPCL` | 关闭时挂断调制解调器 |
+| `CLOCAL` | 忽略调制解调器控制线 |
+| `CRTSCTS` | 使能硬件流控 |
+
+Linux 下波特率由 `CBAUD` 位掩码的若干 bit 指定,并提供 `cfgetispeed()`/`cfsetispeed()`/`cfsetospeed()`/`cfsetspeed()` 函数获取与设置。
+
+### 2.2.4 本地模式 c_lflag
+
+控制终端的本地数据处理和工作模式。
+
+| 标志 | 含义 |
+| ---- | ---- |
+| `ISIG` | 收到信号字符(INTR、QUIT 等)则产生相应信号 |
+| `ICANON` | 启用规范模式 |
+| `ECHO` | 启用输入字符的本地回显功能 |
+| `ECHOE` | 若设置 `ICANON`,则允许退格操作 |
+| `ECHOK` | 若设置 `ICANON`,则 KILL 字符会删除当前行 |
+| `ECHONL` | 若设置 `ICANON`,则允许回显换行符 |
+| `ECHOCTL` | 若设置 `ECHO`,控制字符会显示成 `^X` |
+| `ECHOPRT` | 若设置 `ICANON` 和 `IECHO`,删除字符和被删除字符都显示 |
+| `ECHOKE` | 若设置 `ICANON`,允许回显 `ECHOE`/`ECHOPRT` 中设定的 KILL 字符 |
+| `NOFLSH` | 通常收到 INTR/QUIT/SUSP 会清空输入输出队列;设置该标志则不清空 |
+| `TOSTOP` | 后台进程写控制终端时,系统向该进程组发送 `SIGTTOU` |
+| `IEXTEN` | 启用输入处理功能 |
+
+### 2.2.5 特殊控制字符 c_cc
+
+| 宏 | 对应键 | 作用 |
+| -- | ------ | ---- |
+| `VEOF` | Ctrl+D | 文件结尾符 EOF;read 返回 0 表示文件结束 |
+| `VEOL` | CR | 附加行结尾符 |
+| `VEOL2` | LF | 第二行结尾符 |
+| `VERASE` | Backspace | 删除输入行最后一个字符 |
+| `VINTR` | Ctrl+C | 向与终端相连的进程发送 `SIGINT` |
+| `VKILL` | Ctrl+U | 删除整个输入行 |
+| `VMIN` | — | 非规范模式下,最少读取的字符数 MIN |
+| `VQUIT` | Ctrl+Z | 发送 `SIGQUIT` |
+| `VSTART` | Ctrl+Q | 重新启动被 STOP 暂停的输出 |
+| `VSTOP` | Ctrl+S | 停止向终端的进一步输出(XON/XOFF 流控) |
+| `VSUSP` | Ctrl+Z | 发送 `SIGSUSP`,挂起当前应用程序 |
+| `VTIME` | — | 非规范模式下,读取每个字符之间的超时(单位:十分之一秒) |
+
+## 2.3 终端的三种工作模式
+
+终端有**规范模式**、**非规范模式**、**原始模式**三种,通过 `c_lflag` 的 `ICANON` 标志区分,默认是规范模式。
+
+- **规范模式(canonical)**:所有输入基于行处理。用户输入行结束符(回车、EOF 等)之前,`read()` 读不到任何字符;除 EOF 外的行结束符与普通字符一样被读入缓冲区;支持行编辑,一次 `read()` 最多读一行。
+- **非规范模式(non-canonical)**:所有输入即时有效,不需要行结束符,不可行编辑。由 `MIN`(`c_cc[VMIN]`)与 `TIME`(`c_cc[VTIME]`)决定 `read()` 行为:
+
+| MIN | TIME | read() 行为 |
+| --- | ---- | ----------- |
+| 0 | 0 | 总是立即返回:有数据则读并返回字节数,否则返回 0 |
+| >0 | 0 | 阻塞直到有 MIN 个字符可读才返回;到达文件尾返回 0 |
+| 0 | >0 | 只要有数据可读,或经过 TIME 个十分之一秒,立即返回;超时无数据返回 0 |
+| >0 | >0 | 有 MIN 个字节可读,或两字符间隔超过 TIME 个十分之一秒才返回;至少读一个字节 |
+
+- **原始模式(raw)**:严格说是一种特殊的非规范模式。所有输入数据以字节为单位处理,终端不回显,禁用输入输出字符的所有特殊处理。通过 `cfmakeraw()` 设置,其内部等价于:
+
+```c
+termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
+                        | INLCR | IGNCR | ICRNL | IXON);
+termios_p->c_oflag &= ~OPOST;
+termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+termios_p->c_cflag &= ~(CSIZE | PARENB);
+termios_p->c_cflag |= CS8;
+```
+
+**什么时候用原始模式?** 当串口不只做人机交互(数据按字符/ASCII 传输),而是与其他设备或传感器进行**二进制数据通信**时,数据不应做任何特殊处理,必须用原始模式。
+
+## 2.4 串口编程 API 汇总
+
+| 函数 | 原型 | 作用 |
+| ---- | ---- | ---- |
+| `tcgetattr` | `int tcgetattr(int fd, struct termios *termios_p)` | 获取当前配置(便于事后恢复) |
+| `cfmakeraw` | `void cfmakeraw(struct termios *termios_p)` | 配置为原始模式 |
+| `cfsetispeed` | `int cfsetispeed(struct termios *, speed_t)` | 设置输入波特率 |
+| `cfsetospeed` | `int cfsetospeed(struct termios *, speed_t)` | 设置输出波特率 |
+| `cfsetspeed` | `int cfsetspeed(struct termios *, speed_t)` | 一次设置输入输出波特率 |
+| `tcflush` | `int tcflush(int fd, int queue_selector)` | 清空输入/输出缓冲区 |
+| `tcdrain` | `int tcdrain(int fd)` | 阻塞直到输出缓冲区数据全部发送完毕 |
+| `tcflow` | `int tcflow(int fd, int action)` | 暂停/重启数据收发 |
+| `tcsetattr` | `int tcsetattr(int fd, int optional_actions, const struct termios *)` | 写入配置使其生效 |
+| `read`/`write` | — | 读写数据 |
+
+`tcflush()` 的 `queue_selector`:
+
+| 取值 | 含义 |
+| ---- | ---- |
+| `TCIFLUSH` | 清空接收到但未被读取的数据 |
+| `TCOFLUSH` | 清空尚未传输成功的输出数据 |
+| `TCIOFLUSH` | 前两种都清空 |
+
+`tcflow()` 的 `action`:
+
+| 取值 | 含义 |
+| ---- | ---- |
+| `TCOOFF` | 暂停数据输出 |
+| `TCOON` | 重新启动暂停的输出 |
+| `TCIOFF` | 发送 STOP 字符,停止终端设备向系统发送数据 |
+| `TCION` | 发送 START 字符,启动终端设备向系统发送数据 |
+
+`tcsetattr()` 的 `optional_actions`(生效时机):
+
+| 取值 | 含义 |
+| ---- | ---- |
+| `TCSANOW` | 配置立即生效 |
+| `TCSADRAIN` | 在所有写入 fd 的输出传输完毕之后生效 |
+| `TCSAFLUSH` | 所有已接收但未读取的输入在配置生效前被丢弃 |
+
+**配置步骤**(以原始模式为例):
+
+```c
+struct termios new_cfg;
+memset(&new_cfg, 0x0, sizeof(struct termios));
+
+cfmakeraw(&new_cfg);                 // 原始模式
+new_cfg.c_cflag |= CREAD;            // 接收使能
+cfsetspeed(&new_cfg, B115200);       // 波特率
+
+new_cfg.c_cflag &= ~CSIZE;           // 清数据位
+new_cfg.c_cflag |= CS8;              // 8 位数据位
+
+/* 奇校验 */
+new_cfg.c_cflag |= (PARODD | PARENB);
+new_cfg.c_iflag |= INPCK;
+/* 偶校验 */
+new_cfg.c_cflag |= PARENB;
+new_cfg.c_cflag &= ~PARODD;
+new_cfg.c_iflag |= INPCK;
+/* 无校验 */
+new_cfg.c_cflag &= ~PARENB;
+new_cfg.c_iflag &= ~INPCK;
+
+new_cfg.c_cflag &= ~CSTOPB;          // 1 个停止位(置位则 2 个)
+
+new_cfg.c_cc[VTIME] = 0;             // MIN/TIME 在原始模式下同样有效
+new_cfg.c_cc[VMIN]  = 0;             // 均置 0:read 立即返回(非阻塞效果)
+
+tcflush(fd, TCIOFLUSH);              // 清缓冲
+tcsetattr(fd, TCSANOW, &new_cfg);    // 生效
+```
+
+打开串口设备时使用 `O_NOCTTY`,告知系统该设备不会成为进程的控制终端:
+
+```c
+fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);
+```
+
+## 2.5 完整源码:uart_test.c
+
+> 源码路径:`11、Linux C应用编程例程源码/26_uart/uart_test.c`。功能:串口在原始模式下收发数据,支持命令行指定设备、波特率、数据位、校验、停止位与读写类型;读操作使用**异步 I/O(信号驱动)**。
+
+```c
+#define _GNU_SOURCE     //在源文件开头定义_GNU_SOURCE宏
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <errno.h>
+#include <string.h>
+#include <signal.h>
+#include <termios.h>
+
+typedef struct uart_hardware_cfg {
+    unsigned int baudrate;      /* 波特率 */
+    unsigned char dbit;         /* 数据位 */
+    char parity;                /* 奇偶校验 */
+    unsigned char sbit;         /* 停止位 */
+} uart_cfg_t;
+
+static struct termios old_cfg;  //用于保存终端的配置参数
+static int fd;      //串口终端对应的文件描述符
+
+/**
+ ** 串口初始化操作
+ ** 参数device表示串口终端的设备节点
+ **/
+static int uart_init(const char *device)
+{
+    /* 打开串口终端 */
+    fd = open(device, O_RDWR | O_NOCTTY);
+    if (0 > fd) {
+        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
+        return -1;
+    }
+
+    /* 获取串口当前的配置参数 */
+    if (0 > tcgetattr(fd, &old_cfg)) {
+        fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
+        close(fd);
+        return -1;
+    }
+
+    return 0;
+}
+
+/**
+ ** 串口配置
+ ** 参数cfg指向一个uart_cfg_t结构体对象
+ **/
+static int uart_cfg(const uart_cfg_t *cfg)
+{
+    struct termios new_cfg = {0};   //将new_cfg对象清零
+    speed_t speed;
+
+    /* 设置为原始模式 */
+    cfmakeraw(&new_cfg);
+
+    /* 使能接收 */
+    new_cfg.c_cflag |= CREAD;
+
+    /* 设置波特率 */
+    switch (cfg->baudrate) {
+    case 1200: speed = B1200;
+        break;
+    case 1800: speed = B1800;
+        break;
+    case 2400: speed = B2400;
+        break;
+    case 4800: speed = B4800;
+        break;
+    case 9600: speed = B9600;
+        break;
+    case 19200: speed = B19200;
+        break;
+    case 38400: speed = B38400;
+        break;
+    case 57600: speed = B57600;
+        break;
+    case 115200: speed = B115200;
+        break;
+    case 230400: speed = B230400;
+        break;
+    case 460800: speed = B460800;
+        break;
+    case 500000: speed = B500000;
+        break;
+    default:    //默认配置为115200
+        speed = B115200;
+        printf("default baud rate: 115200\n");
+        break;
+    }
+
+    if (0 > cfsetspeed(&new_cfg, speed)) {
+        fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno));
+        return -1;
+    }
+
+    /* 设置数据位大小 */
+    new_cfg.c_cflag &= ~CSIZE;  //将数据位相关的比特位清零
+    switch (cfg->dbit) {
+    case 5:
+        new_cfg.c_cflag |= CS5;
+        break;
+    case 6:
+        new_cfg.c_cflag |= CS6;
+        break;
+    case 7:
+        new_cfg.c_cflag |= CS7;
+        break;
+    case 8:
+        new_cfg.c_cflag |= CS8;
+        break;
+    default:    //默认数据位大小为8
+        new_cfg.c_cflag |= CS8;
+        printf("default data bit size: 8\n");
+        break;
+    }
+
+    /* 设置奇偶校验 */
+    switch (cfg->parity) {
+    case 'N':       //无校验
+        new_cfg.c_cflag &= ~PARENB;
+        new_cfg.c_iflag &= ~INPCK;
+        break;
+    case 'O':       //奇校验
+        new_cfg.c_cflag |= (PARODD | PARENB);
+        new_cfg.c_iflag |= INPCK;
+        break;
+    case 'E':       //偶校验
+        new_cfg.c_cflag |= PARENB;
+        new_cfg.c_cflag &= ~PARODD; /* 清除PARODD标志,配置为偶校验 */
+        new_cfg.c_iflag |= INPCK;
+        break;
+    default:    //默认配置为无校验
+        new_cfg.c_cflag &= ~PARENB;
+        new_cfg.c_iflag &= ~INPCK;
+        printf("default parity: N\n");
+        break;
+    }
+
+    /* 设置停止位 */
+    switch (cfg->sbit) {
+    case 1:     //1个停止位
+        new_cfg.c_cflag &= ~CSTOPB;
+        break;
+    case 2:     //2个停止位
+        new_cfg.c_cflag |= CSTOPB;
+        break;
+    default:    //默认配置为1个停止位
+        new_cfg.c_cflag &= ~CSTOPB;
+        printf("default stop bit size: 1\n");
+        break;
+    }
+
+    /* 将MIN和TIME设置为0 */
+    new_cfg.c_cc[VTIME] = 0;
+    new_cfg.c_cc[VMIN] = 0;
+
+    /* 清空缓冲区 */
+    if (0 > tcflush(fd, TCIOFLUSH)) {
+        fprintf(stderr, "tcflush error: %s\n", strerror(errno));
+        return -1;
+    }
+
+    /* 写入配置、使配置生效 */
+    if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) {
+        fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
+        return -1;
+    }
+
+    /* 配置OK 退出 */
+    return 0;
+}
+
+/**
+ ** 打印帮助信息
+ **/
+static void show_help(const char *app)
+{
+    printf("Usage: %s [选项]\n"
+        "\n必选选项:\n"
+        "  --dev=DEVICE     指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n"
+        "  --type=TYPE      指定操作类型, 读串口还是写串口, 譬如--type=read(read表示读、write表示写、其它值无效)\n"
+        "\n可选选项:\n"
+        "  --brate=SPEED    指定串口波特率, 譬如--brate=115200\n"
+        "  --dbit=SIZE      指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n"
+        "  --parity=PARITY  指定串口奇偶校验方式, 譬如--parity=N(N表示无校验、O表示奇校验、E表示偶校验)\n"
+        "  --sbit=SIZE      指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n"
+        "  --help           查看本程序使用帮助信息\n\n", app);
+}
+
+/**
+ ** 信号处理函数,当串口有数据可读时,会跳转到该函数执行
+ **/
+static void io_handler(int sig, siginfo_t *info, void *context)
+{
+    unsigned char buf[10] = {0};
+    int ret;
+    int n;
+
+    if(SIGRTMIN != sig)
+        return;
+
+    /* 判断串口是否有数据可读 */
+    if (POLL_IN == info->si_code) {
+        ret = read(fd, buf, 8);     //一次最多读8个字节数据
+        printf("[ ");
+        for (n = 0; n < ret; n++)
+            printf("0x%hhx ", buf[n]);
+        printf("]\n");
+    }
+}
+
+/**
+ ** 异步I/O初始化函数
+ **/
+static void async_io_init(void)
+{
+    struct sigaction sigatn;
+    int flag;
+
+    /* 使能异步I/O */
+    flag = fcntl(fd, F_GETFL);  //使能串口的异步I/O功能
+    flag |= O_ASYNC;
+    fcntl(fd, F_SETFL, flag);
+
+    /* 设置异步I/O的所有者 */
+    fcntl(fd, F_SETOWN, getpid());
+
+    /* 指定实时信号SIGRTMIN作为异步I/O通知信号 */
+    fcntl(fd, F_SETSIG, SIGRTMIN);
+
+    /* 为实时信号SIGRTMIN注册信号处理函数 */
+    sigatn.sa_sigaction = io_handler;   //当串口有数据可读时,会跳转到io_handler函数
+    sigatn.sa_flags = SA_SIGINFO;
+    sigemptyset(&sigatn.sa_mask);
+    sigaction(SIGRTMIN, &sigatn, NULL);
+}
+
+int main(int argc, char *argv[])
+{
+    uart_cfg_t cfg = {0};
+    char *device = NULL;
+    int rw_flag = -1;
+    unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,
+                0x55, 0x66, 0x77, 0x88};    //通过串口发送出去的数据
+    int n;
+
+    /* 解析出参数 */
+    for (n = 1; n < argc; n++) {
+
+        if (!strncmp("--dev=", argv[n], 6))
+            device = &argv[n][6];
+        else if (!strncmp("--brate=", argv[n], 8))
+            cfg.baudrate = atoi(&argv[n][8]);
+        else if (!strncmp("--dbit=", argv[n], 7))
+            cfg.dbit = atoi(&argv[n][7]);
+        else if (!strncmp("--parity=", argv[n], 9))
+            cfg.parity = argv[n][9];
+        else if (!strncmp("--sbit=", argv[n], 7))
+            cfg.sbit = atoi(&argv[n][7]);
+        else if (!strncmp("--type=", argv[n], 7)) {
+            if (!strcmp("read", &argv[n][7]))
+                rw_flag = 0;        //读
+            else if (!strcmp("write", &argv[n][7]))
+                rw_flag = 1;        //写
+        }
+        else if (!strcmp("--help", argv[n])) {
+            show_help(argv[0]); //打印帮助信息
+            exit(EXIT_SUCCESS);
+        }
+    }
+
+    if (NULL == device || -1 == rw_flag) {
+        fprintf(stderr, "Error: the device and read|write type must be set!\n");
+        show_help(argv[0]);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 串口初始化 */
+    if (uart_init(device))
+        exit(EXIT_FAILURE);
+
+    /* 串口配置 */
+    if (uart_cfg(&cfg)) {
+        tcsetattr(fd, TCSANOW, &old_cfg);   //恢复到之前的配置
+        close(fd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 读|写串口 */
+    switch (rw_flag) {
+    case 0:  //读串口数据
+        async_io_init();	//我们使用异步I/O方式读取串口的数据,调用该函数去初始化串口的异步I/O
+        for ( ; ; )
+            sleep(1);   	//进入休眠、等待有数据可读,有数据可读之后就会跳转到io_handler()函数
+        break;
+    case 1:   //向串口写入数据
+        for ( ; ; ) {   		//循环向串口写入数据
+            write(fd, w_buf, 8); 	//一次向串口写入8个字节
+            sleep(1);       		//间隔1秒钟
+        }
+        break;
+    }
+
+    /* 退出 */
+    tcsetattr(fd, TCSANOW, &old_cfg);   //恢复到之前的配置
+    close(fd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+**逐段解释**
+
+- `uart_init()`:打开设备(`O_RDWR | O_NOCTTY`),并 `tcgetattr()` 保存原配置到 `old_cfg`(便于退出时恢复)。
+- `uart_cfg()`:清零 `new_cfg` 后 `cfmakeraw()`;使能接收 `CREAD`;`cfsetspeed()` 设波特率;`~(CSIZE)` 清零后按参数置 `CS5~CS8`;校验同时操作 `c_cflag`(`PARENB`/`PARODD`)与 `c_iflag`(`INPCK`);停止位用 `CSTOPB`;`VMIN=VTIME=0` 使 `read()` 立即返回;`tcflush()` 清缓冲;最后 `tcsetattr(TCSANOW)` 生效。
+- `io_handler()`:异步 I/O 信号处理函数,判断 `POLL_IN` 后一次性读取最多 8 字节并打印十六进制。可读数据大于 8 字节时,多余数据留待下次 `read()`。
+- `async_io_init()`:设置 `O_ASYNC` 使能异步 I/O,`F_SETOWN` 指定接收信号的进程,`F_SETSIG` 指定实时信号 `SIGRTMIN`,再用 `sigaction(SA_SIGINFO)` 注册处理函数。
+- `main()`:解析 `--dev/--brate/--dbit/--parity/--sbit/--type/--help`;`type=read` 时初始化异步 I/O 后 `sleep` 等待信号;`type=write` 时每秒写 8 字节 `0x11..0x88`。
+
+## 2.6 交叉编译与实验步骤
+
+```bash
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+${CC} -o testApp uart_test.c
+file testApp
+```
+
+实验步骤:
+
+1. ALPHA 板有 UART1(USB 调试串口,`/dev/ttymxc0`)和 UART3(RS232/RS485,`/dev/ttymxc2`)。板上 485 和 232 **共用 UART3,不能同时使用**,由 JP1 端子选择。
+2. **不能用 USB 调试串口测试**(它是系统控制台),使用 RS232 接口,通过 USB 转 RS232 线连到 PC。
+3. 拷到开发板后查看帮助:`./testApp --help`。
+4. 读测试:`./testApp --dev=/dev/ttymxc2 --type=read`,在 PC 串口调试助手(如 XCOM)发送 8 字节 `[0x11 0x22 ... 0x88]`,开发板打印收到的数据。
+5. 写测试:`./testApp --dev=/dev/ttymxc2 --type=write`,PC 端收到开发板每秒发来的 8 字节。
+6. `Ctrl+C` 结束。
+
+## 2.7 扩展:GPS NMEA 数据解析
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+GPS 模块通常通过串口输出 NMEA 0183 文本报文。把上面的串口初始化到 **9600、8N1、原始模式**,即可按行读取并解析:
+
+```c
+/* 假设已用 uart_init()/uart_cfg() 将串口配为 9600-8-N-1 原始模式 */
+static void nmea_parse(const char *line)
+{
+    /* $GNRMC,hhmmss.ss,A,ddmm.mmmm,N,dddmm.mmmm,E,ss.s,ccc,v*CS */
+    char buf[128];
+    char *p, *field[16];
+    int i = 0;
+
+    strncpy(buf, line, sizeof(buf) - 1);
+    buf[sizeof(buf) - 1] = '\0';
+
+    p = strtok(buf, ",");
+    while (p && i < 16) { field[i++] = p; p = strtok(NULL, ","); }
+
+    if (i > 6 && (!strcmp(field[0] + 3, "RMC")) && field[2][0] == 'A') {
+        double lat = atof(field[3]);   /* ddmm.mmmm 格式,需换算成度 */
+        double lon = atof(field[5]);
+        int lat_deg = (int)(lat / 100);
+        int lon_deg = (int)(lon / 100);
+        double lat_min = lat - lat_deg * 100;
+        double lon_min = lon - lon_deg * 100;
+
+        printf("纬度: %d.%06d %s, 经度: %d.%06d %s\n",
+               lat_deg, (int)(lat_min * 10000), field[4],
+               lon_deg, (int)(lon_min * 10000), field[6]);
+    }
+}
+```
+
+实际工程建议:使用环形缓冲区按 `\n` 切分报文;对 `$GNRMC`/`$GNGGA` 分别处理;用校验和(`*` 后的两位十六进制)验证完整性。
+
+---
+
+# 第三部分 ALSA 音频应用编程
+
+## 3.1 ALSA 与 alsa-lib
+
+ALSA 是 **Advanced Linux Sound Architecture**(高级 Linux 声音体系)的缩写,是 Linux 下的主流音频体系架构,提供音频和 MIDI 支持,替代了旧的 OSS。ALSA 本身是内核中的音频驱动框架,设计复杂、采用分离分层思想;但作为**应用编程**,我们无需研究它。
+
+- **硬件层**:ALSA 驱动框架注册的 sound 设备在 `/dev/snd` 下生成设备节点。
+- **应用层**:ALSA 提供标准 API,即 **alsa-lib**,应用程序调用即可控制底层音频硬件(播放、录音)。
+
+```mermaid
+flowchart TB
+    accTitle: ALSA 音频软件栈
+    accDescr: 应用程序调用 alsa-lib 库函数,alsa-lib 通过 ioctl/read/write 操作 /dev/snd 下的 sound 设备节点,最终由 ALSA 内核驱动框架控制音频硬件。
+    APP["应用程序<br/>(aplay / 自研程序)"] --> LIB["alsa-lib<br/>统一 C API"]
+    LIB --> NODE["/dev/snd/*<br/>pcmC0D0p / pcmC0D0c / controlC0"]
+    NODE --> DRV["ALSA 内核驱动框架"]
+    DRV --> HW["音频编解码芯片<br/>WM8960 / ES8388"]
+```
+
+> ALPHA I.MX6U 开发板 V2.4 及之前搭载 WM8960,V2.8 以后搭载 ES8388,均支持播放与录音。Mini 板没有板载音频编解码芯片,无法测试本章例程。
+
+## 3.2 sound 设备节点
+
+`/dev/snd` 下常见设备节点:
+
+| 设备节点 | 含义 |
+| -------- | ---- |
+| `controlC0` | 声卡控制设备(通道选择、混音器、麦克风控制等),C0 = 声卡 0 |
+| `pcmC0D0c` | 声卡 0、设备 0 的**录音** PCM 设备(c = capture) |
+| `pcmC0D0p` | 声卡 0、设备 0 的**播放** PCM 设备(p = playback) |
+| `pcmC0D1c` | 声卡 0、设备 1 的录音 PCM 设备 |
+| `pcmC0D1p` | 声卡 0、设备 1 的播放 PCM 设备 |
+| `timer` | 定时器 |
+
+`/proc/asound` 目录记录声卡信息:
+
+| 文件/命令 | 作用 |
+| --------- | ---- |
+| `cat /proc/asound/cards` | 列出系统中注册的所有声卡 |
+| `cat /proc/asound/devices` | 列出所有声卡注册的设备(control、pcm、timer、seq 等) |
+| `cat /proc/asound/pcm` | 列出所有 PCM 设备(playback 与 capture) |
+
+## 3.3 音频基本概念
+
+| 概念 | 说明 |
+| ---- | ---- |
+| 样本长度(Sample) | 记录音频数据最基本的单元;即采样位数/位深度,如 8bit、16bit、24bit |
+| 声道数(channel) | 单声道 Mono=1,双声道/立体声 Stereo=2 |
+| 帧(frame) | 一个声音单元,长度 = 样本长度 × 声道数。16bit 双声道一帧 = 16×2/8 = 4 字节 |
+| 采样率(Sample rate) | 每秒采样次数(针对帧)。8KHz 电话、22.05KHz FM、44.1KHz CD、48KHz 数字电视 |
+| 交错模式(interleaved) | 数据以连续帧存放:先帧 1 左、右声道样本,再帧 2 左、右……多数情况使用交错模式 |
+| 周期(period) | 音频设备读写数据的单位,一个周期包含若干帧,如 1024 帧 |
+| 缓冲区(buffer) | 由若干周期组成的一块空间,如 16 个周期 |
+| XRUN | 录音时应用读得慢导致数据被覆盖叫 **overrun**;播放时应用写得慢导致缓冲"饿死"叫 **underrun**,统称 XRUN |
+
+**为什么要把 buffer 拆成多个 period?** 底层驱动用 DMA 搬运数据,每搬完一个 period 触发一次中断。若一次性搬完整个 buffer,数据量越大延迟越高。周期越小延迟越低,但中断越频繁、CPU 效率越低。因此在延迟可接受的前提下,周期尽量大一些,具体依应用场合而定。
+
+## 3.4 环形缓冲区与指针
+
+播放时:应用向 buffer 写数据(write pointer 前移),音频设备从 buffer 读数据(read pointer 前移);录音时相反——设备写、应用读。两个指针到达 buffer 末尾都会回到起始位置,构成**环形缓冲区**。
+
+```mermaid
+flowchart LR
+    accTitle: ALSA 环形缓冲区
+    accDescr: buffer 由多个 period 组成,write pointer 指向应用写位置,read pointer 指向音频设备读位置,两者循环移动。
+    subgraph BUF["buffer = N × period"]
+        P0["period0"]
+        P1["period1"]
+        P2["period2"]
+        P3["period3"]
+    end
+    WP["write pointer<br/>应用写入位置"] --> P1
+    RP["read pointer<br/>设备读取位置"] --> P3
+```
+
+**播放数据传输**:应用写多少帧,write pointer 前移多少;写满一个周期,指针前移一个周期。buffer 满时应用不能再写,必须等设备播完一个周期腾出空闲周期。设备每次从 read pointer 读一个周期,读完前移一个周期。
+
+**录音数据传输**:设备(ADC 采样 + DMA)每次采集一个周期写入 buffer,write pointer 前移一个周期;应用从 read pointer 读一个周期,读完后该周期变空闲,等待设备再次写入。
+
+## 3.5 PCM 播放 API
+
+### 3.5.1 打开/关闭 PCM 设备
+
+```c
+int snd_pcm_open(snd_pcm_t **pcmp, const char *name,
+                 snd_pcm_stream_t stream, int mode);
+int snd_pcm_close(snd_pcm_t *pcm);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `pcmp` | 返回 PCM 设备句柄(`snd_pcm_t *`) |
+| `name` | 逻辑设备名,非设备文件名。`"hw:0,0"` = 声卡 0 的 PCM 设备 0,播放对应 `/dev/snd/pcmC0D0p`,录音对应 `pcmC0D0c`;还有 `"plughw:i,j"`、`"default"` 等 |
+| `stream` | `SND_PCM_STREAM_PLAYBACK`(播放)或 `SND_PCM_STREAM_CAPTURE`(采集) |
+| `mode` | 通常为 0(默认,阻塞方式);`SND_PCM_NONBLOCK` 为非阻塞 |
+
+```c
+ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
+if (0 > ret)
+    fprintf(stderr, "snd_pcm_open error: %s\n", snd_strerror(ret));
+```
+
+### 3.5.2 设置硬件参数
+
+用 `snd_pcm_hw_params_t` 描述硬件配置:
+
+```c
+snd_pcm_hw_params_t *hwparams = NULL;
+snd_pcm_hw_params_malloc(&hwparams);   /* 或 snd_pcm_hw_params_alloca() */
+snd_pcm_hw_params_any(pcm_handle, hwparams);   /* 用设备当前配置初始化 */
+/* ... 设置各项参数 ... */
+snd_pcm_hw_params(pcm_handle, hwparams);        /* 加载生效 */
+snd_pcm_hw_params_free(hwparams);
+```
+
+| 设置函数 | 作用 | 关键说明 |
+| -------- | ---- | -------- |
+| `snd_pcm_hw_params_set_access()` | 访问类型 | `SND_PCM_ACCESS_RW_INTERLEAVED`(交错,配 `readi/writei`)、`..._RW_NONINTERLEAVED`(配 `readn/writen`) |
+| `snd_pcm_hw_params_set_format()` | 数据格式 | 常用 `SND_PCM_FORMAT_S16_LE`(有符号 16 位小端) |
+| `snd_pcm_hw_params_set_channels()` | 声道数 | 2 = 双声道 |
+| `snd_pcm_hw_params_set_rate()` | 采样率 | `val` 如 44100;`dir`:-1 实际≤val、0 实际=val、1 实际≥val |
+| `snd_pcm_hw_params_set_period_size()` | 周期大小(帧) | 如 1024 |
+| `snd_pcm_hw_params_set_buffer_size()` | buffer 大小(帧) | 如 16×1024 |
+| `snd_pcm_hw_params_set_periods()` | buffer 大小(周期数) | 如 16 |
+
+> `snd_pcm_hw_params_set_period_size()` 的 `val` 单位是**帧**不是字节;`snd_pcm_hw_params_set_periods()` 的 `val` 单位是**周期**。`snd_pcm_hw_params()` 内部会自动调用 `snd_pcm_prepare()`,使设备进入 `SND_PCM_STATE_PREPARED`。
+
+### 3.5.3 读写数据与阻塞特性
+
+```c
+snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);
+snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size);
+```
+
+- `snd_pcm_writei()` 把应用缓冲 `buffer` 的数据写入驱动层播放环形缓冲区;`snd_pcm_readi()` 从驱动层录音环形缓冲区读到应用缓冲。
+- `size` 以**帧**为单位,通常一次写/读一个周期。
+- 成功返回实际读/写的**帧数**(不一定等于 `size`,仅当发生信号或 XRUN 时可能小于);失败返回负错误码。
+- **阻塞**:`snd_pcm_open()` 用阻塞方式时,录音无数据可读会阻塞、播放缓冲满会阻塞;非阻塞时立即返回错误。`snd_pcm_readi/writei` 仅适用于**交错模式**,非交错用 `snd_pcm_readn/writen`。
+- 在 `PREPARED` 状态下首次调用 `writei`/`readi` 会自动调用 `snd_pcm_start()` 开启设备,因此示例无需手动 start。
+
+> `buffer` 指**应用程序的缓冲区**,不要与驱动层环形缓冲区混淆。
+
+## 3.6 PCM 设备状态
+
+`snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm)` 获取当前状态:
+
+| 状态 | 含义 |
+| ---- | ---- |
+| `SND_PCM_STATE_OPEN` | 设备已打开(`snd_pcm_open()` 后) |
+| `SND_PCM_STATE_SETUP` | 设备已初始化、参数已配置好 |
+| `SND_PCM_STATE_PREPARED` | 设备已就绪,可开始播放/录音 |
+| `SND_PCM_STATE_RUNNING` | 正在运行(播放/录音中) |
+| `SND_PCM_STATE_XRUN` | 发生 XRUN;可调 `snd_pcm_prepare()` 恢复 |
+| `SND_PCM_STATE_DRAINING` | Draining(播放中/采集停止) |
+| `SND_PCM_STATE_PAUSED` | 暂停 |
+| `SND_PCM_STATE_SUSPENDED` | 硬件挂起;可用 `snd_pcm_resume()` 精细恢复 |
+| `SND_PCM_STATE_DISCONNECTED` | 硬件已断开 |
+
+相关函数:
+
+```c
+int snd_pcm_prepare(snd_pcm_t *pcm);          /* 使设备进入 PREPARED */
+int snd_pcm_start(snd_pcm_t *pcm);            /* 启动 */
+int snd_pcm_drain(snd_pcm_t *pcm);            /* 处理完挂起帧后停止 */
+int snd_pcm_drop(snd_pcm_t *pcm);             /* 立即停止,丢弃挂起帧 */
+int snd_pcm_pause(snd_pcm_t *pcm, int enable);/* enable=1 暂停,0 恢复 */
+int snd_pcm_resume(snd_pcm_t *pcm);           /* 从挂起恢复 */
+int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);
+int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);
+```
+
+状态转换:`open()` → OPEN;`snd_pcm_hw_params()` → SETUP → PREPARED(内部调 prepare);首次读写或 `snd_pcm_start()` → RUNNING;`drop()/drain()` → SETUP;XRUN 时处于 XRUN,`prepare()` 可恢复;`pause()` 在 RUNNING 与 PAUSED 间切换。
+
+## 3.7 错误处理
+
+`snd_pcm_readi/writei` 返回负错误码时用 `snd_strerror()` 获取描述:
+
+| 返回值 | 含义 | 处理建议 |
+| ------ | ---- | -------- |
+| `-EBADFD` | PCM 设备状态不对 | 需处于 PREPARED 或 RUNNING,检查状态转换 |
+| `-EPIPE` | 发生 XRUN | `snd_pcm_drop()` 停止,或 `snd_pcm_prepare()` 恢复 |
+| `-ESTRPIPE` | 硬件挂起(SUSPENDED) | `snd_pcm_resume()` 精细恢复;不支持则 `snd_pcm_prepare()` |
+
+## 3.8 混音器 API
+
+混音器用于配置音量、声道、增益等。`snd_mixer_t` 描述混音器,配置项称为**元素(element)**,用 `snd_mixer_elem_t` 描述。初始化流程:
+
+```c
+snd_mixer_t *mixer = NULL;
+
+snd_mixer_open(&mixer, 0);                       /* 打开一个空混音器 */
+snd_mixer_attach(mixer, "hw:0");                 /* 关联声卡控制设备(对应 /dev/snd/controlC0) */
+snd_mixer_selem_register(mixer, NULL, NULL);     /* 注册,options/classp 传 NULL */
+snd_mixer_load(mixer);                           /* 加载 */
+```
+
+遍历与取值:
+
+| 函数 | 作用 |
+| ---- | ---- |
+| `snd_mixer_first_elem(mixer)` / `snd_mixer_last_elem(mixer)` | 第一个/最后一个元素 |
+| `snd_mixer_elem_next(elem)` / `snd_mixer_elem_prev(elem)` | 下一个/上一个元素 |
+| `snd_mixer_selem_get_name(elem)` | 获取元素名字(如 `"Headphone"`、`"Playback"`) |
+| `snd_mixer_selem_has_playback_volume(elem)` / `..._has_capture_volume(elem)` | 是否 volume 类型 |
+| `snd_mixer_selem_has_playback_switch(elem)` / `..._has_capture_switch(elem)` | 是否 switch(bool)类型 |
+| `snd_mixer_selem_has_playback_channel(elem, ch)` / `..._has_capture_channel(elem, ch)` | 是否含指定通道 |
+| `snd_mixer_selem_is_playback_mono(elem)` / `..._is_capture_mono(elem)` | 是否单声道 |
+| `snd_mixer_selem_get_playback_volume_range(elem, &min, &max)` | 获取音量范围 |
+| `snd_mixer_selem_get_playback_volume(elem, ch, &value)` | 获取某声道音量 |
+| `snd_mixer_selem_set_playback_volume(elem, ch, value)` | 设置某声道音量 |
+| `snd_mixer_selem_set_playback_volume_all(elem, value)` | 设置所有声道音量 |
+| `snd_mixer_close(mixer)` | 关闭混音器 |
+
+声道枚举 `snd_mixer_selem_channel_id_t`:`SND_MIXER_SCHN_FRONT_LEFT`(0,左前)、`SND_MIXER_SCHN_FRONT_RIGHT`(右前)、`SND_MIXER_SCHN_REAR_LEFT`、`SND_MIXER_SCHN_REAR_RIGHT`、`SND_MIXER_SCHN_FRONT_CENTER`、`SND_MIXER_SCHN_WOOFER`、`SND_MIXER_SCHN_MONO`(= 左前)。
+
+## 3.9 alsa-utils 工具
+
+出厂系统已移植 alsa-lib 与 alsa-utils,可直接使用:
+
+| 工具 | 用途 |
+| ---- | ---- |
+| `aplay xxx.wav` | 播放 wav(不支持 mp3 解码) |
+| `arecord -f cd -d 10 test.wav` | 录音 10 秒,`-f cd` = 16bit little endian 44100 stereo |
+| `alsamixer` | 字符图形化混音器配置界面(F3 Playback / F4 Capture / F5 All,F6 选声卡) |
+| `amixer scontrols` / `amixer scontents` | 列出配置项 / 查看配置说明 |
+| `amixer sset Headphone 100,100` | 设置耳机音量左右声道 |
+| `amixer sset "Headphone Playback ZC" off` | 开关某项 |
+| `alsactl -f /var/lib/alsa/asound.state store` | 保存声卡配置 |
+| `alsactl -f /var/lib/alsa/asound.state restore` | 加载声卡配置 |
+
+> 开机自动从 `/var/lib/alsa/asound.state` 加载配置,关机时保存。此文件即 WM8960 声卡配置文件,由 `alsactl` 完成。
+
+## 3.10 WAV 文件格式与解析
+
+WAV 是 RIFF 格式,由若干 chunk 构成。例程定义了三个紧凑结构体(`__attribute__((packed))` 保证无填充字节):
+
+```c
+typedef struct WAV_RIFF {
+    char ChunkID[4];         		/* "RIFF" */
+    u_int32_t ChunkSize;        	/* 从下一个地址开始到文件末尾的总字节数 */
+    char Format[4];             	/* "WAVE" */
+} __attribute__ ((packed)) RIFF_t;
+
+typedef struct WAV_FMT {
+    char Subchunk1ID[4];         	/* "fmt " */
+    u_int32_t Subchunk1Size;      	/* 16 for PCM */
+    u_int16_t AudioFormat;        	/* PCM = 1*/
+    u_int16_t NumChannels;       	/* Mono = 1, Stereo = 2, etc. */
+    u_int32_t SampleRate;        	/* 8000, 44100, etc. */
+    u_int32_t ByteRate;           	/* = SampleRate * NumChannels * BitsPerSample/8 */
+    u_int16_t BlockAlign;        	/* = NumChannels * BitsPerSample/8 */
+    u_int16_t BitsPerSample;      	/* 8bits, 16bits, etc. */
+} __attribute__ ((packed)) FMT_t;
+
+typedef struct WAV_DATA {
+    char Subchunk2ID[4];         	/* "data" */
+    u_int32_t Subchunk2Size;      	/* data size */
+} __attribute__ ((packed)) DATA_t;
+```
+
+| 字段 | 含义 |
+| ---- | ---- |
+| `ChunkID` / `Format` | 固定 `"RIFF"` / `"WAVE"` |
+| `Subchunk1ID` | 固定 `"fmt "` |
+| `AudioFormat` | `1` 表示 PCM |
+| `NumChannels` | 声道数 |
+| `SampleRate` | 采样率 |
+| `ByteRate` | 每秒字节数 = 采样率 × 声道数 × 位深/8 |
+| `BlockAlign` | 一帧字节数 = 声道数 × 位深/8(即 frame_bytes) |
+| `BitsPerSample` | 位深 |
+| `Subchunk2ID` / `Subchunk2Size` | 数据块标识 `"data"` 与大小 |
+
+**解析思路**:读 RIFF 头并校验 `"RIFF"`/`"WAVE"`;读 `fmt ` 子块并校验 `"fmt "`,同时拿到 `SampleRate`、`NumChannels`、`BlockAlign`(用于配置 ALSA 参数与缓冲区);用 `lseek` 跳过 `sizeof(RIFF_t)+8+Subchunk1Size`,循环读 `DATA_t` 找 `"data"` 块(不是则 `lseek` 跳过该块),找到后文件位置即 PCM 数据起点。
+
+## 3.11 完整源码一:pcm_playback.c(播放 WAV)
+
+> 源码路径:`11、Linux C应用编程例程源码/28_alsa-lib/pcm_playback.c`。功能:解析 WAV 并在 PCM 播放设备上播放(阻塞方式,一个周期一个周期写)。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <alsa/asoundlib.h>
+
+/************************************
+ 宏定义
+ ************************************/
+#define PCM_PLAYBACK_DEV    "hw:0,0"
+
+/************************************
+ WAV音频文件解析相关数据结构申明
+ ************************************/
+typedef struct WAV_RIFF {
+    char ChunkID[4];         		/* "RIFF" */
+    u_int32_t ChunkSize;        	/* 从下一个地址开始到文件末尾的总字节数 */
+    char Format[4];             	/* "WAVE" */
+} __attribute__ ((packed)) RIFF_t;
+
+typedef struct WAV_FMT {
+    char Subchunk1ID[4];         	/* "fmt " */
+    u_int32_t Subchunk1Size;      	/* 16 for PCM */
+    u_int16_t AudioFormat;        	/* PCM = 1*/
+    u_int16_t NumChannels;       	/* Mono = 1, Stereo = 2, etc. */
+    u_int32_t SampleRate;        	/* 8000, 44100, etc. */
+    u_int32_t ByteRate;           	/* = SampleRate * NumChannels * BitsPerSample/8 */
+    u_int16_t BlockAlign;        	/* = NumChannels * BitsPerSample/8 */
+    u_int16_t BitsPerSample;      	/* 8bits, 16bits, etc. */
+} __attribute__ ((packed)) FMT_t;
+static FMT_t wav_fmt;
+
+typedef struct WAV_DATA {
+    char Subchunk2ID[4];         	/* "data" */
+    u_int32_t Subchunk2Size;      	/* data size */
+} __attribute__ ((packed)) DATA_t;
+
+/************************************
+  static静态全局变量定义
+ ************************************/
+static snd_pcm_t *pcm = NULL;            	//pcm句柄
+static unsigned int buf_bytes;               	//应用程序缓冲区的大小(字节为单位)
+static void *buf = NULL;                  	//指向应用程序缓冲区的指针
+static int fd = -1;                         	//指向WAV音频文件的文件描述符
+static snd_pcm_uframes_t period_size = 1024;  	//周期大小(单位: 帧)
+static unsigned int periods = 16;             	//周期数(设备驱动层buffer的大小)
+
+static int snd_pcm_init(void)
+{
+    snd_pcm_hw_params_t *hwparams = NULL;
+    int ret;
+
+    /* 打开PCM设备 */
+    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
+                    PCM_PLAYBACK_DEV, snd_strerror(ret));
+        return -1;
+    }
+
+    /* 实例化hwparams对象 */
+    snd_pcm_hw_params_malloc(&hwparams);
+
+    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
+    ret = snd_pcm_hw_params_any(pcm, hwparams);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /************** 
+     设置参数
+    ***************/
+    /* 设置访问类型: 交错模式 */
+    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置数据格式: 有符号16位、小端模式 */
+    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置采样率 */
+    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置声道数: 双声道 */
+    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置周期大小: period_size */
+    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置周期数(驱动层buffer的大小): periods */
+    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 使配置生效 */
+    ret = snd_pcm_hw_params(pcm, hwparams);
+    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
+        goto err1;
+    }
+
+    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
+    return 0;
+
+err2:
+    snd_pcm_hw_params_free(hwparams);   //释放内存
+err1:
+    snd_pcm_close(pcm); //关闭pcm设备
+    return -1;
+}
+
+static int open_wav_file(const char *file)
+{
+    RIFF_t wav_riff;
+    DATA_t wav_data;
+    int ret;
+
+    fd = open(file, O_RDONLY);
+    if (0 > fd) {
+        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
+        return -1;
+    }
+
+    /* 读取RIFF chunk */
+    ret = read(fd, &wav_riff, sizeof(RIFF_t));
+    if (sizeof(RIFF_t) != ret) {
+        if (0 > ret)
+            perror("read error");
+        else
+            fprintf(stderr, "check error: %s\n", file);
+        close(fd);
+        return -1;
+    }
+
+    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
+        strncmp("WAVE", wav_riff.Format, 4)) {
+        fprintf(stderr, "check error: %s\n", file);
+        close(fd);
+        return -1;
+    }
+
+    /* 读取sub-chunk-fmt */
+    ret = read(fd, &wav_fmt, sizeof(FMT_t));
+    if (sizeof(FMT_t) != ret) {
+        if (0 > ret)
+            perror("read error");
+        else
+            fprintf(stderr, "check error: %s\n", file);
+        close(fd);
+        return -1;
+    }
+
+    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
+        fprintf(stderr, "check error: %s\n", file);
+        close(fd);
+        return -1;
+    }
+
+    /* 打印音频文件的信息 */
+    printf("<<<<音频文件格式信息>>>>\n\n");
+    printf("  file name:     %s\n", file);
+    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
+    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
+    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
+    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
+    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
+    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
+    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);
+
+    /* sub-chunk-data */
+    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
+                SEEK_SET)) {
+        perror("lseek error");
+        close(fd);
+        return -1;
+    }
+
+    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {
+
+        /* 找到sub-chunk-data */
+        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
+            return 0;
+
+        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
+            perror("lseek error");
+            close(fd);
+            return -1;
+        }
+    }
+
+    fprintf(stderr, "check error: %s\n", file);
+    return -1;
+}
+
+/************************************
+  main主函数
+ ************************************/
+int main(int argc, char *argv[])
+{
+    int ret;
+
+    if (2 != argc) {
+        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 打开WAV音频文件 */
+    if (open_wav_file(argv[1]))
+        exit(EXIT_FAILURE);
+
+    /* 初始化PCM Playback设备 */
+    if (snd_pcm_init())
+        goto err1;
+
+    /* 申请读缓冲区 */
+    buf = malloc(buf_bytes);
+    if (NULL == buf) {
+        perror("malloc error");
+        goto err2;
+    }
+
+    /* 播放 */
+    for ( ; ; ) {
+
+        memset(buf, 0x00, buf_bytes);   //buf清零
+        ret = read(fd, buf, buf_bytes); //从音频文件中读取数据
+        if (0 >= ret)   // 如果读取出错或文件读取完毕
+            goto err3;
+
+        ret = snd_pcm_writei(pcm, buf, period_size);
+        if (0 > ret) {
+            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
+            goto err3;
+        }
+        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
+            //此时我们需要调整下音频文件的读位置
+            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
+            //frame_bytes表示一帧的字节大小
+            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
+                perror("lseek error");
+                goto err3;
+            }
+        }
+    }
+
+err3:
+    free(buf);     //释放内存
+err2:
+    snd_pcm_close(pcm); //关闭pcm设备
+err1:
+    close(fd);      //关闭打开的音频文件
+    exit(EXIT_FAILURE);
+}
+```
+
+**关键逻辑**:`buf_bytes = period_size × BlockAlign`,即一个周期的字节数。每次 `read()` 读一个周期,再 `snd_pcm_writei()` 写一个周期。若实际写入帧数小于 `period_size`,说明有部分未写入,用 `lseek` 把文件读位置**回退** `(period_size - ret) × BlockAlign` 字节,下次重发。阻塞方式下,环形缓冲区未满时 `writei` 立即写入返回;满了则阻塞,直到设备播完一个周期腾出空闲周期。
+
+## 3.12 完整源码二:pcm_capture.c(录音)
+
+> 源码路径:`11、Linux C应用编程例程源码/28_alsa-lib/pcm_capture.c`。功能:从 PCM 采集设备读取数据写入新建文件(16bit 双声道 44100Hz)。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <alsa/asoundlib.h>
+
+/************************************
+ 宏定义
+ ************************************/
+#define PCM_CAPTURE_DEV    "hw:0,0"
+
+/************************************
+  static静态全局变量定义
+ ************************************/
+static snd_pcm_t *pcm = NULL;            	//pcm句柄
+static snd_pcm_uframes_t period_size = 1024; 	//周期大小(单位: 帧)
+static unsigned int periods = 16;             	//周期数(buffer的大小)
+static unsigned int rate = 44100;             	//采样率
+
+static int snd_pcm_init(void)
+{
+    snd_pcm_hw_params_t *hwparams = NULL;
+    int ret;
+
+    /* 打开PCM设备 */
+    ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
+                    PCM_CAPTURE_DEV, snd_strerror(ret));
+        return -1;
+    }
+
+    /* 实例化hwparams对象 */
+    snd_pcm_hw_params_malloc(&hwparams);
+
+    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
+    ret = snd_pcm_hw_params_any(pcm, hwparams);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /************** 
+     设置参数
+    ***************/
+    /* 设置访问类型: 交错模式 */
+    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置数据格式: 有符号16位、小端模式 */
+    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置采样率 */
+    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置声道数: 双声道 */
+    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置周期大小: period_size */
+    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 设置周期数(buffer的大小): periods */
+    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
+        goto err2;
+    }
+
+    /* 使配置生效 */
+    ret = snd_pcm_hw_params(pcm, hwparams);
+    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
+    if (0 > ret) {
+        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
+        goto err1;
+    }
+
+    return 0;
+
+err2:
+    snd_pcm_hw_params_free(hwparams);   //释放内存
+err1:
+    snd_pcm_close(pcm); //关闭pcm设备
+    return -1;
+}
+
+/************************************
+  main主函数
+ ************************************/
+int main(int argc, char *argv[])
+{
+    unsigned char *buf = NULL;
+    unsigned int buf_bytes;
+    int fd = -1;
+    int ret;
+
+    if (2 != argc) {
+        fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 初始化PCM Capture设备 */
+    if (snd_pcm_init())
+        exit(EXIT_FAILURE);
+
+    /* 申请读缓冲区 */
+    buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
+    buf = malloc(buf_bytes);
+    if (NULL == buf) {
+        perror("malloc error");
+        goto err1;
+    }
+
+    /* 打开一个新建文件 */
+    fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
+    if (0 > fd) {
+        fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
+        goto err2;
+    }
+
+    /* 录音 */
+    for ( ; ; ) {
+
+        //memset(buf, 0x00, buf_bytes);   //buf清零
+        ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期
+        if (0 > ret) {
+            fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
+            goto err3;
+        }
+
+        // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数
+        ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
+        if (0 >= ret)
+            goto err3;
+    }
+
+err3:
+    close(fd);  //关闭文件
+err2:
+    free(buf);     //释放内存
+err1:
+    snd_pcm_close(pcm); //关闭pcm设备
+    exit(EXIT_FAILURE);
+}
+```
+
+**关键逻辑**:录音参数固定为 16bit 双声道,一帧 4 字节,故 `buf_bytes = period_size × 4`。`snd_pcm_readi()` 成功返回实际读取的**帧数**,写文件时乘以 4 转成字节数。阻塞方式下缓冲区无数据时 `readi` 阻塞,直到设备采集到一个周期后被唤醒。
+
+## 3.13 完整源码三:混音器与播放控制(pcm_playback_mixer.c 节选)
+
+> 源码路径:`28_alsa-lib/pcm_playback_mixer.c`。在异步方式播放的基础上加入混音器,实现按 `w`/`s` 调节音量、空格暂停、`q` 退出。其余(WAV 解析、异步回调、播放主循环)与前述一致,下面给出混音器初始化与音量调节的核心代码。
+
+```c
+#define PCM_PLAYBACK_DEV    "hw:0,0"
+#define MIXER_DEV           "hw:0"
+
+static snd_mixer_t *mixer = NULL;                   	//混音器句柄
+static snd_mixer_elem_t *playback_vol_elem = NULL;  	//播放<音量控制>元素
+
+static int snd_mixer_init(void)
+{
+    snd_mixer_elem_t *elem = NULL;
+    const char *elem_name;
+    long minvol, maxvol;
+    int ret;
+
+    /* 打开混音器 */
+    ret = snd_mixer_open(&mixer, 0);
+    if (0 > ret) {
+        fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));
+        return -1;
+    }
+
+    /* 关联一个声卡控制设备 */
+    ret = snd_mixer_attach(mixer, MIXER_DEV);
+    if (0 > ret) {
+        fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));
+        goto err;
+    }
+
+    /* 注册混音器 */
+    ret = snd_mixer_selem_register(mixer, NULL, NULL);
+    if (0 > ret) {
+        fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));
+        goto err;
+    }
+
+    /* 加载混音器 */
+    ret = snd_mixer_load(mixer);
+    if (0 > ret) {
+        fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));
+        goto err;
+    }
+
+    /* 遍历混音器中的元素 */
+    elem = snd_mixer_first_elem(mixer);//找到第一个元素
+    while (elem) {
+
+        elem_name = snd_mixer_selem_get_name(elem);//获取元素的名称
+        /* 针对开发板出厂系统:WM8960声卡设备 */
+        if(!strcmp("Speaker", elem_name) ||  //耳机音量<对喇叭外音输出有效>
+           !strcmp("Headphone", elem_name) ||//喇叭音量<对耳机输出有效>
+           !strcmp("Playback", elem_name)) {//播放音量<总的音量控制,对喇叭和耳机输出都有效>
+            if (snd_mixer_selem_has_playback_volume(elem)) {//是否是音量控制元素
+                snd_mixer_selem_get_playback_volume_range(elem, &minvol, &maxvol);//获取音量可设置范围
+                snd_mixer_selem_set_playback_volume_all(elem, (maxvol-minvol)*0.9 + minvol);//全部设置为90%
+
+                if (!strcmp("Playback", elem_name))
+                    playback_vol_elem = elem;
+            }
+        }
+
+        elem = snd_mixer_elem_next(elem);
+    }
+
+    return 0;
+
+err:
+    snd_mixer_close(mixer);
+    return -1;
+}
+
+/* 主循环中的音量调节(w 增加、s 减小) */
+case 'w':           //音量增加
+    if (playback_vol_elem) {
+        snd_mixer_selem_get_playback_volume(playback_vol_elem,
+                SND_MIXER_SCHN_FRONT_LEFT, &vol);
+        vol++;
+        snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
+    }
+    break;
+case 's':           //音量降低
+    if (playback_vol_elem) {
+        snd_mixer_selem_get_playback_volume(playback_vol_elem,
+                SND_MIXER_SCHN_FRONT_LEFT, &vol);
+        vol--;
+        snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
+    }
+    break;
+```
+
+暂停/恢复使用 `snd_pcm_state()` + `snd_pcm_pause()`:
+
+```c
+case ' ':   //空格暂停/恢复
+    switch (snd_pcm_state(pcm)) {
+    case SND_PCM_STATE_PAUSED:
+        snd_pcm_pause(pcm, 0);   //恢复运行
+        break;
+    case SND_PCM_STATE_RUNNING:
+        snd_pcm_pause(pcm, 1);   //暂停
+        break;
+    }
+    break;
+```
+
+> 注意:并非所有音频硬件都支持暂停,可用 `snd_pcm_hw_params_can_pause()` 判断。
+
+## 3.14 读写方式变体:异步 I/O 与 poll
+
+`28_alsa-lib` 目录另提供了 `pcm_playback_async.c`、`pcm_capture_async.c`、`pcm_playback_poll.c`、`pcm_capture_poll.c`,核心区别只在"何时写/读"。
+
+**异步方式**:注册回调,驱动可写时自动触发:
+
+```c
+ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
+
+static void snd_playback_async_callback(snd_async_handler_t *handler)
+{
+    snd_pcm_t *handle = snd_async_handler_get_pcm(handler);
+    snd_pcm_sframes_t avail = snd_pcm_avail_update(handle);
+    while (avail >= period_size) {
+        /* read(fd,...) + snd_pcm_writei(handle, buf, period_size) */
+        avail = snd_pcm_avail_update(handle);
+    }
+}
+```
+
+**poll 方式**:通过 `snd_pcm_poll_descriptors_count()` / `snd_pcm_poll_descriptors()` 拿到 PCM 句柄的轮询描述符,用 `poll()` 等待 `POLLIN`/`POLLOUT`,再用 `snd_pcm_poll_descriptors_revents()` 取事件:
+
+```c
+count = snd_pcm_poll_descriptors_count(pcm);
+pfds = calloc(count, sizeof(struct pollfd));
+snd_pcm_poll_descriptors(pcm, pfds, count);
+
+for (;;) {
+    poll(pfds, count, -1);
+    snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
+    if (revents & POLLERR) goto err;
+    if (revents & POLLOUT) {          /* 录音侧为 POLLIN */
+        avail = snd_pcm_avail_update(pcm);
+        while (avail >= period_size) {
+            /* read(fd,...) + snd_pcm_writei(...) 或 snd_pcm_readi + write(fd) */
+            avail = snd_pcm_avail_update(pcm);
+        }
+    }
+}
+```
+
+## 3.15 交叉编译与实验步骤
+
+```bash
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+
+# 播放程序(链接 alsa-lib)
+${CC} -o pcm_playback pcm_playback.c -lasound
+# 录音程序
+${CC} -o pcm_capture pcm_capture.c -lasound
+# 带混音器的播放程序
+${CC} -o pcm_playback_mixer pcm_playback_mixer.c -lasound
+```
+
+实验步骤:
+
+1. 拷贝可执行文件与一个 `test.wav` 到开发板家目录(`scp`)。
+2. 播放:`./pcm_playback test.wav`,终端打印 WAV 格式信息,喇叭/耳机出声。
+3. 录音:`./pcm_capture out.pcm`(`Ctrl+C` 结束),得到裸 PCM 数据(16bit 双声道 44100)。
+4. 可先用 `./mic_in_config.sh` 中的 `amixer` 命令配置录音/播放音量与输入通道。
+5. 用 `arecord -f cd -d 10 test.wav` 对比验证;`aplay test.wav` 播放。
+
+## 3.16 ALSA 插件
+
+`user 空间的 alsa-lib` 使用**逻辑设备名**而非设备节点。`snd_pcm_open()` 会加载 `/usr/share/alsa/alsa.conf` 并解析,其中又会加载 `/etc/asound.conf` 与 `~/.asoundrc`。每个 `pcm.name { ... }` 定义了一个插件,`type` 字段指定插件类型:
+
+| 插件 | 作用 |
+| ---- | ---- |
+| `hw` | 直接与 ALSA 内核驱动通信,无任何转换的原始通信 |
+| `plughw` | 提供采样率转换等软件特性(硬件不支持时可用) |
+| `dmix` | 混音,把多个应用程序的音频数据混合 |
+| `softvol` | 软件音量 |
+
+`"hw:i,j"` 就是名为 `hw` 的插件,`i` 是声卡号、`j` 是设备号。
+
+---
+
+# 跨平台对比
+
+| 主题 | I.MX6ULL(本教程) | STM32(裸机/HAL) | RK3568(Linux) |
+| ---- | ------------------ | ----------------- | --------------- |
+| 摄像头 | V4L2 `/dev/videoX`,ov5640/ov2640/ov7725/UVC,ioctl + mmap 队列 | DCMI + DMA,直接操作寄存器/LL 库 | V4L2 + Media Controller/V4L2 subdev,Rockchip ISP |
+| 串口 | termios API,`/dev/ttymxcX` | USART,HAL_UART_Transmit/Receive + 中断/DMA | termios API,`/dev/ttySX` |
+| 音频 | ALSA + alsa-lib,WM8960/ES8388 | I2S + 外部 codec,寄存器配置 | ALSA + alsa-lib,通常走 GStreamer/ASoC |
+| 编程层 | 用户态,统一设备框架 | 裸机/RTOS,直接寄存器 | 用户态,统一设备框架 |
+| 关键差异 | 需掌握框架接口与设备节点 | 需手工配置时钟/引脚/DMA | 同 V4L2/ALSA,但多平面、复杂管线常见 |
+
+---
+
+# 面试精选(5 题)
+
+### Q1. V4L2 的 MMAP 采集流程是怎样的?为什么需要"入队/出队"?
+
+**答案要点**:打开设备 → QUERYCAP → ENUM_FMT/S_FMT → REQBUFS → QUERYBUF+mmap → QBUF → STREAMON → 循环 DQBUF/处理/QBUF → STREAMOFF。入队/出队是为了在应用与内核之间高效、安全地传递帧缓冲所有权。
+
+**详细解答**:内核维护一个帧缓冲队列。应用 `VIDIOC_QBUF` 把空闲缓冲交给驱动,驱动填充一帧后缓冲变为"满";应用 `VIDIOC_DQBUF` 取走满缓冲(出队),处理完再 `VIDIOC_QBUF` 归还(入队)。这样避免了每帧内存拷贝——`mmap` 后应用直接读内核缓冲,即零拷贝。`count` 一般取 3~4,太多浪费内存、太少可能丢帧。
+
+**追问**
+1. `VIDIOC_DQBUF` 在缓冲为空时会怎样?(阻塞直到驱动填入一帧;非阻塞打开则立即返回 `EAGAIN`)
+2. `V4L2_MEMORY_MMAP` 与 `V4L2_MEMORY_USERPTR` 区别?(MMAP 由内核分配、应用映射;USERPTR 由应用提供缓冲指针)
+
+### Q2. `VIDIOC_S_FMT` 设置后为什么要回读 `struct v4l2_format`?
+
+**答案要点**:驱动可能按硬件能力修改参数,实际值不一定等于请求值,回读才能拿到真正生效的格式。
+
+**详细解答**:例如请求 800×480 RGB565,但摄像头不支持 800×480,驱动可能改成 640×480;不支持 RGB565 则保留 YUYV。典型处理:
+
+```c
+fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+fmt.fmt.pix.width = 800; fmt.fmt.pix.height = 480;
+fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
+ioctl(fd, VIDIOC_S_FMT, &fmt);
+if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) { /* 不支持,退化为 YUYV 等 */ }
+```
+
+注意 `VIDIOC_TRY_FMT` 只试不改,可在正式设置前探测。
+
+**追问**
+1. 为什么 USB 摄像头常不支持 RGB565?(UVC 标准输出以 YUYV/MJPEG 为主)
+2. `fmt.fmt.pix.sizeimage` 有什么用?(一帧图像数据的总字节数,用于分配缓冲/校验)
+
+### Q3. termios 的 `c_cflag`、`c_iflag`、`c_lflag` 各控制什么?如何配置 115200-8-N-1?
+
+**答案要点**:`c_iflag` 输入处理、`c_oflag` 输出处理、`c_cflag` 硬件特性(波特率/数据位/校验/停止位)、`c_lflag` 本地模式(回显/规范模式等)。
+
+**详细解答**:配置 115200-8-N-1 的原始模式:
+
+```c
+struct termios new_cfg = {0};
+cfmakeraw(&new_cfg);              // 原始模式
+new_cfg.c_cflag |= CREAD;         // 接收使能
+cfsetspeed(&new_cfg, B115200);    // 波特率
+new_cfg.c_cflag &= ~CSIZE;
+new_cfg.c_cflag |= CS8;           // 8 数据位
+new_cfg.c_cflag &= ~PARENB;       // 无校验
+new_cfg.c_iflag &= ~INPCK;
+new_cfg.c_cflag &= ~CSTOPB;       // 1 停止位
+tcsetattr(fd, TCSANOW, &new_cfg);
+```
+
+原始模式会清除 `ICANON/ECHO/ISIG` 等本地标志,并关闭 `OPOST` 输出处理——这是二进制通信必需的。
+
+**追问**
+1. `VMIN=VTIME=0` 时 `read()` 行为?(立即返回:有数据返回字节数,无数据返回 0)
+2. 为什么打开串口要加 `O_NOCTTY`?(避免该终端成为本进程的控制终端)
+
+### Q4. ALSA 中 frame、period、buffer 的关系是什么?为什么 buffer 要分成多个 period?
+
+**答案要点**:frame = 样本长度 × 声道数;period 是设备读写的单位(若干帧);buffer 由若干 period 组成。拆分 period 是为了平衡延迟与中断开销。
+
+**详细解答**:以 16bit 双声道为例,一帧 = 16/8 × 2 = 4 字节。`period_size=1024` 帧即 4096 字节;`periods=16` 则 buffer = 16×1024 帧。DMA 每搬完一个 period 触发一次中断。整块搬运延迟大,period 太小则中断频繁、CPU 开销高。`buf_bytes = period_size × BlockAlign` 即每次 `snd_pcm_writei/readi` 的数据量。
+
+**追问**
+1. overrun 和 underrun 分别发生在播放还是录音?(录音读太慢 → overrun;播放写太慢 → underrun,统称 XRUN)
+2. 发生 XRUN 后如何恢复?(`snd_pcm_prepare()` 回到 PREPARED,或 `snd_pcm_drop()` 停止)
+
+### Q5. `snd_pcm_readi/writei` 的返回值含义与阻塞特性?出错如何处理?
+
+**答案要点**:成功返回实际读/写的帧数(可能小于请求值,仅信号或 XRUN 时),失败返回负错误码;阻塞与否由 `snd_pcm_open` 的 mode 决定。
+
+**详细解答**:阻塞方式下,录音无数据可读、播放缓冲满时调用会阻塞。错误码:`-EBADFD`(状态不对,需 PREPARED/RUNNING)、`-EPIPE`(XRUN,用 `prepare` 恢复)、`-ESTRPIPE`(硬件挂起,用 `resume` 或 `prepare`)。写入帧数小于请求值时,需回退文件读位置重发,见 `pcm_playback.c` 的 `lseek(fd, (ret-period_size)*BlockAlign, SEEK_CUR)`。
+
+**追问**
+1. 非交错模式该用什么函数?(`snd_pcm_readn` / `snd_pcm_writen`)
+2. 为什么 `snd_pcm_hw_params()` 后设备就是 PREPARED?(其内部自动调用 `snd_pcm_prepare()`)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十五章 V4L2摄像头应用编程、第二十六章 串口应用编程、第二十八章 音频应用编程;配套例程源码 `25_v4l2_camera/v4l2_camera.c`、`26_uart/uart_test.c`、`28_alsa-lib/`。

+ 713 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/面试-外设与高级IO编程.md

@@ -0,0 +1,713 @@
+---
+title: 面试-外设与高级IO编程
+tags: [嵌入式Linux, Linux应用编程, 面试, 高级IO, 非阻塞IO, IO多路复用, select, poll, 异步IO, mmap, GPIO, LED, input子系统, tslib, FrameBuffer, LCD, libjpeg, libpng, FreeType, PWM, 看门狗, V4L2, 串口, termios, ALSA]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I_O、第十五~二十八章 外设应用编程"
+---
+
+# 面试-外设与高级IO编程
+
+> 💡 **关联知识**:[[03-外设与高级IO编程/01-高级IO]]、[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/03-输入设备与tslib]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]]。
+
+覆盖《I.MX6U嵌入式Linux C应用编程指南》第十三章至第二十八章外设部分的 6 大主题、30 道题。每题含「答案要点 / 详细解答(代码、对比表)/ 2 条追问」。
+
+---
+
+## 一、高级 IO(5 题)
+
+### Q1.1 阻塞 I/O 与非阻塞 I/O 有什么区别?各自的优缺点是什么?
+
+**答案要点**:阻塞 I/O 在数据不可用时会让调用者休眠、交出 CPU;非阻塞 I/O 立即返回,不挂起进程。
+
+**详细解答**:对某些文件类型(管道、设备、socket),`read()` 无数据可读时会阻塞休眠,直到有数据才被唤醒,这就是阻塞 I/O;非阻塞 I/O 即使无数据也不阻塞、立即返回错误(`EAGAIN`)。普通文件的读写总在有限时间内返回,本质上就是非阻塞的,`O_NONBLOCK` 对它无影响。
+
+启用方式:`open(..., O_NONBLOCK)`,或对已有 fd 用 `fcntl`:
+
+```c
+int flag = fcntl(0, F_GETFL);
+flag |= O_NONBLOCK;
+fcntl(0, F_SETFL, flag);
+```
+
+| 方式 | 数据不可用时 | CPU 占用 | 适用场景 |
+| ---- | ------------ | -------- | -------- |
+| 阻塞 I/O | 休眠等待,让出 CPU | 几乎为 0 | 单一数据源、无需同时处理多路 |
+| 非阻塞 I/O | 立即返回错误 | 轮询时高 | 需同时处理多路,配合轮询/多路复用 |
+
+**追问**
+1. 阻塞式 I/O 的困境是什么?(同时读多个设备时,先读的那个没数据就卡住,后面的读不到,无法并发读取)
+2. 非阻塞 + 轮询有什么缺点?(CPU 空转、占用率高、效率低)
+
+### Q1.2 `select()` 的原理和用法?返回值有哪些情况?
+
+**答案要点**:`select()` 是 I/O 多路复用系统调用,同时监视多个 fd 是否就绪,特征为"外部阻塞、内部监视多路 I/O"。
+
+**详细解答**:`#include <sys/select.h>`,原型 `int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)`。`nfds` 通常取最大 fd + 1;三个集合分别表示关心的可读、可写、异常条件,不关心可传 `NULL`。
+
+```c
+fd_set rdfds;
+FD_ZERO(&rdfds);
+FD_SET(mouse_fd, &rdfds);
+FD_SET(0, &rdfds);
+ret = select(maxfd + 1, &rdfds, NULL, NULL, NULL);
+if (ret > 0 && FD_ISSET(mouse_fd, &rdfds)) { /* 鼠标可读 */ }
+```
+
+三个 fd_set 是**值-结果参数**:调用前放入关心的 fd,返回后只保留就绪的 fd。因此在循环调用中**每轮都要重新初始化并设置集合**。
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| `>0` | 就绪 fd 的个数(同一 fd 同时就绪多个事件会计多次) |
+| `0` | 超时,没有任何 fd 就绪;集合被清空 |
+| `-1` | 出错,设置 errno |
+
+**追问**
+1. `timeout` 传 `NULL`、`0`、非 0 分别代表什么?(一直阻塞 / 不阻塞仅轮询一次 / 阻塞上限)
+2. 为什么每次循环都要重新 `FD_ZERO`+`FD_SET`?(select 返回时修改了集合,只保留就绪 fd)
+
+### Q1.3 `poll()` 与 `select()` 有何区别?`struct pollfd` 的字段含义?
+
+**答案要点**:`poll()` 功能与 `select()` 相同,但用 `struct pollfd` 数组替代三个 fd_set,没有 fd 数量上限。
+
+**详细解答**:`#include <poll.h>`,原型 `int poll(struct pollfd *fds, nfds_t nfds, int timeout)`。
+
+```c
+struct pollfd {
+    int   fd;       /* 文件描述符 */
+    short events;   /* 请求监视的事件(位掩码) */
+    short revents;  /* 返回时就绪的事件(由内核填写) */
+};
+```
+
+- fd 为负值时该元素被忽略、`revents` 置 0;
+- `timeout`:-1 一直阻塞、0 立即返回、>0 阻塞上限(毫秒);
+- 返回值为 `revents` 非 0 的元素个数,0 表示超时,-1 出错。
+
+常用事件:`POLLIN`(普通优先级数据可读)、`POLLOUT`(可写)、`POLLPRI`(高优先级数据可读,常用于中断)、`POLLERR`(错误)。判断用 `pfd.revents & POLLIN`。
+
+**追问**
+1. `events` 和 `revents` 有什么区别?(events 由调用者设置、poll 不改动;revents 由 poll 填写,表示实际发生的事件)
+2. 监测到可读后如果不读走数据会怎样?(就绪状态一直存在,下次 select/poll 仍立即返回,需读取以清除就绪态)
+
+### Q1.4 什么是异步 I/O(信号驱动 I/O)?如何使能?
+
+**答案要点**:异步 I/O 让内核在 fd 可执行 I/O 时主动发信号通知进程,进程无需阻塞也无需轮询。
+
+**详细解答**:通过 `fcntl` 添加 `O_ASYNC`(注意:不能在 `open()` 时直接指定使能),并设置属主进程与通知信号:
+
+```c
+int flag = fcntl(fd, F_GETFL);
+flag |= O_ASYNC;
+fcntl(fd, F_SETFL, flag);            // 使能异步 I/O
+fcntl(fd, F_SETOWN, getpid());       // 设置异步 I/O 所有者
+fcntl(fd, F_SETSIG, SIGRTMIN);       // 指定通知信号(可选)
+
+struct sigaction sa;
+sa.sa_sigaction = handler;
+sa.sa_flags = SA_SIGINFO;            // handler 可拿到 siginfo_t
+sigemptyset(&sa.sa_mask);
+sigaction(SIGIO, &sa, NULL);         // 注册信号处理函数
+```
+
+默认通知信号是 `SIGIO`,处理函数可读取 `siginfo_t.si_code`(与 poll 的 revents 一致)、`si_band`。串口例程 `uart_test.c` 正是用 `SIGRTMIN` + `SA_SIGINFO` 读取数据。
+
+**追问**
+1. 异步 I/O 相比 select/poll 的优势?(select/poll 内部是轮询,fd 很多时性能低;异步 I/O 由内核主动通知,适合大量 fd)
+2. fd 很多时更好的选择是什么?(epoll,性能类似甚至优于异步 I/O)
+
+### Q1.5 `mmap()` 的存储映射 I/O 有什么作用?`MAP_SHARED` 与 `MAP_PRIVATE` 有何区别?
+
+**答案要点**:`mmap()` 把文件(或设备)映射到进程地址空间,读写映射区即读写文件,省去 `read/write` 的数据拷贝。
+
+**详细解答**:原型 `void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)`。
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `addr` | 期望映射地址,通常传 `NULL` 由内核选择 |
+| `length` | 映射长度(字节),内核按页对齐 |
+| `prot` | `PROT_READ`/`PROT_WRITE`/`PROT_EXEC`/`PROT_NONE` |
+| `flags` | `MAP_SHARED`(修改写回文件、进程间共享)或 `MAP_PRIVATE`(写时复制、修改不写回) |
+| `fd`/`offset` | 被映射文件描述符与偏移(通常要求页对齐) |
+
+解除映射用 `int munmap(void *addr, size_t length)`。映射失效会触发 `SIGSEGV`(写只读映射)或 `SIGBUS`(访问已不存在的部分)。LCD 显存、摄像头帧缓冲都靠 mmap 实现"零拷贝"。
+
+**追问**
+1. `msync()` 和 `mprotect()` 有什么用?(`msync` 把映射区修改同步到文件/磁盘;`mprotect` 修改映射区保护权限)
+2. 文件锁 `flock()` 与 `fcntl()` 锁有何区别?(`flock` 是 BSD 风格的整文件锁;`fcntl` 支持记录锁/字节范围锁,功能更强)
+
+---
+
+## 二、GPIO 与 LED(5 题)
+
+### Q2.1 应用层操控底层硬件有哪两种方式?什么是 sysfs?
+
+**答案要点**:通过 `/dev` 设备节点,或通过 `/sys` 下的属性文件;sysfs 是一个基于内存的虚拟文件系统,把内核对象、属性、关系以目录/文件/链接形式导出到用户空间。
+
+**详细解答**:Linux"一切皆文件"。设备文件是硬件向应用层提供的接口,在 `/dev` 下,通过 `open/read/write/ioctl` 操作。另一种是 sysfs:内核对象 → 目录,对象属性 → 文件,对象关系 → 链接。读取属性文件即读设备属性,写属性文件即设置设备状态。
+
+| /sys 子目录 | 作用 |
+| ----------- | ---- |
+| `/sys/devices` | 所有设备,最重要 |
+| `/sys/bus` | 按总线类型分类 |
+| `/sys/class` | 按功能分类(`/sys/class/leds`、`/sys/class/gpio`、`/sys/class/input`、`/sys/class/pwm`) |
+| `/sys/dev` | 按设备号(major:minor)分类 |
+
+简单设备(LED、GPIO)多用 sysfs;复杂设备(LCD、触摸屏、摄像头)多用设备节点。sysfs 属性文件读写的是**字符串(ASCII)**。
+
+**追问**
+1. 通常哪些设备用设备节点方式?(较复杂的设备,如 LCD、触摸屏、摄像头)
+2. 杂项设备(misc)为什么叫"杂散"?(无法归入既有设备分类的外设,接口通常非标准化)
+
+### Q2.2 LED 的 sysfs 属性文件有哪些?trigger 有哪些触发模式?
+
+**答案要点**:`/sys/class/leds/sys-led/` 下关注 `brightness`、`max_brightness`、`trigger`。
+
+**详细解答**:
+
+| 属性文件 | 读写 | 说明 |
+| -------- | ---- | ---- |
+| `brightness` | 可读可写 | 设置/获取亮度,0 灭、正整数亮(PWM LED 有等级,GPIO LED 只有亮灭) |
+| `max_brightness` | 只读 | 最大亮度等级 |
+| `trigger` | 可读可写 | 触发模式,读可获取支持的所有模式及当前模式(方括号内) |
+
+常用触发模式:`none`(无触发)、`mmc0`(读写 mmc0 时闪烁)、`timer`(定时器规律亮灭)、`heartbeat`(心跳呼吸)。
+
+```bash
+echo none > trigger      # 设为无触发
+echo 1 > brightness      # 点亮
+echo 0 > brightness      # 熄灭
+```
+
+控制 LED 的程序需先写 `trigger` 为 `none`,再写 `brightness`。
+
+**追问**
+1. 为什么 GPIO 控制的 LED 没有亮度等级?(只能输出高低电平,不存在占空比)
+2. 应用程序向属性文件写入的数据格式是什么?(字符串,如 `"1"`,不能直接写二进制整数)
+
+### Q2.3 GPIO 的 sysfs 编号如何计算?`export`/`unexport` 有什么用?
+
+**答案要点**:编号 = 该 GPIO 组对应 gpiochip 的 base + 组内序号;`export` 导出引脚以生成对应的 `gpioX` 目录,`unexport` 删除。
+
+**详细解答**:I.MX6UL/I.MX6ULL 有 5 个 GPIO 控制器,对应 `gpiochip0/32/64/96/128`。每个 gpiochip 目录下 `base`(最小编号)、`label`(名字)、`ngpio`(引脚数)均只读。
+
+以 GPIO4_IO16 为例:GPIO4 → gpiochip96,base=96,故编号 = 96 + 16 = 112;GPIO3_IO20 = 64 + 20 = 84。
+
+```bash
+echo 112 > /sys/class/gpio/export    # 导出,生成 gpio112 目录
+echo 112 > /sys/class/gpio/unexport  # 删除
+```
+
+若引脚已被内核驱动占用,则导出失败。导出后 `gpioX` 目录下关注 `direction`、`value`、`active_low`、`edge`。
+
+**追问**
+1. `active_low` 有什么用?(控制极性:=1 时 `value` 写 1 输出低、写 0 输出高)
+2. 为什么有些 GPIO 导出失败?(已被内核中的驱动使用,无法再由 sysfs 接管)
+
+### Q2.4 如何用 sysfs 控制 GPIO 输出和读取输入?
+
+**答案要点**:写 `direction` 为 `out`/`in`,输出写 `value`,输入读 `value`。
+
+**详细解答**:输出示例:
+
+```c
+/* 判断是否已导出,未导出则写 export */
+if (access("/sys/class/gpio/gpio1", F_OK)) {
+    int fd = open("/sys/class/gpio/export", O_WRONLY);
+    write(fd, "1", 1);
+    close(fd);
+}
+/* direction=out, active_low=0, value 由参数决定 */
+fprintf(fd, "out");
+```
+
+输入时写 `direction=in`、`edge=none`,再以 `O_RDONLY` 打开 `value` 读取 1 字节。注意:**只有输入模式下才能用 `cat value` 读电平**,输出模式读不到实际电平。
+
+**追问**
+1. GPIO 输出模式下如何验证电平?(外接 LED 或用万用表测量)
+2. `access(path, F_OK)` 的作用?(判断文件/目录是否存在,用于决定是否需要导出)
+
+### Q2.5 GPIO 中断在应用层如何实现?为什么用 `poll()` 而不是直接 `read()`?
+
+**答案要点**:写 `edge` 为 `rising`/`falling`/`both` 配置边沿触发,再用 `poll()` 监听 `value` 的 `POLLPRI` 事件。
+
+**详细解答**:
+
+```c
+/* direction=in, active_low=0, edge=both */
+pfd.fd = open("/sys/class/gpio/gpio1/value", O_RDONLY);
+pfd.events = POLLPRI;          // 只关心高优先级数据可读(中断)
+read(pfd.fd, &val, 1);         // 先读一次清除状态
+for (;;) {
+    ret = poll(&pfd, 1, -1);
+    if (ret > 0 && (pfd.revents & POLLPRI)) {
+        lseek(pfd.fd, 0, SEEK_SET);   // 读位置移回头部
+        read(pfd.fd, &val, 1);
+        printf("GPIO 中断触发 value=%c\n", val);
+    }
+}
+```
+
+`POLLPRI` 表示有高优先级数据可读,中断即高优先级事件。直接 `read()` 在无中断时会阻塞、且难以区分事件;`poll()` 可同时监视多个 fd 的 I/O 状态变化,也可改用异步 I/O。
+
+**追问**
+1. 进入循环前为什么要先 `read()` 一次?(清除可能存在的旧状态,避免 poll 立即返回误报)
+2. `edge` 配置前需要什么前提?(引脚必须配置为输入模式)
+
+---
+
+## 三、输入设备与 tslib(5 题)
+
+### Q3.1 input 子系统的设备节点在哪里?`read()` 一次读到什么?
+
+**答案要点**:输入设备节点在 `/dev/input/eventX`;每次 `read()` 读到一个 `struct input_event` 结构体。
+
+**详细解答**:输入类设备(按键、键盘、鼠标、触摸屏)由 input 子系统统一管理,设备节点在 `/dev/input/` 下,如 `event0`、`event1`。每次读操作获得一个:
+
+```c
+struct input_event {
+    struct timeval time;   /* 时间戳 */
+    __u16 type;            /* 事件类型 */
+    __u16 code;            /* 具体事件 */
+    __s32 value;           /* 事件值 */
+};
+```
+
+其中 `type`、`code`、`value` 是解析的核心。
+
+**追问**
+1. 输入设备节点在 `/sys` 下如何分类?(`/sys/class/input`)
+2. 一次 `read()` 能读多个事件吗?(通常只读一个;可按结构体大小循环读)
+
+### Q3.2 `input_event` 的 `type`、`code`、`value` 分别怎么理解?
+
+**答案要点**:type 是事件分类,code 是分类下的具体事件,value 的解释随 code 而定。
+
+**详细解答**:
+
+| type | 含义 |
+| ---- | ---- |
+| `EV_SYN`(0x00) | 同步类事件 |
+| `EV_KEY`(0x01) | 按键类事件 |
+| `EV_REL`(0x02) | 相对位移类(鼠标) |
+| `EV_ABS`(0x03) | 绝对位移类(触摸屏) |
+
+- `code` 指定具体事件,如按键 `KEY_A`、`KEY_VOLUMEDOWN`,绝对位移 `ABS_X`、`ABS_Y`、`ABS_MT_SLOT` 等。
+- `value` 随 code 变化:按键事件中 value=1 按下、0 松开、2 长按;绝对位移中 value 即坐标;`ABS_MT_TRACKING_ID` 的 value≥0 表示有效触摸点、-1 表示点被销毁。
+
+所有输入设备都要上报同步事件(通常 `SYN_REPORT`,value=0),告知本轮数据已完整。
+
+**追问**
+1. `value=2` 表示什么?哪些事件不支持?(长按;`BTN_TOUCH` 不支持长按,故 value 不为 2)
+2. 为什么要上报 `EV_SYN`?(一次触摸会产生多个事件,靠 SYN_REPORT 标记一组数据完整)
+
+### Q3.3 单点触摸屏的数据如何解析出坐标?
+
+**答案要点**:读取 `BTN_TOUCH` 判断按下/松开,读取 `ABS_X`/`ABS_Y` 得到坐标,`EV_SYN` 表示一帧结束。
+
+**详细解答**:
+
+```c
+switch (in_ev.type) {
+case EV_ABS:
+    switch (in_ev.code) {
+    case ABS_X: x = in_ev.value; break;
+    case ABS_Y: y = in_ev.value; break;
+    case ABS_MT_TRACKING_ID:
+        if (0 == in_ev.value) down = 1;        /* 按下 */
+        else if (-1 == in_ev.value) down = 0;  /* 松开 */
+        break;
+    }
+    break;
+case EV_KEY:
+    if (BTN_TOUCH == in_ev.code) down = in_ev.value;   /* 1 按下 0 松开 */
+    break;
+case EV_SYN:
+    if (SYN_REPORT == in_ev.code) { /* 打印/使用 x、y */ }
+    break;
+}
+```
+
+手指点击时先上报 `BTN_TOUCH`(value=1),再上报 `ABS_X`/`ABS_Y`,最后 `SYN_REPORT`;滑动时坐标不断更新但不重复上报 `BTN_TOUCH`。
+
+**追问**
+1. 为什么不能只靠 `ABS_X`/`ABS_Y` 判断按下?(需配合 BTN_TOUCH 或 TRACKING_ID 判断触摸状态)
+2. 单点触摸需要处理 slot 吗?(不需要,多点才涉及 slot)
+
+### Q3.4 多点触摸如何解析?如何获取最大触摸点数?
+
+**答案要点**:用 `ABS_MT_SLOT` 区分触摸点,`ABS_MT_TRACKING_ID` 判断点的新建/销毁,`ABS_MT_POSITION_X/Y` 取坐标;用 `EVIOCGABS(ABS_MT_SLOT)` 获取最大触摸点数。
+
+**详细解答**:
+
+```c
+struct input_absinfo slot;
+ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot);
+max_slots = slot.maximum + 1 - slot.minimum;
+
+switch (in_ev.type) {
+case EV_ABS:
+    switch (in_ev.code) {
+    case ABS_MT_SLOT:            slot = in_ev.value; break;
+    case ABS_MT_POSITION_X:      xy[slot].x = in_ev.value; break;
+    case ABS_MT_POSITION_Y:      xy[slot].y = in_ev.value; break;
+    case ABS_MT_TRACKING_ID:     mt[slot].id = in_ev.value; break;   /* -1 表示销毁 */
+    }
+    break;
+case EV_SYN:
+    if (SYN_REPORT == in_ev.code) { /* 一帧完整,处理所有 slot */ }
+    break;
+}
+```
+
+新增点流程:`ABS_MT_TRACKING_ID`(ID≥0) → `ABS_MT_POSITION_X` → `ABS_MT_POSITION_Y` → `BTN_TOUCH`(1) → `ABS_X`/`ABS_Y` → `SYN_REPORT`;松开时上报 `ABS_MT_TRACKING_ID`=-1。
+
+**追问**
+1. `input_absinfo` 里有哪些字段?(`value` 最新值、`minimum`/`maximum` 范围、`fuzz`、`flat`、`resolution`)
+2. 为什么有的屏幕没有 `BTN_TOUCH`?(触摸芯片不同,可能只上报 ABS 事件)
+
+### Q3.5 tslib 是什么?它的编程流程与关键函数?
+
+**答案要点**:tslib 是专为触摸屏开发的开源应用层函数库,封装了原始 input 解析、去噪去抖、坐标变换,把原始坐标转换为屏幕坐标。
+
+**详细解答**:tslib 作为触摸屏驱动与应用层之间的适配层,配置文件 `ts.conf`(`module_raw input`、`module pthres`、`module dejitter`、`module linear` 等),也是 Qt 常用的触摸输入插件。
+
+编程三步:
+
+```c
+struct tsdev *ts = ts_setup(NULL, 0);   /* 打开+配置;NULL 则读 TSLIB_TSDEVICE */
+struct ts_sample samp;
+ts_read(ts, &samp, 1);                   /* 单点 */
+/* 多点:ts_read_mt(ts, &mt_ptr, max_slots, 1); */
+ts_close(ts);
+```
+
+| 函数 | 作用 |
+| ---- | ---- |
+| `ts_open(dev, nonblock)` | 打开触摸屏设备 |
+| `ts_setup(dev, nonblock)` | 打开并配置(内部读环境变量、解析 ts.conf) |
+| `ts_config(ts)` | 解析 ts.conf、加载插件 |
+| `ts_read(ts, &samp, nr)` | 读单点触摸 |
+| `ts_read_mt(ts, &samp, max_slots, nr)` | 读多点触摸 |
+| `ts_close(ts)` | 关闭 |
+
+关键环境变量:`TSLIB_TSDEVICE`、`TSLIB_CONFFILE`、`TSLIB_PLUGINDIR`、`TSLIB_FBDEVICE`。编译:`${CC} -I .../tslib/include -L .../tslib/lib -lts -o testApp testApp.c`。
+
+**追问**
+1. `ts_setup` 相比 `ts_open` 多了什么?(额外完成了设备配置、插件加载)
+2. tslib 提供了哪些测试工具?(`ts_print`、`ts_print_mt`、`ts_test`、`ts_test_mt`)
+
+---
+
+## 四、FrameBuffer 与 LCD(5 题)
+
+### Q4.1 FrameBuffer 是什么?操作 `/dev/fbX` 的核心 ioctl 与结构体有哪些?
+
+**答案要点**:FrameBuffer 是内核为显示设备抽象出的帧缓冲,`/dev/fbX` 是设备节点;核心 ioctl 为 `FBIOGET_VSCREENINFO`、`FBIOPUT_VSCREENINFO`、`FBIOGET_FSCREENINFO`。
+
+**详细解答**:应用层操作 `/dev/fbX` 五步:打开 → `FBIOGET_VSCREENINFO` 取可变参数 → `FBIOGET_FSCREENINFO` 取固定参数 → `mmap` 映射显存 → 读写显存。
+
+| ioctl | 第三个参数 | 说明 |
+| ----- | ---------- | ---- |
+| `FBIOGET_VSCREENINFO`(0x4600) | `struct fb_var_screeninfo *` | 获取可变参数信息 |
+| `FBIOPUT_VSCREENINFO` | `struct fb_var_screeninfo *` | 设置可变参数 |
+| `FBIOGET_FSCREENINFO`(0x4602) | `struct fb_fix_screeninfo *` | 获取固定参数信息 |
+
+`fb_var_screeninfo` 关键字段:`xres`/`yres`(可视区 X/Y 分辨率)、`xres_virtual`/`yres_virtual`、`bits_per_pixel`(bpp)、`red/green/blue`(`struct fb_bitfield`,含 `offset`/`length`)。`fb_fix_screeninfo` 关键字段:`smem_len`(显存长度)、`line_length`(一行字节数)。显存大小可用 `xres * yres * bpp / 8` 或 `line_length * yres`。
+
+**追问**
+1. `fb_var_screeninfo` 与 `fb_fix_screeninfo` 有何区别?(前者可修改的运行参数,后者驱动程序设定、只读的固定参数)
+2. 为什么显存大小通常用 `line_length * yres`?(可能存在行填充,`line_length` 才是真实一行字节数)
+
+### Q4.2 为什么显示编程用 `mmap` 而不是 `read/write`?显存地址如何定位?
+
+**答案要点**:`mmap` 把显存映射到用户空间,直接写映射区即写显存,无需每次 `write` 系统调用的数据拷贝,速度快。
+
+**详细解答**:显示需要频繁刷新整屏或局部区域,用 `write()` 会把像素数据从用户缓冲拷到内核再写显存;`mmap` 后应用直接操作显存,即"零拷贝"。
+
+定位公式:显存起始地址 `base`,每像素 `pix_bytes = bpp/8`,则坐标 `(x, y)` 的地址:
+
+```c
+unsigned short *p = (unsigned short *)base + y * (line_length / 2) + x;
+```
+
+若 `line_length == xres * pix_bytes`,可简化为 `base + (y * xres + x) * pix_bytes`。
+
+**追问**
+1. LCD 横屏转竖屏为什么是应用层的事?(硬件坐标固定,竖屏只是应用"假装"的坐标系,需换算显存地址)
+2. 竖屏 `(x, y)` 对应横屏物理坐标是什么?(如 `(x, 480 - 1 - y)` 等,取决于旋转方向)
+
+### Q4.3 RGB888 如何转 RGB565?像素格式不对会有什么后果?
+
+**答案要点**:分别取 R、G、B 的高位拼成 16 位:`(r & 0xF8) << 8 | (g & 0xFC) << 3 | (b & 0xF8) >> 3`。
+
+**详细解答**:
+
+```c
+#define argb8888_to_rgb565(color) ({ \
+    unsigned int r = (color >> 16) & 0xFF; \
+    unsigned int g = (color >> 8)  & 0xFF; \
+    unsigned int b = (color)       & 0xFF; \
+    (unsigned short)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); \
+})
+```
+
+RGB565 用 5 位 R、6 位 G、5 位 B。若把 RGB888 数据直接写入 RGB565 显存,颜色会错乱(颜色分量错位、图像"花屏")。LCD 出厂系统通常是 RGB565。
+
+**追问**
+1. 为什么 G 分量多 1 位?(人眼对绿色更敏感)
+2. 如何判断屏幕像素格式?(读 `fb_var_screeninfo` 的 `bits_per_pixel` 与 `red/green/blue` 的 `offset`/`length`)
+
+### Q4.4 libjpeg 解码 JPEG 的流程是什么?为什么拿到的是 BGR888?
+
+**答案要点**:创建解码对象 → 设置数据源 → 读文件头 → 开始解码 → 逐行 `jpeg_read_scanlines` → 结束 → 销毁;libjpeg 默认输出 BGR888,需转 RGB565 显示。
+
+**详细解答**:
+
+```c
+struct jpeg_decompress_struct cinfo;
+struct jpeg_error_mgr jerr;
+cinfo.err = jpeg_std_error(&jerr);
+jpeg_create_decompress(&cinfo);
+jpeg_stdio_src(&cinfo, jpeg_file);
+jpeg_read_header(&cinfo, TRUE);
+cinfo.out_color_space = JCS_RGB;      // 默认即 JCS_RGB
+jpeg_start_decompress(&cinfo);
+while (cinfo.output_scanline < cinfo.output_height)
+    jpeg_read_scanlines(&cinfo, &row_buf, 1);   // 每次一行
+jpeg_finish_decompress(&cinfo);
+jpeg_destroy_decompress(&cinfo);
+```
+
+读到的数据在内存中按 B、G、R 字节序排列,故称 BGR888;转 RGB565 时需取对应字节。`cinfo.scale_num/scale_denom` 可缩放解码尺寸。
+
+**追问**
+1. libjpeg 的错误处理怎么设置?(`cinfo.err = jpeg_std_error(&jerr)`,可自定义 `error_exit` 回调)
+2. `jpeg_read_scanlines` 的第二个参数类型?(`unsigned char **`,逐行读取)
+
+### Q4.5 libpng 的 `setjmp/longjmp` 有什么作用?high-level 与 low-level 接口有何区别?
+
+**答案要点**:libpng 默认错误处理会 `longjmp` 跳转,需先用 `setjmp(png_jmpbuf(png_ptr))` 设立错误返回点以正确释放资源;high-level 接口一个函数解完整图,low-level 需逐步调用。
+
+**详细解答**:
+
+```c
+if (setjmp(png_jmpbuf(png_ptr))) {
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+    fclose(png_file);
+    return -1;
+}
+png_init_io(png_ptr, png_file);
+png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);  // high-level
+```
+
+| 接口 | 函数 | 特点 |
+| ---- | ---- | ---- |
+| high-level | `png_read_png()` | 一次性解码整图,内部自动分配缓冲,可用 `png_get_rows()` 取数据 |
+| low-level | `png_read_info`→`png_set_*`→`png_read_update_info`→`png_read_image` | 需用户自行分配行缓冲,可控性强 |
+
+libpng 依赖 zlib,编译需 `-lpng -lz`。
+
+**追问**
+1. `png_set_expand`、`png_set_strip_16`、`png_set_gray_to_rgb` 分别做什么?(位深扩展、16→8 位、灰度转 RGB)
+2. FreeType 中 `bearingY` 与 `advance` 的作用?(`bearingY` 用于确定字形顶部 y 坐标,`advance` 是下一个字符的水平步进)
+
+---
+
+## 五、PWM 与看门狗(5 题)
+
+### Q5.1 如何通过 sysfs 操作 PWM?涉及哪些属性文件?
+
+**答案要点**:`/sys/class/pwm/pwmchipX/` 下用 `export`/`unexport` 导出,生成 `pwm0` 目录,操作 `period`、`duty_cycle`、`enable`、`polarity`。
+
+**详细解答**:I.MX6U 有 8 个 PWM 控制器(`pwmchip0`~`pwmchip7`),每个控制器 1 路 PWM,编号都是 0。
+
+| 文件 | 读写 | 说明 |
+| ---- | ---- | ---- |
+| `npwm` | 只读 | 该控制器下 PWM 路数 |
+| `export` / `unexport` | 只写 | 导出/删除 PWM(写编号,从 0 开始) |
+| `period` | 可读写 | PWM 周期,单位 ns |
+| `duty_cycle` | 可读写 | 占空比,单位 ns |
+| `enable` | 可读写 | `1` 使能、`0` 禁止 |
+| `polarity` | 可读写 | `normal`/`inversed`(硬件不支持则无此接口) |
+
+```bash
+echo 0 > /sys/class/pwm/pwmchip0/export
+echo 500000 > /sys/class/pwm/pwmchip0/pwm0/period      # 500us
+echo 250000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle  # 50%
+echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
+```
+
+**追问**
+1. 上面配置的 PWM 频率是多少?(周期 500us → 频率 2KHz,占空比 50%)
+2. 为什么 I.MX6U 出厂系统用不了 PWM1?(PWM1 已被用作 LCD 背光,需在设备树禁用 LCD/backlight 才能释放)
+
+### Q5.2 PWM 的周期、占空比、极性如何换算?
+
+**答案要点**:`period` 是一个完整周期时长(ns),`duty_cycle` 是高电平时长(ns),占空比 = duty/period;`polarity` 决定高电平有效还是低电平有效。
+
+**详细解答**:频率 = 1e9 / period(ns)。例如 `period=500000ns` → 2KHz;`duty_cycle=250000ns` → 占空比 50%。`polarity=normal` 时占空比对应高电平时间,`inversed` 则反转。周期单位是 **ns**,写入的是**字符串数字**。
+
+**追问**
+1. `period=10000` 对应频率多少?(100KHz)
+2. 很多 SoC 不支持极性配置,表现在哪?(Sysfs 中无 `polarity` 接口,写会失败)
+
+### Q5.3 编写 PWM 应用程序需要哪些步骤?
+
+**答案要点**:判断是否导出 → 写 `export` → 配置 `period` → 配置 `duty_cycle` → 写 `enable=1`。
+
+**详细解答**:程序用 `access()` 判断 `pwm0` 目录是否存在,不存在则向 `export` 写 "0";随后用统一的 `pwm_config(attr, val)` 依次写 `period`、`duty_cycle`、`enable`。各属性文件都是字符串与 `write()` 交互。
+
+```c
+sprintf(pwm_path, "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
+if (access(pwm_path, F_OK)) { /* 打开 export,write("0",1) */ }
+pwm_config("period", argv[2]);
+pwm_config("duty_cycle", argv[3]);
+pwm_config("enable", "1");
+```
+
+测试 PWM1 时输出绑定到 `GPIO1_IO08`(LCD 背光引脚),可用示波器测波形。
+
+**追问**
+1. 配置 PWM 为什么要先 `enable=0` 再改参数?(周期/占空比存在约束关系,运行中改可能失败;部分驱动要求先停)
+2. 如何在不重编设备树的情况下临时释放 PWM1?(在 u-boot 里用 `fdt set` 把 backlight、lcdif 的 status 设为 disabled,仅本次启动有效)
+
+### Q5.4 看门狗的原理是什么?
+
+**答案要点**:看门狗是一个可被复位/重置的计数器;超时未喂狗则计数器溢出,对 CPU 产生复位信号使系统重启。
+
+**详细解答**:看门狗(WDOG)启动后自动计数,程序须在超时前"喂狗"(清零计数器)以免复位;一旦程序跑飞没按时喂狗,系统"被狗咬"复位。有些看门狗也能只产生中断而不复位。I.MX6UL/I.MX6ULL 集成 WDOG1(普通看门狗,支持中断与复位)与 WDOG2(用于安全目的)。
+
+Linux 中看门狗设备节点为 `/dev/watchdogX`,`/dev/watchdog` 是默认看门狗(通常即 `watchdog0`)。
+
+**追问**
+1. 看门狗如何提升系统可靠性?(程序异常/死锁时自动复位,避免系统永久挂死)
+2. 打开看门狗设备后即使没主动开启,关闭时会怎样?(`close()` 时看门狗会自动启动,因此打开后通常先用 `WDIOC_SETOPTIONS` 停止)
+
+### Q5.5 看门狗应用编程用哪些 ioctl?完整流程是怎样的?
+
+**答案要点**:需包含 `<linux/watchdog.h>`,常用 `WDIOC_GETSUPPORT`、`WDIOC_SETTIMEOUT`、`WDIOC_GETTIMEOUT`、`WDIOC_SETOPTIONS`、`WDIOC_KEEPALIVE`。
+
+**详细解答**:
+
+| ioctl | 参数 | 作用 |
+| ----- | ---- | ---- |
+| `WDIOC_GETSUPPORT` | `struct watchdog_info *` | 获取支持的功能(`options` 字段) |
+| `WDIOC_GETTIMEOUT` | `int *` | 获取当前超时时间(秒) |
+| `WDIOC_SETTIMEOUT` | `int *` | 设置超时时间(秒),不能超过最大值 |
+| `WDIOC_SETOPTIONS` | `int *` | `WDIOS_ENABLECARD` 开启 / `WDIOS_DISABLECARD` 停止 |
+| `WDIOC_KEEPALIVE` | `NULL` | 喂狗 |
+
+`watchdog_info.options` 常用标志:`WDIOF_SETTIMEOUT`(支持设置超时)、`WDIOF_KEEPALIVEPING`(支持喂狗)。流程:
+
+```c
+fd = open("/dev/watchdog", O_RDWR);
+int op = WDIOS_DISABLECARD;  ioctl(fd, WDIOC_SETOPTIONS, &op);   // 先停
+int timeout = 10;            ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
+op = WDIOS_ENABLECARD;       ioctl(fd, WDIOC_SETOPTIONS, &op);   // 再启
+for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
+```
+
+代码里打开设备后**立即停止**看门狗,全部设置完再开启;喂狗时间设在超时前(如超时前 100ms)。
+
+**追问**
+1. 测试时提示 `Device or resource busy` 怎么办?(看门狗已被系统服务占用,需注释掉 `/etc/init.d/watchdog.sh`)
+2. 程序被 `Ctrl+C` 终止后会怎样?(停止喂狗但看门狗仍在计时,超时后系统复位,内核打印 `watchdog did not stop!`)
+
+---
+
+## 六、摄像头 / 串口 / 音频(5 题)
+
+### Q6.1 V4L2 摄像头应用编程的完整流程是什么?
+
+**答案要点**:打开设备 → QUERYCAP → ENUM_FMT/S_FMT(含帧率)→ REQBUFS → QUERYBUF+mmap → QBUF → STREAMON → 循环 DQBUF/处理/QBUF → STREAMOFF。
+
+**详细解答**:
+
+| 步骤 | ioctl / 操作 |
+| ---- | ------------ |
+| 打开 | `open("/dev/video0", O_RDWR)` |
+| 查询能力 | `VIDIOC_QUERYCAP`,校验含 `V4L2_CAP_VIDEO_CAPTURE` |
+| 枚举格式 | `VIDIOC_ENUM_FMT`、`VIDIOC_ENUM_FRAMESIZES`、`VIDIOC_ENUM_FRAMEINTERVALS` |
+| 设置格式 | `VIDIOC_S_FMT`(回读确认),`VIDIOC_G_PARM`/`VIDIOC_S_PARM` 设帧率 |
+| 申请缓冲 | `VIDIOC_REQBUFS`(`V4L2_MEMORY_MMAP`) |
+| 映射 | `VIDIOC_QUERYBUF` + `mmap` |
+| 入队 | `VIDIOC_QBUF` |
+| 开采集 | `VIDIOC_STREAMON` |
+| 取帧 | 循环 `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` |
+| 结束 | `VIDIOC_STREAMOFF` |
+
+**追问**
+1. 为什么 `VIDIOC_S_FMT` 后必须回读?(驱动会按硬件能力修改参数,实际值未必等于请求值)
+2. 设备能力里 `V4L2_CAP_STREAMING` 与 `V4L2_CAP_READWRITE` 区别?(前者支持流式 I/O(缓冲队列),后者支持 read/write 方式)
+
+### Q6.2 V4L2 的关键结构体有哪些?各自对应哪条指令?
+
+**答案要点**:`v4l2_capability`、`v4l2_fmtdesc`、`v4l2_frmsizeenum`、`v4l2_frmivalenum`、`v4l2_format`、`v4l2_streamparm`、`v4l2_requestbuffers`、`v4l2_buffer`。
+
+**详细解答**:
+
+| 结构体 | 对应指令 | 关键字段 |
+| ------ | -------- | -------- |
+| `v4l2_capability` | `VIDIOC_QUERYCAP` | `capabilities` |
+| `v4l2_fmtdesc` | `VIDIOC_ENUM_FMT` | `index`、`pixelformat`、`description`、`type` |
+| `v4l2_frmsizeenum` | `VIDIOC_ENUM_FRAMESIZES` | `index`、`pixel_format`、`discrete.width/height` |
+| `v4l2_frmivalenum` | `VIDIOC_ENUM_FRAMEINTERVALS` | `index`、`width/height`、`discrete.numerator/denominator` |
+| `v4l2_format` | `VIDIOC_G_FMT`/`S_FMT` | `type`、`fmt.pix.width/height/pixelformat` |
+| `v4l2_streamparm` | `VIDIOC_G_PARM`/`S_PARM` | `parm.capture.capability`、`timeperframe` |
+| `v4l2_requestbuffers` | `VIDIOC_REQBUFS` | `count`、`type`、`memory` |
+| `v4l2_buffer` | `QUERYBUF`/`QBUF`/`DQBUF` | `index`、`type`、`memory`、`length`、`m.offset` |
+
+`v4l2_fract` 中 `numerator/denominator` 表示采集周期,帧率 = `denominator/numerator`。`pixelformat` 由 `v4l2_fourcc` 合成,如 `V4L2_PIX_FMT_RGB565`、`V4L2_PIX_FMT_YUYV`。
+
+**追问**
+1. `v4l2_format` 是 union,`VIDEO_CAPTURE` 时哪个成员生效?(`fmt.pix`)
+2. `V4L2_BUF_TYPE_VIDEO_CAPTURE` 与 `..._MPLANE` 区别?(后者用于多平面格式,`fmt.pix_mp` 生效)
+
+### Q6.3 为什么 V4L2 采集要用"入队/出队"?应用如何拿到图像数据?
+
+**答案要点**:内核维护帧缓冲队列,应用把空闲缓冲入队交给驱动、驱动填满后应用出队取走,mmap 后应用直接读映射区,实现零拷贝。
+
+**详细解答**:`VIDIOC_QBUF`(入队)把缓冲交给驱动,驱动采集一帧填入后缓冲变"满";`VIDIOC_DQBUF`(出队)取走满缓冲,处理完再入队,往复循环。`v4l2_buffer` 的 `index` 标识缓冲编号,`m.offset` 是在内核申请的整块内存中的偏移,配合 `mmap(fd, m.offset)` 得到用户空间地址。处理时"出队→读映射区→处理→入队"。
+
+**追问**
+1. 帧缓冲数量为什么不能太多?(嵌入式内存紧张,缓冲多会占用过多系统内存;太少又可能丢帧)
+2. USB 摄像头为什么常不能直接设为 RGB565?(UVC 标准通常输出 YUYV/MJPEG,需应用自行转换)
+
+### Q6.4 termios 的 `struct termios` 各字段作用?如何配置串口?
+
+**答案要点**:`c_iflag` 输入处理、`c_oflag` 输出处理、`c_cflag` 硬件特性、`c_lflag` 本地模式、`c_cc` 特殊字符;配置顺序为 cfmakeraw → 波特率 → 数据位 → 校验 → 停止位 → MIN/TIME → tcsetattr。
+
+**详细解答**:
+
+| 字段 | 作用 |
+| ---- | ---- |
+| `c_iflag` | 输入模式(IGNBRK、IGNPAR、INPCK、ICRNL、IXON 等) |
+| `c_oflag` | 输出模式(OPOST、ONLCR 等) |
+| `c_cflag` | 控制模式(波特率、CSIZE/CS5~CS8、CSTOPB、CREAD、PARENB、PARODD、CRTSCTS) |
+| `c_lflag` | 本地模式(ICANON、ECHO、ISIG 等) |
+| `c_cc[]` | 特殊字符(VMIN、VTIME、VINTR、VEOF 等) |
+
+必须用"按位与/或"增删标志,不能整体赋值。终端三种模式:规范、非规范、原始;串口与传感器做二进制通信时用**原始模式**(`cfmakeraw`)。`VMIN`/`TIME` 仅对非规范/原始模式有效:都置 0 则 `read()` 立即返回。
+
+**追问**
+1. 串口为什么常用原始模式?(二进制数据不应被终端解析成 ASCII 或做特殊处理)
+2. `tcsetattr` 的 `TCSANOW`/`TCSADRAIN`/`TCSAFLUSH` 区别?(立即生效 / 输出传完生效 / 生效前丢弃未读输入)
+
+### Q6.5 ALSA 中 frame/period/buffer 的关系?如何用 alsa-lib 播放和录音?
+
+**答案要点**:frame = 样本长度 × 声道数;period 是设备读写单位(若干帧);buffer 由若干 period 组成。播放用 `snd_pcm_writei`,录音用 `snd_pcm_readi`。
+
+**详细解答**:以 16bit 双声道为例,一帧 4 字节;`period_size=1024` 帧,`periods=16` 则 buffer 16×1024 帧。拆分 period 是为平衡 DMA 中断开销与延迟。
+
+编程流程:`snd_pcm_open`(`"hw:0,0"` + PLAYBACK/CAPTURE)→ `snd_pcm_hw_params_malloc` → `snd_pcm_hw_params_any` → 依次 `set_access`(RW_INTERLEAVED)、`set_format`(S16_LE)、`set_rate`、`set_channels`、`set_period_size`、`set_periods` → `snd_pcm_hw_params` 生效 → 循环 `snd_pcm_writei`/`snd_pcm_readi` → `snd_pcm_drain`/`drop` + `snd_pcm_close`。
+
+`snd_pcm_writei/readi` 返回**实际帧数**,失败返回负错误码(`-EPIPE`=XRUN、`-EBADFD`=状态错、`-ESTRPIPE`=挂起);`snd_pcm_hw_params()` 内部会调 `snd_pcm_prepare()`,设备进入 `SND_PCM_STATE_PREPARED`。混音器用 `snd_mixer_open/attach/selem_register/load` 后遍历元素,用 `snd_mixer_selem_set_playback_volume_all` 调音量。
+
+**追问**
+1. overrun 与 underrun 分别指什么?(录音应用读太慢导致数据被覆盖=overrun;播放应用写太慢导致缓冲饿死=underrun,统称 XRUN)
+2. 为什么示例没有调用 `snd_pcm_start()`?(PREPARED 状态下首次 `writei/readi` 会自动调用 `snd_pcm_start()`)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I_O,第十五~二十八章 外设应用编程;配套例程源码 `11、Linux C应用编程例程源码/`。