admin 管理员组文章数量: 1086019
详细到吐血 —— 树莓派驱动开发入门:从读懂框架到自己写驱动
师承陈立臣
目录
- README
- 一、驱动初步认知
- 为什么要学会写驱动?
- 设备号的两个作用?
- 区分硬件
- 索引驱动在驱动链表中的位置
- 从open到设备,从上层到底层,经历了什么?
- 二、基于内核驱动框架编写驱动代码流程
- 1.编写上层应用代码
- 2.根据上层需求修改内核驱动框架代码
- 代码补充解读
- static的作用
- 结构体成员变量的单独赋值
- 结构体`file_operations`
- 手动生成设备
- 3.在Ubuntu上交叉编译(很重要)
- 驱动框架的模块编译并发送至树莓派
- ①修改Makefile
- ②进行模块编译
- ③把`.ko`文件发送至树莓派
- 上层代码的编译并发送至树莓派
- 使用交叉编译工具进行编译
- 发送至树莓派
- 4.树莓派装载驱动并运行
- ①树莓派装载驱动
- ②运行上层代码
- ③增加访问权限再运行
- 是否执行成功:`demsg`指令查看内核打印信息
- 三、3个地址的介绍
- 微机总线地址
- 物理地址
- 虚拟地址
- 简单了解地址框图与内核的页表映射
- 四、实战:操作IO口输出高 / 低电平
- 1.芯片手册导读
- General Purpose I/O (GPIO)板块
- 捕捉信息
- 配置引脚 输入 / 输出
- 配置引脚输出是 0 还是 1
- 清除 0 / 1 状态
- 整理关键内容
- 2.配置3个主要的寄存器地址
- ①在原来框架的基础上,添加寄存器的定义
- 弄清楚寄存器的分组
- volatile的使用
- ②配置寄存器的地址
- 分别找到几个IO寄存器的物理地址(非常易错)
- 弄清楚GPIO的物理地址(真实地址)
- 根据偏移值,弄清楚寄存器的物理地址(真实地址)
- 物理地址转换为虚拟地址:`ioremap`函数
- 3.进行功能配置
- ①在函数pin4_open中配置pin4为输出引脚
- 运用与(&) / 或(|)运算进行位操作
- ==与运算给指定位==(14、13)==赋值0==,其他不变
- ==或运算==给指定位(12)==赋值1==
- ②在函数pin4_write中配置pin4输出 0 / 1
- 获取上层write函数的值:`copy_from_user`函数
- 根据值来操作IO口
- 4.解除虚拟地址映射
- 退出程序卸载驱动的时候,解除映射:`iounmap`函数
- 5.完整代码
- 内核驱动框架
- 上层应用程序
- 6.交叉编译并发送至树莓派
- ①树莓派上卸载之前的pin4驱动,删除上层应用文件和.ko文件
- ②框架和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派
- 7.装载驱动
- 8.运行上层应用文件
- 驱动成功运行
- 驱动运行失败:学会调试
- 奇怪的问题
- 五、其他
- 简单了解:DMA(direct memory access)(直接存储器访问)
- md5sum检查两个文件是否完全一样
README
emmm一不小心写了这么长的篇幅,建议配合目录一起看,从目录点击对应知识点,对知识体系和结构有整体的认识,阅读的时候才不会感到吃力。
一、驱动初步认知
为什么要学会写驱动?
树莓派开发简单是因为有厂家提供的wiringPi库,实现超声波,实现继电器操作,做灯的点亮…都非常简单。
但未来做开发时,不一定都是用树莓派,则没有wiringPi库可以用。但只要能运行Linux,linux的标准C库一定有。
学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路图…就能做开发。
用树莓派学习的目的不仅是为是体验其强大便捷的wiringPi库,更要通过树莓派学会linux内核开发,驱动编写等,做一个属于自己的库。
设备号的两个作用?
区分硬件
linux一切皆为文件,其设备管理同样是和文件系统紧密结合。在目录/dev
下都能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动,那么open怎样区分这些硬件呢?
依靠文件名与设备号。在/dev
下ls -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为例子:
上层open
→sys_call
→sys_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 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
可以看到这两个打印信息,说明内核的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位寄存器GPFSEL0
的14-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.如果是字符则用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(手册内容)。
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.如果是字符则用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;
}
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
唯一标识,相同则两个文件为同一文件
本文标签: 详细到吐血树莓派驱动开发入门从读懂框架到自己写驱动
版权声明:本文标题:详细到吐血 —— 树莓派驱动开发入门:从读懂框架到自己写驱动 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1686559969a10319.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论