资讯动态

psplash源码解析:framebuffer开机动画与嵌入式启动排障

发布时间:2026/10/26 13:28:21 来源:尧图企业网站定制
简介psplash源码是一套面向ARM架构与嵌入式Linux的开源启动画面实现适合树莓派、开发板等设备在系统启动或更新阶段展示进度条与动画界面帮助开发者理解并定制更友好的开机视觉体验。压缩包共79个文件约2.67MB以C源码和头文件为核心辅以目标文件、本地化po、PNG图像资源以及构建脚本和说明文档便于整体把握工程结构与编译脉络。已有667人学习下载。读者可从中梳理启动接管、图形绘制、framebuffer显示后端、配置裁剪与交叉编译集成等关键环节也可据此调整颜色、字体、进度条样式和动画效果针对特定硬件做调试优化或扩展错误提示与系统状态信息从而提升嵌入式Linux启动界面开发与问题排查能力。1. 板子加电屏幕却是黑的那几秒psplash 管的就是这块屏幕板子加电串口日志刷得飞快屏幕却一直黑着——这种情况我遇到过不止一次最后查下来内核没挂只是没人接管那块 framebuffer。psplash 补的就是这个位置一个几百 KB 的常驻进程打开/dev/fb0、mmap 显存、贴一张 logo然后挂一条 FIFO 等启动脚本把进度报过来。它不依赖 X、不依赖 Wayland、不需要 Qt内核把 framebuffer 驱动一注册它就能跑所以从 initramfs 阶段就能上屏。适合两类人正在给定制板子做开机体验的嵌入式工程师以及想找一个「能一口气读完」的 framebuffer 绘图样本的 C 语言学习者。代码量小到一晚上能看完但真上板子时踩的坑一晚上不一定填得完。2. 技术栈与目录结构几百 KB 的进程凭什么能在 initramfs 里跑起来2.1 选型逻辑直写 /dev/fb0绕开整条图形栈这个项目的技术栈可以说是「几乎没有技术栈」。它只依赖 libc、ioctl、mmap和 POSIX 的mkfifo/select编译产物是两三个可执行文件——一个主进程psplash一个小工具psplash-write。没有图形库没有字体引擎没有窗口系统。带来的是启动时序上的巨大优势在 init 进程还没把 udev、dbus、显示服务拉起来的时候只要内核的 framebuffer 驱动 probe 成功/dev/fb0就在psplash 就能画。代价也很明确它的能力边界就是「一张静态图 一条进度条 一行可选文字」。没有缩放、没有旋转之外的合成、没有抗锯齿、没有透明混合。图片是编译期以 C 数组形式烧进.rodata的运行时不做任何解码。这一点直接影响你的镜像体积换一张 1920×1080 的 RGBA 图就是 1920×1080×4 约 8MB 直接进二进制很多人第一次换图发现 rootfs 突然大了一截原因就在这。刷新策略也值得说清楚它直接往 mmap 出来的显存里写没有双缓冲、没有 vsync 等待靠的是「只重绘变化的小矩形」来避免整屏撕裂。psplash_fb_flush在不同分支里实现不一样有的版本是空实现fbdev 本身就即时生效有的版本会调FBIO_WAITFORVSYNC或者msync。所以你在桌面 Linux 上跑它看到的可能是闪一下在真正的嵌入式屏上反而稳得多。还有一层是编译系统。仓库用的是 autotoolsconfigure.acMakefile.am交叉编译时靠--host传工具链前缀。对只改图的人来说这层可以完全无视但要做产品化裁剪去掉进度条、改 FIFO 路径、换默认图就得从configure.ac里的AC_ARG_ENABLE一路看到psplash-config.h因为这些开关最终都是以-D宏的形式传进编译单元的。2.2 目录里每个文件到底该不该动刚拿到源码的人容易犯一个错以为psplash.c是全部。其实真正干活的绘图代码在psplash-fb.c和psplash-draw.c里psplash.c只负责「开资源、进循环、分发命令」。分清这三个文件的职责后面改起来会顺很多。文件职责改动频率psplash.cmain、FIFO 事件循环、命令分发、退出清理极低一年动一两次psplash-fb.c打开/dev/fb0、ioctl取参数、mmap、画点/画矩形/贴图、flush换屏、换色深时动psplash-draw.c业务层绘图铺底图、画进度条、画文字、背景保存与恢复改视觉时动psplash-write.c客户端小工具把PROGRESS 50这类参数编码成字节流写进 FIFO基本不动psplash-console.cVT 模式切换、光标隐藏、键盘关闭与恢复只在排查时看psplash-colors.h背景色、进度条填充色、边框色、文字色经常改psplash-config.hFIFO 路径、进度条尺寸等编译期宏偶尔改图片头文件由 PNG 转出来的 C 数组含宽高/行跨度/像素格式宏每次换图都动图片头文件的名字各家不一样有的叫psplash-image.h有的按发行版叫别的名字但结构是统一的一堆IMAGE_WIDTH、IMAGE_HEIGHT、IMAGE_BYTES_PER_PIXEL、IMAGE_ROWSTRIDE、IMAGE_PIXEL_FORMAT、IMAGE_BYTES宏加上一个巨大的unsigned char IMAGE[]。这个宏集合就是绘图代码和图片之间唯一的契约换图时这几个宏必须和实际数据严格对齐否则就是典型的花屏——后面排查章节会专门讲这个。Makefile.am里通常还会装一个把 PNG 转成这种头文件的小脚本内部调的是gdk-pixbuf-csource之类的工具。如果你的构建环境里没有这个工具自己用 ImageMagick 导一段 RGBA 原始数据、再拼成 C 数组也完全等价最后一章我给一份能直接抄的转换脚本。3. 主流程拆解一次开机从 main 到第一帧像素3.1 main 里的初始化顺序每一步都有讲究main不长但顺序不能乱先建 FIFO 再开 fb先存 VT 状态再画图先画完底图再进事件循环。下面这段是按源码结构整理的骨架去掉了几处平台分支和错误处理分支。int main (int argc, char **argv) { PSplashFB *fb; int pipe_fd, orientation 0; char buf[2048]; /* 1. 建 FIFO 并立刻以「非阻塞读」打开 如果先阻塞打开读端在没有写端之前进程会挂在这里 后面所有初始化都执行不到屏幕就一直黑着。 */ mkfifo (PSPLASH_FIFO, 0777); pipe_fd open (PSPLASH_FIFO, O_RDONLY | O_NONBLOCK); /* 2. 存下当前控制台VT状态切图形模式、隐藏光标、关键盘 */ psplash_console_save_and_reset (); /* 3. 打开 /dev/fb0拿分辨率/位深/行跨度mmap 出显存指针 */ fb psplash_fb_new (orientation); if (fb NULL) { psplash_console_reset (); return 1; } /* 4. 铺底色 把编译进来的 logo 贴到屏幕中间 */ psplash_draw_splash (fb); /* 5. 事件循环等 FIFO 可读读 1 字节命令再决定画什么 */ for (;;) { fd_set fdset; struct timeval tv { 5, 0 }; FD_ZERO (fdset); FD_SET (pipe_fd, fdset); if (select (pipe_fd 1, fdset, NULL, NULL, tv) 0) { char cmd; if (read (pipe_fd, cmd, 1) 1) { switch (cmd) { case PSPLASH_CMD_MSG: read_arg (pipe_fd, buf, sizeof (buf)); psplash_draw_msg (fb, buf); break; case PSPLASH_CMD_PROGRESS: read_arg (pipe_fd, buf, sizeof (buf)); psplash_draw_progress (fb, atoi (buf)); break; case PSPLASH_CMD_QUIT: goto done; } } } } done: psplash_console_reset (); return 0; }几个参数和细节要说清楚。PSPLASH_FIFO是一个编译期宏常见值是/tmp/psplash_fifo或者/run/psplash_fifo早期版本放在/tmp是因为 initramfs 阶段/run可能还没挂 tmpfs如果你的 rootfs 在切根时会把/tmp清掉FIFO 就消失了表现为切根之后所有命令都没反应——这是很隐蔽的一个坑。orientation是从命令行参数解析出来的旋转角度0/90/180/270只有带旋转需求的屏才用得上。select的 5 秒超时不是为了做心跳而是给进程一个周期性醒来的机会某些分支会借这个时机检查父进程状态或处理被信号打断的EINTR。read_arg是我为了突出主线抽出来的辅助函数实际源码里是原地循环read直到读满或者读到\0逻辑等价。3.2 framebuffer 打开与像素写入所有花屏都出在这二十行psplash_fb_new是这个项目真正的硬件适配层它的核心只有四个调用open、两个ioctl、一个mmap。难点不在调用而在于你不能假设任何事——分辨率、位深、像素通道顺序、行跨度全部要从fb_var_screeninfo和fb_fix_screeninfo里问出来。PSplashFB *psplash_fb_new (int angle) { struct fb_var_screeninfo var; struct fb_fix_screeninfo fix; PSplashFB *fb calloc (1, sizeof (PSplashFB)); fb-fd open (/dev/fb0, O_RDWR); if (fb-fd 0) { free (fb); return NULL; } ioctl (fb-fd, FBIOGET_VSCREENINFO, var); /* 可见分辨率、bpp、通道偏移 */ ioctl (fb-fd, FBIOGET_FSCREENINFO, fix); /* 显存长度、行跨度 line_length */ fb-width var.xres; fb-height var.yres; fb-bpp var.bits_per_pixel; fb-rowstride fix.line_length; /* 关键不是 width * bpp / 8 */ /* 通道顺序以驱动上报为准别写死 RGB */ fb-red_offset var.red.offset; fb-green_offset var.green.offset; fb-blue_offset var.blue.offset; fb-data mmap (NULL, fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fb-fd, 0); if (fb-data MAP_FAILED) { close (fb-fd); free (fb); return NULL; } return fb; }rowstride这一行是整段里最容易出事的地方。很多屏的xres_virtual比xres大硬件为保证 DMA 效率会把每行补齐到 32 字节甚至 64 字节对齐于是line_length大于width * bpp / 8。如果你按width * bpp / 8算偏移画面就会随着行号增加逐渐向右斜形成经典的斜切花屏。用fix.line_length就永远不会错。真正的画点函数反而简单算偏移、按位深写、按通道偏移移位。void psplash_fb_plot_pixel (PSplashFB *fb, int x, int y, uint8_t red, uint8_t green, uint8_t blue) { unsigned int off; /* 越界直接丢省掉上层所有调用者的边界判断 */ if (x 0 || x fb-width || y 0 || y fb-height) return; off y * fb-rowstride x * (fb-bpp / 8); switch (fb-bpp) { case 32: { uint32_t px (red fb-red_offset) | (green fb-green_offset) | (blue fb-blue_offset); *(uint32_t *)(fb-data off) px; break; } case 16: { /* RGB565 常见通道位宽由驱动上报这里按 5/6/5 压缩 */ uint16_t px ((red 3) fb-red_offset) | ((green 2) fb-green_offset) | ((blue 3) fb-blue_offset); *(uint16_t *)(fb-data off) px; break; } default: break; } }参数上要注意red/green/blue传的是 0~255 的满量程值16bpp 分支里必须右移截断否则高位溢出会把相邻通道污染成紫色或者绿色噪点。off用unsigned int而不是int是因为大分辨率下height * rowstride可能超过 2GB 的一半虽然实践中不会但类型选对省事。所有矩形、图片、文字的绘制最终都落到这个函数上所以它的性能就是整个 psplash 的性能——一帧 1080p 全屏重绘大概两百万次调用在 1GHz 的 ARM 上会明显卡顿这也是为什么 psplash 坚持局部重绘。3.3 进度条绘制先存背景再画新值进度条的逻辑看着简单但它要解决一个具体问题每次进度变化时怎么把上一次画的内容擦干净又不影响周围的 logo 和背景。psplash 的做法是保存-恢复——第一次画之前把进度条那个矩形区域的原始像素拷进内存缓冲之后每次重绘都先贴回背景再画新的。void psplash_draw_progress (PSplashFB *fb, int value) { int x, y, w, h; /* 夹紧到 0~100防止启动脚本传进来 -1 或者 120 把矩形画穿 */ if (value 0) value 0; if (value 100) value 100; x (fb-width - BAR_WIDTH) / 2; y (fb-height - BAR_HEIGHT) / 2 BAR_VERTICAL_OFFSET; /* 用存下来的背景把旧进度条擦掉 */ psplash_draw_restore_bg (fb, x, y, BAR_WIDTH, BAR_HEIGHT); /* 外边框1px 的矩形轮廓 */ psplash_fb_draw_rect (fb, x, y, BAR_WIDTH, BAR_HEIGHT, PSPLASH_BAR_BORDER_COLOR); if (value 0) { w (BAR_WIDTH - 2) * value / 100; h BAR_HEIGHT - 2; psplash_fb_draw_rect (fb, x 1, y 1, w, h, PSPLASH_BAR_COLOR); } psplash_fb_flush (fb); }BAR_WIDTH/BAR_HEIGHT来自psplash-config.h或psplash-colors.h是编译期常量改进度条长短就是改这两个宏重编。psplash_draw_restore_bg依赖前面某次psplash_draw_save_bg已经把这块区域的像素存好如果调用顺序错了比如底图还没贴就存背景恢复出来的就是一片纯色表现为进度条周围有一块明显的色块边界。注意w (BAR_WIDTH - 2) * value / 100这个写法——先乘后除是为了避免整数除法过早截断导致进度条前 30% 完全看不出来。这种小细节在嵌入式代码里到处都是读源码时值得停下来想一下作者为什么这么写。3.4 FIFO 通信协议一字节命令 一个以 \0 结尾的字符串主进程和客户端之间的协议极简先写 1 个字节的命令字再写参数字符串字符串以\0结尾。命令字本身是可打印字符MSG是空格PROGRESS是QUIT是Q。客户端psplash-write做的就是把这套编码封起来int main (int argc, char **argv) { int fd; if (argc 2) return 1; /* 这里是阻塞打开没有主进程在跑时会卡住 所以启动脚本里一定不能在 psplash 起来之前调用它 */ fd open (PSPLASH_FIFO, O_WRONLY); if (fd 0) return 1; if (!strcmp (argv[1], PROGRESS) argc 2) { char cmd PSPLASH_CMD_PROGRESS; write (fd, cmd, 1); write (fd, argv[2], strlen (argv[2]) 1); /* 1 把 \0 也写进去 */ } else if (!strcmp (argv[1], MSG) argc 2) { char cmd PSPLASH_CMD_MSG; write (fd, cmd, 1); write (fd, argv[2], strlen (argv[2]) 1); } else if (!strcmp (argv[1], QUIT)) { char cmd PSPLASH_CMD_QUIT; write (fd, cmd, 1); } close (fd); return 0; }两个参数细节决定成败。第一strlen(argv[2]) 1里的1不能省——主进程是靠\0判断参数字符串结束的少了它读端会一直等下一个字节事件循环就卡在那条命令上表现为发完这条命令之后所有进度都不动了。第二open用的是阻塞模式如果 psplash 主进程已经 QUIT 退出客户端会永远挂在open上所以启动脚本里必须先确认主进程在跑或者干脆给open加个O_NONBLOCK并在失败时静默退出。至于那条MSG命令能不能真的显示出文字取决于你手上这份源码有没有编进内置的位图字体。有这个能力的分支会在屏幕底部画一行字没有的分支里psplash_draw_msg可能是个空函数或者只做背景擦除。拿到源码先grep -rn draw_msg看一眼实现比猜快得多。4. 编译与集成先在 x86 上跑通再谈上板4.1 桌面环境先验证逻辑拿到源码别急着交叉编译先在开发机上跑一遍确认能编译、能画、能响应命令把工具链问题排除掉。# 仓库里如果只有 configure.ac 和 Makefile.am先生成 configure autoreconf -fi # 默认配置产物是 ./psplash 和 ./psplash-write ./configure make -j$(nproc)编译通过之后别在图形界面的终端里直接跑。桌面环境的 X 或 Wayland 合成器已经占着显卡/dev/fb0在这种机器上往往是 fbdev 兼容层甚至根本不存在你写进去的像素不会出现在任何窗口里。正确姿势是切到纯文本 TTYCtrlAltF3登录然后以 root 执行./psplash屏幕上应该立刻出现 logo 和进度条框架。这时开另一个 TTY 用./psplash-write PROGRESS 50喂进度能看到进度条跟着走说明整条链路是通的。如果切到 TTY 还是黑屏先确认/proc/fb里有内容、/dev/fb0存在且权限对。这一步排除掉后面交叉编译才有意义。4.2 交叉编译与 rootfs 里的启动时序交叉编译就一行--host真正需要动脑的是裁剪开关。configure.ac里那些AC_ARG_ENABLE决定了要不要进度条、FIFO 放哪、默认用哪张图用./configure --help翻一遍比读源码快。# 以 aarch64 为例按自己工具链前缀替换 ./configure --hostaarch64-linux-gnu \ CCaarch64-linux-gnu-gcc \ CFLAGS-O2 -marcharmv8-a \ --prefix/usr make -j8 make DESTDIR$PWD/_install installCFLAGS里的-O2别省画点函数是调用热点不开优化在低端 ARM 上进度条动画会一顿一顿的--prefix/usr决定装到 rootfs 的哪个位置后面写启动脚本时要和它对齐。make install通常只装psplash、psplash-write两个二进制图片是编进去的没有额外的资源文件。时序才是集成的重点。psplash 必须在屏幕还是空的时候尽早起来抢在 Qt、GStreamer、显示服务之前占住 framebuffer。#!/bin/sh # rootfs 的 init 脚本片段尽早拉起来占住 framebuffer /usr/bin/psplash # 首次画进度前稍等一下避免主进程还没 mmap 完 sleep 0.2 /usr/bin/psplash-write PROGRESS 10 # ……这里是真正耗时的初始化挂载、加载模块、起服务…… /usr/bin/psplash-write PROGRESS 80 # 界面程序接管屏幕之前务必先让 psplash 退出 /usr/bin/psplash-write QUIT这里的sleep 0.2不是迷信是血泪经验。psplash-write用阻塞方式打开 FIFO如果主进程还没创建 FIFO 就已经被调用客户端会一直挂在那短 sleep 是最省事的规避方式更严谨的做法是让 init 脚本轮询 FIFO 文件是否存在。用 systemd 的发行版可以写成 unit关键是DefaultDependenciesno和Beforegettytty1.service[Unit] DescriptionBoot splash DefaultDependenciesno Aftersystemd-vconsole-setup.service Beforegettytty1.service [Service] ExecStart/usr/bin/psplash RemainAfterExityes [Install] WantedBysysinit.targetRemainAfterExityes是为了让 psplash 退出后 unit 状态仍算 active否则QUIT之后 systemd 可能把它判成失败并触发重启策略屏幕会在启动末尾诡异闪一下 logo。4.3 不写代码也能验证协议调试阶段最省事的办法是绕开psplash-write直接用 shell 往 FIFO 里灌字节。协议就两种东西1 字节命令 以\0结尾的参数。# PROGRESS 命令字是 参数以 NUL 结尾 printf %s\0 30 /run/psplash_fifo # MSG 的命令字是空格字符别漏了那个空格 printf %s\0 Booting... /run/psplash_fifo # QUIT 只写一个字节没有参数 printf Q /run/psplash_fifoprintf %s\0这个写法比printf 30\0安全因为部分 shell 会把\0后面跟的数字当成八进制转义序列吃掉最后一个字节就变成了别的值主进程收到未知命令直接忽略你会以为 FIFO 坏了。用%s占位再补\0行为在所有 POSIX shell 里都一致。5. 避坑与排查黑屏、花屏、进度条卡死这几类现场5.1 现象进程在跑屏幕全黑现象ps能看到psplash进程/dev/fb0也存在屏幕就是没反应串口也不报错。原因最常见的两种情况。一是写进去的不是真正在扫描输出的那块缓冲——现在很多 SoC 的显示通路走 DRM/KMSfbdev 只是个模拟层/dev/fb0的写入可能映射到一块从不被扫描的 shadow buffer二是另一个进程Qt 的 EGLFS 后端、某些视频输出插件已经把 KMS 接管了你先写的像素被它后来的一次 modeset 全清掉。解决先用dd if/dev/urandom of/dev/fb0打一屏噪点看屏幕花不花。花了说明 fbdev 这条通路有效问题在时序被后来的进程抢了把 psplash 的启动点提前到该进程之前即可不花说明这条路根本不通要么改用 DRM 直写的方案要么在设备树里把 fbdev 模拟层配成实际输出。判断依据看/proc/fb里列出的设备名以及/sys/class/graphics/下有没有对应的fb0。5.2 现象logo 出来了但颜色不对或者画面斜切现象图能显示但红蓝反了、整体偏色或者越往右下越往右偏边缘出现斜的错位。原因偏色是像素通道顺序对不上。图片头文件里声明的是 RGB 或 RGBA而屏幕上fb_var_screeninfo上报的red.offset/blue.offset是反的BGRA绘图代码如果按写死的移位来 pack 就会红蓝互换。斜切则是行跨度算错用了width * bpp / 8而不是fix.line_length一行差几个字节几百行累积下来就是肉眼可见的斜坡。解决先用fbset -i把真实的xres、xres_virtual、line_length、bits_per_pixel打出来和代码里用的值逐个核对。通道顺序一律以var.red.offset为准别写死。图片转换时统一成 RGBA8888让程序自己按驱动上报的偏移去 pack这样同一份图片换到不同色深的屏上都不用重导。5.3 现象发了 PROGRESS 命令进度条停在第一次的值不动现象第一次PROGRESS 10有效之后无论发什么都毫无反应psplash-write也不报错。原因主进程的事件循环卡在了读参数上。它收到命令字后会一直读直到遇到\0如果客户端写参数时漏了结尾 NUL比如自己用echo -n拼的字节流读端就会永远等待下一个字节select再也不会被重新执行。另一种可能是参数超过缓冲区长度读端填满buf后剩下的字节留在了 FIFO 里下一条命令的解析从此全部错位。解决客户端一律用psplash-write别自己echo拼字节确实要手写就用printf %s\0。MSG的内容控制在几十字节以内长文本不要指望它。调试时用strace -p pid看主进程是不是停在read上一秒钟就能定位。5.4 现象QUIT 之后屏幕残留、光标闪烁、getty 的输出糊在 logo 上现象发完QUITlogo 还留在屏幕上或者光标开始闪串口登录提示直接打在 logo 中间。原因psplash 启动时调了psplash_console_save_and_reset做了切图形模式KD_GRAPHICS、隐藏光标、关键盘这几件事退出时必须一一恢复。如果进程是被SIGTERM直接杀掉的根本没走到done标签后面的psplash_console_resetVT 就一直停在图形模式。解决在主进程里装SIGTERM/SIGINT的处理函数置一个标志位或者直接跳转到清理流程。产品上更稳的做法是让界面程序在接管屏幕前先chvt切一次 VT把 VT 状态强制刷一遍。另外QUIT之后屏幕内容不会自动清空这是 fbdev 的固有行为需要在退出前自己铺一遍背景色。6. 二次开发换图、改视觉本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