【SoC FPGA学习】十二、Linux 设备树的原理与编写I2C控制器应用实例

在《【SoC FPGA学习】八、从零开始体验一把为 HPS 添加外设,以 UART 外设为例》章节,学习了如何使用SoC EDS 软件为创建好的包含 HPS 的 Qsys 系统添加 UART 外设并生成相应的设备树(dts) 文件。在《【SoC FPGA学习】十一、基于 Linux 应用程序的 HPS 配置 FPGA》章节,提到了使用开发软件安装包提供的不含 FPGA 逻辑部分的设备树文件来配合启动 Linux 系统。那么什么是设备树,如何得到适配硬件系统的设备树, linux系统又是如何使用设备树信息来加载各种设备驱动的呢?本节将针对上述问题,以一个具体的实例,讲解设备树的运用。

一、什么是设备树

在讲到设备树之前,先看一个具体的应用场景。 对于一个 ARM 处理器,一般其片上都会集成了有较多的外设接口, 包括 I2C、 SPI、 UART 等。 而I2C、 SPI 都属于总线属性, 在这些总线上又会连接其他的外部器件。 例如在I2C 总线上,又会连接 EEPROM、 I2C 接口的各种传感器、 LCD 显示屏、 RTC等。 那么 Linux 系统如何能够知道 I2C 总线上连接了哪些设备, 又如何知道这些设备的具体信息呢。

在早期的 Linux 系统中,使用的是硬件描述文件的形式来实现该功能的。每一个具体的硬件平台都会在 Linux 系统源码包的 arch/arm/mach-xxx/目录下存在一个硬件信息描述的源码包,在该源码包中定义了 GPIO 的使用,外设, i2c总线等系统信息,如果对某个硬件平台进行了修改, 例如将 EEPROM 的容量从16Kb 更换为了 64Kb,或者在 I2C 总线上新增了一个从机,则需要修改对应的硬件描述文件,然后重新编译内核。

在 arch/arm/下定义了很多 mach-xxx 的文件夹,一般是按照厂商或者平台命名,例如高通平台的为 mach-msm, marvell 的为 mach-mmp, mach-pxa。随着新的硬件平台不断产生, 为了支持这些硬件平台, Linux 系统中会增加越来越多的板级描述文件, 从而导致系统中的冗杂文件越来越多。

为了解决这个问题, Linux 内核从 3.x 开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。

比如在 ARM Linux 内,一个.dts(device tree source)文件对应一个 ARM 的machine,一般放置在内核的"arch/arm/boot/dts/“目录内,比如友晶的 DE0-nano SoC 开发板就是"arch/arm/boot/dts/ socfpga_cyclone5_de0_sockit.dts”。这个文件可以通过$make dtbs 命令编译成二进制的.dtb 文件供内核驱动使用。

基于同样的软件分层设计的思想,由于一个 SoC 可能对应多个 machine,如果每个 machine 的设备树都写成一个完全独立的.dts 文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题, Linux 设备树目录把一个 SoC 公用的部分或者多个 machine 共同的部分提炼为相应的.dtsi 文件。这样每个.dts 就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi 文件, 这样就使整个设备树的管理更加有序。 例如, 对于 Intel 的 SoC FPGA 器件,其包括 Cyclone V、Arria V、 Arria 10 三个系列,这三个系列中,有很多内容是相同的,可以作为公共部分,因此在 linux 源码中,使用了 socfpga.dtsi 文件来描述所有 socfpga 器件通用的部分,然后针对 Cyclone V、 Arria V、 Arria 10 这三个系列,又分别使用了socfpga_cyclone5.dtsi、 socfpga_arria5.dtsi、 socfpga_arria10.dtsi 三个文件来描述各个系列的硬件中公共的部分。当具体到某个特定的硬件板卡,如 DE0-nano-SoC开发,其设备树文件 socfpga_cyclone5_de0_sockit.dts 正文的第一行就是使用了#include “socfpga_cyclone5.dtsi” 来 包 含 cyclone5 器 件 的 通 用 部 分 , 而 在socfpga_cyclone5.dtsi 文件中,正文的第一行又是使用了#include "socfpga.dtsi"来包含所有 socfpga 器件的通用部分。 通过这种方式,简化了设备树的构成。

二、设备树基本格式

设备树用树状结构描述设备信息,它有以下几种特性:

  • 1、每个设备树文件都有一个根节点,每个设备都是一个节点。
  • 2、节点间可以嵌套,形成父子关系,这样就可以方便的描述设备间的关系。
  • 3、每个设备的属性都用一组 key-value 对(键值对)来描述。
  • 4、每个属性的描述用;结束
  • 5、为了方便分析,这里以《【SoC FPGA学习】八、从零开始体验一把为 HPS 添加外设,以 UART 外设为例》章节中生成的 soc_system.dts 文件的内容为例,介绍设备树文件的基本格式。

在这里插入图片描述

第 8 行,一个“/”表示一个硬件平台, 该硬件平台有以下属性

  • model:产品型号,为 Altera SOCFPGA Cyclone V。
  • compatible: 兼容属性,用来描述产品与 Linux 系统中支持的哪个平台兼容。

第 14 行~16 行, 描述了一个基本的以太网节点信息, ethernet@0xff702000表示该以太网位于绝对地址为 0xff702000 的位置,而根据 Cyclone V 器件手册, 0xff702000 这个地址正是 EMAC1 的绝对地址。

第 18 行~36 行, cpus 节点,描述了该开发板上的 CPU 节点信息。 在 SoCFPGA 器件中,包含了两个 Cortex-A9 的 CPU, 因此在 cpus 节点中又包含了两个子节点,分别名为 hps_0_arm_a9_0 和 hps_0_arm_a9_1。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
该部分首先是在第 88 行描述了一个名为 sopc 的节点,而在该节点下,又包含了一个名为 hps_0_bridges 的子节点,该节点表示了"axi_h2f"和 "axi_h2f_lw"两个 HPS 到 FPGA 的通信桥。 在该通信桥节点上,又描述了 I2C 控制器(i2c_0)、FrameReader 控制器(alt_vip_vfr_tft)、设备 ID(sysid_qsys)、 基于 PIO 的 LED控制器(led_pio)、 基于 PIO 的按键控制器(button_pio)、 串口控制器(uart_0、uart_1)、 spi 控制器(spi_0)。这些节点所代表的设备正是我们在 Platform Designer中添加的 FPGA 侧的 IP。 因此,如果我们在 FPGA 侧增加、删除、 修改了某些IP,然后使用 SoC EDS 软件重新生成 dts 文件,这些变化也都会体现在hps_0_bridges 节点下。 例如我们修改添加的 uart_1 控制器的默认波特率为9600bps,然后重新生成 dts 文件,则可以看到 dts 文件中 uart_1 节点下的 currentspeed 属性值会从 115200 变为 9600。

三、设备树加载驱动原理

对于一个特定的设备节点,例如 alt_vip_vfr_tft,又有众多的属性描述来该节点的详细信息,用来提供给 Linux 系统用作设备驱动中需要根据硬件具体设置而修改的一些可变信息。

对于 alt_vip_vfr_tft 节点,其最重要的一条属性是设备兼容属性,即compatible = “ALTR,vip-frame-reader-14.0”, “ALTR,vip-frame-reader-9.1”;该属性指明了节点所描述设备匹配 Linux 系统中的哪一个驱动。 以下为 Linux 系统源码中针对该控制器的驱动源码节选内容,该源码位于 linux-socfpga-socfpga-4.5\drivers\video\fbdev 目录下,名为 altvipfb.c。

在这里插入图片描述
在该文件的第 283~287 行,创建了一个数组,该数组中列出了该驱动程序兼容的设备属性为“altr,vip-frame-reader-1.0”和“altr,vip-frame-reader-9.1”, 所以,当 Linux 驱动程序在加载该设备驱动时,就会读取设备树中是否存在兼容属性为“altr,vip-frame-reader-1.0”或“altr,vip-frame-reader-9.1”的节点,如果存在,就会调用设备驱动加载程序来加载该设备驱动,如果没有找到兼容节点,就会跳过该设备驱动程序的安装。

