简介:面向ARM嵌入式Linux的psplash开源源码包,适用于嵌入式设备(树莓派、开发板)系统启动或升级场景,帮助开发者在启动阶段实现带进度条和动画的自定义图形界面,提升用户体验。资源共79个文件,压缩包约2.67MB,主要包含C源文件、头文件、编译脚本、图形资源及示例配置,便于阅读与二次开发。已有667人学习下载。深入分析源码可以掌握psplash与Linux内核的交互机制、framebuffer显示后端原理,以及进度条绘制和动画刷新的实现方式;同时,目录中提供的Makefile、configure等构建文件,也能帮助开发者快速搭建交叉编译环境,并将psplash集成到目标ARM系统中。对于从事嵌入式Linux界面定制、启动流程优化或BSP开发的工程师而言,这份源码是理解开源启动UI方案的典型范例。
1. 开机画面里的常青树:psplash源码能解决什么问题
搞嵌入式Linux的人对psplash这个名字多少有点印象,但真正把它翻过一遍的人不算多。我最早接触它是在一台7寸屏的工控盒上:内核刚起来、根文件系统还没准备好,屏幕上就得出现logo和进度条,而且系统每拉起一个服务,进度就要往前跳一格。这些看着很简单的效果,放到启动早期那个环境下其实非常不好做——没有桌面框架,没有字体引擎,连显示设备都要自己手动打开。psplash就是在这个背景下被反复使用的方案:一个体积很小的程序,直接操作Linux帧缓冲,用FIFO接收上层进度消息,在桌面环境接管屏幕之前把画面刷出来,然后再利落退出。它适合所有想给开发板或量产设备做启动画面的人,拆一遍源码比从零写要快得多。
2. 三个核心模块:帧缓冲、FIFO和图片头文件的协作链路
psplash源码体量很精简,核心逻辑集中在几个文件里。如果把整条链路看作一次“把图片投到屏幕”的过程,它其实分三步:打开帧缓冲设备,把logo和进度条画到内存映射区域,再通过FIFO消息驱动重绘。这一章就把这三个环节逐一拆开。
2.1 源码目录先扫一遍:每个文件在管什么
解开psplash源码包之后,第一件事是看目录结构。典型的结构大致如下:
psplash/ ├── Makefile.am # automake 构建入口,定义编译目标 ├── configure.ac # 编译参数检查、logo路径选项 ├── psplash.c # 程序入口:初始化后进入事件循环 ├── psplash-fb.c # 帧缓冲操作封装:open、ioctl、mmap、绘制 ├── psplash-fifo.c # FIFO读取:解析外部进程写入的命令 ├── psplash.h # 全局结构体、函数声明 ├── psplash-colors.h # 主题色定义 ├── psplash-config.h # 进度条坐标、尺寸、超时等几何参数 ├── make-image-header.sh # PNG转C头文件的脚本 ├── psplash-systemd.service.in # systemd单元模板 └── psplash.init # sysvinit 启动脚本psplash.c负责把整个程序“搭起来”,psplash-fb.c管所有像素级操作,psplash-fifo.c管外部消息。颜色和几何参数被单独抽到两个头文件里,改主题不用碰逻辑代码,改图形不用碰设备层。我拿到一个不熟悉的源码包时,通常会先找这种“配置与逻辑分离”的边界,找到了就能放心下手。
2.2 帧缓冲封装:打开/dev/fb0之后发生了什么
psplash画图的核心依赖是Linux帧缓冲设备,也就是 /dev/fb0。psplash-fb.c里最重要的函数是初始化部分:
PSplashFB *psplash_fb_init(int angle) { int fd; struct fb_var_screeninfo fbvar; struct fb_fix_screeninfo fbfix; PSplashFB *fb; /* 1. 打开帧缓冲设备 */ fd = open("/dev/fb0", O_RDWR); if (fd < 0) { perror("open /dev/fb0"); return NULL; } /* 2. 取可变信息:分辨率、位深 */ if (ioctl(fd, FBIOGET_VSCREENINFO, &fbvar) < 0) { perror("FBIOGET_VSCREENINFO"); close(fd); return NULL; } /* 3. 取固定信息:每行字节数 */ if (ioctl(fd, FBIOGET_FSCREENINFO, &fbfix) < 0) { perror("FBIOGET_FSCREENINFO"); close(fd); return NULL; } /* 4. 把显存映射到用户空间地址 */ fb = calloc(1, sizeof(PSplashFB)); fb->fd = fd; fb->width = fbvar.xres; fb->height = fbvar.yres; fb->bpp = fbvar.bits_per_pixel; fb->stride = fbfix.line_length; fb->data = mmap(NULL, fbfix.line_length * fbvar.yres, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb->data == MAP_FAILED) { perror("mmap"); close(fd); free(fb); return NULL; } return fb; }这里的参数值得仔细看:fbvar.xres和yres是屏幕实际分辨率,bits_per_pixel是颜色深度,fbfix.line_length是物理上每一行占用的字节数,它可能比你算出来的宽还多几个字节,因为硬件有行对齐要求。mmap映射的长度必须用stride乘高度,而不是width乘height乘bpp,很多人第一次写帧缓冲程序在这里栽跟头。
映射完成后,所有绘制都变成内存操作。比如把一个点写成目标颜色,本质就是往fb->data里对应位置塞一个像素值。这也解释了为什么psplash能在启动早期快速跑起来:除了open和ioctl,它不依赖任何服务,也不需要X11或Wayland那些东西。
2.3 主循环与FIFO:进度条是被谁一步步推着走的
帧缓冲负责“怎么画”,FIFO负责“画什么、画到多少”。psplash会创建一个命名管道,路径通常是/run目录或者/tmp目录下的psplash.fifo,其他进程往这个管道里写一行命令,psplash读取后解析执行。
主循环写法很直接:
int main(int argc, char *argv[]) { PSplashFB *fb; PSplashFIFO *fifo; fd_set fds; fb = psplash_fb_init(0); if (!fb) { fprintf(stderr, "psplash: framebuffer init failed\n"); return 1; } /* 绘制静态logo,参数是图像数据在头文件里的定义 */ psplash_draw_logo(fb, NULL, 0, 0, 0, 0, 0); fifo = psplash_fifo_open(); while (1) { FD_ZERO(&fds); FD_SET(fifo->fd, &fds); /* 阻塞等消息 */ select(fifo->fd + 1, &fds, NULL, NULL, NULL); if (FD_ISSET(fifo->fd, &fds)) { if (psplash_fifo_read(fifo, fb) < 0) break; /* 收到QUIT命令时退出 */ } } psplash_fb_destroy(fb); return 0; }psplash_fifo_read做的事是按行解析,支持的命令有三类:PROGRESS、MSG、QUIT。核心解析代码大致是这样:
int psplash_fifo_read(PSplashFIFO *fifo, PSplashFB *fb) { char buf[256]; while (fgets(buf, sizeof(buf), fifo->fp) != NULL) { if (strncmp(buf, "PROGRESS", 8) == 0) { /* 命令格式: PROGRESS 50 */ int percent = atoi(buf + 8); if (percent >= 0 && percent <= 100) psplash_draw_progress(fb, percent); } else if (strncmp(buf, "MSG", 3) == 0) { /* 命令格式: MSG 正在启动网络服务 */ psplash_draw_message(fb, buf + 3); } else if (strncmp(buf, "QUIT", 4) == 0) { return -1; } } return 0; }注意atoi本身不检查边界,所以代码里先做0到100的范围判断,防止脏数据把进度条画到屏幕外面。psplash_fifo_read返回负值时主循环退出,这是一种很朴素的握手协议:谁写QUIT,谁就交给了画面控制权。
实际使用中,系统服务脚本会在每个启动步骤完成后往FIFO里写一行进度命令。我见过不少初学者的做法是把所有消息一次性写进去,结果进度条瞬间跳到100%。正确做法是按启动顺序逐步写,保持管道处于打开状态,避免反复open/close带来的时序问题。
2.4 make-image-header.sh:logo是怎么变成C数组的
psplash不用png库在运行时解码图片,而是在编译前就把图片转成一个头文件,程序里直接引用内存数组。这正是make-image-header.sh存在的意义。
#!/bin/sh # 用法: ./make-image-header.sh logo.png MYLOGO # 输出: MYLOGO.h PNG="$1" NAME="$2" WIDTH=$(identify -format '%w' "$PNG") HEIGHT=$(identify -format '%h' "$PNG") convert "$PNG" -background black -flatten rgb:- | hexdump -v -e '16/1 "0x%02x," "\n"' | sed "1i\\ #define ${NAME}_HEADER_1 0x89 \\ ... \\ static const uint8_t ${NAME}_IMG[] = {" | sed "1i #define ${NAME}_WIDTH $WIDTH" | sed "1i #define ${NAME}_HEIGHT $HEIGHT"脚本逻辑不复杂:identify取宽高,convert把png转成裸RGB字节流,hexdump把字节按C语言数组格式排列,最后用sed在文件头部塞进宽高宏定义。生成的MYLOGO.h里是一个static const数组,编译时直接展开,运行时零额外开销。
理解这一步,你才能解释一个常见现象:改logo之后重新编译,如果头文件没重新生成,画面还是旧的。因为psplash编译用的是头文件里的数组,而不是磁盘上的png。很多人在这个点上折腾半天,最后发现是make依赖没写对。我一般会直接删掉生成的.h再重新执行脚本,确保数组是新的。
3. 编译与集成:从交叉编译到开发板跑通的完整流程
源码看得再明白,最终要落到板子上跑起来。这一章按我实际动手的顺序写:交叉编译、塞进启动流程、替换logo和调样式。
3.1 交叉编译:configure和make的参数怎么填
psplash的构建系统是autotools,交叉编译的套路和大部分开源组件一致。假设你的开发板是arm架构,工具链前缀是arm-linux-gnueabihf-,完整的编译流程是:
# 1. 生成configure脚本 ./autogen.sh # 2. 设置交叉编译环境变量 export CC=arm-linux-gnueabihf-gcc export CFLAGS="-Os -Wall -Wno-unused-result" # 3. 配置,指定目标平台和logo ./configure --host=arm-linux-gnueabihf \ --prefix=/usr \ --with-logo=/path/to/my_logo.png # 4. 编译并安装到临时目录 make -j4 make install DESTDIR=$PWD/_install # 5. 把产物打包到rootfs cp _install/usr/bin/psplash /your/rootfs/usr/bin/这里有个容易被忽略的点:psplash的设计原则是尽量避免运行时依赖。PNG图片在编译期已经被转成头文件,程序本身不需要链接libpng,链接的库越少,启动早期加载越顺利。当你在configure输出里看到“checking for libpng”之类字样时,要确认它不会把动态库依赖带进最终二进制。
一条验证命令是:
arm-linux-gnueabihf-readelf -d _install/usr/bin/psplash | grep NEEDED如果结果里只有libc和libgcc相关,说明依赖很干净。我习惯把这一步写进构建脚本里,每次交叉编译后自动检查,免得某次配置调整把不需要的库带进来。
3.2 把psplash放进systemd启动链路
psplash需要一个“在系统完全起来之前被拉起来”的位置。用systemd时,单元文件的写法是:
[Unit] Description=Startup splash screen DefaultDependencies=no After=systemd-vconsole-setup.service Before=sysinit.target [Service] Type=simple ExecStart=/usr/bin/psplash StandardInput=tty StandardOutput=tty StandardError=tty [Install] WantedBy=sysinit.target关键点全在[Unit]段。DefaultDependencies=no是告诉systemd不要按默认依赖顺序乱排;Before=sysinit.target保证了psplash会在系统初始化目标达成之前启动。如果把它写成一个普通的After=multi-user.target服务,那开机画面出现时桌面也可能同时出现了,起不到“启动早期显示”的作用。
服务端写进度消息的脚本,我习惯用一路持续打开的FD来做:
#!/bin/sh # 99-boot-progress.sh FIFO=/run/psplash.fifo # 用exec把FD 3长期绑到fifo写端,避免每次echo都重新打开 exec 3> "$FIFO" kmsg() { echo "PROGRESS $1" >&3 echo "MSG $2" >&3 } /etc/init.d/udev start kmsg 20 "detecting hardware" /etc/init.d/networking start kmsg 45 "starting network" /etc/init.d/display-manager start kmsg 80 "loading desktop" echo "QUIT" >&3 exec 3>&-这样做的原因是命名管道在读写两端都打开时才能正常通信。psplash持有读端,如果脚本每次echo都重新open写端,管道缓冲区可能在上一次还没写完时就关闭了,偶尔会丢消息。
3.3 替换logo与进度条样式
替换logo要重新生成头文件。假设你的logo叫launch.png,想做出来一个名为LAUNCH的图像定义:
cd psplash sh make-image-header.sh /path/to/launch.png LAUNCH ls -lh LAUNCH.h然后把LAUNCH.h加入编译路径,在代码里找到绘制logo的地方,改成引用LAUNCH_IMG。整个过程不需要改任何图片解码逻辑,因为这个组件本来就是“图片进、数组出”。
进度条的样式调整集中在psplash-config.h和psplash-colors.h两个文件。常见的参数如下:
/* 进度条在主画面上的横向起始位置 */ #define PSPLASH_BAR_X 100 /* 进度条宽度和高度 */ #define PSPLASH_BAR_WIDTH 400 #define PSPLASH_BAR_HEIGHT 24 /* 进度条离底部的距离 */ #define PSPLASH_BAR_Y_OFFSET 80颜色则在psplash-colors.h里改:
#define PSPLASH_BAR_BG_COLOR 0x1a1a1a /* 背景色,接近黑 */ #define PSPLASH_BAR_FG_COLOR 0x2e8b57 /* 前景色,我用墨绿,耐看 */改完之后重新编译,重新打包,进度条的位置和颜色就变了。如果你要适配竖屏,init里传给psplash一个旋转角度参数,比如90度,渲染层会把坐标系转过来,画布本身不需要改。
4. 避坑排查记录:画面不出、花屏、卡死的定位思路
psplash代码量不大,翻车场景却高度集中。这里把最常见的四类情况按“现象、原因、解决”三个步骤写清楚,都是我实际处理过的方向。
4.1 现象:psplash一闪而过,或者根本没有出现
有几次板子启动后,屏幕上干干净净,跟没集成一样。查psplash进程状态,发现服务根本没起来,或者起来一下就被杀掉。
原因通常有两个。第一个是/dev/fb0在psplash启动时还不存在。psplash在init里open设备节点,如果帧缓冲驱动没加载完,open会失败,进程直接退出。第二个是systemd的单元文件排序不对,服务被拖到很晚才启动,画面一闪就进入桌面。
解决思路是:确认内核配置里有对应的显示驱动,并且在initramfs阶段或者udev规则里保证/dev/fb0先出现。我后来习惯用一条udev规则兜底:
KERNEL=="fb0", TAG+="systemd", ACTION=="add", RUN+="/usr/bin/psplash --daemon"如果psplash做成了daemon模式,设备节点一出现就会自动拉起,不依赖服务排序,这个方案在几块不同板子上都工作正常。
4.2 现象:分辨率不对导致花屏、拉伸
画面出来了,但明显花或者比例不对。这个问题的根源通常是帧缓冲的分辨率和屏幕物理分辨率不一致。psplash绘制logo时按fbvar.xres和yres计算位置,如果分辨率被设置成1280x720但屏幕实际是1024x768,图片自然会错位。
原因在于启动早期bootloader传给内核的video参数,或者内核配置里默认的分辨率。
解决方法是明确指定video参数:
video=1024x768-24@60写在启动参数里。注意位深也要带上,24位和32位的像素排列并不一样,psplash按bpp做字节偏移,位深错了就会花。改完参数后,可以用一串命令确认实际生效值:
cat /sys/class/graphics/fb0/virtual_size cat /sys/class/graphics/fb0/bits_per_pixel4.3 现象:进度条一动不动
画面出来了,logo正常,但进度条卡在初始位置。排查步骤是:先看FIFO文件是否存在,再看写端的权限是否正确。
常见的原因是psplash创建FIFO时权限不够,或者systemd服务脚本跑在PrivateTmp这种隔离空间,导致它写不到psplash打开的管道。现象就是写端打开成功但消息发不进去。
解决方法是把FIFO放到固定路径,并显式给权限:
# 在psplash启动脚本里预先创建 mkfifo /run/psplash.fifo 2>/dev/null || true chmod 0666 /run/psplash.fifo进度条不动的另一个隐蔽原因是atoi解析失败。写过“PROGRESS 100%”这种格式,数字后面带百分号,atoi遇到非数字字符会停下来,解析结果可能不对。我在之前项目里遇到过一次,排查了很久才发现是脚本里多打了个百分号。从那以后我对所有写进FIFO的命令都严格限制格式,不跟任何多余的符号。
4.4 现象:logo颜色明显不对或发黑
这通常是位深或字节序的问题。psplash在做RGB到fb像素的转换时,对不同bpp有不同处理:16位是RGB565,24位是RGB888,32位还要考虑alpha通道。如果屏幕本身是32位但驱动实际只填了24位有效数据,字节序不对就会出现红蓝互换或者整体偏色。
解决方法是检查fb_var_screeninfo里的bits_per_pixel和red、green、blue的偏移定义:
fbset -i /dev/fb0fbset输出里能看到R、G、B的位域偏移,如果偏移和psplash里默认假设不一致,就需要给psplash打补丁或者调整内核驱动配置。这块没有通用解,必须按每块屏的实际参数来对齐。
5. 二次开发实战:给psplash扩展一个滚动文本状态区
很多产品在启动画面上不只显示logo和进度条,还想叠加一行文字,比如“正在加载内核模块”“正在启动网络”。psplash原生只支持MSG消息,但它只是一行短暂的文本提示,不是持续停留的状态区。这一章我用一个滚动文本状态区的例子,演示从数据结构到协议扩展再到验证的完整二次开发流程。
5.1 改哪些文件、加什么数据结构
改动集中在三处:psplash.h里加一个文本区结构体,psplash-fb.c里加绘制函数,psplash-fifo.c的解析分支里加TEXT命令。先定义数据结构:
/* psplash.h 中新增 */ #define TEXT_LINE_MAX 64 #define TEXT_LINE_COUNT 4 typedef struct { int x, y, w, h; /* 文本区位置和大小 */ char lines[TEXT_LINE_COUNT][TEXT_LINE_MAX]; int line_count; /* 当前写了多少行 */ } PSplashTextArea;它的作用是把文本区设计成一个能容纳四行文字的小窗口,新消息进来时向上滚动,最上面一行被顶掉。
绘制逻辑用一个简单的5x7点阵字体,不依赖任何字体服务。字形的数据用一个二维数组存,这里只列两个字符示意:
/* 5x7 ASCII 点阵数据,只列 A 和 B 做示例 */ static const uint8_t font5x7[][7] = { ['A'] = {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, ['B'] = {0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E}, }; static void psplash_text_draw_char(PSplashFB *fb, int x, int y, char c) { int i, j; uint8_t glyph; if (c < 0 || c > 127) c = '?'; glyph = font5x7[(unsigned char)c][0]; for (j = 0; j < 7; j++) { glyph = font5x7[(unsigned char)c][j]; for (i = 0; i < 5; i++) { if (glyph & (0x10 >> i)) { /* 命中一个前景像素,画一个1x1白点 */ psplash_fb_fill_rect(fb, x + i, y + j, 1, 1, 0xff, 0xff, 0xff); } } } }这段代码本质上是把字形数组里的每一位映射成屏幕上的一个小方点。5x7的意思是宽5像素高7像素,拉到屏幕上如果不明显,可以把绘制时的单位放大2倍或3倍。psplash_fb_fill_rect调用的是已有函数,不新增设备操作,所以这条二次开发路径非常干净。
5.2 FIFO协议扩展:新增TEXT命令
在psplash_fifo_read里加一个分支,命令格式是“TEXT 一行要显示的文字”:
else if (strncmp(buf, "TEXT ", 5) == 0) { /* 去掉行尾换行符 */ buf[strcspn(buf, "\n")] = '\0'; psplash_text_scroll(fb, buf + 5); }滚动函数负责把现有三行往上移一行,把新内容写到最下面:
void psplash_text_scroll(PSplashFB *fb, const char *msg) { PSplashTextArea *ta = &text_area; int i; /* 旧内容整体上移一行 */ for (i = 1; i < ta->line_count; i++) { strncpy(ta->lines[i - 1], ta->lines[i], TEXT_LINE_MAX - 1); } /* 新内容写到最末行 */ if (ta->line_count < TEXT_LINE_COUNT) ta->line_count++; strncpy(ta->lines[ta->line_count - 1], msg, TEXT_LINE_MAX - 1); /* 重绘整个文本区,先清后画 */ psplash_fb_fill_rect(fb, ta->x, ta->y, ta->w, ta->h, 0x00, 0x00, 0x00); for (i = 0; i < ta->line_count; i++) { psplash_text_draw_string(fb, ta->x, ta->y + i * 10, ta->lines[i]); } psplash_fb_flush(fb); }参数说明:text_area的x和y是文本区左上角坐标,w和h定义区域大小,重绘时先用背景色填充整个区域,再逐行画字形,避免老内容和残留像素叠在一起。psplash_text_draw_string是psplash_text_draw_char的循环包装,内部遍历字符串逐个字符调用绘制函数。
这套协议扩展后,启动脚本里的写法就变成:
echo "TEXT loading kernel modules" > /run/psplash.fifo echo "PROGRESS 30" > /run/psplash.fifo echo "TEXT starting network" > /run/psplash.fifo5.3 验证修改:在虚拟帧缓冲上跑一把
改完代码先别急着烧板子。psplash依赖/dev/fb0,在普通PC上不一定有真实帧缓冲设备,所以我一般用内核的虚拟显卡驱动跑验证。做法是启动一个带vfb驱动的虚拟机内核,把/dev/fb0设备虚拟出来,然后把编译好的psplash放进去执行。
验证步骤很简单:
# 在虚拟机的rootfs里执行 /usr/bin/psplash & sleep 1 echo "TEXT hello from splash" > /run/psplash.fifo echo "PROGRESS 50" > /run/psplash.fifo echo "QUIT" > /run/psplash.fifo第一行文字出现,进度条走到一半,再退出,整个链路就算通了。如果虚拟显卡的深度设置和实际板子有差异,我在命令行加一个Angle旋转参数和默认分辨率参数去模拟目标环境,测试时可以省去反复烧写rootfs的时间。这个验证习惯是我踩过一次坑之后养成的:上次只改了一个颜色宏,直接烧板子,结果因为binfmt配置问题浪费了半天,从那以后凡是动了渲染相关代码,都会先在虚拟帧缓冲环境跑一遍再进真实硬件。
6. 参数调优与收尾习惯:量产前的最后几道工序
功能跑通只是开始,要让psplash在生产环境稳定工作,还有几个细节值得看。
6.1 几个值得调的参数
| 参数 | 位置 | 作用 |
|---|---|---|
| PSPLASH_BAR_X / PSPLASH_BAR_Y | psplash-config.h | 进度条坐标,多分辨率适配时优先调整 |
| PSPLASH_BAR_WIDTH / HEIGHT | psplash-config.h | 进度条尺寸,视觉效果相关 |
| PSPLASH_BAR_FG_COLOR | psplash-colors.h | 进度条前景色,量产时和品牌色统一 |
| Angle旋转参数 | 启动参数 | 竖屏设备需要传90或270 |
参数的坑在于不同分辨率下边距和宽度不成比例。我一般用宽高比去算,而不是固定像素值。比如进度条宽取屏幕宽度的60%,高度取4%左右,代码里改成从fb->width计算,这样换屏幕后只要重新编译,视觉比例基本不变。
6.2 发布前我固定会做的一套检查
每次给新板子做启动画面,我都会强制走一遍这几步:确认/dev/fb0设备权限可读可写,确认FIFO路径和systemd脚本里写的一致,确认进度脚本在QUIT之后不会再有新消息写入,最后用readelf检查二进制依赖是否干净。其中FIFO权限最常见问题,量产rootfs里用户和组可能和开发环境不一样,我一般直接在/systemd服务里预创建并chmod,不依赖父进程继承。
从那次因为权限问题导致开机画面进度条卡住之后,我每次移植psplash都会先在最小系统里手工执行一遍FIFO通信测试,再进完整rootfs验证,这套流程承受住了几次真实量产项目的考验。psplash本身是个小项目,但把它接入启动流程这件事非常吃时序和环境细节,希望这篇拆解能帮你少走几趟弯路,顺利把启动画面跑起来。
本文还有配套的精品资源,点击获取