uboot環境變數實現分析

weixin_34126215發表於2016-02-19

u-boot的環境變數用來儲存一些常常使用的引數變數。uboot希望將環境變數儲存在靜態儲存器中(如nand nor eeprom mmc)。

當中有一些也是大家經常使用。有一些是使用人員自定義的。更改這些名字會出現錯誤,以下的表中我們列出了一些經常使用的環境變數:

     bootdelay    執行自己主動啟動的等候秒數
     baudrate     串列埠控制檯的波特率
     netmask     乙太網介面的掩碼
     ethaddr       乙太網卡的網路卡實體地址
     bootfile        預設的下載檔案
     bootargs     傳遞給核心的啟動引數
     bootcmd     自己主動啟動時執行的命令
     serverip       伺服器端的ip地址
     ipaddr         本地ip 地址
     stdin           標準輸入裝置
     stdout        標準輸出裝置
     stderr         標準出錯裝置

上面這些是uboot預設存在的環境變數,uboot本身會使用這些環境變數來進行配置。我們能夠自定義一些環境變數來供我們自己uboot驅動來使用。

Uboot環境變數的設計邏輯是在啟動過程中將env從靜態儲存器中讀出放到RAM中。之後在uboot下對env的操作(如printenv editenv setenv)都是對RAMenv的操作。僅僅有在執行saveenv時才會將RAM中的env又一次寫入靜態儲存器中。

這樣的設計邏輯能夠加快對env的讀寫速度。

基於這樣的設計邏輯。2014.4版本號uboot實現了saveenv這個儲存env到靜態儲存器的命令。而沒有實現讀取envRAM的命令。

那我們就來看一下ubootenv的資料結構 初始化 操作怎樣實現的。

一 env資料結構

include/environment.h中定義了env_t,例如以下:

#ifdef CONFIG_SYS_REDUNDAND_ENVIRONMENT
# define ENV_HEADER_SIZE    (sizeof(uint32_t) + 1)
# define ACTIVE_FLAG   1
# define OBSOLETE_FLAG 0
#else
# define ENV_HEADER_SIZE    (sizeof(uint32_t))
#endif
#define ENV_SIZE (CONFIG_ENV_SIZE - ENV_HEADER_SIZE)
typedef struct environment_s {
    uint32_t    crc;        /* CRC32 over data bytes    */
#ifdef CONFIG_SYS_REDUNDAND_ENVIRONMENT
    unsigned char   flags;      /* active/obsolete flags    */
#endif
    unsigned char   data[ENV_SIZE]; /* Environment data     */
} env_t;

CONFIG_ENV_SIZE是我們須要在配置檔案裡配置的環境變數的總長度。

這裡我們使用的nand作為靜態儲存器,nand的一個block128K,因此選用一個block來儲存envCONFIG_ENV_SIZE128K

Env_t結構體頭4bytes是對datacrc校驗碼,未定義CONFIG_SYS_REDUNDAND_ENVIRONMENT。所以後面緊跟data陣列,陣列大小是ENV_SIZE.

ENV_SIZECONFIG_ENV_SIZE減掉ENV_HEADER_SIZE,也就是4bytes

所以env_t這個結構體就包括了整個我們規定的長度為CONFIG_ENV_SIZE的儲存區域。

4bytescrc校驗碼。後面剩餘的空間所實用來儲存環境變數。

須要說明的一點,crc校驗碼是uboot中在saveenv時計算出來。然後寫入nand,所以在第一次啟動ubootcrc校驗會出錯,

由於ubootnand上讀入的一個block資料是隨機的。沒有意義的,執行saveenv後重新啟動ubootcrc校驗就正確了。

data 欄位儲存實際的環境變數。u-boot  的 env  按 name=value”\0”的方式儲存,在全部env 的最後以”\0\0”表示整個 env  的結束。

新的name=value 對總是被加入到 env  資料塊的末尾,當刪除一個 name=value 對時,後面的環境變數將前移,對一個已經存在的環境變數的改動實際上先刪除再插入。 
u-boot env_t  的資料指標儲存在了另外一個地方,這就 
是 gd_t  結構(不同平臺有不同的 gd_t  結構 ),這裡以ARM 為例僅列出和 env  相關的部分 

typedef struct global_data 
{ 
     … 
     unsigned long env_off;        /* Relocation Offset */ 
     unsigned long env_addr;       /* Address of Environment struct ??

? */       unsigned long env_valid       /* Checksum of Environment valid */       …  } gd_t; 


二 env的初始化

ubootenv的整個架構能夠分為3層:

(1) 命令層。如saveenvsetenv editenv這些命令的實現,還有如啟動時呼叫的env_relocate函式。

(2) 中間封裝層,利用不同靜態儲存器特性封裝出命令層須要使用的一些通用函式,如env_init,env_relocate_spec,saveenv這些函式。實現檔案在common/env_xxx.c

