字符设备
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
字符设备驱动工作流程
加载/卸载驱动模块
Linux的驱动有两种工作方式:
- 驱动编译进kernel,kernel启动时自动运行驱动程序
- 驱动编译为模块(
.ko),kernel启动后调用insmod加载驱动模块
模块有加载和卸载两种操作:
module_init(xxx_init); //注册模块加载module_exit(xxx_exit); //注册模块卸载module_init用于向Kernel注册一个模块加载函数,当insmod时,该函数就会被Kernel调用。
字符设备驱动的模块加载和卸载模板如下:
/* 驱动入口 */static int __init xxx_init(void){ /* 入口函数具体内容 */ return 0;}
/* 驱动出口 */static void __exit xxx_exit(void){ /* 出口函数具体内容 */}
/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit);驱动编译完成后,Kernel有两种命令用于加载驱动模块:insmod和modprobe:
insmod是最简单的模块加载命令,用于加载指定的.ko模块,比如insmod drv.ko。但insmod不能解决模块的依赖关系。modprobe可以自动分析模块的依赖关系,然后加载所有的依赖模块到Kernel中。
模块卸载则使用rmmod或modprobe -r。
推荐:加载使用modprobe,卸载使用rmmod。
注册与注销
模块加载成功后需要注册字符设备,卸载模块时也需要注销相关字符设备:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operation *fops)static inline void unregister_chrdev(unsigned int major, const char *name)register_chrdev用于注册字符设备,形参有:
major:主设备号name:设备名fops:结构体file_operations类型指针,指向操作函数集合变量
unregister_chrdev用于注册字符设备,形参有:
major:主设备号name:设备名
一半字符设备的注册在入口函数中进行,注销在出口函数中执行:
static struct file_operations test_fops;
static int __init xxx_init(void){ int retvalue = 0;
/* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_ops); /* 主设备号200,设备名"chrtest" */ if (retvalue < 0) { error_handler(); }
return 0;}
static void __exit xxx_exit(void){ /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest");}
/* 将个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit);终端中car /proc/devices可查看当前已被使用的设备号
实现设备的具体操作函数
在对test_fops进行初始化之前,首先要分析需求,即chrtest这个设备需要实现哪些功能:
- 对
chrtest能够进行打开和关闭操作。这是最基本的要求 - 对
chrtest进行读写操作。假设设备控制着一段缓冲区(内存),应用需要通过read和write对缓冲区进行读写操作,因此需要实现file_operations中的read和write两个函数。
修改代码:
/* 打开设备 */static int chrtest_open(struct inode *inode, struct file *filp){ return 0;}
/* 从设备读取 */static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){ return 0;}
/* 向设备写数据 */static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){ return 0;}
/* 关闭/释放设备 */static int chrtest_release(struct inode *inode, struct file *filp){ return 0;}
/* 初始化file_operations结构体 */static struct file_operations test_fops = { .owner = THIS_MODULE, .open = chrtest_open, .read = chrtest_read, .write = chrtest_write, .release = chrtest_release,};
static int __init xxx_init(void){ int retvalue = 0;
/* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_ops); /* 主设备号200,设备名"chrtest" */ if (retvalue < 0) { error_handler(); }
return 0;}
static void __exit xxx_exit(void){ /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest");}
/* 将个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit);这里的函数形参需要使用Linux驱动框架所规定的标准格式:
struct inode *inode:指向设备文件的 索引节点,描述了文件的元数据(如权限、文件类型等)struct file *filp:表示 已打开的文件,包含文件的当前状态、访问模式等信息。char __user *buf:用户空间的缓冲区,内核需要将数据拷贝到这个缓冲区size_t cnt:写入的字节数,表示用户请求写入的长度loff_t *offt:文件偏移量,用于支持文件的随机访问
添加LICENSE和作者信息
LICENSE不添加编译会报错:
MODULE_LICENSE();设备号分配
静态分配
WIP
动态分配
推荐使用动态分配设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)dev:保存申请到的设备号baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。count:要申请的设备号数量name:设备名
释放设备号函数:
void unregister_chrdev_region(dev_t from, unsigned count)from:要释放的设备号count:从from开始要释放的设备号数量
cdev
cdev 是 Linux 内核中用于表示字符设备的一个结构体。它是 Linux 字符设备驱动中非常重要的一部分,主要用于管理字符设备的注册和文件操作的绑定。当注册一个字符设备时,cdev 会帮助内核处理与设备相关的操作。
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; // 设备的文件操作结构 struct list_head list; // 链接到系统设备列表 dev_t dev; // 设备号};cdev 的主要任务是将文件操作函数(例如:open、read、write 等)和设备进行关联。当设备被打开、读写或释放时,内核会通过 cdev 结构体找到相应的操作函数并调用它们。
cdev应当按照顺序被使用:
- 初始化
struct cdev chrdev_cdev;cdev_init(&chrdev_cdev, &fops);这个操作可以通过cdev_alloc()来一次性完成:
struct cdev *cdev_alloc(void){ struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); if (p) { INIT_LIST_HEAD(&p->list); kobject_init(&p->kobj, &ktype_cdev_dynamic); } return p;}- 注册字符设备:使用
cdev_add函数将其注册到内核。这样,内核就能够在收到对该设备的请求时,调用对应的文件操作函数。
int ret = cdev_add(&chrdev_cdev, dev_num, 1);if (ret < 0) { printk("Failed to add cdev\n"); return ret;}- 注销字符设备
cdev_del(&chrdev_cdev);其他
驱动中还会用到copy_to_user函数:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)该函数用于将内核空间中的数据复制到用户空间。
测试APP
C库文件操作
open()
int open(const char *pathname, int flags)pathname:要打开的设备或者文件名flags:文件打开模式,以下三种模式必选其一:O_RDONLY只读模式O_WRONLY只写模式O_RDWR读写模式
如果文件打开成功的话返回文件的文件描述符.
read()
ssize_t read(int fd, void *buf, size_t count)fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符buf:数据读取到此 buf 中count:要读取的数据长度,也就是字节数
读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
write()
ssize_t write(int fd, const void *buf, size_t count);fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符buf:要写入的数据count:要写入的数据长度,也就是字节数
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回 负值,表示写入失败
close()
int close(int fd);0 表示关闭成功,负值表示关闭失败。
fops、cdev、class、device
- 定义 fops,提供 read/write/open/close/ioctl 操作
- 定义 cdev,并绑定 fops
- 注册 cdev,并分配 major/minor 设备号
- 创建 class,供 udev 识别
- 创建 device,在
/dev/下自动生成设备文件
完整流程
- 定义设备信息,用于注册设备至kernel
static dev_t chrdev_devno;- 定义文件操作结构体,用于封装用于操作外设的功能函数
static struct file_operations chrdev_fops = { .owner = THIS_MODULE, .open = chrdev_open, .read = chrdev_read, .write = chrdev_write, .release = chrdev_release,};- 实现设备操作函数
static int chrdev_open(struct inode *inode, struct file *filp) { return 0;}
static ssize_t chrdev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0;}
static ssize_t chrdev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { return 0;}
static int chrdev_release(struct inode *inode, struct file *filp) { return 0;}- 初始化和清理
static int __init chrdev_init(void) { int ret; ret = alloc_chrdev_region(&chrdev_devno, 0, 1, "chrdevbase"); //动态分配设备号 if (ret < 0) { printk("Failed to register device\n"); return ret; } cdev_init(&chrdev_cdev, &chrdev_fops); //注册cdev ret = cdev_add(&chrdev_cdev, chrdev_devno, 1); //添加cdev if (ret < 0) { unregister_chrdev_region(chrdev_devno, 1); printk("Failed to add cdev\n"); return ret; } return 0;}
static void __exit chrdev_exit(void) { cdev_del(&chrdev_cdev); //销毁cdev unregister_chrdev_region(chrdev_devno, 1); //注销设备号}
module_init(hello_init);module_exit(hello_exit);- 创建节点。如果应用程序要想使用设备,还必须创建字符设备节点:
mknod /dev/chrdev c 249 0然后应用程序才能将设备号和具体的设备连接起来并使用。
如果初始化中创建了device和class类,会自动生成节点。
LED点灯示例
/* **************Includes************** */#include "linux/fs.h"#include "linux/printk.h"#include <linux/types.h>#include <linux/kernel.h>#include <linux/delay.h>#include <linux/ide.h>#include <linux/init.h>#include <linux/module.h>#include <linux/errno.h>#include <linux/gpio.h>#include <linux/cdev.h>#include <linux/device.h>#include <asm/mach/map.h>#include <asm/uaccess.h>#include <asm/io.h>
/* **************Defines************** */#define LED_DEVICE_CNT 1#define LED_DEVICE_NAME "led_dev"#define LEDON 1#define LEDOFF 0
/* 寄存器物理地址 */#define CCM_CCGR1_BASE (0x020C406C)#define SW_MUX_GPIO1_IO03_BASE (0x020E0068)#define SW_PAD_GPIO1_IO03_BASE (0x020E02F4)#define GPIO1_DR_BASE (0x0209C000)#define GPIO1_GDIR_BASE (0x0209C004)
/* 寄存器虚拟地址 */static void __iomem *IMX6U_CCM_CCGR1;static void __iomem *SW_MUX_GPIO1_IO03;static void __iomem *SW_PAD_GPIO1_IO03;static void __iomem *GPIO1_DR;static void __iomem *GPIO1_GDIR;
/* 设备结构体 */struct led_dev{ dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor;};
/* 声明实例 */struct led_dev led;
/* LED切换开关函数 */void led_switch(u8 status){ u32 val = 0;
if (status == LEDON){ val = readl(GPIO1_DR); val &= ~(1 << 3); writel(val, GPIO1_DR); }else if (status == LEDOFF){ val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); }}
/*********** fops结构体函数填充 **************/static int led_open(struct inode *inode, struct file *filp){ filp->private_data = &led; /* 设置私有变量 */ return 0;}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){ return 0;}
static ssize_t led_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){ int retvalue; unsigned char databuf[1]; unsigned char led_status;
retvalue = copy_from_user(databuf, buf, cnt); if (retvalue < 0){ printk("copy from user failed\r\n"); return -EFAULT; }
led_status = databuf[0];
switch (led_status) { case LEDON: led_switch(LEDON); break; case LEDOFF: led_switch(LEDOFF); break; }
return 0;}
static int led_release(struct inode *inode, struct file *filp){ return 0;}
/* fops结构体 */static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release,};
/* 设备初始化函数 */static int __init led_init(void){ int retvalue = 0; u32 val = 0;
/* 获取寄存器虚拟地址 */ IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/* 使能时钟 */ val =readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); val |= (3 << 26); writel(val, IMX6U_CCM_CCGR1);
/* 设置 GPIO1_IO03 复用 */ writel(5, SW_MUX_GPIO1_IO03);
/* 配置 GPIO1_IO03 */ writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 设置 GPIO1_IO03 为输出 */ val = readl(GPIO1_GDIR); val &= ~(1 << 3); val |= (1 << 3); writel(val, GPIO1_GDIR);
/* 注册设备 */ if (led.major){ /* 如果事先指定了主设备号 */ led.devid = MKDEV(led.major, 0); register_chrdev_region(led.devid, LED_DEVICE_CNT, LED_DEVICE_NAME); }else { /* 动态分配 */ alloc_chrdev_region(&led.devid, 0, LED_DEVICE_CNT, LED_DEVICE_NAME); led.major = MAJOR(led.devid); led.minor = MINOR(led.devid); } printk("led register ok: major= %d, minor = %d\r\n", led.major, led.minor);
/* 初始化Cdev */ led.cdev.owner = THIS_MODULE; cdev_init(&led.cdev, &led_fops);
/* 向内核注册cdev */ retvalue = cdev_add(&led.cdev, led.devid, LED_DEVICE_CNT); if (retvalue < 0){ printk("cdev add failed\r\n"); return retvalue; }
/* 创建设备类 */ led.class = class_create(THIS_MODULE, LED_DEVICE_NAME); if (IS_ERR(led.class)){ printk("class create failed\r\n"); return PTR_ERR(led.class); }
/* 创建设备实例 */ led.device = device_create(led.class, NULL, led.devid, NULL, LED_DEVICE_NAME); if (IS_ERR(led.device)){ printk("device create failed\r\n"); return PTR_ERR(led.device); }
return 0;}
/* 设备出口函数 */static void __exit led_exit(void){ /* 释放寄存器映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR);
/* 销毁cdev,注销设备号 */ cdev_del(&led.cdev); unregister_chrdev_region(led.devid, LED_DEVICE_CNT);
/* 销毁设备类和设备实例 */ device_destroy(led.class, led.devid); class_destroy(led.class);}
module_init(led_init);module_exit(led_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("aki");