uboot源码分析(基于S5PV210)之uboot的硬件驱动部分

时间:2022-09-26 12:10:38

一、uboot与linux驱动

1、uboot本身是裸机程序

(1)裸机本来是没有驱动的概念的(狭义的驱动的概念就是操作系统中用来具体操控硬件的那部分代码叫驱动)

(2)裸机程序中是直接操控硬件的,操作系统中必须通过驱动来操控硬件。这两个有什么区别?本质区别就是软件设计中的分层。

2、uboot的虚拟地址对硬件操作的影响

(1)操作系统(这里指的是linux)下MMU肯定是开启的,也就是说linux驱动中肯定都使用的是虚拟地址。而纯裸机程序中根本不会开MMU,全部使用的是物理地址。这是裸机下和驱动中操控硬件的一个重要区别。

(2)uboot早期也是纯物理地址工作的,但是现在的uboot开启了MMU做了虚拟地址映射,这个东西驱动也必须考虑。在之前的文章中:查我的开发板的uboot中的虚拟地址映射表,发现除了0x30000000-0x3FFFFFFF映射到了0xC0000000-0xCFFFFFFF之外,其余的虚拟地址空间全是原样映射的。而我们驱动中主要是操控硬件寄存器,而S5PV210的SFR都在0xExxxxxx地址空间,因此驱动中不必考虑虚拟地址。

3、uboot借用(移植)了linux驱动

(1)linux驱动本身做了模块化设计。linux驱动本身和linux内核不是强耦合的,这是linux驱动可以被uboot借用(移植)的关键。

(2)uboot移植了linux驱动源代码。uboot是从源代码级别去移植linux驱动的,这就是linux系统的开源性。

(3)uboot中的硬件驱动比linux简单。linux驱动本身有更复杂的框架,需要实现更多的附带功能,而uboot本质上只是个裸机程序,uboot移植linux驱动时只是借用了linux驱动的一部分而已。

二、iNand介绍

1、iNand/eMMC/SDCard/MMCCard的关联

(1)最早出现的是MMC卡,卡片式结构,按照MMC协议设计。(相较于NandFlash芯片来说,MMC卡有2个优势:第一是卡片化,便于拆装;第二是统一了协议接口,兼容性好。)

(2)后来出现SD卡,兼容MMC协议。SD卡较MMC有一些改进,譬如写保护、速率、容量等。

(3)SD卡遵守SD协议,有多个版本。多个版本之间向前兼容。

(4)iNand/eMMC在SD卡的基础上发展起来,较SD卡的区别就是将SD卡芯片化了(解决卡的接触不良问题,便于设备迷你化)。

(5)iNand和eMMC的关联:eMMC是协议,iNand是Sandisk公司符合eMMC协议的一种芯片系列名称。

2、iNand/eMMC的结构框图及其与NandFlash的区别

(1)iNand内部也是由存储系统和接口电路构成(和Nand结构特性类似,不同之处在于接口电路功能不同)。

(2)iNand的接口电路挺复杂,功能很健全。譬如:

第一,提供eMMC接口协议,和SoC的eMMC接口控制器通信对接。

第二,提供块的ECC校验相关的逻辑,也就是说iNand本身自己完成存储系统的ECC功能,
SoC使用iNand时自己不用写代码来进行ECC相关操作,大大简化了SoC的编程难度。
(NandFlash分2种:SLC和MLC,SLC更稳定,但是容量小价格高;MLC容易出错,但是
容量大价格低)

第三,iNand芯片内部使用MLC Nand颗粒,所以性价比很高。

第四,iNand接口电路还提供了cache机制,所以inand的操作速度很快。

3、iNand/eMMC的物理接口和SD卡物理接口的对比

(1)S5PV210芯片本身支持4通道的SD/MMC,在X210中实际是在SD/MMC0通道接了iNand芯片,而SD/MMC2接了SD卡(SD/MMC3也接了SD卡)。

(2)对比inand和SD卡接线,发现:这两个接线几乎是一样的,唯一的区别就是SD卡IO线有4根,而iNand的IO线有8根。
uboot源码分析(基于S5PV210)之uboot的硬件驱动部分
uboot源码分析(基于S5PV210)之uboot的硬件驱动部分
(3)这个告诉我们,我们在实际操作iNand芯片时和操作SD卡时几乎是一样的(物理接线几乎一样,软件操作协议几乎一样)。

结论:iNand/eMMC其实就是芯片化的SD/MMC卡,软件操作和SD卡相同。分析iNand芯片的操作代码时,其实就是以前的SD卡的操作代码。一些细节的区别就是为了区分各种不同版本的SD卡、iNand的细节差异。

三、SD卡/iNand操作

SD卡初始化:
uboot源码分析(基于S5PV210)之uboot的硬件驱动部分
SD卡数据传输:
uboot源码分析(基于S5PV210)之uboot的硬件驱动部分

1、硬件接口:DATA、CLK、CMD

(1)iNand的IO线有8根,支持1、4、8线并行传输模式;SD卡IO线有4根,支持1、4线并行传输模式。

(2)CMD线用来传输命令、CLK线用来传输时钟信号。

(3)接口有CLK线,工作时主机SoC通过CLK线传输时钟信号给SD卡/iNand芯片,说明:SD/iNand是同步的,SD/iNand的工作速率是由主机给它的CLK频率决定的

2、命令响应的操作模式

  SD协议事先定义了很多标准命令(CMD0、CMD1·····),每个命令都有它的作用和使用条件和对应的响应。SD卡工作的时候就是一个一个的命令周期组合起来的,在一个命令周期中,主机先发送CMD给SD卡,然后SD卡解析这个命令并且执行这个命令,然后SD卡根据结果回发给主机SoC一个响应。(有些命令是不需要响应的,这时SD卡不会给主机回发响应,主机也不用等待响应)。标准的命令+响应的周期中,主机发完一个命令后应该等待SD卡的响应而不是接着发下一条命令。
uboot源码分析(基于S5PV210)之uboot的硬件驱动部分

3、SD/iNand的体系结构图

  SD卡内部有一个接口控制器,这个控制器类似于一个单片机,这个单片机的程序功能就是通过CMD线接收外部主机SoC发给SD卡的命令码,然后执行这个命令并且回发响应给主机SoC。这个单片机处理命令及回发响应遵循的就是SD协议。这个单片机同时可以控制SD卡内部的存储单元,可以读写存储单元。
uboot源码分析(基于S5PV210)之uboot的硬件驱动部分

4、SD/iNand的寄存器(重点是RCA寄存器)

(1)注意这里说的是SD卡内部的寄存器,而不是主机SoC的SD控制器的寄存器。(很多外置芯片内部都是有寄存器的,这些寄存器可以按照一定的规则访问,访问这些寄存器可以得知芯片的一些信息)。

(2)RCA(relative address,相对地址寄存器)。我们在访问SD卡时,实际上SD卡内部每个存储单元的地址没有绝对数字,都是使用相对地址。相对地址由SD卡自己决定的,存放在RCA寄存器中。

5、SoC的SD/MMC/iNand控制器简介

(1)不同的SoC可能在SD/MMC/iNand等支持方面有差异,但是如果支持都是通过内部提供SD控制器来支持的。

(2)S5PV210的SD卡控制器在Section8.7部分

相关文档资料:
「S5PV210_UM_REV1.1.pdf」https://www.aliyundrive.com/s/SC3gQoi37Mc
点击链接保存,或者复制本段内容,打开「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。

链接:https://pan.baidu.com/s/1nKWz7Ma6fr9OdmwSbdaN5g 
提取码:ln4s 
--来自百度网盘超级会员V6的分享

6、命令码CMD和ACMD

(1)SD卡工作在命令+响应的模式下。

(2)SD协议的命令分2种:CMDx和ACMDx。CMD是单命令命令,就是单独发一个CMD即可表示一个意思。ACMD是一种扩展,就是发2个CMD加起来表示一个意思。可以认为ACMDx = CMDy+CMDz(y一般是55)

7、卡类型识别SD or MMC?

(1)MMC协议、SD协议、eMMC协议本身是一脉相承的,所以造成了一定的兼容性,所以当我们SoC控制器工作时连接到SoC上的可能是一个MMC卡、也可能是SD卡、也可能是iNand芯片。主机SoC需要去识别这个卡到底是什么版本的卡

(2)SoC如何区分卡种类?
  因为不同版本的卡内部协议不同的,所以对卡识别命令的响应也是不同的。SoC通过发送一些命令、听取响应就可以根据不同的响应判定卡的版本。

8、卡状态

  SD卡内部的接口控制器类似于一个单片机,这个单片机其实是一个状态机。所以SD卡任何时候都属于某一种状态(空闲状态、准备好状态、读写状态、出错状态····都是事先定义好的),在这种状态下能够接受的命令是一定的,接受到命令之后执行一定的操作然后根据操作结果会跳转为其他状态。如果主机发过来的命令和当前状态不符状态机就不响应,如果收到命令和当前状态相符就会执行相应操作,执行完之后根据结果跳转为其他状态。

