需求
前文中实现了一款简单的 2nd Bootloader,能够跳转执行存储在 QSPI Flash 中的应用程序,但 2nd Bootloader 如果仅仅只是用于跳转执行程序的话,岂不是有些太简单了?从本章开始,将会讲解几种 2nd Bootloader 进阶设计,实现类似 ISP 更新固件的功能,以及在 OTA 升级时避免变“砖”等设计,以及讲解一些 2nd Bootloader 的程序设计思路。
本文将以 Ymodem 协议获取应用程序的二进制文件为例,实现类似 ISP 更新固件的功能。
需要注意:
下文中提到的 ISP 仅指由 2nd Bootloader 实现的定制 ISP,而非微控制器本身的 ISP。
上位机发送的文件是二进制(.bin)文件,而不是 Intel Hex 标准的 hex (.hex)文件。
目前仅考虑直接覆盖的方式烧写程序,即获取到一段二进制数据后,直接写入到 QSPI Flash 的对应位置。
Ymodem 介绍
Ymodem 协议是一个文件传输协议,通常用于在资源受限的设备中传输文件,它可以一次传输1024字节的信息块,同时还支持传输多个文件。
Ymodem 协议有较多的变种,本文使用的是常用的 Ymodem-1K 协议。
通信时序
通讯时序如图1:
图1 Ymodem 通信时序
帧格式
Ymodem 有两种帧格式:
帧头为 SOH 时,信息块长度为128字节,总长度133字节。
帧头为 STX 时,信息块长度为1024字节,总长度1029字节。
两种帧的帧格式如表1所示:
表1 SOH / STX 帧格式
包号从0x00起始,每成功传输一帧数据后包号加1,计数到0xFF后,下一次包号重新从0x00开始计数。
包号反码是包号取反的数值,如0x00的包号,包号反码为0xFF,0x01的包号,包号反码为0xFE。
信息块是要传输的具体数据块,起始帧包含了文件名和文件大小,数据帧包含了分段的数据内容。
校验采用 CRC 校验,仅校验信息块的内容。
除了两种帧格式外,还有 ACK、NAK、CAN、EOT、字符 'C' 五种命令,长度仅有1字节。
起始帧、数据帧、结束帧
起始帧采用帧头为 SOH 的帧格式传输,包号为 0x00,信息块中包含文件名字符串和文件大小字符串 (十进制表示),字符串以0x00结尾,信息块剩余部分以0x00填充。
数据帧采用帧头为 STX 的帧格式传输,包号从0x01开始计数,信息块中包含分段的文件内容。
当最后一段要发送的数据块大小超过128字节但小于1024字节时,采用帧头 STX 的帧格式传输,信息块结尾用 0x1A 填充。小于128字节,采用帧头为 SOH 的帧格式传输,信息块结尾依然用 0x1A 填充。
结束帧和起始帧一样,唯一不同的是没有文件名和文件大小,即信息块的内容全为 0x00。
通讯指令
通讯指令如表2所示。
表2 Ymodem通讯指令
协议实现
CRC 校验:
Ymodem 协议中提供了 CRC 校验的 C 代码片段,但由于该协议发布时的 C 标准不同于现在,因此不能直接使用,此处提供一份 CRC 校验的实现代码,通过调用 crc_calc() 来实现对信息块内容的校验:
static uint16_t crc_update(uint16_t crc, uint8_t data) { uint32_t crc32 = crc; uint32_t data32 = data; for (uint32_t i = 0u; i < 8u; i++) { if (0 != (crc32 & 0x8000u) ) { crc32 <<= 1u; crc32 += ( ( (data32 <<= 1) & 0x0100u) != 0u); crc32 ^= 0x1021; } else { crc32 <<= 1u; crc32 += ( ( (data32 <<= 1) & 0x0100u) != 0u); } } return (uint16_t)(crc32 & 0xFFFFu); } static uint16_t crc_calc(uint8_t * buffer, uint32_t size) { uint16_t crc = 0u; for ( uint32_t i = 0u; i < size; i++) { crc = crc_update(crc, buffer[i]); } crc = crc_update(crc, 0u); crc = crc_update(crc, 0u); return crc; }
当我们需要对一段数据进行 CRC 计算时,调用 crc_calc() 函数,传入数据起始地址和数据长度即可计算出 CRC 校验值。
STX 包处理:
通过对 Ymodem 协议的介绍可知,STX 包只在接收数据的过程中使用,因此收到 STX 包时,仅需要进行如下处理:
CRC校验信息块。
计算信息块的有效数据长度(需要注意最后一帧数据的有效长度不定)。
存储数据。
发送 ACK 指令或 NAK 指令。
SOH 包处理:
SOH 包的处理要比 STX 包的处理复杂,因为包含了起始帧和结束帧的处理。
起始 / 结束帧和数据帧只能通过当前状态来判断,其中,除信息块长度不同外,数据帧的处理同 STX 包的处理一致。
由于 Ymodem 可以多文件传输的特性,处于该收到结束帧的状态时也有可能收到起始帧,因此起始帧和结束帧需要进行一个判断:信息块第一个字节是否为 0x00。如果不是 0x00 则为起始帧,否则为结束帧。
起始帧要携带文件名和文件大小,信息块的第一个字节一定是一个可显示的字符,当收到起始帧时,需进行如下处理:
读取文件名和文件大小。
进入读数据块的状态。
发送 ACK。
发送 字符 'C'。
结束帧的信息块全为0x00,收到结束帧时,需进行如下处理:
发送 ACK。
结束 Ymodem 传输。
EOT 指令处理:
EOT 代表本次文件传输结束(但不代表所有文件都已发送完毕),因此,收到 EOT 指令时,需将当前状态调整为起始状态,准备接收新的文件,具体处理如下:
进入起始状态。
发送 ACK。
发送字符 'C'。
CAN 指令处理:
CAN 是 cancel 的缩写,当收到 CAN 指令后,表示后续的 Ymodem 传输终止,该指令是双向的,既可以由 Host 发送, 也可以是 Device 发送,收到 CAN 指令后,具体操作如下:
退出 Ymodem 传输。
接收超时处理:
Device 在接收数据前,会先向 Host 发送字符 'C',但如果此时 Host还没有将文件准备好,则会卡死在准备接收状态。
Device 在接收数据过程中,如果少接收到某个字节数据,信息不完整,则会卡死在接收数据的过程中。
Device 发送某个指令后,Host 可能没有收到指令,不会继续下一帧数据的发送,Device 还是会卡死在接收的过程中。
因此,需要引入接收超时的操作。
当接收超时后,判断状态,如果是起始状态,且没有收到任何字节,则可能是 Host 还没有准备发送文件,重新发送字符 'C'。
如果数据没有接收完整,则可能是少收到几个字节的数据,发送 NAK,让 Host 重新发送数据。
如果没有收到数据,则 Host 可能没有收到回复的指令,重新发送上次发送的指令。
软件设计
就像是计算机进入 BIOS 设置,需要用户在开机的瞬间不停按下键盘上某个按键那样,为了使 2nd Bootloader 知道自己是该跳转执行应用程序,还是进入 ISP 等模式,需要外界有一个输入:这个输入可以是某个引脚的电平变化,也可以是在有限的时间里通过某种通信接口获取到一段外界指令,当 2nd Bootloader 读取到这个来自外界的输入后,才能知道自己接下来要干什么。因此,除了实现 ISP 下载的功能外,我们还需要实现选择工作模式的功能,如图2所示:
图2 软件设计
Ymodem 只是获取二进制文件的一种方式,除了 Ymodem,我们也可以采用 Xmodem,Zmodem协议,除了串口,还可以使用 CAN,甚至通过 USB 读取 U 盘里的文件等方式。
综上所述,在设计 2nd Bootloader 时,不能绑死选择工作模式的方式,也不能绑死 ISP 的工作方式,甚至,不能绑死 2nd Bootloader 只能在两种工作模式下二选一(不要使用 if & else 的语句区分工作模式,而应使用 switch 语句区分工作模式),因此,2nd Bootloader 的顶层应用逻辑,只能是下面的设计:
int main(void) { ...... switch (get_run_mode()) { case EXEC_QSPI: jump_to_app(QSPI_BASE); break; case ISP: isp(); break; ...... default: jump_to_app(QSPI_BASE); break; } ...... }
如果我们期望从某种工作模式下切换到另一种工作模式,最好的做法是先让外界输入保持为目标工作模式的状态,然后让微控制器复位,再次进入 2nd Bootloader,这样的做法是能够保持微控制器切换工作模式后,仍然保持相对 “干净” 的环境状态,例如,微控制器前一次进入到了 ISP 模式,通过串口更新了应用程序,如果直接跳转到应用程序,则发现串口依然保持打开的状态,这对应用程序而言可能不是期望的结果,那提前关闭串口呢?还有 GPIO 引脚的配置没有改动……最简单省事的做法,其实就是直接让微控制器复位,而串口和串口的 GPIO 引脚也就会在微控制器复位之后,处于默认相对比较 “干净” 的状态。这也是为什么图x所示的流程图,ISP 模式的下一步是复位微控制器。
当然,如果在 get_run_mode() 的时候就用到了串口,那还是老老实实在 get_run_mode() 执行到 return 之前,就把串口和 GPIO 处理干净。
这里提一个比较“花”的设计方法,我们可以把 ISP 也做成应用程序,下载到片内 Flash 中 一块确认好的位置(假设起始地址为 ISP_BASE),然后同样使用 jump_to_app() 跳转,只是输入参数从 QSPI_BASE 变为了 ISP_BASE,这个做法会用在 USB DFU 模式上,因为一旦进入了 USB DFU 模式,USB 就不能再作为其它设备进行工作,当 USB 设备支持 USB DFU 时,就需要使用这种办法单独进入到 DFU 模式下。
测试
选择工作模式:
在这里,我们通过读取指定引脚的电平状态来确定该进入何种工作模式。
uint32_t get_run_mode() { ...... if (GPIO_ReadInDataBit(BOARD_BOOT_GPIO_PORT, BOARD_BOOT_GPIO_PIN)) { return EXEC_QSPI; } else { return ISP; } ...... }
ISP 模式:
当进入 ISP 模式后,开始使用 Ymodem 协议接收数据。
void isp() { ...... /* get new app bin & write to qspi flash. */ ymodem_recv_start(&ym, 100000); while(0 == (YMODEM_STATUS_DONE &ym.status) ) { ymodem_recv_byte_handler(&ym); } /* reset mcu. */ __set_FAULTMASK(1); NVIC_SystemReset(); }
生成应用程序的二进制文件:
我们仍然以 MindSDK 的 hello_world 样例工程为例,修改其 Linker 文件并检查代码,使其成为一个可存储在 QSPI Flash 上的应用程序,随后在 MDK 工程中,点击魔术棒(Options for Target...),点击 User 列表,如图3所示,在指定位置(红框中的 User Command)加入下面这句话,并在前面打上对勾:
fromelf.exe --bin -o "@L.bin" "#L"
然后编译工程,就能在工程文件所在的目录下找到生成的 bin 文件。
图3 生成二进制文件
本文使用 TeraTerm 软件进行 Ymodem 传输文件,如图4所示:
图4 TeraTerm Ymodem 发送文件
但在测试时发现,当文件传输到 100% 时,TeraTerm 并没有结束传输,但对 2nd Bootloader 的代码进行分析后并没有发现存在逻辑问题,因此对 TeraTerm 的 Ymodem 协议产生了怀疑,如图5所示。
图5 TeraTerm 传输文件,总卡在 100% 处
使用两个 USB 串口模块,将其 TXD 与 RXD 相连,其中一个串口模块使用 TeraTerm 打开,另一个使用 SSCOM 打开(为了能够发送和显示一些非字符类的控制指令),使用 TeraTerm 的 Ymodem 协议发送文件, SSCOM 接收来自 TeraTerm 的数据,并按照 Ymodem 协议回复指令,模拟完整的 Ymodem 传输协议,如图6所示。
图6 模拟 Ymodem 协议传输文件过程
结果发现,TeraTerm 实现的 Ymodem 协议在发送单个文件的时候,存在以下问题:
发送 EOT 指令后,需接收两次 ACK 和字符 ‘C’。
没有发送 last block。
因此,我们需要针对 TeraTerm 的问题,对 Ymodem 的实现做一些改动,或者使用其它软件通过 Ymodem 传输二进制文件。修改后的时序图如下:
图7 针对 TeraTerm 的 Ymodem 实现进行的改动
删去了对 last block 的接收,并且在收到 EOT 后,主动发送两次 ACK 和 字符 ‘C’,经过修改后测试,TeraTerm 的 Ymodem 能够按照传输完成的方式正常退出。
下载程序,如图8所示:
图8 下载应用程序
运行应用程序,如图9所示:
图9 运行应用程序
结语
本文在 2nd Bootlaoder 的基础上实现了基于 Ymodem 协议的 ISP 功能,能够通过复位后指定引脚的电平状态来区分该执行应用程序还是进入 ISP 模式,进入 ISP 模式后,可以使用 TeraTerm 等软件,通过串口,使用 Ymodem 协议将二进制文件下载到与微控制器连接的 QSPI Flash 中,实现固件更新的功能。
但本文并没有对固件更新过程中可能出现的意外进行处理,所以这种 ISP 的办法不能直接用在 OTA 升级中,在下一章中,我们将会探讨 OTA 升级时可能会出现的意外情况,并且进行处理。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !