全志T113应用程序开发全流程记录

起因

T113 是全志(Allwinner)的三核 SoC(1× Cortex-A7 @1.2GHz + 2× HiFi4 DSP),跑 Linux 做带屏的中小型产品性价比很高。但从前零开始把一个应用程序做到"上电即运行"的量产状态,中间要穿过环境搭建、内核裁剪、设备树、打包烧写、自启动、开机动画、显示、GPIO、输入等一系列环节,每个环节的资料都散落在不同的文档里。这篇把整条链路串成一篇全流程记录,既是备忘,也给同样用 Tina SDK 做 T113 开发的人一个整体地图。

需求

整条流水线就是一个嵌入式 Linux 产品的完整生命周期,覆盖以下环节:

  1. 虚拟机开发环境建设
  2. SDK 编译体系与编译选项配置修改
  3. 设备树(dts)配置修改
  4. 编译与打包
  5. 烧录固件(OpenixSuit)
  6. 固化应用(打进固件,而不是每次 adb push)
  7. 应用自启动
  8. 修改启动图片与启动声音
  9. 修改显示驱动相关配置
  10. GPIO 控制
  11. 输入设备(USB / SD / KEY)

虚拟机开发环境建设

为什么用虚拟机

全志的交叉编译工具链、打包脚本全部是 Linux 下的,Windows 原生没法跑。选虚拟机而不是双系统/WSL 的原因:快照可以随便回滚(折腾 SDK 很容易把环境搞脏)、和 Windows 宿主机共享剪贴板/文件方便、随时迁移。

环境要求

  • Ubuntu 18.04(SDK 推荐版本,工具链是在这个版本的 glibc 环境下验证的;用太新的发行版容易碰到工具链和 glibc 的兼容问题)
  • 磁盘 至少 80GB(SDK 源码解压后 20GB+,编译过程中 out 目录还会继续膨胀)
  • 内存 8GB 起(make -j 并行编译很吃内存)
  • CPU 核数给足,首次全量编译 1~2 小时很正常

装好系统后先补齐编译依赖:

1
2
3
4
sudo apt update
sudo apt install build-essential git bc bison flex libncurses5-dev \
    libssl-dev zlib1g-dev lib32z1 lib32stdc++6 gcc-multilib \
    gawk gettext ccache

其中 lib32z1/lib32stdc++6 这两个 32 位库容易漏——SDK 里部分预编译的打包工具是 32 位的,缺了会在 pack 阶段报 No such file or directory,而且报错信息完全不提示是缺库。

共享目录

Windows 侧的工程放在共享目录里(VMware Tools 挂载,/mnt/share),比如本项目:

1
/mnt/share/linux_t113_project/app_launcher

注意:共享目录(hgfs)只适合放源码和交换文件,不要直接在共享目录里编译。hgfs 不支持符号链接、没有真实的权限位、大小写行为也和 ext4 不一致,链接阶段各种莫名其妙的错误都是它引起的。正确做法是拷回虚拟机本地 ext4 分区再编。

编译体系与编译选项

Tina SDK 的结构

Tina 是全志基于 OpenWrt 深度改造的构建系统,目录结构:

1
2
3
4
5
6
7
8
tina-d1-h/
├── build/          # envsetup.sh、编译控制脚本
├── device/         # 芯片与板级配置(dts、分区表、打包配置都在这)
│   └── config/chips/t113/configs/<板名>/
├── openwrt/        # OpenWrt 构建树(包管理、rootfs 生成)
├── kernel/         # Linux 内核源码
├── scripts/        # 辅助脚本
└── out/            # 编译输出(img 也在里面)

理解一条主线就够了:lunch 选板 → make 编译(bootloader/内核/OpenWrt 包/rootfs)→ make pack 打包成 img

加载开发环境

1
2
source build/envsetup.sh
lunch

输出,我这里选择5,我自己创建好的板子配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ubuntu@ubuntu1804:~/tina-d1-h$ lunch

You're building on Linux

Lunch menu... pick a combo:
     1. d1-h_nezha_min-tina
     2. d1-h_nezha-tina
     3. d1s_nezha-tina
     4. t113_100ask-tina
     5. t113_musicpanel-tina

Which would you like? [Default t113_musicpanel]: 5

