本人下载了CSDN博主hyyoxhk移植的适用于迅为itop4412精英版SCP 1GB的uboot,他这个uboot能成功从SD卡启动,然后读取保存在SD卡里面的linux内核并运行,但是却无法从emmc启动。
于是,笔者开始探究他移植的这个uboot为什么不能从emmc启动。

不会使用此uboot的读者请先阅读:迅为iTOP-4412精英版exynos4412开发板搭建原生Linux最小系统

打开uboot debug调试

首先第一步就是打开DEBUG调试功能,看看uboot输出的调试信息,以便分析程序的运行流程。

(1)修改include/configs/itop4412.h文件,在末尾添加#define DEBUG,然后用make命令编译uboot,但是编译不通过,报错:
  LD      spl/u-boot-spl
ld.bfd: arch/arm/mach-exynos/built-in.o: in function `exynos5420_spi_config':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/arch/arm/mach-exynos/pinmux.c:444: undefined reference to `printf'
ld.bfd: arch/arm/mach-exynos/built-in.o: in function `exynos4_pinmux_config':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/arch/arm/mach-exynos/pinmux.c:822: undefined reference to `printf'
ld.bfd: arch/arm/mach-exynos/built-in.o: in function `exynos4x12_pinmux_config':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/arch/arm/mach-exynos/pinmux.c:854: undefined reference to `printf'
ld.bfd: arch/arm/mach-exynos/built-in.o: in function `exynos_pinmux_config':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/arch/arm/mach-exynos/pinmux.c:878: undefined reference to `printf'
ld.bfd: drivers/built-in.o: in function `s5p_gpio_get_bank':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/drivers/gpio/s5p_gpio.c:58: undefined reference to `printf'
ld.bfd: drivers/built-in.o:/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/drivers/gpio/s5p_gpio.c:63: more undefined references to `printf' follow
make[1]: *** [scripts/Makefile.spl:361: spl/u-boot-spl] Error 1
make: *** [Makefile:1411: spl/u-boot-spl] Error 2
提示找不到printf函数。printf函数位于lib/vsprintf.c中,所以我们必须想办法把lib/文件夹添加到编译列表中。
在scripts/Makefile.spl文件中有这样一句话:libs-$(CONFIG_SPL_LIBGENERIC_SUPPORT) += lib/
这说明,决定/lib是否参与SPL编译的开关是CONFIG_SPL_LIBGENERIC_SUPPORT。
[root@exynos4412 learn]# grep CONFIG_SPL_LIBGENERIC_SUPPORT .config
# CONFIG_SPL_LIBGENERIC_SUPPORT is not set
果然这个开关没有打开。
这个开关位于common/spl/Kconfig文件中:
config SPL_LIBGENERIC_SUPPORT
        bool "Support generic libraries"
        help
          Enable support for generic U-Boot libraries within SPL. These
          libraries include generic code to deal with device tree, hashing,
          printf(), compression and the like. This option is enabled on many
          boards. Enable this option to build the code in lib/ as part of an
          SPL build.
没错,就是它!
菜单路径是:SPL / TPL ---> Support generic libraries
make menuconfig,选中上述菜单项。

笔者是直接在板子上本地编译uboot,用的gcc版本是11.2,bc版本是1.06(https://ftp.gnu.org/gnu/bc/bc-1.06.tar.gz)。
上面两张图是在板子的串口里面执行make menuconfig的效果。

(2)用make命令再次编译uboot,提示还缺少puts函数:
  LD      spl/u-boot-spl
ld.bfd: lib/built-in.o: in function `printf':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/lib/vsprintf.c:772: undefined reference to `puts'
make[1]: *** [scripts/Makefile.spl:361: spl/u-boot-spl] Error 1
make: *** [Makefile:1411: spl/u-boot-spl] Error 2
puts函数位于common/console.c里面,对应的开关是CONFIG_SPL_LIBCOMMON_SUPPORT。
于是还需要选中这个菜单项:SPL / TPL ---> Support common libraries

(3)再次编译uboot,报错:
  LD      spl/u-boot-spl
