利用现有API,如何实现从耳机端通过uart通信,获取MCU OTA升级的bin文件,拿到完整bin之后,校验ok,再升级。请给出程序设计思路,以及具体实现
1.设计思路
在应用固件中用 UART DMA + Idle 中断接收耳机发送的 OTA 数据流。
协议三段:META(固件大小+CRC32)、DATA(连续数据流)、END(结束)。MCU边接收边写入暂存区 0x10000,并同步计算 CRC32。
接收完整 size 后计算的 CRC32 与 META 提供一致则:
写 DFU 标记到 0x1E000(DFU_/OK__)
触发复位,Bootloader 启动后搬运到 0x2000 并清标记
不一致则擦除暂存区并返回 NACK,不升级。
2.实现要点
PCLK1=16 MHz 已确认;不影响本流程。
Flash 暂存区:0x10000,大小 56KB(与 Boot 搬运一致)。
DFU 标记地址:0x1E000。Boot 已实现搬运与校验。
使用 HAL_UARTEx_ReceiveToIdle_DMA 和 HAL_UARTEx_RxEventCallback 进行分包接收。
新增 OTA 接收与校验模块(CRC32)
c
#pragma once
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint32_t size; // 固件总长度(字节)
uint32_t version; // 可选版本
uint32_t crc32_storage; // 耳机端整体 CRC32
} ota_meta_t;
void ota_init(void);
void ota_on_uart_bytes(const uint8_t* data, uint16_t len); // 在 UART Rx 回调中调用
// 可选:外部查询状态
bool ota_is_in_progress(void);
bool ota_last_result_ok(void);
c
#include "ota_fw_update.h"
#include "stm32c0xx_hal.h"
#include "usart.h" // 使用 huart1
#include <string.h>
#define DFU_MARK_ADDR (0x1E000u)
#define STAGING_ADDR (0x10000u)
#define FLASH_PAGE_SIZE (0x800u) // 2KB 页
#define STAGING_SIZE (56*1024u) // 与 boot 搬运一致
// DFU 标记魔数
#define DFU_ ('D' + ('F' << 8) + ('U' << 16) + ('_' << 24))
#define OK__ ('O' + ('K' << 8) + ('_' << 16) + ('_' << 24))
typedef enum {
OTA_IDLE = 0,
OTA_WAIT_META,
OTA_RECV_DATA,
OTA_WAIT_END,
OTA_DONE,
OTA_FAIL
} ota_state_t;
static ota_state_t s_state = OTA_IDLE;
static ota_meta_t s_meta = {0};
static uint32_t s_crc = 0xFFFFFFFFu;
static uint32_t s_written = 0;
static uint32_t s_flash_addr = STAGING_ADDR;
static bool s_last_ok = false;
// 简易 CRC32 (Poly 0xEDB88320)
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, uint32_t len) {
crc = ~crc;
for (uint32_t i = 0; i < len; ++i) {
crc ^= data[i];
for (int j = 0; j < 8; ++j)
crc = (crc >> 1) ^ (0xEDB88320u & (-(int32_t)(crc & 1)));
}
return ~crc;
}
static void flash_erase_range(uint32_t addr, uint32_t bytes) {
FLASH_EraseInitTypeDef erase = {0};
uint32_t page_err = 0;
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.Page = addr / FLASH_PAGE_SIZE;
erase.NbPages = (bytes + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE;
HAL_FLASH_Unlock();
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
HAL_FLASHEx_Erase(&erase, &page_err);
HAL_FLASH_Lock();
}
static bool flash_write_chunk(const uint8_t *buf, uint32_t len) {
HAL_FLASH_Unlock();
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
uint32_t i = 0;
while (i + 8 <= len) {
uint64_t qw;
memcpy(&qw, buf + i, 8);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, s_flash_addr, qw) != HAL_OK) {
HAL_FLASH_Lock();
return false;
}
s_flash_addr += 8;
i += 8;
}
if (i < len) {
uint8_t tail[8] = {0};
memcpy(tail, buf + i, len - i);
uint64_t qw;
memcpy(&qw, tail, 8);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, s_flash_addr, qw) != HAL_OK) {
HAL_FLASH_Lock();
return false;
}
s_flash_addr += 8;
}
HAL_FLASH_Lock();
return true;
}
static void write_dfu_mark(void) {
// 擦除标记页并写 DFU_/OK__
FLASH_EraseInitTypeDef erase = {0};
uint32_t page_err = 0;
HAL_FLASH_Unlock();
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.Page = DFU_MARK_ADDR / FLASH_PAGE_SIZE;
erase.NbPages = 1;
HAL_FLASHEx_Erase(&erase, &page_err);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, DFU_MARK_ADDR, DFU_);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, DFU_MARK_ADDR + 4, OK__);
HAL_FLASH_Lock();
}
// 简单协议:
// META 帧: "FWUP" + size(4) + version(4) + crc32(4) 共 16 字节
// DATA 流: 紧随其后 size 字节原始数据(可分多次到达)
// END 帧: "FWED" 4 字节
static const uint8_t k_meta_magic[4] = {'F','W','U','P'};
static const uint8_t k_end_magic[4] = {'F','W','E','D'};
void ota_init(void) {
s_state = OTA_WAIT_META;
s_last_ok = false;
s_crc = 0xFFFFFFFFu;
s_written = 0;
s_flash_addr = STAGING_ADDR;
// 擦除暂存区(56KB)
flash_erase_range(STAGING_ADDR, STAGING_SIZE);
}
bool ota_is_in_progress(void) { return s_state == OTA_RECV_DATA || s_state == OTA_WAIT_META || s_state == OTA_WAIT_END; }
bool ota_last_result_ok(void) { return s_last_ok; }
// 在 UART Rx 回调中将收到的字节流喂入此函数
void ota_on_uart_bytes(const uint8_t* data, uint16_t len) {
if (len == 0) return;
switch (s_state) {
case OTA_WAIT_META:
if (len < 16 || memcmp(data, k_meta_magic, 4) != 0) {
s_state = OTA_FAIL;
return;
}
memcpy(&s_meta.size, data + 4, 4);
memcpy(&s_meta.version, data + 8, 4);
memcpy(&s_meta.crc32_storage, data + 12, 4);
if (s_meta.size == 0 || s_meta.size > STAGING_SIZE) {
s_state = OTA_FAIL;
return;
}
s_state = OTA_RECV_DATA;
break;
case OTA_RECV_DATA: {
// 分批写入并更新 CRC
s_crc = crc32_update(s_crc, data, len);
s_written += len;
if (!flash_write_chunk(data, len)) {
s_state = OTA_FAIL;
return;
}
if (s_written >= s_meta.size) {
s_state = OTA_WAIT_END; // 等待结束魔数
}
break;
}
case OTA_WAIT_END:
if (len >= 4 && memcmp(data, k_end_magic, 4) == 0) {
// 校验一致则标记并复位
if (s_written == s_meta.size && s_crc == s_meta.crc32_storage) {
write_dfu_mark();
s_last_ok = true;
s_state = OTA_DONE;
// 告知耳机 ACK
const uint8_t ack[4] = {'O','T','A','K'};
HAL_UART_Transmit(&huart1, (uint8_t*)ack, sizeof(ack), 50);
// 复位进入 Boot 搬运
NVIC_SystemReset();
} else {
s_last_ok = false;
s_state = OTA_FAIL;
const uint8_t nack[4] = {'O','T','A','N'};
HAL_UART_Transmit(&huart1, (uint8_t*)nack, sizeof(nack), 50);
// 清理暂存区
flash_erase_range(STAGING_ADDR, STAGING_SIZE);
}
}
break;
default:
break;
}
}
2.启用 UART DMA + Idle 回调,转发到 OTA 模块
使用现有 HAL API(usart.c 中的 huart1),在初始化后启动空闲接收;在 HAL_UARTEx_RxEventCallback 中将数据喂给 ota_on_uart_bytes。
c
// ...existing code...
#include "ota_fw_update.h"
// ...existing code...
// 简易接收缓冲
static uint8_t s_uart_rx_buf[256];
void USART1_OTA_Start(void)
{
// 启动 UART1 DMA Idle 接收(CubeMX 要确保 DMA 已配置)
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, s_uart_rx_buf, sizeof(s_uart_rx_buf));
// 禁用半传输中断,减少回调次数
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
}
// UART Idle 回调:把接收到的有效字节喂入 OTA 状态机
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1 && Size > 0) {
ota_on_uart_bytes(s_uart_rx_buf, Size);
// 继续接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, s_uart_rx_buf, sizeof(s_uart_rx_buf));
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
}
}
// ...existing code...
3.在应用入口处初始化 OTA 接收
在 System 初始化完成后调用 ota_init() 和 USART1_OTA_Start()。你可以在主循环或任务中根据条件(如耳机指令)再启动 OTA;下面示例直接启用。
c
// ...existing code...
#include "ota_fw_update.h"
// ...existing code...
int main(void)
{
HAL_Init();
SystemClock_Config();
// ...existing code...
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 启动 OTA 会话(也可根据耳机侧协商后再调用)
ota_init();
extern void USART1_OTA_Start(void);
USART1_OTA_Start();
// ...existing code...
while (1) {
// 你的应用逻辑
}
}
// ...existing code...
交互与异常处理
耳机侧发送顺序:META(16B) → DATA(size B,可分包) → END(4B)。
MCU 每帧可回发简单 ACK/NACK(示例里在 END 时回发)。
若 UART 中途断开或 size 超出 56KB,状态机进入 FAIL 并擦除暂存区。
成功后写 DFU 标记并复位,Boot 会把 0x10000 搬到 0x2000(你的 boot 已实现:擦 28 页、拷贝 56KB、验证后清标记)。
可选增强
将 CRC32 改为 SHA‑256(更强一致性保障)。
为 DATA 分包加序号与重传机制(提升鲁棒性)。
OTA 过程中暂停其他外设操作,确保 Flash 编程不被打断。