车用BootLoader是指车辆ECU中的一段用来更新ECU应用程序的代码,它与APP共同存在于ECU中,车辆正常运行时ECU执行APP代码,当有软件更新需求时,上位机可通过预定义好的机制触发ECU BootLoader代码运行,将旧的APP擦除,写入新的APP,从而在不拆除ECU的情况下实现车辆ECU软件的更新。之前写过一篇《英飞凌XC2000系列CAN BootLoader(CAN_BSL)的实现》,感兴趣的朋友看这里:
https://blog.csdn.net/weixin_42967006/article/details/100575202
这篇介绍的是利用英飞凌内置的BootLoader机制实现程序的更新,优点是开发难度小,但不能实现程序更新流程的完全自定义。而现在汽车上控制器的程序更新流程往往由整车厂定义,单片机内置的BootLoader机制往往不能满足要求。而本文所讲的BootLoader是基于UDS,可实现整个流程的自定义,而且基本理念对于各个厂商的单片机都适用,方便移植。本次开发平台是Keil。
关键词:BootLoader、CAN、Flash擦写、程序跳转、错误校验
底层通信协议是用的CAN(ISO 11898),网络层是用的ISO 15765-2,应用层协议是UDS(车辆通用诊断协议,ISO 14229-1),不了解的朋友可自行搜索相关学习资料。想要标准(英文原版)的朋友可点下面的链接:
ISO 11898:
https://download.csdn.net/download/weixin_42967006/11958376
ISO 15765-2/3(15765-2是2015最新DIS版):
https://download.csdn.net/download/weixin_42967006/11958360
ISO 14229-1:
https://download.csdn.net/download/weixin_42967006/11141272
(1)内存分配
将单片机整个Flash存储区域划分为三块,最低地址处用于存放BootLoader代码,上电自动执行本段代码。要注意的是某些型号的单片机上电默认从最高地址处开始执行代码,那么BootLoader代码存储区就应该放在最高地址处。BootLoader代码后的一小块区域用来存放数据和一些标志位,BootLoader代码和APP代码共用该部分空间。其余空间用于存放APP。
英飞凌XC2300系列单片机有两块Flash,地址如下:
因为单片机上电从0xC00000地址处开始执行代码,所以BootLoader程序存放在0xC00000-0xC0BFFF,0xC0C000-0xC0FFFF用作数据存储区,C10000-C3FFFF用来存放APP,Flash1因为代码量比较小没有用到。
BootLoader的Keil工程中进行如下设置:
APP的Keil工程中的设置如下:
(2)内存擦写
这部分内容是每款单片机可能都不一样的地方,每家厂商会定义自己的Flash擦写规则。对于Flash,总的原则是在写入之前必须先擦除,而且不能单字节操作,要以一定字节数为一个单位,而且通常一个擦除单位要包含几个写入单位。下面结合XC2300的Flash来详细说一下。所有与Flash有关的内容在芯片手册中都可以找到,芯片手册可以官网下载,嫌麻烦的朋友也可以从下面的链接下载:
https://download.csdn.net/download/weixin_42967006/11958446
①单位划分:
从小到大依次是:
Block:包含16个字节和9个错误校验位;
Page:包含8Block,128字节;
Sector:包含32Page,4096字节;
Array:Flash0总容量256KB,包含64Sector;Flash1总容量64KB,包含16Sector。
最小的擦除和写入单位都是Page,但同一个Sector中的Page由于物理层的原因会互相干扰(drain disturb),影响数据稳定性,官方提示在使用擦除Page的指令时要小心。所以最好以Sector为擦除单位,以Page为写入单位。我这次因为是试验,所以就擦除和写入都以Page为单位了。
②擦除和写入操作:
擦除是将该区域内所有单元置0。写入也叫编程,每次写入一个Page,写入值的类型是64字节Int型数组,且Page在每次擦除后只能写入一次,修改值需要擦除重新写入。
对于XC2300系列,用户手册中给出了操作Flash的指令序列,在3.10.3.4章节有目录,3.10.4章节是详细介绍。英飞凌官方给提供了一套Flash模拟EEPROM的代码,其中有FlashDriver,接口可供用户直接调用。想研究Flash模拟EEPROM的同学可以在下面的连接下载这套代码:
https://download.csdn.net/download/weixin_42967006/11958660
如果只是想擦写Flash,可以用我下面这套驱动代码,自己做了一些更方便的接口函数,包括按页擦除、按扇区擦除、Int型数组写入、Char型数组写入、Int型标志位写入等函数。基于Keil,可直接调用。
https://download.csdn.net/download/weixin_42967006/11958666
//将char型数组DataPtr写入address地址处
void FlashWrite(char *DataPtr, unsigned long address);
//按扇区擦除,扇区起始地址为sector
void FlashErase(unsigned long sector);
//按页擦除,页起始地址为sector
void FlashErasePage(unsigned long sector);
//将int型数组DataPtr写入address地址处
void FlashWrite_int(uword *DataPtr,unsigned long address);
//先擦除address地址起始的页,再将一个两字节的int型标志位Data写入address地址
void FlashWrite_IntFlag(int Data,unsigned long address);
本流程参考了论文《基于CAN总线的Bootloader研究与实现》,在此表示感谢。这篇论文写得非常好,从底层到网络层、应用层都介绍得比较详细,BootLoader实现机制也很完善,且符合目前主流车企的BootLoader规范,大家可以参考。为防止出现版权问题这里就不放下载链接了。下面来详细说一下流程。
(1)ECU启动流程
这里需要两个标志位,一个是外部更新请求标志位(其实这里就叫更新请求标志位就可以,之所以大家习惯加个外部,是因为原来老的更新机制里可能会在外部设置一个引脚来做更新请求标志位,用引脚电平的高低来判断是否需要更新,相当于硬件标志位,但现在大家都是用软件来实现这个标志位了),另一个是应用程序有效性标志位,在我的程序中是这样定义的:
int huge *UpdateFlag=(int huge *)0xC0C000;
#define update 0x5555
#define normal 0xCCCC
int huge *APPValidation=(int huge *)0xC0C080;
#define APPValid 0x5555
#define APPInvalid 0xCCCC
想改写标志位的时候调用上文FlashDriver中FlashWrite_IntFlag函数即可。
①UpdateFlag:当ECU运行在APP中时,如果上位机给ECU发送诊断请求进入编程会话模式,则程序将它置为update,然后执行重启,重启时ECU检查该标志位,发现是update,则直接进入BootLoader代码中的编程会话模式,等待上位机的指令进行程序更新。进入编程会话的同时将该标志位置为normal。如果在编程会话中接收到进入默认会话的请求或会话层S3定时器超时需要返回默认会话,那么ECU执行重启,这时因为该标志位是normal,所以不再进入BootLoader的编程会话。
②APPValidation:当ECU检查完更新请求标志位后,检查APP有效性标志位。如果APP有效,则跳转到APP运行,如果APP无效,则停留在BootLoader中,进入默认会话模式。该标志位在开始擦除内存(后面讲流程的时候会讲到)前置为APPInvalid,注意这里不能在擦除内存完成后再把这个标志位置为无效,因为这样的话万一擦除内存过程中发生了断电等意外,而此时程序也擦了一半不能运行,就会导致ECU上电后跳转到无效的APP,导致死机。当程序下载完成且检查依赖性(见下文)通过后,也就是保证APP是有效且与硬件兼容的之后,再将这个标志位置为APPValid。
③BootLoader和APP程序之间的跳转:这个也是难点之一。
首先要解决程序跳转到指定地址处执行的问题,这里参考了一篇帖子,讲得简单明了,见下面的链接:
https://blog.csdn.net/electrocrazy/article/details/79709947
我的APP的起始地址是0xC10000,所以跳转代码如下:
((void( huge *)(void))0xC10000)();
但还有个问题是中断向量表的问题,因为BootLoader代码放在Flash的最低地址处,而默认中断向量表地址也是在Flash的起始处,如果跳转到APP之后不修改中断向量表的地址,那么APP的中断将无法使用。市面上各个厂商的单片机基本都会设置一个中断向量基地址寄存器,飞思卡尔的单片机好像叫VBR,我这次用的XC2300叫VECSEG,对于这个寄存器的详细定义可以搜索芯片手册。所以在执行上面的程序跳转代码前,需要设置 VECSEG 寄存器,如下:
VECSEG=0x00C1;
((void( huge *)(void))0xC10000)();
这样就能成功实现程序的跳转了。
④关于程序跳转还有一个需要注意的地方,就是ECU运行过程中难免会出现意想不到的意外情况,比如APP代码运行过程中损坏了自身的一部分代码,造成APP不能运行,而此时更新请求标志位无法置为update,应用程序有效性标志位也处于APPValid状态,ECU每次重启都会自动跳转到无效的代码,造成死机。
我们可以添加一个机制来避免以上情况的出现,就是在上电后一段时间内(如20ms)先不去检查两个标志位的状态,而是等待一帧特定的报文,我这次用的是诊断报文【31 01 FF FF FF FF FF FF】,当收到这帧报文后,将应用程序有效性标志位置为无效。这样我们就可以用上位机以更小的时间间隔持续不断的给ECU发送这帧报文,然后给ECU重新上电,确保ECU在等待的时间内能收到至少一帧报文,这样就可以使ECU停留在BootLoader代码中运行了。
⑤关于XC2300单片机的复位这里做一个简单的介绍:
复位操作与三个寄存器有关:SWRSTCON、RSTCON0、SWRSTCON,具体定义我这里就不多说了,大家可以去用户手册里搜。需要注意的一点是这些寄存器都系统控制寄存器,平时是要上锁的,不能轻易改动,所以在改动前要先解锁才行。复位代码如下:
void MAIN_vUnlockProtecReg(void)
{
uword uwPASSWORD;
SCU_SLC = 0xAAAA; // command 0
SCU_SLC = 0x5554; // command 1
uwPASSWORD = SCU_SLS & 0x00FF;
uwPASSWORD = (~uwPASSWORD) & 0x00FF;
SCU_SLC = 0x9600 | uwPASSWORD; // command 2
SCU_SLC = 0x0000; // command 3
}
void Reset()
{
MAIN_vUnlockProtecReg();
SCU_SWRSTCON = 0x0000; //SWCFG =0000,0000b (standard start)
SCU_RSTCON0 = 0xC000; //.SW=application reset
SCU_SWRSTCON|=0x0002; // generate reset
}
①进入扩展会话模式,以执行下面的操作。功能寻址发送给所有ECU。
②检查编程预条件:这一步主要是检查车辆是否静止、发动机是否停止、电池电压是否满足要求等,因为更新程序会使ECU正常功能失效,所以要确保安全后才能执行。
③关闭DTC更新:该指令用功能寻址发送给所有ECU,因为当前ECU在更新过程中功能失效,可能会造成其他ECU记录故障码,所以暂时停掉所有ECU的故障码更新。
④关闭通信报文:功能寻址发送给所有ECU,主要是为了减少总线负载。
⑤读取认证信息:该步为可选,可以读取一些必要的信息。
(3)主编程
主编程环节中上位机将向ECU发送程序代码,ECU擦除旧的APP,写入新的APP。
①进入编程会话。
②安全访问:通常BootLoader模式下都有单独的安全访问层级,与APP不通用。
③写指纹信息:指纹信息包括编程日期、诊断仪序列号等,以便之后追踪。这个指纹要写在Flash中划分出用来存储数据的区域。
④下载Flash驱动:某些整车厂规定Flash驱动代码不能常驻于Flash中,以便程序运行过程中误调用这些代码破坏Flash存储的信息。但个人觉得意义不大,因为大多数程序运行过程中也需要操作Flash,是需要Flash驱动代码的,所以这次就没有做这一步。如果要做的话,传输FlashDriver代码也是用下文步骤⑥⑦⑧⑨的循环指令来下载。
⑤擦除内存:擦除内存用的是$31服务(详见14229-1),上位机把新的程序需要占用的存储空间的起始地址和长度通过这个指令发送给ECU,ECU接到指令后开始将这段区域擦除。如上文所述,擦除前要将应用程序有效性标志位置为无效。
⑥请求开始传输。
⑦传输APP代码数据。
⑧传输完毕请求退出。以上三个指令的详细定义见14229。
⑨检查完整性:这一步主要是为了检查传输过程中是否出错,也就是上位机发送的数据和ECU收到的数据是否一致。通常用的是CRC32算法,上位机将发送的所有数据用该算法算出一个校验和发送给ECU,ECU收到指令和校验和后,用相同的算法计算所收到的所有程序数据的校验和,如果匹配则校验通过。对于CRC32算法汽车行业通常采用IEEE 802.3的规定,多项式为04C11DB7h,初始值为FFFFFFFFh,校验结果需和FFFFFFFFh按位进行异或计算。算法的实现我参考了下面的代码,亲测可用。
#include
#include
typedef struct {
unsigned long crc;
} CRC32_CTX;
static unsigned long crc32_tbl[256] = {
0x00000000L, 0x77073096L, 0xEE0E612CL, 0x990951BAL, 0x076DC419L,
0x706AF48FL, 0xE963A535L, 0x9E6495A3L, 0x0EDB8832L, 0x79DCB8A4L,
0xE0D5E91EL, 0x97D2D988L, 0x09B64C2BL, 0x7EB17CBDL, 0xE7B82D07L,
0x90BF1D91L, 0x1DB71064L, 0x6AB020F2L, 0xF3B97148L, 0x84BE41DEL,
0x1ADAD47DL, 0x6DDDE4EBL, 0xF4D4B551L, 0x83D385C7L, 0x136C9856L,
0x646BA8C0L, 0xFD62F97AL, 0x8A65C9ECL, 0x14015C4FL, 0x63066CD9L,
0xFA0F3D63L, 0x8D080DF5L, 0x3B6E20C8L, 0x4C69105EL, 0xD56041E4L,
0xA2677172L, 0x3C03E4D1L, 0x4B04D447L, 0xD20D85FDL, 0xA50AB56BL,
0x35B5A8FAL, 0x42B2986CL, 0xDBBBC9D6L, 0xACBCF940L, 0x32D86CE3L,
0x45DF5C75L, 0xDCD60DCFL, 0xABD13D59L, 0x26D930ACL, 0x51DE003AL,
0xC8D75180L, 0xBFD06116L, 0x21B4F4B5L, 0x56B3C423L, 0xCFBA9599L,
0xB8BDA50FL, 0x2802B89EL, 0x5F058808L, 0xC60CD9B2L, 0xB10BE924L,
0x2F6F7C87L, 0x58684C11L, 0xC1611DABL, 0xB6662D3DL, 0x76DC4190L,
0x01DB7106L, 0x98D220BCL, 0xEFD5102AL, 0x71B18589L, 0x06B6B51FL,
0x9FBFE4A5L, 0xE8B8D433L, 0x7807C9A2L, 0x0F00F934L, 0x9609A88EL,
0xE10E9818L, 0x7F6A0DBBL, 0x086D3D2DL, 0x91646C97L, 0xE6635C01L,
0x6B6B51F4L, 0x1C6C6162L, 0x856530D8L, 0xF262004EL, 0x6C0695EDL,
0x1B01A57BL, 0x8208F4C1L, 0xF50FC457L, 0x65B0D9C6L, 0x12B7E950L,
0x8BBEB8EAL, 0xFCB9887CL, 0x62DD1DDFL, 0x15DA2D49L, 0x8CD37CF3L,
0xFBD44C65L, 0x4DB26158L, 0x3AB551CEL, 0xA3BC0074L, 0xD4BB30E2L,
0x4ADFA541L, 0x3DD895D7L, 0xA4D1C46DL, 0xD3D6F4FBL, 0x4369E96AL,
0x346ED9FCL, 0xAD678846L, 0xDA60B8D0L, 0x44042D73L, 0x33031DE5L,
0xAA0A4C5FL, 0xDD0D7CC9L, 0x5005713CL, 0x270241AAL, 0xBE0B1010L,
0xC90C2086L, 0x5768B525L, 0x206F85B3L, 0xB966D409L, 0xCE61E49FL,
0x5EDEF90EL, 0x29D9C998L, 0xB0D09822L, 0xC7D7A8B4L, 0x59B33D17L,
0x2EB40D81L, 0xB7BD5C3BL, 0xC0BA6CADL, 0xEDB88320L, 0x9ABFB3B6L,
0x03B6E20CL, 0x74B1D29AL, 0xEAD54739L, 0x9DD277AFL, 0x04DB2615L,
0x73DC1683L, 0xE3630B12L, 0x94643B84L, 0x0D6D6A3EL, 0x7A6A5AA8L,
0xE40ECF0BL, 0x9309FF9DL, 0x0A00AE27L, 0x7D079EB1L, 0xF00F9344L,
0x8708A3D2L, 0x1E01F268L, 0x6906C2FEL, 0xF762575DL, 0x806567CBL,
0x196C3671L, 0x6E6B06E7L, 0xFED41B76L, 0x89D32BE0L, 0x10DA7A5AL,
0x67DD4ACCL, 0xF9B9DF6FL, 0x8EBEEFF9L, 0x17B7BE43L, 0x60B08ED5L,
0xD6D6A3E8L, 0xA1D1937EL, 0x38D8C2C4L, 0x4FDFF252L, 0xD1BB67F1L,
0xA6BC5767L, 0x3FB506DDL, 0x48B2364BL, 0xD80D2BDAL, 0xAF0A1B4CL,
0x36034AF6L, 0x41047A60L, 0xDF60EFC3L, 0xA867DF55L, 0x316E8EEFL,
0x4669BE79L, 0xCB61B38CL, 0xBC66831AL, 0x256FD2A0L, 0x5268E236L,
0xCC0C7795L, 0xBB0B4703L, 0x220216B9L, 0x5505262FL, 0xC5BA3BBEL,
0xB2BD0B28L, 0x2BB45A92L, 0x5CB36A04L, 0xC2D7FFA7L, 0xB5D0CF31L,
0x2CD99E8BL, 0x5BDEAE1DL, 0x9B64C2B0L, 0xEC63F226L, 0x756AA39CL,
0x026D930AL, 0x9C0906A9L, 0xEB0E363FL, 0x72076785L, 0x05005713L,
0x95BF4A82L, 0xE2B87A14L, 0x7BB12BAEL, 0x0CB61B38L, 0x92D28E9BL,
0xE5D5BE0DL, 0x7CDCEFB7L, 0x0BDBDF21L, 0x86D3D2D4L, 0xF1D4E242L,
0x68DDB3F8L, 0x1FDA836EL, 0x81BE16CDL, 0xF6B9265BL, 0x6FB077E1L,
0x18B74777L, 0x88085AE6L, 0xFF0F6A70L, 0x66063BCAL, 0x11010B5CL,
0x8F659EFFL, 0xF862AE69L, 0x616BFFD3L, 0x166CCF45L, 0xA00AE278L,
0xD70DD2EEL, 0x4E048354L, 0x3903B3C2L, 0xA7672661L, 0xD06016F7L,
0x4969474DL, 0x3E6E77DBL, 0xAED16A4AL, 0xD9D65ADCL, 0x40DF0B66L,
0x37D83BF0L, 0xA9BCAE53L, 0xDEBB9EC5L, 0x47B2CF7FL, 0x30B5FFE9L,
0xBDBDF21CL, 0xCABAC28AL, 0x53B39330L, 0x24B4A3A6L, 0xBAD03605L,
0xCDD70693L, 0x54DE5729L, 0x23D967BFL, 0xB3667A2EL, 0xC4614AB8L,
0x5D681B02L, 0x2A6F2B94L, 0xB40BBE37L, 0xC30C8EA1L, 0x5A05DF1BL,
0x2D02EF8DL
};
void CRC32_Init(CRC32_CTX *ctx)
{
ctx->crc = 0xFFFFFFFFL;
}
void CRC32_Update(CRC32_CTX *ctx, const unsigned char *data, size_t len)
{
for (size_t i = 0; i < len; i++) {
ctx->crc = (ctx->crc >> 8) ^ crc32_tbl[(ctx->crc & 0xFF) ^ *data++];
}
}
void CRC32_Final(CRC32_CTX *ctx, unsigned char *md)
{
ctx->crc ^= 0xFFFFFFFFUL;
*md++ = (ctx->crc & 0xFF000000UL) >> 24;
*md++ = (ctx->crc & 0x00FF0000UL) >> 16;
*md++ = (ctx->crc & 0x0000FF00UL) >> 8;
*md++ = (ctx->crc & 0x000000FFUL);
}
int main(int main, char *argv[])
{
unsigned char crc32[4] = { 0 };
const unsigned char crcBuff[] = "abcdef";
CRC32_CTX ctx = { 0 };
CRC32_Init(&ctx);
CRC32_Update(&ctx, crcBuff, strlen(crcBuff));
CRC32_Final(&ctx, crc32);
printf("%02X%02X%02X%02X\n", crc32[0], crc32[1], crc32[2], crc32[3]);
return 0;
}
⑩检查依赖性:这一步的目的是在确认收到的数据和所发送的数据一致的情况下,检查APP与ECU是否兼容,以防止诊断仪错刷其他ECU的程序给当前ECU。这个通常有这样两种实现方法。第一种是检查程序中某些“桩点”的值,所谓桩点,就是在程序存储地址范围内的、即使程序升级更新它的值也不会变的点,可以采用在APP程序中将常量值定义在指定地址处来实现。第二种方法是选择一种算法,算出一个校验值,插入到程序的特定位置,这个校验值收到后不需要写在内存里,只用来校验。我这次采用的是第二种方法,将编译好的APP程序文件算出一个校验值,加到程序文件中,ECU在检查依赖性步骤中检查这个校验值是否正确。需要注意的是通常编译好的文件是Hex格式或S19格式,这两种格式每行结尾都有校验码,在添加校验值的时候要相应的修改该行的校验码。在检查依赖性通过后,要将应用程序有效标志位置为有效。
①复位:重启被更新的ECU,使其进入APP运行。
②进入扩展会话以进行下面的操作,功能寻址发送给所有ECU。
③使能通信报文:功能寻址发送给所有ECU,重新开启通信报文的发送。
④开启故障码更新:功能寻址发送给所有ECU,注意本步骤和③的顺序不能调换,因为如果先开启故障码记录,那么在还没有开启通信报文的短暂间隙时间内,某些ECU可能会记录其他ECU丢失通信的故障码。
⑤返回默认会话:功能寻址,使所有ECU恢复到正常工作的状态。
至此,整个更新流程就完成了。BootLoader在车上有很大的用处,尤其是售后方面,省去了拆装ECU的麻烦,对更新条件的要求也低,不需要专用烧写工具,有诊断仪即可。尤其是在汽车OTA发展越来越快的今天,对于车上ECU软件更新的需求会越来越大,OTA最底层的实现方法其实也是这种BootLoader,只是可能以后安全算法方面会加强。有关BootLoader的分享就是这些了,谢谢大家的时间和关注。