目录
什么是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的文章,实现的方式有很多种,感兴趣的同学可以去看一下。
OTA应用开发系列合集:https://blog.csdn.net/ShenZhen_zixian/article/details/129074047
那么这一期我来介绍一下如何自己制作一个BootLoader程序,并且通过串口或者RS485实现OTA升级。
关于STM32以及Keil的环境这里就不具体介绍了,网上教程也很多,不懂的同学自行查阅资料。
在做bootloader之前一定要先想好升级的途径和方式,这样才好规划分区以及制作bootloader。
关于bootloader详细的讲解,可以看下我之前发的博客:
STM32 OTA应用开发——自制BootLoader
分区介绍:
我用的是STM32F407,内存是512K的(想用内存更小的MCU也是可以的,改下各个分区的内存分配就行了)。
注:F4系列的MCU不像F1那样,内存扇区都很大(最少也是16K),而且同一块扇区只能一起擦除,所以就没办法分的那么细了。详细的内存分布可以参考下面的两个图。
STM32F4x扇区分布图如下:

STM32F1x扇区分布图如下:

那么我这里呢,就用一个512k的内存,分成3个区域,来实现一个OTA的功能。
分区表如下:
| 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区,在实际的应该中如果有需要记录一些掉电后还能保存的数据,那么这块区域就可以用得上了。
不管用的是什么MCU,要使用OTA都离不开BootLoader,BootLoader是一个统称,它其实只是一段引导程序,在MCU启动的时候会先运行这段代码,判断是否需要升级,如果不需要升级就跳转到APP分区运行用户代码,如果需要升级则先通过一些硬件接口接收和搬运要升级的新固件,然后再跳转到APP分区运行新固件,从而实现OTA升级。
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 ----------\r\n");
uart_log("\r\n");
uart_log("======== flash pration table =========\r\n");
uart_log("| name | offset | size |\r\n");
uart_log("--------------------------------------\r\n");
uart_log("| boot | 0x%08X | 0x%08X |\r\n", BOOT_SECTOR_ADDR, BOOT_SECTOR_SIZE);
uart_log("| setting | 0x%08X | 0x%08X |\r\n", SETTING_SECTOR_ADDR, SETTING_SECTOR_SIZE);
uart_log("| app | 0x%08X | 0x%08X |\r\n", APP_SECTOR_ADDR, APP_SECTOR_SIZE);
uart_log("======================================\r\n");
}
void print_wait_message(void)
{
uart_log("------- Please enter parameter -------\r\n");
uart_log("[1].Start program\r\n");
uart_log("[2].Update program\r\n");
uart_log("--------------------------------------\r\n");
}
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)\r\n", WAIT_TIMEOUT - timerout);
delay_ms(1000);
timerout ++;
if(timerout >= WAIT_TIMEOUT)
{
set_ymodem_status(START_PROGRAM);
}
break;
case START_PROGRAM:
uart_log("start app...\r\n");
delay_ms(50);
if (!jump_app(APP_SECTOR_ADDR))
{
uart_log("start app failed: app no program\r\n");
delay_ms(1000);
}
break;
case UPDATE_PROGRAM:
ymodem_c();
uart_log("update app program...\r\n");
delay_ms(1000);
break;
case UPDATE_SUCCESS:
uart_log("update success\r\n");
uart_log("system reboot...\r\n");
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[3];
buf[0] = YMODEM_ACK;
buf[1] = 0x0D;
buf[2] = 0x0A;
RS485_Send_Data(buf, 3);
}
void ymodem_nack(void)
{
uint8_t buf[3];
buf[0] = YMODEM_NAK;
buf[1] = 0x0D;
buf[2] = 0x0A;
RS485_Send_Data(buf, 3);
}
void ymodem_c(void)
{
uint8_t buf[3];
buf[0] = YMODEM_C;
buf[1] = 0x0D;
buf[2] = 0x0A;
RS485_Send_Data(buf, 3);
}
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%08X\r\n", APP_SECTOR_ADDR);
mcu_flash_erase(ymodem.addr, APP_ERASE_SECTORS_NUM);
uart_log("erase flash success\r\n");
ymodem_ack();
ymodem_c();
ymodem.status++;
}
else if (type == '1')
{
uart_log("start program now\r\n");
ymodem.process = START_PROGRAM;
}
else if (type == '2')
{
uart_log("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_init(void)
{
RS485_Init(115200);
timer_init();
queue_initiate(&rx_queue);
}
关于bootloader详细的讲解,可以看下我之前发的博客:
STM32 OTA应用开发——自制BootLoader
完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87553496
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("======================================\r\n");
uart_log("-------------- Enter APP -------------\r\n");
uart_log ("app version is: %s\r\n", APP_VERSION);
uart_log("======================================\r\n");
}
int main(void)
{
ota_app_vtor_reconfig();
delay_init(168);
uart_init(115200);
print_boot_message();
led_init();
uart_log ("app init success\r\n");
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
因为我们对stm32的内存进行了分区,不同的代码要存放在不同的区域,因此,我们在编译工程之前需要先定义好各自的区域,以免出现内存越界。
Bootloader的起始地址不需要改,按flash默认地址即可,size需要改成实际分区大小。

APP的起始地址和size都需要根据实际的分区来改。

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

2)使用其他下载工具
如果是用J-Flash或者STlink的工具烧录的话注意烧录的起始地址是0x08000000就好了。
1)使用Keil uVision下载
跟BootLoader一样,我们按照前面分配好的空间配置APP的参数即可。

2)使用其他下载工具
如果是用J-Flash或者STlink的工具烧录的话注意烧录的起始地址是0x08008000就好了。
用串口助手查看运行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 OTA升级的介绍就讲到这里,本文列举的例子其实只是升级的其中一种方式,只是提供一个思路,不是唯一的方法,实际上最好还是根据自己实际的需求来做。我之前也发给几篇OTA相关的文章,用的都是不同的方式,各有各的优点和缺点,感兴趣的同学可以去看一下。
需要源码的同学可以在下面的链接下载,我把BootLoader和APP都上传了。
如果你有什么问题或者有更好的方法,欢迎在评论区留言。
完整代码下载地址:https://download.csdn.net/download/ShenZhen_zixian/87553496
更多相关文章:
OTA应用开发系列合集:https://blog.csdn.net/ShenZhen_zixian/article/details/129074047
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R
是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty