一、驱动认知1.1为何要学习写驱动

蓝莓派开发简单是由于有厂家提供的wiringPi库,实现超声波,实现熔断器操作,做灯的照亮…都十分简单。

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

学会依照标准C库编撰驱动,只要能领到linux内核源码,领到芯片指南,电路图…就能做开发。

用覆盆子派学习的目的除了是为是体验其强悍方便的wiringPi库,更要通过猕猴桃派学会linux内核开发,驱动编撰等,做一个属于自己的库。

1.2文件名与设备号

linux一切皆为文件,其设备管理同样是和文件紧密结合。在目录/dev下都能见到键盘,鼠标,屏幕,并口等设备文件,硬件要有相对应的驱动,这么open如何分辨这种硬件呢?

借助文件名与设备号。在/dev下ls-l可以看见

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

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

编撰完驱动程序,加载到内核

用户空间open后,调用驱动程序(驱动程序就是操作寄存器来驱动IO口,单片机51,32就是这些操作)

驱动插入到数组的位置(次序)由设备号检索。

1.3open函数打通下层到底层硬件的详尽过程

用户空间调用open(例如open(“/dev/pin4”,O_RDWR))形成一个软中断(中断号是0x80),步入内核空间调用sys_call,这个sys_call在内核上面是汇编的,用SourceInsight搜索不到。

sys_calll真正调用的是sys_open(属于VFS层虚拟文件系统,由于c盘的分区和引脚分区不一样,为了实现下层统一化),按照你的设备名例如pin4去到内核的驱动数组,依据其主设备号与次设备号找到相关驱动函数。

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

sbit pin4 = P1^4;
pin4=1;

二、基于框架编撰驱动代码2.1编撰下层应用代码

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

在下层访问一个设备跟访问普通的文件没哪些区别。试写一个简单的open和write去操作设备"pin4"。

#include 
#include 
#include 
#include 
int main()
{
	int fd;
	fd = open("/dev/pin4",O_RDWR);
	if(fd < 0){
		printf("open failedn");
		perror("reson");
	}else{
		printf("open successn");
	}
	fd = write(fd,'1',1);//写一个字符'1',写一个字节
	return 0;
}

按照前面提及的驱动认知,有个大致的概念,以open为反例:

下层open→sys_call→sys_open→内核驱动数组节点→执行节点里的open

其实,没有装载驱动的话这个程序执行一定会报错。只有在内核装载了驱动但是在/dev下生成了“pin4”这样一个设备能够运行。

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

2.2更改内核驱动框架代码

所谓框架,就是在往驱动数组上面加驱动的时侯要符合内核规则,它是定死的东西,基本的句子必需要有,少一个都不行。

尽管有如此多的代码,但核心运行的就两个printk。

#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_openn");  //内核的打印函数,和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_writen");
    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.1static的作用

内核代码数目庞大,为了避免与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。内核源码上面运用了大量的static,由于内核源码诸多,一万五千多个C文件,很容易导致代码命名的冲突。

2.3.2结构体file_operations(最终加载到内核驱动数组)

在SourceInsight中查看结构体file_operations,可以发觉好多的函数表针(指向函数的表针,函数内进行一些程序的执行),这种函数名跟系统下层对文件的操作差不多。(read,write,llseek)

假如下层想要实现read,就复制过来,根据格式改一改才能使用。

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

2.3.3自动生成设备

框架中有手动生成设备的代码,这么自动生成设备是怎样样的呢?

步入/dev目录,查看帮助可晓得创建规则

内核驱动力是什么意思_linux内核驱动_内核驱动程序

sudomknod设备名称设备类型主设备号次设备号

使用如下创建名称为zhu,主设备号为8,次设备号为1的字符设备。

sudo mknod zhu c 8 1

三、驱动代码编译和测试3.1驱动框架的模块编译并发送至猕猴桃派

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

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

①Makefile内添加生成.o

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

vi Makefile

其实不一定要置于/char下。但注意:置于那个文件夹下,就更改那种文件夹的Makefile即可。

Makefile:

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

在Makefile上面添加:

obj-m                           += pin44driver2.o

-m就是模块的方式

如图:

②模块编译生成.ko文件

之前编译内核镜像的时侯用的是这个:

现今只需进行模块编译,不须要生成zImage,dtbs文件;

回到源码目录/linux-rpi-4.14.y再执行下边指令

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

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

编译完成生成的一些文件如下:

③把.ko文件发送至猕猴桃派

scp pin44driver2.ko pi@192.168.43.44:/home/pi

3.2下层代码交叉编译发送至猕猴桃派

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

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

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

发送至猕猴桃派

scp pin4test pi@192.168.43.44:/home/pi

3.3猕猴桃派装载驱动并运行①树莓派加载内核驱动(insmod)

sudo insmod pin44driver2.ko

查看是否早已成功添加驱动

可以去设备下查看

ls /dev/pin4 -l

见到驱动添加成功,主设备号231,次设备号0,和内核上面的代码对应上。

内核驱动力是什么意思_linux内核驱动_内核驱动程序

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

假如须要卸载驱动,就sudormmodpin44driver2

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

./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

chmod744仅容许用户(所有者)执行所有操作,而组和其他用户只容许读。

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

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

dmesg | grep pin4

可以看见这两个复印信息,说明内核的printk早已被成功调用,我们早已成功完成了下层对内核的调用!

