cortex-a8 uboot系列:第十五章 uboot硬件驱动
0赞一、 Uboot和linux驱动
1. Uboot本身是裸机程序
裸机本身是没有驱动的概念的(狭义的驱动的概念就是操作系统中用来具体操控硬件的那部分代码叫驱动)
裸机程序中是直接操控硬件的(也就是操作寄存器),操作系统中必须通过驱动来操控硬件。这两个的本质区别就是分层。
2. Uboot的虚拟地址对硬件操作的影响
操作系统(linux)下MMU肯定是开启的,也就是说linux驱动中用的是虚拟地址。而裸机程序中一般不会使用MMU,全部使用的是物理地址。这是裸机下和驱动中操控硬件的一个重要区别。
Uboot早期也是纯物理地址工作,但是现在的uboot开启了MMU做了虚拟地址映射,这个东西驱动也必须考虑。查uboot中的虚拟地址映射表,发现除了0x30000000-0x3fffffff映射到了0xc0000000-0xcfffffff之外,其余的虚拟地址空间全是原样映射的。而驱动中主要是操作硬件的寄存器,而S5PV210的SFR都在0xexxx_xxxx地址空间,因此驱动中不必考虑虚拟地址。
3. Uboot借用(移植)了linux驱动
Linux驱动本身做了模块化设计。
Linux启动本身和linux内核不是强耦合的,这是linux驱动可以被uboot借用(移植)的关键。
Uboot移植了linux驱动源代码。
Uboot是从源代码级别去移植linux驱动的,这就是linux系统的开源性。
Uboot中的硬件驱动比linux简单。
Linux驱动本身有更复杂的框架,需要实现更多的附带功能。而uboot本质上只是个裸机程序,uboot移植linux驱动时只是借用了linux驱动的一部分而已。
MMC驱动框图解析
其中红色的框,表示函数是在mmc.c中。与具体soc无关。
绿色的框,表示函数是在s3c_hsmmc中,与具体的soc有关。
二、 iNand/SD驱动解析一
1. 从start_armboot开始
在start_armboot中,有调用mmc_initialize,对mmc进行注册以及初始化。
2. mmc_initialize
函数位于drivers/mmc/mmc.c中。对开发板上的MMC进行初始化,初始化包含:soc与MMC相关的GPIO的初始化,soc里面的MMC控制器初始化(MMC系统时钟的初始化、专用SFR初始化),外接SD卡/iNand芯片的初始化。
Uboot下的drivers目录下,全是驱动的代码。
这里关注的是mmc目录,里面是关于MMC的驱动。其中mmc.c是通用MMC驱动,和硬件无关,s3c_hsmmc.c和硬件相关的驱动。
这个函数定义并且实例化一个struct mmc类型的对象(定义了一个指针,并且给指针指向有意义的位置),然后填充对象的各个成员,最后调用mmc_register来注册mmc设备驱动。
函数开始定义了一个mmc结构体(定义drivers/mmc/mmc.c中)指针。Uboot将mmc相关的内容封装成了一个结构体。此结构体和硬件是无关的,因此是通用的MMC驱动。
对mmc的双向链表头结点初始化。
这是一个宏,将双向链表的后项节点指向自己,前项节点也指向自己。
mmc_devices是定义的全局双向链表list_head变量。这个双向链表是linux内核提供的。这个变量用来记录系统中所有已经注册的SD/iNand设备的。所以当系统中加入了一个SD/iNand设备,系统驱动就会向mmc_devices链表中插入一个数据结构来表示这个设备。
cur_dev_num是定义的当前操作的是哪一个设备。
board_mmc_init函数(drivers/mmc/mmc.c)直接返回-1。因此是执行cpu_mmc_init
1) cpu_mmc_init
所以执行cpu_mmc_init函数(cpu/s5pc11x/cpu.c中)。调用三个函数来完成。
a) 时钟设置
第一个setup_hsmmc_clock函数(cpu/s5pc11x/setup_hsmmc.c中),初始化soc内部的mmc控制器的时钟。
九鼎开发板使用的是通道0和通道2,因此对MMC0和MMC2的时钟进行设置。
选择MMC控制器的时钟源,使用SCLKMPLL,操作的CLK_SRC4寄存器的低四位。设置为0x6,表示选择时钟源为SCLKMPLL。
选择MMC控制器的时钟源的分频值,操作CLK_DIV4寄存器的低四位。
通过get_MPLL_CLK函数(cpu/s5pc11x/s5pc110/speed.c中)获取到MPLL的输出频率。
通过查看x210_sd.h中关于MPLL的三个参数的设置,和数据手册中推荐的值对比,可以知道MPLL的输出频率是667M。
所以clock变量的值为 = 667000000 / 1000000 = 667。
设定MMC控制器的时钟不能超过50M。所以
MMC0_RATIO = MOUTMMC0(667M) / SCLK_MMC0(最大50M) – 1。
使用for循环来计算应该设置的分频值。计算得出后,在写入到CLK_DIV4寄存器的低四位中,以设置MMC0的时钟。
通过上面这个函数setup_hsmmc_clock,就设置好了MMC0和MMC2控制器的时钟。
b) gpio设置
第二个函数:
setup_hsmmc_cfg_gpio(cpu/s5pc11x/setup_hsmmc.c中)
以通道0为例:
配置的是GPG0的管脚。读取GPG0CON寄存器,将低28位清零,然后或上0x0222_2222,在写入到GPG0CON寄存器中,将GPG0的管脚功能设置为SD卡功能。
往GPG0PUD寄存器写入0x0000_2aaa,开启上拉使能。说明使用MMC控制器时,对应的GPIO管脚要开启上拉功能。
往GPG0DRV写入0x0000_3ffff,也就是将驱动能力全部设置为4x。
如果使用的是8bit模式。还需要设置GPG1的管脚。
将GPG1CON的27-12位设置为0x3333,表示管脚为SD0的高4位数据线功能。
后面的设置就和GPG0一样了,开启上拉,设置驱动强度为4x。
同理,MMC2的GPIO也是同样设置。只是操作的寄存器不一样而已。
c) MMC结构体初始化
第三个函数:
smdk_s3c_hsmmc_init函数(drivers/mmc/s3c_hsmmc.c中,三星的专用MMC驱动)。对MMC的0通道和2通道进行初始化。
实质上是调用s3c_hsmmc_initialize函数(drivers/mmc/s3c_hsmmc.c中)来初始化。该函数接收一个参数,channel,指的是MMC的通道。
这里是对mmc结构体设置参数。Uboot对mmc的管理使用了一个结构体。因为mmc结构体和具体的硬件无关,所以是定义在mmc.c的通用mmc驱动文件中。
mmc_channel结构体数组是在(drivers/mmc/s3c_hsmmc.c中)定义,表示各个通道的SD/MMC。
MMC_MAX_CHANNEL是在x210_sd.h中定义的,为4,表示最多支持4个MMC通道。
定义一个mmc结构体指针mmc,将该指针指向对应的MMC通道,然后对mmc的各个参数赋值,其实就是对mmc通道进行赋值了。
定义了一些参数和函数指针。这样,以后要操作哪一个通道的SD/MMC卡,就调用对应的通道的mmc变量里面的函数指针即可。
mmc_host(drivers/mmc/s3c_hsmmc.c中)为定义的mmc控制器,这个就和具体的硬件有关系了,所以是定义在三星的专用MMC驱动文件中。
往MMC控制器的ioaddr写入自己通道的寄存器基地址。这样,控制器以后可以通过该基地址,就可以操作自己控制器的各个寄存器了。
最后调用mmc_register函数(drivers/mmc/mmc.c),通用驱动。对设备进行注册。
该函数功能是进行mmc设备的注册,注册方法其实就是将当前这个struct mmc使用链表连接到mmc_devices这个全局变量中去。
该函数其实就是设置mmc通道变量的一些参数,然后将设备添加在MMC链表中,以便管理。
block_dev是block_dev_desc_t结构体(include/path.h中)的例化。
INIT_LIST_HEAD函数,将链节点初始化,后项指向自己,前项也指向自己。
list_add_tail函数,将mmc->link连接到mmc_devices的尾部。
3. find_mmc_device
查找mmc设备。使用函数find_mmc_device(drivers/mmc/mmc.c),在系统中去查找已注册的mmc设备对象。找到返回对象的指针。
函数工作原理就是通过遍历mmc_devices链表,去依次寻找系统中已经注册的设备mmc设备,然后对比设备编号和当前要查找的设备编号是否一致,如果相同则就找到了要找的设备。
在九鼎开发板中,注册了两个mmc设备,一个编号0,一个编号2。
4. MMC控制器初始化
找到设备的对象指针后。调用mmc_init函数(drivers/mmc/mmc.c中)
函数输入参数是mmc设备的指针。
先调用mmc设备的初始化函数指针。在之前初始化的时候,将init指向了s3c_hsmmc_init函数(drivers/mmc/s3c_hsmmc.c)。
因此实质上是去执行s3c_hsmmc_init函数。
通过自己的priv元素,得到自己的MMC控制器的指针。
调用sdhci_reset函数(drivers/mmc/s3c_hsmmc.c),对MMC控制器进行复位。
以通道0为例,往偏移地址0x2f写入0x01。MMC0控制器的基地址是0xeb00_0000。所以就是往地址0xeb00_002f写入0x01。
对于该寄存器,最低位为1,表示复位所有(包括命令线和数据线)。
需要时间完成复位操作的,所以这里在等待复位操作完成。
设定超时时间为100ms,然后循环读取0xeb00_002f寄存器,判断最低位是否为0,为0的话,说明复位完成。如果100ms到,都不为0,说明复位失败。
读取MMC控制器的版本信息。
调用sdhci_init函数(drivers/mmc/s3c_hsmmc.c),对MMC控制器的SFR设置。
首先对MMC控制器再进行了一次复位。
往寄存器0xeb00_0034,0xeb00_0038(手册1100p)里面写入intmask的值。
设置普通状态中断和错误状态中断的使能。Intmask的值由以下的宏定义决定。
使用sdhci_change_clock函数(drivers/mmc/s3c_hsmmc.c),改变MMC控制器的输出时钟。
开始往寄存器0xeb00_0080的5-4位写入10。选择MMC的时钟源为SYSCON。
往CLKCON0(手册1088p)往写入0。将MMC控制器的时钟关掉。
根据待改变的时钟,计算分频值,然后左移8位写入到clk中,同时将clk最低位置1.然后将clk值写入到CLKCON0寄存器。设置MMK控制器时钟分频,并开启时钟。
循环读取CLKCON0寄存器的第二位,为1,表示时钟稳定,0就是不稳定。循环的同时也设定了超时时间。
将CLKCON0的第二位设置为1。开启外部SD时钟。
这样,就完成了MMC控制器的内部SFR初始化。自此,soc内部的MMC控制器的初始化,GPIO初始化完毕。剩下,就是要初始化外部的SD/MMC卡。
5. 初始化外部SD/MMC卡
对外部的SD/MMC卡进行初始化,这样,在之后,才可以对外部的SD/MMC卡进行操作使用。
初始化外部SD/MMC卡,其实就是给外部卡发送各种命令,来进行初始化。
通过发送不同的命令,让卡进入到不同的状态。
对于mmc_go_idle函数(drivers/mmc/mmc.c中),发送CMD0,没有参数,没有响应,标志位0。
通过mmc_send_cmd函数(drivers/mmc/mmc.c中)将命令发送出去,
mmc_send_cmd函数,实质上是调用初始化时的send_cmd函数指针,这个函数指针是指向s3c_hsmmc_send_command函数(drivers/mmc/s3c_hsmmc.c中)
首先往NORINTSTS ,ERRINTSTS 两个寄存器写入自身寄存器的值,清除错误中断状态和普通中断状态。
读取CONTROL4寄存器的值,判断当前控制器是否状态忙。设定10ms的时间判断。
读取Present State Register 的最低两位,判断命令是否能发送命令。
如果发送的命令有数据的话,提前要把数据给准备好。uboot中使用传输数据的方式是用DMA方式。所以要先配置下DMA。
往SDMA System Address Register,写入内存的地址。因为uboot中用的是虚拟地址,所以使用virt_to_phys将虚拟地址转换为物理地址。
说明MMC控制器中设置的DMA的内存地址是物理地址。
然后设置Present State Register的[4:3]位,开启SDMA选择。
设置Host DMA Buffer Boundary and Transfer Block Size Register,这个寄存器的BUF BOUND指示DMA传输的最大数据量。当DMA传输的数据量达到最大数据量后,就会停止DMA传输,并产生中断信号,这里设置为最大512Kb。BLKSIZE 是指块的大小,对于SD卡来说,这个值为512字节。
往Blocks Count for Current Transfer 中写入要传输数据的块数。
往Command Argument Register 寄存器中写入命令的参数。
如果有数据的话,设定传输模式。
调用sdhci_set_transfer_mode函数(drivers/mmc/s3c_hsmmc.c中)
该Transfer Mode Register Setting 寄存器,设定传输的模式。根据不同的选择,设定该寄存器不同的值。
根据不同的命令类型,决定不同的flags的值,这个flag的值,最后要写入到命令寄存器中去。
往命令寄存器里面写入值。
读取Normal Interrupt Status Register 寄存器最低位,判断传输是否完成。如果超时都没有完成,就打印信息,并返回超时。
读取响应信息到mmc设备的响应元素中。响应信息保存到4个寄存器中。
如果有数据线传输数据,判断数据传输是否成功。
通过发送不同的命令后,就将外部的SD/MMC卡初始化了。
6. 总结
分析可知:
mmc_init函数内部依次通过MMC控制器向外部MMC发送命令,来初始化外部的SD/MMC卡。
mmc_init:
host->init(): 对MMC控制器的SFR寄存器进行设置
mmc_go_idle(): 通过MMC控制器给外部MMC卡发送复位命令
mmc_send_cmd(): 发送命令函数
mmc_send_if_cond(): 通过MMC控制器给外部MMC卡发送命令,获取卡版本
mmc_send_cmd(): 发送命令函数
mmc_send_app_op_cond(): 通过MMC控制器给外部MMC卡发送命令,获取卡的
工作条件
mmc_send_cmd(): 发送命令函数
mmc_startup():通过MMC控制器给外部MMC卡发送命令,使外部卡进入工作
状态
mmc_send_cmd(): 发送命令函数
通过以上操作,就将SD/MMC控制器给初始化了,GPIO也初始化了,外部的SD/MMC卡也初始化了。
三、 SD/MMC驱动解析二
1. Struct mmc
驱动的设计中有一个关键数据结构。如MMC驱动的结构体就是struct。
mmc结构体中包含一些成员属性和一些函数指针,变量用来记录驱动相关的属性,函数指针用来记录驱动相关的操作方法。这些变量和函数指针加起来就构成了驱动。
驱动被抽象成一个结构体。
一个驱动工作时,就要就分以下几个部分:驱动初始化(构建一个struct mmc结构体然后填充里面的变量和函数指针)、驱动运行(调用这些函数指针指向的函数和变量)
2. 分离思想
在驱动中将操作方法和数据分开。
操作方法就是函数,数据就是变量。所谓操作方法和数据分离的意思是:在不同的地方来存储和管理驱动的操作方法和变量,这样的优势驱动便于移植。
3. 分层思想
分层思想是指一个整个驱动分为好多个层次。简单理解就是驱动分为很多个源文件,放在很多个文件夹中。如mmc的驱动设计到drivers/mmc下面文件和cpu/s5pc11x下面的文件。
以MMC驱动为例分析各个文件的作用。
drivers/mmc/mmc.c: 跟MMC卡操作有关的函数,但是操作和硬件无关。硬件操作最终指向的是struct mmc结构体中的函数指针,这些函数指针是在驱动初始化的时候和真正硬件操作的函数挂接的(真正的硬件操作的函数在别的文件中)
drivers/mmc/s3c_hsmmc.c: soc内部MMC控制器的硬件操作方法。如向SD卡发送命令函数(s3c_hsmmc_send_conmand),读写SD卡的函数(s3c_hsmmc_set_ios)。这些函数就是具体操作硬件的函数,也就是mmc.c中需要的那些硬件操作函数。这些函数在驱动初始化的时候挂接。
priv指向MMC控制器的结构体。
send_cmd函数指针指向 s3c_hsmmc_send_conmand函数
set_ios函数指针指向 s3c_hsmmc_set_ios 函数
init函数指针指向 s3c_hsmmc_init 函数
所以,mmc.c和s3c_hsmmc.c构成了一个分层,mmc.c中调用了s3c_hsmmc.c中的函数,所以mmc.c在上层,s3c_hsmmc.c在下层。这两个分层后mmc.c中不涉及具体硬件的操作,s3c_hsmmc.c中不涉及驱动工程中的时序操作。
因此移植的时候,把mmc驱动移植到别的soc上的mmc.c就不用修改,s3c_hsmmc.c修改就可以了。如果soc没变,但是SD卡升级了,这时候只需要修改mmc.c,不需要修改s3c_hsmmc.c了。
至于cpu/s5pc110/cpu.c,也和MMC去驱动有关(时钟和GPIO初始化)。这里的初始化不放到drivers目录下,而要放在cpu目录下,因为这里面的调用的2个函数(setup_hsmmc_clock和setup_hsmmc_cfg_gpio)都是和soc有关的初始化函数,因此这两个函数不能放到drivers目录下。