BootLoader设计
·
一、传输协议
从应用程序切换到BootLoader进行数据传输,常用的协议有Y - Modem协议,此外还有X - Modem协议、Z - Modem协议、UDS协议等,以下是对它们的介绍:
- Y - Modem协议:Y - Modem是基于Xmodem的增强型文件传输协议,适用于串行通信和嵌入式系统场景。它支持128字节(SOH模式)和1024字节(STX模式)的数据块大小,使用CRC - 16循环冗余校验,可一次性发送多个文件,支持文件名和文件大小元数据传递,并通过ACK/NAK机制实现自动重传,防止数据溢出。
- X - Modem协议:X - Modem是串行通信中的一种简单的异步文件传输协议。一个完整的数据包为132字节,其中包含128字节数据,数据包以固定长度的3个字节帧头开始,最后一个字节为校验和。传输过程由接收方启动,接收方正确接收到一个包的数据,发送控制字符ACK给发送方,若接收出错,发送NAK请求重发。
- Z - Modem协议:Z - Modem是对X - Modem的进一步改进,它只需要对损坏的块进行重发,其他正确的块不需要发送确认字节,减少了通信量,传输效率更高,并且可以更好地实现断开后恢复传输。
- UDS协议:UDS(ISO 14229)是一种诊断协议,通过该协议实现数据传输时,采用0x34 RequestDownload和0x36 TransferData服务进行数据传输。典型流程包括进入扩展会话模式、检查预编程条件、擦除目标存储区、分块传输固件数据、校验并激活新固件。
- CAN - Bootloader协议:在汽车电子中常用,基于CAN总线,支持多节点升级。例如ISO - 15765协议,具有错误处理和分段传输功能,通过CAN总线分块传输数据,常用于实现汽车电子设备的固件升级。
- USB - DFU协议:DFU(Device Firmware Update)由USB - IF定义,支持高速传输,设备可在运行时进入DFU模式。STM32等一些微控制器有内置DFU支持,通过USB接口进行固件升级,适合批量更新。
使用时,上位机需要通过串口发送符合Y-Modem协议的固件数据,常用的工具如TeraTerm、Xshell等都支持Y-Modem协议传输。实际应用中还需添加超时检测和异常处理,进一步提高可靠性。
二、设计方案
通过 Y-Modem 协议接收升级数据包的功能。
Y-Modem协议帧结构
- 起始字符:SOH(0x01,128字节数据) 或 STX(0x02,1024字节数据)
- 块编号:1字节(0x00-0xFF)
- 块编号反码:1字节(与块编号互补)
- 数据区:128字节或1024字节
- 校验:CRC16(2字节)或校验和(1字节,较少用)
- 结束帧:EOT(0x04)
- 通信流程
- 启动时发送’C’字符,表示准备接收并使用CRC16校验
- 接收第一帧(包编号0)解析文件名和文件大小
- 后续帧按编号顺序接收,验证包编号和CRC16
- 接收完成后处理EOT结束帧,确认传输完成
基于环形队列的BootLoader设计可以有效处理数据包的异步接收,提高升级过程的稳定性。下面是一个基于环形队列并通过Y-Modem 协议接收升级数据包制。
代码示例
- 基本分区规划
BootLoader区:0x08000000 - 0x08003FFF (16KB)
应用程序区:0x08004000 - 0x0800FFFF (48KB)
升级包缓冲区:0x08010000 - 0x08017FFF (32KB)
标志区:使用备份寄存器存储升级标志
// bootloader.h
#ifndef __BOOTLOADER_H
#define __BOOTLOADER_H
#include "stm32f10x.h"
// 分区地址定义
#define BOOTLOADER_ADDR 0x08000000
#define APP_ADDR 0x08004000
#define UPDATE_BUF_ADDR 0x08010000
#define UPDATE_FLAG_ADDR 0x08018000 // 使用最后一个扇区存储标志
// 环形队列定义
#define QUEUE_SIZE 1024
typedef struct {
uint8_t buffer[QUEUE_SIZE];
uint16_t head;
uint16_t tail;
uint16_t len;
} RingQueue;
// 升级状态定义
typedef enum {
UPDATE_IDLE = 0,
UPDATE_RECEIVING,
UPDATE_VERIFYING,
UPDATE_WRITING,
UPDATE_SUCCESS,
UPDATE_FAILED
} UpdateState;
// 函数声明
void Queue_Init(RingQueue *q);
uint8_t Queue_Put(RingQueue *q, uint8_t data);
uint8_t Queue_Get(RingQueue *q, uint8_t *data);
uint16_t Queue_GetLen(RingQueue *q);
void USART_Config(void);
void USART_SendByte(uint8_t data);
void USART1_IRQHandler(void);
void Flash_ErasePage(uint32_t addr);
uint8_t Flash_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len);
uint32_t CalculateCRC16(uint8_t *data, uint32_t len);
void YModem_Process(void);
void JumpToApp(void);
void UpdateProcess(void);
#endif
// bootloader.c
#include "bootloader.h"
#include "stm32f10x_flash.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include <string.h>
// Y-Modem协议相关定义
#define SOH 0x01 // 128字节数据帧
#define STX 0x02 // 1024字节数据帧
#define EOT 0x04 // 传输结束
#define ACK 0x06 // 确认
#define NAK 0x15 // 否定确认
#define CAN 0x18 // 取消传输
#define CRC16_FLAG 0x43 // 'C',表示使用CRC16校验
#define PACKET_128 128 // 128字节数据包
#define PACKET_1024 1024 // 1024字节数据包
RingQueue rxQueue;
UpdateState updateState = UPDATE_IDLE;
uint32_t fileSize = 0; // 固件文件大小
uint32_t receivedSize = 0; // 已接收大小
uint8_t packetNumber = 0; // 当前包编号
// 环形队列初始化
void Queue_Init(RingQueue *q) {
q->head = 0;
q->tail = 0;
q->len = 0;
}
// 入队操作
uint8_t Queue_Put(RingQueue *q, uint8_t data) {
if (q->len >= QUEUE_SIZE) {
return 0; // 队列满
}
q->buffer[q->tail] = data;
q->tail = (q->tail + 1) % QUEUE_SIZE;
q->len++;
return 1;
}
// 出队操作
uint8_t Queue_Get(RingQueue *q, uint8_t *data) {
if (q->len == 0) {
return 0; // 队列空
}
*data = q->buffer[q->head];
q->head = (q->head + 1) % QUEUE_SIZE;
q->len--;
return 1;
}
// 获取队列长度
uint16_t Queue_GetLen(RingQueue *q) {
return q->len;
}
// USART配置 - 用于接收升级数据
void USART_Config(void) {
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置RX引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 配置中断优先级
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能USART
USART_Cmd(USART1, ENABLE);
}
// USART发送函数
void USART_SendByte(uint8_t data) {
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, data);
}
// USART接收中断服务程序
void USART1_IRQHandler(void) {
uint8_t data;
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
data = USART_ReceiveData(USART1);
Queue_Put(&rxQueue, data); // 将接收到的数据放入环形队列
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
// 擦除Flash页
void Flash_ErasePage(uint32_t addr) {
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
FLASH_ErasePage(addr);
FLASH_Lock();
}
// 写入数据到Flash
uint8_t Flash_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len) {
uint32_t i;
if ((addr % 4) != 0) return 0; // 地址必须是4字节对齐
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
for (i = 0; i < len; i += 4) {
// 处理不足4字节的情况
uint32_t data = 0;
memcpy(&data, &buf[i], (len - i) >= 4 ? 4 : (len - i));
if (FLASH_ProgramWord(addr + i, data) != FLASH_COMPLETE) {
FLASH_Lock();
return 0; // 写入失败
}
}
FLASH_Lock();
return 1; // 写入成功
}
// 计算CRC16校验
uint16_t CalculateCRC16(uint8_t *data, uint32_t len) {
uint16_t crc = 0;
uint32_t i, j;
for (i = 0; i < len; i++) {
crc ^= ((uint16_t)data[i] << 8);
for (j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
return crc;
}
// 跳转到应用程序
void JumpToApp(void) {
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
uint32_t JumpAddress;
// 检查应用程序是否存在
if (((*(__IO uint32_t*)APP_ADDR) & 0x2FFE0000) == 0x20000000) {
// 设置栈顶指针
JumpAddress = *(__IO uint32_t*)(APP_ADDR + 4);
Jump_To_Application = (pFunction)JumpAddress;
// 关闭所有中断
__disable_irq();
// 设置向量表偏移
SCB->VTOR = APP_ADDR;
// 初始化应用程序栈
__set_MSP(*(__IO uint32_t*)APP_ADDR);
// 跳转到应用程序
Jump_To_Application();
}
}
// Y-Modem协议解析
void YModem_Process(void) {
static uint8_t state = 0;
static uint8_t frameBuffer[PACKET_1024 + 5]; // 最大帧缓存
static uint16_t frameIndex = 0;
uint8_t data;
while (Queue_GetLen(&rxQueue) > 0) {
Queue_Get(&rxQueue, &data);
switch (state) {
// 状态0:等待开始,发送'C'请求使用CRC16
case 0:
USART_SendByte(CRC16_FLAG); // 发送'C',表示准备接收
state = 1;
frameIndex = 0;
break;
// 状态1:接收帧头(SOH/STX)
case 1:
if (data == SOH || data == STX) {
frameBuffer[frameIndex++] = data;
state = 2;
} else if (data == EOT) {
// 收到结束符,发送确认
USART_SendByte(ACK);
state = 5; // 进入完成状态
}
break;
// 状态2:接收帧内容
case 2:
frameBuffer[frameIndex++] = data;
// 判断帧类型,确定帧长度
uint16_t frameSize = (frameBuffer[0] == SOH) ?
(PACKET_128 + 4) : (PACKET_1024 + 4);
// 帧接收完成
if (frameIndex >= frameSize) {
state = 3;
}
break;
// 状态3:验证帧
case 3: {
uint8_t packetNum = frameBuffer[1];
uint8_t packetNumComplement = frameBuffer[2];
uint16_t dataSize = (frameBuffer[0] == SOH) ? PACKET_128 : PACKET_1024;
// 验证包编号
if (packetNum == (uint8_t)(~packetNumComplement) &&
packetNum == packetNumber) {
// 验证CRC16
uint16_t receivedCrc = (frameBuffer[frameIndex - 2] << 8) |
frameBuffer[frameIndex - 1];
uint16_t calculatedCrc = CalculateCRC16(&frameBuffer[3], dataSize);
if (receivedCrc == calculatedCrc) {
USART_SendByte(ACK); // 确认接收成功
// 处理第一帧(包含文件名和大小)
if (packetNumber == 0) {
char *fileName = (char*)&frameBuffer[3];
char *sizeStr = strchr(fileName, ',') + 1;
fileSize = atol(sizeStr);
// 擦除应用程序区
Flash_ErasePage(APP_ADDR);
packetNumber++;
} else {
// 写入固件数据(处理实际有效数据)
uint32_t writeSize = (receivedSize + dataSize > fileSize) ?
(fileSize - receivedSize) : dataSize;
Flash_WriteBuffer(APP_ADDR + receivedSize,
&frameBuffer[3], writeSize);
receivedSize += writeSize;
// 检查是否接收完成
if (receivedSize >= fileSize) {
state = 4; // 等待结束帧
}
packetNumber++;
}
} else {
USART_SendByte(NAK); // CRC错误,请求重发
}
} else {
USART_SendByte(NAK); // 包编号错误,请求重发
}
frameIndex = 0;
state = 1; // 准备接收下一帧
break;
}
// 状态4:等待结束帧
case 4:
if (data == EOT) {
USART_SendByte(ACK); // 确认收到结束帧
state = 5;
}
break;
// 状态5:传输完成
case 5:
updateState = UPDATE_SUCCESS;
break;
}
}
}
// 升级处理流程
void UpdateProcess(void) {
switch (updateState) {
case UPDATE_IDLE:
// 检查升级标志(同之前逻辑)
if (*(uint32_t*)UPDATE_FLAG_ADDR == 0xAAAAAAAA) {
updateState = UPDATE_RECEIVING;
*(uint32_t*)UPDATE_FLAG_ADDR = 0x00000000; // 清除标志
fileSize = 0;
receivedSize = 0;
packetNumber = 0;
} else {
JumpToApp();
}
break;
case UPDATE_RECEIVING:
YModem_Process(); // 运行Y-Modem协议处理
break;
case UPDATE_SUCCESS:
// 升级成功,跳转到新应用
JumpToApp();
break;
case UPDATE_FAILED:
// 升级失败,保留原应用
*(uint32_t*)UPDATE_FLAG_ADDR = 0x00000000;
JumpToApp();
break;
}
}
// 主函数
int main(void) {
// 初始化系统时钟
SystemInit();
// 初始化环形队列
Queue_Init(&rxQueue);
// 初始化USART
USART_Config();
// 使能全局中断
__enable_irq();
// 主循环
while (1) {
UpdateProcess();
// 喂狗,防止升级过程中死机
// IWDG_Configuration(); // 实际应用中需要配置独立看门狗
}
}
设计要点
- 分区保护:
- BootLoader区域设置为写保护,防止被意外擦除或修改
- 应用程序和升级缓冲区严格分离
- 数据校验:
- 所有接收的升级数据进行CRC校验
- 写入Flash后再次读取验证,确保数据正确性
- 升级失败处理:
- 升级失败时自动清除升级标志,启动原有应用程序
- 关键步骤添加超时检测,防止无限等待
- 看门狗保护:
- 配置独立看门狗,在升级过程中定期喂狗
- 任何步骤卡住都会触发复位,回到BootLoader
- 升级标志安全处理:
- 使用特定魔术字作为升级标志
- 标志写入时采用原子操作,防止断电导致标志错误
关键点解析
1. 函数指针定义
typedef void (*pFunction)(void); // 定义一个无参数、无返回值的函数指针类型
pFunction Jump_To_Application; // 声明该类型的函数指针,用于指向应用程序入口
- 在 ARM 架构中,程序的启动本质上是跳转到某个函数地址执行,这里用函数指针来实现地址跳转。
2. 应用程序存在性检查
if (((*(__IO uint32_t*)APP_ADDR) & 0x2FFE0000) == 0x20000000) { ... }
- 作用:判断应用程序区(
APP_ADDR起始地址)是否存在有效的应用程序。 - 细节解析:
APP_ADDR是应用程序在 Flash 中的起始地址(如示例中的0x08004000)。*(__IO uint32_t*)APP_ADDR:将APP_ADDR强制转换为 32 位指针,再取值。这是应用程序的栈顶地址(ARM 程序启动时,第一个数据必须是栈顶地址,由编译器写入)。0x2FFE0000是掩码,0x20000000是 STM32F1 系列的 SRAM 起始地址。
栈顶地址必须落在 SRAM 范围内(STM32F103 的 SRAM 地址是0x20000000 ~ 0x2000FFFF),因此通过判断“栈顶地址 & 掩码 == SRAM 起始地址”,可验证应用程序是否有效(避免跳转到空数据或错误地址)。
3. 获取应用程序入口地址并跳转
// 获取应用程序入口地址(复位向量)
JumpAddress = *(__IO uint32_t*)(APP_ADDR + 4);
Jump_To_Application = (pFunction)JumpAddress;
- ARM 中断向量表规则:Flash 起始地址开始的前 4 字节是栈顶地址,接下来的 4 字节是复位向量(即程序入口地址,应用程序的
main函数最终会映射到这里)。 - 因此,
APP_ADDR + 4指向的是应用程序的入口地址,将其赋值给函数指针,即可通过指针调用跳转到应用程序。
4. 跳转前的关键配置
__disable_irq(); // 关闭所有中断
- 防止跳转过程中被 BootLoader 的中断干扰(应用程序可能有自己的中断配置)。
SCB->VTOR = APP_ADDR; // 设置中断向量表偏移
- 中断向量表:STM32 中断服务程序的地址表,默认位于 Flash 起始地址(
0x08000000,即 BootLoader 区)。 - 应用程序的中断向量表在自己的起始地址(
APP_ADDR),因此需要通过SCB->VTOR寄存器修改偏移,让 CPU 从应用程序区读取中断向量,否则应用程序中断会失效。
__set_MSP(*(__IO uint32_t*)APP_ADDR); // 初始化应用程序的栈顶指针
__set_MSP是 ARM 内核指令,用于设置主栈指针(Main Stack Pointer)。- 应用程序有自己的栈空间,需将栈顶指针更新为应用程序定义的栈顶(即
APP_ADDR处的值),否则应用程序运行时会因栈地址错误崩溃。
5. 最终跳转
Jump_To_Application(); // 执行函数指针,跳转到应用程序
- 此时 CPU 会跳转到应用程序的入口地址,开始执行应用程序代码(相当于应用程序的“复位启动”)。
6. 总结
这段代码的核心是遵循 ARM 程序启动规范:先验证应用程序有效性,再更新栈指针和中断向量表,最后通过函数指针跳转到应用程序入口。这是 BootLoader 与应用程序协作的关键环节,确保应用程序能正确启动并独立运行。
实际应用中,还需要设计详细的通信协议(如数据包格式、握手机制、重传机制等),并针对STM32F103R8T6的Flash特性进行优化,确保升级过程的可靠性。
三、应用程序
在STM32中,从应用程序(运行态)切换到BootLoader运行,通常需要通过软件触发复位并在复位后让BootLoader检测到特定标志,从而进入升级模式。
核心原理
- 应用程序在切换前,向备份寄存器(BKP) 或Flash特定地址写入一个“升级标志”(如魔术字
0xAAAAAAAA)。 - 应用程序通过软件触发芯片复位(如
NVIC_SystemReset())。 - 复位后,BootLoader首先运行,检测到“升级标志”后进入升级模式,否则直接跳转到应用程序。
1. 应用程序中触发切换到BootLoader的函数
#include "stm32f10x.h"
#include "stm32f10x_bkp.h"
#include "stm32f10x_rcc.h"
// 定义升级标志(存储在备份寄存器中,复位后不丢失)
#define UPDATE_FLAG 0xAAAAAAAA
#define BKP_DR_UPDATE_FLAG BKP_DR1 // 使用备份寄存器1存储标志
// 初始化备份寄存器(需要开启PWR和BKP时钟)
void BKP_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE); // 允许访问备份寄存器
BKP_DeInit(); // 复位备份寄存器
}
// 触发切换到BootLoader
void JumpToBootLoader(void) {
// 1. 写入升级标志到备份寄存器
BKP_WriteBackupRegister(BKP_DR_UPDATE_FLAG, UPDATE_FLAG);
// 2. 软件复位(复位后BootLoader会检测到标志)
NVIC_SystemReset();
}
2. BootLoader中检测标志并进入升级模式
在BootLoader的初始化阶段添加标志检测逻辑:
// BootLoader初始化函数
void BootLoader_Init(void) {
// 初始化备份寄存器(与应用程序中一致)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
// 检测升级标志
if (BKP_ReadBackupRegister(BKP_DR_UPDATE_FLAG) == UPDATE_FLAG) {
// 清除标志(避免下次复位再次进入升级模式)
BKP_WriteBackupRegister(BKP_DR_UPDATE_FLAG, 0x00000000);
// 进入升级模式(启动接收升级数据的逻辑)
EnterUpdateMode();
} else {
// 无升级标志,直接跳转到应用程序
JumpToApp();
}
}
3. 关键细节说明
- 备份寄存器的作用:
STM32的备份寄存器(BKP)在VDD电源断开后,可通过VBAT(备用电源)保持数据,因此复位后仍能读取到“升级标志”。这是实现软件切换的核心存储媒介。 - 软件复位的实现:
使用NVIC_SystemReset()函数触发芯片复位,该函数会执行以下操作:- 置位
SCB->AIRCR寄存器的复位位 - 芯片重启,从Flash起始地址(BootLoader区)开始执行
- 置位
- 标志的清除时机:
BootLoader检测到标志后必须立即清除,否则下次复位会再次进入升级模式,导致应用程序无法正常启动。 - 硬件触发作为备份:
为防止软件切换失败(如标志写入异常),通常会设计硬件触发方式(如特定引脚接低电平复位),作为防变砖的最后手段:// BootLoader中添加硬件检测 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) { // 检测到硬件触发信号(如PA0接低电平),进入升级模式 EnterUpdateMode(); }
流程总结
- 应用程序调用
JumpToBootLoader(),写入标志并复位。 - 芯片复位后,BootLoader先运行,检测备份寄存器中的标志。
- 若标志有效,BootLoader进入升级模式;否则跳转到应用程序。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)