首页 / 资讯中心 / 文章详情

全志T113-i嵌入式Linux启动优化实战:从6秒到2.5秒

全志T113-i嵌入式Linux启动优化实战:从6秒到2.5秒 ★ FEATURED ARTICLE
做嵌入式的人应该都有这种经历产品硬件选型定了屏幕、外设、协议栈都跑通了结果一上电系统启动画面要等三四秒甚至更久才出来。客户不会管你是跑高大上的Android还是精简的Linux他们只关心“按下开机键到界面可操作”要多久。这个项目就是围绕全志T113-i这颗芯片把启动时间从原来的6秒左右压到2.5秒以内的完整实战记录涉及U-Boot裁剪、内核启动优化、根文件系统精简以及Qt和LVGL这两套GUI框架的上层配合。目标很直接不换硬件、不加料用软件手段把启动时间“抠”出来。无论你是刚接触全志平台还是在做类似ARM Linux产品启动优化这篇文章都值得参考。先说结论最终在量产板上实现了上电到Qt首帧显示约2.3秒到LVGL界面完整可交互约2.1秒。整个优化过程不是某一步的功劳而是“引导、内核、根文件系统、应用加载”四段接力每一段都省几百毫秒最后才有肉眼可见的质变。下面按实操顺序拆开讲每个环节都会给出配置和实测数据。1. 项目概览与优化目标拆解1.1 为什么把目标定在2.5秒用户对嵌入式设备的耐心阈值很低尤其是工业HMI、车载后装、手持终端这类场景。行业里有个不成文的体验标准从上电到首帧UI出现在2秒左右人基本感知不到“卡”一旦超过3秒就会有明显的等待焦虑。T113-i是双核Cortex-A7主频1.2GHz定位中低端工控和智能硬件跑Linux系统本身不算快直接开机不做优化通常需要5到8秒。因此我把目标定为2.5秒既是一个有挑战但可实现的数字也刚好卡在用户体验的临界点上。还有一层原因T113-i内部集成了64MB DDR3没有独立内存颗粒这在高性价比方案里很常见。内存小意味着我们不能靠“预加载”“大页缓存”这些吃内存的手段必须从系统启动链路的每一毫秒上做文章。这个约束贯穿了整个项目的决策过程。1.2 启动时间基线测量与分解优化之前先量化。我在uboot命令行里用time命令手动记录关键节点在Linux内核里通过printk.time打开时间戳再用bootchart和initcall_debug拿到内核各阶段的耗时分布。第一次测量的基线数据大致是阶段耗时说明SPL到U-Boot加载约300ms含DDR初始化、SPL自拷贝U-Boot完整启动约900ms含外设初始化、env加载、kernel引导内核启动到挂载根文件系统约1.8s含驱动初始化、device tree解析init到Qt首帧约2.4s含文件系统挂载、库加载、QML解析合计约5.4s基线这个基线数据说明问题很清楚大头不在U-Boot而在内核初始化和用户态应用加载。很多人一上来就死磕U-Boot结果省了200ms用户态那里浪费的时间完全没动体验提升有限。我做优化的顺序是先砍大头再抠小头。1.3 整体优化路线图整个项目分四条线并行推进U-Boot侧去掉不必要的外设初始化、裁掉没用命令、优化DDR初始化流程和环境变量加载逻辑。内核侧裁剪驱动、关闭没用子系统、开启initcall异步或精简initcall层级、调整根文件系统挂载方式。根文件系统侧从完整Buildroot rootfs降到最小rootfs去掉bash、systemd等重型组件换成busybox init。应用侧Qt程序采用静态加载加预初始化必要时直接改用LVGL绕开QtEventLoop启动开销。最终版本在内核侧没有用太激进的手段比如没有做bootchart级联优化也没有上systemd-analyze这类工具因为T113-i资源有限保持配置简单才是长期维护的正道。2. U-Boot裁剪与SPL优化2.1 从SPL到U-Boot的启动流程T113-i从片上ROM启动后会先加载SPLSecondary Program Loader也就是T113-i的boot ROM固化代码负责初始化DDR、时钟然后从SD卡、eMMC或SPI Nor Flash加载下一级U-Boot。很多优化文章把SPL和U-Boot混在一起但它们的职责完全不同。SPL的耗时主要卡在DDR初始化而U-Boot的耗时主要卡在板级初始化、外设枚举、环境变量解析和boot命令执行。实际测量中SPL占用约200msU-Boot开机二级加载到kernel引导约900ms。SPL部分的裁剪空间很小因为DDR初始化是硬流程。我做的优化主要有两点一是把U-Boot中不用的设备树节点裁掉减少FDT解析时间二是把CONFIG_SPL_DM和CONFIG_DM相关选项精简去掉不需要的驱动模型。2.2 U-Boot配置裁剪关键项T113-i开发板使用全志的sunxi平台U-Boot。实际操作中我在configs/sun8i_t113_defconfig基础上做了以下几项裁剪去掉网络命令。没有网络启动需求就关掉CONFIG_CMD_NET、CONFIG_CMD_TFTP、CONFIG_CMD_PING等可以省掉协议栈初始化实测减少约80ms。关掉USB主机控制器。设备只有USB Device烧录口没有USB Host外设把CONFIG_USB、CONFIG_USB_EHCI、CONFIG_USB_OHCI关掉减少约50ms。精简命令集用\#define CONFIG_CMD_BOOTM、CONFIG_CMD_LOADB关闭CONFIG_CMD_FAT、CONFIG_CMD_EXT4因为kernel镜像直接放在根文件系统分区前段采用bootm或booti直接加载。设定CONFIG_BOOTDELAY0跳过倒计时等待。需要注意关掉文件系统支持后image加载方式会变化。我的量产方案是把kernel和dtb放到专门的小分区U-Boot直接用fatload或mmc read读固定扇区。因为这个缘故CONFIG_CMD_FAT虽然省时间但如果生产时用SD卡升级固件还是需要保留。我是开发阶段保留量产固件关闭两套defconfig分开维护。2.3 环境变量与bootcmd优化U-Boot启动时默认会读取环境变量如果环境变量存储在FAT分区或eMMC的某个环境块里加载过程会非常慢。实测eMMC环境下env default和env load可能各占几十毫秒。我直接改成固定环境变量关闭CONFIG_ENV_IS_IN_FAT改用CONFIG_ENV_IS_IN_MMC并把环境变量保存在U-Boot二进制后面的固定扇区这样读取速度非常快。另外优化了bootcmd。原来开发板的默认命令包含一串复杂的run distro_bootcmd会去扫描多个启动设备、多个分区非常耽误时间。我改成直接计算kernel在eMMC中的偏移用mmc read把kernel和dtb读到内存然后bootz ${kernel_addr_r} - ${fdt_addr_r}。这样整个bootcmd执行时间从原来的500ms左右降到80ms。# 原版bootcmd示意 run distro_bootcmd # 优化后bootcmd setenv bootcmd mmc dev 0; mmc read ${kernel_addr_r} 0x1000 0x4000; mmc read ${fdt_addr_r} 0x5000 0x1000; bootz ${kernel_addr_r} - ${fdt_addr_r}其中0x1000和0x5000是块地址0x4000是块数不同机器block大小可能不同要根据实际分区表换算。这个写死偏移的方式虽然不够通用但对量产固件来说完全可行而且能大幅减少启动时间。2.4 U-Boot阶段实测数据与心得优化后的U-Boot阶段数据优化项优化前优化后说明SPL加载210ms190msDDR初始化无法减少SPL自拷贝稍优化U-Boot外设初始化850ms610ms去掉网络/USB/FATbootcmd执行500ms80ms去掉扫描逻辑总耗时1.56s0.88s节省约680ms实际操作中有一个容易被忽略的地方U-Boot的CONFIG_SYS_MALLOC_LEN和驱动模型DM初始化。如果关了一些驱动DM框架本身仍会遍历所有驱动节点反而可能变慢。我一开始只关命令发现启动时间没怎么降后来把CONFIG_DM下的DM_GPIO、DM_SERIAL、DM_MMC留齐把无关的如DM_ETH、DM_USB关掉后时间才明显下降。裁剪不是盲目删要理解驱动模型的注册机制。3. 内核启动优化3.1 内核配置裁剪与尺寸控制U-Boot之后就是内核。T113-i在Buildroot下标准内核配置一般超过3MB解压和加载耗时比较多。我的做法是先精简内核配置目标是内核体积控制在2.5MB以内。关键措施是修改defconfig关闭CONFIG_SND、CONFIG_DRM、CONFIG_FB这些与显示输出无关的选项如果用Qt linuxfb其实不需要DRMLVGL更不需要。关闭CONFIG_NET和CONFIG_INET如果产品不需要网络功能。注意如果需要OTA升级就必须保留网络配置这时优先考虑模块化。关闭CONFIG_WIRELESS、CONFIG_INPUT_TOUCHSCREEN中未用到的具体触摸驱动。关闭内核DEBUG信息如CONFIG_DEBUG_KERNEL、CONFIG_KALLSYMS、CONFIG_DEBUG_FS但这些在调试阶段建议保留只在量产版本关闭。关闭CONFIG_SYSFS_DEPRECATED等兼容选项。内核镜像我用的是Image.gzU-Boot直接bootz加载。CONFIG_CMDLINE设置成consolettyS0,115200 root/dev/mmcblk0p4 rootwait rw避免从DTB里再读取多余参数。请注意rootwait会等待根设备对eMMC来说本来就很快但如果你用的是SD卡rootwait是必须的否则内核可能在mmc驱动完全初始化前就尝试挂载根文件系统导致启动失败。这个参数不能随便删。3.2 驱动初始化主要耗时点printk加initcall_debug以后能看到完整的内核initcall耗时表。T113-i上最耗时的是mmc_init、i2c_init、input_init和clk_init。其中mmc_init不仅初始化控制器还会做card detect和capacity检测耗时约100ms。i2c_init通常注册adapter耗时不大但如果有touchscreen在i2c总线上等待时间会长。我的做法是把不用的平台设备从设备树中删除尤其是不存在的i2c子节点、pwm节点、spi节点。关闭CONFIG_DEVTMPFS不这个不能关因为udev和mdev需要devtmpfs来创建设备节点。但可以关闭CONFIG_DEVKMSG等调试节点。把触摸屏驱动编成模块实际上模块机制反而会增加启动耗时因为要等udev加载不如直接编进内核且减少探测等待。对于I2C触摸屏探测期间如果设备没就绪I2C读操作会重试非常耗时。我量过某款触摸屏导致内核启动多了300ms最后通过修改驱动关闭设备不存在时的等待。在T113-i这种芯片上还有一个特殊点全志的sunxi驱动框架里有些驱动会做clk_prepare_enable和reset_control_assert/deassert如果时钟树配置不好某些外设的初始化会等待锁相环稳定。我会把不复用的外设时钟直接关闭避免无谓的初始化。3.3 init进程与根文件系统挂载方式优化内核启动最后阶段会执行根文件系统上的/init。Buildroot默认生成的是/sbin/init它要按/etc/inittab启动一系列脚本这部分很容易吃时间。我做了这些修改使用busybox init替代systemd。T113-i跑systemd是很吃力的事情启动时间会有2~3秒的额外开销。换成busybox init后用户态启动时间大幅下降。精简/etc/inittab只保留::sysinit:/etc/init.d/rcS和::respawn:/sbin/getty甚至不要getty。量产设备不需要登录shell直接去掉getty。每去掉一个脚本大概能省20~50ms。把rcS脚本合并、删除等待。常见问题是有脚本里用sleep来等待设备节点比如等待触摸屏设备我们改为在内核驱动里直接确保设备先注册或者用mdev -s一次性扫描。我等/dev/input/event0节点的做法是写了个循环结果浪费了600ms。后来直接关闭该脚本改为应用层等待设备节点实测更好。根文件系统挂载方式也有讲究。我在kernel cmdline设root/dev/mmcblk0p4并且根文件系统是只读挂载ro然后上层用mount -o remount,rw按需切换。这样能避免fsck检查也能减少ext4日志开销。但是注意如果根文件系统只读用户数据分区要单独用/data挂载这部分我单独划分了一个分区。3.4 内核启动耗时实测优化后内核从U-Boot收到bootz到挂载根文件系统的时间从1.8s降到了1.1s。细分的几个节点是节点优化前优化后内核解压200ms100mssetup_arch与内存初始化150ms120ms驱动initcall900ms400ms挂载rootfs150ms80ms启动init400ms300ms可以看到大头确实在驱动initcall这快减少了一半还多。这里我要特别推荐一个工具/proc/interrupts和时间戳。T113-i的串口会输出很多全志相关log一定要把quiet加进cmdline减少打印时间串口波特率调到1.5Mbps或更高速率也能减少输出阻塞。实测波特率只影响看得见的打印时间不影响内核逻辑但提高波特率后启动日志缩短了200ms收益不小。4. Qt/LVGL应用层优化4.1 Qt嵌入式方案选择与linuxfb平台T113-i上跑Qt最典型的两种方式使用linuxfb作为QPA平台或者使用EGLFS/LinuxDRM。因为T113-i带GPU的能力并不强我做的是纯2D界面所以选择了linuxfb把Qt当成直接写framebuffer的程序来跑绕开DRM、EGL带来的额外开销。Qt 5.15.2交叉编译时需要先编译linuxfb插件。很多人在configure阶段就会遇到类似Could not find the Qt platform plugin linuxfb的问题原因通常是没有编译libqlinuxfb.so或者运行时QT_QPA_PLATFORM没设置好。我的编译命令里加上../qt-everywhere-src-5.15.2/configure -prefix /opt/qt-5.15.2 \ -opensource -confirm-license -release -no-opengl -no-gtk \ -no-xcb -no-cups -no-iconv -qt-zlib -qt-libpng -qt-libjpeg \ -linuxfb注意T113-i内存紧张Qt库不要全部编进去我通常只编qtbase里用得到的模块比如QtCore、QtGui、QtWidgets或QtQml。如果界面是QML还需要加上qtdeclarative。我这里最终用的是Qt Widgets因为界面控件简单Widgets更轻加载和绘制比QML快。运行时设置export QT_QPA_PLATFORMlinuxfb export QT_QPA_FB_DRM0 export QT_QPA_FB_NO_ALPHA1 export QT_QPA_FB_HIDECURSOR1这几个环境变量很重要。QT_QPA_FB_HIDECURSOR1能隐藏鼠标光标LVGL和触摸屏控制界面时不需要光标。QT_QPA_FB_NO_ALPHA1能减少framebuffer的alpha叠加开销。实测QT_QPA_FB_NO_ALPHA在T113-i上能节省5ms一帧虽然不大但对流畅度有好处。4.2 Qt程序加载速度优化Qt程序本身的加载速度是用户态优化的大头。一个典型的Qt Widgets程序启动过程包括加载QtCore、QtGui、QtWidgets库初始化字体系统创建QApplication加载中文字体构建主窗口控件。这个过程在T113-i上可能需要1秒多。我做的优化包括预加载Qt库。在init脚本里用ldconfig或手动dlopen预加载libQt5Core.so、libQt5Gui.so让库在exit启动早期就映射进内存。因为内核页面缓存没有清理应用起来后直接从page cache读取省去磁盘I/O。使用-no-gtk、-no-xcb把不需要的插件全部不编。Qt会扫描platforms文件夹下的所有插件如果那里有多个so它会尝试加载反而报错。只保留libqlinuxfb.so。中文字体裁剪。默认Qt加载的字体文件很大比如wqy-microhei.ttc有5MB。我改用裁剪后的字体子集只保留常用汉字并把fontconfig关掉直接用QFontDatabase指定字体路径。实测光字体扫描就少了200ms。使用静态链接Qt这块收益大但复杂度也大而且静态Qt库授权要小心。我这次用的是动态加载加预加载效果也可以。如果产品对启动时间特别敏感可以评估Qt 5.15.2静态编译但要准备好处理GPL/LGPL合规问题。还有一点不要在main函数开头做太多初始化。有些程序启动时先建数据库连接、启动网络监听这些都会拖慢首帧。正确的做法是先显示主窗口并调用QApplication::processEvents然后再异步初始化其他模块。Qt的首帧时间以show()函数返回后第一次绘制为准。4.3 LVGL移植与渲染优化如果界面复杂度不高我更推荐LVGL而不是Qt。LVGL在T113-i这种双核A7上能做到非常流畅而且它没有重量级消息循环和库加载开销整体启动时间可以进一步缩短到接近1.8秒。LVGL移植T113-i的关键是lv_conf.h里的配置尤其要关注LV_TICK_CUSTOM用系统gettimeofday或硬件定时器提供tick不要用SysTick否则FreeRTOS或Linux下都会冲突。我这里用Linux的clock_gettime封装。LV_MEM_SIZET113-i上我设了(64U * 1024U * 1024U)其实不LVGL内存池一般在8KB~128KB就够。我设置LV_MEM_SIZE 512 * 1024对常见控件足够。LV_COLOR_DEPTH设为16和framebuffer一致避免颜色转换。开启LV_USE_PERF_MONITOR调试完要关闭。LV_USE_GPU和LV_USE_GPU_RASTER在无GPU平台必须关掉。LVGL显示驱动的flush函数是性能瓶颈。我的优化是使用双缓冲加partial refresh但T113-i上的fb如果使用mmap只绘制脏矩形区域可以显著降低数据搬运量。还有一个坑全志的linuxfb在开启FBIOPAN_DISPLAY时如果不用dirty rectangle而全屏flush显示会很卡。后来我改成只flusharea流畅度提升明显。LVGL和FreeRTOS常被一起提但在这个项目里LVGL运行在Linux进程下不用FreeRTOS。要注意LVGL的lv_task_handler必须在主循环里周期调用否则界面不刷新。当时有个同事把lv_task_handler放到另一个线程里结果所有输入和动画都乱套因为LVGL默认不是线程安全的。官方没有强制线程模型最好的做法就是让lv_task_handler和lv_tick保持在同一个线程。4.4 应用层启动流程并发化应用启动阶段有很多独立任务加载配置文件、打开触摸屏设备、初始化网络、加载字体、解析图片资源。如果串行执行每一步都要等。我在Qt版本里用了个简单办法主窗口先显示启动页一个纯色或logo然后把耗时的资源加载放到QThreadPool后台任务中完成后通过信号槽更新状态。具体做法是int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); // 先显示 QTimer::singleShot(0, w, SLOT(initAsync())); // 异步加载 return app.exec(); }initAsync里不要做UI阻塞操作比如数据库连接可以放到QtConcurrent::run。实测这种方式让用户感知到首帧时间提前了200ms左右。如果是LVGL则直接在lv_init()后先画一个简单主界面再在后台加载复杂页面资源。LVGL本身是事件驱动后台线程完成资源加载后用原子变量通知前端刷新或者通过event queue丢给主循环。不要跨线程调用lv_*对象操作。5. 联调实操与问题排查5.1 启动阶段时间分布分析整个优化做完以后我又接上逻辑分析仪用GPIO拉高拉低来记录每个阶段的实际时间。软件时间戳容易受到串口打印、调度器延迟影响GPIO才是最硬的证据。我在SPL入口、U-Boot启动、kernel解压、init进程、Qt main函数、Qt首帧这六个位置分别翻转一个GPIO测量结果如下阶段耗时msSPL到U-Boot280U-Boot到kernel600kernel到rootfs950rootfs到Qt main220Qt main到首帧250总计约2.3s可见最终瓶颈已经转移到必须串行的链路DDR初始化、内核驱动初始化、Qt首帧。此时再想压缩到2秒以内就得换启动方式如从SPL直接加载一个小型RTOS和图形栈或者用提前初始化驱动的方案。T113-i的SoC设计上不支持类似Rockchip的“Android快速启动”那样从休眠状态恢复所以我判断2.0秒是这台设备的极限。5.2 常见问题速查表现象原因解决方案U-Boot启动后不停重启环境变量分区偏移配错核对EMMC分区表在u-boot命令行用mmc part确认内核启动卡住不打印或打印很慢串口波特率不匹配或cmdline的console参数不对统一使用consolettyS0,115200作为最终参数Could not find the Qt platform plugin linuxfb缺少libqlinuxfb.so或路径不对检查plugins/platforms目录在main前设置QCoreApplication::setLibraryPathsQt界面显示花屏或颜色不对framebuffer的bpp或颜色深度不匹配lv_conf.h的LV_COLOR_DEPTH与fb中bits_per_pixel保持一致LVGL内存分配失败lv_mem_alloc返回NULLLV_MEM_SIZE过小或有内存泄漏开LV_USE_MEM_MONITOR观察循环中lv_mem_monitor打印峰值触摸没反应但鼠标光标能用input设备节点没打开或TSLIB配置错误检查/dev/input/eventX直接cat验证是否有数据init脚本里有sleep等待设备节点系统启动时会卡住删除sleep用进程内等待机制代替根文件系统挂在/dev/mmcblk0p4不稳定内核CONFIG_MMC_SUNXI未编入或根设备太早确保mmc驱动编进内核不是模块Qt程序编译时版本不匹配如cannot mix incompatible Qt library用了多个Qt版本混编清理Makefile重新qmake显式指定-qt路径5.3 几条实战心得第一别迷信某一个“大招”。网上很多文章说把CONFIG_SYS_BOOTM_LEN调大就能提速实际上对T113-i没什么用。真正有效的是用数据驱动每一步都量化、记录然后优先解决耗时大头。第二内核裁剪的性价比最高。我花了两个晚上把内核从3MB裁到2MB启动时间减少近300ms而Qt优化花了很多天才省了200ms不到。如果时间有限优先做内核配置裁剪和device tree精简。第三全志的BSP里有很多默认开启的调试项。像SUNXI_DEBUG、SUNXI_DE2的log输出在量产版本中一定要关掉还有initcall_debug也不要留在cmdline里。我当时因为忘记关initcall_debug启动日志变长白白增加了0.3秒。第四调试时要保留一个“优化前对比镜像”。迭代过程中很容易改乱保留一个基线镜像可以快速对比验证某种改法是否真的有效。我在eMMC里放了三份镜像分别是原始版本、优化中版本和稳定版本通过U-Boot环境变量切换非常方便。第五LVGL的内存策略不要让每个控件都动态分配一个大buffer。启用LV_MEM_CUSTOM后用malloc/free配合lv_mem监控实时看内存使用。对于需要频繁创建的列表项最好用对象池复用否则长时间运行之后内存碎片会很严重最终表现为系统越用越卡。最后再说一个小技巧在Qt入口点前用mlockall(MCL_CURRENT | MCL_FUTURE)锁定内存可以避免应用启动时因为页面换入换出产生延迟。这个操作在内存充裕的设备上很稳T113-i上虽然内存紧张但锁定关键Qt库页面后qt main到首帧的时间能减少约50ms。代价是系统可分配内存会减少所以只锁一段关键区域就好不要全锁。这次项目最终交付时客户反馈“按下开机到看到home界面明显快了很多几乎不用等”。我自己的体会是嵌入式启动优化没有银弹无非是“测量、裁剪、再测量、再裁剪”的循环以及时不时回到整体视角去问自己这一步节省的时间是否值得后续维护成本只要这个问题能回答清楚优化工作就不会跑偏。
阅读完成 · 觉得有帮助?
咨询建站