简介这份文档围绕粤嵌GEC6818开发板项目实践展开面向学习ARM嵌入式Linux与C语言编程的初学者及课程设计人群可用于了解开发板外设驱动、交叉编译和综合项目组织方式。整包仅含1个docx文件约13KB内容紧凑便于快速浏览与检索。内容梳理智能家居控制、多媒体影音、电子相册、RFID刷卡、五子棋、密码锁屏和智能语音家居等案例并涉及触摸屏交互、V4L2摄像头采集、mmap显示及UDP上传等实现思路。已有5763人学习可作为嵌入式课程实训、项目选题与功能拆解时的参考资料也能帮助读者对照开发板环境梳理模块边界、外设调用与调试方向。1. GEC6818 这套板子资源适合把 ARM 开发 Linux 的整条链路一次跑通很多人第一次拿到 GEC6818 这类 ARM 开发板第一反应是刷个系统、点个灯然后就卡在“下一步干什么”。这份项目资料的价值不在于教你点亮一个 LED而在于它把 C 语言编程、Linux 系统调用、交叉编译、framebuffer、触摸屏输入、V4L2 摄像头采集、UDP 上传这一整条链路串成了若干个能独立跑起来的小系统智能家居控制界面、电子相册、音乐视频播放、五子棋、密码锁、弹球游戏以及摄像头图像走 UDP 回传上位机。它适合两类人一类是刚学完 C 语言、想找个真实硬件把知识点落地的学生另一类是已经会写 Linux 应用、但没碰过 framebuffer 和 V4L2 的开发者想拿一块成本不高的板子把驱动接口摸一遍。读完你至少能判断哪些 demo 可以直接抄哪些地方会翻车以及怎么把散装代码收成一个能演示的工程。2. 交叉编译环境与文件传输从工具链到板子上真正跑起来2.1 为什么必须在主机上交叉编译而不是在板子上编译GEC6818 这类板子的定位是运行目标程序不是开发机。板子上通常没有完整的 gcc、头文件和 make存储空间和内存也不适合跑一次大型编译更现实的问题是编译一个带 V4L2 和 framebuffer 的工程需要内核头文件和对应架构的库板端环境往往被裁剪得很干净。所以常规做法是在 x86 主机上装一套 ARM 交叉工具链把源码编译成 ARM 可执行文件再传到板子上运行。工具链命名里几个字段要能看懂arm是目标架构linux是目标操作系统gnueabihf表示使用 glibc 且是硬浮点 ABI。GEC6818 的 SoC 是 Cortex-A53 架构属于 ARMv8 的 64 位核但项目里跑的通常是 32 位 ARM 用户态系统所以工具链要用 32 位的那一套别拿 aarch64 的工具链去编编出来的程序在板子上会直接报“cannot execute binary file”。选型上有一个容易忽略的点工具链的 glibc 版本不能高于板子根文件系统里的 glibc 版本。主机上装了新版工具链编出来的程序依赖GLIBC_2.29板子上只有GLIBC_2.23运行时报版本符号找不到。血泪经验是优先用板子厂商配套的那套工具链或者先用strings看一眼板子/lib/libc.so.6的版本再决定主机装哪个。2.2 环境变量与第一个能跑通的 ARM 程序工具链解压后一般是个带bin/的目录。不建议把路径写死到每一条命令里直接在 shell 里导出或者写进~/.bashrc。下面这段是常见的初始化方式路径按自己实际解压位置改。# 工具链根目录改成自己解压出来的路径 export TOOLCHAIN/opt/toolchain/gcc-arm-linux-gnueabihf export PATH$TOOLCHAIN/bin:$PATH # 约定交叉编译器前缀Makefile 里直接引用这个变量 export CROSS_COMPILEarm-linux-gnueabihf- # 验证能打印出版本号说明 PATH 生效 ${CROSS_COMPILE}gcc -v参数说明CROSS_COMPILE这个变量名是 Linux 内核和大量开源工程约定的前缀后面拼gcc、ld、strip就是具体工具。把它导出来之后Makefile 里写CC : $(CROSS_COMPILE)gcc就能自动切换主机编译和交叉编译不需要改代码。先写一个最小的程序确认链路通了不要一上来就编整个工程。/* hello_arm.c只验证交叉编译产物能在板子上跑 */ #include stdio.h #include sys/utsname.h int main(void) { struct utsname u; if (uname(u) 0) { printf(machine: %s\n, u.machine); /* 期望输出 armv7l 或 armv8l */ } printf(hello from cross compile\n); return 0; }编译并检查产物架构# -march 指定指令集-g 保留调试符号-O2 开常规优化 ${CROSS_COMPILE}gcc -marcharmv7-a -g -O2 hello_arm.c -o hello_arm # file 看架构出现 ARM 字样才对出现 x86-64 说明用错了编译器 file hello_arm # readelf 看动态链接依赖确认没有引用主机上才有的库 ${CROSS_COMPILE}readelf -d hello_arm | head -20逻辑说明file判断的是 ELF 头里的机器类型这是最直接的一道自检readelf -d看的是.dynamic段重点确认NEEDED里只有libc.so.6这类板子上必然存在的库。如果你不小心用主机 gcc 编了一遍file会显示x86-64传上去执行就是Exec format error这个报错基本可以一口断定是架构不对不用再怀疑别的。2.3 文件传输NFS、tftp、ZMODEM 三条路怎么选传文件这件事不同阶段用不同方案混着用会浪费大量时间。方式适用阶段优点明显短板NFS 挂载开发调试期主机改完代码直接跑不用反复传需要主机开 NFS 服务网络配置要对tftp传单个内核、设备树、小文件板端 busybox 一般自带客户端无认证、无断点续传大文件容易失败ZMODEMsz/rz串口终端里临时传小文件不用配网络SecureCRT 类终端直接支持走串口速度慢只适合几 MB 以内的文件U 盘 / SD 卡交付演示、批量拷贝稳定、不依赖网络每次要拔插迭代效率低开发期我一般会用 NFS主机上exports一个目录板子上mount -t nfs -o nolock挂到/mnt然后把可执行文件直接编译输出到那个目录。这样改一行代码重新 make板子上再执行一次就行省掉了“传—改权限—运行”的三步。注意nolock这个参数板子上的 busybox 有时没起 rpc.statd不加会卡在挂载阶段。ZMODEM 的用法在原项目里也提到了终端软件里选中文件发送板子端执行rz接收反过来板子端sz 文件名是把文件发回主机。它的实际价值是网络还没配通、或者只是临时看一眼日志文件时能救急。传输完别忘了权限chmod x hello_arm。FAT 格式的 U 盘和 SD 卡没有可执行位概念拷进去再拷出来可执行权限会丢这一步漏了会得到一个很迷惑的Permission denied。2.4 一份够用的 Makefile 骨架工程一旦超过三个源文件手敲编译命令就会开始出错。下面这份骨架覆盖了交叉编译、调试符号、清理三个最基本的需求可以直接抄。# 交叉编译器前缀和 shell 里导出的保持一致 CROSS_COMPILE ? arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc STRIP : $(CROSS_COMPILE)strip # -Wall 打开常见警告-g 保留调试符号-I 指定头文件目录 CFLAGS : -Wall -O2 -g -I./include LDFLAGS : -lpthread TARGET : app SRCS : $(wildcard src/*.c) OBJS : $(SRCS:.c.o) $(TARGET): $(OBJS) $(CC) $^ -o $ $(LDFLAGS) # 每个 .c 编译成 .o$ 是第一个依赖$ 是目标 %.o: %.c $(CC) $(CFLAGS) -c $ -o $ # 发布版本去掉调试符号体积能小一半以上 release: $(TARGET) $(STRIP) $(TARGET) clean: rm -f $(OBJS) $(TARGET) .PHONY: clean release逻辑说明wildcard自动收集src下所有.c加文件不用改 Makefile%.o: %.c是模式规则把编译和链接拆开改一个文件只重编一个.o。release目标单独做 strip是因为调试阶段需要-g保留符号配合 gdbserver但交付时符号表会让二进制大出一截而且没必要暴露函数名。调试环节补一句板子上跑gdbserver :1234 ./app主机上arm-linux-gnueabihf-gdb ./app然后target remote 板子IP:1234可以在主机上单步调试板子上的程序。这个组合在排查段错误时比 printf 高效得多尤其是崩溃点在库函数内部的时候。3. 触摸屏与 LCD把功能选择界面做成可扩展的菜单框架3.1 读触摸事件/dev/input/eventX 与坐标范围GEC6818 配的 7 寸电容屏在 Linux 下走输入子系统应用层看到的是/dev/input/event0这类字符设备。先确认是哪个节点别猜。# 列出所有输入设备看 Name 里带 touch 的那个对应 event 几 cat /proc/bus/input/devices # 直接读原始事件流手指按屏幕时终端会刷出二进制CtrlC 退出 hexdump -C /dev/input/event1 | head确认节点后应用层读的是固定结构体struct input_event包含type、code、value三个关键字段。触摸屏一般上报EV_ABS类型的ABS_X、ABS_Y单点或ABS_MT_POSITION_X、ABS_MT_POSITION_Y多点。这里第一个坑是屏幕上报的坐标范围不等于屏幕分辨率。1024×600 的屏驱动上报的 X 范围可能是 0 到 1023也可能是 0 到 4095取决于触摸 IC 和驱动配置。正确做法是用EVIOCGABS这个 ioctl 在运行时把范围读出来而不是硬编码。/* 读取触摸屏 X/Y 的原始范围避免硬编码分辨率 */ #include linux/input.h #include sys/ioctl.h #include fcntl.h #include stdio.h static void query_abs_range(int fd, int code, const char *name) { struct input_absinfo info; if (ioctl(fd, EVIOCGABS(code), info) 0) { printf(%s: min%d max%d fuzz%d flat%d\n, name, info.minimum, info.maximum, info.fuzz, info.flat); } } int main(void) { int fd open(/dev/input/event1, O_RDONLY); if (fd 0) { perror(open event); return -1; } query_abs_range(fd, ABS_X, ABS_X); query_abs_range(fd, ABS_Y, ABS_Y); close(fd); return 0; }参数说明input_absinfo里minimum和maximum就是该轴的有效范围fuzz是驱动做过的噪声过滤值flat是死区。拿到范围后映射到屏幕坐标就是一次线性变换screen_x (raw_x - min) * lcd_width / (max - min)。这一步不做触摸点会整体偏移或者只在屏幕一角响应现象很像“触摸不准”实际是没做坐标换算。3.2 framebuffer 直接画mmap 映射与像素格式没有桌面环境界面就是往 framebuffer 里写像素。流程固定打开/dev/fb0用FBIOGET_VSCREENINFO拿分辨率、位深、虚拟分辨率用FBIOGET_FSCREENINFO拿line_length然后mmap一段内存之后所有绘制都是往这块内存写。/* 初始化 framebuffer拿到显存起始地址和行宽 */ #include linux/fb.h #include sys/mman.h #include sys/ioctl.h #include fcntl.h #include unistd.h #include stdio.h static unsigned char *fb_mem; static struct fb_var_screeninfo vinfo; static struct fb_fix_screeninfo finfo; int fb_init(const char *dev) { int fd open(dev, O_RDWR); if (fd 0) { perror(open fb); return -1; } if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) 0) return -1; if (ioctl(fd, FBIOGET_FSCREENINFO, finfo) 0) return -1; printf(res%dx%d bpp%d line_length%d\n, vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, finfo.line_length); /* 映射长度用虚拟分辨率乘行宽不能用 xres*yres*bpp/8 */ size_t map_size finfo.line_length * vinfo.yres_virtual; fb_mem mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb_mem MAP_FAILED) { perror(mmap fb); return -1; } return 0; } /* 写一个像素RGB565 格式下 r/g/b 各占 5/6/5 位 */ static inline void put_pixel(int x, int y, unsigned short color) { unsigned short *p (unsigned short *)(fb_mem y * finfo.line_length x * 2); *p color; }逻辑说明vinfo.bits_per_pixel决定一个像素占几个字节16 位就是 RGB56532 位一般是 ARGB8888。finfo.line_length是每行实际字节数它可能大于xres * bpp / 8因为硬件对行宽做了对齐所以计算偏移必须用line_length而不是xres。这是颜色错位、画面斜切这类问题的根源我见过不止一次有人在这里写成xres * 2。参数说明mmap的长度按line_length * yres_virtual算虚拟分辨率可能比实际可见分辨率大双缓冲少映射了会越界写症状是程序偶尔崩、偶尔不崩很难查。写显存时也建议先写yres_virtual内的内存再通过FBIOPAN_DISPLAY切换避免撕裂。触摸和显示这两层搞定界面框架就有基础了。3.3 用函数指针表把界面做成可扩展菜单原项目里的功能选择界面包含电子相册、游戏、音乐播放、视频播放四五个入口。如果写成一大串if/else加一个功能要动三处代码。可行的做法是定义一张页面表每个页面挂两个函数指针绘制和触摸处理。/* 页面表新增功能只加一行不改主循环 */ typedef struct { const char *name; void (*draw)(void); /* 绘制整页 */ int (*on_touch)(int x, int y); /* 返回 1 表示需要重绘 */ } page_t; static void home_draw(void); static int home_touch(int x, int y); static void photo_draw(void); static int photo_touch(int x, int y); static const page_t pages[] { { 电子相册, photo_draw, photo_touch }, { 音乐播放, music_draw, music_touch }, { 五子棋, chess_draw, chess_touch }, { 设置, setting_draw, setting_touch }, }; #define PAGE_CNT (sizeof(pages) / sizeof(pages[0]))主循环里只做三件事读一个input_event把坐标归一化交给当前页面的on_touch返回 1 就调draw重绘按返回键就让cur_page归零回主页。这样每个功能模块只关心自己的绘制区域和点击判定互不干扰。新增一个弹球游戏就是写两个函数、在数组里加一行主循环一行不动。这种结构在演示时特别有用临时想加个密码锁屏页面五分钟能挂上去。提示触摸事件会连续上报按下、移动、抬起三类菜单点击建议只在抬起EV_KEY且value 0时判定按下就触发会导致一次触摸触发两次功能切换。4. 摄像头链路V4L2 采集、mmap 显示与 UDP 上传4.1 V4L2 的固定调用序列V4L2 的接口看起来吓人但采集一帧的流程是固定的按顺序走完就行打开设备、查询能力、设置格式、申请缓冲、映射缓冲、入队、开流、取帧、再入队。中间任何一步返回值小于 0 都要打印errno并退出不要跳过去继续走后面的调用会因为状态不对而给出完全无关的错误码。/* V4L2 初始化到开流的核心序列省略错误打印 */ #include linux/videodev2.h #include sys/ioctl.h #include sys/mman.h #include fcntl.h #include string.h #include stdio.h #define BUF_CNT 4 static int v4l2_start(int *fd_out, void **bufs, size_t *lens) { int fd open(/dev/video0, O_RDWR); if (fd 0) { perror(open video); return -1; } struct v4l2_capability cap; ioctl(fd, VIDIOC_QUERYCAP, cap); /* 设置采集格式优先 YUYV部分摄像头只支持 MJPEG */ struct v4l2_format fmt; memset(fmt, 0, sizeof(fmt)); 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; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(S_FMT); return -1; } /* 申请 4 个缓冲驱动可能返回比请求少的数量以实际值为准 */ struct v4l2_requestbuffers req; memset(req, 0, sizeof(req)); req.count BUF_CNT; 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 (int)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); /* 每个缓冲单独映射长度由驱动给出 */ bufs[i] mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); lens[i] buf.length; ioctl(fd, VIDIOC_QBUF, buf); /* 先入队再开流 */ } int type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, type); *fd_out fd; return 0; }逻辑说明REQBUFS返回的count可能小于请求值循环里必须用req.count而不是宏BUF_CNT否则会映射到未申请的缓冲。S_FMT之后最好再调一次G_FMT把驱动实际接受的格式读回来因为驱动可能把你的 640×480 YUYV 改成 640×480 MJPEG 或者干脆改了分辨率后面按错误的分辨率算 YUV 大小会直接花屏。参数说明memory选V4L2_MEMORY_MMAP表示用内核缓冲映射到用户态零拷贝适合帧率要求不高的场景如果要用 DMA 到自己的内存得走USERPTR但麻烦很多教学项目一般用不到。取帧用select或poll等可读然后VIDIOC_DQBUF拿到一个buf.index处理完再VIDIOC_QBUF还回去。不还回去缓冲池很快就空了select会一直阻塞看起来像“摄像头挂死”。4.2 YUYV 转 RGB565 再上屏摄像头输出的 YUYV 是每两个像素共用一组 Y排列是Y0 U Y1 V。要在 RGB565 的 framebuffer 上显示得做一次颜色空间转换。公式用整数近似就够浮点版本在 A53 上跑 640×48030fps 会明显掉帧。/* YUYV - RGB565一次处理两个像素 */ static inline unsigned short yuv_to_rgb565(int y, int u, int v) { int r y (int)(1.402f * (v - 128)); int g y - (int)(0.344f * (u - 128) 0.714f * (v - 128)); int b y (int)(1.772f * (u - 128)); /* 钳位到 0-255越界会溢出成完全错误的颜色 */ r r 0 ? 0 : (r 255 ? 255 : r); g g 0 ? 0 : (g 255 ? 255 : g); b b 0 ? 0 : (b 255 ? 255 : b); return ((r 3) 11) | ((g 2) 5) | (b 3); } /* 把一整帧 YUYV 写进显存x_off/y_off 是左上角偏移 */ static void yuyv_to_fb(const unsigned char *yuyv, int w, int h, int x_off, int y_off) { for (int y 0; y h; y) { const unsigned char *row yuyv y * w * 2; for (int x 0; x w; x 2) { int Y0 row[0], U row[1], Y1 row[2], V row[3]; put_pixel(x_off x, y_off y, yuv_to_rgb565(Y0, U, V)); put_pixel(x_off x 1, y_off y, yuv_to_rgb565(Y1, U, V)); row 4; } } }逻辑说明YUYV 一行的字节数是width * 2所以行偏移乘 2内层循环每次前进两个像素、四个字节。put_pixel用的是第 3 章里那个带line_length的版本不能换成x * 2否则图像会斜。参数说明钳位那三行是必须的。YUV 转 RGB 的系数会让结果轻微超出 0-255不钳位直接强转成unsigned short的低位会让本来接近纯色的区域出现刺眼的色斑看起来像摄像头坏了其实是数值溢出。4.3 UDP 上传自定义帧头解决粘包和丢帧原项目里有一项是把摄像头采集的图像通过 UDP 上传。UDP 本身不保证顺序、不保证到达图像数据动辄几百 KB必须自己切包并加帧头。常见的做法是定义一个固定长度的头结构带上帧号、包序号、包总数和数据长度。/* 自定义协议头结构体要按 1 字节对齐避免编译器插入填充 */ #pragma pack(push, 1) typedef struct { unsigned int magic; /* 固定魔数比如 0x47554338 */ unsigned int frame_id; /* 帧序号接收端按它重组 */ unsigned short pkt_idx; /* 当前包序号从 0 开始 */ unsigned short pkt_total; /* 这一帧一共切了几包 */ unsigned short data_len; /* 本包有效数据长度 */ unsigned short reserved; } img_hdr_t; #pragma pack(pop) /* 按 1400 字节切片发送避免 IP 层分片 */ int send_frame(int sock, const unsigned char *data, int len, unsigned int fid) { const int payload 1400; int total (len payload - 1) / payload; unsigned char pkt[sizeof(img_hdr_t) 1400]; for (int i 0; i total; i) { int off i * payload; int n (len - off payload) ? payload : (len - off); img_hdr_t *h (img_hdr_t *)pkt; h-magic 0x47554338; h-frame_id fid; h-pkt_idx i; h-pkt_total total; h-data_len n; memcpy(pkt sizeof(img_hdr_t), data off, n); if (sendto(sock, pkt, sizeof(img_hdr_t) n, 0, (struct sockaddr *)dst, sizeof(dst)) 0) { perror(sendto); return -1; } } return 0; }逻辑说明#pragma pack(1)是必须的。结构体里unsigned int和unsigned short混排默认对齐会让pkt_idx前面插入 2 字节填充接收端如果按另一种对齐方式解析字段全错位现象是“能收到数据但帧号是乱的”。参数说明载荷取 1400 是个经验值。以太网 MTU 是 1500减去 IP 头 20 字节和 UDP 头 8 字节理论上限 1472留一点余量比较稳。超过这个值IP 层会分片任意一片丢了整帧都拼不回来而在局域网上分片丢失的概率并不低。帧号用来丢弃过期帧接收端发现frame_id比当前正在组的帧旧直接扔掉不要试图拼接否则画面会来回跳。5. 避坑与排查五个卡住半天的现场问题5.1 程序在板子上直接 Segmentation fault主机上跑得好好的现象同样的代码在主机上./app正常交叉编译后传到板子一执行就段错误连第一行 printf 都没输出。原因通常有两类一是结构体里有指针字段被硬编码成主机地址或者用了long当指针容器32 位 ARM 上long是 4 字节主机 64 位是 8 字节强转后截断二是用了主机上存在、板子上版本不同的库函数签名对不上参数被错误解析。解决办法是先用readelf -d核对依赖再在板子上用 gdbserver 跑一遍看崩溃时的调用栈确认崩溃位置养成习惯指针一律用void *或uintptr_t存取不用long。5.2 触摸点整体偏移点在按钮左边却触发了右边现象界面能显示点击也有反应但落点系统性偏移越靠近屏幕边缘偏差越大。原因是没有做坐标归一化把驱动上报的原始值直接当成像素坐标用了。触摸 IC 上报的范围和 LCD 分辨率不是一回事驱动可能上报 0-4095而屏幕只有 1024 宽。解决办法是用EVIOCGABS读出minimum和maximum按(raw - min) * screen_w / (max - min)换算再做一次边界钳位。另一种情况是用了多点触摸协议但只读了ABS_X多指时ABS_X不上报得读ABS_MT_POSITION_X并配合ABS_MT_TRACKING_ID。5.3 画面颜色不对、整屏偏绿或者红蓝互换现象framebuffer 能刷出内容但照片颜色明显偏色或者纯红显示成纯蓝。原因基本锁定在像素格式上驱动配置的是 RGB565程序按 ARGB8888 写或者 RGB 顺序搞反成了 BGR。解决办法是先打印vinfo.bits_per_pixel和vinfo.red.offset、blue.offset这几个字段别假设。16 位时按 RGB565 打包32 位时按AARRGGBB打包写完先用纯红0xF800、纯绿0x07E0、纯蓝0x001F三个色块验证一遍比拿照片试快得多。5.4 摄像头取几帧后卡住select 一直超时现象前几帧正常跑几秒后画面停住select返回 0 或者干脆阻塞。原因是DQBUF取出的缓冲没有QBUF还回队列缓冲池被耗尽。四个缓冲用完就再也没有可用的驱动不再产生新帧。解决办法是检查每一帧处理完的路径确保QBUF一定被调用包括continue和错误返回的分支也要还回去。另一个易错点是STREAMON之前忘了先把所有缓冲入队开流时队列里是空的表现同样是取不到帧。5.5 UDP 上位机收到图但画面下半部分是花的现象接收端能拼出图像上半部分正常下半部分像被错位覆盖或者整帧有一部分是上一帧的内容。原因通常是发送端在帧还没发完时缓冲就被下一帧覆写或者接收端按pkt_total拼包时把不同frame_id的包混在一起了。解决办法是在发送端对一帧数据做一次本地拷贝再切片或者用双缓冲轮流用接收端严格按frame_id分组pkt_total不匹配的帧整体丢弃不要“凑合拼”。另外确认data_len是按最后一包实际长度算的最后一包不足 1400 字节时按 1400 算会把后面的栈内存一起发出去。6. 进阶把散装 demo 收成一个能演示的工程散装 demo 的通病是每个功能单独一个main演示时要来回换程序。真正要拿去答辩或者给同事看最好收成一个进程用页面表串起来再补两个东西一个是统一的日志宏一个是启动自检。日志宏这一步很值。板子上没有图形调试器出问题时只能靠串口输出如果每个文件都用不同的 printf 风格排查会很痛苦。/* log.h统一日志出口编译时可用 -DLOG_LEVEL 控制级别 */ #ifndef LOG_H #define LOG_H #include stdio.h #define LOG_LEVEL_DEBUG 0 #define LOG_LEVEL_INFO 1 #define LOG_LEVEL_ERR 2 #ifndef LOG_LEVEL #define LOG_LEVEL LOG_LEVEL_INFO #endif #define LOGD(fmt, ...) do { if (LOG_LEVEL LOG_LEVEL_DEBUG) \ fprintf(stderr, [D] fmt \n, ##__VA_ARGS__); } while (0) #define LOGI(fmt, ...) do { if (LOG_LEVEL LOG_LEVEL_INFO) \ fprintf(stderr, [I] fmt \n, ##__VA_ARGS__); } while (0) #define LOGE(fmt, ...) do { \ fprintf(stderr, [E] %s:%d fmt \n, __FILE__, __LINE__, ##__VA_ARGS__); } while (0) #endif参数说明LOG_LEVEL在编译时用-DLOG_LEVEL0打开全部输出发布时用默认值只留错误。LOGE里带__FILE__和__LINE__是因为板子上出错的现场往往没法复现能定位到行号就省一次来回传程序。输出走stderr而不是stdout是为了在串口里和正常业务输出分开重定向时也不会被缓冲吃掉。启动自检我一般会做四项做成一个self_check()在进入主循环前跑一遍framebuffer 能否打开、触摸设备节点是否存在、摄像头能否完成一次REQBUFS、存储目录是否可写。任何一项失败就在屏幕上打一行红字而不是让程序带着半残的状态跑起来。之前吃过一次亏摄像头没插好程序照常进主循环用户点“拍照”没反应我在那儿查了半天触摸逻辑最后发现是open(/dev/video0)返回 -1 被一个没写返回值的if吞掉了。从那以后我每次加新硬件模块都强制先在自检里加一行状态打印跑通了再写业务。工程目录也顺手理一下src/放业务代码drivers/放 framebuffer、touch、v4l2 的封装include/放头文件assets/放图片和字库编译产物统一输出到build/NFS 直接挂这个目录。这套结构不算精致但换一块板子移植的时候只需要改drivers/里的路径和格式定义业务代码基本不用动这是最省后悔药的做法。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?