不用拆板!STM32串口无BOOT OTA保姆级教程:64KB Flash从0到1搞明白
文章摘要: 本文详细介绍了如何在STM32F103C8T6(64KB Flash)上实现无需BOOT引脚的串口OTA升级方案。核心思路是将Flash分为APP区(运行程序)和OTA区(暂存新固件),通过标志位管理升级过程。文章从Flash分区、OTA逻辑设计到具体实现,详细讲解了擦除、写入Flash的关键函数,并强调了对齐和边界检查的重要性。该方法具有掉电保护功能,即使升级过程中断电也能恢复,为嵌
不用拆板!STM32串口无BOOT OTA保姆级教程:64KB Flash从0到1搞明白
你是不是也遇到过这种崩溃时刻?给STM32更固件时,得拆机器、找杜邦线、怼BOOT引脚,折腾半小时还可能因为手滑焊错线;要是设备已经装在天花板上、柜子里这种犄角旮旯,拆一次能累出一身汗?
今天咱就绕开这个麻烦——不用BOOTloader,直接用串口给STM32做OTA升级!就用常见的STM32F103C8T6,64KB Flash就能搞定,步骤拆解得明明白白,小白也能跟上,看完你说不定会惊呼:“原来OTA还能这么简单?”
一、先搞懂:STM32无BOOT OTA到底是咋回事?
OTA(Over-The-Air)说通俗点,就是让设备“自己在线装新程序”——不用拆机器、不用插下载器,靠串口收新固件,再自己写到芯片里替换老程序。核心就俩关键动作,少一个都不行:
- 收固件:通过串口把新程序“传”到STM32里;
- 写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:“有没有新程序要更?现在要不要搬?”
整个流程就像“给房子换家具”,稳得很,连掉电都不怕:
- 传新固件:通过串口把新固件传到OTA暂存区(先把“家具”运到临时仓库);
- 开始搬家:把OTA区的新固件“搬”到APP区,同时把“小开关”拨到1(告诉芯片:“我正在搬家,别捣乱”);
- 收尾重启:搬完后清空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“通个话”
写好函数,得先试试好不好使——咱用串口调试助手做个简单测试:
- 串口配置:波特率115200,校验位无,数据位8,停止位1(STM32串口初始化也按这个来);
- 发送数据:在调试助手里发“qwe”;
- 看接收结果:芯片收到后,会通过串口回复
[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引脚了——坐在电脑前发个固件,设备自己就更完了,省心又省力!
更多推荐



所有评论(0)