树莓派——linux内核与驱动

文章目录

  • Linux内核基础框架
    • 内核结构框图
    • linux系统架构
      • shell
  • 驱动
    • 为什么要学习写驱动
    • 什么是驱动:
    • 硬件设备分类:
    • 文件名与设备号
    • 驱动结构框图的认知
      • 内核的 sys_open 、sys_read 会做什么?
    • 驱动程序开发步骤
    • 字符设备驱动
      • 基于驱动框架的代码开发【不涉及硬件操作】
      • 驱动模块代码编译和测试
      • 驱动的硬件代码编写
      • ==完整案例==:
        • volatile关键字的使用
        • copy_from_user和copy_to_user 函数的用法
        • **基于驱动框架的驱动代码**
        • **应用层代码**

Linux内核基础框架

  • 对内核结构框图有个总体的把握,有助于理解为什么驱动要这样写,为什么写应用程序所用的C库接口(文件、进程、线程、网络等)能够完成相应的功能。

内核结构框图

树莓派——linux内核与驱动_第1张图片

名词解释:

  • 【用户程序】:基础C + 函数库

  • 【函数库】:利用linux C库提供的API去支配内核(也可以调用第三方库例如wiringPi库:树莓派的IO库)【是对系统调用接口的封装】

  • 【系统调用接口】:会调用sys_call。linux C库与系统调用接口打交道,系统调用接口是最小功能函数

  • 【虚拟文件VFS接口】:会调用sys_open、sys_read等函数

  • 【硬件】可以分为磁盘或设备,虚拟文件系统会识别用户输入的指令是操作磁盘还是设备,若操作设备还会经过设备驱动程序,设备驱动里面会有硬件控制的代码

  • linux系统内核相当于是超级裸机,直接操作硬件,但会把硬件底层的东西抽象化,对用户来说只需要调API就好了,根本不需要管寄存器,协议,总线…(单片机会去直接操作),这些全部由操作系统做好。

linux系统架构

树莓派——linux内核与驱动_第2张图片

  • 为了方便调用内核,Linux将内核的功能接口制作成系统调用(system call)。用户不需要了解内核的复杂结构,就可以使用内核。系统调用是操作系统的最小功能单位。一个操作系统,以及基于操作系统的应用,都不可能实现超越系统调用的功能。

  • 系统调用提供的功能非常庞大,所以使用起来很麻烦。一个简单的给变量分配内存空间的操作,就需要动用多个系统调用。Linux定义一些库函数(library routine)来将系统调用组合成某些常用的功能。上面的分配内存的操作,可以定义成一个库函数,比如常用的malloc。

  • linux中有两百多个系统调用,在命令中输入man 2 syscalls可以查询,2表示在2类(系统调用类)中查询,具体各类是什么可以在man man中查询
    -树莓派——linux内核与驱动_第3张图片

  • c库是对系统调用的封装,可以完成进程,线程,网络编程,文件IO等操作【进程、线程、文件IO可以统称“系统编程”】

  • 设备驱动需要使用第三方库,比如wiringPi库,将引脚进行重新配置【wiringPi库中也有线程进程API等,但是不采用】

shell

  • shell(壳)是一个特殊的应用,也经常被称为命令行 。可以理解为是一个命令解释器
  • 一个shell对应一个终端,在终端输入的文本会给shell程序分析,shell程序可以运行库函数或系统调用等
  • 本质就是提供和内核交互的程序

驱动

为什么要学习写驱动

  • 原来树莓派开发使用厂家提供的wiringPi库,开发简单。

  • 但未来做开发时,不一定都是用树莓派,没有wiringPi库可以用。但只要能运行Linux,Linux的标准C库一定有。

  • 学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路等就能做开发。

什么是驱动:

  • 驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。

硬件设备分类:

linux系统将设备分为3类:字符设备、块设备、网络设备。

  1. 字符设备:指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
  2. 块设备: 指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
  3. 网络设备: 网络设备可以是一个硬件设备,如网卡; 但也可以是一个纯粹的软件设备, 比如回环接口(lo).一个网络接口负责发送和接收数据报文。

文件名与设备号

linux一切皆为文件,其设备管理同样是和文件系统紧密结合,各种设备都以文件的形式存放在/dev目录下,称为设备文件

硬件要有相对应的驱动,那么open怎样区分这些硬件对应的驱动程序

  • 依靠文件名(设备名)与设备号

  • 查询设备号:依靠文件名(设备名)与设备号。在/dev下ls -l可以看到
    树莓派——linux内核与驱动_第4张图片

对于常用设备

  • Linux有约定俗成的编号,如硬盘的主设备号是3。 一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。

