前言
我由于做软件业务的需要,在这几年开发经历中,发现一个现象:各家芯片厂商boot开放的资料较少,不支持或少量支持定制化功能。可能也是需求少吧,毕竟对基线的改动需要的工作量也不小。但这也导致各家芯片的boot开发体验都不是太顺畅,开发者要自己摸索boot的一些定制化实现方案。
这篇内容接着上一篇 => 【填坑】ESP32 bootloader初探(上),看看bootloader里我是怎么搞定外设使用的。
开发工作
bootloader二看
在上篇中,已经初步了解过bootloader的文件结构,一些需要注意的特点,以及究竟如何修改bootloader文件,让自己的功能得到实现。
到这里,来看下具体到boot功能开发中,外设开发有哪些要解决的问题。
我在boot里主要就用到了串口和定时器两种外设。在官方说明里,boot支持的外设操作并不多,不包括我这次要用的这两种。需要自己编写函数,再放入驱动接口在内,给自己调用。
驱动接口上哪找
HAL层 —— 目录\component\hal
- 在app应用层可以直接调用driver层的接口,比如
uart_driver_install
和uart_param_config
来初始化串口;uart_wait_tx_done
或uart_tx_chars
来发送;timer_init
来初始化定时器等等。 - 在boot中,driver层 (目录\component\driver) 的接口不能使用,即使拿源文件过来放在目录下,配置好CMake,也是会编译报错的。其实进到driver层接口里面去看,driver层还会使用到freertos的各种功能,包括进入退出临界区、heap内存空间申请等等,这些依赖操作系统的调度是无法使用的。
- 一开始我也并不知道无法使用heap接口,一度尝试着把heap相关的源文件全部移入boot去编译,几番操作下来,错误越来越多。之后在论坛咨询才知道原来这些都是根本无法使用的,其实boot的ROM限制——64K,也不允许移入这么多复杂的外部组件功能。
LL层 —— 目录\components\hal\esp32c3\include\hal
- 观察hal层的接口,又可以发现内部调用了更加底层的ll层。从目录也可以看出,ll层已经跟具体的型号密切相关了,这也是可以在boot中移植目录下的源文件就编译使用的。
还是建议使用hal层的接口,这一层改完,未来适配不同型号的芯片,更加通用灵活,改动也不必太大。
我的做法是依照了driver的流程,把临界区保护和动态申请空间的接口都去除了,只保留对硬件驱动的流程,这已经对大部分driver接口都适用了。
- 值得一提的是中断的配置接口
esp_intr_alloc_intrstatus
。不需要像app那样动态申请空间给空闲的中断号,boot里直接固定一个中断号就行了,可以根据该接口调用的位置,看看传入参数是怎么样的,再到接口内部实现去看有哪些流程可以直接干掉的。- 比如flag参数涉及的流程,默认就都是0在生效;固定cpu核运行的流程也一样,可以干掉那些限制条件不会运行到的内容。
驱动移植遇到的问题
头、源文件找不到
- 在hal层的源码里,经常看到头文件的包含使用了绝对路径比如:#include “hal/timer.h”。当你直接把这样的源文件加入到boot工程中去编译,它就告诉你hal的目录找不到。这时,你就需要把源文件或头文件直接拿到bootloader_support的目录下面去,修改好CMake的路径编译就没有问题了。
外设功能开发
串口和定时器开发中,都有遇到一些或大或小的问题,记录下来,看到的小伙伴也可以避坑了。
串口
- 只有一路能自定义使用。ESP32-C3的串口总共有两路,UART0和UART1。UART0在内部已经作为了系统的打印输出口,无法取消掉。毕竟你一取消,那设备运行状态就一无所知了,不利于开发的推进。所以只有一路UART1给客户自己用的。
- 中断的配置可以挪用单片机MCU的开发经验。回想在MCU上配置中断的步骤,基本就是要清楚外设所在的中断号,配置优先级,编写中断回调函数这些步骤。在这里也可以依照这个思路。
- 中断号由自己确定一个固定给该外设就可以。优先级其实在这里没有特点的体现,我关注到的就是需要固定一个cpu的核给中断号使用,应该理解为该中断号与cpu挂靠好,cpu会自动去调度了吧。最后就是配置好中断回调的东西。
- 这些步骤在上面提到的
esp_intr_alloc_intrstatus
接口里面都能找到,只不过要自己删减不支持和不会运行到的步骤,也不算太复杂。
定时器
- 注意修改后函数参数的使用。在这里我其实已经根据上述过程移植好了,但运行定时器发现时间到了该定时器运行,设备就死机的情况,我一直很困惑怎么回事。仔细检查几遍自己修改的接口才发现问题。原来是回调函数的参数传入,应该给一个定时器结构体的指针,但我给了NULL,这就导致回调函数使用到原来指针的内容就访问错误了。
- 看门狗问题。boot流程中其实开启了看门狗功能,而且这个初始化的调用层级比较深,很难注意到该功能已经被启动了。 我是在调式定时器中,总是发现还没到我设定的定时器回调启动时间,设备就自动复位,且时间总是规律的10s左右。我以为是定时器不准确,问了原厂才发现是没有做喂狗导致的。
- 两种解决方式
- 根据esp_flash_encrypt_region的流程,修改自己花费时间长的函数流程,添加喂狗过程
- 修改Bootloader config -> Timeout for RTC watchdog (ms) ,改大时间比如 30000
- 第2种方式适合boot中花费时间比较固定的流程,像我这次的串口交互功能时间不固定,就只能采用第1种方式来解决。
- 两种解决方式
- 基线要选择好。这个就是我在上篇中提到的情况。旧基线修改完毕定时器所有接口之后,怎么调试定时器功能都运行不起来,换了新基线IDF v4.4.2之后,立马就好了。
退出boot
boot流程完成后,因为我们自己开启了外设的各种配置包括中断号等等的,切记要调用关闭这两个外设的初始化和中断的接口。否则跑到app你再去使用这些外设可能会有异常,别造成更多开发问题。
bootloader三看
终于啊,bootloader外设都单独调试好了,我们把更多的逻辑流程放入进去,跟外设功能结合起来。可算要完工了,编译一下… 出错了。boot编译出来太大,超过了默认分配给boot的32K大小。那没办法,只能再扩充到64K了。
- 这部分可以回顾上篇 => bootloader初探(上) 的bootloader初看 - 特点,固件大小这一节,也有怎么调整boot大小的内容。
menuconfig
- 这是改大boot比较关键的步骤,运行
make menuconfig
,如下图操作:
- menuconfig修改完千万不要忘了烧录工具的partition-table.bin的偏移也要改,如下图。没有改这里,boot烧录进去,运行时会出现boot跳转到app固件出错的log。
分区表
在这次的boot开发中,我还涉及到存储数据到分区中,app再去用的功能。这就必须使用一个自定义的分区来放数据。接下来记录,新建一个自定义分区,在boot和app中怎么用。
① 怎么创建
- 旧基线的分区表改法如下图,可以生效。但新的基线上这么改已经会编译报错了。
- 这个位置的subtype是ota,因为ota这个类别本身就有一定自定义空间大小的能力,而且也够用。
- 新基线的分区表改法如下图,这样的改法能在IDF v4.4.2成功生效并在程序中使用起来。
- 这里有我在ESP32论坛提问的帖子,可以直达去看看官方的回复 => 新增分区后编译失败的解决方式
② 怎么使用
按照上述方法创建好自定义分区后,怎么在boot和app程序中使用起来?
在app中
- 读写擦除的接口在partition.c中能找到。初始化我的写法如下:
esp_partition_t *p_partition = NULL;
//初始化接口
bool my_flash_init(void)
{
p_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, (const char *)"my_partition");
if (p_partition == NULL || p_partition->size == 0)
{
my_DebugPrint("my partition error!");
return false;
}
p_partition->encrypted = 0;
my_DebugPrint("my partition: type=%d, subtype=0x%02x, address=0x%x, size=0x%x, label=\"%s\", encrypted=%d",
p_partition->type,
p_partition->subtype,
p_partition->address,
p_partition->size,
p_partition->label,
p_partition->encrypted);
return true;
}
//读接口
esp_err_t esp_partition_read(const esp_partition_t* partition,
size_t src_offset, void* dst, size_t size);
//写接口
esp_err_t esp_partition_write(const esp_partition_t* partition,
size_t dst_offset, const void* src, size_t size);
//擦除接口
esp_err_t esp_partition_erase_range(const esp_partition_t* partition,
size_t offset, size_t size);
在boot中
- 读写擦除的接口在bootloader_flash.c中能找到。初始化调用方式和我的写法如下:
/*********** bootloader_start.c begin ************/
...
void __attribute__((noreturn)) call_start_cpu0(void)
{
...
bootloader_state_t bs = {0};
int boot_index = select_partition_number(&bs);
if (boot_index == INVALID_INDEX) {
bootloader_reset();
}
+++ my_bl_init_flash(bs);
...
}
...
/*********** bootloader_start.c end ************/
bootloader_state_t my_ps = {0};
//初始化接口
void my_bl_init_flash(bootloader_state_t ps)
{
ESP_LOGI(LOG_TAG, "my_bl_init_flash: app_offset = 0x%x, app_size = 0x%x\n", ps.factory.offset, ps.factory.size);
ESP_LOGI(LOG_TAG, "my_bl_init_flash: ota[0] offset = 0x%x, ota[0] size = 0x%x\n", ps.ota[0].offset, ps.ota[0].size);
memcpy(&my_ps, &ps, sizeof(bootloader_state_t));
}
//读接口
esp_err_t bootloader_flash_read(size_t src_addr, void *dest, size_t size, bool allow_decrypt);
//写接口
esp_err_t bootloader_flash_write(size_t dest_addr, void *src, size_t size, bool write_encrypted);
//擦除接口
esp_err_t bootloader_flash_erase_sector(size_t sector);
- 我相信看代码就能直观了解如何使用自己开辟的flash分区了吧。
链接文件
关于链接文件.ld的修改,主要决定了boot中能够使用的RAM资源有多少。
我自己在摸索RAM的大小分配上浪费了很多精力,建议先看官方文档 《ESP32-C3 技术参考手册》 的第三章“系统和存储器”章节 => 去看看,里面详细描述了各个存储区的地址范围。
- 在这里主要就是SRAM的范围, 如下图可以看到,实际能用的大小是384K+400K,对boot开发来说绰绰有余了。只要修改ld文件中的部分内容就可以使用到这些范围的RAM资源。
- 这里出现问题时我也有在ESP32论坛提问,可以直达去看看官方的回复 => Bootloader.ld文件的iram_seg范围是多大
记录到这,总算接近尾声了。多唠叨几句,开发中碰到问题,需要求助外部时,优先在ESP的论坛请教,官方的回复挺及时的,而且也可靠。
虽然如此,在提问前还是要多思考,多假设验证;求助时要会提有效的问题,而不是一股脑抛出心里想到的各种假设又不去做基本的验证,大家时间都很宝贵的。
最后,善用技术文档、各种手册资料,希望你在开发路上能披荆斩棘,不惧困难。共勉…
(*^▽^*)