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

  • 07/15 14:18

什么是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

人工客服
(售后/吐槽/合作/交友)
  • 器件型号:ECS-2520MV-333.3-CM-TR
    • 数量 1
    • 建议厂商 ECS International Inc
    • 器件描述 HCMOS Output Clock Oscillator,
    • 参考价格 暂无数据
    • 风险等级
    • ECAD模型
    • 数据手册
    • 查看更多信息
  • 器件型号:H11G1
    • 数量 1
    • 建议厂商 Motorola Mobility LLC
    • 器件描述 1 CHANNEL DARLINGTON OUTPUT OPTOCOUPLER
    • 参考价格 $0.89
    • 风险等级
    • ECAD模型
    • 数据手册
    • 查看更多信息
  • 器件型号:M24M01-RMN6TP
    • 数量 1
    • 建议厂商 STMicroelectronics
    • 器件描述 1 Mbit serial I<sup>2</sup>C bus EEPROM
    • 参考价格 $1.51
    • 风险等级
    • ECAD模型

      ECAD模型

      下载ECAD模型
    • 数据手册
    • 查看更多信息

相关资讯

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
  8. 8.
  9. 9.
  10. 10.
  11. 11.
  12. 12.
  13. 13.
  14. 14.
  15. 15.
  16. 16.
  17. 17.
  18. 18.
  19. 19.
  20. 20.
查看全部20条内容