在这里插入图片描述

一、串口升级原理与内存划分

1.1 串口升级的概念

串口升级(UART Firmware Update)是通过串口接收新固件数据,并将其写入 Flash,实现固件更新的方式。其有以下特点和作用:

  • 无需专用烧录器
  • 可以远程升级
  • 便于现场维护
  • 成本低(仅需串口连接)

1.2 Flash 分区(128KB)

典型的Flash分区布局:

Flash 内存布局(128KB):
┌─────────────────────────────────────────────────────────┐
│ 0x08000000 - 0x08003FFF (16KB)                          │
│ 引导程序区 (Bootloader)                                  │
│ - 串口升级程序                                           │
│ - 应用程序跳转逻辑                                       │
│ - 不能被覆盖                                             │
├─────────────────────────────────────────────────────────┤
│ 0x08004000 - 0x0801FFFF (112KB)                         │
│ 应用程序区 (Application)                                 │
│ - 用户应用程序                                           │
│ - 可以被升级覆盖                                         │
│ - 起始地址:0x08004000                                   │
└─────────────────────────────────────────────────────────┘
分区 起始地址 大小 用途
引导区(Bootloader) 0x08000000 16KB (0x4000) whr-ees-bootloader-arm
应用区(Application) 0x08004000 112KB (0x1C000) whr-ees-bootloader-app

通过将内存分区,可以实现:

  • 引导程序区:永久驻留,负责升级和跳转
  • 应用程序区:可更新的用户程序

1.3 RAM 分区(8KB)

分区 起始地址 大小 用途
RAM 0x20000000 8KB (0x2000) 两个项目共享

1.4 典型的串口升级流程

有效
无效
系统启动
初始化串口/GPIO
检查应用程序是否有效?
延时1秒
进入引导程序模式
跳转到应用程序
等待串口命令
接收到升级命令?
进入升级模式
继续等待或超时跳转
擦除应用程序区Flash
擦除成功?
发送ACK确认
发送错误响应
等待接收固件数据
接收数据包
验证数据包CRC
校验通过?
写入Flash
发送错误响应
写入成功?
发送ACK
所有数据接收完?
接收结束命令
验证固件完整性
验证通过?
发送升级成功
发送验证失败
系统重启
应用程序运行

二、创建一个小的应用程序

本示例使用 凌动微 MM32SPIN0280,参数信息如下:

MM32SPIN0280 Series 

Subfamily: MM32SPIN0280

Frequency: 96MHz 
Memory: 128 kB Flash, 8 kB RAM
Communication: up to 3 x UART, 2 x SPI, 1 x I2C,2 x I2S,  
Analog : ADC, COMP,OPAMP
power saving mode:stop, sleep, and standby mode

作为原始程序和要升级的程序,只实现简单的点LED。v1版本开机闪一下,v2版本开机闪两下LED。

1. main.c

#include "hal_gpio.h"
#include "hal_rcc.h"
#include "hal_uart.h"
#include "mm32_device.h"

/**
 * @brief 简单的延时函数(毫秒级)
 * @param ms 延时的毫秒数
 */
static void Delay_Ms(u32 ms)
{
    /* 简单的循环延时,根据实际系统时钟调整 */
    volatile u32 i, j;
    for (i = 0; i < ms; i++) {
        for (j = 0; j < 8000; j++) {  // 根据系统时钟调整此值
            __ASM("nop");
        }
    }
}

/**
 * @brief 发送单个字符
 * @param ch 要发送的字符
 */
static void UART_SendChar(char ch)
{
    UART_SendData(UART1, (u16)ch);
    /* 等待发送完成 */
    while (RESET == UART_GetFlagStatus(UART1, UART_FLAG_TXEPT)) {
    }
}

/**
 * @brief 发送字符串
 * @param text 要发送的字符串指针
 */
static void UART_SendString(const char* text)
{
    if (text == NULL) {
        return;
    }
    /* 逐个发送字符 */
    while (*text != '\0') {
        UART_SendChar(*text++);
    }
}

/**
 * @brief 初始化串口
 * @note 使用 UART1,PB8(RX), PB9(TX),波特率 115200
 */
