继续固件升级的一些内容-下位机部分
本地固件升级分上位机和下位机两部分内容,我们分开讨论。
关于下位机
固件升级下位机部分我们也可以理解为两部分。BootLoader和APP,前面也简单介绍过这两部分的作用和一些注意事项。现在再详细说说。
BootLoader
BootLoader简单来说主要有两个作用:
- 接收并固件包
- 指定程序的跳转
当然也可以做其他的事,比如,在这部分对程序进行一定的保护,比如基于ChipUniqueID对程序进行加密(前面专门介绍过),对Flash进行读、写保护或者解除Flash的读写保护、程序更新失败的回滚或者升级备份等等。
如何接收固件包
关于如何接收固件包,建议大家采用一个协议,无论是现成的Ymodem、Xmodem之类的或者自定义的也好,这样可以给固件包的传输提供一定的保障(解决丢包、包错误等问题)。
每个包有固定的包序号,包大小,帧头帧尾和CRC校验等。我前面介绍的Ymodem协议就是一个不错的选择。
接收固件包的时候要注意一边接收检验一边写Flash,因为大多数单片机的RAM空间并不足以在运行BootLoader的同时还能存放所有的固件包数据。
写Flash之前一定要解锁FLASH_Unlock()
。本来在初始化程序的时候就解锁了Flash,但是在程序加密的时候不小心又上锁了FLASH_Lock()
,然后就导致接收完一帧数据写入失败,刚开始还以为是协议哪里出了问题,花大半天时间去研究Ymodem协议,所幸后来很快就通过在线调试定位到了问题。
接收数据的手段也有很多,SPI
、IIC
、UART
、USB
等等,有兴趣的,甚至可以研究一下通过IO
来接收数据。常用的有串口(UART)升级、SD卡升级、U盘(USB)升级。因为产品是通过串口通信的,所以这里选择的是串口IAP升级。
另外就是在串口结束数据的时候,有网友说利用空闲中断接收过长的数据很容易出现问题,所以建议关闭串口中断,不使用中断的方式接收数据。
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
USART_ITConfig(USART2, USART_IT_IDLE, DISABLE);
关于Ymodem协议的CRC16计算:
/**
* @brief Cal CRC16 for YModem Packet
* @param data
* @param length
* @retval CRC value
*/
uint16_t Cal_CRC16(const uint8_t* pData, uint32_t len)
{
uint16_t crc = 0;
while(len--)
{
crc ^= (uint16_t)(*(pData++)) << 8;
for(int i = 0; i < 8; i++)
{
if(crc & 0x8000)
{
crc = (crc << 1) ^ 0x1021;
}
else
{
crc = crc << 1;
}
}
}
return crc;
}
Ymodem接收数据:
/**
* @brief Receive a packet from sender
* @param data
* @param length
* @param timeout
* 0: end of transmission
* -1: abort by sender
* >0: packet length
* @retval 0: normally return
* -1: timeout or packet error
* 1: abort by user
*/
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{
uint16_t i, packet_size, computedcrc;
uint8_t c;
*length = 0;
if (Receive_Byte(&c, timeout) != 0)
{
return -1;
}
switch (c) //The first byte of the received data
{
case SOH: //Packet start
packet_size = PACKET_SIZE;
break;
case STX: //Start of Text
packet_size = PACKET_1K_SIZE;
break;
case EOT: //End of packet
return 0;
case CA: //The sender stop transmission
if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
{
*length = -1;
return 0;
}
else
{
printf(" stop transmission!\r\n");
return -1;
}
case ABORT1:
case ABORT2:
return 1;
default:
return -1;
}
*data = c;
/*Acquiring the remaining data (in bytes)*/
for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++)
{
if (Receive_Byte(data + i, timeout) != 0)
{
printf(" Acquiring the remaining data err!\r\n");
return -1;
}
}
/*Check serial number and complement*/
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))
{
printf(" Check serial number and complement err!\r\n");
return -1;
}
/* Compute the CRC */
computedcrc = Cal_CRC16(&data[PACKET_HEADER], (uint32_t)packet_size);
/* Check that received CRC match the already computed CRC value
data[packet_size+3]<<8) | data[packet_size+4] contains the received CRC
computedcrc contains the computed CRC value */
if (computedcrc != (uint16_t)((data[packet_size+3]<<8) | data[packet_size+4]))
{
/* CRC error */
return -1;
}
*length = packet_size;
return 0;
}
/**
* @brief Receive a file using the ymodem protocol
* @param buf: Address of the first byte
* @retval The size of the file
*/
int32_t Ymodem_Receive (uint8_t *buf)
{
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
int32_t i, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
uint32_t flashdestination, ramsource;
/* Initialize flashdestination variable */
flashdestination = APPLICATION_ADDRESS;
Send_Byte(CRC16);
for (session_done = 0, errors = 0, session_begin = 0; ;)
{
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
{
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
{
case 0:
errors = 0;
switch (packet_length)
{
/* Abort by sender */
case - 1:
Send_Byte(ACK);
return 0;
/* End of transmission */
case 0:
Send_Byte(ACK);
file_done = 1;
break;
/* Normal packet */
default:
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
{
Send_Byte(NAK);
}
else
{
if (packets_received == 0)
{
/* Filename packet */
if (packet_data[PACKET_HEADER] != 0)
{
/* Filename packet has valid data */
for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
{
FileName[i++] = *file_ptr++;
}
FileName[i++] = '\0';
for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < (FILE_SIZE_LENGTH - 1));)
{
file_size[i++] = *file_ptr++;
}
file_size[i++] = '\0';
Str2Int(file_size, &size);
/* Test the size of the image to be sent */
/* Image size is greater than Flash size */
if (size > (USER_FLASH_SIZE + 1))
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -1;
}
/* erase user application area */
FLASH_If_Erase(APPLICATION_ADDRESS);
Send_Byte(ACK);
Send_Byte(CRC16);
}
/* Filename packet is empty, end session */
else
{
Send_Byte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
/* Data packet */
else
{
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
ramsource = (uint32_t)buf;
/* Write received data in Flash */
if ( FLASH_If_Write(&flashdestination, (uint32_t*) ramsource, (uint16_t) packet_length/4) == 0)
{
Send_Byte(ACK);
}
else /* An error occurred while writing to Flash memory */
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -2;
}
}
packets_received ++;
session_begin = 1;
}
}
break;
case 1:
Send_Byte(CA);
Send_Byte(CA);
return -3;
default:
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS)
{
Send_Byte(CA);
Send_Byte(CA);
return 0;
}
Send_Byte(CRC16);
break;
}
if (file_done != 0)
{
break;
}
}
if (session_done != 0)
{
break;
}
}
return (int32_t)size;
}
串口数据的接收和发送函数就粘贴了,一个字节的收发而已,没什么需要特别注意的。
关于固件的保存
BootLoader从接收固件到保存固件,主要有下面几个步骤:
- 初始化Flash(主要是解锁)
- 基于Ymodem协议从串口接收数据
- 校验接收的数据
- 擦除Flash
- 写Flash,写完之后再读出来,和接收内存中的数据比对一下,防止写FLASH错误
初始化Flash:
/**
* @brief Initialize the FLASH.
* @param None
* @retval FLASH Status
*/
FLASH_Status stm32_flash_init(void)
{
/*Unlock the Flash to enable the flash control register access*/
FLASH_Unlock();
/*Clear pending flags*/
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
/*Lock the Flash to disable the flash control register access*/
return FLASH_COMPLETE;
}
擦除Flash:
/**
* @brief This function does an erase of all user flash area
* @param StartSector: start of user flash area
* @retval 0: user flash area successfully erased
* 1: error occurred
*/
uint32_t FLASH_If_Erase(uint32_t StartSector)
{
uint32_t flashaddress;
flashaddress = StartSector;
while (flashaddress <= (uint32_t) USER_FLASH_LAST_PAGE_ADDRESS)
{
if (FLASH_ErasePage(flashaddress) == FLASH_COMPLETE)
{
flashaddress += FLASH_PAGE_SIZE;
}
else
{
/* Error occurred while page erase */
return (1);
}
}
return (0);
}
写固件到Flash:
/**
* @brief This function writes a data buffer in flash (data are 32-bit aligned).
* @note After writing data buffer, the flash content is checked.
* @param FlashAddress: start address for writing data buffer
* @param Data: pointer on data buffer
* @param DataLength: length of data buffer (unit is 32-bit word)
* @retval 0: Data successfully written to Flash memory
* 1: Error occurred while writing data in Flash memory
* 2: Written Data in flash memory is different from expected one
*/
uint32_t FLASH_If_Write(__IO uint32_t* FlashAddress, uint32_t* Data ,uint16_t DataLength)
{
uint32_t i = 0;
for (i = 0; (i < DataLength) && (*FlashAddress <= (USER_FLASH_END_ADDRESS-4)); i++)
{
/* the operation will be done by word */
if (FLASH_ProgramWord(*FlashAddress, *(uint32_t*)(Data+i)) == FLASH_COMPLETE)
{
/* Check the written value */
if (*(uint32_t*)*FlashAddress != *(uint32_t*)(Data+i))
{
/* Flash content doesn't match SRAM content */
return(2);
}
/* Increment FLASH destination address */
*FlashAddress += 4;
}
else
{
/* Error occurred while writing data in Flash memory */
return (1);
}
}
return (0);
}
有时候可能会对固件写保护,这样可以保护存在MCU上的代码 不会被异常修改,但是在正常的代码更新时要先失能对应的保护位,更新完成再次恢复之前的保护位
/**
* @brief Disable FLASH Write ProtectionPages.
* @param None
* @retval None
*/
void FLASH_DisableWriteProtectionPages(void)
{
uint32_t UserMemoryMask = 0;
uint32_t useroptionbyte = 0, WRPR = 0;
uint16_t var1 = OB_IWDG_SW, var2 = OB_STOP_NoRST, var3 = OB_STDBY_NoRST;
FLASH_Status status = FLASH_BUSY;
WRPR = FLASH_GetWriteProtectionOptionByte();
/*Test whether the write protection*/
if ((WRPR & UserMemoryMask) != UserMemoryMask)
{
useroptionbyte = FLASH_GetUserOptionByte();
UserMemoryMask |= WRPR;
status = FLASH_EraseOptionBytes();
if (UserMemoryMask != 0xFFFFFFFF)
{
status = FLASH_EnableWriteProtection((uint32_t)~UserMemoryMask);
}
if ((useroptionbyte & 0x07) != 0x07)
{
/*Re-save option byte*/
if ((useroptionbyte & 0x01) == 0x0)
{
var1 = OB_IWDG_HW;
}
if ((useroptionbyte & 0x02) == 0x0)
{
var2 = OB_STOP_RST;
}
if ((useroptionbyte & 0x04) == 0x0)
{
var3 = OB_STDBY_RST;
}
FLASH_UserOptionByteConfig(var1, var2, var3);
}
if (status == FLASH_COMPLETE)
{
printf("Write Protection disabled...\r\n");
printf("...and a System Reset will be generated to re-load the new option bytes\r\n");
/*Initiate a system reset request to reset the MCU*/
NVIC_SystemReset();
}
else
{
printf("Error: Flash write unprotection failed...\r\n");
}
}
else
{
printf("Flash memory not write protected\r\n");
}
}
写保护:
先上代码:
/**
* @brief Write protects the desired pages
* @note This function can be used for all STM32F10x devices.
* @param FLASH_Pages: specifies the address of the pages to be write protected.
* This parameter can be:
* @arg For @b STM32_Low-density_devices: value between FLASH_WRProt_Pages0to3 and FLASH_WRProt_Pages28to31
* @arg For @b STM32_Medium-density_devices: value between FLASH_WRProt_Pages0to3
* and FLASH_WRProt_Pages124to127
* @arg For @b STM32_High-density_devices: value between FLASH_WRProt_Pages0to1 and
* FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to255
* @arg For @b STM32_Connectivity_line_devices: value between FLASH_WRProt_Pages0to1 and
* FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to127
* @arg For @b STM32_XL-density_devices: value between FLASH_WRProt_Pages0to1 and
* FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to511
* @arg FLASH_WRProt_AllPages
* @retval FLASH Status: The returned value can be: FLASH_ERROR_PG,
* FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
*/
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages)
{
uint16_t WRP0_Data = 0xFFFF, WRP1_Data = 0xFFFF, WRP2_Data = 0xFFFF, WRP3_Data = 0xFFFF;
FLASH_Status status = FLASH_COMPLETE;
/* Check the parameters */
assert_param(IS_FLASH_WRPROT_PAGE(FLASH_Pages));
FLASH_Pages = (uint32_t)(~FLASH_Pages);
WRP0_Data = (uint16_t)(FLASH_Pages & WRP0_Mask);
WRP1_Data = (uint16_t)((FLASH_Pages & WRP1_Mask) >> 8);
WRP2_Data = (uint16_t)((FLASH_Pages & WRP2_Mask) >> 16);
WRP3_Data = (uint16_t)((FLASH_Pages & WRP3_Mask) >> 24);
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if(status == FLASH_COMPLETE)
{
/* Authorizes the small information block programming */
FLASH->OPTKEYR = FLASH_KEY1;
FLASH->OPTKEYR = FLASH_KEY2;
FLASH->CR |= CR_OPTPG_Set;
if(WRP0_Data != 0xFF)
{
OB->WRP0 = WRP0_Data;
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
}
if((status == FLASH_COMPLETE) && (WRP1_Data != 0xFF))
{
OB->WRP1 = WRP1_Data;
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
}
if((status == FLASH_COMPLETE) && (WRP2_Data != 0xFF))
{
OB->WRP2 = WRP2_Data;
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
}
if((status == FLASH_COMPLETE)&& (WRP3_Data != 0xFF))
{
OB->WRP3 = WRP3_Data;
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
}
if(status != FLASH_TIMEOUT)
{
/* if the program operation is completed, disable the OPTPG Bit */
FLASH->CR &= CR_OPTPG_Reset;
}
}
/* Return the write protection operation Status */
return status;
}
这些基本都是官方库里面的操作。
量产的时候可以如此操作,也是对代码的一种保护。
对应的还有读保护和解除读保护,后面遇到再说。
关于代码的跳转运行
BootLoader主要负责的还有代码的跳转执行,大体框架如下:
if("升级条件")
{
//升级引导操作;
}
else
{
/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
if (((*(vu32*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
{
/* Jump to user application */
JumpAddress = *(vu32*) (APPLICATION_ADDRESS + 4);
JumpToApplication = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(vu32*) APPLICATION_ADDRESS);
JumpToApplication();
}
else
{
printf(" no user Program\r\n\n");
//升级引导操作/其他操作;
}
pFunction JumpToApplication;
typedef void (*pFunction)(void);
这里面注意的有三点:
- 触发升级的条件
- 跳转的APP程序地址
- 跳转方式
如何触发升级:也有很多选择,比如新增加一个按键,或者上电后延时一定的升级选择时间然后跳转APP,在或者置一个标志位。
在本着能不改动硬件就不改动硬件的原则,我一般在APP程序中通过串口接收一个特定的字符串,然后在Flash的某个地方写入一个标志位,比如在备份寄存器写版本号,然后软件复位单片机。
__disable_fault_irq();
NVIC_SystemReset();
单片机复位后运行BootLoader,先检查该Flash区域有无内容,有则引导升级,升级完成后再擦除对应Flash里面的内容。如果上电后没在对应的区域内检测到内容,则先跳转APP程序。跳转APP程序后也会先判断APP区的内容,没有的话(第一次只烧写了BootLoader,还未烧写APP)再次跳转到升级引导。
跳转的APP程序地址这个是提前要做的内容,根据BootLoader的大小,预留出足够的BootLoader空间和一些必须的Flash空间,然后指定APP的起始地址。
说到Flash空间分配的问题,值得注意的是如果设计OTA升级,最好预留一个升级失败的处理方案,比如原固件的备份与回滚问题,都得提前考虑好。本地升级好处是升级失败了大不了再升级一次,OTA升级失败可能会干扰联网以及数据传输问题。
跳转方式涉及到函数指针的内容,typedef void (*pFunction)(void)
定义了一个新的函数指针类型,指向的函数形参为void
,返回值为void
。
关于函数指针
这个本人接触的也比较少,按照网友的说法,一步一步来分析:
- 定义一个指向 unsigned char 型的指针:``unsigned char *p`;
- 将一个指针转化成指向 unsigned char 数据的的指针:
(unsigned char*) p
; - 定义一个类型,该类型为int,并定义一个指向该类型的指针p:
typedef int INT_Typedef INT_Typedef *p
; - 定义一个函数指针,该函数无参数,无返回值:
typedef void (*Func) (void)
;
typedef
的作用是把已知的类型定义新类型,所以新类型(*Func)(void)的返回值是void。
如常见的main()
,括号内是main的参数,所以该函数指针指向的函数无形参。其次(*Func)
等同于main的地位。所以 Func就是函数指针了。
APP
相对于BootLoader程序,APP程序要做的工作就少很多了。
- APP程序和BootLoader用的是不同的中断向量表,所以要重新指定中断向量表地址
- 设置Flash的起始地址和Flash大小
重新指向中断向量飙
先说如何重新定位中断向量表,在程序最前面加上这么一句即可:
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x5000);
或者:
SCB->VTOR = FLASH_BASE | 0x5000;
上面不同的方式其实是同样的方式,只不过第一个是加了一个函数的外壳,Flash基地址加偏移量,偏移量就是在设计BootLoader的时候分配占用的空间大小。
/**
* @brief Sets the vector table location and Offset.
* @param NVIC_VectTab: specifies if the vector table is in RAM or FLASH memory.
* This parameter can be one of the following values:
* @arg NVIC_VectTab_RAM
* @arg NVIC_VectTab_FLASH
* @param Offset: Vector Table base offset field. This value must be a multiple
* of 0x200.
* @retval None
*/
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset)
{
/* Check the parameters */
assert_param(IS_NVIC_VECTTAB(NVIC_VectTab));
assert_param(IS_NVIC_OFFSET(Offset));
SCB->VTOR = NVIC_VectTab | (Offset & (uint32_t)0x1FFFFF80);
}
APP程序的工程配置
- 打开
Options for Target
Target
:修改IROM1 Start地址和Size大小;Start地址为Flash基地址0x8000000
+BootLoader分配的地址大小,Size一般为单片机总Flash大小减去BootLoader分配的地址大小 IROM1地址:0x8000000->0x8005000 大小剩余:0x80000->0x7B000 内存IRAM无需修改- User:勾选Run#1 fromelf.exe --bin -o "[email protected]" "#L" ,把生成的Hex文件转换为bin文件,省去了上位机进行文件转换的麻烦
- Linker --勾选Use Memory Layout from Target Dialog,启用在Target中对Flash和RAM的地址配置
上位机下载
如果选用secure CRT
或者Xsehll
上位机下载,软件需要设置:选项--会话选项--XYZmodem--1024byte
最后
下位机部分要注意的基本就是这些,如果有遗漏的后面再补充。上位机的部分后面有时间在写。欢迎指正
至此,所有!
叨叨几句... NOTHING