汽车电子ECU bootloader设计与实现(下位机代码篇)

本文选用的芯片为车规级AC7802X系列,开发环境为MDK,总线通信基于LIN通信

(一)实现功能介绍

       在嵌入式系统运行中,下位机收到上位机发送的升级指令后(我这里的是一帧ID为0x11的报文),从而下位机进行原APP程序的擦除操作,当程序擦除完毕后,向上位机发送一帧表示已擦除完毕的报文,上位机收到改报文后,将更新的APP程序进行按地址拆包发送,下位机收到上位机发送的数据包之后,进行组包写入Flash中,待写入完毕后进行校验。最后软件复位,重新运行bootlaoder,随后跳转至APP并运行,从而实现软件的更新。

(二)下位机bootloader代码实现

1)对bootloder与app进行预分区

单片机的Flash空间是有限的,片内的Flash又分为P-FLASH,D-FLASH,信息区等,P-FLASH主要用于存储用户的代码和数据,Dflash用于存储用户的数据,信息区又叫做选项字节区,放置读写保护信息。芯片手册对其进行了分类,用户是可以查阅的。

      可见bootloader与app基于以上信息是存放在P-Flash中的,上电后程序先运行bootloader,所以其起始地址从0x0800 0000开始,程序编写结束后,进行编译,通过查看编译size大小,再判断结束地址设置为多少较为合理。这里举个例子,可参考。

//Pflash 页容量 512B  					Dflash 8B
#define Page_Size 512               
#define Boot_Address 0x08000000
#define Boot_Size		 0x00002000
#define APP_Address (Boot_Address+Boot_Size)

2)实现Lin总线的通信

lin总线的基础只是这里不做讲解,通讯协议都大差不差,不太懂得可以去看看别的大佬的帖子。

这里下位机设置为从机模式,上位机为主机模式,相关配置如下。

void LIN_InitLin(uint8_t mode, uint8_t schTblIdx, uint16_t baudrate)
{
    uint8_t linCtrl = 0;
    UART_ConfigType uartConfig;
    memset((void *)&uartConfig, 0, sizeof(UART_ConfigType));
    /*!初始化LIN的引脚功能
    包括RX/TX及LIN的收发器引脚*/
    GPIO_SetFunc(SWLIN_RX, RXPinFunc);
    GPIO_SetFunc(SWLIN_TX, TXPinFunc);
    GPIO_SetDir(SWLIN_SLP, GPIO_OUT);
    GPIO_SetPinLevel(SWLIN_SLP, GPIO_LEVEL_HIGH);
    /*!初始化Uart_LIN及相关参数
    包括波特率及其他配置*/
    uartConfig.baudrate = baudrate;
    uartConfig.dataBits = UART_WORD_LEN_8BIT;
    uartConfig.stopBits = UART_STOP_1BIT;
    uartConfig.parity = UART_PARI_NO;
    uartConfig.fifoByteEn = DISABLE;
    uartConfig.sampleCnt = UART_SMP_CNT0;
    uartConfig.callBack = LIN_IntCallback;
    UART_Init(UART_LIN, &uartConfig);
    /*!配置LIN需要使能的功能*/
    linCtrl = UART_LINCR_LINEN_Msk | UART_LINCR_LBRKIE_Msk | (1 ? UART_LINCR_LBRKDL_Msk : 0) | (1 ? UART_LINCR_LABAUDEN_Msk : 0);
    UART_SetLIN(UART_LIN, linCtrl);
    /*!配置LIN模块相关中断*/
    UART_SetRXNEInterrupt(UART_LIN, ENABLE);
    UART_SetTXEInterrupt(UART_LIN, DISABLE);//发送中断
    NVIC_ClearPendingIRQ(UART_LIN_IRQn);
    NVIC_EnableIRQ(UART_LIN_IRQn);
}

lin中断处理,这里中断处理为从电平隐性为判断是否为lin报文,识别lin报文后,根据主机的报头进行相关的处理。我这里数据的传输以及flash的擦写操作都是在中断函数中处理的。

