面试-外设与高级IO编程.md 40 KB


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

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

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. timeoutNULL0、非 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)

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. eventsrevents 有什么区别?(events 由调用者设置、poll 不改动;revents 由 poll 填写,表示实际发生的事件)
  2. 监测到可读后如果不读走数据会怎样?(就绪状态一直存在,下次 select/poll 仍立即返回,需读取以清除就绪态)

Q1.4 什么是异步 I/O(信号驱动 I/O)?如何使能?

答案要点:异步 I/O 让内核在 fd 可执行 I/O 时主动发信号通知进程,进程无需阻塞也无需轮询。

详细解答:通过 fcntl 添加 O_ASYNC(注意:不能在 open() 时直接指定使能),并设置属主进程与通知信号:

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_SHAREDMAP_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/ 下关注 brightnessmax_brightnesstrigger

详细解答

属性文件 读写 说明
brightness 可读可写 设置/获取亮度,0 灭、正整数亮(PWM LED 有等级,GPIO LED 只有亮灭)
max_brightness 只读 最大亮度等级
trigger 可读可写 触发模式,读可获取支持的所有模式及当前模式(方括号内)

常用触发模式:none(无触发)、mmc0(读写 mmc0 时闪烁)、timer(定时器规律亮灭)、heartbeat(心跳呼吸)。

echo none > trigger      # 设为无触发
echo 1 > brightness      # 点亮
echo 0 > brightness      # 熄灭

控制 LED 的程序需先写 triggernone,再写 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。

echo 112 > /sys/class/gpio/export    # 导出,生成 gpio112 目录
echo 112 > /sys/class/gpio/unexport  # 删除

若引脚已被内核驱动占用,则导出失败。导出后 gpioX 目录下关注 directionvalueactive_lowedge

追问

  1. active_low 有什么用?(控制极性:=1 时 value 写 1 输出低、写 0 输出高)
  2. 为什么有些 GPIO 导出失败?(已被内核中的驱动使用,无法再由 sysfs 接管)

Q2.4 如何用 sysfs 控制 GPIO 输出和读取输入?

答案要点:写 directionout/in,输出写 value,输入读 value

详细解答:输出示例:

/* 判断是否已导出,未导出则写 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=inedge=none,再以 O_RDONLY 打开 value 读取 1 字节。注意:只有输入模式下才能用 cat value 读电平,输出模式读不到实际电平。

追问

  1. GPIO 输出模式下如何验证电平?(外接 LED 或用万用表测量)
  2. access(path, F_OK) 的作用?(判断文件/目录是否存在,用于决定是否需要导出)

Q2.5 GPIO 中断在应用层如何实现?为什么用 poll() 而不是直接 read()

答案要点:写 edgerising/falling/both 配置边沿触发,再用 poll() 监听 valuePOLLPRI 事件。

详细解答

/* 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/ 下,如 event0event1。每次读操作获得一个:

struct input_event {
    struct timeval time;   /* 时间戳 */
    __u16 type;            /* 事件类型 */
    __u16 code;            /* 具体事件 */
    __s32 value;           /* 事件值 */
};

其中 typecodevalue 是解析的核心。

追问

  1. 输入设备节点在 /sys 下如何分类?(/sys/class/input
  2. 一次 read() 能读多个事件吗?(通常只读一个;可按结构体大小循环读)

Q3.2 input_eventtypecodevalue 分别怎么理解?

答案要点:type 是事件分类,code 是分类下的具体事件,value 的解释随 code 而定。

详细解答

type 含义
EV_SYN(0x00) 同步类事件
EV_KEY(0x01) 按键类事件
EV_REL(0x02) 相对位移类(鼠标)
EV_ABS(0x03) 绝对位移类(触摸屏)
  • code 指定具体事件,如按键 KEY_AKEY_VOLUMEDOWN,绝对位移 ABS_XABS_YABS_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 表示一帧结束。

详细解答

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) 获取最大触摸点数。

详细解答

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_XABS_MT_POSITION_YBTN_TOUCH(1) → ABS_X/ABS_YSYN_REPORT;松开时上报 ABS_MT_TRACKING_ID=-1。

追问

  1. input_absinfo 里有哪些字段?(value 最新值、minimum/maximum 范围、fuzzflatresolution
  2. 为什么有的屏幕没有 BTN_TOUCH?(触摸芯片不同,可能只上报 ABS 事件)

Q3.5 tslib 是什么?它的编程流程与关键函数?

答案要点:tslib 是专为触摸屏开发的开源应用层函数库,封装了原始 input 解析、去噪去抖、坐标变换,把原始坐标转换为屏幕坐标。

详细解答:tslib 作为触摸屏驱动与应用层之间的适配层,配置文件 ts.confmodule_raw inputmodule pthresmodule dejittermodule linear 等),也是 Qt 常用的触摸输入插件。

编程三步:

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_TSDEVICETSLIB_CONFFILETSLIB_PLUGINDIRTSLIB_FBDEVICE。编译:${CC} -I .../tslib/include -L .../tslib/lib -lts -o testApp testApp.c

追问

  1. ts_setup 相比 ts_open 多了什么?(额外完成了设备配置、插件加载)
  2. tslib 提供了哪些测试工具?(ts_printts_print_mtts_testts_test_mt

四、FrameBuffer 与 LCD(5 题)

Q4.1 FrameBuffer 是什么?操作 /dev/fbX 的核心 ioctl 与结构体有哪些?

答案要点:FrameBuffer 是内核为显示设备抽象出的帧缓冲,/dev/fbX 是设备节点;核心 ioctl 为 FBIOGET_VSCREENINFOFBIOPUT_VSCREENINFOFBIOGET_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_virtualbits_per_pixel(bpp)、red/green/bluestruct fb_bitfield,含 offset/length)。fb_fix_screeninfo 关键字段:smem_len(显存长度)、line_length(一行字节数)。显存大小可用 xres * yres * bpp / 8line_length * yres

