本文为记录学习的过程,如有错误,欢迎指正
一、关于BootLoader的注意事项
1、通常情况下,STM32的Bootloader程序和Application(APP)程序都需要烧录到板子中。
Bootloader程序是位于芯片内部的一段代码,用于引导和执行APP程序。它负责初始化硬件,加载APP程序,并启动APP程序的执行。Bootloader程序通常较小,只包含基本的引导功能。
APP程序则是用户自行开发的应用程序,包含具体的功能代码。它通常较大,占用较多的存储空间。APP程序烧录完毕后,可以在系统启动后被Bootloader程序调用执行。
在开发过程中,通常先将Bootloader程序烧录到芯片中,然后再将APP程序烧录到合适的存储器中,如Flash内存或外部存储器。这样,在系统启动时,Bootloader程序会先执行,然后根据其逻辑选择加载并执行APP程序。
2、在系统启动时,可以通过以下方式来判断Bootloader是否执行了:
-
系统启动信息:有些开发板或系统会在启动过程中输出一些启动信息,包括Bootloader的版本号、初始化的硬件设备等。您可以观察启动信息,看是否存在Bootloader相关的提示信息
- 硬件或开发板上的LED指示灯:可以在Bootloader中控制LED的亮灭状态,以表示Bootloader正在运行。
- 调试信息输出:在Bootloader中可以通过串口、JTAG等方式输出调试信息,可以通过观察调试信息来确定Bootloader是否执行了。
- 引导选择开关:有些开发板或系统设计会提供一个引导选择开关,用于选择使用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协议用在串口IAP升级中,一般都是通过ymodem协议将bin文件发送给单片机,然后单片机写bin文件到flash中 ,支持ymodem协议发送的上位机软件一般用SecureCRT。
4.1、YMODEM帧格式
Ymodem 有两种帧格式,主要区别是信息块长度不一样。
帧头 | 包序号 | 包序号取反 | 信息快 | 校验 |
---|---|---|---|---|
SOH/STX | PN | XPN | DATA | CRC |
1byte | 1byte | 1byte | 128/1024byte | 2byte |
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字节。格式如下。
帧头 | 包序号 | 包序号取反 | 文件名称 | 文件大小 | 填充区 | 校验 |
---|---|---|---|---|---|---|
SOH | 0x00 | 0xFF | filename+0x00 | filesize+0x00 | n字节0x00 | CRC16 |
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填充。
帧头 | 包序号 | 包序号取反 | 信息块 | 校验 |
---|---|---|---|---|
0x01 | 0x00 | 0xff | 128个0x00 | 0x00 0x00 |
4.5、YMODEM握手信号
握手信号由接收方发起,在发送方开始传输文件前,接收方需发送YMODEM_C (字符C,ASII码为0x43)命令,发送方收到后,开始传输起始帧。
4.5、YMODEM命令
命令 | 命令码 | 说明 |
---|---|---|
SOH | 0x01 | 128字节数据包 |
STX | 0x02 | 1024字节的数据包 |
EOT | 0x04 | 结束传输 |
ACK | 0x06 | 回应 |
NAK | 0x15 | 没回应,需要重传当前数据包 |
CA | 0x18 | 取消传输 |
C | 0x43 | 握手 |
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();
//添加自己的代码
}