2955 字
15 分钟
Linux字符设备驱动框架

字符设备#

字符设备是 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有两种命令用于加载驱动模块:insmodmodprobe

  • insmod是最简单的模块加载命令,用于加载指定的.ko模块,比如insmod drv.ko。但insmod不能解决模块的依赖关系。
  • modprobe可以自动分析模块的依赖关系,然后加载所有的依赖模块到Kernel中。

模块卸载则使用rmmodmodprobe -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这个设备需要实现哪些功能:

  1. chrtest能够进行打开和关闭操作。这是最基本的要求
  2. chrtest进行读写操作。假设设备控制着一段缓冲区(内存),应用需要通过readwrite对缓冲区进行读写操作,因此需要实现file_operations中的readwrite两个函数。

修改代码:

/* 打开设备 */
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应当按照顺序被使用:

  1. 初始化
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;
}
  1. 注册字符设备:使用 cdev_add 函数将其注册到内核。这样,内核就能够在收到对该设备的请求时,调用对应的文件操作函数。
int ret = cdev_add(&chrdev_cdev, dev_num, 1);
if (ret < 0) {
printk("Failed to add cdev\n");
return ret;
}
  1. 注销字符设备
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#

  1. 定义 fops,提供 read/write/open/close/ioctl 操作
  2. 定义 cdev,并绑定 fops
  3. 注册 cdev,并分配 major/minor 设备号
  4. 创建 class,供 udev 识别
  5. 创建 device,在 /dev/ 下自动生成设备文件

完整流程#

  1. 定义设备信息,用于注册设备至kernel
static dev_t chrdev_devno;
  1. 定义文件操作结构体,用于封装用于操作外设的功能函数
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};
  1. 实现设备操作函数
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;
}
  1. 初始化和清理
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);
  1. 创建节点。如果应用程序要想使用设备,还必须创建字符设备节点:
mknod /dev/chrdev c 249 0

然后应用程序才能将设备号和具体的设备连接起来并使用。

如果初始化中创建了deviceclass类,会自动生成节点。

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");
Linux字符设备驱动框架
https://arkmostima.pages.dev/posts/linux字符设备驱动框架/
作者
Aki
发布于
2025-03-14
许可协议
CC BY-NC-SA 4.0