(3) 驅動層,實現不同靜態儲存器的讀寫擦等操作,這些是uboot下不同子系統都必須的。

依照執行流順序,首先分析一下uboot啟動的env初始化過程。

首先在board_init_f中呼叫init_sequenceenv_init,這個函式是不同儲存器實現的函式,nand中的實現例如以下:

<span style="font-size:14px;">/*
 * This is called before nand_init() so we can't read NAND to
 * validate env data.
 *
 * Mark it OK for now. env_relocate() in env_common.c will call our
 * relocate function which does the real validation.
 *
 * When using a NAND boot image (like sequoia_nand), the environment
 * can be embedded or attached to the U-Boot image in NAND flash.
 * This way the SPL loads not only the U-Boot image from NAND but
 * also the environment.
 */
int env_init(void)
{
    gd->env_addr    = (ulong)&default_environment[0];
    gd->env_valid   = 1;
    return 0;
}</span>

從凝視就基本能夠看出這個函式的作用,由於env_init要早於靜態儲存器的初始化,所以無法進行env的讀寫,這裡將gd中的env相關變數進行配置,

預設設定envvalid

方便後面env_relocate函式進行真正的envnandramrelocate

繼續執行,在board_init_r中,例如以下:

/* initialize environment */
    if (should_load_env())
        env_relocate();
    else
        set_default_env(NULL);

這是在全部儲存器初始化完畢後執行的。

首先呼叫should_load_env。例如以下:

/*
 * Tell if it's OK to load the environment early in boot.
 *
 * If CONFIG_OF_CONFIG is defined, we'll check with the FDT to see
 * if this is OK (defaulting to saying it's not OK).
 *
 * NOTE: Loading the environment early can be a bad idea if security is
 *       important, since no verification is done on the environment.
 *
 * @return 0 if environment should not be loaded, !=0 if it is ok to load
 */
static int should_load_env(void)
{
#ifdef CONFIG_OF_CONTROL
    return fdtdec_get_config_int(gd->fdt_blob, "load-environment", 1);
#elif defined CONFIG_DELAY_ENVIRONMENT
    return 0;
#else
    return 1;
#endif
}

從凝視能夠看出,CONFIG_OF_CONTROL未定義,鑑於考慮安全性問題,假設我們想要推遲envload,能夠定義CONFIG_DELAY_ENVIRONMENT,這裡返回0,就呼叫set_default_env使用預設的env。預設env是在配置檔案裡CONFIG_EXTRA_ENV_SETTINGS設定的。

我們能夠在之後的某個地方在呼叫env_relocateload env

這裡我們選擇在這裡直接load env。所以未定義CONFIG_DELAY_ENVIRONMENT,返回1。呼叫env_relocate

common/env_common.c中:

void env_relocate(void)
{
#if defined(CONFIG_NEEDS_MANUAL_RELOC)
    env_reloc();
    env_htab.change_ok += gd->reloc_off;
#endif
    if (gd->env_valid == 0) {
#if defined(CONFIG_ENV_IS_NOWHERE) || defined(CONFIG_SPL_BUILD)
        /* Environment not changable */
        set_default_env(NULL);
#else
        bootstage_error(BOOTSTAGE_ID_NET_CHECKSUM);
        set_default_env("!bad CRC");
#endif
    } else {
        env_relocate_spec();
    }
}

Gd->env_valid在之前的env_init中設定為1。所以這裡呼叫env_relocate_spec

這個函式也是不同儲存器的中間封裝層提供的函式,對於nandcommon/env_nand.c中,例如以下:

void env_relocate_spec(void)
{
   int ret;
    ALLOC_CACHE_ALIGN_BUFFER(char, buf, CONFIG_ENV_SIZE);
    ret = readenv(CONFIG_ENV_OFFSET, (u_char *)buf);
    if (ret) {
        set_default_env("!readenv() failed");
        return;
    }
    env_import(buf, 1);
} 

首先定義一個長度為CONFIG_ENV_SIZEbuf,然後呼叫readenv

CONFIG_ENV_OFFSET是在配置檔案裡定義的envnand中偏移位置。我們這裡定義的是在4M的位置。

Readenv也在env_nand.c中,例如以下:

int readenv(size_t offset, u_char *buf)
{
    size_t end = offset + CONFIG_ENV_RANGE;
    size_t amount_loaded = 0;
    size_t blocksize, len;
    u_char *char_ptr;
    blocksize = nand_info[0].erasesize;
    if (!blocksize)
        return 1;
    len = min(blocksize, CONFIG_ENV_SIZE);
    while (amount_loaded < CONFIG_ENV_SIZE && offset < end) {
        if (nand_block_isbad(&nand_info[0], offset)) {
            offset += blocksize;
        } else {
            char_ptr = &buf[amount_loaded];
            if (nand_read_skip_bad(&nand_info[0], offset,
                           &len, NULL,
                           nand_info[0].size, char_ptr))
                return 1;
            offset += blocksize;
            amount_loaded += len;
        }
    }

    if (amount_loaded != CONFIG_ENV_SIZE)
        return 1;

    return 0;
}