ld.bfd: drivers/built-in.o: in function `get_current':
/root/uboot/u-boot-2017.11-itop4412-2.0.0/learn/drivers/serial/serial.c:379: undefined reference to `default_serial_console'
make[1]: *** [scripts/Makefile.spl:361: spl/u-boot-spl] Error 1
make: *** [Makefile:1411: spl/u-boot-spl] Error 2
经检查发现,uboot主体部分和uboot spl部分生成的o文件不一样,spl部分把不该编译的serial.o编译出来了:
[root@exynos4412 learn]# ls drivers/serial/*.o
drivers/serial/built-in.o       drivers/serial/serial_s5p.o
drivers/serial/serial-uclass.o
[root@exynos4412 learn]# ls spl/drivers/serial/*.o
spl/drivers/serial/built-in.o    spl/drivers/serial/serial_s5p.o
spl/drivers/serial/serial.o
问题定位到drivers/serial/Makefile里面,编译uboot主体部分时CONFIG_DM_SERIAL=y,但编译spl部分时CONFIG_DM_SERIAL却未定义,导致编译错了文件。
经检查,.config里面CONFIG_DM_SERIAL=y,但scripts/Makefile.uncmd_spl里面CONFIG_DM_SERIAL为空,而编译spl时用的Makefile文件scripts/Makefile.build用include指令包含的正是scripts/Makefile.uncmd_spl。也就是说,scripts/Makefile.uncmd_spl把.config里面的CONFIG_DM_SERIAL=y值覆盖掉了。
所以解决办法找到了:
修改scripts/Makefile.uncmd_spl文件,将CONFIG_DM_SERIAL=用#号注释掉。
然后修改include/config_uncmd_spl.h文件,将里面的#undef CONFIG_DM_SERIAL注释掉(注释必须用/**/,不能用//)。
这个不怪uboot移植者,笔者看了下uboot官网的原版u-boot-2017.11.tar.bz2,这两句话确实本来就在里面,哎。也就是说,spl本身就禁用了串口,编译得过才怪。
不过笔者也能理解uboot作者这样做的目的,因为很多芯片对spl程序的大小是有要求的,要是不在spl里面禁用串口、gpio、i2c、spi这些库的话,编译出来的spl很大怎么办,那不是就完蛋了。

(4)再次编译uboot,就成功了。
注意一下生成的spl/u-boot-spl.bin大小,exynos4412芯片的BL2_SPL程序(uboot spl)的大小不允许超过14KB-4字节(14332字节)。
exynos4412的BL1程序(三星不开放源码的那段启动程序)只能把BL2_SPL程序(uboot的spl部分)复制到iRAM里面执行,iRAM里面是装不下完整的uboot程序的(BL2)。BL2_SPL的作用就是把完整版uboot(BL2)复制到DDR3内存里面,然后跳过去执行,这不属于BL1程序的任务。

(5)修改arch/arm/mach-exynos/spl_boot.c文件,在board_init_f函数里面加两条printf打印,试试看能不能打印出来:

修改完成后重新编译uboot,然后用dd命令烧写到SD卡,重启板子:

果然可以打印出来,虽然第一行第一个字母丢失了。。。。。第二行第一个字母也错了。
不过最后uboot还是能够正常启动linux内核。
这下我们知道CONFIG_SYS_TEXT_BASE的值是0x43e00000了。这说明,BL2_SPL程序(uboot spl)是将uboot的主体部分(BL2)从SD卡复制到DDR3内存的0x43e00000地址上的。

警告:exynos4412 SPL程序(BL2_SPL)的大小绝对不允许超过14KB-4字节,否则SPL程序运行会出错。
最好在board/samsung/itop4412/tools/mkitop4412spl.c里面加一条判断,一检测到len变量的值大于14332,就printf报错信息,然后exit(EXIT_FAILURE),这样比较保险。
或者简单一点,在烧写uboot的脚本里面判断一下:

size=$(ls -l ../u-boot-2017.11/spl/u-boot-spl.bin | awk "{print \$5}")
if [ $size -gt 14332 ]
then
        echo "spl size=$size too big!!!"
        exit 1
fi

(6)把同样的uboot bin文件烧写到emmc里面,然后板子重启,从emmc启动试试看:
请注意烧写位置是emmc boot0分区(/dev/mmcblk1boot0)的0号扇区。

echo "writting ..."

device=/dev/mmcblk1boot0
if [ -b $device ] ; then
        echo 0 > /sys/block/mmcblk1boot0/force_ro
        /usr/local/bin/dd iflag=dsync oflag=dsync if=u-boot-iTOP-4412.bin of=$device
        echo "writting success"
else
        echo "writting failure"
fi

其中/usr/local/bin/dd是gnu官网coreutils里面的dd命令,不是busybox自带的那个dd命令。
如果想用busybox自带的那个dd命令,则应该写成:dd if=u-boot-iTOP-4412.bin of=$device

打印出了几行字就死机了,很显然,是copy_uboot_to_ram()没有正确将保存在emmc中的uboot完整版程序复制到DDR3内存的0x43e00000地址处。
现在有了printf打印,调试SPL程序非常方便了啊。可以把#define DEBUG删了,在SPL程序里加入自己的调试信息了。

解决uboot spl程序无法将uboot完整版程序从emmc加载到内存的问题

笔者发现,拨码开关选择emmc启动时,arch/arm/mach-exynos/spl_boot.c的void copy_uboot_to_ram(void)函数里面,CONFIG_SUPPORT_EMMC_BOOT为真,bootmode = get_boot_mode()取得的值为40(BOOT_MODE_EMMC_SD)。switch (bootmode)走的是default: break那条语句,什么都没做就退出函数了,当然(*uboot)()一跳转就死机了。
所以,我们还需加一个case BOOT_MODE_EMMC_SD分支。直接加到case BOOT_MODE_EMMC下面就行了,这两种取值走的是同一条路径:

