HAL库+IAP升级+串口Ymode协议学习

本文为记录学习的过程,如有错误,欢迎指正

一、关于BootLoader的注意事项

1、通常情况下,STM32的Bootloader程序和Application(APP)程序都需要烧录到板子中。

Bootloader程序是位于芯片内部的一段代码,用于引导和执行APP程序。它负责初始化硬件,加载APP程序,并启动APP程序的执行。Bootloader程序通常较小,只包含基本的引导功能。

APP程序则是用户自行开发的应用程序,包含具体的功能代码。它通常较大,占用较多的存储空间。APP程序烧录完毕后,可以在系统启动后被Bootloader程序调用执行。

在开发过程中,通常先将Bootloader程序烧录到芯片中,然后再将APP程序烧录到合适的存储器中,如Flash内存或外部存储器。这样,在系统启动时,Bootloader程序会先执行,然后根据其逻辑选择加载并执行APP程序。

2、在系统启动时,可以通过以下方式来判断Bootloader是否执行了:

  1. 系统启动信息:有些开发板或系统会在启动过程中输出一些启动信息,包括Bootloader的版本号、初始化的硬件设备等。您可以观察启动信息,看是否存在Bootloader相关的提示信息

  2. 硬件或开发板上的LED指示灯:可以在Bootloader中控制LED的亮灭状态,以表示Bootloader正在运行。
  3. 调试信息输出:在Bootloader中可以通过串口、JTAG等方式输出调试信息,可以通过观察调试信息来确定Bootloader是否执行了。
  4. 引导选择开关:有些开发板或系统设计会提供一个引导选择开关,用于选择使用Bootloader还是直接执行APP程序。通过设置引导选择开关的状态,可以确定是否启动了Bootloader。

3、bootloader 实现的两种设计方式:

(1)一直是在BootLoader中做文件的拷贝、版本对比与APP的跳转

好处:不需要在BootLoader中添加通讯协议及相关通讯功能,升级失败后仍可以运行之前的APP,保证机器能一直有APP可用

问题:需要一个备份区来进行存储升级数据,占用空间较大;

(2)在BootLoader中增加通讯功能,让其具备接收数据,并将数据写到指定位置的特性

好处:实现不需要备份区来升级

问题:每次进行升级都会对APP区进行擦写;当升级失败后会一直停留在boot等待再一次升级直到升级成功才能去运行APP。

一般来说都是选择第一种BootLoader的方式。但本文用的是第二种方式。

二、创建BootLoader工程

1、使能以下外设:PA6,PA7用于LED指示灯,PC8,PC9用于按键,使能串1,串口2用于信息打印和接受.bin 文件

2、 打开工程,勾选options->Target->Use MicroLIB,更改IROM1的Size:

3、配置串口打印重映射函数,在usart.h中添加#include <stdio.h>, 在usart.c中添加如下代码

int fputc(int ch, FILE *F)
{
	HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}

三、创建APP工程

参考博客:【STM32】HAL库 STM32CubeMX教程七---PWM输出(呼吸灯)

使用呼吸灯程序方便查看APP程序是否正常工作,以此提醒自己要开中断

对于APP程序,需要修改中断向量表的偏移,在system_stm32f1xx.c中

四、YMODEM协议

参考文章:Ymodem 协议详解 ---- Ming天过后

                  Ymodem传输协议 ---- 树哥

Ymodem协议用在串口IAP升级中,一般都是通过ymodem协议将bin文件发送给单片机,然后单片机写bin文件到flash中 ,支持ymodem协议发送的上位机软件一般用SecureCRT。

4.1、YMODEM帧格式

Ymodem 有两种帧格式,主要区别是信息块长度不一样。

帧头包序号包序号取反信息快校验
SOH/STXPNXPNDATACRC
1byte1byte1byte128/1024byte2byte
4.1.1、帧头

帧头表示两种数据帧长度,主要是信息块长度不同。
(1)当帧头为SOH(0x01)时,信息块为128字节;
(2)当帧头为STX(0x02)时,信息块为1024字节;

4.1.2、包序号

数据包序号只有1字节,因此计算范围是0~255;对于数据包大于255的,序号归零重复计算。

4.1.3、帧长度

(1)以SOH(0x01)开始的数据包,信息块是128字节,该类型帧总长度为133字节;
(2)以STX(0x02)开始的数据包,信息块是1024字节,该类型帧总长度为1029字节。

4.1.4、校验

Ymodem采用的是CRC16校验算法,校验值为2字节,传输时CRC高八位在前,低八位在后;CRC计算数据为信息块数据,不包含帧头、包号、包号反码。