四、三种地址介绍

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

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

4.1总线地址

浅显来说:cpu才能访问的显存范围

现象:笔记本装了32位(bit)的系统,明明显存条有8G,却只能辨识3.8G左右linux内核驱动,这是由于32位仅能表示/访问232=4,294,967,296bit=4,194,304Kb=4096Mb=4G左右。只有装了64位的,才才能辨识到8G。32位、64位是计算机CPU一次处理数据能力的大小。

猕猴桃派装载32位操作系统,轮询自然是4G。

猕猴桃派的显存:大约是927M

cat /proc/meminfo

4.2化学地址

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

4.3虚拟地址

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

虚拟地址的作用:

以猕猴桃派为例linux 版本,总线可以访问4G,化学地址只有1G,但须要运行的程序小于1G,假如把程序全部都加载到显存是不可取的。

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

猕猴桃派3b的cpu机型是BCM2835,它是ARM-cotexA53构架

4.4MMU显存管理单元

地址框图

可以看见总线地址为FFFFFFFF,即为4G;

内核的页表映射

化学地址的1M通过映射成为4M的虚拟地址(我们写的所有的代码都是在操控虚拟地址,都是假的),这中间有个设计的算法叫页表。

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

五、博通BCM2835第六章IO口配置寄存器5、1GeneralPurposeI/O(GPIO)蓝筹股

查看芯片指南的目的性很强:做哪一块的开发,就只看那一块linux 常用命令,如今要开发的是GPIO,熟悉控制IO口的寄存器最为重要。

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

①操作逻辑:简言之就是如何进行配置相关寄存器,这种配置步骤和思想虽然都很类似。

②需要重点把握的寄存器有什么?诸如输入/输出控制寄存器,输出0/1控制寄存器,消除状态寄存器

5.2、RegisterView导读

在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,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引脚为输出。

六、寄存器地址配置(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、要求每次直接从寄存器里读值。因为随着程序的执行,会改变寄存器当中的数据linux内核驱动,而读取的都是显存上面的备份数据,数据的时效性没有这么强,读的可能是一个老数据。在内核中对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和BCM2709IO起始地址不同,BCM2708是0x20000000,BCM2709是0x3f000000

按照偏斜值,弄清楚寄存器的化学地址(真实地址)

同样的方式,寄存器GPCLR0的偏斜值为28。

化学地址转换为虚拟地址:ioremap函数

由于无论内核代码还是下层代码操作的都是虚拟地址,代码中直接用化学地址肯定不行,须要进行转换,将IO口寄存器映射成普通显存单元进行访问。

使用函数ioremap:

函数原型:void*ioremap(unsignedlongphys_addr,unsignedlongsize)

phys_addr:要映射的化学地址的基地址;

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

七、寄存器功能配置①在函数pin4_open中配置pin4为输出引脚

只要32位寄存器GPFSEL0的14-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,则11恰好对上14、13位。

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

2)、或运算给指定位(12bit)形参1

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

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

函数介绍

unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn)

内核驱动力是什么意思_linux内核驱动_内核驱动程序

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

第一个参数to是内核空间的数据目标地址表针,

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

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

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

按照值来操作IO口

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

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

①这个GPSET0,0指的是分组,不是设置成低电平。

②左移4位,是由于GPSET0寄存器的第4位对应pin4,只要把第4位设置为1,表示这个寄存器就对pin4发挥作用,设置成高电平,倘若是0则noeffct(指南内容)。

3、解除虚拟地址映射(iounmap)

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

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

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

八、完整代码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";   //模块名
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_openn");  //内核的打印函数,和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_writen");
	//获取上层write的值
	copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
	
	//根据值来执行操作
	if(userCmd == 1){
		printk("set 1n");
		*GPSET0 |= 0x1 << 4;
	}else if(userCmd == 0){
		printk("set 0n");
		*GPCLR0 |= 0x1 << 4;
	}else{
		printk("cmd errorn");//加入调试信息,方便通过查看内核信息进行修改
	}
	
    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 successn");
    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");

2、上层应用程序

#include 
#include 
#include 
#include 
int main()
{
        int fd;
        int cmd;
        fd = open("/dev/pin4",O_RDWR);
        if(fd < 0){
                printf("open failedn");
                perror("reson");
        }else{
                printf("open successn");
        }
        printf("请输入0 / 1n 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;
}

3、交叉编译并发送至猕猴桃派①树莓派上卸载之前的pin4驱动、删除猕猴桃派下层可执行程序pin4test和pin44driver2.ko文件

sudo rmmod pin44driver2

用lsmod查看是否卸载成功。

基本上就会手动卸载驱动的,由于上一节框架代码最后有卸载驱动的代码操作。

②驱动框架模块化形式编译和下层应用程序在Ubuntu中进行交叉编译并发送至猕猴桃派

注意:

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

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

4、树莓派装载驱动

sudo insmod pin44driver.ko

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

给权限

sudo chmod 666 /dev/pin4

运行下层应用文件

./pin4test

运行成功!

5、驱动运行成功

输入1时,用命令gpioreadall查看pin4引脚变化,应为OUT1

输入0时,再用命令gpioreadall查看pin4引脚变化,应为OUT0

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

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

本文原创地址://lrxjmw.cn/qdsmpkfdxxlc.html编辑:刘遄,审核员:暂无