基于STM32+ucosiii的CAN_BOOTLOADER

概述

乘着假期做了一套完整的基于stm32的can BootLoader系统,包括上位机和BootLoader代码。上位机的IDE为vs2017,编程语言为VB.net,can驱动基于周立功can盒,下位机IDE为Keil5。硬件为STM32103开发板,自己又移植了ucosiii。

程序烧录方式

程序烧录的方式分为三种:ICP、ISP、IAP。
ICP:In Circuit Programing 在电路编程:最“简单粗暴”的一种烧录方式,通过IDE配置相应的信息即可将程序烧录进单片机内,比如最常用的J-Link硬件通过JTAG/SWD协议进行程序烧录,但是面对的问题是需要将JTAG/SWD端口裸露在容易接插的地方。
ISP:In System Programing,在系统编程:ISP是指可以在板级上进行程序烧录,一般是通过ISP接口线将程序烧录至内部Flash存储器中,芯片一般固化了用来ISP升级的boot程序。例如STM32芯片就内置了ISP的BootLoader,存储在内部ROM中。简单介绍一下ISP的实现方式,STM32具有三种启动方式:
1、主闪存存储器= 芯片内置的Flash(程序烧录至此)
2、SRAM = 芯片内置的RAM 区(内存)
3、系统存储器= 芯片内部一块特定的区域,芯片出厂时在这个区域预置了一段Bootloader(ISP程序),这个区域的内容在芯片出厂后没有人能够修改或擦除(ROM )。
STM32 芯片有两个管脚BOOT0 和BOOT1,这两个管脚在芯片复位时的电平状态决定了芯片复
位后从哪个区域开始执行程序,具体配置及状态见下表:
在这里插入图片描述
在使用ISP进行程序烧录时,需要先将连接上位机至相应的ISP通讯端口(CAN.串口.SPI或其他)再将BOOT0置高,BOOT1置低,复位单片机后即可进入BootLoader模式,上位机开始程序烧录,结束之后需要将BOOT0置低,再复位单片机后即可从内部Flash开始运行应用程序(APP)。
当前更多的是一键ISP下载,通过上位机和硬件电路的配合,可以实现无需手动进行BOOT脚的复位即可进行程序烧录,同时通过串口或者CAN通讯,可以将通讯距离延长,弥补ICP方式的不足,但是ISP依旧是一种在线烧录的方式,面对一般的应用足够方便了,但是面对使用场景的发展,比如智能手环或智能汽车,需要升级程序时不能再像原先的方式要求到店或召回升级,OTA(Over-the-Air Technology)技术应运而生,IAP就是OTA的基础。
IAP:In Applicating Programing,在应用编程:将内部Flash进行“分区”,比如D区存储应用程序代码(APP),C区存储Bootloader代码;C区Bootloader代码不执行正常的应用程序代码,而是通过某种特定的通讯方式接受APP程序文件,并对D区代码进行更新,也就是用程序来改变程序,修改程序的一部分达到升级、消除bug的目的。IAP无需更改硬件,基于现有的通讯接口即可完成程序的升级,更加灵活。
Alt

空间分配

我所用的STM32103系列的芯片,内部Flash总容量为512k(0x80000),每页为2k(0x800),总共256页。
在这里插入图片描述
如图所示,STM32内部Flash的地址区间为0x0800 0000-0x0807 FFFF;分配A1区16k,从0x0800 0000 - 0x0800 3FFF;B1区空间128k(0x3C000),从0x0800 8000 - 0x0802 7FFF;C1区在0x0800 4000- 0x0800 47FF,共2k(0x800)。
A1区空间存储BootLoader的应用代码,B1区存储APP的应用代码,C1区则存储一些BootLoader相关的参数,比如节点ID和波特率。
BootLoader Keil配置:
在这里插入图片描述
APP Keil配置为:
在这里插入图片描述

通讯协议

采样CAN作用IAP的通讯介质,采用数据帧,帧ID为0x1的标准帧格式。整个利用BootLoader升级的过程中只需要0x1这么一个ID,根据步骤和指令符的不同执行不同内容。
在这里插入图片描述

操作过程

在这里插入图片描述
大致的流程图如上图所示,通过can协议实现不同的功能,在接收数据之前,先确认ID,再握手,通过之后进行相关Flash区域的擦除(Flash写之前必须先执行擦除操作,而且擦除操作是按页进行的,每次),然后再进行数据传输和写入的相关操作。

