树莓派——linux内核与驱动

2023-11-14

Linux内核基础框架

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

内核结构框图

在这里插入图片描述

名词解释:

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

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

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

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

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

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

linux系统架构

在这里插入图片描述

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

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

  • linux中有两百多个系统调用,在命令中输入man 2 syscalls可以查询,2表示在2类(系统调用类)中查询,具体各类是什么可以在man man中查询
    -在这里插入图片描述

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

设备号又分为:

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

设备号的作用:

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

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

驱动结构框图的认知

在这里插入图片描述

字符驱动代码执行流程:

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

注意:

  • 从用户态到内核态会携带参数进入,通过指令携带参数的方式,或在寄存器里面存值
    在这里插入图片描述
    内核根据用户态传递进入的值去判断调用什么函数, sys_call_table 是函数指针数组
    在这里插入图片描述

内核的 sys_open 、sys_read 会做什么?

在这里插入图片描述

驱动程序开发步骤

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

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

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

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

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

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

字符设备驱动

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

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

上层调用代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
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 <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;  
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编译器中不能这样使用】
    在这里插入图片描述

  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中即可。
    在这里插入图片描述

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

    • 使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。
    • 编译完成后在/drivers/char目录下会生成以下几个文件:
      • 在这里插入图片描述

      • .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,包括四个核

在这里插入图片描述

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

  1. 树莓派有54个引脚,即I/O口
    在这里插入图片描述
  2. 树莓派有GPIO有41个寄存器,都是用来管理GPIO的。每个寄存器都是32位。Address表示的是总线地址,而不是寄存器地址

在这里插入图片描述
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,而是驱动第四引脚的电平拉低,完成清除功能
在这里插入图片描述

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

在这里插入图片描述

  1. 虚拟地址与物理地址转换问题:
  • 写的是物理地址,需要经过转换为虚拟地址,可以在初始化函数内重新赋值,使用ioremap函数完成映射
  • ioremap(物理地址,物理地址字节数);由于地址是32位,也就是4个字节
ioremap宏定义在asm/io.h内:
#include <asm/io.h>				//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引脚
    在这里插入图片描述
  • 通过cat /proc/iomen可以查看物理地址与虚拟地址的映射关系【下图仅是个例子,非此开发板】
    在这里插入图片描述
  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 <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;  
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
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;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

