最近一周一直在基于STM32F429项目的IAP工程,耗时4天才完成,得空记录下来。
文章主要涵盖了以下几点:
1. IAP是什么?
2. bin文件和hex文件的差别
3. ymodem协议介绍及其缺陷
4. RS485通讯
5. IAP的main()函数代码片段
项目的框架如下:
ymodem协议
PC机_超级终端 -----------------> STM32产品的串口
RS485通讯线
从ST官网下载的IAP的SDK,其中包含了经典的ymodem协议和基于STM32F429的HAL库,工程就是基于该SDK开发的。
IAP的具体概念解析网上搜索一大堆,在这里简单描述。IAP(In Application Programming)即在应用编程,常听说的还有ISP(In System Programming)即在系统编程。ISP指的是将通过JTAG等接口将单片机程序烧录进单片机的FLASH(当然也可以是其他存储介质,如SRAM),而IAP指的是采用引导程序(Boot) + 应用程序(App)的方式烧写单片机程序,App是真正实现业务逻辑功能的代码。
一般产品的调试口,也就是JTAG口是被置于机壳里面的,烧写需要打开机壳,也需要专业工具和电脑桌面软件。IAP的Boot程序通过ISP的方式烧录到单片机的低地址的FLASH处,每次单片机复位后会先执行Boot程序,在Boot程序中进行判断,用户是否要升级,若是则从串口(或者网口/CAN通信口)读取App程序写到高地址的FLASH,读写完毕后再跳转到FLASH上App的起始地址,执行业务逻辑功能代码,若否则直接跳转到App的代码处理:
|-- 要升级 --> 读取读取串口发来的APP程序,写入FLASAH目标地址 --|
| |
Boot: 判断是否要升级App -| |
|-- 不升级 ---------------------------------------------------------> 跳转到APP程序起始地址处理
Boot和App都是单片机程序,只是实现的功能不同,前者是为了引导App,后者是为了实现业务逻辑功能。这里有一个关键的动作,就是跳转,即从Boot跳转到App起始地址处。
需要清晰2个概念:
(1) 程序的起始地址
程序的起始地址默认是被放在FLASH的起始地址处,即0x08000000:
Boot是放在这个默认起始地址的,App则要往后移动,这里设置为0x08008000:
需要注意,这是在App没有采用分散加载时设置的程序存放起始地址,若采用了分散加载,则需要修改工程中的.sct文件。详细内容可参照杜春雷的《ARM体系结构及编程》。
(2) 中断向量表的地址
对于STM32来说,每个单片机程序都有一张中断向量表,也就是说,在存有Boot和App的FLASH上就有两张中断向量表,Boot根据Boot程序中的中断向量表发生中断跳转,同理,App就要根据App程序的中断向量表发生中断跳转。中断向量表的摆放位置正是程序的开始地址。所以需要将App的中断向量表的摆放位置放在0x08004000。在system_stm32f4xx.c中:
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif
程序运行时候发生中断/异常/系统调度时就会去读取SCB->VTOR以获取中断向量表的地址;FLASH_BASE即STM32F429的(内置)FLASH的起始地址:
#define FLASH_BASE ((uint32_t)0x08000000)
VECT_TAB_OFFSET是偏移量,所以我们可以通过设置此值指定中断向量表的存放地址。
讲到这里,顺便介绍STM32的中断向量表。表的形态如下:(摘自startup_stm32f429xx.s)
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
; ...
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
; ...
DCD LTDC_ER_IRQHandler ; LTDC error
DCD DMA2D_IRQHandler ; DMA2D
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
显然,Boot程序结束后程序运行指针跳转到App的__initial_sp处,__initial_sp处存放的是App的栈的起始地址。中断向量表的作用是当程序发生异常/中断的时候会根据表中的标号而跳转到具体对应的中断处理函数(ISR)。
关于ymodem协议的接收网上资料甚多,不做过多的介绍了。简单分析ymodem的数据包格式:
/* /-------- Packet in IAP memory ------------------------------------------\
* | 0 | 1 | 2 | 3 | 4 | ... | n+4 | n+5 | n+6 |
* |------------------------------------------------------------------------|
* | unused | start | number | !num | data[0] | ... | data[n] | crc0 | crc1 |
* \------------------------------------------------------------------------/
* the first byte is left unused for memory alignment reasons */
(1) unused:无意义,用于字节对齐
(2) start:起始信号,”SOH”表本数据包的数据区有128字节,”STX”则表示有1024字节
(3) number:数据包的编号,编号为0x00-0xFF。到数据包编号到达255后,将会从0开始计数。
(4) !num:数据包编号的反码
(5) data[0]…data[n]:数据区。对于第一个数据包(编号为0),存放的是文件名
(6) CRC0、CRC1:校验码(只有数据区参与校验)
下面是通讯流程:
(1) 接收方发送一个字符’C’,也就是十六进制0x43。代表接收方已经处于接收数据的状态
(2) 发送方接收到’C’之后,发送头帧数据包,数据包格式如上所述,此时数据区的数据是文件名和文件大小
(3) 接收方收到数据包后发送ACK应答正确,然后发送一个字符’C’,发送方收到’C’后开始发送第二帧数据,第二帧数据即使第一个数据包
(4) 接收方收好数据包后,发送ACK正确应答,然后等待下一包数据传送完毕,继续ACK应答,如此循环
(5) 数据传输完毕后,发送方第一次发EOT,第一次接收方以NAK应答,进行二次确认
(6) 发送方收到NAK后,第二次发EOT。接收方第二次收到结束符,依次以ACK和C做应答
(7) 发送方收到ACK和C之后,发送结束符
(8) 接收方收到结束符之后,以ACK做应答,然后通信正式结束
ymodem协议除了接收数据包外还会将数据包按照指定地址写入FLASH。本SDK使用的是HAL库。
这部分的实现在ymodem.c的Ymodem_Receive()函数中:
#define PACKET_HEADER_SIZE ((uint32_t)3)
#define PACKET_DATA_INDEX ((uint32_t)4)
#define PACKET_START_INDEX ((uint32_t)1)
#define PACKET_NUMBER_INDEX ((uint32_t)2)
#define PACKET_CNUMBER_INDEX ((uint32_t)3)
#define PACKET_TRAILER_SIZE ((uint32_t)2)
#define PACKET_OVERHEAD_SIZE (PACKET_HEADER_SIZE + PACKET_TRAILER_SIZE - 1)
#define PACKET_SIZE ((uint32_t)128)
#define PACKET_1K_SIZE ((uint32_t)1024)
uint32_t flashdestination;
uint8_t aPacketData[PACKET_1K_SIZE + PACKET_DATA_INDEX + PACKET_TRAILER_SIZE];
static HAL_StatusTypeDef ReceivePacket(uint8_t *p_data, uint32_t *p_length, uint32_t timeout)
{
uint32_t crc;
uint32_t packet_size = 0;
HAL_StatusTypeDef status;
uint8_t char1;
*p_length = 0;
//调用HAL库函数接收字符,UartHandle是数据包的句柄
status = HAL_UART_Receive(&UartHandle, &char1, 1, timeout);
if (status == HAL_OK)
{
switch (char1)
{
case SOH:
packet_size = PACKET_SIZE; //PACKET_SIZE为128
break;
case STX:
packet_size = PACKET_1K_SIZE; //PACKET_1K_SIZE为1024
break;
case EOT: //数据传输完毕
break;
case CA:
if ((HAL_UART_Receive(&UartHandle, &char1, 1, timeout) == HAL_OK) && (char1 == CA))
{
packet_size = 2;
}
else
{
status = HAL_ERROR;
}
break;
case ABORT1:
case ABORT2:
status = HAL_BUSY;
break;
default:
status = HAL_ERROR;
break;
}
*p_data = char1;
if (packet_size >= PACKET_SIZE )
{
//接收真正的数据包
status = HAL_UART_Receive(&UartHandle, &p_data[PACKET_NUMBER_INDEX], packet_size + PACKET_OVERHEAD_SIZE, timeout);
//检验数据包
if (status == HAL_OK )
{
if (p_data[PACKET_NUMBER_INDEX] != ((p_data[PACKET_CNUMBER_INDEX]) ^ NEGATIVE_BYTE))
{
packet_size = 0;
status = HAL_ERROR;
}
else
{
/* Check packet CRC */
crc = p_data[ packet_size + PACKET_DATA_INDEX ] << 8;
crc += p_data[ packet_size + PACKET_DATA_INDEX + 1 ];
if (Cal_CRC16(&p_data[PACKET_DATA_INDEX], packet_size) != crc )
{
packet_size = 0;
status = HAL_ERROR;
}
}
}
else
{
packet_size = 0;
}
}
}
*p_length = packet_size;
return status;
}
COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size ) //p_size为输出型参数,用于存放从PC端发来的文件的大小
{
uint32_t i, packet_length, session_done = 0, file_done, errors = 0, session_begin = 0;
uint32_t ramsource, filesize;
uint8_t *file_ptr;
uint8_t file_size[FILE_SIZE_LENGTH], tmp, packets_received;
COM_StatusTypeDef result = COM_OK;
/* APPLICATION_ADDRESS是App的起始地址 */
flashdestination = APPLICATION_ADDRESS;
while ((session_done == 0) && (result == COM_OK))
{
packets_received = 0; //记录接收到的数据包的个数
file_done = 0;
while ((file_done == 0) && (result == COM_OK))
{
switch (ReceivePacket(aPacketData, &packet_length, DOWNLOAD_TIMEOUT))
{
case HAL_OK:
errors = 0;
switch (packet_length)
{
case 2:
/* 发送方终止发送 */
Serial_PutByte(ACK);
result = COM_ABORT;
break;
case 0:
/* 正常结束传输 */
Serial_PutByte(ACK);
file_done = 1;
break;
default:
/* 数据包编号出错 */
if (aPacketData[PACKET_NUMBER_INDEX] != packets_received)
{
Serial_PutByte(NAK);
}
else
{
if (packets_received == 0)
{
/* 数据包编号为0,证明这是数据区存放文件名的帧数据,事实上这个判断是有误的 */
if (aPacketData[PACKET_DATA_INDEX] != 0)
{
//读取文件名
i = 0;
file_ptr = aPacketData + PACKET_DATA_INDEX;
while ( (*file_ptr != 0) && (i < FILE_NAME_LENGTH))
{
aFileName[i++] = *file_ptr++;
}
aFileName[i++] = '\0';
//读取文件大小
i = 0;
file_ptr ++;
while ( (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH))
{
file_size[i++] = *file_ptr++;
}
file_size[i++] = '\0';
Str2Int(file_size, &filesize);
if (*p_size > (USER_FLASH_SIZE + 1)) //文件大过于可供存储的FLASH的空间
{
tmp = CA;
HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
result = COM_LIMIT;
}
/* 擦除扇区 */
FLASH_If_Erase(APPLICATION_ADDRESS);
*p_size = filesize;
Serial_PutByte(ACK);
Serial_PutByte(CRC16);
}
/* 文件头为空,传输结束 */
else
{
Serial_PutByte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
else /* 真正的数据包 */
{
//将收到的数据存放到FLASH
ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];
if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
{
flashdestination += packet_length;
Serial_PutByte(ACK);
}
else //写入失败
{
/* End session */
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_DATA;
}
}
packets_received ++;
session_begin = 1;
}
break;
}
break;
case HAL_BUSY:
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_ABORT;
break;
default:
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS) errors大于MAX_ERRORS,PC端将收到"接收端未响应的提示",终止传输
{
/* Abort communication */
Serial_PutByte(CA);
Serial_PutByte(CA);
}
else
{
Serial_PutByte(CRC16); //返回'C'字符,PC端提示接收端未响应并记录次数,超过次数PC端也将提出
}
break;
}
}
}
return result;
}
事实上,对数据包的接收处理操作是有问题的:packets_received用于记录数据包的个数,它是uint8_t类型,取值是0-255这确实是符合ymodem协议的,但是超过255的数据包呢,观察上面代码可以发现并没有对第超过255个数据包,也就是第256个数据包的处理。第256个数据包的编号也是为0,会进入:
if (packets_received == 0)
{
if (aPacketData[PACKET_DATA_INDEX] != 0)
{
//进行读取文件名、文件大小、擦除FLASH操作
}
}
但是编号为0的第256个数据包的数据区事实上是数据,收到该数据包还是要以真正的数据包的写FLASH等操作。
一开始我利用IAP传输小于256K的APP的时候是正常运行的,后来传输400+k的APP就会出现问题:无法跳转至APP。经过多番调试才定位于此,所以简单修改上面的代码,代码片段为:
volatile int8_t is_first_pack = 1;
switch (RPreturn)
{
case HAL_OK:
errors = 0;
after_isp = 1;
switch (packet_length)
{
case 2:
Serial_PutByte(ACK);
result = COM_ABORT;
break;
case 0:
Serial_PutByte(ACK);
file_done = 1;
break;
default:
if (aPacketData[PACKET_NUMBER_INDEX] != packets_received) //ÅжÏÊý¾Ý°üµÄ±àºÅÊÇ·ñÕýÈ·
{
Serial_PutByte(NAK);
}
else
{
if (packets_received == 0 )
{
if (aPacketData[PACKET_DATA_INDEX] != 0 )
{
if (is_first_pack) //第一个编号为0的数据包
{
//...
is_first_pack = 0;
}
else //即使数据编号为0但不是第一个数据包,采取存储操作
{
ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];
if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
{
flashdestination += packet_length;
Serial_PutByte(ACK);
}
else
{
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_DATA;
}
}
}
else
{
Serial_PutByte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
else
{
ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];
if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
{
flashdestination += packet_length;
Serial_PutByte(ACK);
}
else
{
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_DATA;
}
}
packets_received ++;
packets_received = packets_received % 256;
session_begin = 1;
}
break;
}
break;
//..
break;
}
第一次接触Hex文件一般是在学习51内核单片机的时候,通过烧录器烧录到开发板上使用的就是Hex格式的文件。Hex文件是以ASCII文本形式保存编译后的二进制文件信息,注意这里强调的是文本文件(而非数据文件,即二进制文件)。文本文件是人们可以看得懂的文件,但是计算机/MCU只认识二进制数据文件(Bin文件),Bin文件才是MCU固件烧写的最终形式,也就是说MCU的ROM中烧写的内容完全是Bin文件。由此可得,我们通过烧录器烧录Hex文件到单片机的ROM时,烧录器其实会将Hex文件的数据转为Bin文件的数据,最后才烧录到ROM。
其实明白这一点我们就知道在IAP工程中,APP文件需要是Bin格式的文件而不能是Hex文件。既然Bin文件是MCU/计算机最终想要的,为什么我们不直接生成Bin文件,而却要生成Hex文件?其实Hex文件保存的不仅是Bin文件的内容,还有一些附属配置信息,随便拿个项目Hex文件分析:
: 10 9AB0 00 6841298459D0420F9AC4641EFBC10408 2E
: 10 9AC0 00 E7604BC5CC64011029B85A6980413C55 08
: 08 9AD0 00 55557C2964291C81 15
: 04 0000 05 080081AD C1
: 00 0000 01 FF
Hex文件中的数据是ASCII编码,所以是人们能看懂的。上面3行内容,每行都是以’:’开始的,之后是数据长度、地址域、数据类型、数据域、校验和。
/* /-------- Hex Data format -------------------------------------------------------------\
* | 0 | 1 | 3、4 | 5 | 6 | 7 | ... | n | n + 1 |
* |--------------------------------------------------------------------------------------|
* | : 开始 | 数据长度 |地址域 | 记录类型 | 数据域 | 数据域 | 数据域 | 数据域n | 校验和 |
* \--------------------------------------------------------------------------------------/
* 每行数据都是以冒号开始的 */
注:记录类型的意义
(1) 00: 数据记录
(2) 01: 文件结束记录
(3) 02: 扩展段地址记录
(4) 03: 段开始地址记录
(5) 04: 扩展线性地址记录
(6) 05: 线性地址开始记录
由此可见,生成Hex文件的意义在于:
(1) Hex文件使用ASCII文本保存固件信息,方便查看固件内容
(2) 文件内容每行的校验和与最后一行的文件结束标志,在文件的传输与保存过程中能够检验固件是否完整
因此hex文件更适用于保存与传输。相比之下,Bin文件纯二进制文件,内部只包含程序编译后的机器码和变量数据。当文件损坏时,我们也无法知道文件已损坏。不过在IAP中,Bin文件仍旧是不可替代的。
从ST官网下载的的IAP SDK是基于RS232通讯的,即ymodem默认是从串口接收在APP数据的。但是我这个实际项目中用到的是采用RS485的通讯。RS232转为RS485,在软件上只是多了一步方向控制操作。因为RS485是半双工通讯,所以在发送单片机需要发送数据时需要将RS485总线设置为发送状态,接收数据则需要设置为接受状态。关于收发控制,软件上实现只是拉高/拉低对应控制RS485控制芯片的接收状态的GPIO即可。我的做法是默认是接收状态,当要发送时在发送函数内切换为发送状态,函数退出之前又切回接收状态。具体关于RS485通讯的详细,可阅读http://blog.csdn.net/qq_29344757/article/details/71516037一文中。
IAP升级代码工程,一般需要基于一个能跑起来的通讯(这里是指RS485)和LED灯(用于指示状态,当然也可以用LCD等其他提示状态)运行正常的工程代码。下面是main()函数:
void SerialDownload(void);
//定义函数指针,用于跳转到APP
typedef void (*pFunction)(void);
extern pFunction JumpToApplication;
extern uint32_t JumpAddress;
UART_HandleTypeDef UartHandle;
int main(void)
{
HAL_Init();
SystemClock_Config();
LED_BSP_Init();
FLASH_If_Init();
//初始化RS485
UART_Init();
RS485_RX_ENABLE(); //默认设置为接收状态
HAL_Delay(10);
IAP:
Serial_PutString((uint8_t *)"\r\n====================================================================");
Serial_PutString((uint8_t *)"\r\n= IAP For STM32F429xx =");
Serial_PutString((uint8_t *)"\r\n====================================================================\r\n");
SerialDownload();
HAL_Delay(10);
//APPLICATION_ADDRESS是在FLASH中存放APP的起始地址
//此判断是为了保证APP的栈地址是在SRAM中。其实结果并不一定是0x20000000。有些APP可能定义的全局变量较多,那么栈的起始地址会偏移
if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
{
Serial_PutString((uint8_t *)"\r\n======================= Run application ======================= \r\n\n");
HAL_Delay(10);
/* Jump to user application */
JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
JumpToApplication = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
JumpToApplication();
}
Serial_PutString((uint8_t *)"\r\n ======================= Download error, once again ======================= \r\n\n");
goto IAP;
}
IAP的实现还是十分简单,但是中间走了N多弯路,特别是在调试ymodem接收大于256kb的APP上。关键要注意上述几点内容。当然,上述内容属于个人见解。