固件在线升级与版本管理-实战篇1:Flash读写实践01

笔者已经在前面的篇章中大致的描述了OTA整体的工作流和方案选型,从这一篇开始可以开始设计一些具体的、通用的技能实践了,在本片,我们先对STM32内部的Flash读写功能进行初步学习,我们通过学习ST官方的相关功能例程,了解Flash相关API的使用和调用逻辑。

本文仅针对STM32的HAL相关例程与API进行分析,标准库的例程和API暂不纳入说明范围。

一、资源获取

  1. 有句话说:毒蛇所在之处,三步之内,必有解药!要想学习ST官方的例程,还得去ST官方网站检索;ST官方提供了中文官网https://www.st.com.cn/,直接浏览器访问即可。

  2. 在检索栏搜索关键词“STM32F1”,回车。

  1. 找到自己使用的MCU型号,单击进入。

  1. 点击“工具与软件”,在置顶资源链接中即可找到我们需要的“STM32CubeF1”,内含底层库与在官方开发板上的运行示例,点击进入。

  1. 点击“获取软件”,下载相关源码程序。

  1. 点击“文件”,查看关联文档。

  1. 在“应用手册”可以找到整体的示例介绍;在“用户手册”有部分高级示例的源码与说明文档。我们在这里先仅关注整体的示例介绍,点击文档在线阅读或下载。

  1. 在文档前文简要介绍了一下示例存放路径、路径结构、存放内容,可以阅读了解。

  1. 在后面列出了提供的各个功能的例程,我们找到Flash相关项,可以看到在HAL库例程路径Example下,有2个Flash例程,分别用于Flash读写与开关Flash写保护功能;在右侧列出了在各个官方开发板下的支持情况,仅在NUCLEO-F103RB开发板有相关例程支持。

  1. 打开之前下载的源码程序压缩包,根据第7点提到的文档对例程路径的提示打开“Projects”,再打开拥有我们需要例程的开发板路径“STM32F103RB-Nucleo”,再打开HAL库例程路径“Examples”,最后打开“FLASH”,就能找到我们需要的ST官方功能例程了,大功告成!!!

二、代码研读

2.1 准备工作

  1. 打开官网下载源码程序压缩包内“Projects\STM32F103RB-Nucleo\Examples\FLASH\FLASH_EraseProgram”路径下的Flash读写例程。

  2. 笔者使用Keil MDK开发代码,进入“MDK-ARM”文件夹,双击打开工程文件。

  3. 打开“Options for Target…”。

4.在Target标签页,选择”Use default compiler version 6“。(工程中使用了需要v6编译器的库文件)

  1. 在C/C++(AC6)标签页,将Language C的C语言标准设置为c99,同时去掉Misc中的“-C99”(v6编译器不识别该参数语法)。

6.编译工程,使各个源代码所需的头文件关联到工程中,便于后面定义跳转和特定符号的全局搜索。

2.2 源码分析

  1. 在 Flash 存储器(如 NAND Flash 或 NOR Flash)中,其内部存储单元的组织结构从小到大依次为:页(Page) → 扇区(Sector)/块(Block)

  2. 页是 Flash 存储器中最小可操作的数据单元。主机对 Flash 的读取和写入(编程)操作,都必须以页为单位进行。典型的页大小通常为 4KB、8KB 或 16KB。

  3. 块是 Flash 存储器中最小可擦除的数据单元,由多个连续的页组成。由于 Flash 的物理特性,数据无法被单独擦除,必须将整个块内的数据全部擦除后才能重新写入。

  1. 本例程的主要实现均位于main.c中,笔者重点分析这部分。

  2. 定义自定义用户可编程Flash空间起始、结束地址,FLASH_PAGE_SIZE为HAL库系统宏,等于1页Flash的大小,工程指定不同的芯片时,该宏会切换为对应该型号的参数,在F103RB中,该宏的值为0x400U(1KB)。

  3. 也就是说,用户可写入Flash范围为48-127页(含127页本身,因此需要在FLASH_USER_END_ADDR增加1页的长度)。

/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Start @ of user Flash area(自定义用户可编程Flash空间起始地址)*/
/*ADDR_FLASH_PAGE_48:第48页起始地址((uint32_t)0x0800C000)*/
#define FLASH_USER_START_ADDR   ADDR_FLASH_PAGE_48 
​
/* End @ of user Flash area(自定义用户可编程Flash空间结束地址) */
/*ADDR_FLASH_PAGE_127:第127页起始地址((uint32_t)0x0801FC00)*/
#define FLASH_USER_END_ADDR     ADDR_FLASH_PAGE_127 + FLASH_PAGE_SIZE   
​
#define DATA_32                 ((uint32_t)0x12345678)  /*自定义写入内容*/
​
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
uint32_t Address = 0, PAGEError = 0;
__IO uint32_t data32 = 0 , MemoryProgramStatus = 0;
​
/*Variable used for Erase procedure*/
static FLASH_EraseInitTypeDef EraseInitStruct;  /*定义一个HAL库Flash擦除操作结构体对象*/
  1. 在main函数中,先进行基础初始化,这里不必多说。
