Linux内核字符设备驱动开发详解

1. 字符设备驱动基础

1.1 设备驱动定义与特点

定义:能够使硬件工作的软件

驱动两大核心

  1. 必须操作硬件
  2. 必须给用户应用提供操作接口,根据操作接口访问硬件(服务)
    • 驱动最终必须服务于应用程序

1.2 Linux内核设备驱动分类

驱动类型访问方式示例设备
字符设备驱动按字节流访问LED、蜂鸣器、按键、LCD显示屏、触摸屏、UART接口外设、各种传感器、摄像头、声卡、显卡
块设备驱动按数据块访问(一次访问512字节)硬盘、U盘、TF卡、SD卡、EMMC、Norflash、Nandflash
网络设备驱动网络通信有线网卡、无线网卡

1.3 Linux系统理念:一切皆文件

理念:任何硬件外设在Linux系统中都是以文件的形式存在,访问文件即是访问硬件

结论

  • 字符设备文件:代表字符设备硬件
  • 块设备文件:代表块设备硬件
  • 网络设备:无设备文件,通过socket套接字访问

1.4 字符设备文件属性特点

查看字符设备文件示例:
ls /dev/ttySAC* -lh

输出结果:

crw-rw---- 204,  64  /dev/ttySAC0
crw-rw---- 204,  65  /dev/ttySAC1  
crw-rw---- 204,  66  /dev/ttySAC2
crw-rw---- 204,  67  /dev/ttySAC3

属性说明

  • c:表示字符设备文件
  • 204:主设备号
  • 64~67:次设备号
  • ttySAC0~3:UART串口硬件对应的字符设备文件名
访问字符设备文件示例:
// 1. 打开串口
int fd = open("/dev/ttySAC0", O_RDWR);

// 2. 向串口发送数据
write(fd, "hello,world\n", strlen("hello,world\n"));

// 3. 从串口读取数据  
char buf[1024] = {0};
read(fd, buf, 1024);

// 4. 关闭串口
close(fd);
字符设备文件创建方式:
  1. 手动创建
    mknod /dev/myled c 250 0
    
  2. 自动创建:(后续讲解)

1.5 设备号管理

设备号组成:
  • 设备号:包含主设备号和次设备号,类型为dev_t(32位无符号整型)
    • 高12位:主设备号
    • 低20位:次设备号
设备号操作宏:
dev_t dev = MKDEV(主设备号, 次设备号);    // 合成设备号
int major = MAJOR(dev);                 // 提取主设备号  
int minor = MINOR(dev);                 // 提取次设备号
设备号作用:
  • 主设备号:应用程序根据主设备号在内核中找到对应的驱动程序
  • 次设备号:驱动根据次设备号找到对应的具体硬件设备
设备号申请与释放:
// 申请设备号
int alloc_chrdev_region(dev_t *dev, 
                       unsigned baseminor, 
                       unsigned count,
                       const char *name);

// 释放设备号
void unregister_chrdev_region(dev_t from, unsigned count);

案例:设备号申请测试

上位机实施:
mkdir /opt/drivers/day02/1.0 -p 
cd /opt/drivers/day02/1.0
vim led_drv.c 
vim Makefile
make
cp led_drv.ko /opt/rootfs/home/drivers/
下位机测试:
cd /home/drivers/
insmod led_drv.ko 
cat /proc/devices  # 查看申请到的主设备号

# 输出示例:
Character devices:
  1 mem
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  10 misc
  ...
  244 chrdev        # 申请的主设备号为244

rmmod led_drv

2. 字符设备驱动框架设计

2.1 字符设备驱动数据结构设计

操作接口结构体:
struct file_operations {
    int (*open)(...);    // 打开设备接口
    int (*read)(...);    // 读设备接口  
    int (*write)(...);   // 写设备接口
    int (*close)(...);   // 关闭设备接口
};
字符设备结构体:
struct char_device {
    char *name;                          // 驱动名称
    dev_t dev;                           // 设备号
    int count;                           // 次设备号个数
    const struct file_operations *ops;   // 操作接口
};

2.2 字符设备驱动实现步骤

// 1. 定义初始化操作接口对象
struct file_operations led_fops = {
    .open = led_open,
    .close = led_close, 
    .read = led_read,
    .write = led_write
};

// 2. 定义初始化字符设备对象
struct char_device led_dev;
char_device_init(&led_dev, &led_fops);

// 3. 注册字符设备
char_device_add(&led_dev, dev, 1);

// 4. 实现各个接口函数
int led_open(...) { ... }
int led_close(...) { ... }
int led_read(...) { ... }  
int led_write(...) { ... }

// 5. 卸载时删除字符设备
char_device_del(&led_dev);

