ESP8266原厂提供了Non-OS和RTOS版本的SDK。
Non-OS版本SDK主要使用定时器和回调函数的方式实现各个功能事件嵌套,达到设定条件后触发指定的事件及回调函数。同时Non-OS使用的是espconn接口实现网络操作,开发者须按照espconn接口使用规则进行网络应用开发。
RTOS版本SDK使用FreeRTOS嵌入式实时操作系统,开发者使用FreeRTOS的标准接口实现资源管理、定时、延时、任务间信息传递和同步等面向任务的设计流程。同时RTOS版本SDK使用标准LwIP API,同事提供BSD Socket API接口的封装实现,开发者可直接使用socket API开发网络应用。RTOS SDK兼容Non-OS SDK中的Wi-Fi、SmartConfig、Sniffer、系统、定时器、FOTA和外围驱动相关接口。
目前原厂上海乐鑫已将RTOS版SDK(ESP8266_RTOS_SDK)开源,同时ESP8266_RTOS_SDK内用的到很多第三方开源代码也开放在github上,github地址为:https://github.com/espressif/ESP8266_RTOS_SDK。github更新记录显示,原厂持续对SDK进行更新升级中。
第三方开源代码位于ESP8266_RTOS_SDK的third-party目录下,已开源的包括嵌入式实时操作系统(FreeRTOS)、TCP/IP协议栈(LwIP)、SPI文件系统(spiffs)、SSL的多种不同实现(mbedtls、openssl、ssl)、WebSocket(nopoll)、cjson、espconn等。
在SDK的lib目录下,有以上各开源代码编译完成的lib库文件,在third-party目录下执行下sh make_all_lib.sh命令,即可编译全部lib库文件至lib目录,也可单独更新指定lib库文件。
今天学习的是ESP8266上自带的嵌入式操作系统FreeRTOS,通过FreeRTOS运行分析,学习ESP8266的RTOS移植、启动、任务切换、内存管理等细节。
ESP8266_Free_RTOS使用的FreeRTOS版本为7.5.2,包括以下文件:
通过与FreeRTOS官方同一版本源码逐一比对,可以发现ESP8266对FreeRTOS源码修改基于以下几点:
1. 将源文件中的开中断portDISABLE_INTERRUPTS()与关中断portENABLE_INTERRUPTS()替换为PortEnableInt_NoNest()与PortDisableInt_NoNest();这2个函数在port.c内实现。
void PortDisableInt_NoNest( void )
{
if(NMIIrqIsOn == 0)
{
if( ClosedLv1Isr !=1 )
{
portDISABLE_INTERRUPTS();
ClosedLv1Isr = 1;
}
}
}
void PortEnableInt_NoNest( void )
{
if(NMIIrqIsOn == 0)
{
if( ClosedLv1Isr ==1 )
{
ClosedLv1Isr = 0;
portENABLE_INTERRUPTS();
}
}
}
这2个函数对NMI中断标志进行了判断,只有在未进入NMI中断情况下才能开关中断,NMI中断为ESP8266最高优先级中断。
然后调用portDISABLE_INTERRUPTS()和portENABLE_INTERRUPTS(),同时对开关中断次数使用ClosedLv1Isr标志进行了保护。
2. 在很多函数前加ICACHE_FLASH_ATTR定义;该宏通过makefile中的ICACHE_FLASH宏开启。#define ICACHE_FLASH_ATTR __attribute__((section(".irom0.text")))。
在Non-OS版本SDK,添加了“ICACHE_FLASH_ATTR”宏的函数,将存放到IROM中,CPU仅在调用到它的时候,将其读取cache中运行;没有添加“ICACHE_FLASH_ATTR”宏的函数,将在一上电时就加载到IRAM中运行;由于ESP8266的RAM空间有限,所有无法将所有代码一次性加载到IRAM中运行。故在大部分函数前添加“ICACHE_FLASH_ATTR”宏,将其放至IROM中。
但是ESP8266_RTOS_SDK,函数默认存放在IROM中,中断处理函数也可以定义在IROM中,所以无需特意添加“ICACHE_FLASH_ATTR”宏。如开发者需要将一些频繁调用的函数指定在IRAM中,应在函数前添加“IRAM_ATTR”宏。
3. 加入了memleak内存泄漏检测工具,将源码中全部的pvPortMalloc/vPortFreet修改为os_malloc和os_free。
对于ESP8266_RTOS_SDK目前不支持memleak检测内存泄漏。
4. heap4.c针对heap分配起始地址和大小进行了修改,修改为编译器自动获取。
FreeRTOS移植适配包括系统节拍中断,任务栈初始化、开关中断、任务切换、调度器启动等,ESP8266是在port.c和portmarco.h内实现的。
操作系统的运行是由系统节拍时钟来驱动的,系统的延时和阻塞时钟都是以系统节拍时钟周期为单位。FreeRTOS配置文件FreeRTOSConfig.h中定义了configCPU_CLOCK_HZ,可以改变系统节拍时钟的中断频率。
ESP8266配置文件内已定义#define configTICK_RATE_HZ( ( portTickType ) 100 ),系列时钟节拍周期为10ms。
SysTick初始化、使能是在port.c内的xPortStartScheduler()函数调用_xt_tick_timer_init()函数完成的,该函数在库文件libmain.a内实现,我们无法获知具体实现细节。
/* Initialize system tick timer interrupt and schedule the first tick. */
_xt_tick_timer_init();
SysTick中断函数xPortSysTickHandle()在port.c内实现,使用的是标准代码,该函数应该已经在中断向量表中被调用。
void xPortSysTickHandle (void)
{
if(xTaskIncrementTick() !=pdFALSE )
{
vTaskSwitchContext();
}
}
中断函数调用xTaskIncrementTick(),如该函数返回为真,说明处于就绪态任务的优先级比当前运行任务的优先级高,则调用vTaskSwitchContext()做一次任务切换。任务切换将在下一节中讲解。
portSTACK_TYPE * ICACHE_FLASH_ATTR
pxPortInitialiseStack(portSTACK_TYPE *pxTopOfStack, pdTASK_CODE pxCode, void *pvParameters )
{
#define SET_STKREG(r,v) sp[(r) >> 2] = (portSTACK_TYPE)(v)
portSTACK_TYPE *sp, *tp;
/* Create interrupt stack frame aligned to 16 byte boundary */
sp = (portSTACK_TYPE*) (((INT32U)(pxTopOfStack+1) - XT_CP_SIZE - XT_STK_FRMSZ) & ~0xf);
/* Clear the entire frame (do not use memset() because we don't depend on C library) */
for (tp = sp; tp <= pxTopOfStack; ++tp)
*tp = 0;
/* Explicitly initialize certain saved registers */
SET_STKREG( XT_STK_PC, pxCode); /* task entrypoint */
SET_STKREG( XT_STK_A0, 0); /* to terminate GDB backtrace */
SET_STKREG( XT_STK_A1, (INT32U)sp + XT_STK_FRMSZ); /* physical top of stack frame */
SET_STKREG( XT_STK_A2, pvParameters); /* parameters */
SET_STKREG( XT_STK_EXIT, _xt_user_exit); /* user exception exit dispatcher */
/* Set initial PS to int level 0, EXCM disabled ('rfe' will enable), user mode. */
#ifdef __XTENSA_CALL0_ABI__
SET_STKREG( XT_STK_PS, PS_UM | PS_EXCM );
#else
/* + for windowed ABI also set WOE and CALLINC (pretend task was 'call4'd). */
SET_STKREG( XT_STK_PS, PS_UM | PS_EXCM | PS_WOE | PS_CALLINC(1) );
#endif
return sp;
}
首先创建16字节对齐的中断堆栈指针sp,由于堆栈是向下生长的(在protmacro.h中定义portSTACK_GROWTH为-1),故sp为栈顶减去全部特殊寄存器空间。然后对sp开始的地址置0,并将任务切换需要的参数保存至指定的特殊寄存器,返回sp为特殊寄存器首地址。
OS任务切换通过PendSV中断实现,在ESP8266在port.c文件实现了PendSV()函数。我们知道中断函数是无法传递参数的,显然该函数不是中断函数。
void PendSV( char req )
{
char tmp=0;
if( NMIIrqIsOn == 0 )
{
vPortEnterCritical();
tmp = 1;
}
if(req ==1)
{
SWReq = 1;
}
else if(req ==2)
HdlMacSig= 1;
if(PendSvIsPosted == 0)
{
PendSvIsPosted = 1;
xthal_set_intset(1<
在portmacro.h中定义了2个宏函数
#define portYIELD() PendSV(1)
#define HDL_MAC_SIG_IN_LV1_ISR() PendSV(2)
在FreeRTOS的API中,调用portYIELD()将会触发一次强制任务切换。而HDL_MAC_SIG_IN_LV1_ISR()函数,经过查对,并没有在任何文件中有调用,但在libmain.a和libpp.a中都有调用PendSV函数。故推测PendSV(2)可能是与WiFi相关的中断调用。
在PendSV()函数内,通过传入参数req设置不同的标志。并通过ETS_SOFT_INUM使能软中断。在软中断处理函数内,判断调用PendSV()不同类型,如为HalMacSig则调用函数MacIsrSigPostDefHdl(),返回值为高优先级使能标志。如需要做任务切换,调用_xt_timer_int1()
,该函数在libmain.a中实现,应该是触发真正的PendSV中断做任务切换。
开关中断是任何一个OS都需要实现的接口。通常为了加快开关中断的速度,会使用宏函数来实现这部分代码。
ESP8266开关中断函数在“portmacro.h”中实现。
/* Disable interrupts, saving previous state in cpu_sr */
#define portDISABLE_INTERRUPTS() \
__asm__ volatile ("rsil %0, " XTSTR(XCHAL_EXCM_LEVEL) : "=a" (cpu_sr) :: "memory")
/* Restore interrupts to previous level saved in cpu_sr */
#define portENABLE_INTERRUPTS() __asm__ volatile ("wsr %0, ps" :: "a" (cpu_sr) : "memory")
portDISABLE_INTERRUPTS()为关中断,portENABLE_INTERRUPTS()为开中断。这2个函数是gcc内嵌汇编实现的。
通过查阅gcc关于内嵌汇编语法可以获知,“__asm__”表示后面的代码为内嵌汇编,“__volatile”表示后面的代码不需要编译器优化,括号内的为汇编指令。
FreeRTOS通过调用vPortEnterCritical()进入临界区,调用vPortExitCritical()退出临界区。这2个函数不同芯片核实现方式不同,ESP8266在port.c内实现。
void vPortEnterCritical( void )
{
if(NMIIrqIsOn == 0)
{
if( ClosedLv1Isr !=1 )
{
portDISABLE_INTERRUPTS();
ClosedLv1Isr = 1;
}
uxCriticalNesting++;
}
}
void vPortExitCritical( void )
{
if(NMIIrqIsOn == 0)
{
if(uxCriticalNesting > 0)
{
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
if( ClosedLv1Isr ==1 )
{
ClosedLv1Isr = 0;
portENABLE_INTERRUPTS();
}
}
}
else
{
ets_printf("E:C:%d\n",uxCriticalNesting);
PORT_ASSERT((uxCriticalNesting>0));
}
}
}
这2个函数同样对NMI中断标志进行了判断,只有在未进入NMI中断情况下才能进入和退出临界区。然后调用portDISABLE_INTERRUPTS()和portENABLE_INTERRUPTS(),同时对开关中断次数使用ClosedLv1Isr标志进行了保护。
FreeRTOS使用uxCriticalNesting静态全局变量管理进入/退出临界区。默认为0,进入临界区后+1,退出临界区-1。uxCriticalNesting为0时调用portENABLE_INTERRUPTS()使能中断,恢复任务调度。
开发者通过调用vTaskStartScheduler()启动FreeRTOS调度器,vTaskStartScheduler()函数内又调用xPortStartScheduler(),该函数不同芯片核实现方式不同,ESP8266在port.c内实现。
portBASE_TYPE ICACHE_FLASH_ATTR xPortStartScheduler( void )
{
//set pendsv and systemtick as lowest priority ISR.
//pendsv setting
/*******software isr*********/
_xt_isr_attach(ETS_SOFT_INUM, SoftIsrHdl, NULL);
_xt_isr_unmask(1<
在xPortStartScheduler()中,通过_xt_isr_attach()设置软中断处理回调函数,同时初始化pendsv和SysTick中断,并调用vTaskSwitchContext()查询并切换到最高优先级任务。
XT_RTOS_INT_EXIT()是一个宏定义,定义在xtensa_rtos.h文件内。该函数在库文件libmain.a内实现,我们无法获知具体实现细节。根据注释可以得知,该函数完成中断使能,并使最高优先级任务启动。
ESP8266的RAM总共有160KB,分为IRAM和DRAM。
IRAM空间为64KB,前32KB用为IRAM,用来存放没有加ICACHE_FLASH_ATTR的代码,即.text段,会通过ROM code或二级boot从SPI FLASH中的bin中加载到IRAM中。后32KB被映射作为iCache,放在SPI Flash中的加了ICACHE_FLASH_ATTR的代码会被从SPI Flash自动动态加载到iCache。
DRAM空间为96KB,ESP8266_RTOS_SDK将96KB用来存放.data/.bss/rodata/heap,heap区的大小取决于.data/.bss/.rodata的大小。
在FreeRTOS中,采用的是heap4.c动态内存管理方式。ESP8266的heap起始地址通过外部变量_heap_start获取。在首次调用pvPortMalloc()中通过prvHeapInit()初始化Heap中被使用。
在user_init()中调用函数system_print_meminfo()可以获取当前系统内存相关信息:
data : 0x3ffe8000 ~ 0x3ffe884e, len: 2126
rodata: 0x3ffe8850 ~ 0x3ffe8aa8, len: 600
bss : 0x3ffe8aa8 ~ 0x3ffef4a0, len: 27128
heap : 0x3ffef4a0 ~ 0x40000000, len: 68448
从以上打印信息可以看到,ESP8266的DRAM地址范围是0x3ffe8000~0x40000000,合计为96KB。同时也可以通过system_get_free_heap_size()获取当前剩余堆空间大小。
在task.c源文件中日志添加打印printf("pcName:%s usStackDepth:%d uxPriority:%d\n",
pcName, usStackDepth, uxPriority);可以获知,ESP8266在启动时,SDK总共创建了7个任务。
pcName:ppT usStackDepth:512 uxPriority:13
pcName:pmT usStackDepth:256 uxPriority:1
pcName:tiT usStackDepth:512 uxPriority:10
pcName:uiT usStackDepth:640 uxPriority:14
pcName:IDLE usStackDepth:384 uxPriority:0
pcName:Tmr Svc usStackDepth:512 uxPriority:2
pcName:rtT usStackDepth:512 uxPriority:12
其中有2个任务是FreeRTOS内核创建的,“Tmr svc”为软件定时器任务,优先级为2,“IDLE”为idle任务,优先级最低为0。
其他任务为SDK创建,“uiT”为watchdog任务,优先级最高为14;“ppT”优先级为13;“rtT”为高精度timer,任务优先级为12;“tiT”为TCP/IP任务,优先级为10。
用户在开发应用代码时创建的任务不能高于SDK创建任务的优先级,优先级设置范围为1-9。