4.2、YMODEM起始帧

Ymodem起始帧并不直接传输文件内容,而是先将文件名和文件大小置于数据帧中传输;起始帧是以SOH 133字节长度帧传输,帧长 = 3字节数据首部 + 128字节数据 + 2字节(CRC16校验码)= 133字节。格式如下。

帧头包序号包序号取反文件名称文件大小填充区校验
SOH0x000xFFfilename+0x00filesize+0x00n字节0x00CRC16

1.其中包号为固定为0;
2.filename为文件名称,文件名称后必须加0x00作为结束;
3.filesize为文件大小值,文件大小值后必须加0x00作为结束;
4.余下未满128字节数据区域,则以0x00填充;
5.可以看出起始帧也是遵守4.1中Ymodem包格式的;

4.3、YMODEM数据帧

Ymodem数据帧传输,在信息块填充有效数据,传输有效数据时主要考虑的是最后一包数据的是处理,SOH帧和STR帧有不同的处理。
(1)对于SOH帧,若余下数据小于128字节,则以0x1A填充,该帧长度仍为133字节。
(2)对于STX帧需考虑几种情况:
    1.余下数据等于1024字节,以1029长度帧发送;
    2.余下数据小于1024字节,但大于128字节,以1029字节帧长度发送,无效数据以0x1A填充;
    3.余下数据等于128字节,以133字节帧长度发送;
    4.余下数据小于128字节,以133字节帧长度发送,无效数据以0x1A填充;

4.4、YMODEM结束帧

Ymodem的结束帧采用SOH 133字节长度帧传输,该帧不携带数据(空包),即数据区、校验都以0x00填充。

帧头包序号包序号取反信息块校验
0x010x000xff128个0x000x00 0x00
4.5、YMODEM握手信号

握手信号由接收方发起,在发送方开始传输文件前,接收方需发送YMODEM_C (字符C,ASII码为0x43)命令,发送方收到后,开始传输起始帧。

4.5、YMODEM命令
命令命令码说明
SOH0x01128字节数据包
STX0x021024字节的数据包
EOT0x04结束传输
ACK0x06回应
NAK0x15没回应,需要重传当前数据包
CA0x18取消传输
C0x43握手
4.6、YMODEM工具

有些工具是支持YMODEM传输的,比如SecureCRT等。

五、学习及问题记录

1、但是程序中应该使用协议接收,STM32官方提供了一个示例程序:

STM32F10xxx in-application programming using the USART (AN2557)

但是STM32官方示例程序是用标准库写的,我想将其移植到HAL库中

首先就是创建一个BootLoader工程,然后对比与官方例程中,主要的文件是ymodem.c(包含ymodem协议接受函数等), common.c(接口函数封装文件),download.c(接受文件函数),upload.c(上传文件函数),这里目前只需要考虑接受文件的情况。

2、 将官方程序中c文件修改,保留ymodem.c和load.c,如下:

/*******************************************************************************
** 文件名: 		ymodem.c
** 功能:		和Ymodem.c的相关的协议文件
                负责从超级终端接收数据(使用Ymodem协议),并将数据加载到内部RAM中。
                如果接收数据正常,则将数据编程到Flash中;如果发生错误,则提示出错。
** 修改日志:	2023-07-20    创建文档
*******************************************************************************/

/* 包含头文件 *****************************************************************/

#include "ymodem.h"


/* 变量声明 -----------------------------------------------------------------*/
uint8_t file_name[FILE_NAME_LENGTH];
//用户程序Flash偏移
uint32_t FlashDestination ;
uint16_t PageSize = PAGE_SIZE;
uint32_t EraseCounter = 0x0;
uint32_t NbrOfPage = 0;
HAL_StatusTypeDef FLASHStatus = HAL_OK;
uint32_t RamSource;
extern uint8_t tab_1024[1024];

static uint8_t stmflash_get_error_status(void)
{
    uint32_t res;
    res = FLASH->SR;

    if (res & (1 << 0))return 1;    /* BSY = 1      , 忙 */
    if (res & (1 << 2))return 2;    /* PGERR = 1    , 编程错误*/
    if (res & (1 << 4))return 3;    /* WRPRTERR = 1 , 写保护错误 */
    
    return 0;   /* 没有任何错误 操作完成. */
}

static uint8_t stmflash_wait_done(uint32_t time)
{
    uint8_t res;

    do
    {
        res = stmflash_get_error_status();

        if (res != 1)
        {
            break;      /* 非忙, 无需等待了, 直接退出 */
        }
        
        time--;
    } while (time);

    if (time == 0)res = 0XFF;   /* 超时 */

    return res;
}

