本文章是我摸索软件仿真的一些笔记思路。

写在前面

当你看到这篇文章的时候,你会很困惑,为啥,要在PC上模拟单片机的Bootloader设计呢?这人是不是太清闲了,得给他来点活儿!好吧,也许我可能走了一条奇怪得路径。
先说需求与计划:

  • 我想将嵌入式的通用设计、通用软件模块或者框架挪到PC端进行直接开发并辅助开发上层应用。
  • 所有高级产品都会做软件升级的要求,无论是通过蓝牙、wifi还是USB、CAN这些接口;嵌入式产品,程序升级的目的都是一致的。所以这部分内容我想剥离开来统一设计。

经常看我文章的读者应该有印象,在一期TinyUSB章节使用uf2格式文件U盘拖拽式更新程序那一篇文章,其实就是一种Bootloader以及软件升级的应用。感兴趣的还可以回过去看那篇文章。

一种简易的软件升级机制设计

有关Bootloader的内容,我想大家应该不是陌生的,毕竟软件升级无处不在,在之前有一章关于TinyUSB模拟U盘拖拽升级就是一个典型的Bootloader应用,感兴趣可以传送过去阅读。

使用PC端C语言程序模拟单片机的流程

借助于上面的软件升级机制,我们要明确下面三个要素:

  • 公共的配置文件,用来配置当前的应用程序是否有效以及是否请求更新程序等信息
  • Bootloader.exe程序,这个程序模拟单片机上电运行
  • App.exe程序,该程序为实际应用程序
  • 升级数据的类型,实际中可以CAN、USB,只要是通信口就行;这里模拟我暂时使用串口作为通信通道。

公共配置文件

公共配置文件,在实际单片机应用中,应该存放在公共数据区,可以是eeprom或者flash作为载体。在PC端模拟以文件的形式给出,我们暂定使用json数据格式文件作为配置文件。
有关json数据的读写以及解析可以参照之前的文章。

通信COM口

如果不太熟悉C语言实现串口通信,可以借助于AI的帮助,现在都挺好用的。

Bootloader.exe的实现

按照流程图,在上电复位后,程序首先应该直接读取配置信息,判断当前是否需要升级,并且应用程序是否合法有效。如果无需升级操作并且程序有效,直接跳转app程序。在Windows平台,我使用system函数执行额外的exe程序,这是一个挂起式调用,直到程序退出才会执行下一行的内容;所以你会看到,当应用程序退出即是复位,这跟单片机端很像。

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
int main(void) {
while (1) {
RESET:
printf("-----Chip Reset-----\n");
ReadGlobalConfiguration();
if (request_update == false && app_is_valid == true) {
system("app_demo.exe");
goto RESET;
}

if (request_update == true && app_is_valid == true) {
request_update = false;
}

/**************下面是Bootloader的程序内容****************/
printf("[BOOT] This is Bootloader Program\n");

ComInit();

while(1) {
if(CheckJumpToApplication()) {
CloseHandle(hSerial);
request_update = false;
/*重新写入配置标志到json文件*/
WriteGlobalConfiguration();
break;
}
}
}
}

如果两个条件其中一个不满足,就在当前的bootloader程序中,等待软件升级或者等待重新跳转app的指令;

Application.exe的实现

在应用程序中,主要就是执行应用功能,并根据命令判断是否需要复位进入(Bootloader)进行软件升级;如果需要,会在跳出之前,更新配置信息,请求升级的标志会被更新。

1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {
printf("[APP] This is Application Program\n");
ComInit();
while (1) {
if(CheckJumpToBootloader()) {
CloseHandle(hSerial);
/*重新写入配置标志到json文件*/
WriteGlobalConfiguration();
break;
}
}
}

通过串口传输程序文件实现升级完整流程

处理数据传输与写入数据的过程是由Bootloader实现的,我们只需要在上面原有代码的基础上增加数据接收写入到文件的逻辑:

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
static bool trig_file = false;

bool CheckJumpToApplication(void) {
bool ret = false;
char szBuff[128];
DWORD dwBytesRead = 0;
if (ReadFile(hSerial, szBuff, sizeof(szBuff), &dwBytesRead, NULL)) {
if (dwBytesRead > 0) {
szBuff[dwBytesRead] = '\0'; // 添加终止符
// printf("Get data command: %s\n", szBuff);
if (szBuff[0] == 'A' && szBuff[2] == 'P') {
ret = true;
printf("[BOOT] request to jump tp application\n");
} else if (szBuff[0] == 'V' && szBuff[4] == 'D') {
app_is_valid = true;
printf("[BOOT] updated and app is valid\n");
} else if (szBuff[0] == 'I' && szBuff[1] == 'N') {
app_is_valid = false;
printf("[BOOT] updated and app is invalid\n");
} else if (szBuff[0] == 'F' && szBuff[3] == 'E') {
trig_file = true;
printf("[BOOT] request data of .exe file\n");
}
}
}
return ret;
}

bool AppUpdate(void) {
bool ret = false;
char szBuff[512000]; // 分配512K缓冲区
DWORD dwBytesRead = 0;
bool trig_file = false;
if (ReadFile(hSerial, szBuff, sizeof(szBuff), &dwBytesRead, NULL)) {
if (dwBytesRead > 0) {
szBuff[dwBytesRead] = '\0';
printf("[BOOT] Get file size: %d\n", dwBytesRead);
ret = true;
}
}

if (ret == true) {
app_is_valid = false;
/* open json file */
FILE *file = NULL;
file = fopen("app_demo.exe", "wb");
if (file == NULL) {
printf("Open file failed!\n");
ret = false;
}

/* write json file */
if (ret == true) {
int ret = fwrite(szBuff, sizeof(char), dwBytesRead, file);
if (ret == 0) {
printf("write to file failed!\n");
ret = false;
}

fclose(file);
}
}
return ret;
}

我这里写的比较简单,纯粹验证可行性;不能忘记在main的执行中调用AppUpdate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**************下面是Bootloader的程序内容****************/
printf("[BOOT] This is Bootloader Program\n");

ComInit();

while(1) {
if (CheckJumpToApplication()) {
CloseHandle(hSerial);
// printf("Need Jump to Application\n");
request_update = false;
/*重新写入配置标志到json文件*/
WriteGlobalConfiguration();
break;
}
if (trig_file == true) {
AppUpdate();
trig_file = false;
}
}

工程代码与效果演示

源码demo

有关工程代码,由于比较小众,我久不花时间放gitee之类平台,感兴趣的话可以后台私信我加群获取。

演示

写在后面

关注久的朋友知道,笔者做汽车电子开发工作,所以在汽车电子中,CAN线LIN线的软件升级刷写占了软件开发的很大一部分。这也是我想剥离它,使用通用平台模拟的原因。为了以后开发提高效率。如果有想学习或者交流的,推荐大家后台私信我聪拌面技术交流,我拉大家入群。