ESP32开发环境的搭建
ESP-IDF 支持以下调试方法:
- 一. JTAG
- 二. GDB调试
- 三. 日志系统
- 四. Core Dump
- 五. esp32 heap 内存管理简析
ESP32开发环境的搭建
先安装VSCode(官网下载Lunix系统)
指令如下:
sudo dpkg -i xxx.deb
code .(打开目标文件下打开VScode)
开发过程:
(1) 首先安装编译好的ESP-IDF所需的包:
指令为 sudo apt-get install gcc git wget make libncurses-dev flex bison gperf python python-pip python-setuptools python-serial
(2) 然后下载脚本和执行脚本:
指令为
cd $HOME #进入HOME目录
wget https://esphuifeng.github.io/build_development_environment.sh #下载脚本文件
chmod u+x build development environment.sh #为脚本添加可执行权限
./build_development_environment.sh(这个过程需要等待下载时间)
(3) 配置
使用make menuconfig 配置编译参数。设置serial port为实际COM口,对于Ubuntu系统,可以使用ls /dev/tty*指令进行查看串口设备名称,该指令要进行两次命令,第一次是断开开发板,第二次是连接开发板。既可以看出来所使用的串口设备名称,然后运行make menuconfig去修改串口名称并保存。指令为(此对于Linux系统)
ls /dev/tty*(未连接开发板)
ls /dev/tty*(连接开发板)
make menuconfig(设置串口名称)
- 注意:此处需要注意的是一般使用USB口,无论USB转什么口,串口之类的,启动时容易出现:/dev/ttyUSB0 permission denied.因为一般情况下不是root用户,对端口没有权限.遇到这种情况,我一般这样做:
sudo chmod 777 /dev/ttyUSB0
修改权限为可读可写可执行,但是这种设置电脑重启后,又会出现这种问题,还要重新设置.因此查询资料,可以用下面这条指令:
sudo usermod -aG dialout xxx
其中xxx是我的用户名,换成你想用USB的用户名即可.把此用户名加入dialout用户组,然后注销下电脑,即可.这样下次重启也不用修改权限了
(4) 验证开发环境(esp-idf中的实例)指令为 (启用监视器:使用传统 GNU Make 编译系统)
cd ~/esp/esp-idf/examples/get-started/hello_world/
export IDF_PATH="$HOME/esp/esp-idf" #环境变量
make -j all #全速编译
make flash #烧录刚编译好的应用
make monitor #打开 monitor 工具观察串口输出
(5)若使用 CMake 编译系统,开发环境如下:
先安装Python,然后搭建cmake环境:
cd ~/esp
python2.7 -m pip install --user -r $IDF_PATH/requirements.txt
cd ~/esp/hello_world
idf.py menuconfig
idf.py build
idf.py -p /dev/ttyUSB0 monitor
在用户配置文件中添加 IDF_PATH 和 idf.py PATH
使用基于 CMake 的构建系统和 idf.py 工具,用户需修改两处系统环境变量:
- IDF_PATH 需设置为含有 ESP-IDF 目录的路径
- 系统 PATH 变量需包括含有 idf.py 工具 (属于 ESP-IDF 一部分)的目录
要设置 IDF_PATH,并在 PATH 中添加 idf.py,请将以下两行代码添加至你的 ~/.profile 文件中:
export IDF_PATH=~/esp/esp-idf
export PATH="$IDF_PATH/tools:$PATH"
~/.profile 表示在你的电脑用户主目录中,后缀为 .profile 的文件。(~ 为 shell 中的缩写)。
一、JTAG调试
这里先介绍第一种调试方法-JTAG调试,JTAG是从STM32转过来的开发者的第一反应,这里放在最前面介绍。但是对于双核多任务的ESP32应用程序,并非最优的调试方法。
1. 官方文档
关于调试环境的搭建,参考乐鑫官方文档即可,有中文版详细配置过程:
2. 硬件准备
2.1 ESP32 WROVER KIT
ESP-WROVER-KIT板载了JTAG功能(FT2232),但是需要连接4个跳线帽接通线路,如下图:
FT2232 是多协议 USB 转串口桥接器。在这里替换了cp2102,并且可以同时提供 USB-to-JTAG ,USB-to-Serial 接口功能,便利开发人员的应用开发与调试。具体配置信息可以查阅 ESP-WROVER-KIT V4.1 入门指南
2.2. ESP32 Core Board V2
ESP32 Core Board V2 没有JTAG插口,如果需要硬件调试,需要准备:
按照如下方式连接:
| | ESP32 引脚 | JTAG 信号 |
| 1 | CHIP_PU | TRST_N |
| 2 | MTDO / GPIO15 | TDO |
| 3 | MTDI / GPIO12 | TDI |
| 4 | MTCK / GPIO13 | TCK |
| 5 | MTMS / GPIO14 | TMS |
| 6 | GND | GND |
ESP32 使用标准的 JTAG 接口,支持STM32常用的J-Link,如何使用 J-Link 调试 ESP32,但是不支持ST-Link,也不支持SWD接口:
官方描述: 在软件方面,OpenOCD 支持相当多数量的 JTAG 适配器,可以参阅 OpenOCD 支持的适配器列表 (尽管上面显示的器件不太完整),这个页面还列出了兼容 SWD 接口的适配器,但是请注意,ESP32 目前并不支持 SWD。此外那些被硬编码为只支持特定产品线的 JTAG 适配器也不能在 ESP32 上工作,比如用于 STM32 产品家族的 ST-LINK 适配器。
3. JTAG调试笔记
大家也可以参考此链接
3.1. 开启openOCD
cd ~/esp/openocd-esp32
bin/openocd -s share/openocd/scripts -f interface/ftdi/esp32_devkitj_v1.cfg -f board/esp-wroom-32.cfg #在openocd安装目录执行,开启openOCD server
或者
cd ~/esp/openocd-esp32
export OPENOCD_SCRIPTS=$PWD/tcl
sudo openocd -f interface/ftdi/esp32_devkitj_v1.cfg -f board/esp-wroom-32.cfg
或
src/openocd -f interface/ftdi/esp32_devkitj_v1.cfg -f board/esp-wroom-32.cfg
注意如果不是用sudo可能会出现失败的问题,启动 OpenOCD 之后要保持此窗口打开
补充:openocd需要使用usb,建议把/usr/local/share/openocd/contrib/60-openocd.rules拷贝到/etc/udev/rules.d,这样openocd就有使用usb调试设备的权限了,不用每次sudo (配置完成后需要重启生效)
3.2 启动开发板和monitor监视器(可选)
连接好开发版,将JTAG相关的跳线帽连接好,正常下载启动,使用make -j4 flash monitor下载工程并打开的监视器,查看运行状态。
二、GDB调试
1. 配置GDB
在你要调试的工程下面 创建一个初始化配置文件
当启动调试器时,通常需要提供几个配置参数和命令,为了避免每次都在命令行中逐行输入这些命令,我们可以新建一个配置文件,并将其命名为 gdbinit:
1. target remote :3333
2. set remote hardware-watchpoint-limit 2
3. mon reset halt
4. flushregs
5. thb app_main
6. c
然后运行在终端中输入以下内容,启动 GDB:(这里的 build/blink.elf 请切换到你工程的 elf 文件路径)
cd ~/esp/esp-idf-v3.2.2/examples/get-started/blink
xtensa-esp32-elf-gdb -x gdbinit build/blink.elf #在工程目录执行,连接openOCD
其中对于配置GDB解释:
- set remote hardware-watchpoint-limit 2 — 限制 GDB 仅使用 ESP32 支持的两个硬件观察点。更多资料可以查阅GDB 配置远程目标。
- mon reset halt — 复位芯片并使 CPU 停止运行。
- flushregs — monitor (mon) 命令无法通知 GDB 目标状态已经更改,GDB 会假设在 mon reset halt 之前所有的任务堆栈仍然有效。实际上,复位后目标状态将发生变化。执行 flushregs 是一种强制 GDB 从目标获取最新状态的方法。
- thb app_main — 在 app_main 处插入一个临时的硬件断点,如果有需要,可以将其替换为其他函数名。
- c — 恢复程序运行,它将会在 app_main 的断点处停止运行。
2. 输入gdb命令,进行调试
- 断点设置:
break N:在第N行设置断点
delete N:删除第N行的断点
info break:查看已设置的断点数量和位置
- 调试运行
s(或step):单步调试,可以跳入函数内部
n(或next):单步调试,不跳入函数内部
c:Continue,继续运行,到断点处停止
Ctrl+C:随机停止
q:结束会话
- 查看、修改内存
x /1wx 0x3FF44004 #查看0x3FF44004内存位置内容
0x3ff44004: 0x00000000
set {unsigned int}0x3FF44004=0x000010 #设置0x3FF44004内存位置内容
p i:print,打印当前i的值
watch i:在发生的代码处插入所谓的“观察点”
- 浏览代码,查看堆栈和线程
i threads:查看当前所有线程
thread N:查看编号N线程的代码
bt(或backtrace):查看上一层,查看当前函数调用处(仅查看,未跳出)
l/list:打印停止点处代码
l 30, 40:打印第30-40行代码
- 查看帮助
help xxx:查看指令xxx的帮助
- 只需输入 help 命令,即可获得高级命令列表,帮助你了解更多详细信息。此外,还可以参考一些GDB命令速查表,比如 http://darkdust.net/files/GDB Cheat Sheet.pdf
- 注意事项和补充内容
具体可以查看:使用命令行的调试示例请细看关于GDB调试命令及用法。
运行的结果如图所示
3. 其他讨论
附1:通过gdb命令点亮LED灯
ESP32 WROVER KIT 自带RGB指示灯,分别接入了IO0、IO2、IO4,注意IO0、IO2的特殊作用。
ESP32管脚 | RGB LED |
---|---|
GPIO0 | 红色 |
GPIO2 | 绿色 |
GPIO4 | 蓝色 |
该寄存器可以用来控制(设置或者清除)某个 GPIO 的电平
查询当前寄存器值:
(gdb) x /1wx 0x3FF44004
0x3ff44004: 0x00000000 #返回0x00000000
设置GPIO2对应的寄存器标志位为1,点亮LED:
(gdb) set {unsigned int}0x3FF44004=0x00000004
遗留问题: 只有写入0x00000004(对应IO2)才能正常点亮,IO0、IO4直接写GPIO_OUT_REG(GPIO 0-31 output register: 0x3FF44004 ),无法实现灯的操作。怀疑受到其他寄存器钳制。
附2: openOCD与xtensa-esp32-elf-gdb关系?
无论先使用命令行还是eclipse调试,都需要先打开openOCD,关系图如下:
cd ~/esp/openocd-esp32
bin/openocd -s share/openocd/scripts -f interface/ftdi/esp32_devkitj_v1.cfg -f board/esp-wroom-32.cfg
- OpenOCD:
- 创建一个Server接收 gdb debugger 的连接请求
- 通过JTAG适配器与ESP32 交互,控制设备运行状态
- xtensa-esp32-elf-gdb debugger:
- 作为一个client,连接OpenOCD
- 与用户交互,接收用户的 debug 命令(s\n等指令),发送到OpenOCD
- eclipse
-图形化方式调用xtensa-esp32-elf-gdb debugger,包装命令行
- openOCD是硬件层次的调试,gdb是源码层次的调试。
- 在gdb中可以使用monitor发送openOCD的命令,例如monitor reset;halt
三、日志系统
1. 流程图
2. printf和ets_printf
- printf:一个ISR应该避免调用不可重入函数。一些标准库函数是不可重入的,如经常实现的malloc和printf。因为中断可能发生在执行任务中,因为任务可能是在“malloc”的函数调用中,如果ISR调用此相同的不可重入函数,由此产生的行为可能是灾难性的。
不可重入函数是指这样的一类函数,不可以在它还没有返回就再次被调用。例如printf,malloc,free等都是不可重入函数。因为信号可能在任何时候发生,例如在printf执行过程中,因此不能在信号处理函数里调用printf,否则printf将会被重入。 函数不可重入大多数是因为在函数中引用了全局变量。
例如,printf会引用全局变量stdout,malloc,free会引用全局的内存分配表。
不建议在 ISR 中使用 printf 和其余输出函数。出于调试的目的,可以在ISR 中使用 ESP_EARLY_LOGx 来输出日志,不过要确保将 TAG 和格式字符串都放在了DRAM 中。用在ISR中的常量数据 - ets_printf:打印字符串到uart或其他设备,类似于printf,比printf简单。无法打印浮点数据格式或长数据格式,所以我们可能只在ROM中使用它。
3. ESP_LOG日志
日志库有两种管理日志详细程度的方法:编译阶段,通过菜单设置;运行阶段,使用esp_log_level_set()函数设置。
日志等级有:错误,警告,信息,调试和详细(详细度从最低到最高)
- ESP_LOGE - 错误(最低)
- ESP_LOGW - 警告
- ESP_LOGI - 信息
- ESP_LOGD - 调试
- ESP_LOGV - 详细(最高)
在编译阶段,使用CONFIG_LOG_DEFAULT_LEVEL选项过滤。所有等级状态高于CONFIG_LOG_DEFAULT_LEVELD的日志将会被处理器移除。
在运行阶段,所有低于 CONFIG_LOG_DEFAULT_LEVEL 的日志被默认使能。esp_log_level_set() 函数可以用来减少每个模块的日志等级。模块通过标签识别,这些标签是可读的零结尾的ASCII字符串。
注意 esp_log_level_set() 函数不能提高到超过 CONFIG_LOG_DEFAULT_LEVEL 设置的等级。在编译阶段,为了给特殊文件提高日志等级,可以使用LOG_LOCAL_LEVEL宏(详见下)。
void esp_log_level_set(const char* tag, esp_log_level_t level);
typedef {
ESP_LOG_NONE,/ *!<无日志输出* /
ESP_LOG_ERROR,/ *!<严重错误,软件模块无法自行恢复* /
ESP_LOG_WARN,/ *!<采取恢复措施的错误条件* /
ESP_LOG_INFO,/ *!<描述正常事件流的信息消息* /
ESP_LOG_DEBUG,/ *!<正常使用不需要的其他信息(值,指针,大小等)。*/
ESP_LOG_VERBOSE / *!<较大的调试信息块或频繁出现的消息,有可能淹没输出。*/
} esp_log_level_t;
使用的方式如下形式,在运行时配置每个模块的日志记录输出,向函数添加调用:
esp_log_level_set("*", ESP_LOG_ERROR); // set all components to ERROR level
esp_log_level_set("wifi", ESP_LOG_WARN); // enable WARN logs from WiFi stack
esp_log_level_set("dhcpc", ESP_LOG_INFO); // enable INFO logs from DHCP client
下面是 <stdarg.h> 里面重要的几个宏定义如下:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“…”之前的那个参数;
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。
va_list list;
va_start(list, format);
(*s_log_print_func)(format, list);
va_end(list);
4. 控制台终端
ESP-IDF 提供了 console 组件,它包含了开发基于串口的交互式控制终端所需要的所有模块,主要支持以下功能:
- 行编辑,由 linenoise 库具体实现。linenoise实例
- 将命令行拆分为参数列表。
- 参数解析,由 argtable3 库具体实现,它支持解析 GNU 样式的命令行参数。argtable3安装地址以及argtable3教程
- 用于注册和调度命令的函数。
控制台终端具体详解
5. 高级筛选
6. 错误码到错误消息
-
在sdkconfig文件中CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
该功能默认启用。用户也可以禁用该功能,既将y去掉不使能,从而减小应用程序的二进制文件大小。注意,该功能对禁用并不影响 esp_err_to_name() 和 esp_err_to_name_r() 函数的定义,用户仍可调用这两个函数转化错误码。在这种情况下,esp_err_to_name() 函数在遇到无法匹配错误码的情况会返回 UNKNOWN ERROR,而 esp_err_to_name_r() 函数会返回 Unknown error 0xXXXX(YYYYY),其中 0xXXXX 和 YYYYY 分别代表错误代码的十六进制和十进制表示。 -
紧急处理程序
[外链图片转存中…(img-Qi3T0b5S-1574132788122)] -
寄存器转储与回溯
回溯行包含了当前任务中每个堆栈帧的 PC:SP 对(PC 是程序计数器,SP 是堆栈指针), 若要查找发生严重错误的代码位置,请查看 “Backtrace” 的后面几行,发生严重错误的代码显示在顶行,后续几行显示的是调用堆栈。
在minicom调试中,如果之后Backtrace后面的字符串,对Backtrace的后面进行解析可以使用该命令xtensa-esp32-elf-addr2line -fiap -e build/hello_world.elf Backtrace: 0x400d2182:0x3ffb4c70 0x400d09b2:0x3ffb4ca0 0x40084b95:0x3ffb4cc0
如果使用sudo minicom /dev/ttyUSB0中,可以用Ctrl+A之后再按Q就可以退出。
自动解码地址详细说明
- 配置 GDBStub 以启用 GDB
IDF 监控器在后台运行如下命令:xtensa-esp32-elf-gdb -ex “set serial baud BAUD” -ex “target remote PORT” -ex interrupt build/PROJECT.elf
启用GDB运行
7. Guru Meditation 错误
四、Core Dump调试
##1. ESP32 Core Dump
ESP-IDF 支持在不可恢复的软件错误上生成 Core Dump。这个技术可以对软件发生故障时的软件状态进行事后分析。在系统崩溃进入 Panic 状态时,根据配置打印一些信息并停止或重新启动。
ESP-IDF 提供特殊脚本 espcoredump.py,以帮助用户检索和分析 Core Dump。此工具提供两个用于 Core Dump 分析的命令:
- info_corefile - 打印崩溃的任务的寄存器,调用堆栈,系统中可用任务的列表,内存区域和存储在 Core Dump (TCB 和堆栈)中的内存内容。
- dbg_corefile - 创建 Core Dump ELF 文件并使用此文件运行 GDB 调试会话。
1.1. 配置
有许多与Core Dump相关的配置选项,用户可以在应用程序的配置菜单中选择 (make menuconfig或者用)。
- Core Dump 数据目标(Components -> ESP32-specific config -> Core dump destination):
- 禁用 Core Dump 生成
- 将 Core Dump 保存到 Flash
- 将 Core Dump 打印到 UART
- 核心转储中的最大任务快照数(Components -> ESP32-specific config -> Core dump -> Maximum number of tasks)。
- Core Dump 打印到 UART 之前的延迟时间(Components -> ESP32-specific config -> Core dump print to UART delay)。值以 ms 为单位。
1.2. 操作流程
- 先将目标文件进行编译,生成.elf文件(此文件要有错误码,错误代码例如如下所示);
- 与core dump配置选项,配置好;(将配置选择在FLASH上)
- 将程序进行编译
make -j all
make flash
make monitor
或者idf.py build flash monitor -p /dev/ttyUSB2 -b 912600
- 然后在 esp-idf/components/espcoredump 下运行;
用于分析来自闪存的核心转储的通用命令示例为:espcoredump.py -p </ path / to / serial / port> info_corefile </ path / to / program / elf / file> 或espcoredump.py -p </ path / to / serial / port> dbg_corefile </ path / to / program / elf / file>
6. 与core dump配置选项,配置好;(将配置选择在USRT上)
7. 将程序进行编译
make -j all
make flash
make monitor
8. 用户应手动将Core Dump文本正文保存到某个文件(此处保存在了hello文件中),CORE DUMP START 和 CORE DUMP END 行不得包含在 Core Dump 文本文件中。
code hello
或者
vim hello
- (将Core Dump打印到UART)运行下面指令
系统出现紧急情况时,base64编码的内核转储将打印在UART上的通用命令示例为
espcoredump.py info_corefile -t b64 -c </path/to/saved/base64/text> </path/to/program/elf/file>
或者 espcoredump.py dbg_corefile -t b64 -c </path/to/saved/base64/text> </path/to/program/elf/file>
python espcoredump.py dbg_corefile -t b64 -c ./hello ~/esp/esp-idf/examples/get-started/hello_world/build/hello-world.elf
注意: 如果在make menuconfig中,串口没有修改,只是还是选择的是core dump destination下的None选项,则编译会出现如下情况,不能得到想要的core dump文本,如下的文本将不能得到。
================= CORE DUMP START =================
<body of base64-encoded core dump, save it to file on disk>
================= CORE DUMP END ===================
五、esp32 heap 内存管理简析
-
Heap管理主要函数接口与数据结构
1.1 主要函数接口
ESP32的SDK对于heap部分管理的源码位于路径\esp-idf\components\heap下,可以简单的认为分为两层:heap_caps_init.c与heap_caps.c构成一层函数接口封装;multi_heap.c等文件内是具体函数接口的实现。主要接口:
Heap初始化: voidheap_caps_init(void)
Heap分配: void *heap_caps_malloc( size_t size,uint32_t caps )
Heap释放: void heap_caps_free(void *ptr)
ESP32 SDK中的malloc/calloc/free等系统调用,最终都是调用以上函数执行(参见syscall_stub_table)。
1.2 重要数据结构
Heap_t是Heap管理代码的核心数据结构。但是在heap_private.h与multi_heap.c下各有一个该类型定义。其实并不矛盾,前者是Heap管理中用于描述某个“Heap分区”的;后者定义仅限于multi_heap.c文件内部,作为“Heap分区”下”MetaData头”,示意如下:
由数据结构heap_t可以初步探知ESP32 SDK的Heap管理的结构:
(1) 整个Heap空间被作为多个Heap分区进行管理,描述Heap分区的结构体定义在heap_private.h文件下的heap_t,分区之间用链表关联;
(2) 每个Heap分区实体由MedaData头以及Data区构成:MedaData头定义在multi_heap.c文件下的heap_t;Data区分为已用空间(used space)以及可用空间(free space)。Data区以Heap_Block(heap_block_t)作为管理单元,通过链表关联。 -
heap_caps_init与Heap空间的初始化
2.1 Heap空间初始化
系统调用heap_caps_init实现Heap空间初始化的过程,实质就是初始化1.2节所示数据结构的过程。简单的看可以分为三个步骤:
(1) 遍历当前系统的可用内存区域,并对类型相同的相邻区域进行合并
(2) 将可用内存区域通过register_heap初始化为Heap分区的结构(建立Meta Data头以及数据区的链表)
(3) 将所有Heap分区信息通过链表连接到registered_heaps
2.2 heap_caps_init
比对2.1节三个步骤,截取主要代码如下:
(1) 遍历与合并当前系统内存可用区域,内存区域见数组soc_memory_regions
(2) 将所有可用内存区初始化为Heap分区
(3) 将所有Heap分区信息通过链表连接到registered_heaps
2.3 register_heap
register_heap将普通的内存区域初始化为Heap分区结构,multi_heap_register_impl 是其最终实现功能的接口。下图示意这个过程:
(1) 一段可用的连续内存被初始化为Heap分区时,分区起始地址会写入一个MetaData头(即是multi_heap_info类型)。MetaData 头后紧接着是第一个真正可以用来分配的内存块block0,block0包含一个信息头heap_block_t以及实际数据区。在Heap分区的最后,有一个heap_block_t的信息头作为整个分区的结尾。heap_block_t结构中有指向下个节点的指针(既*next_free),因此分区所有block组成链表用于访问。
(2) 当一段可用的连续内存被初始化Heap分区,它的实际可用空间是:对应如下图的“实际数据区”
-
heap_caps_malloc与Heap空间分配
Heap空间是系统运行期间交给用户申请的,如何分配与释放是关键问题。本节描述关于Heap空间如何分配的问题。
3.0 次要问题
为了突出“Heap空间分配机制”这个主干,先对几个Heap分配的次要问题进行说明:
(1) Malloc Cap:ESP32 SDK的Heap源码中,经常出现函数接口的参数是Malloc Caps。该参数的涵义是“待分配内存的特性”。例如:8/16/32位对齐,内存能否执行程序,是内部/外部内存等等。这个其实非常重要,与ESP32特性紧密相关。
(2) 内存临界区与跨界问题:某些时刻Heap的分配可能会涉及不同位置的内存空间。这里主要针对OTA升级功能。Over The Air Updates(OTA)
3.1 Heap空间分配(用户Heap空间申请)
对于Heap空间的分配过程,通过对代码的理解,可以简单的概括为三个步骤:
当用户向系统通过malloc申请Heap内存空间时:
(1) 系统会遍历每个Heap分区(通过registered_heaps),找到满足申请内存类型(例如:3.0所述Malloc Cap)的分区;再依次遍历分区下所有可用的Heap Block,找到可用空间满足申请内存大小的Heap Block。
(2) 遍历所有Heap Block期间,会出现几种可能:
A:若Heap Block的可用空间小于用户申请的内存大小,显然不能使用,继续查找下一个Heap Block;
B:若Heap Block的可用空间正好等于用户申请的内存大小,这时系统出现了最佳的分配选择,策略选择该Heap Block作为本次分配的Best Heap Block,停止遍历。
C:若Heap Block的可用空间大于用户申请的内存大小,则先行记录下这个Heap Block的位置以及它的实际可用空间大小,然后继续遍历查找;在找到下一个满足条件的Heap Block时,比较两者,选取可用空间较小的记录下来。策略的目的即是保证最终得到一个既满足用户申请需求又最不浪费空间的Best Heap Block。
(3) 对应步骤(2)选出的Best Heap Block:
A:若Best HeapBlock不存在,意味着用户申请失败;
B:若Best HeapBlock空间正好满足用户申请的需求,则无需另做处理,直接给到用户即可;
C:若Best HeapBlock的空间除去用户申请的需求还有盈余,则需要进行分割。盈余部分需要分割成为新的可用(空闲)Heap Block,等待下次内存申请。
下图是关于Heap空间分配的示意,体现出Heap Block的连接变化:
3.2 Heap 空间分配相关函数
ESP32 SDK Heap空间分配的相关函数以及调用过程:
Malloc ->
_malloc_r ->
heap_caps_malloc_default ->
heap_caps_malloc ->
multi_heap_malloc ->
multi_heap_malloc_impl
另外的重要函数,用于分割Best Heap Block: split_if_necessary
- heap_caps_free与Heap空间释放
Heap空间的释放是分配的逆过程。释放某个Block时同时还会探查该Block的前后Block是否可用;若可用则合并前/后Block:
ESP32 SDK Heap空间释放的相关函数以及调用过程:
_free_r->
heap_caps_free->
multi_heap_free->
multi_heap_free_impl->
merge_adjacent
注意:
- 以上关于Heap空间分配/释放的描述,隐去了一些细节。事实上可以认为Heap分区的Block管理使用了两个链表:一个用于Block的连接,另一个用于连接空闲的Block,两者被同一个结构体包括了。这里稍微关注一下Heap Block头的定义就知道了;
- 关于Heap空间释放的描述是有问题的,代码中发现,系统只是合并当前被释放的Block以及其“前一个”和“后一个”空闲可用的Block,这些Block可能并不是地址连续的。
- ESP32 Heap空间使用情况的观测
根据第1节对于Heap管理数据结构的描述就可以知道,查看Heap空间的使用情况,就是遍历各个分区,遍历各个分区下的Block链表。
相关函数有:
heap_caps_dump
heap_caps_get_minimum_free_size
heap_caps_get_free_size
heap_caps_print_heap_info
详细的说明链接Heap Memory Allocation
详细的说明链接Heap Memory Debugging
六、中断分配
可以产生中断的外设可以分为两种类型:
- 外部外围设备,在ESP32内,但在Xtensa核心之外。大多数ESP32外围设备属于这种类型。
- 内部外围设备,Xtensa CPU内核的一部分。
这两种外设之间的中断处理略有不同。
1.内部外设中断
每个Xtensa CPU内核都有自己的六个内部外设:
- 三个定时器比较器
- 性能监视器
- 两个软件中断。
内部中断源在esp_intr_alloc.h中定义为ETS_INTERNAL_*_INTR_SOURCE。
2.外部外设中断
其余的中断源来自外部外设。这些在soc/soc.h中定义为ETS_*_INTR_SOURCE。
两个CPU内核中的非内部中断插槽连接到中断多路复用器,可用于将任何外部中断源路由到任何这些中断插槽。
- 分配外部中断将始终将其分配给执行分配的核心。
- 释放外部中断必须始终在分配的同一核心上进行。
- 允许从另一个核心禁用和启用外部中断。
- 多个外部中断源可以通过将ESP_INTR_FLAG_SHARED标志传递给esp_intr_alloc()来共享中断插槽。
从未固定到核心的任务调用esp_intr_alloc()时应小心。在任务切换期间,这些任务可以在核心之间迁移。因此,无法分辨哪个CPU分配了中断,这使得释放中断句柄变得困难,并且还可能导致调试困难。建议使用带有特定CoreID参数的xTaskCreatePinnedToCore()来创建将分配中断的任务。对于内部中断源,这是必需的。
七、 grep命令的使用
- 语法格式:grep 【options】【pattern】【file】 grep [参数] [匹配模式] [查找的文件]
- 常用指令
grep -nr “file content” 查找内容
grep -c “file content” ./* 统计次数
八、esp32系列的使用
- esp-idf 基础框架
- esp-adf 适用于自动语音识别应用的开发框架
- esp-who 适用于一个面部检测和识别平台的开发框架
- esp-IOT-solution 各种物联网实例的具体使用的例程
- arduino-esp32 esp32的idf与arduino的idf结合使用