OSTaskStkInit():任務堆棧結構的初始化
OSTaskCreate()和OSTaskCreateExt()通過調用OSTaskStkInit(),初始化任務的棧結構。因此,堆棧看起來就像中斷剛發生過一樣,所有寄存器都儲存在堆棧中。OSTaskStkInit()的示意性代碼如下所示。
OSTaskStkInit()的示意性代碼L:
OS_STK *OSTaskStkInit (void(*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt)
{
類比帶參數(pdata)的函數調用; // (1)
類比ISR向量; //(2)
按照預先設計的寄存器值初始化堆棧結構; // (3)
返回棧頂指標給調用該函數的函數; // (4)
}
下圖顯示了OSTaskStkInit()在建立任務時,任務棧應該初始化成何種形式。
注意:在這裡假定堆棧是從上往下遞減的。下面的討論同樣適用於以相反方向從下往上遞增的堆棧結構。
以下的程式清單給出了OSTaskCreate(),OSTaskCreateExt()及OSTaskStkInit()的函數原型,其中參數task,pdata,ptos及opt是調用OSTaskStkInit()函數時須傳遞過去的參數。因為只有OSTaskCreate()函數不支援附加的opt選項,因此,當OSTaskCreate()調用OSTaskStkInit()時,將opt設定為0x0000。
程式清單:
INT8U OSTaskCreate(void (*task)(void*pd), void *pdata, OS_STK *ptos, INT8U prio);
INT8U OSTaskCreateExt(void (*task)(void*pd), void *pdata, OS_STK *ptos, INT8U prio, INT16U id, OS_STK*pbos, INT32U stk_size, void *pext, INT16U opt);
OS_STK *OSTaskStkInit(void (*task)(void*pd), void *pdata, OS_STK *ptos, INT16U opt);
回顧一下,任務可以是一個無限的迴圈,也可以在一次執行完畢後被刪除掉。這裡要注意的是,任務代碼並不是被真正地刪除了,而只是uC/OS-II不再理會該任務代碼,所以該任務代碼不會再運行。任務看起來與任何C函數一樣,具有一個傳回型別和一個參數,只是它決不返回。任務的傳回型別必須定義成void型。
也就是說,在uC/OS-II中,任務是一個無限的迴圈,其他部分看起來與別的C函數沒有什麼不同。
當uC/OS-II讓任務開始執行時,任務就會收到一個參數,好像是被其他任務調用了,如以下代碼所列。
任務代碼:
void MyTask(void *pdata)
{
for(;;){
}
}
如果是從其他函數中調用MyTask(),那麼C編譯器就會先將調用MyTask()函數的返回地址儲存到堆棧中,再將參數儲存到堆棧中。OSTaskStkInit()需要模仿編譯器的這種動作。實際上有些編譯器會將pdata參數放在一個或多個寄存器中傳遞,後面會討論這類情況。
F(1)
L(1)假定pdata會被編譯器儲存到堆棧中,OSTaskStkInit()就會模仿編譯器的這種行為,將pdata儲存到堆棧中。
F(2)
L(1)與C函數的調用不一樣的是,調用者的返回地址是未知的,因為該函數根本就沒有被調用,只是為了建立一個任務的堆棧結構,好像這段任務代碼被調用過一樣。OSTaskStkInit()函數僅僅知道任務的起始地址(是作為參數傳遞過來的)。換言之,也不需要知道返回地址,因為任務並不再返回。
F(3)
L(2)這時,OSTaskStkInit()需將處理器的寄存器儲存到堆棧中。當處理器識別並開始執行中斷時,它會自動地完成該過程。一些處理器會將所有的寄存器都推入堆棧,而另一些處理器只將部分寄存器推入堆棧。一般而言,處理器至少需將程式計數器的值(中斷返回地址)和處理器的狀態字存入堆棧。很明顯,處理器是按一定的順序將寄存器推入堆棧的,而使用者在將寄存器推入堆棧時,也就必須依照這一順序。
F(4)
L(3)OSTaskStkInit()需要將剩下的處理器寄存器儲存到堆棧中。入棧的順序取決於處理器是否允許按不同順序操作。有些處理器用一條指令就可以一次將全部寄存器都儲存起來,有些則要幾條指令。必須嚴格按照處理器要求的入棧順序完成這一過程。例如,Intel80x86有PUSHA指令,可將8個寄存器推入堆棧;對Motorola68HC11處理器而言,在中斷響應期間,所有的寄存器都會按一定順序自動地儲存到堆棧中。所以在初始化堆棧時,也必須符合這一順序。
F(5)
L(4)在初始化堆棧以後,OSTaskStkInit()應當返回堆棧指標所指向的地址。OSTaskCreate()或OSTaskCreateExt()得到這個地址,並且儲存在任務控制塊中。處理器的文檔應該告訴使用者,堆棧指標是指向下一個可以使用的堆棧空間,還是指向上次入棧的資料。例如80x86就是指向上次儲存的資料,而Motorola的68HC11處理器是指向下一個可以使用的棧空間。
現在是討論這個問題的時候了:如果使用者的C編譯器用寄存器傳遞pdata參數,而不使用堆棧傳遞參數,該怎麼辦。請看下圖。
F(1)
L(1)同上一種情況類似,OSTaskStkInit()模仿調用應用程式任務代碼的情況,將任務起始地址儲存在堆棧中。
F(2)
L(2)同樣地,OSTaskStkInit()將寄存器儲存到堆棧中。當處理器識別並開始執行中斷時,自動地將寄存器推入堆棧中。一些處理器會將所有的寄存器都推入堆棧,而另外一些處理器只將部分寄存器推入堆棧。一般而言,處理器至少需將程式計數器的值(中斷返回地址)和處理器的狀態字存入堆棧。很明顯,處理器是按一定的順序將寄存器推入堆棧的,而使用者在將寄存器推入堆棧時,也必須嚴格依照同樣的順序。
F(3)
L(3)OSTaskStkInit()會將其餘的處理器寄存器儲存到堆棧中。入棧的順序取決於處理器是否允許按不同順序操作。有些處理器可以使用一條指令將很多寄存器推入堆棧,有些則需要幾條指令。這時,應該類比出這些指令。因為編譯器是使用寄存器傳遞參數的(至少部分參數是通過寄存器傳遞的),所以應從相應文檔中弄清楚pdata是通過哪個寄存器傳遞的,並且將該寄存器也推入堆棧。
F(4)
L(4)在初始化堆棧後,OSTaskStkInit()函數應當返回堆棧指標所指向的地址。OSTaskCreate()或OSTaskCreateExt()得到這個地址並且儲存在任務控制塊中。再次強調,應該清楚堆棧指標是指向下一個空的堆棧空間,還是指向上次推入堆棧的值。
總結:OSTaskStkInit()函數由OSTaskCreate()或OSTaskCreateExt()調用,需要傳遞的參數是任務代碼的起始地址、參數指標(pdata)、任務堆棧頂端的地址和附加的opt選項,用來初始化任務的堆棧,初始狀態的堆棧類比發生一次中斷後的堆棧結構。堆棧初始化工作結束後,OSTaskStkInit()返回新的堆棧棧頂指標,OSTaskCreate()或OSTaskCreateExt()將指標儲存在任務的OS_TCB中。也就是說,調用OSTaskStkInit()給任務做一個初始的任務上下文堆棧。
uC/OS-II在80x86上的移植(OSTaskStkInit()部分)
下圖說明了OSTaskStkInit()初始化後的堆棧內容。請注意,圖中的堆棧結構不是調用OSTaskStkInit()任務之後的,而是新建立任務之後的。
以下為OSTaskStkInit()的程式清單:
|
程式清單L OSTaskStkInit()
void *OSTaskStkInit (void (*task)(void*pd), void *pdata, void *ptos, INT16U opt)
{
INT16U*stk;
opt= opt;
stk= (INT16U *)ptos;
*stk--= (INT16U)FP_SEG(pdata);
*stk--= (INT16U)FP_OFF(pdata);
*stk--= (INT16U)FP_SEG(task);
*stk--= (INT16U)FP_OFF(task);
*stk--= (INT16U)0x0202;
*stk--= (INT16U)FP_SEG(task);
*stk--= (INT16U)FP_OFF(task);
*stk--= (INT16U)0xAAAA;
*stk--= (INT16U)0xCCCC;
*stk--= (INT16U)0xDDDD;
*stk--= (INT16U)0xBBBB;
*stk--= (INT16U)0x0000;
*stk--= (INT16U)0x1111;
*stk--= (INT16U)0x2222;
*stk--= (INT16U)0x3333;
*stk--= (INT16U)0x4444;
*stk= _DS;
return((OS_STK *)stk);
}
|
L(1)
由於80x86堆棧是16位寬的(以字為單位),OSTaskStkInit()將建立一個指向以字為單位記憶體地區的指標,同時要求堆棧指標指向空堆棧的頂端。
L(2)
由於筆者使用的BorlandC/C++編譯器配置為用堆棧而不是寄存器來傳送參數pdata,所以此時參數pdata的段地址和位移量都將被儲存在堆棧中。
L(3)
堆棧中緊接著是任務函數的起始地址,理論上,此處應該為任務的返回地址,但在uC/OS-II中,任務函數必須為無限迴圈結構,不能有返回點。
L(4)
返回地址下面是狀態字(SW),設定狀態字也是為了類比中斷髮生後的堆棧結構。堆棧中的SW初始化為0x0202,這將使任務啟動後允許中斷髮生;如果設為0x0002,則任務啟動後將禁止中斷。需要注意的是,如果選擇任務啟動後允許中斷髮生,則所有的任務運行期間中斷都允許;同樣,如果選擇任務啟動後禁止中斷,則所有的任務都禁止中斷髮生,而不能有所選擇。
如果確實需要突破上述限制,可以通過參數pdata向任務傳遞希望實現的中斷狀態。如果某個任務選擇啟動後禁止中斷,那麼其他的任務在啟動並執行時候需要重新開啟中斷。同時還要修改OS_TaskIdle()和OS_TaskStat()函數,在運行時開啟中斷。如果以上任何一個環節出現問題,系統就會崩潰。所以筆者還是推薦設定SW為0x0202,在任務啟動時開啟中斷。
L(5)
堆棧中還要留出各個寄存器的空間,注意寄存器在堆棧中的位置要和運行指令PUSHA,PUSH ES和PUSHDS的壓入堆棧的次序相同。上述指令在每次進入中斷服務程式時都會調用。
AX,BX,CX,DX,SP,BP,SI和DI的次序是和指令PUSHA的壓棧次序相同的。如果使用沒有PUSHA指令的80x86處理器,就要使用多個PUSH指令壓入上述寄存器,且順序要與PUSHA相同。在程式清單中每個寄存器被初始化為不同的值,這是為了調試方便。
L(6)
Borland編譯器支援偽寄存器變數操作,可以用_DS關鍵字取得CPUDS寄存器的值,程式清單中的標記處是用_DS直接把DS寄存器拷貝到堆棧中。
堆棧初始化工作結束後,OSTaskStkInit()返回新的堆棧棧頂指標,OSTaskCreate()或OSTaskCreateExt()將指標儲存在任務的OS_TCB中。
另外注意:OSStartHighRdy()將永遠不返回到OSStart(),因為OSStartHighRdy()被OSStart()調用的函數返回地址以及相關的上下文環境都沒有被儲存。