本设计模拟一个车载电子油门踏板检测系统,采用NXP汽车级主控芯片S32K118,使用磁角度传感器AS5147P来模拟检测汽车的油门脚踏板磁角度,采用CAN进行通信控制Boot升级。
升级是用户的程序在运行过程中对Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。 通常实现功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在Flash不同区域中,当芯片上电后,首先是第一个项目代码开始运行,如果进行升级的话接收完代码数据擦写到相应的地址然后跳转执行。
第一份代码通过JTAG或者SWD烧入,第二份功能代码通过第一份代码接收擦写烧入。第一份代码我们称之为Bootloader
程序,第二个功能代码称为APP
程序。这两个程序存放在Flash的不同地址,一般刚一开始是Bootloader
程序,后面紧接着是APP
程序。本次项目因为要实现回滚升级,在升级失败的时候可以回到上一个版本的代码,所以本次Flash中存放两个APP
代码。
本次项目主要分为两部分主机和从机。主机的功能是实现UART转CAN
,进行升级代码的数据传输。从机主要是执行Boot
程序,在进行升级后跳转执行APP
程序,将测量的磁角度数据显示在OLED上。
在硬件方面,主要是稳压电路
,传感器电路
和CAN收发器电路
,比较简单。
主机程序在上电之后,就开始等待PC上位机发送第一帧代码数据,当接收到一帧数据后,通过CAN
发送一帧数据给从机,在收到从机的应答之后再发送下一帧数据,保证数据的同步性,最后在收到上位机发出的EOT结束信号结束接收数据。
数据通信采用Xmodem
协议,Xmodem
协议的细节可以看我另一个博客。主要流程如下图
int main(void)
{
flexcan_msgbuff_t recvBuff;
uint32_t bytesRemaining = 0;
uint8_t frame_buf[XMOLEN] = {0};
GPIOInit();
FlexCANInit();
LPUART_DRV_Init(INST_LPUART1, &lpuart1_State, &lpuart1_InitConfig0);
LPUART_DRV_ReceiveDataPolling(INST_LPUART1, frame_buf, XMOLEN); /* 接收第一帧数据 */
while(LPUART_DRV_GetReceiveStatus(INST_LPUART1, &bytesRemaining) != STATUS_SUCCESS);
CANSendData(&(frame_buf[DATABIT]), DATALEN);
while(1)
{
CANRevData(&recvBuff); /* 接收从机的应答信号,有信号才传输下一帧 */
Xmodem_CANSend(frame_buf, recvBuff.data, 700000); /* Xmodem数据解析传输 */
}
}
从机在一上电的时候执行Boot
程序,等待是否进入升级模式
倒数15秒,超时没有接收到信号,会跳转执行未升级的APP
程序;如果在这期间接收到BOOT
信号,则进入升级流程,不断接收代码数据并擦写到对应地址的Flash。如果长时间接收不到数据或者升级期间出现错误,就会进行回滚升级执行上一个版本的代码。最后接收到OVER
命令结束升级,跳转执行升级APP
程序。
升级方案是两块Flash进行轮流擦写。初始化之后,先判断这次应该升级哪块Flash,接收到升级命令后,不断接收代码数据进行擦写,升级完毕进行跳转执行,升级失败进行回滚到另外一块Flash执行上次的代码。
需要更改每个代码的链接文件来进行区域划分,我的区域划分如下:
int main(void)
{
flexcan_msgbuff_t recvBuff = {0};
uint32_t appEntry, appStack;
uint32_t EarseAddr, BackupAddr;
uint8_t YES[8] = "YES";
uint8_t data[DATALEN] = {0};
uint32_t time = 500000;
FlexCANInit();
flash_init();
Tim_Init();
if(*(uint8_t *)(FLASH_FLAG_ADDR) == 'A') { /* A代表这次要擦写从Flash */
EarseAddr = APP_BACKUP_START;
BackupAddr = APP_IMAGE_START;
PINS_DRV_ClearPins(PTA, (1 << 12)); /* 黄灯亮烧写B代码 */
}
else {
EarseAddr = APP_IMAGE_START;
BackupAddr = APP_BACKUP_START;
PINS_DRV_ClearPins(PTA, (1 << 13)); /* 绿灯亮烧写A代码 */
}
CANRevBoot(&recvBuff, data, 8); /* 接收Boot命令帧,十秒内没接收到自动执行APP函数 */
INT_SYS_DisableIRQ(LPTMR0_IRQn);
if(strncmp((char *)data, "BOOT", 5) == 0) {
CANRevData(&recvBuff, data, DATALEN); /* 接收一帧Xmodem数据 */
while(1) {
//CANRevData(&recvBuff, data, DATALEN); /* 接收一帧Xmodem数据 */
if(strncmp((char *)data, "OVER", 5) == 0) { /* 接收到OVER,数据传输完毕后跳转执行APP代码 */
if(EarseAddr == APP_IMAGE_START) {
flash_erase_sector(FLASH_FLAG_ADDR); /* 记录上次擦写哪块扇区 */
flash_program_data(FLASH_FLAG_ADDR, (uint8_t *)"A", 8);
}
else {
flash_erase_sector(FLASH_FLAG_ADDR);
}
appStack = *(uint32_t *)(EarseAddr);
appEntry = *(uint32_t *)(EarseAddr + 4);
while(time--); //必须等待一会
Bootup_Application(appEntry, appStack);
}
if(TimeOutBack == 0) { /* 传输超时,执行上次代码 */
break;
}
WriteDataFlash(EarseAddr, data); /* 将接收的代码擦写进相应的地址 */
CANSendData(YES); /* 给主机应答,表示可以传输下一帧数据 */
CANRevData(&recvBuff, data, DATALEN); /* 接收一帧Xmodem数据 */
}
}
appStack = *(uint32_t *)(BackupAddr);
appEntry = *(uint32_t *)(BackupAddr + 4);
while(time--); //必须等待一会
Bootup_Application(appEntry, appStack); //跳转执行APP函数
}
在需要读取芯片寄存器的值时,需要按照流程进行地址读取,每个寄存器有对应的地址,读命令帧由14位寄存器地址,读写位和奇偶校验位组成。发起一次读命令的过程如下图,首先MOSI
发送读命令帧,对应的MISO
返回该地址的寄存器的值。
下面是读取寄存器地址的代码和奇偶校验的代码。
/*
* 读取一次5147P的寄存器数据
* Addr : 寄存器的地址
* 返回值 : 返回16位寄存器数据
*/
uint16_t SPI_Read_5147Data(uint16_t Addr)
{
uint8_t buff[2] = {0};
uint8_t read_command[2];
/* 读写14位设置为1 为读模式 */
Addr |= (1 << 14);
/* 奇偶检验为奇数,校验位为1 */
if(0 != Check_Parity(Addr) % 2) Addr |= (1 << 15);
read_command[0] = (uint8_t)(Addr & 0xFF);
read_command[1] = (uint8_t)((Addr >> 8) & 0xFF);
LPSPI_DRV_MasterTransferBlocking(LPSPICOM1, read_command, buff, 2, 100UL);
return (uint16_t)(buff[1] << 8 | buff[0]);
}
/*
* 奇偶检验位计算函数
*/
uint8_t Check_Parity(uint16_t data)
{
uint8_t num = 0;
while(data) {
data &= (data - 1);
num++;
}
return num;
}
在需要写芯片寄存器的值时,需要连续发送两次数据帧,第一个是写命令帧,第二个是要写的值,写命令帧也是由14位寄存器地址,读写位和奇偶校验位组成。发起一次写命令的过程如下图,首先MOSI
连续发送写命令帧和数据,对应的MISO
返回该地址的寄存器的值。
/*
* 写入一次5147P的寄存器数据
* Addr : 寄存器的地址
* data : 要写入的数据
*/
void SPI_Write_5147Data(uint16_t Addr, uint16_t data)
{
uint8_t write_command[4];
/* 奇偶检验为奇数,校验位为1 */
if(0 != Check_Parity(Addr) % 2) Addr |= (1 << 15);
if(0 != Check_Parity(data) % 2) data |= (1 << 15);
write_command[0] = (uint8_t)(Addr & 0xFF);
write_command[1] = (uint8_t)((Addr >> 8) & 0xFF);
write_command[2] = (uint8_t)(data & 0xFF);
write_command[3] = (uint8_t)((data >> 8) & 0xFF);
LPSPI_DRV_MasterTransferBlocking(LPSPICOM1, write_command, NULL, 4, 100UL);
}
AS5147P磁角度传感器集成了4个霍尔传感器,测量上方磁铁旋转的绝对位置角度信息。我选择的是最高分辨率12位,每转产生1024个脉冲信号,将相应的数据储存到14位的寄存器,所以相当于14位的寄存器对应了旋转的360度。在测量旋转方向时,在A相超前B相90度的时候为顺时针;A相滞后B相90度的时候为逆时针方向。I相主要用来零相校准。
/*
* 角度数据转换函数
* data : 要转换的数据
* 返回值 : 返回转换后的角度
*/
uint16_t Angle_Conversion(uint16_t data)
{
uint16_t angle = 0;
data &= 0x3FFF; /* 提取后14位数据 */
/* 14位数据最大16383对应360°,每转90°增加或减小4096,1°对应变化45个数 */
angle = data * 360 / 16383;
return angle;
}