[X]关闭

[米联客-XILINX-H3_CZ08_7100] LINUX驱动篇连载-04 平台驱动实现GPIO

文档创建者:LINUX课程
浏览次数:240
最后更新:2024-09-09
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-SOC » 2_LINUX应用开发
本帖最后由 LINUX课程 于 2024-9-11 11:05 编辑

软件版本:vitis2021.1(vivado2021.1)
操作系统:WIN10 64bit
硬件平台:适用XILINX Z7/ZU系列FPGA
登录“米联客”FPGA社区-www.uisrc.com视频课程、答疑解惑!

1 概述
我们已经从寄存器控制GPIO,过渡到子系统控制GPIO。而现在将进入更高一层的抽象,进入到平台总线部分。
本实验是通过平台总线来控制GPIO的相关功能。
实验目的:
  • 理解需要平台总线的原因。
  • 理解平台总线的具体实现。
  • 掌握具体的平台总线构建方式。
2 系统框图
        这里先简单的介绍一下平台总线的构成。平台总线由三个部分构成,驱动(platform_driver),设备(platform_device),和总线(platform_bus)。
image.jpg
        一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、 USB、 I2C、SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,在SoC系统中集成的独立外设控制器、挂接在SoC内存空间的外设等却不依附于此类总线。
  • bus:总线---用于维护device和driver
  • device:设备(名词)--描述硬件的信息
  • driver:驱动(动词)--描述操作硬件的方法
3 介绍
        介绍部分将主要讲解为什么需要使用平台总线,而不是之前的子系统。
3.1 驱动软件框架
        在平台总线使用之前的驱动程序是这样的。为了兼容所有型号的硬件,就不得不在驱动里加上各个硬件的硬件信息,就拿GPIO来说,A厂家的硬件GPIO口可能是1、2、3,B厂家为4、5、6,那为了兼容的话就需要分别封装好加入驱动程序,久而久之驱动就会变得非常臃肿,如下图左侧所示:
image.jpg
为了兼容所有的硬件设备信息,就得不停的给驱动程序加入设备信息,随着硬件设备的不断增加。驱动程序也会变得越来越臃肿。为了解决这个问题,Linux提出了软件分层的想法,也就将驱动分为了平台设备和平台驱动和总线,如上图右侧。
不仅如此,Linux还提炼一个input的核心层出来,把跟Linux接口以及整个一套input事件的汇报机制都在这里面实现,进而更进一步的减少了重复的代码的出现。
image.jpg
在Linux设备驱动框架的设计中, 除了有分层设计以外, 还有分隔的思想。实现多核cpu通过核心层的通用API对不同的外设进行访问。
image.jpg
一个驱动可以对应多个设备,但是一个设备只能对应一个驱动,在这些设备匹配驱动时,统一通过总线来进行访问。
image.jpg
4 搭建工程
        搭建工程vivado部分具体参考本教程的第一章的Hello world部分。这里仅将实验的准备文件准备好。
4.1 上传文件
        打开“5.平台总线”文件夹,将内部的“1.Platform”文件上传至已经完成了开发环境部署的虚拟机。具体上传方式,可以选择 U 盘,scp,或者 vtools。
image.jpg
image.jpg
准备工作完成。
5 程序分析
        这部分将着重讲述平台驱动和平台设备这两个部分。首先讲的将是平台设备。
5.1 平台设备(platform_device)
Platform_led_dev.c
  1. //添加头文件
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. #include <linux/platform_device.h>

  5. #define MIO_RES_BASE 970 // 1024 - (54 + 64)
  6. #define MIO_RES_SIZE 2

  7. //底层硬件信息存储在resource中
  8. //start---需要传递MIO启始引脚值
  9. //end-----传递多少个MIO
  10. //name----资源名称
  11. //flags---标志位IORESOURCE_IO表示IO资源,IORESOURCE_MEM表示内存资源
  12. struct resource led_res[] = {
  13. [0] = {
  14.   .start = MIO_RES_BASE,
  15.   .end = MIO_RES_BASE + MIO_RES_SIZE,
  16.   .name = "zynqmp_res",
  17.   .flags = IORESOURCE_IO,
  18. }};

  19. static void platform_device_led_release(struct device *dev)
  20. {
  21. }

  22. //当dev和drv根据name在内核链表中进行配对,成功之后drv获取dev中的resource资源
  23. static struct platform_device led_pdev = {
  24. .name = "zynqmp_led",
  25. .id = -1,
  26. .num_resources = ARRAY_SIZE(led_res),
  27. .resource = led_res,
  28. .dev = {
  29.   .release = platform_device_led_release,
  30. }};

  31. //实现装载入口函数和卸载入口函数
  32. static __init int platform_led_dev_init(void)
  33. {
  34. //创建pdev并添加到总线中
  35. return platform_device_register(&led_pdev);
  36. }

  37. static __exit void platform_led_dev_exit(void)
  38. {
  39. //注销设备
  40. platform_device_unregister(&led_pdev);
  41. }

  42. //声明装载入口函数
  43. module_init(platform_led_dev_init);
  44. module_exit(platform_led_dev_exit);

  45. //添加GPL协议
  46. MODULE_LICENSE("GPL");
  47. MODULE_AUTHOR("msxbo");
