news 2026/10/21 13:01:04

嵌入式Linux开机黑屏终结者:psplash源码深度解析与实战改造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux开机黑屏终结者:psplash源码深度解析与实战改造

简介:psplash源码是一份面向嵌入式Linux开发者的开源启动画面工具包,专为ARM架构设备设计,可在系统启动或更新阶段接管屏幕,显示带进度条与动画效果的图形界面,常见于树莓派、开发板等平台。资源包共79个文件,约2.67MB,以C源码与头文件为核心,辅以Makefile、configure等构建脚本,以及po本地化文件、png图片素材和sample示例,覆盖初始化、图形绘制、framebuffer后端与配置等模块,便于理解其与Linux内核协作获取加载进度的机制。已有667人学习下载。通过阅读源码,开发者可掌握定制界面、调整动画、修改config.h配色字体、交叉编译集成到目标系统的方法,并借助backend目录适配不同硬件,为嵌入式产品打造更友好的启动体验。

1. 开机第一帧画面从哪来:psplash 源码能帮你解决什么

做过嵌入式 Linux 的朋友大概都遇到过这个场景:内核已经跑起来了,串口日志哗哗地刷,但屏幕还是一片漆黑,直到十几秒后 Qt 或 LVGL 的界面才慢悠悠地弹出来。这中间十几秒的黑屏,客户看到的第一反应往往是「这机器是不是坏了」。psplash 就是专门填这段空白的那个东西——它是个极轻量的用户态启动画面程序,由 systemd 或 init 脚本在极早期拉起,把一张图片直接画到 framebuffer 上,等真正的 UI 进程准备好之后再退出。

这份 psplash 源码包适合两类人:一类是正在做嵌入式产品、被开机黑屏问题卡住的开发者,另一类是接手了老项目、发现启动 logo 改不动、想搞清楚底层渲染链路的人。它不依赖 X11、不依赖 Wayland,甚至连图形库都不需要,直接操作/dev/fb0,代码量小到可以一个下午通读完。下面我就按「它怎么跑起来 → 核心渲染链路怎么走 → 怎么改成自己的东西」这条线,把这份源码拆开讲一遍。

2. 编译与运行链路:从 autotools 到 framebuffer 的第一帧

2.1 构建系统与依赖关系

psplash 用的是经典的 autotools 构建体系,源码根目录下能看到configure.ac、Makefile.am这些文件。它对外部依赖的要求极低,核心只依赖 libc 和 framebuffer 设备节点,如果要用到图片转换工具,才会额外依赖 libpng 之类的库。

先看构建入口的配置逻辑:

# 生成 configure 脚本,注意 autoreconf 的版本兼容性 autoreconf -vfi # 配置编译选项,--enable-xxx 控制是否编译辅助工具 ./configure --host=arm-linux-gnueabihf \ --prefix=/usr \ --disable-static # 编译并安装到目标根文件系统 make -j4 make install DESTDIR=/path/to/rootfs

这里有几个参数值得说明。--host指定交叉编译工具链前缀,嵌入式场景下必须写对,否则会编出 x86 的二进制。--prefix=/usr决定安装路径,psplash 默认会装到/usr/bin/psplash,同时把图片转换脚本装到/usr/bin/psplash-img之类的位置。--disable-static是为了减小体积,嵌入式根文件系统寸土寸金,动态链接更划算。

编译完成后会得到两个关键产物:一个是psplash主程序,另一个是psplash-write这个小工具。后者很多人会忽略,但它其实是运行时和 psplash 通信的入口,后面讲进度条更新时会用到。

2.2 启动脚本与进程拉起时机

psplash 不是自己启动的,它需要被 init 系统在合适的时机拉起来。常见做法是在 systemd 里写一个 service,或者在 SysV init 的 rcS 脚本里加一行。关键点是:它必须在内核 framebuffer 驱动初始化完成之后、真正的 UI 进程启动之前运行。

# /etc/init.d/rcS 中的典型写法 if [ -e /dev/fb0 ]; then /usr/bin/psplash --angle=0 --fbdev=/dev/fb0 & echo $! > /var/run/psplash.pid fi

--angle参数控制画面旋转角度,支持 0、90、180、270 四个值,横屏竖屏切换时用得上。--fbdev指定 framebuffer 设备节点,多屏场景下可能是/dev/fb1。末尾的&让 psplash 后台运行,不阻塞启动流程。把 PID 写进文件是为了后续能精确控制它退出。

这里有个容易翻车的点:如果 psplash 启动太早,/dev/fb0还没被内核创建出来,程序会直接报错退出,屏幕依然黑着。所以脚本里那个if [ -e /dev/fb0 ]的判断不是可有可无的装饰,而是血泪经验换来的。

2.3 主程序入口与初始化流程

psplash 的main函数在psplash.c里,逻辑非常直白。它做的事情可以概括为:解析命令行参数、打开 framebuffer 设备、把图片数据刷到显存、然后进入一个事件循环等待外部命令。

int main(int argc, char *argv[]) { struct psplash_pipe_handler handler; int fbdev = -1; int angle = 0; /* 解析命令行参数,--angle 和 --fbdev 在这里被读取 */ parse_options(argc, argv, &angle, &fbdev); /* 打开 framebuffer 设备,失败则直接退出 */ if (fbdev < 0) { fbdev = open("/dev/fb0", O_RDWR); if (fbdev < 0) { fprintf(stderr, "cannot open fb0\n"); return 1; } } /* 初始化 framebuffer 信息结构,获取分辨率、bpp 等 */ if (psplash_fb_open(&fb, fbdev, angle) < 0) return 1; /* 绘制初始启动画面 */ psplash_draw_splash(&fb); /* 进入主循环,监听管道命令 */ psplash_main(&fb, &handler); return 0; }

psplash_fb_open这个函数是理解整个渲染链路的关键。它会通过FBIOGET_VSCREENINFO和FBIOGET_FSCREENINFO两个 ioctl 拿到屏幕的物理参数,包括分辨率、每像素位数、行字节数等。这些参数决定了后面画图时怎么计算偏移量。如果屏幕是 16bpp 的 RGB565,而你的图片是 32bpp 的 ARGB8888,那就需要在写入前做格式转换,否则颜色会花掉。

psplash_draw_splash负责把编译进二进制的图片数据拷贝到 framebuffer 的 mmap 内存区域。注意这里用的是 mmap 而不是 write,因为 framebuffer 的写入是直接操作显存,mmap 之后就是内存拷贝,效率高得多。

3. 核心渲染机制:framebuffer 操作与图片数据组织

3.1 framebuffer 参数获取与 mmap 映射

framebuffer 是 Linux 内核提供的一套统一显示接口,应用程序通过 ioctl 获取屏幕信息,通过 mmap 把显存映射到用户空间,之后就可以像操作普通内存一样往屏幕上画东西。psplash 把这套流程封装在psplash_fb.c里。

int psplash_fb_open(struct psplash_fb *fb, int fd, int angle) { struct fb_var_screeninfo vinfo; struct fb_fix_screeninfo finfo; /* 获取可变屏幕信息:分辨率、bpp、时序等 */ if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) < 0) return -1; /* 获取固定屏幕信息:显存起始地址、长度、行字节数 */ if (ioctl(fd, FBIOGET_FSCREENINFO, &finfo) < 0) return -1; fb->width = vinfo.xres; fb->height = vinfo.yres; fb->bpp = vinfo.bits_per_pixel; fb->line_length = finfo.line_length; /* 计算显存总大小并做 mmap 映射 */ fb->fb_size = finfo.line_length * vinfo.yres; fb->data = mmap(0, fb->fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb->data == MAP_FAILED) return -1; return 0; }

vinfo.xres和vinfo.yres是屏幕的可见分辨率,finfo.line_length是每一行占用的字节数。注意这个行字节数不一定等于xres * bpp / 8,因为有些硬件会对齐到 32 字节或 64 字节边界。如果你自己写画图函数时用xres * bpp / 8去算偏移,在某些平台上就会画歪,这是嵌入式图形开发里非常经典的坑。

