ESP8266_RTOS_SDK学习笔记之 FreeRTOS移植浅析

ESP8266原厂提供了Non-OSRTOS版本的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-FiSmartConfigSniffer、系统、定时器、FOTA和外围驱动相关接口。

目前原厂上海乐鑫已将RTOSSDK(ESP8266_RTOS_SDK)开源,同时ESP8266_RTOS_SDK内用的到很多第三方开源代码也开放在github上,github地址为:https://github.com/espressif/ESP8266_RTOS_SDKgithub更新记录显示,原厂持续对SDK进行更新升级中。

第三方开源代码位于ESP8266_RTOS_SDKthird-party目录下,已开源的包括嵌入式实时操作系统(FreeRTOS)TCP/IP协议栈(LwIP)SPI文件系统(spiffs)SSL的多种不同实现(mbedtlsopensslssl)WebSocket(nopoll)cjsonespconn等。

SDKlib目录下,有以上各开源代码编译完成的lib库文件,在third-party目录下执行下sh make_all_lib.sh命令,即可编译全部lib库文件至lib目录,也可单独更新指定lib库文件。

今天学习的是ESP8266上自带的嵌入式操作系统FreeRTOS,通过FreeRTOS运行分析,学习ESP8266RTOS移植、启动、任务切换、内存管理等细节。

一、FreeRTOS源码

ESP8266_Free_RTOS使用的FreeRTOS版本为7.5.2,包括以下文件:

  1. croutine.c:协程功能的实现;非必须包含文件
  2. heap_4.c:内存管理实现;有多种内存管理方式可选,heap1.cheap4.c实现方式不同
  3. list.c:链表管理实现;该文件为必须包含文件
  4. port.cFreeRTOS移植层代码;ESP8266底层移植实现文件
  5. queue.c:与队列相关实现;该文件为必须包含文件
  6. tasks.c:与任务相关实现;该文件为必须包含文件
  7. timers.c:软件定时器的实现;该文件非必须包含文件,但如需使用软件定时器就需包含。

通过与FreeRTOS官方同一版本源码逐一比对,可以发现ESP8266FreeRTOS源码修改基于以下几点:

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中运行;由于ESP8266RAM空间有限,所有无法将所有代码一次性加载到IRAM中运行。故在大部分函数前添加“ICACHE_FLASH_ATTR”宏,将其放至IROM中。

但是ESP8266_RTOS_SDK,函数默认存放在IROM中,中断处理函数也可以定义在IROM中,所以无需特意添加“ICACHE_FLASH_ATTR”宏。如开发者需要将一些频繁调用的函数指定在IRAM中,应在函数前添加“IRAM_ATTR”宏。

3. 加入了memleak内存泄漏检测工具,将源码中全部的pvPortMalloc/vPortFreet修改为os_mallocos_free

对于ESP8266_RTOS_SDK目前不支持memleak检测内存泄漏。

4. heap4.c针对heap分配起始地址和大小进行了修改,修改为编译器自动获取。

二、FreeRTOS移植分析

FreeRTOS移植适配包括系统节拍中断,任务栈初始化、开关中断、任务切换、调度器启动等,ESP8266是在port.cportmarco.h内实现的。

1. SysTick中断

操作系统的运行是由系统节拍时钟来驱动的,系统的延时和阻塞时钟都是以系统节拍时钟周期为单位。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()做一次任务切换。任务切换将在下一节中讲解。

2. 任务栈初始化

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为特殊寄存器首地址。

3. 任务切换

OS任务切换通过PendSV中断实现,在ESP8266port.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)

FreeRTOSAPI中,调用portYIELD()将会触发一次强制任务切换。而HDL_MAC_SIG_IN_LV1_ISR()函数,经过查对,并没有在任何文件中有调用,但在libmain.alibpp.a中都有调用PendSV函数。故推测PendSV(2)可能是与WiFi相关的中断调用。

PendSV()函数内,通过传入参数req设置不同的标志。并通过ETS_SOFT_INUM使能软中断。在软中断处理函数内,判断调用PendSV()不同类型,如为HalMacSig则调用函数MacIsrSigPostDefHdl(),返回值为高优先级使能标志。如需要做任务切换,调用_xt_timer_int1()

,该函数在libmain.a中实现,应该是触发真正的PendSV中断做任务切换。

4. 开关中断

开关中断是任何一个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”表示后面的代码不需要编译器优化,括号内的为汇编指令。

5. 临界区

FreeRTOS通过调用vPortEnterCritical()进入临界区,调用vPortExitCritical()退出临界区。这2个函数不同芯片核实现方式不同,ESP8266port.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,退出临界区-1uxCriticalNesting0时调用portENABLE_INTERRUPTS()使能中断,恢复任务调度。

6. 调度器启动

开发者通过调用vTaskStartScheduler()启动FreeRTOS调度器,vTaskStartScheduler()函数内又调用xPortStartScheduler(),该函数不同芯片核实现方式不同,ESP8266port.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()设置软中断处理回调函数,同时初始化pendsvSysTick中断,并调用vTaskSwitchContext()查询并切换到最高优先级任务。

XT_RTOS_INT_EXIT()是一个宏定义,定义在xtensa_rtos.h文件内。该函数在库文件libmain.a内实现,我们无法获知具体实现细节。根据注释可以得知,该函数完成中断使能,并使最高优先级任务启动。

三、内存管理

ESP8266RAM总共有160KB,分为IRAMDRAM

IRAM空间为64KB,前32KB用为IRAM,用来存放没有加ICACHE_FLASH_ATTR的代码,即.text段,会通过ROM code或二级bootSPI FLASH中的bin中加载到IRAM中。后32KB被映射作为iCache,放在SPI Flash中的加了ICACHE_FLASH_ATTR的代码会被从SPI Flash自动动态加载到iCache

DRAM空间为96KBESP8266_RTOS_SDK96KB用来存放.data/.bss/rodata/heapheap区的大小取决于.data/.bss/.rodata的大小。

FreeRTOS中,采用的是heap4.c动态内存管理方式。ESP8266heap起始地址通过外部变量_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

从以上打印信息可以看到,ESP8266DRAM地址范围是0x3ffe8000~0x40000000,合计为96KB。同时也可以通过system_get_free_heap_size()获取当前剩余堆空间大小。

四、FreeRTOS运行

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”为软件定时器任务,优先级为2IDLE”为idle任务,优先级最低为0

其他任务为SDK创建,“uiT”为watchdog任务,优先级最高为14;“ppT”优先级为13;“rtT”为高精度timer,任务优先级为12;“tiT”为TCP/IP任务,优先级为10

用户在开发应用代码时创建的任务不能高于SDK创建任务的优先级,优先级设置范围为1-9


你可能感兴趣的:(FreeRTOS,Esp8266)