前面我們基礎工作已經完成,具備了shell交互能力,可以任意的導入導出片內mem和SPIFLASH中的內容。現在開始就可以進入最重要的環節,鏡像的燒錄以及加載鏡像運行。首先我們需要設計鏡像在SPIFLASH中的存儲結構,需要一個HEAD信息去告訴boot哪里是設備樹,哪里是內核鏡像,加載到設備樹和內核到那里去等信息。然后需要實現加載器LOAD功能,用于從SPIFLASH加載設備樹和內核到SDRAM中運行,這一篇就來實現這部分內容。
代碼見:https://github.com/qinyunti/stm32f429-boot.git
我們首要的是要設計設備樹和內核鏡像在SPIFLASH中的布局,當然可能還有其他用戶自定義的內容在SPIFLASH中的布局,這些可以通過HEAD信息來管理,這樣只需要解析HEAD就可以自行解析上述信息,即可從指定地址加載指定內容到SDRAM的指定地方。
HEAD信息就放在固定地址,這里固定為0,當前也可以固定到其它地址,這樣找到HEAD就可以根據HEAD解析其他內容的地址等。
我們設計HEAD信息和存儲布局如下
首先整個存儲劃分為兩個部分,HEAD和sections段。
其中HEAD信息是固定的,sections用戶可以自行分配,其中設備樹文件和內核鏡像是必須的,其他的可以分配給根文件系統,保留區,用戶文件系統等。

HEAD定義如下,分為頭部分和section信息部分。

