本帖最后由 LINUX课程 于 2024-9-11 11:06 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述 在完成了平台总线的学习之后,我们将进行设备树的学习。设备树实际上是对平台总线的一种简化。学习完本章之后,你应该能有所了解。 实验目的: - 理解设备树的结构。
- 掌握设备树的编写。
- 理解为什么需要设备树。
- 掌握基于设备树的驱动编写。
2 系统框图 和平台总线实现的驱动不同的地方在于,设备树将平台设备,也就是platform_device进行了完全的抽象,对于不同的设备并不需要去写一个个平台设备。而只是需要修改设备树就行了。设备树的框图具体如下。 实质上与平台总线的实现并没有太大的区别,重要的是,通过设备树简化了平台设备的部分。 3 介绍 这个部分将主要讲述设备树的结构。设备树启动过程。设备树该如何写。 3.1 设备树 设备树是一个用节点描述系统中设备的树状结构。如下图。 一根节点多个子节点,子节点又可以包含多个子节点,以树状的结构散开。在程序分析部分将会看到详细的设备树的代码。 (在部署好开发环境的虚拟机上的 /uisrc-lab-xlnx/sources/kernel/arch/arm64/boot/dts/xilinx 文件中能够查看到具体的设备树文件。这个目录也是之前制作系统时,所替换设备树文件的地方。) 3.2 设备树文件类型 在设备树目录内,将会出现不同后缀名的文件。这里介绍一下,不同后缀名的意思。 早期的Linux还没有设备树这个概念的时候,为了防止内核的体积太过臃肿,设计了平台设备和平台驱动两个结构,但是随着设备树的引进,平台设备被设备树所取代。 在编写驱动程序时,我们只需关心设备树和平台驱动两样东西,使用平台驱动直接调用设备树的内容。 在了解设备树之前,先要搞清楚以下几个概念: - DTS:dts文件是对Device Tree的描述,放置在内核的/arch/arm64/boot/dts目录,描述了一个板子的硬件资源。以前写在mach-xxx文件中的内容被转成了dts文件。
- DTC:编译工具,存放在目录scripts/dtc位置,它可以将.dts文件编译成.dtb文件。
- DTB:DTC编译*.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。
- DTSI:由于同一系列SOC很多相同的地方,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。
3.3 dts节点(node) 设备树节点是用节点名和单元地址和一个用大括号作为节点定义的开始和结尾来定义的。 - [lable:]:设备树文件允许标签附加在任何节点或者属性上。
- node-name:是指节点的名字。
- [@unit-address]:是指节点所在的基地址。
- [properties definitions]:是指相关属性的定义。
- [child nodes]:是指相关的子节点。
如果父节点中有子节点相同的属性,那么以设备树的父节点的属性为主。 3.4 dts文件结构 所有的dts文件都需要有一个root节点,并且root节点内必须有一个cpus节点和至少一个的memory节点。如下图。 3.5 root节点 设备树文件有一个root节点,所有的其他设备节点都是它的子节点。下面展示的是它的一些必须属性和可选属性。 3.6 aliases节点 设备树可以有一个别名节点,也就是aliases节点。用来定义一个或者更多的属性。别名节点只能存在于设备树的root节点中,并且节点名字为aliaes。 客户端程序会通过alias的属性名来引用设备的全路径,或者部分路径。 3.7 chosen节点 /chosen 节点并不代表系统中的一个真实的设备,但是,描述了当系统固件在运行的时候会被选择或者指定的参数。这个节点一定得在root节点下。下图是他的可选属性。 3.8 memory节点 所有的设备树文件都需要内存设备节点,用来描述系统物理内存的布局。如果一个系统有多个范围的内存,多个内存节点将会被创建。或者可以在一个单独的内存节点的reg属性中指定多个范围的内存。下图是它可选和必须属性。 3.9 cpus节点 设备树所必须的cpus节点,它不代表系统中一个真实的设备。它作为一个系统cpus的子cpu节点的容器存在。下图是它所需的属性。 1:/cpus/cpu节点 一个cpu节点就代表一个硬件的可执行块。能够运行操作系统而不会受到运行其他操作系统cpu的干扰。下图为它所需的属性。 4 搭建工程 这个部分主要讲如何把dts文件放入开发环境,然后编译成系统文件。当然,附件中已经提供好修改过设备树的文件,在BOOT内,只需要将挂载的SD卡中的BOOT部分替换为附件中的就行。 4.1 根据dts文件制作系统 在已经部署好开发环境的基础上进行,确定系统的其他文件已经拷贝至对应目录。参考“[米联客-XILINX-H3_CZ08_7100] LINUX驱动篇连载-01 Hello World”的制作系统部分。这里在已经制作过的系统的基础上,仅替换 dts 设备文件,做简单演示。 将“6.DeviceTree”中“soc_dts”文件夹内的dts文件复制至“boards/mz7x/ubuntu/output/files/kernel-dts”、“boards/mz7x/ubuntu/output/files/uboot-dts”两个文件夹内。 在 “uisrc-lab-xlnx” 目录下,打开终端输入“source /script/mz7xcfg.sh”,先后输入 “move_file.sh”, “make_uboot.sh”,“make_kernel.sh”,“create_image.sh”。 在完成了系统制作之后,打开“uisrc-lab-xlnx/boards/mz7x/ubuntu/images/boot”。找到生成的boot文件。将其拷贝复制进U盘的BOOT文件内。 4.2 上传驱动文件 将“DriveTree”文件夹上传至虚拟机。使用各种方式都可以。 至此,准备工作已经完成。 5 程序分析 程序分析部分和之前有些不一样,这次除了分析驱动文件之外,还需要分析一个设备树文件。在理解前面的设备树介绍的情况下,我们来实践分析一下设备树文件。 5.1 设备树分析 zynq-mz7x.dts - /*
- * Copyright (C) 2011 - 2014 Xilinx
- * Copyright (C) 2012 National Instruments Corp.
- *
- * This software is licensed under the terms of the GNU General Public
- * License version 2, as published by the Free Software Foundation, and
- * may be copied, distributed, and modified under those terms.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- */
- /dts-v1/;
- /include/ "zynq-7000.dtsi"
- / {
- model = "Zynq MZ7X Development Board";
- compatible = "xlnx,zynq-MZ7X", "xlnx,zynq-7000";
- aliases {
- ethernet0 = &axi_ethernet_0;
- ethernet1 = &gem1;
- serial0 = &uart0;
- spi0 = &qspi;
- mmc0 = &sdhci0;
- mmc1 = &sdhci1;
- };
- memory@0 {
- device_type = "memory";
- reg = <0x0 0x40000000>;
- };
- chosen {
- bootargs = "";
- stdout-path = "serial0:115200n8";
- };
- usb_phy1: phy0@e0003000 {
- compatible = "ulpi-phy";
- #phy-cells = <0>;
- reg = <0xe0003000 0x1000>;
- view-port = <0x0170>;
- drv-vbus;
- };
- gpio-led {
- compatible = "xlnx,zynqmp-led-1.0";
- gpios = <&gpio0 0x7 0x0>;
- };
- };
- / {
- cpus {
- cpu@0 {
- operating-points = <666666 1000000 333333 1000000>;
- };
- };
- };
- / {
- amba_pl: amba_pl {
- #address-cells = <1>;
- #size-cells = <1>;
- compatible = "simple-bus";
- ranges ;
- axi_dma_0: dma@40400000 {
- #dma-cells = <1>;
- axistream-connected = <&axi_ethernet_0>;
- axistream-control-connected = <&axi_ethernet_0>;
- clock-names = "s_axi_lite_aclk", "m_axi_sg_aclk", "m_axi_mm2s_aclk", "m_axi_s2mm_aclk";
- clocks = <&clkc 15>, <&clkc 15>, <&clkc 15>, <&clkc 15>;
- compatible = "xlnx,eth-dma";
- interrupt-names = "mm2s_introut", "s2mm_introut";
- interrupt-parent = <&intc>;
- interrupts = <0 29 4 0 30 4>;
- reg = <0x40400000 0x10000>;
- xlnx,addrwidth = /bits/ 8 <0x20>;
- xlnx,include-dre ;
- xlnx,num-queues = /bits/ 16 <0x1>;
- };
- axi_ethernet_0: ethernet@41000000 {
- axistream-connected = <&axi_dma_0>;
- axistream-control-connected = <&axi_dma_0>;
- clock-frequency = <100000000>;
- clock-names = "s_axi_lite_clk", "axis_clk", "gtx_clk", "ref_clk";
- clocks = <&clkc 15>, <&clkc 15>, <&clkc 16>, <&clkc 17>;
- compatible = "xlnx,axi-ethernet-7.2", "xlnx,axi-ethernet-1.00.a";
- device_type = "network";
- interrupt-names = "mac_irq", "interrupt";
- interrupt-parent = <&intc>;
- interrupts = <0 31 1 0 32 4>;
- local-mac-address = [00 0a 35 00 00 00];
- phy-mode = "rgmii";
- reg = <0x41000000 0x40000>;
- xlnx = <0x0>;
- xlnx,axiliteclkrate = <0x0>;
- xlnx,axisclkrate = <0x0>;
- xlnx,channel-ids = <0x1>;
- xlnx,clockselection = <0x0>;
- xlnx,enableasyncsgmii = <0x0>;
- xlnx,gt-type = <0x0>;
- xlnx,gtinex = <0x0>;
- xlnx,gtlocation = <0x0>;
- xlnx,gtrefclksrc = <0x0>;
- xlnx,include-dre ;
- xlnx,instantiatebitslice0 = <0x0>;
- xlnx,num-queues = /bits/ 16 <0x1>;
- xlnx,phy-type = <0x8>;
- xlnx,phyaddr = <0x1>;
- xlnx,phyrst-board-interface-dummy-port = <0x0>;
- xlnx,rable = <0x0>;
- xlnx,rxcsum = <0x0>;
- xlnx,rxlane0-placement = <0x0>;
- xlnx,rxlane1-placement = <0x0>;
- xlnx,rxmem = <0x1000>;
- xlnx,rxnibblebitslice0used = <0x0>;
- xlnx,tx-in-upper-nibble = <0x1>;
- xlnx,txcsum = <0x0>;
- xlnx,txlane0-placement = <0x0>;
- xlnx,txlane1-placement = <0x0>;
- xlnx,versal-gt-board-flow = <0x0>;
- phy-handle = <&phy0>;
- axi_ethernet_0_mdio: mdio {
- #address-cells = <1>;
- #size-cells = <0>;
- phy0: phy@1 {
- device_type = "ethernet-phy";
- reg = <0x1>;
- };
- };
- };
- axi_vdma_0: dma@43000000 {
- #dma-cells = <1>;
- clock-names = "s_axi_lite_aclk", "m_axi_mm2s_aclk", "m_axis_mm2s_aclk";
- clocks = <&clkc 15>, <&clkc 15>, <&clkc 15>;
- compatible = "xlnx,axi-vdma-6.3", "xlnx,axi-vdma-1.00.a";
- interrupt-names = "mm2s_introut";
- interrupt-parent = <&intc>;
- interrupts = <0 33 4>;
- reg = <0x43000000 0x10000>;
- xlnx,addrwidth = <0x40>;
- xlnx,flush-fsync = <0x1>;
- xlnx,num-fstores = <0x1>;
- dma-channel@43000000 {
- compatible = "xlnx,axi-vdma-mm2s-channel";
- interrupts = <0 33 4>;
- xlnx,datawidth = <0x20>;
- xlnx,device-id = <0x0>;
- xlnx,genlock-mode ;
- };
- };
- axi_vdma_lcd {
- compatible = "topic,vdma-fb";
- dmas = <&axi_vdma_0 0>;
- dma-names = "axivdma";
- };
- };
- };
- &gem1 {
- status = "okay";
- phy-mode = "rgmii-id";
- xlnx,ptp-enet-clock = <0x69f6bcb>;
- phy-handle = <ðernet_phy>;
- ethernet_phy: ethernet-phy@0 {
- reg = <0>;
- device_type = "ethernet-phy";
- };
- };
- &gpio0 {
- emio-gpio-width = <64>;
- gpio-mask-high = <0x0>;
- gpio-mask-low = <0x5600>;
- };
- &intc {
- num_cpus = <2>;
- num_interrupts = <96>;
- };
- &qspi {
- u-boot,dm-pre-reloc;
- status = "okay";
- is-dual = <1>;
- num-cs = <1>;
- flash@0 {
- compatible = "n25q128a11";
- reg = <0x0>;
- spi-tx-bus-width = <8>;
- spi-rx-bus-width = <8>;
- spi-max-frequency = <125000000>;
- };
- };
- &sdhci0 {
- u-boot,dm-pre-reloc;
- status = "okay";
- xlnx,has-cd = <0x0>;
- xlnx,has-power = <0x0>;
- xlnx,has-wp = <0x0>;
- };
- &sdhci1 {
- status = "okay";
- xlnx,has-cd = <0x0>;
- xlnx,has-power = <0x0>;
- xlnx,has-wp = <0x0>;
- };
- &uart0 {
- u-boot,dm-pre-reloc;
- device_type = "serial";
- port-number = <0>;
- status = "okay";
- };
- &usb1 {
- status = "okay";
- dr_mode = "host";
- usb-phy = <&usb_phy1>;
- usb-reset = <&gpio0 46 0>;
- };
- &clkc {
- fclk-enable = <0x7>;
- ps-clk-frequency = <33333333>;
- };
复制代码需要将LED的设备节点插入到设备树文件中,可以手动修改,也能直接将提供的设备树直接替换原来的,设备树的路径如下: - 内核设备树:/uisrc-lab-xlnx/sources/kernel/arch/arm64/boot/dts/Xilinx/zynqmp-mzux.dts
- uboot设备树:/uisrc-lab-xlnx/sources/uboot/arch/arm/dts/zynqmp-mzux.dts
1:gpio-led 节点 - gpio-led {
- compatible = "xlnx,zynqmp-led-1.0";
- gpios = <&gpio0 0x7 0x0>;
- };
复制代码含义:在设备树中添加gpio-led节点。 具体分析: - gpio-led:设备节点名,也就是node-name。
- compatible:兼容名,驱动将会通过这个在设备树中找到该节点。
- gpio:定义的两个led的管脚,分别是MIO38,和MIO23。
5.2 驱动程序分析 platform_gpio_drv.c - //1、添加头文件
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/of_gpio.h>
- #include <linux/platform_device.h>
- #include <linux/mod_devicetable.h>
- static unsigned int led_major;
- static struct class *led_class;
- struct led_driver
- {
- int gpio1;
- // int gpio2;
- int irq;
- struct device dev;
- };
- struct led_driver *led_dri = NULL;
- const struct file_operations led_fops = {
- };
- //在probe函数中打印获取数据包里面的名字及GPIO
- int gpio_pdrv_probe(struct platform_device *pdev)
- {
- struct device_node *node;
- // unsigned int gpio1, gpio2;
- unsigned int gpio1;
- unsigned int ret = 0;
- printk("gpio pdrv probe!\n");
- printk("pdrv name = %s!\n", pdev->name);
- //申请主设备号
- led_major = register_chrdev(0, "led_drv", &led_fops);
- if (led_major < 0)
- {
- printk("register chrdev led major error!\n");
- return -ENOMEM;
- }
- //创建类
- led_class = class_create(THIS_MODULE, "led_class");
- //创建设备
- device_create(led_class, NULL, MKDEV(led_major, 0), NULL, "led_device%d", 0);
- //硬件初始化
- //申请空间
- led_dri = devm_kmalloc(&pdev->dev, sizeof(struct led_driver), GFP_KERNEL);
- if (led_dri == NULL)
- {
- printk("devm kmalloc led_driver error!\n");
- return -1;
- }
- //获取从设备节点传过来的pdev中的dev及node节点
- led_dri->dev = pdev->dev;
- node = pdev->dev.of_node;
- //从node节点处获得GPIO号
- gpio1 = of_get_gpio(node, 0);
- printk("of get gpio1 number = %d\n", gpio1);
- if (gpio1 < 0)
- {
- printk("of get gpio error!\n");
- return -1;
- }
- // gpio2 = of_get_gpio(node, 1);
- // printk("of get gpio2 number = %d\n", gpio2);
- // if (gpio2 < 0)
- // {
- // printk("of get gpio error!\n");
- // return -1;
- // }
- //申请GPIO
- ret = gpio_request(gpio1, "plattree_led");
- if (ret < 0)
- {
- printk("plattree led gpio request error!\n");
- return ret;
- }
- // ret = gpio_request(gpio2, "plattree_led");
- // if (ret < 0)
- // {
- // printk("plattree led gpio request error!\n");
- // return ret;
- // }
- //设置GPIO为输出模式,并设备为0,灭灯
- gpio_direction_output(gpio1, 0);
- // gpio_direction_output(gpio2, 0);
- led_dri->gpio1 = gpio1;
- // led_dri->gpio2 = gpio2;
- return 0;
- }
- int gpio_pdrv_remove(struct platform_device *pdev)
- {
- printk("led pdrv remove!\n");
- gpio_set_value(led_dri->gpio1, 1);
- // gpio_set_value(led_dri->gpio2, 1);
- gpio_free(led_dri->gpio1);
- // gpio_free(led_dri->gpio2);
- device_destroy(led_class, MKDEV(led_major, 0));
- class_destroy(led_class);
- unregister_chrdev(led_major, "led_drv");
- return 0;
- }
- //of_match_table实现
- const struct of_device_id gpio_of_match_table[] = {
- {
- .compatible = "xlnx,zynqmp-led-1.0",
- },
- {}};
- //当驱动在设备中找到name之后,进行配对获取resource资源,进入probe函数
- struct platform_driver gpio_drv = {
- .driver = {
- .name = "zynqmp_led",
- .of_match_table = gpio_of_match_table,
- },
- .probe = gpio_pdrv_probe,
- .remove = gpio_pdrv_remove,
- };
- //实现装载入口函数和卸载入口函数
- static __init int platform_gpio_drv_init(void)
- {
- //创建pdrv,并且注册到总线中
- return platform_driver_register(&gpio_drv);
- }
- static __exit void platform_gpio_drv_exit(void)
- {
- //注销设备
- platform_driver_unregister(&gpio_drv);
- }
- //声明装载入口函数和卸载入口函数
- module_init(platform_gpio_drv_init);
- module_exit(platform_gpio_drv_exit);
- //添加GPL协议
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("msxbo");
复制代码行12~22,创建一个结构体用来存放设备信息。 行30~165,初始化函数,完成的任务有,申请设备号,创建类,创建设备,申请空间,获取node节点,获取GPIO号,申请GOIO,设置GPIO输出模式并置0。 行167~189,驱动注销,先将LED灯全部点亮,然后释放LED占用的GPIO资源,最后依次注销初始化中的内容。 1:platform_driver_register函数 - int platform_driver_register(struct platform_driver *driver)
复制代码含义:向平台总线注册平台驱动。 具体分析: - pdrv:注册的平台驱动。
- 返回值:0,设置成功;负值,设置失败。
2:platform_driver结构体 - //2.当驱动在设备中找到name之后,进行配对获取resource资源,进入probe函数
- struct platform_driver gpio_drv = {
- .driver = {
- .name = "zynqmp_led",
- .of_match_table = gpio_of_match_table,
- },
- .probe = gpio_pdrv_probe,
- .remove = gpio_pdrv_remove,
- };
复制代码含义:构建平台注册的结构体。 具体分析: - .of_match_table:用来在设备树中找到对应node的函数。
- .probe:gpio初始化函数。
- .remoe:当移除gpio时会执行的函数。
3:of_get_gpio函数 ~/uisrc-lab-xlnx/sources/kernel/include/linux/of_gpio.h - /**
- * of_get_gpio() - Get a GPIO number to use with GPIO API
- * @np: device node to get GPIO from
- * @index: index of the GPIO
- *
- * Returns GPIO number to use with Linux generic GPIO API, or one of the errno
- * value on the error condition.
- */
- static inline int of_get_gpio(struct device_node *np, int index)
- {
- return of_get_gpio_flags(np, index, NULL);
- }
复制代码含义:从节点出通过index获取GPIO号。 具体分析: 4:gpio_request函数 - int gpio_request(unsigned gpio, const char *label)
复制代码含义:gpio_request函数用来向系统申请一个GPIO管脚,每一个管脚在使用前都需要使用这个函数初始化,若不使用这个函数直接使用GPIO,则GPIO不会反应。 具体分析: - 参数:gpio,需要申请的gpio标号;lable,gpio的名字,可自由设定。
- 返回值:0,申请成功;其他值,申请失败。
5:gpio_direction_output函数 - static inline int gpio_direction_output(unsigned gpio, int value)
复制代码含义:设置gpio为输出模式,并设置0或1,来控制输出的开关。 具体分析: - gpio:为需要申请的gpio标号。
- value:0为灭灯,1为开灯。
6:gpio_set_value函数 - static inline void gpio_set_value(unsigned gpio, int value)
复制代码含义:设置gpio的引脚值。 6 程序编译 这个部分先介绍makefile文档,然后再在虚拟机终端进行交叉编译,生成驱动文件。 6.1 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_gpio_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
- #指定编译哪个文件
- obj-m += $(MODULE).o
复制代码第10行输入自定义的文件名。其他部分不用修改。详细解释参考hello world的程序编译部分。 6.2 交叉编译 打开4.1节已经上传完成的文件。右击桌面打开终端,确定终端目录在当前文件夹内。输入“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 程序准备 将6.2于虚拟机中交叉编译完成的驱动文件上传至开发板。 确认开发板上的灯是否都亮起。 在终端管理员权限下输入“insmod platform_gpio_drv.ko”。 7.3 实验结果。 打开终端,输入dmesg。内核打印出驱动中所要求的数据。 |