T113-S3这块芯片我断断续续折腾了小半年,从最开始的uboot串口乱码,到内核启动卡死在driver probe,再到Buildroot编译到一半报错退出,几乎是每个环节都踩过一遍坑。做完整个项目回头再看,发现网上关于这颗芯片的资料虽然不少,但大多停留在“怎么编译”的层面,真正把uboot、内核、根文件系统串起来讲全栈构建、并且把排错思路讲清楚的,真不多。
这篇东西就是冲着补这个缺口来的。如果你手上正好有T113-S3的核心板,或者准备在类似的全志/晶晨方案上做Linux产品,想搞明白从芯片上电到应用跑起来这中间每一层到底发生了什么、出了问题怎么定位,那这篇文章应该能帮你省下不少时间。我会按照实际开发的顺序,从uboot讲起,再到内核和设备树,最后落到Buildroot构建根文件系统,每一段都配上我在实测中遇到的坑和排查思路。
1. 项目整体设计与方案选型
1.1 全栈构建到底是在构建什么
先把概念理清楚。嵌入式Linux的全栈开发,不是说你会写几个应用就完了,而是从芯片复位后第一条指令开始,把每一层软件都搭起来。对于T113-S3这种SoC,完整启动链路是这样的:
- Boot ROM(芯片内部固化,上电后自动执行,负责从SD卡/eMMC/SPI Nor等介质加载SPL)
- SPL(Secondary Program Loader,属于uboot的一部分,负责最基础的时钟、DDR初始化,然后加载ATF和uboot主体)
- ATF(Arm Trusted Firmware,提供EL3运行时环境)
- U-Boot proper(完整引导程序,负责加载内核镜像和设备树)
- Linux内核(内核解压初始化、驱动挂载、挂载根文件系统)
- 根文件系统(BusyBox或者完整发行版,提供init进程和应用程序运行环境)
T113-S3这颗芯片比较特殊的地方在于,它集成了双核Cortex-A7和一个RISC-V协处理器,同时芯片内部直接封装了DDR3内存。这意味着不做外部DDR颗粒选型和布线,硬件设计上确实省事,但软件上SPL阶段对DDR控制器的初始化参数必须和内部颗粒完全匹配,否则uboot根本起不来。这也是为什么很多人拿到核心板先卡在SPL阶段的原因。
1.2 为什么选Buildroot而不是Yocto或纯手动交叉编译
搭建根文件系统有三条路线:纯手动下载BusyBox源码交叉编译、用Buildroot、用Yocto。三者的取舍非常现实,直接决定项目节奏。
纯手动方式说穿了就是自己下载BusyBox、glibc、各种库源码,一个个交叉编译再手动拷贝到rootfs目录。好处是每一层都透明、可控,坏处是依赖关系全得自己背,第一次搭建顺利的话也要两三天,中间遇到哪个库的configure脚本不认交叉编译环境,排查起来非常头痛。产品原型验证阶段,这条路太慢。
Yocto是另一个极端,功能全、可定制性强、社区包多,但学习曲线陡峭,光是把bitbake的语法和layer机制搞明白就需要一周时间,而且首次构建要下载几个G的源码包,构建时间也长。如果团队里只有一两个人做BSP,Yocto很容易变成“构建系统占用的时间比写业务代码还多”。
Buildroot恰好卡在中间。它本质是一套基于Kconfig和Makefile的自动化构建框架,只需要配置好交叉编译工具链和目标架构,然后勾选需要的软件包,剩下的事情全部自动化:下载源码、打补丁、编译、安装到rootfs目录、最终打包成镜像。整个系统构建时间大概在20到40分钟(取决于软件包数量),拿来量产和做产品原型都够用。我这次T113-S3项目就用的Buildroot,具体版本是2023.02 LTS,内核是5.4,工具链用的Buildroot内置的gcc 9.2版本,跑起来稳定,也没发现什么兼容性问题。
1.3 T113-S3的资源盘点与系统分区规划
在动手编译之前,先得把T113-S3的资源摸清。它集成了128MB DDR3,CPU频率最高可以到1.2GHz,支持RGB/LVDS/MIPI DSI显示接口、百兆以太网(内部集成MAC,只有MII/RMII接口引出)、4路SDIO、6路UART、2路SPI、双CAN等。做低成本Linux产品(比如工业HMI、智能网关、简单平板)它确实够用。
系统分区规划我直接给出实测可行的方案(以8GB eMMC为例):
| 分区 | 起始偏移 | 大小 | 内容 |
|---|---|---|---|
| boot0 | 8KB | 256KB | SPL,由SDK的烧录脚本写入 |
| uboot | 264KB | 1MB | u-boot.bin(含ATF) |
| boot | 1.3MB | 32MB | ext4,存放kernel Image、dtb、boot.scr |
| rootfs | 33MB | 剩余 | ext4,Buildroot生成的rootfs |
这个分区的思路是boot和rootfs分离,方便后续OTA升级——只更新boot分区或者只更新rootfs分区互不影响。uboot的环境变量里设置好bootcmd从boot分区读内核,再挂载rootfs分区作为根文件系统。
2. U-Boot构建与底层细节
2.1 交叉编译工具链的选择
U-Boot构建第一步是准备好交叉编译工具链。T113-S3的SDK默认用的是arm-none-eabi或者arm-linux-gnueabihf这类工具链。我自己使用的是Buildroot编译产出的工具链——也就是host目录下的arm-buildroot-linux-gnueabihf- 前缀的工具链。好处是工具链和后续内核、根文件系统完全同源,避免出现glibc版本不一致导致的“应用程序加载不了”这种玄学问题。
如果单独编译uboot,可以不用Buildroot,直接下载Linaro GCC或者ARM官方工具链。但要注意:核内浮点abi必须选对。T113-S3的Cortex-A7支持硬件浮点,编译时务必加上-mfloat-abi=hard -mfpu=neon-vfpv4,否则内核引导阶段fpsimd相关功能可能出问题。
2.2 T113-S3的U-Boot配置与SPL/ATF协同
U-Boot对全志芯片有一套非常成熟的框架,T113-S3在主线U-Boot中对应sun8i平台。但用主线uboot直接编译T113-S3会比较麻烦,因为T113的DDR初始化、PMIC配置等部分厂商还是以闭源方式提供。更稳妥的做法是用全志SDK里带的uboot,在其基础上改。下面是我基于T113-S3 SDK的u-boot编译流程。
make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- t113_i_defconfig make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- -j8这里defconfig我直接用了厂商的t113_i_defconfig,没手动去调内存参数。编译产物中u-boot.bin是裸的uboot,不能直接烧录,需要用全志的mksunxi工具打上头部。SDK一般会提供打包脚本,直接运行就能生成sunxi-spl.bin和u-boot.itb(含ATF)。
SPL在这里扮演的角色很关键:它负责最基础的DDR初始化,然后把ATF和uboot proper从启动介质中读入内存。常见错误是SPL阶段串口无任何输出——这种情况八成是DDR初始化没通过,或者是波特率、晶振频率的配置和实际硬件不匹配。T113-S3外部晶振默认是24MHz,如果你用的核心板换成了16MHz或12MHz晶振,SPL阶段串口就会完全没输出,因为PLL频率全乱了。排查这类问题首选示波器测晶振引脚,确认起振频率,再去uboot源码里改CONFIG_SYS_CLK_FREQ。
2.3 启动logo与显示初始化
很多产品(尤其HMI类)要求上电后立刻显示logo,不等内核起来。这在uboot阶段就要驱动显示控制器。T113-S3自带DE2显示引擎,支持RGB和LVDS输出。全志SDK的uboot里对显示部分的初始化路径大致是:board_init -> sunxi_display_init -> 解析disp环境变量里的LCD参数(时序、分辨率、色深),配置显示控制器和PLL_VIDEO,最后把framebuffer内容刷到屏幕。
我之前在uboot阶段加开机动画(其实是静态logo),发现一个非常隐蔽的坑:如果内核的设备树里lcd的时序和uboot环境变量里的disp_mode参数不一致,uboot下logo显示正常,但内核起来后会花屏几秒钟,然后才恢复正常。这是因为内核的显示驱动会重新初始化面板,如果时序参数不对,面板会进入错误的时钟训练状态。排查了很久才意识到是设备树和uboot的display模式没对齐。经验就是:uboot里设置的分辨率和刷新率,要原封不动地同步到内核设备树里。
2.4 分区表与boot.scr
U-Boot的环境变量、分区表、引导脚本是三大件。分区表我用的是mtdparts(对于eMMC则用mmc parts),在uboot命令行可以用mmc part list mmc 1查询分区列表。要确保uboot里的分区起始偏移和烧录脚本一致,否则烧进去内核和rootfs都对不上。
boot.scr是uboot引导内核的脚本,里面写的是load内核镜像到内存、加载dtb到内存、然后bootz执行的指令。我用的boot.scr内容大致如下:
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk1p4 rootwait rw' load mmc 1:3 0x40200000 boot/Image load mmc 1:3 0x40800000 boot/sun8i-t113.dtb bootz 0x40200000 - 0x40800000注意load mmc 1:3表示mmc设备1(eMMC)的第3个分区(boot分区),0x40200000和0x40800000这两个内存地址是经验值,只要不和其他阶段的内存布局冲突就行(考虑到ATF占据了高位内存,这里选择在128MB内存中间偏前的区域是安全的)。
3. Linux内核配置与设备树调试
3.1 内核裁剪与关键配置项
T113-S3的内核我用的5.4版本。内核配置基于全志sun8i平台的defconfig,打上T113相关补丁后,主要关注这几个配置组:
- 内核必须选中Device Tree support、ARM EABI、内核大小压缩选项
- 显示驱动相关:CONFIG_DRM_SUN4I、CONFIG_DRM_SUN8I_UI/VI、CONFIG_DRM_PANEL_SIMPLE
- 以太网:CONFIG_SUN8I_EMAC(内部百兆MAC的驱动)
- CAN总线:CONFIG_CAN_SUN4I(T113的CAN控制器驱动)
- 电源管理:CONFIG_CPUFREQ_DT,用于DVFS调频
一个容易踩的坑是CONFIG_CMDLINE_FORCE。全志SDK内核默认会把cmdline写成固定值,如果开了这个选项,uboot传入的bootargs会被覆盖,结果就是root=参数失效,内核起不来。我一般把CONFIG_CMDLINE_FORCE关掉,让uboot的bootargs生效。
内核编译命令:
make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- sunxi_defconfig make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- -j8生成的内核是arch/arm/boot/zImage,如果开了CONFIG_ARM_APPENDED_DTB则是Image,看具体配置。前面boot.scr里load的是Image,对应的是未压缩的vmlinux,或者开了XZ压缩的内核映像是Image.gz,加载时需先解压。我习惯直接编译出Image,省掉解压步骤,只是镜像文件大一点(约8MB),对eMMC来说无所谓。
3.2 设备树的编写与修改
设备树是整个BSP里改动最频繁的部分。T113-S3的dtsi在SDK里已经定义好了CPU、内存、中断控制器、GPIO控制器等基础节点,我们需要修改的是板级dts文件,主要关注:
- chosen节点:设置stdout-path为serial0
- memory节点:reg = <0x40000000 0x08000000>(128MB内存的起始地址和大小)
- 串口节点:确认uart0的pinctrl引脚配置
- 以太网节点:配置phy-mode、phy地址、复位GPIO和时钟源
- LCD节点:timing的clock-frequency、hactive、vactive、hfront-porch等参数
我遇到过最典型的问题:内核启动串口完全没输出,但uboot是正常的。一般来说uboot正常说明硬件没大问题,那问题就出在内核早期阶段,很可能是dts里chosen节点的stdout-path写错了,或者对应uart节点被status="disabled"。如果串口没有输出,先在uboot命令行用printenv确认bootargs里的console参数正确,再用fdtget确认dtb里的chosen节点存在。我就在这上面浪费了不少时间。
3.3 常见的内核启动失败排查
内核启动卡住的点五花八门,我整理一个排查顺序:
- 启动卡在“Starting kernel ...”:通常是内核无法解压或者dtb地址错了,确认load到内存的地址和bootz参数一致。
- 卡在“smp: Bringing up secondary CPUs”:多核启动有问题,一般是CPU release地址没对,或者是ATF版本和内核的PSCI接口不兼容。
- 卡在某个驱动probe,比如lcd或者emac:一般是dts节点里reg或者interrupt属性写错,导致驱动请求资源失败。打开内核的dyndbg可以快速定位。
另外一个非常有效的排查手段是开启内核早期的printk:在bootargs里加一个earlyprintk,这样即使后面驱动崩了,也能看到内核日志最后一条到底在哪。这比盲猜强太多。
4. Buildroot构建与常见问题排查
4.1 Buildroot配置的完整流程
Buildroot 是整套全栈构建里最省心也最坑爹的一环。省心在于它帮你处理了大部分依赖,坑爹在于一旦它下载源码失败或者某个包编译不过,你需要理解它那套包管理规则才能快速解决。
我是这样配置的:
make menuconfigTarget options里选择ARM cortex-A7、EABIhf。Toolchain类型选Buildroot internal工具链,C库选glibc。System configuration里设置主机名、root密码、启动脚本路径。Target packages里按需勾选busybox、dropbear(SSH)、i2c-tools、can-utils、strace、gdbserver这些调试利器。
配置完成后直接make,在等待编译完成的过程中,我一般顺便把rootfs的overlay准备一下——Buildroot支持BR2_ROOTFS_OVERLAY,把需要预置到根文件系统的自定义文件放到指定目录,编译时自动拷贝进去。这个功能非常好用,比如放一个rc.local脚本、预装应用二进制、配置文件等。
构建产物在output/images/目录下,最关键的是rootfs.ext4和sdcard.img(如果配置了genimage)。
4.2 编译中高频错误与修复思路
Buildroot编译期的问题五花八门,但有几个高频坑可以说说。
第一个坑:软件包源码下载失败。Buildroot从上游官网拉源码包,国内网络环境下这些下载经常超时或者TLS握手失败。我的做法是修改BR2_PRIMARY_SITE和BR2_BACKUP_SITE,指向我本地搭建的源码镜像服务器;也可以配置BR2_DL_DIR把下载目录固定到本地已有缓存的路径。在CI环境或多人协作时,这个DL_DIR共享非常方便。
第二个坑:某个包编译报错,比如openssl的汇编代码在ARM上不认识某些指令。多数情况是工具链版本和包版本不匹配。解决办法有两个方向:要么在menuconfig里升级包版本,要么给包打补丁。Buildroot的补丁机制是把补丁文件放到package/xxx/目录下名字带序号即可,它会自动应用。我一般在正式项目里锁定稳定版本,并把自己打的补丁作为v2系列放进去,方便以后升级。
第三个坑:编译到最后生成rootfs时提示“No space left on device”。这不是磁盘满了,而是Buildroot用临时文件系统做rootfs的makedev时inode耗尽,解决办法是加大tmpfs空间,直接挂载参数加size=4G。
4.3 运行时问题:文件系统报错与启动卡死
Buildroot做完镜像烧进去之后,运行时问题才叫人抓狂。我遇到过一个诡异现象:rootfs前几次启动正常,但重启几次后ext4文件系统报错,文件损坏。排查下来是ext4的delayed allocation特性在突然掉电时容易丢数据,对于工业设备这种可能随时断电的场景,建议把rootfs做成只读squashfs,数据分区单独挂载可写。这是产品化时必须考虑的问题。
还有一类问题:Buildroot启动后卡在“Waiting for root device”。这通常是内核没有挂载根文件系统成功,要么是root=参数指定的分区不对,要么是内核缺少对应的文件系统驱动(比如没编入ext4支持)。先在内核config里确认CONFIG_EXT4_FS=y,再在uboot命令行确认mmc设备号正确。T113-S3的eMMC控制器在Linux里一般是mmcblk1,SD卡是mmcblk0,别搞混了。
调试这类运行期问题,我通常用nfsroot方式启动:构建完rootfs后不解包,直接在开发机上搭一个NFS共享目录,把Buildroot的target目录用NFS导出,内核bootargs改成root=/dev/nfs nfsroot=192.168.1.100:/t113_target。这样改文件系统内容后重启即生效,不需要反复烧录,迭代效率高很多。等调试完毕再打包成ext4或squashfs烧入设备。
4.4 一个完整的异常定位实例
说一个我印象最深的排查过程。有次用户反馈设备偶发死机,串口打印停在“VFS: Mounted root (ext4 filesystem) read-only”,然后就没有任何内核日志了。这个现象很像是rootfs只读挂载导致的用户空间初始化失败。
我的排查手段是从两个方向同时进行的。一方面,在Buildroot的启动脚本里加/sbin/init前的延迟和日志输出,确认用户空间程序能不能正常启动;另一方面,在内核启动参数里加panic=-1,让系统在内核panic后重启而不是停在那里。
最终定位到问题是内核在ext4挂载后尝试执行init时,initramfs检测和rootfs的混乱导致的。具体原因是我在Buildroot里同时开了initramfs和内嵌rootfs,Buildroot生成的镜像里包含了两个rootfs,启动时内核优先使用initramfs——也就是那8MB的内存文件系统,它里面缺少init程序和动态链接器,所以起不来。解决办法是Buildroot配置里只保留一种rootfs方式,不叠加initramfs。
这个案例给我一个很大的启发:排查问题时,多想想启动链路里是否存在“多层软件叠在一起”的情况,并且不要忽视Buildroot配置项之间的互斥关系。类似的情况还有BR2_TARGET_ROOTFS_CPIO和BR2_TARGET_ROOTFS_EXT2同时打开的冲突,很多人会踩到。
5. 磁盘布局与量产烧录的经验
5.1 用genimage生成完整烧录镜像
Buildroot内置的genimage工具可以很轻松地生成sdcard.img这种完整烧录镜像。我在Buildroot的board目录下放一个genimage.cfg,把boot分区、rootfs分区、uboot镜像和SPL按规划好的偏移拼成一个裸镜像。出来的sdcard.img直接可以用dd烧录:
sudo dd if=output/images/sdcard.img of=/dev/sdb bs=1M conv=fsync量产时如果不想让每一台设备都去dd整个镜像,可以先烧录一份镜像到eMMC,然后用dd把整张eMMC导出为.img,再批量用烧录器克隆。这种方式在产线上很常见,效率也高,但要注意不同批次的eMMC容量可能略有差异,克隆前最好统一硬件批次。
5.2 SPI Nor与eMMC之间的切换
T113-S3支持从SPI Nor、SD卡、eMMC多个介质启动,通过芯片的BOOT引脚选择。我在核心板上默认从eMMC启动,但开发阶段经常要从SD卡启动刷系统,方便回退。这里踩过一个坑:SPI Nor flash如果是空的,而启动引脚又配置成优先从SPI Nor启动,那么即使eMMC里有系统,板子也起不来。所以用SD卡刷机时,务必确认启动脚位拨到了SD卡优先的位置。
开发阶段的“三启动”顺序我实测下来最省心:SD卡(用于烧录,可随时拔掉)、eMMC(正式系统)、SPI Nor(放一个最小的uboot救援系统,只做网络加载或者串口命令行)。这样无论哪个启动介质坏了,都有办法用另一个介质把系统救回来。
5.3 量产时的校验与设备唯一性
量产阶段,我习惯在Buildroot里加一个firstboot脚本:系统首次启动时读取eMMC的CID序列号,结合MAC地址生成一个设备唯一ID,写入/etc/device-id,再生成SSH host key。这样每台设备出厂状态完全一致,但网络身份彼此独立,不至于出现两台设备SSH host key相同的安全隐患。
这个过程如果在Buildroot构建时做就会导致所有设备一模一样,一定要做成“首次启动时生成”的运行时逻辑。这也是很多做网关产品的人容易忽略的生产细节。
6. 调优与后续扩展
全栈跑通只是第一步,T113-S3这种资源有限的芯片,调优才是产品化的关键。我主要做了三个方向的优化:
第一,降低内核镜像体积。用CONFIG_CC_OPTIMIZE_FOR_SIZE重新编译内核,再把不需要的驱动全部编成模块或者直接去掉,内核从8MB降到5MB左右,启动时间能压缩几百毫秒。
第二,缩短uboot阶段的启动延迟。uboot默认会等待用户按键进入命令行,这个等待时间在量产固件里要改成0,否则每次开机都会停顿。T113-S3的uboot里CONFIG_BOOTDELAY默认是2秒,我改成-1(直接跳过等待)。
第三,构建rootfs时去掉不需要的时区和locale数据,再把BusyBox按需裁剪,最终rootfs控制在6MB左右,加上内核和uboot,整个系统镜像不到20MB。这个体积对eMMC容量紧张的产品非常友好。
如果后续要做OTA升级,可以基于Buildroot的fwup或者swupdate来做双分区A/B切换。T113-S3的eMMC容量做双rootfs分区非常富余,A/B方案虽然占空间,但升级失败可以自动回滚,对远程设备来说多花这点空间完全值得。
最后说一个我个人体会最深的事:全栈构建最忌讳的就是“照着手册敲命令,敲通了但不知道为什么”。T113-S3的SDK能让你很快出一个可以启动的固件,但如果你不去理解boot0和ATF的关系、不去理解uboot的device tree overlay、不去理解Buildroot的package依赖机制,一旦出了手册之外的问题,就完全抓瞎。我写这篇文章的初衷,也是希望读者能把每一步都吃透,遇到问题时有自己的判断力和排查路径,而不是到处找别人踩过的坑。如果这篇能帮你少踩五个坑、少熬三个夜,那我这半年折腾得也算值了。