基于ZYNQ芯片的外设驱动技术方案

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

一、BootLoader的移植制作 (2)
1、生成uboot.elf文件 (3)
2、system.bit生成 (5)
3、创建fsbl (8)
4、生成BOOT.BIN (9)
二、配置并编译linux内核 (10)
1、修改设备树内容 (10)
2、配置编译linux内核 (12)
三、Linux设备驱动移植 (14)
1、Linux设备驱动模型 (14)
2、Linux设备驱动移植 (15)
在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。

可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。

在嵌入式系统中,通常并没有像BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。

在一个基于zynq的嵌入式Linux系统中,系统在上电或复位时通常都从地址0x00000000处开始执行,而在这个地址处安排的通常就是系统的BootLoader程序。

想要在zynq芯片上顺利启动Linux并且驱动相关模块正常工作,首先需要正确移植BootLoader。

作为初始化硬件平台的一段Bare Metal代码,Bootloader的移植也并入了我们的工作。

所以综合起来说,我们的工作主要分为了三个部分:Bootloader的移植以及Linux内核和设备驱动的移植。

在进行移植工作之前,首先要做的是要在宿主机上面搭建好我们目标板的开发平台,以及下载好uboot源代码,Linux内核源代码以及相关驱动源代码。

(有的模块的驱动源代码在Linux内核源代码中已经包含进去了,那么就不用单独下载。

需要下载的驱动源代码只是针对标准Linux内核中不支持的部件而言。


一、BootLoader的移植制作
每一种操作系统都需要有自己的引导程序,最为人们所知的就是Windows 的BIOS。

缺失了这一部分,系统将无法正常启动。

对比与Windows的BIOS,android系统拥有自己的专属的BootLoader,它主要负责初始化系统平台的硬件设备,建立内存空间的映射关系。

在它完成初始化工作之后,继续负责把内核镜像和yaffs2格式的文件系统从SD卡载入到RAM中,完成Linux内核和文件系统的加载并设置内核的启动参数,紧接着启动内核。

等到内核真正启动了之后,BootLoader就杀死自己,把CPU让出给内核使用。

之后内核根据硬件环境做出系统初始化设置,并将CPU转让给yaffs2文件系统运行管理。

至此,Linux系统成功的启动起来。

下图1为bootloader的启动过程。

图1 BootLoader启动流程
在我们移植的BootLoader中,由于XC7Z020-CLG484-1包含了可编程逻辑(PL)和处理系统(PS)两个部分,所以它的启动过程与一般的BootLoader稍有不同。

BootLoader在它的启动过程中需要负责配置可编程逻辑(PL)和处理系统(PS)这两个部分。

使用Xilinx官方提供的SDK生成的BOOT.BIN文件包含了fsbl(first stage boot loader)文件、system.bit文件和uboot.elf文件,其中fsbl 文件主要用来加载system.bit并且配置处理系统(PS),而system.bit文件起到了配置可编程逻辑(PL)的作用,注意该文件其实并不是必要的,如果在生成BOOT.BIN的过程中缺少该文件,那么PL部分得不到配置,那么我们只是相当于把XC7Z020-CLG484-1当成ARM来使用。

之后系统开始加载ssbl(second stage boot loader)到RAM中,即uboot.elf文件,它主要作用是加载内核镜像到RAM 中。

如下图所示。

1、生成uboot.elf文件
Uboot源码的主要目录及其描述
根据上表可知,在进行uboot移植的过程中需要把相关目录下与目标平台相关的配置文件进行相应的修改,同时集成各种函数库以及驱动程序的支持。

我们使用make zynq_zed_config来配置uboot,修改代码的目录列举如下:
为uboot.elf,以便Xilinx ISE组件里面的SDK能够识别并用于生成BOOT.BIN。

2、system.bit生成
在现有的Digilent官网提供的基于XC7Z020-CLG484-1能够正常运行的linux 设计包ZedBoard_OOB_Design中,已经包含了最重要的系统组件的驱动程序,如串口、网口,USB等接口。

针对该设计包,我们需要根据我们的硬件环境进行适当的修改使之能够在我们的平台上运行,例如添加并打开模块所需要的SDIO1通道。

由于zynq芯片内部有54个MIO的引脚,这些引脚在满足数量的情况下可以分配给SDIO、SPI 或者Ethernet等外部设备,SD/SDIO的控制系统如下图所示。

故我们可以使用MIO接口来将SDIO1的通道引出到芯片外部,同时不需要我们自己添加约束文件。

Interconnect
Interconnect
SD/SDIO控制系统图
下表展示了SDIO接口与MIO的连接方式。

有引脚
根据查询上表的内容,我们结合第二章的图?,将SDIO1与MIO通道进行了如下的分配。

IO 外设信号
MIO 10 SDIO 1 data[0]
MIO 11 SDIO 1 cmd
MIO 12 SDIO 1 clk
MIO 13 SDIO 1 data[1]
MIO 14 SDIO 1 data[2]
MIO 15 SDIO 1 data[3]
MIO 16 SDIO 1 SD1_cd 在Xilinx Platform Studio中配置好上述MIO接口之后,确保zynq的系统图和MIO接口图如下所示,就可以开始生成网格以及system.bit比特流文件了。

3、创建fsbl
在成功生成比特流文件system.bit以后,我们需要将该设计导出到SDK中进行fsbl的创建以及BOOT.BIN的生成。

点击上图中Export Design,并选择:
在SDK中,我们需要创建FSBL类型的程序,用来初始化PS部分。

查看fsbl工程的代码,它同其它裸机代码一样都有一个main主函数入口,但是由于在fsbl加载之后,处理器PS部分并没有执行C代码的能力,而是只能执行简单的汇编代码,我们可以在
zynq_fsbl_bsp==>ps7_cortexa9_0==>libsrc==>standalone_v3_07_a==>src里找到asm_vector.S,它包含了BSP所提供的汇编指令。

它在零地址处给出了一个如下的代码段,在系统上电之后,PS部分会自动跳转到零地址并执行该地址的汇编指令。

码中执行_boot下的代码段,该代码段作用在于对系统进行初始化。

完成该代码段之后,此时PS就具备了执行C代码的能力了。

在_boot代码段的最后又有一个跳转指令,此时将跳转至xil.crt0.S中的_start代码段,该代码段将继续对系统进行初始化设置,我们可以看到,该代码第一段就是执行一个初始化CPU的动
函数并开始执行。

至此,系统根据main函数的代码开始初始化MIO,PLL,CLOCK,DDR等外部设备。

4、生成BOOT.BIN
最后在Xilinx SDK中,选择Create Boot Image选项,并将上面生成的三个文件uboot.elf、system.bit以及fsbl文件添加进去,点击生成BOOT.BIN。

此文件即是我们需要的BootLoader。

二、配置并编译linux内核
1、修改设备树内容
自linux3.x版本之后,嵌入式linux开始使用设备树的方式来存储目标板的硬件配置信息。

设备树是一种用来阐述目标板硬件结构设备信息的二进制文件,它提供了一种方法,基于该方法可以轻松的将目标板的各种外设转换为以数据驱动,而非以前内核中所使用的硬编码方式。

在Linux内核开始使用设备树之前,编译过的内核包含了对于硬件的全部描
述,BootLoader 只是单独的加载一个以二进制文件存在的内核镜像。

此外,BootLoader 还需要通过r2寄存器将存有额外信息的ATAGS 的指针传递给内核,以及通过r1寄存器传递机器码的方式通知内核当前在什么设备商被启动。

而如今的Linux 内核中不再包含对于硬件的描述,这部分已经被搬移到dtb(与Linux 内核独立的设备树二进制文件)中了。

所以如今的BootLoader 又增加了加载设备树二进制文件的功能,BootLoader 通过r2寄存器传递dtb 文件的地址。

如下图所示。

r1=<machine type>
r2=<pointer to ATAGS>
r1=忽略
r2=<pointer to DTB>
在ARM 平台中,所有设备树源代码都存储在arch/arm/boot/dts ,其中包含了两种源代码dts 和dtsi ,
※ .dts 板级定义。

※ .dtsi 为dts 文件所需要包含的头文件,通常该文件包含了SoC 级别定义。

这两种文件都遵循了相同的设备树源文件的语法规则,如下图。

/ { a-string-property = "A string";
a-string-list-property = "first string", "second string"; a-byte-data-property = [0x01 0x23 0x34 0x56];
child-node@0 {
second-child-property = <1>;
};
child-node@1 {
an-empty-property;
a-cell-property = <1 2 3 4>;
child-node@0 { }; };};
属性值node0的属性
字节字符
pHandle
设备树基本语法
我们可以在arch/arm/boot/dts/目录下找到digilent-zed.dts设备树源码,另外在OOB设计中也可以找到devicetree.dts设备树源码。

由于ZedBoard和我们的目标板都是采用相同的主控芯片XC7Z020-CLG484-1,我们选用OOB里面的设备树源码更接近我们的目标板硬件环境的设备树源码,由于wifi模块是我们自己添加的,在OOB设计中并没有定义,所以需要针对该模块对设备树源码中的以下
2、配置编译linux内核
下载linux内核源码:
Linux内核源码非常庞大,对于可以适用于Android的版本就有数种之多,
Android使用了linux作为它的内核,通常情况下开发板都提供了根据自身开发板硬件环境能够驱动起来的linux内核,Digilent公司为我们提供了基于XC7Z020-CLG484-1的zynq的linux内核源码,它在GitHub上的网址为:https:///Digilent/linux-digilent。

我们可以使用git clone的方式从该仓库
上述命令将会将该git仓库的代码克隆到kernel目录下。

下载完成后,需要
重要的目录以及它所包含的内容的描述。

在编译linux内核之前,我们先进行内核的配置,通过配置内核可以对内核进行适当的剪裁,使得系统在可以运行并且满足功能要求的前提下得到最小化,这在资源有限的嵌入式平台中是非常重要的。

因为我们需要在X86平台编译Linux的内核,所以我们需要用到前面提到的交叉编译环境。

同样,在编译内核时我们需要指定CPU架构以及交叉编译工具。

由于Linux内核比较庞大,有很多的选项我们都不需要使用到。

如果需要我们自己手动去一项一项的配置,那么将是一项非常冗杂的工作。

不过好在针对我们的硬件环境,Digilent公司为我们提供了专门的配置文件,在终端执行以下命
有安装好相关的依赖包导致的,在网上很容易找到解决的办法。

三、Linux设备驱动移植
Linux 设备驱动的移植需要对Linux 设备驱动的模型和Linux 设备驱动的初始化过程有一定了解。

我们首先通过对Linux 设备驱动的初始化过程的简单介绍来描述Linux 设备驱动模型,然后再详细阐述驱动具体移植步骤。

1、Linux设备驱动模型
在对处理器完成一些如MMU、Cache、控制寄存器和页表等基本的初始化以后,系统调用函数start_kernel()来进行内核子系统的初始化。

Start_kernel()比较复杂,在该函数的最后,调用了函数rest_init()。

在rest_init()中,系统通过kernel_thread()函数创建了一个名叫kernel_init()的内核线程,设备模型初始化相关的重要函数do_basic_setup()就出现在这个函数中。

其中,函数driver_init()用来建立设备模型子系统,而do_initcalls()函数完成包括设备在内的所有初始化函数的调用。

于前一部分完成。

下面分别对这10个函数的功能做简单的介绍。

1. Devtmpfs_init() 函数的主要功能是用来创建设备文件系统,类型为
dev_fs_type。

每个核心设备都会注册设备结点到这个文件系统下。

Devfs文件系统被挂载到/dev 目录。

2. Devices_init()函数中完成对设备子系统的初始化。

具体表现为将devices、dev、block、char 等kset、kobject 对象分别添加到sysfs 系统中。

函数执行完成后,就在/sys 目录下建立devices 和dev 的目录项,block、char 两种类型也
被添加到其中。

3. Buses_init()为总线子系统创建kset 对象,并将其添加到sysfs 中。

4. Class_init()为类子系统创建kset 对象,并将其添加到sysfs 中。

5. Fireware_init()为firmware 子系统创建kobject 对象,并将其添加到sysfs 中。

6. hypervisor_init()为hypervisor 子系统创建kobject 对象,并将其添加到sysfs中。

7. Platform_bus_init()函数是整个设备子系统初始化过程中非常重要的一个函数。

从功能上面来说,它完成了platform总线的初始化。

platform device driver 机制由两部分组成:platform_device 和platform_driver。

该机制下开发设备驱动的大致流程如图所示:
图设备驱动开发流程
8. System_bus_init()函数跟platform_bus_init()类似,区别在于此函数初始化的是系统总线。

9. Cpu_dev_init()函数的作用是在前面初始化完成的基础上,对cpu 系统设备进行注册。

10. Memery_dev_init()函数的作用同上,是向设备文件系统添加对存储设备的支持。

通常情况下只要和内核本身运行依赖性不大,拥有各自的地址总线和中断号等资源的外围设备,都可以用platform_driver实现。

如:LCD、TouchScreen、KeyBoard、网卡、USB、UART 等。

而如timer,irq 之类系统设备则一般不用platform_driver机制来实现。

2、Linux设备驱动移植
我们在Digilent网站下载而得到的Linux内核源代码里面,包含了Linux内
核本身以及以及BSP和目标板硬件环境的系统驱动。

它们都存放在drivers目录下,只不过我们需要在剪裁配置内核的过程中将其包含进去,以达到驱动各种外部设备的目的。

所以在我们下载得到的代码中,有许多现有的驱动已经集成在其中并且可以直接使用,我们将其列举在下表中。

(具体可以参考/Linux+Drivers)
下面我们以SD卡控制器的驱动为例进行大致的分析。

SDIO设备的驱动可以分为主从设备这两方面的驱动,对应于驱动SD卡,我们的目标板上的zynq相当于主设备,而SD卡就相当于从设备。

由于SD卡只是一个具有存储功能的外部设备,所以只需要有主设备驱动(上表中表明SD卡的驱动在host目录下),就可以驱动该模块正常工作。

在linux系统中,将每个host设备封装成platform_device来逐一进行注册。

在Linux内核完成了platform_device的注册之后,就应该来到对应的platform_driver的注册了,它的注册就是上表中列举的
测,首先会根据结构体中.driver的.name成员,亦即”sdhci-zynq”在platform_bus 总线上查找具有相同名字的platform_device,每一次找到具有相同名字”sdhci-zynq”的platform_device,就会调用一次sdhci_zynq_driver中的.probe即sdhci_zynq_probe函数,下面是该函数的主体:
static int __devinit sdhci_zynq_probe(struct platform_device *pdev)
{
int ret;
const void *prop = NULL;
struct device_node *np = pdev->dev.of_node;
struct sdhci_host *host = NULL;
ret = sdhci_pltfm_register(pdev, &sdhci_zynq_pdata);
if (ret == 0) {
prop = of_get_property(np, "xlnx,has-cd", NULL);
if (prop == NULL) {
host = platform_get_drvdata(pdev);
host->quirks |= SDHCI_QUIRK_BROKEN_CARD_DETECTION;
} else if (!(u32) be32_to_cpup(prop)) {
host = platform_get_drvdata(pdev);
host->quirks |= SDHCI_QUIRK_BROKEN_CARD_DETECTION;
}
} else
printk("sdhci platform registration failed\n");
return ret;
}
分析代码可以知道,该函数的主要功能是通过struct sdhci_host *host = NULL 构造出一个host,它是一个sdhci_host结构体的变量。

紧接着根据sdhci-pltfm.c 中声明的sdhci_pltfm_register来,并调用sdhci_add_host来注册host,相关代码可以见下。

然后根据传入的pdev->dev.of_node通过of_get_property来获取到相应节点的属性。

然后判断prop的属性,并根据此属性调用platform_get_drvdata 来获取之前通过platform_set_drvdata保存的平台总线设备数据,并赋值给host。

最主要的还是最后的对mmc进行注册,mmc具有许多参数,我们先来看看
对从设备的电源和时钟频率等进行设置;get_ro用来检测从设备是否开启了写保护……
再回到sdhci_add_host函数,在注册中断的函数requset_irq中,host->irq取决于资源文件内部的irq参数,sdhci_irq就是我们需要的中断处理程序,而第三个参数是中断标志位,可以为IRQF_DISABLED、IRQF_SHARED和IRQF_SAMPLE_RANDOM三者之一,在本SD卡的程序中选用IRQF_SHARED,表示多个中断处理程序共享irq中断线。

该函数内部mmc_add_host函数的功能是调用device_add把mmc设备注册到Linux设备,从而完成注册sdhci。

当前的值,该寄存器中有两个比特位用来表示SD卡的insert和remove的动作。

接着通过一个判断语句根据读取到的intmask来判断中断源对应的到底是insert 还是remove的操作,同时将这两个比特位清零,为下一次中断处理做好准备。

再次回到sdhci_add_host函数,在声明完成中断处理程序之后,程序将会调用sdhci_tasklet_card函数,该函数的作用很简单,就是通过判断是否有SD卡存在于SD卡插槽,如果有的话,就通过mmc_detect_change调用mmc_rescan函数,
这个函数的第二个参数告诉了系统应该用什么样的频率去对SD卡进行扫描检测,当成功使用某一频率扫描到SD卡的时候,将会执行break退出扫描循环。

CMD0命令,从而让SD设备进入空闲状态。

然后接着发送一个CMD8命令,得到该SD设备支持的电压,最后分别调用mmc_attach_sdio、mmc_attach_sd和mmc_attach_mmc一次检测该SD设备到底是sdio、sd还是mmc卡。

相关文档
最新文档