mmap的最后一个参数是偏移量,framebuffer 场景下固定为 0。映射大小用finfo.line_length * vinfo.yres而不是xres * yres * bpp / 8,同样是为了兼容行对齐。

3.2 图片数据的编译期嵌入

psplash 默认会把一张 PNG 图片转换成 C 数组,直接编译进二进制文件。这样做的好处是启动时不需要挂载文件系统、不需要读文件,速度最快。转换脚本通常是psplash-img或者make-image-header.sh之类的工具。

# 把 PNG 转成 C 头文件,生成 g_image_data 数组 ./make-image-header.sh logo.png POKY > logo.h # 生成的头文件里包含这样的结构 # static const uint32_t g_image_data[] = { 0xRRGGBBAA, ... }; # static const int g_image_width = 480; # static const int g_image_height = 272;

生成的头文件里,每个像素用一个 32 位整数表示,格式通常是 ARGB8888。图片的宽高也会作为常量写进去,方便运行时做缩放或居中计算。如果你想把启动 logo 换成自己的,最直接的办法就是替换这个 PNG 然后重新编译。

但这里有个限制:图片尺寸最好和屏幕分辨率一致,或者至少保持宽高比。如果图片比屏幕大,psplash 不会自动缩放,超出部分会被截断;如果比屏幕小,周围会留黑边。我一般会先把 logo 做成和屏幕分辨率完全一致的尺寸,省去运行时计算的麻烦。

3.3 像素格式转换与颜色空间处理

不同 framebuffer 设备的像素格式差异很大,常见的有 16bpp RGB565、24bpp RGB888、32bpp ARGB8888。psplash 内部统一用 32bpp 处理图片数据,写入 framebuffer 时再根据实际 bpp 做转换。

/* 根据 framebuffer 的 bpp 选择不同的写入路径 */ static void psplash_fb_plot(struct psplash_fb *fb, int x, int y, uint32_t pixel) { if (x < 0 || x >= fb->width || y < 0 || y >= fb->height) return; /* 计算目标像素在显存中的字节偏移 */ uint8_t *dst = fb->data + y * fb->line_length + x * (fb->bpp / 8); switch (fb->bpp) { case 16: /* RGB565:取高 5 位红、6 位绿、5 位蓝 */ *(uint16_t *)dst = ((pixel >> 8) & 0xf800) | ((pixel >> 5) & 0x07e0) | ((pixel >> 3) & 0x001f); break; case 24: dst[0] = (pixel >> 16) & 0xff; dst[1] = (pixel >> 8) & 0xff; dst[2] = pixel & 0xff; break; case 32: *(uint32_t *)dst = pixel; break; } }

这段代码里 16bpp 的转换逻辑值得细看。RGB565 的红色占高 5 位、绿色占中间 6 位、蓝色占低 5 位,所以从 32 位像素提取时要先右移再掩码。如果移位量写错,颜色就会偏。比如红色右移 8 位后取0xf800,实际拿到的是原始红色分量的高 5 位。这个转换在 16bpp 屏幕上如果搞错,整个画面会偏绿或偏紫,非常显眼。

24bpp 的写入顺序是 BGR 还是 RGB,取决于具体硬件,psplash 默认按 RGB 顺序写。如果发现颜色通道反了,红蓝互换,那就是字节序问题,需要调整写入顺序。

4. 运行时控制:进度条更新与进程间通信

4.1 管道通信机制

psplash 启动后会创建一个命名管道,通常是/tmp/psplash_fifo或者通过环境变量指定的路径。其他进程往这个管道里写命令,psplash 读取后执行相应动作。这套机制让启动过程中的各个阶段可以动态更新画面,比如显示「正在加载内核模块」「正在挂载文件系统」之类的进度。

/* psplash 主循环中读取管道命令 */ static int psplash_main(struct psplash_fb *fb, struct psplash_pipe_handler *handler) { char buf[256]; int fifo_fd = open(PSPLASH_FIFO, O_RDONLY | O_NONBLOCK); while (1) { int n = read(fifo_fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; /* 解析命令并分发处理 */ if (strncmp(buf, "PROGRESS", 8) == 0) { int pct = atoi(buf + 9); psplash_draw_progress(fb, pct); } else if (strncmp(buf, "QUIT", 4) == 0) { break; } } usleep(100000); /* 100ms 轮询间隔 */ } return 0; }

