Linux内核字符设备驱动开发详解
·
Linux内核字符设备驱动开发详解
1. 字符设备驱动基础
1.1 设备驱动定义与特点
定义:能够使硬件工作的软件
驱动两大核心:
- 必须操作硬件
- 必须给用户应用提供操作接口,根据操作接口访问硬件(服务)
- 驱动最终必须服务于应用程序
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);
字符设备文件创建方式:
- 手动创建:
mknod /dev/myled c 250 0 - 自动创建:(后续讲解)
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字诀):
- 搭框架:编写头文件,入口函数和出口函数
- 各种该:该声明的声明,该定义的定义,该初始化的初始化
- 各种填:填充入口函数和出口函数,先写注释后塞代码
- 写接口:根据用户需求完成各个接口函数的功能
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 自动创建流程
- 驱动程序调用
class_create和device_create - 内核解析
/proc/sys/kernel/hotplug找到/sbin/mdev mdev根据设备号和设备名自动创建设备文件- 设备文件信息存放在
/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内核字符设备驱动的开发方法和技巧。
更多推荐


所有评论(0)