详细到吐血 —— 树莓派驱动开发入门:从读懂框架到自己写驱动_树莓派 编译驱动-程序员宅基地

技术标签: ubuntu  linux  内核  树莓派  

师承陈立臣

目录

README

emmm一不小心写了这么长的篇幅,建议配合目录一起看,从目录点击对应知识点,对知识体系和结构有整体的认识,阅读的时候才不会感到吃力。
在这里插入图片描述

一、驱动初步认知

为什么要学会写驱动?

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

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

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

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

设备号的两个作用?

区分硬件

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

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

在这里插入图片描述

索引驱动在驱动链表中的位置

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

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

  • 编写完驱动程序,加载到内核
  • 用户空间open后,调用驱动程序

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

从open到设备,从上层到底层,经历了什么?

  • 用户层调用open产生一个软中断(中断号是0x80),进入内核空间调用sys_callsys_call。
  • sys_callsys_call真正调用的是sys_open,去内核的驱动链表根据主设备号与次设备号找到相关驱动函数。
  • 调用驱动函数里面的open,去设置IO口引脚电平。

(对应下图的粉色笔迹)

在这里插入图片描述

二、基于内核驱动框架编写驱动代码流程

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

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.根据上层需求修改内核驱动框架代码

所谓框架,就是定死的东西,基本的语句必须要有,少一个都不行。

虽然有这么多的代码,但核心运行的就两个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函数
static ssize_t pin4_write(struct file *file,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,
    .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下自动生成设备
    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);  //卸载驱动

}

module_init(pin4_drv_init);  //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

不方便在注释中标注的,在下面详细说明:

代码补充解读

static的作用

内核代码数量庞大,为了防止与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。

结构体成员变量的单独赋值
static struct file_operations pin4_fops = {
    

    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};

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

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

结构体file_operations

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

在这里插入图片描述

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

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

手动生成设备

框架中有自动生成设备的代码

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

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

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

  • 使用如下命令创建名称为zhu,主设备号为8,次设备号为1的字符设备。
sudo mknod zhu c 8 1

ls -l可以看到已经创建成功

在这里插入图片描述

3.在Ubuntu上交叉编译(很重要)

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

在ubuntu中,进入字符设备驱动目录linux-rpi-4.14.y/drivers/char

拷贝上文分析过的驱动框架代码,创建名为pin4drive.c的文件.

①修改Makefile

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

vi Makefile

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

Makefile:
在这里插入图片描述

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

添加

obj-m                           += pin4drive.o

如图:

在这里插入图片描述

②进行模块编译

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

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

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

下图中,老师演示过程中有提示:
cc [M] /drives/char/pin4driver2.o

而我的没有
在这里插入图片描述

但是打开/char目录发现已经生成了两个文件。
在这里插入图片描述

③把.ko文件发送至树莓派
scp pin4drive.ko pi@192.168.43.250:/home/pi

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

上层代码的编译并发送至树莓派

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

使用交叉编译工具进行编译
arm-linux-gnueabihf-gcc pin4drivertest.c -o pin4test
发送至树莓派
scp pin4test pi@192.168.43.250:/home/pi

4.树莓派装载驱动并运行

①树莓派装载驱动

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

可以去设备下查看

ls /dev/pin4 -l

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

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

②运行上层代码

./pin4test

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

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

在这里插入图片描述

③增加访问权限再运行

解决方法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

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

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

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

dmesg | grep pin4

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

在这里插入图片描述

三、3个地址的介绍

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

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

微机总线地址

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

现象:电脑装了32位的系统,明明内存条有8G,却只能识别3.8G左右,这是因为32位仅能表示/访问2^32^=4,294,967,296bit=4,194,304Kb=4096Mb=4G左右。只有装了64位的,才能够识别到8G。

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

树莓派的内存

cat /proc/meminfo

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

物理地址

硬件实际地址或绝对地址

虚拟地址

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

虚拟地址的作用:

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

物理地址数据的运行真正是拿虚拟地址来操作的,虚拟地址可以比1G大,总线地址能看到4个G,就可以把1个G的物理地址映射成4个的虚拟地址。当物理地址不能满足程序运行空间需求时,如果没有虚拟地址,程序就不能正常运行。

树莓派3b的cpu型号是

cat /proc/cpuinfo

BCM2835,它是ARM-cotexA53架构
在这里插入图片描述

简单了解地址框图与内核的页表映射

  • 地址框图

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

在这里插入图片描述

  • 内核的页表映射

在这里插入图片描述
物理地址的1M通过扩充映射成为4M的虚拟地址,这中间有个设计的算法叫页表。

这个表决定了这个4M被映射到虚拟内存的哪一个段,通过MMU进行管理。

如果想要更多地了解Linux对内存的管理,推荐书《unix设计与实现》,像是内核设计文档。

四、实战:操作IO口输出高 / 低电平

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

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

1.芯片手册导读

General Purpose I/O (GPIO)板块

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

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

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

捕捉信息

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

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

在这里插入图片描述

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

配置引脚 输入 / 输出

这20~29的IO口属于分组2
在这里插入图片描述

IO编号要看好

在这里插入图片描述

配置引脚输出是 0 还是 1

在这里插入图片描述

清除 0 / 1 状态

在这里插入图片描述

整理关键内容

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

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

操作逻辑:
GPFSEL0寄存器举例,要操作的引脚为pin4,其对应的分组为0。只要在这个分组下,把14-12位设置为001,就能配置pin4引脚为输出。

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

2.配置3个主要的寄存器地址

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

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

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

弄清楚寄存器的分组

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

volatile的使用

volatile在此处是 : 防止编译器优化(可能是省略,也可能是更改)这些寄存器变量,常见于在内核中对IO口进行操作。

②配置寄存器的地址

在①的基础上,在驱动的初始化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的偏移量0x2000000,所以GPIO的实际物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。

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

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

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

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

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

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

使用函数ioremap:

函数原型:void *ioremap(unsigned long phys_addr, unsigned long size)
phys_addr:要映射的起始的IO物理地址;
size:要映射的空间的大小;

3.进行功能配置

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

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

运用与(&) / 或(|)运算进行位操作
*GPFSEL0 &= ~(0x6 << 12);
*GPFSEL0 |= (0x1 << 12);

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

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

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

在这里插入图片描述

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

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

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

在这里插入图片描述

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

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

或运算给指定位(12)赋值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.如果是字符则用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");
}

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

4.解除虚拟地址映射

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

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

iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);

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.如果是字符则用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 <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;
}

6.交叉编译并发送至树莓派

①树莓派上卸载之前的pin4驱动,删除上层应用文件和.ko文件

sudo rmmod pin4drive

用lsmod查看是否卸载成功。

②框架和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派

上文(二.3:在Ubuntu上交叉编译) 有涉及,一样的操作不再赘述。

注意:

因为在上文( 二、3,4)中,树莓派上已经有来自Ubuntu发送过来的上层文件pin4test和驱动文件.ko

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

但这都没关系,直接复制上文新的驱动框架,新的上层代码,打开原来的2个.c文件(在上文 二.3 中创建的),覆盖。之后进行交叉编译,新生成的文件会覆盖掉原来的文件。

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

7.装载驱动

sudo insmod pin4drive.ko

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

在这里插入图片描述

给权限

sudo chmod 666 /dev/pin4

8.运行上层应用文件

./pin4test

驱动成功运行

输入1时,用命令gpio readall查看pin4引脚变化,应为OUT 1
在这里插入图片描述
输入0时,再用命令gpio readall查看pin4引脚变化,应为OUT 0

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

在这里插入图片描述

驱动运行失败:学会调试

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

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

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(direct memory access)(直接存储器访问)

在这里插入图片描述

大数据的快速拷贝单元。

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

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

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

md5sum file.c

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44742824/article/details/113816599

智能推荐

leetcode 172. 阶乘后的零-程序员宅基地

