文章目录
简介
本文基于 mOTA v2.0 进行协议移植与优化,主要实现以下目标:
- 突破传统 OTA 升级的协议限制,支持 UART、IIC、SPI、USB、CAN 等多种协议
- 对核心代码进行功能注释和优化
- 模块化重构QT固件发送器,实现任意单片机之间的互升级操作
硬件平台 :正点原子 STM32F103ZET6 最小系统板
工程代码 :「miniOTA教程文档工程.zip」
mOTA 仓库 :https://gitee.com/DinoHaw/mOTA
一、mOTA组件介绍
mOTA 是一款专为 32 位 MCU 开发的轻量级 OTA 组件,组件包含三部分:
- Bootloader:负责固件的下载、解析、解密、存储、更新等操作
- 固件打包器(Firmware_Packager):将 bin 文件打包成 fpk 固件包
- 固件发送器:基于不同协议的固件发送工具(如 YModem_Sender)
mOTA组件核心功能特性:
| 功能 | 说明 |
|---|---|
| 固件包完整性检查 | 自动检测固件 CRC 值,验证固件数据的准确性 |
| 固件加密 | 支持 AES256 加密算法,提高固件的安全性 |
| APP 完整性检查 | 支持在 APP 运行前进行完整性检查,确认可运行的固件通过数据校验 |
| 断电保护 | 更新过程中断电,上电后仍能确保有可用固件(需配置双分区或三分区) |
| 固件水印检查 | 检测固件包是否携带特殊水印,防止错误更新 |
| 固件自动更新 | 检测到新固件时自动开始更新 |
| 恢复出厂设置 | factory 分区存放稳定版固件,支持一键恢复 |
| 无须 deinit | 采用再入 bootloader 方式,免去外设 deinit 代码 |
| 功能可裁剪 | 通过 bootloader_config.h 实现功能裁剪和方案切换 |
| SPI Flash 支持 | 支持将固件存放至 SPI flash,使用 SFUD 驱动库 |
软件架构:
mOTA 采用分层架构设计,从底层到顶层依次为:硬件层、硬件抽象层、驱动层、数据传输层、协议析构层、应用层。

二、快速开始
说明 :后续的所有 demo 工程都基于正点原子的 STM32F103ZET6 最小系统板。

- 克隆仓库
bash
git clone https://gitee.com/DinoHaw/mOTA.git
- 打开
example/STM32F1/bootloader_ymodem工程,修改IROM1地址,编译、烧录。

- 打开
example/STM32F1/app_v1工程,修改IROM1地址,编译生成hex文件。

- 打开
mOTA\tools\firmware_packager\exe下的firmware-packager.exe文件,对app_v1.bin进行打包成app_v1.fpk文件。

- 打开
mOTA\tools\YModem_Sender\exe下的YModem_Sender.exe文件,对单片机进行OTA升级。

- 状态指示:
· 绿灯闪烁:bootloader等待上位机发送数据
· 红灯闪烁:APP正常运行
· 按下KEY0按键进入bootloader模式,等待上位机发送数据
三、移植mOTA
3.1 UART DMA接收数据
基础配置步骤:
- RCC 配置:选择外部高速时钟(HSE)
- SYS 配置:Debug 选择 Serial Wire
- USART1 配置 :
- 通信模式:异步(Asynchronous)
- 波特率:根据实际需求设置(如 115200)
- 使能中断:USART1 global interrupt
- DMA 配置 :
- 添加 USART1_RX 的 DMA 通道
- 模式:循环模式(Circular)
- 数据宽度:Byte
- 时钟配置:主频配置为 72MHz
参考文档 :STM32 HAL 库实现乒乓缓存加空闲中断的串口 DMA 收发机制
-
使用cubemx生成工程


-
配置RCC、SYS


-
配置debug串口,注意选择异步通信模式、波特率、中断


-
配置DMA接收,循环模式

-
修改时钟配置,主频72M

