树莓派基于Linux内核驱动开发详解

目录

  • 一、驱动认知
    • 1.1 为什么要学习写驱动
    • 1.2 文件名与设备号
    • 1.3 open函数打通上层到底层硬件的详细过程
  • 二、基于框架编写驱动代码
    • 2.1 编写上层应用代码
    • 2.2 修改内核驱动框架代码
    • 2.3 部分代码解读
      • 2.3.1 static的作用
      • 2.3.2 结构体成员变量赋值方式
      • 2.3.3 结构体file_operations(最终加载到内核驱动链表)
      • 2.3.4 手动生成设备
  • 三、驱动代码编译和测试
    • 3.1 驱动框架的模块编译并发送至树莓派
      • ①Makefile内添加生成.o命令
      • ②模块编译生成.ko文件
      • ③把.ko文件发送至树莓派
    • 3.2 上层代码交叉编译发送至树莓派
    • 3.3 树莓派装载驱动并运行
      • ①树莓派加载内核驱动(insmod)
      • ②运行上层代码(无权限)
      • ③增加访问权限再运行
      • ④检查是否执行成功:demsg指令查看内核打印信息
  • 四、三种地址介绍
    • 4.1 总线地址
    • 4.2 物理地址
    • 4.3 虚拟地址
    • 4.4 MMU内存管理单元
  • 五、IO口驱动编程实战
    • 5.1 芯片手册导读
      • 5.1.1 General Purpose I/O (GPIO)板块
      • 5.1.2 Register View 导读
      • 5.1.3 配置引脚功能为输入/输出的寄存器
      • 5.1.4 配置引脚输出0/1的寄存器
      • 5.1.5 配置引脚清除0/1状态的寄存器
      • 整理关键内容
    • 5.2 寄存器地址配置(ioremap、volatile物理地址映射成虚拟地址)
    • 5.3 寄存器功能配置
    • 5.4 解除虚拟地址映射(iounmap)
    • 5.5 完整代码
    • 5.6 交叉编译并发送至树莓派
    • 5.7 树莓派装载驱动
    • 5.8 运行上层应用文件
      • 5.8.1 驱动运行成功
      • 5.8.2 驱动运行失败:学会调试
  • 六、其他
    • 简单了解DMA
    • md5sum检查两个文件是否完全一样

一、驱动认知

1.1 为什么要学习写驱动

树莓派开发简单是因为有厂家提供的wiringPi库,实现超声波,实现继电器操作,做灯的点亮…都非常简单。

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

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

用树莓派学习的目的不仅是为是体验其强大便捷的wiringPi库,更要通过树莓派学会linux内核开发,驱动编写等,做一个属于自己的库。

1.2 文件名与设备号

linux一切皆为文件,其设备管理同样是和文件系统紧密结合。在目录/dev下都能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动,那么open怎样区分这些硬件呢?

依靠文件名与设备号。在/devls -l可以看到

在这里插入图片描述

设备号又分为:主设备号用于区别不同种类的设备;次设备号区别同种类型的多个设备

内核中存在一个驱动链表,管理所有设备的驱动。 驱动开发无非以下两件事:

  • 编写完驱动程序,加载到内核
  • 用户空间open后,调用驱动程序(驱动程序就是操作寄存器来驱动IO口,单片机51,32就是这种操作)

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

1.3 open函数打通上层到底层硬件的详细过程

  • 用户空间调用open(比如open(“/dev/pin4”,O_RDWR))产生一个软中断(中断号是0x80),进入内核空间调用sys_call,这个sys_call在内核里面是汇编的,用Source Insight搜索不到。

  • sys_calll真正调用的是sys_open(属于VFS层虚拟文件系统,因为磁盘的分区和引脚分区不一样,为了实现上层统一化),根据你的设备名比如pin4去到内核的驱动链表,根据其主设备号与次设备号找到相关驱动函数

  • 调用驱动函数里面的open,这个open就是对寄存器的操作,从而设置IO口引脚电平。这件事对于单片机来说特变容易,就两句话搞定:

sbit pin4 = P1^4;
pin4=1;

(对应下图的粉色笔迹)

在这里插入图片描述

二、基于框架编写驱动代码

2.1 编写上层应用代码

目的是用简单的例子展示从用户空间到内核空间的整套流程

在上层访问一个设备跟访问普通的文件没什么区别。试写一个简单的open和write去操作设备"pin4"。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>int main()
{int fd;fd = open("/dev/pin4",O_RDWR);if(fd < 0){printf("open failed\n");perror("reson");}else{printf("open success\n");}fd = write(fd,'1',1);//写一个字符'1',写一个字节return 0;
}

