简介:psplash源码是一份面向ARM架构嵌入式开发者的开源启动画面工具包,主要用于在Linux系统启动或更新阶段接管屏幕,显示带进度条与动画效果的图形界面,常见于树莓派、开发板等设备,帮助开发者定制开机Logo与加载提示,提升产品交互体验。资源包共79个文件,约2.67MB,以C源码与头文件为主体,包含psplash.c、psplash-fb.c等核心实现,以及configure、Makefile等构建脚本,另有po本地化文件、png图片素材与sample示例,覆盖framebuffer后端、颜色配置、图像头生成等模块,便于理解启动画面从初始化到渲染的完整链路。目前已有667人学习下载。通过研读源码,读者可掌握进度条绘制、动画刷新与内核进度交互的实现方式,并据此修改config.h调整颜色字体、适配特定硬件后端,为嵌入式系统打造更友好的开机界面。
1. 开机第一帧画面从哪来:一个被忽略的嵌入式启动组件
做过嵌入式 Linux 的人大概都遇到过这种场景:板子上电,内核已经跑起来了,但屏幕要等好几秒才亮,中间那段黑屏或者花屏的时间,客户一眼就看到了。很多人第一反应是去查 LCD 驱动、背光时序,折腾半天发现驱动没问题——真正卡住的是用户态第一个图形程序还没起来。psplash 就是干这件事的:它是嵌入式系统里那个「开机第一帧」的轻量级启动画面程序,通常由 init 脚本在挂载根文件系统后立刻拉起,在完整桌面环境或 Qt 应用启动之前,先把一张静态图或者进度条画到 framebuffer 上。它不依赖 X11、不依赖 Wayland,直接操作/dev/fb0,编译出来只有几十 KB,启动时间以毫秒计。如果你正在做嵌入式产品、想定制开机 logo、或者想搞清楚「从内核到应用」这段空窗期到底发生了什么,这份 psplash 源码值得逐行拆一遍。
2. 架构与目录结构:一个 framebuffer 程序的极简骨架
2.1 它到底怎么把图显示出来的
psplash 的核心思路非常朴素:打开 framebuffer 设备,把一张预先转好的图片数据直接写进显存。它不解析 PNG/JPG,而是用一个叫make-image-header.sh的脚本,把图片转成 C 数组头文件,编译进二进制。这样运行时不需要任何图片解码库,省掉了 libpng、libjpeg 这些依赖,启动路径极短。
整个程序是单进程、单线程的事件循环模型。主流程大致是:解析命令行参数 → 打开/dev/fb0→ 读取 framebuffer 的var和fix信息(分辨率、位深、stride)→ 把图片数据按行拷贝到映射的内存 → 进入一个管道读取循环,等待外部通过 FIFO 发来的进度更新命令。这个 FIFO 默认是/tmp/psplash_fifo,别的程序(比如 init 脚本或升级程序)往里面写PROGRESS 50这样的文本,psplash 就重绘进度条。
这种设计的好处是解耦:psplash 本身不知道系统启动到哪一步了,它只负责「画」,进度由外部驱动。坏处也很明显——如果没人往 FIFO 写数据,它就永远停在初始画面。
2.2 目录里都有什么
拿到源码包,顶层通常是这几个文件:
psplash.c 主程序,事件循环和绘制逻辑 psplash.h 公共头文件,结构体与宏定义 psplash-fb.c framebuffer 操作封装 psplash-fb.h psplash-console.c 与虚拟终端(VT)的交互 psplash-console.h psplash-colors.h 颜色配置 psplash-poky-img.h 默认图片数据(由脚本生成) make-image-header.sh 图片转 C 数组的脚本 psplash-systemd.c systemd 集成辅助 psplash-write.c 往 FIFO 写命令的小工具psplash-fb.c是理解整个项目的钥匙。它把 framebuffer 的打开、映射、像素写入、区域刷新都封装成函数,psplash.c只调用这些接口。这种分层让移植到不同屏幕时,改动集中在psplash-fb.c里。
2.3 关键数据结构
psplash.h里定义了两个核心结构体,理解它们就理解了 psplash 的数据流:
/* psplash.h 节选 */ struct PSplashFB { int fd; /* /dev/fb0 的文件描述符 */ struct fb_var_screeninfo var; /* 可变参数:分辨率、位深 */ struct fb_fix_screeninfo fix; /* 固定参数:显存起始、长度 */ char *data; /* mmap 映射后的显存指针 */ int stride; /* 每行字节数,注意可能大于 width*bpp */ int bpp; /* bits per pixel */ int rgb; /* 像素格式标志 */ }; struct PSplashImage { int width; int height; const unsigned char *data; /* 编译进来的图片数据 */ };stride这个字段是新手最容易翻车的地方。很多 LCD 控制器要求每行字节数按 4 字节或 16 字节对齐,所以stride往往不等于width * bpp / 8。如果你按width去算偏移,画面会斜掉。psplash 在psplash-fb.c里用fix.line_length来取 stride,这是正确做法。
3. 核心代码拆解:从打开 framebuffer 到画出进度条
3.1 打开并映射 framebuffer
psplash-fb.c里的psplash_fb_new是整个程序的入口函数,它完成了设备打开和内存映射:
/* psplash-fb.c 节选,逻辑说明见下方 */ PSplashFB *psplash_fb_new(int angle, int fbdev_id) { struct fb_var_screeninfo var; struct fb_fix_screeninfo fix; char fbdev[16]; int fd; PSplashFB *fb; /* 默认用 /dev/fb0,也支持传入 fb1、fb2 */ snprintf(fbdev, sizeof(fbdev), "/dev/fb%d", fbdev_id); fd = open(fbdev, O_RDWR); if (fd < 0) { fprintf(stderr, "cannot open %s: %s\n", fbdev, strerror(errno)); return NULL; } /* 先读 fix,拿到显存长度和行跨度 */ if (ioctl(fd, FBIOGET_FSCREENINFO, &fix) < 0) { close(fd); return NULL; } /* 再读 var,拿到分辨率和位深 */ if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0) { close(fd); return NULL; } fb = malloc(sizeof(PSplashFB)); fb->fd = fd; fb->var = var; fb->fix = fix; fb->stride = fix.line_length; fb->bpp = var.bits_per_pixel; /* 把显存映射到用户空间,之后直接写 fb->data 就是写屏幕 */ fb->data = mmap(NULL, fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb->data == MAP_FAILED) { free(fb); close(fd); return NULL; } return fb; }这段代码有几个参数值得注意。fbdev_id默认是 0,对应/dev/fb0;如果板子有双屏,可以传 1 用/dev/fb1。angle参数用于旋转显示,psplash 支持 0、90、180、270 四个角度,旋转逻辑在像素写入函数里处理,不是在这里。mmap的长度用fix.smem_len,这是驱动报告的显存总长度,不要自己用width * height * bpp / 8去算,因为可能包含双缓冲或对齐填充。
映射成功后,fb->data就指向了显存。往这个指针写数据,屏幕立刻变化,不需要write系统调用,也不需要ioctl刷新(除非驱动要求)。
3.2 像素写入与区域刷新
psplash 不是每次重绘整屏,而是只更新变化的区域。核心函数是psplash_fb_draw_rect和psplash_fb_flush:
/* 在指定矩形区域填充颜色 */ void psplash_fb_draw_rect(PSplashFB *fb, int x, int y, int width, int height, uint32_t color) { int i, j; char *row = fb->data + y * fb->stride + x * (fb->bpp / 8); for (i = 0; i < height; i++) { uint32_t *p = (uint32_t *)row; for (j = 0; j < width; j++) { *p++ = color; /* 假设 32bpp,直接写像素值 */ } row += fb->stride; /* 跳到下一行,注意用 stride 不是 width */ } }这里color的格式取决于 framebuffer 的像素格式。如果是 RGB565(16bpp),就不能用uint32_t *去写,需要按 16 位处理。psplash 在初始化时会根据var.red.offset、var.green.offset、var.blue.offset判断格式,但简化版代码通常只处理 32bpp 和 16bpp 两种。
psplash_fb_flush在大多数驱动上是空操作,因为 framebuffer 是直接映射的。但在某些驱动上,需要调用ioctl(fd, FBIOPAN_DISPLAY, &var)来翻页,或者用FBIOPUT_VSCREENINFO触发刷新。如果你发现画面写进去了但不显示,先检查这里。
3.3 进度条是怎么画出来的
进度条本质是一个矩形,宽度随进度百分比变化。psplash 在psplash.c里维护了一个progress变量,每次收到 FIFO 命令就更新它,然后重绘进度条区域:
/* psplash.c 节选,进度条绘制逻辑 */ static void psplash_draw_progress(PSplashFB *fb, int progress) { int bar_width, bar_height; int x, y; /* 进度条位置和尺寸,通常从图片元数据或宏定义来 */ bar_width = 400; bar_height = 20; x = (fb->var.xres - bar_width) / 2; y = fb->var.yres - 100; /* 先画背景框 */ psplash_fb_draw_rect(fb, x, y, bar_width, bar_height, 0x333333); /* 再画已完成部分,宽度按百分比算 */ psplash_fb_draw_rect(fb, x, y, bar_width * progress / 100, bar_height, 0x00AA00); }progress的取值范围是 0 到 100。FIFO 里收到的命令格式是PROGRESS 50,解析后调用这个函数。注意进度条区域每次都要先擦掉旧内容再画新的,否则会出现残影。psplash 的做法是先用背景色填满整个进度条区域,再画已完成部分,这样即使进度回退也能正确显示。
3.4 FIFO 命令循环
主循环在psplash.c的main函数末尾,它打开 FIFO 并阻塞读取:
/* 打开 FIFO,如果不存在就创建 */ mkfifo("/tmp/psplash_fifo", 0666); fd = open("/tmp/psplash_fifo", O_RDONLY | O_NONBLOCK); while (1) { char buf[64]; int n = read(fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; if (strncmp(buf, "PROGRESS", 8) == 0) { int p = atoi(buf + 9); psplash_draw_progress(fb, p); } else if (strncmp(buf, "QUIT", 4) == 0) { break; /* 收到退出命令,清理后结束 */ } } usleep(100000); /* 100ms 轮询一次,避免空转占 CPU */ }这里用O_NONBLOCK加轮询而不是阻塞读,是为了能同时处理退出信号。如果直接阻塞在read上,收到SIGTERM时可能无法及时退出。usleep(100000)让出 CPU,100ms 的延迟对进度条来说完全够用。
外部程序往 FIFO 写命令的方式很简单:
echo "PROGRESS 30" > /tmp/psplash_fifopsplash-write.c编译出的小工具就是干这个的,它把命令行参数拼成字符串写进 FIFO,方便在 init 脚本里调用。
4. 编译与运行:从源码到板子上的第一张图
4.1 交叉编译环境准备
psplash 的构建系统是 Autotools,需要先配置交叉编译工具链。假设你的工具链前缀是arm-linux-gnueabihf-,典型流程如下:
# 生成 configure 脚本,源码包通常已带,没有就执行 ./autogen.sh # 配置交叉编译,指定目标架构和安装路径 ./configure --host=arm-linux-gnueabihf \ --prefix=/usr \ CFLAGS="-O2 -march=armv7-a" # 编译 make -j4 # 安装到临时目录,方便打包进根文件系统 make DESTDIR=/path/to/rootfs install--host告诉 configure 这是交叉编译,它会去找arm-linux-gnueabihf-gcc。如果工具链不在 PATH 里,需要先export PATH=$PATH:/opt/toolchain/bin。CFLAGS里的-march根据你的板子 CPU 调整,Cortex-A7 用armv7-a,Cortex-A53 用armv8-a。
编译产物主要是psplash可执行文件和psplash-write小工具。如果启用了 systemd 集成,还会生成psplash-systemd。
4.2 替换开机图片
默认图片是 poky 的 logo,要换成自己的,用make-image-header.sh:
# 把 PNG 转成 C 头文件,参数是图片路径和输出名 ./make-image-header.sh my-logo.png POKY # 生成的 my-logo-img.h 会覆盖默认的 psplash-poky-img.h # 重新编译即可 make clean && make脚本内部调用 ImageMagick 的convert把 PNG 转成 RGBA 原始数据,再用xxd或自定义工具转成 C 数组。注意图片尺寸不要超过屏幕分辨率,否则会被截断。格式建议用 PNG,脚本会自动处理 alpha 通道。
如果你不想重新编译,也可以把图片数据放到文件系统里,运行时读取。但 psplash 默认不支持这种模式,需要改代码——在psplash_fb_new之后加一段从文件加载图片的逻辑,把数据指针指向读到的 buffer。这种做法适合需要频繁换图的场景,代价是启动时多一次文件 IO。
4.3 在 init 脚本里拉起 psplash
psplash 通常放在/etc/init.d/或 systemd 的psplash-start.service里。以 SysV init 为例:
#!/bin/sh # /etc/init.d/S00psplash case "$1" in start) # 后台启动 psplash,指定 FIFO 路径 /usr/bin/psplash -f /tmp/psplash_fifo & ;; stop) # 通知 psplash 退出 echo "QUIT" > /tmp/psplash_fifo ;; esac-f参数指定 FIFO 路径,默认就是/tmp/psplash_fifo,可以不写。启动顺序很重要:psplash 必须在 framebuffer 驱动加载之后、其他图形程序之前启动。如果系统用 systemd,可以写一个After=sysinit.target的 service,Type=forking,ExecStart指向 psplash。
停止 psplash 的时机也要注意。如果 Qt 应用启动后 psplash 还在后台,它会继续占用 framebuffer,导致 Qt 画面被覆盖。正确做法是在 Qt 启动前发QUIT命令,或者让 Qt 启动脚本先 kill 掉 psplash 进程。
4.4 验证是否生效
板子上电后,如果一切正常,你应该在 1 到 2 秒内看到图片。如果没看到,按这个顺序排查:
先确认/dev/fb0存在且权限正确,ls -l /dev/fb0应该有crw-rw----。然后手动运行psplash,看有没有报错输出。如果提示cannot open /dev/fb0,检查内核是否编译了 framebuffer 驱动,以及设备节点是否创建。如果提示mmap failed,可能是显存长度不对,用fbset命令查看当前 framebuffer 参数。
画面显示但颜色不对,通常是像素格式不匹配。用fbset -i看bits_per_pixel和rgba字段,确认 psplash 里的格式判断逻辑是否覆盖了你的屏幕。如果屏幕是 BGR 而不是 RGB,需要在psplash-fb.c里调整颜色通道顺序。
5. 避坑与常见问题:那些让我熬夜的 framebuffer 细节
5.1 画面斜掉或错位
现象:图片显示出来了,但每一行都往右偏移一点,整体呈平行四边形。
原因:像素写入时用了width * bpp / 8作为行偏移,而不是fix.line_length。很多 LCD 控制器要求行对齐,比如宽度 800、32bpp 时,800 * 4 = 3200字节,但如果控制器要求 16 字节对齐,实际 stride 可能是 3200 或 3216。用错 stride 就会逐行累积偏移。
解决:所有行偏移计算统一用fb->stride,这个值在psplash_fb_new里从fix.line_length取得。检查psplash_fb_draw_rect和图片拷贝函数,确保没有硬编码width * 4。
5.2 进度条不更新
现象:图片显示了,但往 FIFO 写PROGRESS 50没反应。
原因:FIFO 打开方式不对,或者写命令的格式不匹配。psplash 用O_NONBLOCK打开 FIFO,如果写端在 psplash 启动前就打开了,读端可能收不到数据。另外命令字符串必须完全匹配,PROGRESS 50后面不能有多余空格或换行以外的字符。
解决:先用echo "PROGRESS 50" > /tmp/psplash_fifo手动测试,确认 FIFO 存在且权限是 0666。如果手动写有效但脚本里无效,检查脚本是否在 psplash 启动前就执行了写操作。可以在写之前加sleep 1等待 psplash 就绪。
5.3 退出后屏幕花屏
现象:发QUIT命令后 psplash 退出了,但屏幕残留半张图或者花屏。
原因:psplash 退出时没有清理 framebuffer 内容,或者清理不完整。某些驱动在进程退出后不会自动清屏,显存里还是旧数据。
解决:在QUIT处理分支里,退出前用背景色填满整个屏幕。调用psplash_fb_draw_rect(fb, 0, 0, fb->var.xres, fb->var.yres, 0x000000)清屏,再munmap和close。如果花屏依旧,可能是驱动双缓冲问题,尝试在清屏后调用ioctl(fd, FBIOPAN_DISPLAY, &var)强制翻页。
5.4 高分辨率屏幕启动慢
现象:1080p 或更高分辨率的屏幕上,psplash 启动要好几秒才出图。
原因:图片数据太大,从存储介质读取和写入显存的时间变长。如果图片是未压缩的 RGBA,1920x1080 就是 8MB 左右,从 eMMC 读进来再写显存,耗时明显。
解决:降低图片分辨率,或者用 RGB565 代替 RGBA8888,数据量减半。如果必须用高分辨率,考虑把图片放在 initramfs 里,减少存储 IO 延迟。另外可以只画屏幕中央一块区域,四周用纯色填充,减少数据量。
5.5 和 Qt 应用抢 framebuffer
现象:Qt 应用启动后画面闪烁,或者 psplash 的图盖在 Qt 界面上。
原因:psplash 没有及时退出,两个程序同时往/dev/fb0写数据。
解决:在 Qt 启动脚本的最前面加echo "QUIT" > /tmp/psplash_fifo,然后sleep 0.5等 psplash 完全退出再启动 Qt。更稳妥的做法是在 psplash 的QUIT处理里加一个信号量或锁文件,Qt 启动前检查锁文件是否存在,存在就等待。
6. 二次开发:把 psplash 改成你自己的启动画面系统
6.1 从静态图到动态效果
默认 psplash 只显示一张静态图加进度条,但你可以让它动起来。最直接的做法是在主循环里加定时器,每隔几百毫秒重绘一部分区域,实现旋转图标或渐变动画。比如做一个旋转的 loading 圈:
/* 在主循环里加一个计数器,每次轮询更新角度 */ static int angle = 0; angle = (angle + 10) % 360; psplash_draw_spinner(fb, center_x, center_y, radius, angle);psplash_draw_spinner需要自己实现,思路是用三角函数算出圆弧上若干点的坐标,用psplash_fb_draw_rect画小方块拼成圆环。注意每次重绘前擦掉上一帧,否则会拖影。这种动画对 CPU 占用很低,因为只更新圆环区域,不是全屏重绘。
6.2 支持多张图片切换
产品启动过程可能分几个阶段:uboot logo → kernel logo → psplash 第一阶段 → psplash 第二阶段。你可以在 FIFO 命令里加一个IMAGE指令,让 psplash 切换到不同的图片:
if (strncmp(buf, "IMAGE", 5) == 0) { int idx = atoi(buf + 6); if (idx >= 0 && idx < image_count) { current_image = &images[idx]; psplash_fb_draw_image(fb, current_image); } }images数组里存放多张图片的元数据,每张图对应一个启动阶段。init 脚本在适当时机发IMAGE 1、IMAGE 2来切换。这样比重新启动 psplash 进程更平滑,没有闪烁。
6.3 把进度信息接入 systemd
如果系统用 systemd,可以让 psplash 自动跟踪服务启动进度。psplash-systemd.c就是干这个的,它监听 systemd 的 D-Bus 信号,把服务启动状态转成进度百分比写入 FIFO。你需要确保编译时启用了 systemd 支持:
./configure --host=arm-linux-gnueabihf --with-systemd然后启用psplash-start.service和psplash-systemd.service。systemd 版本不同,D-Bus 接口可能有差异,如果进度不动,先用busctl monitor看信号是否正常发出。
6.4 验证改造效果的方法
每次改完 psplash,不要直接烧到板子上试,先在本地用fbset模拟一个 framebuffer 环境。Linux 桌面可以用uvesafb或vesafb加载一个虚拟 framebuffer,然后运行 psplash 看效果。更简单的方法是用 QEMU 模拟 ARM 环境,挂载一个最小根文件系统,在虚拟机里验证启动流程。
我自己的习惯是:改任何 framebuffer 相关代码,先在 QEMU 里跑一遍,确认没有越界写和 stride 错误,再上真板子。因为 framebuffer 越界写往往不会立刻崩溃,而是花屏或者随机重启,排查起来非常痛苦。从那以后我每次动psplash-fb.c都强制走一遍 QEMU 验证,这个习惯帮我省了至少三个通宵。希望帮到你。
本文还有配套的精品资源,点击获取