简介:这是一份面向ARM架构Linux嵌入式开发者的psplash源码包,聚焦系统启动阶段的图形化启动画面与进度条显示。源码覆盖初始化入口、界面绘制、显示后端、配置头文件及构建脚本等核心模块,可用于树莓派、开发板等设备的开机界面定制,也适合希望理解Linux启动流程与嵌入式UI实现的开发者研读。压缩包共79个文件,约2.67MB,以C源码与头文件为主体,辅以PO本地化文件、PNG图片素材、样例配置及构建脚本,目录组织保留了上游工程结构。已有667人学习,说明其在嵌入式启动画面方向具有一定参考价值。读者可据此梳理psplash与内核的协作方式,掌握进度条、动画和framebuffer后端的实现思路,并借助配置与构建体系完成交叉编译、界面替换和特定硬件适配,为自定义启动界面与性能优化提供可直接复用的代码基础。
1. 通电到 GUI 之间那 3 秒黑屏:psplash 源码到底补上了什么
一块板子从按下电源到 Qt 主界面出来要八秒,中间有三秒是纯黑:kernel logo 已经淡出,显示服务还没拉起,屏幕就那么亮着。客户验收时第一句话往往是"这机器是不是坏了"。psplash 就是填这段空窗期的:一个编译进几百 KB 的 C 程序,把一张 PNG 烧进二进制,开机早期直接往/dev/fb0上画,同时开一个 FIFO 管道,让后面的启动脚本用PROGRESS 40、MSG Loading...这样的短命令远程改进度条。它不碰 X、不碰 Wayland、不碰 GPU,也不需要 DRM 合成器,只要 framebuffer 设备节点在,它就能出图。适合自己维护 rootfs、手上有一块能cat /sys/class/graphics/fb0/bits_per_pixel的开发板、并且愿意为启动体验改几行源码的人。
2. 拆开 psplash 源码:主循环、FIFO 协议与 framebuffer 三条线怎么咬合
拿到压缩包先别急着make,花十分钟把文件职责认清楚,后面所有定制都是在这几条线上改。很多人第一次改 psplash 失败,不是代码难,是不知道该动哪个文件——想把进度条往上挪二十个像素,结果跑去改了配置宏;想换颜色,结果去重编了 PNG。把职责分清楚,改起来就是改一行的事。
2.1 编译产物不止一个可执行文件
源码目录里通常有这几个 C 文件,各自产出的东西不一样:
| 源文件 | 产物 | 职责 |
|---|---|---|
psplash.c | psplash | 主程序:解析参数、开 framebuffer、建 FIFO、画底图、进消息循环 |
psplash-fb.c | 编进psplash | framebuffer 的打开、mmap、矩形填充、flush |
psplash-console.c | 编进psplash | 把虚拟终端切到图形模式,避免光标和日志打到图上 |
psplash-write.c | psplash-write | 一个几十行的小客户端,往 FIFO 写一行命令 |
psplash-systemd.c | psplash-systemd | 部分版本带,把进度转成 systemd 通知 |
主程序的骨架很短,main()里基本就是下面这条线:解析命令行参数 → 调psplash_fb_new()打开/dev/fb0并 mmap → 把编译进来的图片数组画一遍 →mkfifo()建管道 → 阻塞在read()上等命令。每读到一个命令,就判断前缀,画一下进度条或者换一行文字,然后继续等。收到QUIT就收尾退出,顺手把 FIFO 文件删掉。
这条线里唯一需要留神的是"先画什么"。psplash 没有双缓冲,所有绘制直接写 mmap 那块显存,画到一半被扫描出去就会看到撕裂。所以源码里的顺序是固定的:整图先一次性铺完,再叠加底部的进度槽,最后才进入消息循环。自己加功能时也别在这个顺序里插队。
启动之后怎么确认它真的跑起来了,别只看屏幕亮没亮:
pgrep -x psplash # 有没有主进程 PID=$(pgrep -x psplash) ls -l /proc/$PID/fd # 能看到打开的 /dev/fb0 和 FIFO 读端 cat /proc/$PID/maps | grep fb0 # 确认显存映射的大小fd里如果只看到/dev/fb0而没有 FIFO 的读端,说明管道没建成功——大概率是PSPLASH_FIFO_DIR指向的目录在启动早期还不存在,mkfifo静默失败了。
2.2 FIFO 管道:三条命令构成的协议
psplash 的对外接口就这么简单,全部走一个命名管道,命令用空格分隔,关键字大小写敏感。日常用得上的只有三条:
# 换底部那行提示文字,空格后面的整串都会显示出来 psplash-write "MSG Bringing up network" # 把进度条推到 65%,参数是 0-100 的整数,超出范围会画到屏幕外 psplash-write "PROGRESS 65" # 让主程序退出并清理 FIFO,必须放在启动脚本最后 psplash-write "QUIT"不想依赖psplash-write这个客户端也能写,管道就是个文件,直接重定向就行:
FIFO=/run/psplash_fifo # 真实路径以 config.h 里的 PSPLASH_FIFO_DIR 为准 if [ -p "$FIFO" ]; then printf 'PROGRESS 30' > "$FIFO" else echo "FIFO 不存在,psplash 没起来或者已经退出了" >&2 fi注意这里有个很容易被忽略的行为:FIFO 的写端open()在没有任何读端时会一直阻塞。也就是说主程序已经退出、FIFO 文件还留在文件系统上的时候,printf > $FIFO会挂在那里不动,你的启动脚本就此卡死。生产脚本里要么先pgrep判断,要么套一层timeout,这个在第 5 章还会展开说。
还有一个隐性约束:FIFO 只允许一个读端,也就是只能有一个 psplash 主程序在跑。如果你在调试时手动起了一个,systemd 又拉了一个,第二个会直接失败退出,屏幕上看到的是前一个实例画的图,怎么发命令都没反应,容易误判成"消息协议不管用"。
2.3 framebuffer 通路:为什么它不依赖图形栈
psplash_fb_new()干的事情很朴素:open("/dev/fb0", O_RDWR),两次ioctl(FBIOGET_VSCREENINFO和FBIOGET_FSCREENINFO)把分辨率、位深、每行字节数、RGB 掩码读出来,然后mmap一整块显存。之后的绘制就是往这块内存里按rowstride逐行写像素,psplash_fb_flush()负责把脏矩形区域刷出去。
正因为直接吃 framebuffer,它对驱动有两个硬要求:一是 fb 设备节点在启动早期就必须存在,二是驱动上报的像素格式必须和硬件真实格式一致。前者决定了 psplash 在启动时序里的位置,后者决定了颜色对不对。常见的 16bpp RGB565 和 32bpp ARGB8888 它都能处理,内部会按var.red.offset、var.green.offset、var.blue.offset换算,但驱动把掩码报错的情况并不罕见,那时候只能在上层对调通道。
动手之前先摸清屏幕的真实参数,比对着源码猜要快得多:
cat /sys/class/graphics/fb0/virtual_size # 输出形如 1024,600 cat /sys/class/graphics/fb0/bits_per_pixel # 16 或 32 cat /sys/class/graphics/fb0/stride # 每行字节数,注意可能大于 width*bpp/8 cat /sys/class/graphics/fb0/modes # 当前模式,形如 U:1024x600p-60stride那一行值得单独盯一眼。有些驱动会把每行补齐到 64 字节边界,这时候stride != width * bpp / 8,如果你的图片数据按紧凑的width*2打包,而 fb 实际要求补齐,画出来就会是一条一条斜的。psplash 内部用的是 fb 自己上报的rowstride,所以走源码路径不会踩这个;但你自己写脚本生成图片数据时,必须按 fb 的stride来,不能想当然。
3. 交叉编译 psplash:从 autoreconf 到能跑在板子上的二进制
源码包的形式有两种:一种是打了configure的发布包,解压就能配;另一种是从版本库直接拉的,只有configure.ac和Makefile.am,得先自己生成构建脚本。两种都走一遍,后面换平台就不用重新摸。
3.1 依赖与构建脚本生成
主机侧需要 autotools 全套、pkg-config,以及提供gdk-pixbuf-csource的那个开发包(用来把 PNG 转成 C 数组)。Debian 系的安装命令大致是这样:
sudo apt install -y build-essential autoconf automake libtool pkg-config \ libgdk-pixbuf-2.0-dev然后在源码目录里生成构建脚本。如果目录下有autogen.sh,直接跑它最省事;没有就手动来一遍,顺序不能乱:
cd psplash aclocal # 收集宏定义,生成 aclocal.m4 libtoolize --force --copy # 如果项目用了 libtool autoconf # configure.ac -> configure autoheader # 生成 config.h.in automake --add-missing --foreign # 补齐缺失的辅助脚本跑完ls configure确认文件在。这一步报错九成是因为configure.ac里的AC_INIT版本号和AM_INIT_AUTOMAKE不匹配,或者缺NEWS、README这类 automake 强制要求的占位文件,--foreign就是用来放宽这个检查的。
3.2 交叉编译配置与那个必踩的坑
配置命令本身不复杂,难的是让构建系统用主机上的图片转换工具,而不是去找目标机架构的版本:
GDK_PIXBUF_CSOURCE=/usr/bin/gdk-pixbuf-csource \ ac_cv_path_GDK_PIXBUF_CSOURCE=/usr/bin/gdk-pixbuf-csource \ PKG_CONFIG_PATH= \ ./configure \ --host=arm-linux-gnueabihf \ --prefix=/usr \ --disable-static \ CFLAGS="-Os -march=armv7-a -mfpu=neon"几个参数值得说清楚。--host决定工具链前缀,arm-linux-gnueabihf对应硬浮点的 gnueabihf 工具链,如果你的交叉编译器叫别的名字,这里要跟着换。--prefix=/usr是因为 psplash 要装到/usr/bin,早期启动脚本才找得到。CFLAGS里用-Os而不是-O2,启动画面这种场景二进制小一点更划算;真要抠那几毫秒,可以把-O2编一版对比,通常差不了多少。
中间那两行环境变量是整个交叉编译里最关键的地方。configure默认会用pkg-config去找gdk-pixbuf-csource,而--host一旦指定,pkg-config就会去搜目标架构的.pc文件,找到的是给 ARM 编的gdk-pixbuf-csource——这玩意在 x86 主机上根本执行不了,make阶段会抛Exec format error。ac_cv_path_*是 autoconf 的缓存变量,直接告诉它"别找了,就用这个路径",把搜索整个跳过。PKG_CONFIG_PATH=置空是同一目的的第二道保险。
编完检查一下产物,别急着拷到板上:
make -j"$(nproc)" file psplash psplash-write # 期望看到:ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked readelf -d psplash | grep NEEDED # 看链接了哪些库 ls -lh psplash # 通常在 100KB 到 300KB 之间,取决于图片大小NEEDED里如果只有libc.so.6,说明链接很干净,早期启动不会因为缺库起不来。如果多出libgdk_pixbuf之类,那说明图片转换被拖进了运行时依赖,得回头检查configure的输出里那段checking for gdk-pixbuf-csource到底用了哪个路径。
3.3 不用 autotools 的最小手搓方案
有些裁剪过的源码包压根不带构建脚本,或者你只想临时改一行重新编,这时候手搓更快。关键点在于psplash.c里会#include "config.h",而这个文件是configure生成的,里面全是PSPLASH_FIFO_DIR、PSPLASH_BAR_COLOR这类宏。
我的做法是先跑一遍configure把config.h生出来,哪怕不完整编译:
# 第一步:先骗出一份 config.h(用主机编译器即可) ./configure --disable-static ls config.h # 有了它,后面手搓才可能成功然后用它直接编,把生成的头文件和源码目录一起加进包含路径:
arm-linux-gnueabihf-gcc -Os -Wall \ -I. -I./images \ # 图片头文件所在目录 -o psplash \ psplash.c psplash-fb.c psplash-console.c \ -lm编不过时最常见的两个报错:config.h: No such file or directory说明第一步没做;PSPLASH_IMAGE相关的符号未定义,说明图片头文件没进包含路径,或者头文件里的结构体变量名和源码里引用的名字对不上——后者基本都发生在你自己换了图片之后,下一章会详细讲。
产物放到板子上之后,想验证它能不能跑,最省事的是先在开发机上编一个 x86 版本,在本地 tty 的 framebuffer 上跑一次(Ctrl+Alt+F2切到纯文本终端,/dev/fb0是现成的)。同样的图、同样的参数,先在本地确认不崩,再去调交叉编译,能省掉一大半"到底是编译问题还是板子问题"的纠结。
4. 换掉那张默认图:PNG 转 C 数组的两条路与进度条参数
默认那张图是给 640x480 做的,放到 1280x800 的屏上就是左上角一小块,右下大片黑。改图这件事看着简单,实际是整个 psplash 定制里最容易出错的一环,因为涉及像素格式、行宽、结构体布局三个变量的对齐。
4.1 图片规格:尺寸、位深和透明通道
先定尺寸。查屏幕:
cat /sys/class/graphics/fb0/virtual_size # 例如 1024,600PNG 就按这个尺寸做,或者做成和屏幕等宽。psplash 画图时是按图片自身的宽高逐行拷贝的,屏幕比图大就露黑底,比图小就截断——它不做缩放。想居中,得去psplash.c里找绘制图片的那个函数,在里面加个偏移量;想铺满,就把 PNG 直接做成屏幕尺寸,别指望代码帮你拉伸。
位深方面,用 24 位 RGB 就够了,透明通道会被丢掉。psplash 处理的是裸像素数组,alpha 通道除了让文件变大没有任何作用。进度条底图同理,它是靠矩形填充画的,不是靠贴一张带透明的图。
4.2 路线一:用 gdk-pixbuf-csource 生成头文件
源码自带的构建规则就是这么干的,手动执行一次方便理解:
gdk-pixbuf-csource --raw \ --name=psplash_img \ splash-1024x600.png > psplash-img.h--raw表示不做 RLE 压缩,直接输出裸像素数据,psplash 只认这种;--name决定生成的结构体变量名,这个值必须和psplash.c里引用的一致,不然就是一堆未定义符号。生成的头文件是个匿名结构体,前四个字段是宽、高、行跨度和是否有 alpha,后面跟着unsigned char pixel_data[]。
这个工具输出的字节序跟随主机。在 x86 上生成、在 ARM 小端板上使用没有问题;如果目标是大端平台,得确认gdk-pixbuf-csource生成的数据和 fb 的字节序是否一致,不一致时颜色会整体错乱。
4.3 路线二:没有 gdk-pixbuf 时用 Python 转 RGB565
CI 环境里往往不想为了一个启动图装 gdk-pixbuf 整套依赖。我自己常备一个脚本,逻辑就是逐像素量化到 RGB565,再按小端顺序铺成字节流:
#!/usr/bin/env python3 """把 PNG 转成 psplash 可直接 include 的 C 头文件(RGB565 裸数据)""" import sys from PIL import Image def rgb565(r, g, b): # <p> <a href="https://download.csdn.net/download/chidaoqi1607/12373339" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>