4、传感器驱动开发环境搭建:交叉编译工具链配置、Linux内核驱动模块基础、设备树编写
好,咱们进入正题。这一章要聊的是传感器驱动开发的基础设施——说白了,就是让你写的代码能在嵌入式平台上跑起来。我见过不少新手,上来就写驱动代码,结果编译都过不了,或者烧进去系统直接崩了。为什么?环境没搭对。
嵌入式开发不是PC开发。你想想看,你的开发机是x86架构,目标板可能是ARM、RISC-V或者MIPS。代码得在开发机上编译,生成目标平台能执行的二进制文件。这就需要一个关键工具——交叉编译工具链。
4.1 交叉编译工具链配置
交叉编译工具链,我习惯叫它“跨平台编译器全家桶”。它包含编译器(gcc)、链接器(ld)、汇编器(as)、调试器(gdb)以及各种库文件。你从芯片厂商(比如NXP、TI、Allwinner)的SDK里拿到的,通常是一个预编译好的工具链压缩包。
配置步骤其实不复杂,但细节容易翻车。
- 下载解压:拿到类似
gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz的文件,解压到/opt/或/usr/local/下。我个人习惯放/opt/,方便管理。 - 设置环境变量:把工具链的bin目录加到
PATH里。编辑~/.bashrc或~/.zshrc,加上一行:
然后export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/binsource ~/.bashrc让它生效。 - 验证安装:运行
arm-linux-gnueabihf-gcc --version。如果能看到版本信息,说明环境变量配对了。
还有一个容易忽略的点:工具链的命名规则。比如 arm-linux-gnueabihf 这个前缀,它告诉编译器目标架构是ARM,使用Linux系统调用,支持硬件浮点(hf)。如果你的目标板是软浮点(比如某些Cortex-A5),就得用 arm-linux-gnueabi。搞错了,编译出来的程序一运行就崩。
4.2 Linux内核驱动模块基础
传感器驱动在Linux里通常以内核模块的形式存在。为什么?因为模块可以动态加载卸载,不用每次改驱动都重新编译整个内核。调试起来方便多了。
一个最简单的内核模块长什么样?我给你看个例子:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init sensor_driver_init(void)
{
printk(KERN_INFO "Sensor driver loaded!\n");
return 0;
}
static void __exit sensor_driver_exit(void)
{
printk(KERN_INFO "Sensor driver unloaded!\n");
}
module_init(sensor_driver_init);
module_exit(sensor_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple sensor driver module");
嗯,这里要注意几个关键点:
module_init和module_exit是入口和出口。驱动加载时调用init,卸载时调用exit。printk是内核的打印函数,相当于用户空间的printf。但它的日志级别(KERN_INFO)决定了消息会不会显示在控制台。MODULE_LICENSE必须声明。不声明的话,内核会报“tainted kernel”警告。
编译这个模块,你需要一个Makefile。我项目中常用的模板是这样的:
obj-m += sensor_driver.o
KDIR := /path/to/your/kernel/source
CROSS_COMPILE := arm-linux-gnueabihf-
CC := $(CROSS_COMPILE)gcc
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
这里 KDIR 指向你目标板的内核源码树。注意,这个源码树必须是已经编译过的,否则会报缺少 Module.symvers 的错误。我刚开始做驱动时,就卡在这一步好几天——后来发现是内核源码没先编译一遍。
.ko 文件拷贝到板子上,用 insmod 或 modprobe 加载。
4.3 设备树编写
设备树(Device Tree)是什么?说白了,它就是一份描述硬件信息的“配置文件”。以前的老内核(2.6时代)把硬件信息硬编码在C代码里,换一个板子就得改代码重新编译。现在好了,设备树把硬件描述和驱动代码分离开。
设备树文件通常以 .dts 结尾,编译后生成 .dtb 二进制文件。内核启动时,bootloader会把 .dtb 传给内核,内核解析后就知道有哪些硬件设备了。
假设我们有一个I2C接口的传感器,挂载在I2C总线上,地址是0x48。设备树里怎么写?
/dts-v1/;
/ {
model = "MyEmbeddedBoard";
compatible = "vendor,myboard";
i2c0: i2c@ff000000 {
compatible = "vendor,i2c-controller";
reg = <0xff000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
temperature_sensor: sensor@48 {
compatible = "vendor,temperature-sensor";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
};
};
};
我来解释一下关键字段:
compatible:驱动和设备树匹配的关键。驱动里会声明of_match_table,内核根据这个字符串找到对应的驱动。reg:地址信息。对于I2C设备,就是从设备地址(0x48)。对于内存映射设备,是基地址和范围。interrupts:中断配置。这里指定了GPIO1的第5号引脚,上升沿触发中断。
reg 顺序和驱动里 platform_get_resource 读取的顺序是一一对应的。我曾经犯过一个错——设备树里写了两个reg区域,驱动里读取时顺序搞反了,结果读到的寄存器地址全是错的。排查了整整一个下午。
编译设备树也很简单:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
dtc 是设备树编译器,通常在Linux内核源码的 scripts/dtc/ 目录下。你也可以单独安装 device-tree-compiler 包。
4.4 整合:从驱动到设备树的配合
驱动和设备树是怎么配合的?我举个例子。假设你的传感器驱动里这样写:
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "vendor,temperature-sensor", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
static int sensor_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct i2c_client *client;
// 从设备树获取I2C设备信息
client = of_find_i2c_device_by_node(dev->of_node);
if (!client) {
dev_err(dev, "Failed to find I2C client\n");
return -ENODEV;
}
// 读取中断号
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(dev, "Failed to get IRQ\n");
return irq;
}
// 注册中断处理
devm_request_irq(dev, irq, sensor_isr, IRQF_TRIGGER_RISING,
"temperature_sensor", client);
return 0;
}
你看,驱动里通过 of_match_table 匹配设备树中的 compatible 字符串。匹配成功后,内核调用 probe 函数。在 probe 里,驱动从设备树中读取中断号、I2C地址等信息。这样,硬件配置变了,只需要改设备树,驱动代码不用动。
earlycon 和 ignore_loglevel,这样能看到更详细的设备树解析日志。如果设备树写错了,内核通常会打印类似 OF: fdt: not found /soc/i2c@... 的提示。
好了,这一章的内容就这些。环境搭好了,下一章我们就可以真正开始写传感器驱动了。记住,交叉编译工具链、内核模块框架、设备树,这三样东西是嵌入式驱动开发的“三驾马车”。任何一个环节出问题,你的传感器都跑不起来。