lunch 本质是设置一批环境变量(TARGET_PRODUCTTARGET_PLATFORMTARGET_BUILD_VARIANT 等),后续 make/pack 全靠它们定位"给哪块板编"。所以每开一个新终端都要重新 source + lunch,否则 make 找不到目标。

编译选项配置修改

两套 menuconfig,管的层面不同,不要混淆:

make menuconfig —— 目标系统(rootfs)配置

决定最终文件系统里有什么:busybox 特性、alsa 音频库、libgpiod、你的应用程序包等。选中的包会被编进固件的 rootfs。

1
make menuconfig

改完保存会写入 out/t113_musicpanel/.config,下次 make 时生效。

make kernel_menuconfig —— 内核配置

决定内核驱动和特性:USB host、各种文件系统、INPUT 设备、GPIO 等。对应内核 .config

1
make kernel_menuconfig

典型要开/确认的项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Device Drivers --->
    USB support --->        # U盘等 USB 输入
        <*> EHCI HCD (USB 2.0) support
        <*> OHCI HCD (USB 1.1) support
        <*> USB Mass Storage support
    Input device support --->
        <*> Event interface   # /dev/input/event*,按键必用
    Character devices --->
        -*- GPIO Support
File systems --->
    DOS/FAT/EXFAT and NT Design decisions --->   # SD卡/U盘 FAT 支持
        <*> VFAT fs support

menuconfig 陷阱:直接改 kernel/.config 没用,Tina 会用保存的 defconfig 覆盖;要持久化必须通过 kernel_menuconfig 保存,它会写回板级 defconfig。

设备树(dts)配置修改

dts 在哪、怎么生效

T113 Tina 的板级设备树在:

1
device/config/chips/t113/configs/musicpanel/board.dts

musicpanel 对应 lunch 选的板级配置名。自己建板时,惯例是拷一份最接近的板子目录改名,再逐项改。)

编译时这份 dts 被编成 dtb,和内核一起打进 boot 包,不需要单独烧 dtb 分区。改完 dts 后要重新编译打包烧写才生效。

运行时想确认板子实际用的是哪份设备树,看:

1
2
ls /proc/device-tree/          # 就是当前生效的 dtb 的镜像
cat /proc/device-tree/model

常改的节点

外设的开关和引脚分配都在 dts 里,改外设 = 改对应节点 + 重新打包。几个高频场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 串口:使能并指定引脚 */
&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart1_pins_a>;
    status = "okay";
};

/* I2C:挂外设 */
&twi1 {
    status = "okay";
    touchscreen@14 {
        compatible = "...";
        reg = <0x14>;
    };
};

/* PWM 背光 */
&pwm0 {
    pinctrl-names = "default";
    pinctrl-0 = <&pwm0_pin>;
    status = "okay";
};

/* USB host:供电使能引脚 */
&usbc1 {
    usb_drv_vbus_gpio = <&pio PE 13 GPIO_ACTIVE_HIGH>;
    status = "okay";
};

/* SD 卡:插拔检测脚 */
&sdc2 {
    cd-gpios = <&pio PF 6 GPIO_ACTIVE_LOW>;
    status = "okay";
};

引脚配置的因果链:pinctrl 把引脚切换到复用功能(mux)并设置上下拉/驱动能力,外设驱动 probe 时向 pinctrl 子系统申请这些引脚。所以"引脚不工作"先查两件事——status 是不是 okay、pinctrl 引脚和原理图是否对得上。

编译与打包

1
make -j$(nproc)     # 全量编译:bootloader、内核、模块、rootfs

首次全量编译较久;增量编译只重编改动部分。内核单独改动时 make 会自动识别。编不过先别急着重来,串口板子上看不到的编译错误都在终端输出里。

编译产物在 out/t113_musicpanel/ 下,其中:

1
2
3
4
out/t113_musicpanel/
├── boot.img        # 内核+dtb+ramdisk 的启动包
├── rootfs.img      # 根文件系统
└── images/         # make pack 的最终产物

打包:

1
make pack

packdevice/config/chips/t113/configs/musicpanel/ 下的分区表(sys_partition.fex)把各镜像组装成整包刷机镜像,输出在 out/t113_musicpanel/images/ 下的 tina_*_cube*.img