管道用O_NONBLOCK打开,配合usleep做轮询,而不是阻塞在read上。这样做的好处是 psplash 还能同时处理其他事情,比如响应信号。100ms 的轮询间隔意味着进度条更新有最多 100ms 的延迟,对启动画面来说完全够用。

PSPLASH_FIFO这个宏定义在头文件里,默认路径是/tmp/psplash_fifo。如果根文件系统的/tmp不可写,就需要在编译时改掉这个路径,或者通过环境变量覆盖。

4.2 进度条绘制与局部刷新

进度条不是重新画整张图,而是只更新变化的那一小块区域。psplash 会保存进度条的起始坐标、宽度、高度,每次更新时只重绘这个矩形范围。

void psplash_draw_progress(struct psplash_fb *fb, int percentage) { int bar_width = fb->width * percentage / 100; int bar_x = (fb->width - bar_width) / 2; int bar_y = fb->height - 40; /* 距底部 40 像素 */ /* 只刷新进度条区域,避免全屏重绘 */ for (int y = bar_y; y < bar_y + 20; y++) { for (int x = bar_x; x < bar_x + bar_width; x++) { psplash_fb_plot(fb, x, y, 0xFFFFFFFF); } } }

进度条的位置和尺寸是硬编码的,bar_y = fb->height - 40表示距底部 40 像素,高度 20 像素。如果要调整位置,改这两个值就行。颜色0xFFFFFFFF是白色,改成其他值就能换颜色。

局部刷新的意义在于减少显存带宽占用。全屏重绘在低端硬件上可能导致画面撕裂或闪烁,只刷变化区域就稳得多。这也是为什么 psplash 能在资源极其有限的设备上跑得很流畅。

4.3 psplash-write 工具的使用

psplash-write是配套的命令行工具,用来往管道里写命令。启动脚本里可以在不同阶段调用它更新进度。

# 更新进度到 30% psplash-write "PROGRESS 30" # 更新进度到 80% psplash-write "PROGRESS 80" # 通知 psplash 退出,让真正的 UI 接管屏幕 psplash-write "QUIT"

QUIT命令会让 psplash 跳出主循环、清理资源、退出进程。这一步很关键:如果 psplash 不退出,它会一直占着 framebuffer,后面的 UI 进程就没法往屏幕上画东西了。常见做法是在 Qt 或 LVGL 启动脚本的最后一行调用psplash-write "QUIT",然后立刻启动 UI 进程。

如果发现 UI 启动后屏幕还是 psplash 的画面,八成是QUIT没发出去,或者 psplash 进程卡死了。这时候可以ps aux | grep psplash看看进程还在不在,在的话手动kill掉。

5. 避坑与常见问题排查

5.1 屏幕全黑但进程还在

现象:ps aux能看到 psplash 进程,但屏幕没有任何变化。

原因通常是 framebuffer 设备打开失败或者 mmap 失败,但程序没有正确报错退出。有些平台的/dev/fb0权限不对,普通用户无法打开;还有些平台 framebuffer 驱动加载较慢,psplash 启动时设备节点还没创建好。

解决办法:在启动脚本里加延时或循环等待,确认/dev/fb0存在且可读写之后再拉起 psplash。另外检查 psplash 的 stderr 输出,编译时加上-DDEBUG可以看到更多日志。

5.2 颜色显示异常

现象:画面能出来,但颜色明显偏色,比如红色变蓝色、整体偏绿。

原因:像素格式不匹配。图片数据是 ARGB8888,但 framebuffer 实际是 RGB565,转换时的移位量算错了。或者 24bpp 模式下字节序搞反了。

解决办法:先用fbset命令确认屏幕的实际 bpp 和像素格式,然后对照psplash_fb_plot里的转换逻辑逐位检查。RGB565 的红色掩码是0xf800,绿色是0x07e0,蓝色是0x001f,这三个值不能搞混。