#ifdef CONFIG_SUPPORT_EMMC_BOOT
	case BOOT_MODE_EMMC:
	case BOOT_MODE_EMMC_SD:
		/* Set the FSYS1 clock divisor value for EMMC boot */
#ifndef CONFIG_ITOP4412
		/* just for exynos5 can be call */
		emmc_boot_clk_div_set();
#endif
		copy_bl2_from_emmc = get_irom_func(EMMC44_INDEX);
		end_bootop_from_emmc = get_irom_func(EMMC44_END_INDEX);

		copy_bl2_from_emmc(BL2_SIZE_BLOC_COUNT, CONFIG_SYS_TEXT_BASE);
		end_bootop_from_emmc();
		break;
#endif

然后,copy_bl2_from_emmc函数的地址为0x007f58,这个函数来自于exynos4412的iROM,也就是BL0。这个函数专门负责将emmc中的数据拷贝到内存。
网上可以下载到Android_Exynos4412_iROM_Secure_Booting_Guide_Ver.1.00.01.pdf这个手册,里面讲了BL0和BL1的实现细节。虽然三星提供的BL0和BL1是不开源的,但是里面究竟执行了什么东西,可以在这本手册里面查到。

最左边的Address那一栏是什么意思呢,是这个函数的地址吗?错,是函数的地址的地址!要不然,两个函数怎么可能挨那么近?难道说MSH_ReadFromFIFO_eMMC函数的大小才4个字节吗?
因为*(unsigned int *)0x2020044==0x007f58,*(unsigned int *)0x2020048==0x007f68,所以这两个函数的地址分别是0x007f58和0x007f68。
程序里面,copy_bl2_from_emmc=0x007f58,end_bootop_from_emmc=0x007f68。
再来看看copy_bl2_from_emmc的两个参数。第一个参数SrcBlock的意思是要读取的数据块的个数(也就是扇区个数),每个数据块(扇区)的大小为512字节。第二个参数DstByte是读出来的数据要存放到内存的哪个位置上。
函数end_bootop_from_emmc的意思结束读取emmc,这个没什么好说的。

我们知道,我们烧写进去的uboot镜像u-boot-iTOP-4412.bin的结构是:BL1(E4412_N.bl1.bin)+ BL2_SPL(itop4412-spl.bin)+ 环境变量存储区(env.bin)+ BL2(u-boot.bin)。
BL1的大小是8KB(占16个扇区),BL2_SPL的大小是16KB(14KB-4B的程序,4B的校验和,2KB的填充0,占32个扇区),env.bin的大小是8KB(占16个扇区)。
启动spl程序时,芯片刚读完BL2_SPL(第16~47扇区),所以emmc读指针位于env.bin的位置上,也就是emmc boot0区的第48扇区。
那么这样一来,原有的程序就有问题,copy_bl2_from_emmc(BL2_SIZE_BLOC_COUNT, CONFIG_SYS_TEXT_BASE)这句话相当于copy_bl2_from_emmc(1024, 0x43e00000),是从env.bin开始读1024个扇区,然后写入0x43e00000内存。env.bin里面保存的是环境变量的数据,并不是可执行程序,所以(*uboot)()跳过去还是会出错。

因此我们必须再加一条语句,跳过env.bin:

copy_bl2_from_emmc(CONFIG_ENV_SIZE / 512, CONFIG_SYS_TEXT_BASE);
copy_bl2_from_emmc(BL2_SIZE_BLOC_COUNT, CONFIG_SYS_TEXT_BASE);
end_bootop_from_emmc();

先读CONFIG_ENV_SIZE/512=32个扇区的env.bin数据,写入0x43e00000内存。
然后再读BL2_SIZE_BLOC_COUNT=1024个扇区的u-boot.bin数据,仍是写入0x43e00000内存。
这样一来,0x43e00000地址处就是正确的BL2程序内容了。

这样改完之后呢,编译uboot并烧写到emmc的boot0分区的0号扇区处,uboot就能从emmc启动了,也能进入命令行。
然而还有一个问题,在uboot中输入mmc list,只能检测到SD卡,无法检测到emmc。这导致uboot启动内核时,只能从SD卡读取linux内核和linux设备树。

如果卡槽里面没插SD卡 ,就是下面的情景:

将emmc添加到uboot设备树

我们只要将emmc添加到uboot的设备树里面,uboot就能同时检测到sd卡和emmc了。
(1)打开arch/arm/dts/exynos4412-itop4412.dts文件,修改sdhci@12510000:

sdhci@12510000 {
	samsung,bus-width = <8>;
	cd-gpios = <&gpk0 2 0>;
	status = "okay";
};
sdhci@12530000 {
	samsung,bus-width = <4>;
	status = "okay";
};

