嵌入式Linux C应用编程指南:从文件IO到V4L2与MQTT实战
简介:《I.MX6U 嵌入式 Linux C 应用编程指南 V1.4》是正点原子面向嵌入式 Linux 应用开发初学者与进阶工程师编写的实战型教程,以 I.MX6U 微处理器及 ALPHA/Mini 开发板为硬件背景,帮助读者从标准 C 与系统调用逐步过渡到板级外设编程。内容覆盖文件 I/O、进程管理、信号处理、Socket 与网络基础、GPIO、FrameBuffer、PWM、串口、V4L2 摄像头、音频、CAN、MQTT 物联网及 CMake 工程组织,并含视频监控实战项目,章节按入门篇到实战递进,便于按模块查阅。压缩包内含 1 个 PDF 文件,约 36.27MB,版本更新记录与完整目录齐备。目前已有 1539 人学习。读者可从中获得系统调用与库函数的对照理解、外设接口的调用范例,以及从单点实验到综合项目的组织思路,适合作为案头手册与课程配套资料。
1. 板子能跑起来,但自己的程序怎么往上写
串口终端刷完一屏启动日志,最后停在login:,很多人到这一步就卡住了:内核有了、驱动也编进去了,接下来要写自己的业务程序,open一个设备节点该传什么标志、屏幕怎么刷、摄像头出图走哪几个ioctl,全靠东拼西凑。这份《嵌入式 Linux C 应用编程指南 V1.4》补的正是这一段,34 章正文从系统调用、文件 I/O 一路铺到 FrameBuffer、V4L2、Socket、CAN、MQTT,最后拿一个视频监控小项目收尾,全篇对着 Cortex-A7 开发板跑,ARM 平台上的交叉编译和板端调试贯穿始终。适合刚做完驱动移植、准备往上写应用层的人,也适合写过 STM32 裸机、想把手上的 C 语言底子迁到嵌入式 Linux 上的开发者。它不讲内核源码,讲的是你和内核之间的那层接口怎么用对。
2. 章节地图与技术分层:34 章内容是怎么长出来的
2.1 从版本轨迹看文档的组织逻辑
翻更新说明能看出一条很清楚的生长线:V1.0 只有入门、文件 I/O、标准 I/O、文件属性这几个基础章节,V1.1 一次性补进了点亮 LED、GPIO 应用编程、输入设备、tslib、FrameBuffer、JPEG/PNG 显示、横竖屏切换、LCD 显示字符、PWM、V4L2 摄像头、串口、看门狗,V1.2 加了音频、网络基础和 socket,V1.3 补 CAN、CMake 和 MQTT 物联网,V1.4 只加了一章——视频监控实战。
这个节奏说明它不是先定目录再填内容,而是跟着开发板上一个一个外设长出来的。对读者来说这是好事:每一章基本对应一个能上手验证的外设或接口,读完就能在那块板子上跑一遍,不存在「理论讲了三百页、实验要等第五章」的情况。也正因为是逐版追加,知识点有一定的线性依赖,第 1 到第 5 章的文件操作概念会在后面 GPIO、FrameBuffer、V4L2 里反复出现,前面偷懒后面必然卡。
2.2 五个层次,按依赖关系排
把 34 章按依赖关系重新归一下,比按原书顺序读更省力:
| 层次 | 章节范围 | 核心内容 | 学完能干什么 |
|---|---|---|---|
| 概念层 | 第 1 章 | 系统调用、库函数、main 函数、开发环境 | 分清用户态与内核态的边界 |
| 文件层 | 第 2 至 5 章 | open/read/write/lseek、inode、errno、stdio 缓冲、stat | 用「一切皆文件」的思路操作资源 |
| 设备层 | 第 15 至 28 章 | GPIO、输入设备、FrameBuffer、PWM、V4L2、串口、看门狗、音频 | 直接读写设备节点控制硬件 |
| 通信层 | 第 29 至 31 章 | 网络基础、socket、CAN | 板子和板子、板子和服务器对话 |
| 工程层 | 第 32 至 34 章 | CMake、MQTT、视频监控项目 | 把散落的 .c 组织成能交付的工程 |
这张表最有用的地方在于:如果你只做数据采集上报,文件层加通信层就够了,设备层挑串口和 GPIO 两章;如果你做的是带屏的终端,FrameBuffer 和输入设备必须啃透,tslib 那章还得反复看,因为触摸校准的坑多半出在那里。别按顺序从头读到尾,先确认自己的目标落在哪一层。
2.3 开发环境:Ubuntu 装 Eclipse 加交叉编译器
第 1 章花了不小篇幅讲 Ubuntu 下装 Eclipse,这在当年是主流选择,现在用 VS Code 加 C/C++ 插件的人更多,但底层那套东西没变:交叉编译工具链、头文件路径、板端运行。装好工具链后先验证版本:
# 确认交叉编译器能跑,前缀随厂商提供的不一样 arm-linux-gnueabihf-gcc -v # 单独编一个文件,看产物架构对不对 arm-linux-gnueabihf-gcc -o hello hello.c file hello # 期望输出里能看到 ARM, EABI5 之类的字样file那一步别跳,我见过太多次拿 x86 的 gcc 编完拷到板子上跑不起来的情况,报的还是No such file or directory,看着像文件不存在,其实是架构不对,内核连加载都做不到。
编译通过只是第一步,程序得送到板子上。常见做法是 NFS 挂载根目录,Ubuntu 上把编译输出目录导出,板子启动参数里指定nfsroot,改完代码在 PC 上重新make,板子上直接执行,不用每次拷贝。另一条路是 tftp 传输加串口调试,适合不方便搭网络的场景。这两套环境一旦搭好,后面 30 章的实验都能复用,值得在开头多花半天。
2.4 三条阅读路线,按目标选
手上是块带屏的板子、要做人机界面,路线是第 1-5 章打底,然后跳到第 15 章 GPIO、第 17 章输入设备、第 18 章 tslib、第 19 章 FrameBuffer、第 20 至 23 章的图片和字符显示,最后看第 22 章横竖屏切换,因为实际项目里屏的方向经常和出厂默认不一致。
做网关或数据采集,路线是第 1-5 章、第 26 章串口、第 29-31 章网络与 socket、第 33 章 MQTT,第 27 章看门狗顺手看一眼,产品化时要用。
做视频相关,路线短但深:第 1-5 章、第 19 章 FrameBuffer、第 25 章 V4L2、第 34 章视频监控,中间第 32 章 CMake 必须看,不然多文件工程编不起来。
3. 文件 I/O 与系统调用:从 open 到原子操作的完整链路
3.1 系统调用和库函数的分界在哪
第 1 章把这条线划得很清楚:系统调用是内核暴露给用户态的唯一入口,open、read、write、ioctl这些最终都会陷入内核;库函数是 C 库在系统调用之上包的一层,printf底下是write,fopen底下是open。理解这层的意义不在概念本身,而在于出问题时知道往哪查。
举个常见的:printf打印不出东西,你会怀疑程序没跑到那一行,实际上可能只是数据还躺在 stdio 的用户态缓冲区里没刷下去。而write是直接进内核的,写完就在内核缓冲区里了。两者的行为差异,决定了调试期该用哪个。
3.2 一个最小可跑的文件读写闭环
第 2 章开头那个例子值得完整抄一遍,因为它把六个基础接口串成了一条链:
#include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #define BUF_SIZE 64 int main(void) { int fd; char wbuf[BUF_SIZE] = "hello embedded linux"; char rbuf[BUF_SIZE] = {0}; ssize_t n; /* O_CREAT 不存在则创建,O_TRUNC 打开即清空,0664 是文件权限 */ fd = open("./demo.txt", O_RDWR | O_CREAT | O_TRUNC, 0664); if (fd < 0) { perror("open"); return -1; } n = write(fd, wbuf, strlen(wbuf)); if (n < 0) { perror("write"); close(fd); return -1; } /* 关键一步:把读写位置拨回开头,否则 read 读到的是文件末尾 */ if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { perror("lseek"); close(fd); return -1; } n = read(fd, rbuf, sizeof(rbuf) - 1); if (n < 0) { perror("read"); close(fd); return -1; } rbuf[n] = '\0'; printf("read back: %s\n", rbuf); close(fd); return 0; }几个参数得说透。open的第三个参数mode只有在带了O_CREAT时才生效,不带O_CREAT时传了也会被忽略,这一点新手容易记反。write的返回值是实际写入的字节数,不是「成功/失败」,理论上可能小于请求长度,写大文件时要循环。lseek返回新的偏移量,失败返回-1,所以判断时要强转成off_t再比,否则在某些平台上off_t是无符号的,比较会失效。read同理,返回 0 表示读到文件末尾,返回负数才是出错。
这段代码里最容易翻车的是漏掉lseek。写完立刻读,读到的是空字符串,因为文件偏移已经跑到末尾了。这不是 bug,是文件描述符自带位置的正常行为。
3.3 errno 与错误处理:别把错误码扔了
第 3 章专门讲了errno。系统调用失败时返回-1,具体原因放在全局变量errno里,可以用strerror转成字符串,或者直接用perror打印:
int fd = open("/dev/not_exist", O_RDONLY); if (fd < 0) { /* 两种写法等价,perror 会自动拼上 ": 错误描述" */ fprintf(stderr, "open failed: errno=%d (%s)\n", errno, strerror(errno)); perror("open"); return -1; }注意errno只在函数失败的那一刻有效,任何后续的库函数调用都可能把它覆盖掉。所以要么立刻打印,要么先存到局部变量里。我见过在错误分支里先printf一堆上下文再perror,结果打出来的是别的错误码,排查方向直接跑偏。错误处理要写在紧挨着调用的位置,这不是代码风格问题,是正确性问题。
另外,输出走stderr而不是stdout,在嵌入式场景里很实际:stdout可能被重定向到别处,stderr默认还是终端。
3.4 空洞文件、O_APPEND 与 dup2
第 3 章有几个实验挺值得动手。空洞文件就是lseek跳着写:
int fd = open("./hole.bin", O_RDWR | O_CREAT | O_TRUNC, 0644); write(fd, "x", 1); /* 位置 0 */ lseek(fd, 4095, SEEK_SET); /* 跳到 4095,中间 4094 字节没写 */ write(fd, "y", 1); /* 位置 4095 */ close(fd); /* ls -l 显示 4096 字节,du 只算实际占用的块 */文件长度是 4096,但中间那段没落盘,不占实际存储。这个特性在做大文件预分配或者稀疏存储时会用到,但也要知道:拷到不支持稀疏的文件系统上,空洞会被填成 0,体积瞬间涨上去。
O_APPEND是另一个常被误解的标志。它保证每次write都追加到文件末尾,而且是原子操作——定位和写入在同一个内核动作里完成。多进程往同一个日志文件写时,这个标志比手动lseek到末尾再写要安全得多,后者在并发下会出现互相覆盖。
dup2用来复制文件描述符,最实用的场景是把标准输出接到文件上:
int fd = open("./app.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open"); return -1; } /* 让 1 号描述符指向这个文件,之后所有 printf 都落盘 */ if (dup2(fd, STDOUT_FILENO) < 0) { perror("dup2"); return -1; } close(fd); /* 已经复制给 1 号了,原来的 fd 可以关 */ printf("this line goes to app.log\n"); fflush(stdout);dup2执行后两个描述符指向同一个打开文件表项,共享偏移量,所以关掉原fd不影响stdout。这一招在守护进程里几乎是标配,程序启动时就把输出重定向到日志文件,不依赖外部>/path。
3.5 标准 I/O 的缓冲:printf 为什么没输出
第 4 章讲 stdio 缓冲,这是嵌入式调试阶段的头号疑案。printf默认是行缓冲,遇到换行才刷;重定向到文件后变成全缓冲,缓冲区满了才写。程序崩在printf之后,缓冲区没刷,日志就丢了。
/* 调试期直接关掉 stdout 缓冲,每写一次就落一次 */ setvbuf(stdout, NULL, _IONBF, 0); /* 或者保留缓冲,在关键位置手动刷 */ printf("camera init ok\n"); fflush(stdout);第 4 章还区分了内核缓冲和stdio缓冲,以及用O_DIRECT绕过内核缓冲的直接 I/O。后者在要求低延迟、数据不能缓存的场景才有意义,日常应用别碰,对齐要求能把人折腾够呛。实际项目里我一般是调试期关缓冲,上线后恢复默认并加fflush,两害相权。
4. 设备节点与实战链路:GPIO、FrameBuffer、V4L2 到 Socket
4.1 GPIO:sysfs 和 libgpiod 是两代写法
第 15、16 章讲点亮 LED 和 GPIO 应用编程。这一块最大的坑是:书里演示的/sys/class/gpio接口在新内核上已经被标记为废弃,很多发行版的默认配置里直接关掉了。先看内核版本再决定走哪条路。
老接口长这样:
#include <fcntl.h> #include <unistd.h> int fd, val_fd; /* 第一步:把引脚号导出到用户空间,编号按芯片手册和板子原理图换算 */ fd = open("/sys/class/gpio/export", O_WRONLY); if (fd < 0) { perror("export"); return -1; } write(fd, "139", 3); close(fd); /* 第二步:设方向 */ fd = open("/sys/class/gpio/gpio139/direction", O_WRONLY); if (fd < 0) { perror("direction"); return -1; } write(fd, "out", 3); close(fd); /* 第三步:写电平,写 "1" 拉高 */ val_fd = open("/sys/class/gpio/gpio139/value", O_WRONLY); if (val_fd < 0) { perror("value"); return -1; } write(val_fd, "1", 1);这套接口的优点是简单直观,缺点也明显:每次操作都要open+write+close,开销大,做不了高频翻转;而且要手动管理 export/unexport,程序异常退出没清理,下次就导不进去。新内核推荐用libgpiod,命令行侧是gpioset、gpioget,代码侧是gpiod_chip_open加gpiod_line_request_output,直连字符设备/dev/gpiochipN,性能和资源管理都规范得多。
还有一层更深的坑:引脚能不能用,不取决于你的程序,取决于设备树里的 pinmux 有没有把它分给 GPIO 控制器。如果这个脚已经被别的驱动占用,export会直接失败或者导进去也控制不了。排查时看/sys/kernel/debug/pinctrl/下的引脚状态,比在程序里反复试有效得多。
4.2 FrameBuffer:mmap 一块显存,按像素点画
第 19 章是显示这条线的地基。核心思路是:/dev/fb0对应一块物理显存,用mmap映射到用户空间,之后往里写字节就是在改屏幕内容。
#include <linux/fb.h> #include <sys/ioctl.h> #include <sys/mman.h> int fd = open("/dev/fb0", O_RDWR); if (fd < 0) { perror("open fb0"); return -1; } struct fb_fix_screeninfo finfo; struct fb_var_screeninfo vinfo; /* 先拿固定信息:显存起始地址、每行字节数 line_length */ ioctl(fd, FBIOGET_FSCREENINFO, &finfo); /* 再拿可变信息:分辨率、bpp、像素格式 */ ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); size_t screensize = finfo.line_length * vinfo.yres_virtual; unsigned char *fbp = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fbp == MAP_FAILED) { perror("mmap"); return -1; }两个结构体别搞混:fix里是硬件固定的东西,line_length尤其关键,它不一定等于xres * bpp / 8,有些控制器会做行对齐,按分辨率算偏移会花屏;var里是可以改的显示参数,bits_per_pixel决定每个像素占几个字节。
画一个像素:
/* 32bpp 下按 ARGB 写,先定位到行再定位到列 */ unsigned int *pixel = (unsigned int *)(fbp + y * finfo.line_length + x * (vinfo.bits_per_pixel / 8)); *pixel = 0x00FF0000; /* 不透明红 */16bpp 的 RGB565 就是unsigned short *,值要拆成 5-6-5 位。第 20 至 23 章讲的 JPEG/PNG 解码显示、横竖屏切换、字符显示,底层都是这套 mmap 加指针偏移。横屏转竖屏那一章尤其值得看,它不是简单调个参数,涉及分辨率交换后重新计算line_length和像素落点,改错了画面就是斜的。
提示:
vinfo.bits_per_pixel和finfo.line_length一定要从驱动里读,不能按经验值写死。同一份代码换块屏就不对,十有八九是这里。
4.3 V4L2:摄像头采集的最小闭环
第 25 章的 V4L2 是全书步骤最多的一个实验,但顺序是固定的:查询能力、设置格式、申请缓冲、映射、入队、开流、出队、处理。
int fd = open("/dev/video0", O_RDWR | O_NONBLOCK); struct v4l2_capability cap; ioctl(fd, VIDIOC_QUERYCAP, &cap); /* 确认支持 capture 和 streaming */ struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; /* 驱动不支持会回填实际格式 */ fmt.fmt.pix.field = V4L2_FIELD_NONE; ioctl(fd, VIDIOC_S_FMT, &fmt); struct v4l2_requestbuffers req = {0}; req.count = 4; /* 缓冲区数量,太少会丢帧 */ req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, &req);接下来对每个 buffer 做VIDIOC_QUERYBUF拿到长度和偏移,mmap进用户空间,然后全部VIDIOC_QBUF入队,最后VIDIOC_STREAMON开流。采集循环里select等可读,VIDIOC_DQBUF取出一个填好数据的 buffer,用完再VIDIOC_QBUF塞回去。
顺序错一步就卡住:先开流再入队,DQBUF会一直阻塞;只入队一部分 buffer,帧率上不去。VIDIOC_S_FMT传进去的格式驱动不一定支持,它会回填实际使用的格式,后面解析数据必须按回填的值来,不能按你请求的来。
4.4 Socket 与 MQTT:把数据送出去
第 29、30 章讲网络基础和 socket,第 33 章用 MQTT 做物联网小项目。TCP 客户端和服务端之间的连接建立就几行:
#include <sys/socket.h> #include <arpa/inet.h> int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(8888); /* 端口要转网络字节序 */ inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr); if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("connect"); return -1; } /* 之后 write/read 就是收发数据 */第 33 章把 MQTT 接在这上面,用的是一个轻量客户端库:
#include <mosquitto.h> struct mosquitto *mosq = mosquitto_new("node-01", true, NULL); mosquitto_connect(mosq, "192.168.1.100", 1883, 60); const char *payload = "{\"temp\":26.5}"; mosquitto_publish(mosq, NULL, "dev/telemetry", strlen(payload), payload, 1, false); /* qos=1,retain=false */ mosquitto_loop(mosq, -1, 1);参数里qos值得展开:0 是最多发一次,丢了不管;1 是至少一次,可能重复;2 是恰好一次,开销最大。设备上报遥测数据用 1 就够,服务端按时间戳去重;下发控制指令才考虑 2。retain设成 true 时,broker 会保留最后一条消息,新订阅者一连上就能收到当前状态,做设备影子很方便。
注意:连接要带心跳,
mosquitto_connect的最后一个参数是 keepalive 秒数。无线网络下超过这个时间没交互,broker 会认为客户端掉线并触发遗嘱消息。心跳设太短会频繁重连,设太长掉线发现慢,一般 30 到 60 秒之间。
4.5 CMake:把散落的 .c 组织起来
第 32 章的 CMake 是为最后那个视频监控项目服务的。前面每章都是单文件gcc编,真做成项目就得分模块。交叉编译的CMakeLists.txt关键是开头这几行:
cmake_minimum_required(VERSION 3.10) project(video_node C) # 交叉编译三件套,告诉 CMake 别用主机编译器 set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) find_package(Threads REQUIRED) add_executable(video_node src/main.c src/camera.c src/fb.c src/net.c) target_link_libraries(video_node PRIVATE Threads::Threads mosquitto)CMAKE_SYSTEM_NAME设成Linux是开启交叉编译模式的开关,不设这一行,后面指定的编译器会被当成普通编译器用,编出来的还是 x86 的产物。target_link_libraries比全局link_libraries好,依赖关系明确,模块多了不会互相污染。采集、显示、网络拆成独立.c,接口在头文件里,后面想换传输协议只改net.c,采集和显示一行不动。
5. 避坑与排查:嵌入式 Linux 应用编程的五个翻车现场
5.1 板子上运行没输出,重定向到文件反而有
现象:程序在开发板的串口终端跑,printf一句都不显示,echo $?返回值还是 0;但写成./app > out.txt再cat就有内容。
原因:stdout连终端时是行缓冲,连文件或管道时是全缓冲。如果代码里printf结尾没带\n,或者程序在缓冲区刷新前就退出了(比如调了_exit),数据就丢了。第 3 章讲exit和_exit的区别正是这里:exit会刷标准 I/O 缓冲,_exit直接走系统调用,不刷。
解决:调试期用setvbuf(stdout, NULL, _IONBF, 0)关掉缓冲,或者在每个关键printf后面补fflush(stdout);日志统一走stderr,它默认无缓冲。程序退出路径上用exit别用_exit,除非你确定缓冲区已经清空。
5.2 mmap 显存成功,但屏幕是花屏或偏移
现象:open("/dev/fb0")和mmap都返回成功,往映射区写数据,屏幕上是斜的条纹或者颜色全错。
原因有三种。一是按xres * bpp / 8算行偏移,而实际每行长度是finfo.line_length,两者因为行对齐经常不等;二是像素格式判断错,按 RGB565 的unsigned short去写一块 ARGB8888 的屏,颜色和位置全乱;三是mmap的长度用了xres * yres * bpp / 8,忽略了yres_virtual可能大于yres,尾部越界。
解决:坐标换算一律用finfo.line_length,长度用line_length * vinfo.yres_virtual;写像素前printf一下bits_per_pixel和line_length确认格式;用ioctl(FBIOGET_VSCREENINFO)读出来的值,不要写死。
5.3 V4L2 的 DQBUF 一直阻塞不返回
现象:摄像头的VIDIOC_STREAMON返回成功,进入采集循环后VIDIOC_DQBUF卡死,select也一直超时。
原因:多数是 buffer 没有全部入队就开了流,驱动手里没有可写的 buffer,自然不会产出数据。另一种是VIDIOC_REQBUFS申请的count被驱动降到了更小的值(比如请求 4 个实际只给 2 个),代码里还按 4 个去循环QBUF,多出来的两个是无效的。
解决:REQBUFS之后读回req.count,按实际值循环;严格按「申请 → QUERYBUF → mmap → QBUF 全部 → STREAMON」的顺序;DQBUF用非阻塞加select,超时了打日志而不是死等,方便定位是驱动没数据还是流程错了。
5.4 交叉编译的程序拷到板子上报「No such file or directory」
现象:ls -l看文件明明在,权限也是可执行,一运行就报这个错,甚至./app补全都正常。
原因:这个报错很多时候不是说文件不存在,而是说动态链接器不存在。readelf -l app看interpreter那一行,指向的路径(比如/lib/ld-linux-armhf.so.3)在板子的根文件系统里没有,或者版本对不上。也可能是拿 x86 工具链编的,架构根本不匹配。
解决:先用file app确认是 ARM 架构;再用readelf -l看解释器路径,和板子上ls /lib/ld-*对一下;路径不一致就换匹配的工具链,或者用-static静态链接绕开动态库依赖(体积会大不少,但调试期省事)。
5.5 gpio export 失败或者导进去也控制不了
现象:往/sys/class/gpio/export写引脚号,返回Device or resource busy;或者 export 成功,direction也设成了 out,写value电平不变。
原因:前者通常是这个引脚已经被设备树分配给别的驱动了,比如被复用成 I2C 或者 PWM;后者多半是 pinmux 没配成 GPIO 功能,或者引脚号换算错了(不同芯片的 GPIO 编号基数不一样)。
解决:先看cat /sys/kernel/debug/pinctrl/*/pinmux-pins,确认目标引脚当前归谁;再翻芯片手册核对引脚编号的基址和偏移;实在不行改设备树,把冲突的节点status = "disabled"掉再试。新内核上直接换libgpiod的gpioset命令验证硬件通不通,比在程序里反复编快得多。
6. 二次开发:把第 34 章的项目拆成自己的骨架
第 34 章的视频监控是个完整闭环:V4L2 采集、FrameBuffer 本地显示、socket 传到远端。拿它当起点比自己从零搭要快,但直接改成产品会遇到一个问题——采集、显示、传输三件事全缠在main的循环里,想加个录像或者换个协议就得动主流程。我的做法是先做一次解耦,拆成三个线程加两个队列:
| 模块 | 职责 | 对外接口 |
|---|---|---|
| camera | V4L2 初始化、取帧、归还 buffer | camera_get_frame()返回一帧的指针和时间戳 |
| display | FrameBuffer 映射、YUV 转 RGB、刷屏 | display_show(frame) |
| net | socket 发送、断线重连、心跳 | net_send(frame) |
队列用固定长度的环形缓冲,满了丢最旧的一帧而不是阻塞采集,实时性优先。三个线程用条件变量同步,采集线程出队唤醒显示和网络,主线程只负责信号处理和退出清理。
改造时有个细节要盯住:V4L2 的 buffer 是驱动拥有的,DQBUF之后你只有很短的时间窗口能用这块内存,处理完必须QBUF还回去,否则采集会停。所以不能把 buffer 指针直接扔进队列让别的线程慢慢用,要么拷贝一份,要么做引用计数,处理完最后一个使用者再归还。我第一版就是直接把指针传出去,跑了十几秒画面就冻住,查了半天才发现 buffer 全被占着没还。
验证方式分两步。先在开发板上单独跑每个模块,camera 打印每秒帧数,display 刷纯色测试映射和格式,net 用nc起个假服务端看数据到不到。三个都通了再接起来,出了问题也知道往哪个模块查。第二步加异常注入:拔网线看重连,拔摄像头看错误处理,kill -9看资源有没有残留。
提示:设备节点打开失败、
ioctl返回EINVAL、buffer 数量不足,这三个错误占了嵌入式应用调试的大半时间。每个失败点都打一条带errno的日志,比事后猜高效太多。
从那以后我每次接一个新板子,第一件事就是写个最小程序把open、ioctl、mmap这条链路单独跑通,确认设备节点和数据结构都对,再往里填业务逻辑,绝不把环境问题和逻辑问题混在一起查。希望帮到你。
本文还有配套的精品资源,点击获取