简介:《I.MX6U 嵌入式 Linux C 应用编程指南 V1.4》面向已经掌握 C 语言、正从单片机转向 ARM 嵌入式 Linux 的开发者,以正点原子 I.MX6U ALPHA/Mini 开发板为硬件平台,帮助读者跨越从裸机思维到应用层编程的这道门槛。内容从系统调用与库函数的区别入手,依次讲解文件 I/O、进程管理、信号处理、Socket 通信、GPIO 点灯与按键、FrameBuffer、输入设备与 tslib、LCD 显示图像字符以及横竖屏切换、PWM、V4L2 摄像头、串口和看门狗等接口的使用方法;随后延伸到音频、网络基础、CAN 和 CMake 构建,并以 MQTT 物联网与视频监控两个实战小项目收尾。每个接口都给出了调用流程和示例思路,章节顺序就是学习顺序,也便于按接口类型查阅;从 V1.0 到 V1.4 的多次修订陆续补充了 CAN、CMake、视频监控等内容,体系相对完整。资源是单个 PDF 文件,约 36.27MB,附有版本更新记录和完整目录。目前已有 1539 人学习,适合对照开发板边读边验证。
1. 从裸机转到ARM Linux应用层:这份V1.4指南到底解决哪几个硬问题
拿到一份嵌入式 Linux C 应用编程指南 V1.4,最容易被目录吓到:文件 I/O、标准 I/O、进程、信号、Socket、GPIO、FrameBuffer、V4L2、CAN、CMake、MQTT 全都有。它面向的是已经会写 C、但一上 ARM Linux 就不知道从哪下手的开发者,把应用层最常碰的“文件、进程、网络、外设、工程化”串成一条线。价值不在背 API,而在让你能独立写出可交叉编译、可上板运行、可排查的 C 程序。下面从文件 I/O、进程与网络、显示多媒体、工程化、避坑和进阶验证几个落点往下拆。
2. 文件I/O与标准I/O:open/read/write跑通后再谈框架
2.1 系统调用与库函数的分层
嵌入式 Linux C 应用编程的第一道分水岭,是分清系统调用和库函数。系统调用是内核暴露给用户态的入口,比如open、read、write、close、lseek、ioctl;库函数是 C 库在系统调用之上做的封装,比如fopen、fread、fwrite、fprintf。两者不是谁高级谁低级,而是缓冲策略、错误返回方式、可移植性不同。文件 I/O 更贴近设备节点,适合操作/dev下的字符设备、块设备和普通文件;标准 I/O 自带用户态缓冲,适合格式化读写和按行处理配置。
我一般让新手先写文件 I/O,不是因为标准 I/O 不重要,而是因为文件描述符、errno、偏移量、权限这些概念在文件 I/O 里藏不住。先把open返回 -1 时怎么查、read返回 0 时意味着什么、write少写了怎么办弄明白,再切到FILE *才不会把缓冲问题当成“玄学”。常见做法是先建一个独立目录,把交叉编译器前缀、头文件路径、库路径固定下来,后面所有示例都复用这套编译命令。
# 建立练习目录,交叉编译器前缀按自己手头的工具链替换 mkdir -p ~/arm-linux-io/practice cd ~/arm-linux-io/practice # 只编译不链接,先确认头文件和语法没问题 arm-linux-gnueabihf-gcc -c file_io_demo.c -o file_io_demo.o # 链接成可执行文件,上板前用 file 看架构 arm-linux-gnueabihf-gcc file_io_demo.c -o file_io_demo file file_io_demo这里的参数重点看三个:编译器前缀决定生成的是 ARM 指令还是主机指令;-c只做编译,适合快速排除语法和头文件缺失;file用来确认输出架构,别把 x86 程序拷到板子上再回头查半天。路径不要写死到某个绝对目录,练习阶段用相对路径,上板阶段再用配置文件或命令行参数传入。
2.2 最小文件读写回路
文件 I/O 的最小闭环不是“能打开就行”,而是“打开、写入、定位、读回、关闭、检查错误”六步都走一遍。下面这段代码故意把open、write、lseek、read、close放在一个流程里,方便观察文件偏移量的变化。代码里保留perror,因为上板后最常见的错误就是权限、路径和设备节点不存在,errno比打印一句“失败”有用得多。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char *argv[]) { const char *path = (argc > 1) ? argv[1] : "/tmp/io_demo.bin"; const char *msg = "hello arm linux c"; char buf[64] = {0}; int fd; ssize_t n; /* O_CREAT 创建文件,O_TRUNC 清空旧内容,0664 是权限掩码 */ fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0664); if (fd < 0) { perror("open"); return 1; } n = write(fd, msg, strlen(msg)); if (n < 0) { perror("write"); close(fd); return 1; } printf("write bytes: %ld\n", (long)n); /* 把偏移量移回文件头,否则 read 从当前末尾读不到内容 */ if (lseek(fd, 0, SEEK_SET) < 0) { perror("lseek"); close(fd); return 1; } n = read(fd, buf, sizeof(buf) - 1); if (n < 0) { perror("read"); close(fd); return 1; } printf("read bytes: %ld, content: %s\n", (long)n, buf); close(fd); return 0; }逻辑说明:open的第二个参数是标志位组合,O_RDWR表示读写,O_CREAT表示不存在就创建,O_TRUNC表示存在就截断;第三个参数只在创建时生效,0664会受进程umask影响。write返回实际写入字节数,不是“成功/失败”布尔值,短写要考虑循环补写。lseek只改文件偏移量,不读不写。read返回 0 表示读到文件末尾,不是错误;返回 -1 才看errno。close之后不要再使用这个fd,否则就是悬空描述符。
上板运行时先给一个可写路径,比如/tmp或应用自己的数据目录。不要一上来就写/etc、/dev这类目录,权限和只读文件系统会让你误判成代码问题。常见做法是先用/tmp验证逻辑,再把路径改成配置项。
2.3 文件描述符、inode与共享偏移
文件描述符是进程级的小整数,本质上是进程打开文件表的下标。open一次就产生一个描述符,背后指向一个打开文件表项,里面记录文件偏移量和状态标志;打开文件表项再指向 inode,inode 才记录文件真实数据块、权限、大小这些元信息。这个分层解释了三个常见现象:同一进程多次open同一文件,会得到不同描述符和不同偏移量;dup复制出来的描述符共享同一个偏移量;fork之后父子进程也共享打开文件表项,偏移量会互相影响。
验证方法不用写复杂程序:在同一个进程里连续open两次同一路径,先对第一个描述符写 3 字节,再对第二个描述符读,观察读到的内容是不是从文件头开始。常见结果是第二个描述符从自己的偏移 0 读,说明它们不共享偏移;再用dup复制第一个描述符,对副本读,偏移会跟着原描述符走。O_APPEND是另一个容易翻车的点:它让每次write前自动把偏移移到末尾,但这个“移到末尾再写”在多个进程同时写时必须是原子的,否则就会互相覆盖。
| 场景 | 常用函数/标志 | 注意点 |
|---|---|---|
| 同一文件多次打开 | open两次 | 描述符不同,偏移量独立 |
| 复制描述符 | dup、dup2 | 共享打开文件表项和偏移量 |
| 追加写日志 | O_APPEND | 依赖内核原子追加,不要自己先lseek再写 |
| 截断重写 | O_TRUNC | 只在有写权限时生效,注意误清数据 |
| 多进程共享 | fork后继承 | 父子共享偏移,读位置会互相推进 |
这里最容易出现的误用是“先lseek到末尾,再write”来做多进程日志。单进程看起来没问题,多进程并发时两个进程可能都定位到同一个末尾,然后一个覆盖另一个。需要追加就老老实实用O_APPEND,需要更复杂的并发控制就上文件锁或独立日志进程。原子操作不是口号,是你在设计打开标志时就要决定的事。
2.4 缓冲与直接I/O的取舍
文件 I/O 默认走内核页缓存,write成功只代表数据进了内核缓冲,不代表已经落到存储介质。标准 I/O 又在用户态加了一层缓冲,fwrite之后不fflush或fclose,数据可能还在用户态。指南里把内核缓冲、fsync、O_DIRECT、O_SYNC放在一起讲,是因为掉电丢数据、性能抖动、设备读写对齐这些问题都绕不开缓冲策略。
我一般按数据重要性分三档:普通日志允许丢最后几行,用默认缓冲就行;配置文件、升级标志、计费数据写完要fsync,确保落到介质;摄像头帧、高速采集这类大块数据,如果驱动和文件系统支持,再考虑O_DIRECT绕过页缓存,但要注意地址对齐、长度对齐和块大小对齐,否则read/write直接返回EINVAL。O_SYNC每次写都等落盘,安全但慢,不适合高频小写。
| 策略 | 作用 | 适用场景 | 主要代价 |
|---|---|---|---|
| 默认页缓存 | 合并写、加速读 | 普通文件、日志 | 掉电可能丢最近数据 |
fsync | 把指定文件数据刷到介质 | 配置、状态文件 | 调用频繁会拖慢 |
O_SYNC | 每次写同步落盘 | 极关键小数据 | 吞吐明显下降 |
O_DIRECT | 绕过页缓存 | 大块采集、视频帧 | 对齐要求严,容易EINVAL |
| stdio 缓冲 | 用户态聚合读写 | 格式化文本 | 不fflush可能丢 |
提示:判断要不要
fsync,不要看“程序有没有报错”,要看“掉电后这一笔能不能丢”。能丢就别加,不能丢就加,别靠感觉。
3. 进程、信号与Socket:应用层稳定性的三条主线
3.1 进程创建与回收:fork/exec/waitpid的边界
嵌入式 Linux 应用里,进程模型通常比线程模型更容易排查:一个采集进程、一个处理进程、一个通信进程,出事时看ps和日志就能定位。fork创建子进程,子进程复制父进程的地址空间和文件描述符;exec用新程序替换当前进程映像;waitpid回收子进程退出状态。三者的边界要清楚:fork之后父子进程谁先跑不确定,exec成功不返回,waitpid不调用就会留下僵尸进程。
下面这段代码只做最小演示:父进程fork后等子进程退出,子进程sleep一秒再返回。重点看waitpid的参数和返回值,别把fork返回值 0 当成失败。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* 子进程:这里可以 exec 新程序,演示阶段先睡眠后退出 */ printf("child pid=%d\n", getpid()); sleep(1); return 7; } int status = 0; /* 父进程阻塞等待指定子进程,status 里带退出码和终止信号 */ pid_t ret = waitpid(pid, &status, 0); if (ret < 0) { perror("waitpid"); return 1; } if (WIFEXITED(status)) { printf("child exit code=%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("child killed by signal=%d\n", WTERMSIG(status)); } return 0; }参数说明:fork在父进程返回子进程 PID,在子进程返回 0,失败返回 -1。waitpid第一个参数传具体 PID 就等这个子进程,传 -1 等任意子进程;第二个参数接收状态;第三个参数0表示阻塞等待。WIFEXITED判断是否正常退出,WEXITSTATUS取退出码;WIFSIGNALED判断是否被信号终止,WTERMSIG取信号编号。常见翻车是父进程不waitpid,子进程退出后占着进程表项,时间一长fork失败。
3.2 信号处理:别在信号处理函数里写业务
信号是异步事件,SIGINT、SIGTERM、SIGCHLD、SIGALRM在应用层很常见。信号处理函数的第一原则是短:置一个volatile sig_atomic_t标志,或者往管道/eventfd 写一个字节,让主循环去处理业务。不要在信号处理函数里printf、malloc、加锁、写复杂日志,这些都不是异步信号安全的,出了问题时表现很随机,像黑匣子。
signal简单但可移植性和语义较弱,工程里更常见sigaction。下面这个片段用sigaction捕获SIGINT,只置位标志,主循环检查后退出。
#include <signal.h> #include <unistd.h> #include <stdio.h> static volatile sig_atomic_t g_stop = 0; static void on_sigint(int signo) { (void)signo; g_stop = 1; /* 只做异步信号安全的事 */ } int main(void) { struct sigaction sa; sa.sa_handler = on_sigint; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) < 0) { perror("sigaction"); return 1; } while (!g_stop) { /* 主循环做真正的清理、落盘、关设备 */ usleep(100 * 1000); } printf("prepare to exit\n"); return 0; }sa_mask是处理函数执行期间额外屏蔽的信号集,sa_flags可以控制是否重启被中断的系统调用。volatile sig_atomic_t保证编译器不会把这个变量优化进寄存器,读写尽量原子。常见错误是把SIGCHLD处理函数写成waitpid加业务逻辑,子进程多时容易丢信号;更稳的做法是处理函数里只置标志,主循环用waitpid(-1, ..., WNOHANG)循环回收。
| 信号 | 默认行为 | 常见用途 | 注意 |
|---|---|---|---|
SIGINT | 终止 | 交互式退出 | 做优雅退出标志 |
SIGTERM | 终止 | 守护进程停止 | 清理资源后再退出 |
SIGCHLD | 忽略 | 子进程退出通知 | 循环waitpid防丢 |
SIGALRM | 终止 | 超时控制 | 定时器精度有限 |
SIGPIPE | 终止 | 写已关闭 socket | 网络程序常忽略 |
3.3 Socket编程:TCP客户端/服务器的最小闭环
Socket 编程在指南里从网络基础知识讲到 TCP/UDP,再到 MQTT 小项目。落地顺序建议先 TCP 回环,再 UDP,再上 MQTT。TCP 服务器的最小闭环是socket、bind、listen、accept、recv/send、close;客户端是socket、connect、send/recv、close。这里最容易忽视的是字节序和地址复用:端口号、IP 地址要经过htons/htonl转换,服务器重启时SO_REUSEADDR能避免“Address already in use”。
/* TCP 服务器核心步骤,省略错误处理细节 */ int listen_fd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(9000); bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)); listen(listen_fd, 5); int conn_fd = accept(listen_fd, NULL, NULL); char buf[128]; ssize_t n = recv(conn_fd, buf, sizeof(buf), 0); /* recv 返回 0 表示对端关闭,返回 -1 看 errno,短读要循环 */参数说明:AF_INET是 IPv4,SOCK_STREAM是 TCP;SO_REUSEADDR解决重启绑定问题;INADDR_ANY表示监听本机所有网卡;listen的 backlog 是等待队列长度,不是最大连接数。recv返回 0 是对端正常关闭,不是“读到空数据”;返回 -1 要区分EINTR、EAGAIN。上板调试时先ping通、再telnet或nc测端口,最后才上业务代码,否则容易把网络不通和代码 bug 混在一起。
UDP 的步骤更短:socket(AF_INET, SOCK_DGRAM, 0)、bind、recvfrom、sendto。没有连接状态,丢包和乱序要应用层自己处理。做设备发现、周期状态上报可以用 UDP;做可靠配置下发、升级指令还是 TCP 或 MQTT 更省心。
3.4 串口与CAN:把设备当文件读写时的参数表
串口在 Linux 下就是设备节点,常见是/dev/ttyS*或/dev/ttyUSB*。打开之后要配termios:波特率、数据位、停止位、校验位、流控,还要把终端切成 raw 模式,关掉回显和行缓冲。很多人第一次写串口,读不到数据不是硬件问题,而是没设CLOCAL | CREAD、没关ICANON,或者超时参数VMIN/VTIME设得不合适。
| 参数 | 常见值 | 作用 | 误设后果 |
|---|---|---|---|
| 波特率 | 115200 | 收发速率 | 乱码或收不到 |
| 数据位 | 8 | 单字节位宽 | 帧错误 |
| 停止位 | 1 | 帧结束 | 边界错 |
| 校验位 | 无 | 错误检测 | 对端不匹配 |
VMIN | 1 | 最少读字节 | 0 会立即返回 |
VTIME | 10 | 十分之一秒超时 | 读阻塞或过早超时 |
CAN 不是普通字符设备,通常用 SocketCAN:socket(PF_CAN, SOCK_RAW, CAN_RAW),bind到can0,用struct can_frame收发。配置比特率要用ip link把接口 up 起来,标准帧和扩展帧、远程帧、错误帧在结构体标志位里区分。CAN 发送成功只代表进了控制器,不代表总线上有节点应答;排查时先看ip -details link show can0的状态和错误计数,再查终端电阻和波特率。
提示:串口和 CAN 的权限问题很常见,先确认当前用户对设备节点有读写权限,再看参数。不要一上来就改内核驱动。
4. 显示与多媒体外设:FrameBuffer、tslib、V4L2与音频的落地顺序
4.1 FrameBuffer:从点屏到画线的最小验证
FrameBuffer 是把显示控制器抽象成一块内存映射区域,应用层通过open("/dev/fb0")、ioctl取屏幕信息、mmap映射显存,然后往映射地址写像素。落地顺序建议:先取FBIOGET_FSCREENINFO和FBIOGET_VSCREENINFO,打印xres、yres、bits_per_pixel、line_length,再映射,最后填一块纯色。不要一上来就画复杂 UI,先确认“整屏能不能变红”,能变红再谈坐标和字体。
struct fb_var_screeninfo vinfo; struct fb_fix_screeninfo finfo; int fb_fd = open("/dev/fb0", O_RDWR); ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo); ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); size_t screensize = finfo.line_length * vinfo.yres; unsigned char *fbp = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); /* 以 32bpp 为例,按 line_length 逐行定位,避免把行对齐当连续像素 */ for (int y = 0; y < vinfo.yres; y++) { unsigned int *row = (unsigned int *)(fbp + y * finfo.line_length); for (int x = 0; x < vinfo.xres; x++) { row[x] = 0x00FF0000; /* 红,具体格式看 vinfo 的通道定义 */ } }关键参数:line_length是每行字节数,可能大于xres * bpp/8,因为硬件有行对齐;bits_per_pixel决定用unsigned char *、unsigned short *还是unsigned int *访问;vinfo.red/green/blue的偏移和长度决定颜色值怎么拼。常见翻车是直接按xres * 4算行偏移,遇到行对齐就花屏;或者写完不munmap、不关fb_fd,反复运行后资源泄漏。
4.2 tslib与输入设备:触摸校准和事件读取
触摸屏在 Linux 下走输入子系统,设备节点通常是/dev/input/event*。原始事件是struct input_event,里面带时间戳、类型、代码、值;应用层要自己处理ABS_X/ABS_Y、BTN_TOUCH、SYN_REPORT。tslib 的价值是把校准、去抖、事件抽象做掉,适合快速验证触摸。步骤是:确认 event 节点、设置TSLIB_TSDEVICE、跑校准、跑测试,再把 tslib 接到自己的界面框架。
| 环境变量 | 作用 | 常用值 |
|---|---|---|
TSLIB_TSDEVICE | 指定触摸设备节点 | /dev/input/event1 |
TSLIB_CALIBFILE | 校准文件路径 | /etc/pointercal |
TSLIB_CONFFILE | tslib 配置文件 | /etc/ts.conf |
TSLIB_PLUGINDIR | 插件目录 | 按库安装路径 |
校准不是玄学,它解决的是触摸坐标和屏幕坐标的线性映射。电阻屏需要校准,电容屏多数已经在上报层面处理好,但多屏、旋转、分辨率变化时仍要检查坐标变换。读事件时注意read可能一次返回多个input_event,要按sizeof(struct input_event)循环解析;SYN_REPORT表示一组事件结束,别把它当无效数据丢掉。
4.3 V4L2摄像头:采集格式、缓冲区与取流顺序
V4L2 的调用顺序比参数更容易记错。典型流程是:open设备、VIDIOC_QUERYCAP查能力、VIDIOC_S_FMT设格式、VIDIOC_REQBUFS申请缓冲区、VIDIOC_QUERYBUF查缓冲区偏移、mmap映射、VIDIOC_QBUF入队、VIDIOC_STREAMON开流、select/poll等可读、VIDIOC_DQBUF出队、处理、再QBUF入队、最后STREAMOFF和munmap。顺序错一步,常见表现就是select一直超时或者DQBUF返回EINVAL。
| 步骤 | ioctl | 作用 | 失败先看 |
|---|---|---|---|
| 查能力 | VIDIOC_QUERYCAP | 是否支持 capture/streaming | 设备节点对不对 |
| 设格式 | VIDIOC_S_FMT | 分辨率、像素格式 | 驱动是否支持该格式 |
| 申请缓冲 | VIDIOC_REQBUFS | 分配内核缓冲区 | 数量是否超限 |
| 查缓冲 | VIDIOC_QUERYBUF | 取偏移和长度 | 是否先 REQBUFS |
| 入队 | VIDIOC_QBUF | 把缓冲交给驱动 | 是否已 mmap |
| 开流 | VIDIOC_STREAMON | 启动采集 | 缓冲是否都入队 |
| 出队 | VIDIOC_DQBUF | 取一帧 | 是否先等可读 |
| 停流 | VIDIOC_STREAMOFF | 停止采集 | 退出前必须调用 |
像素格式要跟后续处理匹配:YUYV 是未压缩,带宽大但兼容性好;MJPEG 是压缩帧,应用层要解码;RGB 格式要看驱动支持。缓冲区数量不是越多越好,2 到 4 个常见,太少容易丢帧,太多增加延迟。采集线程和处理线程之间用队列传递缓冲区索引,不要在两处同时DQBUF。
4.4 音频与PWM/看门狗:外设编程的共性套路
音频、PWM、看门狗看起来分散,套路其实一致:找设备节点、打开、配置参数、读写或ioctl、错误处理、关闭。音频要关注采样率、声道数、样本格式、周期大小;PWM 常见通过 sysfs 的period、duty_cycle、enable控制;看门狗通过/dev/watchdog打开后要按时喂狗,否则超时复位。看门狗最容易被误用成“打开就不管”,结果程序跑飞后系统反复重启,日志还来不及落盘。
| 外设 | 常见节点/接口 | 关键参数 | 注意 |
|---|---|---|---|
| 音频 | ALSA 设备节点 | 采样率、声道、格式 | 周期太小会欠载 |
| PWM | sysfspwmchip* | 周期、占空比、极性 | 先 export 再设置 |
| 看门狗 | /dev/watchdog | 超时时间 | 退出前要关闭或喂狗 |
| GPIO | sysfs 或字符设备 | 方向、电平、中断 | 注意电平有效极性 |
音频调试先确认aplay -l能看到设备,再用固定采样率的短音频测试;PWM 先用示波器或万用表看占空比,再接到电机或背光;看门狗先在测试环境把超时设短,确认喂狗节奏,再上正式环境。外设编程的坑多半不是 API 不会,而是参数单位和生效顺序弄错。
5. 常见问题与排查:五个高频翻车点
5.1 文件与权限类
现象:open返回 -1,perror打印Permission denied,但程序在主机上跑得好好的。原因通常不是代码,而是上板后当前用户对设备节点或目标目录没有写权限,或者根文件系统是只读的。先看ls -l确认节点权限,再看mount确认有没有ro标志,最后才怀疑open标志。解决方式是改权限、换可写目录,或者由启动脚本以合适权限运行;不要用“把整个目录改成 777”当长期方案。
现象:read返回 0,有人当成“读失败”直接退出。原因是对端关闭或文件已到末尾,read返回 0 是正常语义;返回 -1 才看errno。解决方式是把“读到 0”和“读出错”分开处理,循环读时以 0 作为结束条件,以 -1 作为错误分支。串口和 socket 上还要区分EINTR和EAGAIN,前者重试,后者等下一次可读。
5.2 进程与信号类
现象:程序跑一段时间后fork失败,ps里一堆defunct。原因是子进程退出后父进程没waitpid,僵尸进程占进程表项。解决方式是在SIGCHLD处理函数里只置标志,主循环用waitpid(-1, &status, WNOHANG)循环回收;或者用signal(SIGCHLD, SIG_IGN)让内核自动回收,但后者会影响需要拿退出码的场景,别一刀切。
现象:按 Ctrl+C 后程序卡住不退出,或者日志打印到一半乱掉。原因是信号处理函数里调了非异步信号安全函数,或者主循环没检查退出标志。解决方式是把处理函数缩到只置volatile sig_atomic_t标志,把清理、关设备、落盘放回主循环。需要唤醒阻塞在select/poll的主循环时,可以用自管道或 eventfd,别在信号处理函数里做复杂同步。
5.3 显示与外设类
现象:FrameBuffer 填色后屏幕花屏、错位,或者只有一部分变色。原因是把xres * bytes_per_pixel当成行跨度,忽略了line_length的行对齐;也可能是像素格式判断错,32bpp 当 16bpp 写。解决方式是始终用finfo.line_length算行偏移,先打印vinfo和finfo,再用单色块验证。映射大小按line_length * yres算,不要只按xres * yres * 4。
现象:V4L2 开流后select一直超时,或者DQBUF报EINVAL。常见原因是缓冲区没有全部QBUF就STREAMON,或者DQBUF前没等设备可读;也可能是S_FMT设了驱动不支持的格式,驱动悄悄改了格式但应用层还按原格式解析。解决方式是每步ioctl后检查返回值,打印实际格式和缓冲区数量,按“申请、映射、入队、开流、等可读、出队”的顺序逐步验证。
5.4 网络与部署类
现象:TCP 服务器重启时报Address already in use,或者客户端连不上但ping通。前者是端口处于TIME_WAIT,加SO_REUSEADDR并在bind前设置;后者先看防火墙和监听地址,bind到127.0.0.1外部当然连不上。解决方式是服务器监听INADDR_ANY,用ss -lntp确认端口状态,客户端先用短连接测试,再上长连接和心跳。
现象:MQTT 客户端频繁掉线,日志里一会儿连接成功一会儿断开。常见原因是客户端 ID 冲突、保活间隔大于服务端限制、网络切换后没有重连退避。解决方式是保证客户端 ID 唯一,按服务端要求设置 keepalive,断线重连加指数退避,别在回调里做阻塞操作。部署脚本里把配置、证书、日志目录分开,升级时只替换可执行文件和配置,不要整目录覆盖。
6. 进阶验证:把示例代码改成可维护的工程骨架
6.1 用CMake组织多目录工程
示例代码能跑之后,下一步是把散落的.c文件收成工程。CMake 的关键不是写多少命令,而是工具链文件:指定系统名、处理器架构、C 编译器、系统根目录。工具链文件写对,后面cmake ..和make只是顺带的事。下面是一个最小工具链文件,前缀按自己手头的交叉编译器替换。
# toolchain-arm-linux.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) # 系统根目录指向开发板对应的 sysroot,头文件和库都从这里找 set(CMAKE_SYSROOT /opt/arm-sysroot) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) # 程序在主机找,头文件和库在 sysroot 找 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)顶层CMakeLists.txt只做三件事:设置 C 标准、按目录加子工程、把公共头文件路径和库链接集中管理。每个模块一个CMakeLists.txt,生成静态库或可执行文件,避免所有源文件堆在一个目标里。编译时先cmake -DCMAKE_TOOLCHAIN_FILE=...,再make VERBOSE=1看实际编译命令,确认没有误用主机头文件。
6.2 上板验证清单
| 验证项 | 命令/动作 | 通过标准 |
|---|---|---|
| 架构 | file app | 显示 ARM,不是 x86 |
| 依赖库 | ldd app | 没有not found |
| 设备节点 | ls -l /dev/fb0 /dev/input/event* | 节点存在且权限够 |
| 网络 | ping、ss -lntp | 通、端口监听正确 |
| 日志 | 运行后看应用日志 | 关键步骤有错误码 |
| 退出 | Ctrl+C 或kill | 资源释放,无僵尸 |
6.3 一个具体技巧:先跑最小闭环再谈业务
我吃过一次亏:摄像头采集程序写了完整业务,结果上板黑屏,查了两天才发现是S_FMT之后没重新读回实际格式。从那以后我每次把示例改成工程,都先跑一遍最小闭环:打开设备、读一次、写一次、看日志,再谈业务。这个习惯比任何调试工具都管用。希望帮到你。
本文还有配套的精品资源,点击获取