根据上面提到的驱动认知,有个大致的概念,以open为例子:
上层opensys_callsys_open→内核驱动链表节点→执行节点里的open

当然,没有装载驱动的话这个程序执行一定会报错。只有在内核装载了驱动并且在/dev下生成了“pin4”这样一个设备才能运行。

接下来介绍最简单的字符设备驱动框架。

2.2 修改内核驱动框架代码

所谓框架,就是在往驱动链表里面加驱动的时候要符合内核规则,它是定死的东西,基本的语句必须要有,少一个都不行。

虽然有这么多的代码,但核心运行的就两个printk。

#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";   //模块名 上层的名字//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数,和printf类似return 0;
}//pin4_write函数  因为上层需要open和write这两个函数 
//            如果上层需要调用read等其他函数,可用SourceInsight去内核源码搜索,照着格式修改即可使用 在file_operations结构体里面
static ssize_t pin4_write(struct file *file1,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, .open  = pin4_open,  //上层有读 底层就要有open的支持.write = pin4_write, //上层有写 底层就要有write的支持
};int __init pin4_drv_init(void)   //驱动的真正入口
{int ret;devno = MKDEV(major,minor);//创建设备号 //********************注册驱动 加载到内核驱动链表***********//主设备号231 模块名pin4 上面的结构体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);       //删除设备  /dev底下的 上面也是创建了设备和类class_destroy(pin4_class);              //删除类unregister_chrdev(major, module_name);  //卸载驱动 就是删除链表节点的驱动}module_init(pin4_drv_init);  //入口:内核加载驱动的时候,这个宏(module_init它不是个函数)会被调用,而真正的驱动入口是它里面调用的函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

2.3 部分代码解读

2.3.1 static的作用

内核代码数量庞大,为了防止与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。内核源码里面运用了大量的static,因为内核源码众多,一万五千多个C文件,很容易造成代码命名的冲突。

2.3.2 结构体成员变量赋值方式

static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,
};

这是内核代码中常见的对结构体的操作方式,单独给指定结构体某些元素赋值。

注意:在keil的编译工具环境中不允许这样写,linux可以。

2.3.3 结构体file_operations(最终加载到内核驱动链表)

在SourceInsight中查看结构体file_operations,可以发现很多的函数指针(指向函数的指针,函数内进行一些程序的执行),这些函数名跟系统上层对文件的操作差不多。(read,write,llseek)(在课程视频9:36)

在这里插入图片描述

如果上层想要实现read,就复制过来,按照格式改一改就能使用。

上层对应底层,上层想要用read,底层就要有read的支持

2.3.4 手动生成设备

框架中有自动生成设备的代码,那么手动生成设备是怎么样的呢?(一般不这样干,麻烦,仅作为演示)

  • 进入/dev目录,查看帮助可知道创建规则

sudo mknod 设备名称 设备类型 主设备号 次设备号
在这里插入图片描述

  • 使用如下命令创建名称为zhu,主设备号为8,次设备号为1的字符设备。
sudo mknod zhu c 8 1
  • 1
ls -l可以看到已经创建成功 在这里插入图片描述

三、驱动代码编译和测试

3.1 驱动框架的模块编译并发送至树莓派

在ubuntu中,进入Linux内核源码(前一章节编译好的)字符设备驱动目录linux-rpi-4.14.y/drivers/char(IO口属于字符设备驱动)。进入源码目录下的原因是,写驱动必须要链接到源码(源码定义好了结构体等等),必须要有源码

拷贝上文分析过的驱动框架代码,拿到这个文件夹下 ,并创建成名字为pin4drive.c的文件

①Makefile内添加生成.o命令

进行配置,使得工程编译时可以编译这个文件

vi Makefile
  • 1

当然不一定要放在/char下。但注意:放在哪个文件夹下,就修改那个文件夹的Makefile即可。

Makefile:
在这里插入图片描述

模仿这些文件的编译方式,以编译成模块的形式(还有一个方式为编译进内核)编译pin4drive.c

在Makefile里面添加:

obj-m                           += pin4drive.o
  • 1

-m就是模块的形式

如图:

在这里插入图片描述

②模块编译生成.ko文件

之前编译内核镜像的时候用的是这个命令:

在这里插入图片描述现在只需进行模块编译,不需要生成zImage,dtbs文件;

  • 回到源码目录/linux-rpi-4.14.y再执行下面指令
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