exynos4412一共有五个SDMMC。其中SDMMC0接的是eMMC,CD引脚为GPK02,采用8位数据线。SDMMC2接的是SD卡,CD引脚为GPX07(没写在uboot设备树里面),采用4位数据线。
核心板原理图上Xmmc2CDn(GPK22)接的是GPS33_EN,跟SD卡没有关系。
底板原理图上,SD卡槽的卡检测引脚(第9脚)接的是核心板的GPX0_7,是一个普通I/O口。低电平是插了卡,高电平(1.8V)是没插卡。
SD内存卡本身只有8个引脚,多出来的第9脚是卡槽上的弹簧,实现了卡检测功能。
SD卡的供电和SD卡的I/O引脚(卡检测引脚除外),高电平都是2.8V。

(2)打开include/configs/itop4412.h文件,我们把linux启动参数改一下:

#define CONFIG_EXTRA_ENV_SETTINGS \
	"dtbaddr=0x41000000\0" /* DDR3内存中存放设备树的位置 */ \
	"loadaddr=0x40007000\0" /* DDR3内存中存放内核的位置 */ \
	"bootargs=console=ttySAC2,115200n8 earlyprintk root=/dev/mmcblk1p2 rw rootfstype=ext4 init=/linuxrc\0" /* 内核启动参数 */
#define CONFIG_BOOTCOMMAND \
	"mmc dev 0;" /* 切换到eMMC */ \
	"mmc read ${dtbaddr} 0x800 0xa0;" /* [读设备树] 将SD卡的第2048~2207扇区(共计80KB)读到DDR3内存的dtbaddr地址处 */ \
	                                  /* 请注意mmc read的最后两个参数必须是十六进制格式 */ \
	"mmc read ${loadaddr} 0x1000 0x4000;" /* [读Linux内核] 将SD卡的第4096~20479扇区(共计8MB)读到DDR3内存的loadaddr地址处 */ \
	"bootm ${loadaddr} - ${dtbaddr}" /* [启动内核] 使用带3个参数的bootm命令, 第一个参数是内核地址loadaddr, 第二个参数是initrd地址(填"-"代表没有initrd), 第三个参数是设备树地址dtbaddr */

bootargs里面,将根文件系统设为emmc的第二分区(/dev/mmcblk1p2),文件系统类型为ext4。

请注意:在uboot里面,emmc是mmc dev 0,sd卡是mmc dev 1;但在linux内核里面,sd卡是/dev/mmcblk0,emmc是/dev/mmcblk1。也就是说设备号刚好相反,切记!
无论SD卡卡槽有没有插卡,这个顺序都不变!经测试,如果没有插SD卡,linux内核里面执行ls /dev/mmcblk*命令,列出的全是emmc的设备文件mmcblk1:

[root@exynos4412 /]# ls /dev/mmcblk*
/dev/mmcblk1       /dev/mmcblk1boot1  /dev/mmcblk1p2     /dev/mmcblk1rpmb
/dev/mmcblk1boot0  /dev/mmcblk1p1     /dev/mmcblk1p3

(3)修改读取和保存环境变量的设备号:

#define CONFIG_SYS_MMC_ENV_DEV		0 /* 保存环境变量的设备号 */

我们希望uboot从emmc启动,那么环境变量也应该保存到emmc里面。emmc在uboot中的设备号是0,所以CONFIG_SYS_MMC_ENV_DEV应该定义为0。

修改完成后,编译并烧写uboot,启动时的打印输出如下:

U-Boot 2017.11 (Nov 01 2021 - 22:23:54 +0800) for itop-4412

CPU:   Exynos4412 @ 1 GHz
Model: itop-4412 based on Exynos4412
Board: itop-4412 based on Exynos4412
DRAM:  1 GiB
WARNING: Caches not enabled
MMC:   SAMSUNG SDHCI: 0, SAMSUNG SDHCI: 1
sdhci_transfer_data: Error detected in status(0x208002)!
sdhci_transfer_data: Error detected in status(0x208002)!
sdhci_transfer_data: Error detected in status(0x208000)!
*** Warning - read failed, using default environment

Hit any key to stop autoboot:  0
sdhci_transfer_data: Error detected in status(0x208002)!
sdhci_transfer_data: Error detected in status(0x208002)!
sdhci_transfer_data: Error detected in status(0x208000)!
switch to partitions #0, OK
mmc0(part 0) is current device

MMC read: dev # 0, block # 2048, count 160 ... 0 blocks read: ERROR

MMC read: dev # 0, block # 4096, count 16384 ... 0 blocks read: ERROR
Wrong Image Format for bootm command
ERROR: can't get kernel image!
u-boot #

启动失败。uboot无法从emmc读取环境变量,也无法读取emmc数据块。提示sdhci_transfer_data: Error detected in status(0x208002)!的错误。

不过,uboot已经能正确识别emmc和sd卡:

u-boot # mmc list
SAMSUNG SDHCI: 0 (eMMC)
SAMSUNG SDHCI: 1
u-boot # mmc dev 1
switch to partitions #0, OK
mmc1 is current device
u-boot # mmc list
SAMSUNG SDHCI: 0 (eMMC)
SAMSUNG SDHCI: 1 (SD)

而且也能列出sd卡里面的文件:
(ls mmc 1:2的意思是列出mmc1设备(也就是sd卡)第2个分区的文件)

