概述
1.驱动的认知
打开文件 打开的是 文件名 (存放在/dev下面)
设备号
设备号又分为 主设备号
次设备号
主设备号就像华为手机,苹果手机
次设备号就是主设备号旗下的手机型号华为mate P 等
2.驱动链表
驱动插入到链表的顺序通过设备号进行查改
驱动的链表包括添加,查找
添加:将书写的驱动程序添加到驱动的链表当中
查找:通过设备号找到驱动程序,用户空间进行调用该驱动
分为三个层次 第一个是用户层
第二个是内核态
第三个是硬件层
该图简单的介绍了驱动如何从用户层进入到内核中
用户使用设备名找到设备号,进行调用
VFS虚拟文件系统调用sys_open 通过设备名找到设备号,进入到设备的open函数中进行调用
2.驱动框架解读
内核中printk使用的
- 设备从入口module_init(pin4_drv_init); //1.入口 进入
- 进入到_init函数中
- 首先进行的是创建设备号 通过主设备号和次设备号创建生成设备号devno = MKDEV(major,minor);
- 把写好的主设备号,设备号,创建的结构体写入驱动链表当中
- 注册驱动设备在内核中
- 首先创建生成设备
- 创建生成文件 创建生成文件成功后会在/dev下面生成相应的驱动程序
- 卸载驱动 卸载驱动的顺序是相反的 首先销毁设备文件,销毁生成的设备 最后卸载驱动
i2c_dev_class = class_create(THIS_MODULE, “i2c-dev”); //创建一个名称为i2c-dev的class /sys/class
确实在/sys/class里面看到了下面定义的那个文件名
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class; //pin4 的类
static struct device *pin4_class_dev; //pin4 的设备
static dev_t devno; //设备号(设备名)
static int major =231; //主设备号
static int minor =4; //次设备号
static char *module_name="pin4"; //模块名
//进行初始化配置引脚的寄存器
volatile unsigned int *GPFSEL0=NULL;
volatile unsigned int *GPSET0 =NULL;
volatile unsigned int *GPCLR0 =NULL;
//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_openn"); //内核的打印函数和printf类似
//配置pin4引脚为输出引脚
//通过或运算与运算实现14 13 12 位为001
*GPFSEL0&=~(0x6 << 12 ); //左移12位取反以后就变为了001 相与见零为零
//所以保障了第14 13 位为0
*GPFSEL0|=(0x1<<12); //左移12位取反变成了 001 进行或运算 见一为一
// 所以保障了第12位为1
return 0;
}
static ssize_t pin4_read(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_readn");
return 0;
}
//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;
printk("pin4_writen");
//获取上层write函数的值
copy_from_user(&userCmd,buf,count); //获取上层的write的值
//根据值来操作io口 低电平或者高电平
printk("get valuen");
if(userCmd==1){
*GPSET0 |= (0x1 << 4); //把引脚4置1 (置一)
printk("get 1n");
}else if(userCmd==0){
*GPCLR0 |= (0x1 << 4); //把引脚4置1 (清零)
printk("get 0n");
}else{
printk("undon");
}
return 0;
}
//static表示该命名仅在该驱动程序中有效 防止在其他的驱动文件中出现重复命名
//把该结构体放入到内核驱动的链表中
static struct file_operations pin4_fops = {
.owner = THIS_MODULE, //内核中书写结构体指定某一位书写
.open = pin4_open,
.write = pin4_write,
.read = pin4_read
};
int __init pin4_drv_init(void) //2.真实驱动入口
{
int ret;
//主设备号 //次设备号
devno = MKDEV(major,minor); //3.创建设备号
//把上面的结构体 pin4_fops 放入驱动链表当中
// 主设备号 设备名 结构体
ret = register_chrdev(major, module_name,&pin4_fops); //4.注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
//让代码生成设备的逻辑类
pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //5.让代码在dev中生成设备 (华为手机)
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件 (mate30 ) 设备的类 设备号 模块名
//创建完成就会在/dev生成模块名对应的驱动程序
//配置 把物理地址转换成虚拟地址
return 0;
}
void __exit pin4_drv_exit(void) //卸载的时候是反着卸载的
{
//1.卸载映射空间
//2.销毁设备文件
//3.销毁生成设备
//4.卸载驱动
iounmap(GPFSEL0);
iounmap(GPSET0 );
iounmap(GPCLR0 ); //卸载映射的虚拟空间
device_destroy(pin4_class,devno); //销毁pin4的设备文件
class_destroy(pin4_class); //销毁pin4 的生成设备
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //1.入口 内核加载该驱动时,会启动宏
module_exit(pin4_drv_exit); //6.卸载驱动
MODULE_LICENSE("GPL v2");
3.驱动代码的编译
3.1驱动代码的编译
-
打开内核文件
进入到 drivers 文件夹下面
创建的是字符设备 因此我们需要进入到char文件夹字符文件
进入到 char文件夹下面 -
创建pin4drivers.c文件
-
放到哪里改哪里的Makefile
保存完成后修改Makefile -
进入vi Makefile
书写成编译成为模块的方式
该创建创建的是以模块的方式加载到内核 obj-m m表示的是模块
obj-m += pin4drivers.o (这个名字和创建的pin4drivers名字一样,只是后缀不同)
写完后就配置完成 -
返回到内核源码文件夹 (liunx…)
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
进行编译 -
编译没有错误生成pin4drivers.ko
传给树莓派
scp ./drivers/char/pin4drivers.ko pi@192.168.43.58:/home/pi
同时通过交叉编译把主程序发送
3.2在树莓派:
- 加载内核驱动
sudo insmod pin4drivers2.ko
- 卸载内核驱动 不需要写.ko 卸载内核驱动
sudo rmmod pin4drivers2
- 查看当前内核的驱动模块
lsmod
- 把dev里面的pin4设置为所有用户可读可写
sudo chmod 666 /dev/pin4
- 查看用户权限
ls -l
- 查看内核驱动
dmesg
可以查看到pin4的驱动
GPIO的寄存器
GPFSELn 输入模式选择寄存器 pin0~pin9 每个GPIO引脚占3位
000 = GPIO Pin 9 is an input
001 = GPIO Pin 9 is an output
GPSETn 将指定引脚置一
GPCLRn 将指定位置引脚清零
volatile 的用处
vloatile关键字表示提醒当前编译器该变量是易变的,
即每次使用和读取该变量的时候都需要重新向变量地址中读取数据
地址
在编写驱动的时候IO口的驱动地址是在0x3f00 0000加上GPIO的偏移量0x20 0000
因此GPIO的物理地址是从0x3f20 0000开始的
在这个基础上通过Linux系统的MMU内存虚拟化管理,映射到虚拟内存
寄存器的偏移地址
寄存器的地址是物理地址加上寄存器的偏移地址
寄存器的地址是物理地址需要使用ioremap函数把物理地址转换成虚拟地址
ioremap函数
ioremap(物理地址,长度);
寄存器代码的编写
1.首先在最开始的地方定义全局变量
volatile unsigned int * GPFSEL1 =NULL;
2.在入口函数处把物理地址转换成虚拟地址
GPFSEL1 = (volatile unsigned int *)ioremap(0x3f20004,4);
3.在函数中进行寄存器的按位操作 初始化的pin17引脚
*GPFSEL1 &= ~(0x6 << 21) ;//该操作是把23 22位置0 不确定21位当前的状态
*GPFSEL1 |= (0x1 << 21); //该操作是不改变其他位的状态,只让21位的值是1
4.在驱动结束的函数卸载映射的内存空间
4.驱动编写
驱动中有两个函数
read 和write
read
ssize_t (*read)(struct file *filp, char __user *buf, size_t count, lofft *f_pos);
filp:待操作的设备文件file结构体指针
buf:待写入所读取数据的用户空间缓冲区指针
count:待读取数据字节数
f_pos:待读取数据文件位置,读取完成后根据实际读取字节数重新定位
返回:成功实际读取的字节数,失败返回负值
内核空间-->用户空间
copy_to_user函数
unsigned long copy_to_user(void *to, const void *from, unsigned long n)
to:目标地址(用户空间)
from:源地址(内核空间)
n:将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数
write
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
filp:待操作的设备文件file结构体指针
buf:待写入所读取数据的用户空间缓冲区指针
count:待读取数据字节数
f_pos:待读取数据文件位置,写入完成后根据实际写入字节数重新定位
返回:成功实际写入的字节数,失败返回负值
用户空间-->内核空间
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
to:目标地址(内核空间)
from:源地址(用户空间)
n:将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数
最后
以上就是凶狠楼房为你收集整理的驱动认知 驱动的编写的全部内容,希望文章能够帮你解决驱动认知 驱动的编写所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复