本帖最后由 LINUX课程 于 2024-9-11 11:05 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述 我们已经从寄存器控制GPIO,过渡到子系统控制GPIO。而现在将进入更高一层的抽象,进入到平台总线部分。 本实验是通过平台总线来控制GPIO的相关功能。 实验目的: - 理解需要平台总线的原因。
- 理解平台总线的具体实现。
- 掌握具体的平台总线构建方式。
2 系统框图 这里先简单的介绍一下平台总线的构成。平台总线由三个部分构成,驱动(platform_driver),设备(platform_device),和总线(platform_bus)。 一个现实的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,那为了兼容的话就需要分别封装好加入驱动程序,久而久之驱动就会变得非常臃肿,如下图左侧所示: 为了兼容所有的硬件设备信息,就得不停的给驱动程序加入设备信息,随着硬件设备的不断增加。驱动程序也会变得越来越臃肿。为了解决这个问题,Linux提出了软件分层的想法,也就将驱动分为了平台设备和平台驱动和总线,如上图右侧。 不仅如此,Linux还提炼一个input的核心层出来,把跟Linux接口以及整个一套input事件的汇报机制都在这里面实现,进而更进一步的减少了重复的代码的出现。 在Linux设备驱动框架的设计中, 除了有分层设计以外, 还有分隔的思想。实现多核cpu通过核心层的通用API对不同的外设进行访问。 一个驱动可以对应多个设备,但是一个设备只能对应一个驱动,在这些设备匹配驱动时,统一通过总线来进行访问。 4 搭建工程 搭建工程vivado部分具体参考本教程的第一章的Hello world部分。这里仅将实验的准备文件准备好。 4.1 上传文件 打开“5.平台总线”文件夹,将内部的“1.Platform”文件上传至已经完成了开发环境部署的虚拟机。具体上传方式,可以选择 U 盘,scp,或者 vtools。 准备工作完成。 5 程序分析 这部分将着重讲述平台驱动和平台设备这两个部分。首先讲的将是平台设备。 5.1 平台设备(platform_device) Platform_led_dev.c - //添加头文件
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/platform_device.h>
- #define MIO_RES_BASE 970 // 1024 - (54 + 64)
- #define MIO_RES_SIZE 2
- //底层硬件信息存储在resource中
- //start---需要传递MIO启始引脚值
- //end-----传递多少个MIO
- //name----资源名称
- //flags---标志位IORESOURCE_IO表示IO资源,IORESOURCE_MEM表示内存资源
- struct resource led_res[] = {
- [0] = {
- .start = MIO_RES_BASE,
- .end = MIO_RES_BASE + MIO_RES_SIZE,
- .name = "zynqmp_res",
- .flags = IORESOURCE_IO,
- }};
- static void platform_device_led_release(struct device *dev)
- {
- }
- //当dev和drv根据name在内核链表中进行配对,成功之后drv获取dev中的resource资源
- static struct platform_device led_pdev = {
- .name = "zynqmp_led",
- .id = -1,
- .num_resources = ARRAY_SIZE(led_res),
- .resource = led_res,
- .dev = {
- .release = platform_device_led_release,
- }};
- //实现装载入口函数和卸载入口函数
- static __init int platform_led_dev_init(void)
- {
- //创建pdev并添加到总线中
- return platform_device_register(&led_pdev);
- }
- static __exit void platform_led_dev_exit(void)
- {
- //注销设备
- platform_device_unregister(&led_pdev);
- }
- //声明装载入口函数
- module_init(platform_led_dev_init);
- module_exit(platform_led_dev_exit);
- //添加GPL协议
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("msxbo");
复制代码行9~20,resource资源,描述了硬件的相关信息与位置。 行26~34,配置平台设备的各项内容,本章节使用名称配对。 行36~41,平台设备初始化函数,这里向系统注册了一个设备驱动。 行43~47,平台设备注销函数,在被注销时通知系统注销这个平台设备。 1:platform_device_register函数 - int platform_device_register(struct platform_device *pdev)
复制代码含义:创建pdev并添加到总线中去。 具体分析: 在不支持设备树的Linux版本中,我们需要在platform_device结构体来描述设备信息,然后使用platform_device_register注册到系统的内核中去。 - pdev:注册的平台设备。
- 返回值:0,设置成功;负值,设置失败。
2:platform_device 结构体 - struct platform_device
- {
- const char *name;
- int id;
- bool id_auto;
- struct device dev;
- u32 num_resources;
- struct resource *resource;
- const struct platform_device_id *id_entry;
- char *driver_override; /* Driver name to force a match */
- /* MFD cell pointer */
- struct mfd_cell *mfd_cell;
- /* arch specific additions */
- struct pdev_archdata archdata;
- };
复制代码含义:platform_device结构体代表了平台设备,一般用于设定设备的硬件参数,比如寄存器位置,硬件位置等信息。可以在 /include/linux/platform_device.h中找到该结构体。 具体分析: - *name:名称,观察11.1.2的图可发现名称是驱动和设备匹配的关键
- id:设备数量,-1表示仅此一个设备
- num_resources:资源的数量
- resources:资源数组
3:resource结构体 - struct resource {
- resource_size_t start;
- resource_size_t end;
- const char *name;
- unsigned long flags;
- unsigned long desc;
- struct resource *parent, *sibling, *child;
- };
复制代码含义:底层硬件信息被储存在resource中。可以在/include/linux/ioport.h中找到该结构体。 具体分析: - start:需要传递MIO起始引脚值,可以参考GPIO子系统部分MIO引脚的计算。
- end:MIO结束的引脚值。
- name:资源名称。
- flag:资源类型。
4:platform_device_unregister函数 - void platform_device_unregister(struct platform_device *pdev)
复制代码含义:注销平台设备,从总线中清除该设备。 5.2 平台驱动(platform_driver) platform_led_drv.c - //填加头文件
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/platform_device.h>
- //3.在probe函数中打印获取数据包里面的名字及GPIO
- int led_pdrv_probe(struct platform_device *pdev)
- {
- printk(KERN_CRIT "led pdrv probe!\n");
- printk(KERN_CRIT "GPIO = %lld\n", pdev->resource->start);
- printk(KERN_CRIT "led res name = %s\n", pdev->resource->name);
- return 0;
- }
- int led_pdrv_remove(struct platform_device *pdev)
- {
- printk("led pdrv remove!\n");
- return 0;
- }
- //2.当drv在dev中找到name之后,进行配对获取resource资源,进入probe函数
- struct platform_driver led_drv = {
- .driver = {
- .name = "zynqmp_led",
- },
- .probe = led_pdrv_probe,
- .remove = led_pdrv_remove,
- };
- //实现装载入口函数和卸载入口函数
- static __init int platform_led_drv_init(void)
- {
- //1.创建pdrv,并且注册到总线中
- return platform_driver_register(&led_drv);
- }
- static __exit void platform_led_drv_exit(void)
- {
- //4.注销设备
- platform_driver_unregister(&led_drv);
- }
- //声明装载入口函数和卸载入口函数
- module_init(platform_led_drv_init);
- module_exit(platform_led_drv_exit);
- //添加GPL协议
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("msxbo");
复制代码行6~13,当普通设备与平台驱动建立匹配关系时,自动调用改函数进行初始化。这里我们读取了平台设备的几个参数,用来验证平台驱动已与平台设备达成匹配。 行15~19,注销函数,其功能与初始化函数相反,其在驱动注销时自动调用,释放初始化函数所申请的所以资源。 行21~28,之前已经讲过的平台驱动结构体,用来匹配设备,获取资源,设置初始化与注销函数。 行30~35,驱动入口函数,向系统注册了一个平台驱动。 行37~41,驱动卸载函数,用来注销平台驱动。 1:platform_driver_register函数 - int platform_driver_register(struct platform_driver *driver)
复制代码含义:向平台总线注册平台驱动。 具体分析: - pdrv:注册的平台驱动。
- 返回值:0,设置成功;负值,设置失败。
2:platform_driver结构体 - struct platform_driver {
- int (*probe)(struct platform_device *);
- int (*remove)(struct platform_device *);
- void (*shutdown)(struct platform_device *);
- int (*suspend)(struct platform_device *, pm_message_t state);
- int (*resume)(struct platform_device *);
- struct device_driver driver;
- const struct platform_device_id *id_table;
- bool prevent_deferred_probe;
- };
复制代码含义:platform_driver结构体代表了设备驱动,用于提供平台设备来控制硬件的动作,匹配后的平台驱动可以访问到平台设备中的信息,它们靠名字来识别匹配。 具体分析: - probe:在平台设备与平台驱动匹配成功时,会调用此函数,顾名思义其作用是初始化,原来完成原来的驱动入口程序的相关任务。
- remove:在平台设备或平台驱动被卸载时自动调用,与初始化的工作相反,remove函数需要逐一注销之前初始化过的设备。
- driver:用来匹配平台设备的结构体,本章节使用该结构体的name成员来匹配。
3:probe函数 - int led_pdrv_probe(struct platform_device *pdev)
- {
- printk(KERN_CRIT "led pdrv probe!\n");
- printk(KERN_CRIT "GPIO = %lld\n", pdev->resource->start);
- printk(KERN_CRIT "led res name = %s\n", pdev->resource->name);
- return 0;
- }
复制代码简单分析下,这个函数。当平台设备和平台驱动都被安装进入内核后,平台驱动可以获得到平台设备内的数据。也就是这里pdev结构体中的数据。 4:platform_driver_unregister函数 - void platform_driver_unregister(struct platform_driver *drv)
复制代码含义:将注册在平台中的驱动进行注销。 6 程序编译 这部分将简单讲述下编译过程。详细的Makefile参考hello world部分。 6.1 Makefile文件 Makefile - #已经编译过的内核源码路径
- KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel
- export ARCH=arm
- export CROSS_COMPILE=arm-linux-gnueabihf-
- #当前路径
- CURRENT_DIR = $(shell pwd)
- MODULE = platform_led_dev
- MODULE2 = platform_led_drv
- all :
- #进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
- rm -rf *.mod.c *.mod.o *.symvers *.order *.o
- ifneq ($(APP), )
- $(CROSS_COMPILE)gcc $(APP).c -o $(APP)
- endif
- clean :
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
- rm -rf $(APP) *.tmp
- #指定编译哪个文件
- obj-m += $(MODULE).o
- obj-m += $(MODULE2).o
复制代码第10和11行,将文件名填入正确。 6.2 虚拟机进行交叉编译 在虚拟机上进行交叉编译,具体请参考第一章hello world部分。在已经将完成了开发环境部署的虚拟机的终端上输入“make”。 编译成功如上图。 7 演示 7.1 硬件准备 SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”) 将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。 7.2 程序准备 确认“platform_led_dev.ko”和“platform_led_drv.ko”已经在开发板上。 在管理员权限下,先输入“insmod platform_led_dev.ko”,然后再输入“insmod platform_led_drv.ko”。先平台设备还是先平台驱动,对结果并没有影响。这里只是为了避免一次输入两个驱动,从而导致只安装了一个驱动的错误。 7.3 实验结果 如果是用串口连接的话,在完成两次驱动安装之后,终端会直接显示结果。 如果是通过ssh连接的。那么在终端输入“dmesg | tail”,也会打印出相同的结果。 终端输出的数据是平台驱动通过平台总线从平台设备处获取的。 测试成功。 |