本帖最后由 lulugl 于 2023-8-2 21:30 编辑
#申请原创# #有奖活动#[url=home.php?mod=space&uid=760190]@21小跑堂 [/url]
IAP(In applicating Programing)
- IAP就是通过软件实现在线电擦除和编程的方法。IAP技术是从结构上将Flash存储器映射为两个存储体,当运行一个存储体上的用户程序时,可对另一个存储体重新编程,之后将程序从一个存储体转向另一个。
- IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写。简单来说,就是开发者代码出bug了或者添加新功能了,能够利用预留的通讯接口,对代码进行升级
- UART、SPI、IIC、USB等等,当然还有wifi、4G、蓝牙等无线通讯手段,都可以作为IAP升级的方式,今天主要介绍如何使用串口对CW32L052固件进行升级。
- 要想设计IAP,首先需要对MCU的代码启动过程有个了解,先来看看CW32L的代码启动过程是怎样的吧。
- 首先,我们的CW32L系列的内核为ARMCortex-M0+,在数据手册《CW32L052_DataSheet_CN_V1.1.pdf》表明:
在《ARM Cortex-M0 Cortex-M0+权威指南》(第2版)第7章存储系统一章中描述了Cortex-M0/Mo+处理器架构定义的存储器映射:
从上图中看到,Cortex-M0+的代码是从地址0x00000000开始,到0x1FFFFFFF的512M存储空间。在CW32L052的数据手册中,他的FLASH有64K的空间,也就是从0x0000-0xFFFF。如下图所示。
在《ARM Cortex-M0 Cortex-M0+权威指南》(第2版)第7章第4节中描述程序存储器、Bootloader和存储器重映射中,描述,当Cortex-M0+处理器从复位中启动时,会首先访问0地址的向量表,从而读取MSP的初始值和复位向量,然后从复位和向量开始执行程序。
CW32L052的代码启动过程
1、上电复位后,从 0x0000 0000 地址取出栈顶地址赋给MSP寄存器(主堆栈寄存器),即MSP = __initial_sp。这一步是由硬件自动完成的
2、从0x0000 0004 地址取出复位程序的地址给PC寄存器(程序计数器),即PC = Reset_Handler。这一步也是由硬件自动完成调用SystemInit函数初始化系统时钟
3、跳到C库的__main函数初始化堆栈(初始化时是根据前面的分配的堆空间和栈空间来初始化的)和代码重定位(初始RW 和ZI段),然后跳到main函数执行应用程序
IAP设计思路
大体分为两部分设计,bootloader、APP代码设计,bootloader用于检查APP区代码是否需要更新,以及跳转到APP区执行APP程序
我设计的如下图所示升级流程:
Flash分区
CW32L052的flash分区,我们编写好bootloader后,代码的长度为8k,为此我们分把前面的12k分配给bootloader,后面的52k留给APP。分区如下图所示:
Bootloader区为0x000-0x2FFF,在工程里设置ROM如下图:
APP大小为52K,ROM起始地址为0x3000,长度为0xD000。MDK设置如下:
Bootloader,我们这次采用的是用串口升级,使用Ymodem协议进行数据传输。在武汉芯源的官方有应用文档( HYPERLINK "https://www.whxy.com/files/doc/CW32F030_IAP_Fuction_CN_V1.0.pdf" )
Ymodem协议
Ymodem协议用于计算机间传输文件,同样适用于嵌入式领域,如MCU升级固件时,可以使用Ymodem协议传输固件文件,传输总线不限于USB、UART、CAN等。
Ymodem 帧格式
Ymodem 有两种帧格式,主要区别是信息块长度不一样。
帧头:
帧头表示两种数据帧长度,主要是信息块长度不同。
包序号
数据包序号只有1字节,因此计算范围是0~255;对于数据包大于255的,序号归零重复计算。
帧长度
【1】以SOH(0x01)开始的数据包,信息块是128字节,该类型帧总长度为133字节。
【2】以STX(0x02)开始的数据包,信息块是1024字节,该类型帧总长度为1029字节。
校验
Ymodem采用的是CRC16校验算法,校验值为2字节,传输时CRC高八位在前,低八位在后;CRC计算数据为信息块数据,不包含帧头、包号、包号反码。
Ymodem握手信号
握手信号由接收方发起,在发送方开始传输文件前,接收方需发送YMODEM_C (字符C,ASII码为0x43)命令,发送方收到后,开始传输起始帧。
Ymodem起始帧
Ymodem起始帧并不直接传输文件内容,而是先将文件名和文件大小置于数据帧中传输;起始帧是以SOH 133字节长度帧传输,格式如下。
其中包号为固定为0;Filename为文件名称,文件名称后必须加0x00作为结束;Filesize为文件大小值,文件大小值后必须加0x00作为结束;余下未满128字节数据区域,则以0x00填充。
Ymodem数据帧
Ymodem数据帧传输,在信息块填充有效数据。
传输有效数据时主要考虑的是最后一包数据的是处理,SOH帧和STR帧有不同的处理。
【1】对于SOH帧,若余下数据小于128字节,则以0x1A填充,该帧长度仍为133字节。
【2】对于STX帧需考虑几种情况:
●余下数据等于1024字节,以1029长度帧发送;
●余下数据小于1024字节,但大于128字节,以1029字节帧长度发送,无效数据以0x1A填充。
●余下数据等于128字节,以133字节帧长度发送。
●余下数据小于128字节,以133字节帧长度发送,无效数据以0x1A填充。
Ymodem结束帧
Ymodem的结束帧采用SOH 133字节长度帧传输,该帧不携带数据(空包),即数据区、校验都以0x00填充。
Ymodem命令
代码实现:
Ymodem开源的资料非常多。主要实现有4个文件一个是common.c/common.h,ymodem.c/ymodem.h。
Common.c中主要的功能是实现串口接收字符、发送字符的通用接口
/**
* [url=home.php?mod=space&uid=247401]@brief[/url] Test to see if a key has been pressed on the HyperTerminal
* @param key: The key pressed
* @retval 1: Correct
* 0: Error
*/
uint32_t SerialKeyPressed(uint8_t *key)
{
if (UART_GetFlagStatus(IAP_UARTx, UART_FLAG_RC) != RESET)
{
*key = (uint8_t)IAP_UARTx->RDR;
UART_ClearFlag(IAP_UARTx, UART_FLAG_RC);
return 1;
}
else
{
return 0;
}
}
/**
* @brief Print a character on the HyperTerminal
* @param c: The character to be printed
* @retval None
*/
void SerialPutChar(uint8_t c)
{
UART_SendData_8bit(IAP_UARTx, c);
while (UART_GetFlagStatus(IAP_UARTx, UART_FLAG_TXE) == RESET);
}
串口下载函数SerialDownload:
void SerialDownload(void)
{
uint8_t Number[10] = " ";
int32_t Size = 0;
SerialPutString("\n\n\rWaiting for the file to be sent ... (press 'a' to abort)\n\r");
Size = Ymodem_Receive(&tab_1024[0]);
if (Size > 0)
{
SerialPutString("\n\n\r Programming Completed Successfully!\n\r--------------------------------\r\n Name: ");
SerialPutString(file_name);
Int2Str(Number, Size);
SerialPutString("\n\r Size: ");
SerialPutString(Number);
SerialPutString(" Bytes\r\n");
SerialPutString("-------------------\n");
SerialPutString("\n\n\r MCU is going to reset...\n");
NVIC_SystemReset();
}
else if (Size == -1)
{
SerialPutString("\n\n\rThe image size is higher than the allowed space memory!\n\r");
}
else if (Size == -2)
{
SerialPutString("\n\n\rVerification failed!\n\r");
}
else if (Size == -3)
{
SerialPutString("\r\n\nAborted by user.\n\r");
}
else
{
SerialPutString("\n\rFailed to receive the file!\n\r");
}
}
具的代码见附件
Ymodem.c主要是实现数据包的接收与解析、FLASH的刷写,主要的函数有如下两个,一个是接收一个包并做解析的Receive_Packet:
/**
* @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;
uint8_t c;
*length = 0;
if (Receive_Byte(&c, timeout) != 0)
{
return -1;
}
switch (c)
{
case SOH:
packet_size = PACKET_SIZE;
break;
case STX:
packet_size = PACKET_1K_SIZE;
break;
case EOT:
return 0;
case CA:
if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
{
*length = -1;
return 0;
}
else
{
return -1;
}
case ABORT1:
case ABORT2:
return 1;
default:
return -1;
}
*data = c;
for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++)
{
if (Receive_Byte(data + i, timeout) != 0)
{
return -1;
}
}
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))
{
return -1;
}
*length = packet_size;
return 0;
}
另一个是接收一个文件并刷写到flash里面:
/**
* @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, j, packet_length, session_done, file_done, packets_received, errors, session_begin;
static int32_t size = 0;
int32_t state_receive_p;
/* Initialize FlashDestination variable */
FlashDestination = ApplicationAddress;
for (session_done = 0, errors = 0, session_begin = 0; ;)
{
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
{
state_receive_p = Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT);
switch (state_receive_p)
{
case 0:
errors = 0;
switch (packet_length)
{
/* Abort by sender */
case - 1:
Send_Byte(ACK);
return 0;
/* End of transmission */
case 0:
if (file_done == 0)
{
Send_Byte(NAK);
file_done = 1;
}
else if (file_done == 1)
{
Send_Byte(ACK);
Send_Byte(CRC16);
file_done = 2;
}
break;
/* Normal packet */
default:
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
{
if (file_done == 0)
{
Send_Byte(NAK);
}
else
{
Send_Byte(ACK);
file_done = 3;
}
}
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 */
FLASH_UnlockPages(FlashDestination, FlashDestination+ (PageSize * NbrOfPage));
FLASH_ErasePages(FlashDestination, FlashDestination+ (PageSize * NbrOfPage));
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)
{
/* 把接收到的数据编写到Flash中 */
FLASH_WriteWords(FlashDestination, (uint32_t*)RamSource, 1);
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 == 3)
{
session_done = 1;
break;
}
}
if (session_done != 0)
{
break;
}
}
return (int32_t)size;
}
【工程实现步骤】
打开一个初始化串口工程示例,在工程里面添加commom.c/h,以及ymodem.c/h。
初始化板载的KEY来做升级检测标志,在复位的1秒之内按下按键,来当做升级标志。如果在1秒之内没有检测到标志,则直接跳转到APP。如果检测到标志,则进入SerialDownload函数,等待上位机发送固件。
int32_t main(void)
{
volatile uint32_t u32Ticks, u32ElapsedTicks;
RCC_Configuration();
InitTick(SystemCoreClock); // 配置SYSTICK频率为1ms
GPIO_Configuration();
UART_Configuration();
UART_SendString(IAP_UARTx, "start...\r\n");
u32Ticks = GetTick();
do
{
u32ElapsedTicks = GetTick() - u32Ticks;
if (!KEY_GETVALUE()) // 检测按键
{
// 按下按键
break;
}
}
while(u32ElapsedTicks < 1000); // 等待1s
if (u32ElapsedTicks < 1000)
{
// 1s内有按键按下,进入串口升级流程
// 配置串口,波特率115200
UART_SendString(IAP_UARTx, "serialDownload...\r\n");
SerialDownload(); // 通过YMODEM协议下载升级程序
}
else
{
// 超时,从boolloader程序向用户APP程序跳转
UART_SendString(IAP_UARTx, "goto app...\r\n");
Boot2APP();
}
while (1);
}
跳转到APP的函数为:
void Boot2APP(void)
{
__disable_irq(); // 关中断
if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000) //判断跳转的地址是否有合法程序存在
{
// 向用户的APP程序进行跳转
JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); // ResetHandle函数的地址
Jump_To_Application = (func_ptr_t) JumpAddress; // 将地址强制转换为函数指针
__set_MSP(*(__IO uint32_t*) ApplicationAddress); // 设置用户APP程序的栈地址
Jump_To_Application(); // 跳入用户APP程序的ResetHandle处
}
}
APP程序:
我们在ROM中指定固件的起始地址为0x3000
代码如下,初始IO与串口,进入APP时LED1闪烁,并在串口中打印出字符串。
int32_t main(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
LogInit();
RCC_HSI_Enable(RCC_HSIOSC_DIV6);
__RCC_GPIOC_CLK_ENABLE();
GPIO_InitStruct.IT = GPIO_IT_NONE;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pins = LED_GPIO_PINS;
GPIO_Init(LED_GPIO_PORT, &GPIO_InitStruct);
while (1)
{
GPIO_TogglePin(LED_GPIO_PORT, LED_GPIO_PINS);
Delay(0xFFFF);
printf("hello cw32l052!\r\n");
printf("这是一个IAP升级的DEMO\r\n");
}
}
同时为了生成.bin文件,我在user下面添加生成.bin的命令:$K\ARM\ARMCC\bin\fromelf.exe --bin --output=..\@L.bin !L
升级的操作示例我们先把IAP的固件用wch_link烧到开发板,然后打印超级终端,我这里使用SerureCRT的Ymodem发送工具。
然后我们设置ymodem发送为1024字节:
连接终端,开机后按下key1键,就会出现如下提示:
我们选择需要发送的bin文件:
出现如下提示,显示传完成,并且成功的完成APP的跳转。
【总结】
经过一个星期的学习,终于掌握了IAP的串口升级活动。主要的难点是如何匹配串口接收完一个数据包,写入flash的原理。
期间遇到非常多的问题,武汉芯源的技术支持耐心的帮助我排查问题,在此特别感谢孙工、张工、吴工。
附件:
Template.zip
(2.71 MB)
|