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<<ETS_SOFT_INUM);
	}

	if(tmp == 1)
		vPortExitCritical();
}

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<<ETS_SOFT_INUM);

    /* Initialize system tick timer interrupt and schedule the first tick. */
    _xt_tick_timer_init();

    vTaskSwitchContext();

	XT_RTOS_INT_EXIT();

	/* Should not get here as the tasks are now running! */
return pdTRUE;	
}

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章