实现

can驱动

由于数据传输过程中讲究效率,因此需要再短时间内将几十k的数据传输给Bootloader,且can通讯每帧只能发送8个字节的数据,因此在程序更新过程中,通信量巨大且对数据传输的正确性要求极高,所以在STM32固件库的can驱动基础上增加了环形队列的can接收缓存,缓存大小为1026。

/*缓存区大小*/
#define Max_Size 1026

/*环形队列数据*/
typedef struct
{
    uint8_t head;
    uint8_t tail;
    CanRxMsg RingBuf[Max_Size];
} RingBuf;
/*实例化*/
RingBuf RingBuf_Rec;

/*向缓存区写数据*/
void RingBuf_Write(CanRxMsg *RecData)
{
    RingBuf_Rec.RingBuf[RingBuf_Rec.tail]=*RecData;
    if(++RingBuf_Rec.tail>= Max_Size) RingBuf_Rec.tail=0;

}
/*读取缓存区数据*/
uint8_t RingBuf_Read(CanRxMsg *ReadData)
{
    if(RingBuf_Rec.tail==RingBuf_Rec.head)
    {
        return 1;
    }
    else
    {
        *ReadData=RingBuf_Rec.RingBuf[RingBuf_Rec.head];
        if(++RingBuf_Rec.head>= Max_Size) RingBuf_Rec.head=0;
        return 0;
    }
}

擦除

/**
  * @Name:	    CAN_BOOT_ErasePage
  * @Function:	擦除指定区域
  * @Input:	    起始地址和结尾地址
  * @Output:    FLASH_Status
  */
FLASH_Status CAN_BOOT_ErasePage(uint32_t StartPageAddr,uint32_t EndPageAddr)
{
    uint32_t i;
    FLASH_Status FLASHStatus=FLASH_COMPLETE;

    FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);

    for(i=StartPageAddr; i<EndPageAddr; i+=PAGE_SIZE) {
        FLASHStatus = FLASH_ErasePage(i);
        if(FLASHStatus!=FLASH_COMPLETE) {
            printf("%d,%d\r\n",i,FLASHStatus);
            return	FLASHStatus;
        }
    }

    return FLASHStatus;
}

其中printf是调试时使用的,串口直接输出文字,让调试更有温度。
擦除时需要先将中断关闭,并将Flash解锁,然后再清除标志位,接着就可以按页或按扇区对Flash指定地址进行擦除操作。

    /*关中断-解锁Flash*/
    __set_PRIMASK(1);
    FLASH_Unlock();
    ......
    /*开中断-锁上Flash*/
    __set_PRIMASK(1);
    FLASH_Unlock();    

该函数返回的是执行擦除操作之后的状态,调用的是固件库中的结构体,具体定义如下:

/** 
  * @brief  FLASH Status  
  */

typedef enum
{ 
  FLASH_BUSY = 1,
  FLASH_ERROR_PG,
  FLASH_ERROR_WRP,
  FLASH_COMPLETE,
  FLASH_TIMEOUT
}FLASH_Status;

不同芯片对应的页大小也不同,本次我选用的是STM32103,单页大小为2k(0x800)。

/* Base address of the Flash sectors */
#if defined (STM32F10X_MD) || defined (STM32F10X_MD_VL)
#define PAGE_SIZE                         (0x400)    /* 1 Kbyte */
#define FLASH_SIZE                        (0x20000)  /* 128 KBytes */
#elif defined STM32F10X_CL
#define PAGE_SIZE                         (0x800)    /* 2 Kbytes */
#define FLASH_SIZE                        (0x40000)  /* 256 KBytes */
#elif defined STM32F10X_HD
#define PAGE_SIZE                         (0x800)    /* 2 Kbytes */
#define FLASH_SIZE                        (0x80000)  /* 512 KBytes */
#elif defined STM32F10X_XL
#define PAGE_SIZE                         (0x800)    /* 2 Kbytes */
#define FLASH_SIZE                        (0x100000) /* 1 MByte */
#else
#error "Please select first the STM32 device to be used (in stm32f10x.h)"    
#endif