5.3 进度条不更新

现象:psplash 画面出来了,但调用psplash-write "PROGRESS 50"之后进度条纹丝不动。

原因:管道路径不一致。psplash 主程序打开的管道路径和psplash-write写入的路径不是同一个。默认都是/tmp/psplash_fifo,但如果编译时改了宏定义,或者环境变量PSPLASH_FIFO设置不同,就会对不上。

解决办法:用strace跟踪psplash-write的open调用,看它实际打开的是哪个路径,然后确认 psplash 主程序打开的是同一个。另外检查管道文件的权限,确保写入进程有写权限。

5.4 退出后屏幕残留

现象:psplash-write "QUIT"之后 psplash 进程退出了,但屏幕还停留在启动画面,UI 进程启动后画面叠加在一起。

原因:psplash 退出时没有清屏,framebuffer 里还保留着最后画的像素。UI 进程如果只画了部分区域,没覆盖到的地方就会露出 psplash 的残留。

解决办法:在 psplash 退出前把 framebuffer 清成黑色,或者让 UI 进程启动时先做一次全屏填充。我一般会在QUIT处理逻辑里加一个memset(fb->data, 0, fb->fb_size),把显存清零再退出。

5.5 旋转角度不生效

现象:--angle=90传进去了,但画面还是横屏显示,没有旋转。

原因:psplash 的旋转是在软件层面做的,需要重新计算每个像素的映射坐标。如果psplash_fb_open里没有正确处理 angle 参数,或者旋转后的宽高没有交换,画面就会错乱。

解决办法:确认psplash_fb_open里对 angle 的处理逻辑,旋转 90 度和 270 度时要把宽高对调。另外旋转后的坐标映射公式要仔细推导,x 和 y 的变换关系容易写反。

6. 二次开发改造:换 logo、加动画、对接 systemd

6.1 替换启动 logo 的完整流程

换 logo 是最常见的改造需求。流程不复杂,但有几个细节容易忽略。首先准备一张和屏幕分辨率一致的 PNG 图片,然后用转换脚本生成头文件,最后重新编译。

# 1. 准备图片,确保尺寸和屏幕一致 # 假设屏幕是 800x480 convert logo.png -resize 800x480! logo_resized.png # 2. 生成头文件,注意输出变量名要和源码里的引用一致 ./make-image-header.sh logo_resized.png POKY > logo.h # 3. 替换源码中的 logo.h,重新编译 cp logo.h /path/to/psplash/ make clean && make -j4

convert命令里的!表示强制缩放到指定尺寸,不保持宽高比。如果不想变形,去掉!但要注意留黑边的问题。生成的头文件里变量名通常是POKY对应的g_image_data,如果源码里引用的是别的名字,需要同步改掉。

6.2 添加开机动画的思路

psplash 本身是静态画面加进度条,但它的渲染循环结构很容易改造成动画。核心思路是在主循环里加一个定时器,每隔固定时间切换一帧图片。

/* 在主循环里加入帧切换逻辑 */ static int frame_index = 0; static const uint32_t *frames[] = { frame0_data, frame1_data, frame2_data }; static const int frame_count = 3; while (1) { /* 每 200ms 切换一帧 */ psplash_draw_image(fb, frames[frame_index]); frame_index = (frame_index + 1) % frame_count; usleep(200000); /* 同时还要处理管道命令,不能阻塞 */ /* ... */ }

这里的关键是动画刷新和管道命令处理要放在同一个循环里,不能各跑各的。如果动画帧率要求高,usleep的时间要相应缩短,但要注意别把 CPU 占满。我一般会把帧间隔控制在 100ms 到 300ms 之间,既能看出动画效果,又不会明显增加启动耗时。

6.3 与 systemd 的集成方式

现代嵌入式系统越来越多用 systemd 做 init,psplash 需要写一个 service 文件。关键点是Type=simple和Before=的依赖顺序。

