注:本文是学习朱老师课程整理的笔记,基于uboot-1.3.4和s5pc11x分析。
环境变量的作用
可以不用修改uboot的源代码,而是通过修改环境变量来影响uboot运行时的一些数据和特性。譬如说通过修改bootdelay环境变量就可以更改系统开机自动启动时倒数的秒数。
环境变量的优先级
如果环境变量为空则使用代码中的值;如果环境变量不为空则优先使用环境变量对应的值。
譬如machid(机器码)。uboot中在x210_sd.h中定义了一个机器码2456,如果要修改uboot中配置的机器码,可以修改x210_sd.h中的机器码,但是修改源代码后需要重新编译烧录,很麻烦;比较简单的方法就是使用环境变量machid。如:set machid 0x998,有了machid环境变量后,系统启动时会优先使用machid对应的环境变量,这就是优先级问题。
环境变量的工作方式
默认环境变量,在uboot/common/env_common.c中default_environment:
uchar default_environment[CFG_ENV_SIZE] = {
"bootargs=" CONFIG_BOOTARGS "\0"
"bootcmd=" CONFIG_BOOTCOMMAND "\0"
"mtdpart=" CONFIG_MTDPARTITION "\0"
"nfsboot=" CONFIG_NFSBOOTCOMMAND "\0"
"bootdelay=" MK_STR(CONFIG_BOOTDELAY) "\0"
"baudrate=" MK_STR(CONFIG_BAUDRATE) "\0"
"ethaddr=" MK_STR(CONFIG_ETHADDR) "\0"
"ipaddr=" MK_STR(CONFIG_IPADDR) "\0"
"serverip=" MK_STR(CONFIG_SERVERIP) "\0"
"gatewayip=" MK_STR(CONFIG_GATEWAYIP) "\0"
"netmask=" MK_STR(CONFIG_NETMASK) "\0"
"\0"
};
环境变量在内存中的存储大体如下:
这东西本质是一个字符数组,大小为CFG_ENV_SIZE(16kb),里面内容就是很多个环境变量连续分布组成的,每个环境变量最末端以’\0’结束。
SD卡中环境变量分区,在uboot的raw分区中。存储时是把DDR中的环境变量整体的写入SD卡中分区里。所以当我们saveenv时其实整个所有的环境变量都被保存了一遍,而不是只保存更改了的。
DDR中环境变量就是default_environment字符数组,在uboot中其实是一个全局变量,链接时在数据段,重定位时default_environment就被重定位到DDR中一个内存地址处了。
刚烧录的SD卡中环境变量分区是空白的,uboot第一次运行时加载的是uboot代码中自带的一份环境变量,叫默认环境变量。我们在saveenv时DDR中的环境变量会被更新到SD卡中的环境变量中,就可以被保存下来,下次开机会将环境变量从SD卡中relocate到DDR中去。
default_environment中的内容虽然被uboot源代码初始化为一定的值(这个值就是我们的默认环境变量),但是在uboot启动的第二阶段,env_relocate时代码会去判断SD卡中的env分区的crc是否通过。如果crc校验通过说明SD卡中有正确的环境变量存储,则relocate函数会从SD卡中读取环境变量来覆盖default_environment字符数组,从而每次开机可以保持上一次更改过的环境变量。
环境变量相关命令源码解析
- printenv
int do_printenv (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
int i, j, k, nxt;
int rcode = 0;
if (argc == 1) { /* Print all env variables */
for (i=0; env_get_char(i) != '\0'; i=nxt+1) {
for (nxt=i; env_get_char(nxt) != '\0'; ++nxt)
;
for (k=i; k<nxt; ++k)
putc(env_get_char(k));
putc ('\n');
if (ctrlc()) {
puts ("\n ** Abort\n");
return 1;
}
}
printf("\nEnvironment size: %d/%ld bytes\n",
i, (ulong)ENV_SIZE);
return 0;
}
for (i=1; i<argc; ++i) { /* print single env variables */
char *name = argv[i];
k = -1;
for (j=0; env_get_char(j) != '\0'; j=nxt+1) {
for (nxt=j; env_get_char(nxt) != '\0'; ++nxt)
;
k = envmatch((uchar *)name, j);
if (k < 0) {
continue;
}
puts (name);
putc ('=');
while (k < nxt)
putc(env_get_char(k++));
putc ('\n');
break;
}
if (k < 0) {
printf ("## Error: \"%s\" not defined\n", name);
rcode ++;
}
}
return rcode;
}
找到printenv命令所对应的函数。通过printenv的help可以看出,这个命令有2种使用方法。第一种直接使用不加参数则打印所有的环境变量;第二种是printenv name则只打印出name这个环境变量的值。
do_printenv函数首先判断argc是否等于1,若argc=1那么就循环打印所有的环境变量出来;如果argc不等于1,则后面的参数就是要打印的环境变量,给哪个环境变量就打印哪个。
argc=1时用双重for循环来依次打印所有的环境变量。第一重for循环就是处理各个环境变量。所以有多少个环境变量则第一重就执行循环多少圈。
env_get_char函数中又调用了 env_get_char_memory:
uchar env_get_char_memory (int index)
{
if (gd->env_valid) {
return ( *((uchar *)(gd->env_addr + index)) );
} else {
return ( default_environment[index] );
}
}
上面两条return的语句其实可以相等。
在env_init函数中可以看出:
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 1;
总结:这个函数要看懂,首先要明白整个环境变量在内存中如何存储的。
- setenv
命令定义对应的函数在uboot/common/cmd_nvedit.c中,对应的函数为do_setenv。
int do_setenv (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
if (argc < 2) {
printf ("Usage:\n%s\n", cmdtp->usage);
return 1;
}
return _do_setenv (flag, argc, argv);
}
do_setenv中又调用了_do_setenv,_do_setenv的思路就是:先去DDR中的环境变量处寻找原来有没有这个环境变量,如果原来就有则需要覆盖原来的环境变量,如果原来没有则在最后新增一个环境变量即可。
第1步:遍历DDR中环境变量的数组,找到原来就有的那个环境变量对应的地址。
/*
* search if variable with this name already exists
*/
oldval = -1;
for (env=env_data; *env; env=nxt+1) {
for (nxt=env; *nxt; ++nxt)
;
if ((oldval = envmatch((uchar *)name, env-env_data)) >= 0)
break;
}
第2步:擦除原来的环境变量
第3步:写入新的环境变量
if (*++nxt == '\0') { /* 擦除原来的环境变量 */
if (env > env_data) {
env--;
} else {
*env = '\0';
}
} else {
for (;;) { /* 写入新的环境变量 */
*env = *nxt++;
if ((*env == '\0') && (*nxt == '\0'))
break;
++env;
}
}
*++env = '\0';
}
本来setenv做完上面的就完了,但是还要考虑一些附加的问题。
问题一:环境变量太多超出DDR中的字符数组,溢出的解决方法。
问题二:有些环境变量如baudrate、ipaddr等,在gd中有对应的全局变量。这种环境变量在set更新的时候要同时去更新对应的全局变量,否则就会出现在本次运行中环境变量和全局变量不一致的情况。
- saveenv
在uboot/common/cmd_nvedit.c中,对应函数为do_saveenv
int do_saveenv (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
extern char * env_name_spec;
printf ("Saving Environment to %s...\n", env_name_spec);
return (saveenv() ? 1 : 0);
}
从uboot实际执行saveenv命令的输出,可以知道env_name_spec的定义在env_auto.c中。接着saveenv()就是定义在env_auto.c中:
int saveenv(void)
{
#if defined(CONFIG_S5PC100) || defined(CONFIG_S5PC110) || defined(CONFIG_S5P6442)
if (INF_REG3_REG == 2)
saveenv_nand();
else if (INF_REG3_REG == 3)
saveenv_movinand();
else if (INF_REG3_REG == 1)
saveenv_onenand();
else if (INF_REG3_REG == 4)
saveenv_nor();
else
printf("Unknown boot device\n");
return 0;
}
使用宏定义的方式去条件编译了各种常见的flash芯片(如movinand、norflash、nand等)。然后在程序中读取INF_REG(OMpin内部对应的寄存器)从而知道我们的启动介质,然后调用这种启动介质对应的函数来操作。这里我们的INF_REG3_REG =3,它的赋值在start.s中:
/* SD/MMC BOOT */
cmp r2, #0xc
moveq r3, #BOOT_MMCSD /* #define BOOT_MMCSD 0x3 */
……
ldr r0, =INF_REG_BASE
str r3, [r0, #INF_REG3_OFFSET]
INF_REG3_REG 寄存器地址:E010F000+0C=E010_F00C,在芯片数据手册中查到该寄存器是用户自定义数据。我们在start.S中判断启动介质后将#BOOT_MMCSD(就是3,定义在x210_sd.h)写入了这个寄存器,所以这里读出的肯定是3,经过判断就是movinand。所以实际执行的函数是:saveenv_movinand。
int saveenv_movinand(void)
{
movi_write_env(virt_to_phys((ulong)env_ptr));
puts("done\n");
return 1;
}
真正执行保存环境变量操作的是:cpu/s5pc11x/movi.c中的movi_write_env函数,这个函数肯定是写sd卡,将DDR中的环境变量数组(其实就是default_environment这个数组,大小16kb,刚好32个扇区)写入iNand中的ENV分区中。
void movi_write_env(ulong addr)
{
movi_write(raw_area_control.image[2].start_blk,
raw_area_control.image[2].used_blk, addr);
}
raw_area_control是uboot中规划iNnad/SD卡的原始分区表,这个里面记录了我们对iNand的分区,env分区也在这里,下标是2。追到这一层就够了,再里面就是调用驱动部分的写SD卡/iNand的底层函数了。
- getenv和getenv_r
getenv是不可重入函数(关于函数的可重入性分析见函数的可重入性理解)。实现方式就是去遍历default_environment数组,挨个拿出所有的环境变量比对name,找到相等的直接返回这个环境变量的首地址即可。
getenv_r是可重入函数。getenv函数是直接返回这个找到的环境变量在DDR中环境变量处的地址,而getenv_r函数的做法是找到了DDR中环境变量地址后,将这个环境变量复制一份到提供的buf中,而不动原来DDR中环境变量。
所以差别就是:getenv中返回的地址只能读不能随便乱写,而getenv_r中返回的环境变量是在自己提供的buf中,是可以随便改写加工的。两者功能是一样的,但是可重入版本会比较安全一些,建议使用。