• 树莓派(十二)树莓派驱动开发入门:从读懂框架到自己写驱动(下)


    嵌入式之路,贵在日常点滴

                                                                    ---阿杰在线送代码

    目录

    一、3个地址的介绍

    微机总线地址 

    物理地址

    虚拟地址

    虚拟地址的作用:

    树莓派3b的cpu型号是

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

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

    1.芯片手册导读

    配置引脚 输入 / 输出 

    配置引脚输出是 0 还是 1 

    清除 0 / 1 状态 

    整理关键内容 

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

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

    弄清楚寄存器的分组

    volatile的使用(笔试)

    ②配置寄存器的地址

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

    3.进行功能配置

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

    ②在函数pin4_write中配置pin4输出 0 / 1

    根据值来操作IO口 

    4.解除虚拟地址映射

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

    5.完整代码

    内核驱动框架

    上层应用程序 

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

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

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

    7.装载驱动 

    8.运行上层应用文件 

    驱动运行失败:学会调试

    有关驱动代码里面GPIO口地址的问题:​

    其他 

    简单了解:DMA(direct memory access)(直接存储器访问)

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

    如何清空vi文件下所有的内容 


     

    一、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架构 

     

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

    BCM2835是树莓派3B CPU的型号,是ARM-cotexA53架构 

    cpu Bus是地址总线00000000~FFFFFFFFCPU寻址的范围(4G) 

    DMA是高速拷贝单元,CPU可以发动DMA直接让DMA进行数据拷贝,直接内存访问单元。

    物理地址(PA)1G虚拟地址(VA)4G

    若程序大于物理地址1G,是不是就跑不了了,不是的,它有个MMU的单元,把物理地址映射成虚拟地址,我们操作的代码基本上都是在虚拟地址,它有一个映射页表(如下)

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

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

    将虚拟地址映射为物理地址有一个算法(页表)决定了将虚拟地址映射到物理地址的哪个位置,页表是通过MMU(分页内存管理单元)来管理的,就是设计完页表后通过MMU来执行将虚拟地址映射为物理地址。 

    MMU:Memory Management Unit 

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

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

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

    1.芯片手册导读

    General Purpose I/O (GPIO)板块

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

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

    ①操作逻辑:简言之就是怎么进行配置相关寄存器,这些配置步骤和思想其实都很类似。
    ②需要重点掌握的寄存器有哪些?

    例如输入 / 输出控制寄存器

    输出 0 / 1控制寄存器

    清除状态寄存器

    捕捉信息

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

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

    在这里插入图片描述

    描述部分也很重要,大多涉及使用方法 

     

    配置引脚 输入 / 输出 

    总共有6个组(0组~5组)

    pin0-pin9 0组

     

    pin10-pin19 1组 

    pin20-pin29 2组 

    pin30-pin39 3组 

    pin40-pin49 4组 

    pin50-pin53 5组 

     

    IO编号要看好

    配置引脚输出是 0 还是 1 

    清除 0 / 1 状态 

    整理关键内容 

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

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

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

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

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

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

    1. volatile unsigned int* GPFSEL0 = NULL;
    2. volatile unsigned int* GPSET0 = NULL;
    3. volatile unsigned int* GPCLR0 = NULL;

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

    弄清楚寄存器的分组

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

    volatile的使用(笔试)

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

    volatile的作用是作为指令关键字,确保本条 指令不会因编译器的优化而省略,且要求每次直接读值

    ②配置寄存器的地址

    在①的基础上,在驱动的初始化pin4_drv_init中添加寄存器地址配置

    1. GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
    2. GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
    3. GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);

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

    分别找到几个IO寄存器的物理地址(非常易错)

    弄清楚GPIO的物理地址(真实地址)

    并不是用下图这个地址来对应GPIO功能选择寄存器0的地址,否则编译后运行会有段错误。

    在这里插入图片描述

    IO口的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的实际物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。 

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

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

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

    寄存器GPFSEL0的偏移值为0,即0x3f200000

    物理地址转换为虚拟地址: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位。

    运用与(&) / 或(|)运算进行位操作 

    1. *GPFSEL0 &= ~(0x6 << 12);
    2. *GPFSEL0 |= (0x1 << 12);

    ②在函数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口 

    1. int userCmd;上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
    2. copy_from_user(&userCmd,buf,count);
    3. if(userCmd == 1){
    4. printk("set 1\n");
    5. *GPSET0 |= 0x1 << 4;
    6. }else if(userCmd == 0){
    7. printk("set 0\n");
    8. *GPCLR0 |= 0x1 << 4;
    9. }else{
    10. printk("cmd error\n");
    11. }

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

    4.解除虚拟地址映射

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

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

    1. iounmap(GPFSEL0);
    2. iounmap(GPSET0);
    3. iounmap(GPCLR0);

    5.完整代码

    内核驱动框架

    1. #include //file_operations声明
    2. #include //module_init module_exit声明
    3. #include //__init __exit 宏定义声明
    4. #include //class devise声明
    5. #include //copy_from_user 的头文件
    6. #include //设备号 dev_t 类型声明
    7. #include //ioremap iounmap的头文件
    8. static struct class *pin4_class;
    9. static struct device *pin4_class_dev;
    10. static dev_t devno; //设备号
    11. static int major =231; //主设备号
    12. static int minor =0; //次设备号
    13. static char *module_name="pin4"; //模块名
    14. volatile unsigned int* GPFSEL0 = NULL;
    15. volatile unsigned int* GPSET0 = NULL;
    16. volatile unsigned int* GPCLR0 = NULL;
    17. //pin4_open函数
    18. static int pin4_open(struct inode *inode,struct file *file)
    19. {
    20. printk("pin4_open\n"); //内核的打印函数,和printf类似
    21. //open的时候配置pin4为输出引脚
    22. *GPFSEL0 &= ~(0x6 << 12);
    23. *GPFSEL0 |= (0x1 << 12);
    24. return 0;
    25. }
    26. //pin4_write函数
    27. static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
    28. {
    29. int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
    30. printk("pin4_write\n");
    31. //获取上层write的值
    32. copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
    33. //根据值来执行操作
    34. if(userCmd == 1){
    35. printk("set 1\n");
    36. *GPSET0 |= 0x1 << 4;
    37. }else if(userCmd == 0){
    38. printk("set 0\n");
    39. *GPCLR0 |= 0x1 << 4;
    40. }else{
    41. printk("cmd error\n");
    42. }
    43. return 0;
    44. }
    45. static struct file_operations pin4_fops = {
    46. .owner = THIS_MODULE,
    47. .open = pin4_open,
    48. .write = pin4_write,
    49. };
    50. int __init pin4_drv_init(void) //驱动的真正入口
    51. {
    52. int ret;
    53. printk("insmod driver pin4 success\n");
    54. devno = MKDEV(major,minor); //创建设备号
    55. ret = register_chrdev(major, module_name,&pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
    56. pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //由代码在/dev下自动生成设备
    57. pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件
    58. GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
    59. GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
    60. GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
    61. return 0;
    62. }
    63. void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
    64. {
    65. iounmap(GPFSEL0);
    66. iounmap(GPSET0);
    67. iounmap(GPCLR0);
    68. device_destroy(pin4_class,devno);
    69. class_destroy(pin4_class);
    70. unregister_chrdev(major, module_name); //卸载驱动
    71. }
    72. module_init(pin4_drv_init); //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
    73. module_exit(pin4_drv_exit);
    74. MODULE_LICENSE("GPL v2");

    上层应用程序 

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. int fd;
    8. int cmd;
    9. fd = open("/dev/pin4",O_RDWR);
    10. if(fd < 0){
    11. printf("open failed\n");
    12. perror("reson");
    13. }else{
    14. printf("open success\n");
    15. }
    16. printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");
    17. scanf("%d",&cmd);
    18. if(cmd == 0){
    19. printf("pin4设置成低电平\n");
    20. }else if(cmd == 1){
    21. printf("pin4设置成高电平\n");
    22. }
    23. fd = write(fd,&cmd,1);//写一个字符'1',写一个字节
    24. return 0;
    25. }

    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前两个参数传入的是空类型的指针,在框架代码中没有进行强制转换)

    pin4_drv_init函数是在demsg后被调用

     

    7.装载驱动 

    sudo insmod pin4driver.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打印信息,变量的值等等。
      

    有关驱动代码里面GPIO口地址的问题:
    在这里插入图片描述

    在这里插入图片描述 

    • 7Ennnnn意思是7E00000到7EFFFFFF,F2000000是3F000000映射的虚拟地址,然后7E00000和F200000对应,芯片手册里面使用的是和虚拟地址F200000有着对应关系的地址——7E00000,芯片手册上面地址偏移多少物理地址就偏移多少
    • 根据上方图片描述,外设的物理地址范围是m 0x3F000000 to 0x3FFFFFFF,所以你看到的7E200000对应的实际物理地址应该是0x3F000000 + (7E200000-7E000000)
    • GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4); GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4); GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
    • 0x3f200000,0x3f20001C,0x3f200028是物理地址,树莓派的外设空间的起始地址是0x3f000000,根据芯片手册可知对应寄存器的偏移量为,比如GPFSEL0寄存器的实际地址是0x3f200000=0x3F000000 + (7E200000-7E000000)
    • 然后通过数据手册可以看到,树莓派相关寄存器的总线地址(和映射的虚拟地址有某种对应关系的地址)进而可得知偏移量,如下图所示:

    在这里插入图片描述

    其他 

    简单了解:DMA(direct memory access)(直接存储器访问)

    在这里插入图片描述

    大数据的快速拷贝单元。

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

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

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

    md5sum file.c
    

    唯一标识,相同则两个文件为同一文件
     

    如何清空vi文件下所有的内容 

    在命令模式下,首先执行 gg 这里是跳至文件首行

    再执行:dG 这样就清空了整个文件!  

  • 相关阅读:
    【数据分享】2006-2021年我国城市级别的市容环境卫生相关指标(20多项指标)
    驱动开发:内核中的自旋锁结构
    XPS—专项文献阅读-科学指南针
    PyCharm因安装了illuminated Cloud插件导致加载项目失败
    【七大排序算法】插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序、归并排序
    XXL-Job分布式任务调度框架-知识点汇总5
    TortoiseGit设置作者信息和用户名、密码存储
    WPF中prism模块化
    640.Solve the Equation
    分享一些我的远程办公经验
  • 原文地址:https://blog.csdn.net/weixin_50546241/article/details/126528016