頭部分對應結構體如下,注意這里的結構體不要求和存儲二進制對應,所以沒有使用packed,結構體和存儲進行手動的轉換,不使用強制類型轉換以避免不同平臺對齊問題,load_section_info_st也是一樣。
/*** \struct load_head_st* HEAD信息 這里不采用packed方式, HEAD結構體和Buffer最好使用手動轉換方式,不要使用直接類型強制轉換*/typedef struct{uint8_t magic[5]; /**< "MLOAD" */uint8_t version; /**< 版本信息 */uint8_t flag; /**< 標志用于表示HEAD是否有校驗信息 */uint8_t sectinos; /**< 后面的section_info個數 */uint32_t len; /**< 用于表示HEAD的長度,包括整個區域 */uint32_t check; /**< 用于表示HEAD的校驗信息,如果flag的bit0為1 *//* 后面就是sectinos個load_section_info_st */load_section_info_st section_info[0];}load_head_st;
然后接下來是每個段信息,段個數由頭中的sectinos指定。
/*** \struct load_section_info_st* SECTION信息*/typedef struct{load_section_type_e type; /**< \ref load_section_type_e */uint8_t subtype; /**< 用戶定義的段時進一步區分 */uint8_t flag; /**< 標志信息用于表示是否有校驗,加密等標志 一個類型對應一位 \ref load_section_flag_e */uint32_t vma; /**< 加載的目的地址,運行地址,即在SDRAM中的地址 */uint32_t lma; /**< 加載的源地址,加載地址,即在存儲中的地址 */uint32_t len; /**< 段的有效數據長度,字節數 */uint32_t check; /**< 段的有效數據對應的校驗信 */} load_section_info_st;
下面介紹下各個字段的含義
其中所有字段使用小端
Magic:固定為5字節的字符”MLOAD”,即我們把我們的加載器取名為my load,MLOAD。
VERSION:暫時為0x01版本
FLAG:表示是否有校驗,暫時bit0=1表示有CRC16校驗,其他bit保留擴展。
SECTIONS:表示后面段信息個數
LEN:表示整個HEAD長度,即16+sections*19
CHECK:表示后面sections*18字節的段信息的校驗,對于FLAG的bit1=1時使用CRC32
然后是sections個段信息
TYPE表示段的類型
暫時定義了以下類型
/*** \enum load_section_type_e* SECTION類型*/typedef enum{LOAD_SECTION_TYPE_DTB, /**< 設備樹段 */LOAD_SECTION_TYPE_KERNEL, /**< 內核鏡像段 */LOAD_SECTION_TYPE_ROOTFS, /**< 根文件系統段 */LOAD_SECTION_TYPE_USER, /**< 保留段,可以包括多個段,用戶段內自行去區分 */LOAD_SECTION_TYPE_RVD, /**< 保留段 */} load_section_type_e;
SUBTYPE表示USER段時,用于區分不同的USER段。
FLAG表示標志信息用于表示是否有校驗,加密等標志 暫時bit1=1表示使用CRC32
Bit4=1表示需要加載
/*** \enum load_section_flag_e* SECTION標志類型*/typedef enum{LOAD_SECTION_FLAG_NONE = 0, /**< 無標志 */LOAD_SECTION_FLAG_CRC16 = 1, /**< CRC16校驗 */LOAD_SECTION_FLAG_CRC32 = 2, /**< CRC32校驗 */LOAD_SECTION_FLAG_NEEDLOAD = (1u<<4), /**< 表示是否需要加載 */} load_section_flag_e;
VMA即加載到SDRAM的運行地址
LMA即存儲中的偏移地址即加載地址
LEN是段有效長度字節數
我們之前已經具備了燒錄任意內容到FLASH的任意地址的的能力,所以現在只需要在PC上準備好HEAD和section內容,然后通過xmodem寫入即可。
也可以再添加更多的shell命令行,比如修改head信息等,后續再完善。為了方便還可以設計上位機的鏡像打包工具,即所有section和HEAD按照布局拼接成一個鏡像,這樣一次燒錄到SPIFLASH即可。
詳見load.h/load.c,設計考慮需要適配不同的存儲,所以對于底層的存儲讀寫接口采用動態配置方式。
typedef uint32_t (*load_mem_write_pf)(uint8_t* buffer, uint32_t addr, uint32_t len);typedef uint32_t (*load_mem_read_pf)(uint8_t* buffer, uint32_t addr, uint32_t len);/*** \fn load_mem_itf_set* 設置LOAD的存儲讀寫接口* \param[in] write_itf 寫接口* \param[in] read_itf 讀接口*/void load_mem_itf_set(load_mem_write_pf write_itf, load_mem_write_pf read_itf);void load_mem_itf_set(load_mem_write_pf write_itf, load_mem_write_pf read_itf){s_load_mem_write = write_itf;s_load_mem_read = read_itf;}
初始化時需要配置
/* SPIFLASH boot */load_mem_itf_set(flash_itf_write,flash_itf_read);load_load_sections();load_boot(); /* 此時如果有有效dtb和kernel加載則直接啟動 *//* @todo 其他boot *///load_mem_itf_set(xxx_write,xxx_read);//load_load_sections();//load_boot();
然后調用
load_load_sections();
加載段,該函數解析HEAD,然后遍歷sections,加載單個段
int load_load_sections(void){load_head_st hdr;load_section_info_st info;if((s_load_mem_write == 0) || (s_load_mem_read == 0)){return -1;}/* 先讀hdr部分 */if(LOAD_HDR_LEN != s_load_mem_read(s_load_mem_buffer,LOAD_MEM_ADDR,LOAD_HDR_LEN)){return -2;}/* 解析hdr部分 */hdr.len = 0;load_buffer2hdr(s_load_mem_buffer, &hdr);/* hdr部分不能超過最大值 */if(hdr.len > LOAD_HDR_INFO_MAX_LEN){return -3;}if((hdr.magic[0] != load_mem_magic[0]) ||(hdr.magic[1] != load_mem_magic[1]) ||(hdr.magic[2] != load_mem_magic[2]) ||(hdr.magic[3] != load_mem_magic[3]) ||(hdr.magic[4] != load_mem_magic[4])){return -4;}/* 讀info部分 */if((hdr.len-LOAD_HDR_LEN) != s_load_mem_read(s_load_mem_buffer+LOAD_HDR_LEN,LOAD_MEM_ADDR+LOAD_HDR_LEN,hdr.len-LOAD_HDR_LEN)){return -5;}/* 遍歷sections */uint8_t* p_info = s_load_mem_buffer + LOAD_HDR_LEN;for(int i=0; i<hdr.sectinos; i++){load_buffer2sectioninfo(p_info, &info);load_one_section(&info);}return 0;}
加載單個段,即從存儲加載數據到指定的運行地址即可,只有標志需要加載才會加載
int load_one_section(load_section_info_st* section_info){uint8_t* p_dst;if(section_info->flag & LOAD_SECTION_FLAG_NEEDLOAD){/* 加載 */p_dst = (uint8_t*)(section_info->vma);if(section_info->len != s_load_mem_read(p_dst, section_info->lma, section_info->len)){return -1;}/* 校驗 */if(section_info->flag & LOAD_SECTION_FLAG_CRC32){/* @todo */}if(section_info->type == LOAD_SECTION_TYPE_KERNEL){s_kernel_addr = section_info->vma;s_get_dtb_kernel_flag |= 0x01;}else if(section_info->type == LOAD_SECTION_TYPE_DTB){s_dtb_addr = section_info->vma;s_get_dtb_kernel_flag |= 0x02;}}return 0;}
上述加載過程會記錄dtb和kerne的地址
所以load_boot時即會跳到對應的地址運行
void load_boot(void){if(s_get_dtb_kernel_flag == 0x03){start_kernel(s_dtb_addr, s_kernel_addr);}}
如果外部存儲中加載系統失敗,則從默認的片內FLASH處運行,這樣還可以實現安全。
即片內FLASH保留一個最小版本系統,發布之后不再修改,能夠運行基本功能實現通訊,能夠升級外部存儲的系統等。
正常運行時加載外部存儲的系統運行,外部存儲系統有問題時運行片內系統,保證還能夠升級片外系統。
/* SPIFLASH boot */load_mem_itf_set(flash_itf_write,flash_itf_read);load_load_sections();load_boot(); /* 此時如果有有效dtb和kernel加載則直接啟動 *//* @todo 其他boot *///load_mem_itf_set(xxx_write,xxx_read);//load_load_sections();//load_boot();/* 如果沒有dtb和kernel加載則按照默認地址啟動 */start_kernel(DTB_ADDR, KERNEL_ADDR);
以上我們就實現了load加載器的功能,可以從存儲中解析HEAD加載鏡像運行。下一篇我們就進行實踐演示。