生成代码后,配置乒乓缓存实现
c
// 定义接收缓冲区
#define RX1_BUF_SIZE (200)
uint8_t USART1_Rx_buf[RX1_BUF_SIZE];
// 在main函数中调用一次DMA接收,可以放在MX_USART1_UART_Init中调用
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, USART1_Rx_buf, RX1_BUF_SIZE);
编写回调函数
c
/* USER CODE BEGIN 0 */
#define RX1_BUF_SIZE (200)
uint8_t USART1_Rx_buf[RX1_BUF_SIZE];
// 半满、全满、空闲中断均会调用此函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
static uint8_t Rx1_buf_pos;
static uint8_t Rx1_length;
if (huart == &huart1)
{
Rx1_length = Size - Rx1_buf_pos;
if (Rx1_length < RX1_BUF_SIZE)
{
HAL_UART_Transmit(&huart1, &USART1_Rx_buf[Rx1_buf_pos], Rx1_length, 0xFFFF);
// 存入fifo
}
Rx1_buf_pos += Rx1_length;
if (Rx1_buf_pos >= RX1_BUF_SIZE)
{
Rx1_buf_pos = 0;
}
// 空闲中断判断帧结束
if (huart1.RxEventType == HAL_UART_RXEVENT_IDLE)
{
}
}
}
/* USER CODE END 0 */
完成编译、烧录代码后,可以看到收发的数量均相等,说明DMA搬运接收的代码配置成功实现。

3.2 适配代码
准备工作
1. LED状态 :配置LED灯,表示运行状态。
低电平LED亮起,高电平熄灭,GPIO配置默认输出高电平。

2. ring buffer接收数据 :使用DMA搬运数据到ring buffer中,使用DMA目的是为了减少CPU占用,使用ring buffer目的是为了有效利用内存,下面使用lwrb实现ring buffer接收数据,详细用法可以参考官方文档。
添加源文件和头文件到工程中
c
Lib/lwrb/src/lwrb.c
Lib/lwrb/src/lwrb_ex.c
Lib/lwrb/include/lwrb/lwrb.h
注意:编译器需要选择6,不然会出现报错。

随后,添加lwrb初始化、读取、写入等函数。
c
#include "lwrb/lwrb.h"
// 定义缓冲区长度
#define LWRB_UART1_RX_LEN (1024 * 2)
// 2k字节
static uint8_t Rx1_data_buf[LWRB_UART1_RX_LEN];
// 句柄
static lwrb_t Rx1_lwrb_handle;
// 初始化
void lwrb_rx1_init(void)
{
if (!lwrb_init(&Rx1_lwrb_handle, Rx1_data_buf, LWRB_UART1_RX_LEN))
{
printf("lwrb_init failed\r\n");
}
}
// 写入数据
lwrb_sz_t lwrb_rx1_write(const void *data, lwrb_sz_t size)
{
return lwrb_write(&Rx1_lwrb_handle, data, size);
}
// 读取数据
lwrb_sz_t lwrb_rx1_read(void *data, lwrb_sz_t size)
{
return lwrb_read(&Rx1_lwrb_handle, data, size);
}
// 获取缓冲区中当前可用的字节数
lwrb_sz_t lwrb_rx1_full(void)
{
return lwrb_get_full(&Rx1_lwrb_handle);
}
修改HAL_UARTEx_RxEventCallback函数,将数据写入ringbuffer,添加读取结束标志位。
c
#define RX1_BUF_SIZE (200)
uint8_t USART1_Rx_buf[RX1_BUF_SIZE];
static volatile uint8_t uart1_frame_received_flag = 0;
// 在main函数中调用一次DMA接收,可以放在MX_USART1_UART_Init中调用
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, USART1_Rx_buf, RX1_BUF_SIZE);
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
static uint8_t Rx1_buf_pos;
static uint8_t Rx1_length;
// 串口1 DMA接收
if (huart == &huart1)
{
Rx1_length = Size - Rx1_buf_pos;
if (Rx1_length < RX1_BUF_SIZE)
{
// 将数据写入ring buffer
if (Rx1_length != lwrb_rx1_write(&USART1_Rx_buf[Rx1_buf_pos], Rx1_length))
{
printf("LWRB_UART1_RX overflow!\r\n");
}
}
Rx1_buf_pos += Rx1_length;
if (Rx1_buf_pos >= RX1_BUF_SIZE)
{
Rx1_buf_pos = 0;
}
// 空闲中断判断帧结束
if (huart1.RxEventType == HAL_UART_RXEVENT_IDLE)
{
uart1_frame_received_flag = 1;
}
}
}
uint8_t get_uartx_frame_received_flag(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
return uart1_frame_received_flag;
}
return 0;
}
void clear_uartx_frame_received_flag(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
uart1_frame_received_flag = 0;
}
}
在 main.c 中读取数据,并将读取到的数据输出。如果收发一致说明ring buffer已配置成功。

移植
- 复制
mOTA\source文件夹到miniOTA\Lib\mOTA下 - 添加源文件到工程中
- 参考:README.md
Bootloader/Core:
bash
添加 bootloader.c 进工程中,实现 bootloader 的核心功能。
添加 bootloader_port.c 进工程中,若此部分逻辑有不同,则按实际情况进行修改。bootloader 的非核心和可移植的部分,主要是通信和协议部分的代码。
添加 firmware_manage.c 进工程中,固件的管理接口层,提供了固件的所有操作接口。
添加 data_transfer.c 进工程中,数据传输层,对外提供数据发送和接收的接口。
添加 data_transfer_port.c 进工程并实现内部的公共函数,若与案例一致,则无需修改。数据传输层的移植位置,便于修改为其它通讯接口。
添加 protocol_parser.c 进工程并实现内部的公共函数,若与案例一致,则无需修改。协议析构层,实现协议的解包和封包。
Bootloader/Component:
bash
添加 crcLib.c 进工程中,crc校验库。
添加 fal_stm32f1_flash.c 进工程中,stm32f1的flash库。
添加 perf_counter.c 进工程中
添加 aes.c 进工程中,aes库。
添加 bsp_flash.c 进工程中,flash板级支持包。
添加 bsp_key.c 进工程中,key板级支持包。
添加 bsp_timer.c 进工程中,软件timer板级支持包。
源文件:

头文件路径声明:

- 解决编译错误:
如果遇到error: #20: identifier "FLASH_SECTOR_TOTAL" is undefined错误:
- 修改工程的 IRAM1、IRAM2 配置
- 确保 Flash 分区配置正确

修改IRAM1、IRAM2

如果没有 user.h 文件,则可以根据下面内容自行创建。
c
/**
* \file user.h
* \brief configuration of the user application
*/
/*
* Copyright (c) 2022 Dino Haw
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* This file is part of mOTA - The Over-The-Air technology component for MCU.
*
* Author: Dino Haw <347341799@qq.com>
* Change Logs:
* Version Date Author Notes
* v1.0 2022-11-23 Dino the first version
* v1.1 2022-12-04 Dino 增加 VERSION_WRITE_TO_APP
* v1.2 2023-12-10 Dino 1. 改名为 user
* 2. 剥离 bootloader 部分
*/
#ifndef __USER_H__
#define __USER_H__
/* 定义项 */
#define RTOS_USING_NONE 0 /* 不使用 RTOS */
#define RTOS_USING_RTTHREAD 1 /* RT-Thread */
#define RTOS_USING_UCOS 2 /* uC/OS */
/* 配置选项 */
#define ENABLE_ASSERT 0 /* 是否使能函数入口参数检查 */
#define ENABLE_DEBUG_PRINT 1 /* 是否使能调试信息打印 */
#define EANBLE_PRINTF_USING_RTT 0 /* BSP_Print 函数是否使用 SEGGER RTT 作为输出端口 */
#define USING_RTOS_TYPE RTOS_USING_NONE
#define SEGGER_RTT_PRINTF_TERMINAL 0 /* SEGGER RTT 的打印端口 */
#define MAX_NAME_LEN 8
#define SOC_SERIES_STM32F1
#endif
- 修改部分源码
在 main.h 中添加添加BSP_Printf()日志打印声明

在SysTick_Handler()函数中为软件定时器添加一个1ms的时钟基准。

删除bsp_uart.h文件和引用

在data_transfer_port.h中关闭断帧检测

在data_transfer_port.c中修改数据发送接口、帧结束检测接口。


在 data_transfer_port.c 中,注意需要删除部分内容


在 bootloader_port.c 中删除数据传输层初始化的函数。

在 bootloader_port.c 中添加代码逻辑,将数据长度、数据内容存入 _dev_rx_len 和 _dev_rx_buff 中。

编译验证
编译步骤:
- 检查文件完整性:确认所有源文件已添加到工程
- 配置头文件路径:确认 mOTA 头文件路径配置正确
- 解决编译错误:根据错误提示逐一解决
- 生成固件:编译通过后生成 hex 文件
验证方法:
上电后状态指示:
- 绿灯闪烁(300ms 间隔):bootloader 等待下载状态
- 红灯闪烁(100ms 间隔):OTA 更新成功,APP 正常运行
