STM32 IAP应用开发——通过串口/RS485实现固件升级(方式2)



By
jonson
15 7 月 24
0
comment

STM32 IAP应用开发——通过串口/RS485实现固件升级(方式1)

什么是IAP?

IAP(In-Application Programming) 指MCU可以在系统中获取新代码并对自己重新编程,即可用程序来改变程序。在应用编程(IAP)是用户的应用代码对片内Flash存储器进行擦除/编程的方法。这种方式的典型应用就是用一小段代码来实现程序的下载,实际上单片机的ISP功能就是通过IAP技术来实现的,即片子在出厂前就已经有一段小的boot程序在里面,片子上电后,开始运行这段程序,当检测到上位机有下载要求时,便和上位机通信,然后下载数据到数据存储区,从而实现固件升级。

什么是BootLoader?

百度百科:在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。在嵌入式系统中,通常并没有像BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。

实际上,BootLoader不仅仅在操作系统上使用,在一些内存小,功能应用较为简单的单片机设备上面也可以通过BootLoader来完成固件升级。

我之前也有发过一些关于STM32远程升级的文章,实现的方式有很多种,感兴趣的同学可以去看一下。

那么这一期我来介绍一下如何自己制作一个BootLoader程序,并且通过串口或者RS485实现固件升级。

1 环境搭建

关于STM32以及Keil的环境这里就不具体介绍了,网上教程也很多,不懂的同学自行查阅资料。

2 功能描述

在做bootloader之前一定要先想好升级的途径和方式,这样才好规划分区以及制作bootloader。

关于bootloader详细的讲解,可以看下我之前发的博客:STM32 IAP应用开发——自制BootLoader

分区介绍:

我用的是STM32F407,内存是512K的(想用内存更小的MCU也是可以的,改下各个分区的内存分配就行了)。

注:F4系列的MCU不像F1那样,内存扇区都很大(最少也是16K),而且同一块扇区只能一起擦除,所以就没办法分的那么细了。详细的内存分布可以参考下面的两个图。

STM32F4x扇区分布图如下:
请添加图片描述

STM32F1x扇区分布图如下:

请添加图片描述

那么我这里呢,就用一个512k的内存,分成3个区域,来实现一个升级的功能。

分区表如下:

name offset size function
boot 0x08000000 0x00004000 存放boot程序
setting 0x08004000 0x00004000 存放设备需要保存的一些参数
app 0x08008000 0x00078000 存放应用程序

请添加图片描述

方案介绍:

1)bootloader部分:

开始运行后先等待5s,在这个时间内如果收到串口2或者RS485的升级命令就进入升级模式,如果超时则跳转到用户程序(APP)。

在升级模式,可以通过串口2或者RS485传输要升级的固件,传输的数据协议我这里图方便就直接用Ymodem了,不知道Ymodem协议的可以先自行查阅一下资料。

请添加图片描述

2)APP部分:

APP部分修改一下中断向量表地址即可,其他的随便你做什么应用。

另外,我在分区的时候留了一块settimg区,在实际的应该中如果有需要记录一些掉电后还能保存的数据,那么这块区域就可以用得上了。

3 程序编写

3.1 BootLoader部分

不管用的是什么MCU,要实现固件升级都离不开BootLoader,BootLoader是一个统称,它其实只是一段引导程序,在MCU启动的时候会先运行这段代码,判断是否需要升级,如果不需要升级就跳转到APP分区运行用户代码,如果需要升级则先通过一些硬件接口接收和搬运要升级的新固件,然后再跳转到APP分区运行新固件,从而实现固件升级。

BootLoader的制作需要根据实际的需求来做,不同的运行方式或者升级方式在做法上都是有区别的,包括BootLoader所需要的内存空间也不尽相同。

不过不管是用什么方式,Bootloader都应该尽可能做的更小更简洁,这样的话内存的开销就更小,对于内存较小的MCU来说压力就没那么大了。

注:我这里是基于正点原子的工程模板改的,增加了自己的功能。

示例代码如下:

Bootloader分区定义:

#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     // BOOT sector start addres
#define BOOT_SECTOR_SIZE            0x4000         // BOOT sector size    
#define SETTING_SECTOR_ADDR         0x08004000     // SETTING sector start addres  
#define SETTING_SECTOR_SIZE         0x4000         // SETTING sector size     
#define APP_SECTOR_ADDR             0x08008000     // APP sector start address  
#define APP_SECTOR_SIZE             0x78000        // APP sector size    

#define BOOT_ERASE_SECTORS_NUM      1  // 16k
#define SETTING_ERASE_SECTORS_NUM   1  // 16k
#define APP_ERASE_SECTORS_NUM       6  // 16k + 16k + 64k + 128k + 128k + 128k

main函数:

#include "bootloader.h"
#include "usart.h"
#include "rs485.h"
#include "delay.h"
#include "ymodem.h"

#define WAIT_TIMEOUT   5

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     | 0x%08X | 0x%08X |rn", BOOT_SECTOR_ADDR, BOOT_SECTOR_SIZE);
    uart_log("| setting  | 0x%08X | 0x%08X |rn", SETTING_SECTOR_ADDR, SETTING_SECTOR_SIZE);
    uart_log("| app      | 0x%08X | 0x%08X |rn", APP_SECTOR_ADDR, APP_SECTOR_SIZE);
    uart_log("======================================rn");
}

void print_wait_message(void)
{
    uart_log("------- Please enter parameter -------rn");
	uart_log("[1].Start programrn");
	uart_log("[2].Update programrn");
    uart_log("--------------------------------------rn");
}