树莓派——linux内核与驱动 的相关文章

  • 在Linux中断上下文中运行用户线程

    我正在编写一些定制的应用程序 并允许更改 Linux 内核中的中断处理程序代码 我有一个用户线程正在等待中断发生 如果发生中断 那么我要做的第一件事就是执行该用户线程 有什么办法让它发挥作用吗 Thanks 创建一个字符设备 这就是内核所做
  • 在 Ubuntu 16.04 上找不到 printf.c

    我最近切换到Ubuntu 16 04 我在用vscode作为 Ubuntu 上的 IDE 我配置了其他语言 但我无法做到这一点C C 我创建c cpp properties json launch json tasks json 当我开始编
  • 每个进程每个线程的时间量

    我有一个关于 Windows 和 Linux 中进程和线程的时间量子的问题 我知道操作系统通常为每个线程提供固定的时间量 我知道时间量根据前台或后台线程而变化 也可能根据进程的优先级而变化 每个进程有固定的时间量吗 例如 如果操作系统为每个
  • 无需 cron 在后台发送邮件

    我想知道是否有一种方法可以运行 PHP 循环 以便在后台向订阅者发送几百封电子邮件 我的目标是格式化新闻通讯 单击发送 然后关闭浏览器或更改页面 当然 发送电子邮件的实际过程将在后台运行 不会因浏览器关闭而中断 我知道这可以通过 cron
  • 劫持系统调用

    我正在编写一个内核模块 我需要劫持 包装一些系统调用 我正在暴力破解 sys call table 地址 并使用 cr0 来禁用 启用页面保护 到目前为止一切顺利 一旦完成 我将公开整个代码 因此如果有人愿意 我可以更新这个问题 无论如何
  • 设置 Apache POI 的路径

    我想创建 Excel 文件并使用 java 程序在该文件中写入数据 That is here http www techbrainwave com p 554我在 java 文件所在的位置提取了 Apache POI 并将该路径包含在路径变
  • Linux shell 脚本中的 while 循环超时

    这工作正常 无限循环 while TRUE do printf done 我在尝试着timeout this while loop与timeout命令 所有这些都不起作用 timeout 5 while TRUE do printf don
  • 归档文件系统或格式

    我正在寻找一种文件类型来存储已退役系统的档案 目前 我们主要使用 tar gz 但从 200GB tar gz 存档中查找并提取几个文件是很麻烦的 因为 tar gz 不支持任何类型的随机访问读取规定 在你明白之前 使用 FUSE 安装 t
  • 在 /dev/input/eventX 中写入事件需要哪些命令?

    我正在开发一个android需要将触摸事件发送到 dev input eventX 的应用程序 我知道C执行此类操作的代码结构如下 struct input event struct timeval time unsigned short
  • PHP 致命错误:未找到“MongoClient”类

    我有一个使用 Apache 的网站 代码如下 当我尝试访问它时 我在 error log 中收到错误 PHP Fatal Error Class MongoClient not found 以下是可能错误的设置 但我认为没有错误 php i
  • 并行运行 make 时出错

    考虑以下制作 all a b a echo a exit 1 b echo b start sleep 1 echo b end 当运行它时make j2我收到以下输出 echo a echo b start a exit 1 b star
  • 在 Linux 中禁用历史记录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 要在 Linux 环境中禁用历史记录 我执行了以下命令 export HISTFILESIZE 0 export HISTSIZE 0 u
  • ansible 重新启动 2.1.1.0 失败

    我一直在尝试创建一个非常简单的 Ansible 剧本 它将重新启动服务器并等待它回来 我过去在 Ansible 1 9 上有一个可以运行的 但我最近升级到 2 1 1 0 并且失败了 我正在重新启动的主机名为 idm IP 为 192 16
  • Locale.getDefault() 始终返回 en

    unix 机器上的服务器始终使用 en 作为默认区域设置 以下是区域设置输出 LANG en US LC CTYPE C LC NUMERIC C LC TIME C LC COLLATE C LC MONETARY C LC MESSAG
  • 为什么我收到“无法进行二进制日志记录”的信息。在我的 MySQL 服务器上?

    当我今天启动 MySQL 服务器并尝试使用以下命令进行一些更改时用于 MySQL 的 Toad http www quest com toad for mysql 我收到此消息 MySQL 数据库错误 无法进行二进制日志记录 消息 交易级别
  • 强制卸载 NFS 安装目录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • GLIBCXX_3.4.26 未找到在 BeagleBone 上运行交叉编译的程序

    我有以下程序 include
  • 从 csv 文件中删除特定列,保持输出上的相同结构[重复]

    这个问题在这里已经有答案了 我想删除第 3 列并在输出文件中保留相同的结构 输入文件 12 10 10 10 10 1 12 23 1 45 6 7 11 2 33 45 1 2 1 2 34 5 6 I tried awk F 3 fil
  • 如何根据 HTTP 请求使用 Python 和 Flask 执行 shell 命令并流输出?

    下列的这个帖子 https stackoverflow com questions 15092961 how to continuously display python output in a webpage 我能够tail f网页的日志
  • Linux TUN/TAP:无法从 TAP 设备读回数据

    问题是关于如何正确配置想要使用 Tun Tap 模块的 Linux 主机 My Goal 利用现有的路由软件 以下为APP1和APP2 但拦截并修改其发送和接收的所有消息 由Mediator完成 我的场景 Ubuntu 10 04 Mach

随机推荐

  • async与await

    async await 是ES7提出的基于Promise的解决异步的最终方案 一 async async是一个加在函数前的修饰符 被async定义的函数会默认返回一个Promise对象resolve的值 因此对async函数可以直接then
  • UI 自动化测试框架:PO 模式+数据驱动 【详解版】

    目录 1 PO 设计模式简介 什么是 PO 模式 PO 模式的优点 2 工程结构简介 工程结构 框架特点 3 工程代码示例 page 包 action 包 business process 包 util 包 conf 包 1 PO 设计模式
  • 基于Centos 7虚拟机的磁盘操作(添加磁盘、分区、格式分区、挂载)

    目录 一 添加硬盘 二 查看新磁盘 三 磁盘分区 3 1新建分区 3 2 格式分区 3 3 挂载分区 3 4 永久挂载新分区 3 5 取消挂载分区 一 添加硬盘 1 在虚拟机处选择编辑虚拟机设置 然后选择添加 2 选择硬盘 然后选择下一步
  • 敏捷宣言以及敏捷开发的特点

    敏捷宣言 敏捷宣言 也叫做敏捷软件开发宣言 正式宣布了对四种核心价值和十二条原则 可以指导迭代的以人为中心的软件开发方法 敏捷宣言强调的敏捷软件开发的四个核心价值是 个体和互动高于流程和工具 工作的软件高于详尽的文档 客户合作高于合同谈判
  • Go_流程、跳转控制语详解

    流程控制语句分类 流程控制语句可以控制代码的执行顺序和条件 顺序结构 普通的代码 由上而下依次执行 分支结构 if switch 循环结构 for if语句 条件表达式值必须是bool类型 不可省略括号 且左大括号不能另起一行 格式1 if
  • AI绘图Midjourney手把手教程

    以下是使用Midjourney AI绘画程序的注册和入门指南 Midjourney是一款功能强大的绘图软件 通过输入一段图片的文字描述即可生成精美的绘画 我们将为您提供详细的操作步骤 让您轻松上手 下面是使用Midjourney AI绘画程
  • Android绘制笔记——Color、Shader

    Android 颜色Color 1 颜色 1 十六进制ARGB颜色值表示 Int color 0xFFFF0000 红色 2 Color类常量 本质为十六进制表示 Int color Color RED 红色 ColorInt public
  • ffmpeg-android dlopen failed: library “libclang_rt.ubsan_standalone-aarch64-android.so“ not found

    NDK编译的ffmpeg 库 在android上使用 提示动态库错误 15 08 54 276 18357 18357 E AndroidRuntime java lang UnsatisfiedLinkError dlopen faile
  • 报时机器人的rasa shell执行流程分析

    本文以报时机器人为载体 介绍了报时机器人的对话能力范围 配置文件功能和训练和运行命令 重点介绍了rasa shell命令启动后的程序执行过程 一 报时机器人项目结构 1 对话能力范围 1 能够识别欢迎语意图 greet 和拜拜意图 good
  • Linux 或者 Docker 容器通过 date 设置系统时间

    目录 1 Linux 2 Docker 容器 2 1 进入容器内部修改 2 2 可能会遇到的问题 1 Linux 要在Linux系统中设置日期和时间 可以使用date命令 使用以下命令格式来设置日期和时间 sudo date s YYYY
  • kubernetes二进制单节点和多节点部署(多节点+dashbord)

    kubernetes二进制单节点和多节点部署 多节点 dashbord kubernetes单节点部署 环境准备 在 master01 节点上操作 上传 master zip 和 k8s cert sh 到 opt k8s 目录中 解压 m
  • YOLOv5 Focus C3 各模块详解及代码实现

    目录 yolov5s yaml yolov5s yaml基本参数含义 一些基本参数 BackBone Head Focus 一 Focus模块的作用 Focus的参数量 Yolov3和Yolov5的改进对比 关于Focus的补充 网络结构图
  • 轻量级人像分割深度学习模型PP-HumanSeg树莓派部署

    人像分割是图像分割领域的高频应用 PaddleSeg推出在大规模人像数据上训练的人像分割系列模型PP HumanSeg 包括超轻量级模型PP HumanSeg Lite 满足在服务端 移动端 Web端多种使用场景的需求 本项目将PP Hum
  • Ceph分布式存储详解

    一 Ceph概述 1 存储发展史 企业中使用存储按照其功能 使用场景 一直在持续发展和迭代 大体上可以分为四个阶段 DAS Direct Attached Storage 即直连存储 第一代存储系统 通过SCSI总线扩展至一个外部的存储 磁
  • ChatGPT泄露用户聊天记录标题;Adobe加入AIGC战局;阿里大模型前带头人杨红霞加盟字节跳动丨每日大事件...

    数据智能产业创新服务媒体 聚焦数智 改变商业 企业动态 诸葛智能推出 诸葛CDP 2 0 等三大产品升级 3月22日 容联云旗下场景化数据智能服务商 诸葛智能 举办2023春季发布会 推出客户数据管理平台 诸葛CDP 2 0 一站式用户行为
  • freemarker动态生成word和pdf

    1 使用freemarker生成word freemarker生成word的方法网上有很多 比较简单 基本上都差不多 所需工具 freemarker
  • Python爬虫之Js逆向案例(3)-X品牌手机社区

    声明 XX手机社区加密逆向分析仅用于研究和学习 这篇文章的学习内容是以XX手机社区为案例 对JS逆向的整个过程进行详细分析 下面会进行以下几步进行分析 下方演示过程全部使用chrome浏览器 锁定关键接口 锁定关键字段 破解关键字段 pyt
  • (Java)leetcode-4 Median of Two Sorted Arrays(寻找两个正序数组的中位数)

    题目描述 给定两个大小为 m 和 n 的正序 从小到大 数组 nums1 和 nums2 请你找出这两个正序数组的中位数 并且要求算法的时间复杂度为 O log m n 你可以假设 nums1 和 nums2 不会同时为空 示例 1 num
  • 三步实现Android任意控件悬浮效果

    Tag 项目介绍 之前做项目的时候实现的一个悬浮效果 如图 可能不够清晰 接下来就是实现效果 如图所示 demo直接用的截图 原理很简单 用RecyclerView addHeaderView的方式实现 实现步骤 1 添加依赖 compil
  • 树莓派——linux内核与驱动

    文章目录 Linux内核基础框架 内核结构框图 linux系统架构 shell 驱动 为什么要学习写驱动 什么是驱动 硬件设备分类 文件名与设备号 驱动结构框图的认知 内核的 sys open sys read 会做什么 驱动程序开发步骤