Bootstrap

STM32 BootLoader 刷新项目 (十一) Flash写操作-命令0x57

STM32 BootLoader 刷新项目 (十一) Flash写操作-命令0x57

1. 引言

嵌入式系统中,BootLoader 是设备启动的第一部分代码,负责硬件初始化和主程序加载。在 STM32F407 中,BootLoader 的另一重要功能是支持应用程序的在线升级,这需要用到 Flash 写操作。本文详细介绍如何实现这一功能。

1.1 Flash Write命令的应用场景

在嵌入式设备中,固件更新和管理是实现产品生命周期内功能扩展与问题修复的重要手段,而 BootLoader 的 Flash Write 命令就是固件更新流程的核心之一。

  1. 固件在线升级
    BootLoader 的主要功能是加载主程序,但为了支持在线升级,它需要实现对 Flash 的擦除和写入功能。通过网络(如 WiFi、以太网)或串行接口(如 UART、CAN),将新固件下载到设备中并写入 Flash,完成程序的更新。

  2. 分区存储管理
    在复杂的应用中,可能会使用 Flash 的不同分区存储多个固件版本或配置文件。Flash Write 命令可用于将新版本的固件写入备用分区,并在验证后切换到新的固件运行。

  3. 设备参数或日志存储
    除了固件,Flash 写命令还可用于存储一些需要长期保存的数据,如设备参数、运行日志等。相比 EEPROM,Flash 提供更大的存储空间和更快的读写速度。


1.2 Flash Write命令的作用

Flash Write 命令作为 BootLoader 的核心功能模块,直接决定了系统的升级能力和稳定性,其作用主要包括以下几点:

  1. 支持固件安全升级
    通过 Flash Write 命令,新的固件数据可以被可靠地写入到指定的 Flash 区域,完成固件升级任务。

  2. 提高系统稳定性和安全性
    在升级过程中,Flash Write 命令结合校验机制可确保数据完整性和准确性,避免因写入错误导致设备崩溃。

  3. 实现高效的存储操作
    Flash Write 命令封装了 Flash 写操作的底层细节,让开发者无需关注复杂的硬件接口配置,只需调用接口完成数据存储。

  4. 与擦除操作结合使用
    Flash 写命令通常和擦除命令配合工作,确保写入新数据前清空目标区域,避免残留数据干扰。


2. STM32F407 Flash 简介

2.1 Flash介绍

STM32F407 芯片采用 Cortex-M4 内核,内置 Flash 存储用于程序和数据存储。STM32F407 的 Flash 大小为 512KB,分为若干个扇区(Sector),每个扇区的大小并不相同:

  • 扇区 0 到 3:16KB 每扇区
  • 扇区 4:64KB
  • 扇区 5 到 11:128KB 每扇区
扇区编号起始地址大小
00x0800000016 KB
10x0800400016 KB
20x0800800016 KB
30x0800C00016 KB
40x0801000064 KB
5-110x08020000 开始每扇区128 KB

在进行 Flash 写入时,需注意:

分区结构
Flash 存储器分为多个扇区,每个扇区可以独立擦除和写入,大小从 16 KB 到 128 KB 不等。

支持字节、半字、字写入
Flash 写入操作的基本单位是 字(word, 32 bits),并且地址需要 4 字节对齐。

支持电压范围宽
Flash 支持的工作电压范围为 2.7V 至 3.6V。

操作速度
Flash 写操作较慢,通常需要几个微秒到几百微秒。

image-20241114070535836

2.2 BootLoader Flash划分

由下图可以看出本BootLoader的Flash的划分,其中划给BootLoader为32KB,从0x0800 0000-0x0800 7FFF,占用Sector 0-1两个段。App应用程序占用0x0800 8000-0x080F FFFF,总共10个Sector。

image-20241114070819942

下面是BootLoader的跳转过程,关于具体的跳转过程,可以参考上一篇文章: STM32 BootLoader 刷新项目 (九) 跳转指定地址-命令0x55

image-20241114230528904

image-20241114230647450

2.3 STM32F407 Flash 写入的原理

2.3.1 Flash 的存储机制

STM32F407 的 Flash 是基于浮栅晶体管(Floating Gate Transistor)技术的非易失性存储器。其基本存储原理如下:

  1. 写入数据
    写入操作通过向 Flash 单元中的浮栅施加高压,将电子注入其中,从而改变其电导状态。
  2. 擦除数据
    Flash 擦除是将目标扇区内的所有单元恢复到初始状态(全为 1),需要通过高电压释放浮栅中的电子。
  3. 对齐限制
    Flash 地址必须以 4 字节对齐进行写入,否则写入会失败。