对应的can通讯指令为:
在这里插入图片描述
BootLoader在接收到擦除指令之后,将会上传动作状态以及动作结果,上位机根据相应的上传信息显示具体步骤内容。
在这里插入图片描述
执行擦除指令的代码:

        //CMD_List.Erase£¬²Á³ýFlashÖеÄÊý¾Ý£¬ÐèÒª²Á³ýµÄFlash´óС´æ´¢ÔÚData[4]µ½Data[7]ÖÐ
        if(can_cmd == CMD_List.Erase) {
            FlashSize = (pRxMessage->Data[4]<<24)|(pRxMessage->Data[5]<<16)|(pRxMessage->Data[6]<<8)|(pRxMessage->Data[7]<<0);
            printf("FlashSize:%d\r\n",FlashSize);
            if(can_addr != 0x00) {
                TxMessage.DLC = 0;
                TxMessage.Data[0] = DataCheck.ID;//½ÚµãµØÖ·
                TxMessage.Data[1] = 0x06;
                TxMessage.Data[2] = 0xF4;//´Î°æ±¾ºÅ£¬Á½×Ö½Ú
                TxMessage.Data[3] = CMD_List.CmdStart;
                TxMessage.Data[4] = FlashSize>>24;
                TxMessage.Data[5] = (FlashSize>>16) & 0x00FF;
                TxMessage.Data[6] = (FlashSize>>8) & 0x00FF;
                TxMessage.Data[7] = FlashSize & 0x00FF;
                CAN_Write(1,8,1,BootCANID,TxMessage.Data);
            }

            if(cmd_temp==0xAA) {  //²Á³ýÖ÷³ÌÐòÇø
            	 __set_PRIMASK(1); //¹ØÖжÏ
            	FLASH_Unlock();           
                DataCheck.Step_Now = 1;  //²½ÖèÖÃλ
                ret = CAN_BOOT_ErasePage(APP_START_ADDR,APP_START_ADDR+FlashSize);
              	FLASH_Lock();
            	__set_PRIMASK(0);
           		 delay_ms(5);              
            }
            else return;
            TxMessage.Data[0] = DataCheck.ID;//½ÚµãµØÖ·
            TxMessage.Data[1] = 0x06;
            TxMessage.Data[2] = 0xF4;//´Î°æ±¾ºÅ£¬Á½×Ö½Ú
            if(ret==FLASH_COMPLETE) {
                TxMessage.Data[3] = CMD_List.CmdSuccess;
            } else {
                TxMessage.Data[3] = CMD_List.CmdFaild;
            }
            TxMessage.Data[4] = 0xFF;
            TxMessage.Data[5] = 0xFF;
            TxMessage.Data[6] = 0xFF;
            TxMessage.Data[7] = 0xFF;
            CAN_Write(1,8,1,BootCANID,TxMessage.Data);
            return;
        }

读/写Flash

每次写Flash之前都需先对相应的Flash空间进行擦除操作,读/写Flash之前也须关闭中断,解锁Flash,再执行相应的操作。读按字节为单位进行,进行对相应地址取值;写可以按半字和全字进行,32位芯片,全字为32bit,相对应的是4byte,半字则为2byte。

/**
  * @Name:	    ReadFlashNBtye
  * @Function:	读字节
  * @Input:	   	起始地址,数据读至该数组,读取的长度
  * @Output:    Null
  */
void ReadFlashNBtye(uint32_t ReadAddress, uint8_t *ReadBuf, uint32_t ReadNum)
{
    uint16_t i = 0;

    while(i < ReadNum)
    {
        *(ReadBuf + i) = *(__IO uint8_t*) ReadAddress++;
        i++;
    }
}

/**
  * @Name:	    DatatoFlash
  * @Function:	写操作
  * @Input:	    起始地址,数据来源,写的数量长度
  * @Output:    FLASH_Status
  */
FLASH_Status DatatoFlash(uint32_t StartAddress,uint8_t *pData,uint32_t DataNum)
{
    FLASH_Status FLASHStatus = FLASH_COMPLETE;

    uint32_t i;

    if(StartAddress<APP_EXE_FLAG_START_ADDR) {
        return FLASH_ERROR_PG;
    }
    /* Clear All pending flags */
    FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);

    for(i=0; i<(DataNum>>2); i++)
    {
        FLASHStatus = FLASH_ProgramWord(StartAddress, *((uint32_t*)pData));
        if (FLASHStatus == FLASH_COMPLETE) {
            StartAddress += 4;
            pData += 4;
        } else {
            return FLASHStatus;
        }
    }
    return	FLASHStatus;
}

程序中写按全字进行,因此写完之后地址自加4。

