在嵌入式Linux系统开发中,设备驱动是连接硬件和操作系统的关键桥梁。RK3588作为一款高性能的ARM处理器,其Android12系统底层同样基于Linux内核。字符设备驱动是最基础也是最常用的驱动类型之一,它以字节流的形式进行数据传输,常见的应用包括串口、键盘、鼠标等外设。
字符设备驱动的核心在于实现file_operations结构体中的各种操作函数,如open、read、write等。与块设备不同,字符设备不需要缓冲区,数据直接以字节流形式传输,这使得它特别适合那些不需要固定大小数据块的设备。
提示:在开始开发前,建议先熟悉Linux内核模块的基本开发流程,包括模块的编译、加载和卸载等操作。
Linux内核通过设备号来唯一标识一个设备。设备号由主设备号和次设备号组成:
内核提供了以下宏来操作设备号:
c复制#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
在RK3588平台上,设备号的分配可以通过静态或动态两种方式:
注意:在Android系统中,设备号的分配需要特别注意与现有设备的冲突问题。
字符设备驱动的核心数据结构包括:
c复制struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
其中最重要的成员是ops,它指向file_operations结构体,定义了设备的各种操作函数。
一个完整的字符设备驱动开发通常包含以下步骤:
静态分配示例:
c复制dev_t devno;
int ret;
devno = MKDEV(MAJOR_NUM, MINOR_NUM);
ret = register_chrdev_region(devno, 1, "my_char_dev");
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev region\n");
return ret;
}
动态分配示例:
c复制dev_t devno;
int ret;
ret = alloc_chrdev_region(&devno, 0, 1, "my_char_dev");
if (ret < 0) {
printk(KERN_ERR "Failed to alloc chrdev region\n");
return ret;
}
c复制struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, devno, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(devno, 1);
return ret;
}
file_operations结构体定义了设备的各种操作函数,最基本的包括:
c复制static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
c复制static int my_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "Device released\n");
return 0;
}
c复制static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
int ret;
char data[] = "Hello from kernel!\n";
ret = copy_to_user(buf, data, sizeof(data));
if (ret) {
return -EFAULT;
}
return sizeof(data);
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
char data[256];
int ret;
if (count > sizeof(data)) {
return -EINVAL;
}
ret = copy_from_user(data, buf, count);
if (ret) {
return -EFAULT;
}
printk(KERN_INFO "Received data: %s\n", data);
return count;
}
在驱动加载后,需要在/dev目录下创建设备节点:
bash复制mknod /dev/my_char_dev c 主设备号 次设备号
在Android系统中,通常会在init.rc或ueventd.rc中配置自动创建设备节点。
在Android系统中,设备节点的权限通常在ueventd.rc中配置:
code复制/dev/my_char_dev 0666 root root
ioctl用于实现设备特定的控制命令:
c复制#define MY_IOCTL_CMD _IOR('k', 1, int)
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case MY_IOCTL_CMD:
printk(KERN_INFO "Received ioctl command\n");
break;
default:
return -ENOTTY;
}
return 0;
}
对于需要实现异步I/O的设备,需要实现poll操作:
c复制static unsigned int my_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(filp, &my_wait_queue, wait);
if (data_available) {
mask |= POLLIN | POLLRDNORM;
}
return mask;
}
对于简单的字符设备,可以使用misc设备框架简化开发:
c复制static struct miscdevice my_misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "my_misc_dev",
.fops = &my_fops,
};
static int __init my_init(void)
{
return misc_register(&my_misc_dev);
}
misc设备会自动分配主设备号10,并自动创建设备节点。
设备节点不存在:
权限问题:
驱动加载失败:
在RK3588平台上开发字符设备驱动时,需要注意:
在实际项目中,我曾遇到一个性能问题:频繁的小数据量传输导致系统开销过大。通过实现一个环形缓冲区,将多个小数据包合并传输,性能提升了约40%。