Linux设备驱动开发:基于Device Tree的I2C/SPI外设驱动编写与中断线程化处理
扫描二维码
随时随地手机看文章
随着嵌入式Linux系统的复杂度不断增加,设备驱动开发面临着新的挑战。传统的内核编码方式已难以满足现代SoC平台硬件配置的灵活性和可维护性需求,而设备树(Device Tree)技术的引入,彻底改变了Linux内核与硬件平台的交互方式。本文将深入探讨基于设备树的I2C/SPI外设驱动开发,并重点解析中断线程化这一提高系统实时性的关键技术。
一、设备树:从硬编码到动态配置的革命
设备树是描述系统硬件拓扑结构的标准化数据格式,将硬件描述从内核代码中分离出来。通过设备树,同一内核镜像可以适配多种硬件配置,极大地提高了代码重用性。
1.1 设备树基础结构
设备树采用节点(node)和属性(property)的树状结构描述硬件。每个节点代表一个硬件设备或总线,属性则描述设备的配置参数。
// 示例:I2C控制器节点定义
i2c1: i2c@40005400 {
compatible = "st,stm32f4-i2c"; // 驱动匹配字符串
reg = <0x40005400 0x400>; // 寄存器地址范围
interrupts = <31>; // 中断号
clocks = <&rcc 0 192>; // 时钟引用
#address-cells = <1>; // 子节点地址单元格数
#size-cells = <0>; // 子节点大小单元格数
// 连接的I2C设备
eeprom: eeprom@50 {
compatible = "atmel,24c08";
reg = <0x50>; // 设备地址
pagesize = <16>; // 页大小
};
};
1.2 SPI设备树配置
与I2C类似,SPI设备也在设备树中描述,但需要额外指定总线特性和片选信号。
// SPI控制器及设备配置
spi1: spi@40013000 {
compatible = "st,stm32f4-spi";
reg = <0x40013000 0x400>;
interrupts = <35>;
clocks = <&rcc 0 256>;
cs-gpios = <&gpioa 4 GPIO_ACTIVE_LOW>; // 片选引脚
status = "okay";
// 连接的SPI设备
flash: w25q128@0 {
compatible = "winbond,w25q128";
reg = <0>; // 片选编号
spi-max-frequency = <50000000>; // 最大频率
spi-cpol; // 时钟极性
spi-cpha; // 时钟相位
};
};
二、基于设备树的I2C驱动实现
2.1 驱动框架与设备树匹配
现代I2C驱动采用platform driver框架,通过设备树与硬件绑定。
// I2C驱动框架示例
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
// 设备树匹配表
static const struct of_device_id eeprom_of_match[] = {
{ .compatible = "atmel,24c08", .data = NULL },
{ .compatible = "atmel,24c16", .data = NULL },
{ .compatible = "microchip,24c32", .data = NULL },
{ /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, eeprom_of_match);
// 设备私有数据结构
struct eeprom_data {
struct i2c_client *client;
struct mutex lock;
u8 *buffer;
size_t size;
};
// 探测函数 - 设备树匹配时调用
static int eeprom_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct device *dev = &client->dev;
struct device_node *node = dev->of_node;
struct eeprom_data *data;
int ret;
// 分配设备私有数据
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
mutex_init(&data->lock);
// 从设备树读取属性
ret = of_property_read_u32(node, "pagesize", &data->page_size);
if (ret) {
// 使用默认值
data->page_size = 16;
dev_info(dev, "使用默认页大小: %d\n", data->page_size);
}
// 配置I2C适配器
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
dev_err(dev, "I2C适配器不支持I2C功能\n");
return -ENODEV;
}
// 读取EEPROM大小
if (of_device_is_compatible(node, "atmel,24c08"))
data->size = 1024; // 1KB
else if (of_device_is_compatible(node, "atmel,24c16"))
data->size = 2048; // 2KB
else
data->size = 4096; // 默认4KB
// 分配缓冲区
data->buffer = devm_kzalloc(dev, data->size, GFP_KERNEL);
if (!data->buffer)
return -ENOMEM;
// 注册设备
i2c_set_clientdata(client, data);
// 创建设备节点
device_create_file(dev, &dev_attr_size);
dev_info(dev, "EEPROM设备初始化成功,大小: %zu字节\n", data->size);
return 0;
}
// 移除函数
static int eeprom_remove(struct i2c_client *client)
{
struct eeprom_data *data = i2c_get_clientdata(client);
device_remove_file(&client->dev, &dev_attr_size);
dev_info(&client->dev, "EEPROM设备移除\n");
return 0;
}
// I2C设备驱动结构
static struct i2c_driver eeprom_driver = {
.driver = {
.name = "eeprom",
.owner = THIS_MODULE,
.of_match_table = eeprom_of_match,
},
.probe = eeprom_probe,
.remove = eeprom_remove,
.id_table = eeprom_ids, // 兼容旧式驱动匹配
};
module_i2c_driver(eeprom_driver);
2.2 文件操作接口
为EEPROM设备实现标准的文件操作接口。
// 文件操作实现
static ssize_t eeprom_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct eeprom_data *data = filp->private_data;
struct i2c_client *client = data->client;
int ret;
if (*f_pos >= data->size)
return 0;
// 计算实际可读字节数
if (*f_pos + count > data->size)
count = data->size - *f_pos;
mutex_lock(&data->lock);
// 构造I2C读取消息
struct i2c_msg msg[2];
u8 addr[2];
u16 mem_addr = *f_pos;
// 写入内存地址
addr[0] = (mem_addr >> 8) & 0xFF;
addr[1] = mem_addr & 0xFF;
msg[0].addr = client->addr;
msg[0].flags = 0; // 写操作
msg[0].len = 2;
msg[0].buf = addr;
// 读取数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读操作
msg[1].len = count;
msg[1].buf = data->buffer;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret < 0) {
mutex_unlock(&data->lock);
return ret;
}
// 拷贝到用户空间
if (copy_to_user(buf, data->buffer, count)) {
mutex_unlock(&data->lock);
return -EFAULT;
}
*f_pos += count;
mutex_unlock(&data->lock);
return count;
}
三、基于设备树的SPI驱动实现
3.1 SPI驱动框架
SPI驱动同样采用platform driver框架,但处理方式与I2C有所不同。
// SPI Flash驱动示例
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>
#include <linux/of.h>
#include <linux/of_device.h>
// 设备树匹配表
static const struct of_device_id w25q_of_match[] = {
{ .compatible = "winbond,w25q128" },
{ .compatible = "winbond,w25q256" },
{ .compatible = "macronix,mx25l3206e" },
{ /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, w25q_of_match);
// 设备私有数据结构
struct flash_data {
struct spi_device *spi;
struct mtd_info mtd;
struct mutex lock;
u8 *command;
u32 page_size;
u32 sector_size;
u32 total_size;
};
// 从设备树读取SPI配置
static int flash_parse_dt(struct device *dev, struct flash_data *data)
{
struct device_node *np = dev->of_node;
u32 max_freq = 0;
// 读取最大频率
if (of_property_read_u32(np, "spi-max-frequency", &max_freq)) {
dev_warn(dev, "未指定最大频率,使用默认值\n");
max_freq = 10000000; // 10MHz
}
data->spi->max_speed_hz = max_freq;
// 读取时钟模式
if (of_find_property(np, "spi-cpol", NULL))
data->spi->mode |= SPI_CPOL;
if (of_find_property(np, "spi-cpha", NULL))
data->spi->mode |= SPI_CPHA;
if (of_find_property(np, "spi-3wire", NULL))
data->spi->mode |= SPI_3WIRE;
return 0;
}
// 探测函数
static int flash_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
struct flash_data *data;
int ret;
// 分配私有数据
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->spi = spi;
mutex_init(&data->lock);
spi_set_drvdata(spi, data);
// 解析设备树配置
ret = flash_parse_dt(dev, data);
if (ret)
return ret;
// 配置SPI设备
spi->bits_per_word = 8;
ret = spi_setup(spi);
if (ret) {
dev_err(dev, "SPI配置失败: %d\n", ret);
return ret;
}
// 读取Flash ID
u8 cmd[4] = {0x9F, 0, 0, 0}; // 读取ID命令
u8 id[3];
struct spi_transfer t = {
.tx_buf = cmd,
.rx_buf = id,
.len = 4,
};
ret = spi_sync_transfer(spi, &t, 1);
if (ret) {
dev_err(dev, "读取Flash ID失败\n");
return ret;
}
// 根据ID确定容量
switch (id[1]) {
case 0x18: // W25Q128
data->total_size = 0x1000000; // 16MB
data->page_size = 256;
data->sector_size = 4096;
break;
case 0x19: // W25Q256
data->total_size = 0x2000000; // 32MB
data->page_size = 256;
data->sector_size = 4096;
break;
default:
dev_err(dev, "未知的Flash芯片\n");
return -ENODEV;
}
// 初始化MTD设备
data->mtd.name = devm_kasprintf(dev, GFP_KERNEL, "%s.%d",
dev_name(&spi->dev), spi->chip_select);
data->mtd.type = MTD_NORFLASH;
data->mtd.flags = MTD_CAP_NORFLASH;
data->mtd.size = data->total_size;
data->mtd.erasesize = data->sector_size;
data->mtd.writesize = data->page_size;
data->mtd.writebufsize = data->page_size;
data->mtd._read = flash_read;
data->mtd._write = flash_write;
data->mtd._erase = flash_erase;
data->mtd._sync = flash_sync;
data->mtd.priv = data;
// 注册MTD设备
ret = mtd_device_register(&data->mtd, NULL, 0);
if (ret) {
dev_err(dev, "MTD设备注册失败: %d\n", ret);
return ret;
}
dev_info(dev, "SPI Flash初始化成功,大小: %dMB\n",
data->total_size >> 20);
return 0;
}
四、中断线程化处理
中断线程化是提高Linux系统实时性的关键技术,特别适用于处理时间较长的中断服务程序。
4.1 传统中断与线程化中断对比
传统中断处理在硬中断上下文中进行,会阻塞其他中断,影响系统响应。线程化中断将中断处理程序移至内核线程中执行,允许调度和抢占。
// 线程化中断实现示例
#include <linux/interrupt.h>
#include <linux/sched.h>
// 设备私有数据
struct my_device {
struct device *dev;
int irq;
struct tasklet_struct tasklet; // 任务let
struct work_struct work; // 工作队列
struct workqueue_struct *wq; // 工作队列
};
// 传统中断处理程序
static irqreturn_t hardware_isr(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
// 快速处理硬件相关操作
u32 status = read_reg(dev, STATUS_REG);
if (status & DATA_READY_MASK) {
// 触发软件处理
tasklet_schedule(&dev->tasklet);
}
return IRQ_HANDLED;
}
// 任务let处理程序(软中断)
static void tasklet_handler(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
// 处理数据,在软中断上下文
process_data(dev);
}
// 工作队列处理程序
static void work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
// 复杂处理,在进程上下文
complex_processing(dev);
}
// 线程化中断处理程序
static irqreturn_t threaded_isr(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status;
// 第一部分:在硬中断上下文执行
status = read_reg(dev, STATUS_REG);
if (status & ERROR_MASK) {
// 错误处理必须立即执行
handle_error(dev);
return IRQ_HANDLED;
}
// 返回IRQ_WAKE_THREAD触发线程处理
return IRQ_WAKE_THREAD;
}
// 线程处理函数
static irqreturn_t threaded_isr_bottom(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
// 第二部分:在内核线程中执行
// 可以执行耗时操作,可以调度
process_large_data(dev);
sleepable_operations(dev);
return IRQ_HANDLED;
}
4.2 线程化中断的配置与注册
// 线程化中断注册
static int setup_threaded_irq(struct my_device *dev)
{
int ret;
struct device *pdev = dev->dev;
// 方法1:使用request_threaded_irq
ret = request_threaded_irq(dev->irq,
threaded_isr, // 顶半部
threaded_isr_bottom, // 底半部
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"my_device",
dev);
if (ret) {
dev_err(pdev, "注册线程化中断失败: %d\n", ret);
return ret;
}
// 方法2:通过设备树获取中断
dev->irq = irq_of_parse_and_map(pdev->of_node, 0);
if (dev->irq <= 0) {
dev_err(pdev, "无法从设备树获取中断\n");
return -ENODEV;
}
// 配置中断线程属性
ret = irq_set_irq_type(dev->irq, IRQF_TRIGGER_RISING);
if (ret) {
dev_err(pdev, "设置中断触发类型失败\n");
free_irq(dev->irq, dev);
return ret;
}
// 设置中断线程的调度策略
ret = sched_setscheduler_nocheck(current, SCHED_FIFO,
&(struct sched_param){.sched_priority = 50});
if (ret)
dev_warn(pdev, "无法设置实时调度策略\n");
return 0;
}
4.3 线程化中断的调试与优化
// 中断延迟测量
static void measure_irq_latency(struct my_device *dev)
{
static ktime_t irq_time, thread_time;
// 在顶半部记录时间
irq_time = ktime_get();
// 在线程处理开始时记录
thread_time = ktime_get();
s64 latency_ns = ktime_to_ns(ktime_sub(thread_time, irq_time));
// 统计延迟
static s64 max_latency = 0, total_latency = 0;
static u32 count = 0;
if (latency_ns > max_latency)
max_latency = latency_ns;
total_latency += latency_ns;
count++;
if (count % 1000 == 0) {
dev_info(dev->dev,
"中断延迟: 平均=%.2fµs, 最大=%.2fµs\n",
(total_latency / count) / 1000.0,
max_latency / 1000.0);
}
}
// 中断线程的CPU亲和性设置
static int set_irq_affinity(struct my_device *dev, int cpu)
{
cpumask_t mask;
cpumask_clear(&mask);
cpumask_set_cpu(cpu, &mask);
return irq_set_affinity(dev->irq, &mask);
}
五、完整示例:带中断线程化的I2C触摸屏驱动
// 触摸屏驱动完整示例
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/of_device.h>
struct touch_data {
struct i2c_client *client;
struct input_dev *input;
int irq;
struct work_struct work;
char phys[32];
};
// 从设备树读取触摸屏配置
static int touch_parse_dt(struct device *dev, struct touch_data *data)
{
struct device_node *np = dev->of_node;
u32 max_x, max_y;
if (of_property_read_u32(np, "touchscreen-size-x", &max_x))
max_x = 1024;
if (of_property_read_u32(np, "touchscreen-size-y", &max_y))
max_y = 768;
// 设置输入设备范围
input_set_abs_params(data->input, ABS_X, 0, max_x, 0, 0);
input_set_abs_params(data->input, ABS_Y, 0, max_y, 0, 0);
return 0;
}
// 工作队列处理程序
static void touch_work_handler(struct work_struct *work)
{
struct touch_data *data = container_of(work, struct touch_data, work);
struct i2c_client *client = data->client;
u8 buf[8];
int ret;
u16 x, y;
// 读取触摸数据
ret = i2c_smbus_read_i2c_block_data(client, 0x00, 8, buf);
if (ret < 0) {
dev_err(&client->dev, "读取触摸数据失败\n");
return;
}
// 解析坐标
x = (buf[1] << 8) | buf[2];
y = (buf[3] << 8) | buf[4];
// 报告触摸事件
input_report_abs(data->input, ABS_X, x);
input_report_abs(data->input, ABS_Y, y);
input_report_key(data->input, BTN_TOUCH, 1);
input_sync(data->input);
// 报告释放事件
input_report_key(data->input, BTN_TOUCH, 0);
input_sync(data->input);
}
// 线程化中断顶半部
static irqreturn_t touch_isr_top(int irq, void *dev_id)
{
struct touch_data *data = dev_id;
// 快速处理:调度工作队列
schedule_work(&data->work);
return IRQ_HANDLED;
}
// 驱动探测函数
static int touch_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct device *dev = &client->dev;
struct touch_data *data;
int ret;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 初始化工作队列
INIT_WORK(&data->work, touch_work_handler);
// 创建输入设备
data->input = devm_input_allocate_device(dev);
if (!data->input)
return -ENOMEM;
data->input->name = "I2C Touchscreen";
data->input->id.bustype = BUS_I2C;
// 设置设备物理地址
snprintf(data->phys, sizeof(data->phys), "%s/input0", dev_name(dev));
data->input->phys = data->phys;
// 配置输入设备
__set_bit(EV_ABS, data->input->evbit);
__set_bit(EV_KEY, data->input->evbit);
__set_bit(BTN_TOUCH, data->input->keybit);
// 解析设备树配置
touch_parse_dt(dev, data);
// 注册输入设备
ret = input_register_device(data->input);
if (ret) {
dev_err(dev, "注册输入设备失败\n");
return ret;
}
// 获取中断
data->irq = client->irq;
if (data->irq <= 0) {
dev_err(dev, "设备未指定中断\n");
return -ENODEV;
}
// 注册线程化中断
ret = devm_request_threaded_irq(dev, data->irq,
NULL, touch_isr_top,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"touch", data);
if (ret) {
dev_err(dev, "注册中断失败\n");
return ret;
}
dev_info(dev, "触摸屏驱动初始化成功\n");
return 0;
}
六、调试技巧与最佳实践
6.1 设备树调试
# 1. 查看解析后的设备树
cat /proc/device-tree/i2c@40005400/eeprom@50/compatible
# 2. 查看设备树节点
ls /proc/device-tree/
# 3. 查看I2C设备
i2cdetect -y 1
# 4. 查看SPI设备
cat /sys/bus/spi/devices/spi0.0/of_node/name
6.2 驱动调试
// 调试信息输出
#define DEBUG_LEVEL 1
#ifdef DEBUG_LEVEL
#define dbg_print(level, fmt, ...) \
do { \
if (level <= DEBUG_LEVEL) \
dev_dbg(dev, fmt, ##__VA_ARGS__); \
} while(0)
#else
#define dbg_print(level, fmt, ...)
#endif
// 在驱动中使用
dbg_print(1, "探测设备: 地址=0x%02x\n", client->addr);
6.3 中断统计
# 查看中断统计
cat /proc/interrupts
# 查看特定中断
cat /proc/irq/31/spurious
基于设备树的驱动开发代表了Linux内核驱动开发的现代化方向,它不仅提高了硬件的描述能力,也增强了系统的可维护性和可移植性。结合中断线程化技术,可以构建出既稳定又具备良好实时性的嵌入式系统。
随着Linux内核的不断演进,设备树和中断线程化等技术将继续发挥重要作用。掌握这些技术,将有助于开发出更加健壮、高效的嵌入式系统驱动。