文章浏览阅读63次。题目给定一个整数 n,返回 n! 结果尾数中零的数量。解题思路每个0都是由2 * 5得来的,相当于要求n!分解成质因子后2 * 5的数目,由于n中2的数目肯定是要大于5的数目,所以我们只需要求出n!中5的数目。C++代码class Solution {public: int trailingZeroes(int n) { ...

Day15-【Java SE进阶】IO流(一):File、IO流概述、File文件对象的创建、字节输入输出流FileInputStream FileoutputStream、释放资源。_outputstream释放-程序员宅基地

文章浏览阅读992次,点赞27次,收藏15次。UTF-8是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。文件字节输入流:每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1。注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。UTF-8字符集:汉字占3个字节,英文、数字占1个字节。GBK字符集:汉字占2个字节,英文、数字占1个字节。GBK规定:汉字的第一个字节的第一位必须是1。_outputstream释放

jeecgboot重新登录_jeecg 登录自动退出-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏3次。解决jeecgboot每次登录进去都会弹出请重新登录问题,在utils文件下找到request.js文件注释这段代码即可_jeecg 登录自动退出

数据中心供配电系统负荷计算实例分析-程序员宅基地

文章浏览阅读3.4k次。我国目前普遍采用需要系数法和二项式系数法确定用电设备的负荷,其中需要系数法是国际上普遍采用的确定计算负荷的方法,最为简便;而二项式系数法在确定设备台数较少且各台设备容量差..._数据中心用电负荷统计变压器

HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板_网页设计成品百度网盘-程序员宅基地

文章浏览阅读7k次,点赞4次,收藏46次。HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 明星、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 军事、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他 等网页设计题目, A+水平作业_网页设计成品百度网盘

【Jailhouse 文章】Look Mum, no VM Exits_jailhouse sr-iov-程序员宅基地

文章浏览阅读392次。jailhouse 文章翻译,Look Mum, no VM Exits!_jailhouse sr-iov

随便推点

chatgpt赋能python:Python怎么删除文件中的某一行_python 删除文件特定几行-程序员宅基地

文章浏览阅读751次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python 删除文件特定几行

Java过滤特殊字符的正则表达式_java正则表达式过滤特殊字符-程序员宅基地

文章浏览阅读2.1k次。【代码】Java过滤特殊字符的正则表达式。_java正则表达式过滤特殊字符

CSS中设置背景的7个属性及简写background注意点_background设置背景图片-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏17次。css中背景的设置至关重要,也是一个难点,因为属性众多,对应的属性值也比较多,这里详细的列举了背景相关的7个属性及对应的属性值,并附上演示代码,后期要用的话,可以随时查看,那我们坐稳开车了······1: background-color 设置背景颜色2:background-image来设置背景图片- 语法:background-image:url(相对路径);-可以同时为一个元素指定背景颜色和背景图片,这样背景颜色将会作为背景图片的底色,一般情况下设置背景..._background设置背景图片

Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程-程序员宅基地

文章浏览阅读2.6k次,点赞2次,收藏8次。Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程

PyCharm2021安装教程-程序员宅基地

文章浏览阅读10w+次,点赞653次,收藏3k次。Windows安装pycharm教程新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入下载安装PyCharm1、进入官网PyCharm的下载地址:http://www.jetbrains.com/pycharm/downl_pycharm2021

《跨境电商——速卖通搜索排名规则解析与SEO技术》一一1.1 初识速卖通的搜索引擎...-程序员宅基地

文章浏览阅读835次。本节书摘来自异步社区出版社《跨境电商——速卖通搜索排名规则解析与SEO技术》一书中的第1章,第1.1节,作者: 冯晓宁,更多章节内容可以访问云栖社区“异步社区”公众号查看。1.1 初识速卖通的搜索引擎1.1.1 初识速卖通搜索作为速卖通卖家都应该知道,速卖通经常被视为“国际版的淘宝”。那么请想一下,普通消费者在淘宝网上购买商品的时候,他的行为应该..._跨境电商 速卖通搜索排名规则解析与seo技术 pdf

推荐文章

热门文章

相关标签