目录
什么是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升级。
关于STM32以及Keil的环境这里就不具体介绍了,网上教程也很多,不懂的同学自行查阅资料。
在做bootloader之前一定要先想好升级的途径和方式,这样才好规划分区以及制作bootloader。
方案介绍:
1)bootloader部分:
运行时从setting里面读一些参数,确定是否需要升级,如果需要,则把download分区的固件搬运到app分区,如果不需要升级则直接跳转到app分区。
2)APP部分:
运行时先连接USB(以USB CDC的方式),然后等待上位机发送升级命令,如果收到命令,则进入下载模式。
我这里图方便,USB传输固件的方式我采用的是Ymodem协议,因为这个协议很多tool都可以用,就不用专门做一个上位机了。如果你想用其他的协议或者自定义协议其实都是可以的,稍做修改就行。

分区介绍:
我用的是STM32F103,内存是128K的(想用内存更小的MCU也是可以的,改下各个分区的内存分配就行了)。
分区表如下:
| name | offset | size |
|---|---|---|
| boot | 0x08000000 | 0x00003000 |
| setting | 0x08003000 | 0x00001000 |
| app | 0x08004000 | 0x0000E000 |
| download | 0x08012000 | 0x0000E000 |

不管用的是什么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
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
我们的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就好了。
用串口助手查看运行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
如果你有什么问题或者有更好的方法,欢迎在评论区留言。
更多相关文章:
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
尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我构建了两个需要相互通信和发送文件的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项目中使用Pow和powifygem。现在我尝试升级我的ruby版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘
我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121
我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden