不用拆板!STM32串口无BOOT OTA保姆级教程:64KB Flash从0到1搞明白

你是不是也遇到过这种崩溃时刻?给STM32更固件时,得拆机器、找杜邦线、怼BOOT引脚,折腾半小时还可能因为手滑焊错线;要是设备已经装在天花板上、柜子里这种犄角旮旯,拆一次能累出一身汗?

今天咱就绕开这个麻烦——不用BOOTloader,直接用串口给STM32做OTA升级!就用常见的STM32F103C8T6,64KB Flash就能搞定,步骤拆解得明明白白,小白也能跟上,看完你说不定会惊呼:“原来OTA还能这么简单?”

一、先搞懂:STM32无BOOT OTA到底是咋回事?

OTA(Over-The-Air)说通俗点,就是让设备“自己在线装新程序”——不用拆机器、不用插下载器,靠串口收新固件,再自己写到芯片里替换老程序。核心就俩关键动作,少一个都不行:

  1. 收固件:通过串口把新程序“传”到STM32里;
  2. 写Flash:把收到的新固件安稳存到芯片自带的Flash里,最后让新程序“上岗”。

先明确咱的“硬件基础”:这次用的STM32F103C8T6,自带的Flash就64KB(相当于一个迷你U盘),而且Flash是“按页操作”的——每一页1KB(就像U盘里的小文件夹,擦除、写入都得按整页来,不能只改半页)。它的地址范围也得记牢:从0x08000000开始,到0x08010000结束,后面分区全靠这俩数。

二、Flash分区:给程序“分好房间”

OTA的核心是“不能让新程序和老程序打架”——总不能一边跑老程序,一边直接改老程序的存储区吧?所以得给Flash“分房间”:一个房间让老程序“干活”,一个房间暂存新程序。

咱这例子里,编译出来的APP程序才18KB(很小巧),所以干脆把64KB Flash“一分为二”,简单粗暴还不容易出错:

  • APP区(干活区):32KB,地址0x08000000 ~ 0x08008000,老程序就存在这儿,平时设备就靠它工作;
  • OTA区(暂存区):32KB,地址0x08008000 ~ 0x08010000,新固件先存在这儿“过渡”,等搬完再清空。

当然啦,要是你家程序更大(比如30KB),也能自己调分区——比如APP区分40KB,OTA区分24KB,只要不超出64KB总大小就行,灵活得很。

三、OTA核心逻辑:像“搬家”一样稳妥

给Flash分好区,下一步就是“怎么把新程序搬去干活区”。这里有个关键设计:在Flash最后一页(地址0x0800FC00)放个“小开关”(标志位),用来告诉STM32:“有没有新程序要更?现在要不要搬?”

整个流程就像“给房子换家具”,稳得很,连掉电都不怕:

  1. 传新固件:通过串口把新固件传到OTA暂存区(先把“家具”运到临时仓库);
  2. 开始搬家:把OTA区的新固件“搬”到APP区,同时把“小开关”拨到1(告诉芯片:“我正在搬家,别捣乱”);
  3. 收尾重启:搬完后清空OTA区(把临时仓库腾出来),再把“小开关”拨回0,最后重启芯片——新程序就正式“上岗”了!

哪怕中途突然掉电也不怕!芯片重启后会先查“小开关”:要是看到开关是1,就知道“上次没搬完”,接着把OTA区的新固件往APP区搬,搬完清仓库、拨开关、重启,照样能更成功,一点不翻车。

四、实操:写3个“Flash工具函数”

要实现上面的逻辑,得先有“操作Flash的工具”——就像用U盘得有“复制”“删除”功能一样。下面这3个函数,就是用来擦除、写入、读取Flash的,代码里的关键细节我都标成大白话了,跟着看就行。

1. 头文件:先定义“固定参数”(flash_on_chip.h)

先把Flash的地址、页大小这些“不会变的数”定义成宏,写代码时不用记一堆数字,不容易错:

