【简单嵌入式】在 Linux 内核中增加一个驱动6 个核心阶段
一、前期准备:明确需求与调研
✅ 工作内容:
- • 芯片型号(如 MCP3208、ADS1115)
- • 协议细节(命令格式、时序、数据位宽、参考电压)
- 2. 检查内核是否已有驱动
# 在内核源码中搜索
git grep -l "mcp3208" drivers/iio/adc/
ls drivers/iio/adc/*ads* # 常见 TI/ADI 芯片通常已有驱动
→ 若已有,只需配置设备树,无需重写!
- • ADC/DAC → IIO(Industrial I/O)
- • 简单 GPIO 设备 → gpiolib + sysfs
- • 自定义协议 → 字符设备(miscdevice)
💡 原则:优先复用标准子系统,避免造轮子。
二、设备树(Device Tree)配置
✅ 工作内容:
- 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>; // 参考电压(可选)
};
};
- • 确保 SCLK/MOSI/MISO/CS 引脚被设为 SPI 功能,而非 GPIO
⚠️ 常见错误:
- •
compatible 字符串与驱动不匹配 → 驱动无法 probe - • 忘记
status = "okay" → 总线未启用
三、编写内核驱动代码
✅ 核心任务(以 IIO ADC 为例):
1. 定义通道描述(iio_chan_spec)
static conststruct iio_chan_spec mcp3208_channels[] = {
IIO_CHANNEL_RAW(0),
IIO_CHANNEL_RAW(1),
// ... 支持 8 通道
};
→ 描述每个通道的类型(电压)、索引、支持的操作(raw/scale)
2. 实现 .read_raw 回调
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
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 并匹配设备树
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);
🔑 关键点:
- • 不要手动创建
/dev 节点 —— IIO Core 自动处理;
四、内核配置与编译
✅ 工作内容:
- 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. 配置内核选项
make menuconfig
# Device Drivers → Industrial I/O Support → ADC → [*] MCP3208
- 3. 编译
# 编译进内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
# 或编译为模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
五、部署与测试
✅ 工作内容:
- 2. 验证驱动加载
dmesg | grep mcp3208 # 应有 "probed" 日志
ls /sys/bus/iio/devices/ # 应出现 iio:deviceX
- 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
- • 触发采样:
echo hrtimer > .../trigger/current_trigger - • 缓冲区读取:
dd if=/dev/iio:device0 ...
六、集成到构建系统(Buildroot/Yocto)
✅ 工作内容(以 Buildroot 为例):
- 1. 创建驱动包
mkdir package/mcp3208-driver
- 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. 启用驱动
make menuconfig
# Target packages → Hardware handling → [*] mcp3208-driver
- 4. 构建完整系统
→ 驱动自动打包进 rootfs,开机可自动加载。
七、关键原则总结
| |
|---|
| 设计 | 优先使用标准子系统(IIO/hwmon),避免自定义 ioctl |
| 实现 | 利用内核提供的资源管理(devm_)、并发保护(mutex) |
| 接口 | 通过 sysfs/chrdev 暴露标准化属性,而非私有 API |
| 测试 | 用标准工具(iio_info)验证,而非仅自测程序 |
| 维护 | |
八、分步详解:三者如何联动
步骤 1:DTS —— 告诉内核“硬件长什么样”
// myboard.dts
&spi1 {
status = "okay";
adc: mcp3008@0 {
compatible = "microchip,mcp3008"; // ← 关键标识
reg = <0>; // CS0
spi-max-frequency = <1000000>;
};
};
🔗 作用:
- •
&spi1:声明该设备挂载在 SoC 的 SPI1 总线上; - •
compatible:提供唯一标识符,用于匹配驱动; - •
spi-max-frequency:配置 SPI 时钟上限。
💡 DTS 本身不包含代码,它只是数据,由内核在启动时解析。
步骤 2:内核 —— 创建设备实例并匹配驱动
- • 创建
实例,填充:
spi->modalias = "spi:mcp3008";
spi->of_node->compatible = "microchip,mcp3008";
spi->chip_select = 0;
spi->max_speed_hz = 1000000;
- • 比较
driver.of_match_table 与 spi_device.of_node.compatible; - • 匹配成功 → 调用
driver.probe(spi_device*)。
🔑 此时 DTS 与驱动通过 compatible 字符串建立联系。
步骤 3:驱动 —— 实现硬件操作并注册标准接口
// 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:
#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 | spi_sync(spi, &msg) |
| 驱动 → POSIX | | iio_device_register() |
| POSIX → 用户 | | open() |
十、一个完整数据流示例(读取 ADC)
- 1. 用户输入:
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw
- • Shell 调用
open() → read() → write(stdout)
- • 根据路径
/sys/.../raw 找到对应的 sysfs attribute
- • 调用驱动的
.read_raw(..., IIO_CHAN_INFO_RAW)
- • 构造 SPI 命令 → 调用
spi_sync()
- • 路由到 SoC 的 SPI 控制器驱动(如
spi-rockchip.c)
- • SoC 发出 SCLK/MOSI,ADC 返回 MISO 数据
- • 驱动解析 raw 值 → 返回给 IIO Core → sysfs → Shell → 终端显示
🔁 全程无需用户知道: