第二章 嵌入式Linux开发环境搭建:交叉编译工具链、TFTP/NFS服务器配置、串口终端与调试工具

说实话,做ADAS域控制器移植,第一步不是写代码,而是搭环境。我见过太多新手一上来就急着编译内核,结果卡在工具链报错上,一卡就是半天。环境搭好了,后面的事才能顺风顺水。

这一章,咱们就把开发环境彻底搞定。包括交叉编译工具链、TFTP/NFS服务器、串口终端和调试工具。这些都是我每天吃饭的家伙,一个都不能少。

2.1 交叉编译工具链:为什么非它不可?

你想想看,你的PC是x86架构,ADAS域控制器通常是ARM或RISC-V架构。PC上编译出来的程序,域控制器根本跑不了。怎么办?交叉编译就是干这个的。

说白了,交叉编译就是在你的x86主机上,生成目标板(ARM)能执行的二进制文件。工具链就是这套编译、链接、调试工具的集合。

2.1.1 工具链的组成

一套完整的交叉编译工具链,通常包含这几部分:

  • 编译器:比如arm-linux-gnueabihf-gcc,负责把C代码变成汇编
  • 汇编器:arm-linux-gnueabihf-as,把汇编变成机器码
  • 链接器:arm-linux-gnueabihf-ld,把多个目标文件合成一个可执行文件
  • 调试器:arm-linux-gnueabihf-gdb,远程调试目标板上的程序
  • 库文件:glibc或uClibc的ARM版本,程序运行时依赖它们

我个人习惯用Linaro提供的预编译工具链,稳定又省事。当然,你也可以用Yocto或Buildroot自己编译一套,但那是进阶玩法了。

2.1.2 安装与验证

安装其实很简单。以ARM Cortex-A72为例(很多ADAS域控制器用这个核):

# 下载Linaro工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到指定目录
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 添加环境变量
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# 验证是否安装成功
arm-linux-gnueabihf-gcc --version

看到版本号输出,就说明装好了。嗯,这里要注意:环境变量最好写到~/.bashrc里,不然每次开终端都要重新export。

我的经验: 我曾经在某个项目里,因为工具链版本不对,编译出来的内核在启动时直接panic。查了两天才发现是gcc版本太新,生成的指令集和芯片不匹配。所以,工具链版本一定要和芯片厂商推荐的保持一致

2.1.3 测试编译

写个简单的hello world,交叉编译一下:

// hello.c
#include <stdio.h>

int main() {
    printf("Hello from ADAS Domain Controller!\n");
    return 0;
}
arm-linux-gnueabihf-gcc -o hello hello.c
file hello

输出应该是:ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)。看到ARM字样,就对了。

2.2 TFTP/NFS服务器配置:让目标板“吃”到文件

开发阶段,你不可能每次改个驱动都重新烧写整个镜像。太慢了。TFTP和NFS就是用来解决这个问题的。

  • TFTP:简单文件传输协议,用来从主机下载内核镜像、设备树到目标板
  • NFS:网络文件系统,让目标板直接挂载主机的文件系统,共享根文件系统

我一般这么用:uboot阶段用TFTP下载内核,内核启动后用NFS挂载根文件系统。这样开发效率能提升好几倍。

2.2.1 搭建TFTP服务器

以Ubuntu主机为例:

# 安装TFTP服务器
sudo apt-get install tftpd-hpa

# 配置TFTP目录
sudo mkdir -p /tftpboot
sudo chmod 777 /tftpboot

# 修改配置文件 /etc/default/tftpd-hpa
# 内容如下:
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/tftpboot"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure"

# 重启服务
sudo systemctl restart tftpd-hpa

把编译好的内核镜像zImage和设备树.dtb文件丢到/tftpboot目录下。目标板uboot里这样用:

# 在uboot命令行中
setenv serverip 192.168.1.100   # 主机IP
setenv ipaddr 192.168.1.10      # 目标板IP
tftp 0x82000000 zImage
tftp 0x83000000 your-board.dtb
bootz 0x82000000 - 0x83000000
注意: 防火墙可能会拦截TFTP的69端口。如果传输失败,先检查防火墙:sudo ufw disable 或开放69端口。我曾经被这个问题坑过,TFTP一直timeout,最后发现是防火墙在作怪。

2.2.2 搭建NFS服务器

NFS用来共享根文件系统。目标板启动后,直接通过网络访问主机上的文件系统目录。

# 安装NFS服务器
sudo apt-get install nfs-kernel-server

# 创建共享目录,比如放你制作好的根文件系统
sudo mkdir -p /srv/nfs/rootfs
sudo chown nobody:nogroup /srv/nfs/rootfs

# 配置 /etc/exports,添加一行:
/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)

# 重启NFS服务
sudo systemctl restart nfs-kernel-server

