3、Sensor驱动开发环境搭建:Linux内核驱动开发环境、交叉编译工具链、设备树基础、I2C/SPI通信协议

做Sensor驱动开发,第一关就是搭环境。很多人觉得这事简单,装个虚拟机、下个内核源码就开干。但实际项目里,环境配不好,后面全是坑。我见过不少同事,代码写得挺溜,结果编译出来的模块一加载就崩,查了半天发现是交叉编译工具链版本不对。

这一章,我就把环境搭建的要点掰开揉碎讲清楚。从Linux内核驱动开发环境怎么配,到交叉编译工具链怎么选,再到设备树和I2C/SPI通信协议的核心知识点,咱们一个一个过。

3.1 Linux内核驱动开发环境

先说主机环境。我个人习惯用Ubuntu 20.04 LTS或22.04 LTS,这两个版本对嵌入式开发的支持最稳定。你不需要装桌面版,服务器版就行,省资源。

核心依赖包就这些:

sudo apt-get update
sudo apt-get install build-essential
sudo apt-get install linux-headers-$(uname -r)
sudo apt-get install git fakeroot ncurses-dev
sudo apt-get install xz-utils libssl-dev bc
sudo apt-get install flex bison libelf-dev

这里有个坑——linux-headers包必须和你的内核版本完全一致。我遇到过有人用5.4的内核,结果装了5.10的头文件,编译出来的模块根本插不进去。

注意: 如果你要编译的是目标板的内核模块,主机上只需要装对应版本的交叉编译工具链和内核源码,不需要装主机本地的linux-headers。

内核源码获取方式有两种:

  • 从kernel.org下载:稳定版,适合学习
  • 从芯片厂商获取:比如NXP、Rockchip、Allwinner提供的BSP包,里面带了补丁和配置文件

我建议初学者先从标准内核开始。厂商BSP虽然方便,但里面改的东西太多,有时候你都不知道某个配置是干嘛的。

3.2 交叉编译工具链

交叉编译,说白了就是在你的x86电脑上编译出ARM架构能跑的二进制文件。Sensor驱动通常跑在ARM Cortex-A系列处理器上,所以你得用ARM交叉编译工具链。

常用的工具链有几种:

工具链 适用场景 安装方式
gcc-arm-linux-gnueabihf ARM 32位,硬件浮点 apt-get install
gcc-aarch64-linux-gnu ARM 64位 apt-get install
Linaro GCC 性能优化更好 官网下载
芯片厂商定制版 特定SoC优化 BSP包自带

安装命令很简单:

sudo apt-get install gcc-arm-linux-gnueabihf
sudo apt-get install gcc-aarch64-linux-gnu

装完之后验证一下:

arm-linux-gnueabihf-gcc --version

如果能看到版本号,说明工具链装好了。

个人经验: 我建议用Linaro的工具链,它对ARM Cortex-A系列优化得更好。特别是做Sensor驱动时,浮点运算性能直接影响图像数据的处理速度。

编译内核模块时,需要指定交叉编译器:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

这里有个细节——CROSS_COMPILE后面的横线不能丢。它表示工具链的前缀,系统会自动补全gcc、ld、objcopy等命令。

3.3 设备树基础

设备树(Device Tree)是Linux内核用来描述硬件信息的一种数据结构。以前的内核用板级文件(board-xxx.c)来定义硬件,但那种方式太死板,换一个板子就得改内核代码。

设备树解决了这个问题。它把硬件描述和内核代码分离开,你只需要写一个.dts文件,告诉内核“这个Sensor挂在I2C总线上,地址是0x48”,内核就能自动识别。

一个典型的Sensor设备树节点长这样:

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;

    imx219: camera@10 {
        compatible = "sony,imx219";
        reg = <0x10>;
        clocks = <&clks IMX219_CLK>;
        clock-names = "xvclk";
        DOVDD-supply = <&vreg_dovdd>;
        AVDD-supply = <&vreg_avdd>;
        DVDD-supply = <&vreg_dvdd>;
        reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
    };
};

我来解释一下关键字段:

  • compatible:驱动匹配的关键字,驱动里通过of_match_table来匹配
  • reg:I2C设备地址,0x10就是IMX219的默认地址
  • clocks:时钟源,Sensor需要外部时钟才能工作
  • supply:电源域,Sensor通常需要多路电压
  • reset-gpios:复位引脚,低电平有效

编译设备树的方法:

dtc -I dts -O dtb -o imx219.dtb imx219.dts

生成的.dtb文件就是内核启动时加载的二进制设备树。

