目录
向 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)
如果上层要实现读取,只要复制一份,按照格式修改一下就可以使用了。
上层和底层是对应的,上层要用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文件即可,类似上层编译。编译后生成以下文件:
③将.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上逐位赋的值不是会丢失吗?
我稍后有时间会练习一下答案。
回答:
答案来了。我为 pin4 和 pin5 编写了两个引脚驱动程序。熟能生巧。
从结果来看,各个实验之间互不影响,多动手实践才能获得真正的知识。
扫一扫在手机端查看
-
Tags : 树莓派驱动开发
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。