void LIN_IntCallback(void *device, uint32_t wpara, uint32_t lpara)
{
    if (0 != (lpara & UART_LSR1_FBRK_Msk))
    {
        LIN_IfcAux();                              //中断场
        UART_LIN->LSR1 |= UART_LSR1_FBRK_Msk;   ///<write 1 to clear break status
    }
    if (0 != (wpara & UART_LSR0_DR_Msk))
    {
        LIN_IfcRx();                               //从总线中接受报头或者数据
    }
}

PID读取以及checksum处理

uint8_t LIN_MakeProtId(uint8_t idData)//
{
    union {
        uint8_t  byte;
        struct {
            uint8_t  d0: 1;
            uint8_t  d1: 1;
            uint8_t  d2: 1;
            uint8_t  d3: 1;
            uint8_t  d4: 1;
            uint8_t  d5: 1;
            uint8_t  p0: 1;
            uint8_t  p1: 1;
        } bit;
    } buf;
  
    buf.byte = idData & (uint8_t)0x3F;
    /* Set the two parity bits.  */
    buf.bit.p1 = ~(buf.bit.d1 ^ buf.bit.d3 ^ buf.bit.d4 ^ buf.bit.d5);
    buf.bit.p0 = buf.bit.d0 ^ buf.bit.d1 ^ buf.bit.d2 ^ buf.bit.d4;
    return buf.byte;
}

uint8_t LIN_MakeChecksum(uint8_t protectId, uint8_t length, uint8_t *data)
{
    uint8_t i = 0, checksum = 0;
    uint16_t sum = protectId;
    for (i = 0; i < length; i++)
    {
        sum += data[i];
        if (sum >= 0x100)
        {
            sum -= 0xFF;
        }
    }
    checksum = ~(uint8_t)sum;
    return checksum;
}

         对于ID的处理,设置了写指令0x01,读指令0x02,升级指令ID0x11,由于lin总线的特殊机制,在测试的过程中,无法判断从机是否收到指令,故可先发送写指令指导下位机进行相关的数据处理操作,然后发送读指令,判断是否确实收到。

        这里的协议是我自拟的,设计思路,上位机分析APP数据并拆包,告诉下位机,APP的起始地址和size,下位机收到后,根据该信息进行擦除flash,这样的操作可以延长falsh的寿命。擦除完毕后返回一帧41 42 41 42。随后上位机开始分包传输数据,我这里设置的是8字节传输,下位机收到一包后反一个继续传输的指令,上位机继续发送下一包,凑够一页512字节后写入flash,随后进行数据的校验,循环操作,直到所有数据传输完毕。详细代码如下。