#ifndef __FLASH_ON_CHIP_H
#define __FLASH_ON_CHIP_H

#include <stdio.h>
#include <string.h>
#include <stdint.h>

// APP区起始地址(干活区的大门)
#define FLASH_BASE_ADDR  (0x08000000UL)
// APP区结束地址(干活区的后门)
#define APP_LIMIT        (0x08008000UL)
// OTA区起始地址(暂存区的大门)
#define STAGING_BASE     (0x08008000UL)
// Flash总结束地址(整个Flash的后门)
#define FLASH_END_ADDR   (0x08010000UL)
// 单页Flash大小(1KB,擦写必须按整页来)
#define PAGE_SIZE        (1024U)
// 标志位地址(Flash最后一页,放“小开关”的地方)
#define Flage_PAGE_ADDR  (0x0800FC00UL)

// 函数声明(告诉编译器:我后面会实现这几个函数)
int flash_write(uint32_t addr, const uint8_t *data, uint32_t len);
int flash_erase(uint32_t addr, uint32_t len);
int flash_read(uint32_t addr, uint8_t *buf, uint32_t len);

#endif

2. 擦除函数:清空Flash(flash_erase)

擦除是写Flash的“前提”——STM32的Flash不能直接“覆盖写”,得先把要写的区域清空(就像写字前要先擦黑板)。这个函数负责清空指定地址的Flash:

#include "flash_on_chip.h"
#include "spi.h"

/**
 * @brief  清空指定地址的Flash(按页擦除)
 * @param  addr:起始地址(必须是1KB的倍数,不然擦不了,就像黑板擦不能只擦一半)
 * @param  len:要清空的字节长度(比如要清2KB,就填2048)
 * @retval 0:成功;-1:参数错(地址超范围/长度为0);-2:地址没对齐;-3:擦除失败
 */
int flash_erase(uint32_t addr, uint32_t len)
{
    // 长度为0?白忙活,直接返回错
    if (len == 0) return -1;
    // 地址超出Flash范围?相当于往空气里擦,错
    if (addr < FLASH_BASE_ADDR || (addr + len) > FLASH_END_ADDR) return -1;
    // 地址不是1KB的倍数?Flash不支持,错
    if (addr % PAGE_SIZE != 0) return -2;

    uint32_t end = addr + len;  // 要擦除的结束地址
    HAL_FLASH_Unlock();         // 解锁Flash(就像开黑板擦的锁,不然用不了)
    FLASH_EraseInitTypeDef er = {0};  // 擦除配置结构体
    uint32_t err = 0;           // 错误码

    // 计算要擦除的第一页和最后一页(Flash按页算,得知道从哪页擦到哪页)
    uint32_t first_page = (addr - FLASH_BASE_ADDR) / PAGE_SIZE;
    uint32_t last_page  = (end - FLASH_BASE_ADDR + PAGE_SIZE - 1) / PAGE_SIZE;

    // 一页一页擦(不能跳着擦,得按顺序来)
    for (uint32_t p = first_page; p < last_page; ++p)
    {
        er.TypeErase   = FLASH_TYPEERASE_PAGES;  // 擦除类型:按页擦
        er.PageAddress = FLASH_BASE_ADDR + p * PAGE_SIZE;  // 当前要擦的页地址
        er.NbPages     = 1;  // 每次擦1页

        // 要是擦除失败,先锁Flash,再返回错
        if (HAL_FLASHEx_Erase(&er, &err) != HAL_OK)
        {
            HAL_FLASH_Lock();
            return -3;
        }
    }

    HAL_FLASH_Lock();  // 擦完锁上Flash,防止误操作
    return 0;  // 全部擦完,成功!
}

3. 写入函数:把数据写到Flash(flash_write)

擦完Flash,就能把新固件写进去了。注意:STM32写Flash必须按“半字”(2字节)写,所以地址得是2的倍数,不然写不进去(就像往格子本里写字,得按格子写,不能跨格子):

/**
 * @brief  往Flash里写数据(按半字写,2字节为单位)
 * @param  addr:起始地址(必须是2的倍数,不然写不进去)
 * @param  data:要写的数据(新固件就存在这里面)
 * @param  len:数据长度(字节数,比如写4字节,就填4)
 * @retval 0:成功;-1:参数错(地址超范围);-2:地址没对齐;-3:写入失败
 */
int flash_write(uint32_t addr, const uint8_t *data, uint32_t len)
{
    // 长度为0?不用写,直接返回
    if (len == 0) return 0;
    // 地址超出Flash范围?错
    if (addr < FLASH_BASE_ADDR || (addr + len) > FLASH_END_ADDR) return -1;
    // 地址不是2的倍数?不能写,错
    if (addr % 2 != 0) return -2;

    HAL_FLASH_Unlock();  // 解锁Flash

    // 按2字节一组写(因为要按半字写)
    for (uint32_t i = 0; i < len; i += 2)
    {
        // 把两个字节拼成一个半字(比如data[0]是低8位,data[1]是高8位)
        // 要是最后只剩1个字节,就补0xFF(避免数据不全)
        uint16_t half_word = data[i] | ((i + 1 < len ? data[i + 1] : 0xFF) << 8);

        // 要是写入失败,先锁Flash,再返回错
        if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr + i, half_word) != HAL_OK)
        {
            HAL_FLASH_Lock();
            return -3;
        }
    }

    HAL_FLASH_Lock();  // 写完锁上Flash
    return 0;  // 全部写完,成功!
}

4. 读取函数:从Flash读数据(flash_read)

写完数据,得能读出来验证对不对——这个函数最简单,直接把Flash地址当成“数组”读就行:

/**
 * @brief  从Flash指定地址读数据
 * @param  addr:起始地址(只要在Flash范围内就行,不用对齐)
 * @param  buf:存数据的缓冲区(读出来的数据放这儿)
 * @param  len:要读的字节数(比如读3字节,就填3)
 * @retval 0:成功;-1:参数错(地址超范围)
 */
int flash_read(uint32_t addr, uint8_t *buf, uint32_t len)
{
    // 长度为0?不用读,直接返回
    if (len == 0) return 0;
    // 地址超出Flash范围?错
    if (addr < FLASH_BASE_ADDR || (addr + len) > FLASH_END_ADDR) return -1;

    // 把Flash地址转换成字节指针(相当于把Flash当成一个大数组)
    const uint8_t *flash_addr = (const uint8_t *)addr;

    // 一个字节一个字节读,存到缓冲区里
    for (uint32_t i = 0; i < len; i++)
    {
        buf[i] = flash_addr[i];
    }

    return 0;  // 读完,成功!
}

五、测试:先让串口和Flash“通个话”

写好函数,得先试试好不好使——咱用串口调试助手做个简单测试:

  1. 串口配置:波特率115200,校验位无,数据位8,停止位1(STM32串口初始化也按这个来);
  2. 发送数据:在调试助手里发“qwe”;
  3. 看接收结果:芯片收到后,会通过串口回复[INFO] UART HAL_UARTEx_RxEventCallback-82 uartl interrupt qwe

这说明啥?串口能正常收发,Flash的读写接口也能配合工作——OTA的“基础链路”通了,接下来就是把这些函数串起来,实现完整的OTA升级。

后续更精彩:这些玩法马上更!

这只是“STM32串口无BOOT OTA”的第一部分——后面还会讲:

  • 无BOOT OTA的完整功能实现(从发固件到重启升级,一步不落);
  • 带BOOTloader的串口OTA(适合对升级安全性要求更高的场景);
  • ESP32和STM32联动:用ESP32发串口数据,给STM32做远程OTA。

跟着一步步来,下次给STM32更固件,你再也不用拆机器、怼BOOT引脚了——坐在电脑前发个固件,设备自己就更完了,省心又省力!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