2.3 应用与驱动调用关系

  • 应用open → C库open → 软中断 → 内核sys_open → 驱动open接口
  • 应用read → C库read → 软中断 → 内核sys_read → 驱动read接口
  • 应用write → C库write → 软中断 → 内核sys_write → 驱动write接口
  • 应用close → C库close → 软中断 → 内核sys_close → 驱动close接口

3. Linux内核字符设备驱动API

3.1 内核提供的结构体和函数

操作接口结构体:
struct file_operations {
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
    // ...
};
字符设备结构体:
struct cdev {
    dev_t dev;                           // 设备号
    int count;                           // 次设备号个数  
    struct file_operations *ops;         // 操作接口
    // ...
};
配套操作函数:
// 初始化字符设备
void cdev_init(struct cdev *dev, const struct file_operations *fops);

// 注册字符设备  
int cdev_add(struct cdev *dev, dev_t dev, unsigned count);

// 卸载字符设备
void cdev_del(struct cdev *dev);

案例:LED字符设备驱动实现

驱动开发步骤(12字诀):
  1. 搭框架:编写头文件,入口函数和出口函数
  2. 各种该:该声明的声明,该定义的定义,该初始化的初始化
  3. 各种填:填充入口函数和出口函数,先写注释后塞代码
  4. 写接口:根据用户需求完成各个接口函数的功能

4. 字符设备驱动操作接口详解

4.1 write接口

应用层write使用:
// 1. 写入字符串
char *pstr = "hello,world\n";
write(fd, pstr, strlen(pstr));

// 2. 写入整型数字  
int cmd = 1;
write(fd, &cmd, sizeof(cmd));

// 3. 写入数组数据
int arr[2] = {1, 2};
write(fd, arr, sizeof(arr));

// 4. 写入结构体数据
struct A { int a; int b; };
struct A aa = {100, 200};
write(fd, &aa, sizeof(aa));
驱动层write接口:
ssize_t (*write)(struct file *file, const char __user *buf, 
                size_t count, loff_t *ppos);

安全数据拷贝函数

// 从用户空间拷贝数据到内核空间
int copy_from_user(void *to, const void __user *from, int n);

// 使用示例:
int kcmd;
copy_from_user(&kcmd, buf, sizeof(kcmd));

4.2 read接口

应用层read使用:
// 1. 读取字符串
char str[1024] = {0};
read(fd, str, 1024);

// 2. 读取整型数  
int status;
read(fd, &status, sizeof(status));

// 3. 读取数组数据
int arr[2] = {0};
read(fd, arr, 8);

// 4. 读取结构体数据
struct A { int a; int b; };
struct A aa;
read(fd, &aa, sizeof(aa));
驱动层read接口:
ssize_t (*read)(struct file *file, char __user *buf, 
               size_t count, loff_t *ppos);

安全数据拷贝函数

// 从内核空间拷贝数据到用户空间
int copy_to_user(void __user *to, void *from, int n);

// 使用示例:
int kstatus = 1;
copy_to_user(buf, &kstatus, sizeof(kstatus));

4.3 ioctl接口

应用层ioctl使用:
// 命令定义
#define LED_ON  (0x100001)   // 开灯命令
#define LED_OFF (0x100002)   // 关灯命令

// 1. 传递2个参数
ioctl(fd, LED_ON);

// 2. 传递3个参数  
int index = 2;
ioctl(fd, LED_ON, &index);   // 发送命令并传递参数
驱动层ioctl接口:
long (*unlocked_ioctl)(struct file *file, 
                      unsigned int cmd, 
                      unsigned long buf);

5. 设备文件自动创建

5.1 自动创建设备文件函数

struct class *cls;

// 创建设备类(类似创建树枝)
cls = class_create(THIS_MODULE, "chrdev1");

// 自动创建设备文件(类似长苹果)
device_create(cls, NULL, dev, NULL, "myled");

// 自动删除设备文件(类似摘苹果)  
device_destroy(cls, dev);

// 删除设备类(类似砍树枝)
class_destroy(cls);

5.2 自动创建流程

  1. 驱动程序调用class_createdevice_create
  2. 内核解析/proc/sys/kernel/hotplug找到/sbin/mdev
  3. mdev根据设备号和设备名自动创建设备文件
  4. 设备文件信息存放在/sys/class/chrdev1目录下

6. 指针操作说明

int a = 520;
int *pa = &a;
*pa = 521;                    // 直接指针访问

void *pv = &a;  
*(int *)pv = 521;             // void指针类型转换访问

unsigned long p = (unsigned long)&a;
*(int *)p = 521;              // 整型转指针访问

通过以上完整的字符设备驱动开发框架,可以系统性地掌握Linux内核字符设备驱动的开发方法和技巧。

更多推荐