在嵌入式开发中,使产品的软件能够更新升级是个很常见的需求,作为开发人员,能够快速熟练地实现该功能是基本能力。

一般当设备不具备直接获取完整的更新文件时,需要逐包获取更新文件,因此要设计专门的更新代码以实现此功能。

在此记录下OTA的常用升级代码,以备不时之需。

1.数据结构

首先先来定义OTA需要的数据结构:

  • 具有更新功能的软件,必须定义软件版本号,以下为一个参考:
1
2
3
4
5
6
7
8
9
10
11
12
//version.h
#ifndef __VERSION_H__
#define __VERSION_H__

#define PROJECT_ID 0x2A4B /**<each project has its own ID*/
#define BOOT_VERSION 0x0 /**<BOOT LOADER version*/
#define STACK_VERSION 0x16 /**<stack version*/
#define USER_APP_VERSION 0x0002 /**<user only allow to change app version*/

#define APP_VERSION ((BOOT_VERSION << 24) | (STACK_VERSION << 16) | USER_APP_VERSION) /**<app code version, only low uin16 can be set by user*/

#endif
  • 获取更新包时需要的变量:
1
2
3
4
5
6
7
8
9
10
uint8_t    updata_ok   =  0;	//更新数据传输完成的标志位,当置1时设备复位升级
uint32_t ota_offset = 16; //更新包请求偏移量,一般前16个字节是更新包的头文件,用来保存版本号、校验码等,从16字节开始是设备更新文件
uint16_t pk = 1024; //一般获取更新包时,每次请求1024个字节,因为一般写flash时是以1024字节为单位
uint16_t x_bk = 0; //请求1024字节数据包的个数
uint16_t remain = 0; //不足1024字节时,最后一包的字节数

UPDATE_HEADER sys_vs_info = {0};//软件版本信息
UPDATE_HEADER ota_vs_info = {0};//ota文件版本信息

uint8_t ota_buff[1024 + 32] = {0}; //接收更新数据
  • 以下为更新包16字节头的参考结构,其中必须要包含的是软件版本号、更新文件长度、校验码:
1
2
3
4
5
6
7
8
9
10
typedef struct
{
uint16 project_id; /**< each project has its unique id */
uint8 root_version; /**<secondary boot update*/
uint8 stack_version; /**<stack update*/
uint32 app_version; /**<app update*/
uint32 code_size; /**< updated code size*/
uint32 crc32; /**<crc32*/
} UPDATE_HEADER;

2.OTA过程

  • OTA开始条件:一般设定一个时间间隔或定时来 check 一次是否有新版本更新,或是接收升级指令更新。check 有符合更新的版本时,即可开始请求。

  • 获取更新数据:如果从其它模块获取更新数据,需要等待模块下载完成存储到数组中后,触发更新请求。OTA请求的第一个包是16字节的头文件,之后是设备更新数据。每次请求数据,offset 是数组的下标,长度为 pk 或 remain,更新数据写入 flash 的预留空间中。

  • 判断接收完成:当接收的总字节数等于头文件的字节数时,即可置位 updata_ok,在合适的时间重启。

  • 完成更新:重启后,进入 bootloader——如果设备有更新功能,则必须有 bootloader,每次设备复位后先进入 bootloader,检查 flash 预留空间是否有更新包可用,如果有则在校验通过后将更新包拷贝到应用程序空间,然后跳转到应用程序。

3.获取更新数据程序

OTA的其它过程没什么说的,主要是获取更新数据的过程,分为发送和接收。发送和接收又分为头16字节数据和剩余数据。在剩余数据中,又分为1024字节整的数据包和最后一个不足1024字节的数据包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
//接收16字节更新头文件
bool check_ota_head(uint8_t *pData,uint16_t lens)//lens = 16
{
bool s = false;
uint16_t sysAppNum = 0;
uint16_t otaAppNum = 0;
uint8_t otaApproot = 0;
uint8_t otaAppstack = 0;
ota_init();//获取软件的版本号等信息
memcpy(&ota_vs_info,pData,lens);//文件头16字节得到文件大小和CRC32
sysAppNum = sys_vs_info.app_version;
otaAppNum = ota_vs_info.app_version;
otaApproot = ota_vs_info.app_version >> 24;
otaAppstack = ota_vs_info.app_version >> 16;
if(ota_vs_info.project_id == sys_vs_info.project_id
&& ota_vs_info.root_version == 0
&& ota_vs_info.stack_version == 0
&& sys_vs_info.root_version == otaApproot
&& sys_vs_info.stack_version == otaAppstack
&& sysAppNum != otaAppNum
&& ota_vs_info.code_size <= (40 * 1024))//版本比较以及其它控制信息
{
s = true;
//更新控制信息,进入bootloader后判断是否更新
write_ota_ctrl(WRITE_OTA_INFO_CTRL);
write_ota_ctrl(WRITE_OTA_FILE_SIZE_CTRL);
x_bk = (ota_vs_info.code_size) / pk;
remain = (ota_vs_info.code_size) % pk;
}
return s;
}

//接收设备更新数据
bool check_bin_file(uint8_t *pData,uint16_t lens)
{
uint8_t i,j,k;
k = lens / 128;//这里flash一次写入128字节,也有512或1024
j = lens % 128;
if(ota_offset < ota_vs_info.code_size)
{
for(i = 0;i < k;i++)
{
ota_write_ota_data(&pData[i * 128],128);//写入flash
}
if(j)
{
ota_write_ota_data(&pData[i*128],j);
}
}
ota_offset += lens;
//接收完成所有bin file复位升级
if(ota_offset >= (ota_vs_info.code_size))
{
dly_us(100000);
write_ota_ctrl(WRITE_OTA_FILE_CRC32_CTRL);
updata_ok = 1;
ota_offset = 16;
}
return updata_ok;
}

//请求设备更新数据数据
uint8_t copy_bin_file(uint8_t *p)
{
uint8_t len = 0;
pk_t pt = {0};
if(ota_offset / pk < x_bk)
{
pt.addr = ota_offset;
pt.pk = pk;
}
else if(remain)
{
pt.addr = ota_offset;
pt.pk = remain;
}
len = sizeof(pk_t);
memcpy(p,(uint8_t*)&pt,len);
return len;
}
//发送请求
void send_uartttl_data(void)
{
uint16_t ask = 0;
uartMsg uMsg = {0};

memset(pub_buf.buf, 0, sizeof(pub_buf.buf));
pst_info_t s_cat1_data = (pst_info_t)&pub_buf.buf;
getUartQueMsg(&uMsg);

switch(uMsg.msgType)
{
case OTA_HEAD:
{
ask = 1;//请求头文件时offset为0,对端响应16字节头文件数据
}
break;

case OTA_DATA:
{
ask = copy_bin_file((uint8_t*)&pub_buf.buf[5]);
}
break;

default:
break;
}

if(ask)
{
s_cat1_data -> head = CAT1_OTA_HEAD;
s_cat1_data -> cmd = uMsg.msgType ;
s_cat1_data -> len = ask;
s_cat1_data -> crc = m_crc8((uint8_t*)&pub_buf.buf[5], ask);
uart0_send((uint8_t*)pub_buf.buf, 5 + ask);
uartQueMsg.w_count += 1;
}
}
//接收数据
void read_uartttl_data(void)
{
uint8_t t_crc = 0;

if(mxd_ttl.priv_len)
{
t_crc = m_crc8(&mxd_ttl.buf[5], mxd_ttl.priv_len - 5);

if(*(mxd_ttl.buf + 4) != t_crc
|| mxd_ttl.buf[0] != CAT1_OTA_HEAD)
{
clear_ttl();
return ;
}

switch(*(mxd_ttl.buf + 1))
{
case OTA_HEAD:
{
if(check_ota_head(&mxd_ttl.buf[5], mxd_ttl.priv_len - 5))
{
set_uart_queue(OTA_DATA);//接收到ota头后,请求发送剩余数据
}
getUartQueMsgNumAdd();
uartQueMsg.w_count = 0;//清空超时
}
break;

case OTA_DATA:
{
if(check_bin_file(&mxd_ttl.buf[5], mxd_ttl.priv_len - 5))
{
//接收完成,复位
}
else
{
set_uart_queue(OTA_DATA);//未接收完整,继续请求发送数据
}

getUartQueMsgNumAdd();
uartQueMsg.w_count = 0;//清空超时
}
break;
}
clear_ttl();
}
}

4.OTA常见的问题

  1. (stm32f103c8t6)传输完第一个1024字节后,mcu写入flash保存,写入之后程序挂死。

    经过检查,有关升级的代码部分没有问题,查看程序大小,发现超出了自己所分配的程序运行空间(分配了25K的程序运行空间,紧接着是25K的升级包存储空间,而实际上程序大小超出了25K达到了30K),导致升级包被写入到了程序运行空间中。

    解决方法是优化程序大小,或者重新分配程序运行空间和升级包存储空间的大小。