第二章 嵌入式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。
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
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里的文件,目标板立即可见。改驱动、调应用,再也不用反复烧写了。
/etc/exports里忘了加no_root_squash。这个选项允许目标板的root用户拥有主机上的root权限,否则会被映射成nobody,很多操作就受限了。
2.3 串口终端与调试工具:和硬件对话的窗口
ADAS域控制器通常没有显示器、键盘。你怎么和它交互?串口。串口是嵌入式开发最基础、最可靠的调试手段。
2.3.1 串口连接与配置
硬件上,一般用USB转串口模块(比如FT232、CP2102)连接目标板的UART接口。接线很简单:
- GND接GND
- TXD接目标板的RXD
- RXD接目标板的TXD
软件上,我推荐用screen或minicom。我个人更习惯用screen,轻量又好用:
# 查看串口设备
ls /dev/ttyUSB*
# 连接串口,波特率115200,8数据位,1停止位,无校验
sudo screen /dev/ttyUSB0 115200
如果看到uboot启动日志,就说明连上了。按Ctrl+A,再按K可以退出screen。
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 本章小结
环境搭建这块,说白了就是三板斧:
- 交叉编译工具链:选对版本,装好路径,验证编译
- TFTP/NFS:TFTP传内核,NFS共享根文件系统,开发效率翻倍
- 串口与调试:串口是生命线,printk是基本功,动态调试是进阶武器
这些环境配好了,后面移植内核、写驱动才能跑得起来。别嫌麻烦,一次配好,后面省心。下一章,咱们就开始真正的内核移植了——从拿到一个空板子,到看到内核启动日志。