目录
程序升级的概念
1.OTA 和 IAP
OTA 是 Over-the-Air 的简写,即空中下载技术,通过移动通信网络(2G/3G/4G 或 Wifi)对设备终端上固件、数据及应用进行远程管理的技术。简单来说 OTA 技术实现分三步:首先将更新软件上传到 OTA 中心,然后 OTA 中心无线传输更新软件到设备端,最后设备端自动更新软件。
下图中,OTA 终端和 OTA 云端交互,下载到固件,然后烧录:
IAP 是“In Application Programming”的简写,就是用户程序运行时对 Flash 的某些区域进行烧写,可以写入新版本的软件、用户数据等。IAP 主要包括 BootLoader 和应用程序两部分:在升级时运行的是 Bootloader,它接收新版本的应用程序,烧写在 Flash 上。
要实现 OTA(就是通过网络升级软件),需要使用 IAP 技术。要使用 IAP,需要引入 Bootloader。
上电后,先运行的程序被称为 BootLoader。它的作用和工作流程如下:
① 判断是否需要升级固件,如果无需升级,就启动 Flash 另一个区域的应用程序
② 如果需要升级,通过 OTA 数据包交互协议,接收新版本的软件、烧录到 FLASH 上,然后设置升级标志位并重启
③ 重启后运行的仍然是 Bootloader,它根据标志位启动新版本的软件。
2.阿里 OTA 服务器简单体验
在阿里云物联网平台,可以购买 OTA 服务:
它的 OTA 升级流程如下:
由于要使用 MQTT 协议,所以我们暂时不深究 OTA 技术,本文讲解 IAP 并编写代码实现程序升级。
3.IAP 与 Bootloader
在 Linux 系统中,软件组成可以跟 Windows 进行类比:
BootLoader 的主要作用是:
① 初始化硬件:比如设置时钟、初始化内存
② 启动内核:从 Flash 读出内核、存入内存、给内核设置参数、启动内核
③ 调试作用:在开发产品时需要经常调试内核,使用 BootLoader 可以方便地更新内核
在单片机中,软件没那么复杂,一般只有一个程序,上电就运行这个程序,并不需要 BootLoader。
但是涉及软件升级时,必须引入要 BootLoader。假设没有 BootLoader,程序无法升级自己: ① 单片机资源比较紧张,Flash 容量比较小,一般无法存储两份程序
② 当前的程序在 Flash 上运行,它无法更新自己:通过网络等手段下载程序到内存后,烧写到 Flash 不就破坏本身正在运行的程序了吗?
所以在单片机中,涉及软件升级时,必须引入 BootLoader:
Flash 上烧写有 BootLoader 和 APP(用户程序),启动过程如下:
① 上电时 BootLoader 先运行
② BootLoader 判断发现:Flash 上有 APP 并且无需升级,BootLoader 就会启动 APP
③ BootLoader 判断发现:Flash 上没有 APP 或者需要升级,BootLoader 执行升级操作
(理解上面这些步骤,我们需要在Flash里分配最前面的一段空间,用来烧写 Boot Loader ,上电时默认从Flash最前面取代码来执行,在Flash最后面后续会分配一小部分空间用来存放配置信息,可以根据这些配置信息来判断是否要进行程序升级等等,后续会有代码讲解,慢慢看下去 )
实现升级功能
1.升级方案设计
① 上位机与下位机
上位机与下位机使用 USB 串口相连:
上位机使用 sscom 串口调试助手发送固件,如下图(数据定义后面再设计):
① 先发送文件信息
② 再发送文件
下位机:等待文件信息、读取上位机发来的数据、烧写。
② Flash 使用规划
STM32H563RIV 内置 2MB Flash,划分如下:
① Bootloader 占据 256KB 空间
② APP 占据 1784KB 空间
③ 配置信息占据最后一个扇区 8KB 空间:用来保存 APP 版本、大小、校验码等信息。
(不同型号的芯片需要自己去查看 Flash 的大小)
③ 下位机启动流程
Bootloader 流程图如下:
所以进入升级模式有两种方式:
① 根本没有应用程序,即没有配置信息,这时就会进入升级模式,烧写应用程序
② 上位机发来的固件信息里的版本号比当前应用程序的版本号要高,也会进入升级模式
2.必备知识
① Cortex M3/M4/M33 启动流程
上电后,CPU 默认从 0 地址开始启动:
① 地址 0 就是默认的异常向量表基地址,使用 Flash 启动时 0 地址被映射到 Flash 基地址 0x08000000。
② CPU 读取异常向量表第 1 个 word(4 字节),写入 SP 寄存器
③ CPU 读取异常向量表第 2 个 word(4 字节),跳转执行:这就是 CPU 运行的第 1 个指令
② 异常向量表
当发生各类异常、中断时:
① 硬件会从异常向量表中,根据异常号、中断号找到一项,这项里保存的是“处理函数的地址”
② 硬件跳转执行这个处理函数。
以 SysTick 中断为例,SysTick 中断发生时,硬件会调用如下函数:
能正确使用中断的前提是:
① 把异常向量表的基地址告诉 CPU:这可以设置 SCB 里的 VTOR 寄存器(寄存器地址为 0xE000ED08)
② 在异常向量表里,填充中断处理函数
③ CPU 内部寄存器
无论是 cortex-M3/M4/M33,CPU 内部都有 R0、R1、……、R15 寄存器;它们可以用来“暂存”数据。
对于 R13、R14、R15,还另有用途:
① R13:别名 SP(Stack Pointer),栈指针
② R14:别名 LR(Link Register),用来保存返回地址
③ R15:别名 PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转
④ 几条汇编指令
读内存:Load
# 示例
LDR R0, [R1, #4] ; 读地址"R1+4", 得到的4字节数据存入R0
写内存:Store
# 示例
STR R0, [R1, #4] ; 把R0的4字节数据写入地址"R1+4"
加减:
ADD R0, R1, R2 ; R0=R1+R2
ADD R0, R0, #1 ; R0=R0+1
SUB R0, R1, R2 ; R0=R1-R2
SUB R0, R0, #1 ; R0=R0-1
比较:
CMP R0, R1 ; 结果保存在PSR(程序状态寄存器)
跳转:
B main ; Branch, 直接跳转
BL main ; Branch and Link, 先把返回地址保存在LR寄存器里再跳转
BX R1 ; 先在R1里保存地址再跳转
3.编写 APP
编写一个 APP(点灯)
h5_app:
烧写在 Flash 开头时能看到灯闪烁。然后修改 RO 地址为0x08040000,烧写后灯不闪烁(需要先擦除 Flash 开头的程序)。
要点:
① 设置 RO 地址为 0x08040000:
② 不要使用默认的异常向量表:
这段代码表示使用Flash开头的异常向量表,但是我们修改了应用程序的烧写位置。以后我们要在Flash开头一段空间内烧写bootloader程序,并且将异常向量表的基地址放在属于应用程序的Flash空间的开头。接下来我们来编写 bootloader 的代码。
4.编写 Bootloader 实现启动功能
(注意:要重新创建另外一个工程,boot loader的代码和app的代码不在同一个工程里)
Bootloader 要启动 APP,需要模仿硬件上电后做的事情:
① 读取异常向量表的第 1 个 word,设置进 SP 寄存器
② 读取异常向量表的第 1 个 word,跳转执行
h5_bootloader:
汇编代码:
Jump.S
AREA |.text|, CODE, READONLY
; Reset Handler
start_app PROC
EXPORT start_app
; SET vector, r0(0x08040000)==>VTOR
LDR R1, =0xE000ED08
STR R0, [R1]
; READ val of address(0x08040000), set to SP
LDR R1, [R0]
MOV SP, R1
; READ val of address(0x08040004), jump
LDR R1, [R0, #4]
BX R1
ENDP
END
为了使用新的异常向量表,Bootloader 还要设置 VTOR 寄存器为新的异常向量表。参考《ARM Cortex-M3 与 Cortex-M4 权威指南.pdf》,如下图:
并且,APP 工程里不应该再设置 VTOR(上一个小节讲过):
我们使用 STM32CubeMX 创建工程时经常看到如下警告:
如果不想看到上述警告,可以使能 ICACHE,如下操作:
注意!!Bootloader 和 APP,只能让一个程序使能 ICACHE。如果两个程序都使能 ICACHE 的话,APP 再次初始化 ICACHE 时会导致死机。
最终效果:
① 编译、烧写 h5_app:灯不闪烁。(app烧写在0x80400000)
② 编译、烧写 h5_bootloader:灯闪烁。(bootloader烧写在Flash开头)
5.定义下载协议
下载协议可以自己定义,根据使用流程定义如下:
① Bootloader 发出获取固件信息的请求:发出“1”字符给上位机
② 上位机发送固件信息:先发出 5 个“0x5a”数据给下位机,用于同步,再发送固件信息。
固件信息如下定义:
struct FirmwareInfo {
uint32_t version;//版本号
uint32_t file_len;//文件大小
uint32_t load_addr;//加载地址
uint32_t crc32;//校验码
uint8_t file_name[16];//文件名
/*共32字节*/
};
注意:为了方便在串口里操作,上位机发送 uint32_t 的整数时,先发送高字节(大字节序)。
③ Bootloader 发出获取固件的请求:发出“2”字符给上位机
④ 上位机发送 bin 文件
⑤ Bootloader 在烧写过程中,可以发送进度:“$1%”、“$2%”、“$100%”。以字符“$”开头、字符“%”结束。(可有可无)
在 Keil 里生成 bin、反汇编文件:
fromelf --bin --output app.bin app.axf
在 keil 里添加用户命令生成 bin 文件后,可以使用这个工具生成固件信息:
https://pan.baidu.com/s/1prNlDVciWKLF6_DpibVmdQ?pwd=epqw 提取码: epqw
它的用法为(尖括号表示的参数是不可省略的,中括号表示的参数可以省略,version是整数):
create_firmware_info.exe [load_addr]
它会分析 bin 文件,打印出长度、加载地址、校验码、文件名。
示例如下:
把“create_firmware_info.exe”复制到 bin 文件目录下,然后在命令行执行如下命令:
6.编写 Bootloader 实现下载功能
bootloader.h
#ifndef _BOOTLOADER_H
#define _BOOTLOADER_H
#include <stdint.h>
typedef struct FirmwareInfo {
uint32_t version;
uint32_t file_len;
uint32_t load_addr;
uint32_t crc32;
uint8_t file_name[16];
}FirmwareInfo, *PFirmwareInfo;
/*实现这个任务,用freeRTOS创建*/
void BootLoaderTask( void *pvParameters );
#endif /* _BOOTLOADER_H */
在 bootloader.h 里定义结构体,后续我们就通过结构体里的数据来实现bootloader。
bootloader.c
(重点看BootLoaderTask函数)
/*
* 包含各种头文件
*/
/*存放配置信息(固件信息)的地址*/
#define CFG_OFFSET 0x081FE000
#define UPDATE_TIMEOUT 1000
/*用来存放操作串口的句柄,前几个博客经常看到*/
static struct UART_Device *g_pUpdateUART;
/*大字节序转小字节序*/
static uint32_t BE32toLE32(uint8_t *buf)
{
return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] << 0);
}
/*获得本地固件信息*/
static int GetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
/*出错*/
if (ptFlashInfo->file_len == 0xFFFFFFFF)
return -1;
/*如果没错,将固件信息赋给传入的参数*/
*ptFirmwareInfo = *ptFlashInfo;
return 0;
}
/*获得服务器的固件信息(本文用串口程序代替服务器)*/
static int GetServerFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
uint8_t data = '1';
uint8_t buf[sizeof(FirmwareInfo)];
/* 发送字符1给PC,表示自己开始等待接收固件信息 */
if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
return -1;
/* wait for response */
while (1)
{
/*由于上位机我们用串口软件来模拟,手动发送固件信息没那么快,所以等待10s*/
if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &data, UPDATE_TIMEOUT*10))
return -1;
/*一开始我们会在固件信息前随便加几个0x5a,接收到5a就把它丢掉,我们要等待真正的数据*/
if (data != 0x5a)
{
/*进入这里表示接收到了第一个真正的字节,存到数组里*/
buf[0] = data;
break;
}
}
/* get firmware info */
for (int i = 1; i < sizeof(FirmwareInfo); i++)
{
/*buf[0]已经有数据了,所以从buf[1]开始,存放固件信息*/
if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT))
return -1;
}
/*更新固件信息*/
ptFirmwareInfo->version = BE32toLE32(&buf[0]);
ptFirmwareInfo->file_len = BE32toLE32(&buf[4]);
ptFirmwareInfo->load_addr = BE32toLE32(&buf[8]);
ptFirmwareInfo->crc32 = BE32toLE32(&buf[12]);
strncpy((char *)ptFirmwareInfo->file_name, (char *)&buf[16], 16);
return 0;
}
/*获得服务器发来的固件(编译出来的bin文件)*/
static int GetServerFirmware(uint8_t *buf, uint32_t len)
{
uint8_t data = '2';
/* 发送字符2表示可以接收固件了 */
if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
return -1;
/* get firmware info */
for (int i = 0; i < len; i++)
{
if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT*10))
return -1;
}
return 0;
}
/*计算校验码可以看下面这个网址,不做过多阐述*/
/* https://lxp32.github.io/docs/a-simple-example-crc32-calculation/ */
static int GetCRC32(const char *s,size_t n)
{
uint32_t crc=0xFFFFFFFF;
for(size_t i=0;i<n;i++) {
char ch=s[i];
for(size_t j=0;j<8;j++) {
uint32_t b=(ch^crc)&1;
crc>>=1;
if(b) crc=crc^0xEDB88320;
ch>>=1;
}
}
return ~crc;
}
/************************************************************************************/
void BootLoaderTask( void *pvParameters )
{
struct UART_Device *pUSBUART = GetUARTDevice("usb");
FirmwareInfo tLocalInfo;
FirmwareInfo tServerInfo;
int err;
int need_update = 0;
uint8_t *firmware_buf;
/*先等待一会,PC可能要进行某些操作*/
vTaskDelay(10000); /* wait for pc ready */
pUSBUART->Init(pUSBUART, 115200, 'N', 8, 1);
g_pUpdateUART = pUSBUART;
while (1)
{
/* 获取本地的固件信息 */
err = GetLocalFirmwareInfo(&tLocalInfo);
if (err)
{
/* 如果本地没有固件信息,就设置更新标志位 */
need_update = 1;
}
/*不管成功还是失败,都去获取服务器的固件信息*/
err = GetServerFirmwareInfo(&tServerInfo);
if (!err)
{
/* 如果获取到服务器发来的固件信息,就比较 */
if (tServerInfo.version > tLocalInfo.version)
{
/* 服务器发来的版本号比本地的版本号高,就设置更新标志位 */
need_update = 1;
}
}
else
{
need_update = 0;
}
/*
* 进入更新模式
*/
if (need_update)
{
/*开辟空间存放固件,大小从服务器发来的固件信息里得到*/
firmware_buf = pvPortMalloc(tServerInfo.file_len);
/*获得服务器发来的固件*/
err = GetServerFirmware(firmware_buf, tServerInfo.file_len);
if (!err)
{
/* 根据发来的固件计算校验码 */
uint32_t crc = GetCRC32((const char *)firmware_buf, tServerInfo.file_len);
if (crc == tServerInfo.crc32)
{
/* OK */
/* burn */
pUSBUART->Send(pUSBUART, (uint8_t *)"Download OK\r\n", 13, UPDATE_TIMEOUT);
}
}
}
else
{
/* 进入这里表示不用进入升级模式,跳转执行应用程序 */
}
}
}
注释写的比较清除,逻辑不是很复杂,我把代码精简了一部分,把调试信息和出错的分支删减了一些,自己写的时候可以加一些打印信息,这样就知道代码在哪一步出错了。
注意:烧写(burn)和跳转执行应用程序的代码我们还没有写,会在下个小节写,这小节先实现下载功能,看能不能成功获取固件信息及固件(编译出来的bin文件)。
上机实验:
① 生成 h5_app.bin 后,制作固件信息,在串口工具中粘贴待用:
② 烧写 h5_bootloader_download 程序
③ 观察串口工具,接收到“1”字符时,点击“发送”:发送固件信息
④ 观察串口工具,接收到“2”字符时,点击“发送文件”:发送 bin 文件
⑤ 观察串口工具,可以看到“Download OK”
7.编写 Bootloader 实现烧录功能
本节要实现 2 个功能:
① 烧录
② 启动
① 烧写 Flash
对于烧录,要先擦除(使用“ HAL_FLASHEx_Erase ”函数),再烧写(使用“ HAL_FLASH_Program ”函数)。
HAL_FLASHEx_Erase函数比较复杂,要构造相关结构体,需要对Flash结构有一定了解,这里就不展开,感兴趣的可以自己学习一下。
HAL_FLASH_Program
烧写固件和固件信息的函数如下(在“Core\Src\bootloader.c”):
#define SECTOR_SIZE (8*1024)
/*烧写固件*/
static int WriteFirmware(uint8_t *firmware_buf, uint32_t len, uint32_t flash_addr)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t sectors = (len + (SECTOR_SIZE - 1)) / SECTOR_SIZE;
uint32_t flash_offset = flash_addr - 0x08000000;
uint32_t bank_sectors;
uint32_t erased_sectors = 0;
/*先要解锁才能操作Flash,返回之前要lock*/
HAL_FLASH_Unlock();
/* erase bank1 */
if (flash_offset < 0x100000)
{
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_1;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASHEx_Erase Failed\r\n", strlen("HAL_FLASHEx_Erase Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
flash_offset += erased_sectors*SECTOR_SIZE;
}
sectors -= erased_sectors;
flash_offset -= 0x100000;
/* erase bank2 */
if (sectors)
{
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASHEx_Erase Failed\r\n", strlen("HAL_FLASHEx_Erase Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
}
/* program */
len = (len + 15) & ~15;
for (int i = 0; i < len; i+=16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)firmware_buf))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASH_Program Failed\r\n", strlen("HAL_FLASH_Program Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
firmware_buf += 16;
}
HAL_FLASH_Lock();
return 0;
}
/*----------------------------------------------------------------------------------*/
/*烧写固件信息*/
static int WriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t flash_addr = CFG_OFFSET;
uint8_t *src_buf = (uint8_t *)ptFirmwareInfo;
HAL_FLASH_Unlock();
/* erase bank2 */
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = (flash_addr - 0x08000000 - 0x100000) / SECTOR_SIZE;
tEraseInit.NbSectors = 1;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASHEx_Erase Failed\r\n", strlen("HAL_FLASHEx_Erase Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
/* program */
for (int i = 0; i < sizeof(FirmwareInfo); i+=16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)src_buf))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASH_Program Failed\r\n", strlen("HAL_FLASH_Program Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
src_buf += 16;
}
HAL_FLASH_Lock();
return 0;
}
代码大概讲一下:在烧写之前先擦除;对于烧写固件,擦除的时候要根据文件大小判断擦除哪个bank(有两个bank,一个bank大小为1M),然后16字节16字节去烧写;对于烧写固件信息,就不需要判断bank了,我们人为设定bank2最后一个扇区存放的是配置信息,同样擦除后再烧写,感兴趣的可以自己看看。(Flash首地址为0x80000000)
然后补全上一小节的下载功能的代码,等会我们就要编写启动应用程序的代码。
② 启动
启动 APP 之前,应该让系统“尽量”处于初始状态。比如:关闭各类中断、让各类设备处于初始状态。有一个办法可以轻松实现这点:软件复位。
所以,Bootloader 启动 APP 时,可以这样改进:
① 触发软件复位
② 会再次运行 Bootloader
③ Bootloader 在初始各类硬件之前判断复位原因,发现是软件服务时,启动 APP
关键代码如下(在“Core\Src\bootloader.c”):
/*声明为外部可调用,复位后进入main函数时,判断是否为软件复位*/
int isSoftReset(void)
{
return HAL_RCC_GetResetSource() & RCC_RESET_FLAG_SW;
}
/*声明为外部可调用*/
/*获得app的异常向量表*/
uint32_t get_app_vector(void)
{
/*CFG_OFFSET = 0x081FE000*/
PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
/*返回加载地址*/
return ptFlashInfo->load_addr;
}
static void SoftReset(void)
{
__set_FAULTMASK(1);//关闭所有中断
HAL_NVIC_SystemReset();
}
static void start_app_c(void)
{
/* 触发软件复位 */
SoftReset();
}
在 main 函数前面,判断是否软件复位,是的话启动 APP:
int main(void)
{
/* USER CODE BEGIN 1 */
/*如果是软件复位,就跳转执行应用程序*/
if (isSoftReset())
{
/*声明我们之前写的汇编文件*/
extern void start_app(uint32_t vector);
/*传入加载地址,执行应用程序,start_app是我们之前编写的汇编代码*/
start_app(get_app_vector());
}
/* USER CODE END 1 */
/*略*/
}
注意:start_app是我们之前编写的汇编文件。
上机实验:
① 生成 h5_app.bin 后,制作固件信息,在串口工具中粘贴待用:
② 烧写 h5_bootloader_ok 程序
③ 观察串口工具,接收到“1”字符时,点击“发送”:发送固件信息
④ 观察串口工具,接收到“2”字符时,点击“发送文件”:发送 bin 文件
⑤ 观察串口工具,可以看到“Download OK”
⑥ 观察串口工具,可以看到“start app”,并且看到 LED 闪烁
⑦ 修改 h5_app,让 LED 闪烁更快,重新编译:
⑧ 重新制作固件信息,在串口里粘贴待用:
⑨ 手工复位开发板(一定要手工复位,手工复位才会进入bootloader),重复步骤③④,可以观察到烧录成功后,程序被启动(LED 闪烁更快)。