STM32实现固件分区

一、技术背景

以前我用过一款庆科的WiFi模组——EMW3162,它由一块STM32F205RG芯片 + SDIO接口的射频芯片组成,有趣的是官方将这颗STM32芯片内部Flash做了很多块的划分,如下图所示。

STM32实现固件分区_第1张图片 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分配如下:

STM32实现固件分区_第2张图片 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,分成了驱动开发和应用开发。在软件上做好分层的规划、使用低耦合的程序框架,才能写出优秀的代码。

你可能感兴趣的:(STM32)