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移植、啓動、任務切換、內存管理等細節。
一、FreeRTOS源碼
ESP8266_Free_RTOS使用的FreeRTOS版本爲7.5.2,包括以下文件:
- croutine.c:協程功能的實現;非必須包含文件
- heap_4.c:內存管理實現;有多種內存管理方式可選,heap1.c至heap4.c實現方式不同
- list.c:鏈表管理實現;該文件爲必須包含文件
- port.c:FreeRTOS移植層代碼;ESP8266底層移植實現文件
- queue.c:與隊列相關實現;該文件爲必須包含文件
- tasks.c:與任務相關實現;該文件爲必須包含文件
- timers.c:軟件定時器的實現;該文件非必須包含文件,但如需使用軟件定時器就需包含。
通過與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移植分析
FreeRTOS移植適配包括系統節拍中斷,任務棧初始化、開關中斷、任務切換、調度器啓動等,ESP8266是在port.c和portmarco.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中斷實現,在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<<ETS_SOFT_INUM);
}
if(tmp == 1)
vPortExitCritical();
}
在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中斷做任務切換。
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個函數不同芯片覈實現方式不同,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()使能中斷,恢復任務調度。
6. 調度器啓動
開發者通過調用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<<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()設置軟中斷處理回調函數,同時初始化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()獲取當前剩餘堆空間大小。
四、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”爲軟件定時器任務,優先級爲2,“IDLE”爲idle任務,優先級最低爲0。
其他任務爲SDK創建,“uiT”爲watchdog任務,優先級最高爲14;“ppT”優先級爲13;“rtT”爲高精度timer,任務優先級爲12;“tiT”爲TCP/IP任務,優先級爲10。
用戶在開發應用代碼時創建的任務不能高於SDK創建任務的優先級,優先級設置範圍爲1-9。