/**
 * @brief       页擦除
 * @param       saddr   : 页地址 0 ~ 256
 * @retval      执行结果
 *   @arg       0   : 已完成
 *   @arg       2   : 编程错误
 *   @arg       3   : 写保护错误
 *   @arg       0XFF: 超时
 */
uint8_t stmflash_erase_sector(uint32_t saddr)
{
    uint8_t res = 0; 
    res = stmflash_wait_done(0X5FFFFF);     /* 等待上次操作结束, >20ms */

    if (res == 0)
    {
        FLASH->CR |= 1 << 1;    /* 页擦除 */
        FLASH->AR = saddr;      /* 设置页地址(实际是半字地址) */
        FLASH->CR |= 1 << 6;    /* 开始擦除 */
        res = stmflash_wait_done(0X5FFFFF); /* 等待操作结束, >20ms */

        if (res != 1)   /* 非忙 */
        {
            FLASH->CR &= ~(1 << 1); /* 清除页擦除标志 */
        }
    }

    return res;
}

/*******************************************************************************
  * @函数名称:Int2Str
  * @函数说明:整形数据转到字符串
  * @输入参数:intnum:数据
  * @输出参数:str:转换为的字符串
  * @返回参数:无
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
void Int2Str(uint8_t* str, int32_t intnum)
{
    uint32_t i, Div = 1000000000, j = 0, Status = 0;

    for (i = 0; i < 10; i++)
    {
        str[j++] = (intnum / Div) + 48;

        intnum = intnum % Div;
        Div /= 10;
        if ((str[j-1] == '0') & (Status == 0))
        {
            j = 0;
        }
        else
        {
            Status++;
        }
    }
}

/*******************************************************************************
  * @函数名称:Str2Int
  * @函数说明:字符串转到整形数据
  * @输入参数:inputstr:需转换的字符串
  * @输出参数:intnum:转好的数据
  * @返回参数:转换结果
               1:正确
               0:错误
*******************************************************************************/
uint32_t Str2Int(uint8_t *inputstr, int32_t *intnum)
{
    uint32_t i = 0, res = 0;
    uint32_t val = 0;

    if (inputstr[0] == '0' && (inputstr[1] == 'x' || inputstr[1] == 'X'))
    {
        if (inputstr[2] == '\0')
        {
            return 0;
        }
        for (i = 2; i < 11; i++)
        {
            if (inputstr[i] == '\0')
            {
                *intnum = val;
                //返回1
                res = 1;
                break;
            }
            if (ISVALIDHEX(inputstr[i]))
            {
                val = (val << 4) + CONVERTHEX(inputstr[i]);
            }
            else
            {
                //无效输入返回0
                res = 0;
                break;
            }
        }

        if (i >= 11)
        {
            res = 0;
        }
    }
    else//最多10为2输入
    {
        for (i = 0; i < 11; i++)
        {
            if (inputstr[i] == '\0')
            {
                *intnum = val;
                //返回1
                res = 1;
                break;
            }
            else if ((inputstr[i] == 'k' || inputstr[i] == 'K') && (i > 0))
            {
                val = val << 10;
                *intnum = val;
                res = 1;
                break;
            }
            else if ((inputstr[i] == 'm' || inputstr[i] == 'M') && (i > 0))
            {
                val = val << 20;
                *intnum = val;
                res = 1;
                break;
            }
            else if (ISVALIDDEC(inputstr[i]))
            {
                val = val * 10 + CONVERTDEC(inputstr[i]);
            }
            else
            {
                //无效输入返回0
                res = 0;
                break;
            }
        }
        //超过10位无效,返回0
        if (i >= 11)
        {
            res = 0;
        }
    }

    return res;
}
/*******************************************************************************
  * @函数名称:FLASH_PagesMask
  * @函数说明:计算Flash页
  * @输入参数:Size:文件长度
  * @输出参数:无
  * @返回参数:页的数量
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
uint32_t FLASH_PagesMask(__IO uint32_t Size)
{
    uint32_t pagenumber = 0x0;
    uint32_t size = Size;

    if ((size % PAGE_SIZE) != 0)
    {
        pagenumber = (size / PAGE_SIZE) + 1;
    }
    else
    {
        pagenumber = size / PAGE_SIZE;
    }
    return pagenumber;

}

uint32_t FLASH_OB_GetWRP(void)
{
  /* Return the FLASH write protection Register value */
  return (uint32_t)(READ_REG(FLASH->WRPR));
}
/*******************************************************************************
  * @函数名称:SerialKeyPressed
  * @函数说明:测试超级终端是否有按键按下
	* @输入参数:key:是STM32串口接受到的指令,也就是说SecureCRT发送过来的指令
  * @输出参数:无
  * @返回参数:1:正确
               0:错误
*******************************************************************************/
uint32_t SerialKeyPressed(uint8_t *key)
{

    if ( __HAL_UART_GET_FLAG(&huart2, UART_FLAG_RXNE) != RESET)
    {
        *key = (uint8_t)USART2->DR;
        return 1;
    }
    else
    {
        return 0;
   		}
}

/*******************************************************************************
  * @函数名称:GetKey
  * @函数说明:通过超级中断回去键码
  * @输入参数:无
  * @输出参数:无
  * @返回参数:按下的键码
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
uint8_t GetKey(void)
{
    uint8_t key = 0;

    //等待按键按下
    while(1)
    {
        if(SerialKeyPressed((uint8_t *)&key)) break;
    }
    return key;

}
void SerialPutChar(uint8_t ch)
{
   HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xffff);
//	 while (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TXE) == RESET);
}
/*******************************************************************************
  * @函数名称:Serial_PutString
  * @函数说明:串口发送一个字符串
  * @输入参数:*s:需发送的字符串
  * @输出参数:无
  * @返回参数:无
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
void Serial_PutString(uint8_t *s)
{
	while(*s != '\0')
    {
        SerialPutChar(*s);
        s++;
    }
  
}


/*******************************************************************************
  * @函数名称:Receive_Byte
  * @函数说明:从发送端接收一个字节
  * @输入参数:c: 接收到的指令
                timeout: 超时时间
  * @输出参数:无
  * @返回参数:接收的结果
               0:成功接收
               1:时间超时
*******************************************************************************/
static  int32_t Receive_Byte (uint8_t *c, uint32_t timeout)
{
    while (timeout-- > 0)
    {
        if (SerialKeyPressed(c) == 1)
        {
            return 0;
        }
    }
    return -1;
}