程序跳转

/**
  * @Name:	    CAN_BOOT_JumpToApplication
  * @Function:	跳转至相应地址,跳转地址需和魔法棒内配置的起始地址一致
  * @Input:	    跳转至的目标地址
  * @Output:    null
  */
void CAN_BOOT_JumpToApplication(uint32_t Addr)
{
    static pFunction Jump_To_Application; //给函数类型的参数具体化(函数内的静态局部变量)
    __IO uint32_t JumpAddress; //IO(读写)操作
    /* Test if user code is programmed starting from address "ApplicationAddress" */
    if (((*(__IO uint32_t*)Addr) & 0x2FFE0000 ) == 0x20000000)//检测栈顶地址是否合法,是否在0x2000 0000至0x2000 2000之间
    {
        /* Jump to user application */
        JumpAddress = *(__IO uint32_t*) (Addr + 4); //用户代码第二个字节为程序开始地址
        Jump_To_Application = (pFunction) JumpAddress; //´指向复位函数所在的地址
        __set_PRIMASK(1);//关中断
        /* Initialize user application's Stack Pointer */
        __set_MSP(*(__IO uint32_t*)Addr);//设置主函数栈指针
        Jump_To_Application();//执行复位函数,把APP代码的复位地址交给PC指针
    }
}

程序跳转之前已将中断关闭,因此,在APP运行之前,应先将中断打开,由于我这次移植了ucosiii,因此开/关中断的操作与固件库的方式不同,需要更改中断向量表的起始地址,否则中断还是会进到BootLoader进行处理,导致APP程序无法正常运行,具体操作如下:

int  main (void)
{

    OS_ERR  err;


    OSInit(&err);                                     /* Init uC/OS-III.*/
	/*在任务结构体创建之前,更改中断向量表中的起始地址*/
	NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000); //APP起始地址为0x0800 8000
    OSTaskCreate((OS_TCB     *)&AppTaskStartTCB,                /* Create the start task */
                 (CPU_CHAR   *)"App Task Start",
                 (OS_TASK_PTR ) AppTaskStart,
                 (void       *) 0,
                 (OS_PRIO     ) APP_TASK_START_PRIO,
                 (CPU_STK    *)&AppTaskStartStk[0],
                 (CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,
                 (CPU_STK_SIZE) APP_TASK_START_STK_SIZE,
                 (OS_MSG_QTY  ) 5u,
                 (OS_TICK     ) 0u,
                 (void       *) 0,
                 (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                 (OS_ERR     *)&err);

								 
    OSStart(&err);                                              /* Start multitasking (i.e. give control to uC/OS-III). */

}

执行跳转指令的代码如下:

        /*Ö´ÐÐÌøתָÁbootÌøתÖÁAPP*/
        if(can_cmd == CMD_List.Excute) {
            /*Éϱ¨Ìøת״̬*/
            TxMessage.Data[0] = DataCheck.ID;//½ÚµãµØÖ·
            TxMessage.Data[1] = 0x06;
            TxMessage.Data[2] = 0xF7;//´Î°æ±¾ºÅ£¬Á½×Ö½Ú
            TxMessage.Data[3] = cmd_temp;
            TxMessage.Data[4] = 0xFF;
            TxMessage.Data[5] = 0xFF;
            TxMessage.Data[6] = 0xFF;
            TxMessage.Data[7] = 0xFF;
            CAN_Write(1,8,0,BootCANID,TxMessage.Data);
            delay_ms(5);
            CAN_BOOT_JumpToApplication(APP_START_ADDR);
            return;
        }

数据传输

在数据传输之前,需先进行握手,上位机发送握手指令,BootLoader接收到相应握手指令之后上传缓存区的大小,具体通讯协议如下:
在这里插入图片描述
BootLoader接收的数据先保存于缓存区内,接收数据达到缓存区大小或数据接收完成时,再将接收到的数据进行CRC校验,校验通过再写入相应Flash区域内,否则上报错误状态,停止数据传输,同时还需要检测超时状态,如果超时通用上报错误状态,并停止数据传输。由于can每帧数据只能传输8Byte数据,因此根据BootLoader缓存区的大小,将数据分割成多份,称之为Pack。此次缓存区大小为1026,实际数据为1024的数据以及2byte的crc。每个PACK所需要发送的总帧数称为Data_Num,除了最后一个Pack,其他Pack所需要传输的数量均为1026字节,所以帧数为1024/8+1=129帧,最后一帧CAN的数据长度为2,其他均为8,最后一PACK数据根据剩下的数据计算所需要发送的can帧数以及最后一帧can的数据长度。
在进行数据传输握手时(建立多包传送),上位机会下发当前的Pack数以及当前Pack所需要发送的帧数,BootLoader接收到此握手信号后上传接收到的信息,上位机判断正确的话开始发送该Pack的数据,而BootLoader通过累加接收到的每帧数据长度和接收到的帧数判断此PACK是否接收完成,判断完成时对该Pack数据进行1024位的CRC校验,并和上位机下发的最后两位数据进行对比,如果正确则将缓存区内的1024位数据写入相应位置的Flash内,Flash操作的起始地址在每Pack数据接收完成时也会进行累加。当最后一Pack数据接收完成后,上报程序更新完成状态,并跳转至APP。
数据传输握手(预写-握手建立多包发送)协议如下:
在这里插入图片描述
程序升级过程中上位机显示状态如下图,途中数字即为当前的Pack数。
在这里插入图片描述
握手和多包传输建立的握手代码如下:

//  //CMD_List.WriteInfo£¬ÉèÖÃдFlashÊý¾ÝµÄÏà¹ØÐÅÏ¢£¬ÏȶÁÈ¡»º´æÇø´óС£¬ÔÙ¸ù¾Ý»º´æÇø´óСÉèÖÃ×Ü°üÊý
        if((can_cmd == CMD_List.WriteInfo)) {
            /*ÎÕÊÖ£¬Éϱ¨»º´æÇø´óС*/
            if(cmd_temp == 0x02) {
                TxMessage.Data[0] = DataCheck.ID;//½ÚµãµØÖ·
                TxMessage.Data[1] = 0x06;
                TxMessage.Data[2] = 0xF6;//´Î°æ±¾ºÅ£¬Á½×Ö½Ú
                TxMessage.Data[3] = 0x02;
                TxMessage.Data[4] = 0x04;  //»º´æÇø´óС1024=0x400
                TxMessage.Data[5] = 0x00;  //»º´æÇø´óС1024=0x400
                TxMessage.Data[6] = 0xFF;
                TxMessage.Data[7] = 0xFF;
                CAN_Write(1,8,0,BootCANID,TxMessage.Data);
                printf("ÎÕÊÖ1,»º´æ´óС:%d\r\n",TxMessage.Data[4]*256+TxMessage.Data[5]);
                DataCheck.Step_Now = 2; //²½ÖèÖÃλ
                Step_Timer = 0;
            }
            /*Êý¾Ý´«Êä¹ý³Ì£¬Ã¿Ò»PACK´«ÊäÒ»´Î*/
            if(cmd_temp == 0x03) {
                DataCheck.PackNum_Sum = pRxMessage->Data[4];
                DataCheck.PackNum_Now = pRxMessage->Data[5];
                DataCheck.DataNum_Sum = (pRxMessage->Data[6])*256 + (pRxMessage->Data[7]);
                DataCheck.DataNum_Now = 0;
                TxMessage.Data[0] = DataCheck.ID;//½ÚµãµØÖ·
                TxMessage.Data[1] = 0x06;
                TxMessage.Data[2] = 0xF6;//´Î°æ±¾ºÅ£¬Á½×Ö½Ú
                TxMessage.Data[3] = 0x03;
                TxMessage.Data[4] = DataCheck.PackNum_Sum;
                TxMessage.Data[5] = DataCheck.PackNum_Now;
                TxMessage.Data[6] = DataCheck.DataNum_Sum >> 8;
                TxMessage.Data[7] = DataCheck.DataNum_Sum & 0xFF;
                CAN_Write(1,8,0,BootCANID,TxMessage.Data);
//                printf("ÎÕÊÖ2,×Ü°üÊý:%d;µÚÒ»°üDataÊý:%d\r\n",DataCheck.PackNum_Sum,DataCheck.DataNum_Sum);
                DataCheck.WriteFlag = 1; //¿ªÊ¼½ÓÊÕÊý¾Ý±êÖ¾ÖÃλ
                DataCheck.Step_Now = 3; //²½ÖèÖÃλ
                Step_Timer = 0;
            }
            return;
        }

最终效果

在这里插入图片描述
上位机丑陋了一些,先实现功能,界面的事情后面再慢慢优化。

  • 8
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值