STM32 OTA应用开发——通过USB实现OTA升级

news2025/7/28 21:58:35

STM32 OTA应用开发——通过USB实现OTA升级

目录

  • STM32 OTA应用开发——通过USB实现OTA升级
    • 前言
    • 1 环境搭建
    • 2 功能描述
    • 3 BootLoader的制作
    • 4 APP的制作
    • 5 烧录下载配置
    • 6 运行测试
    • 结束语

前言

什么是OTA?

百度百科:空中下载技术(Over-the-Air Technology; OTA),是通过移动通信的空中接口实现对移动终端设备及SIM卡数据进行远程管理的技术。经过公网多年的应用与发展,已十分成熟,网络运营商通过OTA技术实现SIM卡远程管理,还能提供移动化的新业务下载功能。

实际上,现在我们所说的OTA比百度百科的定义还要更广泛,OTA的形式已经不再局限于手机和SIM卡,只要涉及到远程下载升级程序的方式我们都可以称之为OTA。例如通过4G,5G,WiFI,蓝牙等无线通讯进行下载升级的可以称为OTA,通过U盘,RS485等串行接口进行升级的也可以称之为OTA。

OTA的作用?
OTA的意义在于它在一定程度上突破了距离的限制,在不借助烧录器的情况下完成固件的下载升级,极大的方便了产品的升级和维护,降低售后成本。

什么是BootLoader?

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

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

我之前也有发过一些关于STM32远程OTA的文章,但用的是第三方BootLoader,而且是基于操作系统实现的,BootLoader占用的内存也比较大,而且不开源。
那么这一期我就来介绍一下如何自己制作一个BootLoader程序,并且通过USB实现OTA升级。

1 环境搭建

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

2 功能描述

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

方案介绍:
1)bootloader部分:
运行时从setting里面读一些参数,确定是否需要升级,如果需要,则把download分区的固件搬运到app分区,如果不需要升级则直接跳转到app分区。
2)APP部分:
运行时先连接USB(以USB CDC的方式),然后等待上位机发送升级命令,如果收到命令,则进入下载模式。
我这里图方便,USB传输固件的方式我采用的是Ymodem协议,因为这个协议很多tool都可以用,就不用专门做一个上位机了。如果你想用其他的协议或者自定义协议其实都是可以的,稍做修改就行。

在这里插入图片描述

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

分区表如下:

nameoffsetsize
boot0x080000000x00003000
setting0x080030000x00001000
app0x080040000x0000E000
download0x080120000x0000E000

请添加图片描述

3 BootLoader的制作

不管用的是什么MCU,要使用OTA都离不开BootLoader,BootLoader是一个统称,它其实只是一段引导程序,在MCU启动的时候会先运行这段代码,判断是否需要升级,如果不需要升级就跳转到APP分区运行用户代码,如果需要升级则先通过一些硬件接口接收和搬运要升级的新固件,然后再跳转到APP分区运行新固件,从而实现OTA升级。
BootLoader的制作需要根据实际的需求来做,不同的运行方式或者升级方式在做法上都是有区别的,包括BootLoader所需要的内存空间也不尽相同。
不过不管是用什么方式,Bootloader都应该尽可能做的更小更简洁,这样的话内存的开销就更小,对于内存较小的MCU来说压力就没那么大了。

示例代码如下:
分区定义:

#define FLASH_SECTOR_SIZE       1024
#define FLASH_SECTOR_NUM        128    // 128K
#define FLASH_START_ADDR        ((uint32_t)0x8000000)
#define FLASH_END_ADDR          ((uint32_t)(0x8000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))