/*******************************************************************************
  * @函数名称:Send_Byte
  * @函数说明:发送一个字符
  * @输入参数:c: 发送的字符
  * @输出参数:无
  * @返回参数:发送的结果
               0:成功发送
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
static uint32_t Send_Byte (uint8_t c)
{
    Serial_PutString((uint8_t *)&c);
    return 0;
}

/*******************************************************************************
  * @函数名称:Receive_Packet
  * @函数说明:从发送端接收一个数据包
  * @输入参数:data :数据指针
               length:长度
               timeout :超时时间
  * @输出参数:无
  * @返回参数:接收的结果
               0: 正常返回
               -1: 超时或者数据包错误
               1: 用户取消
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
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) //-1接受超时,0成功接受
    {
        return -1;
    }
    switch (c) //接收到的指令
    {
    case SOH:  //帧头(133字节帧):0x01
        packet_size = PACKET_SIZE;
        break;
    case STX: //帧头(1029字节帧):0x02
        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; //接收到的指令字符C存储到*data指向的内存中
    for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++) //将接收到的数据存在data数组中
    {
        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;  //如果数据包验证通过,将数据包信息块的大小赋值给length中
    return 0;
}

/*******************************************************************************
  * @函数名称:Ymodem_Receive
  * @函数说明:通过 ymodem协议接收一个文件,最大字节数1024
  * @输入参数:buf: 首地址指针
  * @输出参数:无
  * @返回参数:0	结束传输
							-1  数据包过大
							-2  写入flash出错
							-3  超时或数据包错误
							-4 
							size	文件长度
							 
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
int32_t Ymodem_Receive (uint8_t *buf_ptr,int32_t size)
{
    uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr;
    int32_t i, j, packet_length, session_done, file_done, packets_received, errors, session_begin;

    //初始化Flash地址变量
    FlashDestination = App2Address;
		//外循环负责处理整个会话(session)
    for (session_done = 0, errors = 0, session_begin = 0; ;)
    {
			//内循环负责处理一个文件(file)
        for (packets_received = 0, file_done = 0; ;)
        {
            switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT)) //0: 正常返回;-1: 超时或者数据包错误;1: 用户取消
            {
            case 0: //数据接收正常返回
                errors = 0; //没有发生一个错误
                switch (packet_length)
                {
                    //发送端终止
                case - 1:
                    Send_Byte(ACK);
                    return -4;
                    //结束传输
                case 0:
                    Send_Byte(ACK);
                    file_done = 1;
                    break;
                    //正常的数据包
                default:
									//首先判断接收到的数据包序列号是否与期望值一致,如果不一致,则向发送端发送NAK信号
                    if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
                    {
											Send_Byte(NAK); //通知发送包接收错误
                    }
                    else
                    {   //如果当前接收到的是第一个数据包,则处理文件名数据包
                        if (packets_received == 0)
                        {
                            //文件名数据包
													if (packet_data[PACKET_HEADER] != 0) //检查数据包头字节是否为0 ,以确保数据包有效
                            {
                                //从数据包中提取文件名并保存到file_name数组中
                                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';
																//从数据包中提取文件大小并保存到file_size数组中
                                for (i = 0, file_ptr ++; (*file_ptr != 0) && (i < FILE_SIZE_LENGTH);)
                                {
                                    file_size[i++] = *file_ptr++;
                                }
                                file_size[i++] = '\0'; 
                                Str2Int(file_size, &size); //将文件大小转换为整数,保存在size变量中

                                //测试数据包是否过大
                                if (size > (FLASH_SIZE - 1))
                                {
                                    //结束
                                    Send_Byte(CA);
                                    Send_Byte(CA);
                                    return -1;
                                }

                                //计算需要擦除Flash的页
                                NbrOfPage = FLASH_PagesMask(size);
																HAL_FLASH_Unlock(); //解锁flash
							
                                //擦除Flash
                                for (EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == HAL_OK); EraseCounter++)
                                {	 
//                                   FLASH_PageErase(FlashDestination + (PageSize * EraseCounter));//这里不用hal库自带的擦除函数
																	 stmflash_erase_sector(FlashDestination + (PageSize * EraseCounter));
//																	 FLASHStatus = FLASH_WaitForLastOperation((uint32_t)0x000B0000);
                                }
																HAL_FLASH_Lock();
																//擦除完成后,向发送端发送ACK和CRC16信号,表示可以接受文件数据
                                Send_Byte(ACK);
                                Send_Byte(CRC16);
																memset(packet_data, 0, (packet_length + PACKET_OVERHEAD));
                            }
                            //文件名数据包空,结束传输
                            else
                            {
                                Send_Byte(ACK);
                                file_done = 1;
                                session_done = 1;
                                break;  //跳出内循环
                            }
                        }
                        //不是第一个数据包,将数据包中的有效数据拷贝到内存缓冲区buf中,并将数据写入flash
                        else
                        {   //将数据包的信息块数据复制到buf_ptr
                            memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
//                            RamSource = (uint32_t)buf_ptr;  //将指针表示的内存地址作为一个整数值
                            for (j = 0; (j < packet_length) && (FlashDestination <  App2Address + size); j += 4)
                            {
																HAL_FLASH_Unlock(); //恢复写flash
                                //把接收到的数据编写到Flash中
                                HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FlashDestination, *((uint32_t *)RamSource));
//																FLASH_WaitForLastOperation((uint32_t)0x00000fff);
															//检查写入的数据是否正确,如果flash中数据和RamSource 中数据不相等,则发送中断信号CA并返回错误代码-2
                                if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
                                {
                                    //结束
                                    Send_Byte(CA);
                                    Send_Byte(CA);
                                    return -2;
                                }
                                FlashDestination += 4;
                                RamSource += 4;
                            }
                            Send_Byte(ACK);
														HAL_FLASH_Lock(); //锁住flash,以保护写入的数据不受意外擦除或修改。
														memset(packet_data, 0, (packet_length + PACKET_OVERHEAD));
//														memset(buf_ptr, 0, packet_length);
                        }
                        packets_received ++; //接受包数目加1
                        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 -5;
                }
                Send_Byte(CRC16);  //错误次数没有超过最大错误此时,发送校验码CRC16继续执行接收下一轮接收数据包的循环
                break;
            }
            if (file_done != 0) //文件传输完成
            {
                break;
            }
        }
        if (session_done != 0)
        {
            break;
        }
    }
    return (int32_t)size;
}

/*******************************************************************************
  * @函数名称:UpdateCRC16
  * @函数说明:更新输入数据的CRC校验
  * @输入参数:crcIn 当前CRC16值
               byte  用来计算校验码的字节
  * @输出参数:无
  * @返回参数:CRC校验值
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
uint16_t UpdateCRC16(uint16_t crcIn, uint8_t byte)
{
    uint32_t crc = crcIn;
    uint32_t in = byte |  0x100;
    do
    {
        crc <<= 1;
        in <<= 1;
        if (in&0x100)
            ++crc;
        if (crc&0x10000)
            crc ^= 0x1021;
    }
    while (!(in&0x10000));
    return crc&0xffffu;
}


*******************************************************************************/
uint16_t Cal_CRC16(const uint8_t* data, uint32_t size)
{
    uint32_t crc = 0;
    const uint8_t* dataEnd = data+size;
    while (data<dataEnd)
        crc = UpdateCRC16(crc,*data++);

    crc = UpdateCRC16(crc,0);
    crc = UpdateCRC16(crc,0);
    return crc&0xffffu;
}


