本帖最后由 shanyuxiang 于 2024-4-27 18:27 编辑
#申请原创# @21小跑堂
用Xmodem协议实现APM32的固件升级
一、前言
1.1、关于Xmodem协议
Xmodem是一个经典的串口文件输协议,发送端发送大小为128字节或1024字节的包,每一包都带有CRC校验,接收端根据接收到的包返回一个字节的控制字。
由发送端发出的控制字:
由接收端发出的控制字:

简单地说说Xmodem的通信流程:
接收端(MCU)运行后间每隔大约1s发送一个字节的0x43('C'),发送端(PC上位机)收到0x43后发送第一包,
接收端收到这一帧并且CRC校验正确后返回ACK,然后发送端才会会发下一包;如果CRC不对返回的是NAK,则发送端重传该包。
当所有数据包传输完成后,发送端发送0x04(EOT)表示文件已经全部传完。
网上关于Xmodem资料很多,有兴趣的话可以找来看看,这里就不展开了。
Xmodem这种具有校验并重传的协议非常适合用来做固件的传输,有固定的包大小也便于flash的扇区写入。
这里就用一个Xmodem的开源协议栈代码来实现APM32的固件下载和升级。
1.2、准备工作
xmodem的协议栈代码:
Xmodem function source code
https://www.menie.org/georges/embedded/#xmodem
上位机软件:
支持xmodem协议的文件发送软件或串口助手皆可,例如SecureCRT
SDK工程:
APM32E10x_SDK_V1.1
二、Xmodem协议栈的移植
2.1 源码文件的添加
首先添加下载下来的开源代码 xmodem.c 、crc16.c,同时新建一个 xmodem_port.c 存放待补充的代码。