分区表决定了 bootlogo、rootfs 等各分区大小,rootfs 快装满时要么裁剪要么改分区表:

1
2
3
4
5
6
7
8
/* sys_partition.fex 片段示意 */
[partition]
    name         = bootlogo
    size         = 1024
[partition]
    name         = rootfs
    downloadfile = "rootfs.fex"
    user_type    = 0x8000

烧录固件

日常主流程就是四步循环:选择板子(lunch)→ 编译(make)→ 打包(pack)→ 烧录测试。烧录这环目前用的是第三方的 OpenixSuit

工具选型

工具 性质 说明
PhoenixSuit 官方,Windows 闭源 全志配套的图形烧写工具
LiveSuit 官方 跨平台老工具,体验一般
OpenixSuit 第三方开源(Rust) 跨平台(Linux/Windows),一个工具覆盖 PhoenixSuit(FEL 烧写)、PhoenixCard(SD 启动卡制作)、awimgunpack(img 解包)三件套的事,脚本化批量烧写友好

选 OpenixSuit 的原因:开源、可在 Linux 虚拟机里直接跑(不用来回切 Windows)、能命令行驱动,产线/老化脚本可以一把梭。

FEL 模式

全志 SoC 的 BootROM 里固化了一段 USB 下载协议(FEL),这是不依赖 eMMC 内容的最后防线——板子变砖也能救回来。进入方式:

  • 按住 FEL/升级键再上电(原理图上找 FEL_KEY/UBOOT 引脚)
  • 系统还活着时,uboot 命令行里输入 efex 重启直接进 FEL
  • OTG 口连 PC,lsusb 能看到全志的 FEL 设备即说明就位

烧录

1
2
3
# OpenixSuit 装好后(具体子命令以 openixsuit --help 为准,版本间有变动)
# 板子进 FEL → 烧写整包
openixsuit flash out/t113_musicpanel/images/tina_t113_musicpanel_linux_cube_uart0.img

烧写时串口(115200)能看到下载进度和重启后的完整启动 log。烧完必看串口——这一步的 log 是"启动不了/改了没生效"类问题的第一现场,比在系统里瞎猜快十倍。

开发节奏:把烧录循环压缩成 push 循环

整包烧录一轮几分钟,一天迭代几十次会浪费大量时间。按改动层级选更新方式,是整个生命周期里最重要的节奏:

改动类型 更新方式 耗时
应用代码 adb push app_launcher /tmp/ && /tmp/app_launcher 秒级
配置/资源文件(开机图、音、脚本) adb push 到对应分区挂载点/路径 秒级
内核、dts、rootfs 布局、分区表 只能重新 make + pack + 烧录 分钟级

应用级改动先 push 验证,跑通了再固化进固件重打包——这也是下一节"固化应用"存在的意义。

SD 启动卡

OpenixSuit 同样能把整包写进 SD 卡做成启动卡(板子拨到 SD 启动/或不焊 eMMC)。适合不动板子原有系统做验证、批量老化测试、给客户的演示卡。

固化应用

adb push 上的程序断电就没(写到 tmpfs 或者下次烧写被覆盖),量产要固化:把应用打进 rootfs。两条路:

正规做法:做成 Tina 包

openwrt/package/ 下建自己的包:

1
2
3
4
5
openwrt/package/app_launcher/
├── Makefile      # OpenWrt 包描述:SECTION/CATEGORY/DEPENDS/安装规则
└── src/
    ├── Makefile  # 应用自身的编译规则(交叉编译)
    └── main.c

包 Makefile 里 INSTALL_FILES/INSTALL_BIN 把二进制装到 rootfs 的 /usr/bin/,然后 make menuconfig 里选中它,重新 make + pack。好处是有依赖管理、版本可控。

快速做法:rootfs overlay

Tina 允许板级目录直接覆盖 rootfs(在板子配置目录下的 base-files 类 overlay 目录,具体目录名以手头 SDK 的 device/config/chips/t113/configs/musicpanel/ 实际结构为准),把二进制丢进去对应路径即可。适合调试期。

应用源码本身放在共享目录 /mnt/share/linux_t113_project/app_launcher,在虚拟机内用 SDK 的交叉工具链编译:

1
2
# 工具链在 out/t113_musicpanel/compile_dir/host 或 prebuilt 目录下
arm-openwrt-linux-gnueabi-gcc -o app_launcher main.c -lm

应用自启动

Busybox init 从 /etc/init.d/ 里按文件名排序跑 S??* 脚本。加一个自启动脚本(overlay 或包里都行):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/sh /etc/rc.common
START=99
STOP=10

start() {
    export QT_QPA_PLATFORM=linuxfb:fb=/dev/fb0
    cd /usr/share/app_launcher
    app_launcher &
}
stop() {
    killall app_launcher
}

要点:

  • START=99 保证轮到自己时声卡、显示、输入设备都已就绪;太早启动会碰到 /dev/fb0 还没出来
  • 帧缓冲应用要设好 QT_QPA_PLATFORM=linuxfb 之类的平台变量(lvgl 则是各自的显示接口配置)
  • 要"独占整机性能"时,还可以把启动级别设高于其他不必要服务,或者干脆裁掉用不上的服务

修改启动声音和启动图片

启动画面有三个层次,改哪层取决于你想要什么效果:

1. U-Boot 阶段的 bootlogo(静态图)

最早的画面,存在 bootlogo 分区,格式 BMP(分辨率要和屏幕一致,否则花屏/黑屏)。换图:把新 BMP 放进板级打包配置目录替换原 bootlogo.bmp,重新 make pack(不动其他镜像)烧写。它不占系统资源,是"产品上电第一眼"的标准做法。

2. 内核 logo

内核把自己的 logo 打在 fbcon 上,改内核源码 drivers/video/logo/ 下的 logo 文件并配 CONFIG_LOGO。一般产品会把它关掉(menuconfig 里 Device Drivers → Graphics support → Bootup logo),避免和 bootlogo/application 之间闪一下企鹅。

3. 进系统后的开机动画/开机音

应用起来前后的过渡动画与声音。做法一是 SDK 的 bootanimation 机制(bootresource 分区放动画 zip 和音频,由启动阶段的播放程序处理,是否支持以手头 SDK 版本为准);做法二最稳——自启动脚本里在拉起主程序的同时播放开机音:

1
2
amixer cset numid=1 60        # 音量(numid 以 amixer controls 输出为准)
aplay /usr/share/sounds/boot.wav &

alsa 的 aplay 播 WAV 最省事;MP3 需要 madplay/gplay 之类解码器(menuconfig 里选)。

修改显示驱动相关配置

T113 的显示链路:DE(Display Engine) → panel/LVDS-RGB 输出 → 屏幕,驱动框架在内核里,产品级改动基本不用动 C 代码,改 dts 就够:

  • 屏幕时序:panel 节点的 hfp/hbp/hsync/vfp/vbp/vsyncxres/yres、像素时钟,数据手册给多少填多少,花屏/偏移/闪屏九成是时序错
  • 接口类型:RGB 并口 / LVDS / MIPI 在 panel 节点 compatible 和数据格式字段里切换
  • 背光:PWM 通道 + 占空比,brightness 节点;不亮先万用表量 PWM 脚有没有波形
  • 旋转:dts 里 rotation = <90> 或应用侧各自处理

验证与调试:

1
2
3
4
cat /sys/class/graphics/fb0/mode       # 当前显示模式
cat /sys/class/disp/disp/attr          # disp 驱动状态(Tina 的 sunxi disp 框架)
fbset                                  # framebuffer 参数
cat /dev/urandom > /dev/fb0            # 花屏测试:屏幕满屏雪花说明通路是好的

最后一条是经典排查法:能刷出雪花,说明 DE→panel 通路没问题,问题在应用层;刷不出雪花,回头查 dts 时序和电源。

GPIO

找到 GPIO 编号

T113 的 GPIO 按 bank 分组(PA、PB、PC……),每个 bank 32 个脚。用户空间访问有新旧两套接口:

libgpiod(新接口,推荐)

1
2
3
4
gpiodetect                # 列出所有 gpiochip
gpioinfo gpiochip0        # 列出每根线的状态和占用
gpioset gpiochip0 35=1    # PE3 拉高(32+3=35,bank 内偏移)
gpioget gpiochip0 35      # 读输入