/*******************************************************************************
  * @函数名称:CalChecksum
  * @函数说明:计算YModem数据包的总大小
  * @输入参数:data :数据
               size :长度
  * @输出参数:无
  * @返回参数:数据包的总大小
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
uint8_t CalChecksum(const uint8_t* data, uint32_t size)
{
    uint32_t sum = 0;
    const uint8_t* dataEnd = data+size;
    while (data < dataEnd )
        sum += *data++;
    return sum&0xffu;
}


/*******************************文件结束***************************************/
/*******************************************************************************
** 文件名: 		ymodem.h
** 功能:		和Ymodem.c的相关的头文件
** 修改日志:	2023-07-20   创建文档
*******************************************************************************/

/* 防止重定义 *****************************************************************/
#ifndef _YMODEM_H_
#define _YMODEM_H_

#include "main.h"
#include "usart.h"
#include <string.h>

/* 宏 ------------------------------------------------------------------------*/
#define PACKET_SEQNO_INDEX      (1)
#define PACKET_SEQNO_COMP_INDEX (2)

#define PACKET_HEADER           (3)
#define PACKET_TRAILER          (2)
#define PACKET_OVERHEAD         (PACKET_HEADER + PACKET_TRAILER)
#define PACKET_SIZE             (128)
#define PACKET_1K_SIZE          (1024)

#define FILE_NAME_LENGTH        (128)
#define FILE_SIZE_LENGTH        (4)

#define SOH                     (0x01)  //128字节数据包开始
#define STX                     (0x02)  //1024字节的数据包开始
#define EOT                     (0x04)  //结束传输
#define ACK                     (0x06)  //回应
#define NAK                     (0x15)  //没回应
#define CA                      (0x18)  //这两个相继中止转移
#define CRC16                   (0x43)  //'C' == 0x43, 需要 16-bit CRC 

#define ABORT1                  (0x41)  //'A' == 0x41, 用户终止 
#define ABORT2                  (0x61)  //'a' == 0x61, 用户终止

#define NAK_TIMEOUT             (0x100000)
#define MAX_ERRORS              (15)

#define App1Address     		0x08010000
#define App2Address             0x08020000
#define PAGE_SIZE               (0x800)    /* 2 Kbytes */
#define FLASH_SIZE              (0x40000)  /* 256 KBytes */
#define FLASH_IMAGE_SIZE        (uint32_t) (FLASH_SIZE - (App2Address - 0x08000000))

#define IS_AF(c)  					    ((c >= 'A') && (c <= 'F'))
#define IS_af(c)  						((c >= 'a') && (c <= 'f'))
#define IS_09(c)  						((c >= '0') && (c <= '9'))
#define ISVALIDHEX(c)  					IS_AF(c) || IS_af(c) || IS_09(c)
#define ISVALIDDEC(c)  					IS_09(c)
#define CONVERTDEC(c)  					(c - '0')
#define CONVERTHEX_alpha(c)  		    (IS_AF(c) ? (c - 'A'+10) : (c - 'a'+10))
#define CONVERTHEX(c)  					(IS_09(c) ? (c - '0') : CONVERTHEX_alpha(c))

#define SerialPutString(x) 			    Serial_PutString((uint8_t*)(x))

typedef  void (*pFunction)(void);
extern uint32_t NbrOfPage;

/* 函数声明 ------------------------------------------------------------------*/
extern void  FLASH_PageErase(uint32_t PageAddress); //告诉编译器该函数定义在其他地方,并可以正确地连接到该定义所在的源文件或库


int32_t Ymodem_Receive (uint8_t *buf_ptr,int32_t size);
uint8_t Ymodem_Transmit (uint8_t *,const  uint8_t* , uint32_t );
void Int2Str(uint8_t* str, int32_t intnum);
uint32_t Str2Int(uint8_t *inputstr, int32_t *intnum);
uint32_t FLASH_PagesMask(__IO uint32_t Size);
uint32_t SerialKeyPressed(uint8_t *key);
uint8_t GetKey(void);
void Serial_PutString(uint8_t *s);
void Main_Menu(void);
uint8_t stmflash_erase_sector(uint32_t saddr);
#endif  /* _YMODEM_H_ */

