我们已经准备好了,你呢?

2024我们与您携手共赢,为您的企业形象保驾护航!

目录

向 clc 学习

1. 驱动认知 1.1 为什么要学写驱动?

树莓派开发很简单,因为有厂商提供的库,实现超声波,操作继电器,点亮灯都很简单。

但是以后你可能不会一直用树莓派开发,所以就没有可用的库了。但是只要你能跑Linux,就一定会有Linux标准C库。

学习基于标准C库编写驱动。只要能拿到Linux内核源代码、芯片手册、电路图等就可以进行开发。用树莓派学习的目的不仅仅是体验一下它的强大和方便的库,更希望通过树莓派学习Linux内核开发、驱动编写等,制作自己的库。

1.2 文件名和设备号

Linux中一切都是文件,它的设备管理也和文件系统紧密结合,在/dev目录下可以看到鼠标、键盘、屏幕、串口等设备文件,硬件都要有对应的驱动,那么open是如何区分这些硬件的呢?

取决于文件名和设备号。在/dev下用ls -l可以看到

在这里插入图片描述

设备号分为:主设备号,用于区分不同类型的设备;次设备号,用于区分同一类型的多个设备。

内核中有一个驱动列表,管理所有的设备驱动。驱动开发无非就是下面两件事:

通过设备编号可以检索驱动程序插入到链接列表的位置(顺序)。

1.3 上层与底层硬件连接开放函数详细流程

sbit pin4 = P1^4;
pin4=1;

2.基于框架编写驱动代码 2.1编写上层应用程序代码

目的是用一个简单的例子来展示从用户空间到内核空间的整个过程。

访问上层设备与访问普通文件没什么区别。尝试编写一个简单的 open 和 write 来操作设备“pin4”。

#include 
#include 
#include 
#include 
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;
}

基于上面提到的驾驶员认知,有一个大概的概念,以开放式为例:

上层打开→→→内核驱动链表节点→执行中打开节点

当然,如果没有加载驱动程序,程序就会失败。只有将驱动程序加载到内核并在 /dev 下创建了诸如“pin4”之类的设备后,程序才能运行。

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

2.2 修改内核驱动框架代码

所谓框架,就是在把一个驱动加入到驱动列表的时候,必须符合内核的规则,是一个固定的东西,基本的语句都要有,缺一个都不能缺。

虽然代码那么多,但是运行的核心只有两个。

#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";   //模块名 上层的名字
//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 功能

内核代码庞大,并且为了防止变量命名与其他文件冲突,变量的作用范围只限制在本文件中。内核源代码使用了大量C文件,非常容易造成代码命名冲突。

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

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

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

注意:这在Keil编译器环境中是不允许的,但在Linux中是允许的。

2.3.3 结构(最终加载到内核的驱动列表)

在结构体中,可以发现很多函数指针(指向函数的指针,有些程序就是在函数中执行的),这些函数名类似于系统上层对文件的操作(read,write)(课程视频9:36)

内核编译是什么意思_内核编译要多久_linux内核 编译 arm

如果上层要实现读取,只要复制一份,按照格式修改一下就可以使用了。

上层和底层是对应的,上层要用read,底层就必须有read的支持。

2.3.4 手动生成设备

框架中有自动生成设备的代码,那么手动生成设备怎么办?(我一般不这么做,麻烦,只是为了演示)

sudo mknod 设备名称 设备类型 主设备号 次设备号

sudo mknod zhang c 3 1

使用ls -l查看已经创建成功

3.驱动代码编写及测试 3.1驱动框架模块编写并发送到树莓派

在 中,进入Linux内核源代码(上一章编译好的)字符设备驱动目录linux-rpi-4.14.y//char(IO口属于字符设备驱动)。之所以要进入源代码目录,是因为写驱动是要和源代码挂钩的(源代码中定义结构体等),而且要有源代码。

将上面分析的驱动框架代码复制过来,放在此文件夹下,并建立名为.c的文件

① 添加生成.o的命令

配置使得此文件可以在项目编译时被编译

vi Makefile

当然,不一定要放在/char下。但请注意:你可以修改放置它的文件夹。

模仿这些文件的编译方式,编译成模块(还有一种方式就是编译进内核)

在里面添加:

obj-m                           += pin4drive.o

-m 是模块的形式

如图所示:

在这里插入图片描述

② 模块编译生成.ko文件

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

在这里插入图片描述

现在只需要编译模块,无需生成dtbs文件;

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

注意:如果编译时出现错误信息,按照错误信息修改.c文件即可,类似上层编译。编译后生成以下文件:

linux内核 编译 arm_内核编译要多久_内核编译是什么意思

③将.ko文件发送到树莓派

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

之前犯的一个小错误就是树莓派的IP地址和电脑的IP地址一样,导致连接不上,只要改一下树莓派的IP地址就可以了。

3.2 交叉编译上层代码并送入树莓派

将上面分析的上层代码复制到 ,我将其命名为 .c

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

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

发送到 Pi

scp pin4test pi@192.168.101.19:/home/pi

3.3 树莓派加载驱动并运行 ① 树莓派加载内核驱动()

sudo insmod pin4drive.ko

您可以在设备下检查

ls /dev/pin4 -l

我们看到驱动添加成功,主设备号为231,次设备号为0,与内核中的代码相对应。

在这里插入图片描述

或者 lsmod 查看内核挂载的驱动

在这里插入图片描述

如果需要卸载驱动程序,只需 sudo rmmod

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

./pin4test

发现无法访问设备 pin4

在这里插入图片描述

③添加访问权限并再次运行

解决方案 1:添加超级用户

sudo ./pin4test

解决方案 2:添加“所有用户均可访问”(推荐)

sudo chmod 666 /dev/pin4

运行成功:

扩展 >> 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 x
what to permit - r: read, w: write, x: execute
permission to  - user: the owner that create the file/folder
                 group: the users from group that owner is member
                 other: all other users

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

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

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

dmesg | grep pin4

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

4. 三种地址类型介绍

写驱动的目的是为了操作IO口,实现自己的库,与硬件进行交互。

首先我们需要了解以下三个地址的概念:

4.1 总线地址

通俗的说就是:CPU可以访问的内存范围

现象:电脑装的是32位系统,内存条虽然有8G,但是只能识别到3.8G左右,这是因为32位只能表示/访问2^32^=4,294,967,=4,194,304Kb==大约4G,只有装了64位系统才能识别到8G,32位和64位指的是电脑CPU一次性处理数据的能力大小。

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

Pi 的内存

cat /proc/meminfo

约9.27亿

在这里插入图片描述

4.2 物理地址

实际硬件地址或者绝对地址就是硬盘上的地址。

4.3 虚拟地址

又叫逻辑地址(基于算法的地址、软件层面的地址、假地址)叫虚拟地址

虚拟地址的作用:

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

对物理地址数据的操作其实就是对虚拟地址的操作,虚拟地址可以大于1G,总线地址(CPU可访问的地址范围)能看到4G,所以1G的物理地址可以映射到4G的虚拟地址。当物理地址不能满足程序运行空间需求时,如果没有虚拟地址,程序就无法正常运行。单片机51、STM32如果程序太大是禁止烧写的,但在Linux系统环境下是可以的。

Pi 3b 的 CPU 型号为 ARM-

cat /proc/cpuinfo

4.4 MMU内存管理单元

可以看到总线地址是FF FF FF FF,也就是4G;

在这里插入图片描述

1M的物理地址映射到4M的虚拟地址(我们写的代码都是操作虚拟地址的,都是假的),这中间有一个设计好的算法,叫页表。

这个表决定了这4M映射到虚拟内存的哪一段,由MMU来管理。单片机和ARM处理器的区别在于ARM有MMU(内存管理单元)和CACHE(缓存),如下图所示:

如果想进一步了解Linux的内存管理,我推荐《Unix设计与实现》这本书,它类似于一个内核设计文档,讲的是内核设计的思想,不太讲代码。

5.IO口驱动编程实践

前面的驱动框架代码只是用来检测和走一遍整个驱动架构,本节我们开始实现pin4的高/低电平输出。

驱动开发两大利器:芯片手册、电路图(电路图主要用来找寄存器,树莓派的芯片手册把各个寄存器都给的很清楚了,电路图比较难找)。

5.1 芯片手册介绍5.1.1 I/O(GPIO)模块

看芯片手册有一个很强的目的性:只看你开发的部分。现在我们要开发GPIO,所以熟悉控制IO口的寄存器是最重要的。

如果读完这部分文档之后,你对下面的问题有了明确的答案(后面会有解释),那么说明你真正理解了开发的这一部分。

① 操作逻辑:简单来说就是如何配置相关的寄存器,这些配置步骤和思路其实很类似。

②需要掌握哪些寄存器?例如输入/输出控制寄存器、输出0/1控制寄存器、清除状态寄存器

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

从下图我们可以粗略的了解到所有的IO端口被分为0~5组。

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

总共有41个寄存器,每个寄存器32位。

在这里插入图片描述

描述部分也很重要,主要涉及如何使用