设备号又分为:

  • 主设备号:用于区别不同种类的设备;
  • 次设备号:区别同种类型的多个设备。

设备号的作用:

  • 区分不同种类的设备和不同类型的设备

  • 驱动插入到链表的位置(顺序)由设备号检索

驱动结构框图的认知

树莓派——linux内核与驱动_第5张图片

字符驱动代码执行流程:

  1. 应用层调用C库函数open(“/dev/pin4”,O_RDWR);
  2. 触发软中断(系统调用专用,触发cpu异常,中断号是0x80,0x80代表发生了一个系统调用) ,进入系统内核
  3. 内核层中系统调用接口sys_call(汇编语言,执行效率高),通过设备名找到设备号
  4. 内核层中虚拟文件系统 VFS会区分是操作磁盘还是设备,根据设备号调用指定链表节点(驱动程序)内的sys_open ,即在内核的驱动链表里面根据设备名和设备号查找到相关的设备驱动函数(每一个驱动函数是一个节点)
  5. 驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能。

注意:

  • 从用户态到内核态会携带参数进入,通过指令携带参数的方式,或在寄存器里面存值
    树莓派——linux内核与驱动_第6张图片
    内核根据用户态传递进入的值去判断调用什么函数, sys_call_table 是函数指针数组
    树莓派——linux内核与驱动_第7张图片

内核的 sys_open 、sys_read 会做什么?

树莓派——linux内核与驱动_第8张图片

驱动程序开发步骤

驱动开发无非以下两件事:

  1. 编写完驱动程序,加载到内核 ,为其添加设备号和设备名、 设备驱动函数
  2. 用户空间open设备名后,调用驱动程序,驱动程序通过操作寄存器来驱动IO口

Linux 内核就是由各种驱动组成的,内核源码中有大约 85%是各种驱动程序的代码。内核中驱动程序种类齐全,可以在同类驱动的基础上进行修改以符合要求的驱动。

编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有【驱动程序的框架】,在这个框架中加入这个硬件。

一般来说,编写一个 linux 设备驱动程序的大致流程如下:

  • 查看原理图、数据手册,了解设备的操作方法;
  • 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
  • 实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序;
  • 设计所要实现的操作,比如 open、close、read、write 等函数;
  • 实现中断服务(中断并不是每个设备驱动所必须的);
  • 编译该驱动程序到内核中,或者用 insmod 命令加载;
  • 测试驱动程序;

字符设备驱动

下面就以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。

基于驱动框架的代码开发【不涉及硬件操作】

上层调用代码

#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");
 

驱动框架设计流程

  1. 确定主设备号

  2. 定义结构体 类型 file_operations

  3. 实现对应的 drv_open/drv_read/drv_write 等函数,在 file_operations 结构体中注册相应的函数

  4. 实现驱动入口:安装驱动程序时,就会去调用这个入口函数,执行工作:
    ① 创建设备号:MKDEV
    ① 把 file_operations 结构体告诉内核:注册驱动程序:register_chrdev.
    ② 创建类class_create.
    ③ 创建设备节点device_create.

  5. 实现驱动出口:卸载驱动程序时,就会去调用这个出口函数,执行工作:

    ① 把 file_operations 结构体从内核注销:unregister_chrdev.
    ② 销毁类class_create.
    ③ 销毁设备结点device_destroy.

  6. 其他完善:GPL协议、入口加载、出口加载

注意:

  1. file_operations 结构体如下图所示,大部分为函数指针,并不是所有的成员都需要赋初始值,可以只给上层应用所调用的函数添加对应的成员【在keil编译器中不能这样使用】
    树莓派——linux内核与驱动_第9张图片

  2. static关键字设置驱动代码中变量的作用域,防止命名冲突【有静态函数,静态全局变量】

驱动模块代码编译和测试

驱动模块代码编译(模块的编译需要配置过的内核源码。放在指定的目录下,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)