Readenv函式利用nand_info[0]nand進行讀操作,讀出指定位置。指定長度的資料到buf中。Nand_info[0]是一個全域性變數,來表徵第一個nand device,這裡在nand_init時會初始化這個變數。Nand_init必須在env_relocate之前。

回到env_relocate_spec中,buf讀回後呼叫env_import,例如以下:

/*
 * Check if CRC is valid and (if yes) import the environment.
 * Note that "buf" may or may not be aligned.
 */
int env_import(const char *buf, int check)
{
    env_t *ep = (env_t *)buf;

    if (check) {
        uint32_t crc;

        memcpy(&crc, &ep->crc, sizeof(crc));

        if (crc32(0, ep->data, ENV_SIZE) != crc) {
            set_default_env("!bad CRC");
            return 0;
        }
    }

    if (himport_r(&env_htab, (char *)ep->data, ENV_SIZE, '\0', 0,
            0, NULL)) {
        gd->flags |= GD_FLG_ENV_READY;
        return 1;
    }

    error("Cannot import environment: errno = %d\n", errno);

    set_default_env("!import failed");

    return 0;
}

首先將buf強制轉換為env_t型別,然後對data進行crc校驗,跟buf中原有的crc對照,不一致則使用預設env

最後呼叫himport_r,該函式將給出的data依照‘\0’切割填入env_htab的雜湊表中。

之後對於env的操作。如printenv setenv editenv,都是對該雜湊表的操作。

Env_relocate執行完畢。env的初始化就完畢了。


三 env的操作實現

Ubootenv的操作命令實如今common/cmd_nvedit.c中。

對於setenv printenv editenv3個命令。看事實上現程式碼,都是對relocateRAM中的env_htab的操作。這裡就不再具體分析了,重點來看一下savenv實現。

static int do_env_save(cmd_tbl_t *cmdtp, int flag, int argc,
               char * const argv[])
{
    printf("Saving Environment to %s...\n", env_name_spec);

    return saveenv() ? 1 : 0;
}

U_BOOT_CMD(
    saveenv, 1, 0,  do_env_save,
    "save environment variables to persistent storage",
    ""
);

do_env_save呼叫saveenv,這個函式是不同儲存器實現的封裝層函式。對於nand,在common/env_nand.c中,例如以下:

int saveenv(void)
{
    int ret = 0;
    ALLOC_CACHE_ALIGN_BUFFER(env_t, env_new, 1);
    ssize_t len;
    char    *res;
    int env_idx = 0;
    static const struct env_location location[] = {
        {
            .name = "NAND",
            .erase_opts = {
                .length = CONFIG_ENV_RANGE,
                .offset = CONFIG_ENV_OFFSET,
            },
        },
#ifdef CONFIG_ENV_OFFSET_REDUND
        {
            .name = "redundant NAND",
            .erase_opts = {
                .length = CONFIG_ENV_RANGE,
                .offset = CONFIG_ENV_OFFSET_REDUND,
            },
        },
#endif
    };

    if (CONFIG_ENV_RANGE < CONFIG_ENV_SIZE)
        return 1;

    res = (char *)&env_new->data;
    len = hexport_r(&env_htab, '\0', 0, &res, ENV_SIZE, 0, NULL);
    if (len < 0) {
        error("Cannot export environment: errno = %d\n", errno);
        return 1;
    }
    env_new->crc   = crc32(0, env_new->data, ENV_SIZE);
#ifdef CONFIG_ENV_OFFSET_REDUND
    env_new->flags = ++env_flags; /* increase the serial */
    env_idx = (gd->env_valid == 1);
#endif

    ret = erase_and_write_env(&location[env_idx], (u_char *)env_new);
#ifdef CONFIG_ENV_OFFSET_REDUND
    if (!ret) {
        /* preset other copy for next write */
        gd->env_valid = gd->env_valid == 2 ? 1 : 2;
        return ret;
    }

    env_idx = (env_idx + 1) & 1;
    ret = erase_and_write_env(&location[env_idx], (u_char *)env_new);
    if (!ret)
        printf("Warning: primary env write failed,"
                " redundancy is lost!\n");
#endif

    return ret;
}

定義env_t型別的變數env_new。準備來儲存env

利用函式hexport_renv_htab操作。讀取env內容到env_new->data

校驗data。獲取校驗碼env_new->crc

最後呼叫erase_and_write_envenv_new先擦後寫入由location定義的偏移量和長度的nand區域中。

這樣就完畢了env寫入nand的操作。

在savenv readenv函式以及printenv setenv的實現函式中涉及到的函式himport_r hexport_r hdelete_r hmatch_r都是對env_htab雜湊表的一些基本操作函式。

這些函式都封裝在uboot的lib/hashtable.c中。這裡就不細緻分析這些函式了。


相關文章