#define BOOT_SECTOR_ADDR        0x08000000     // BOOT sector start address 
#define BOOT_SECTOR_SIZE        0x3000         // BOOT sector size
#define SETTING_SECTOR_ADDR     0x08003000     // SETTING sector start address 
#define SETTING_SECTOR_SIZE     0x1000         // SETTING sector size
#define APP_SECTOR_ADDR         0x08004000     // APP sector start address  
#define APP_SECTOR_SIZE         0xE000         // APP sector size
#define DOWNLOAD_SECTOR_ADDR    0x08012000     // Download sector start address
#define DOWNLOAD_SECTOR_SIZE    0xE000         // 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)
{
    printf("---------- Enter BootLoader ----------\r\n");
    printf("\r\n");
    printf("======== flash pration table =========\r\n");
    printf("| name     | offset     | size       |\r\n");
    printf("--------------------------------------\r\n");
    printf("| boot     | 0x08000000 | 0x00003000 |\r\n");
    printf("| setting  | 0x08003000 | 0x00001000 |\r\n");
    printf("| app      | 0x08004000 | 0x0000E000 |\r\n");
    printf("| download | 0x08012000 | 0x0000E000 |\r\n");
    printf("======================================\r\n");
}

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;

    uart1_init();
    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:
                printf("start app...\r\n");
                delay_ms(50);
                if (!jump_app(boot_parameter.addr)) 
                {
                    printf("no program\r\n");
                    delay_ms(1000);
                }
                printf("start app failed\r\n");
                break;
            case UPDATE_PROGRAM:
                printf("update app program...\r\n");
                app_addr = APP_SECTOR_ADDR;
                down_addr = DOWNLOAD_SECTOR_ADDR;

                printf("app addr: 0x%08X \r\n", app_addr);
                printf("down addr: 0x%08X \r\n", down_addr);

                printf("erase mcu flash...\r\n");
                mcu_flash_erase(app_addr, APP_ERASE_SECTORS);  
                printf("mcu flash erase success\r\n");
            
                printf("write mcu flash...\r\n");
                // memset(down_buf, 0, sizeof(down_buf));
                for (i = 0; i < APP_ERASE_SECTORS * 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;
                    // printf("mcu_flash_write: %d\r\n", i);
                }
                printf("mcu flash write success\r\n");

                set_boot_state(UPDATE_SUCCESS);
                break;
            case UPDATE_SUCCESS:
                printf("update success\r\n");
                boot_state = UPDATE_SUCCESS_STATE;
                write_setting_boot_state(boot_state);
                set_boot_state(START_PROGRAM);
                break;
            default:
                break;
        }
    }
}

关于bootloader详细的讲解,可以看下我之前发的博客:
STM32 OTA应用开发——自制BootLoader
完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87462312

4 APP的制作

APP部分根据自己实际的功能来做,我这里用的是USB CDC连接PC端,然后传输固件的协议用的是Ymodem。
实际上USB也可以用HID或者其他的,协议也是可以自定义,只要能正确的把固件从PC端搬运到MCU的flash就行了。

示例代码如下:
Ymodem协议部分:
注:详细的协议解析这里就不讲解了,不懂的同学自行查阅资料。

void ymodem_ack(void) 
{
    usb_printf("%c\r", YMODEM_ACK);
}

void ymodem_nack(void) 
{
    usb_printf("%c\r", YMODEM_NAK);
 
}

void ymodem_c(void) 
{
    usb_printf("%c\r", YMODEM_C);
}

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;
                mcu_flash_erase(ymodem.addr, ERASE_SECTORS);
                ymodem_ack();
                ymodem_c();
                ymodem.status++;
            }
            else if (type == '1') 
            {// 为了方便调试,简单发一个字符"1"就可以进入升级模式了
                printf("enter update mode\r\n");
                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:
            usb_printf("C\r\n");
            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);
            printf("firmware download success\r\n");
            printf("system reboot...\r\n");
            delay_ms(2000);
            system_reboot();
            break;
        default:
            break;
    }
}

主函数:

#define APP_VERSION   "V100"
void print_boot_message(void)
{
    printf("======================================\r\n");
    printf("-------------- Enter APP -------------\r\n");
    printf ("app version is: %s\r\n", APP_VERSION);
    printf("======================================\r\n");
}

void user_usb_init(void)
{
    USB_Port_Set(0); 	
    delay_ms(700);
    USB_Port_Set(1);	
    Set_USBClock();   
    USB_Interrupts_Config();    
    USB_Init();	
}

int main(void)
{
    SysTick_Init_Config();
    USART1_Init_Config(115200);
    print_boot_message();
    ymodem_init();
    user_usb_init();
    printf ("app init success\r\n");
	while (1)
	{
        ymodem_handle();
	}
}

修改中断向量:
bootloader的运行地址是在起始地址上的,所以中断向量是0,不用改。
但是app的运行地址是在起始地址上做了偏移的,所以中断向量也要改,不然会运行会出问题。

#define VECT_TAB_OFFSET  0x4000

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

5 烧录下载配置

我们的Bootloader做好以后需要烧录到MCU里面,可以直接用Keil uVison来下载,也可以用J-Flash或者其他,这个都没关系,但是要注意内存的分配,要把固件烧到对应的内存地址上。

1)BootLoader部分:
我这里做出来的bootloader bin只有8K,不过为了方便后续在这部分增加新功能,我实际分配了12K的空间,地址区间是0x08000000-0x08003000。

