目录
修改记录
| 序号 | 修改时间 | 修改内容 |
|---|---|---|
| 1 | 2024.4.6 | 初始版本 |
1 摘要
本篇文章介绍了一个基于STM32F103的串口更新固件App程序的Bootloader设计,该设计使用STM32F103C8T6作为主MCU,其串口通过CH340可以连接电脑的USB接口,使用电脑进行STM32的固件更新,并且传输的固件APP的BIN文件使用AES128加密,更加的安全可靠。
效果演示
基于STM32固件更新的Bootloader设计
若是你很懒,不想自己一步一步自己做的话,可以直接去购买本设计的全部源代码。
全家桶:【全家桶】基于STM32固件更新的Bootloader设计
温馨提示:我觉得你看完这篇文章完全可以自己做出来的,没必要花钱的,所以请一定要仔细看下去哦!
2 功能分析
本设计主要分为三大部分:Bootloader、bin文件传输工具、bin文件加密工具,下面先介绍一下整体的流程,再分别介绍一下每个部分的具体流程:
2.1 整体流程
首先通过编译软件将写好的代码生成bin文件,使用加密工具将bin文件加密,电脑连接已经进入到Bootloader中的硬件设备,打开bin文件传输工具,选择设备所连接的串口,开始传输,传输完成,传输完成后,则可以将硬件设备重新上电/复位,跳转到更新后的App程序。
2.2 Bootloader
Bootloader其实说白了就是一段程序,和App程序一样,区别在于Bootloader是在MCU上电后执行的第一段程序,在此段程序中有条件的跳转到App或者在Bootloader中处理数据(像一般程序来说,上电之后执行的第一段程序就是App,不会进行跳转);不知道大家是不是用过Windows的双系统(或2个以上的系统),就是电脑里面有多个系统,每次开机的时候会让你选择进入到哪一个系统,bootloader就像这种,可以根据设定的条件跳转到不同的App中;
在本次设计中,根据硬件上的按钮是否按下,来确定是停留在Bootloader中进行APP固件更新,还是直接跳转到APP中。
1)上电时,按下按钮- 停留在Bootloader中,等待APP固件更新 - 硬件上绿灯常亮 – 开始传输bin文件 — bin文件传输完成 — 固件APP更新完成 — 重新上电/复位;
2)上电时,不按下按钮 - 执行Bootloader,直接跳转到APP中 — 执行APP内容。
2.3 bin文件传输工具设计
bin文件传输工具顾名思义,就是用来传输Bin文件的工具,大致流程:将加密完的App_lock.bin文件放置到指定路径,打开工具,选择连接硬件的串口,开始传输数据,等待传输完成即可,流程相对简单,后续进行详细的介绍。
2.4 bin文件加密工具设计
bin文件加密工具主要用于bin文件的加密,保证App固件的安全性,本文中使用AES128加密,大家也可以用其他的方式进行加密,或者不加密,Bootloader中有对应的操作即可,这部分具体在后续程序设计中详细介绍,大致流程:将App.bin和Key.txt文件放置到指定路径,打开工具,等待加密完成,完成后,会在同级路径下生成一个App_lock.bin文件。
3 硬件设计
3.1 主要器件
1)STM32F103C8T6
使用STM32F103C8T6作为主控MCU,这里可以选择F103系列的其他型号,主要使用到1个串口、4个GPIO即可。
2)CH340
使用CH340 + type-c母座进行USB转串口,其中需要注意引脚少的type-c母座是没有信号的引脚的,只能供电,所以在选型的时候必须选择带信号引脚的母座。
3)供电
供电直接使用type-c母座进行供电(5V),使用AMS-3.3V,将5V转为3.3V为硬件整体供电。
3.2 整体硬件设计
本次硬件设计就很简单了:除了上面说的主要器件外,剩下的就是晶振、按钮、指示灯、电容、电阻等基本器件,图片如下:

