简介:一份围绕粤嵌GEC6818 ARM开发板的项目案例整理文档,面向嵌入式Linux入门者、课程实训学生与需要做课程设计的开发者,帮助其快速了解开发板可落地的功能方向与知识脉络。内容覆盖智能家居控制系统、多媒体与娱乐影音、电子相册、音乐视频播放、五子棋游戏、RFID刷卡、密码锁屏、弹球游戏及智能语音家居等案例,并串联交叉编译、驱动设计流程、文件操作、触摸屏与V4L2、mmap显示、UDP传输等要点,可作为选题参考与实现思路清单。包内共1个docx文件,约13KB,篇幅紧凑,适合当作项目索引与复习提纲使用,按模块浏览即可定位感兴趣的方向。该文档已有5761人学习,说明其在GEC6818教学与实训场景中具有一定参考价值,适合用来对照课程实验、梳理功能模块并规划后续编码实践。
1. 一块 7 寸屏的 ARM 板能练到什么程度:从点亮 LED 到摄像头 UDP 推流
带 7 寸电容屏的 GEC6818 开发板,在 ARM 开发这条路上属于「什么都能往里塞」的那类板子:跑 Linux 操作系统,用 C 语言写应用层,屏幕、触摸、摄像头、音频、串口、网口全焊在板子上,不用额外接一堆模块就能把一个完整产品形态跑通。这份项目合集里装了智能家居控制、电子相册、音乐视频播放、五子棋、RFID 刷卡、密码锁屏、摄像头 UDP 推流等一串案例,粒度从点灯到多线程取流都有。它适合两类人:刚学完 C 语言想找块真硬件落地的,以及做过单片机但从没碰过 Linux 应用层的。真正决定你能不能跑通这些项目的,从来不是案例数量,而是环境、设备节点和时序这三件事。
2. 环境先立住:交叉编译链、串口与网络挂载的三角验证
拿到板子第一天最容易犯的错,是急着找案例代码编译。源码再对,工具链版本、库依赖、文件传输通道有一处不对,你就只能对着一行Segmentation fault猜。我一般会先把「编得出来、传得进去、跑得起来」这三件事分别验证一遍,再动业务代码。
2.1 工具链不是解压完就完事:版本、glibc 与静态链接
这类板子的资料包里通常带一份交叉工具链,名字多为arm-linux-gcc或arm-none-linux-gnueabi,版本集中在 4.9.x 到 5.4 之间。别用主机上的gcc编,编出来是 x86-64 指令集,拷到板子上必然报「无法执行二进制文件」。工具链解压后要做的是把bin目录塞进PATH,并且写进~/.bashrc——只export一次的话,换个终端窗口就失效,第二天你自己都会忘了上次为什么能编过。
# 1. 解压到 /opt,路径不要带中文和空格 sudo tar -xjf arm-linux-gcc-4.9.4.tar.bz2 -C /opt/ # 2. 永久写入 PATH,避免每开一个终端就重新 export echo 'export PATH=/opt/arm-linux-gcc-4.9.4/bin:$PATH' >> ~/.bashrc source ~/.bashrc # 3. 验证三件事:版本对、能编出 ARM 架构、依赖库不超标 arm-linux-gcc -v arm-linux-gcc -o hello hello.c file hello readelf -d hello | grep NEEDEDfile那一步是分水岭:输出里必须出现ARM和EABI5,如果显示x86-64,说明PATH里排在前面的还是主机gcc,用which arm-linux-gcc确认一下到底调到了谁。readelf -d看的是动态依赖,正常情况下只该依赖libc.so.6、libm.so.6、libpthread.so.0这几个基础库;如果你链接了libjpeg、libmad之类,就要确认板子根文件系统里有没有对应版本的.so,没有就静态链接,或者把库一并拷进/lib。
血泪经验是:交叉工具链的 glibc 版本必须不高于板子根文件系统中的 glibc。工具链用 2.23、板子上是 2.19,编出来的程序一跑就报GLIBC_2.23 not found。遇到这种情况别想着升级板子上的库,改工具链或者加-static更省事。加-static会让可执行文件从几十 KB 涨到几 MB,但换来的是「拷过去就能跑」,调阶段我基本都这么干,等功能稳定了再切回动态链接。
2.2 三条传输通道:串口、tftp 与 NFS 各管一段
文件怎么进板子,是第二个卡点。这个平台上常见的三条路各有用处:调试阶段用串口终端(SecureCRT、minicom 都行)的 zmodem 传小文件,几 KB 的测试程序够用;日常迭代用 tftp,一条命令几秒钟;真正要频繁改代码就用 NFS,把主机的目录直接挂到板子上,改完在板子上重新执行即可,省掉反复拷贝。
# 串口通道:板子端执行 rz 接收(需要板子上有 busybox rz/sz) rz -y # tftp 通道:先把编译产物放到主机 tftp 根目录,再在板子上拉取 cp app /var/lib/tftpboot/ tftp -g -r app -l /tmp/app 192.168.1.100 # NFS 通道:主机先导出目录,再在板子上挂载 # 主机 /etc/exports 中加一行:/home/dev/nfsroot *(rw,sync,no_subtree_check) sudo exportfs -arv mount -t nfs -o nolock,vers=3 192.168.1.100:/home/dev/nfsroot /mnt参数上要注意两点。tftp -g -r的-r是远端文件名、-l是本地落盘路径,写反了会得到一个空文件;NFS 的nolock不能省,板子上没跑rpc.statd,不加这个参数会卡在挂载阶段十几秒然后超时。vers=3是给老内核兜底的,不写的话某些 busybox 版本会默认去协商 v4 而直接失败。
网络不通的时候别急着怀疑驱动,先ifconfig eth0 192.168.1.20 netmask 255.255.255.0 up手动配一遍 IP,再ping主机。板子默认 IP 常常和主机不在一个网段,这是最常见的「网线插了但不通」。
2.3 第一次上屏:先验证 framebuffer,再谈业务
在写任何业务逻辑之前,我会先编一个最小程序往屏幕上刷纯色。这一步能一次性验证四件事:交叉编译链对不对、文件传输通不通、程序有没有执行权限、framebuffer 设备节点在不在。刷出一整屏红,后面所有和人机界面相关的项目才有讨论的基础;刷不出来,问题范围立刻收缩到显示子系统。
/* 最小验证:打开 /dev/fb0,mmap 后整屏填充 0x00FF0000(红) */ #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <linux/fb.h> int main(void) { int fd = open("/dev/fb0", O_RDWR); if (fd < 0) { perror("open fb0"); return -1; } struct fb_var_screeninfo vinfo; struct fb_fix_screeninfo finfo; ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); /* 可变信息:分辨率、bpp */ ioctl(fd, FBIOGET_FSCREENINFO, &finfo); /* 固定信息:显存长度、行宽 */ long size = finfo.line_length * vinfo.yres_virtual; unsigned int *mem = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { perror("mmap"); return -1; } printf("screen %ux%u, %u bpp, line_length=%u\n", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, finfo.line_length); for (long i = 0; i < size / 4; i++) mem[i] = 0x00FF0000; /* 32bpp 下按 ARGB 填色 */ munmap(mem, size); close(fd); return 0; }vinfo和finfo的区别值得记住:vinfo是可变的(分辨率、色深能改),finfo是固定的(显存起始物理地址、每行字节数)。算显存大小时必须用finfo.line_length * vinfo.yres_virtual,不能简单用xres * yres * 4——某些分辨率下line_length会做对齐填充,差几个字节就会写越界,表现是刷完屏程序随机崩。这条在 800×480 和 1024×600 之间切换时尤其明显。
提示:如果刷出来颜色偏了(要红出蓝),多半是像素格式不是 ARGB8888 而是 RGB565,用
vinfo.bits_per_pixel判断,16bpp 下要按 565 拆位写。
3. 触摸屏与人机层:input 事件解码、坐标映射与事件循环
这类项目的功能清单看着吓人——电子相册、音乐播放、五子棋、密码锁屏、滑动解锁,剥掉业务外衣之后骨架完全一致:一个事件循环,从触摸设备读坐标,判断落在哪个按钮的矩形区域里,切到对应页面。把这层写对了,往上堆功能就是加法。
3.1 找到真正的触摸设备节点,别猜 event 编号
/dev/input/event0这个编号在不同板子上是变的,同一个板子插拔设备后也可能变。写死编号的代码换块板子就跑不了,典型现象是「触摸完全没反应,但程序不报错」——read一直阻塞,因为读的是一个按钮或电源管理芯片的事件节点。
# 列出所有输入设备,找到名字里带 touch / gt / ft 的那个 cat /proc/bus/input/devices # 往事件节点读原始数据,用手点屏幕,看有没有 EV_ABS / ABS_X 输出 hexdump -C /dev/input/event0/proc/bus/input/devices的Handlers一行会给出eventN,这比在代码里循环试要可靠得多。如果设备名里出现了触摸控制器的型号前缀,那基本就锁定了。找到之后建议在代码里做一次「按名字查找」而不是硬编码:遍历/dev/input/event*,用EVIOCGNAME取名字做匹配,找不到再回落到默认值。多花二十行代码,换来的是一份能在不同批次板子上都跑的代码。
3.2 input_event 到屏幕坐标:一次线性映射加四点校准
触摸屏给的是屏幕物理坐标,需要映射到 LCD 的像素坐标。两者原点方向可能相反,比例也可能不一致,所以必须做线性变换。这个平台上的触摸数据一般是EV_ABS类型,ABS_X/ABS_Y给出原始值,范围由驱动上报(常见是 0 到 1024 或 0 到 800),不是像素坐标。
/* 读取一次触摸事件并换算成屏幕像素坐标 */ #include <linux/input.h> #define RAW_X_MIN 0 #define RAW_X_MAX 800 #define RAW_Y_MIN 0 #define RAW_Y_MAX 480 int touch_read(int fd, int screen_w, int screen_h, int *sx, int *sy) { struct input_event ev; static int raw_x, raw_y, pressed = 0; while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) { if (ev.type == EV_ABS) { if (ev.code == ABS_X) raw_x = ev.value; if (ev.code == ABS_Y) raw_y = ev.value; } else if (ev.type == EV_KEY && ev.code == BTN_TOUCH) { pressed = ev.value; /* 1 按下,0 抬起 */ } else if (ev.type == EV_SYN) { if (!pressed) continue; /* 线性映射:原始值 -> 像素坐标 */ *sx = (raw_x - RAW_X_MIN) * screen_w / (RAW_X_MAX - RAW_X_MIN); *sy = (raw_y - RAW_Y_MIN) * screen_h / (RAW_Y_MAX - RAW_Y_MIN); return 1; /* 返回一次有效点击 */ } } return 0; }逻辑上关键在EV_SYN这个同步事件:一次完整的触摸动作由若干个EV_ABS/EV_KEY加一个EV_SYN收尾,在EV_SYN到来时才认为坐标齐了。如果在每个EV_ABS上就触发业务判断,会读到 X 变了 Y 还没更新的中间态,表现是点击位置偶尔偏几十像素,这类「玄学偏移」基本都是这个原因。
RAW_X_MAX这几个宏必须按实际硬件实测,不能照抄。最快的标定办法是写个程序把收到的原始ev.value打印出来,用手指点屏幕左上角和右下角记下两组值,填进去即可。要求再高一点就用 tslib,跑一次ts_calibrate生成/etc/pointercal,之后用ts_read拿到的直接就是像素坐标,缺点是给根文件系统增加了一个依赖,裁剪过的系统里不一定有。
注意:X 和 Y 方向如果反了(点左边亮右边),把映射公式里的
(raw - min)换成(max - raw),别去改硬件接线。
3.3 事件循环的组织方式:单线程轮询与页面状态机
功能一多,最容易写崩的地方是事件循环。常见写法是把电子相册、音乐播放、五子棋全都塞进一个while(1)里,用一堆if判断当前在哪个页面,最后代码长到没人敢改,加一个功能就碰坏另一处。我的做法是定义一个页面结构体,每个页面只暴露enter、exit、draw、on_touch四个函数指针,主循环只做「取事件 → 派发给当前页面」这一件事。
typedef struct page { void (*enter)(void); void (*exit)(void); void (*draw)(void); int (*on_touch)(int x, int y); /* 返回 0 留在本页,返回其他值是目标页号 */ } page_t; static const page_t *pages[PAGE_MAX]; static int cur = PAGE_HOME; while (running) { int x, y; if (touch_read(touch_fd, SCR_W, SCR_H, &x, &y)) { int next = pages[cur]->on_touch(x, y); if (next != cur) { pages[cur]->exit(); cur = next; pages[cur]->enter(); pages[cur]->draw(); } } pages[cur]->draw(); /* 有动画的页面在这里推进帧 */ }这样的结构下,新增一个页面就是新增一个文件加一次注册,主循环一行都不用改。音频播放、幻灯片自动翻页这类需要「定时推进」的逻辑,放在各自页面的draw里按时间戳判断,别开一堆线程——这块板子上多线程共享 framebuffer 很容易出现半屏花屏,调试成本极高。真需要后台播放音乐,就单独起一个线程只碰 ALSA,不碰显存。
draw每轮都调用看着浪费,实际在 800×480 分辨率下刷一帧纯色或贴一张已解码的图,耗时在几毫秒量级,用usleep(20000)控制到 50Hz 完全够用,还能顺手压住 CPU 占用。真正的性能瓶颈通常不在绘制,而在每帧都重新解码 JPEG——这个坑在电子相册项目里几乎人人踩一遍。
4. 摄像头与多媒体:V4L2 取流、YUV 转换与 UDP 上传
实训项目里最有含金量的一块,是把摄像头采集到的图像通过 UDP 传到主机显示。这条链路串起了 V4L2 编程、framebuffer 显示、触摸控制和 socket 网络编程,几乎覆盖了这类板子应用层的主要知识点。它能跑通,说明你对「设备文件 + ioctl + mmap + 非阻塞 IO」这套组合已经熟了。
4.1 V4L2 取流的五步固定套路
V4L2 的调用顺序是固定的,顺序错了就是Invalid argument:查询能力 → 设置格式 → 申请缓冲 → 映射并入队 → 开流。中间任何一步返回值没检查,后面都会以一个完全不相关的错误码报出来,这是它最不友好的地方。
/* V4L2 采集骨架:打开、设格式、申请 4 个 mmap 缓冲、开流 */ #include <linux/videodev2.h> int cam_open(const char *dev, int w, int h) { int fd = open(dev, O_RDWR | O_NONBLOCK); if (fd < 0) { perror("open video"); return -1; } struct v4l2_capability cap; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { perror("QUERYCAP"); return -1; } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, "not a capture device\n"); return -1; } struct v4l2_format fmt; memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = w; fmt.fmt.pix.height = h; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; /* 多数 USB 摄像头默认给 YUYV */ fmt.fmt.pix.field = V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("S_FMT"); return -1; } /* S_FMT 可能被驱动改写,务必回读 fmt 而不是相信自己设的值 */ printf("actual: %ux%u, fourcc=%c%c%c%c\n", fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF, (fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF); struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("REQBUFS"); return -1; } for (int i = 0; i < req.count; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; ioctl(fd, VIDIOC_QUERYBUF, &buf); /* 拿长度和偏移 */ void *p = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); ioctl(fd, VIDIOC_QBUF, &buf); /* 入队,交给驱动 */ } enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, &type); return fd; }S_FMT之后回读fmt这一步救过很多人。摄像头不一定支持你请求的分辨率,驱动会静默改成最接近的支持值,然后你的缓冲区按 640×480 分配、实际收到 320×240 的数据,画面一半是花屏。REQBUFS的count给 4 是比较稳的经验值:给 1 容易在取帧处理时丢帧,给太多会占掉宝贵的内存。VIDIOC_QBUF在mmap之后立刻调用,不要等到开流才入队,否则驱动手里没有可写缓冲,开流直接失败。
取帧循环用非阻塞VIDIOC_DQBUF,没数据时返回EAGAIN,加个usleep重试就行。拿到帧处理完必须立刻VIDIOC_QBUF还回去,忘了还的话,几帧之后DQBUF就永远失败——这个错误几乎每个初写 V4L2 的人都犯过一次。
4.2 YUYV 转 RGB 与 UDP 分包发送
摄像头给的多是 YUYV(每 2 个像素占 4 字节,Y0 U Y1 V),而 framebuffer 要的是 RGB888 或 RGB565,中间必须转一次。转换公式本身不难,麻烦的是性能:640×480 的帧,逐像素调函数转,在这块板子上能吃掉几十毫秒,20fps 就别想了。我的做法是把转换写成内层循环展开的形式,同时固定用整数运算,避免浮点。
/* YUYV -> RGB888,一次处理两个像素,整数近似公式 */ static inline void yuv2rgb(int y, int u, int v, unsigned char *out) { int c = y - 16, d = u - 128, e = v - 128; int r = (298 * c + 409 * e + 128) >> 8; int g = (298 * c - 100 * d - 208 * e + 128) >> 8; int b = (298 * c + 516 * d + 128) >> 8; out[0] = r < 0 ? 0 : (r > 255 ? 255 : r); out[1] = g < 0 ? 0 : (g > 255 ? 255 : g); out[2] = b < 0 ? 0 : (b > 255 ? 255 : b); } void yuyv_to_rgb(const unsigned char *src, unsigned char *dst, int pixels) { for (int i = 0; i < pixels / 2; i++) { int y0 = src[0], u = src[1], y1 = src[2], v = src[3]; yuv2rgb(y0, u, v, dst); /* 前一个像素 */ yuv2rgb(y1, u, v, dst + 3); /* 后一个像素共用 UV */ src += 4; dst += 6; } }UDP 发送这一侧的核心不是sendto,而是分包规则。一帧 RGB888 的 640×480 数据接近 900KB,UDP 单个数据报超过 MTU(1500 字节)就会被 IP 层分片,任何一片丢了整帧报废,而 UDP 本身不重传。常见做法是自己分片:每个包 1400 字节左右,包头上带一个自定义结构体,包括帧号、包序号、本帧总包数。
struct pkt_head { unsigned int frame_id; /* 帧号,接收端用来判断是否同一帧 */ unsigned short seq; /* 包序号,从 0 开始 */ unsigned short total; /* 本帧总包数,seq == total-1 表示结束 */ unsigned short len; /* 本包有效数据长度 */ }; /* 发送一帧:切块、打包头、sendto 到目标地址 */ for (int i = 0, off = 0; off < frame_len; i++, off += CHUNK) { int n = (frame_len - off) > CHUNK ? CHUNK : (frame_len - off); struct pkt_head h = { .frame_id = fid, .seq = i, .total = (frame_len + CHUNK - 1) / CHUNK, .len = n }; memcpy(txbuf, &h, sizeof(h)); memcpy(txbuf + sizeof(h), frame + off, n); sendto(sock, txbuf, sizeof(h) + n, 0, (struct sockaddr *)&dst, sizeof(dst)); }CHUNK取 1400 是留了余量:以太网 MTU 1500,减去 IP 头 20 字节和 UDP 头 8 字节,理论上限 1472,留出头的空间更保险。frame_id单调自增,接收端靠它判断有没有跨帧错位——如果收到的包frame_id变了但上一帧还没收齐,直接丢掉上一帧重新开始,比等重传现实得多。
4.3 带宽边界:什么时候该放弃 UDP 裸传
实际跑起来会发现一个硬约束:640×480 RGB888 一帧 900KB,20fps 就是 18MB/s,百兆网口理论带宽才 12.5MB/s,必然卡。把分辨率降到 320×240、帧率降到 10fps,降到 2.3MB/s 就舒服了。或者只传 JPEG 压缩后的数据,一帧压到几十 KB,代价是板子上要多跑一个编码器,CPU 占用上去。
我一般先按 320×240、10fps 把链路跑通,确认显示端能连续出画面,再逐步往上加。反过来一上来就设 640×480 30fps,画面卡成幻灯片的时候你会怀疑是代码问题,其实是带宽早就满了。判断方法也简单:接收端统计每秒收到的字节数,如果稳定贴着网口上限,那就是带宽瓶颈,跟代码无关。
5. 避坑与排查:五条卡过三天的现场记录
这些坑的共同点是「代码看起来没问题,现象却完全不符合预期」,靠读代码找不出来,只能靠对设备和时序的理解去定位。
5.1 上电与外设类
现象:程序在主机上跑得好好的,拷到板子上执行报Permission denied,ls -l看权限是rw-r--r--。原因是通过 tftp 或串口传过来的文件默认不带执行位,不是权限被限制。解决是chmod +x app,或者在传输脚本里带上一句。这个坑低级但极高频,我后来养成习惯,传输命令后面直接串一条chmod。
现象:刷屏程序偶尔崩,位置随机,改小分辨率就没事。原因是显存大小算错,用了xres * yres * 4而不是finfo.line_length * yres_virtual。部分分辨率下行宽会做对齐,实际每行字节数比xres * 4大,mmap的长度不够,写入就越界。解决是按finfo计算,并且munmap用同一个长度。
现象:触摸完全无响应,程序不报错也不退出。原因是设备节点写死成event0,而实际触摸挂在event2上,read阻塞在一个不会产生触摸事件的节点上。解决是从/proc/bus/input/devices里按名字匹配,或者用EVIOCGNAME在代码里遍历查找。
5.2 多媒体与网络类
现象:摄像头VIDIOC_STREAMON返回Invalid argument。原因通常是mmap之后忘了VIDIOC_QBUF,驱动手里一个可用缓冲都没有。另一个可能是fmt.type或req.memory没填对。解决是严格按「查询 → 设格式 → 申请 → 映射 → 入队 → 开流」的顺序,每一步检查返回值,出错立刻打perror定位到具体是哪一步。
现象:UDP 接收端画面撕裂,一半新一半旧。原因是发送端在空中直接覆盖了正在被DQBUF处理的缓冲区,或者接收端在收齐一帧之前就把它送去显示了。解决是发送端持有独立的发送缓冲、不要直接sendto驱动映射的地址;接收端用seq和total做完整性校验,收齐再显示。这个问题的隐蔽之处在于本地网线直连时几乎不出现,一换成路由器就开始花屏,很容易被误判成网络问题。
注意:所有涉及
ioctl的调用都养成检查返回值的习惯。这类接口失败时不设errno的情况不少,返回值是唯一线索。
6. 把散装 Demo 收成一个工程:Makefile、自启与一次冷启动验证
案例代码散落在各个目录、每个都带着自己的编译命令,这种状态做演示可以,交付就不行了。收尾阶段我会做两件事:用一个 Makefile 把所有模块管住,让make一条命令产出整个程序;把启动脚本写进开机流程,然后断网、断电、冷启动验证三遍,确认不依赖任何手工操作。
# 交叉编译统一入口:所有模块共用一套标志 CC := arm-linux-gcc CFLAGS := -Wall -O2 -I./include -pthread LDFLAGS := -lpthread -lm TARGET := app SRCS := $(wildcard src/*.c) OBJS := $(SRCS:.c=.o) $(TARGET): $(OBJS) $(CC) -o $@ $^ $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET) install: cp $(TARGET) /home/dev/nfsroot/wildcard自动收集源文件,新增模块不用改 Makefile;-pthread放CFLAGS里是为了让预处理阶段就带上线程宏,只放在链接阶段某些头文件行为会不一致。install目标直接拷到 NFS 目录,配合板子上的挂载,改完make install就能跑。
开机自启我倾向放在/etc/init.d/rcS末尾,加一行后台启动并把日志重定向到文件:
# /etc/init.d/rcS 末尾追加,& 让启动流程不被阻塞,日志留作事后排查 /mnt/app > /tmp/app.log 2>&1 &容易翻车的地方是启动时机:rcS执行时网络和 framebuffer 未必都初始化完了,程序一上来就open("/dev/fb0")可能失败。可靠的做法是在程序内部对关键设备做带超时的重试,最多等 5 秒,而不是在脚本里sleep——sleep的具体秒数你永远猜不准,换一批板子就得改。
冷启动验证我固定走三步:拔电源等十秒再上电,看功能是否自动起来;拔网线再插上,看 UDP 那条链路能否自愈;连续开关机五次,看有没有偶发启动失败。第三步最容易被跳过,但恰恰是暴露时序问题最有效的手段。从那以后,我每次交付这类板子上的项目,都会强制走一遍「断电冷启动 × 5 + 断网重连 × 1」,宁可多花二十分钟,也不想在现场被人问「为什么这次没起来」。希望帮到你。
本文还有配套的精品资源,点击获取