简介:psplash源码包是一份面向ARM嵌入式Linux平台的开机启动界面实现,主要解决系统引导与升级过程中屏幕反馈缺失的问题,常用于树莓派、开发板等设备,以进度条和动画效果提升用户体验。资源共79个文件,压缩包大小约2.67MB,文件类型涵盖C语言源码、头文件、Makefile与configure构建脚本、shell辅助脚本、PNG图像素材,以及编译过程中生成的.o对象文件,能够完整反映从配置到生成可执行文件的构建链条,结构清晰便于按模块查阅。该资源已有667人学习浏览。源码内含framebuffer显示后端、图形绘制、控制台输出与进度写入等核心模块,并预留颜色、字体、进度条样式等配置项,适合嵌入式开发者分析启动进度条渲染机制、定制动画参数或移植到特定硬件平台。结合源码结构和构建配置,还可深入理解Linux启动阶段用户界面的实现方式,以及交叉编译工具链的典型用法,为优化启动流程、压缩内存占用提供实际参考。
1. 开机Logo其实不是一张图:psplash解决的是启动期的那段黑屏
设备上电到桌面出现之间的那几秒,一直是嵌入式开发最不想碰的“黑匣子”。内核自带的Logo一闪而过之后,用户态图形界面还在慢慢初始化,屏幕通常就是一片死黑。psplash源码就是为这段空白准备的:它直接往framebuffer上写像素,不依赖X11、不依赖Wayland,也不需要完整的图形栈,只要内核把/dev/fb0暴露出来,它就能在启动早期把自定义Logo和进度条画到屏幕上。适合三类人看:被开机黑屏坑过的BSP工程师、想给设备定制开机页的产品开发者、以及想搞明白“没有图形栈怎么画图”的底层学习者。读这份源码之前先记住一个反直觉的结论:开机画面不是一张图片文件,而是一段C代码直接在显存里画出来的。
2. 源码结构速览:两个可执行程序加一个图像预处理脚本
拿到源码包解压后,第一件事是先把目录结构摸清楚。psplash的代码量不大,但布局很典型,是一个标准的 autotools 工程:顶层有configure.ac、Makefile.am,源码集中在根目录下,附带一个图像处理脚本和一个init脚本模板。整体逻辑可以概括成一句话:一个程序负责画,一个程序负责收消息,一个脚本负责把图片变成C数组。
2.1 三个关键文件:psplash、psplash-write、make-image-header.sh的分工
我习惯先用tree扫一眼全貌,重点关注跟业务直接相关的几个文件:
psplash/ ├── configure.ac ├── Makefile.am ├── psplash.c ├── psplash-fb.c ├── psplash-fb.h ├── psplash-write.c ├── psplash.h ├── psplash-config.h.in ├── psplash-init.sh └── make-image-header.sh这套源码里最核心的产物是两个可执行程序:psplash是主程序,负责初始化framebuffer、绘制背景、绘制进度条和文字;psplash-write是个极小的小工具,由init脚本调用,把进度百分比写进一条命名管道。make-image-header.sh则是构建期工具,把设计师给的PNG图片转成一个C头文件,里面是一段gzip压缩后的像素数组,编译时直接嵌进二进制。
三个文件的分工非常清楚:主程序不会自己去读文件系统里的图片,因为启动早期根文件系统可能还没就绪,所以图片以数组形式内置;psplash-write也不做任何绘图,它只负责打通“shell脚本 → 管道 → 主程序”这条链路。这种职责拆分让主程序在任何时刻都不依赖外部工具,只要内核没崩,它就能画。
2.2 图像预处理:PNG图片是如何变成头文件里的一串数字
如果只改Logo不动其他逻辑,你只需要会跑make-image-header.sh就够了。这个脚本的核心思路是:把PNG解成未压缩的像素流,量化到256色以内,再gzip压缩,最后用xxd生成C头文件的字节数组。核心转换逻辑类似这样:
#!/bin/sh # usage: make-image-header.sh <input.png> <output_name> pngtopnm "$1" 2>/dev/null | pnmquant 255 | pnmtohex > /tmp/img.hex gzip -c /tmp/img.hex | xxd -i -n "$2" > "$2.h"因为图片数据是gzip压缩后内嵌的,主程序运行时需要先解压再渲染。这样做的原因很朴实:一张全屏RGB图片解压前的体积可能超过1MB,但嵌入式设备的Flash存储和RAM都很紧张,压缩后往往能小一个量级。取值上pnmquant 255把颜色数压到256色,是为了配合脚本里后续的索引色绘制,如果你的Logo渐变很多,可以在这里调大色数,但二进制体积会随之上涨。
2.3 configure.ac与Makefile.am:autotools是怎么把这两段代码拼起来的
构建系统用的是标准的 autoconf + automake,configure.ac里做了三件关键事情:检测交叉编译工具链、配置framebuffer头文件路径、根据--with-color-depth之类的参数确定像素格式。
AC_INIT([psplash], [0.1]) AC_PROG_CC AC_CONFIG_HEADERS([psplash-config.h]) AC_ARG_WITH([color-depth], [AS_HELP_STRING([--with-color-depth], [bits per pixel: 16, 32])], [COLOR_DEPTH="$withval"], [COLOR_DEPTH="32"]) AC_SUBST([COLOR_DEPTH]) AC_CONFIG_FILES([Makefile]) AC_OUTPUT--with-color-depth是二次开发时最常碰到的开关。它最终会变成psplash-config.h里的一个宏,主程序编译时根据这个宏决定按RGB565还是RGB888来写像素。如果显示屏是常见的RGB565小屏,你交叉编译时就要--with-color-depth=16,否则画出来颜色会发紫发绿,后面第5章会专门聊这个坑。
3. 渲染核心拆解:从/dev/fb0到一行一行把画面画出来
理解psplash的渲染逻辑,等于理解Linux framebuffer最朴素的用法。整个链路是:打开设备文件 → 用ioctl查询屏幕参数 →mmap映射显存 → 往显存里填像素。这个过程不经过任何中间层,所以快,也正因为快,它对时序很敏感,哪里慢半拍,屏幕就花给你看。
3.1 打开设备并映射显存:fbdev三段式操作
主程序启动后第一件事就是初始化PSplashFB结构体,核心代码在psplash-fb.c里,逻辑可以简化成下面这样:
static int fb_fd; static struct fb_var_screeninfo vinfo; static struct fb_fix_screeninfo finfo; static uint8_t *fb_base; int psplash_fb_init(PSplashFB *fb) { fb_fd = open("/dev/fb0", O_RDWR); if (fb_fd < 0) return -1; ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo); fb->width = vinfo.xres; fb->height = vinfo.yres; fb->depth = vinfo.bits_per_pixel; fb->stride = finfo.line_length; fb->data = mmap(NULL, fb->stride * fb->height, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); return 0; }这里三个ioctl之外的细节值得解释:finfo.line_length是一行像素在内存里占的字节数,它不一定等于width * (depth/8),因为内核为了DMA对齐可能在一行末尾填充多余字节。写像素时如果只按width计算而不按stride跳行,画面就会斜着裂开,这是新人最常见的花屏原因之一。另外MAP_SHARED必须加,因为主程序要实时看到内核刷新后的显存内容,如果用MAP_PRIVATE,写进去的数据不会真正落到设备上。
3.2 像素格式适配:RGB565与RGB888不是玄学,是位运算
拿到了bits_per_pixel之后,还要处理颜色格式。同样是32位深,有的屏是ARGB8888,有的屏是BGRA8888,如果不做转换,红色和蓝色通道就会对调。psplash 的做法是把像素格式抽象成一组偏移量,然后统一走一个psplash_fb_pixel函数拼颜色值:
static inline uint32_t psplash_fb_pixel(PSplashFB *fb, uint8_t r, uint8_t g, uint8_t b) { if (fb->depth == 16) { return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); } return (r << fb->red_offset) | (g << fb->green_offset) | (b << fb->blue_offset); }注意16位深时不需要看偏移量,直接按RGB565拼,因为这是约定俗成的标准;但32位深时一定要从fb_var_screeninfo结构体里读真实的red_offset、green_offset、blue_offset字段。有些GPU的默认偏移是22、11、0,有些是16、8、0,写死任何一个都会翻车。源码里那个颜色校正逻辑其实就是针对这个问题的补丁,很多人在自己的板子上看到Logo颜色不对,第一反应是屏幕坏了,实际上是这里少读了一个字段。
3.3 绘制画面:背景、Logo、进度条的叠画顺序
psplash 的绘制是典型的“先整体后局部”:先清屏填背景色,再画Logo,然后画进度条轨道,最后画进度条填充。绘制矩形最底层的函数长这样:
void psplash_fb_draw_rect(PSplashFB *fb, int x, int y, int w, int h, uint32_t color) { int i, j; for (j = y; j < y + h; j++) { for (i = x; i < x + w; i++) { *((uint32_t *)(fb->data + j * fb->stride + i * (fb->depth / 8))) = color; } } }这个双重循环看着笨,但在嵌入式上是能接受的,因为启动画面不需要每帧60fps,它只需要在进度条变化时重绘一小块区域。你会在源码里看到调用方传入的不是全屏坐标,而是psplash_fb_draw_rect(fb, 20, height-50, progress_percent * width / 100, 20, color)这样的局部矩形,重绘快,也不会和屏幕刷新打架。字体方面,psplash 内置了一套点阵字体数组,psplash_fb_draw_text按位扫描字体点阵,逐像素落到显存里,所以它不依赖任何字体库,这也是它能在极早期运行的原因。
4. 进程间通信设计:一条命名管道串起两个二进制
绘图主程序跑起来了,脚本怎么告诉它“现在下载进度到42%了”?答案是/psplash_fifo这条命名管道。这种设计把“绘制”和“业务逻辑”彻底隔离,init脚本只管往管道里丢字符串,主程序收到后再去更新画面。
4.1 为什么选命名管道而不是Unix socket或共享内存
启动早期根文件系统可能还是只读的,网络栈更没起来,Unix socket 依赖/var/run目录而且需要管理权限;共享内存需要额外锁机制,脚本侧操作起来太重。命名管道是内核提供的最小IPC:文件系统一就绪就能用,shell里一条echo "PROGRESS 50" > /psplash_fifo就能写数据,主程序侧只需要poll一个文件描述符。代价是它语义是“一次写穿”,如果写入长度超过PIPE_BUF,单次写可能被拆成多次,好在进度消息都很短,完全够用。
4.2 管道创建与写入端:一个极小的psplash-write工具
psplash-write的整个main函数可能不到40行,核心就是打开管道写字符串:
int main(int argc, char **argv) { int fd = open("/psplash_fifo", O_WRONLY | O_NONBLOCK); if (fd < 0) return -1; if (argc > 1) { write(fd, argv[1], strlen(argv[1])); } close(fd); return 0; }这个工具的巧妙之处在于完全不解析消息内容,它只负责把参数原样写进管道。真正的协议解析在主程序那边:PROGRESS 42表示设置进度,MSG hello表示显示一条滚动文字。这样写的好处是脚本侧不需要依赖任何库,BusyBox环境里也能直接跑。
4.3 主循环读取:poll加sscanf驱动重绘
主程序这边用poll循环监听管道读端,收到数据后按消息类型分别处理:
int psplash_main_loop(PSplashFB *fb) { int fifo_fd = open("/psplash_fifo", O_RDONLY | O_NONBLOCK); struct pollfd fds = { .fd = fifo_fd, .events = POLLIN }; while (1) { poll(&fds, 1, 1000); if (fds.revents & POLLIN) { char buf[128] = {0}; read(fifo_fd, buf, sizeof(buf) - 1); char cmd[16]; int value; if (sscanf(buf, "PROGRESS %d", &value) == 1) { psplash_draw_progress(fb, value); } else if (strncmp(buf, "MSG ", 4) == 0) { psplash_draw_text(fb, buf + 4, ...); } } } }注意这里读端也加了O_NONBLOCK,否则在没有任何写端的系统上,open会永远卡死在管道上,导致开机画面直接黑屏。如果你在自己移植时遇到“主程序起不来,日志停在打开FIFO”,十有八九是漏了O_NONBLOCK。poll的超时设成1000毫秒,是为了在没有任何消息进来时也能定期重绘一次,处理进度条动画和旋转图标的时钟刷新。
5. 避坑与排查:黑屏、花屏、进度条不动的四段实操经验
我在实际板子上跑psplash时,踩过的坑比想象中多得多。这里挑四个最有代表性的写下来,每一条都经历过“现象 → 找原因 → 解决”的完整过程,希望能让你少走点弯路。
5.1 现象:内核有启动信息,但主程序一跑就退出,屏幕还是黑的
排查这个问题的顺序很重要。先看/dev/fb0存不存在,ls -l /dev/fb0如果直接报No such file,那问题根本不在psplash,而在内核的framebuffer驱动没起来。用dmesg | grep fb能看到具体原因,常见的是设备树里没有使能显示控制器节点,或者时钟没配置对。驱动正常但依然黑屏的,去查主程序是不是因为/psplash_fifo打开失败而提前退出——早期根文件系统如果只挂载了只读根,mkfifo可能根本没有权限创建管道节点。
解决方式:把psplash的启动尽量往后挪,放在mount -a完成之后;或者更稳妥一点,让init脚本先手动创建FIFO再启动主程序:mkfifo /psplash_fifo 2>/dev/null; psplash &。
5.2 现象:Logo颜色整个偏掉,红色变蓝色,渐变区域全是噪点
这个我在一块ARM板子上折腾了一下午。原因基本锁定为位深和通道偏移不匹配。psplash默认按32位真彩色编译,但你的屏可能是16位RGB565的,或者反过来。颜色偏蓝偏绿时,先确认--with-color-depth的值跟fb_var_screeninfo里读到的bits_per_pixel一致。还有一种情况是主程序里的red_offset是从旧版结构体偏移硬读的,拿到的是0而不是16,导致红色通道整个丢了。
解决方式:在psplash_fb_init里加一行调试打印:
printf("offset: R=%d G=%d B=%d depth=%d\n", vinfo.red.offset, vinfo.green.offset, vinfo.blue.offset, vinfo.bits_per_pixel);把打出来的值和屏幕实际显示效果对比,比瞎猜快得多。
5.3 现象:init脚本卡死,后续服务起不来,进度条卡在某个百分比
这个坑最隐蔽,因为它不是psplash本身的问题,而是命名管道的阻塞特性引起的。init脚本执行echo 100 > /psplash_fifo时,如果此时主程序还没有打开管道读端,这个echo会一直阻塞等待,后面的启动步骤全部卡住。常见场景是主程序启动时动态链接库加载失败退出了,但脚本不知道,照写管道,整个用户态启动流程就死锁了。
解决方式:在psplash-write里加超时或者直接忽略错误,脚本侧可以用timeout 2 sh -c 'echo PROGRESS 50 > /psplash_fifo'包一层超时。更干净的做法是让主程序先创建并打开FIFO,再让init脚本后续写入,从根上避免竞态。
5.4 现象:交叉编译后放到板子上直接Segmentation fault,本地编译却没这个问题
这种“本地好好的,一交叉编译就崩”的问题,多半是编译期的configure检测脚本在交叉编译下跑错了。psplash的configure.ac里如果有AC_TRY_RUN之类的运行时检测,交叉编译时没法执行测试程序,会得到错误结果,生成错误的配置头文件。另外要注意CFLAGS里有没有带上-march相关的CPU指令集参数,给ARMv7编译的程序用了ARMv8的指令,跑到老内核上必崩无疑。
解决方式:交叉编译时用干净的--host=arm-linux-gnueabihf参数,同时把ac_cv_*相关变量显式传值,跳过运行时探测。举个例子:
./configure --host=arm-linux-gnueabihf \ --with-color-depth=16 \ ac_cv_func_malloc_0_nonnull=yes这个命令我在不同的板子上试过多次,能规避掉90%的交叉编译翻车现场。
6. 二次开发实战:换Logo、适配高分辨率、加一个固定消息区域
到这里,psplash的运行机制已经清楚了,把它改成自己的样子就不难。这一章讲三个我能直接给到你手上的改造,尤其是最后一个固定消息区域的技巧,很适合做“正在启动XX服务”这类提示。
先说换Logo。改Logo不需要碰任何C代码,只要拿到一张和目标屏幕分辨率匹配的PNG图,重新跑一遍图像预处理脚本再编译即可:
./make-image-header.sh my-logo.png psplash-logo # 会生成 psplash-logo.h把生成的头文件替换掉原来的Logo头文件,重新make,新的二进制里就内嵌了新图片。要注意的是图像比例,psplash默认把图片居中绘制在屏幕宽度50%的位置,如果原屏是800x480,图片做成400x240比较稳。
然后是分辨率适配。psplash本身是动态读取vinfo.xres和vinfo.yres的,理论上换屏不需要重编译,但字体的点阵大小和进度条宽度是编译期固定好的。改分辨率时,重点改psplash.h里的SPLASH_IMAGE_WIDTH、SPLASH_IMAGE_HEIGHT和进度条的高宽常量,这几个值直接决定画面在屏幕上占多大面积。如果产品屏幕从4.3寸换到7寸,通常把进度条宽度从屏幕宽度的70%调整到80%,视觉上更协调。
最后是一个具体改造:让MSG消息在屏幕底部固定显示,而不是默认的滚动字幕。psplash默认收到MSG会把文字放在顶部滚动,滚动逻辑在psplash_draw_text里会不断重绘位置。改成固定消息需要两步:在主循环里新增一个static字符串变量,收到MSG时更新字符串而不是直接绘制;然后单独画一个底部区域:
/* 主循环收到 MSG 时走这里 */ static char status_msg[128]; strncpy(status_msg, buf + 4, sizeof(status_msg) - 1); /* 每次重绘时在底部固定位置输出 */ psplash_fb_draw_rect(fb, 0, fb->height - 40, fb->width, 40, background_color); psplash_fb_draw_text(fb, status_msg, 8, fb->height - 28, message_color);这样改完之后,脚本里调用psplash-write "MSG 正在加载网络服务"就能在屏幕底部看到固定的状态文字。验证方法也很简单:开发机上加载虚拟framebuffer内核模块,比如modprobe vfb,然后直接运行编译好的二进制,在另一个终端里反复执行./psplash-write "MSG test",观察底部文字是否变化,不用每次改动都烧一遍SD卡。
从那以后,我每次给新板子适配开机画面,都强制自己走一遍这个流程:先确认/dev/fb0出现,再打印通道偏移校准颜色,最后用一条管道消息验证通信链路。这套顺序下来,黑屏和花屏基本都死在前两个环节,省下来的时间够改好几个版本了。希望帮到你。
本文还有配套的精品资源,点击获取