追问

  1. fb_var_screeninfofb_fix_screeninfo 有何区别?(前者可修改的运行参数,后者驱动程序设定、只读的固定参数)
  2. 为什么显存大小通常用 line_length * yres?(可能存在行填充,line_length 才是真实一行字节数)

Q4.2 为什么显示编程用 mmap 而不是 read/write?显存地址如何定位?

答案要点mmap 把显存映射到用户空间,直接写映射区即写显存,无需每次 write 系统调用的数据拷贝,速度快。

详细解答:显示需要频繁刷新整屏或局部区域,用 write() 会把像素数据从用户缓冲拷到内核再写显存;mmap 后应用直接操作显存,即"零拷贝"。

定位公式:显存起始地址 base,每像素 pix_bytes = bpp/8,则坐标 (x, y) 的地址:

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

详细解答

#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_screeninfobits_per_pixelred/green/blueoffset/length

Q4.4 libjpeg 解码 JPEG 的流程是什么?为什么拿到的是 BGR888?

答案要点:创建解码对象 → 设置数据源 → 读文件头 → 开始解码 → 逐行 jpeg_read_scanlines → 结束 → 销毁;libjpeg 默认输出 BGR888,需转 RGB565 显示。

详细解答

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 需逐步调用。

详细解答

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_infopng_set_*png_read_update_infopng_read_image 需用户自行分配行缓冲,可控性强

libpng 依赖 zlib,编译需 -lpng -lz

追问

  1. png_set_expandpng_set_strip_16png_set_gray_to_rgb 分别做什么?(位深扩展、16→8 位、灰度转 RGB)
  2. FreeType 中 bearingYadvance 的作用?(bearingY 用于确定字形顶部 y 坐标,advance 是下一个字符的水平步进)

五、PWM 与看门狗(5 题)

Q5.1 如何通过 sysfs 操作 PWM?涉及哪些属性文件?

答案要点/sys/class/pwm/pwmchipX/ 下用 export/unexport 导出,生成 pwm0 目录,操作 periodduty_cycleenablepolarity

详细解答: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(硬件不支持则无此接口)
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) 依次写 periodduty_cycleenable。各属性文件都是字符串与 write() 交互。

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_GETSUPPORTWDIOC_SETTIMEOUTWDIOC_GETTIMEOUTWDIOC_SETOPTIONSWDIOC_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(支持喂狗)。流程:

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_FMTVIDIOC_ENUM_FRAMESIZESVIDIOC_ENUM_FRAMEINTERVALS
设置格式 VIDIOC_S_FMT(回读确认),VIDIOC_G_PARM/VIDIOC_S_PARM 设帧率
申请缓冲 VIDIOC_REQBUFSV4L2_MEMORY_MMAP
映射 VIDIOC_QUERYBUF + mmap
入队 VIDIOC_QBUF
开采集 VIDIOC_STREAMON
取帧 循环 VIDIOC_DQBUF → 处理 → VIDIOC_QBUF
结束 VIDIOC_STREAMOFF

追问

  1. 为什么 VIDIOC_S_FMT 后必须回读?(驱动会按硬件能力修改参数,实际值未必等于请求值)
  2. 设备能力里 V4L2_CAP_STREAMINGV4L2_CAP_READWRITE 区别?(前者支持流式 I/O(缓冲队列),后者支持 read/write 方式)

Q6.2 V4L2 的关键结构体有哪些?各自对应哪条指令?

答案要点v4l2_capabilityv4l2_fmtdescv4l2_frmsizeenumv4l2_frmivalenumv4l2_formatv4l2_streamparmv4l2_requestbuffersv4l2_buffer

详细解答

结构体 对应指令 关键字段
v4l2_capability VIDIOC_QUERYCAP capabilities
v4l2_fmtdesc VIDIOC_ENUM_FMT indexpixelformatdescriptiontype
v4l2_frmsizeenum VIDIOC_ENUM_FRAMESIZES indexpixel_formatdiscrete.width/height
v4l2_frmivalenum VIDIOC_ENUM_FRAMEINTERVALS indexwidth/heightdiscrete.numerator/denominator
v4l2_format VIDIOC_G_FMT/S_FMT typefmt.pix.width/height/pixelformat
v4l2_streamparm VIDIOC_G_PARM/S_PARM parm.capture.capabilitytimeperframe
v4l2_requestbuffers VIDIOC_REQBUFS counttypememory
v4l2_buffer QUERYBUF/QBUF/DQBUF indextypememorylengthm.offset

v4l2_fractnumerator/denominator 表示采集周期,帧率 = denominator/numeratorpixelformatv4l2_fourcc 合成,如 V4L2_PIX_FMT_RGB565V4L2_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_bufferindex 标识缓冲编号,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. tcsetattrTCSANOW/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_mallocsnd_pcm_hw_params_any → 依次 set_access(RW_INTERLEAVED)、set_format(S16_LE)、set_rateset_channelsset_period_sizeset_periodssnd_pcm_hw_params 生效 → 循环 snd_pcm_writei/snd_pcm_readisnd_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应用编程例程源码/