注:如果说编译中途提示出错,照着错误提示去修改.c文件即可,和上层编译类似。

编译完成生成的一些文件如下:
在这里插入图片描述

③把.ko文件发送至树莓派

scp pin4drive.ko pi@192.168.101.19:/home/pi

之前犯的一个小错误是树莓派和ubuntu的ip地址一样,导致连接不上,修改树莓派的ip地址即可

3.2 上层代码交叉编译发送至树莓派

拷贝上文分析的上层代码到ubuntu中,此处我命名为pin4drivertest.c

使用交叉编译工具进行编译

arm-linux-gnueabihf-gcc pin4drivertest.c -o pin4test

发送至树莓派

scp pin4test pi@192.168.101.19:/home/pi

3.3 树莓派装载驱动并运行

①树莓派加载内核驱动(insmod)

sudo insmod pin4drive.ko
  • 查看是否已经成功添加驱动

可以去设备下查看

ls /dev/pin4 -l

看到驱动添加成功,主设备号231,次设备号0,和内核里面的代码对应上。
在这里插入图片描述

或者lsmod查看内核挂载的驱动
在这里插入图片描述

如果需要卸载驱动,就sudo rmmod pin4drive

②运行上层代码(无权限)

./pin4test

发现没有对设备pin4的访问权限
在这里插入图片描述

crw是超级用户所拥有的权限,而框中两类用户则无读写的权限(下面有详细说明)

在这里插入图片描述

③增加访问权限再运行

解决方法1:加超级用户

sudo ./pin4test
  • 1

解决方法2:增加“所有用户都可以访问的权限”(建议)

sudo chmod 666 /dev/pin4
  • 1

运行成功:
在这里插入图片描述

拓展 >> chmod 命令用于更改文件/文件夹的属性(读,写,执行)

permission to:  user(u)   group(g)   other(o)     /¯¯¯\      /¯¯¯\      /¯¯¯\
octal:            6          6          6
binary:         1 1 0      1 1 0      1 1 0
what to permit: r w x      r w x      r w xwhat to permit - r: read, w: write, x: executepermission to  - user: the owner that create the file/foldergroup: the users from group that owner is memberother: all other users

EG: chmod 744 仅允许用户(所有者)执行所有操作,而组和其他用户只允许读。

④检查是否执行成功:demsg指令查看内核打印信息

用dmesg命令显示内核缓冲区信息,并通过管道筛选与pin4相关信息

dmesg | grep pin4
  • 1

可以看到这两个打印信息,说明内核的printk已经被成功调用,我们已经成功完成了上层对内核的调用 !

在这里插入图片描述

四、三种地址介绍

写驱动是为了操作IO口,实现自己的wiringpi库,跟硬件打交道。

首先要理解以下3个地址的概念:

4.1 总线地址

通俗来说:cpu能够访问的内存范围

现象:电脑装了32位(bit)的系统,明明内存条有8G,却只能识别3.8G左右,这是因为32位仅能表示/访问2^32^=4,294,967,296bit=4,194,304Kb=4096Mb=4G左右。只有装了64位的,才能够识别到8G。32位、64位是计算机CPU一次处理数据能力的大小。

树莓派装载32位操作系统,寻址自然是4G。

树莓派的内存

cat /proc/meminfo
  • 1

大概是927M
在这里插入图片描述

4.2 物理地址

硬件实际地址或绝对地址,就是硬盘上的排列地址

4.3 虚拟地址

又叫逻辑地址(基于算法的地址,软件层面的地址,是假地址)便称为虚拟地址

虚拟地址的作用:

以树莓派为例,总线可以访问4G,物理地址只有1G,但需要运行的程序大于1G,如果把程序全部都加载到内存是不可取的。

物理地址数据的运行真正是拿虚拟地址来操作的,虚拟地址可以比1G大,总线地址(CPU能访问的地址范围)能看到4个G,就可以把1个G的物理地址映射成4个G的虚拟地址。当物理地址不能满足程序运行空间需求时,如果没有虚拟地址,程序就不能正常运行。单片机51和STM32如果程序过大,是禁止你烧写的,而在Linux系统环境下是可以的。

树莓派3b的cpu型号是BCM2835,它是ARM-cotexA53架构

cat /proc/cpuinfo
  • 1
在这里插入图片描述

4.4 MMU内存管理单元

  • 地址框图

可以看到总线地址为FF FF FF FF,即为4G;

在这里插入图片描述

  • 内核的页表映射

在这里插入图片描述
物理地址的1M通过映射成为4M的虚拟地址(我们写的所有的代码都是在操控虚拟地址,都是假的),这中间有个设计的算法叫页表。

这个表决定了这个4M被映射到虚拟内存的哪一个段,通过MMU进行管理。单片机和ARM处理器的区别就是ARM有MMU(内存管理单元)和CACHE(高速缓存),如下图所示:

在这里插入图片描述

如果想要更多地了解Linux对内存的管理,推荐书《unix设计与实现》,类似内核设计文档,讲的是内核设计的思路,讲代码讲的不多。

五、IO口驱动编程实战

之前驱动框架的代码仅仅用来检测、走一遍整个驱动的架构。那么在这一个小节,将着手实现pin4引脚输出高 / 低电平。

驱动开发两大利器:芯片手册和电路图(电路图主要用来寻找寄存器,树莓派的芯片手册清楚地给出了各个寄存器,所以电路图很难找到)。

5.1 芯片手册导读

5.1.1 General Purpose I/O (GPIO)板块

查看芯片手册的目的性很强:做哪一块的开发,就只看那一块,现在要开发的是GPIO,熟悉控制IO口的寄存器最为重要。

如果看完这部分的文档,你对于以下几个问题(后面有解析)有清晰的答案,说明你真正读懂了这一部分的开发。

①操作逻辑:简言之就是怎么进行配置相关寄存器,这些配置步骤和思想其实都很类似。
②需要重点掌握的寄存器有哪些?例如输入 / 输出控制寄存器输出 0 / 1控制寄存器清除状态寄存器

5.1.2 Register View 导读

在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,0/1,怎么清除,上升沿下降沿等。(配置过32 / 51寄存器的应该对这些很熟悉)

从下图中可以大概了解到所有的IO口被分成了0~5组。

有意思的是,下图最第一列的地址Address是树莓派总线地址,一般芯片手册给的都是真正的物理地址。第二列是寄存器的名字,第三列寄存器功能描述。

一共有41个寄存器,每个寄存器都是32位。
在这里插入图片描述

在这里插入图片描述

描述部分也很重要,大多涉及使用方法
在这里插入图片描述

5.1.3 配置引脚功能为输入/输出的寄存器

这20~29的IO口(第二列)属于分组2

在这里插入图片描述

IO编号要看好

在这里插入图片描述

5.1.4 配置引脚输出0/1的寄存器

在这里插入图片描述

5.1.5 配置引脚清除0/1状态的寄存器

在这里插入图片描述

整理关键内容

通过文档阅读,可以整理出关键的信息:

有3个最基本的要清楚:
①选择IO是 输入 / 输出控制寄存器:GPFSEL
②输出0 / 1寄存器:GPSET
③清除寄存器:GPCLR

操作逻辑:
GPFSEL0寄存器举例,引脚pin4对应的分组就是第0组(51单片机引脚也是分成第0组、第1组、第2组、第3组)。只要在这个分组下,把14-12位设置为001,就能配置pin4引脚为输出。

总之还是要自己多看多翻阅,这里仅为简单的导读。

5.2 寄存器地址配置(ioremap、volatile物理地址映射成虚拟地址)

①在原来框架的基础上,添加寄存器的定义

volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;

要想写出上面的代码,要掌握以下几点:

弄清楚寄存器的分组

其中寄存器的0表示的是分组,目标操作的IO是pin4,由文档可知,属于寄存器分组0。

volatile的使用

volatile作用是 : 1、防止编译器优化(你给的这个地址编译器可能认为不好,可能会省略,也可能会进行更改)这些寄存器变量;2、要求每次直接从寄存器里读值。由于随着程序的执行,会改变寄存器当中的数据,而读取的都是内存里面的备份数据,数据的时效性没有那么强,读的可能是一个老数据。在内核中对IO口进行操作都要有volatile。

②配置寄存器的地址

在①的基础上,在驱动的初始化pin4_drv_init中添加寄存器地址配置

GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);

要想写出上面的代码,要掌握以下几点:

分别找到几个IO寄存器的物理地址

弄清楚GPIO的物理地址(真实地址)

并不是用下图这个地址来对应GPIO功能选择寄存器0的地址,否则编译后运行会有段错误在这里插入图片描述
IO口的起始地址是0x3f000000(从网上找的,树莓派手册第一列是总线地址),加上GPIO的偏移量0x200000,所以GPIO的实际物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。

继续顺着操作手册找到GPSET0和GPCLR0寄存器的偏移量,见下图。其实寄存器的名字是人为的根据功能命名的,本质是一串客观存在的物理地址。
在这里插入图片描述

特别注意:BCM2708 和BCM2709 IO起始地址不同,BCM2708是0x20000000,BCM2709是0x3f000000

根据偏移值,弄清楚寄存器的物理地址(真实地址)

可以看到寄存器GPSET0相对于GPIO物理地址的偏移值为1C。在这里插入图片描述

同样的方法,寄存器GPCLR0的偏移值为28。

物理地址转换为虚拟地址:ioremap函数

因为无论内核代码还是上层代码操作的都是虚拟地址,代码中直接用物理地址肯定不行,需要进行转换,将IO口寄存器映射成普通内存单元进行访问。

使用函数ioremap:

函数原型:void *ioremap(unsigned long phys_addr, unsigned long size)
phys_addr:要映射的物理地址的基地址;
size:要映射的空间的大小(一个寄存器4个字节);

5.3 寄存器功能配置

①在函数pin4_open中配置pin4为输出引脚

可以看到只要32位寄存器GPFSEL014-12位配置为001,其它位不管,即可配置pin4为输出引脚
在这里插入图片描述
当然直接暴力赋值(0000…001…0000)是不可取的,会把其他的IO口给影响。最好的结果是只改变了14-12位。

运用与(&) / 或(|)运算进行位操作

*GPFSEL0 &= ~(0x6 << 12);//110左移12位 取反 与运算
*GPFSEL0 |= (0x1 << 12); //001左移12位      或运算

想要写出以上代码,必须清楚下面两个步骤

1)、与运算给指定位(14bit、13bit)赋值0,其他不变

为了方便描述,这里把需要“与”上的数称为 “辅助数”。(寄存器中的数是假设的)

在这里插入图片描述

但为了方便(1越少,用计算器换算就越简单)得到这个第13、14位为0的数,选择对辅助数“取两次反“。

第一次取反为: 00000…110…00000

用计算器在二进制BIN中输入110(方便就在这,你要是直接在计算器中输入目标辅助数进行换算,数有多少个1都很吃力!!)

在这里插入图片描述

0110,向左移12位,低位自动补0,则1 1正好对上14、13位。

再取反(~),得到一开始想要的让寄存器的数14、13位与上0的辅助数。

2)、或运算给指定位(12bit)赋值1

是同样的思路,不再赘述

②在函数pin4_write中配置pin4输出 0 / 1(高低电平)

获取上层write函数的值:copy_from_user函数

函数介绍
unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)

此函数将from指针指向的用户空间地址开始的连续n个字节的数据送到to指针指向的内核空间地址,简言之是用于将用户空间的数据传送到内核空间

第一个参数to是内核空间的数据目标地址指针,
第二个参数from是用户空间的数据源地址指针,
第三个参数n是数据长度

如果数据拷贝成功,则返回;否则,返回没有拷贝成功的数据字节数。

根据值来操作IO口

int userCmd;上层写的是整型数1,底层就要对应起来用int.如果是字符则用charcopy_from_user(&userCmd,buf,count);if(userCmd == 1){printk("set 1\n");//内核调试信息*GPSET0 |= 0x1 << 4;
}else if(userCmd == 0){printk("set 0\n");*GPCLR0 |= 0x1 << 4;
}else{printk("cmd error\n");
}

说明(这也是操作逻辑的一部分啦):
①这个GPSET0,0指的是分组,不是设置成低电平。
②左移4位,是因为GPSET0寄存器的第4位对应pin4,只要把第4位设置为1,表示这个寄存器就对pin4发挥作用,设置成高电平,如果是0则 no effct(手册内容)。

5.4 解除虚拟地址映射(iounmap)

退出程序卸载驱动的时候,解除映射:iounmap函数

void iounmap(void* addr)//取消ioremap所映射的IO地址

iounmap(GPFSEL0);  //init是相反的执行顺序
iounmap(GPSET0);
iounmap(GPCLR0);

5.5 完整代码

内核驱动框架

