一、框架介绍
上一篇我们已经完成了CherryUSB的移植,今天我们就来基于CherryUSB来制作一个USB的bootloader。其实实现方式有挺多种,例如像cdc,hid,msc,dfu等等,只要能实现用usb将固件文件安全的发送至单片机,单片机再将收到的数据存到对应的app flash区域,然后再跳转运行bootloader即可。为此我编写了一个简单的bootloader框架,PlumBL,因为有了CherryUSB和PlumBL这样两个框架,在对不同平台的适配将会方便很多,usb只需完成usb_dc的移植,而bootloader只需要移植flash读写以及跳转等等。
CherryUSB 上一篇文章我们已经介绍过了,并且完成了CH582 usb_dc的编写,这次我们来简单介绍一下PlumBL这个bootloader小框架。仓库地址放在了文章的末尾。
首先我们看看核心的API:
/**
* @brief Whether the hardware enters the bootloader
* @pre None
* @param[in] None
* @retval bool true: Enter bootloader // false: Do not enter bootloader
*/
bool lgk_boot_hard_is_enter(void);
/**
* @brief Wait ms
* @pre None
* @param[in] ms Waiting time
* @retval None
*/
void lgk_boot_deley_ms(uint32_t ms);
/**
* @brief Boot loader jump to app
* @pre App is vaild
* @param[in] app_add App start address
* @retval None
*/
void lgk_boot_jump_app(uint32_t app_add);
/**
* @brief System software reset
* @pre None
* @param[in] None
* @retval None
*/
void lgk_boot_sys_reset(void);
/**
* @brief Check app is vaild
* @pre None
* @param[in] check_code_add Check code address
* @retval bool true: app is vaild // false is not vaild
*/
bool lgk_boot_app_is_vaild(uint32_t check_code_add);
/**
* @brief Code flash erase
* @pre None
* @param[in] start_add Erase start address
* @param[in] size Erase size
* @retval None
*/
void lgk_boot_flash_erase(uint32_t start_add, uint32_t size);
/**
* @brief Code flash write
* @pre None
* @param[in] start_add Write start address
* @param[in] buffer Data address to be written
* @param[in] size Length of written data
* @retval None
*/
void lgk_boot_flash_write(uint32_t start_add, void *buffer, uint32_t size);
/**
* @brief Code flash read
* @pre None
* @param[in] start_add Read start address
* @param[in] buffer Store read data
* @param[in] size Length of data read
* @retval None
*/
void lgk_boot_flash_read(uint32_t start_add, void *buffer, uint32_t size);
/**
* @brief Initialize the interface for firmware upgrade
* @pre None
* @param[in] None
* @retval None
*/
void lgk_boot_intf_init(void);
/**
* @brief Basic system initialization
* @pre None
* @param[in] None
* @retval None
*/
void lgk_boot_sys_init(void);
这些函数针对不同的平台需要做不同的实现。下面简单介绍一下这些函数的功能。
1、bool lgk_boot_hard_is_enter(void);
这个函数就是用硬件来判断是否要进入bootloader,例如我们可以使用一个普通的gpio,在上电初始化完以后判断这个gpio电平的高低来判断是否要进入bootloader。
2、void lgk_boot_deley_ms(uint32_t ms);
这个函数就是简单的延时。
3、void lgk_boot_jump_app(uint32_t app_add);
这个函数是用来跳转到app里面。
4、void lgk_boot_sys_reset(void);
系统的复位函数。
5、bool lgk_boot_app_is_vaild(uint32_t check_code_add);
检查app是否合法。
6、void lgk_boot_flash_erase(uint32_t start_add, uint32_t size);
flash擦除。
7、void lgk_boot_flash_write(uint32_t start_add, void *buffer, uint32_t size);
flash写入。
8、void lgk_boot_flash_read(uint32_t start_add, void *buffer, uint32_t size);
flash读取。
9、void lgk_boot_intf_init(void);
bootloader接口初始化,例如UF2我们需要初始化一个MSC设备,DFU需要初始化USB DFU设备。
10、void lgk_boot_sys_init(void);
系统初始化,时钟的一些配置等等。
知道这些函数的基本功能以后,我们就来看一下框架如何搭建。
首先我们需要看一个比较关键的变量:
/**
* Find the pointer at the top of the stack
*/
extern uint32_t _eusrstack[];
#define lgk_boot_flag _eusrstack[0]
对于CH582我将lgk_boot_flag定义为指向栈顶地址的变量,因为我希望这个变量在软复位的时候不被初始化,(当然了你也可以在链接脚本里面重新写一个段用no_init属性来限制一下)。如果放到栈顶就需要改一下ram的区域,默认的链接脚本RAM长度为32K,而且根据CH582的链接脚本,会将栈顶地址设置为RAM区域的末尾地址,这样如果不把最后面的四个字节空出来,往栈顶地址存数据会出现访存出错的问题,所以我们需要将长度修改为32K-4,这样根据CH582的链接脚本,栈顶地址就是0x20007FFC。因为RISCV的栈是向下增长的,所以0x20007FFC-0x20007FFF这四个byte系统不会用到,可以用来存储lgk_boot_flag。
接下来我们看看主函数,用lgk_boot_flag来判断进入哪个部分,一共分为三个部分
一、直接进入APP
二、直接进入Bootloader(超时1分钟无操作会自动退出)
三、上电默认的状态(进入bootloader 且不会因为超时退出)
先看第一部分---直接进入APP:
如果条件成立,清掉lgk_boot_flag,然后调用跳转到app的函数。
第二部分---进入Bootloader(超时1分钟无操作会自动退出)
条件成立需要初始化一些接口,然后需要对USB枚举不成功做超时操作,接着红色框子里面的是对系统的几个状态的判断,在用UF2boot的时候会用到,具体实现请看仓库里面的代码。
第三部分---进入Bootloader(进入bootloader 且不会因为超时退出)
跟二差不多,只是少了超时退出的操作。
二、bootloadr 接口
一、uf2
uf2是微软的一个文件格式,我也没做多少了解,我们只需要知道,用uf2搭配USBMSC设备,可以做一个U盘,并且只需要根据官方提供的py脚本将bin或者hex固件转成uf2文件,就可以放进U盘完成固件升级。下面我们看看如何做支持uf2文件的u盘。
上图中的这些文件,红色箭头所指的都是从tinyuf2那边拿过来的,msc_flash.c这个文件是将USB设备做成MSC类设备的应用文件,会提供两个读写函数:
这个文件就是用CherryUSB里面的demo改了一点,上图中usbd_msc_sector_write这个函数是USB主机将数据下发至USB设备,然后我们将固件数据推送给uf2进行处理。usbd_msc_sector_read这个函数是将USB设备的数据返回给主机。
另外我们还需要注意的是这个BOARD_UF2_FAMILY_ID,在后面制作UF2文件的时候会要用到。制作UF2文件时传入的ID参数要与这个保持一致。
port_uf2.c这个文件实现了uf2读写的一些依赖,如下图:
这几个函数通常必须要实现,board_flash_write会被uf2_write_block调用,所以要保证能正确的将指定长度的数据写入到指定的flash地址中。
接下来就是uf2接口的初始化。
完成这些,编译成功以后下载到芯片里面,插上电脑就会弹出一个U盘。但是这个时候U盘还不能用,因为我们还没有实现:
void lgk_boot_flash_write(uint32_t start_add, void *buffer, uint32_t size);
void lgk_boot_flash_read(uint32_t start_add, void *buffer, uint32_t size);
这些函数。
二、dfu
见CherryUSB里面的例子或者直接去PlumBL仓库里面看。
三、移植CH582
经过前面的介绍,框架我们已经搭建完成了,接下来针对不同的平台做移植,这里只介绍移植CH582,ST那些请看仓库里面的代码。
移植一个平台需要编写两个文件,并且实现以下函数:
这个jumpApp()其实就是一个函数指针,
#define jumpApp ((void (*)(void))((uint32_t *)APP_START_ADDRESS))
要能正确完成跳转,需要在启动的时候开启机器模式。
如上图的修改,不知道现在wch的sdk有没有修改过来。
这个函数需要用户根据自己的应用自己实现一下,默认是返回true。
上图是操作flash的部分,直接调用sdk的api即可。
上图的接口初始化在port_uf2.c里面已经完成编写。系统初始化在ch582平台上就是初始化时钟,如果要使用串口来输出调试信息,还需要初始化串口。
这个硬件决定是否进入bootloadr,我们这里只介绍双击reset的方式,首先这个DBRST_TAP_REG其实和lgk_boot_flag是同一个变量,在第一次初始化运行到lgk_boot_deley_ms的时候,DBRST_TAP_REG==DBRST_TAP_MAGCI,如果在延时的期间再次发生复位,系统运行到这个函数的时候则会被检测到双击reset,函数会返回true,则会进入到bootloader。如果在延时的期间一直没有发生复位,则会清除DBRST_TAP_REG,返回false。
如果想软件(从app中软件操作)进入bootloader,则需要在app中运行类似下面的代码:
void boot_jump(void)
{
uint32_t *boot_magic = (uint32_t *)0x20007ffc;
*boot_magic = 0xc220b134;
mcu_reset();
}
四、制作app文件
首先需要将你的app链接脚本中的flash起始地址改为0x10000,RAM长度修改为32K-4,像下面的图一样:
启动文件要记得开启机器模式,前文提到过。
接着就可以编译生成hex或者bin文件。然后确保电脑有安装python,下载了uf2conv.py和uf2families.json两个文件,接着你就可以运行下面的命令:
python uf2conv.py test.hex -o test.uf2 -c -f 0xabcdc582
python uf2conv.py test.bin -o test.uf2 -c -f 0xabcdc582 -b 0x10000
最后将生成的test.uf2文件直接拖入U盘完成下载。