驱动编译前提:驱动代码的编译需要提前编译号的内核
驱动(3种实现方法,2条路线)_驱动编写三种方法-CSDN博客
一、概念
1.1、驱动认识
1、裸机程序中是直接操控硬件的,操作系统中必须通过驱动来操控硬件。这两个的本质区别就是分层
2、linux驱动本身做了模块化设计,linux驱动本身和linux内核不是强耦合的
3、驱动的设计中有一个关键数据结构(结构体),结构体中包含一些变量和一些函数指针
变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法
这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体
4、一个驱动工作时主要就分几部分:驱动构建(构建一个struct mmc然后填充它)
驱动运行时(调用这些函数指针指针的函数和变量)
5、分离思想
分离思想就是说在驱动中将操作方法和数据分开
操作方法就是函数,数据就是变量
在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。
6、分层思想
分层思想是指一个整个的驱动分为好多个层次
简单理解就是驱动分为很多个源文件,放在很多个文件夹中
1.2、什么是驱动
1、设备和用户之间的桥梁,内核结构由用户级,内核级,硬件级
2、驱动,操作硬件部分代码
1.3、驱动分为3种,及区别
1、字符设备,以字节为单位读写串口,led等
2、块设备,以块为单位读写,sd卡等
3、网络设备,网卡,socket等
1.4、模块化
1、宏内核:操作系统是整体,紧耦合,直接调用函数,简单高效
微内核:相当于是多个进程,一个错误不会影响其他,Windows
2、静态模块化:程序编译内核,编译时可裁剪,加模块要重新编译烧录
动态模块化:加模块,不需要重新编译
1.5、安全性
1、驱动程序崩溃,内核可能崩溃
2、驱动效率影响内核效率
3、安全,漏洞
未初始化,例如函数指针
恶意用户程序,利用驱动的漏洞,例如传参
缓冲区溢出,例如超出缓存区大小
竞争状态,自旋锁等
1.6、应用驱动硬件的流程
APP------>调用C库种(open,write,read)等函数产生软中断(中断号0x80),从用户空间进入到内核空间--------->sys_call函数根据驱动被设备名去找到设备号---------->虚拟文件系统VAFS(sys_open,sys_write)去找相应驱动引脚的驱动文件open------------->驱动文件open直接操作寄存器
1.7、设备文件
1、各种设备以文件的形式存放在**/dev目录,称设备文件**
2、主设备号,次设备号
1、主设备号:不同的设备(硬盘,led)
2、次设备号:同一类的多个设备(led1,led2)
1.8、驱动链表,数组 (驱动插入链表的顺序由设备号检索)
1、管理所有设备的驱动
2、添加(驱动插入链表顺序由设备号检索)
编写驱动,加载到内核
设备名,设备号,设备驱动函数:操作寄存器来驱动I/O
3、查找 调用
调用驱动程序,用户应用去open
1.9、
1.9、udev机制
1、udev是一个设备管理工具,udev以守护进程的形式运行
2、通过侦听内核发出来的uevent来管理/dev目录下的设备文件。
3、udev在用户空间运行,而不在内核空间运行。
4、它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。
5、udev会根据/etc/udev/udev.conf文件中的udev_rules指定的目录,逐个检查该目录下的
文件,这个目录下的文件都是针对某类或某个设备应该施行什么措施的规则文件。
二、实现一个驱动代码
1、添加驱动需要什么
1、设备名
2、设备号
3、驱动代码
2、假设树莓派dev目录下已经存在了一个 pin4的驱动
应用层访问/dev/pin4驱动代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd;
char ch = '1';
fd = open("/dev/pin4" ,O_RDWR);
if (-1 == fd)
{
perror("erron");
printf("open failed!\n");
}
else
{
printf("open success\n");
}
fd = write(fd , (void *)&ch ,1);
return 0;
}
3、写一个字符设备驱动
因为驱动程序是通过驱动链表进行添加的,首先开发需要拿到固定驱动框架,下面是一个字符驱动框架
#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 device声明
#include <linux/uaccess.h> //copy_from_user的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //iorsemap iorsemap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major=231; //主设备号
static int minor = 0; //次设备号
static char *module_name = "pin4"; //模块名
static ssize_t pin4_read(struct file *file ,char __user *buf,size_t size ,loff_t *ppos)
{
printk("pin4_read\n");
return 0;
}
// led_open函数
static int pin4_open(struct inode *inode, struct file *file)
{
printk("pin4_open\n"); //内和打印函数和printf差不多,可以用dmesg命令查看内核的打印
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file, const char __user *buf, size_t count,loff_t *ppos)
{
printk("pin4_write\n");
return 0;
}
// 这个结构结构体会被注册进驱动链表
static struct file_operations pin4_fops ={
.owner = THIS_MODULE,
//将对应的函数关联在file_operations的结构体中
.open = pin4_open,
.write = pin4_write,
.read = pin4_read,
};
int __init pin4_drv_init(void) //真实驱动入口
{
int ret;
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动,告诉内核 把这个驱动加到内核的链表中
pin4_class = class_create( THIS_MODULE,"myfirstdemo"); //让代码在dev自动生成设备
pin4_class_dev = device_create(pin4_class,NULL,devno,NULL,module_name);//创建设备文件
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major,module_name); //卸载驱动
}
module_init(pin4_drv_init); // 入口,内核加载该驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
3、对驱动代码进行编译(驱动可以编译成模块或编译进内核)
把写的驱动代码复制到下载的Linux源码文件里的drviers/char字符驱动设备下
更改Makefile文件 将驱动编译成模块
编译驱动代码
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
交叉编译调用该驱动的应用层文件,并将驱动模块文件和应用层文件都考入树莓派
加载内核驱动
sudo insmod pin4drivers.ko
lsmod //这个命令可以查看内核驱动模块
rmmod //删除驱动模块 例:rmmod pin4drivers
运行驱动测试代码
sudo ~/pin4test
dmesg 查看内核打印的信息
应用层调用驱动
引用层open通过文件名找到驱动的设备号————>产生软中断,调用sys_call————>调用虚拟文件系统的sys_open通过设备号去驱动链表找到相应的硬件驱动文件————>执行驱动文件里面的open。
三、通过树莓派的芯片手册写引脚 4的驱动程序
1、芯片 BCM2835
GPFSEL0 GPIO Function Select 0 功能选择 输入/输出
GPSET0 GPIO Pin Output Set 0 输出1
GPCLR0 GPIO Pin Output Clear 0 清0
2、寄存器
GPIO功能选择寄存器 GPIO Function Select Registers 32位
配置引脚4 GPFSEL0 14-12位 配置 001 = GPIO Pin 4 is an output
GPIO输出设置寄存器 GPIO Pin Output Set Registers
GPIO清理基础器 GPIO Pin Output Clear Registers (GPCLRn)
驱动程序
#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 device声明
#include <linux/uaccess.h> //copy_from_user的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //iorsemap iorsemap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major=231; //主设备号
static int minor = 0; //次设备号
static char *module_name = "pin4"; //模块名
volatile unsigned int* GPFSEL0 = NULL; //功能选择寄存器0(物理地址0x3f200000) 可配置(FSEL0~FSEL9 引脚的功能) (bit14-12用来配置引脚4)
volatile unsigned int* GPSET0 = NULL; //输出设置寄存器0(物理地址0x3f20001C) 0无效 bit4配置为1,表示为高电平
volatile unsigned int* GPCLR0 = NULL; //清零设置寄存器0(物理地址0x3f200028) 0无效 bit4配置成1,表示为低电平
static ssize_t pin4_read(struct file *file ,char __user *buf,size_t size ,loff_t *ppos)
{
printk("pin4_read\n");
// copy_to_you函数
return 0;
}
// led_open函数
static int pin4_open(struct inode *inode, struct file *file)
{
printk("pin4_open\n"); //内和打印函数和printf差不多,可以用dmesg命令查看内核的打印
//配置bcm4引脚为输出引脚 把功能寄存器0的bit14-12配置成001
*GPFSEL0 &= ~(0x6 << 12); //按位操作,防止直接设置,影响其它位控制的功能
*GPFSEL0 |= (0X1 << 12);
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file, const char __user *buf, size_t count,loff_t *ppos)
{
int userCmd;
unsigned long copy_from_user_num = -1;
printk("pin4_write\n");
//1、获取应用层write函数的值 ,函数copy_from_user(void *to ,const void __user *from ,unsigned long n)
// to:指向内核空间的指针,数据将被复制到这个位置。
// from:指向用户空间的指针,数据将从这里被复制。
// n:要复制的数据的字节数
copy_from_user_num = copy_from_user((void *)&userCmd ,buf ,count);
printk("%s |%s |%d: count=%u , copy_from_user_num=%lu\n" ,__FILE__,__func__,__LINE__,count,copy_from_user_num);
if (count != copy_from_user_num)
{
// 处理错误情况,例如打印错误消息、释放资源等
printk(KERN_ERR "copy_from_user failed to copy %lu bytes\n", count - copy_from_user_num);
// 可能需要返回错误代码或执行其他恢复措施
}
printk("get value , userCmd = %c\n" ,(char)userCmd);
//2、根据值来控制IO口(输出高电平或低电平)
if ('1' == userCmd)
{
//电平拉高
printk("set 1\n");
*GPSET0 |= 0x1 << 4;
}
else if ('0' == userCmd)
{
//低电平 清零
printk("set 0\n");
*GPCLR0 |= 0x1 << 4;
}
else
{
printk("undo\n");
}
return 0;
}
static struct file_operations pin4_fops ={
.owner = THIS_MODULE,
//将对应的函数关联在file_operations的结构体中
.open = pin4_open,
.write = pin4_write,
.read = pin4_read,
};
int __init pin4_drv_init(void) //真实驱动入口
{
int ret;
printk("insmod driver pin4 success\n");
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动,告诉内核 把这个驱动加到内核的链表中
pin4_class = class_create( THIS_MODULE,"myfirstdemo"); //让代码在dev自动生成设备
pin4_class_dev = device_create(pin4_class,NULL,devno,NULL,module_name);//创建设备文件
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000 ,4); //ioremap函数将物理地址映射成虚拟地址,函数参数(物理地址,映射的大小4个字节)
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C ,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028 ,4);
return 0;
}
void __exit pin4_drv_exit(void)
{
//解锁地址映射
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major,module_name); //卸载驱动
}
module_init(pin4_drv_init); // 入口,内核加载该驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
测试Pin4主函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd;
char ch = '1';
fd = open("/dev/pin4" ,O_RDWR);
if (-1 == fd)
{
perror("erron");
printf("open failed!\n");
}
else
{
printf("open success\n");
}
fd = write(fd , (void *)&ch ,1);
return 0;
}
运行测试程序,对驱动进行验证
运行pin4test之前
gpio口状态
内核打印信息
执行pintest之后
gpio口状态
内核打印信息