之前已经在板子上跑通了Ymodem协议通信,本次就做一下基于Ymodem协议实现IAP Bootloader代码更新。之前也在其他MCU上实现过Ymodem更新,这次基于之前的经验,很快也实现了这个基于STM32G070的Bootloader。
Ymodem协议介绍:
首先简单介绍一下Ymodem协议。Ymodem协议是一种基于Xmodem改进的文件传输协议,广泛用于嵌入式系统串口传输。它支持文件名、文件大小、块大小可选(128或1024字节),并采用CRC16校验,可靠性更高。下面详细介绍其帧格式和传输流程。
Ymodem的数据包分为三种:开始包(文件名/大小)、数据包(文件内容)、结束包(空包)。每种包都包含包头、序号、序号补码、数据区、CRC校验 。
- 包头 :
SOH(128字节)或 STX(1024字节)。
- 序号 :从0开始递增,每个包序号加1(模256)。开始包序号为0,数据包从1开始。
- 补码 :
0xFF - 序号,用于简单校验。
- 数据区 :包体内容。对于开始包,为文件名字符串 + 空格 + 文件大小(十进制字符串),不足长度用
0x00填充;对于数据包,为文件数据;对于结束包,全为0x00。
- CRC :对数据区 计算得到的CRC16-CCITT值(多项式
0x1021),先高字节后低字节。
YModem传输流程:
接收方通常首先发送字符C,通知发送方已准备好使用CRC校验。
- 接收方发送
C (可重复发送直到收到第一个包或超时)。
- 发送方发送开始包 (包序号0,数据区包含
文件名 文件大小,末尾填充0x00)。
- 接收方收到后,若CRC正确且序号匹配,回复
<span leaf="">ACK</span>,然后发送C请求下一个包。
- 若CRC错误或序号错,回复
NAK,发送方重发。
- 发送方发送数据包 (序号从1开始,每包数据长度128或1024,最后一个包可能不足)。
- 接收方每正确接收一包,回复
ACK,序号递增。
- 若出错,回复
NAK要求重发当前包。
- 发送方发送EOT (表示文件数据发送完毕)。
- 发送方发送结束包 (包序号0,数据区全0x00)。
接下来实现Ymodem协议接口的读写flash回调。这里注意STM32G070的flash每次是8字节写入。
volatile uint32_t ymodem_tick;
static uint32_t get_tick(void) {
return ymodem_tick;
}
uint8_t yModem_PutChar(uint8_t ch)
{
uart_send_char(1, ch);
return 0;
}
uint8_t yModem_GetChar(uint8_t *ch)
{
return uart_recv_char(1, ch);
}
uint32_t CurAddrWrite;
uint32_t CurFileSize;
uint32_t CurFileWriteSize;
uint32_t CurFileInfoSize;
uint64_t CurFileInfoBuff[64]; //64*8=512字节
/* 进度回调 */
static void progress_callback(uint32_t total, uint32_t current)
{
uint8_t percent = (current * 100) / total;
ymodem_printf("Progress: %d%%\r\n", percent);
}
static uint32_t file_open_write(const char* name, uint32_t size)
{
uint32_t addr,i,page_err,status;
FLASH_EraseInitTypeDef flash_erase_temp;
/* 打开文件准备写入 */
ymodem_printf("ymodem recv file: %s,lenght:%u.\r\n",name,size);
CurFileSize = size;
CurFileWriteSize = 0;
CurFileInfoSize = 0;
if(CurFileSize > APP_FW_FLASH_SIZE)
{
return 1;
}
CurAddrWrite = APP_FW_START_ADDR;
flash_erase_temp.TypeErase = FLASH_TYPEERASE_PAGES;
flash_erase_temp.Page = 10; // 从 N page * 2K = 20K开始擦除
flash_erase_temp.NbPages = 50; // 擦除50个page页
// flash_erase_temp.Banks =
//擦除
HAL_FLASH_Unlock();
do{
status = HAL_FLASHEx_Erase(&flash_erase_temp,&page_err);
}while(status != HAL_OK);
HAL_FLASH_Lock();
ymodem_printf("flash erase over.\r\n");
progress_callback(CurFileSize,CurFileWriteSize);
return 0;
}
static uint32_t file_write(const uint8_t* buff, uint32_t len)
{
/* 写入文件 */
uint32_t i;
uint64_t *dat;
uint32_t g_status;
ymodem_printf("ymodem write file:%u.\r\n",len);
if(CurFileSize > APP_FW_FLASH_SIZE)
{
return 0;
}
HAL_FLASH_Unlock();
for(i=0;i<len;i+=8) //page=2048byte
{
dat = (uint64_t *)(buff+i);
do{
g_status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,CurAddrWrite,*dat);
}while(g_status != HAL_OK);
CurAddrWrite += 8;
}
HAL_FLASH_Lock();
CurFileWriteSize += len;
ymodem_printf("flash write OK.\r\n");
progress_callback(CurFileSize,CurFileWriteSize);
return len;
}
static uint32_t file_close_write(void)
{
/* 关闭文件 */
uint32_t g_status;
HAL_FLASH_Unlock();
do{
g_status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, APP_OK_MAGIC_WORD_ADDR, 0xFFFFFFFF00000000 | APP_OK_MAGIC_WORD);
}while(g_status != HAL_OK);
HAL_FLASH_Lock();
progress_callback(CurFileSize,CurFileWriteSize);
// HAL_FLASH_Unlock();
// do{
// g_status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,APP_SOFTRST_MAGIC_WORD_ADDR , 0xFFFFFFFF00000000 | APP_SOFTRST_MAGIC_WORD);
// }while(g_status != HAL_OK);
// HAL_FLASH_Lock();
ymodem_printf("bootloader ymodem write file over.\r\n");
return 0;
}
再就是Ymodem协议处理主函数:
ymodem_handle_t g_flash_ymodem;
void YModem_Flash_Proc(void)
{
CurAddrWrite = 0;
CurFileSize = 0;
ymodem_tick = 0;
ymodem_init(&g_flash_ymodem, yModem_GetChar, yModem_PutChar, get_tick);
ymodem_start_receive(&g_flash_ymodem, 25000, file_open_write, file_write, file_close_write);
while (1)
{
ymodem_result_t res = ymodem_receive_process(&g_flash_ymodem);
if (res == YMODEM_RESULT_SUCCESS) {
ymodem_printf("\n\nsuccess!\n");
break;
} else if (res != YMODEM_RESULT_IN_PROGRESS) {
ymodem_printf("\n\nerror: %d\n", res);
break;
}
//将串口FIFO内数据继续发送
for(int i=0;i<UART_DEV_NUM;i++)
{
uart_flush_data(i);
}
}
while(uart_flush_data(0));
while(uart_flush_data(1));
// 直接调用库函数
NVIC_SystemReset();
return ;
}
接着是检查标志从Bootloader跳转APP运行。这里通过按复位键强制进入Bootloader,或者向指定地址写标志数据,软复位后检查标志强制进入Bootloader。
uint32_t UpdateFlag,RstTimeTick; //
//typedef void (*pFun)(void);
//pFun Jump_to_app;
//uint32_t Jump_addr;
#if defined (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)
/* Avoids the semihosting issue */
__asm(" .global __ARM_use_no_argv\n");
#elif defined(__GNUC__)
/* Disables part of C/C++ runtime startup/teardown */
void __libc_init_array (void) {}
#endif
#if defined(__CC_ARM)
__asm void modify_stack_pointer_and_start_app(uint32_t r0_sp, uint32_t r1_pc)
{
MOV SP, R0
BX R1
}
#elif defined(__GNUC__)
void modify_stack_pointer_and_start_app(uint32_t r0_sp, uint32_t r1_pc)
{
uint32_t z = 0;
__asm volatile ( "msr control, %[z] \n\t"
"isb \n\t"
"mov sp, %[r0_sp] \n\t"
"bx %[r1_pc]"
:
: [z] "l" (z),
[r0_sp] "l" (r0_sp),
[r1_pc] "l" (r1_pc)
);
}
#else
#error "Unknown compiler!"
#endif
//这里检查复位标识。
void IAP_CheckFlag(void)
{
uint32_t Rstflag;
uint32_t *pdat;
UpdateFlag = 0;
//软件复位或者RST引脚复位进入Bootloader。
Rstflag = RCC->CSR & 0xFF000000;
RCC->CSR |= (0x00000001 << 23); //清复位标志
//0x10000000 = 软复位,0x04000000=复位脚按键复位
if(Rstflag == 0x04000000)
{
UpdateFlag |= 0x00000001; //需要更新
}else if(Rstflag == 0x10000000)
{
//软复位后,检查特定地址值确定是否进入bootloader。
pdat = (uint32_t *)(APP_SOFTRST_MAGIC_WORD_ADDR);
if(*pdat == APP_SOFTRST_MAGIC_WORD) //
{
UpdateFlag |= 0x00000001; //需要更新
}
}
//=====================================================
pdat = (uint32_t *)(APP_OK_MAGIC_WORD_ADDR);
if(*pdat == APP_OK_MAGIC_WORD) //有app代码
{
UpdateFlag |= 0x00000002;
}else
{
UpdateFlag |= 0x00000001; //需要更新
}
if(UpdateFlag == 2)
{
if(((*(__IO uint32_t *)APP_FW_START_ADDR)&0x2FFE0000)==0x20000000)
{
// Jump_addr = *(__IO uint32_t *)(APP_FW_START_ADDR + 4);
// Jump_to_app = (pFun)Jump_addr;
// __set_MSP(*(__IO uint32_t *)(APP_FW_START_ADDR));
// Jump_to_app();
modify_stack_pointer_and_start_app(*(volatile uint32_t *)APP_FW_START_ADDR,
(*(volatile uint32_t *)(APP_FW_START_ADDR + 4)));
}
UpdateFlag = 1;
}
RstTimeTick = 10000;
}
APP的代码空间分配:前20K用于Bootloader代码。后面从0x08005000开始的512字节为信息标志区。从0x08005200开始为APP代码区。
#define APP_FW_INFO_ADDR (uint32_t)(0x08005000)
#define APP_FW_START_ADDR (APP_FW_INFO_ADDR + 0x200)
#define APP_FW_FLASH_SIZE (uint32_t)(1024*100) //100K
#define APP_FW_PAGE_SIZE (uint32_t)(0x00000800) //2048
#define APP_FW_END_ADDR (APP_FW_START_ADDR + APP_FW_FLASH_SIZE)
//存储APP标志数值地址和标志值
#define APP_OK_MAGIC_WORD_ADDR (uint32_t)(APP_FW_START_ADDR - 0X08)
#define APP_OK_MAGIC_WORD (uint32_t)(0x555AA5AA)
#define APP_SOFTRST_MAGIC_WORD_ADDR (uint32_t)(APP_FW_START_ADDR - 0X108)
#define APP_SOFTRST_MAGIC_WORD (uint32_t)(0x12345678)
//=========================================================================
extern void IAP_CheckFlag(void);
extern void YModem_Flash_Proc(void);
主函数:

上面就完成了Ymodem的Bootloader的代码,下面做个简单的app代码测试更新效果。app实现一个简单的点灯和串口打印功能。

APP代码还要修改中断向量表。设置偏移地址为0x08005200。

再就是flash分配链接地址也要设置在0x08005200之后。

接着就是将hex转为bin文件。
D:\Keil_v5\ARM\ARMCLang\bin\fromelf.exe --bin -o PocketDev_demo.bin .\PocketDev\PocketDev.axf

编译之后通过Ymodem协议软件发送固件更新。

