本文选用的芯片为车规级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内进行操作,操作难度比较大,对时间的把握比较难,成功概率很低,且需要对车辆进行反复的上下电,十分的不方便。