4 程序设计
接下来介绍程序设计,主要分为四个部分:传输协议、bin文件加密工具程序、bin传输工具程序、Bootloader程序,下面一一进行介绍:
4.1 传输协议
在开始写程序之前,首先要先定义传输的协议(指:bin传输工具 与 Bootloader),即传输的数据格式,此处可以使用已有的传输协议,像Ymodem协议,也可以使用自己定义协议,像本文就是使用我本人自己定义的传输协议,十分简单。
主要分为4条命令:查询命令、文件信息命令、下载bin文件命令、上载bin文件命令。
注:所有命令都以大端形式传输,且均为16进制。
4.1.1 查询命令
命令: 01 00 06 46 61 6E 4A 69 65 [和校验]
回复: 01 00 01 xx [和校验]
其中,x的含义如下:
| 编码代号 | 含义 |
|---|---|
| 0x00 | 正确回复 |
| 0x01 | 命令输入错误 |
4.1.2 文件信息命令
命令: 02 00 04 xx xx xx xx [和校验]
其中,x表示:文件大小(占4个字节);
回复: 02 00 01 00 [和校验]
4.1.3 下载bin文件命令
命令: 03 xx xx yy yy zz zz zz zz zz zz zz … zz [和校验]
其中:
x表示:数据字节个数(占2个字节),仅包括数据包个数(y)和数据(z);
y表示:数据包个数(占4个字节);
z表示:数据(最大1024字节)。
回复: 03 00 01 xx [和校验]
4.1.4 上载bin文件命令
命令: 04 00 02 yy yy 04
回复: 04 xx xx yy yy zz zz zz zz zz zz zz … zz [和校验]
其中:
x表示:数据字节个数(占2个字节),仅包括数据包个数(y)和数据(z);
y表示:当前数据包(占2个字节);
z表示:数据(最大1024字节)。
注:其中除最后一包数据外,其他的数据帧中的数据(z)个数均为1024字节。
4.2 bin文件加密工具程序
加密工具使用C语言实现,加密方式使用AES128加密,AES128加密的安全性可谓是相当可靠,市场上很多产品的不拆壳更新固件(就是相当于使用BootLoader进行更新)的加密方式就是使用的AES加密,当然此处的加密方式大家也可以选择其他的加密方式,或者干脆就不加密也ok,后续我则按照AES加密进行讲解,不了解AES加密的小伙伴可以自行去搜索学习。
程序主要分为:读取key和iv、读取bin文件、加密,大致流程如下:
4.2.1 读取key和iv
key.txt 文件:

1)读取“key.txt”文件的大小,该文件中存放着加密的key和iv:
// path表示为key.txt的所在的路径
int get_filesize(char* path)
{
int size = 0;
FILE* file;
errno_t err = fopen_s(&file, path, "r");
if (!err)
{
fseek(file, 0, SEEK_END);
size = ftell(file);
fclose(file);
printf("Key文件大小:%d 字节\n", size);
printf("\r\n");
}
else
{
printf("打开文件错误,请检查文件夹中的%s文件!\n", KEY_PATH_USE);
printf("\r\n");
while (1);
}
return size;
}
2)该文件的大小不为0时,申请该文件大小的内存,并将“key.txt”文件的数据内容读出,读取文件使用fread()函数即可:
…
pkey = (unsigned char*)malloc(file_size * sizeof(char)); // pkey为指针、file_size为读取到的key.txt文件大小
…
fopen_s(&file, path, "r");// 打开key.txt文件
…
fread(pkey, sizeof(unsigned char), file_size, file); // 读取key.txt文件内容
…
3)读取完成之后,需要对读取出来的key和iv进行分析,读出出来的是字符形式,需要转换为16进制数,程序如下,以分析key为例,iv同理:
unsigned char analysis_flag = 0;
for (int i = 0; i < file_size; i++)
{
if (buf[i] == 'k')
{
if (buf[i + 1] == 'e')
{
if (buf[i + 2] == 'y')
{
if (buf[i + 3] == ' ')
{
if (buf[i + 4] == ':')
{
if (buf[i + 5] == ' ')
{
for (int j = 0; j < 90;)
{
if (buf[i + 5 + j + 1] == '0')
{
if (buf[i + 5 + j + 2] == 'x')
{
if (buf[i + 5 + j + 3] >= '0' && buf[i + 5 + j + 3] <= '9')
{
aes_key[j / 5] = (buf[i + 5 + j + 3] - '0') << 4;
}
else if (buf[i + 5 + j + 3] >= 'A' && buf[i + 5 + j + 3] <= 'F')
{
aes_key[j / 5] = (buf[i + 5 + j + 3] - 'A' + 10) << 4;
}
else if (buf[i + 5 + j + 3] >= 'a' && buf[i + 5 + j + 3] <= 'f')
{
aes_key[j / 5] = (buf[i + 5 + j + 3] - 'a' + 10) << 4;
}
else
{
break;
}
if (buf[i + 5 + j + 4] >= '0' && buf[i + 5 + j + 4] <= '9')
{
aes_key[j / 5] |= buf[i + 5 + j + 4] - '0';
}
else if (buf[i + 5 + j + 4] >= 'A' && buf[i + 5 + j + 4] <= 'F')
{
aes_key[j / 5] |= buf[i + 5 + j + 4] - 'A' + 10;
}
else if (buf[i + 5 + j + 4] >= 'a' && buf[i + 5 + j + 4] <= 'f')
{
aes_key[j / 5] |= buf[i + 5 + j + 4] - 'a' + 10;
}
else
{
break;
}
j += 5;
if (j/5 >= 16)
{
// 最后一个
analysis_flag |= 0x01;
}
}
else
{
break;
}
}
else
{
break;
}
}
if ((analysis_flag & 0x01) == 0x01)
{
printf("key : ");
for (int x = 0; x < 16; x++)
printf("0x%02x ", aes_key[x]);
printf("\r\n");
}
}
}
}
}
}
}
}
...
4.2.2 读取bin文件
1)读取bin文件与读取上面的key.txt文件类似,但要注意打开文件方式“rb”:
fopen_s(&file, path, "rb");
2)读取bin文件大小该文件的大小不为0时,申请该文件大小的内存,并将“App.bin”文件的数据内容读出,值得注意的是,AES128加密必须是数据必须是16的倍数,所以在申请内存之前,要判断“App.bin”文件大小是否是16的倍数,若不是则要补齐不足的字节,再申请内存。
remainder = bin_size % 16; // 取余,aes128加密bin文件必须是16的倍数
if (remainder != 0)
{
// bin文件不是16的倍数
// 不足的地方补0xFF;
remainder = 16 - remainder;
}
pbin = (unsigned char *)malloc((bin_size + remainder) * sizeof(char));
fopen_s(&file, path, "rb");
fread(pbin, sizeof(unsigned char), bin_size, file);
3)读取App.bin的数据后,后面要是有补的字节,需要将其全部赋值为0xFF,之所以要赋值为0xFF是因为当加密后的App传输给STM32后,由STM32解密后,最后补齐的字节也为0xFF,将其写入到STM32的FLASH中不会发现变化,即相当于未写入数据(FLASH未写入数据时为0xFF)。
void polish_bin(unsigned char* buf,unsigned char num)
{
for (unsigned char i = 0; i < num; i++)
{
buf[i] = 0xFF;
}
}
4.2.3 加密
加密可以直接使用AES128_CBC_encrypt_buffer(),该函数可以在网上搜索aes.c和aes.h文件,里面有函数原型,在此也感谢分享的大佬们。
1)此处再对AES128_CBC_encrypt_buffer()进行一次封装,后续直接调用封装的函数即可,此处使用封装一次的好处就是可以在这个函数中直接修改加密的方式,就可以实现其他的加密方式。
void code(unsigned char * input,unsigned char * output,unsigned int length,unsigned int id)
{
// aes_key、aes_iv为从key.txt读取出来的key和iv。
AES128_CBC_encrypt_buffer(output,input,length, aes_key, aes_iv,id);
}
2)此外,以1024字节为一包进行加密,最后一包可以不为1024字节:
pbin_lock = (unsigned char*)malloc((bin_size + remainder) * sizeof(char));
unsigned int data_num = (bin_size + remainder) / 1024;
unsigned int last_num = (bin_size + remainder) % 1024;
if (last_num != 0)
{
data_num++;
}
else
{
last_num = 1024;
}
for (unsigned int i = 0; i < data_num; i++)
{
if (i == data_num - 1)
{
// 最后一包
code(&pbin[i*1024], &pbin_lock[i * 1024], last_num, i);
}
else
{
code(&pbin[i * 1024], &pbin_lock[i * 1024], 1024, i);
}
}
3)加密完成后,使用write_bin()函数将加密后的数据写入到”App_Lock.bin”,即完成加密。
void write_bin(unsigned char * path, unsigned char * buf, unsigned int num)
{
FILE* file;
fopen_s(&file, path, "wb");
fwrite(buf, sizeof(char), num, file);
// 关闭文件
fclose(file);
}
4)最后,使用free函数将malloc函数申请的内存释放掉即可,当然结束到程序之后,内存也会被释放掉,但是为了养成良好的习惯,最好使用free手动释放掉。
// 释放内存
void free_pbuf(void)
{
if (pbin != NULL)
{
free(pbin);
}
if (pbin_lock != NULL)
{
free(pbin_lock);
}
if (pbin_unlock != NULL)
{
free(pbin_unlock);
}
}
4.2.4 流程演示:
1)首先确保“encryption_app.exe”所在的同级目录下有App.bin 和 key.txt 两个文件,后续在BootLoader部分中会具体介绍如何得到App.bin文件。

