概述
1. 背景和总体介绍
一直以来都忙于做各种项目和产品,使得始终未能抽空很好的总结一下积累的在STM32L系列和ESP32这些MCU上的各种知识点,正好这次开发完之前的主体框架及版本后距离下一个大版本有了个空档期,我打算详细的记录一下这些知识点和学习心得并和各位进行交流。希望在和大家交流的同时,也能得到反馈并更正自己认识上的不足或者错误。
由于接触和掌握的局限性,这里记录的内容只是我自己在工作和学习中了解和使用到的,我不打算把别的不熟悉的内容也整理在一起增强篇幅,也不打算把一些我认为是大家都理解的东西再次详述一下。但是,为了叙述的必要,特别是和一些工程师讨论和接触后,发现很多人其实对某些基础知识不是非常清楚,关于这些有用的(或者是必要的)基础知识,我将以我az的观点进行一下简单描述。因此我整理了以下提纲作为这份资料的范畴:
编译及运行
编译的本质以及目标代码的分段定义
编译后的文件
系统的启动
应用程序加载
FreeRTOS的ESP32多核支持
MicroPython的ESP32版本
低功耗设计
2. 编译及运行
应用程序主要由一些C或者类C语言编写,编译后烧写到MCU的内置或者FLASH上然后再引导运行。把这些语言代码转变为机器代码的过程就叫编译。
2.1. 编译的本质以及目标代码分段
MCU的运转是按照数据流水线和指令流水线分工进行的,顾名思义数据对应的就是全局或者局部变量的读或者写操作,指令对应的就是各种运算和分支控制。而所有这些都是基于编译好的二进制指令进行的,这些编译后的指令按照属性不同被分为不同的段(SEGMENT):
代码段(TEXT SEGMENT)
非全零数据段(DATA SEGMENT)
全零数据段(BSS SEGMENT)
以上是编译阶段的三个典型目标,而我们所说的某个编译后的最终ELF或者BIN文件实质上就是上述不同分段的最终合成文件。这个过程是由编译器完成的,具体实行中编译器又分了两步来实现这个过程:
1) 编译(COMPILE)
根据编译规则和各MCU的特点、限制将原始代码转变为机器代码或者二进制数据块。以C语言为例,这一编译过程会产生很多.o结尾的二进制文件。
2) 链接(LINK)
把所有需要的各段机器代码或者数据块(以上若干.o文件)拼接为一个完整的最终目标文件。
除了以上三个典型分段之外,代码执行中还有两个非常重要的元素:
堆(HEAP)
栈(STACK)
堆本质上就是利用空余的内存进行运行中的动态分配和释放,这个空余的内存在大多数实现上用的都是以上三段的最高端结尾指针和堆栈的下端之间的的空余内存(中间可能预留部分缓冲)。对于很多裸操作系统的上位机MCU实现来说,压根就没用到堆。从程序运转来说,堆并不是一个必须的元素,完全取决于应用的需要,或者OS的倾好。
栈比堆则重要很多,我们口中经常提到的堆栈二字,大多数情况下说的其实只有栈而不包含堆。栈的操作有别与所有其它的,因为它是按照子上而下,或者从后向前的顺序进行的。以STACK_TOP = 0x20008000为例,当栈是全空时SP栈指针指向0x20008000,此时如果需要分配一个UINT32变量,那么该变量的地址则是SP-4 = 0x20007FFFC。同样,如果该局部操作结束,局部变量重新返回栈后,SP=SP+4=0x20007FFFC+4=0x20008000。这样的操作特点和我们的函数调用逻辑完全匹配,也即只有被调用者退出释放栈后,调用者才能继续退出并释放栈。但是这个设计对于程序的栈踩踏将是灾难的,假如被调用者的局部变量地址指针为uint32 *callee = (*uint32)0x20007FFF8,而调用者的局部变量指针为uint32 *caller=(*uint32)0x20007FFFC,如果被调用者出现程序故障并踩踏了下一个内存(callee[1]=0x12345678),那么这个操作实际上就会踩踏上一级调用者的空间(&callee[1] = 0x20007FFF8 + 4 = 0x20007FFFC)而不是向前空余的栈空间。以上栈踩踏是代码设计中应该严格检查并避免的。
注:
1) 以上caller/callee仅仅只是个举例,实际栈操作时还会压入调用者的下一步返回地址,这样被调用者通过获得这个地址可以实现函数结束返回上级调用者的下一步进行继续处理。假如栈踩踏的是这个返回地址,那么情况更糟,可能会出现意想不到的灾难性结果。
编译过程产生的.MAP文件可以非常方便的查看所有代码的符号地址、所属分段以及各分段地址。下面我们来看一段代码对各段的划分进行了解。
static char buf[1024];
void main(void) {
char *str;
const char *pattern = “hello,world!”;
str = (char*)malloc(1024);
str[0] = 0;
for( int i = 0; i <3; i++ ) {
strcpy(str+=strlen(str), (char*)pattern);
}
strcpy(buf,str);
free(str)
printf(“buf=%sn”, buf);
}
// 最终运行结果
buf=hello,world!hello,world!hello,world!`
在STM32或者ESP32MCU上,以上代码中各分段划分以及物理空间占用如下:
**[.TEXT]**
main()函数代码,若干Bytes按照WORD对齐
**[.DATA]**
“hello,world!” 结尾共13个字节,WORD 对齐后实际占用16个字节
**[.BSS]**
buf 共计1024字节
**[.HEAP]** (动态产生,编译时不存在)
str指向的内存空间
**[.STACK]** (main函数执行时)
str 只有4字节指针地址,指向分配的堆内存空间
i 四个字节变量空间
注:
1) 大多数编译优化后简单代码中的变量(<=机器字长度,STM32/ESP32系统中的char/short/int/long等类型,以及各种指针类型变量)实际并不占用栈空间,而是采用MCU快速寄存器来提高运转效率。
2.2. 编译后的文件
编译的目标是生成一个ELF扩展名的可执行文件,但是这个ELF由于包含了各段的定义和位置,除了体积比较大之外比较致命的缺点是离了加载程序的支持并无法直接启动系统的,而大多数MCU系统都又不支持ELF的加载,因此这个ELF程序还要经过一个叫OBJCOPY的过程按照设定的段位置拼接为一个可以用于直接烧写并执行的BIN格式。
但是EPS32这里有个很大的不同,ESP32的BIN并不是利用XTENSA-MIPS版本的OBJCOPY产生的最终BIN格式,而是利用esptool.py这个工具里面的ELF2IMAGE函数转换为了一个特殊的BIN格式。这个ESP32的特殊BIN格式在每个二进制段内容头部增加了一个IMAGE HEADER,而这个HEADER除了描述各段位置、长度外还定义了程序主入口的位置,和ELF一样这样的BIN其实是无法直接启动MCU的,为此ESP32是通过自己开发的(不对外)内置ROM程序进行加载这个特殊的BIN格式。但是这个ESP32特殊的BIN格式,只是增加了一个很小的IMAGE HEADER,其本身类似于传统的OBJCOPY方式,因此最终的ESP32的BIN格式相比传统BIN格式并没有增大很多大小。
那么问题来了,ESP32为啥要这么复杂搞这么一套BIN的格式和配套的加载?答案是资源的不连续和运行位置的特殊要求(IRAM/DRAM),比如如果一段1KB的代码要求运行在0x40070000,而另一端2KB代码要求运行在0x40080000的话,如果作为一个BIN文件那么需要产生66KB的一个BIN文件并烧写到FLASH(1KB + 63KB填充 + 2KB),而实际上这个有效代码只有3KB,额外的63KB全都是为了连续而做的额外填充,这样子的BIN会占用很多不必要的FLASH空间,而且假如这个填充的中间目标地址中有洞(某些加载地址无法访问)的话这个方式还会导致加载异常。这个资源碎片化问题是我觉得ESP32相比STM32系列,除了GPIO端口数目不足之外另一个明显不足。不过ESP32通过内部440KB ROM空间,以大量的不开放的基础软件将这个问题给克服了,但是大多数ESP32的方案就只能用额外的FLASH进行开发和部署,这个内部440KB的空间对大多数用户而言只能获取并执行一些底层函数,并无法进行自由改写。
2.3. 系统的启动
MCU的系统启动是通过上电复位中断根据预设中断向量表中的地址获得RESET指针然后跳转到这个指针所指向的代码空间实现后续引导。上电或者硬复位、软复位等各种复位中断是MCU芯片自带的硬件逻辑,一般可以用GPIO PINSTRAP设置一些不用的引导位置,比如RAM BOOT, ROM FLASH BOOT其对应的预设中断向量表地址均可不同,以此实现从RAM或者FLASH等不同的位置加载启动代码。但无论这个预设基地址位于哪里,STM32从该预设基地址+0位置获取向量表,并实现相应的跳转。这就是为何最初阶段的引导程序开头部分总是若干函数指针的原因,该系列函数指针就对应各中断实际的中断处理函数。该中断向量表的位置其实是可以被后期进行修改的,很多MCU都支持启动后软件方式修改这个向量表,这样做的主要目的是:APP引导后可以用自身的中断处理函数代替LOADER时所定义的中断处理函数。不过对于典型的STM32单一引导应用(业务逻辑和引导均在一个BIN文件里面),这个中断向量表的位置在STM32上很少会被修改。
2.3.1. STM32的引导
以下是STM32的引导代码所对应的中断向量表定义(可从STM32启动汇编中得到这些):
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler
……
可以看出每次系统复位或者上电均会产生中断号#1的复位中断,MCU据此从以上表格的#1位置获取Reset_Handler,注意#0定义的是栈顶位置。这个中断表的其它指针分别对应其余不同中断级别的处理函数。从Reset_Handler()之后所有的代码无论是汇编还是C我们都可以从代码库中看到,这个过程也十分清晰,简单总结一下就是:
1) 根据以上_estack定义初始化SP栈顶为以后的正常函数调用特别是C函数调用准备
2) 根据.BSS的位置和大小初始化相应内存,注意BSS段最终存放于RAM里面可供读或者写
3) 从FLASH上加载.DATA段,这个又分两部分:
a) RO (只读部分)
保留在FLASH上不变,代码将直接从FLASH上获取相应的数据
b) RW (读写部分)
从FLASH上搬运到RAM指定位置,后续代码只访问RAM,而不再访问FLASH上的该部分数据。
4) 加载后续初始化代码实现后续正常引导
以下示例中, g_common_help将会以RO形式保留在FLASH上,而g_default_pattern将最终搬运到RAM里面,因此如果打印两个指针的地址,前者将是FLASH映射的指针位置,后者将是RAM指针位置:
const char g_common_help[] = “Help Information!n”;
char g_default_pattern = “ABCD EFGHn”;
2.3.2. ESP32的引导
和STM32显著不同的是,ESP32的系统复位总是从0x40000400这一内建ROM位置开始,而且不可更改。虽然后续软件可以修改中断向量表,但是无法修改这个复位地址。不过ESP32当加载失败后可以加载内建的ROM Loader(一套TinyBasic程序)可以用于分析诊断系统。截止到目前,我没有看到这个内建ROM的源码,但是相关资料倒是可以看到不少,结合一系列的SDK针对内建ROM的.LD定义文件以及官网介绍,大体上也能得到很多信息。ESP32称这个固定的ROM 加载为第一阶段引导,通过第一阶段ROM固定从FLASH偏移0x1000的位置进行现第二阶段引导,并通过第二阶段的BIN IMAGE HEADER完成内存搬运,并通过HEADER里面的call_start_cpu0()入口函数指针实现管理权交接。交接之后的各种内容和操作就可以通过代码看到了,这里不再过多介绍。需要注意的是:
1) 第一阶段的片内ROM Loader总是把FLASH 偏移0x1000的第二阶段Loader加载到内存0x40070000的起始位置并交接系统控制权给内存中的第二阶段Loader,所以:
LOADER阶段 代码位置 运行位置
第一阶段 片内ROM(0x40000400) 片内ROM直接运行
第二阶段 FLASH (0x1000) 加载并从RAM 运行(0x40078000)
2) 整个第一和第二阶段引导只有PRO CPU参与,APP CPU全程保持RESET状态。直到call_start_cpu1()函数开始执行才启动APP CPU
3) 如果查看ESP32代码时会看到两个call_start_cpu0(),一个位于bootloader目录的是第二阶段引导的开始部分,另一个是APP的用于第二阶段引导指示APP的开始部分
4) 从第二阶段引导到APP引导这一个过程并不是一成不变的,虽然ESP32的IDF框架默认采用了这样的流程和机制,但是我们一样可以把所有功能都在第二阶段引导里实现,对于简单的应用来说,只要一个IMAGE就可以实现主要功能了
5) ESP32的资源比较碎,而且很多空间并不连续,因此引导初始化时需要处理不同的加载DATA段或者BSS段,这也给阅读和理解带来一些不便
当采用ESP32推荐的标准二阶段引导+APP方式进行启动时,最终FLASH(不是内置ROM)上的格式将为:
偏移 长度 内容
0x00000 0x1000 保留
0x01000 0x7000 第二阶段Loader
0x08000 0x0C00 FLASH分区表(说明各分区用途位置和大小)
PART0_OFS PART0_LEN #0分区(位置用途大小取决于以上分区表)
PART1_OFS PART1_LEN #1分区(位置用途大小取决于以上分区表)
……
PARTn_OFS PARTn_LEN #n分区(位置用途大小取决于以上分区表)
2.4. 应用程序的加载
2.4.1. STM32的应用程序加载
很多STM32项目采用单一文件架构,Loader和APP是二合一的,但有些STM32的实现项目支持Loader/APP分离的IMAGE架构,这样Loader在对系统进行必要的初始化和加载后,可以进行APP IMAGE的加载。这个APP IMAGE其实也是烧写在内部FLASH上,并且编译时就是按照烧写位置确定的基地址和符号地址,因此这个加载只要进行一个跳转就行了。于此同时Loader部分仍旧提供包括SysTick中断在内的各种中断服务,APP部分通过调用Loader接口实现部分业务中断的配置和中断函数回调处理。这个架构类似于Linux Kernel + RootFS Image的形式,只是受限于资源和MMU的管理缺失,最终都是在一个内存平面下(UMA)实现Loader和APP的共存。
注:
1) 这个Loader/APP分离的设计和STM32本身并不太大关系,完全是开发者根据项目需要自己组织并开发的
2) 中断实现可以有两种做法:Loader负责中断并提供操作辅助函数给APP;APP接管一切包括中断向量表
2.4.2. ESP32的应用程序加载
标准ESP32 SDK采用的就是Loader/APP分离的架构,并且APP接管全部中断。由于ESP32拥有两级Loader,其分工各不相同:
第一阶段片内ROM Loader
提供基本的引导
实现第二阶段加载和系统控制权传递
以ROM直接运行的方式提供各种底层库和函数给后续Loader或者APP
即便APP运转起来后,这部分ROM资源一样可用,而且SDK中很多函数的最底层实现都是调用这个ROM函数实现的。具体函数命名和指针,通过若干.LD文件确定。我觉得这个设计好(节省空间),但是也不好。不好的原始是因为.LD是个固定的文件,其函数指针位置也是固定的,假如后期因各种原因需要升级并调整片内ROM,这个指针位置如果也被影响了那就是巨大的,会导致所有之前SDK开发的代码无法正常运转。好在目前没有遇到这样的情况,为什么不用动态函数指针呢?牺牲一个函数列表,用1KB空间把上百个函数列表指针全都定义在ROM里面的某个Well-known地址不是更好么?我在STM32上做的Loader/App分离的设计就是这样实现Loader和App的函数共享的,并且Loader的升级完全不影响App。
第二阶段FLASH加载到RAM的Loader
只是完成APP的引导(包括Factory/OTA等策略以及相应的MMU映射)
APP引导后,第二阶段Loader将被覆盖并失去作用,其占用资源将可以被APP利用
由于第二阶段引号是个专属的Loader程序,可以实现复杂的APP应用程序管理策略。常见的如OTA1、OTA2启动选择,恢复出厂版本等选项。但是由于RAM空间的不足,所有这种APP最终仍将选择在FLASH地址上运行,而不能像第二阶段Loader那样加载到内存中运行。这种基于FLASH地址的运行,和OTA1/OTA2有很大关系,因为所有的符号表和指针地址都是编译阶段决定的,而运行时如果烧写到不同的FLASH,那么这个编译确定的地址将会是错误的,导致无法正常运行。对于很多没有MMU的MCU来说,一般会这样设计FLASH分区:
1) 运行区域
2) OTA1区域
3) OTA2区域
所有APP版本编译时均按照运行区域的基地址进行编译和链接,但是烧写时仍旧按照OTA策略烧写如OTA1或者OTA2区域。在Loader加载APP时会根据情况将正确的OTA1/OTA2再次烧写到运行区域后,从运行区域加载这个新的版本。这样做的代价有两个:1)运行时的加载比较缓慢;2)总有一块额外的运行区域被占用,导致实际可用FLASH空间减少。
ESP32对于以上FLASH加载不同区域的解决办法是利用自带的MMU:
1) 所有APP编译时均按照一个虚拟的运行地址进行(0x3ff40000)
2) 第二阶段Loader通过MMU把FLASH上的实际位置映射为以上虚拟地址并实现APP的基于这个转换的虚拟地址的跳转
和第二阶段BIN格式文件一样,APP的BIN格式文件一样也有一个IMAGE HEADER可以让Loader知道APP的启动函数指针(仍旧是APP里面的call_cpu_start0函数)实现系统控制权的传递。至此APP在ESP32上就实现了成功加载。
3. FreeRTOS的ESP32多核支持
ESP32的SDK默认就集成了FreeRTOS V8.2.0版本,所以对想用FreeRTOS实现多任务调度的项目来说具有天然的优势。
3.1. ESP32的多核
ESP32有两个运转在240MHz的同架构CPU:PRO CPU和APP CPU,从系统复位开始到APP被加载,全程只有PRO CPU保持运转,APP CPU均处于RESET状态(SDK喜欢用STALL来形容这个状态)。
在APP的入口函数call_start_cpu0()内,通过以下步骤激活APP CPU:
1) 清空并使能APP CPU Cache
2) 使能(UNSTALL)APP CPU
3) 通过call_start_cpu1()作为入口函数设置APP CPU的起始运转点
4) 等待APP CPU启动正常(通过全局标志变量app_cpu_started)
而APP CPU通过call_start_cpu1()实现自身的初始化和状态通告,并最终实现后续各种应用程序的运行。
至此ESP32上两个CPU均成功运行。这两个CPU运行的最终程序分别通过start_cpu0() 和start_cpu1()区分。PRO CPU对应#0 (同样用于其余CORE ID判断,识别PRO CPU), APP CPU对应#1。以上start_cpu0() / start_cpu1()最终的默认执行函数是start_cpu0_default() / start_cpu1_default().
3.2. FreeRTOS 多核调度初始化
FreeRTOS支持多核的任务调度,在ESP32上FreeRTOS的多核调度本质上是单进程顺序方式进行的,通过port_xSchedulerRunning[2]这个标志队列实现了顺序初始化FreeRTOS任务。这个过程可以描述为:
1) PRO CPU 通过start_cpu0_default()进入正常初始化流程,同时APP CPU也通过start_cpu1_default()进入但是始终轮询port_xSchedulerRunning[0]标志。此时本质上等于只有PRO CPU在运行
2) PRO CPU继续其它的初始化操作并通过FreeRTOS的xTaskCreatePinnedToCore()函数在PRO CPU上挂起一个新的任务(对应任务入口函数main_task())。但是由于尚未激活FreeRTOS的任务调度,此时这个挂起的任务并未得到执行
3) PRO CPU最终初始化并调用FreeRTOS的任务调度器激活PRO CPU上的任务调度策略,同时设置port_xSchedulerRunning[0]=1通知APP CPU
4) 之前PRO CPU上被挂起的main_task()进入运行状态,但是这个函数一开头就会轮询检查port_xSchedulerRunning[1]标志等待APP CPU进入正常调度状态,因此此时本质上等于PRO CPU暂停(不停的轮询检查标志),APP CPU进入正常处理流程
5) APP CPU终于等到了PRO CPU释放的port_xSchedulerRunning[0]标志开始执行start_cpu1_default()函数内的后续操作,并最终成功激活APP CPU上的FreeRTOS任务调度,实现了FreeRTOS的双核调度,同时APP CPU还设置了port_xSchedulerRunning[1]标志用于通知还在等待中的PRO CPU上的main_task()
6) 此时PRO CPU / APP CPU同时工作,PRO CPU进行main_task()的后续处理,并最终通过app_main()函数实现更高层次的业务逻辑入口;APP CPU处于等待任务过程中()此时只有PRO CPU上的main_task()任务,并无多余的任务用于APP CPU执行调度)。FreeRTOS至此完成了ESP32上的双核调度初始化,进入正常工作状态
3.3. FreeRTOS 多核调度场景
ESP32支持两种主流场景的任务调度:
1) 任务主动调度
这种情况主要用于当前工作任务结束、等待,或者需要别的事件输入时,任务将主动放弃当前调度并通过FreeRTOS的TaskYield处理实现任务调度。
2) 基于中断的调度
a) SysTick调度
ESP32保持了一个1000Hz的SysTick中断,除了实现CPU运转Tick计数和基于Tick的某些定时功能外,还可以用来触发产生任务调度。这样即便没有外围其它中断触发,高优先级任务也能再合适的时间得到调度。
b) 各种中断事件调度
所有中断处理的最后(包括SysTick中断)都会通过XT_RTOS_INT_ENTER()和XT_RTOS_INT_EXIT()函数实现当前任务堆栈的保存和调度切换(根据优先级和当前任务执行时间),这样任何外部事件或者中断触发都可以得到一次任务调度机会。
4. MicroPython的ESP32版本
ESP32对MicroPython的支持是比较成功的,利用SDK集成的FreeRTOS以及多核调度,通过APP CPU绑定的任务mp_task()就实现了对MicroPython的支持。由于是绑定CPU的任务方式,MicroPython的所有操作实际上都只会由APP CPU完成,这样避免了很多多核调度时导致的各种问题,对于MicroPython这类应用来说虽然是个妥协,但是绝对是个稳妥的解决办法。
5. 低功耗设计
完美的低功耗设计应该是MCU停止在某处等待定时器或者外部中断触发激活,此时外设IO电位保持不变,系统设计人员根据需要选择OUTPUT/INPUT等状态以满足系统运行的需求,同时系统处于一个非常低的电流消耗状态可以提供长时间的电池续航能力。当满足定时器或者中断触发条件后,MCU可以从停止处继续,所有的RAM和ROM FLASH状态都不变,甚至RAM里的信息都原封不动的保存下来。这才是一个完美的低功耗系统!
5.1. STM32L系列低功耗实现
STM32 L1/L4是目前最主流的低功耗MCU系列,对于系统设计人员来说STM32 L系列绝对是一个完美的低功耗系统:
1) 支持STOP模式,并且MCU可以做到<2 uA 的STOP模式待机电流消耗
2) RAM处于低功耗刷新状态,但是所有内容均保持
3) 所有IO状态和ROM FLASH均保持STOP前的状态
4) 支持各种定时器和外部中断触发系统恢复正常模式
除了这个完美的STOP模式外,STM32 L系列也提供其它大多说其它低功耗系统的SLEEP模式,但是在STM32L的 STOP模式面前这个SLEEP模式实在不值一提。待会儿ESP32部分会介绍这个模式。在STM32L上对于一个外设、IO有很多额外要求的略微复杂的系统来说,低功耗就意味着STOP模式而已,没有之一。
唱了这么多赞歌,还需要对其中一点额外多说几句,那就是低功耗状态下确切的说是STOP状态下的IO和外设。STM32L没有明确要求处于STOP模式时,哪个IO必须什么状态,这一切完全由MCU软件控制。如果需要通过GPIO给某个外部设备供电,STOP时保持OUTPUT有效不变即可(其实对STM32L来说就是一行代码都不要写,默认就是保持所有状态不变);如果需要STOP时关闭某些IO,特别是某些输出为高的IO以进一步省电,那么可以设置这些IO为高阻输入后再STOP即可。同样,UART, I2C, SPI所有这些总线和硬件模块都可以根据需要选择是否在STOP期间关闭,总之一句话,想怎么做就就怎么做,STM32L没特殊要求。假如进入STOP时,系统开了很多IO并且都有相关外设与之相连,结果MCU自身肯定可以保证进入STOP低功耗模式,但是这些IO和外设肯定会消耗明显的电流,这个是系统设计问题,并不能归咎于STM32 L的低功耗缺陷,而且完全可以避免。
最后用一段经典的STM32 L STOP模式进入代码来致敬这个完美的低功耗设计吧:
BACKUP_PRIMASK();
DISABLE_IRQ( );
/*clear wake up flag*/
SET_BIT(PWR->CR, PWR_CR_CWUF);
RESTORE_PRIMASK( );
/* Enter Stop Mode */
HAL_PWR_EnterSTOPMode ( PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI );
5.2. ESP32低功耗实现
对于系统设计人员来说ESP32系列是一个完美的IOT集成开发系统,但是绝对不是一个完美的低功耗系统。典型的Deep Sleep模式时ESP32的主流低功耗实现方法,具体为:
1) 通过Deep Sleep + RTC模式,MCU<10uA电流消耗
2) ULP协处理器需要消耗最大150uA,一般不要使用
3) RTC必须开启,否则无法保持某些IO状态和内存状态
4) 系统可以设置为定时器或者外部中断触发醒来
注:
1) 但是醒来后通过SystemReset进入,而不是像STM32L系列STOP模式那样进入到Sleep指令的下一条语句。因此除了某些RTC管脚状态外,系统所有总线和其余IO都是复位状态,知道醒来后再次初始化。
由于每次Deep Sleep后都会经历System Reset才能复位,那么是几秒的Sleep也需要经历一次完整的一级Loader、二级Loader和APP这样的加载逻辑,只不过ESP32 SDK配合硬件能够提供辅助信息让应用程序知道这次启动的原始不是上电复位而是Sleep后的复位并据此可以做些特殊的动作。可想而知,如果上电复位后应用程序想恢复到Sleep之前的所有状态的话,还是需要一些额外的逻辑的。这给一个较复杂的系统会带来不少的麻烦和额外工作量(对比STM32 L系列的低功耗STOP模式)。
另外一点,ESP32在Deep Sleep期间只有部分IO管脚可以保持状态,并且这些管脚必须明确被代码设置为PULL_HOLD状态。这个状态将一直延续到MCU醒来,并且二次启动结束,应用程序再次接管系统后仍旧有效,此时应该主动调用Disable函数来释放管脚为正常状态,解除PULL_HOLD的锁定。而这所有相关操作管脚都只针对RTC管脚有效,除了RTC管脚外其余管脚即便尝试软件PULL_HOLD锁定,Sleep期间一样会回到默认复位状态,这个必须要注意。ESP32支持Sleep期间锁定状态的管脚有下列几个:
GPIO管脚ID 支持的模式
34/35/37/38/39 INPUT
0/2/4/12/13/14/15 INPUT&OUTPUT
25/26/27/32/33 INPUT&OUTPUT
最后
以上就是羞涩胡萝卜为你收集整理的IOT主流MCU: STM32和ESP32的学习了解的全部内容,希望文章能够帮你解决IOT主流MCU: STM32和ESP32的学习了解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复