如果是用keil下载的话,需要注意flash的配置,具体如下:
请添加图片描述
请添加图片描述
如果是用J-Flash或者STlink的工具烧录的话注意烧录的起始地址是0x08000000就好了。

2)APP部分:
跟BootLoader一样,我们按照前面分配好的空间配置APP的参数。
在这里插入图片描述
在这里插入图片描述
如果是用J-Flash或者STlink的工具烧录的话注意烧录的起始地址是0x08004000就好了。

6 运行测试

用串口助手查看运行log(我这里用的是XShell,用其他的也是可以的)。

不需要升级时直接跳转到App区,如下图:
在这里插入图片描述

进入APP之后,往USB发送一个字符"1",进入升级模式,然后通过调试工具发送新固件的bin文件。
注:为了方便调试才用了一个字符"1",实际使用的话最好改一下,太简单的话容易出现误操作。调试工具我用的是XShell,实际上用其他工具也行,只要支持Ymodem方式传输文件即可。
串口调试窗口log如下图:
在这里插入图片描述

USB调试窗口log如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

结束语

好了,关于自制BootLoader并实现USB OTA升级的介绍就讲到这里,本文列举的例子其实只是升级的其中一种方式,只是提供一个思路,不是唯一的方法,实际上最好还是根据自己实际的需求来做。
需要源码的同学可以在下面的链接下载,我把BootLoader和APP都上传了。

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/367960.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Zabbix4.0架构理解-zabbix的工作方式

目录 1.1、zabbix4.0架构图 1.2、zabbix的进程 1、 zabbix server 2、zabbix agent 3、 zabbix proxy 4、 java gateway 5、zabbix get 1.3、zabbix的几种工作方式 1、通过zabbix agent 2、通过zabbix proxy 3、通过 zabbix java gateway 4、其他 1.3、zabbix 数据走…

虹科案例 | Redis企业版数据库:金融行业客户案例解读

传统银行无法提供无缝的全渠道客户体验、无法实时检测欺诈、无法获得业务洞察力、用户体验感较差、品牌声誉受损和业务损失&#xff1f;Redis企业版数据库具有低延迟、高吞吐和可用性性能&#xff0c;实施Redis企业版数据库&#xff0c;可以使金融机构实现即时的客户体验、实现…

python+django篮球NBA周边商城vue

目 录 第一章 绪 论 1 1.1背景及意义 1 1.2国内外研究概况 1 1.3 研究的内容 1 第二章 关键技术的研究 3 2.1 vue技术介绍 3 myproject/ <-- 高级别的文件夹 |-- myproject/ <-- Django项目文件夹 | |-- myproje…

分阶段构建golang运行环境Dockerfile镜像

在开始这项工作之前大家可以先去看一下docker官方给出关于空镜像scratch的说明&#xff0c;采用官方简单的一句话就是&#xff1a;scratch是一个明确的空图像&#xff0c;特别是对于“从头开始”构建图像。分阶段构建镜像就会用到scratch这个空镜像&#xff0c;这样的好处是可以…

pnpm / yarn / npm管理依赖包

pnpm pnpm官网&#xff1a;https://pnpm.io/zh/ pnpm安装方式有很多&#xff0c;详见官网。 用最简单的npm来安装pnpm&#xff1a;npm install -g pnpm pnpm安装依赖包 pnpm install # 安装所有项目中的依赖包 pnpm install vue # 安装依赖到dependencies pnpm in…

硬件系统工程师宝典(11)-----去耦电容布局“有讲究”

各位同学大家好&#xff0c;欢迎继续做客电子工程学习圈&#xff0c;今天我们继续来讲这本书&#xff0c;硬件系统工程师宝典。 上篇我们说到在电源完整性分析的目标就是要做到电源的干净、稳定和快速响应&#xff0c;以及针对不同噪声处理的实现方法。今天我们来看看去耦电容…

JS词法环境和执行上下文

前言 JavaScript是一门解释性动态语言&#xff0c;但同时它也是一门充满神秘感的语言。如果要成为一名优秀的JS开发者&#xff0c;那么对JavaScript程序的内部执行原理要有所了解。 本文以最新的ECMA规范中的第八章节为基础&#xff0c;理清JavaScript的词法环境和执行上下文…

嵌入式常问问题和知识