void LIN_IfcRx(void)
{
    // Receive data
    uint8_t data = 0, id = 0, checksum = 0;
    data = UART_ReceiveData(UART0); //单字节发送
    switch (lin_state)
    {
    case  LIN_STATE_RECEIVE_SYNC:  //4  接收同步
        if (data == 0x55) lin_state = LIN_STATE_RECEIVE_IDENTIFIER;  //6  接收标识符
        else lin_state = LIN_STATE_IDLE;
        break;
    case LIN_STATE_RECEIVE_IDENTIFIER:  //6 接收标识符
        id = data & 0x3F;
        if (data == LIN_MakeProtId(id))
        {
            if (id == Master_Write_ID)      //主节点写指令ID :0x01
            {
                lin_state = LIN_STATE_RECEIVE_DATA;//接收数据段  8
                g_rx_id = id;
            } else if (id==Master_Read_ID)  //读指令 0x02
            {
                if (g_erase_finish_flag==1)
                {//擦写状态 stat
                    LINTxDataBuffer[0]=0x41;
                    LINTxDataBuffer[1]=0x42;
                    LINTxDataBuffer[2]=0x41;
                    LINTxDataBuffer[3]=0x42;
                    g_erase_finish_flag=0;
                }
                //计算校验和
                lin_state = LIN_STATE_SEND_DATA;    //7 发送数据段
                g_rx_id = id;
            } else if (id==Master_Updata_ID)// 写入升级指令专用ID 0x11
            {
                g_receive_0x11_flag=1;
                g_rx_id = id;
            }
        }
        break;
    case LIN_STATE_RECEIVE_DATA:  //8
        if (g_rxCount < 9)//接收数据中
        {
            LINRxDataBuffer[g_rxCount] = data;
            g_rxCount++;
        }
        if (g_rxCount >= 9)//接收完毕
        {
            checksum = LIN_MakeChecksum((g_chkmode == ENHANCED_CHECKSUM) ? LIN_MakeProtId(g_rx_id) : 0, 8, LINRxDataBuffer);
            if (checksum == LINRxDataBuffer[g_rxCount - 1])
            {
                //Temp_Length = 8;
                Temp_ID = g_rx_id;
                if (LINRxDataBuffer[0]==0x73 && LINRxDataBuffer[1]==0x74 && LINRxDataBuffer[2]==0x61 && LINRxDataBuffer[3]==0x72 && LINRxDataBuffer[4]==0x74)
                {//握手标志
                    g_Shark_Hand_Flag =1;
                    LINTxDataBuffer[0]=0x11;
                    LINTxDataBuffer[1]=0x12;
                    LINTxDataBuffer[2]=0x12;
                    LINTxDataBuffer[3]=0x13;
                } else if (LINRxDataBuffer[0]==0x65 && LINRxDataBuffer[1]==0x6E && LINRxDataBuffer[2]==0x64)
                {//结束标志
                    g_End_Flag =1;
                    LINTxDataBuffer[0]=0x11;
                    LINTxDataBuffer[1]=0x12;
                    LINTxDataBuffer[2]=0x13;
                    LINTxDataBuffer[3]=0x13;
                } else if (LINRxDataBuffer[0]==0x61 && LINRxDataBuffer[1]==0x64 && LINRxDataBuffer[2]==0x64 && LINRxDataBuffer[3]==0x72)
                {//传输App起始地址
                    g_app_address = LINRxDataBuffer[7] |
                                    (LINRxDataBuffer[6] << 8) |
                                    (LINRxDataBuffer[5] << 16) |
                                    (LINRxDataBuffer[4] << 24);
                    LINTxDataBuffer[0]=0x21;
                    LINTxDataBuffer[1]=0x22;
                    LINTxDataBuffer[2]=0x21;
                    LINTxDataBuffer[3]=0x22;
                    g_app_address_recive_flag = 1;
                } else if (LINRxDataBuffer[0]==0x70 && LINRxDataBuffer[1]==0x61 && LINRxDataBuffer[2]==0x67 && LINRxDataBuffer[3]==0x65)
                {//传输有效长度
                    g_erase_pagenum = LINRxDataBuffer[7] |
                                      (LINRxDataBuffer[6] << 8) |
                                      (LINRxDataBuffer[5] << 16) |
                                      (LINRxDataBuffer[4] << 24);
                    LINTxDataBuffer[0]=0x31;
                    LINTxDataBuffer[1]=0x32;
                    LINTxDataBuffer[2]=0x31;
                    LINTxDataBuffer[3]=0x32;
                    g_erase_pagenum_recive_flag = 1;
                } else if (LINRxDataBuffer[0]==0x63 && LINRxDataBuffer[1]==0x6F && LINRxDataBuffer[2]==0x6E)
                {//继续传输
                    g_Continue_Flag =1;
                    LINTxDataBuffer[0]=0x11;
                    LINTxDataBuffer[1]=0x11;
                    LINTxDataBuffer[2]=0x12;
                    LINTxDataBuffer[3]=0x13;
                } else {//程序镜像
                    memcpy(Temp_Data+Temp_Length,LINRxDataBuffer,8);
                    Temp_Length+=8;
                    if (Temp_Length == Page_Size)
                    {
                        u_EFLASH_Write(APP_Address+g_Offset,Temp_Data,Page_Size);
                        g_Offset+=Page_Size;
                        Temp_Length = 0;
                        memset(Temp_Data,0,Page_Size);
                    }
                }
            }
        }
        break;

    default:
        break;
    }
    if (lin_state == LIN_STATE_SEND_DATA)//发送数据段
    {
        checksum = LIN_MakeChecksum((g_chkmode == ENHANCED_CHECKSUM) ? LIN_MakeProtId(g_rx_id) : 0, 8, LINTxDataBuffer);//计算校验和
        for (int k = 0; k < 8; k++)
        {
            UART_SendData(UART_LIN, LINTxDataBuffer[k]);//发送数据(单字节),发送data
        }
        UART_SendData(UART_LIN, checksum);//发送数据(单字节),发送checksum
        memset(&LINTxDataBuffer, 0, 10);
        lin_state = LIN_STATE_IDLE;//空闲段
    }
}

3)FLASH 擦写操作

       需要注意的是,再对Flash进行操作前,首先需要解除读写保护以及锁定,在每次对flash进行操作的时候都需要解锁,操作完毕后都需要进行锁定。对于flash的操作,是比较灵活的,可以进行页编程,页擦除,整片擦除,快擦除,擦除验证等操作。

       flash编程函数较为简单,这里不做说明,flash擦除函数需要注意一下,由于flash擦除是需要时间的,但是程序运行的速度时非常快的,取决于单片机内部的晶振,如果再在执行擦除的操作时,由于擦除的操作还没有执行完毕,也就是出现,单片机擦除程序执行到下一条,上一个空间的擦除操作内部还没有完成,就会引起程序的崩溃。那么如何解决这个问题呢?可以在每次擦除程序执行后加入一个延时处理,等待单片机内部的擦除操作,这样便不会出现程序崩溃的情况,我这里设置的是一个for循环。延时时间具体多长呢?由于每个muc的情况是不一样的,需要开发的时候进行大量的测试。

EFLASH_StatusType u_EFlash_Erase(uint32_t pageAddress,uint8_t pagenum)
{
	EFLASH_StatusType ret;
	uint8_t erase_count = 0;
	uint32_t offset = 0;
	ret = EFLASH_UnlockCtrl();  
	//验证解锁是否成功
	if (ret != EFLASH_STATUS_SUCCESS)
	{
		return EFLASH_STATUS_ERROR;
	}
	//pagenum = pagenum/512;
	//开始页擦除
	for(erase_count = 0;erase_count<pagenum;erase_count++)
	{
		ret = EFLASH_PageErase(pageAddress+offset);   
		if (ret != EFLASH_STATUS_SUCCESS)
		{
			EFLASH_LockCtrl();
			return EFLASH_STATUS_ERROR;
		}
		//验证是否被擦除
		ret = EFLASH_PageEraseVerify(pageAddress+offset,Page_Size/4); 
		if (ret != EFLASH_STATUS_SUCCESS)
		{
			EFLASH_LockCtrl();
			return EFLASH_STATUS_ERROR;
		}
		
		offset = offset + Page_Size;
		//延时,等待FLASH擦除与验证
		for(int i = 65530;i>0;i--);
	}
	EFLASH_LockCtrl();
	return EFLASH_STATUS_SUCCESS;
}

4)升级标志位的处理

      当收到了上位机的升级指令后,下位机需要将升级标志位置为,在bootloader程序运行时进行自检,从而进行更新的流程,放在D-falsh区域是比较合适的,直接操作D-FLash,数据是不会被清除的,这样不管实在bootloader阶段还是在app阶段,收到指令后,标志位都不会被清除,当数据更新完毕之后再将D-flash中的标志清0即可。根据芯片的指导手册,可以查看D-flash的起始地址位0x0802 0000,那么我们直接操作这个地址,将数据存放到此。具体代码见主函数。

5)定时器

      本文配置了一个1s的定时器,为什么需要定时器呢?bootloader的功能就是为了对程序进行升级,那么当我们不需要进行升级的时候,系统上电后就要迅速的跳转到app进行相关的控制。定时器配置十分基础,这里不展示代码。

6)跳转函数

     这里注意一下APP的起始地址就好,然后了解一下栈顶指针。

typedef void (*pFunction)(void);
static pFunction s_jumpToApplication;

void BOOT_JumpToApplication(void)
{
    uint32_t JumpAddress;
    if(((*(__IO uint32_t *)APP_Address) & 0x2FFE0000) == 0x20000000)
    {
        __ASM("CPSID I");//关全局中断 
        JumpAddress = *(__IO uint32_t *)(APP_Address+4); // Jump to user application
        s_jumpToApplication = (pFunction)JumpAddress;   // Initialize user application's Stack Pointer
        __set_MSP(*(__IO uint32_t*)APP_Address);
				__ASM(" CPSIE i");//开全局中断
        s_jumpToApplication();  /* jump to app */		
    }
}

7)主函数讲解

int main()
{
    uint32_t app_updata_flag[8];
    DisableInterrupts;
	  InitDelay();                     /*! 延时函数初始化 */
    LIN_InitLin(1, 0,LIN_BAUDRATE);  /*! LIN从机初始化 */
    UART_Cfg_Init();                 /*! 串口1初始化 */
    TIMER_PrdInit();                 /*! 1S定时器初始化 */
    EnableInterrupts;
    //在 2s 内,如果收到了0x11的报文,则将Dflash中标志位擦除,然后写为1 (有升级的需求)
    //如果没有收到0x11的报文,则说明没有升级需求,2s后跳出死循环 然后进行标志位判断
    while (1)
    {
        //未收到升级要求
        if (g_1s_flag == 1)
        {
            g_1s_flag = 0;
            BOOT_JumpToApplication();
        }
        //通过LIN通信,收到了上位机的升级消息
        if (g_receive_0x11_flag == 1)
        {
            EFLASH_UnlockCtrl();
            EFLASH_DFBlockErase(0x08020000);  //块擦除 擦16字节
            EFLASH_PageProgram(0x08020000, (uint32_t *)set_buffer, 1); //写一个字,4字节,写为1
            EFLASH_LockCtrl();
            EFLASH_UnlockCtrl();//解锁
            EFLASH_Read(0x08020000,(uint32_t *)app_updata_flag,2);  //从DFLASH起始地址读2字节放入标志位中
            EFLASH_LockCtrl();//上锁
            if (app_updata_flag[0] !=1) //说明上位机没有发升级请求,那么直接跳转APP
            {
                BOOT_JumpToApplication();
            }
            else
            {
                ///在LIN接收中完成更新
                for (;;)
                {
                    if (g_app_address_recive_flag == 1&&g_erase_pagenum_recive_flag==1) //表示已成功接收到
                    {
                        DisableInterrupts;
                        u_EFlash_Erase(g_app_address, g_erase_pagenum);
                        g_app_address_recive_flag=0;
                        g_erase_pagenum_recive_flag=0;
                        g_erase_finish_flag=1;//表示擦除过程已完成
                        EnableInterrupts;
                    }
                    if (g_End_Flag == 1)///在所有数据传输完成后,清除更新标志位
                    {
                        EFLASH_UnlockCtrl();
                        ///清除更新标志位
                        EFLASH_DFBlockErase(0x08020000);  //块擦除 擦16个字节
                        EFLASH_PageProgram(0x08020000, (uint32_t *)empty_buffer, 1); //写4个字节
                        EFLASH_LockCtrl();
                        //BOOT_JumpToApplication();
												NVIC_SystemReset();	
                    }
                }
            }
        }
    }
}

       当收到了升级报文后,擦除Dfalsh标志位区域,将set_buffer中的1写入到0x0802 0000,然后读取该地址上的数据放入app_updata_flag数组中,随后判断app_updata_flag中是否置位!没有升级要求就跳转至app,有则进行数据更新。这里需要注意一下,由于数据传输过程是在中断场进行处理的,在擦除过程中需要关闭通信,从而避免边擦除边写入的情况,导致程序崩溃(因为擦除是没有写入速度快的),这里进行了全局中断关闭的操作。在确认擦除完毕之后,打开中断,恢复通信,再进行数据传输实现更新,更新完毕后需要清除DFLASH。这里empty_buffer是全为0的数组。

       再更新完毕之后需要进行复位操作,不可以直接进行跳转,由于此时bootloader内的底层硬件驱动配置依然存在,可能会引起程序跳转到app后,由于app的驱动配置不同,此时程序执行的是app,但是驱动仍然是bootloader的配置,导致出现一系列问题。

     复位之后,程序重新运行到bootloader,对中断进行关闭后跳转到app,重新初始化硬件层,执行新APP程序。

     再APP程序中同样也可以进行操作,收到升级标志位之后立马改写Dflash,随后进行复位操作,从bootloader进行自检标志位,进行升级操作,这样不管是程序运行到了bootloader阶段还是app阶段,都可以收到上位机的升级指示进行刷写操作。

      这样的处理在实车操作上是非常方便的,只要车辆处于上电过程,都可以进行刷写。若只在bootloader进行判断,在1s内进行操作,操作难度比较大,对时间的把握比较难,成功概率很低,且需要对车辆进行反复的上下电,十分的不方便。

      

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值