u-boot # ls mmc 1:2
<DIR>       4096 .
<DIR>       4096 ..
<DIR>      16384 lost+found
<DIR>       4096 bin
<DIR>       4096 dev
<DIR>       4096 etc
<DIR>       4096 home
<DIR>       4096 lib
<SYM>         11 linuxrc
<DIR>       4096 mnt
<DIR>       4096 opt
<DIR>       4096 proc
<DIR>       4096 root
<DIR>       4096 sbin
<DIR>       4096 sys
<DIR>       4096 tmp
<DIR>       4096 usr
<DIR>       4096 var
            8700 .bash_history
<DIR>       4096 lectured

解决sdhci_transfer_data: Error detected in status(0x208002)!的问题

本节参考资料:u-boot内存容量显示修复及SD卡驱动问题_xiaojimmychen的专栏-CSDN博客首先,先修复上一个篇文章遗留下来的问题,即uboot起来后显示内容容量只有512M的问题,后来查看了4412的芯片手册,发现是有寄存器没有设置正确,所以才导致内存容量识别有问题,按照下面的修改,我们的uboot就能识别到1G的内存容量了!diff --git a/arch/arm/mach-exynos/exynos4_setup.h b/arch/arm/mach-exynos/exynos4_shttps://blog.csdn.net/u013779722/article/details/60311216 这其实是因为uboot使用的drivers/mmc/s5p_sdhci.c驱动和exynos4412芯片不是很匹配,根据目标时钟频率clock计算分频系数div的算法有问题。
div根本就不应该两倍两倍地算,而且exynos4412的SDHCI_CLOCK_CONTROL(0x2c)寄存器[15:4]位是保留位,不存在DIVIDER和DIVIDER_HI位。

【探究过程】
(1)uboot中,可以用cpu_is_exynos4()判断芯片是否是exynos4系列,用proid_is_exynos4412()判断芯片是不是exynos4412,用#ifdef CONFIG_ITOP4412判断menuconfig菜单里面是否选择的是迅为itop4412的板子。
include/configs/exynos-common.h里面定义的CONFIG_SYS_CLK_FREQ=24MHz是XusbXTI晶振的大小,所有的系统时钟都是从这个晶振上倍频得到的。
(2)exynos4412明明有五个sdmmc(MMC0~4),但clock_init_exynos4.c的system_clock_init函数里面只初始化了CLK_SRC_FSYS(&clk->src_fsys)的MMC1~4的时钟,偏偏就没有初始化eMMC用的MMC0。
在spl程序中加printf打印发现,赋值前CLK_SRC_FSYS寄存器的值为0x66666,赋值后仍然为0x66666。也就是说所有MMCx_SEL的初值都是6(可能是最开始三星的BL0或BL1程序设置好的),clrsetbits_le32(&clk->src_fsys, clr, set)这句话并没有起实际作用。所以system_clock_init函数没有修改MMC0_SEL也没有影响。
当MMC0_SEL=6时选择的输入时钟是SCLKMPLL_USER_T=get_pll_clk(MPLL)=800MHz。
arch/arm/mach-exynos/clock.c里面的exynos4_get_mmc_clk函数可以用来计算MMC(n)的时钟频率。
计算公式是(sclk/(ratio+1))/(pre_ratio+1),其中sclk=800MHz,ratio=7,pre_ratio是set_mmc_clk函数传入exynos4_set_mmc_clk函数的div参数
所以化简下来就是100MHz/(div+1)
(3)在arch/arm/mach-exynos/clock.c的set_mmc_clk函数中强制div的值为255,那么理论上的时钟就是100MHz/(255+1)=0.390625MHz
现在MMC0(eMMC)和MMC2(SD卡)都是固定390.625kHz的时钟频率了,用示波器实测SD卡时钟引脚(第5脚),频率为391kHz,脉宽为1.28us,(1/1.28)/2刚好就是0.390625。这说明计算公式是正确的。

 (4)把set_mmc_clk函数改回来。发现eMMC和SD卡初始化的时候,div=127,算出来的频率是781.25kHz,大于了SD卡规范里面的400kHz。初始化完毕后,div=0,算出来的频率是100MHz,查阅eMMC手册可知eMMC最大允许频率为52MHz,这就是eMMC读写失败的原因!
set_mmc_clk函数是由s5p_set_clock函数调用的,s5p_set_clock函数又是由drivers/mmc/sdhci.c的sdhci_set_clock函数通过host->ops->set_clock(host, div)方式调用的。
sdhci_set_clock函数里面的clock参数就是要设置的频率值,host->max_clk=52000000,div变量相当于公式里面的(div+1)。显然就是sdhci_set_clock函数根据clock计算div值的算法有问题。
他的算法如下:
/* Version 2.00 divisors must be a power of 2. */
for (div = 1; div < SDHCI_MAX_DIV_SPEC_200; div *= 2) {
    if ((host->max_clk / div) <= clock)
        break;
}
div >>= 1;
当参数clock=400000,他算出来的div变量值是128,于是set_mmc_clk(0, 128),公式中的div+1=128,得到的实际频率是781.25kHz
当参数clock=52000000,他算出来的div变量值是0,于是set_mmc_clk(0, 0),set_mmc_clk函数中if (div > 0)不成立,div不减1,所以公式中的div=0,div+1=1,得到的实际频率是100MHz
充分说明clock值是对的,但div值没算对。
(5)exynos4412的SDHCI_CLOCK_CONTROL寄存器里面根本没有DIVIDER和DIVIDER_HI位,所以下面前两行代码无实际效果:
clk |= (div & SDHCI_DIV_MASK) << SDHCI_DIVIDER_SHIFT;
clk |= ((div & SDHCI_DIV_HI_MASK) >> SDHCI_DIV_MASK_LEN)
    << SDHCI_DIVIDER_HI_SHIFT;
clk |= SDHCI_CLOCK_INT_EN;
sdhci_writew(host, clk, SDHCI_CLOCK_CONTROL);
(6)
sdhci_set_clock函数改一下:
if (host->ops && host->ops->set_clock)
{
    int mmcdiv = div;
    if (proid_is_exynos4412()) {
        for (mmcdiv = 1; mmcdiv < 256; mmcdiv++) {
            if (100000000 / mmcdiv <= clock)
                break;
        }
    }
    host->ops->set_clock(host, mmcdiv);
    printf("Requested MMC%d clock: %uHz\n", host->index, clock);
    printf("Real MMC%d clock: %uHz (mmcdiv=%d)\n", host->index, get_mmc_clk(host->index), mmcdiv);
}
串口打印:
Requested MMC0 clock: 400000Hz
Real MMC0 clock: 400000Hz (mmcdiv=250)
Requested MMC0 clock: 52000000Hz
Real MMC0 clock: 50000000Hz (mmcdiv=2)
eMMC初始化时的频率是400kHz,正常读写时是50MHz。
Requested MMC2 clock: 400000Hz
Real MMC2 clock: 400000Hz (mmcdiv=250)
Requested MMC2 clock: 50000000Hz
Real MMC2 clock: 50000000Hz (mmcdiv=2)
SD卡初始化时的频率是400kHz,正常读写时是50MHz。
问题解决。

【解决方案】

[简单改法]
drivers/mmc/sdhci.c的sdhci_set_clock函数的if (host->ops && host->ops->set_clock)里面改成:

if (host->ops && host->ops->set_clock)
{
    int mmcdiv = div;
    if (proid_is_exynos4412()) {
        for (mmcdiv = 1; mmcdiv < 256; mmcdiv++) {
            if (100000000 / mmcdiv <= clock)
                break;
        }
    }
    host->ops->set_clock(host, mmcdiv);
}

[标准化改法(严格遵循分层思想)]
(1) 修改include/sdhci.h文件,给struct sdhci_ops结构体添加一个calc_div成员:

int     (*calc_div)(struct sdhci_host *host, u32 clock);

(2) 修改drivers/mmc/s5p_sdhci.c文件,给struct sdhci_ops s5p_sdhci_ops结构体变量的calc_div成员赋值:

static const struct sdhci_ops s5p_sdhci_ops = {
	.set_clock	= &s5p_set_clock,
	.set_control_reg = &s5p_sdhci_set_control_reg,
	.calc_div = &s5p_calc_div,
};

然后实现s5p_calc_div函数:

static int s5p_calc_div(struct sdhci_host *host, u32 clock)
{
	return calc_mmc_div(host->index, clock);
}

(3) 修改arch/arm/mach-exynos/include/mach/clk.h文件,添加calc_mmc_div函数的声明:

int calc_mmc_div(int dev_index, unsigned long clock);

(4) 修改arch/arm/mach-exynos/clock.c文件,实现calc_mmc_div函数:

static int exynos4_calc_mmc_div(int dev_index, unsigned long clock)
{
#ifdef CONFIG_ITOP4412
	struct exynos4x12_clock *clk =
		(struct exynos4x12_clock *)samsung_get_base_clock();
#else
	struct exynos4_clock *clk =
		(struct exynos4_clock *)samsung_get_base_clock();
#endif
	unsigned long uclk, sclk;
	unsigned int sel, ratio;
	int div, shift = 0;

	sel = readl(&clk->src_fsys);
	sel = (sel >> (dev_index << 2)) & 0xf;

	if (sel == 0x6)
		sclk = get_pll_clk(MPLL);
	else if (sel == 0x7)
		sclk = get_pll_clk(EPLL);
	else if (sel == 0x8)
		sclk = get_pll_clk(VPLL);
	else
		return 0;

	switch (dev_index) {
	case 0:
	case 1:
		ratio = readl(&clk->div_fsys1);
		break;
	case 2:
	case 3:
		ratio = readl(&clk->div_fsys2);
		break;
	case 4:
		ratio = readl(&clk->div_fsys3);
		break;
	default:
		return 0;
	}

	if (dev_index == 1 || dev_index == 3)
		shift = 16;

	ratio = (ratio >> shift) & 0xf;
	for (div = 1; div < 256; div++) {
		uclk = (sclk / (ratio + 1)) / div;
		if (uclk <= clock)
			break;
	}
	return div;
}

int calc_mmc_div(int dev_index, unsigned long clock)
{
	if (cpu_is_exynos4())
		return exynos4_calc_mmc_div(dev_index, clock);
	else
		return -1;
}

(5) 修改drivers/mmc/sdhci.c文件,将sdhci_set_clock函数的if (host->ops && host->ops->set_clock)里面改成:

if (host->ops && host->ops->set_clock) {
	int mmcdiv = -1;
	if (host->ops->calc_div)
		mmcdiv = host->ops->calc_div(host, clock);
	
	if (mmcdiv == -1)
		host->ops->set_clock(host, div);
	else
		host->ops->set_clock(host, mmcdiv);
}

经测试,uboot能正常从emmc加载linux内核并运行。saveenv保存环境变量到emmc也没有问题。

emmc的环境变量区

u-boot-iTOP-4412.bin的结构如下:

文件名E4412_N.bl1.binitop4412-spl.binenv.binu-boot.bin
说明BL1程序BL2 SPL程序环境变量存储区(SD卡专用)BL2完整程序
大小8KB16KB8KB

其中env.bin的大小为8192字节,内容为全0,是SD卡专用的环境变量存储区。为什么说是SD卡专用的呢?
因为SD卡只有用户空间,而emmc除了用户空间以外,还有boot0、boot1、rpmb区。
我们把uboot烧写到sd卡时,是用dd命令把u-boot-iTOP-4412.bin烧写到sd卡的1号扇区处。板子开机时,exynos4412芯片也是从SD卡的1号扇区去读8KB的BL1程序。saveenv保存uboot环境变量是保存到第49扇区处的(env.bin空间)。
然而我们把uboot烧写到emmc时,是在板子上用dd命令把u-boot-iTOP-4412.bin烧写到boot0区的0号扇区处,开机时芯片自动从boot0区读取BL1程序到iRAM,然后BL1读取BL2 SPL程序到iRAM,然后BL2 SPL读取BL2完整版程序到DDR3内存,这些都是读的boot0区。在DDR3内存里面运行uboot时,读环境变量却是从用户空间的第49扇区去读,saveenv保存的位置也是用户空间的第49扇区。之后加载linux内核和设备树,分别是从用户空间的第4096、第2048扇区读取。
所以emmc boot0区的第49~64扇区根本就没有用到,一直都是全0数据。
这也就导致了如下的现象:
(1)重新烧写uboot到sd卡时,环境变量区被清零,uboot启动时采用include/configs/itop4412.h里面的默认值
(2)重新烧写uboot到emmc时,清除的是boot0区的第49~64扇区,用户空间里面的环境变量区没有被清除,所以一旦saveenv保存了环境变量,永久生效

为了清除真正的emmc环境变量区,可以用一个shell脚本erase_emmc_env.sh完成:

#!/bin/sh
dd if=/dev/zero of=/dev/mmcblk1 seek=49 count=16

一个uboot文件同时兼容emmc和sd卡,自动根据OM引脚判断启动方式

关于emmc和sd卡的uboot,我们有以下三种方案。

方案一:
emmc的uboot,设备树里面只定义emmc。
sd卡的uboot,设备树里面只定义sd。
这样的话,uboot启动时只能访问一个设备,环境变量也从那个设备读取。我们要分别为emmc和sd卡做两个uboot出来。

方案二:
设备树里面emmc和sd卡都定义。
emmc的uboot,#define CONFIG_SYS_MMC_ENV_DEV 0,环境变量从emmc读取。
sd卡的uboot,#define CONFIG_SYS_MMC_ENV_DEV 1,环境变量从sd卡读取。
CONFIG_SYS_MMC_ENV_DEV这个宏是在include/configs/itop4412.h里面定义的。不是在menuconfig菜单里面!!!
这样的话,uboot启动时两个设备都能访问,但要分别为emmc和sd卡做两个uboot出来。

方案三:
设备树里面emmc和sd卡都定义。
找一个地方(比如board/samsung/itop4412/itop4412.c)重写mmc_get_env_dev函数,根据芯片的OM引脚来判断环境变量该从哪个设备读取。

#include <mach/power.h>
int mmc_get_env_dev(void)
{
    unsigned int bootmode;
    
    bootmode = get_boot_mode();
    if (bootmode == BOOT_MODE_EMMC || bootmode == BOOT_MODE_EMMC_SD)
        return 0; // emmc
    else
        return 1; // sd卡
}