对于 alt_vip_vfr_tft 节点, 还有一些其他的属性描述,如显示最大宽度(max-width),显示最大高度(max-height), 每个元色的数据位宽(bits-percolor), 每个颜色由多少个元色组成(colors-per-beat) 等, 这些信息在该驱动程序正式加载时会读取,并用作驱动程序中的相关参数。例如在 altvipfb.c 文件的第 84 行,有一个名为 altvipfb_of_setup()函数,该函数中第 90 行就读取了设备树该节点中的"max-width"属性并存储到了 fb 设备的 xres 参数中。 同样的,第98 行读取了设备树该节点中的"max-height"属性并存储到了 fb 设备的 yres 参数中。 然后实际运行时, fb 设备的驱动程序使用 xres 和 yres 参数来支持图像的正常显示。

在这里插入图片描述

可以看到,通过使用设备树, Linux 系统实现了硬件描述和软件编程的分离。 使用通用的软件程序,然后通过读取设备树中的各种节点信息,来完成驱动的加载以及驱动中参数的初始化,最终实现 Linux 系统启动时正确加载各个设备的驱动程序。

四、编写I2C控制器设备节点

在《【SoC FPGA学习】八、从零开始体验一把为 HPS 添加外设,以 UART 外设为例》实验中, 针对 Platform Designer中添加的 OC_I2C 控制器,由于 soc_system.sopcinfo 文件中没有对该控制器的各种属性进行描述,因此实验生成的 soc_system.dts 文件中 i2c_0 节点的compatible 属性值为 unknown, 导致 Linux 系统无法正确识别并加载该设备驱动。 程序清单如下所示

在这里插入图片描述

所以在开发板的串口终端中查看 dev 目录下面的内容时,只能看到 2 个 i2c控制器,分别为 i2c-0 和 i2c-1,如下图所示。

在这里插入图片描述
那么如何才能让 Linux 系统能够正确识别该 I2C 控制器并成功加载驱动呢? 由于 SoC EDS 无法正确的生成第三方 IP 核的设备树节点信息,所以我们需要手动修改 dts 文件,补充这些控制器的设备树节点信息。

linux-socfpga-socfpga-4.5\Documentation\devicetree\bindings 目录下, 对每一个 Linux 系统源码中已经支持的设备驱动程序, 都提供了一个设备树节点编写的说明文件, 例如在 Documentation\devicetree\bindings\i2c 目录下,有一个名为“i2c-ocores.txt”的文件,该文件就是 OC_I2C 控制器的设备树节点编写指导文件。该文件内容如下所示:

Device tree configuration for i2c-ocores

Required properties:
- compatible      : "opencores,i2c-ocores" or "aeroflexgaisler,i2cmst"
- reg             : bus address start and address range size of device
- interrupts      : interrupt number
- clocks          : handle to the controller clock; see the note below.
                    Mutually exclusive with opencores,ip-clock-frequency
- opencores,ip-clock-frequency: frequency of the controller clock in Hz;
                    see the note below. Mutually exclusive with clocks
- #address-cells  : should be <1>
- #size-cells     : should be <0>

Optional properties:
- clock-frequency : frequency of bus clock in Hz; see the note below.
                    Defaults to 100 KHz when the property is not specified
- reg-shift       : device register offsets are shifted by this value
- reg-io-width    : io register width in bytes (1, 2 or 4)
- regstep         : deprecated, use reg-shift above

Note
clock-frequency property is meant to control the bus frequency for i2c bus
drivers, but it was incorrectly used to specify i2c controller input clock
frequency. So the following rules are set to fix this situation:
- if clock-frequency is present and neither opencores,ip-clock-frequency nor
  clocks are, then clock-frequency specifies i2c controller clock frequency.
  This is to keep backwards compatibility with setups using old DTB. i2c bus
  frequency is fixed at 100 KHz.
- if clocks is present it specifies i2c controller clock. clock-frequency
  property specifies i2c bus frequency.