在这里插入图片描述

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

IO 端口 20 至 29(第二列)属于第 2 组。

在这里插入图片描述

注意IO数量

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

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

在这里插入图片描述

整理重点内容

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

有三件最基本的事情需要明确:

①选择IO作为输入/输出控制寄存器:

② 输出0/1寄存器:GPSET

③ 清除寄存器:GPCLR

操作逻辑:

以寄存器为例,4号引脚对应的组为0组(51单片机的引脚也分为0组、1组、2组、3组),只要将该组中的14-12位设置为001,就可以将4号引脚配置为输出。

总之还是要自己多看一些,这里只是简单的介绍。

5.2 寄存器地址配置(物理地址映射到虚拟地址) ①在原有框架基础上增加寄存器定义

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

要编写上述代码,需要掌握以下几点:

弄清楚寄存器的分组

寄存器0代表组,目标操作的IO是pin4,根据文档,属于寄存器组0。

使用

添加的作用是:1.防止编译器对这些寄存器变量进行优化(编译器可能会认为你给的地址不好,可能会省略,也可能会改变);2.要求每次都直接从寄存器中读取值。随着程序的执行,寄存器中的数据会发生变化,而且读到的数据是内存中的备份数据,对时效性没那么敏感,读到的数据可能是旧数据。这对于内核中操作IO口是必须的。

②配置寄存器地址

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

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口的起始地址为(网上找的,树莓派手册第一栏就是总线地址),加上GPIO偏移量,所以GPIO的实际物理地址应该就是起始地址,然后在此基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。编程就是操作虚拟地址。

继续按照手册找到寄存器的偏移量,如下图所示。其实寄存器的名字都是人为根据功能命名的,其实质是一串客观的物理地址。

特别说明:与IO起始地址不同,是的,是的

根据偏移值,计算出寄存器的物理地址(真实地址)

可以看出该寄存器相对GPIO物理地址的偏移值为1C。

在这里插入图片描述

同理,寄存器偏移值为28。

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

因为内核代码和上层代码都是对虚拟地址进行操作的,所以在代码中直接使用物理地址肯定是不行的,需要经过转换才能将IO端口寄存器映射成普通的内存单元进行访问。

使用函数:

函数原型:void *(long, long size)

:需要映射的物理地址的基地址;

size:要映射的空间的大小(每个寄存器 4 个字节);

5.3 寄存器功能配置 ① 在功能中配置pin4为输出引脚

可以看出只要将32位寄存器的14-12位配置为001,其他位忽略,将pin4配置为输出引脚即可。

当然,不建议直接赋值(0000...001...0000),因为会影响其他IO口,最好的结果是只改变14-12位。

使用 AND (&) / OR (|) 运算符执行按位运算

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

要编写上述代码,您必须了解以下两个步骤

1)AND运算将0赋给指定位(14bit,13bit),其余保持不变

为了方便描述,将需要进行“与”运算的数称为“辅助数”。(寄存器中的数是假设的)

在这里插入图片描述

但为了方便(1越少,用计算器换算越方便),我们选择对辅助数“两次反转”,得到第13和第14位为0的数。

第一次反转是:00000…110…00000

用计算器把110输入到二进制BIN中(这里就很方便了,如果直接把目标辅助数输入计算器进行换算的话,会很难算出有多少个1!!)

0110,左移12位,并自动用0填充低位,因此1 1与第14位和第13位完全匹配。

然后对其取反(~),得到我们一开始想要的辅助数,也就是用高位0代替寄存器的第14位和第13位。

2)或运算将1分配给指定位(12位)

思路是一样的,不需要重复

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

获取上层写入函数的值:函数

功能介绍

长整型(void * 至,const void * 来自,长整型 n)

该函数将从from指针指向的用户空间地址开始连续n个字节的数据,向to指针指向的内核空间地址发送。简单来说就是用来将数据从用户空间传输到内核空间。

第一个参数是内核空间的数据目标地址指针。

第二个参数from是用户空间的数据源地址指针。

第三个参数n是数据长度。

如果数据复制成功,则返回零;否则,返回未成功复制的数据字节数。

根据值操作IO口

int userCmd;上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
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");
}

说明(这也是操作逻辑的一部分):

①这里的0是指分组,不设置为低位。

②左移4位,因为寄存器第4位对应pin4,只要第4位为1,就代表这个寄存器对pin4有作用,设置为高电平,如果为0,则无作用(手册内容)。

5.4 Unmap ()退出程序,卸载驱动时,unmap:函数

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

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

5.5 完整代码内核驱动框架

