名词解释:
【用户程序】:基础C + 函数库
【函数库】:利用linux C库提供的API去支配内核(也可以调用第三方库例如wiringPi库:树莓派的IO库)【是对系统调用接口的封装】
【系统调用接口】:会调用sys_call。linux C库与系统调用接口打交道,系统调用接口是最小功能函数
【虚拟文件VFS接口】:会调用sys_open、sys_read等函数
【硬件】可以分为磁盘或设备,虚拟文件系统会识别用户输入的指令是操作磁盘还是设备,若操作设备还会经过设备驱动程序,设备驱动里面会有硬件控制的代码
linux系统内核相当于是超级裸机,直接操作硬件,但会把硬件底层的东西抽象化,对用户来说只需要调API就好了,根本不需要管寄存器,协议,总线…(单片机会去直接操作),这些全部由操作系统做好。
为了方便调用内核,Linux将内核的功能接口制作成系统调用(system call)。用户不需要了解内核的复杂结构,就可以使用内核。系统调用是操作系统的最小功能单位。一个操作系统,以及基于操作系统的应用,都不可能实现超越系统调用的功能。
系统调用提供的功能非常庞大,所以使用起来很麻烦。一个简单的给变量分配内存空间的操作,就需要动用多个系统调用。Linux定义一些库函数(library routine)来将系统调用组合成某些常用的功能。上面的分配内存的操作,可以定义成一个库函数,比如常用的malloc。
linux中有两百多个系统调用,在命令中输入man 2 syscalls
可以查询,2表示在2类(系统调用类)中查询,具体各类是什么可以在man man中查询
-
c库是对系统调用的封装,可以完成进程,线程,网络编程,文件IO等操作
【进程、线程、文件IO可以统称“系统编程”】
设备驱动需要使用第三方库,比如wiringPi库,将引脚进行重新配置【wiringPi库中也有线程进程API等,但是不采用】
应用
,也经常被称为命令行
。可以理解为是一个命令解释器
本质就是提供和内核交互的程序
。原来树莓派开发使用厂家提供的wiringPi库,开发简单。
但未来做开发时,不一定都是用树莓派,没有wiringPi库可以用。但只要能运行Linux,Linux的标准C库一定有。
学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路等就能做开发。
linux系统将设备分为3类:字符设备、块设备、网络设备。
linux一切皆为文件,其设备管理同样是和文件系统紧密结合,各种设备都以文件的形式存放在/dev目录下,称为设备文件。
硬件要有相对应的驱动,那么open怎样区分这些硬件对应的驱动程序
对于常用设备
设备号又分为:
设备号的作用:
区分不同种类的设备和不同类型的设备
驱动插入到链表的位置(顺序)由设备号检索
字符驱动代码执行流程:
触发软中断(系统调用专用,触发cpu异常,中断号是0x80,0x80代表发生了一个系统调用) ,进入系统内核
注意:
驱动开发无非以下两件事:
Linux 内核就是由各种驱动组成的,内核源码中有大约 85%是各种驱动程序的代码
。内核中驱动程序种类齐全,可以在同类驱动的基础上进行修改以符合要求的驱动。
编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有【驱动程序的框架】,在这个框架中加入这个硬件。
一般来说,编写一个 linux 设备驱动程序的大致流程如下:
下面就以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。
上层调用代码
#include
#include
#include
#include
void main()
{
int fd,data;
fd = open("/dev/pin4",O_RDWR);
if(fd<0){
printf("open fail\n");
perror("reson:");
}
else{
printf("open success\n");
}
data = write(fd,'1',1);
}
驱动框架代码
#include //file_operations声明
#include //module_init module_exit声明
#include //__init __exit 宏定义声明
#include //class devise声明
#include //copy_from_user 的头文件
#include //设备号 dev_t 类型声明
#include //ioremap iounmap的头文件
//确定主设备、变量定义
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"; //模块名
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数,和printf类似
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;
}
//定义file_operations结构体,给指定成员添加函数指针
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
//驱动的入口函数
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自动生成设备
//除此之外还可以手动生成设备,在dev目录下 sudo mknod +设备名字 +设备类型(c表示字符设备驱动) +主设备号+次设备号。
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); //卸载驱动
}
//GPL协议,入口加载,出口加载
module_init(pin4_drv_init); //入口,内核加载该驱动(insmod)的时候,这个宏被使用,非函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
驱动框架设计流程
确定主设备号
定义结构体 类型 file_operations
实现对应的 drv_open/drv_read/drv_write 等函数,在 file_operations 结构体中注册相应的函数
实现驱动入口
:安装驱动程序时,就会去调用这个入口函数,执行工作:
① 创建设备号:MKDEV
① 把 file_operations 结构体告诉内核:注册驱动程序:register_chrdev
.
② 创建类class_create.
③ 创建设备节点device_create.
实现驱动出口
:卸载驱动程序时,就会去调用这个出口函数,执行工作:
① 把 file_operations 结构体从内核注销:unregister_chrdev.
② 销毁类class_create.
③ 销毁设备结点device_destroy.
其他完善:GPL协议、入口加载、出口加载
注意:
file_operations 结构体如下图所示,大部分为函数指针,并不是所有的成员都需要赋初始值,可以只给上层应用所调用的函数添加对应的成员【在keil编译器中不能这样使用】
static关键字设置驱动代码中变量的作用域,防止命名冲突【有静态函数,静态全局变量】
驱动模块代码编译(模块的编译
需要配置过的内核源码
。放在指定的目录下,编译、连接后生成的内核模块后缀为.ko
,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)
操作步骤:
由于操作的是字符设备驱动,所以将该驱动代码pin4test.c拷贝到ubuntu的 linux-rpi-4.14.y/drivers/char
目录下的文件夹中(也可选择设备目录下其它文件)
修改该文件夹下Makefile文件(驱动代码放到哪个目录,就修改该目录下的Makefile),将字符驱动代码编译为模块
,文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),因为最终是要将模块加载到开发板上而不是虚拟机中,所以要生成模块然后移植到开发板挂载,所以只需要将obj-m += pin4drive.o添加到Makefile中即可。
回到linux-rpi-4.14.y/编译驱动文件
将生成的.ko
文件发送给树莓派
将上层调用代码(pin4test.c)进行 交叉编译后发送给树莓派,
就可以看到树莓派目录下存在发送过来的pin4driver.ko文件
和pin4test
这两个文件, 都已经完成编译
打开树莓派,执行加载内核驱动sudo insmod pin4drive.ko
。此时dev下就有该驱动设备名称
lsmod
查看系统的驱动模块是否完成
执行编译后的文件./pin4test
若权限不够则使用chmod 666 /dev/pin4
查看内核打印的信息, dmesg |grep pin4
在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写.ko)
将驱动卸载。
查看树莓派芯片型号cat /proc/cpuinfo
,该树莓派CPU型号为BCM2835,包括四个核
硬件寄存器部分代码需要看电路图(为了找寄存器)和芯片手册,树莓派芯片手册给出了IO口对应的寄存器
3. GPIO功能选择寄存器有6个,负责管理54个引脚,例如:GPFEL0 管理0-9引脚的输入/输出的功能 ------ GPFEL1管理 10-19引脚的输入/输出的功能 以此类推,GPFSEL5就是pin50~pin53的配置寄存器。引脚4位于GPFEL0功能选择寄存器管理返回,而该功能选择寄存器发送的是32位,32位如何控制10个引脚呢?通过下面的表格可知,引脚4由第12-14位决定,按照27-29给出的提示,这三比特位写001表示输出,写000表示输入
4. 通过设置GPSET0和GPCLR0可以将输出口置0或置1,
输出寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
GPCLRn是清零功能寄存器。输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。
这里的置1不是输出1,而是启动该寄存器,按照功能的设定输出0,同样的,清除功能寄存器的第四比特位置1并不是输出1,而是驱动第四引脚的电平拉低,完成清除功能
IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的
,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。特别注意,不同的CPU型号的起始地址不同,起始地址写错可能会出现“段错误”的原因。根据总线地址可以得出相对于起始地址的偏移量
可以在初始化函数内重新赋值,使用ioremap函数完成映射
ioremap(物理地址,物理地址字节数);
由于地址是32位,也就是4个字节ioremap宏定义在asm/io.h内:
#include //ioremap iounmap 的头文件
开始映射:void* ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags)
//用map映射一个设备意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或写入,实际上就是对设备的访问。
第一个参数是映射的起始地址
第二个参数是映射的长度
第二个参数怎么定啊?
====================
这个由你的硬件特性决定。
比如,你只是映射一个32位寄存器,那么长度为4就足够了。
(这里树莓派IO口功能设置寄存器、IO口设置寄存器都是32位寄存器,所以分配四个字节就够了)
比如:GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
这三行是设置寄存器的地址,volatile的作用是作为指令关键字
确保本条指令不会因编译器的优化而省略,且要求每次直接读值
ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。
解除映射:void iounmap(void* addr)//取消ioremap所映射的IO地址
比如:
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0); //卸载驱动时释放地址映射
参数:
phys_addr:要映射的起始的IO地址
size:要映射的空间的大小
flags:要映射的IO空间和权限有关的标志
返回:
注意:
cat /proc/iomen
可以查看物理地址与虚拟地址的映射关系【下图仅是个例子,非此开发板】进行取反后再进行按位与操作是为了不影响其他引脚
31 30 ······14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 0 ······0 0 1 0 0 0 0 0 0 0 0 0 0 0
//配置pin4引脚为输出引脚 bit 12-14 配置成001 先取反为110,即十六进制的0x6
*GPFSEL0 &= ~(0x6 <<12); // 把bit13 、bit14置为0
// 与看false,有0则0,所以上面只能保证13和14位是与001或操作之后输出是0,不能保证12位输出是1,还需要进行或操作,有1则1,确保12位输出的是1
*GPFSEL0 |= (0x1 <<12); //把12置为1 |按位或
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;
int copy_cmd;
copy_cmd = copy_from_user(&userCmd,buf,count);
//函数的返回值是,如果成功的话返回0,失败的话就是返回用户空间的字节数
//函数的第一个参数为应用的存储空间地址,第二个为内核的存储空间,和pin4write函数的参数一样,第三个参数也和pin4write函数的参数一样
}
驱动参考博文:
树莓派高级开发之树莓派博通BCM2835芯片手册导读与及“相关IO口驱动代码的编写”
对应硬件的GPFSEL0设置功能模式为输出模式
对应硬件的输出高低电平
在此处的作用:防止编译器优化(可能是省略,也可能是更改)这些寄存器地址变量,常见于在内核中对IO口进行操作
作用:确保指令不会因编译器的优化而省略,且要求每次直接读值,在这里的意思就是确保地址不会被编译器更换
函数copy_from_user原型:
copy_from_user(void *to, const void __user *from, unsigned long n)
返回值:
参数详解:
#include //file_operations声明
#include //module_init module_exit声明
#include //__init __exit 宏定义声明
#include //class devise声明
#include //copy_from_user 的头文件
#include //设备号 dev_t 类型声明
#include //ioremap iounmap的头文件
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"; //模块名--这个模块名到时候是在树莓派的/dev底下显示相关驱动模块的名字
volatile unsigned int* GPFSEL0 = NULL; //寄存器变量,不希望寄存器优化它,加上volatile,是个地址加上unsigned
volatile unsigned int* GPSET0 = NULL; //volatile指令不会因编译器的优化而省略,且要求每次直接读值
volatile unsigned int* GPCLR0 = NULL;
//volatile关键字的作用:确保指令不会因编译器的优化而省略,且要求每次直接读值,在这里的意思就是确保地址不会被编译器更换
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
//设置引脚的功能为输出模式
//由于pin4在 14-12位,所以将14-12位分别置为001即为输出引脚,所以下面的那两个步骤分别就是将14,13置为0,12置为1
*GPFSEL0 &= ~(0x6 << 12); //把13,14位 置为0
*GPFSEL0 |= (0x1 << 12); //把12位 置为1
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;
int copy_cmd;
printk("pin4_write\\n");
//copy_from_user(void *to, const void __user *from, unsigned long n)
copy_cmd = copy_from_user(&userCmd,buf,count); //将用户空间传递的buf指针赋值给内核空间开辟的userCmd
if(copy_cmd != 0)
{
printk("fail to copy from user\n");
}
if(userCmd == 1)
{
printk("set 1\n");
*GPSET0 |= (0x1 << 4); //这里的1左移4位的目的就是促使寄存器将电平拉高,即变为HIGH
}
else if(userCmd == 0)
{
printk("set 0\n");
*GPCLR0 |= (0x1 << 4); //这里的1左移4位也是一样只是为了让寄存器将电平拉低,即变为LOW
}
else
{
printk("nothing undo\n");
}
return 0;
}
static ssize_t pin4_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
printk("pin4_read\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.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); //创建设备文件
//由于以下的地址全是物理地址,所以我们要将物理地址转换成虚拟地址
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4); //由于寄存器是32位的,所以是映射4个字节,一个字节为8位
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); //先卸载设备号(主设备号和次设备号) 即mate20
class_destroy(pin4_class); //再卸载类 即华手机
unregister_chrdev(major,module_name); //内核链表中的节点,成功卸载驱动 //需要注意第一个参数是主设备号
}
module_init(pin4_drv_init); //真正的入口
module_exit(pin4_drv_exit); //卸载驱动
MODULE_LICENSE("GPL v2");
#include
#include
#include
#include
int main()
{
int fd;
int userCmd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0)
{
printf("fail to open the pin4\n");
perror("the reason:");
}
else
{
printf("success to open the pin4\n");
}
printf("please Input 1-HIGH,0-LOW \n");
scanf("%d",&userCmd);
write(fd,&userCmd,4); //这里userCmd是一个整型数,所以写的是4个字节
return 0;
}