一、传输协议

从应用程序切换到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(); // 实际应用中需要配置独立看门狗
    }
}
设计要点
  1. 分区保护
    • BootLoader区域设置为写保护,防止被意外擦除或修改
    • 应用程序和升级缓冲区严格分离
  2. 数据校验
    • 所有接收的升级数据进行CRC校验
    • 写入Flash后再次读取验证,确保数据正确性
  3. 升级失败处理
    • 升级失败时自动清除升级标志,启动原有应用程序
    • 关键步骤添加超时检测,防止无限等待
  4. 看门狗保护
    • 配置独立看门狗,在升级过程中定期喂狗
    • 任何步骤卡住都会触发复位,回到BootLoader
  5. 升级标志安全处理
    • 使用特定魔术字作为升级标志
    • 标志写入时采用原子操作,防止断电导致标志错误
关键点解析
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检测到特定标志,从而进入升级模式。

核心原理
  1. 应用程序在切换前,向备份寄存器(BKP)Flash特定地址写入一个“升级标志”(如魔术字0xAAAAAAAA)。
  2. 应用程序通过软件触发芯片复位(如NVIC_SystemReset())。
  3. 复位后,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. 关键细节说明
  1. 备份寄存器的作用
    STM32的备份寄存器(BKP)在VDD电源断开后,可通过VBAT(备用电源)保持数据,因此复位后仍能读取到“升级标志”。这是实现软件切换的核心存储媒介。
  2. 软件复位的实现
    使用NVIC_SystemReset()函数触发芯片复位,该函数会执行以下操作:
    • 置位SCB->AIRCR寄存器的复位位
    • 芯片重启,从Flash起始地址(BootLoader区)开始执行
  3. 标志的清除时机
    BootLoader检测到标志后必须立即清除,否则下次复位会再次进入升级模式,导致应用程序无法正常启动。
  4. 硬件触发作为备份
    为防止软件切换失败(如标志写入异常),通常会设计硬件触发方式(如特定引脚接低电平复位),作为防变砖的最后手段:
    // BootLoader中添加硬件检测
    if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) {
        // 检测到硬件触发信号(如PA0接低电平),进入升级模式
        EnterUpdateMode();
    }
    
流程总结
  1. 应用程序调用JumpToBootLoader(),写入标志并复位。
  2. 芯片复位后,BootLoader先运行,检测备份寄存器中的标志。
  3. 若标志有效,BootLoader进入升级模式;否则跳转到应用程序。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