资讯动态

psplash源码解析:从帧缓冲到FIFO,玩转嵌入式Linux启动画面

发布时间:2026/10/17 14:28:32 来源:尧图企业网站定制
简介面向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.shlogo是怎么变成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 CCarm-linux-gnueabihf-gcc export CFLAGS-Os -Wall -Wno-unused-result # 3. 配置指定目标平台和logo ./configure --hostarm-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] DescriptionStartup splash screen DefaultDependenciesno Aftersystemd-vconsole-setup.service Beforesysinit.target [Service] Typesimple ExecStart/usr/bin/psplash StandardInputtty StandardOutputtty StandardErrortty [Install] WantedBysysinit.target关键点全在[Unit]段。DefaultDependenciesno是告诉systemd不要按默认依赖顺序乱排Beforesysinit.target保证了psplash会在系统初始化目标达成之前启动。如果把它写成一个普通的Aftermulti-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规则兜底KERNELfb0, TAGsystemd, ACTIONadd, RUN/usr/bin/psplash --daemon如果psplash做成了daemon模式设备节点一出现就会自动拉起不依赖服务排序这个方案在几块不同板子上都工作正常。4.2 现象分辨率不对导致花屏、拉伸画面出来了但明显花或者比例不对。这个问题的根源通常是帧缓冲的分辨率和屏幕物理分辨率不一致。psplash绘制logo时按fbvar.xres和yres计算位置如果分辨率被设置成1280x720但屏幕实际是1024x768图片自然会错位。原因在于启动早期bootloader传给内核的video参数或者内核配置里默认的分辨率。解决方法是明确指定video参数video1024x768-2460写在启动参数里。注意位深也要带上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位是RGB56524位是RGB88832位还要考虑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_Ypsplash-config.h进度条坐标多分辨率适配时优先调整PSPLASH_BAR_WIDTH / HEIGHTpsplash-config.h进度条尺寸视觉效果相关PSPLASH_BAR_FG_COLORpsplash-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本身是个小项目但把它接入启动流程这件事非常吃时序和环境细节希望这篇拆解能帮你少走几趟弯路顺利把启动画面跑起来。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