int main() 
{
    process_status process;
    uint16_t timerout = 0;

    delay_init(168);
    uart_init(115200);
    ymodem_init();
    print_boot_message();
    print_wait_message();

    while (1) 
    {
        process = get_ymodem_status();
        switch (process) 
        {
            case WAIT_START_PROGRAM:
                uart_log("wait start app...(%ds)rn", WAIT_TIMEOUT - timerout);
                delay_ms(1000);
                timerout ++;
                if(timerout >= WAIT_TIMEOUT)
                {
                    set_ymodem_status(START_PROGRAM);
                }
                break;
            case START_PROGRAM:
                uart_log("start app...rn");
                delay_ms(50);
                if (!jump_app(APP_SECTOR_ADDR)) 
                {
                    uart_log("start app failed: app no programrn");
                    delay_ms(1000);
                }
                break;
            case UPDATE_PROGRAM:
                ymodem_c();
                uart_log("update app program...rn");
                delay_ms(1000);
                break;
            case UPDATE_SUCCESS:
                uart_log("update successrn");
                uart_log("system reboot...rn");
                delay_ms(1000);
                system_reboot();
                break;
            default:
                break;
        }
    }
}

Ymodem协议处理:

#define YMODEM_SOH		0x01
#define YMODEM_STX		0x02
#define YMODEM_EOT		0x04
#define YMODEM_ACK		0x06
#define YMODEM_NAK		0x15
#define YMODEM_CA		0x18
#define YMODEM_C		0x43

#define MAX_QUEUE_SIZE  1200

typedef void (*ymodem_callback)(process_status);

typedef struct 
{
	process_status process;
	uint8_t status;
	uint8_t id;
	uint32_t addr;
	uint8_t sectors_size;
	ymodem_callback cb;
} ymodem_t;

//顺序循环队列的结构体定义如下:
typedef struct
{
	uint8_t queue[MAX_QUEUE_SIZE];
	int rear;  //队尾指针
	int front;  //队头指针
	int count;  //计数器
} seq_queue_t; 

typedef struct 
{
	uint8_t data[1200];
	uint16_t len;
} download_buf_t;

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 = APP_SECTOR_ADDR;
                uart_log("erase flash: 0x%08Xrn", APP_SECTOR_ADDR);
                mcu_flash_erase(ymodem.addr, APP_ERASE_SECTORS_NUM);
                uart_log("erase flash successrn");
                ymodem_ack();
                ymodem_c();
                ymodem.status++;
            }
            else if (type == '1') 
            {
                uart_log("start program nowrn");
                ymodem.process = START_PROGRAM;
            }
            else if (type == '2') 
            {
                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_init(void)
{
    RS485_Init(115200);
    timer_init();
    queue_initiate(&rx_queue);
}

关于bootloader详细的讲解,可以看下我之前发的博客:

STM32 IAP应用开发——自制BootLoader

完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87553496

3.2 APP的制作

APP部分根据自己实际的功能来做,只要记得修改中断向量表地址即可。地址的值等于你APP区的起始地址。

示例代码如下:

main函数:

#include "main.h"
#include "usart.h"
#include "delay.h"

#define APP_VERSION          "V100"
#define NVIC_VTOR_MASK       0x3FFFFF80
#define APP_PART_ADDR        0x08008000

void ota_app_vtor_reconfig(void)
{
    /* Set the Vector Table base location by user application firmware definition */
    SCB->VTOR = APP_PART_ADDR & NVIC_VTOR_MASK;
}

void led_init(void)
{         
    GPIO_InitTypeDef  GPIO_InitStructure;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOF, &GPIO_InitStructure);
    GPIO_SetBits(GPIOF, GPIO_Pin_9);
}

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)
{
    ota_app_vtor_reconfig();
    delay_init(168);
    uart_init(115200);
    print_boot_message();
    led_init();

    uart_log ("app init successrn");
    while (1)
    {
        GPIO_SetBits(GPIOF, GPIO_Pin_9);
        delay_ms(1000);
        GPIO_ResetBits(GPIOF, GPIO_Pin_9);
        delay_ms(1000);
    }
}

完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87553496

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)开始运行代码

等待5s,如果不需要升级就跳转到App区,如下图:

请添加图片描述

2)发送命令1

在等待的5s内通过串口2或者RS485发送一个’1’,直接跳转到APP。

注:我这里为了方便调试才用的这种方式,实际上可以根据自己的需求来做。

请添加图片描述

3)发送命令2,进入升级模式

在等待的5s内通过串口2或者RS485发送一个’2’,进入升级模式。

注:我这里为了方便调试才用的这种方式,实际上可以根据自己的需求来做。比如用按键进入,或者用其他串口,USB之类的,也可以在APP部分做这个功能。

串口调试窗口log如下图:

请添加图片描述

4)通过Ymodem传输新固件

调试工具我用的是XShell,实际上用其他工具也行,只要支持Ymodem方式传输文件即可。

请添加图片描述

请添加图片描述

5)升级固件

固件升级完成后自动重启,重新运行Bootloader和APP。

请添加图片描述

至此,整个升级流程就走完了。

结束语

好了,关于自制BootLoader并实现串口以及RS485升级固件的介绍就讲到这里,本文列举的例子其实只是升级的其中一种方式,只是提供一个思路,不是唯一的方法,实际上最好还是根据自己实际的需求来做。我之前也发给几篇升级相关的文章,用的都是不同的方式,各有各的优点和缺点,感兴趣的同学可以去看一下。

需要源码的同学可以在下面的链接下载,我把BootLoader和APP都上传了。

如果你有什么问题或者有更好的方法,欢迎在评论区留言。

完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87553496

发表回复