此博客主要用于记录学习过程中的心得以及防止遗忘,下面的一些图片来源于网上,如有侵犯请联系。
STM32芯片的升级方式众多,这里简单介绍下,主要有ICP((In-Circuit Programming – ICP)、ISP(In-System Programming)、IAP((In-Application Programming – IAP),定义和区别如下:
其中ISP属于ICP中的一种,在下载的时候需要控制boot0/boot1引脚,使用系统自带的加载程序(bootloader)进行升级用户程序,优点是:快递编程、芯片内部可以不需要任何的程序(即空白芯片也可以升级)、芯片占用的资源少。缺点是:只能使用固定的外设和协议,其中stmf10x只能通过USART1来进行升级,灵活性不高。
IAP使用的boot loader是用户自己编写的,所以需要提前把这部分程序烧入flash中。其优点是:很灵活,可以通过各种外设给芯片进行升级,如USART、SPI、以太网、以及SD卡等等,协议完全可以有自己定义,只要上位机和下位机一致既可。缺点是IAP占用一部分flash资源,需要提前把程序烧入进去。
ISP和IAP是STM32芯片升级的趋势,JTAGA和SWD方式在调试程序的时候使用的会多些,但是在出厂后,使用这些方式升级相对来说很麻烦。所以基本上使用ISP和IAP,我做的项目中上位机使用的是3399,采集信号使用的是stm32f10r8,使用SPI外设直接进行升级,3399上面跑安卓系统,只要把升级文件下载下来就可以进行升级。当然这只是其中的一种方式,还可以通过无线通信等方式对stm32进行升级,非常的灵活。
上面的链接是下载官方的IAPdemo,理解了官方的demo后可以自己编写符合自身要求的boot loader程序,需要注意的是这demo使用的固件库是3.3的比较旧。
咱们都知道IAP升级是使用客户自己编写的boot loader而不是系统自带的,所以flash需要为两部分,一部分用来存放IAP(boot loader)程序,另外一部分用于存放用户程序,即芯片真正需要跑的程序。
咱们需要知道芯片从上电到运行的过程。M3的启动流程是:上电后从0x0800 0004取出复位中断向量地址,然后跳转到复位中断程序入口(主要是初始化系统时钟,以及调用_main),执行结束后跳转到main函数。流程如下:
上图是针对运行一个程序的流程图,咱们的IAP+user Programs属于两个程序,所以咱们需要把IAP放在flash的起始地址,即上电后先执行boot loader,查看升级条件是否满足(如按键、引脚的电位等),满足的话就进行升级用户程序,不满足的话直接跳转到之前的用户程序。升级其实就是把flash内的内容擦除然后写入新的程序。
IAP执行流程
板子上电,依然从0x08000004处取出复位中断向量地址,执行复位中断函数后跳转到IAP的main(标号①所示),在IAP的main函数判断升级条件,如果升级条件满足则进行升级,否则强制跳转到0x08000004+N+M处(标号②所示),即用户程序中断向量表的复位中断地址处,执行复位中断,最后跳转到用户main函数中(标号③所示),运行应用程序。
当发生中断请求后,程序跳转到新的中断向量表中取出新的中断服务函数入口地址,再跳转到新的中断服务函数中执行(标号④⑤所示),执行完中断函数后再返回到main函数中来(标号⑥所示)。所以在执行应用程序的时候,需要先设置中断向量表的偏移。要不然用户程序中的中断还是会去原来的中断向量表中获取中断服务函数,这里可能会导致运行死机的情况。
对于步骤④⑤,在main函数的执行过程中,如果CPU得到一个中断请求,PC指针本来应该跳转到0x08000004处的中断向量表,由于我们设置了中断向量表偏移量为N+M,因此PC指针被强制跳转到0x08000004+N+M处的中断向量表中得到相应的中断函数地址,再跳转到相应新的中断服务函数,执行结束后返回到main函数中来
原理就是这么简单,下面分析下官方的demo,这里以stm32f10x为例(AN2557)。
IAP/src/main.c
int main(void)
{
/* Flash unlock */
/*flash解锁,因为需要操作flash*/
FLASH_Unlock();
/* Initialize Key Button mounted on STM3210X-EVAL board */
/*初始化按键,demo中的升级触发条件为按键按下*/
STM_EVAL_PBInit(BUTTON_KEY, BUTTON_MODE_GPIO);
/* Test if Key push-button on STM3210X-EVAL Board is pressed */
if (STM_EVAL_PBGetState(BUTTON_KEY) == 0x00)
{
/* If Key is pressed */
/*如果按键按下,即触发升级,进行升级*/
/* Execute the IAP driver in order to re-program the Flash */
/*初始化串口,demo里面使用的是usart1 + Y-MODe协议*/
IAP_Init();
SerialPutString("\r\n================================================================");
SerialPutString("\r\n= (C) COPYRIGHT 2010 STMicroelectronics =");
SerialPutString("\r\n= =");
SerialPutString("\r\n= In-Application Programming Application (Version 3.3.0) =");
SerialPutString("\r\n= =");
SerialPutString("\r\n= By MCD Application Team =");
SerialPutString("\r\n============================================================");
SerialPutString("\r\n\r\n");
/*升级菜单*/
Main_Menu ();
}
/* Keep the user application running */
else
{
/* Test if user code is programmed starting from address "ApplicationAddress" */
/*升级条件不满足,跳转到用户程序处执行用户程序*/
if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)
{
/* Jump to user application */
/*ApplicationAddress为用户程序的栈地址,+4便为用户程序的复位中断向量地址*/
JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);
Jump_To_Application = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) ApplicationAddress);
/*执行用户空间的复位中断向量函数,里面主要是进行系统时钟配置,执行用户空间的main函数数*/
Jump_To_Application();
}
}
while (1)
{}
}
上述的就是IAP的入口,先查看是否触发了升级条件,触发了进行升级处理,要是没有触发则跳转到用户中断向量表中获取复位中断函数的入口,然后执行复位中断服务函数,最后运行用户空间的main函数。note:只要修改触发方式,根据自己的板子来选择相应的触发方式,以及相应的外设传输。
升级条件满足后进入升级主菜单中, IAP/src/common.c 文件里面,升级主菜单主要是选择相应的操作指令,进行相应的操作。这里有下载用户程序指令、获取用户程序指令、跳转到用户程序指令以及解除写保护指令。note:咱们根据自己的情况可以适当的裁剪,比如不想使用过多的存储器资源的话,可以只实现下载升级指令。
void Main_Menu(void)
{
uint8_t key = 0;
/* Get the number of block (4 or 2 pages) from where the user program will be loaded */
/*计算IAP占用的flash页数*/
BlockNbr = (FlashDestination - 0x08000000) >> 12;
/* Compute the mask to test if the Flash memory, where the user program will be
loaded, is write protected */
#if defined (STM32F10X_MD) || defined (STM32F10X_MD_VL)
UserMemoryMask = ((uint32_t)~((1 << BlockNbr) - 1));
#else /* USE_STM3210E_EVAL */
if (BlockNbr < 62)
{
UserMemoryMask = ((uint32_t)~((1 << BlockNbr) - 1));
}
else
{
UserMemoryMask = ((uint32_t)0x80000000);
}
#endif /* (STM32F10X_MD) || (STM32F10X_MD_VL) */
/* Test if any page of Flash memory where program user will be loaded is write protected */
/*检测flash中用户空间的写保护锁是否开启*/
if ((FLASH_GetWriteProtectionOptionByte() & UserMemoryMask) != UserMemoryMask)
{
FlashProtection = 1;
}
else
{
FlashProtection = 0;
}
while (1)
{
SerialPutString("\r\n================== Main Menu ============================\r\n\n");
SerialPutString(" Download Image To the STM32F10x Internal Flash ------- 1\r\n\n");
SerialPutString(" Upload Image From the STM32F10x Internal Flash ------- 2\r\n\n");
SerialPutString(" Execute The New Program ------------------------------ 3\r\n\n");
if(FlashProtection != 0)
{
SerialPutString(" Disable the write protection ------------------------- 4\r\n\n");
}
SerialPutString("==========================================================\r\n\n");
key = GetKey();
if (key == 0x31)
{
/* Download user application in the Flash */
/*下载程序*/
SerialDownload();
}
else if (key == 0x32)
{
/* Upload user application from the Flash */
SerialUpload();
}
else if (key == 0x33)
{
JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);
/* Jump to user application */
Jump_To_Application = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) ApplicationAddress);
Jump_To_Application();
}
else if ((key == 0x34) && (FlashProtection == 1))
{
/* Disable the write protection of desired pages */
FLASH_DisableWriteProtectionPages();
}
else
{
if (FlashProtection == 0)
{
SerialPutString("Invalid Number ! ==> The number should be either 1, 2 or 3\r");
}
else
{
SerialPutString("Invalid Number ! ==> The number should be either 1, 2, 3 or 4\r");
}
}
}
}
下面主要以下载指令为线索简介下demo中的过程,当然这部分完全可以由用户自己实现,比如使用别的外设传输。其它的功能可以自己查看下代码,其实很简单的。下载代码程序的核心在 IAP/src/ymodem.c 中的int32_t Ymodem_Receive (uint8_t *buf)函数里面实现,使用的是Y-mode协议。如不太熟悉ymodem协议的可以查https://blog.csdn.net/huangdenan/article/details/103611081这个链接介绍的不错。
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, j, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
/* Initialize FlashDestination variable */
FlashDestination = ApplicationAddress;
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);)
{
file_name[i++] = *file_ptr++;
}
file_name[i++] = '\0';
for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
{
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 > (FLASH_SIZE - 1))
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -1;
}
/* Erase the needed pages where the user application will be loaded */
/* Define the number of page to be erased */
NbrOfPage = FLASH_PagesMask(size);
/* Erase the FLASH pages */
for (EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == FLASH_COMPLETE); EraseCounter++)
{
FLASHStatus = FLASH_ErasePage(FlashDestination + (PageSize * EraseCounter));
}
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;
for (j = 0;(j < packet_length) && (FlashDestination < ApplicationAddress + size);j += 4)
{
/* Program the data received into STM32F10x Flash */
FLASH_ProgramWord(FlashDestination, *(uint32_t*)RamSource);
if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -2;
}
FlashDestination += 4;
RamSource += 4;
}
Send_Byte(ACK);
}
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;
}
这里就不一一讲解了,主要讲解下框架,在这个函数中,一直读取上位机发送过来的包。y-modem协议的起始帧会有升级文件的大小以及文件名,后面的数据帧传送的是升级文件的内容。在起始帧收到后解析升级文件的大小,然后把将要占用的flash给擦除。在后面接收到的升级数据后直接写入flash中,这里有校验过程,即写入的和读出的不一样便会提示升级错误。note:这里没有使用文件的长度来确定传送结束的边界,而是在结束传输后,上位机发送一个空的帧。也没有校验数据CRC,所以传输出错的话升级估计也会存在问题。这部分咱们可以自己实现以及修改。如使用别的外设升级,主要是修改这部分。
最后在讲解下用户空间主应用的代码,有个地方需要特别注意的,就是用户空间的代码不在flash的起始地址,所以中断向量表需要修改下,要不然的话响应中断会跳到IAP中断向量表中。在用户程序中需要添加下列代码来设置新的中断向量表。中断向量表的位置便是用户运用程序的起始地址。
/* Set the Vector Table base location at 0x3000 */
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000);
demo给IAP分配的空间为12k(0x08000000-0x08003000),所以用户程序的起始地址也为0x08003000,所以设置新的向量表的时候需要把中断向量表偏移个0x3000。
所以在demo中会出现下列的代码,用户程序的起始地址为0x08003000,这个地址也是flash的目标地址。即接收到的数据就往这个地址写入就可以。
其实IAP的程序还是比较容易,思路就是检测到升级条件满足后,然后从相应的外设把升级文件接收再写入相应的flash就可以。
上述讲解的demo中IAP空间配置为12k,这个咱们可以自行修改,但是必须要是以页为单位。闪存空间不足的时候可以减小些。在IAP代码中修改了,怎么告诉用户程序呢,用户程序怎么知道IAP程序的结束位置呢?其实很简单的,在keil中编译用户程序的时候,告诉keil新的用户程序的ROM起始地址就就可以了,然后程序就会从指定的ROM地址开始编译,具体的设置如下图。这个是用户程序的配置,IAP的配置和之前的还是一致的。
这样用户程序的中断位置便是正确的,要不然的话中断程序无法正确处理。
其次IAP升级的时候需要使用bin文件,在keil中的配置为:
基本的命令格式是fromelf --bin !L --output xxx.bin 这里需要注意空格,大小写。
窗口中有如下的提示信息,表示bin文件生成成功。
好了 这就是IAP升级的主要注意事项和流程。要是有不明白的地方可以查阅an2557文档,这些文档ST官网都有。如果大家觉得有必要写下ISP的升级流程的话,到时我也会写个上位机控制的升级流程。