内核启动参数里加上NFS挂载:

# 在uboot中设置bootargs
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs rw ip=192.168.1.10:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off'

# 然后启动内核
bootz 0x82000000 - 0x83000000

目标板启动后,你直接在主机上修改/srv/nfs/rootfs里的文件,目标板立即可见。改驱动、调应用,再也不用反复烧写了。

避坑指南: 我曾经在NFS挂载时遇到“permission denied”,折腾了半天。后来发现是/etc/exports里忘了加no_root_squash。这个选项允许目标板的root用户拥有主机上的root权限,否则会被映射成nobody,很多操作就受限了。

2.3 串口终端与调试工具:和硬件对话的窗口

ADAS域控制器通常没有显示器、键盘。你怎么和它交互?串口。串口是嵌入式开发最基础、最可靠的调试手段。

2.3.1 串口连接与配置

硬件上,一般用USB转串口模块(比如FT232、CP2102)连接目标板的UART接口。接线很简单:

  • GND接GND
  • TXD接目标板的RXD
  • RXD接目标板的TXD

软件上,我推荐用screenminicom。我个人更习惯用screen,轻量又好用:

# 查看串口设备
ls /dev/ttyUSB*

# 连接串口,波特率115200,8数据位,1停止位,无校验
sudo screen /dev/ttyUSB0 115200

如果看到uboot启动日志,就说明连上了。按Ctrl+A,再按K可以退出screen。

注意: 串口参数必须和目标板uboot中设置的完全一致。常见的是115200 8N1,但也有用57600或38400的。如果输出乱码,八成是波特率不对。

2.3.2 调试工具:minicom与kermit

除了screen,minicom也是老牌工具。功能更丰富,支持文件传输、脚本等。

# 安装minicom
sudo apt-get install minicom

# 配置
sudo minicom -s

# 进入配置菜单,设置:
# Serial port setup -> Serial Device: /dev/ttyUSB0
#                     Bps/Par/Bits: 115200 8N1
#                     Hardware Flow Control: No
#                     Software Flow Control: No

# 保存为默认配置
# 退出配置,进入终端

minicom支持用sz/rz通过串口传文件,虽然慢,但在没有网络的情况下很管用。

2.3.3 内核调试:printk与动态调试

串口连上了,怎么调试内核?最常用的就是printk。它和用户空间的printf类似,但输出到内核日志缓冲区,通过串口显示。

// 在驱动代码中
printk(KERN_INFO "my_driver: probe called, irq=%d\n", irq_num);
printk(KERN_ERR "my_driver: DMA transfer failed!\n");

日志级别从高到低:KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG。

我一般用KERN_INFO打印正常流程,KERN_ERR打印错误。调试完了记得删掉或改成KERN_DEBUG,不然生产环境刷屏刷到飞起。

更高级的调试手段是动态调试(dynamic debug)。它允许你在运行时动态开启或关闭某个文件的调试信息,不用重新编译内核。

# 在目标板上
# 开启某个文件的动态调试
echo 'file drivers/i2c/busses/i2c-imx.c +p' > /sys/kernel/debug/dynamic_debug/control

# 查看当前所有动态调试信息
cat /sys/kernel/debug/dynamic_debug/control

这个功能在调试I2C、SPI这类总线驱动时特别好用。我曾经调一个I2C触摸屏驱动,死活读不到数据。开了动态调试后,发现是地址发送时序不对,差了一个时钟周期。

2.3.4 其他实用调试工具

工具 用途 我的用法
strace 跟踪用户空间系统调用 调试应用程序为什么卡住,看它调用了哪些syscall
gdb + gdbserver 远程调试应用程序 目标板上跑gdbserver,主机上gdb远程连接,单步调试
perf 性能分析 找出CPU热点,优化关键路径
devmem2 直接读写物理内存 调试寄存器映射,看某个外设寄存器值对不对

举个例子,用devmem2读一个GPIO寄存器:

# 读物理地址0x20A0000处的值
devmem2 0x20A0000

# 写值到物理地址
devmem2 0x20A0000 w 0x1

这个工具在调试硬件初始化时简直是神器。不用写驱动,直接看寄存器值,就知道硬件有没有正常工作。

2.4 本章小结

环境搭建这块,说白了就是三板斧:

  1. 交叉编译工具链:选对版本,装好路径,验证编译
  2. TFTP/NFS:TFTP传内核,NFS共享根文件系统,开发效率翻倍
  3. 串口与调试:串口是生命线,printk是基本功,动态调试是进阶武器

这些环境配好了,后面移植内核、写驱动才能跑得起来。别嫌麻烦,一次配好,后面省心。下一章,咱们就开始真正的内核移植了——从拿到一个空板子,到看到内核启动日志。