STM32 IAP固件升级实验系列文章
- 一、Flash和RAM的区域划分、工程建立、程序分散加载、程序烧写
- 二、Stm32 bootloader、application、firmware 程序的分析和编写
- 三、使用DMA收发串口的不定长数据
- 四、通信协议的设计
- 五、STM32 IAP程序的设计
- 六、上位机的程序的编写
一、前言
为了能使上位机和下位机能进行可靠的通信,所以需要设计一个相对可靠的协议。当然设计的协议也不会太复杂,但是该有的功能还是得有。数据头部,控制指令,数据长度,数据,校验码。这种类型通讯协议用于串口相对还是比较简单便捷的,当然也可以设计的很复杂例如:加入版本控制,物理设备号等等。在本文中就不涉及太复杂的东西了,就设计一个相对完整又相对简单的协议。话不多说,接下来进入正题吧。
二、通讯格式
设计的通讯格式为问答式(即一问一答的方式),分为控制指令,和应答指令。
1、控制指令格式
首部 : 所有的数据包都要加包头:0xA5A5
父指令: 根据需求来设计
子指令: 根据需求来设计
长度 : 数据长度字节,用来指定该帧中携带数据的长度(单位是长度)
数据 : 该帧中写带的数据内容
校验码: 校验码使用校验和的方式;校验和是子指令到校验和之间的所有字节之和,超出 2 字节的进位忽略
2、应答指令格式
首部 : 所有的数据包都要加包头:0xA5A5
父指令: 根据需求来设计
子指令: 根据需求来设计
长度 : 数据长度字节,用来指定该帧中携带数据的长度(单位是长度)
数据 : 该帧中写带的数据内容
校验码: 校验码使用校验和的方式;校验和是子指令到校验和之间的所有字节之和,超出 2 字节的进位忽略
三、控制指令设计
1、查询指令/复位指令
2、程序更新控制指令
3、数据传送指令
四、应答指令设计
1、查询应答指令
2、状态指令
五、帧管理程序设计
1、帧管理数据结构
/* 回调函数的类型定义如下 */
/* typedef uint16_t (*FRAME_VALIDATE)(u8 *buf, u16 len); */
typedef struct
{
u8 *buffer;//指向一块buff,用于存储一帧的数据
u16 head; //数据存储在头部的位置
u16 count; //接收到的数据
u16 BUFFERLENGTH; //buffer的最大长度
u16 MIN_FRAME_LEN; //最小帧
FRAME_VALIDATE ValidateFrame; //这个是一个回调函数的指针,主要是处理帧的回掉函数
} FrameBufferStr;
2、结构体初始化函数
注意修改堆(heap)的大小
void Frame_Buffer_Init(FrameBufferStr *frame, u16 len, FRAME_VALIDATE validate_cb)
{
/* 但是要值得注意启动文件的堆内存的大小(默认是 512Byte)。*/
/* 如果使用大于等于512Byte的大小,则需要修改堆内存大小,否则会报 Fault 异常 */
/* 由于本实验需要的缓存需要3Kb,所以已经在启动文件里面修改为4KB */
frame->buffer = (u8*)malloc(len);
frame->BUFFERLENGTH = len;
frame->ValidateFrame = validate_cb;
Frame_Buffer_Clean(frame);
}
3、清buff函数
void Frame_Buffer_Clean(FrameBufferStr *frame)
{
memset(frame->buffer, 0, frame->BUFFERLENGTH);
frame->head = 0;
frame->count = 0;
}
4、帧管理程序
void Append_Frame_Buffer(FrameBufferStr *frame, u8 *input, u16 length)
{
u16 i = 0;
//s16 head=0;
u16 mlen=0;
// 如果上传数据长度比缓存总长度还要长
if (length > frame->BUFFERLENGTH)
{
// 此处可添加错误代码
return;
}
/* 需要将后面的数据copy到前面 */
if((u16)frame->count + length > frame->BUFFERLENGTH)
{
/* frame->head 记录frame->buffer的头部,frame->count记录接收到数据的尾部*/
memmove(frame->buffer, frame->buffer+frame->head, frame->count - frame->head);
/* 重新计算接收到数据的尾部 */
frame->count = frame->count - frame->head;
/* 头部指向为0 */
frame->head = 0;
/* 将后面的数据清零 */
memset(frame->buffer + frame->count, 0, frame->BUFFERLENGTH - frame->count);
/* 后面需要添加的数据超出了fream的范围 */
if((frame->count + length) > frame->BUFFERLENGTH)
{
// index out of buffer range
/* 将数据清零 */
frame->count=0;
frame->head=0;
return;
}
}
if(length != 0)
{
/* 复制后面添加进来的数据 */
memcpy(frame->buffer+frame->count, input, length);
frame->count+=length;
}
i = 0;
while(frame->head + i < frame->count)
{
/* 调用回调函数,解析接收数据,一个一个往下迭代,直到找到需要校验的起始头部 */
mlen = frame->ValidateFrame(frame->buffer + frame->head + i, frame->count - frame->head - i);
if(mlen > 0)
{
/* 返回的长度比buffer的总长度长,也将frame初始化 */
if(mlen > frame->BUFFERLENGTH)
{
frame->head = 0;
frame->count = 0;
memset(frame->buffer, 0, frame->BUFFERLENGTH);
break;
}
/* 处理完这帧数据了,将数据清零 */
frame->head = frame->head + i + mlen;
/* 如果计算完的后的frame->head后比 frame->count大,说明接收的数据有偏差*/
if(frame->head > frame->count)
{
frame->count = frame->head;
}
i = 0;
if(frame->head == frame->count)
{
/* 有偏差则需要重新将frame->buffer初始化 */
frame->head = 0;
frame->count = 0;
memset(frame->buffer, 0, frame->BUFFERLENGTH);
break;
}
}
else
{
i++;
}
}
}
六、在串口使用
1、DMA的回调函数
在IAP实验三里面有讲到,使用DMA接收串口数据时,首先需要给USART2_Service函数指针赋值让DMA接收函数回调。下面的USART2_DMA_Callback就是赋值给USART2_Service的函数。而这个函数也比较简单,就是将数据存放到帧管理程序中。
void USART2_DMA_Callback(u8 *buff, u16 len)
{
/* 里面调用帧管理程序,用于接收一帧数据 */
Append_Frame_Buffer(&USART2_Frame,buff,len);
}
2、USART2 的 Frame 管理程序初始化
/* 定义一个 FrameBufferStr 变量*/
FrameBufferStr USART2_Frame;
/* frame_cb 是帧管理程序的回调函数,由用户定义,在IAP实验中,主要用于处理协议的 */
void USART2_Frame_Init(FRAME_VALIDATE frame_cb)
{
/* 初始化USART2_Frame */
Frame_Buffer_Init(&USART2_Frame,USART2_FRAME_BUFFER_LEN,frame_cb);
/* 给 USART2_Service 赋值*/
USART2_Service = USART2_DMA_Callback;
}
3、帧管理程序的回调函数
/* 这个函数就是处理协议的回调函数了,具体怎么处理就留给用户来设计了 */
/* 在本实验中,这里只是做一个接收复位指令,然后执行软件复位 */
uint16_t Protocol_Handle(u8 *buf, u16 len)
{
/* 判断头是否正确 */
if(buf[0] == 0xa5 && buf[1] == 0xa5){
/* 等待接收够一帧数据 */
/* 这个 len 是包括头部的,不是协议上的len*/
if(len >= 8){
/* 判断校验位 */
uint16_t check = (uint16_t)(buf[len-2]<<8 | buf[len-1]);
if(check == 1){
/* 判断是否是软复位指令 */
if(buf[2] == 0 && buf[3] == 1){
/* 执行软复位 */
SoftReset();
}
}
return len;
}
}
return 0;
}
七、总结&实验现象
1、使用方法
通过上面步骤的层层封装,只要使用下面的步骤就可以正常使用:
定义一个帧管理的回调函数
调用 USART2_Frame_Init 初始化
使能串口
在回调函数中处理协议
2、本实验的现象
当通过 USART2 发送复位指令之后(下图中左边的调试助手)就可以看 STM32 复位,并且打印复位信息(右边的调试住手)。
这个实验是在 application 环境下执行的。