【填坑】ESP32 bootloader初探(下) 您所在的位置:网站首页 如何烧录bin文件 【填坑】ESP32 bootloader初探(下)

【填坑】ESP32 bootloader初探(下)

2023-06-16 04:53| 来源: 网络整理| 查看: 265

前言

我由于做软件业务的需要,在这几年开发经历中,发现一个现象:各家芯片厂商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,如下图操作:

找到partition table修改的地方 修改offset值

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的论坛请教,官方的回复挺及时的,而且也可靠。

虽然如此,在提问前还是要多思考,多假设验证;求助时要会提有效的问题,而不是一股脑抛出心里想到的各种假设又不去做基本的验证,大家时间都很宝贵的。 最后,善用技术文档、各种手册资料,希望你在开发路上能披荆斩棘,不惧困难。共勉… (*^▽^*)



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有