嵌入式这行的选型,往往不是从“哪块芯片跑分高”开始的,而是从一张接口清单和一张温宽表格开始的。前段时间接手一个工业现场的项目,需求摊开来看不算复杂:双千兆网口要做链路冗余,两路CAN要接现场总线,本地要一块7寸屏做参数配置,整机要能在-20℃到70℃的环境里连续跑上几个月不重启,最好还能跑个轻量容器方便远程升级。这一堆条件往桌上一摆,能选的方案其实就剩下那么几款,T536就是其中一个被反复提起的名字。它吸引我的地方不是参数表上那几个数字,而是它背后那套相对完整的ARM开源生态——主线内核有支持、U-Boot能自己改、Buildroot和Yocto都能接得上,这意味着后面遇到问题我不用干等着别人给补丁。这篇东西就是我围绕这块板子从选型、搭环境、改设备树、点外设到产品化收尾的一整套记录,里面既有踩过的坑,也有几条省时间的野路子。不管你是刚摸ARM开发板的新手,还是已经在做工业网关、边缘计算盒子、充电桩主控的老手,应该都能从里面抠出点能直接抄的东西。
1. 先把需求翻译成硬件语言:T536到底适合干什么
1.1 从接口清单倒推芯片能力边界
我一直觉得选型这事儿得反着来。先别急着看芯片手册,拿张纸把项目里所有要接的东西列出来,标上数量和速率,再回头去套芯片,这样效率高得多。拿这个项目举例,清单大概是这样的:两路千兆以太网(其中一路要做环网冗余)、两路CAN-FD(500kbps/2Mbps数据段)、一路RS485、一路MIPI-DSI接7寸1024×600屏、若干路GPIO做指示灯和按键、一路USB做本地维护口、一路SD卡槽做配置导入。把这些需求往T536身上套,会发现它的资源配比刚好卡在一个比较舒服的位置:四核Cortex-A55负责应用层和协议栈,独立的RISC-V小核可以拿去做实时性要求高的任务,两路GMAC原生支持,CAN-FD也是原生外设而不是靠SPI转出来的,显示接口给了MIPI-DSI和LVDS两条路。
这里有个很关键的判断点,很多人会忽略:外设是原生的还是桥接的。就拿CAN来说,市面上不少低成本方案是通过SPI外挂MCP2518FD这类控制器来实现的,能用,但代价是中断延迟不可控、高负载下容易丢帧、驱动还要额外维护一套。T536把CAN-FD做进了SoC内部,走的是原生控制器加收发器的路子,配合内核里的SocketCAN框架,上层应用直接按网络设备的方式来操作,省掉了一层转接的不确定性。这种差异在实验室里可能看不出来,到了现场总线负载跑到70%以上的时候就会非常明显。
再说网络。双GMAC在工业场景里几乎是刚需,一路接现场设备网,一路接管理网,物理隔离比VLAN隔离可靠得多。T536的两路GMAC都是千兆能力,实际能跑多少要看PHY选型和PCB走线,但至少从SoC层面不会成为瓶颈。我在做需求映射的时候会把每个接口对应到具体的外设控制器编号上,形成一张对照表,后面写设备树的时候直接照着填,能省掉大量来回翻手册的时间。
| 项目需求 | T536对应资源 | 实现方式 | 需要额外确认的点 |
|---|---|---|---|
| 双千兆网口 | 2×GMAC | 原生MAC + 外部PHY | PHY型号、RGMII延迟、变压器选型 |
| 两路CAN-FD | 2×CAN控制器 | 原生 + 外部收发器 | 收发器供电、终端电阻、共模电感 |
| 7寸屏 | MIPI-DSI / LVDS | 原生显示通路 | 屏参、背光升压、时序匹配 |
| 本地维护 | USB 2.0 | 原生OTG/HOST | ID脚处理、供电限流 |
| 配置导入 | SDIO / SPI | SD卡槽 | 卡检测脚、上电时序 |
| 实时任务 | RISC-V小核 | 异构核 | 核间通信机制、固件加载方式 |
这张表看起来简单,但它是我后面所有工作的索引。设备树里每一个节点、内核里每一个config选项,都能回溯到这张表上的某一行。养成这个习惯之后,你会发现调试效率有明显的提升——外设不工作时,你不用满脑子乱猜,直接按表逐行核对就行。
1.2 开源生态这四个字,含金量到底在哪
“开源生态好”这句话被说烂了,但它到底指什么,很多人其实没想清楚。我的理解是分层的:最底层是芯片能不能在Linux主线里找到支持,中间层是U-Boot和各类构建系统有没有现成的板级配置,最上层是社区里有没有人踩过你即将要踩的坑。
先看最底层。T536这类ARM64平台,能不能进主线内核,直接决定了你后面几年维护的成本。进了主线意味着什么?意味着内核升级的时候你不用把厂商补丁一个个往上搬,意味着安全更新能跟着上游走,意味着你遇到的bug别人大概率也遇到过。反过来,如果只能靠厂商提供的私有内核树,那每次升级都是体力活,三五年之后这棵树跟上游分叉得面目全非,谁接手谁头疼。所以在选型阶段,我做的第一件事通常是去翻一翻这个平台在主线的支持程度,看看设备树文件有没有、驱动覆盖到什么程度、最近的提交活跃不活跃。
中间层就是构建系统。T536这边常见的有几条路:厂商SDK自带的构建流程、Buildroot、Yocto,还有社区维护的发行版适配。这几条路没有绝对的好坏,关键在于你的团队熟悉哪一套、以及产品对可复现构建的要求有多高。小团队赶进度,用厂商SDK改起来最快;要做长期维护、要过审计、要给客户交付源码,那Yocto或者至少是Buildroot更合适,因为整个镜像的构成是可追溯的,谁在什么时候加了哪个包一目了然。
最上层其实是隐性的,但价值巨大。举个例子,你在设备树里改了一个时钟配置,结果屏幕点不亮,这时候如果社区里有人贴过类似的现象和解决思路,你可能半小时就搞定了;要是完全没人踩过,你可能得拿示波器量一整天的MIPI时钟。生态的本质是别人已经替你付过的时间成本,这句话我觉得比任何参数表都实在。
1.3 异构核这件事,别当成营销词看
T536除了四个A55大核,还带了一个RISC-V小核。刚开始我也觉得这就是个宣传点,实际用起来才发现它在工业场景里挺有用。大核跑Linux,任务调度、内存管理、网络协议栈这些东西都很成熟,但Linux本身不是实时的——你写个中断处理,从触发到用户态响应,中间隔着调度器、中断下半部、可能有几十微秒到几毫秒的抖动。对于需要微秒级响应的场合,这个抖动是不能接受的。
小核跑裸机或者RTOS,中断响应能做到确定性的几微秒。把电机控制环路、编码器采样、急停逻辑这类任务挪到小核上,大核专心做业务逻辑和通信,两边通过共享内存加中断的方式交换数据,这个架构在实际项目里非常好用。代价是你要多维护一套固件、要设计一套可靠的核间通信协议、调试手段也要多准备一套。我一般会在项目早期就把这条边界划清楚:哪些任务必须确定性、哪些任务可以接受Linux的调度抖动,想明白了再动手,比后期重构省事得多。
这里还有一个容易忽略的细节:小核的固件怎么加载。常见做法是打包进boot分区,由U-Boot或者大核在启动阶段把固件搬到指定内存地址再把小核从复位状态放出来。这套流程一旦跑通就很省心,但第一次配的时候很容易卡在内存地址冲突上——大核的保留内存区域和小核的代码段重叠了,表现就是大核启动到一半莫名挂死,串口只吐半行日志就没了。这类问题后面在排查章节我会详细说。
2. 开发环境搭建:从一台干净的机器到第一行串口日志
2.1 主机侧环境与工具链的取舍
主机环境这块我的建议很直接:用Ubuntu LTS,别折腾花活。不是别的发行版不行,而是绝大多数SDK、交叉工具链、构建脚本都是在这个环境下验证过的,你换个环境省下的那点心理满足感,往往会被各种莫名其妙的依赖问题消耗掉。我自己的机器上长期留一个22.04的物理机或者虚拟机专门做嵌入式开发,不装乱七八糟的东西,减少变量。
磁盘空间要提前规划。一个完整的SDK加上编译产物,50GB起步,如果同时保留多个构建目录和rootfs,100GB也不嫌多。我第一次搞的时候只给了40GB,编译到一半报“设备空间不足”,清理了半天缓存才继续下去,非常影响心情。建议单独挂一块盘或者分一个大分区给构建目录,别跟系统盘挤在一起。
依赖包的安装是第一步,也是最容易被跳过的一步。很多人直接clone代码就开始编译,结果报一堆“找不到头文件”“找不到工具”的错误。我的做法是先把常用依赖一次性装齐:
sudo apt update sudo apt install -y build-essential git curl wget \ bison flex texinfo libncurses5-dev libssl-dev \ python3 python3-pip python3-dev \ device-tree-compiler bc rsync cpio \ libelf-dev libyaml-dev swig \ u-boot-tools mtools dosfstools \ gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ minicom picocom screen这里有几个包值得单独说一下。device-tree-compiler提供dtc,没有它设备树编译直接失败,而且版本太老会报一些奇怪的语法错误;libssl-dev是内核签名和部分工具链需要的;bc看起来无关紧要,但内核编译的脚本里用到了它,缺了会在编译后期报错,非常浪费时间。至于交叉工具链,用系统自带的gcc-aarch64-linux-gnu起步是最省事的,跑通了再考虑换成厂商提供的工具链或者自己用crosstool-ng构建。
工具链的选择上我的经验是:先跟着SDK走,跑通了再说优化。厂商SDK里通常会指定某个版本的工具链,甚至直接打包在SDK里。一开始就换成自己熟悉的版本,很容易遇到ABI不匹配、libc版本冲突这类问题,而且因为你是“唯一一个这么干的人”,出了问题也没人问。等整个流程跑通了、你对手册和代码都熟悉了,再评估要不要换。
2.2 SDK目录结构:先看懂再动手
拿到SDK之后,别急着执行编译脚本。花半小时把目录结构摸清楚,后面能省下好几个小时。典型的SDK结构大致是这样:
sdk/ ├── build/ # 构建脚本与配置 ├── device/ # 板级配置、分区表、环境变量 ├── kernel/ # 内核源码 ├── u-boot/ # 引导加载程序 ├── brandy/ # 早期固件与DDR初始化相关 ├── rootfs/ # 根文件系统骨架与预置文件 ├── external/ # 第三方包与应用 ├── out/ # 编译产物(通常按板型分目录) └── tools/ # 打包、烧录、签名等工具device/这个目录最值得先看。里面通常有板级配置文件,定义了用哪个设备树、内存大小、存储介质、分区布局。我接手一个新板子的时候,第一件事就是把这里的配置和自己手上的硬件对一遍:DDR容量对不对、eMMC还是NAND、串口用哪一个、默认启动介质是什么。这些信息错一个,后面可能就是几个小时的黑屏排查。
分区表也需要重点看。典型的布局大概长这样:
| 分区 | 大小 | 内容 | 说明 |
|---|---|---|---|
| boot0/boot1 | 各 1MB 左右 | 引导固件 | 由SoC ROM加载,一般不随意改 |
| uboot | 2MB | U-Boot + 环境变量 | 留点余量便于加功能 |
| boot | 64MB | 内核 + 设备树 + 小核固件 | 内核带模块的话要放大 |
| rootfs A | 512MB | 根文件系统A | 支持双分区升级时用 |
| rootfs B | 512MB | 根文件系统B | 冗余备份 |
| data | 剩余 | 应用数据 | 独立挂载,升级不动它 |
分区大小的计算其实有讲究。boot分区的64MB不是拍脑袋来的:一个典型的内核镜像(Image格式)大概20-30MB,设备树几百KB,小核固件几MB,再加上几个备份和升级过程中的临时文件,64MB是留了接近一倍余量的结果。如果你开了内核调试信息或者加了initramfs,那得再往上加。rootfs的分区则取决于你要装多少东西,我一般会先在开发板上把应用装齐,用du -sh量一下实际占用,再乘以2作为分区大小,留出日志和临时文件的空间。
2.3 编译、打包与第一次烧录
配置确认完之后就可以编译了。大多数SDK的用法都差不多,先选板型,再选要构建的模块:
cd sdk source build/envsetup.sh lunch t536_industrial_board make kernel -j$(nproc) make rootfs -j$(nproc) make pack -j$(nproc)-j$(nproc)这个参数不是随便写的,它等于CPU核心数。理论上并发数可以开到核心数的1.5倍左右,但对内存的消耗也会上去,8GB内存的机器开太多并发容易触发OOM,反而更慢。我的经验是:物理机上用核心数,虚拟机上用核心数的一半,比较稳妥。
打包完成后会生成一个可以直接烧录的镜像文件。烧录方式取决于板子的启动介质,常见的有几种:用厂商提供的PC端工具通过USB走FEL模式烧录、通过SD卡启动后再写到eMMC、或者用网络TFTP加载。第一种最省事但依赖工具,第二种最通用,第三种在反复调试阶段效率最高。
我个人的习惯是先做一张SD启动卡,把系统跑起来,确认没问题之后再从SD卡系统里把镜像写到eMMC。这样做的好处是砖了不心疼——eMMC写坏了还得想办法救,SD卡拔出来重新做一个就行。制作启动卡的命令大致如下:
# 确认SD卡设备名,千万别搞错,写错盘会丢数据 lsblk sudo dd if=out/t536_industrial/images/sdcard.img of=/dev/sdX bs=4M status=progress conv=fsync sync提示:
dd这条命令没有撤销键。执行前一定用lsblk确认设备名,把U盘和SD卡区分清楚。我见过不止一次把系统盘写掉的案例,代价是重装一下午。
第一次上电,串口是你的唯一信息来源。接线的时候注意三点:TX接RX、RX接TX、GND必须共地。波特率一般是115200,8N1,无流控。我习惯用picocom,退出方便:
picocom -b 115200 -d 8 -p n -y n /dev/ttyUSB0如果上电后一片空白,先别怀疑板子坏了,按这个顺序查:串口线有没有接反、USB转串口模块的电压电平对不对(有些模块是3.3V有些是5V)、波特率有没有搞错、板子是不是根本没启动(量一下各路电源和复位脚)。这四步能解决九成以上的“串口没输出”问题。
2.4 网络调试通道:比串口好用得多
串口能救命,但日常开发光靠串口效率太低。系统起来之后第一件事就是把网络通道打通,后面传文件、远程登录、跑测试都靠它。
板子默认的IP配置一般在/etc/network/interfaces或者systemd-networkd的配置文件里。调试阶段我一般直接给个静态IP,避免DHCP服务器不在的时候抓瞎:
# 临时配置,重启失效 ip addr add 192.168.10.50/24 dev eth0 ip link set eth0 up ip route add default via 192.168.10.1 # 验证 ping -c 3 192.168.10.1主机这边对应配一个同网段的地址,然后用scp传文件,比串口传快几个数量级。如果板子上有sshd,直接ssh登录,配合tmux用起来跟本地终端没什么区别。
传大文件的时候scp其实不是最快的,它加密开销比较大。局域网内我一般用nc或者直接起个临时HTTP服务:
# 主机侧,在文件所在目录执行 python3 -m http.server 8000 # 板子侧 wget http://192.168.10.100:8000/firmware.bin这个方法简单粗暴,测固件、传镜像都好使。注意只在可信的局域网里这么干,公网环境别这么玩。
3. 开机流程拆解:搞清楚每一步在干什么
3.1 从按下电源到login提示符
理解启动链条的价值在于:出问题的时候你能快速定位到是哪一级挂的。T536这类ARM64 SoC的启动流程大致分四到五级。
第一级是SoC内部的BootROM,固化在芯片里,改不了。上电后它根据几个引脚的电平决定从哪个介质加载下一级代码,比如eMMC、SD卡、SPI NAND或者USB。这一级如果失败,串口通常什么都不会输出,因为串口还没初始化。判断方法就是看有没有任何输出——完全没有,问题就在这一级或者更靠前的电源、时钟、复位。
第二级是boot0这类一级引导固件,负责DDR初始化和加载U-Boot。DDR初始化参数不对的话,表现是串口吐出几行乱码然后停住,或者干脆什么都不出。这块的调试手段比较有限,通常靠厂商提供的工具做DDR压力测试,确认参数没问题再往下走。
第三级是U-Boot,负责加载内核、设备树、可选的initramfs,然后把控制权交给内核。U-Boot阶段能做的事情很多:改启动参数、进命令行、通过TFTP加载镜像、做环境变量管理。这个阶段串口输出最丰富,也是最容易插入调试手段的地方。
第四级是内核,从解压自身、初始化内存管理、探测设备、挂载根文件系统,到启动第一个用户态进程。内核的log会打到串口上,dmesg里能看到全部内容。这一步卡住通常表现为内核日志打到某个设备就停了,这时候要看最后几行日志指向哪个驱动。
第五级是用户态,init进程拉起各种服务,最后给你一个登录界面。这一级出问题通常是配置文件错误、服务依赖缺失、权限不对,相对好查,因为你有完整的shell可以用(如果串口登录还能进的话)。
我把这套流程整理成一张排查表,实际用起来很顺手:
| 现象 | 最可能的阶段 | 首要排查动作 |
|---|---|---|
| 串口完全无输出 | BootROM / 电源 / 时钟 | 量各路电源、查启动介质选择脚 |
| 输出几行乱码后停住 | DDR初始化 | 核对DDR参数、做压力测试 |
| 停在U-Boot提示符不动 | 启动参数 / 镜像加载 | 检查bootargs、确认镜像存在 |
| 内核日志中途停止 | 驱动探测 / 根文件系统 | 看最后几行日志定位驱动 |
| 内核起来了但进不了shell | 用户态init | 检查init配置、挂载参数 |
3.2 设备树:改对这几处,大部分外设就活了
设备树是ARM Linux开发绕不过去的东西。它的本质是把“板子上有什么、怎么连的”从内核代码里剥离出来,变成一个可以独立修改的数据文件。同一颗SoC可以对应无数种板子,内核代码不用动,只改设备树就行。
T536的设备树文件通常在kernel/arch/arm64/boot/dts/下面,分为SoC级的.dtsi和板级的.dts。SoC级的文件描述芯片内部有什么控制器、寄存器地址在哪、默认时钟和中断怎么连;板级的文件描述这块具体的板子上,哪些控制器被引出来了、接了什么外设、用哪个引脚。
我改设备树的时候有个固定套路,按顺序检查四项:引脚复用、时钟、电源、中断。这四项任何一项不对,外设就工作不了,而且报错信息往往很含糊。
引脚复用是最容易出问题的。同一个物理引脚可能身兼数职,既能做UART又能做PWM还能做GPIO,具体做哪个由复用寄存器决定。设备树里通过pinctrl节点来描述,写法大致是这样:
&pio { uart2_pins: uart2-pins { pins = "PG6", "PG7"; function = "uart2"; drive-strength = <10>; bias-pull-up; }; }; &uart2 { pinctrl-names = "default"; pinctrl-0 = <&uart2_pins>; status = "okay"; };这里status = "okay"很关键,SoC级的dtsi里绝大多数外设默认是disabled,你要用哪个就得在板级文件里显式打开。我见过不少人改了半天驱动,最后发现是设备树里压根没使能,节点根本没被创建。
时钟配置也经常出问题。有些外设需要外部晶振提供时钟源,设备树里要指定clocks和clock-names,配错了表现为外设能识别但速率不对,或者干脆初始化超时。电源部分则涉及regulator节点,尤其是屏幕、摄像头这类需要多路供电的外设,供电顺序错了可能直接不工作甚至损坏器件。
中断配置相对简单,但要注意触发方式(上升沿、下降沿、高电平、低电平)和是否需要消抖。按键类输入如果没配消抖,会看到大量重复中断。
3.3 内核裁剪:别一上来就删
新手容易犯的一个错误是:看到内核配置项成千上万,就想着大刀阔斧地裁剪,追求一个极小的镜像。我不建议在项目初期这么干,原因很简单——你还不确定哪些功能是必须的,删错了排查成本远高于省下来的那点空间。
合理的顺序是:先用默认配置或者厂商提供的defconfig把系统跑通,确保所有外设都工作;然后打开内核的运行时配置查看功能,看看实际加载了哪些模块;再基于实际使用情况做裁剪。
内核配置里有一个很有用的选项,能让你在运行时看到当前内核编译了哪些配置:
# 内核配置里打开 CONFIG_IKCONFIG=y CONFIG_IKCONFIG_PROC=y # 板子上查看 zcat /proc/config.gz | grep -i can裁剪的时候优先动这几类:明显用不到的驱动(比如你的板子没有PCIe设备,那PCIe相关的可以关)、调试符号和追踪框架(量产版本关掉能省不少空间)、用不到的文件系统(只留ext4和vfat,其他关掉)。但要留神,有些选项关了会有连锁反应,比如关掉某个框架,依赖它的驱动就编不过。所以每次裁剪完都要完整编译一遍并且上板验证,别只看编译通过就完事。
模块化也是一个选择。把不常用的驱动编成模块(.ko),需要的时候再加载,既能减小内核体积,又保留了灵活性。代价是根文件系统里要多放这些模块文件,启动时可能要多花点时间加载。
4. 外设实战:一个一个点亮才踏实
4.1 GPIO与中断:最基础也最容易翻车
GPIO看起来是最简单的外设,实际上坑不少。先说要用的几个概念:输入输出方向、电平高低、上下拉、驱动强度、中断触发方式。这些在设备树和用户态接口里都要配对。
用户态操作GPIO现在推荐走字符设备接口(gpiod),老的sysfs接口虽然还能用但已经标记为过时:
# 查看有哪些GPIO控制器 gpiodetect # 查看某个控制器的引脚 gpioinfo gpiochip1 # 导出并设置为输出,拉高 gpioset gpiochip1 12=1 # 读取输入 gpioget gpiochip1 15如果是写C程序,用libgpiod更规范:
#include <gpiod.h> #include <stdio.h> int main(void) { struct gpiod_chip *chip; struct gpiod_line *line; int val; chip = gpiod_chip_open_by_name("gpiochip1"); if (!chip) { perror("open chip"); return 1; } line = gpiod_chip_get_line(chip, 12); if (!line) { perror("get line"); gpiod_chip_close(chip); return 1; } if (gpiod_line_request_output(line, "led", 0) < 0) { perror("request output"); gpiod_chip_close(chip); return 1; } for (int i = 0; i < 5; i++) { gpiod_line_set_value(line, 1); usleep(200000); gpiod_line_set_value(line, 0); usleep(200000); } gpiod_line_release(line); gpiod_chip_close(chip); return 0; }编译的时候记得链接库:gcc gpio_test.c -lgpiod -o gpio_test。
中断这块我踩过最深的坑是按键抖动。机械按键按下和松开的瞬间会产生一串毛刺,如果中断触发方式配成双边沿,一次按键可能触发十几次中断。解决办法有两种:硬件上并一个0.1uF电容做RC滤波,软件上在驱动里做时间窗口过滤(比如20ms内只认第一次)。我一般两个都做,硬件滤波降低中断频率,软件过滤兜底。
还有一个细节:GPIO的中断号在不同的内核版本里可能变化,写死中断号是很危险的做法。正确的做法是通过设备树里的interrupt-parent和interrupts属性让内核自动分配,用户态则通过gpioinfo看实际值。
4.2 双网口与CAN-FD:工业场景的命脉
网络配置这块,两个网口分别命名是很重要的。默认情况下的命名规则可能会变(eth0/eth1谁先谁后不确定),导致你的脚本时好时坏。解决办法是用udev规则或者systemd的link文件按MAC地址或者PCI路径固定命名:
# /etc/systemd/network/10-eth-field.link [Match] Path=platform-xxxx0000.ethernet [Link] Name=ethfield这样命名之后,配置文件里就可以放心地按名字来写,不用管枚举顺序。
CAN-FD的配置比传统CAN稍微复杂一点,主要是数据段和仲裁段可以用不同的速率。配置命令大概是这样:
ip link set can0 down ip link set can0 type can \ bitrate 500000 \ dbitrate 2000000 \ fd on \ sample-point 0.875 \ dsample-point 0.8 \ restart-ms 100 ip link set can0 up这里面的参数需要算一下。采样点的计算公式是:
$$ 采样点 = \frac{1 + t_{seg1}}{1 + t_{seg1} + t_{seg2}} $$
其中tseg1包含传播段和相位缓冲段1,tseg2是相位缓冲段2。以500kbps、时钟80MHz为例,一个位时间是160个时钟周期。取同步段1个tq、传播段+相位缓冲1共139个tq、相位缓冲2共20个tq,采样点就是(1+139)/160 = 87.5%。数据段2Mbps时位时间只有40个时钟周期,一般把采样点设到75%-80%,留出足够的相位缓冲余量。
实际调试时我建议先用candump把原始帧打出来,确认收发链路通了再去做协议解析:
# 终端1:监听 candump -ta can0 # 终端2:发一帧测试 cansend can0 123#DEADBEEF如果发不出去或者收不到,按这个顺序查:收发器供电有没有、终端电阻对不对(总线两端各120欧,中间不要加)、比特率两边是否一致、总线有没有对地短路。终端电阻这个事我遇到过好几次,有人把120欧焊在了每个节点上,总线挂了七八个节点,等效阻抗变成十几欧,信号直接垮掉。
4.3 显示通路:屏点亮那一刻才算真的开始
显示这块是新人最容易卡住的地方。MIPI-DSI的调试涉及面板参数、时序、时钟、电源序列、背光,任何一个不对屏幕都不亮,而且现象往往都一样——黑屏。
调试顺序我一般这么走:先确认背光能亮(这说明供电和背光升压没问题),再看屏的复位信号时序对不对(有些屏要求复位拉低至少10ms再拉高,然后等120ms才能发初始化命令),最后才去看数据通道。
设备树里屏幕节点大致是这样:
&dsi { status = "okay"; panel@0 { compatible = "vendor,panel-7inch-1024x600"; reg = <0>; reset-gpios = <&pio 8 12 GPIO_ACTIVE_LOW>; backlight = <&backlight>; power-supply = <®_lcd>; display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <51200000>; hactive = <1024>; vactive = <600>; hfront-porch = <160>; hback-porch = <160>; hsync-len = <10>; vfront-porch = <12>; vback-porch = <23>; vsync-len = <1>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; }; };clock-frequency这个值要算一下:像素时钟 = 水平总周期 × 垂直总周期 × 刷新率。上面这组参数,水平总周期是1024+160+160+10 = 1354,垂直总周期是600+12+23+1 = 636,按60Hz算就是1354 × 636 × 60 ≈ 51.7MHz,取51.2MHz是常见值。算出来偏差太大会导致画面偏移或者完全无显示。
屏幕不亮的时候,我有个笨但有效的办法:先把数据通道和背光分开验证。拔掉背光,用手电筒照屏幕,如果能看到隐约的图像,说明数据通了背光没通;如果完全看不到,说明数据没通。这一招能省掉一半的排查时间。
4.4 异构核通信:把实时任务安顿好
前面提到小核的用途,这里说说怎么用。核间通信的机制各家不太一样,常见的有共享内存加中断、mailbox控制器、或者基于RPMessage的框架。不管用哪种,核心思路都是:大核和小核各自划一块内存作为通信缓冲区,通过中断通知对方数据准备好了。
内存保留这件事要在设备树里提前声明,而且要在U-Boot阶段就把这块内存从Linux的可用内存里挖掉:
reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; rproc_buf: rproc-buffer@4a000000 { reg = <0x0 0x4a000000 0x0 0x00200000>; no-map; }; }; &rproc { memory-region = <&rproc_buf>; status = "okay"; };no-map这个属性很重要,它告诉内核这块内存不要建立页表映射,避免内核的Cache回写把数据搞乱。0x4a000000这个地址不是随便填的,得对着内存布局表确认它落在DRAM范围内、并且不和内核镜像、设备树、initramfs的加载地址重叠。我前面说的“大核启动到一半挂死”就是栽在这里——把小核的代码段划到了内核解压的地址上,内核把自己解压过去的时候覆盖了小核代码,然后两边一起完蛋。
通信协议的设计上,我一般用一个环形缓冲区加一个简单的头结构:
struct ipc_msg { uint32_t magic; /* 校验用,防止读到脏数据 */ uint16_t type; /* 消息类型 */ uint16_t len; /* 数据长度 */ uint8_t payload[64]; uint32_t crc; /* 数据校验 */ };发送方写好数据后把magic置为约定值,再触发中断;接收方在中断里读magic,对上了才处理。这个额外的校验步骤看起来多余,但在调试阶段能帮你区分“数据没到”和“数据到了但解析错了”两种情况。
5. 产品化收尾:能跑和能交付是两回事
5.1 根文件系统的选择
开发阶段用什么rootfs都行,产品阶段就得认真考虑了。常见的几条路:Buildroot、Yocto、Debian/Ubuntu的ARM版本、以及自己从头搭。
Buildroot的好处是简单直接,make menuconfig选包,编出来的镜像小,依赖关系清晰。缺点是包管理能力弱,加一个新软件可能要写一个新的package规则。适合软件包固定、追求镜像精简的产品。
Yocto更重但更规范,recipe机制让整个构建过程可追溯,适合团队协作和长期维护。学习曲线陡一些,构建一次环境要几个小时,但一旦跑起来,后面加包改配置都有章可循。
Debian这类发行版胜在生态丰富,apt装软件很方便,适合需要频繁更新应用、对镜像大小不敏感的场景。代价是镜像大(几百MB起步)、启动慢、可定制的粒度粗。
我自己的取舍标准是:产品生命周期超过三年、有多个客户版本需要维护,选Yocto;做原型验证或者内部工具,Buildroot;需要跑大量第三方开源软件、对系统一致性要求不高,用发行版。
不管选哪条路,有几件事是必须做的。把只读分区做出来,根文件系统挂成只读,运行时的数据写到独立的数据分区,这样意外断电不会把系统搞坏。把日志管理做起来,要么限制日志文件大小并定期滚动,要么把日志写到独立分区。把时间同步配好,工业设备对时间戳的要求通常很高,没有可靠的时间源,日志和故障记录的价值会大打折扣。
5.2 启动时间优化:从能用到好用
客户对启动时间的要求往往比你想的苛刻。现场设备断电重启,操作工等十秒和等三十秒的体验完全不同。优化的前提是先量化,把每个阶段的耗时画出来。
内核提供了initcall_debug参数,可以打印每个初始化函数的耗时:
# 在bootargs里加上 initcall_debug printk.time=1开机后看dmesg,找出耗时最长的几项,通常是存储控制器初始化、网络PHY协商、文件系统挂载。我做过的一个项目里,光网络PHY自动协商就花了2秒多,后来改成固定速率全双工之后省下来了。
用户态这边用systemd的话,可以这样看:
systemd-analyze systemd-analyze blame systemd-analyze critical-chainblame按耗时排序,一眼就能看出哪个服务拖了后腿。常见的问题服务是各种等待网络的单元,配置成After=network-online.target之后会一直等到网络完全就绪。如果某个服务其实不需要网络,把它改成network.target能省不少时间。
我整理过一份优化前后的对比,供参考:
| 阶段 | 优化前 | 优化后 | 主要手段 |
|---|---|---|---|
| U-Boot | 2.1s | 0.8s | 去掉延时、精简环境变量加载 |
| 内核 | 3.4s | 1.9s | 裁剪驱动、并行探测、关闭调试输出 |
| 网络就绪 | 2.6s | 0.4s | 固定速率全双工、静态IP |
| 用户态服务 | 4.2s | 1.5s | 精简服务、并行启动、延迟非关键服务 |
| 合计 | 12.3s | 4.6s | — |
这张表里的数字不是标准答案,每个项目都不一样,但它说明了一个道理:启动时间的优化空间通常比你想象的大,而且大部分收益来自“去掉不必要的东西”,而不是什么高深技巧。
5.3 长期运行的可靠性设计
工业设备现场没人给你重启的机会,可靠性得在设计阶段就考虑进去。
看门狗是底线。硬件看门狗要开着,但喂狗的策略要设计好,不能简单地在主循环里无脑喂。我的做法是让每个关键线程定期上报心跳,主监控线程检查所有心跳都正常之后才喂狗。这样任何一个关键线程卡死,整机就会在超时后自动重启,而不是带着故障继续跑。
硬件看门狗在Linux下的接口是/dev/watchdog,配置超时时间:
# 查看当前超时 cat /sys/class/watchdog/watchdog0/timeout # 设置为60秒 echo 60 > /sys/class/watchdog/watchdog0/timeout # 打开看门狗(一旦打开,不喂就会重启) echo 1 > /dev/watchdog0注意最后一条命令,打开之后就进入了“不喂就重启”的状态,调试的时候要小心,别在命令行里敲完就去干别的了。建议用systemd的RuntimeWatchdogSec配置来管理,更规范一些:
# /etc/systemd/system.conf RuntimeWatchdogSec=60 RebootWatchdogSec=120双分区升级是另一个必备设计。升级过程最怕的就是断电,单分区写到一半断电,设备就变砖了。双分区的做法是:系统跑在A分区,升级时把新镜像写到B分区,写完之后做校验,校验通过才修改启动参数切到B分区。切过去之后如果新系统起不来,看门狗超时重启时可以回退到A分区(这需要U-Boot配合做启动次数计数)。
温度监控也不能少。工业设备的机箱内部温度可能比环境温度高十几度,夏天户外机柜里轻松上到七十度。读取温度的接口:
# 典型路径,具体看芯片 cat /sys/class/thermal/thermal_zone0/temp返回值一般是毫摄氏度,除以1000就是摄氏度。我一般会在应用里设定两级阈值:超过75度降频或者关掉非关键任务,超过85度告警并记录日志。这些数据在售后分析的时候非常有用。
6. 常见问题排查实录
6.1 启动类问题速查
启动类问题的排查我在第3节已经把框架搭好了,这里补几个具体的案例和排查动作。
现象:串口完全没有输出,电源指示灯正常。先量CPU的几路核心电压是不是在范围内,再量复位脚是不是被拉住了。有些板子的复位电路里有个延时电容,容量选大了会导致复位释放很慢。还有一种可能是启动介质选择引脚被拉错了,比如配置成了从SPI启动但板子上根本没焊SPI Flash,这种情况下BootROM会一直重试,串口什么都不输出。用示波器看启动介质选择引脚的电平是最快的定位方法。
现象:U-Boot能起来,但加载内核时报“Bad Magic Number”。这个错误的意思是镜像格式不对或者读取的位置不对。检查两件事:镜像文件是不是真的在U-Boot要读的那个地址上、镜像有没有被正确打包(有些SDK的镜像需要在前面加一个头部)。还有一个低级但常见的错误:改了内核但忘了重新打包,烧进去的还是旧镜像。
现象:内核日志打到“Freeing unused kernel memory”就停住了。这说明内核已经起来了,卡在找根文件系统或者执行init。检查bootargs里的root=参数对不对、根文件系统的分区是不是真的存在、文件系统类型有没有写错。如果用的是initramfs,检查initramfs有没有正确打包进镜像。
现象:进到initramfs的shell了,但切不到真正的根文件系统。多半是root=指定的设备名不对。/dev/mmcblk0p5和/dev/mmcblk1p5的区别在于用的是哪个控制器,改硬件设计或者改设备树之后这个编号可能会变。用blkid看一下实际的分区UUID,改用root=UUID=xxx会可靠得多。
6.2 外设类问题速查
外设不工作的时候,我有一套固定的排查顺序:设备树有没有使能 → 引脚复用对不对 → 时钟有没有 → 电源有没有 → 驱动有没有加载 → 上层接口对不对。按这个顺序走,大部分问题能在十分钟内定位。
| 症状 | 可能原因 | 排查命令或动作 |
|---|---|---|
| 设备树节点不存在 | status未设为okay | 看/proc/device-tree下有没有对应节点 |
| 节点在但驱动没加载 | 内核配置没开 | lsmod、`dmesg |
| 驱动加载了但初始化失败 | 时钟或电源缺失 | dmesg里的错误码,cat /sys/kernel/debug/clk/clk_summary |
| 初始化成功但无数据 | 引脚复用不对 | 查pinctrl配置、量引脚波形 |
| 数据有但内容错误 | 时序或电气问题 | 示波器看信号完整性、检查上拉电阻 |
举一个具体的例子。有一次CAN收发一直不通,ip link能看到can0接口,状态也是UP,但就是收发不了任何帧。按上面的顺序查:设备树节点在,驱动加载了,时钟也有。然后用万用表量CAN_H和CAN_L之间的电阻,应该是60欧(两个120欧并联),结果量出来是无穷大——说明终端电阻没焊。补上电阻之后立马通了。这个案例说明,电气层面的检查有时候比软件排查更直接,别一上来就怀疑驱动。
再举一个。屏幕背光亮了但画面全白,看起来像是有信号但数据不对。查下来是MIPI-DSI的lane数量和实际硬件不匹配——设备树里配的是4 lane,实际板子上只走了2 lane。改过来之后就正常了。这类问题在硬件改版之后特别容易出现,软件配置没跟着更新。
6.3 性能与功耗类问题
CPU占用率异常高但找不到热点。先看是不是有进程在忙等(死循环),用top排序看一下。如果是内核态占用高,用perf做个采样:
perf top -g如果是中断风暴,/proc/interrupts里能看到某个中断的数量飞速增长。常见原因是某个GPIO中断没消抖,或者某个外设的中断标志位没清,导致中断一直被触发。
系统运行一段时间之后变慢。先看内存,free -h看可用内存和cache的占比,如果可用内存持续下降,可能有内存泄漏。再看存储,eMMC的写入速度会随着使用时间下降,特别是写放大严重的时候。用iostat看一下IO情况。还有一个容易忽略的原因:日志文件写满了分区,导致系统各种操作失败但又不报明显错误。
功耗偏高。嵌入式设备的功耗主要来自几块:CPU频率、DDR频率、外设供电、以太网PHY。调试阶段可以先看CPU频率是不是一直跑在最高档:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq如果项目对功耗有要求,可以考虑在空闲时降频,或者关掉不用的外设电源域。但要注意,频繁的频率切换本身也有开销,而且可能影响实时性,得权衡。
7. 几条踩过坑之后才明白的事
写到这里,技术细节说得差不多了,最后分享几条我觉得比具体命令更有价值的东西。
第一条:先让它跑起来,再让它跑得好。我见过太多人(包括早期的我自己)在项目一开始就追求完美的架构、最小的镜像、最优的性能,结果两周过去了系统还没跑起来。正确的节奏是先跑通最小可用版本,确认每个接口都能工作,然后再一轮轮优化。优化的前提是有基准,没有基准的优化都是瞎猜。
第二条:串口日志要留够,但别刷屏。调试阶段把所有日志都打开是很爽,但量产版本一定要把调试日志关掉或者降到最低级别。我遇到过因为日志级别没改,量产设备运行三个月把eMMC写满导致批量故障的案例。日志是给你看的,不是给Flash看的。
第三条:每个改动都要能回退。用git管理你的设备树、内核配置、构建脚本,每次改动都提交。板上验证通过的版本打个tag。这样出问题的时候你能用git bisect快速定位,而不是靠记忆猜“上周改的那个地方是不是有问题”。这一条看起来是常识,但真正坚持做的人不多。
第四条:硬件问题用硬件工具查。软件排查有极限,当你确认设备树、驱动、配置都没问题但外设就是不工作时,拿起万用表和示波器。量电源、量时钟、量信号完整性。我排查过的问题里,至少有三分之一最终是硬件原因——虚焊、漏焊、元件值不对、走线干扰。软件工程师学会看示波器,能省下大量来回扯皮的时间。
第五条:把调试手段做进产品里。产品交付之后出了问题,你不可能每次都跑到现场接串口。所以在设计阶段就要预留远程诊断能力:日志上传、状态快照、关键寄存器的读取接口。这些东西在产品正常的时候看起来没用,出问题的时候就是救命的。我一般会留一个诊断脚本,能一键收集系统状态、最近的日志、关键外设的寄存器值,打包成一个文件传回来。这个小工具帮我省下的差旅费,远超开发它的时间成本。
第六条:文档是给自己写的。别指望半年后的自己还记得为什么在这个设备树节点里加了个奇怪的延时。改一处配置就顺手写一句为什么,不要求格式,能看懂就行。我现在翻两年前的项目笔记,很多关键决策的原因都写在里面,要是当时没记,那些坑可能得再踩一遍。
这套东西走下来,从拿到板子到产品交付,中间会经历无数次“咦怎么不工作”和“哦原来是这样”。这不是能力问题,嵌入式开发本来就是在软件和硬件的交界处摸爬滚打,任何一层的疏漏都会在另一层表现出来。把排查框架搭好、把工具用熟、把每一次踩坑的原因记下来,下次遇到同类问题你的反应速度会快一个数量级。T536这套平台给我的感觉是,它的开放性让这个过程可控——你能看到底层发生了什么,能自己改,能在社区里找到别人踩过的痕迹。这比一个封闭的、只能靠厂商支持的方案,长期来看要省心得多。