int main(void)
{
  /* STM32F103xB HAL library initialization:
       - Configure the Flash prefetch
       - Systick timer is configured by default as source of time base, but user 
         can eventually implement his proper time base source (a general purpose 
         timer for example or other time source), keeping in mind that Time base 
         duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and 
         handled in milliseconds basis.
       - Set NVIC Group Priority to 4
       - Low Level Initialization
     */
  HAL_Init();               /*HAL库初始化(cubeMX可自动生成)*/
​
  /* Initialize LED2 */
  BSP_LED_Init(LED2);       /*此开发板专用LED初始化函数,这里无需关注*/
​
  /* Configure the system clock to 64 MHz */
  SystemClock_Config();     /*系统时钟初始化(cubeMX可自动生成)*/
  1. 正菜来了,之后解锁Flash,并自定义用户可编程Flash空间内部整体擦除。
  /* Unlock the Flash to enable the flash control register access(解锁Flash,便于后续对其进行控制和读写) *************/
  HAL_FLASH_Unlock();
​
  /* Erase the user Flash area
    (area defined by FLASH_USER_START_ADDR and FLASH_USER_END_ADDR) ***********/
​
  /* Fill EraseInit structure*/
  /*构造擦除操作结构体*/
  EraseInitStruct.TypeErase   = FLASH_TYPEERASE_PAGES;  /*擦除操作类型:页擦除*/
  EraseInitStruct.PageAddress = FLASH_USER_START_ADDR;  /*擦除起始地址*/
  EraseInitStruct.NbPages     = (FLASH_USER_END_ADDR - FLASH_USER_START_ADDR) / FLASH_PAGE_SIZE;    /*擦除页数*/
  1. 调用HAL_FLASHEx_Erase并传入构造完成的擦除操作结构体,即可执行擦除操作,在返回值中传递执行情况,程序需要进行校验,当传出异常值,闪烁板载LED灯作为提示。
  /*擦除Flash*/
  if (HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError) != HAL_OK)
  {
    /*
      Error occurred while page erase.(页擦除时出现错误,返回异常值)
      User can add here some code to deal with this error.
      PAGEError will contain the faulty page and then to know the code error on this page,
      user can call function 'HAL_FLASH_GetError()'
    */
    /* Infinite loop */
    /*通过循环闪烁板载LED提示*/
    while (1)
    {
      /* Make LED2 blink (100ms on, 2s off) to indicate error in Erase operation */
      BSP_LED_On(LED2);
      HAL_Delay(100);
      BSP_LED_Off(LED2);
      HAL_Delay(2000);
    }
  }
  1. 调用HAL_FLASH_Program并指定写入长度、写入地址、写入数据,即可执行数据操作,在返回值中传递执行情况,程序需要进行校验,当传出异常值,闪烁板载LED灯作为提示。

  /* Program the user Flash area word by word(对清空后的用户可编程Flash空间进行写入操作,每次写入一个字的数据(word-4字节))
    (area defined by FLASH_USER_START_ADDR and FLASH_USER_END_ADDR) ***********/
​
  Address = FLASH_USER_START_ADDR;  /*将地址初始化为用户可编程Flash空间起始地址,从此开始操作*/
​
  /*循环遍历整个用户可编程Flash空间*/
  while (Address < FLASH_USER_END_ADDR)
  {
    /*写入Flash,从Address指定地址,写入1个字大小的(word-4字节)数据DATA_32*/
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, DATA_32) == HAL_OK)
    {
      Address = Address + 4;    /*写入成功,地址向后移动1个字的长度*/
    }
    else
    {
      /* Error occurred while writing data in Flash memory.(Flash写入数据出现错误)
         User can add here some code to deal with this error */
      /*通过循环闪烁板载LED提示*/
      while (1)
      {
        /* Make LED2 blink (100ms on, 2s off) to indicate error in Write operation */
        BSP_LED_On(LED2);
        HAL_Delay(100);
        BSP_LED_Off(LED2);
        HAL_Delay(2000);
      }
    }
  }