#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";   //模块名
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.如果是字符则用char
	printk("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 
#include 
#include 
#include 
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驱动,删除树莓派上层可执行程序和.ko文件

sudo rmmod pin4drive

使用lsmod检查卸载是否成功。

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

② 驱动框架采用模块化方式编译,上层应用程序交叉编译后发送到树莓派

相同的操作上面都有提到,这里就不再重复了。该文件之前已经修改过了,所以这里就不需要再次修改了。

注意:

在/char目录下,由于之前的模块编译,生成了.ko,.mod等文件。

不要紧,只要把新的驱动框架和新的上层代码复制到原来的两个.c文件中并保存即可。然后进行交叉编译,新生成的文件就会覆盖原来的文件。

框架交叉编译后:

红色框表示需要的模块已经编译生成。

蓝色框里的警告可以忽略(传入的前两个参数是空类型的指针,框架代码中没有做强制转换)

5.7 树莓派驱动安装

sudo insmod pin4drive.ko

使用dmesg看到内核打印“ ”(打印的信息来自框架代码)

在这里插入图片描述

给予许可

sudo chmod 666 /dev/pin4

或者

sudo chmod +x /dev/pin4

5.8 运行上层应用文件

./pin4test

运行成功!

5.8.1 驱动运行成功

当输入1的时候,使用命令gpio查看pin4引脚变化,应该是OUT 1

在这里插入图片描述

当输入0的时候,使用gpio命令检查pin4引脚的变化,应该是OUT 0

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

在这里插入图片描述

这样就实现了一个类似的库。

5.8.2 驱动程序故障:学习调试

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

出现问题时,先看上层(因为上层简单,容易修改),然后再看下层。

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

看一下内核的打印信息,写出打印信息,变量值等。

奇怪的问题

在上面的代码中,在红框处添加调试信息之前

会出现这样的错误,内核无法接收上层传来的0/1,或者接收到的数据不是0/1?

在这里插入图片描述

用gpio检查,设置高电平或低电平也失败。

但为什么呢?我只是添加了调试信息,这和这有什么关系?我没有改变 cmd 的值。

我以为是偶然出现的问题,于是去掉调试信息,再编译,重新运行,结果cmd仍然出错。

有毒?所以我在顶部加了一行

在这里插入图片描述

运行,功能正常,没有出现cmd错误??

啊哈,怎么了?我真的很想知道

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

因此出现错误:

在这里插入图片描述

发现还有一个可能的问题,上面代码中写入的是一个int类型,4个字节,但是这里只写入了一个,会不会是这个原因呢?

在这里插入图片描述

于是我把有毒的去掉,把1改成4,问题就解决了。

这时候我又好奇了,想看看write的返回值:

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

在这里插入图片描述

我原本计划将命令传输从整数改为字符,但我认为我必须改变底层

会不会是从用户模式转入内核模式的指令()出了问题(出现未转换类型的警告)?

这个问题的讨论到此结束,也许后面会有很好的例子,这个问题就很好解决了,就不多说了,毕竟这是我的第一个驱动程序,看多了也许就明白了。

6.DMA的其他简单理解

通过阅读树莓派芯片手册,找到了一些关于DMA( )( )的介绍,多看这种英文芯片手册,不但可以帮助你了解芯片的一些裸机知识结构,还可以帮助你快速阅读其他型号的英文芯片手册。

在这里插入图片描述

大数据的快速复制单元。

使用cp命令复制大文件会占用大量CPU资源,DMA是微控制器专门用来辅助数据复制的,CPU可以激活DMA来复制数据。

检查两个文件是否相同

它多用于检测原来的“同名”文件是否被新的文件所替换,也可以用来检查在复制过程中是否损坏。

md5sum file.c

唯一标识符,如果两个文件相同,则为同一个文件

七、尚待解决的问题

问题:pin4驱动初始化的时候就配置了这些寄存器的地址,然后逐位赋值,而pin5驱动初始化的时候又要初始化同样的寄存器,这样的话我在pin4上逐位赋的值不是会丢失吗?

内核编译要多久_内核编译是什么意思_linux内核 编译 arm

我稍后有时间会练习一下答案。

回答:

答案来了。我为 pin4 和 pin5 编写了两个引脚驱动程序。熟能生巧。

从结果来看,各个实验之间互不影响,多动手实践才能获得真正的知识。

二维码
扫一扫在手机端查看

本文链接:https://by928.com/6641.html     转载请注明出处和本文链接!请遵守 《网站协议》
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。

项目经理在线

我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

在线客服
联系方式

热线电话

13761152229

上班时间

周一到周五

公司电话

二维码
微信
线