2.2 补充待实现的代码
xmodem.c中还需要实现这几个函数,usleep、_inbyte、_outbyte。
usleep是一个微秒级延时,时间不用太精准,能实现等待就可以,这里滴答定时做一个:
- void usleep(unsigned int us)
- {
- unsigned int temp;
- SysTick->LOAD = us * (SystemCoreClock / 1000000 / 8); //时间加载
- SysTick->VAL = 0x00;
- SysTick->CTRL = 0x01 ;
- do
- {
- temp = SysTick->CTRL;
- }
- while (temp & 0x01 && !(temp & (1 << 16)));
- SysTick->CTRL = 0x00;
- SysTick->VAL = 0X00;
- }
_inbyte是字节的读取,有点类似文件的读取,用串口的轮询接收来实现:
- unsigned short uart_read()
- {
- unsigned short data = 0;
- if (USART_ReadStatusFlag(XMODEM_UART, USART_FLAG_RXBNE) != RESET)
- {
- data = USART_RxData(XMODEM_UART);
- data |= 0x2000;
- }
- return data;
- }
- int _inbyte(unsigned short timeout) // msec timeout
- {
- unsigned short c;
- int delay = timeout << 4;
- while (((c = uart_read()) & 0x2000) == 0)
- {
- usleep(5);//usleep(60); /* 60 us * 16 = 960 us (~ 1 ms) */
- if (timeout)
- {
- if (--delay == 0) return -2;
- }
- }
- return c & 0x0FF;
- }
_outbyte 是字节发送,这个好办,用串口的字节发送实现:- void _outbyte(int byte)
- {
- while (USART_ReadStatusFlag(XMODEM_UART, USART_FLAG_TXBE) == RESET);
- USART_TxData(XMODEM_UART, byte);
- }
2.3 Xmodem的调用
注释掉xmodem.c中宏定义 "TEST_XMODEM_RECEIVE",并给其中的main()函数改个名,改成xmodem_main()。
在真正的main函数循环中执行xmodem_main(),让协议栈一直运行,最后不要忘了初始化一下xmodem要用到的串口。
- xmodem_init(); //初始化Xmodem所需要的串口
- while (1)
- {
- xmodem_main();
- }
至此,Xmodem的移植就完成了,接下来就来实现固件的烧写。
三、固件的烧录
3.1 内部flash的擦写
具体操作可以参考FMC例程,简单的说就是解锁、擦除、编程...擦除、编程、上锁。
因为这个boot要占用4KB多的空间,所以APP就从8KB的位置开始,这也就为什么烧写的起始地址是0x08002000;
该芯片的最小擦除单位是2KB,所以每次就按2KB来写入。
- #define FLASH_PAGE_SIZE 2048
- #define FLASH_BOOT_ADDR 0x08002000
- void xmodem_write_flash(unsigned char buf[], unsigned int len)
- {
- unsigned int sectors, i, j;
- unsigned int word;
- sectors = (len + 2047) / 2048;
- FMC_Unlock();
- for (i = 0; i < sectors; i++)
- {
- FMC_ErasePage(FLASH_BOOT_ADDR + (FLASH_PAGE_SIZE * i));
- for (j = 0; j < FLASH_PAGE_SIZE / 4; j++)
- {
- word = buf[FLASH_PAGE_SIZE * i + j * 4 + 0];
- word |= buf[FLASH_PAGE_SIZE * i + j * 4 + 1] << 8;
- word |= buf[FLASH_PAGE_SIZE * i + j * 4 + 2] << 16;
- word |= buf[FLASH_PAGE_SIZE * i + j * 4 + 3] << 24;
- FMC_ProgramWord(FLASH_BOOT_ADDR + FLASH_PAGE_SIZE * i + j * 4, word);
- }
- }
- FMC_Lock();
- }
3.2 APP的接收与写入
先建一个缓存用来存放接收固件文件,数组地址传给 xmodemReceive 函数,当接收完成后,调用刚才写好 xmodem_write_flash 函数对固件进行烧写。
- int xmodem_main(void)
- {
- int st;
- printf("Send data using the xmodem protocol from your terminal emulator now...\r\n");
- /* the following should be changed for your environment:
- 0x30000 is the download address,
- 65536 is the maximum size to be written at this address
- */
- //st = xmodemReceive((char *)0x30000, 65536);
- st = xmodemReceive((char *)FirmwareBuffer, 65536);
- if (st < 0)
- {
- printf("Xmodem receive error: status: %d\r\n", st);
- }
- else
- {
- printf("Xmodem successfully received %d bytes\r\n", st);
- xmodem_write_flash(FirmwareBuffer, st);
-
- }
- return 0;
- }
3.3 跳转到APP执行
固件烧写完后就可以跳转了,常规流程,先判断一下APP的头,如果正确就跳转到APP。
- typedef void (*pFunction)(void);
- pFunction JumpToApplication;
- unsigned int JumpAddresss;
- void xmodem_jump(void)
- {
- if (((*(__IO uint32_t *)FLASH_BOOT_ADDR) & 0x2FFE0000) == 0x20000000)
- {
- printf("[boot]APM32 xmodem jump\r\n\r\n");
- JumpAddresss = *(__IO uint32_t *)(FLASH_BOOT_ADDR + 4);
- JumpToApplication = (pFunction)JumpAddresss;
- __set_MSP(*(__IO uint32_t *)FLASH_BOOT_ADDR);
- JumpToApplication();
- }
- else
- {
- printf("[boot]APM32 invalid app!\r\n\r\n");
- }
- printf("[boot]APM32 xmodem jump fail!\r\n");
- }
四、固件升级的测试验证
4.1 制作测试用的APP
bootloader代码已经完成,为了测试固件升级功能,还需要做一个app固件。相对于bootloader来说,app部分就比较简单了。
因为APP是打算放在 0x08002000 的位置,所以需要让编译出的APP代码的在 0x08002000 基础上进行偏移,在工程设置中改一下编译的地址:
另外 system_apm32e10x.c 中的向量偏移也改一下:
- #define VECT_TAB_OFFSET 0x2000//0x00
APP中加点串口打印代码便于观察执行情况:
- printf("\r\n\r\n[app]APM32 Xmodem appliaction star...\r\n");
- while (1)
- {
- Delay();
- printf("[app]%03d\r\n", LoopCount++);
- }
为了得到APP的bin文件,在工程设置选项中添加这条命令并编译:
fromelf.exe --bin -o "$L@L.bin" "#L
4.2 用 SecureCRT 发送固件文件
下面是见证奇迹的时刻,先运行bootloader程序,打开 SecureCRT 软件,协议选择"Serial",设置正确的串口号和波特率,点下面的"Connect"按钮:
串口打开后会看到MCU一直在发送字符'C':
这时拖动刚才编译好的bin文件,选择"Send Xmodem...":
等待进度完成,程序跳转,看到App程序执行并打印出串口信息,至此固件成功升级。
不足与改进:
这里的xmodem是接收完整个固件才进行烧录操作,当app固件非常大时需要占用很大的sram空间;
理论上来说xmodem是支持边收边烧写的,比如收够2KB字节就写入到flash中,这样适用面会更广。
|