树莓派基于Linux内核驱动开发 |
您所在的位置:网站首页 › 做驱动开发 › 树莓派基于Linux内核驱动开发 |
一、驱动认知
1.1 为什么要学习写驱动
树莓派开发简单是因为有厂家提供的wiringPi库,实现超声波,实现继电器操作,做灯的点亮…都非常简单。 但未来做开发时,不一定都是用树莓派,则没有wiringPi库可以用。但只要能运行Linux,linux的标准C库一定有。 学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路图…就能做开发。 用树莓派学习的目的不仅是为是体验其强大便捷的wiringPi库,更要通过树莓派学会linux内核开发,驱动编写等,做一个属于自己的库。 1.2 文件名与设备号linux一切皆为文件,其设备管理同样是和文件系统紧密结合。在目录/dev下都能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动,那么open怎样区分这些硬件呢? 依靠文件名与设备号。在/dev下ls -l可以看到 内核中存在一个驱动链表,管理所有设备的驱动。 驱动开发无非以下两件事: 编写完驱动程序,加载到内核 用户空间open后,调用驱动程序(驱动程序就是操作寄存器来驱动IO口,单片机51,32就是这种操作) 驱动插入到链表的位置(顺序)由设备号检索。 1.3 open函数打通上层到底层硬件的详细过程用户空间调用open(比如open(“/dev/pin4”,O_RDWR))产生一个软中断(中断号是0x80),进入内核空间调用sys_call,这个sys_call在内核里面是汇编的,用Source Insight搜索不到。 sys_calll真正调用的是sys_open(属于VFS层虚拟文件系统,因为磁盘的分区和引脚分区不一样,为了实现上层统一化),根据你的设备名比如pin4去到内核的驱动链表,根据其主设备号与次设备号找到相关驱动函数。 调用驱动函数里面的open,这个open就是对寄存器的操作,从而设置IO口引脚电平。这件事对于单片机来说特变容易,就两句话搞定: sbit pin4 = P1^4; pin4=1;目的是用简单的例子展示从用户空间到内核空间的整套流程。 在上层访问一个设备跟访问普通的文件没什么区别。试写一个简单的open和write去操作设备"pin4"。 #include #include #include #include int main() { int fd; fd = open("/dev/pin4",O_RDWR); if(fd printf("open success\n"); } 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_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 static的作用内核代码数量庞大,为了防止与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。内核源码里面运用了大量的static,因为内核源码众多,一万五千多个C文件,很容易造成代码命名的冲突。 2.3.2 结构体file_operations(最终加载到内核驱动链表)在SourceInsight中查看结构体file_operations,可以发现很多的函数指针(指向函数的指针,函数内进行一些程序的执行),这些函数名跟系统上层对文件的操作差不多。(read,write,llseek) 上层对应底层,上层想要用read,底层就要有read的支持。 2.3.3 手动生成设备框架中有自动生成设备的代码,那么手动生成设备是怎么样的呢? 进入/dev目录,查看帮助可知道创建规则 sudo mknod 设备名称 设备类型 主设备号 次设备号 使用如下命令创建名称为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: 在Makefile里面添加: obj-m += pin44driver2.o-m就是模块的形式 如图: 之前编译内核镜像的时候用的是这个命令: 注:如果说编译中途提示出错,照着错误提示去修改.c文件即可,和上层编译类似。 编译完成生成的一些文件如下: 拷贝上文分析的上层代码到ubuntu中,此处我命名为pin44test.c 使用交叉编译工具进行编译 arm-linux-gnueabihf-gcc pin44test.c -o pin4test发送至树莓派 scp pin4test [email protected]:/home/pi 3.3 树莓派装载驱动并运行 ①树莓派加载内核驱动(insmod) sudo insmod pin44driver2.ko查看是否已经成功添加驱动 可以去设备下查看 ls /dev/pin4 -l看到驱动添加成功,主设备号231,次设备号0,和内核里面的代码对应上。 如果需要卸载驱动,就sudo rmmod pin44driver2 ②运行上层代码(无权限) ./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 userschmod 744 仅允许用户(所有者)执行所有操作,而组和其他用户只允许读。 ④检查是否执行成功:demsg指令查看内核打印信息用dmesg命令显示内核缓冲区信息,并通过管道筛选与pin4相关信息 dmesg | grep pin4可以看到这两个打印信息,说明内核的printk已经被成功调用,我们已经成功完成了上层对内核的调用 ! 写驱动是为了操作IO口,实现自己的wiringpi库,跟硬件打交道。 首先要理解以下3个地址的概念: 4.1 总线地址通俗来说:cpu能够访问的内存范围 现象:电脑装了32位(bit)的系统,明明内存条有8G,却只能识别3.8G左右,这是因为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 虚拟地址又叫逻辑地址(基于算法的地址,软件层面的地址,是假地址)便称为虚拟地址 虚拟地址的作用: 以树莓派为例,总线可以访问4G,物理地址只有1G,但需要运行的程序大于1G,如果把程序全部都加载到内存是不可取的。 物理地址数据的运行真正是拿虚拟地址来操作的,虚拟地址可以比1G大,总线地址(CPU能访问的地址范围)能看到4个G,就可以把1个G的物理地址映射成4个G的虚拟地址。当物理地址不能满足程序运行空间需求时,如果没有虚拟地址,程序就不能正常运行。单片机51和STM32如果程序过大,是禁止你烧写的,而在Linux系统环境下是可以的。 树莓派3b的cpu型号是BCM2835,它是ARM-cotexA53架构 4.4 MMU内存管理单元地址框图 可以看到总线地址为FF FF FF FF,即为4G; 物理地址的1M通过映射成为4M的虚拟地址(我们写的所有的代码都是在操控虚拟地址,都是假的),这中间有个设计的算法叫页表。 这个表决定了这个4M被映射到虚拟内存的哪一个段,通过MMU进行管理。单片机和ARM处理器的区别就是ARM有MMU(内存管理单元)和CACHE(高速缓存),如下图所示: 查看芯片手册的目的性很强:做哪一块的开发,就只看那一块,现在要开发的是GPIO,熟悉控制IO口的寄存器最为重要。 如果看完这部分的文档,你对于以下几个问题(后面有解析)有清晰的答案,说明你真正读懂了这一部分的开发。 ①操作逻辑:简言之就是怎么进行配置相关寄存器,这些配置步骤和思想其实都很类似。 ②需要重点掌握的寄存器有哪些?例如输入 / 输出控制寄存器,输出 0 / 1控制寄存器,清除状态寄存器 5.2、Register View 导读在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,0/1,怎么清除,上升沿下降沿等。(配置过32 / 51寄存器的应该对这些很熟悉) 从下图中可以大概了解到所有的IO口被分成了0~5组。 有意思的是,下图最第一列的地址Address是树莓派总线地址,一般芯片手册给的都是真正的物理地址。第二列是寄存器的名字,第三列寄存器功能描述。 一共有41个寄存器,每个寄存器都是32位。 这20~29的IO口(第二列)属于分组2
有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、要求每次直接从寄存器里读值。由于随着程序的执行,会改变寄存器当中的数据,而读取的都是内存里面的备份数据,数据的时效性没有那么强,读的可能是一个老数据。在内核中对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的地址,否则编译后运行会有段错误 特别注意:BCM2708 和BCM2709 IO起始地址不同,BCM2708是0x20000000,BCM2709是0x3f000000 根据偏移值,弄清楚寄存器的物理地址(真实地址) 使用函数ioremap: 函数原型:void *ioremap(unsigned long phys_addr, unsigned long size) phys_addr:要映射的物理地址的基地址; size:要映射的空间的大小(一个寄存器4个字节); 七、寄存器功能配置 ①在函数pin4_open中配置pin4为输出引脚只要32位寄存器GPFSEL0的14-12位配置为001,其它位不管,即可配置pin4为输出引脚 当然直接暴力赋值(0000…001…0000)是不可取的,会把其他的IO口给影响。最好的结果是只改变了14-12位。 运用与(&) / 或(|)运算进行位操作 *GPFSEL0 &= ~(0x6 printk("set 0\n"); *GPCLR0 |= 0x1 printk("pin4_open\n"); //内核的打印函数,和printf类似 //open的时候配置pin4为输出引脚 *GPFSEL0 &= ~(0x6 printk("set 1\n"); *GPSET0 |= 0x1 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"); 2、上层应用程序 #include #include #include #include int main() { int fd; int cmd; fd = open("/dev/pin4",O_RDWR); if(fd 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; } 3、交叉编译并发送至树莓派 ①树莓派上卸载之前的pin4驱动、删除树莓派上层可执行程序pin4test和pin44driver2.ko文件 sudo rmmod pin44driver2用lsmod查看是否卸载成功。 基本上都会自动卸载驱动的,因为上一节框架代码最后有卸载驱动的代码操作。 ②驱动框架模块化方式编译和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派注意: 在Ubuntu的/char目录下因为之前的模块编译生成了.ko ,.mod等文件 没关系,直接复制新的驱动框架、新的上层代码到原来的2个.c文件覆盖保存。然后进行交叉编译,新生成的文件会覆盖掉原来的文件。 4、树莓派装载驱动 sudo insmod pin44driver.ko用dmesg可以看到内核打印出“驱动装载成功”(打印信息来自框架代码) 运行上层应用文件 ./pin4test运行成功! 输入1时,用命令gpio readall查看pin4引脚变化,应为OUT 1 用dmesg打开内核打印界面,可以看到内核的printk已经被调用,配置执行。 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |