什么是IAP?
IAP(In-Application Programming) 指MCU可以在系统中获取新代码并对自己重新编程,即可用程序来改变程序。在应用编程(IAP)是用户的应用代码对片内Flash存储器进行擦除/编程的方法。这种方式的典型应用就是用一小段代码来实现程序的下载,实际上单片机的ISP功能就是通过IAP技术来实现的,即片子在出厂前就已经有一段小的boot程序在里面,片子上电后,开始运行这段程序,当检测到上位机有下载要求时,便和上位机通信,然后下载数据到数据存储区,从而实现固件升级。
什么是BootLoader?
百度百科:在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。在嵌入式系统中,通常并没有像BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。
实际上,BootLoader不仅仅在操作系统上使用,在一些内存小,功能应用较为简单的单片机设备上面也可以通过BootLoader来完成OTA升级。
我之前也有发过一些关于STM32远程升级的文章,实现的方式有很多种,感兴趣的同学可以去看一下。
固件升级系列合集:https://blog.csdn.net/ShenZhen_zixian/article/details/129074047
那么这一期我来介绍一下如何自己制作一个BootLoader程序,并且通过串口或者RS485实现固件升级。
1 环境搭建
关于STM32以及Keil的环境这里就不具体介绍了,网上教程也很多,不懂的同学自行查阅资料。
2 功能描述
在做bootloader之前一定要先想好升级的途径和方式,这样才好规划分区以及制作bootloader。
关于bootloader详细的讲解,可以看下我之前发的博客:
STM32 IAP应用开发——自制BootLoader
分区介绍:
我用的是STM32F407,内存是512K的(想用内存更小的MCU也是可以的,改下各个分区的内存分配就行了)。
注:F4系列的MCU不像F1那样,内存扇区都很大(最少也是16K),而且同一块扇区只能一起擦除,所以就没办法分的那么细了。详细的内存分布可以参考下面的两个图。
STM32F4x扇区分布图如下:
STM32F1x扇区分布图如下:
那么我这里呢,就用一个512k的内存,分成4个区域,来实现升级的功能。
分区表如下:
name | offset | size | function |
---|---|---|---|
boot | 0x08000000 | 0x00004000 | 存放boot程序 |
setting | 0x08004000 | 0x00004000 | 存放升级相关的配置参数 |
app | 0x08008000 | 0x00018000 | 存放应用程序 |
download | 0x08020000 | 0x00020000 | 存放需要升级的新固件 |
方案介绍:
1)bootloader部分:
运行时从setting
分区里面读取升级相关的数据,确定是否需要升级,如果需要,则把download
分区的固件搬运到app
分区,如果不需要升级则直接跳转到app
分区。另外,使用串口1来打印运行的一些信息。
2)APP部分:
通过串口2或者RS485连接到PC端,然后等待上位机发送特定的升级命令,如果MCU收到命令,则进入下载模式,然后通过串口2或者RS485传输新固件到download
分区,并且在下载完成后把升级标志写入到setting
分区里面。
我这里图方便,串口传输固件的方式我采用的是Ymodem协议,因为这个协议很多tool都可以用,就不用专门做一个上位机了。如果你想用其他的协议或者自定义协议其实都是可以的,稍做修改就行。
3 程序编写
3.1 BootLoader部分
不管用的是什么MCU,要实现固件升级都离不开BootLoader,BootLoader是一个统称,它其实只是一段引导程序,在MCU启动的时候会先运行这段代码,判断是否需要升级,如果不需要升级就跳转到APP分区运行用户代码,如果需要升级则先通过一些硬件接口接收和搬运要升级的新固件,然后再跳转到APP分区运行新固件,从而实现固件升级。
BootLoader的制作需要根据实际的需求来做,不同的运行方式或者升级方式在做法上都是有区别的,包括BootLoader所需要的内存空间也不尽相同。
不过不管是用什么方式,Bootloader都应该尽可能做的更小更简洁,这样的话内存的开销就更小,对于内存较小的MCU来说压力就没那么大了。
示例代码如下:
分区定义:
#define FLASH_SECTOR_SIZE 1024
#define FLASH_SECTOR_NUM 512 // 512K
#define FLASH_START_ADDR ((uint32_t)0x8000000)
#define FLASH_END_ADDR ((uint32_t)(0x8000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))
//flash sector addr
#define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000) //sector0 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_1 ((uint32_t)0x08004000) //sector1 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_2 ((uint32_t)0x08008000) //sector2 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_3 ((uint32_t)0x0800C000) //sector3 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_4 ((uint32_t)0x08010000) //sector4 addr, 64 Kbytes
#define ADDR_FLASH_SECTOR_5 ((uint32_t)0x08020000) //sector5 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_6 ((uint32_t)0x08040000) //sector6 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_7 ((uint32_t)0x08060000) //sector7 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_8 ((uint32_t)0x08080000) //sector8 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_9 ((uint32_t)0x080A0000) //sector9 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_10 ((uint32_t)0x080C0000) //sector10 addr,128 Kbytes
#define ADDR_FLASH_SECTOR_11 ((uint32_t)0x080E0000) //sector11 addr,128 Kbytes
#define BOOT_SECTOR_ADDR 0x08000000
#define BOOT_SECTOR_SIZE 0x4000
#define SETTING_SECTOR_ADDR 0x08004000
#define SETTING_SECTOR_SIZE 0x4000
#define APP_SECTOR_ADDR 0x08008000 // APP sector start address
#define APP_SECTOR_SIZE 0x18000 // APP sector size
#define DOWNLOAD_SECTOR_ADDR 0x08020000 // Download sector start address
#define DOWNLOAD_SECTOR_SIZE 0x20000 // Download sector size
程序跳转:
uint8_t jump_app(uint32_t app_addr)
{
uint32_t jump_addr;
jump_callback cb;
if (((*(__IO uint32_t*)app_addr) & 0x2FFE0000 ) == 0x20000000)
{
jump_addr = *(__IO uint32_t*) (app_addr + 4);
cb = (jump_callback)jump_addr;
__set_MSP(*(__IO uint32_t*)app_addr);
cb();
return 1;
}
return 0;
}
主函数:
void print_boot_message(void)
{
uart_log("---------- Enter BootLoader ----------rn");
uart_log("rn");
uart_log("======== flash pration table =========rn");
uart_log("| name | offset | size |rn");
uart_log("--------------------------------------rn");
uart_log("| boot | 0x08000000 | 0x00004000 |rn");
uart_log("| setting | 0x08004000 | 0x00004000 |rn");
uart_log("| app | 0x08008000 | 0x00018000 |rn");
uart_log("| download | 0x08020000 | 0x00020000 |rn");
uart_log("======================================rn");
}
int main()
{
process_status process;
uint16_t i;
uint8_t boot_state;
uint8_t down_buf[128];
uint32_t down_addr;
uint32_t app_addr;
delay_init(168);
uart_init(115200);
print_boot_message();
boot_parameter.process = read_setting_boot_state();
boot_parameter.addr = APP_SECTOR_ADDR;
while (1)
{
process = get_boot_state();
switch (process)
{
case START_PROGRAM:
uart_log("start app...rn");
delay_ms(50);
if (!jump_app(boot_parameter.addr))
{
uart_log("no programrn");
delay_ms(1000);
}
uart_log("start app failedrn");
break;
case UPDATE_PROGRAM:
uart_log("update app program...rn");
app_addr = APP_SECTOR_ADDR;
down_addr = DOWNLOAD_SECTOR_ADDR;
uart_log("app addr: 0x%08X rn", app_addr);
uart_log("down addr: 0x%08X rn", down_addr);
uart_log("erase mcu flash...rn");
mcu_flash_erase(app_addr, APP_ERASE_SECTORS_NUM);
uart_log("mcu flash erase successrn");
uart_log("write mcu flash...rn");
// memset(down_buf, 0, sizeof(down_buf));
for (i = 0; i < (APP_SECTOR_SIZE / 1024) * 8; i++)
{
mcu_flash_read(down_addr, &down_buf[0], 128);
delay_ms(5);
mcu_flash_write(app_addr, &down_buf[0], 128);
delay_ms(5);
down_addr += 128;
app_addr += 128;
// uart_log("mcu_flash_write: %drn", i);
}
uart_log("mcu flash write successrn");
set_boot_state(UPDATE_SUCCESS);
break;
case UPDATE_SUCCESS:
uart_log("update successrn");
boot_state = UPDATE_SUCCESS_STATE;
write_setting_boot_state(boot_state);
set_boot_state(START_PROGRAM);
break;
default:
break;
}
}
}
关于bootloader详细的讲解,可以看下我之前发的博客:
STM32 IAP应用开发——自制BootLoader
完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87546126
3.2 APP的制作
APP部分根据自己实际的功能来做,我这里用的是串口或者RS485连接PC端,然后传输固件的协议用的是Ymodem。
当然了,协议也是可以自定义,只要能正确的把固件从PC端搬运到MCU的flash就行了。
示例代码如下:
Ymodem协议部分:
注:详细的协议解析这里就不讲解了,不懂的同学自行查阅资料。
void ymodem_ack(void)
{
uint8_t buf;
buf = YMODEM_ACK;
RS485_Send_Data(&buf, 1);
}
void ymodem_nack(void)
{
uint8_t buf;
buf = YMODEM_NAK;
RS485_Send_Data(&buf, 1);
}
void ymodem_c(void)
{
uint8_t buf;
buf = YMODEM_C;
RS485_Send_Data(&buf, 1);
}
void set_ymodem_status(process_status process)
{
ymodem.process = process;
}
process_status get_ymodem_status(void)
{
process_status process = ymodem.process;
return process;
}
void ymodem_start(ymodem_callback cb)
{
if (ymodem.status == 0)
{
ymodem.cb = cb;
}
}
void ymodem_recv(download_buf_t *p)
{
uint8_t type = p->data[0];
switch (ymodem.status)
{
case 0:
if (type == YMODEM_SOH)
{
ymodem.process = BUSY;
ymodem.addr = DOWNLOAD_SECTOR_ADDR;
mcu_flash_erase(ymodem.addr, ERASE_SECTORS);
ymodem_ack();
ymodem_c();
ymodem.status++;
}
else if (type == '1')
{
uart_log("enter update modern");
ymodem.process = UPDATE_PROGRAM;
}
break;
case 1:
if (type == YMODEM_SOH || type == YMODEM_STX)
{
if (type == YMODEM_SOH)
{
mcu_flash_write(ymodem.addr, &p->data[3], 128);
ymodem.addr += 128;
}
else
{
mcu_flash_write(ymodem.addr, &p->data[3], 1024);
ymodem.addr += 1024;
}
ymodem_ack();
}
else if (type == YMODEM_EOT)
{
ymodem_nack();
ymodem.status++;
}
else
{
ymodem.status = 0;
}
break;
case 2:
if (type == YMODEM_EOT)
{
ymodem_ack();
ymodem_c();
ymodem.status++;
}
break;
case 3:
if (type == YMODEM_SOH)
{
ymodem_ack();
ymodem.status = 0;
ymodem.process = UPDATE_SUCCESS;
}
}
p->len = 0;
}
void ymodem_handle(void)
{
uint8_t boot_state;
process_status process;
process = get_ymodem_status();
switch (process)
{
case START_PROGRAM:
break;
case UPDATE_PROGRAM:
ymodem_c();
delay_ms(1000);
break;
case UPDATE_SUCCESS:
boot_state = UPDATE_PROGRAM_STATE;
mcu_flash_erase(SETTING_BOOT_STATE, 1);
mcu_flash_write(SETTING_BOOT_STATE, &boot_state, 1);
// mcu_flash_read(SETTING_BOOT_STATE, &boot_state, 1);
// uart_log("boot_state:%drn", boot_state);
uart_log("firmware download successrn");
uart_log("system reboot...rn");
delay_ms(2000);
system_reboot();
break;
default:
break;
}
}
void ymodem_init(void)
{
RS485_Init(115200);
timer_init();
queue_initiate(&rx_queue);
}
主函数:
#define APP_VERSION "V100"
void print_boot_message(void)
{
uart_log("======================================rn");
uart_log("-------------- Enter APP -------------rn");
uart_log ("app version is: %srn", APP_VERSION);
uart_log("======================================rn");
}
int main(void)
{
delay_init(168);
uart_init(115200);
ymodem_init();
print_boot_message();
uart_log ("app init successrn");
while (1)
{
ymodem_handle();
}
}
修改中断向量:
bootloader的运行地址是在起始地址上的,所以中断向量是0,不用改。
但是app的运行地址是在起始地址上做了偏移的,所以中断向量也要改,不然会运行会出问题。
#define VECT_TAB_OFFSET 0x8000
注:这个变量定义在system_stm32f4xx.c中可以找到。
完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87546126
4 修改工程中的内存配置
因为我们对stm32的内存进行了分区,不同的代码要存放在不同的区域,因此,我们在编译工程之前需要先定义好各自的区域,以免出现内存越界。
4.1 Bootloader工程内存配置
Bootloader的起始地址不需要改,按flash默认地址即可,size需要改成实际分区大小。
4.2 APP工程内存配置
APP的起始地址和size都需要根据实际的分区来改。
5 烧录相关配置
我们的Bootloader做好以后需要烧录到MCU里面,可以直接用Keil uVison来下载,也可以用J-Flash或者其他,这个都没关系,但是要注意内存的分配,要把固件烧到对应的内存地址上。
5.1 BootLoader部分
1)使用Keil uVision下载
如果是用keil下载的话,需要注意flash的配置,具体如下:
2)使用其他下载工具
如果是用J-Flash或者STlink的工具烧录的话注意烧录的起始地址是0x08000000就好了。
5.2 APP部分
1)使用Keil uVision下载
跟BootLoader一样,我们按照前面分配好的空间配置APP的参数即可。
2)使用其他下载工具
如果是用J-Flash或者STlink的工具烧录的话注意烧录的起始地址是0x08008000就好了。
6 运行测试
用串口助手查看运行log(我这里用的是XShell,用其他的也是可以的)。
1)开始运行代码
不需要升级时直接跳转到App区,如下图:
2)进入烧录模式
进入APP之后,往串口2/RS485发送一个字符"1",进入升级模式,然后通过调试工具发送新固件的bin文件。
注:为了方便调试才用了一个字符"1",实际使用的话最好改一下,太简单的话容易出现误操作。
串口调试窗口log如下图:
3)通过Ymodem传输新固件
调试工具我用的是XShell,实际上用其他工具也行,只要支持Ymodem方式传输文件即可。
4)升级固件
固件搬运完成后自动重启,重新运行Bootloader,然后进行固件的升级。
至此,整个升级流程就走完了。
结束语
好了,关于自制BootLoader并实现串口和RS485在线升级的介绍就讲到这里,本文列举的例子其实只是升级的其中一种方式,只是提供一个思路,不是唯一的方法,实际上最好还是根据自己实际的需求来做。
需要源码的同学可以在下面的链接下载,我把BootLoader和APP都上传了。
如果你有什么问题或者有更好的方法,欢迎在评论区留言。
完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87546126