- 编写目的
NORDIC的SDK中提供了一个BOOTLOADER程序(以下简称BT),BT有两个版本,分别为DUAL BANK和SINGLE BANK,DUAL BANK的BT有一个固件交换区,新固件的接收先存放到SWAP区,接收完成后再进行校验,然后写入主程序区。SINGLE BANK的BT则不带SWAP区,新固件直接写入固件区,由于没有SWAP区,因此如果升级中途失败的话,固件就无法正常启动,只能一直停留在BT中,等待再次进行升级。另外DUAL还支持SOFTDEVICE和BOOTLOADER自身的更新,SINGLE只支持固件升级,这个是之前FAE的回复,具体我自己没有验证过。
DUAL的优势显而易见,在如今ANDROID机型众多的情况下,由于兼容性、稳定性问题导致的升级失败也可以重新运行旧固件。但缺点也是明显的,就是需要一个与固件空间一样大小的备份区,在资源紧张的MCU中无疑是致命的。
NORDIC提供的SDK提供了两种通过方式的BT,分别为UART和BLE,UART为有线,BLE为无线,另外支持ANT版本的还有ANT BT,由于没有使用到,也不太了解,就不进行描述了。其中,BLE的BT只支持DUAL BANK模式,可能也是NORDIC考虑到BLE的不稳定因素吧,UART的BT则有DUAL和SINGLE两个版本。虽然BLE只有DUAL版本,但是通过替换掉UART SINGLE的dfu_single_bank.c文件,可以很轻易的实现一个SINGLE BANK的BLE BT。
BLE使用了DUAL BAN的BT,无疑留给固件的代码空间是大大缩水了,去掉SOFTDEVICE和BT本身的代码空间,再除去SWAP区,在NRF51822上可以使用的固件代码空间是少之又少。考虑到DUAL BANK的重要性,唯一的办法是将SWAP区移到外部的SPI FLASH上,这样可以预留出多一倍的空间给固件代码。想要改造BT,那必须得对它进行一定的了解才行,本文主要是对NORDIC的BT进行分析,了解其工作机制,方便对其进行SWAP区修改。
- BOOTLOADER分析
本文使用的SDK版本为nRF5_SDK_11.0.0_89a8197,目前最新的稳定SDK应该已经出到了12,为什么选用SDK 11也是有原因的,因为SDK 12之后对BT进行了比较大的修改,使用了一个外部的CRC校验,没有集成到SDK里,使用起来比较麻烦,据说是为了更加安全,在我实际的应用中,SDK 11的BT也是一直挺稳定的,所以就沿用SDK 11进行分析,对安全性较高的应用可以试下SDK 12。
由于目的只是为了进行SWAP区的修改,本文的分析可能并没有深入对BT进行分析,只是分析可能相关的部分,可能更加贴切来说应该叫DFU(device firmware update )分析吧。另外为了分析简单,直接到修改的BLE SINGLE进行分析,因为增加SWAP区的也是应该会在BLE SINGLE的基础上增加,后期如果要对BLE DUAL进行修改,应该也是相同的道理。
结合SDK文档进行分析,SDK文档链接地址如下。
直接查看SDK文档的DFU state machine部分,描述了DFU程序是事件驱动型的,所以必须了解各种状态和事件,从下图可以看出,DFU的入口是dfu_init(...)函数,初始化相关数据并将状态设置为IDLE,空闲状态。
dfu_transport_ble.c主要是负责BLE的通讯,然后根据不同的状态调用dfu_single_bank.c里面相应的事件处理函数。经分析,主要的事件处理函数和调用的方法函数主要有以下几个:
- uint32_t dfu_start_pkt_handle(dfu_update_packet_t * p_packet)
在上图中dfu_init(...)后先有个duf_image_size_set(...)的操作,就是在这个事件中处理的,此函数中判断新固件的长度是否合法,再将返回值通过dfu_transport_ble.c返回给主机。如果长度合法,再设置m_functions函数的prepare方法和cleared方法。
prepare方法用于格式化固件区,并设置DFU状态为DFU_STATE_PREPARING。在DUAL BANK中,此方法 是格式化交换区。
cleared方法 用于格式化固件信息区,此区保存固件的校验码等信息,用于启动时校验固件是否有效。在DUAL BANK中,此方法 是格式化交换区固件信息。
在回调的后面,由于处于IDLE状态,因此会马上调用prepare方法进行固件区域清空。实际上是调用pstorage的方法进行ERASE操作,pstorage操作完成后,会调用pstorage_callback_handler()回调进行DFU状态设置为DFU_STATE_RDY。准备进入下一步操作。
在修改BT支持SPI FLASH的时候,就要在prepare操作里面进行一个修改,进行SPI FLASH的SWAP区格式化。由于没有使用pstorage,所以还要手动调用一次pstorage_callback_handler()回调。
- uint32_t dfu_init_pkt_handle(dfu_update_packet_t * p_packet)
进入DFU_STATE_RDY状态后,主要调用该事件函数进行处理。主要用于设置新固件的信息,包括CRC、版本、HASH之类的信息,这里无需再详细分析。
- uint32_t dfu_init_pkt_complete(void)
INIT数据传输完成后,调用该事件将INIT参数设置到dfu_init_template.c里面,再将DFU状态设置为DFU_STATE_RX_DATA_PKT,准备进行数据包接收。
- uint32_t dfu_data_pkt_handle(dfu_update_packet_t * p_packet)
在该事件处理函数中进行固件数据包的接收,并通过pstorage写入到FLASH中,写入完成后通过pstorage_callback_handler()调用dfu_transport_ble.c中的dfu_cb_handler()回调,告诉主机接收完成。并且在dfu_cb_handler()回调中进行是否接收完最后一个包的判断,通过if (mp_final_packet == p_data)语句进行判断,以进入下一步处理。
在修改BT支持SPI FLASH的时候,这个事件函数也是要进行修改的。修改为将固件数据写入到SPI FLASH中,同样是由于使用了pstorage模块,所以还要手动调用一次pstorage_callback_handler()回调,注意调用的参数需要不同。
另外还要重点注意的是,由于dfu_cb_handler()是通过mp_final_packet进行是否接收完成的判断的,而mp_final_packet的赋值又是在调用dfu_data_pkt_handle()之后的,所以这里可能需要增加一个标志的处理。
- uint32_t dfu_image_validate()
固件数据接收完成后,主机发送BLE_DFU_VALIDATE命令调用此函数进行固件的校验。在SINGLE中是通过dfu_init_postvalidate()函数直接校验固件区的程序的,如果校验错误,则返回错误码,正确则将DFU状态设置为DFU_STATE_WAIT_4_ACTIVATE,在SINGLE中是将新固件参数进行写入。
在修改BT支持SPI FLASH的时候,这个地方也是要进行修改的。在这里将SPI FLASH里面的固件读取出来进行CRC校验,并与刚开始INIT参数的CRC进行比较,如果相等则说明固件校验成功。
- static uint32_t dfu_activate_app(void)
最后通过dfu_activate_app()进行固件的写入。在SINGLE BANK中,此函数实际不进行固件的写入,只是将固件参数进行更新。在DUAL BANK中,则会将交换区数据移动到固件区后,再进行参数更新。
此函数也是重点要修改的地方,可以建一个缓存数据,将SPI FLASH的数据分批读取出来,再通过pstorage操作写入固件区,可以通过pstorage的回调进行重复调用该函数,分批将数据写入,所有数据写入完成后再将参数进行更新。
- 总结
上述分析的红色标注文字基本上是需要修改的地方,理解DFU流程后修改起来应该是比较容易的,注意的是要进行多次测试才能用于量产产品中,而且要对最大长度的固件进行测试。
另外为了不改变其它源文件的内容,可以使用定时器模拟pstorage的回调,主要是不需要修改mp_final_packet的判断是否最终包。