- if opencores,ip-clock-frequency is present it specifies i2c controller
  clock frequency. clock-frequency property specifies i2c bus frequency.

Examples:

	i2c0: ocores@a0000000 {
		#address-cells = <1>;
		#size-cells = <0>;
		compatible = "opencores,i2c-ocores";
		reg = <0xa0000000 0x8>;
		interrupts = <10>;
		opencores,ip-clock-frequency = <20000000>;

		reg-shift = <0>;	/* 8 bit registers */
		reg-io-width = <1>;	/* 8 bit read/write */

		dummy@60 {
			compatible = "dummy";
			reg = <0x60>;
		};
	};
or
	i2c0: ocores@a0000000 {
		#address-cells = <1>;
		#size-cells = <0>;
		compatible = "opencores,i2c-ocores";
		reg = <0xa0000000 0x8>;
		interrupts = <10>;
		clocks = <&osc>;
		clock-frequency = <400000>; /* i2c bus frequency 400 KHz */

		reg-shift = <0>;	/* 8 bit registers */
		reg-io-width = <1>;	/* 8 bit read/write */

		dummy@60 {
			compatible = "dummy";
			reg = <0x60>;
		};
	};

该文件首先说明了 Linux 系统识别该控制器时所需的必备属性, 例如compatible 属性必须为 “opencores,i2c-ocores” 或 “aeroflexgaisler,i2cmst”, 总线的起始和空间大小(reg 属性), 中断编号(interrupts), 时钟(clocks) 等。 然后列举了一些可选的属性,例如 I2C 总线频率(clock-frequency)、寄存器移位值(reg-shift),寄存器位宽(reg-io-width) 等,然后给出了两个具体的节点描述内容编写示例。 根据该示例,我们就可以修改 SoC EDS 软件生成的 dts 文件中 i2c_0 控制器的节点信息了。 修改时主要修改以下内容:

  • compatible :指定 compatible 的值为 “opencores,i2c-ocores”;
  • #address-cells:增加#address-cells = <1>属性;
  • #size-cells: 增加#size-cells = <0>属性;
  • clock-frequency: 增加 clock-frequency 属性并设置值为 400000Hz;
  • reg-shift: 增加 reg-shift = <0>属性
  • reg-io-width = <1>:增加 reg-io-width = <1>属性;
  • 在 i2c 节点上添加了一个空设备节点。

修改完成的 i2c_0 节点的描述内容如下所示:

i2c_0: fpga_i2c@0x100000000 {
	compatible = "opencores,i2c-ocores";
	reg = <0x00000001 0x00000000 0x00000040>;
	interrupt-parent = <&hps_0_arm_gic_0>;
	interrupts = <0 41 4>;
	clocks = <&clk_0>;
	#address-cells = <1>;
	#size-cells = <0>;
	clock-frequency = <400000>; /* i2c bus frequency 400 KHz */
	reg-shift = <0>; /* 8 bit registers */
	reg-io-width = <1>; /* 8 bit read/write */
	dummy@60 {
		compatible = "dummy";
		reg = <0xA0>;
	};
}; //end fpga_i2c@0x100000000 (i2c_0)

修改完成后,在 SoC EDS 中输入以下命令生成新的设备树二进制文件:

make dtb

五、加载 OC_I2C 驱动

将新生成的设备树二进制文件拷贝到开发板的 SD 卡中, 然后启动开发板, 在启动信息中会发现如下图所示的打印信息。

在这里插入图片描述
从该信息可以知道我们所添加的 i2c_0 设备已经被成功识别,由于与 HPS中的 i2c 硬 IP 控制器命名冲突,因此该控制器被重新命名为了 i2c-2,但是通过地址和 fpga_i2c 这个用户命名,我们任然能够唯一确定 i2c-2 就是我们刚刚添加的 OC_I2C 设备。

登录系统之后,使用 ls /dev 命令查看当前系统已经加载的设备,可以看到已经有了一个名为 i2c-2 的设备。 然后我们就可以在 Linux 应用程序中使用文件IO 的方式来操作该控制器了。

在这里插入图片描述

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页