2)点击“encryption_app.exe”,会弹出控制台,如图:

注:此处的打印信息为简略形式,像演示视频中则是把bin和加密后的bin文件的数据打印了出来,可以在加密程序中修改。
3)提示“加密完成”则表示也成功完成加密,同时会在该目录下生成“App_lock.bin”,该文件则为加密后的bin文件。

4.3 bin文件传输工具程序
传输工具同加密工具一样也是使用C语言实现,传输的数据格式为4.1中介绍的自定义协议,程序主要分为:串口选择、读取bin文件、查询设备命令、文件基本信息、下载bin文件命令、上载bin文件命令几个部分,流程如下:
4.3.1 串口选择
1)串口这部分首先需要手动输入COM号,输入后,判读其是否在1-20之间,这步同时也可以过滤掉输入非数字的值,若不在1-20之间,则重新提示并输入COM号,此处还可能会出现一个问题,设备连接到电脑之后,显示的COM号就以及大于20了,like this :

这种情况下需要将其修改到20以内即可。
不会修改的同学可以看这个:如何更改电脑外部串口端口COM号
重新回到程序部分:可以通过do while语句实现:
do {
input_flag = 0;
printf("请输入要使用的串口:(COM1~20)\r\n");
scanf_s("%d", &comnum);
if (comnum > 20 || comnum < 1) {
printf("输入的串口不在1~20之间\r\n");
printf("\r\n");
input_flag = 1;
}
} while (input_flag != 0);
2)输入正确后,程序会打开这个串口,通过CreateFile()函数实现:
// 打开串口
serial_port = CreateFile (com, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0);
if (serial_port == INVALID_HANDLE_VALUE)
{
printf("无法打开串口COM%d。\r\n", comnum);
printf("\r\n");
com_flag = 1;
}
else
{
printf("已打开串口COM%d。\r\n", comnum);
}
其中值得注意的是CreateFile函数的第一个参数:com ,在window 64位系统中,这个参数被定义为wchar_t(宽字符型),而32位系统里面是char,最开始我在网上查到的一些资料,都是用的char类型,而在64位中是无法打开串口的,所以在这里需要对com进行操作,而又因为需要输入具体的COM号,也就表示com这个参数不是固定,而是根据输入而变换的,所以就需要sprintf函数将”COM”与输入的COM号拼接到一起,在前面也说了com是wchar_t类型,所以需要使用wsprintf函数(宽字符的拼接函数),具体实现:
wchar_t com[20];
wsprintf(com, L"COM%d", comnum); // L表示后面的字符串是宽字符,comnum为输入的具体com口号;
3)串口配置:
串口配置主要就将串口配置以为115200-8-N-1即可,程序如下:
// 配置串口
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(serial_port, &dcbSerialParams))
{
printf("无法获取串口配置\r\n");
CloseHandle(serial_port);
return 1;
}
dcbSerialParams.BaudRate = 115200; // 波特率
dcbSerialParams.ByteSize = 8; // 数据位
dcbSerialParams.StopBits = ONESTOPBIT; // 停止位
dcbSerialParams.Parity = NOPARITY; // 校验位
if (!SetCommState(serial_port, &dcbSerialParams))
{
printf("无法应用串口配置\r\n");
CloseHandle(serial_port);
return 1;
}
// 设置超时和缓冲区
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 500;
timeouts.WriteTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
if (!SetCommTimeouts(serial_port, &timeouts))
{
printf("无法设置超时\r\n");
CloseHandle(serial_port);
return 1;
}
4.3.2 读取bin文件
同加密工具的一致,仅是打开的文件从“App.bin”变成了“App_lock.bin”,所以在此就不再赘述了。
4.3.3 查询设备命令
这条命令很简单,发送数据等待回复即可。
所以在这里说一下,如何使用串口发送数据和接收数据。
1)发送数据
发送数据主要是使用WriteFile()函数,将其封装成一个send_data()即可:
// 两个形参array指向要发送的数据数组,num表示要发送的数据个数
void send_data(unsigned char* array, unsigned int num)
{
OVERLAPPED overlappedWrite = { 0 };
DWORD bytesWritten;
if (WriteFile(serial_port, array, num, &bytesWritten, &overlappedWrite))
{
...
}
else
{
if (GetLastError() == ERROR_IO_PENDING)
{
// 等待写入操作完成
if (GetOverlappedResult(serial_port, &overlappedWrite, &bytesWritten, TRUE))
{
...
}
else
{
printf("数据写入失败");
}
}
else
{
printf("数据写入失败");
}
}
}
2)接收数据
串口接收数据主要ReadFile()函数,并对其封装成一个recv_data,该函数无形参,返回值为接收到的字节个数,本程序中的接收函数相当于阻塞式接收,会一直等待串口接收到数据。
DWORD recv_data(void)
{
DWORD bytesRead = 0;
// 等待串口事件
delay_FJ(1000);
ReadFile(serial_port, recv_buf, sizeof(recv_buf), &bytesRead, NULL);
if (bytesRead != 0)
{
// 接收到数据
...
}
return bytesRead;
}
4.3.4 文件信息命令
同“查询命令”,该命令也是发送数据,等待回复,没有太多的难点,此命令主要发送的数据为所要传输的bin文件大小。
4.3.5 下载bin文件
下载命令和后面的上载命令,属于是核心的两个命令,该命令分为以下几步:
1)算出总共要发送的数据包个数和最后一包数据的数据个数
前面说过除了最后一包数据,其他的数据包的数据长度固定为1024字节,所以用bin文件的总大小除以1024就能得到数据包个数,再通过判断是有余数,来确定是否需要加一:
data_pack_all = file_size / 1024; // 总数据包数
last_pack_num = file_size % 1024; // 最后一包数据个数
if (last_pack_num)
{
// 有余数
data_pack_all++;
}
else
{
last_pack_num = 1024;
}
2)循环发送数据即可,直到最后一包数据发完,这个过程中,每次发送完后一包数据后,会等到设备回复,收到正确回复才会继续发送命令。
while (sending)
{
if (data_pack_now == data_pack_all - 1)
{
// 最后一包
// 数据长度
send_buf[1] = (unsigned char)((last_pack_num + 2) >> 8);
send_buf[2] = (unsigned char)((last_pack_num + 2) >> 0);
// 数据
for (int i = 0; i < last_pack_num; i++)
{
send_buf[5 + i] = pbin[data_pack_now * 1024 + i];
}
// 发送数据个数
send_num = 1 + 2 + 2 + last_pack_num + 1;
}
else
{
// 数据长度
send_buf[1] = (unsigned char)(1026 >> 8); // 1024 + 2
send_buf[2] = (unsigned char)(1026 >> 0);
// 数据
for (int i = 0; i < 1024; i++)
{
send_buf[5 + i] = pbin[data_pack_now * 1024 + i];
}
// 发送数据个数
send_num = 1 + 2 + 2 + 1024 + 1;
}
// 当前数据包数
send_buf[3] = (unsigned char)(data_pack_now >> 8);
send_buf[4] = (unsigned char)(data_pack_now >> 0);
send_buf[send_num - 1] = sum_add(send_buf, send_num);
send_data(send_buf, send_num);
while (1)
{
//等待回复
}
}
此处也可以直接用for循环,以data_pack_all(总数据包数)做判断条件也可以。
4.3.6 上载bin文件命令
上载bin文件是指从设备中读取下载进去的bin文件,与实际下载的bin文件数据进行对比,查看是否一致,一致则成功完成更新,有误则提示出来。
整体是一个for循环,以data_pack_all作为判断条件:
for (unsigned short i = 0; i < data_pack_all; i++)
{
// 发送命令
...
While(1)
{
// 等待回复
}
}
在每次循环中,首先使用send_data函数发送命令即可。
发送完数据后,等到回复,并将收到回复与写入的数据进行比对:
// 等待接收回复,并判断
recv_num = recv_data();
if (recv_num != 0)
{
// 验证接收数据
if (check_sum(recv_buf, recv_num))
{
// 和校验正确
if (recv_buf[3] == 0)
{
if (i == (data_pack_all - 1))
{
// 最后一包数据
// 判断接收到数据个数
// 最后一包数据应该接受到数据应为:1(命令号)+ 2(数据长度)+ 2(当前数据包) + last_pack_num(数据不定长)+ 1(校验位) = last_pack_num + 6 字节
// 或者用“数据长度”+ 1(命令号)+ 1(校验位),也可以。
if (recv_num == last_pack_num + 6)
{
// 接收数据个数正常
err = 0;
// 数据
for (int j = 0; j < last_pack_num;j++)
{
if (recv_buf[5 + j] != pbin[i * 1024 + j])
{
// 数据验证有误
err = 1;
break;
}
}
if(err == 1)
{
// 数据验证有误
printf("数据验证错误,请重新烧录bin文件。\r\n");
while (1);
}
else
{
// 数据验证正确
printf("%d / %d\r\n",i+1, data_pack_all);
}
}
else
{
// 接收数据个数有误
printf("接收数据个数有误,请重新烧录bin文件\r\n");
while (1);
}
}
else
{
// 非最后一包数据
// 判断接收到数据个数
// 非最后一包数据应该接受到数据应为:1(命令号)+ 2(数据长度)+ 2(当前数据包) + 1024(数据)+ 1(校验位) = 1030字节
if (recv_num == 1030)
{
// 接收数据个数正常
err = 0;
// 数据
for (int j = 0; j < 1024; j++)
{
if (recv_buf[5 + j] != pbin[i * 1024 + j])
{
// 数据验证有误
err = 1;
break;
}
}
if (err == 1)
{
// 数据验证有误
printf("数据验证错误,请重新烧录bin文件。\r\n");
while (1);
}
else
{
// 数据验证正确
printf("%d / %d\r\n", i + 1, data_pack_all);
}
}
else
{
// 接收数据个数有误
printf("接收数据个数有误,请重新烧录bin文件\r\n");
while (1);
}
}
break;
}
else
{
printf("命令输入错误,请重新烧录bin文件\r\n");
while (1);
}
}
else
{
printf("和校验错误,请重新烧录bin文件\r\n");
while (1);
}
break;
}
4.4 Bootloader程序
终于来到了最核心的内容:Bootloader程序设计,说是Bootloader,其实就和正常的App设计一样,只不过在其程序中实现了一个跳转功能,像大家现在写的程序一般都是上电之后直接就执行App内容,并循环实现,而Bootloader则是一段程序可以执行某些操作(像本文章中就是可以更新App),然后跳转至App,Bootloader大致流程:

通过上电或复位时是否按下按钮进行判断是否停留在Bootloader中进行App更新,所以下面分为两个分支进行展开介绍:直接跳转至App(不按下按钮)和更新App(按下按钮)。
4.4.1 直接跳转至App
直接跳转这部分很简单,属于是标准流程了,所以先进行介绍,主要是通过Jump_To_Application进行跳转,要记住其中的__disable_irq()关中断函数,后续要用到(敲黑板!!):
// 判断跳转地址是否合法
if(((*(__IO uint32_t *)APP_START_ADDR) & 0x2FFE0000) == 0x20000000)
{
// 关中断
__disable_irq();
JumpAddress = *(__IO uint32_t *)(APP_START_ADDR + 4);
// 初始化APP堆栈指针
__set_MSP(*(__IO uint32_t *)APP_START_ADDR);
Jump_To_Application = (pFunction)JumpAddress;
Jump_To_Application();
}
else
{
// 绿灯闪烁
}
其中APP_START_ADDR为宏定义,要求为芯片的Flash区域,本程序中使用0x08002000作为App的起始地址。
4.4.2 更新App
更新App部分主要分为三大部分:串口初始化、串口接收数据、处理数据并回复。
1)串口初始化
串口初始化这部分相对简单,就是正常的STM32串口初始即可,硬件上使用串口一(PA9\PA10),所以对应的程序中要使能这两个引脚,另外并要使能串口接收中断和空闲中断:
// GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
// 配置GPIO的模式和IO口
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;// TX
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;// RX
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; // 模拟输入
GPIO_Init(GPIOA,&GPIO_InitStructure);
// USART1 初始化设置
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, DISABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_ClearFlag(USART1, USART_FLAG_TC);
USART_ClearFlag(USART1, USART_FLAG_IDLE);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);// 接收中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);// 空闲中断
2)串口接收数据
接收数据部分主要是使用串口接收中断和空闲中断,每触发一次接收中断,将接收到数据存到接收数组中,触发空闲中断则表示这一帧数据接收完成,后续对接收数组中的数据进行处理。
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
recv[recv_temp++] = USART_ReceiveData(USART1); //读取接收到的数据
if(recv_temp >= RECV_NUMBER) // 防止溢出
{
recv_temp = 0;
Clear_Array(recv,RECV_NUMBER);
}
}
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //空闲中断
{
USART1->SR; // 清除空闲中断
USART1->DR; // 清除空闲中断
recv_num = recv_temp; // 接收数据个数
recv_temp = 0;
recv_flag = 1;
}
3)处理数据并回复
当触发串口空闲中断后,对接收到的数据进行处理,首先对和校验进行验证:
unsigned char check_sum(unsigned char * buf,unsigned short num)
{
unsigned char sum = 0;
for(int i = 0; i<num-1; i++)
{
sum += buf[i];
}
if(buf[num-1] == sum)
{
return 1; // 和校验正确
}
else
{
return 0; // 和校验错误
}
}
和校验正确后,验证数据长度:
unsigned char check_datanum(unsigned char * buf,unsigned short num)
{
unsigned short temp;
temp = buf[1] * 256 + buf[2];
if(temp == (num - 4)) // 减去1字节命令号、2字节数据长度、1字节和校验
{
return 1;
}
else
{
return 0;
}
}
数据个数验证正确后,开始对命令号进行处理,不同的命令号做出不同的处理以及回复,命令号就是之前介绍过的四条命令:查询命令、文件信息命令、下载bin文件命令、上载bin文件命令,其中查询命令很简单,收到命令后直接回复固定报文即可,在此不过多赘述,后续主要介绍后面3条命令:
(1)处理文件信息命令
命令: 02 00 04 xx xx xx xx [和校验]
其中:x表示:文件大小(占4个字节);
回复: 02 00 01 00 [和校验]
该命令主要是用于获取所要接收的Bin文件大小,并计算出总数据包数和最后一包数据个数,后用于后续的上载bin文件命令的处理。
void file_information_02H(unsigned char *buf)
{
file_size = buf[6];
file_size |= buf[5] << 8;
file_size |= buf[4] << 16;
file_size |= buf[3] << 24;
all_num = file_size / 1024;
last_data = file_size % 1024;
if(last_data != 0)
{
// 有余数
all_num ++;
}
else
{
last_data = 1024;
}
...
}
(2)处理下载bin命令
命令: 03 xx xx yy yy zz zz zz zz zz zz zz … zz [和校验]
其中:
x表示:数据字节个数(占2个字节),仅包括数据包个数(y)和数据(z);
y表示:数据包个数(占4个字节);
z表示:数据(最大1024字节)。
回复: 03 00 01 xx [和校验]
这部分主要是将收到数据写到flash中即可,相对也简单一些,首先先将写入flash函数封装成一个函数,直接调用即可:
// 写入flash数据
// buf : 要写入的数据起始地址
// num :当前数据包数
// lenght :写入的数据长度
void write_flash(unsigned char * buf,unsigned short num,unsigned short lenght)
{
unsigned short * pdata = (unsigned short *)buf;
unsigned int address = APP_START_ADDR + num * 1024;
FLASH_Unlock();
FLASH_ErasePage(address);
for(unsigned short i = 0;i<lenght;i += 2)
{
FLASH_ProgramHalfWord(address,*pdata);
address += 2;
pdata ++;
}
FLASH_Lock();
}
整体实现:
void download_bin_03H(unsigned char *buf)
{
unsigned short data_num; // 数据个数
unsigned short now_num; // 当前数据包
data_num = (buf[1] * 256 + buf[2]) - 2; // 减去2字节的数据包个数
now_num = buf[3] * 256 + buf[4];
memcpy(input,&buf[5],data_num);
// 解密
decode(input,output,data_num,id_num);
// 写入flash
write_flash(output,now_num,data_num);
if(now_num == 0) // 第一包数据
{
id_num = 1;
}
else if(now_num == (all_num - 1))
{
id_num = 0;
}
...
}
其中的decode()解密函数,同上面在加密工具中使用code()加密函数一样,都是对aes.c中断函数进行再一次的封装:
// 解密
void decode(unsigned char * input,unsigned char * output,unsigned int length,unsigned int id)
{
AES128_CBC_decrypt_buffer(output,input,length,aes_key,aes_iv,id);
}
此处的aes_key和aes_iv为解密和解密使用的key和iv,直接在c文件定义:
const unsigned char aes_key[16] = {0x21,0x72,0x13,0x41,0x06,0x9b,0xff,0x34,0x7b,0x94,0x2f,0x9c,0x51,0x5f,0x81,0x3f};
const unsigned char aes_iv[16] = {0x84,0x2a,0xe0,0xa9,0xe4,0xae,0x3c,0x3e,0x41,0xb8,0x4c,0xcf,0x97,0x84,0xaa,0xc2};
加密工具文件中的“key.txt”的key和iv要与此处的key和iv一致才能正确的解密。
另一个值得注意的地方是id_num,这个数值对应decode或者code的函数中最后一个形参id,该数值在第一次加密/解密的时候为0,后续应为非0。
(3)处理上载bin命令
命令: 04 00 02 yy yy 04
回复: 04 xx xx yy yy zz zz zz zz zz zz zz … zz [和校验]
其中:
x表示:数据字节个数(占2个字节),仅包括数据包个数(y)和数据(z);
y表示:当前数据包(占2个字节);
z表示:数据(最大1024字节)。
上载命令处理相对于前面的几个命令复杂一些,需要判断此次要读取数据包是否是最后一包,最后一包数据个数可能不足1024字节所以要做特殊处理。
void upload_bin_04H(unsigned char *buf)
{
unsigned short now_num; // 当前数据包
...
now_num = buf[3] * 256 + buf[4];
if(now_num == (all_num - 1))
{
// 最后一个数据包
read_flash(input,now_num,last_data);
send_buf[1] = (last_data + 2) / 256;
send_buf[2] = (last_data + 2) % 256;
send_num += last_data;
}
else
{
read_flash(input,now_num,1024);
send_buf[1] = (1024 + 2) / 256;
send_buf[2] = (1024 + 2) % 256;
send_num += 1024;
}
// 加密
code(input,output,(send_num - 6),id_num);
if(now_num == (all_num - 1))
{
// 最后一个数据包
memcpy(&send_buf[5],output,last_data);
}
else
{
memcpy(&send_buf[5],output,1024);
}
if(now_num == 0) // 第一包数据
{
id_num = 1;
}
else if(now_num == (all_num - 1))
{
id_num = 0;
}
...
}
(4)回复数据
回复数据则是指串口发送数据,此处将串口发送单个字节数据封装独立的函数,每次调用即可。
// 发送单个字节
void sendByte(USART_TypeDef *USARTx, u16 byte)
{
USART_ClearFlag(USARTx, USART_FLAG_TC); //软件清除发送完成标志位
USART_SendData(USARTx, byte); //发送一个字节
while (!USART_GetFlagStatus(USARTx, USART_FLAG_TC));//等待发送完成
USART_ClearFlag(USARTx, USART_FLAG_TC); //软件清除发送完成标志位
}
// 发送数据
void sendData(unsigned char *array,unsigned short num)
{
for(unsigned short i = 0; i < num; i++)
{
sendByte(USART1,array[i]);
}
}
使用例子,以查询命令的回复为例:
回复:01 00 01 xx [和校验],xx此处取值0,表示正确回复。
send_buf[0] = 0x01;
send_buf[1] = 0x00;
send_buf[2] = 0x01;
send_buf[3] = 0x00;
send_buf[4] = add_sum(send_buf,4);
send_num = 5;
sendData(send_buf, send_num);
其中add_sum为计算和校验:
unsigned char add_sum(unsigned char * buf,unsigned short num)
{
unsigned char sum = 0;
for(int i = 0; i<(num-1);i++)
{
sum += buf[i];
}
return sum;
}
4.5 测试App设计
本部分主要是设计两个简单的App程序用于测试Bootloader是否可以跳转App以及更新App的功能,其中App1实现:硬件上的橙色灯(LED4)作为呼吸灯亮灭,并且通过向其串口发送01 00 01(16进制),App1会回复”This is App1 !”,App2整体上与App1相似:硬件上的红色灯(LED3)作为呼吸灯亮灭,并且通过向其串口发送01 00 01(16进制),App2会回复”This is App2 !”,以此作为区分不同的App。
本部分主要是以App1为主进行介绍,主要分为几个部分:Keil的工程设置、App程序设计、生成bin文件等。
4.5.1 Keil的工程设置
1)点击工程设置按钮,如图:

2)这个按照Bootloader中跳转的App起始地址进行修改,本文使用的地址为0x08002000,文件大小的根据实际大小设置即可。

3)工程设置完成后,对应的代码中,也需要设置,有两种方式:
(1)在void SystemInit (void)函数中找到SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; 将VECT_TAB_OFFSET的宏定义进行修改:#define VECT_TAB_OFFSET 0x0 默认情况为0,此数值相当于对于FLASH起始地址(0x080000000)的偏移量,像本文章中使用的地址是0x08002000,所以对应的偏移量就是0x2000,将其改成#define VECT_TAB_OFFSET 0x2000即可。
(2)第二种方法与第一种方法原理上是一致的,不过更加的简单粗暴,直接在main函数起始位置加上SCB->VTOR = FLASH_BASE | 0x2000;即可。
int main(void)
{
SCB->VTOR = FLASH_BASE | 0x2000;
...
// 其他函数实现。
}
4.5.2 App程序设计
因为仅是用于测试,所以App程序设计的实现就很简单了,主要用到了两部分功能:定时器输出PWM(用于呼吸灯),串口收发数据,下面就简单的介绍一下:
1)定时器输出PWM
首先是初始化定时器以及输出的引脚(连接LED):
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
// 配置GPIO的模式和IO口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_TimeBaseInitStructure.TIM_Period = per; //自动装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = psc; //分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //设置向上计数模式
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OC2Init(TIM3,&TIM_OCInitStructure); //输出比较通道2初始化
TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable); //使能TIMx在 CCR2 上的预装载寄存器
TIM_ARRPreloadConfig(TIM3,ENABLE);//使能预装载寄存器
TIM_Cmd(TIM3,ENABLE); //使能定时器
初始化完成之后,直接使用TIM_SetComparex()函数进行控制输出PWM波即可,其中x为通道数,像本程序中使用的是定时器3通道2,则使用:TIM_SetCompare2(TIM3,…);
此外,因为本身就是使用指示灯表示是哪一个App在运行,所以也可以不用做成呼吸灯,直接LED常亮也OK,这样不用使用到定时器,程序更加的简单。
2)串口收发数据
此部分同样是使用串口1的接收中断和空闲中断,同上面Bootloader中串口部分一致,在此不在重复介绍,简答说下回复的函数:
查看是否收到 01 00 01 ,收到则进行回复。
unsigned char send_version[] = "This is App1 !";
void check_version(unsigned char *buf)
{
if(recv_num == 3)
{
if(buf[0] == 0x01)
{
if(buf[1] == 0x00)
{
if(buf[2] == 0x01)
{
sendData(send_version,(sizeof(send_version)-1));
}
}
}
}
}
不知道大家还记不记得上面敲黑板让大家在介绍Bootloader中记住的__disable_irq()关中断函数(4.4.1),终于在这部分要用到了:因为在Bootloader中跳转到App前使用了__disable_irq()函数,将全局中断关闭,在跳转到App中,不做处理的话,全局中断同样是关闭的,这样就会出现明明已将使能了具体的中断,但是程序无法进入到中断的情况,在本程序中具体的表现就是:串口向设备发送了01 00 01 ,但是程序无法进入到串口中断中,也就不会回复“This is App1 !”。
解决的方法也简单:既然全局中断别关闭了,再打开就好了嘛!使用__enable_irq()函数即可打开全局中断,在使用到中断前打开即可:
int main(void)
{
SCB->VTOR = FLASH_BASE | 0x2000;
// 初始化函数
...
__enable_irq(); // 使能全局中断
while(1)
{
// 主循环
...
}
}
4.5.3 生成Bin文件
由于Keil默认编程生成的是Hex文件,所以需要将这个生成的Hex文件转换成Bin文件,再用于加密和传输。
首先Keil好像是可以直接生成的bin文件的,但是我在网上找了一些介绍的文章简单试了试,但是都没成功,可能是我的操作有问题,所以直接生成的方法我也不知道(哭笑),对使用Keil直接生成Bin感兴趣的小伙伴只能自己去网上找找方法了,我实在是爱莫能助。
所以,在这里我提供另一个方法,使用JFLASH将Hex文件转换成Bin文件:
JFLASH下载地址:点击此处跳转至官方网站
1)打开JFLASH(哪一个版本都行),弹出的“Welcome to J-Flash”窗口直接关掉即可。
2)将keil编译生成的hex文件拖入JFLASH中,拖入之后,此处起始地址应该为你所设置的App起始地址:

3)点击“File”下的“Save data file as…”:

4)在弹出的提示框中,选择所以保存的路径,并将其保存类型修改为.bin即可。

5 总结
到这里,整个设计都全部介绍完成,总体来说,本次设计并不是很难,但是因为要设计程序有加密工具、传输工具、Bootloader、App几部分,整体上的工作量还是蛮大的,目前加密工具、传输工具两个小工具都是使用的控制台方式,看起来并不是很美观,所以后续待我学一学图形界面,重新做一下这两个小工具,使其更加美观和便捷。
此外,本次更新使用的串口进行传输数据进行更新,由此也可以扩展出其他的更新方式,像以太网(有线)传输、WIFI/蓝牙(无线)传输数据等,思路是一样的,大家可以自行设计。
1430

被折叠的 条评论
为什么被折叠?