2.3.2 Flash 写操作流程

STM32F407 的 Flash 写入操作由以下几个步骤组成:

  1. 解锁 Flash 控制器
    写操作前需要解锁 Flash 控制器,通过设置 KEY1KEY2 寄存器完成解锁。解锁后才能对 Flash 进行写入或擦除。
  2. 配置 Flash 操作类型
    STM32F4 提供了 HAL 库的函数,如 HAL_FLASH_Program,开发者可以选择写入的类型(例如 32 位字写入)。
  3. 执行写入操作
    将目标地址和要写入的数据传递给 Flash 控制器,控制器根据设定的地址和数据完成写入。
  4. 锁定 Flash 控制器
    为了避免意外修改 Flash 内容,写操作完成后需锁定 Flash 控制器。

3. 实现 Flash 写操作的Demo代码讲解

在 STM32F407 中,Flash 写入功能由其内部存储器控制器支持,开发者可利用 HAL 库实现简单高效的操作。以下是实现 Flash Write 的核心步骤及示例代码。

3.1 硬件与软件

  • STM32F407 开发板
  • STM32CubeIDE 或 Keil uVision
  • ST-Link 调试器

3.2 配置和解锁 Flash

在进行任何 Flash 写入操作前,需解锁 Flash 控制器,以允许写入。示例如下:

#include "stm32f4xx_hal.h"

// 解锁Flash控制器以允许写入操作
void Flash_Unlock(void) {
    if (HAL_FLASH_Unlock() != HAL_OK) {
        // 错误处理:解锁失败
    }
}

3.3 实现写入单个数据的函数

写入单个 uint32_t 数据到 Flash 指定地址:

HAL_StatusTypeDef Flash_WriteWord(uint32_t address, uint32_t data) {
    Flash_Unlock();  // 解锁Flash

    // 写入一个32位数据到指定地址
    HAL_StatusTypeDef status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);

    HAL_FLASH_Lock();  // 写入完成后立即上锁

    return status;  // 返回操作状态
}

3.4 实现写入多组数据的函数

实际应用中,往往需要一次性写入一组数据。以下是实现批量写入的代码:

HAL_StatusTypeDef Flash_WriteData(uint32_t startAddress, uint32_t *data, uint32_t length) {
    Flash_Unlock();  // 解锁Flash

    HAL_StatusTypeDef status = HAL_OK;

    // 循环写入数据
    for (uint32_t i = 0; i < length; i++) {
        status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, startAddress + (i * 4), data[i]);
        if (status != HAL_OK) {
            break;  // 如果写入失败,退出循环
        }
    }

    HAL_FLASH_Lock();  // 写入完成后立即上锁

    return status;  // 返回写入状态
}

3.5 Flash 写入前的擦除操作

在写入新数据前,需确保目标区域已擦除,避免数据冲突。以下是实现扇区擦除的代码:

HAL_StatusTypeDef Flash_EraseSector(uint32_t sector) {
    FLASH_EraseInitTypeDef eraseInitStruct;
    uint32_t sectorError;

    Flash_Unlock();  // 解锁Flash

    // 配置擦除参数
    eraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;  // 按扇区擦除
    eraseInitStruct.Sector = sector;                     // 目标扇区
    eraseInitStruct.NbSectors = 1;                       // 擦除数量
    eraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 电压范围:2.7V - 3.6V

    // 执行擦除操作
    HAL_StatusTypeDef status = HAL_FLASHEx_Erase(&eraseInitStruct, &sectorError);

    HAL_FLASH_Lock();  // 上锁Flash

    return status;  // 返回擦除状态
}

3.6 综合示例:实现 BootLoader 中的 Flash 写命令

以下是将上述功能整合的一个完整示例:

void Flash_WriteExample(void) {
    uint32_t testData[3] = {0x12345678, 0xABCDEF01, 0x98765432};  // 示例数据

    // 擦除目标扇区(扇区5)
    if (Flash_EraseSector(FLASH_SECTOR_5) == HAL_OK) {
        printf("Sector erased successfully.\n");
    }

    // 写入数据到目标地址
    if (Flash_WriteData(0x08020000, testData, 3) == HAL_OK) {
        printf("Data written successfully.\n");
    }
}

3.7 Write Flash的步骤总结

3.7.1 解锁 Flash

解锁 Flash 控制器的步骤:

  1. 写入解锁密钥 0x456701230xCDEF89AB 到 Flash 解锁寄存器(FLASH->KEYR)。
  2. 确保 Flash 未处于写保护状态。
3.7.2 擦除扇区

写入前需先擦除目标扇区。擦除过程如下:

  1. 配置擦除模式和目标扇区。
  2. 设置擦除启动位(FLASH_CR_STRT)。
  3. 等待擦除完成(检查 FLASH_SR_BSY 位)。
3.7.3 写入数据
  1. 将要写入的数据和目标地址传递给 HAL_FLASH_Program 函数。
  2. 写入完成后,Flash 控制器会自动校验数据完整性。
  3. 若写入失败,会通过状态寄存器返回错误码。
3.7.4 上锁 Flash

写入完成后,设置 FLASH_CR_LOCK 位以锁定 Flash 控制器。

4. 0x57命令介绍–Flash擦除

在本篇文章,我们的主要是介绍0x57的命令,这个命令主要是在BootLoader中写指定Flash Sector的命令。

通过上位机发送7+X+4 Byte的数据,其中第1 Byte为整个数据的长度,第2Byte为指令码,第3-6 Byte为要写入Flash的基地址,第7 Byte是写入数据X的长度,第8-(8+X) Byte为写入数据的值,9+X Byte为前7+X Byte的CRC校验值,上位机通过串口UART发送给下位机,下位机回复地址是否写入成功的标志。

image-20241118072458838

下面是发送命令过程中上位机与BootLoader之间的交互。

image-20241114230607647

5. Flash写-命令程序设计

以下是代码的详细解析和逐行注释,包括函数的含义和执行过程。主要描述了 BootLoader 中的 BL_MEM_WRITE 命令的实现细节,如何从主机接收命令并写入 Flash。


主函数:接收命令并处理

void bootloader_uart_read_data(void)
{
    uint8_t rcv_len=0;

    printmsg_Host("BL_DEBUG_MSG: Receive CMD\n\r"); // 向主机打印调试信息,表示已准备好接收命令
    while (1)
    {
        // 关闭指示灯,表示正在等待命令
        HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
        
        // 清空接收缓冲区,避免数据残留影响
        memset(bl_rx_buffer, 0, 200);

        // 读取命令包的第一个字节,这是命令包的长度字段
        HAL_UART_Receive(C_UART, bl_rx_buffer, 1, HAL_MAX_DELAY);

        // 提取长度字段
        rcv_len = bl_rx_buffer[0];

        // 根据长度字段,接收整个命令包
        HAL_UART_Receive(C_UART, &bl_rx_buffer[1], rcv_len, HAL_MAX_DELAY);

        // 根据命令包的第二个字节(命令代码),选择处理函数
        switch(bl_rx_buffer[1])
        {
            case BL_GET_VER: // 获取 BootLoader 版本号
                bootloader_handle_getver_cmd(bl_rx_buffer);
                break;

            case BL_GET_HELP: // 获取支持的命令列表
                bootloader_handle_gethelp_cmd(bl_rx_buffer);
                break;

            case BL_GET_CID: // 获取芯片 ID
                bootloader_handle_getcid_cmd(bl_rx_buffer);
                break;

            case BL_GET_RDP_STATUS: // 获取读保护状态
                bootloader_handle_getrdp_cmd(bl_rx_buffer);
                break;

            case BL_GO_TO_ADDR: // 跳转到指定地址执行代码
                bootloader_handle_go_cmd(bl_rx_buffer);
                break;

            case BL_FLASH_ERASE: // 擦除 Flash 指定区域
                bootloader_handle_flash_erase_cmd(bl_rx_buffer);
                break;

            case BL_MEM_WRITE: // 写入 Flash
                bootloader_handle_mem_write_cmd(bl_rx_buffer);
                break;

            default: // 无效命令处理
                printmsg("BL_DEBUG_MSG:Invalid command code received from host \n");
                break;
        }
    }
}

命令处理函数:BL_MEM_WRITE

