STM32實現跨bin文件調用函數(Firmware)

一、技術背景

以前我用過一款慶科的WiFi模組——EMW3162,它由一塊STM32F205RG芯片 + SDIO接口的射頻芯片組成,有趣的是官方將這顆STM32芯片內部Flash做了很多塊的劃分,如下圖所示。

EMW316x FLASH分配情況

可以看到1MB的Flash被分割成了5部分,分別是:

1. Bootloader,一段引導代碼,一般用於更新APP程序。

2. 信息區,存放OTA的一些信息和用戶參數。

3. 用戶應用區,也就是APP區,用戶可以二次開發後將代碼燒錄到此處。

4. OTA暫存區,接收OTA數據,接收完成後再複製到用戶應用區。

5. 射頻驅動區,用於存放SDIO射頻模組的驅動,供上層使用。

這樣劃分的好處是顯而意見的,廠家不用提供射頻驅動的源碼,甚至連LIB庫都不用提供,並且用戶在OTA時,僅需更新應用區的代碼,無疑加快了OTA的速度。缺點嘛也是有的,因爲每個區都要預留一些Flash空間,所以劃分得越細,浪費的空間也就越多。當然相比起優點,這點缺點還是可以接受的。下文提供了一種方法來實現這種應用,可能和慶科的實現方式不一樣,僅可作爲一種思路。

二、技術方案

以下將用戶業務層稱爲App,將驅動層稱爲Driver,我們要實現的是將Driver編譯成一個固定的bin文件,即Firmware.bin,讓App能夠跨bin文件調用Driver中的函數,而且要求Driver層的更新不能影響App的訪問。

我們知道,C語言中的函數名實際上是個地址,只要知道了函數的地址、函數的格式,就可以調用這個函數。所以關鍵點在於如何讓App知道Firmware.bin中各個函數的地址。以下提供了幾種不同的方案,爲了方便說明,下文所有所述內容均不包括Bootloader區、Param區,僅有App區和Driver區。

方案一:在Driver工程中,聲明一個結構體,結構體成員爲供App使用的函數指針,然後定義一個初始化函數,該函數用於完成上述結構體指針的初始化,然後將該函數放到某一約定好的地址,App需要根據該地址調用這個初始化函數,這樣App就獲得了所需的Driver中的函數指針。

方案二:將Driver中的對外函數地址按4字節對齊,順序地排列到Driver區的起始地址上,App層聲明一個結構體,結構體成員爲所需的函數指針,然後定義一個該結構體指針,強制指向Driver區的起始地址,因爲STM32中的函數指針是4字節的,所以這個結構體指針的成員實際已經指向了Driver層對應的函數。

由於方案二更簡潔,所以本文使用方案二來實現目的。

三、技術原理

3.1 核心思想

根據方案二所述,最大的難點在於如何將函數地址按順序排列到Driver區的起始地址上。事實上,答案就在ST提供的啓動文件裏,啓動文件爲中斷向量表分配了固定的Flash空間,我們完全可以仿照這一方法。對啓動文件不熟悉的讀者可以參考我的另一篇文章:STM32啓動流程詳解

下面的例子使用的是Keil MDK編譯平臺,芯片爲STM32F103RCT6,Flash和Ram分配如下:

Flash和Ram分配情況

 3.2 Driver工程代碼

以下是Driver工程仿照啓動文件編寫的彙編文件,用於將所需函數地址按序排列在起始地址上。 

                PRESERVE8
                THUMB


; Vector Table Mapped to Address 0 at Reset
                AREA    FIRMWARE, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     FirmwareInit             
                DCD     LED_ON
                DCD     LED_OFF
                DCD     uart_put_char
                DCD     delay_ms
                DCD 	GetCount

__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

                AREA    |.text|, CODE, READONLY
                EXPORT  FirmwareInit                [WEAK]
                EXPORT  LED_ON                      [WEAK]
                EXPORT  LED_OFF                     [WEAK]
                EXPORT	uart_put_char               [WEAK]
                EXPORT  delay_ms                    [WEAK]
                EXPORT  GetCount                    [WEAK]
					
FirmwareInit  	
LED_ON
LED_OFF
uart_put_char
delay_ms
GetCount
                B   .
                END

因爲分配了Flash和Ram地址,且上述彙編文件中定義了新的段:FIRMWARE段,Driver工程的分散加載文件也需要修改,以下是修改後的分散加載文件。對分散加載不夠了解的讀者,可以看我的另一篇文章:STM32鏈接腳本詳解

LR_IROM1 0x08030000 0x00010000  {    ; 加載時域
  ER_IROM1 0x08030000 0x00010000  {  ; 第一段運行時域
   *.o (FIRMWARE, +First)            ; FIRMWARE段最先編譯
   ;*(InRoot$$Sections)              ; 因爲FirmWare沒有__main,所以不需要這個段
   .ANY (+RO)
  }
  RW_IRAM1 0x20008000 0x00004000  {  ; 第二段運行時域
   .ANY (+RW +ZI)
  }
}

由於沒有__main函數幫忙重定位RW數據段和ZI數據段,所以Driver層還需要手動將Flash中的RW段複製到Ram對應的運行地址,並將Ram中對應的ZI段清零,代碼如下。

static void RW_And_ZI_Init (void)
{
    extern unsigned char Image$$ER_IROM1$$Limit;       // 獲取RW段在FLASH中的加載地址
    extern unsigned char Image$$RW_IRAM1$$Base;        // 獲取RW段在RAM中的運行地址    
    extern unsigned char Image$$RW_IRAM1$$RW$$Limit;   // 獲取RW段在RAM中的結束地址
    extern unsigned char Image$$RW_IRAM1$$ZI$$Limit;   // 獲取ZI段在RAM中的結束地址
    unsigned char * psrc, *pdst, *plimt;
		
    psrc  = (unsigned char *)&Image$$ER_IROM1$$Limit;
    pdst  = (unsigned char *)&Image$$RW_IRAM1$$Base;
    plimt = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
    while(pdst < plimt)     // 將FLASH中的RW段拷貝到RAM的RW段運行地址上
    {
        *pdst++ = *psrc++;
    }
 
    psrc  = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
    plimt = (unsigned char *)&Image$$RW_IRAM1$$ZI$$Limit;
    while(psrc < plimt)     // 將RAM中的ZI段清零
    {
        *psrc++ = 0;
    }
} 

然後將RW_And_ZI_Init函數再做一層封裝。以下程序同時初始化了一些外設,並提供了一個測試函數供App獲取Firmware中的全局變量。

/* 初始化Firmware */
void FirmwareInit(void)
{
    SystemInit();       // 初始化系統時鐘、中斷向量
    RW_And_ZI_Init();   // 初始化Firmware的RW段、ZI段
    delay_init();       // systick初始化
    uart_init(115200);  // 串口1初始化
    LED_Init();         // LED初始化
}

/* 返回Firmware中的一個全局變量並加1 */
unsigned int GetCount(void)
{
    return cnt++;
}

3.3 App工程代碼

#define FIRMWARE_ADDR	0x08030000       /* Firmware的起始地址 */

typedef struct FUN                       /* 聲明Firmware提供的函數類型 */
{                                        /* 注意函數的順序要和Firmware保持一致 */
    void (*FirmwareInit)(void);
    void (*LED_ON)(void);
    void (*LED_OFF)(void);
    int (*uart_put_char)(int ch);
    void (*delay_ms)(u16 nms);
    u32 (*GetCount)(void);
}FUNC_S;

/* 定義一個全局結構體指針,強制指向Firmware的起始地址 */
FUNC_S *gFunc = (FUNC_S*)(FIRMWARE_ADDR); 

int main(void)
{	
    gFunc->FirmwareInit();    /* 初始化Firmware */
    while(1)
    {
        printf("Cnt = %d\r\n",gFunc->GetCount());
        gFunc->LED_ON();
        gFunc->delay_ms(500);	
        gFunc->LED_OFF();
        gFunc->delay_ms(500);
    }
}

爲了方便說明,上述程序裁剪掉了printf的實現,並將結構體的聲明直接放在了c文件裏。整個工程僅需一個main.c和ST提供的啓動文件即可,連標準庫或HAL庫都不用添加,因爲底層的初始化已經在Firmware中完成了。App的分散加載文件也需要根據Flash和Ram的劃分做簡單的修改,這裏就不再贅述了。

四、總結

上述例子只是一個demo,實際要考慮的問題更多,例如哪些代碼存放在Firmware中,哪些代碼應該存放App中。上述例子直接將標準庫也放到了Firmware中,實際上會有一些問題,因爲沒有考慮到中斷向量的偏移,但可以事先在Firmware中寫好對應的中斷處理函數,然後在App的中斷服務函數中被調用。關於中斷的設想暫時沒有去實際驗證,但這樣有一個好處:如果有Boot工程,那麼Boot也可以調用Firmware的API,例如避免了Boot和App都存一份標準庫,大大節省空間。

這種開發方式將單片機開發拉向了Linux,分成了驅動開發和應用開發。在軟件上做好分層的規劃、使用低耦合的程序框架,才能寫出優秀的代碼。

發佈了7 篇原創文章 · 獲贊 3 · 訪問量 1324
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章