2. 开发环境搭建:交叉编译工具链、内核源码树、设备树编译工具、TFTP/NFS服务配置
做嵌入式传感器驱动开发,环境搭建是第一道坎。很多人觉得这事简单,照着教程敲几条命令就行。但我见过太多人卡在这一步——工具链版本不对、内核树没配好、设备树编译不过,折腾一整天连个LED都点不亮。
今天我把这套环境拆开揉碎了讲。你跟着我的节奏来,保证一次搞定。
2.1 交叉编译工具链
说白了,交叉编译就是在PC上编译出能在ARM、RISC-V这类嵌入式芯片上跑的程序。你的开发机是x86架构,目标板是ARM架构,编译器得能生成ARM的机器码。
核心原则:工具链版本必须与内核版本匹配。我用过gcc 4.9配Linux 4.14,也用过gcc 9.3配Linux 5.10。跨大版本混用,轻则编译警告,重则运行时莫名其妙崩溃。
2.1.1 获取工具链
我个人习惯用Linaro提供的预编译工具链,省心。ARM Cortex-A系列用gcc-arm-linux-gnueabihf,Cortex-M系列用gcc-arm-none-eabi。
# 下载ARM 32位工具链
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
# 解压到/opt目录
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/
# 添加环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
小技巧:把export命令写到~/.bashrc里,每次开终端自动生效。我习惯再加一句export ARCH=arm和export CROSS_COMPILE=arm-linux-gnueabihf-,后面编译内核和设备树时能省不少事。
2.1.2 验证工具链
装完先跑个hello world,确认能用。
arm-linux-gnueabihf-gcc -v
# 看到gcc version 7.5.0就对了
# 写个测试程序
echo '#include <stdio.h>
int main() { printf("Hello Sensor!\n"); return 0; }' > test.c
arm-linux-gnueabihf-gcc -o test test.c
file test
# 输出: test: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)
看到ARM, EABI5,说明编译出来的确实是ARM指令集。我曾经遇到有人用x86的gcc编译,放到板子上直接报Exec format error——嗯,这种低级错误我也犯过。
2.2 内核源码树
驱动开发离不开内核源码。不是要你把整个内核都看懂,但至少得把编译环境搭好。
2.2.1 获取内核源码
我建议从官方主线拉取,别用发行版自带的。主线代码更新快,文档也全。
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
git checkout v5.10 # 选一个LTS版本,稳定
注意:内核源码树不要放在共享目录或NTFS分区里,否则编译时符号链接会出问题。我吃过这个亏,编译到一半报错,查了半天才发现是文件系统不支持符号链接。
2.2.2 配置和编译内核
先清理,再配置,最后编译。三步走,别跳。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 zImage
-j4是并行编译,4核CPU用4线程。你要是8核机器,可以试试-j8,但别超过物理核心数,否则反而慢。
编译完会在arch/arm/boot/下生成zImage。这就是我们要的内核镜像。
2.2.3 编译内核模块
传感器驱动通常以内核模块形式存在。编译模块需要内核源码树,而且必须和运行内核版本一致。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=./modules
模块会安装到./modules/lib/modules/5.10.0/目录下。把这个目录打包,传到板子上解压到/lib/modules/就行。
2.3 设备树编译工具
设备树(Device Tree)是描述硬件信息的文件。传感器驱动通过设备树获取寄存器地址、中断号、GPIO引脚等信息。
2.3.1 安装dtc工具
设备树编译器叫dtc,把.dts源文件编译成.dtb二进制文件。
# 方法一:从内核源码编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
# 方法二:单独安装dtc
sudo apt-get install device-tree-compiler
dtc --version # 确认版本
个人经验:我习惯用内核源码里的dtc,版本和内核完全匹配。单独安装的dtc有时会解析不了新版内核的dts语法。
2.3.2 编译设备树
假设你写了一个传感器设备树文件my-sensor.dts:
/dts-v1/;
#include "imx6ul.dtsi"
/ {
model = "My Sensor Board";
compatible = "my,imx6ul-sensor";
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
temp_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_FALLING>;
};
};
};
编译命令:
dtc -I dts -O dtb -o my-sensor.dtb my-sensor.dts
反编译查看:
dtc -I dtb -O dts -o my-sensor.dts my-sensor.dtb
这个操作我调试时经常用。板子上跑的是.dtb,但我想确认它是不是我改的那个版本,反编译一看便知。
2.4 TFTP/NFS服务配置
开发阶段最烦的就是反复烧写镜像。用TFTP加载内核,NFS挂载根文件系统,改完代码直接编译、重启、测试,效率翻倍。
2.4.1 TFTP服务器
TFTP用于从网络加载内核和设备树。配置很简单:
# 安装TFTP服务器
sudo apt-get install tftpd-hpa
# 配置文件 /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure"
# 创建目录并设置权限
sudo mkdir -p /srv/tftp
sudo chmod 777 /srv/tftp
# 重启服务
sudo systemctl restart tftpd-hpa
把zImage和.dtb文件复制到/srv/tftp/目录下。板子启动时设置bootargs:
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs,proto=tcp rw ip=192.168.1.10'
setenv bootcmd 'tftp 0x12000000 zImage; tftp 0x13000000 my-sensor.dtb; bootz 0x12000000 - 0x13000000'
boot
避坑指南:我曾经把TFTP目录权限设成755,结果板子下载文件时一直超时。查了半天才发现是tftp用户对目录没有写权限——虽然下载不需要写,但某些TFTP实现会先创建临时文件。改成777就好了。
2.4.2 NFS服务器
NFS让板子通过网络挂载PC上的根文件系统。改完驱动,在PC上编译,重启板子就能看到效果。
# 安装NFS服务器
sudo apt-get install nfs-kernel-server
# 编辑 /etc/exports,添加共享目录
/srv/nfs 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
# 创建根文件系统目录
sudo mkdir -p /srv/nfs
# 把制作好的根文件系统解压到这里
sudo tar -xvf rootfs.tar -C /srv/nfs/
# 重启NFS服务
sudo systemctl restart nfs-kernel-server
板子端uboot设置:
setenv nfsargs 'root=/dev/nfs rw nfsroot=192.168.1.100:/srv/nfs,v3,tcp ip=192.168.1.10:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off'
setenv bootargs 'console=ttymxc0,115200 ${nfsargs}'
saveenv
boot
2.5 环境验证
全部配置完,做个快速验证。写一个简单的内核模块,编译后通过NFS传到板子,加载测试。
// hello_sensor.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello Sensor Driver!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye Sensor Driver!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
Makefile:
obj-m := hello_sensor.o
KDIR := /home/user/linux
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
编译、加载、看日志:
make
cp hello_sensor.ko /srv/nfs/root/
# 在板子上
insmod hello_sensor.ko
dmesg | tail -5
# 应该看到: Hello Sensor Driver!
到这里,你的开发环境就全通了。后面写传感器驱动时,改代码→编译→重启板子→看效果,整个流程跑下来不超过30秒。这才是嵌入式开发的正确姿势。
2.6 知识体系总览
下面这张图把本章的核心内容串起来了。你对照着检查,缺哪个补哪个。
2.7 常见问题速查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
编译报错arm-linux-gnueabihf-gcc: command not found |
环境变量未设置或路径错误 | 检查PATH是否包含工具链bin目录 |
内核编译报No rule to make target 'modules' |
内核源码未配置或未编译基础镜像 | 先执行make defconfig && make zImage |
设备树编译报syntax error |
dts文件语法错误或包含路径不对 | 检查#include路径,使用-I指定搜索目录 |
| TFTP下载超时 | 防火墙未关闭或目录权限不足 | sudo ufw disable,检查/srv/tftp权限 |
NFS挂载失败mount: RPC: Unable to receive |
NFS服务未启动或exports配置错误 | sudo systemctl restart nfs-kernel-server,检查/etc/exports |
最后说一句:环境搭建这事,一次配好,后面全是收益。别嫌麻烦,也别跳过任何一步。我见过太多人图省事跳过NFS配置,结果每次改驱动都要烧写SD卡,一天下来光等烧写就浪费两小时。磨刀不误砍柴工,真的。