当前位置:首页>Linux>【简单嵌入式】在 Linux 内核中增加一个驱动6 个核心阶段

【简单嵌入式】在 Linux 内核中增加一个驱动6 个核心阶段

  • 2026-10-10 11:26:54
【简单嵌入式】在 Linux 内核中增加一个驱动6 个核心阶段

【简单嵌入式】在 Linux 内核中增加一个驱动6 个核心阶段


一、前期准备:明确需求与调研

✅ 工作内容:

  1. 1. 确认硬件规格
    • • 芯片型号(如 MCP3208、ADS1115)
    • • 通信接口(SPI/I2C)
    • • 协议细节(命令格式、时序、数据位宽、参考电压)
    • • 是否支持中断(DRDY)、多通道、差分输入等
  2. 2. 检查内核是否已有驱动



    # 在内核源码中搜索
    git grep -l "mcp3208" drivers/iio/adc/
    ls drivers/iio/adc/*ads*  # 常见 TI/ADI 芯片通常已有驱动


    → 若已有,只需配置设备树,无需重写!

  3. 3. 确定使用子系统
    • • ADC/DAC → IIO(Industrial I/O)
    • • 温度传感器 → hwmon
    • • 简单 GPIO 设备 → gpiolib + sysfs
    • • 自定义协议 → 字符设备(miscdevice)

💡 原则:优先复用标准子系统,避免造轮子。


二、设备树(Device Tree)配置

✅ 工作内容:

  1. 1. 在板级 DTS 文件中添加设备节点



    &spi1 {
        status = "okay";
        pinctrl-0 = <&spi1_pins>;

        my_adc: adc@0 {
            compatible = "mycompany,mcp3208"; // ← 关键!必须唯一且匹配驱动
            reg = <0>;                        // CS 片选号
            spi-max-frequency = <2000000>;    // 最大 SPI 频率
            vref-supply = <&vcc_3v3>;         // 参考电压(可选)
        };
    };


  2. 2. 验证引脚复用(pinctrl)已正确配置
    • • 确保 SCLK/MOSI/MISO/CS 引脚被设为 SPI 功能,而非 GPIO

⚠️ 常见错误:

  • • compatible 字符串与驱动不匹配 → 驱动无法 probe
  • • 忘记 status = "okay" → 总线未启用

三、编写内核驱动代码

✅ 核心任务(以 IIO ADC 为例):

1. 定义通道描述(iio_chan_spec)



1
2
3
4
5

static conststruct iio_chan_spec mcp3208_channels[] = {
    IIO_CHANNEL_RAW(0),
    IIO_CHANNEL_RAW(1),
    // ... 支持 8 通道
};


→ 描述每个通道的类型(电压)、索引、支持的操作(raw/scale)

2. 实现 .read_raw 回调



1
2
3
4
5
6
7
8
9
10
11
12
13
14

static int mcp3208_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
                            int *val, int *val2, long mask)
{
    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        *val = mcp3208_read_channel(chan->channel); // 内部调用 spi_sync
        return IIO_VAL_INT;
    case IIO_CHAN_INFO_SCALE:
        *val = 3300; *val2 = 12; // 3.3V / 2^12
        return IIO_VAL_FRACTIONAL_LOG2;
    }
    return -EINVAL;
}


3. 填充 iio_info 和 iio_dev



1
2
3
4
5
6
7
8
9
10
11
12
13

static conststruct iio_info mcp3208_info = {
    .read_raw = mcp3208_read_raw,
};

static int mcp3208_probe(struct spi_device *spi)
{
struct iio_dev *indio_dev = devm_iio_device_alloc(...);
    indio_dev->info = &mcp3208_info;
    indio_dev->channels = mcp3208_channels;
    indio_dev->num_channels = ARRAY_SIZE(mcp3208_channels);
    indio_dev->name = "mcp3208";
    return devm_iio_device_register(&spi->dev, indio_dev);
}


4. 注册 spi_driver 并匹配设备树



1
2
3
4
5
6
7
8
9
10
11
12
13

static conststruct of_device_id mcp3208_of_match[] = {
    { .compatible = "mycompany,mcp3208" },
    { }
};

staticstruct spi_driver mcp3208_driver = {
    .probe = mcp3208_probe,
    .driver = {
        .name = "mcp3208",
        .of_match_table = mcp3208_of_match,
    },
};
module_spi_driver(mcp3208_driver);


🔑 关键点:

  • • 使用 devm_ 系列函数自动管理资源释放;
  • • 不要手动创建 /dev 节点 —— IIO Core 自动处理;
  • • 并发访问需加锁(mutex)。

四、内核配置与编译

✅ 工作内容:

  1. 1. 将驱动加入 Kconfig 和 Makefile



    # drivers/iio/adc/Kconfig
    config MCP3208
        tristate "Microchip MCP3208 ADC driver"
        depends on SPI
        help
          Say yes here to build support for ...

    # drivers/iio/adc/Makefile
    obj-$(CONFIG_MCP3208) += mcp3208.o


  2. 2. 配置内核选项


    1
    2

    make menuconfig
    # Device Drivers → Industrial I/O Support → ADC → [*] MCP3208


  3. 3. 编译



    # 编译进内核
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage

    # 或编译为模块
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules



五、部署与测试

✅ 工作内容:

  1. 1. 更新目标板
    • • 复制新内核/模块/设备树到开发板
  2. 2. 验证驱动加载



    dmesg | grep mcp3208      # 应有 "probed" 日志
    ls /sys/bus/iio/devices/  # 应出现 iio:deviceX


  3. 3. 用户空间读取数据



    # 单次读取
    cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw

    # 计算电压
    RAW=$(cat .../in_voltage0_raw)
    SCALE=$(cat .../in_voltage_scale)
    echo "$RAW * $SCALE" | bc


  4. 4. 高级功能测试(如支持)
    • • 触发采样:echo hrtimer > .../trigger/current_trigger
    • • 缓冲区读取:dd if=/dev/iio:device0 ...

六、集成到构建系统(Buildroot/Yocto)

✅ 工作内容(以 Buildroot 为例):

  1. 1. 创建驱动包



    mkdir package/mcp3208-driver


  2. 2. 编写 mcp3208-driver.mk



    MCP3208_DRIVER_VERSION = 1.0
    MCP3208_DRIVER_SITE = $(TOPDIR)/package/mcp3208-driver
    MCP3208_DRIVER_SITE_METHOD = local
    MCP3208_DRIVER_MODULE_SUBDIRS = .
    $(eval $(kernel-module))
    $(eval $(generic-package))


  3. 3. 启用驱动



    make menuconfig
    # Target packages → Hardware handling → [*] mcp3208-driver


  4. 4. 构建完整系统



    make


    → 驱动自动打包进 rootfs,开机可自动加载。


七、关键原则总结

阶段
核心思想
设计
优先使用标准子系统(IIO/hwmon),避免自定义 ioctl
实现
利用内核提供的资源管理(devm_)、并发保护(mutex)
接口
通过 sysfs/chrdev 暴露标准化属性,而非私有 API
测试
用标准工具(iio_info)验证,而非仅自测程序
维护
遵循内核编码规范,便于未来合入主线

八、分步详解:三者如何联动

步骤 1:DTS —— 告诉内核“硬件长什么样”



1
2
3
4
5
6
7
8
9

// myboard.dts
&spi1 {
    status = "okay";
    adc: mcp3008@0 {
        compatible = "microchip,mcp3008"; // ← 关键标识
        reg = <0>;                        // CS0
        spi-max-frequency = <1000000>;
    };
};


🔗 作用:

  • • &spi1:声明该设备挂载在 SoC 的 SPI1 总线上;
  • • compatible:提供唯一标识符,用于匹配驱动;
  • • reg:指定片选信号(CS0);
  • • spi-max-frequency:配置 SPI 时钟上限。

💡 DTS 本身不包含代码,它只是数据,由内核在启动时解析。


步骤 2:内核 —— 创建设备实例并匹配驱动

  1. 1. 内核解析 DTS
    • • 遇到 mcp3008@0 节点;
    • • 创建 



      struct spi_device


      实例,填充:




      spi->modalias = "spi:mcp3008";
      spi->of_node->compatible = "microchip,mcp3008";
      spi->chip_select = 0;
      spi->max_speed_hz = 1000000;


  2. 2. 总线匹配驱动
    • • 内核遍历所有已注册的 spi_driver;
    • • 比较 driver.of_match_table 与 spi_device.of_node.compatible;
    • • 匹配成功 → 调用 driver.probe(spi_device*)。

🔑 此时 DTS 与驱动通过 compatible 字符串建立联系。


步骤 3:驱动 —— 实现硬件操作并注册标准接口



1
2
3
4
5
6
7
8
9
10

// mcp3008.c
static int mcp3008_probe(struct spi_device *spi)
{
struct iio_dev *indio_dev = devm_iio_device_alloc(...);

    // 1. 封装 SPI 通信(使用 spi_device)
    // 2. 定义通道(IIO_CHAN_SPEC)
    // 3. 注册到 IIO 子系统
    return devm_iio_device_register(&spi->dev, indio_dev);
}


🔗 驱动做了什么?

  • • 利用 spi_device:调用 spi_sync() 与 SoC SPI 控制器通信;
  • • 注册 IIO 设备

    :IIO Core 自动创建:

    • • sysfs 目录:/sys/bus/iio/devices/iio:device0/
    • • 属性文件:in_voltage0_raw, in_voltage_scale
    • • 字符设备:/dev/iio:device0(用于缓冲采样)

💡 驱动是桥梁:
它把 DTS 描述的硬件 转化为 内核可管理的对象,并暴露标准化接口。


步骤 4:POSIX —— 提供用户空间访问的通用方式

用户程序通过 标准 POSIX 文件 API 访问 ADC:



1
2
3
4
5
6
7
8
9
10
11
12

#include <stdio.h>
#include <fcntl.h>

// 方式1:sysfs(单次读取)
FILE *f = fopen("/sys/bus/iio/devices/iio:device0/in_voltage0_raw", "r");
int raw;
fscanf(f, "%d", &raw); // ← POSIX stdio

// 方式2:字符设备(高速采样)
int fd = open("/dev/iio:device0", O_RDONLY); // ← POSIX open()
char buf[1024];
read(fd, buf, sizeof(buf)); // ← POSIX read()


🔗 POSIX 的角色:

  • • 不关心底层是什么设备,只认“文件路径”;
  • • sysfs 和 /dev 是 Linux 对 POSIX “一切皆文件”的扩展实现;
  • • 所有操作(open/read/write/ioctl)都映射到内核的 VFS(虚拟文件系统)层,最终调用驱动的 .read 或 .ioctl 回调。

✅ POSIX 是用户与内核之间的契约,而 驱动 + DTS 是内核与硬件之间的契约。


九、关键连接点总结

连接点
机制
示例
DTS → 驱动compatible
 字符串匹配
"microchip,mcp3008"
驱动 → 硬件struct spi_device
 / i2c_client
spi_sync(spi, &msg)
驱动 → POSIX
注册 sysfs 属性或字符设备
iio_device_register()
 → /sys/.../raw
POSIX → 用户
标准文件 API
open()
, read(), fscanf()

十、一个完整数据流示例(读取 ADC)

  1. 1. 用户输入:


    1

    cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw


  2. 2. POSIX 层:
    • • Shell 调用 open() → read() → write(stdout)
  3. 3. VFS 层:
    • • 根据路径 /sys/.../raw 找到对应的 sysfs attribute
  4. 4. IIO Core:
    • • 调用驱动的 .read_raw(..., IIO_CHAN_INFO_RAW)
  5. 5. 驱动:
    • • 构造 SPI 命令 → 调用 spi_sync()
  6. 6. SPI 子系统:
    • • 路由到 SoC 的 SPI 控制器驱动(如 spi-rockchip.c)
  7. 7. 硬件:
    • • SoC 发出 SCLK/MOSI,ADC 返回 MISO 数据
  8. 8. 数据回传:
    • • 驱动解析 raw 值 → 返回给 IIO Core → sysfs → Shell → 终端显示

🔁 全程无需用户知道:

  • • SoC 是 RK3588 还是 i.MX6?
  • • ADC 是 SPI 还是 I2C?
  • • 内核版本是多少?

最新文章

随机文章