[Unit] Description=Startup Splash Screen DefaultDependencies=no After=sysinit.target Before=multi-user.target [Service] Type=simple ExecStart=/usr/bin/psplash --angle=0 ExecStop=/usr/bin/psplash-write QUIT RemainAfterExit=yes [Install] WantedBy=multi-user.target

DefaultDependencies=no是为了让 psplash 尽早启动,不受常规依赖链的约束。Before=multi-user.target保证它在用户空间服务启动前运行。ExecStop里的psplash-write QUIT让 systemd 停止服务时能正确通知 psplash 退出,而不是直接 kill。

RemainAfterExit=yes这个选项有点反直觉:它让 systemd 认为服务在ExecStart结束后仍然处于活跃状态。因为 psplash 是后台运行的,如果不加这个,systemd 会以为服务已经退出了。这个坑我踩过不止一次,表现是 psplash 画面一闪而过就没了,查了半天才发现是 systemd 把进程回收了。

从那以后我每次集成 psplash 到 systemd,都会先用systemctl status确认服务状态是active (running)而不是inactive (dead),再去看屏幕。希望这些经验能帮到你,少走点弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 12:57:56

从技能列表到技能系统:能力盘点与项目制学习实战指南

去年年底我给自己做年度复盘&#xff0c;翻出一直在更新的技能清单&#xff0c;发现里面列了二十多项“我会的东西”&#xff1a;Python、剪辑、SQL、会议主持、PPT设计、篮球、吉他……每一项背后都有理由&#xff0c;每一项又都说不清到底到了什么水平。更尴尬的是&#xff0…

作者头像 李华
网站建设 2026/10/21 12:56:29

STM32驱动74HC595级联原理与实现:3个GPIO扩展N×8路输出

简介&#xff1a;这是一份面向STM32开发者的74HC595移位寄存器可级联驱动代码包&#xff0c;适用于需要通过串入并出移位寄存器扩展GPIO、驱动数码管或LED矩阵的常见场景&#xff0c;也适合正在学习Keil MDK下外设驱动开发的初学者。压缩包整体仅1KB&#xff0c;共包含2个文件&…

作者头像 李华
网站建设 2026/10/21 12:53:29

AI漫剧出海全流程:从内容生产线到全球变现策略

1. 项目概述&#xff1a;漫剧出海这件事到底在做什么 1.1 核心需求解析 先把这个话题拆开来看。AI漫剧&#xff0c;就是用AI工具批量生产的有剧情的漫画短视频&#xff0c;单集时长一般在1到3分钟&#xff0c;以竖屏为主&#xff0c;节奏密集、反转频繁、画面简单但有张力。出…

作者头像 李华
网站建设 2026/10/21 12:53:04

多模态嵌入模型EmbeddingGemma 2的离线RAG实践

跑离线 RAG 这件事&#xff0c;最容易被低估的其实是嵌入模型。很多人搭检索链路先盯生成模型&#xff0c;随便拿一个通用向量接口顶上&#xff0c;结果文档切了一堆、索引也建了&#xff0c;一查出来全是噪音。等想换成多模态场景&#xff0c;发现原来的嵌入模型根本不读图&am…

作者头像 李华
网站建设 2026/10/21 12:52:15

从AIOps到Agentic Operations:智能体驱动运维架构与实操指南

1. 从 AIOps 到 Agentic Operations 的范式跃迁 1.1 为什么传统 AIOps 越来越不够用了 过去几年&#xff0c;但凡做运维的&#xff0c;几乎都绕不开 AIOps 这个词。监控告警、日志分析、根因定位、容量预测&#xff0c;每个环节都被贴上了“AI 赋能”的标签。但真正在一线扛过…

作者头像 李华
网站建设 2026/10/21 12:52:03

FAWE中文汉化实战:语言文件修改与排错全教程

跑过一阵子联机服的人应该都遇到过这种情况&#xff1a;装好 FastAsyncWorldEdit&#xff08;后面我都叫它 FAWE&#xff09;&#xff0c;进游戏敲几条//set、//copy&#xff0c;控制台反馈过来的全是英文&#xff0c;或者干脆中英混杂&#xff0c;一句“已设置”后面跟着一段英…

作者头像 李华