/*******************************文件结束***************************************/
/*******************************************************************************
** 文件名: 		download.c
** 生成日期: 	2023-07-20
** 功能:		等待用户选择传送文件操作,或者放弃操作以及一些提示信息,
                但真正实现传送的是ymodem.c源文件。
*******************************************************************************/

/* 包含头文件 *****************************************************************/
#include "ymodem.h"

/* 变量声明 ------------------------------------------------------------------*/
pFunction Jump_To_Application;
pFunction Jump_To_Application2;
uint32_t JumpAddress2;
uint32_t JumpAddress;
uint32_t BlockNbr = 0, UserMemoryMask = 0;
__IO uint32_t FlashProtection = 0;
extern uint32_t FlashDestination;
int32_t size = 0;
extern uint8_t file_name[FILE_NAME_LENGTH];
uint8_t tab_1024[1024] =
{
    0
};

int32_t Size = 0;


/*******************************************************************************
  * @函数名称:SerialDownload
  * @函数说明:通过串口接收一个文件
  * @输入参数:无
  * @输出参数: 无
  * @返回参数: 无
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
void SerialDownload(void)
{

    SerialPutString("Waiting for the file to be sent ... (press 'a' to abort)\n\r");
    Size = Ymodem_Receive(&tab_1024[0], size);
    if (Size > 0)
    {
         printf("\n\n\r Programming Completed Successfully!\n\r--------------------------------\r\n Name: %s",file_name);
		printf("\n\r Size: %d kB\r\n",Size); 
        SerialPutString("-------------------\r\n");
    }
    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 if (Size == -3)
    {
        printf("\r\n\nAborted by user.\n\r");
    }
    else if(Size == -4)
    {
		SerialPutString("\n\rFailed to receive the file!\n\r");
	}
	else if(Size == -5)
	{
		SerialPutString("\n\rSomething is wrong!\n\r");
	}
}

/*******************************************************************************
  * @函数名称:Main_Menu
  * @函数说明:显示菜单栏在超级终端
  * @输入参数:无
  * @输出参数:无
  * @返回参数:无
  * @历史记录:     
     <作者>    <时间>      <修改记录>
*******************************************************************************/
void Main_Menu(void)
{
	uint8_t key = 0;
//	BlockNbr = (FlashDestination - 0x08000000) >> 12;
//	
//#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) */
		
//	if((HAL_FLASHEx_OBGetConfig(pobInit) & UserMemoryMask )!= UserMemoryMask)
//	{
//		FlashProtection = 1;
//	}
//	else 
//	{
//		FlashProtection = 0;
//	}
		
	while(1)
	{
		key = GetKey();
		if(key == 0x31)	//串口发送16进制数字:1
        {
            /* Download user application in the Flash */
		    SerialDownload();	//通过串口1,下载程序到flash
        }
		else if(key == 0x33) //串口发送字符:‘3’
		{
			if (((*(__IO uint32_t*)App1Address) & 0x2FFE0000 ) == 0x20000000)
			{
					JumpAddress = *(__IO uint32_t*)(App1Address + 4);

					/* Jump to user application */
					Jump_To_Application = (pFunction) JumpAddress;
					/* Initialize user application's Stack Pointer */
					__set_MSP(*(__IO uint32_t*) App1Address);
					__disable_irq();
					Jump_To_Application();
			}
			else 
			{
					SerialPutString("no user Program\r\n\n");
			}
		}
		else if(key == 0x34) //串口发送字符:4
		{
			if (((*(__IO uint32_t*)App2Address) & 0x2FFE0000 ) == 0x20000000)
			{
					JumpAddress2 = *(__IO uint32_t*)(App2Address + 4);

					/* Jump to user application */
					Jump_To_Application2 = (pFunction) JumpAddress2;
					/* Initialize user application's Stack Pointer */
					__set_MSP(*(__IO uint32_t*) App2Address);
					__disable_irq();
					Jump_To_Application2();
			}
			else
			{
					SerialPutString("no user Program\r\n\n");
			}
		}
	}
}

/*******************************文件结束***************************************/

 

 

 

 

 

3、有几个坑:1、SecureCRT只传输1K的内容就停止传输了

                       2、官方示例文件中的第一帧的文件名长度和大小设置有问题

~~~~~>

                      3、HAL库中擦除函数有问题(巨坑,解决见另一篇博文)

4、问题记录