​
  /* Lock the Flash to disable the flash control register access (recommended
     to protect the FLASH memory against possible unwanted operation)(完成写入操作,将Flash进行上锁保护) *********/
  HAL_FLASH_Lock();

  1. 最后需要读取Flash的数据,与写入数据对比进行校验,确认写入是否完全正常。读取Flash数据不需要专门的HAL库函数,直接将Flash地址(例如0x0800C000)强制转换为uint32_t *指针类型(这样可以保证读出来的数据长度刚好是一个字uint32_t),再通过*符号进行地址数据读取操作,即可得到对应地址Flash中存储的数据。

  /* Check if the programmed data is OK(对用户可编程Flash空间进行检验操作,将数据重新读取出来与写入数据匹配,确认是否成功)
      MemoryProgramStatus = 0: data programmed correctly
      MemoryProgramStatus != 0: number of words not programmed correctly ******/
  Address = FLASH_USER_START_ADDR;  /*将地址初始化为用户可编程Flash空间起始地址,从此开始操作*/
  MemoryProgramStatus = 0x0;        /*Flash读写内容不匹配计数器,此时初始化为0*/
​
  /*循环遍历整个用户可编程Flash空间*/
  while (Address < FLASH_USER_END_ADDR)
  {
    data32 = *(__IO uint32_t *)Address; /*将Flash地址强制转换为指针,并访问内部数据*/
​
    /*匹配读写数据,若不匹配则自增 Flash读写内容不匹配计数器*/
    if (data32 != DATA_32)
    {
      MemoryProgramStatus++;
    }
      
    Address = Address + 4;  /*校验成功,地址向后移动1个字的长度*/
  }
​
  /*Check if there is an issue to program data(校验 Flash读写内容不匹配计数器,观察是否完全匹配)*/
  if (MemoryProgramStatus == 0)
  {
    /* No error detected. Switch on LED2(完全匹配,写入成功,LED常亮)*/
    BSP_LED_On(LED2);
  }
  else
  {
    /* Error detected. LED2 will blink with 1s period(不完全匹配,写入失败) */
    /*通过循环闪烁板载LED提示*/
    while (1)
    {
      BSP_LED_On(LED2);
      HAL_Delay(1000);
      BSP_LED_Off(LED2);
      HAL_Delay(1000);
    }
  }
​
  /* Infinite loop (空循环,不执行内容)*/
  while (1)
  {
  }
}

2.3 补充说明

  1. 本节课在进行Flash的读写操作,有一个红线是绝不能踩的!我们知道,Flash本身是用于存放程序固件与只读数据区的,这部分存储空间不应该被用户随意的写入。一旦读写操作区域与程序固件与只读数据区,很可能会发生程序崩溃,出现HardFault错误。

  2. 反过来说,如果我们确定了某个区域要用于用户自定义读写,就应该在工程中声明,约束链接器在编译的过程中,不要向用户自定义读写区放入程序和制度数据区。

  3. 在本节学习的例程工程中,在这一点上不太严谨,我们打开“Options for Target…”。

  1. 在Target标签页,可以看到,该工程on-chip IROM1(片上ROM区)的起始地址为0x8000000,大小为0x200000,也就是**0x8000000(ADDR_FLASH_PAGE_0)–0x8020000(ADDR_FLASH_PAGE_127)**的范围。

  2. IROM就是工程向链接器声明的,可以放入代码和只读数据的区域,也就是说,**代码和只读数据区,理论上与用户自定义读写区存在重合!**这是一个隐患。

  1. 之所以本例程在实际运行时没有出现问题,是因为这个工程的固件极小,存放到Flash从0x8000000开始的空间中,实际大小还没有延展到用户自定义读写区。

  2. 我们在编译工程可以看到Keil输出内容如下。其中Code代表代码段;RO-data代表只读数据段;RW-data代表已初始化且初始值 ≠ 0的全局/静态变量(数据段);ZI-data代表零初始化(Zero-Initialized)及未显式初始化 的全局/静态变量。

  3. Flash中存储代码段和只读数据段,两个数据占用空间总和=4040+304=4344=0x10F8,因此Flash用于存放程序和只读数据的实际区域为0x8000000–0x80010F8,没有真正与用户读写区碰撞。

  1. 出于严谨,我们应该对向链接器声明可用的Flash空间作出限制,打开“Options for Target…”,在Target标签页,将工程on-chip IROM1(片上ROM区)的起始地址为0x8000000,大小为0xC000(0x800C000开始为用户读写区)。

  1. 这样,链接器就只会在这个区域存放程序和只读数据区。假设工程后面有更新,需要存放的程序和只读数据已经超过0xC000,链接器会报告错误。而不是静默的向后写入,导致在我们不知情的情况下,程序和只读数据侵入了用户读写区,造成意料之外的错误。
  1. Flash读写操作的代码笔者就带大家领教完了,似乎神秘而高大上的操作其实也没有大家想的困难,我们已经成功的迈出了第一步。

  2. 下一步,我们要将以上的代码移植到STM32核心板上,自己跑一遍。相关内容会在下一篇文章呈现,我们很快再见!

1 个赞