void bootloader_handle_mem_write_cmd(uint8_t *pBuffer)
{
    uint8_t write_status = 0x00; // 写入状态初始化
    uint8_t payload_len = pBuffer[6]; // 从命令包中提取有效载荷长度
    uint32_t mem_address = *((uint32_t *)(&pBuffer[2])); // 提取目标内存地址

    printmsg("BL_DEBUG_MSG:bootloader_handle_mem_write_cmd\n");

    // 计算命令包的总长度
    uint32_t command_packet_len = bl_rx_buffer[0] + 1;

    // 从命令包末尾提取主机发送的 CRC32 校验值
    uint32_t host_crc = *((uint32_t *)(bl_rx_buffer + command_packet_len - 4));

    // 校验 CRC32 数据完整性
    if (!bootloader_verify_crc(&bl_rx_buffer[0], command_packet_len - 4, host_crc))
    {
        printmsg("BL_DEBUG_MSG:checksum success !!\n");

        // 校验通过后向主机发送 ACK 应答
        bootloader_send_ack(pBuffer[0], 1);

        printmsg("BL_DEBUG_MSG: mem write address : %#x\n", mem_address);

        // 验证目标内存地址是否合法
        if (verify_address(mem_address) == ADDR_VALID)
        {
            printmsg("BL_DEBUG_MSG: valid mem write address\n");

            // 关闭指示灯,表示开始写入
            HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);

            // 执行实际的写入操作
            write_status = execute_mem_write(&pBuffer[7], mem_address, payload_len);

            // 打开指示灯,表示写入完成
            HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);

            // 将写入状态反馈给主机
            bootloader_uart_write_data(&write_status, 1);
        }
        else
        {
            printmsg("BL_DEBUG_MSG: invalid mem write address\n");
            write_status = ADDR_INVALID; // 地址无效
            // 向主机反馈地址无效状态
            bootloader_uart_write_data(&write_status, 1);
        }
    }
    else
    {
        printmsg("BL_DEBUG_MSG:checksum fail !!\n");
        // 校验失败,发送 NACK 应答
        bootloader_send_nack();
    }
}

实际写入函数

uint8_t execute_mem_write(uint8_t *pBuffer, uint32_t mem_address, uint32_t len)
{
    uint8_t status = HAL_OK; // 写入状态初始化为成功

    // 解锁 Flash 模块,允许写入
    HAL_FLASH_Unlock();

    // 按字节逐个写入数据到指定地址
    for (uint32_t i = 0; i < len; i++)
    {
        // 写入 Flash 的核心函数,支持字节写入
        status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, mem_address + i, pBuffer[i]);
    }

    // 写入完成后锁定 Flash,防止误操作
    HAL_FLASH_Lock();

    return status; // 返回写入状态
}

流程说明:

  1. 使用 HAL_FLASH_Unlock 解锁 Flash 控制器,获取操作权限。
  2. 逐字节写入数据到指定地址。
  3. 写入完成后调用 HAL_FLASH_Lock 重新锁定控制器。

7. 实战演练

下面是上位机的命令菜单,通过在终端调用Python脚本,然后在终端输入下位机连接的串口号,即可进入命令界面,目前可支持如下命令:

image-20240713104433991

输入写入命令的操作,然后在输入写入App的基地址。

image-20241118074802777

写入过程中:

image-20241118074826562


写入完成

image-20241118074841971

在上述整个写入的过程,上位机和MCU之间一直发送数据,如下图所示,一直发送每一个地址要写入的数据。

image-20241118075028466

后面关于上位机的内容,我单独出一期进行讲解,这里主要讲解MCU侧的操作。

8. 总结

本文详细介绍了在 STM32F407 上实现 Flash 写入的完整步骤,包括解锁、写入单个数据、多组数据及扇区擦除的实现方法。通过上述代码和测试方法,您可以为系统增加可靠的固件升级能力。

欢迎留言交流!如有疑问或建议,请随时反馈。

9. 系列文章

STM32 BootLoader 刷新项目 (一) STM32CubeMX UART串口通信工程搭建

STM32 BootLoader 刷新项目 (二) 方案介绍

STM32 BootLoader 刷新项目 (三) 程序框架搭建及刷新演示

STM32 BootLoader 刷新项目 (四) 通信协议

STM32 BootLoader 刷新项目 (五) 获取软件版本号-命令0x51

STM32 BootLoader 刷新项目 (六) 获取帮助-命令0x52

STM32 BootLoader 刷新项目 (七) 获取芯片ID-0x53

STM32 BootLoader 刷新项目 (八) 读取Flash保护ROP-0x54

STM32 BootLoader 刷新项目 (九) 跳转指定地址-命令0x55

[STM32 BootLoader 刷新项目 (十) Flash擦除-命令0x56](STM32 BootLoader 刷新项目 (十) Flash擦除-命令0x56-CSDN博客)

;