文章目录
- 引言
- 一、字符设备驱动工作原理
- 1.1 系统整体工作原理
- 1.2 字符驱动模型
- 1.2.1 file_operations结构体
- 1.2.2 注册字符设备驱动 --- 申请主设备号
- 1.2.3 内核如何管理字符设备驱动
- 二、字符设备驱动代码实践
- 2.1 编写字符设备驱动的步骤和规范
- 2.2 实践写驱动代码
- 2.3 代码实现
- 2.4 驱动测试
- 三、应用程序如何调用驱动
- 3.1 驱动设备文件的创建
- 3.2 添加读写接口
- 3.3 应用和驱动之间的数据交换
- 3.4 读写接口实践
- 3.5 代码实现
- 3.5.1 驱动源码
- 3.5.2 应用层源码
- 3.5.3 Makefile
- 3.6 测试结果
- 四、驱动中如何操控硬件
- 4.1 硬件物理原理不变
- 4.2 寄存器地址、编程方法改变
- 4.3 内核的虚拟地址映射方法
- 4.3.1 为什么需要虚拟地址映射
- 4.3.2 内核中有2套虚拟地址映射方法
- 4.4 如何选择虚拟地址映射方法
- 4.5 操作寄存器地址的方式
- 五、静态映射操作LED
- 5.1 应用层app源代码
- 5.2 驱动源代码
- 六、动态映射操作LED
- 6.1 如何建立动态映射
- 6.2 如何销毁动态映射
- 6.3 代码实践
- 七、总结
- 附
本文用于复习字符设备驱动知识,从目前的认知层面进行整理,如有不足,恳请大家指出,一起沟通交流,以博会友。
注: Kernel-4.19内核代码阅读网站
引言
在Linux文件系统中,需要了解以下几点:
- 每个文件都用一个struct inode结构体来描述,该结构体记录了这个文件的所有信息,例如文件类型,访问权限等。
- 每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。
- 每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。
- 每个驱动程序都有一个设备号。(用在众多的设备驱动中进行区分)
- 用户必须知道设备驱动对应的设备节点(设备文件)
- linux把所有到设备都看成文件
一、字符设备驱动工作原理
1.1 系统整体工作原理
(1) 应用层 -> API -> 设备驱动 -> 硬件
(2) API:open、read、write、close等
(3) 驱动源码中提供真正的open、read、write、close等函数实体
1.2 字符驱动模型
1.2.1 file_operations结构体
kernel-4.19/include/linux/fs.h
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
......
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
(1) 成员主要是函数指针,用来连接实体函数地址
(2) 每个设备驱动都需要一个该结构体类型的变量
(3) 设备驱动向内核注册时提供该结构体类型的变量
1.2.2 注册字符设备驱动 — 申请主设备号
kernel-4.19/include/linux/fs.h
/*
* 函数作用: 驱动向内核注册函数register_chrdev()
* 返回值 : int 返回0表示注册成功,返回一个负整数表示注册失败
* 参数 :
* unsigned int major 主设备号(1~255),可人为向内核申请,不能与已有的设备号重复。(如果等于0,则采用系统动态分配的主设备号;不为0,则表示静态注册)
* const char *name 输入型参数,表示驱动设备的名字
* const struct file_operations *fops 输入型参数,用于内核注册的结构体指针
*/
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)--------------------------------驱动向内核注册自己的file_operations
{
return __register_chrdev(major, 0, 256, name, fops);
}
static inline 关键字修饰的函数,大部分表现和普通的static关键字修饰的函数一样。只不过在调用static inline 修饰的函数时,gcc会在其调用处,将其汇编码展开编译,而不为这个函数生成独立的汇编码,可以减少开销。(inline 减少函数调用开销
)
1.2.3 内核如何管理字符设备驱动
(1) 内核中有一个数组(最多有256个元素)用来存储注册的字符设备驱动,数组下标跟主设备号有关系
(2) register_chrdev() 内部将要注册的驱动信息存储在数组中相应的位置
(3) cat /proc/devices 查看内核中已经注册过的字符设备驱动和块设备驱动
二、字符设备驱动代码实践
2.1 编写字符设备驱动的步骤和规范
步骤:
1,实现模块加载和卸载入口函数
module_init(chr_dev_init);
module_exit(chr_dev_exit);
2,在模块加载入口函数中
a, 申请主设备号 (内核中用于区分和管理不同字符设备)
register_chrdev(dev_major, "chr_dev_test", &my_fops);
b,创建设备节点文件 (为用户提供一个可操作到文件接口--open())
struct class *class_create(THIS_MODULE, "chr_cls");
struct device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, "chr2");
c, 硬件的初始化
1,地址的映射
gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
2,中断的申请
3,实现硬件的寄存器到初始化
// 需要配置gpio功能为输出
*gpx2conf &= ~(0xf<<28);
*gpx2conf |= (0x1<<28);
d,实现file_operations --------- 核心工作量:file_operations及其元素填充、注册驱动
const struct file_operations my_fops = {
.open = chr_drv_open,
.read = chr_drv_read,
.write = chr_drv_write,
.release = chr_drv_close,
};
由上面的步骤可以看出,驱动开发是有一定的思路和框架的。说到底,就是给空模块添加驱动壳子,再具体实现功能细节。
2.2 实践写驱动代码
脑海里先有框架,要知道目标是什么。
细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改。
写下的所有代码必须心里清楚明白,不能似懂非懂。
(1) 定义file_operations结构体变量
(2) open和close函数原型确定、内容填充
(3) 注册驱动
- 主设备号的选择 (cat /proc/devices 查看当前可用的主设备号,确定没用过的先随便定一个)
- 返回值的检查
(4) 卸载驱动
2.3 代码实现
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h> // 记得包含头文件<linux/fs.h>
#define MYMAJOR 200
#define MYNAME "testchar"
int mymajor;
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
printk(KERN_INFO "test_chrdev_open\n");
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, // 就是这个.open对应的函数
};
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
2.4 驱动测试
(1) 编译等 make && make cp
(2) insmod 查看设备注册的现象
(3) rmmod 查看设备注销的现象
三、应用程序如何调用驱动
3.1 驱动设备文件的创建
- 手动创建–缺点/dev/目录中文件都是在内存中,断电后/dev/文件就会消失
1)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
2)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
- 自动创建(通过udev/mdev机制)
struct class *class_create() // 创建一个类
struct device *device_create() // 创建一个设备文件
3.2 添加读写接口
(1) 在驱动代码中添加(注意截图代码注释)
(2)在应用代码中添加
write(fd, "helloworld2222", 14);
read(fd, buf, 100);
3.3 应用和驱动之间的数据交换
(1) copy_from_user,用来将数据从用户空间复制到内核空间。
(2) copy_to_user,用来将数据从内核空间复制到用户空间。
kernel-4.19/include/linux/uaccess.h
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(check_copy_size(to, n, false)))
n = _copy_from_user(to, from, n);
return n;
}
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}
注意:
- copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果复制失败,则返回没有成功复制剩下的字节数。
- linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或使用memcpy吗?非要使用copy_from_user才行吗?
原因是cpu运行指令时,运行级别分为用户态和数据态,这样可以避免内核数据被污染。
3.4 读写接口实践
(1)完成write和read函数
(2)读写回环测试
// 读写文件
write(fd, "helloworld2222", 14);
read(fd, buf, 100);
printf("读出来的内容是:%s.\n", buf);
3.5 代码实现
(1)目前为止应用已经能够读写驱动中的
(2)后续工作:添加硬件操作代码
3.5.1 驱动源码
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#define MYMAJOR 200
#define MYNAME "testchar"
int mymajor;
char kbuf[100]; // 内核空间的buf
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
printk(KERN_INFO "test_chrdev_open\n");
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf); // 不行,因为2个不在一个地址空间中
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, // 就是这个.open对应的函数
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
3.5.2 应用层源码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE "/dev/test" // 刚才mknod创建的设备文件名
char buf[100];
int main(void)
{
int fd = -1;
fd = open(FILE, O_RDWR);
if (fd < 0)
{
printf("open %s error.\n", FILE);
return -1;
}
printf("open %s success..\n", FILE);
// 读写文件
write(fd, "helloworld2222", 14);
read(fd, buf, 100);
printf("读出来的内容是:%s.\n", buf);
// 关闭文件
close(fd);
return 0;
}
3.5.3 Makefile
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
#开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel
obj-m += module_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
arm-linux-gcc app.c -o app
cp:
cp *.ko /x210_porting/rootfs/rootfs/driver_test
cp app /x210_porting/rootfs/rootfs/driver_test
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf app
3.6 测试结果
四、驱动中如何操控硬件
4.1 硬件物理原理不变
- 硬件操作接口(寄存器)不变
- 硬件操作代码不变
4.2 寄存器地址、编程方法改变
- 寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
- 编程方法不同。裸机中习惯直接用指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
4.3 内核的虚拟地址映射方法
4.3.1 为什么需要虚拟地址映射
更好维护,可移植。因为虚拟地址映射能够解决安全隐患、地址不确定问题,并缓解了效率的问题。
所谓虚拟地址映射就是从虚拟地址映射到物理地址,经由MMU内存管理单元映射到实际的物理地址。
注: 硬件 --> 物理地址(寄存器) --> MMU (MMU是实际管理内存的硬件) --> 虚拟地址 --> 驱动程序
4.3.2 内核中有2套虚拟地址映射方法
动态和静态
- 静态映射方法的特点
内核移植时以代码的形式硬编码(实质就是宏定义),如果要更改必须改源代码后重新编译内核。
在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效;
对于移植好的内核,你用不用他都在那里。 - 动态映射方法的特点
驱动程序根据需要随时动态的建立映射、使用、销毁映射,映射是短期临时的。
4.4 如何选择虚拟地址映射方法
- 2种映射并不排斥,可以同时使用
- 静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)
4.5 操作寄存器地址的方式
0,
led-- GPX2_7 —GPX2CON == 0x1100C40----------------gpio的输出功能配置寄存器
GPX2DAT == 0x1100C44---------------------------------------数据寄存器
将0x11000C40映射成虚拟地址
对虚拟地址中的[31:28] = 0x1
1, volatile unsigned long *gpxcon;
*gpxcon &= ~(0xf<<28);
2, readl/writel();
u32 readl(const volatile void __iomem *addr)//从地址中读取地址空间的值
void writel(unsigned long value , const volatile void __iomem *add) // 将value的值写入到addr地址
例子:
// gpio的输出功能配置寄存器
u32 value = readl(led_dev->reg_virt_base);
value &= ~(0xf<<28);
value |= (0x1<<28)
writel(value, led_dev->reg_virt_bas);
或者:
*gpx2dat |= (1<<7); // 数据寄存器
替换成:
writel( readl(led_dev->reg_virt_base + 4) | (1<<7), led_dev->reg_virt_base + 4 ); // 操作地址时,+1 与+4的区别是因为数据类型不同。
五、静态映射操作LED
- 关于静态映射
- 不同版本内核中静态映射表位置、文件名可能不同
- 不同SoC的静态映射表位置、文件名可能不同
- 所谓映射表其实就是头文件中的宏定义
- 三星版本内核中的静态映射表
(1) 主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
map-s5p.h中定义的就是要用到的几个模块的寄存器基地址(并不全)。定义的是模块的寄存器基地址的虚拟地址。
(2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE (0xFD000000) // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定
(3) GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义
(4) GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
它们的层级关系如下:
- 裸机中的操作方法添加LED操作代码
- 添加驱动中的写函数
- 先定义好应用和用户之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭,应用向驱动写"flash"则驱动让LED闪烁。
- 应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。
注:应用层一般用来满足用户需求,写更多的功能函数;驱动层一般只简单实现硬件的功能,比如控制LED的亮灭。
简言之, 上层玩策略,底层玩机制。 |
- 写应用来测试写函数
- 驱动和应用中来添加读功能
注:内核中也有自己的memset()和strcmp(); 函数用法跟应用层一样,在驱动中使用时要包含头文件#include <linux/string.h>
5.1 应用层app源代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILE "/dev/test" // 刚才mknod创建的设备文件名
char buf[100];
int main(void)
{
int fd = -1;
int i = 0;
fd = open(FILE, O_RDWR);
if (fd < 0)
{
printf("open %s error.\n", FILE);
return -1;
}
printf("open %s success..\n", FILE);
while (1)
{
memset(buf, 0 , sizeof(buf));
printf("请输入 on | off \n");
scanf("%s", buf);
if (!strcmp(buf, "on"))
{
write(fd, "1", 1);
}
else if (!strcmp(buf, "off"))
{
write(fd, "0", 1);
}
else if (!strcmp(buf, "flash"))
{
for (i=0; i<3; i++)
{
write(fd, "1", 1);
sleep(1);
write(fd, "0", 1);
sleep(1);
}
}
else if (!strcmp(buf, "quit"))
{
break;
}
}
// 关闭文件
close(fd);
return 0;
}
5.2 驱动源代码
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#define MYMAJOR 200
#define MYNAME "testchar"
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
int mymajor;
char kbuf[100]; // 内核空间的buf
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
printk(KERN_INFO "test_chrdev_open\n");
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf); // 不行,因为2个不在一个地址空间中
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
if (kbuf[0] == '1')
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (kbuf[0] == '0')
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, // 就是这个.open对应的函数
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的,空白的,没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
六、动态映射操作LED
6.1 如何建立动态映射
(1) request_mem_region,向内核申请(报告)需要映射的内存资源。
(2) ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
6.2 如何销毁动态映射
(1) iounmap ,解除映射
(2) release_mem_region,申请释放
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。
6.3 代码实践
(1) 2个寄存器分开独立映射
(2) 多个寄存器在一起映射
七、总结
常常认为,struct inode描述的是文件的静态信息,即这些信息很少会改变,而struct file描述的是动态信息,即对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_ops(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。此处借用kavin.zhu大佬制作的图,就不重复造轮子了。
从上图可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。
(1) open()函数打开设备文件
时,根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
kernel-4.19/include/linux/fs.h
struct inode {
umode_t i_mode;-------------------- 记录设备类型
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
dev_t i_rdev; ------------------------------- 记录设备号
loff_t i_size;
/* Misc */
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev; ----------------------------记录描述字符设备的结构体
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
void *i_private; /* fs or device private pointer */
}
struct file {
...
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
......
}
(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体的i_cdev成员中,将struct cdev结构体中的函数操作接口地址记录在struct file结构体的f_ops成员中。
(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在有struct file找到操作字符设备的函数接口了。
注:大部分接口都是成对使用的,比如 : register_chrdev与unregister_chrdev。类似的学习博文
附
KERN_ERR、PTR_ERR 与 IS_ERR接口在编写驱动中的应用。
if(IS_ERR(led_dev->dev))
{
printk(KERN_ERR "device_create error\n"); // KERN_ERR
ret = PTR_ERR(led_dev->dev); //将指针出错的具体原因转换成一个出错码
goto err_2;
}