bank 编号换算:线号 = bank序号×32 + 组内脚号,PA 是第 0 组。menuconfig/包里选 libgpiodmake menuconfig → Libraries),C 代码里链 libgpiodgpiod_chip_open_by_name() + gpiod_line_get_value()

sysfs(老接口,内核逐步弃用,但脚本调试快)

1
2
3
echo 35 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio35/direction
echo 1 > /sys/class/gpio/gpio35/value

注意事项

  • 被 dts 里某个外设 pinctrl 占用的脚不能再用 GPIO 方式操作,内核会拒绝申请——两块 subsystem 抢一根物理脚是 GPIO 最常见的"不工作"
  • 输出高不一定等于供电:确认引脚驱动能力(drive strength)够不够带负载,继电器/LED 大电流一律上三极管或驱动芯片

输入:USB / SD / KEY

USB

USB host(插 U 盘、键盘)在 dts 的 usbc 节点里开,供电使能脚 usb_drv_vbus_gpio 别漏。U盘热插拔:

1
2
cat /proc/partitions          # 插上后多出 sda1
mount -t vfat /dev/sda1 /mnt/udisk

自动挂载走 mdev hotplug 脚本(busybox 环境)监听 add/remove 事件。做"插 U 盘升级固件"的产品逻辑,就是在挂载点轮询/监听升级文件。

USB 另一个方向是 device 模式(OTG):T113 出厂固件普遍用 adb 调试(adb push app_launcher /tmp/adb shell),比反复烧写快得多,调试期主力。

SD 卡

SD 卡走 sdc 节点(cd-gpios 配插拔检测),设备是 /dev/mmcblk?p1。两个用途:

  • 数据卡:vfat 挂载存资源文件,和应用一起构成产品
  • 启动卡:PhoenixCard 把整包镜像写进 SD 卡做成启动盘,给"没有烧写器/批量老化测试"的场景用

KEY(按键)

GPIO 直连按键:dts 里配 gpio-keys 节点,内核自动生成 input 设备:

1
2
3
4
5
6
7
8
gpio-keys {
    compatible = "gpio-keys";
    key0 {
        label = "key_back";
        linux,code = <158>;              /* KEY_BACK */
        gpios = <&pio PB 4 GPIO_ACTIVE_LOW>;
    };
};

LRADC 按键(全志特色):一根 AD 引脚挂一串不同阻值的分压电阻,每个按键一个电压区间,省 IO。dts 里配 lradc 节点的键值表。

用户空间统一从 input 子系统读:

1
2
cat /proc/bus/input/devices    # 找到按键对应的 event 节点
evtest /dev/input/event0       # 按一下,看事件流

C 代码里 read() /dev/input/event* 得到 struct input_event,按 type=EV_KEY, value=1/0 分按下/抬起处理。注意:应用要收到按键,make kernel_menuconfigEvent interface 必须开着(前面内核配置一节已列)。

踩坑记录与注意事项

  • 共享目录里编译必出玄学错误:hgfs 没有符号链接和真实权限,链接/安装阶段报错。源码拷到 ext4 本地再编
  • 新终端忘记 source + lunch:make 报找不到目标、pack 打出空包,都是环境变量丢了
  • .config 不生效:Tina 用 defconfig 覆盖,配置改动必须走 menuconfig/kernel_menuconfig 保存
  • 改了 dts 没效果:dts 要重编进 boot 包,只 makemake pack、或者烧写时没选新 img,都等于没改
  • pack 报 32 位程序无法执行:缺 lib32z1/lib32stdc++6
  • bootlogo 黑屏/花屏:BMP 分辨率与屏幕不符、或不是工具要求的 BMP 子格式;先按屏幕原生分辨率出图
  • 自启动的应用崩:START 序号太小,/dev/fb0、声卡、input 还没就绪;调到 99 或脚本里加就绪等待
  • GPIO 申请被拒:脚被 dts 里其他外设的 pinctrl 占了,查 gpioinfo 的 consumer 字段
  • 按键无反应Event interface 没开;或按键在 /proc/bus/input/devices 里根本没枚举出来(dts 没配对)
  • 一切烧写/启动问题先看串口:115200 的启动 log 能直接看到卡在 bootloader、内核还是 rootfs 挂载,比瞎猜快十倍