引言
在Linux操作系统中,设备驱动程序充当硬件和软件之间的桥梁。字符设备驱动是一种特殊类型的驱动,它允许用户以字节流的形式访问硬件设备。这些设备包括键盘、鼠标、串口等。在本博客中,我们将探讨Linux字符设备驱动的基础知识,构建过程,以及如何管理这些设备。
第1章:Linux设备驱动概述
在Linux中,设备通常分为字符设备、块设备和网络设备。字符设备允许按字节进行数据传输,而块设备则基于数据块操作。网络设备则用于处理网络通信。
第2章:字符设备基础
字符设备是可以按字节流进行读写的设备。它们通常不支持随机访问,数据传输必须按顺序进行。在这一章节中,我们将详细讨论字符设备的定义和特点,以及它们在Linux系统中的表示方法。
第3章:字符设备驱动程序结构
在这一章节中,我们将探讨字符设备驱动程序的基本结构。这包括设备文件的创建和注册,以及cdev
结构体的重要性。cdev
结构体是Linux内核用来表示字符设备的核心数据结构,它包含了设备的主要信息和操作函数接口。
在Linux内核中,cdev
结构体是用来表示字符设备的关键数据结构。它包含了字符设备的主要信息和操作函数接口。以下是cdev
结构体的一些主要成员及其作用:
struct cdev {
struct kobject kobj; // 内嵌的内核对象
struct module *owner; // 指向拥有该字符设备的内核模块的指针
const struct file_operations *ops; // 指向文件操作函数的指针,这些函数定义了设备的行为
struct list_head list; // 用于将所有已注册的字符设备链接成一个链表
dev_t dev; // 设备号,由主设备号和次设备号构成
unsigned int count; // 隶属于同一主设备号的次设备号的个数
};
kobj
:用于内核对象模型,提供设备模型与sysfs的接口。owner
:通常设置为THIS_MODULE
,确保在模块被卸载时,设备驱动不会被使用。ops
:指向file_operations
结构,定义了字符设备的操作方法,如open
、read
、write
等。list
:用于将设备添加到内核的设备链表中。dev
:设备号,用于唯一标识设备。count
:表示与该设备关联的次设备号的数量。
如何将一个I2C驱动 描述为字符设备驱动 运用:
...
#define MYMA 301 //主设备号
#define COUNT 1
typedef struct {
struct cdev cdev;
struct i2c_client *cli;
struct class *cls;
} i2cDev_data_t;
int myprobe(struct i2c_client *cli, const struct i2c_device_id *id)
{
struct device_node *np;
struct device *dev;
int ret;
i2cDev_data_t *data;
static int mi = 0;
dev_t devid;
devid = MKDEV(MYMA, mi);
ret = register_chrdev_region(devid, COUNT, cli->name);
if (ret < 0)
goto err0;
data = kzalloc(sizeof(*data), GFP_KERNEL);
if (NULL == data) {
ret = -ENOMEM;
goto err1;
}
cdev_init(&data->cdev, &fops);
data->cdev.owner = THIS_MODULE;
ret = cdev_add(&data->cdev, devid, COUNT);
if (ret < 0)
goto err2;
data->cls = class_create(THIS_MODULE, cli->name);
device_create(data->cls, NULL, devid, NULL, "%s.%d", cli->name, mi++);
data->cli = cli;
i2c_set_clientdata(cli, data);
dev = &cli->dev;
if (!dev)
return -ENODEV;
np = dev->of_node;
return 0;
err2:
kfree(data);
err1:
unregister_chrdev_region(devid, COUNT);
err0:
return ret;
}
...
各API作用如下:
1.MKDEV: #define MKDEV(major, minor) (((major) << MINORBITS) | (minor))
MKDEV
宏用于将主设备号(major number)和次设备号(minor number)组合成一个dev_t
类型的设备编号。这个设备编号通常用于设备文件的创建和设备驱动程序的注册。
2.register_chrdev_region:
在Linux内核编程中,register_chrdev_region
函数用于静态注册一组字符设备编号。如果您已经知道要使用的主设备号和次设备号,可以使用此函数进行注册。以下是该函数的原型和简要说明:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first
:要注册的第一个设备编号,包括主设备号和起始次设备号。count
:要注册的设备数量,即次设备号的个数。name
:与设备编号关联的设备名称,这个名称会出现在/proc/devices
目录下。
当调用register_chrdev_region函数时,如果指定的设备编号范围已经被占用,函数将返回一个负值错误代码。如果注册成功,函数将返回0。在注册设备编号之前,您应该检查/proc/devices以确保所需的设备号没有被占用
3.cdev_init : 用于初始化一个已经分配的cdev
结构体。这个函数将file_operations
结构体与cdev
结构体关联起来,为设备提供必要的文件操作方法。
4.cdev_add:
用于将一个cdev
结构体添加到内核中,使得相应的字符设备立即可用。
5.class_create:
用于创建一个新的设备类,这个类将出现在/sys/class
目录下,成功调用class_create
后,您可以在/sys/class/<name>
下找到新创建的设备类。
6.device_create: 用于在已创建的设备类下创建一个设备,并在/dev
目录下自动创建相应的设备文件节点。
第4章:文件操作接口
字符设备驱动程序通常需要实现一系列文件操作接口,如open
、release
(close
)、read
、write
等。这些接口允许用户空间的程序通过设备文件与设备进行交互。我们将详细讨论这些接口的实现和它们在设备驱动中的作用。
ssize_t chr_write(struct file *fl, const char __user *buf, size_t len,
loff_t *off)
{
struct i2c_msg msg;
struct cdev *cdev = fl->f_path.dentry->d_inode->i_cdev;
i2cDev_data_t *data = container_of(cdev, i2cDev_data_t, cdev);
char *kbuf = NULL;
int ret =0;
char addr;
struct i2c_client *cli = data->cli;
kbuf = kzalloc(len+1, GFP_KERNEL);
ret = copy_from_user(kbuf, buf, len);
addr = atoi(kbuf);
msgs.addr = cli->addr;
msgs.flags = 0;
msgs.len = 1;
msgs.buf = &addr;
ret = i2c_transfer(cli->adapter, &msgs, 1);
if (ret < 0) {
chr_DEBUG("%s error %d\n", __func__, ret);
}
kfree(kbuf);
return len;
}
static long chr_ioctl(struct file *fl, unsigned int cmd, unsigned long arg)
{
printk("chr_ioctl");
return 0;
}
int chr_open(struct inode *ind, struct file *fl)
{
printk("chr_open");
return 0;
}
ssize_t chr_read(struct file *fl, char __user *buf, size_t len, loff_t *off)
{
chr_DEBUG("chr_read audioValue=%d\n", audioValue);
return audioValue;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.read = chr_read,
.open = chr_open,
.write = chr_write,
.unlocked_ioctl = chr_ioctl,
};
1.container_of:
container_of
宏是一个非常有用的工具,它允许您通过结构体的一个成员的地址来获取整个结构体的地址。这在驱动开发和内核编程中非常常见,尤其是当您只有对结构体中某个成员的引用时。以下是container_of
宏的一般用法:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) );})
tr
:指向结构体中成员的指针。type
:结构体的类型。member
:结构体中的成员名称
2.struct file_operations :
struct file_operations {
struct module *owner; // 指向模块所有者的指针
loff_t (*llseek) (struct file *, loff_t, int); // 改变文件读写位置的方法
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 从设备读取数据的方法
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 向设备写入数据的方法
int (*open) (struct inode *, struct file *); // 打开设备的方法
int (*release) (struct inode *, struct file *); // 释放设备的方法
// ... 其他操作方法
};
这些方法对应于用户空间程序对设备文件执行的系统调用。例如,当用户程序调用`read()`系统调用时,内核会调用file_operations中的read方法来从设备读取数据
实际运用将一个I2C驱动添加字符设备接口
cpp
dtsi:
&i2c5 {
status = "okay";
chr_drive: chr_drive@44 {
status = "okay";
compatible = "chr_drive";
reg = <0x44>;
};
};
chr_driver.c
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define MYMA 301
#define COUNT 1
static unsigned char audioValue = 43;
typedef struct {
struct cdev cdev;
struct i2c_client *cli;
struct class *cls;
} i2cDev_data_t;
static int atoi(const char *str)
{
int result = 0;
int sign = 0;
if (str == NULL) {
return -1;
}
while (*str == ' ' || *str == '\t' || *str == '\n')
++str;
if (*str == '-') {
sign = 1;
++str;
} else if (*str == '+') {
++str;
}
while (*str >= '0' && *str <= '9') {
result = result * 10 + *str - '0';
++str;
}
if (sign == 1)
return -result;
else
return result;
}
ssize_t chr_write(struct file *fl, const char __user *buf, size_t len,
loff_t *off)
{
struct i2c_msg msg;
struct cdev *cdev = fl->f_path.dentry->d_inode->i_cdev;
i2cDev_data_t *data = container_of(cdev, i2cDev_data_t, cdev);
char *kbuf = NULL;
int ret =0;
char addr;
struct i2c_client *cli = data->cli;
kbuf = kzalloc(len+1, GFP_KERNEL);
ret = copy_from_user(kbuf, buf, len);
addr = atoi(kbuf);
msg.addr = cli->addr;
msg.flags = 0;
msg.len = 1;
msg.buf = &addr;
printk("chr_write %d",addr);
ret = i2c_transfer(cli->adapter, &msg, 1);
if (ret < 0) {
printk("%s error %d\n", __func__, ret);
}
kfree(kbuf);
return len;
}
static long chr_ioctl(struct file *fl, unsigned int cmd, unsigned long arg)
{
printk("chr_ioctl");
return 0;
}
int chr_open(struct inode *ind, struct file *fl)
{
printk("chr_open");
return 0;
}
ssize_t chr_read(struct file *fl, char __user *buf, size_t len, loff_t *off)
{
printk("chr_read");
return 0;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.read = chr_read,
.open = chr_open,
.write = chr_write,
.unlocked_ioctl = chr_ioctl,
};
int myprobe(struct i2c_client *cli, const struct i2c_device_id *id)
{
struct device_node *np;
struct device *dev;
int ret;
i2cDev_data_t *data;
static int mi = 0;
dev_t devid;
devid = MKDEV(MYMA, mi);
ret = register_chrdev_region(devid, COUNT, cli->name);
if (ret < 0)
goto err0;
data = kzalloc(sizeof(*data), GFP_KERNEL);
if (NULL == data) {
ret = -ENOMEM;
goto err1;
}
cdev_init(&data->cdev, &fops);
data->cdev.owner = THIS_MODULE;
ret = cdev_add(&data->cdev, devid, COUNT);
if (ret < 0)
goto err2;
data->cls = class_create(THIS_MODULE, cli->name);
device_create(data->cls, NULL, devid, NULL, "%s.%d", cli->name, mi++);
data->cli = cli;
i2c_set_clientdata(cli, data);
dev = &cli->dev;
if (!dev)
return -ENODEV;
np = dev->of_node;
return 0;
err2:
kfree(data);
err1:
unregister_chrdev_region(devid, COUNT);
err0:
return ret;
}
int myremove(struct i2c_client *cli)
{
i2cDev_data_t *data = i2c_get_clientdata(cli);
device_destroy(data->cls, data->cdev.dev);
class_destroy(data->cls);
cdev_del(&data->cdev);
unregister_chrdev_region(data->cdev.dev, COUNT);
kfree(data);
return 0;
}
struct i2c_device_id ids[] = {
{ "chr_drive", 0 },
{},
};
MODULE_DEVICE_TABLE(i2c, ids);
static const struct of_device_id chr_of_match[] = {
{ .compatible = "chr_drive" },
{},
};
struct i2c_driver mydrv = {
.probe = myprobe,
.remove = myremove,
.driver = {
.name = "chr_drive",
.of_match_table = chr_of_match,
.owner = THIS_MODULE,
},
.id_table = ids,
};
module_i2c_driver(mydrv);
MODULE_LICENSE("GPL");
注册成功会在dev下生成chr_drive节点