避坑指南: 我曾经在一个项目里,设备树里把Sensor的I2C地址写错了,结果驱动一直probe失败。查了两天才发现是地址少了一位偏移。记住,reg字段写的是7位I2C地址左移1位后的值,不是原始地址。

3.4 I2C通信协议

I2C是Sensor驱动里最常用的通信协议。它只有两根线——SCL(时钟)和SDA(数据),支持多主多从。

Sensor驱动里,I2C主要用来做两件事:

  1. 配置寄存器:写Sensor的初始化参数,比如曝光时间、增益、帧率
  2. 读取状态:读Sensor的ID、温度、错误标志等

Linux内核里操作I2C的标准接口:

// 写寄存器
int i2c_smbus_write_byte_data(struct i2c_client *client, 
                               u8 reg, u8 value);

// 读寄存器
int i2c_smbus_read_byte_data(struct i2c_client *client, 
                              u8 reg);

// 批量写
int i2c_master_send(struct i2c_client *client, 
                    const char *buf, int count);

// 批量读
int i2c_master_recv(struct i2c_client *client, 
                    char *buf, int count);

实际项目中,我更喜欢用i2c_transfer来做批量操作。比如初始化Sensor时,要写几十个寄存器,用i2c_smbus_write_byte_data一个一个写太慢了。

static int sensor_write_array(struct i2c_client *client,
                              struct regval_list *vals, int len)
{
    int i, ret;
    for (i = 0; i < len; i++) {
        ret = i2c_smbus_write_byte_data(client, 
                                        vals[i].reg, 
                                        vals[i].val);
        if (ret < 0)
            return ret;
    }
    return 0;
}

I2C通信的时序要求很严格。特别是Sensor上电后,需要等待一段时间才能开始I2C通信。我见过有人刚上电就去读Sensor ID,结果读回来全是0xFF。

经验之谈: Sensor上电后,先等10ms,再拉高复位引脚,再等20ms,然后才能开始I2C通信。这个时序在数据手册里都有,千万别跳过去。

3.5 SPI通信协议

SPI比I2C快,但线多。它需要4根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。

Sensor驱动里,SPI通常用于高速数据传输,比如高帧率的图像传感器或者需要快速配置的Sensor。

Linux内核的SPI驱动接口:

// 定义SPI传输消息
struct spi_transfer t = {
    .tx_buf = tx_buf,
    .rx_buf = rx_buf,
    .len = len,
    .speed_hz = 10000000,  // 10MHz
    .cs_change = 0,
};

struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);

// 同步传输
ret = spi_sync(spi, &m);

SPI的时钟极性(CPOL)和相位(CPHA)必须和Sensor匹配。这个在设备树里配:

spi_master: spi@ff110000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "snps,dw-apb-ssi";
    reg = <0xff110000 0x1000>;
    interrupts = <0 44 4>;
    clocks = <&spi_clk>;
    num-cs = <1>;
    cs-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;

    sensor@0 {
        compatible = "adi,adxl345";
        reg = <0>;
        spi-max-frequency = <5000000>;
        spi-cpol;   // 时钟极性为1
        spi-cpha;   // 时钟相位为1
    };
};
注意: SPI的速率不是越高越好。Sensor的数据手册会给出最大SPI时钟频率,超过这个值,数据就会出错。我遇到过有人把SPI配到50MHz,结果Sensor返回的数据全是乱的。

3.6 知识体系总览

这一章的内容比较多,我画了一张图帮你理清思路:

Sensor驱动开发环境搭建知识体系 Sensor驱动开发环境搭建知识体系 Sensor驱动开发环境 Linux内核开发环境 Ubuntu 20.04/22.04 LTS build-essential + linux-headers 内核源码(标准/BSP) 交叉编译工具链 ARM 32位/64位选择 CROSS_COMPILE前缀配置 Linaro GCC优化 设备树基础 compatible/reg/clocks 电源域/supply配置 dtc编译.dts → .dtb I2C/SPI通信协议 I2C: i2c_smbus / i2c_transfer

这张图把环境搭建的四个核心模块串起来了。从上到下,先配好Linux内核环境,再搞定交叉编译工具链,然后通过设备树描述硬件,最后用I2C或SPI和Sensor通信。每一步都环环相扣。

核心要点:
  • 环境搭建是基础,工具链版本必须和内核匹配
  • 设备树是硬件和驱动的桥梁,compatible字段是匹配关键
  • I2C适合配置和低速通信,SPI适合高速数据传输
  • 时序和电气特性必须严格遵循数据手册