复制代码
行9~20,resource资源,描述了硬件的相关信息与位置。
行26~34,配置平台设备的各项内容,本章节使用名称配对。
行36~41,平台设备初始化函数,这里向系统注册了一个设备驱动。
行43~47,平台设备注销函数,在被注销时通知系统注销这个平台设备。
1:platform_device_register函数
  1. int platform_device_register(struct platform_device *pdev)
复制代码
含义:创建pdev并添加到总线中去。
具体分析:
        在不支持设备树的Linux版本中,我们需要在platform_device结构体来描述设备信息,然后使用platform_device_register注册到系统的内核中去。
  • pdev:注册的平台设备。
  • 返回值:0,设置成功;负值,设置失败。
2:platform_device 结构体
  1. struct platform_device
  2. {
  3.     const char *name;
  4.     int id;
  5.     bool id_auto;
  6.     struct device dev;
  7.     u32 num_resources;
  8.     struct resource *resource;

  9. const struct platform_device_id *id_entry;
  10. char *driver_override; /* Driver name to force a match */

  11. /* MFD cell pointer */
  12. struct mfd_cell *mfd_cell;

  13. /* arch specific additions */
  14. struct pdev_archdata archdata;
  15. };
复制代码
含义:platform_device结构体代表了平台设备,一般用于设定设备的硬件参数,比如寄存器位置,硬件位置等信息。可以在 /include/linux/platform_device.h中找到该结构体。
具体分析:
  • *name:名称,观察11.1.2的图可发现名称是驱动和设备匹配的关键
  • id:设备数量,-1表示仅此一个设备
  • num_resources:资源的数量
  • resources:资源数组
3:resource结构体
  1. struct resource {
  2.     resource_size_t start;
  3.     resource_size_t end;
  4.     const char *name;
  5.     unsigned long flags;
  6.     unsigned long desc;
  7.     struct resource *parent, *sibling, *child;
  8. };
复制代码
含义:底层硬件信息被储存在resource中。可以在/include/linux/ioport.h中找到该结构体。
具体分析:
  • start:需要传递MIO起始引脚值,可以参考GPIO子系统部分MIO引脚的计算。
  • end:MIO结束的引脚值。
  • name:资源名称。
  • flag:资源类型。
4:platform_device_unregister函数
  1. void platform_device_unregister(struct platform_device *pdev)
复制代码
含义:注销平台设备,从总线中清除该设备。
5.2 平台驱动(platform_driver)
platform_led_drv.c
  1. //填加头文件
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. #include <linux/platform_device.h>

  5. //3.在probe函数中打印获取数据包里面的名字及GPIO
  6. int led_pdrv_probe(struct platform_device *pdev)
  7. {
  8.      printk(KERN_CRIT "led pdrv probe!\n");
  9.     printk(KERN_CRIT "GPIO  =  %lld\n", pdev->resource->start);
  10.     printk(KERN_CRIT "led res name  =  %s\n", pdev->resource->name);
  11.     return 0;
  12. }

  13. int led_pdrv_remove(struct platform_device *pdev)
  14. {
  15.     printk("led pdrv remove!\n");
  16.     return 0;
  17. }

  18. //2.当drv在dev中找到name之后,进行配对获取resource资源,进入probe函数
  19. struct platform_driver led_drv = {
  20.     .driver = {
  21.         .name = "zynqmp_led",
  22.     },
  23.     .probe = led_pdrv_probe,
  24.     .remove = led_pdrv_remove,
  25. };

  26. //实现装载入口函数和卸载入口函数
  27. static __init int platform_led_drv_init(void)
  28. {
  29.     //1.创建pdrv,并且注册到总线中
  30.     return platform_driver_register(&led_drv);
  31. }

  32. static __exit void platform_led_drv_exit(void)
  33. {
  34.     //4.注销设备
  35.     platform_driver_unregister(&led_drv);
  36. }

  37. //声明装载入口函数和卸载入口函数
  38. module_init(platform_led_drv_init);
  39. module_exit(platform_led_drv_exit);

  40. //添加GPL协议
  41. MODULE_LICENSE("GPL");
  42. MODULE_AUTHOR("msxbo");
