在 Linux 中,编写 I2C 设备驱动时,我们通常需要向用户空间提供一个访问接口。最常见的方式之一就是注册为一个字符设备 (Character Device)。
这样,用户应用程序就可以通过标准的 open, read, write, ioctl 系统调用来操作这个 I2C 设备。
1. 框架结构
结合 I2C 驱动框架和字符设备驱动框架:
- I2C 驱动部分:负责设备匹配 (
probe)、硬件通信 (i2c_transfer)。 - 字符驱动部分:在
probe中创建字符设备 (cdev),实现file_operations。
2. 核心代码模板
2.1 定义 file_operations
首先定义用户空间操作的实现函数。
c
static int my_dev_open(struct inode *inode, struct file *file)
{
// 这里的 private_data 通常用来存储 i2c_client 指针
// 为了方便在 read/write 中使用
return 0;
}
static ssize_t my_dev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
// 1. 获取 i2c_client
// 2. 调用 i2c_transfer 或 i2c_smbus_read... 读取硬件数据
// 3. copy_to_user 将数据传给用户
return 0;
}
static ssize_t my_dev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
// 1. copy_from_user 获取用户数据
// 2. 调用 i2c_transfer 写入硬件
return count;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_dev_open,
.read = my_dev_read,
.write = my_dev_write,
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2.2 在 Probe 中注册字符设备
推荐使用 misc_register(杂项设备注册),它自动处理主设备号(10)和设备节点创建,比传统的 register_chrdev_region + cdev_add + class_create + device_create 更简洁。
c
#include <linux/miscdevice.h>
// 定义私有数据结构
struct my_i2c_data {
struct i2c_client *client;
struct miscdevice miscdev;
};
static int my_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct my_i2c_data *data;
int ret;
// 分配内存
data = devm_kzalloc(&client->dev, sizeof(struct my_i2c_data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data); // 保存指针以便 remove 使用
// 填充 miscdevice 结构
data->miscdev.minor = MISC_DYNAMIC_MINOR; // 动态分配次设备号
data->miscdev.name = "my_i2c_sensor"; // /dev/my_i2c_sensor
data->miscdev.fops = &my_fops;
// 注册杂项设备
ret = misc_register(&data->miscdev);
if (ret)
return ret;
dev_info(&client->dev, "Registered /dev/%s\n", data->miscdev.name);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2.3 在 Remove 中注销
c
static int my_remove(struct i2c_client *client)
{
struct my_i2c_data *data = i2c_get_clientdata(client);
misc_deregister(&data->miscdev);
return 0;
}1
2
3
4
5
6
7
2
3
4
5
6
7
2.4 关联 file 和 client
在 open 函数中,我们需要找到对应的 i2c_client。由于 misc_register 的机制,file->private_data 默认指向 miscdevice 结构。我们可以利用 container_of 宏找回我们的 my_i2c_data 结构。
c
static int my_dev_open(struct inode *inode, struct file *file)
{
struct miscdevice *misc = file->private_data;
struct my_i2c_data *data = container_of(misc, struct my_i2c_data, miscdev);
// 将 data 指针保存在 file->private_data 中,覆盖原来的 misc 指针
// 这样 read/write 中就可以直接使用了
file->private_data = data;
return 0;
}
static ssize_t my_dev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
struct my_i2c_data *data = file->private_data;
struct i2c_client *client = data->client;
// 现在可以使用 client 进行 I2C 通信了
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3. 优缺点
- 优点:
- 接口自由灵活,可以自定义协议。
- 用户空间编程简单,像操作文件一样。
- 缺点:
- 需要自己定义数据格式(read/write 的字节含义)。
- 如果不使用标准的子系统(如 IIO, Input, Hwmon),应用程序的可移植性较差(只能针对这个特定的驱动编写)。
4. 替代方案
对于特定类型的 I2C 设备,内核通常提供了标准子系统,建议优先使用:
- 传感器 (加速度、光强等) -> IIO Subsystem
- 输入设备 (触摸屏、键盘) -> Input Subsystem
- 硬件监控 (温度、电压、风扇) -> Hwmon Subsystem
- RTC -> RTC Subsystem
只有当设备不属于上述任何标准类型,或者需要非常特殊的控制时,才编写原始的字符设备驱动。