12、并发和并行的区别&#xff1f; 最本质的区别就是&#xff1a;并发是轮流处理多个任务&#xff0c;并行是同时处理多个任务。 你吃饭吃到一半&#xff0c;电话来了&#xff0c;你一直到吃完了以后才去接&#xff0c;这就说明你不支持并发也不支持并行。 你吃饭吃到一半&…

【ROS学习笔记2】使用vscode开发ROS全流程

【ROS学习笔记2】使用vscode开发ROS全流程 写在前面&#xff0c;本系列笔记参考的是AutoLabor的教程&#xff0c;具体项目地址在 这里 文章目录【ROS学习笔记2】使用vscode开发ROS全流程一、安装终端工具Terminator二、安装VsCode及插件三、使用VsCode开发全流程1、创建工作空…

亚马逊短期疲软,但长期前景乐观

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 由于投资者对亚马逊(AMZN)前景的担忧&#xff0c;导致该公司的股价在过去一年中下跌了39%。然而猛兽财经认为亚马逊近期面临的不利因素只是暂时的&#xff0c;该公司还是有充分的条件可以在医疗保健和物流领域获得重大增长机…

ACM 记忆化搜索

一.记忆化搜索概述 1.概念 搜索是一种简单有效但是效率又很低下的算法结构&#xff0c;其低效的原因主要在于存在很多重叠子问题。而记忆化搜索则是在搜索的基础上&#xff0c;利用数组来记录已经计算出来的重叠子问题状态&#xff0c;进行合理化的剪枝&#xff0c;从而降低时…

高防CDN的知识了解

高防CDN是一种基于CDN&#xff08;内容分发网络&#xff09;技术的网络安全服务&#xff0c;旨在提供高级的防御措施来保护网站或应用程序免受DDoS&#xff08;分布式拒绝服务&#xff09;攻击和其他网络安全威胁。CDN是一种通过将内容分发到全球多个节点来加速网站或应用程序的…

DBC 文件

概述Vector的DBC文件描述了CAN网络的通信规范&#xff0c;通过定义signal可以表示CAN帧中的各个物理信号的含义。通过CANdb Editor软件可以创建和修改DBC文件&#xff0c;一般监控或控制CAN网络内的节点&#xff0c;不需要解析DBC文件里的全部信息&#xff0c;因为有些信息是给…

前端借助Canvas实现压缩base64图片两种方法

一、具体代码 1、利用canvas压缩图片方法一 // 第一种压缩图片方法&#xff08;图片base64,图片类型,压缩比例,回调函数&#xff09;// 图片类型是指 image/png、image/jpeg、image/webp(仅Chrome支持)// 该方法对以上三种图片类型都适用 压缩结果的图片base64与原类型相同// …

02--微信小程序开发流程

开发小程序一般流程&#xff1a;申请小程序帐号安装小程序开发者工具开发小程序提交审核和发布1、注册小程序帐号在微信公众平台官网首页&#xff08;mp.weixin.qq.com&#xff09;点击右上角的“立即注册”按钮。2、填写帐号信息 主体为企业时需要一些信息包括&#xff1a;企业…

狂神说:面向对象(一) —— OOP与方法回顾

OOP详解以类的方式组织代码&#xff0c;以对象的方式组织&#xff08;封装&#xff09;数据什么是面向对象封装 【口袋装数据&#xff0c;留个口&#xff0c;可以用】继承 【儿子和父亲】多态 【同一个事物表现出多种形态】对象和类实际&#xff1a;先有对象后有类代码&#xf…

商城进货记录交易-课后程序(JAVA基础案例教程-黑马程序员编著-第七章-课后作业)

【实验7-2】商城进货记录交易 【任务介绍】 1.任务描述 每个商城都需要进货&#xff0c;而这些进货记录整理起来很不方便&#xff0c;本案例要求编写一个商城进货记录交易的程序&#xff0c;使用字节流将商场的进货信息记录在本地的csv文件中。程序具体要求如下&#xff1a; …

网络编程NIO

Java NIO&#xff08;New IO 或 Non Blocking IO&#xff09;是从Java 1.4 版本开始引入的一个新的IO API&#xff0c;可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。 非阻塞 IO(NIO) 通过Selector去实…

ASP.NET Core MVC 项目 IOC容器

目录 一&#xff1a;什么是IOC容器 二&#xff1a;简单理解内置Ioc容器 三&#xff1a;依赖注入内置Ioc容器 四&#xff1a;生命周期 五&#xff1a;多种注册方式 一&#xff1a;什么是IOC容器 IOC容器是Inversion Of Control的缩写&#xff0c;翻译的意思就是控制反转。 …