static void UART_Init_UART1(void)
{
    GPIO_InitTypeDef gpio;
    UART_InitTypeDef uart;

    /* 使能 GPIOB 和 UART1 时钟 */
    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2ENR_UART1, ENABLE);

    /* 配置 PB8 和 PB9 为 UART1 复用功能 */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_0);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_0);

    /* 配置 PB8 (RX) 为上拉输入 */
    GPIO_StructInit(&gpio);
    gpio.GPIO_Pin = GPIO_Pin_8;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    gpio.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &gpio);

    /* 配置 PB9 (TX) 为复用推挽输出 */
    GPIO_StructInit(&gpio);
    gpio.GPIO_Pin = GPIO_Pin_9;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    gpio.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &gpio);

    /* 配置 UART1 参数:115200, 8N1 */
    UART_StructInit(&uart);
    uart.BaudRate = 115200;
    uart.WordLength = UART_WordLength_8b;
    uart.StopBits = UART_StopBits_1;
    uart.Parity = UART_Parity_No;
    uart.HWFlowControl = UART_HWFlowControl_None;
    uart.Mode = UART_Mode_Rx | UART_Mode_Tx;
    UART_Init(UART1, &uart);

    /* 使能 UART1 */
    UART_Cmd(UART1, ENABLE);
}

int main(void)
{
    GPIO_InitTypeDef gpio;
    
    /* 初始化串口 */
    UART_Init_UART1();
    
    /* 启动时输出版本信息 */
    UART_SendString("v1\r\n");
    
    /* 使能 GPIO 时钟(LED连接在GPIOD Pin 8) */
    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOD, ENABLE);
    
    /* 配置 GPIO 为推挽输出 */
    GPIO_StructInit(&gpio);
    gpio.GPIO_Pin = GPIO_Pin_8;      // LED连接在PD8
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    gpio.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOD, &gpio);
    
    /* 打开LED(启动时闪烁一次) */
    GPIO_SetBits(GPIOD, GPIO_Pin_8);
    Delay_Ms(500);
    GPIO_ResetBits(GPIOD, GPIO_Pin_8);
    
    /* 主循环 - LED持续闪烁 */
    while (1) {
        /* 打开LED */
        GPIO_SetBits(GPIOD, GPIO_Pin_8);
        Delay_Ms(500);
        
        /* 关闭LED */
        GPIO_ResetBits(GPIOD, GPIO_Pin_8);
        Delay_Ms(500);
    }
}

2. APPDEMO.sct

sct文件主要作用是告诉链接器把哪些内容(代码、常量、变量、栈、堆……)放到芯片的哪一段内存里。

; ***********************************************************************
; *** Scatter-Loading Description File for Application ***
; ***********************************************************************
; 应用程序起始地址:0x08004000 (引导程序占用 0x08000000 ~ 0x08003FFF,共16KB)
; 应用程序可用空间:0x08004000 ~ 0x0801FFFF (112KB)

LR_IROM1 0x08004000 0x0001C000 {    ; Flash区域 (0x08004000开始,112KB可用)
  ER_IROM1 0x08004000 0x0001C000 {  ; 代码和数据起始地址:0x08004000
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
   *.o (CHECKSUM, +Last)
  }

  RW_IRAM1 0x20000000 0x00002000 {  ; RAM区域 (8KB)
   .ANY (+RW +ZI)
  }
}

Load Region

链接器会把所有将来要烧进 Flash 的内容安排到这 112 KB 空间里。

Execution Region 1 —— 代码/常量(在 Flash 里原地执行)

在ER_IROM1 大括号里是输入段选择模板,说明如下:

语法 含义
*.o (RESET, +First) 把所有目标文件里的 RESET 输入段放到最开头。RESET 通常就是中断向量表(__Vectors)。
*(InRoot$$Sections) 让链接器把 C/C++ 运行时必须的“根模块”加进来,比如 _main.o__scatter*.o 等,省得自己手动列。
.ANY (+RO) 剩下的所有“只读”段(代码、常量、字符串字面量…)按任意顺序填进来。
.ANY (+XO) __attribute__((execute_only)) 的代码段,防止被当成数据读取,提高安全性。
*.o (CHECKSUM, +Last) 把名为 CHECKSUM 的段放到末尾,常用于 IAP/Bootloader 做完整性校验。

Execution Region 2 —— RAM 区(变量/栈/堆)

RW_IRAM1 0x20000000 0x00002000 {

0x2000_0000 是 STM32 片内 SRAM 的起始地址。
0x00002000 = 8 KB,长度与注释一致。
.ANY (+RW +ZI)
+RW:已初始化的全局/静态变量(.data)。
+ZI:零初始化段(.bss、栈、堆)。
链接器会把这些变量在 Flash 里留一份初值镜像,启动时由 __scatterload 代码自动复制/清零到 RAM。

LR 决定烧录布局,ER 决定运行布局;地址相同就原地执行,地址不同链接器负责搬。

keil里与sct配置保持一致:

在这里插入图片描述

3. 编译bin设置

在keil里设置:

$K\ARM\ARMCLANG\bin\fromelf.exe --bin --output .\Bin\@L.bin !L

在这里插入图片描述

下一篇文章将先实现引导程序项目框架。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