打印
[开发资料]

CW32L052 实现串口IAP升级

[复制链接]
1383|15
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
lulugl|  楼主 | 2023-8-2 21:25 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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)

使用特权

评论回复
沙发
caigang13| | 2023-8-3 08:02 | 只看该作者
要做好FLASH代码的备份。

使用特权

评论回复
板凳
lulugl|  楼主 | 2023-8-3 08:48 | 只看该作者
caigang13 发表于 2023-8-3 08:02
要做好FLASH代码的备份。

如果flash空间够的话,可以考虑双空间。这里是实验性的,只是纯实现。

使用特权

评论回复
地板
libotongxun| | 2024-6-6 23:13 | 只看该作者
中断不能跳转,如何处理

使用特权

评论回复
5
lulugl|  楼主 | 2024-6-7 07:55 | 只看该作者
libotongxun 发表于 2024-6-6 23:13
中断不能跳转,如何处理

是指什么时候,是升级期间,还是运行APP不能跳转?

使用特权

评论回复
6
libotongxun| | 2024-6-7 16:34 | 只看该作者
lulugl 发表于 2024-6-7 07:55
是指什么时候,是升级期间,还是运行APP不能跳转?

串口升级正常,跳转也正常,跳到APP运行后,也可以运行,但是中断没有反应,APP中 第一时间开了enable_irq(),中断向量 SCB->VTOR=0x4000u, 在systemint中和main中都试设过。就是中断没反应。

使用特权

评论回复
评论
libotongxun 2024-6-8 10:00 回复TA
@lulugl :各种方式都试了,就是不行。 
lulugl 2024-6-8 09:49 回复TA
这个我也没有详细的去测试,你看一下,是不是中断向量没有映射过去。 
7
lulugl|  楼主 | 2024-6-9 08:29 | 只看该作者
libotongxun 发表于 2024-6-7 16:34
串口升级正常,跳转也正常,跳到APP运行后,也可以运行,但是中断没有反应,APP中 第一时间开了enable_ir ...

你先不跑APP,先基地址跑一下,看中断是不是正常的。

使用特权

评论回复
8
libotongxun| | 2024-6-10 15:03 | 只看该作者
基地址是正常的。

使用特权

评论回复
9
gouguoccc| | 2024-6-10 22:23 | 只看该作者
IAP可以通过串口,CAN等方式实现。

使用特权

评论回复
10
AdaMaYun| | 2024-6-14 08:40 | 只看该作者
IAP升级非常实用简单

使用特权

评论回复
11
小夏天的大西瓜| | 2024-6-17 22:54 | 只看该作者
升级要做好FLASH代码的备份,以防万一

使用特权

评论回复
12
OKAKAKO| | 2024-6-21 21:10 | 只看该作者
IAP就是通过软件实现在线电擦除和编程的方法。

使用特权

评论回复
13
中国龙芯CDX| | 2024-6-26 16:40 | 只看该作者
IAP就是通过软件实现在线电擦除和编程的方法。

使用特权

评论回复
14
jf101| | 2024-6-27 16:52 | 只看该作者
UART、SPI、IIC、USB等等,当然还有wifi、4G、蓝牙等无线通讯手段,都可以作为IAP升级的方式

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

145

主题

712

帖子

9

粉丝