复制代码
行6~13,当普通设备与平台驱动建立匹配关系时,自动调用改函数进行初始化。这里我们读取了平台设备的几个参数,用来验证平台驱动已与平台设备达成匹配。
行15~19,注销函数,其功能与初始化函数相反,其在驱动注销时自动调用,释放初始化函数所申请的所以资源。
行21~28,之前已经讲过的平台驱动结构体,用来匹配设备,获取资源,设置初始化与注销函数。
行30~35,驱动入口函数,向系统注册了一个平台驱动。
行37~41,驱动卸载函数,用来注销平台驱动。
1:platform_driver_register函数
  1. int platform_driver_register(struct platform_driver *driver)
复制代码
含义:向平台总线注册平台驱动。
具体分析:
  • pdrv:注册的平台驱动。
  • 返回值:0,设置成功;负值,设置失败。
2:platform_driver结构体
  1. struct platform_driver {
  2.     int (*probe)(struct platform_device *);
  3.     int (*remove)(struct platform_device *);
  4.     void (*shutdown)(struct platform_device *);
  5.     int (*suspend)(struct platform_device *, pm_message_t state);
  6.     int (*resume)(struct platform_device *);
  7.     struct device_driver driver;
  8.     const struct platform_device_id *id_table;
  9.     bool prevent_deferred_probe;
  10. };
复制代码
含义:platform_driver结构体代表了设备驱动,用于提供平台设备来控制硬件的动作,匹配后的平台驱动可以访问到平台设备中的信息,它们靠名字来识别匹配。
具体分析:
  • probe:在平台设备与平台驱动匹配成功时,会调用此函数,顾名思义其作用是初始化,原来完成原来的驱动入口程序的相关任务。
  • remove:在平台设备或平台驱动被卸载时自动调用,与初始化的工作相反,remove函数需要逐一注销之前初始化过的设备。
  • driver:用来匹配平台设备的结构体,本章节使用该结构体的name成员来匹配。
3:probe函数
  1. int led_pdrv_probe(struct platform_device *pdev)
  2. {
  3.     printk(KERN_CRIT "led pdrv probe!\n");
  4.     printk(KERN_CRIT "GPIO  =  %lld\n", pdev->resource->start);
  5.     printk(KERN_CRIT "led res name  =  %s\n", pdev->resource->name);
  6.     return 0;
  7. }
复制代码
简单分析下,这个函数。当平台设备和平台驱动都被安装进入内核后,平台驱动可以获得到平台设备内的数据。也就是这里pdev结构体中的数据。
4:platform_driver_unregister函数
  1. void platform_driver_unregister(struct platform_driver *drv)
复制代码
含义:将注册在平台中的驱动进行注销。
6 程序编译
        这部分将简单讲述下编译过程。详细的Makefile参考hello world部分。
6.1 Makefile文件
Makefile
  1. #已经编译过的内核源码路径
  2. KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel

  3. export ARCH=arm
  4. export CROSS_COMPILE=arm-linux-gnueabihf-

  5. #当前路径
  6. CURRENT_DIR = $(shell pwd)

  7. MODULE = platform_led_dev
  8. MODULE2 = platform_led_drv

  9. all :
  10. #进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
  11. make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
  12. rm -rf *.mod.c *.mod.o *.symvers *.order *.o

  13. ifneq ($(APP), )
  14. $(CROSS_COMPILE)gcc $(APP).c -o $(APP)
  15. endif

  16. clean :
  17. make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
  18. rm -rf $(APP) *.tmp

  19. #指定编译哪个文件
  20. obj-m += $(MODULE).o
  21. obj-m += $(MODULE2).o
复制代码
第10和11行,将文件名填入正确。
6.2 虚拟机进行交叉编译
        在虚拟机上进行交叉编译,具体请参考第一章hello world部分。在已经将完成了开发环境部署的虚拟机的终端上输入“make”。
image.jpg
编译成功如上图。
7 演示
7.1 硬件准备
SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”)
2f5038eb9880afd532753935815b079.jpg
将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。
image.jpg
7.2 程序准备
确认“platform_led_dev.ko”和“platform_led_drv.ko”已经在开发板上。
image.jpg
在管理员权限下,先输入“insmod platform_led_dev.ko”,然后再输入“insmod platform_led_drv.ko”。先平台设备还是先平台驱动,对结果并没有影响。这里只是为了避免一次输入两个驱动,从而导致只安装了一个驱动的错误。
image.jpg
7.3 实验结果
如果是用串口连接的话,在完成两次驱动安装之后,终端会直接显示结果。
image.jpg
如果是通过ssh连接的。那么在终端输入“dmesg | tail”,也会打印出相同的结果。
image.jpg
终端输出的数据是平台驱动通过平台总线从平台设备处获取的。
测试成功。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则