#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";   //模块名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_open\n");  //内核的打印函数,和printf类似//open的时候配置pin4为输出引脚*GPFSEL0 &= ~(0x6 << 12);*GPFSEL0 |= (0x1 << 12);return 0;
}//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用charprintk("pin4_write\n");//获取上层write的值copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据//根据值来执行操作if(userCmd == 1){printk("set 1\n");*GPSET0 |= 0x1 << 4;}else if(userCmd == 0){printk("set 0\n");*GPCLR0 |= 0x1 << 4;}else{printk("cmd error\n");//加入调试信息,方便通过查看内核信息进行修改}return 0;
}static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,
};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);GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);return 0;
}void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{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");

上层应用程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>int main()
{int fd;int cmd;fd = open("/dev/pin4",O_RDWR);if(fd < 0){printf("open failed\n");perror("reson");}else{printf("open success\n");}printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");scanf("%d",&cmd);if(cmd == 0){printf("pin4设置成低电平\n");}else if(cmd == 1){printf("pin4设置成高电平\n");}fd = write(fd,&cmd,1);//写一个字符'1',写一个字节return 0;
}

5.6 交叉编译并发送至树莓派

①树莓派上卸载之前的pin4驱动、删除树莓派上层可执行程序pin4testpin4drive.ko文件

sudo rmmod pin4drive
  • 1

用lsmod查看是否卸载成功。

基本上都会自动卸载驱动的,因为上一节框架代码最后有卸载驱动的代码操作。

②驱动框架模块化方式编译和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派

上文有涉及,一样的操作不再赘述。Makefile文件之前修改过了,这里不用再次进行修改。

注意:

在Ubuntu的/char目录下因为之前的模块编译生成了.ko ,.mod等文件

没关系,直接复制新的驱动框架、新的上层代码到原来的2个.c文件覆盖保存。然后进行交叉编译,新生成的文件会覆盖掉原来的文件。

框架交叉编译后:
红色框表示编译生成了需要的模块
而蓝色框的警告可以不用理会(copy_form_user前两个参数传入的是空类型的指针,在框架代码中没有进行强制转换)
在这里插入图片描述

5.7 树莓派装载驱动

sudo insmod pin4drive.ko
  • 1

用dmesg可以看到内核打印出“驱动装载成功”(打印信息来自框架代码)

在这里插入图片描述

给权限

sudo chmod 666 /dev/pin4

或者

sudo chmod +x /dev/pin4

5.8 运行上层应用文件

./pin4test
  • 1

运行成功!
在这里插入图片描述

5.8.1 驱动运行成功

输入1时,用命令gpio readall查看pin4引脚变化,应为OUT 1

在这里插入图片描述


输入0时,再用命令gpio readall查看pin4引脚变化,应为OUT 0

dmesg打开内核打印界面,可以看到内核的printk已经被调用,配置执行。

在这里插入图片描述

这样就实现了类似WiringPi的库。

5.8.2 驱动运行失败:学会调试

当然,很多情况下不是直接就能够成功运行的,学会调试很重要。

出问题先看上层(因为上层简单,好修改),后看底层。

Mode我们配置的是输出模式,如果是IN,或者ALT2等,说明底层的模式配置出了问题,大多是寄存器的移位没搞明白。

多看看内核的打印信息,write打印信息,变量的值等等。

奇怪的问题

上层代码中,在加入红框中的printf调试信息之前

在这里插入图片描述
会出现这样的错误,内核接收不到由上层传输过来的 0 / 1,或者接收到的不是0 / 1?

在这里插入图片描述gpio readall查看,置为高低电平也失效。

但是凭什么??我加入的只是调试信息罢了,关这个什么事儿?我又没有动cmd的值。

我还以为是偶然性问题,去掉调试信息编译后重新运行,果然还是出现了cmd error

printf有毒?于是在上层随便加了一条printf
在这里插入图片描述运行,功能正常,没有出现cmd error??

啊哈,error了个啥?我很想知道

于是在内核框架代码加了一句

在这里插入图片描述
于是在error时就出现了:

在这里插入图片描述
又注意到一个可能的问题:上层代码中write写入的是一个int型,4个字节,而这里只写了一个,会不会是这个原因呢?
在这里插入图片描述于是去掉有毒的printf,把1改成4,问题解决了。

这时候我又好奇了,想看看write的返回值:
在这里插入图片描述

但发现不管是error,还是成功,写入的返回值都是0。为什么写入成功了,返回值会是0呢?不应该是写入的字节数吗?

在这里插入图片描述
本打算把命令传输由整型数换成字符,但想到还要改底层

会不会是那个用户态向内核态传输指令(copy_from_user)搞的鬼(有一个类型未转换的警告)?

对这个问题的探讨先告一段落,说不定后面会有好的例子,这个问题也就很好解决,不能花太多时间纠结于这个上面,毕竟这是我的第一个驱动程序,以后见多了,可能就理解了。

六、其他

简单了解DMA

通过阅读树莓派博通BCM2835芯片手册发现有DMA(direct memory access)(直接存储器访问)的一些介绍,多阅读这本英文芯片手册不仅能够帮助你了解芯片的一些裸机知识结构,同时也利于快速阅读其他型号的英文芯片手册。

在这里插入图片描述

大数据的快速拷贝单元。

使用cp指令拷贝大文件会很大程度占用CPU资源,DMA是专门用来做协助数据拷贝的微控制器,CPU可以发动DMA使其进行数据的拷贝。

md5sum检查两个文件是否完全一样

多用于检测原来的“同名”文件是否已经被新的替代,也可以用作检查拷贝过程中是否损坏。

md5sum file.c
  • 1

唯一标识,相同则两个文件为同一文件
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/265895.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3.X内核下设备树–platform设备驱动

1。历史的车轮总是向前&#xff0c;技术更替。在linus 同学发出那句 WFK 后内核进入了设备树时代&#xff08;站在驱动工程师角度&#xff09;。 前几天我已经被mach-imx 中的文件折磨的夜不能眠。我终于在一个清晨&#xff0c;喝完一杯咖啡后决定放弃蹩脚的传统device描述方式…

六核cpu安装SQL Server 2005时报错,提示启动服务失败

2019独角兽企业重金招聘Python工程师标准>>> 新买的IBM3650M4的服务器上安装SQL server2005 安装到一半时&#xff0c;报"提示&#xff1a;SQL Server 服务无法启动。"错。 一开始以为是操作系统的问题&#xff0c;先重装了一遍&#xff0c;还是不行&…

网络编程知识预备(1) ——了解OSI网络模型

参考&#xff1a;简单了解OSI网络模型 作者&#xff1a;丶PURSUING 发布时间&#xff1a; 2021-03-18 20:07:09 网址&#xff1a;https://blog.csdn.net/weixin_44742824/article/details/114968802?spm1001.2014.3001.5502 根据需求摘抄自下面这篇文章&#xff0c;内容非常详…

antd表格显示分页怎么取消_真相!Word里怎么也删不掉的文档空白页原来是这样...

大家好&#xff0c;我是你们的海宝老师在Word里&#xff0c;经常会遇到这种情况&#xff1a;文档莫名其妙地多出一个或多个空白页&#xff0c;没啥内容却怎么也删不掉。不着急&#xff0c;咱们来一一分析。1、标题前的空白页像这种标题前面有空白&#xff0c;基本就是【段落】设…

网络编程知识预备(2) ——三次握手与四次挥手、流量控制(滑动窗口)、拥塞控制、半连接状态、2MSL

参考&#xff1a;浅显易懂的三次握手与四次挥手 作者&#xff1a;丶PURSUING 发布时间&#xff1a; 2021-03-19 09:33:20 网址&#xff1a;https://blog.csdn.net/weixin_44742824/article/details/114990198?spm1001.2014.3001.5502 参考&#xff1a;&#xff08;四十七&…

面向对象2

python之路——面向对象进阶 阅读目录 isinstance和issubclass反射setattr  delattr  getattr  hasattr__str__和__repr____del__item系列__getitem__  __setitem__  __delitem____new____call____len____hash____eq__回到顶部isinstance和issubclass isinstance(obj…

DM9000网卡原理与基地址设置

从上面可以看出DM9000的地址总线就一根&#xff0c;它不像CS8900那样地址总线和数据总线都齐全。而这里只有一根地址线(CMD)&#xff0c;16跟数据线&#xff0c;所以可以确定位宽为16位&#xff0c;而地址线为什么只有一根&#xff0c;这是DM9000决定的&#xff0c;看手册可以知…

网络编程知识预备(3) ——SOCKET、TCP、HTTP之间的区别与联系

参考&#xff1a;SOCKET,TCP,HTTP之间的区别与联系 作者&#xff1a;丶PURSUING 发布时间&#xff1a; 2021-03-19 11:54:01 网址&#xff1a;https://blog.csdn.net/weixin_44742824/article/details/114992140?spm1001.2014.3001.5502 参考&#xff1a;TCP连接、Http连接与S…

debian 9 安装后的配置,debian 9 开发环境。

注意&#xff1a;以下命令用sudo或者以root用户进行 一.Xterm&#xff08;在安装KDE桌面情况下&#xff09;的配置&#xff08;可以黏贴&#xff0c;复制&#xff09;&#xff1a; 首先在根目录下编辑文件.Xresources(没有可以创建) rootdebian:~# vim ~/.Xresources rootdebi…

嵌入式RT3070 AP模式移植

环境&#xff1a;ubuntu1~14.04.3 编译器&#xff1a;arm-none-linux-gnueabi-gcc-4.8.3 无线网卡为RT3070&#xff0c;驱动分为STA驱动和SoftAP驱动两种&#xff0c;STA驱动支持无线网卡工作在STA模式下&#xff0c;而SoftAP的驱动支持无线网卡工作在软AP的模式下&#xff0…

Wireshark抓包介绍和TCP三次握手分析

wireshark介绍 wireshark的官方下载网站&#xff1a; http://www.wireshark.org/ wireshark是非常流行的网络封包分析软件&#xff0c;功能十分强大。可以截取各种网络封包&#xff0c;显示网络封包的详细信息。 wireshark是开源软件&#xff0c;可以放心使用。 可以运行在Wind…

网络编程知识预备(4) ——了解应用层的HTTP协议与HTTPS协议

参考&#xff1a;简单了解HTTP协议与HTTPS协议 作者&#xff1a;丶PURSUING 发布时间&#xff1a; 2021-03-15 10:55:13 网址&#xff1a;https://blog.csdn.net/weixin_44742824/article/details/114807328?spm1001.2014.3001.5502 编程实现人脸识别需要基于人工智能平台&…

Django之orm查询

ORM相关 MVC或者MVC框架中包括一个重要的部分&#xff0c;就是ORM&#xff0c;它实现了数据模型与数据库的解耦&#xff0c;即数据模型的设计不需要依赖于特定的数据库&#xff0c;通过简单的配置就可以轻松更换数据库&#xff0c;这极大的减轻了开发人员的工作量&#xff0c;不…

智能家居 (2) ——设计模式的引入

目录设计模式的概念引入工厂模式的实现animal.hmainPro.ccat.cdog.cperson.c工厂模式的功能验证往期文章设计模式的概念引入 工厂模式的实现 所有代码最好在Source Insight下编写&#xff0c;并将所有代码进行关联&#xff0c;方便读写。 animal.h #include <stdio.h>s…

卷积核和全连接层的区别_「动手学计算机视觉」第十六讲:卷积神经网络之AlexNet...

前言前文详细介绍了卷积神经网络的开山之作LeNet&#xff0c;虽然近几年卷积神经网络非常热门&#xff0c;但是在LeNet出现后的十几年里&#xff0c;在目标识别领域卷积神经网络一直被传统目标识别算法(特征提取分类器)所压制&#xff0c;直到2012年AlexNet(ImageNet Classific…

vnc配置备忘录

因为我开发板上要用到Qt&#xff0c;所以我在服务器上安装了Qt的开发环境&#xff0c;为了能远程连接到服务器我安装了VNC&#xff0c; 中间也是遇到了很多问题&#xff0c;比如连接上去后&#xff0c;只显示一个控制台的窗口&#xff0c;只能使用命令行。后来几经周折才搞定先…

网络编程知识预备(5) ——libcurl库安装及其编程访问百度首页

本文为学习笔记&#xff0c;整合课程内容以及下列文章&#xff1a; 其中&#xff0c;libcurl函数库常用字段解读部分&#xff1a; 参考博文&#xff1a;原文地址 作者&#xff1a;冬冬他哥哥 目录Libcurl库简介Libcurl等三方库的通用编译方法三方库使用前通读方法库的配置、编…

智能家居 (9) ——人脸识别摄像头安装实现监控功能

目录摄像头模块安装安装mjpg-streamer库开启监控功能往期文章摄像头模块安装 注意&#xff1a;安装要下电安装&#xff0c;不能带电&#xff01;连接其他硬件模块的时候也是。 安装mjpg-streamer库 树莓派利用pi Camera模块&#xff0c;通过mjpg-streamer软件获取视频&#xf…

设备树和pinctrl粗解

上次文章中 我以DS18b20为例&#xff0c;在设备树中定义了ds18b20的资源&#xff08;device&#xff09;&#xff0c;当时是依葫芦画瓢&#xff0c;没有深入探究&#xff0c;本文主要探讨下pin在设备树中的描述 参考文章&#xff1a;Linux内核中的GPIO系统之&#xff08;3&…

八大排序算法(C语言实现)

摘自&#xff1a;八大排序算法&#xff08;C语言实现&#xff09; 作者&#xff1a;2021dragon 发布时间&#xff1a; 2021-05-16 10:46:37 网址&#xff1a;https://blog.csdn.net/chenlong_cxy/article/details/116563972 目录 直接插入排序希尔排序选择排序堆排序冒泡排序快…