9、卡回复类型

(1)一般来说,SD卡的命令都属于:命令+响应的模式。也有极少数的SD卡命令是不需要回复的。

(2)卡回复有R1、R7、R1B等8种类型,每种卡回复类型都有自己的解析规则。然后卡在特定状态下响应特定命令时有可能回复哪种响应都是SD协议事先规定好的,详细细节要查阅协议文档。

四、uboot中的iNand/SD驱动解析

1、从start_armboot开始

(1)驱动整体比较庞大,涉及很多个文件夹下的很多文件,函数更多,贸然插入根本不知道看哪里。学习时必须有顺序。

2、mmc_initialize

(1)函数位于:uboot/drivers/mmc/mmc.c。

int mmc_initialize(bd_t *bis)
{
	struct mmc *mmc;
	int err;

	INIT_LIST_HEAD(&mmc_devices);//初始化双向链表,该链表记录了内核里维护的所有MMC
设备
	cur_dev_num = 0;      //代表第0个插槽,x210共有4个插槽,初始化时指向第0个设备

	if (board_mmc_init(bis) < 0)
		cpu_mmc_init(bis);//初始化MMC控制器,其实在irom内部也初始化过了,否则无法读取BL1

#if defined(DEBUG_S3C_HSMMC)
	print_mmc_devices(',');
#endif

#ifdef CONFIG_CHECK_X210CV3
	mmc = find_mmc_device(1);
#else
	mmc = find_mmc_device(0);
#endif
	if (mmc) {
		err = mmc_init(mmc);
		if (err)
			err = mmc_init(mmc);
		if (err) {
			printf("Card init fail!\n");
			return err;
		}
	}
	printf("%ldMB\n", (mmc->capacity/(1024*1024/(1<<9))));//capacity以扇区为单位,表示
	return 0;   //一个扇区(1<<9),512字节,为何先除以1024*1024在乘(1<<9),担心数字超过数据类型
}       //的范围
int cpu_mmc_init(bd_t *bis)
{
#ifdef CONFIG_S3C_HSMMC      //三星的2410、2440、6410、210的这个宏的名字都是相同的
	setup_hsmmc_clock();      //SD卡控制器的时钟初始化
	setup_hsmmc_cfg_gpio();   //SD卡控制器的GPIO初始化
	return smdk_s3c_hsmmc_init();//使用了部分驱动
#else
	return 0;
#endif
}

(2)从名字可以看出,这个函数的作用就是初始化开发板上MMC系统。MMC系统的初始化应该包含这么几部分:SoC里的MMC控制器初始化(MMC系统时钟的初始化、SFR初始化)、SoC里MMC相关的GPIO的初始化、SD卡/iNand芯片的初始化。

(3)mmc_devices链表全局变量,用来记录系统中所有已经注册的SD/iNand设备。所以向系统中插入一个SD卡/iNand设备,则系统驱动就会向mmc_devices链表中插入一个数据结构表示这个设备。

3、cpu_mmc_init

(1)函数位于:uboot/cpu/s5pc11x/cpu.c中。实质是通过调用3个函数来完成的。

(2)setup_hsmmc_clock,在uboot/cpu/s5pc11x/setup_hsmmc.c中。看名字函数是用来初始化SoC中MMC控制器中的时钟部分的。

(3)setup_hsmmc_cfg_gpio,在uboot/cpu/s5pc11x/setup_hsmmc.c中。看名字函数是用来配置SoC中MMC控制器相关的GPIO的。

4、smdk_s3c_hsmmc_init

(1)函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。

int smdk_s3c_hsmmc_init(void)
{
#ifdef OM_PIN
	if(OM_PIN == SDMMC_CHANNEL0) {
		int err;
		printk("SD/MMC channel0 is selected for booting device.\n");
		err = s3c_hsmmc_initialize(0);
		return err;
	} else if (OM_PIN == SDMMC_CHANNEL1) {
		int err;
		printk("SD/MMC channel1 is selected for booting device.\n");
		err = s3c_hsmmc_initialize(1);
		return err;
	} else
		printk("SD/MMC isn't selected for booting device.\n");
#endif

	int err;

#ifdef USE_MMC0
	err = s3c_hsmmc_initialize(0);
	if(err)
		return err;
#endif

#ifdef USE_MMC1
	err = s3c_hsmmc_initialize(1);
	if(err)
		return err;
#endif	

#ifdef USE_MMC2
	err = s3c_hsmmc_initialize(2);
	if(err)
		return err;
#endif	

#ifdef USE_MMC3
	err = s3c_hsmmc_initialize(3);
	if(err)
		return err;
#endif
	return -1;
}

