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 环境下执行的。

3、工程源码

从码云下载: https://gitee.com/gu_lan/gl_stm32_partition