目前可以实现串口接受.bin 文件,写入flash,但是有个问题未解决就是:确定发送的是APP的bin文件,确定写入地址(0x08020000)没有问题,写入flash中的不是APP程序的.bin文件,而是BootLoader的.bin 文件。现在怀疑是发送bin文件时数据出错,然后就自动载入BootLoader的程序(chat GPT说的)。

目前的问题就是明明下载的APP的.bin文件,flash中却是BootLoader的bin文件。

通过STlink来debug查看memory,其中0x08010000是直接烧录了APP的程序,是作为对照组,然后0x08020000才是通过串口接受.bin文件并且将bin文件写入flash的位置,结果显示如下:

 因为是CM3内核,小端模式,倒着读;第一个值是栈顶地址(堆栈指针MSP的初始值),第二个值是程序计数器指针PC的初始值,该值指向复位后执行的第一条指令。(参考阅读文件:正点原子的《STM32启动文件浅析》P17)

再下载一个HEX查看软件,Winhex,用来确认MDK生成的APP的.bin文件确实没有问题,下载地址 :WinHex,解压后直接用就可以了。

 PC指针初始值就是复位中断入口向量地址,这里确实偏移量是0x20000,与设置的一致,但是APP程序的.bin文件接收写入flash后就出现问题:

查看BootLoader的存储情况,发现下载到0x08020000地址处的程序就是BootLoader程序:

 如果直接将APP程序通过STlink烧录入0x08020000处的flash,就会发现没有出现问题

所以很有可能串口接受到的.bin文件不对,如果后续要继续解决问题的话,就应该查看接受到的bin文件,直接将其打印到串口(待做)

准备做串口打印的时候发现写入flash的代码是有问题的,因为RamSource声明的是全局变量但是并没有用到(用的那句注释了)

后来取消注释,但是总是卡到接受1K字节的数据就卡住:

或者就是接受完一直卡在某个代码处不再反应,还有就是检查写入的时候有时候会写入错误就直接返回错误代码0xFFFFFFFE(-2),这样容错率不太好,调整代码如下:

else
{   //将数据包的信息块数据复制到buf_ptr
		memcpy(buf, packet_data + PACKET_HEADER, packet_length);
		RamSource = (uint32_t)buf;  //将指针表示的内存地址作为一个整数值
		for (j = 0; (j < packet_length) && (FlashDestination <  App2Address + size); j += 4)
		{
				HAL_FLASH_Unlock(); //恢复写flash
				//把接收到的数据编写到Flash中
				HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FlashDestination, *((uint32_t *)RamSource));;
			//检查写入的数据是否正确,如果flash中数据和RamSource 中数据不相等,则发送中断信号CA并返回错误代码-2
				if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
				{
					 HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FlashDestination, *((uint32_t *)RamSource));
					if(*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
					{
						//结束
						Send_Byte(CA);
						Send_Byte(CA);
						return -2;
					}
				}
				FlashDestination += 4;
				RamSource += 4;
		}
		HAL_FLASH_Lock(); //锁住flash,以保护写入的数据不受意外擦除或修改。
		Send_Byte(ACK);
}
packets_received ++; //接受包数目加1
session_begin = 1;

这里还有可能就是接收完到100%后就卡住了:

后突然灵感一现,是不是我CubeMX生成代码段时候没有调整堆栈大小,默认栈为0x400,但实际上ymodem的一帧数据就会有1029个字节,后面调整栈为0x800,代码就运行正确了。

栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部的SRAM大小,但是,如果工程中定义的局部变量比较多,就需要修改栈的大小。

堆主要用于动态内存的分配,想malloc()、calloc()和realloc()等函数申请的内存就在对上面。堆中的内存一般由程序员分配和释放,如果程序员不释放,程序结束后可能由操作系统回收。

5、如果想要从APP跳转到BootLoader,其实就是复位的工作,使用HAL库自带的函数HAL_NVIC_SystemReset();

可以也是用串口接受指令来进行跳转,就在APP程序中的usart.c中添加以下代码:

uint32_t SerialKeyPressed(uint8_t *key)
{

    if ( __HAL_UART_GET_FLAG(&huart2, UART_FLAG_RXNE) != RESET)
    {
        *key = (uint8_t)USART2->DR;
        return 1;
    }
    else
    {
        return 0;
   		}
}

uint8_t GetKey(void)
{
    uint8_t key = 0;

    //等待按键按下
    while(1)
    {
        if(SerialKeyPressed((uint8_t *)&key)) break;
    }
    return key;

}

main.c函数中添加判断即可:

 /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		printf("hello, I try again!\r\n");
		key = GetKey();
		if(key == 0x34)
			HAL_NVIC_SystemReset();
        //添加自己的代码
   }

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值