(2)函数内部通过宏定义USE_MMCx来决定是否调用s3c_hsmmc_initialize来进行具体的初始化操作。

5、s3c_hsmmc_initialize

(1)函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。

static int s3c_hsmmc_initialize(int channel)
{
	struct mmc *mmc;

	mmc = &mmc_channel[channel];

	sprintf(mmc->name, "S3C_HSMMC%d", channel);
	mmc->priv = &mmc_host[channel];
	mmc->send_cmd = s3c_hsmmc_send_command;
	mmc->set_ios = s3c_hsmmc_set_ios;
	mmc->init = s3c_hsmmc_init;

	mmc->voltages = MMC_VDD_32_33 | MMC_VDD_33_34;
	mmc->host_caps = MMC_MODE_4BIT | MMC_MODE_HS_52MHz | MMC_MODE_HS;
#if defined(USE_MMC0_8BIT) || defined(USE_MMC2_8BIT)
	mmc->host_caps |= MMC_MODE_8BIT;
#endif

	mmc->f_min = 400000;
	mmc->f_max = 52000000;

	mmc_host[channel].clock = 0;

	switch(channel) {
	case 0:
		mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_0_BASE;
		break;
	case 1:
		mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_1_BASE;
		break;
	case 2:
		mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_2_BASE;
		break;
#ifdef USE_MMC3
	case 3:
		mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_3_BASE;
		break;
#endif
	default:
		printk("mmc err: not supported channel %d\n", channel);
	}
	
	return mmc_register(mmc);//注册设备驱动
}

(2)定义并且实例化一个struct mmc类型的对象(定义了一个指针,并且给指针指向有意义的内存,或者说给指针分配内存),然后填充它的各种成员,最后调用mmc_register函数来向驱动框架注册这个mmc设备驱动。

(3)mmc_register功能是进行mmc设备的注册,注册方法其实就是将当前这个struct mmc使用链表连接到mmc_devices这个全局变量中去。

(4)我的X210中定义了USE_MMC0和USE_MMC2,因此在我的uboot初始化时会调用2次s3c_hsmmc_initialize函数,传递参数分别是0和2,因此完成之后系统中会注册上2个mmc设备,表示当前系统中有2个mmc通道在工作。

(5)至此cpu_mmc_init函数分析完成。

6、find_mmc_device

(1)这个函数位于:uboot/drivers/mmc/mmc.c中。

struct mmc *find_mmc_device(int dev_num)
{
	struct mmc *m;
	struct list_head *entry;

	list_for_each(entry, &mmc_devices) {//遍历内核链表
		m = list_entry(entry, struct mmc, link);

		if (m->block_dev.dev == dev_num)//是否找到设备编号
			return m;
	}

	printf("MMC Device %d not found\n", dev_num);

	return NULL;
}

(2)这个函数其实就是通过mmc设备编号来在系统中查找对应的mmc设备(struct mmc的对象,根据上面分析系统中有2个,编号分别是0和2)。

(3)函数工作原理就是通过遍历mmc_devices链表,去依次寻找系统中注册的mmc设备,然后对比其设备编号和我们当前要查找的设备编号,如果相同则就找到了要找的设备。找到了后调用mmc_init函数来初始化它

7、mmc_init

(1)函数位于:drivers/mmc/mmc.c中。

int mmc_init(struct mmc *host)
{
	int err;

	err = host->init(host);

	if (err)
		return err;

	/* Reset the Card */
	err = mmc_go_idle(host);

	if (err)
		return err;

	/* Test for SD version  2 */
	err = mmc_send_if_cond(host);

	/* Now try to get the SD card's operating condition */
	err = mmc_send_app_op_cond(host);

	/* If the command timed out, we check for an MMC card */
	if (err == TIMEOUT) {
		err = mmc_send_op_cond(host);

		if (err)
			return UNUSABLE_ERR;
	} else
		if (err)
			return UNUSABLE_ERR;

	return mmc_startup(host);
}

(2)分析猜测这个函数应该要进行mmc卡的初始化了(前面已经进行了SoC端控制器的初始化)