操作步骤:

  1. 由于操作的是字符设备驱动,所以将该驱动代码pin4test.c拷贝到ubuntu的 linux-rpi-4.14.y/drivers/char 目录下的文件夹中(也可选择设备目录下其它文件)

  2. 修改该文件夹下Makefile文件(驱动代码放到哪个目录,就修改该目录下的Makefile),将字符驱动代码编译为模块,文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),因为最终是要将模块加载到开发板上而不是虚拟机中,所以要生成模块然后移植到开发板挂载,所以只需要将obj-m += pin4drive.o添加到Makefile中即可。
    树莓派——linux内核与驱动_第10张图片

  3. 回到linux-rpi-4.14.y/编译驱动文件

    • 使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。
    • 编译完成后在/drivers/char目录下会生成以下几个文件:
      • 树莓派——linux内核与驱动_第11张图片

      • .o的文件是object文件,.ko是kernel object

  4. 将生成的.ko文件发送给树莓派

  5. 将上层调用代码(pin4test.c)进行 交叉编译后发送给树莓派,

  6. 就可以看到树莓派目录下存在发送过来的pin4driver.ko文件pin4test这两个文件, 都已经完成编译

  7. 打开树莓派,执行加载内核驱动sudo insmod pin4drive.ko。此时dev下就有该驱动设备名称

    • 加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中) 加载完成后就可以在dev下面看到名字为pin4的设备驱动(这个和驱动代码里面static char *module_name=“pin4”; //模块名这行代码有关),设备号也和代码里面相关。
  8. lsmod查看系统的驱动模块是否完成

  9. 执行编译后的文件./pin4test

  10. 若权限不够则使用chmod 666 /dev/pin4

  11. 查看内核打印的信息, dmesg |grep pin4

  12. 在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写.ko)将驱动卸载。

驱动的硬件代码编写

查看树莓派芯片型号cat /proc/cpuinfo,该树莓派CPU型号为BCM2835,包括四个核

树莓派——linux内核与驱动_第12张图片

硬件寄存器部分代码需要看电路图(为了找寄存器)和芯片手册,树莓派芯片手册给出了IO口对应的寄存器

  1. 树莓派有54个引脚,即I/O口
    树莓派——linux内核与驱动_第13张图片
  2. 树莓派有GPIO有41个寄存器,都是用来管理GPIO的。每个寄存器都是32位。Address表示的是总线地址,而不是寄存器地址

树莓派——linux内核与驱动_第14张图片
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表示输入
树莓派——linux内核与驱动_第15张图片
树莓派——linux内核与驱动_第16张图片
4. 通过设置GPSET0和GPCLR0可以将输出口置0或置1,
树莓派——linux内核与驱动_第17张图片
输出寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
树莓派——linux内核与驱动_第18张图片
GPCLRn是清零功能寄存器。输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。
树莓派——linux内核与驱动_第19张图片
树莓派——linux内核与驱动_第20张图片
这里的置1不是输出1,而是启动该寄存器,按照功能的设定输出0,同样的,清除功能寄存器的第四比特位置1并不是输出1,而是驱动第四引脚的电平拉低,完成清除功能
在这里插入图片描述

  1. 寄存器地址问题:
  • IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。特别注意,不同的CPU型号的起始地址不同,起始地址写错可能会出现“段错误”的原因。根据总线地址可以得出相对于起始地址的偏移量

树莓派——linux内核与驱动_第21张图片

  1. 虚拟地址与物理地址转换问题:
  • 写的是物理地址,需要经过转换为虚拟地址,可以在初始化函数内重新赋值,使用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空间和权限有关的标志

返回:

  • 该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

注意:

  • gpio readall查看BCM才是芯片手册上的所写的引脚,板子上的引脚和树莓派的引脚不一样,要区分,写应用的时候看wPi引脚写驱动的时候看BCM引脚
    树莓派——linux内核与驱动_第22张图片
  • 通过cat /proc/iomen可以查看物理地址与虚拟地址的映射关系【下图仅是个例子,非此开发板】
    树莓派——linux内核与驱动_第23张图片
  1. 关于设定比特位问题:
  • 进行取反后再进行按位与操作是为了不影响其他引脚
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设置功能模式为输出模式
  • 应用层的写,对应内核层的写,对应硬件的输出高低电平
volatile关键字的使用
  • 在此处的作用:防止编译器优化(可能是省略,也可能是更改)这些寄存器地址变量,常见于在内核中对IO口进行操作

  • 作用:确保指令不会因编译器的优化而省略,且要求每次直接读值,在这里的意思就是确保地址不会被编译器更换

copy_from_user和copy_to_user 函数的用法
  • copy_from_user和copy_to_user这两个函数分别是将用户空间的数据拷贝到内核空间以及将内核空间中的数据拷贝到用户空间

函数copy_from_user原型:

copy_from_user(void *to, const void __user *from, unsigned long n)

返回值:

  • 失败返回没有被拷贝成功的字节数,
  • 成功返回0

参数详解:

  1. to 将数据拷贝到内核的地址,即内核空间的数据目标地址指针
  2. from 需要拷贝数据的地址,即用户空间的数据源地址指针
  3. n 拷贝数据的长度(字节)
    也就是将@from地址中的数据拷贝到@to地址中去,拷贝长度是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;
}

你可能感兴趣的:(树莓派,linux,运维,服务器)