(原本mmc_get_env_dev函数在env/mmc.c里面,是打了__weak标记的,返回的就是CONFIG_SYS_MMC_ENV_DEV宏的值)
这样的话,uboot启动时两个设备都能访问,而且只需要做一个uboot出来,根据芯片OM脚(开发板上的红色拨码开关,通电时决定芯片从哪儿启动uboot)自己就判断了。
然而缺点是,当emmc或sd卡里面的环境变量存储区为无效内容时,uboot会从include/configs/itop4412.h里面读取默认环境变量值,而这个默认值只能有一套!
每次烧写uboot都会覆盖掉环境变量存储区,然后采用默认值。这个就比较麻烦了。

下面讨论如何采用两套默认环境变量值。
include/configs/itop4412.h里面:
CONFIG_EXTRA_ENV_SETTINGS只定义bootargs0和bootargs1,分别是emmc和sd卡的内核启动参数,不定义bootargs。
CONFIG_BOOTCOMMAND加一句mmc dev ${bootdev}。
添加#define CONFIG_MISC_INIT_R宏使能misc_init_r函数。

#define CONFIG_EXTRA_ENV_SETTINGS \
	"dtbaddr=0x41000000\0" /* DDR3内存中存放设备树的位置 */ \
	"loadaddr=0x40007000\0" /* DDR3内存中存放内核的位置 */ \
	"bootargs0=console=ttySAC2,115200n8 earlyprintk root=/dev/mmcblk1p2 rw rootfstype=ext4 init=/linuxrc\0" /* 内核启动参数(eMMC) */ \
	"bootargs1=console=ttySAC2,115200n8 earlyprintk root=/dev/mmcblk0p2 rw rootfstype=ext4 init=/linuxrc\0" /* 内核启动参数(SD卡) */
#define CONFIG_BOOTCOMMAND \
	"mmc dev ${bootdev};" /* 选择设备(eMMC或SD卡) */ \
	"mmc read ${dtbaddr} 0x800 0xa0;" /* [读设备树] 将设备的第2048~2207扇区(共计80KB)读到DDR3内存的dtbaddr地址处 */ \
	                                  /* 请注意mmc read的最后两个参数必须是十六进制格式 */ \
	"mmc read ${loadaddr} 0x1000 0x4000;" /* [读Linux内核] 将设备的第4096~20479扇区(共计8MB)读到DDR3内存的loadaddr地址处 */ \
	"bootm ${loadaddr} - ${dtbaddr}" /* [启动内核] 使用带3个参数的bootm命令, 第一个参数是内核地址loadaddr, 第二个参数是initrd地址(填"-"代表没有initrd), 第三个参数是设备树地址dtbaddr */
#define CONFIG_MISC_INIT_R

由init_sequence_r里面的misc_init_r函数根据OM脚设置bootargs和bootdev环境变量默认值(也就是设备的环境变量存储区为无效数据的情况下采用的值)。
include/samsung/misc.h添加:

#ifdef CONFIG_ITOP4412
void itop4412_misc_init_r(void);
#endif

board/samsung/common/board.c的misc_init_r函数里面添加:

#ifdef CONFIG_ITOP4412
    itop4412_misc_init_r();
#endif

board/samsung/itop4412/itop4412.c里面添加:

#ifdef CONFIG_MISC_INIT_R
#include <environment.h>
void itop4412_misc_init_r(void)
{
    const char *p;
    char varname[30];
    char *bootargs;
    int bootdev;
    int i;
    
    if (gd->flags & GD_FLG_ENV_DEFAULT) // 如果存储器里面没有保存环境变量,则当前采用的是itop4412.h里面定义的默认值
    {
        // 将芯片启动方式保存到bootdev环境变量中
        bootdev = mmc_get_env_dev();
        env_set_ulong("bootdev", bootdev);
    
        // 根据芯片启动方式,将相应的bootargsN环境变量赋值给bootargs环境变量
        snprintf(varname, sizeof(varname), "bootargs%d", bootdev);
        bootargs = env_get(varname);
        if (bootargs != NULL)
            env_set("bootargs", bootargs);
            
        // 删除所有bootargsN环境变量
        for (p = (const char *)default_environment; p[0] != '\0'; p += strlen(p) + 1)
        {
            for (i = 0; p[i] != '\0' && p[i] != '=' && i != sizeof(varname) - 1; i++)
                varname[i] = p[i];
            varname[i] = '\0';
            if (memcmp(varname, "bootargs", 8) == 0 && varname[8] >= '0' && varname[8] <= '9')
                env_set(varname, NULL);
        }
    }
}
#endif

uboot里面0是emmc,1是sd卡。而linux内核里面mmcblk0是sd卡,mmcblk1是emmc。这是由设备树里面定义的顺序决定的。
编译uboot,将编译出来的文件烧写到emmc和sd卡中。启动后可以发现,环境变量里面有bootargs,但没有bootargs0和bootargs1。环境变量里面还有一个自动生成的bootdev。
当红色拨码开关选择emmc启动时,bootdev=0,bootargs=itop4412.h中的bootargs0。
当红色拨码开关选择sd卡启动时,bootdev=1,bootargs=itop4412.h中的bootargs1。
saveenv保存环境变量也能自动判断是哪个存储设备。

Logo

更多推荐