(3)函数的调用关系为:

mmc_init
	mmc_go_idle
		mmc_send_cmd
	mmc_send_if_cond
		mmc_send_cmd
	······

  经过具体分析可以看出,mmc_init函数内部就是依次通过向mmc卡发送命令码(CMD0、CMD2那些)来初始化SD卡/iNand内部的控制器,以达到初始化SD卡的目的,具体需要参考存储芯片的数据手册以及210数据手册中与MMC相关的部分。

8、总结

(1)至此整个MMC系统初始化结束。

(2)整个MMC系统初始化分为2大部分:SoC这一端的MMC控制器的初始化,SD卡这一端卡本身的初始化。前一步主要是在cpu_mmc_init函数中完成,后一部分主要是在mmc_init函数中完成。

(3)整个初始化完成后去使用sd卡/iNand时,操作方法和mmc_init函数中初始化SD卡的操作一样的方式。读写sd卡时也是通过总线向SD卡发送命令、读取/写入数据来完成的。

(4)顺着操作追下去分析,到了mmc_send_cmd函数处就断了,真正的向SD卡发送命令的硬件操作的函数找不到。这就是学习驱动的麻烦之处。

(5)struct mmc结构体是关键。两部分初始化之间用mmc结构体来链接的,初始化完了后对mmc卡的常规读写操作也是通过mmc结构体来链接的。

五、驱动设计中软件架构思想

1、struct mmc

(1)驱动的设计中有一个关键数据结构,不同驱动是不一样的。

  譬如MMC驱动的结构体就是struct mmc这些结构体中包含一些变量和一些函数指针,变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法。这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体。

(2)一个驱动工作时主要就分几部分:驱动构建(构建一个struct mmc然后填充它)、驱动运行时(调用这些函数指针所指的函数和变量)

2、分离思想

(1)分离思想就是说在驱动中将操作方法和数据分开。

(2)操作方法就是函数,数据就是变量。所谓操作方法和数据分离的意思就是:在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。

3、分层思想

(1)分层思想是指一个整个的驱动分为好多个层次。简单理解就是驱动分为很多个源文件,放在很多个文件夹中。譬如本篇文章讲的mmc的驱动涉及到drivers/mmc下面的2个文件和cpu/s5pc11x下的好几个文件。

(2)以mmc驱动为例来分析各个文件的作用:

  uboot/drivers/mmc/mmc.c:本文件的主要内容是和MMC卡操作有关的方法,譬如MMC卡设置空闲状态的、卡读写数据等。但是本文件中并没有具体的硬件操作函数,操作最终指向的是struct mmc结构体中的函数指针,这些函数指针是在驱动构建的时候和真正硬件操作的函数挂接的(真正的硬件操作的函数在别的文件中)。
  uboot/drivers/mmc/s3c_hsmmc.c:本文件中是SoC内部MMC控制器的硬件操作的方法,譬如向SD卡发送命令的函数(s3c_hsmmc_send_command),譬如和SD卡读写数据的函数(s3c_hsmmc_set_ios),这些函数就是具体操作硬件的函数,也就是mmc.c中需要的那些硬件操作函数。这些函数在mmc驱动初始化构建时(s3c_hsmmc_initialize函数中)和struct mmc挂接起来备用。

  分析:mmc.c和s3c_hsmmc.c构成了一个分层,mmc.c中调用了s3c_hsmmc.中的函数,所以mmc.c在上层,s3c_hsmmc.c在下层。这两个分层后我们发现mmc.c中不涉及具体硬件的操作,s3c_hsmmc.c中不涉及驱动工程时的时序操作。因此移植的时候就有好处:譬如我们要把这一套mmc驱动移植到别的SoC上mmc.c就不用动,s3c_hsmmc.c动就可以了;譬如SoC没变但是SD卡升级了,这时候只需要更换mmc.c,不需要更换s3c_hsmmc.即可。

(3)cpu/s5pc11x/下面还有一个setup_hsmmc.c,也和MMC驱动有关。但是这些代码为什么不能放到drivers目录下去,而要放到cpu目录下去?

  因为这里面的2个函数(setup_hsmmc_clock和setup_hsmmc_cfg_gpio)都是和SoC有关的初始化函数,这两个函数不能放到drivers目录下去。实际上如果非把这两个函数放在uboot/drivers/mmc/s3c_hsmmc.c文件中也凑活能说过去。

注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来、引用了百度百科、部分他人博客的内容并结合自己实际开发经历,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。