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
💡 关联知识:[[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 条追问」。
答案要点:阻塞 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 | 立即返回错误 | 轮询时高 | 需同时处理多路,配合轮询/多路复用 |
追问
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 |
追问
timeout 传 NULL、0、非 0 分别代表什么?(一直阻塞 / 不阻塞仅轮询一次 / 阻塞上限)FD_ZERO+FD_SET?(select 返回时修改了集合,只保留就绪 fd)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; /* 返回时就绪的事件(由内核填写) */
};
revents 置 0;timeout:-1 一直阻塞、0 立即返回、>0 阻塞上限(毫秒);revents 非 0 的元素个数,0 表示超时,-1 出错。常用事件:POLLIN(普通优先级数据可读)、POLLOUT(可写)、POLLPRI(高优先级数据可读,常用于中断)、POLLERR(错误)。判断用 pfd.revents & POLLIN。
追问
events 和 revents 有什么区别?(events 由调用者设置、poll 不改动;revents 由 poll 填写,表示实际发生的事件)答案要点:异步 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 读取数据。
追问
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 实现"零拷贝"。
追问
msync() 和 mprotect() 有什么用?(msync 把映射区修改同步到文件/磁盘;mprotect 修改映射区保护权限)flock() 与 fcntl() 锁有何区别?(flock 是 BSD 风格的整文件锁;fcntl 支持记录锁/字节范围锁,功能更强)答案要点:通过 /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)。
追问
答案要点:/sys/class/leds/sys-led/ 下关注 brightness、max_brightness、trigger。
详细解答:
| 属性文件 | 读写 | 说明 |
|---|---|---|
brightness |
可读可写 | 设置/获取亮度,0 灭、正整数亮(PWM LED 有等级,GPIO LED 只有亮灭) |
max_brightness |
只读 | 最大亮度等级 |
trigger |
可读可写 | 触发模式,读可获取支持的所有模式及当前模式(方括号内) |
常用触发模式:none(无触发)、mmc0(读写 mmc0 时闪烁)、timer(定时器规律亮灭)、heartbeat(心跳呼吸)。
echo none > trigger # 设为无触发
echo 1 > brightness # 点亮
echo 0 > brightness # 熄灭
控制 LED 的程序需先写 trigger 为 none,再写 brightness。
追问
"1",不能直接写二进制整数)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 目录下关注 direction、value、active_low、edge。
追问
active_low 有什么用?(控制极性:=1 时 value 写 1 输出低、写 0 输出高)答案要点:写 direction 为 out/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=in、edge=none,再以 O_RDONLY 打开 value 读取 1 字节。注意:只有输入模式下才能用 cat value 读电平,输出模式读不到实际电平。
追问
access(path, F_OK) 的作用?(判断文件/目录是否存在,用于决定是否需要导出)poll() 而不是直接 read()?答案要点:写 edge 为 rising/falling/both 配置边沿触发,再用 poll() 监听 value 的 POLLPRI 事件。
详细解答:
/* 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。
追问
read() 一次?(清除可能存在的旧状态,避免 poll 立即返回误报)edge 配置前需要什么前提?(引脚必须配置为输入模式)read() 一次读到什么?答案要点:输入设备节点在 /dev/input/eventX;每次 read() 读到一个 struct input_event 结构体。
详细解答:输入类设备(按键、键盘、鼠标、触摸屏)由 input 子系统统一管理,设备节点在 /dev/input/ 下,如 event0、event1。每次读操作获得一个:
struct input_event {
struct timeval time; /* 时间戳 */
__u16 type; /* 事件类型 */
__u16 code; /* 具体事件 */
__s32 value; /* 事件值 */
};
其中 type、code、value 是解析的核心。
追问
/sys 下如何分类?(/sys/class/input)read() 能读多个事件吗?(通常只读一个;可按结构体大小循环读)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),告知本轮数据已完整。
追问
value=2 表示什么?哪些事件不支持?(长按;BTN_TOUCH 不支持长按,故 value 不为 2)EV_SYN?(一次触摸会产生多个事件,靠 SYN_REPORT 标记一组数据完整)答案要点:读取 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。
追问
ABS_X/ABS_Y 判断按下?(需配合 BTN_TOUCH 或 TRACKING_ID 判断触摸状态)答案要点:用 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_X → ABS_MT_POSITION_Y → BTN_TOUCH(1) → ABS_X/ABS_Y → SYN_REPORT;松开时上报 ABS_MT_TRACKING_ID=-1。
追问
input_absinfo 里有哪些字段?(value 最新值、minimum/maximum 范围、fuzz、flat、resolution)BTN_TOUCH?(触摸芯片不同,可能只上报 ABS 事件)答案要点:tslib 是专为触摸屏开发的开源应用层函数库,封装了原始 input 解析、去噪去抖、坐标变换,把原始坐标转换为屏幕坐标。
详细解答:tslib 作为触摸屏驱动与应用层之间的适配层,配置文件 ts.conf(module_raw input、module pthres、module dejitter、module 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_TSDEVICE、TSLIB_CONFFILE、TSLIB_PLUGINDIR、TSLIB_FBDEVICE。编译:${CC} -I .../tslib/include -L .../tslib/lib -lts -o testApp testApp.c。
追问
ts_setup 相比 ts_open 多了什么?(额外完成了设备配置、插件加载)ts_print、ts_print_mt、ts_test、ts_test_mt)/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。
追问
fb_var_screeninfo 与 fb_fix_screeninfo 有何区别?(前者可修改的运行参数,后者驱动程序设定、只读的固定参数)line_length * yres?(可能存在行填充,line_length 才是真实一行字节数)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。
追问
(x, y) 对应横屏物理坐标是什么?(如 (x, 480 - 1 - y) 等,取决于旋转方向)答案要点:分别取 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。
追问
fb_var_screeninfo 的 bits_per_pixel 与 red/green/blue 的 offset/length)答案要点:创建解码对象 → 设置数据源 → 读文件头 → 开始解码 → 逐行 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 可缩放解码尺寸。
追问
cinfo.err = jpeg_std_error(&jerr),可自定义 error_exit 回调)jpeg_read_scanlines 的第二个参数类型?(unsigned char **,逐行读取)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_info→png_set_*→png_read_update_info→png_read_image |
需用户自行分配行缓冲,可控性强 |
libpng 依赖 zlib,编译需 -lpng -lz。
追问
png_set_expand、png_set_strip_16、png_set_gray_to_rgb 分别做什么?(位深扩展、16→8 位、灰度转 RGB)bearingY 与 advance 的作用?(bearingY 用于确定字形顶部 y 坐标,advance 是下一个字符的水平步进)答案要点:/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(硬件不支持则无此接口) |
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
追问
答案要点:period 是一个完整周期时长(ns),duty_cycle 是高电平时长(ns),占空比 = duty/period;polarity 决定高电平有效还是低电平有效。
详细解答:频率 = 1e9 / period(ns)。例如 period=500000ns → 2KHz;duty_cycle=250000ns → 占空比 50%。polarity=normal 时占空比对应高电平时间,inversed 则反转。周期单位是 ns,写入的是字符串数字。
追问
period=10000 对应频率多少?(100KHz)polarity 接口,写会失败)答案要点:判断是否导出 → 写 export → 配置 period → 配置 duty_cycle → 写 enable=1。
详细解答:程序用 access() 判断 pwm0 目录是否存在,不存在则向 export 写 "0";随后用统一的 pwm_config(attr, val) 依次写 period、duty_cycle、enable。各属性文件都是字符串与 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 背光引脚),可用示波器测波形。
追问
enable=0 再改参数?(周期/占空比存在约束关系,运行中改可能失败;部分驱动要求先停)fdt set 把 backlight、lcdif 的 status 设为 disabled,仅本次启动有效)答案要点:看门狗是一个可被复位/重置的计数器;超时未喂狗则计数器溢出,对 CPU 产生复位信号使系统重启。
详细解答:看门狗(WDOG)启动后自动计数,程序须在超时前"喂狗"(清零计数器)以免复位;一旦程序跑飞没按时喂狗,系统"被狗咬"复位。有些看门狗也能只产生中断而不复位。I.MX6UL/I.MX6ULL 集成 WDOG1(普通看门狗,支持中断与复位)与 WDOG2(用于安全目的)。
Linux 中看门狗设备节点为 /dev/watchdogX,/dev/watchdog 是默认看门狗(通常即 watchdog0)。
追问
close() 时看门狗会自动启动,因此打开后通常先用 WDIOC_SETOPTIONS 停止)答案要点:需包含 <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(支持喂狗)。流程:
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)。
追问
Device or resource busy 怎么办?(看门狗已被系统服务占用,需注释掉 /etc/init.d/watchdog.sh)Ctrl+C 终止后会怎样?(停止喂狗但看门狗仍在计时,超时后系统复位,内核打印 watchdog did not stop!)答案要点:打开设备 → 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 |
追问
VIDIOC_S_FMT 后必须回读?(驱动会按硬件能力修改参数,实际值未必等于请求值)V4L2_CAP_STREAMING 与 V4L2_CAP_READWRITE 区别?(前者支持流式 I/O(缓冲队列),后者支持 read/write 方式)答案要点: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。
追问
v4l2_format 是 union,VIDEO_CAPTURE 时哪个成员生效?(fmt.pix)V4L2_BUF_TYPE_VIDEO_CAPTURE 与 ..._MPLANE 区别?(后者用于多平面格式,fmt.pix_mp 生效)答案要点:内核维护帧缓冲队列,应用把空闲缓冲入队交给驱动、驱动填满后应用出队取走,mmap 后应用直接读映射区,实现零拷贝。
详细解答:VIDIOC_QBUF(入队)把缓冲交给驱动,驱动采集一帧填入后缓冲变"满";VIDIOC_DQBUF(出队)取走满缓冲,处理完再入队,往复循环。v4l2_buffer 的 index 标识缓冲编号,m.offset 是在内核申请的整块内存中的偏移,配合 mmap(fd, m.offset) 得到用户空间地址。处理时"出队→读映射区→处理→入队"。
追问
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() 立即返回。
追问
tcsetattr 的 TCSANOW/TCSADRAIN/TCSAFLUSH 区别?(立即生效 / 输出传完生效 / 生效前丢弃未读输入)答案要点: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 调音量。
追问
snd_pcm_start()?(PREPARED 状态下首次 writei/readi 会自动调用 snd_pcm_start())内容来源:《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I_O,第十五~二十八章 外设应用编程;配套例程源码 11、Linux C应用编程例程源码/。