在51系列單片機上移植uCOS-II

admin @ 2014-03-25 , reply:0

內容摘要:本文詳細系統地介紹了uC/OS-II在51單片機上的移植、重入實現方法、硬體模擬、固化、人機界面等關鍵內容。

引言:隨著各種應用電子系統的複雜化和系統實時性需求的提高,並伴隨應用軟體朝著系統化方向發展的加速,在16位/32位單片機中廣泛使用了嵌入式實時操作系統。然而實際使用中卻存在著大量8位單片機,從經濟性考慮,對某些應用場合,在8位MCU上使用操作系統是可行的。從學習操作系統角度,uC/OS- II for 51即簡單又全面,學習成本低廉,值得推廣。

結語:μC/OS-II具有免費、簡單、可靠性高、實時性好等優點,但也有缺乏便利開發環境等缺點,尤其不像商用嵌入式系統那樣得到廣泛使用和持續的研究更新。但開放性又使得開發人員可以自行裁減和添加所需的功能,在許多應用領域發揮著獨特的作用。當然,是否在單片機系統中嵌入μC/OS-II應視所開發的項目而定,對於一些簡單的、低成本的項目來說,就沒必要使用嵌入式操作系統了。

uC/OS-II原理:
         uCOSII 包括任務調度、時間管理、內存管理、資源管理(信號量、郵箱、消息隊列)四大部分,沒有文件系統、網路介面、輸入輸出界面。它的移植只與4個文件相關:彙編文件(OS_CPU_A.ASM)、處理器相關C文件(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有64個優先順序,系統佔用8個,用戶可創建56個任務,不支持時間片輪轉。它的基本思路就是 “近似地每時每刻總是讓優先順序最高的就緒任務處於運行狀態” 。為了保證這一點,它在調用系統API函數、中斷結束、定時中斷結束時總是執行調度演算法。原作者通過事先計算好數據,簡化了運算量,通過精心設計就緒表結構,使得延時可預知。任務的切換是通過模擬一次中斷實現的。
        uCOSII工作核心原理是:近似地讓最高優先順序的就緒任務處於運行狀態。
        操作系統將在下面情況中進行任務調度:調用API函數(用戶主動調用),中斷(系統佔用的時間片中斷OsTimeTick(),用戶使用的中斷)。
調度演算法書上講得很清楚,我主要講一下整體思路。
(1) 在調用API函數時,有可能引起阻塞,如果系統API函數察覺到運行條件不滿足,需要切換就調用OSSched()調度函數,這個過程是系統自動完成的,用戶沒有參與。OSSched()判斷是否切換,如果需要切換,則此函數調用OS_TASK_SW()。這個函數模擬一次中斷(在51里沒有軟中斷,我用子程序調用模擬,效果相同),好象程序被中斷打斷了,其實是OS故意製造的假象,目的是為了任務切換。既然是中斷,那麼返回地址(即緊鄰 OS_TASK_SW()的下一條彙編指令的PC地址)就被自動壓入堆棧,接著在中斷程序里保存CPU寄存器(PUSHALL)……。堆棧結構不是任意的,而是嚴格按照uCOSII規範處理。OS每次切換都會保存和恢復全部現場信息(POPALL),然後用RETI回到任務斷點繼續執行。這個斷點就是 OSSched()函數里的緊鄰OS_TASK_SW()的下一條彙編指令的PC地址。切換的整個過程就是,用戶任務程序調用系統API函數,API調用 OSSched(),OSSched()調用軟中斷OS_TASK_SW()即OSCtxSw,返回地址(PC值)壓棧,進入OSCtxSw中斷處理子程序內部。反之,切換程序調用RETI返回緊鄰OS_TASK_SW()的下一條彙編指令的PC地址,進而返回OSSched()下一句,再返回API下一句,即用戶程序斷點。因此,如果任務從運行到就緒再到運行,它是從調度前的斷點處運行。
(2)中斷會引發條件變化,在退出前必須進行任務調度。 uCOSII要求中斷的堆棧結構符合規範,以便正確協調中斷退出和任務切換。前面已經說到任務切換實際是模擬一次中斷事件,而在真正的中斷里省去了模擬 (本身就是中斷嘛)。只要規定中斷堆棧結構和uCOSII模擬的堆棧結構一樣,就能保證在中斷里進行正確的切換。任務切換髮生在中斷退出前,此時還沒有返回中斷斷點。仔細觀察中斷程序和切換程序最後兩句,它們是一模一樣的,POPALL+RETI。即要麼直接從中斷程序退出,返回斷點;要麼先保存現場到 TCB,等到恢復現場時再從切換函數返回原來的中斷斷點(由於中斷和切換函數遵循共同的堆棧結構,所以退出操作相同,效果也相同)。用戶編寫的中斷子程序必須按照uCOSII規範書寫。任務調度發生在中斷退出前,是非常及時的,不會等到下一時間片才處理。OSIntCtxSw()函數對堆棧指針做了簡單調整,以保證所有掛起任務的棧結構看起來是一樣的。
(3)在uCOSII里,任務必須寫成兩種形式之一(《uCOSII中文版》p99頁)。在有些 RTOS開發環境里沒有要求顯式調用OSTaskDel(),這是因為開發環境自動做了處理,實際原理都是一樣的。uCOSII的開發依賴於編譯器,目前沒有專用開發環境,所以出現這些不便之處是可以理解的。
移植過程:
(1)拷貝書後附贈光碟sourcecode目錄下的內容到C:\YY下,刪除不必要的文件和EX1L.C,只剩下p187(《uCOSII》)上列出的文件。
(2)改寫最簡單的OS_CPU.H
數據類型的設定見C51.PDF第176頁。注意BOOLEAN要定義成unsigned char 類型,因為bit類型為C51特有,不能用在結構體里。
EA=0關中斷;EA=1開中斷。這樣定義即減少了程序行數,又避免了退出臨界區后關中斷造成的死機。
MCS-51堆棧從下往上增長(1=向下,0=向上),OS_STK_GROWTH定義為0
#define OS_TASK_SW() OSCtxSw() 因為MCS-51沒有軟中斷指令,所以用程序調用代替。兩者的堆棧格式相同,RETI指令複位中斷系統,RET則沒有。實踐表明,對於MCS-51,用子程序調用入棧,用中斷返回指令RETI出棧是沒有問題的,反之中斷入棧RET出棧則不行。總之,對於入棧,子程序調用與中斷調用效果是一樣的,可以混用。在沒有中斷髮生的情況下複位中斷系統也不會影響系統正常運行。詳見《uC/OS-II》第八章193頁第12行
(3)改寫OS_CPU_C.C
我設計的堆棧結構如下圖所示:

TCB結構體中OSTCBStkPtr總是指向用戶堆棧最低地址,該地址空間內存放用戶堆棧長度,其上空間存放系統堆棧映像,即:用戶堆棧空間大小=系統堆棧空間大小+1。
SP總是先加1再存數據,因此,SP初始時指向系統堆棧起始地址(OSStack)減1處(OSStkStart)。很明顯系統堆棧存儲空間大小=SP-OSStkStart。
任務切換時,先保存當前任務堆棧內容。方法是:用SP-OSStkStart得出保存位元組數,將其寫入用戶堆棧最低地址內,以用戶堆棧最低地址為起址,以 OSStkStart為系統堆棧起址,由系統棧向用戶棧拷貝數據,循環SP-OSStkStart次,每次拷貝前先將各自棧指針增1。
其次,恢復最高優先順序任務系統堆棧。方法是:獲得最高優先順序任務用戶堆棧最低地址,從中取出“長度”,以最高優先順序任務用戶堆棧最低地址為起址,以 OSStkStart為系統堆棧起址,由用戶棧向系統棧拷貝數據,循環“長度”數值指示的次數,每次拷貝前先將各自棧指針增1。
用戶堆棧初始化時從下向上依次保存:用戶堆棧長度(15),PCL,PCH,PSW,ACC,B,DPL,DPH,R0,R1,R2,R3,R4,R5,R6,R7。不保存SP,任務切換時根據用戶堆棧長度計算得出。
OSTaskStkInit函數總是返回用戶棧最低地址。
操作系統tick時鐘我使用了51單片機的T0定時器,它的初始化代碼用C寫在了本文件中。
最後還有幾點必須注意的事項。本來原則上我們不用修改與處理器無關的代碼,但是由於KEIL編譯器的特殊性,這些代碼仍要多處改動。因為KEIL預設情況下編譯的代碼不可重入,而多任務系統要求併發操作導致重入,所以要在每個C函數及其聲明后標註reentrant關鍵字。另外,“pdata”、 “data”在uCOS中用做一些函數的形參,但它同時又是KEIL的關鍵字,會導致編譯錯誤,我通過把“pdata”改成“ppdata”, “data”改成“ddata”解決了此問題。OSTCBCur、OSTCBHighRdy、OSRunning、OSPrioCur、 OSPrioHighRdy這幾個變數在彙編程序中用到了,為了使用Ri訪問而不用DPTR,應該用KEIL擴展關鍵字IDATA將它們定義在內部RAM 中。
(4)重寫OS_CPU_A.ASM
A51宏彙編的大致結構如下:
NAME 模塊名 ;與文件名無關
;定義重定位段 必須按照C51格式定義,彙編遵守C51規範。段名格式為:?PR?函數名?模塊名
;聲明引用全局變數和外部子程序 注意關鍵字為“EXTRN”沒有‘E’
全局變數名直接引用
無參數/無寄存器參數函數 FUNC
帶寄存器參數函數 _FUNC
重入函數 _?FUNC
;分配堆棧空間
只關心大小,堆棧起點由keil決定,通過標號可以獲得keil分配的SP起點。切莫自己分配堆棧起點,只要用DS通知KEIL預留堆棧空間即可。
?STACK 段名與STARTUP.A51中的段名相同,這意味著KEIL在LINK時將把兩個同名段拼在一起,我預留了40H個位元組,STARTUP.A51預留了 1個位元組,LINK完成後堆棧段總長為41H。查看yy.m51知KEIL將堆棧起點定在21H,長度41H,處於內部RAM中。
;定義宏
宏名 MACRO 實體 ENDM
;子程序
OSStartHighRdy
OSCtxSw
OSIntCtxSw
OSTickISR
SerialISR
END ;聲明彙編源文件結束

一般指針佔3位元組。+0類型+1高8位數據+2低8位數據 詳見C51.PDF第178頁
低位地址存高8位值,高位地址存低8位值。例如0x1234,基址+0:0x12 基址+1:0x34

(5)移植串口驅動程序
在此之前我寫過基於中斷的串口驅動程序,包括列印位元組/字/長字/字元串,讀串口,初始化串口/緩衝區。把它改成重入函數即可直接使用。
系統提供的顯示函數是併發的,它不是直接顯示到串口,而是先輸出到顯存,用戶不必擔心IO慢速操作影響程序運行。串口輸入也採用了同樣的技術,他使得用戶在CPU忙於處理其他任務時照樣可以盲打輸入命令。
(6)編寫測試程序Demo(YY.C)
Demo程序創建了3個任務A、B、C優先順序分別為2、3、4,A每秒顯示一次,B每3秒顯示一次,C每6秒顯示一次。從顯示結果看,顯示3個A后顯示1個B,顯示6個A和2個B后顯示1個C,結果顯然正確。
顯示結果如下:
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
CCCCCC666666 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
CCCCCC666666 is active
Demo程序經Keil701編譯后,代碼量為7-8K,可直接在KeilC51上模擬運行。
編譯時要將OS_CPU_C.C、UCOS_II.C、OS_CPU_A.ASM、YY.C加入項目

文件名 : OS_CPU_A.ASM

$NOMOD51
EA BIT 0A8H.7
SP DATA 081H
B DATA 0F0H
ACC DATA 0E0H
DPH DATA 083H
DPL DATA 082H
PSW DATA 0D0H
TR0 BIT 088H.4
TH0 DATA 08CH
TL0 DATA 08AH

NAME OS_CPU_A ;模塊名

;定義重定位段
?PR?OSStartHighRdy?OS_CPU_A SEGMENT CODE
?PR?OSCtxSw?OS_CPU_A SEGMENT CODE
?PR?OSIntCtxSw?OS_CPU_A SEGMENT CODE
?PR?OSTickISR?OS_CPU_A SEGMENT CODE

?PR?_?serial?OS_CPU_A SEGMENT CODE

;聲明引用全局變數和外部子程序
EXTRN IDATA (OSTCBCur)
EXTRN IDATA (OSTCBHighRdy)
EXTRN IDATA (OSRunning)
EXTRN IDATA (OSPrioCur)
EXTRN IDATA (OSPrioHighRdy)

EXTRN CODE (_?OSTaskSwHook)
EXTRN CODE (_?serial)
EXTRN CODE (_?OSIntEnter)
EXTRN CODE (_?OSIntExit)
EXTRN CODE (_?OSTimeTick)

;對外聲明4個不可重入函數
PUBLIC OSStartHighRdy
PUBLIC OSCtxSw
PUBLIC OSIntCtxSw
PUBLIC OSTickISR

;PUBLIC SerialISR

;分配堆棧空間。只關心大小,堆棧起點由keil決定,通過標號可以獲得keil分配的SP起點。
?STACK SEGMENT IDATA
RSEG ?STACK
OSStack:
DS 40H
OSStkStart IDATA OSStack-1

;定義壓棧出棧宏
PUSHALL MACRO
PUSH PSW
PUSH ACC
PUSH B
PUSH DPL
PUSH DPH
MOV A,R0 ;R0-R7入棧
PUSH ACC
MOV A,R1
PUSH ACC
MOV A,R2
PUSH ACC
MOV A,R3
PUSH ACC
MOV A,R4
PUSH ACC
MOV A,R5
PUSH ACC
MOV A,R6
PUSH ACC
MOV A,R7
PUSH ACC
;PUSH SP ;不必保存SP,任務切換時由相應程序調整
ENDM

POPALL MACRO
;POP ACC ;不必保存SP,任務切換時由相應程序調整
POP ACC ;R0-R7出棧
MOV R7,A
POP ACC
MOV R6,A
POP ACC
MOV R5,A
POP ACC
MOV R4,A
POP ACC
MOV R3,A
POP ACC
MOV R2,A
POP ACC
MOV R1,A
POP ACC
MOV R0,A
POP DPH
POP DPL
POP B
POP ACC
POP PSW
ENDM

;子程序
;-------------------------------------------------------------------------
RSEG ?PR?OSStartHighRdy?OS_CPU_A
OSStartHighRdy:
USING 0 ;上電后51自動關中斷,此處不必用CLR EA指令,因為到此處還未開中斷,本程序退出后,開中斷。
LCALL _?OSTaskSwHook

OSCtxSw_in:

;OSTCBCur ===> DPTR 獲得當前TCB指針,詳見C51.PDF第178頁
MOV R0,#LOW (OSTCBCur) ;獲得OSTCBCur指針低地址,指針佔3位元組。+0類型+1高8位數據+2低8位數據
INC R0
MOV DPH,@R0 ;全局變數OSTCBCur在IDATA中
INC R0
MOV DPL,@R0

;OSTCBCur->OSTCBStkPtr ===> DPTR 獲得用戶堆棧指針
INC DPTR ;指針佔3位元組。+0類型+1高8位數據+2低8位數據
MOVX A,@DPTR ;.OSTCBStkPtr是void指針
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1

;*UserStkPtr ===> R5 用戶堆棧起始地址內容(即用戶堆棧長度放在此處) 詳見文檔說明 指針用法詳見C51.PDF第178頁
MOVX A,@DPTR ;用戶堆棧中是unsigned char類型數據
MOV R5,A ;R5=用戶堆棧長度

;恢復現場堆棧內容
MOV R0,#OSStkStart

restore_stack:

INC DPTR
INC R0
MOVX A,@DPTR
MOV @R0,A
DJNZ R5,restore_stack

;恢復堆棧指針SP
MOV SP,R0

;OSRunning=TRUE
MOV R0,#LOW (OSRunning)
MOV @R0,#01

POPALL
SETB EA ;開中斷
RETI
;-------------------------------------------------------------------------
RSEG ?PR?OSCtxSw?OS_CPU_A
OSCtxSw:
PUSHALL

OSIntCtxSw_in:

;獲得堆棧長度和起址
MOV A,SP
CLR C
SUBB A,#OSStkStart
MOV R5,A ;獲得堆棧長度

;OSTCBCur ===> DPTR 獲得當前TCB指針,詳見C51.PDF第178頁
MOV R0,#LOW (OSTCBCur) ;獲得OSTCBCur指針低地址,指針佔3位元組。+0類型+1高8位數據+2低8位數據
INC R0
MOV DPH,@R0 ;全局變數OSTCBCur在IDATA中
INC R0
MOV DPL,@R0

;OSTCBCur->OSTCBStkPtr ===> DPTR 獲得用戶堆棧指針
INC DPTR ;指針佔3位元組。+0類型+1高8位數據+2低8位數據
MOVX A,@DPTR ;.OSTCBStkPtr是void指針
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1

;保存堆棧長度
MOV A,R5
MOVX @DPTR,A

MOV R0,#OSStkStart ;獲得堆棧起址
save_stack:

INC DPTR
INC R0
MOV A,@R0
MOVX @DPTR,A
DJNZ R5,save_stack

;調用用戶程序
LCALL _?OSTaskSwHook

;OSTCBCur = OSTCBHighRdy
MOV R0,#OSTCBCur
MOV R1,#OSTCBHighRdy
MOV A,@R1
MOV @R0,A
INC R0
INC R1
MOV A,@R1
MOV @R0,A
INC R0
INC R1
MOV A,@R1
MOV @R0,A

;OSPrioCur = OSPrioHighRdy 使用這兩個變數主要目的是為了使指針比較變為位元組比較,以便節省時間。
MOV R0,#OSPrioCur
MOV R1,#OSPrioHighRdy
MOV A,@R1
MOV @R0,A

LJMP OSCtxSw_in
;-------------------------------------------------------------------------
RSEG ?PR?OSIntCtxSw?OS_CPU_A

OSIntCtxSw:

;調整SP指針去掉在調用OSIntExit(),OSIntCtxSw()過程中壓入堆棧的多餘內容
;SP=SP-4

MOV A,SP
CLR C
SUBB A,#4
MOV SP,A

LJMP OSIntCtxSw_in
;-------------------------------------------------------------------------
CSEG AT 000BH ;OSTickISR
LJMP OSTickISR ;使用定時器0
RSEG ?PR?OSTickISR?OS_CPU_A

OSTickISR:

USING 0
PUSHALL

CLR TR0
MOV TH0,#70H ;定義Tick=50次/秒(即0.02秒/次)
MOV TL0,#00H ;OS_CPU_C.C 和 OS_TICKS_PER_SEC
SETB TR0

LCALL _?OSIntEnter
LCALL _?OSTimeTick
LCALL _?OSIntExit
POPALL
RETI
;-------------------------------------------------------------------------
CSEG AT 0023H ;串口中斷
LJMP SerialISR ;工作於系統態,無任務切換。
RSEG ?PR?_?serial?OS_CPU_A

SerialISR:

USING 0
PUSHALL
CLR EA
LCALL _?serial
SETB EA
POPALL
RETI
;-------------------------------------------------------------------------
END
;-------------------------------------------------------------------------

文件名 : OS_CPU_C.C

void *OSTaskStkInit (void (*task)(void *pd), void *ppdata, void *ptos, INT16U opt) reentrant
{
OS_STK *stk;

ppdata = ppdata;
opt = opt; //opt沒被用到,保留此語句防止告警產生
stk = (OS_STK *)ptos; //用戶堆棧最低有效地址
*stk++ = 15; //用戶堆棧長度
*stk++ = (INT16U)task & 0xFF; //任務地址低8位
*stk++ = (INT16U)task >> 8; //任務地址高8位
*stk++ = 0x00; //PSW
*stk++ = 0x0A; //ACC
*stk++ = 0x0B; //B
*stk++ = 0x00; //DPL
*stk++ = 0x00; //DPH
*stk++ = 0x00; //R0
*stk++ = 0x01; //R1
*stk++ = 0x02; //R2
*stk++ = 0x03; //R3
*stk++ = 0x04; //R4
*stk++ = 0x05; //R5
*stk++ = 0x06; //R6
*stk++ = 0x07; //R7
//不用保存SP,任務切換時根據用戶堆棧長度計算得出。
return ((void *)ptos);
}

#if OS_CPU_HOOKS_EN
void OSTaskCreateHook (OS_TCB *ptcb) reentrant
{
ptcb = ptcb; /* Prevent compiler warning */
}

void OSTaskDelHook (OS_TCB *ptcb) reentrant
{
ptcb = ptcb; /* Prevent compiler warning */
}

void OSTimeTickHook (void) reentrant
{
}
#endif

//初始化定時器0
void InitTimer0(void) reentrant
{
TMOD=TMOD&0xF0;
TMOD=TMOD|0x01; //模式1(16位定時器),僅受TR0控制
TH0=0x70; //定義Tick=50次/秒(即0.02秒/次)
TL0=0x00; //OS_CPU_A.ASM 和 OS_TICKS_PER_SEC
ET0=1; //允許T0中斷
TR0=1;
}

文件名 : YY.C

#include

#define MAX_STK_SIZE 64

void TaskStartyya(void *yydata) reentrant;
void TaskStartyyb(void *yydata) reentrant;
void TaskStartyyc(void *yydata) reentrant;

OS_STK TaskStartStkyya[MAX_STK_SIZE+1];//注意:我在ASM文件中設置?STACK空間為40H即64,不要超出範圍。
OS_STK TaskStartStkyyb[MAX_STK_SIZE+1];//用戶棧多一個位元組存長度
OS_STK TaskStartStkyyc[MAX_STK_SIZE+1];

void main(void)
{
OSInit();

InitTimer0();
InitSerial();
InitSerialBuffer();

OSTaskCreate(TaskStartyya, (void *)0, &TaskStartStkyya[0],2);
OSTaskCreate(TaskStartyyb, (void *)0, &TaskStartStkyyb[0],3);
OSTaskCreate(TaskStartyyc, (void *)0, &TaskStartStkyyc[0],4);

OSStart();
}


void TaskStartyya(void *yydata) reentrant
{
yydata=yydata;
clrscr();
PrintStr("\n\t\t*******************************\n");
PrintStr("\t\t* Hello! The world. *\n");
PrintStr("\t\t*******************************\n\n\n");

for(;;){
PrintStr("\tAAAAAA111111 is active.\n");
OSTimeDly(OS_TICKS_PER_SEC);
}
}

void TaskStartyyb(void *yydata) reentrant
{
yydata=yydata;

for(;;){
PrintStr("\tBBBBBB333333 is active.\n");
OSTimeDly(3*OS_TICKS_PER_SEC);
}
}

void TaskStartyyc(void *yydata) reentrant
{
yydata=yydata;

for(;;){
PrintStr("\tCCCCCC666666 is active.\n");
OSTimeDly(6*OS_TICKS_PER_SEC);
}
}

重入問題的解決:
任務函數中帶有形參和局部變數時若使用 reentrant關鍵字會引起重入,從C51.PDF 129-131頁的內容知:為了函數重入,形參和局部變數必須保存在堆棧里,由於51硬體堆棧太小,KEIL將根據內存模式在相應內存空間模擬堆棧(生長方向由上向下,與硬體棧相反)。對於大模式編譯,函數返回地址保存在硬體堆棧里,形參和局部變數放在模擬堆棧中,棧指針為?C_XBP,XBPSTACK =1時,起始值在startup.a51中初始化為FFFFH+1。模擬堆棧效率低下,KEIL建議盡量不用,但為了重入操作必須使用。KEIL可以混合使用3種模擬堆棧(大、中、小模式),為了提高效率,針對51推薦統一使用大模式編譯。
為了支持重入,重新設計了堆棧結構(如下圖)。增加了保存模擬堆棧指針?C_XBP和堆棧內容的數據結構。相應改變的文件有:OS_CPU_A.ASM、OS_CPU_C.C、OS_CPU.H、YY.C。由圖可知,用戶棧中保存的模擬棧與硬體棧相向生長,中間為空閑間隔,顯然uCOSII的堆棧檢測函數失效。硬體棧的保存恢復詳見上節,模擬堆棧的保存與 8086移植中的一樣,OS只提供堆棧空間和只操作堆棧指針,不進行內存拷貝,效率相對很高。
建議使用統一的固定大小的堆棧空間,儘管uCOSII原作者把不同任務使用不同空間看成是優點,但為了在51上有效實現任務重入,針對51筆者還是堅持不使用這個優點。
用戶堆棧空間的大小是可以精確計算出來的。用戶堆棧空間=硬體堆棧空間+模擬堆棧空間。硬體棧佔用內部RAM,內部RAM執行效率高,如果堆棧空間過大,會影響KEIL編譯的程序性能。如果堆棧空間小,在中斷嵌套和程序調用時會造成系統崩潰。綜合考慮,我把硬體堆棧空間大小定成了64位元組,用戶根據實際情況可以自行設定。模擬堆棧大小取決於形參和局部變數的類型及數量,可以精確算出。因為所有用戶棧使用相同空間大小,所以取佔用空間最大的任務函數的空間大小為模擬堆棧空間大小。這樣用戶堆棧空間大小就唯一確定了。我將用戶堆棧空間大小用宏定義在OS_CFG.H文件中,宏名為MaxStkSize。
51 的SP只有8位,無法在64K空間中自由移動,只好採用拷貝全部硬體堆棧內容的笨辦法。51 本來就弱,這麼一來缺點更明顯了。其實,引入OS必然要付出代價,一般OS要佔用CPU10%-20%的負荷能力,請權衡利弊決定。切換頻率決定了CPU 的耗費,頻率越高耗費越大,大到一定程度就該換更強的CPU了。我選了50Hz的切換頻率,不高也不低,用戶可以根據需要自行定奪。在耗費無法避免的情況下,我採取了幾個措施來提高效率:1。ret和reti混用減少代碼;2。IE、SP不入出棧,通過另外方式解決;3。用IDATA關鍵字聲明在彙編中用到的全局變數,變DPTR操作為Ri操作;4。設計堆棧結構,簡化演算法;5。讓串口輸入輸出工作在系統態,不佔用任務TCB和優先順序,增加彈性緩衝區,減少等待。

在51單片機上硬體模擬uCOS51的說明:
zyware網友2002/11/22來信詢問uCOS51在單片機上的硬體模擬問題,具體情況是“在51上用uCOS51核,以及一些構件,keilc上模擬通過,用wave接硬體模擬程序亂飛,wave模擬以前的程序沒有問題,不知是何緣故”。
由於我的OS程序已經在KEIL軟體模擬和硬體上實際測試過,所以不可能是程序錯。可能的原因只能是硬體模擬軟體設置問題。本人用的是Medwin軟體,在 Insight上調試,使用uCOS51編譯測試程序一樣跑飛。即使添加修改後的startup.a51(詳見《在51單片機上固化uCOS51的說明》)也不正常。我發現Medwin似乎沒有編譯startup.a51,因為它把該文件加在了other Files目錄下而不是source Files目錄,於是我猜測只有放在source Files目錄下的文件才被編譯。由觀察知,以.c和.asm做後綴的文件均被放在此目錄下且被編譯。於是我立即將startup.a51改成 startup.asm並加入項目編譯,結果測試正常。不必擔心startup改名造成衝突,KEIL在鏈接目標文件時會自動處理重名段,本目錄的文件優先順序高(我是這麼理解的,具體原理不清楚,這只是根據實踐得到的結論,希望了解此處理過程的朋友能告之,不勝感激。)。

具體做法如下:
1。按《在51單片機上固化uCOS51的說明》一文修改startup.a51,並將其更名為startup.asm。
2。將startup.asm、yy1.c、os_cpu_c.c、ucos_ii.c、os_cpu_a.asm五個文件加入項目編譯。
3。運行

在51單片機上固化uCOS51的說明:
近來,收到多位網友來信詢問uCOS51在51單片機上的固化問題,歸納其焦點就是:為什麼OS在KeilC51上模擬可以正常運行,但把它燒錄在CPU上卻不能工作?理論上,程序在軟體模擬通過測試后,將其燒錄在硬體上,硬體調試應該一次成功。許多網友也有這個經驗,可為什麼在調試uCOS51時失效了呢?難道操作系統調試很特殊嗎?
其實問題出在重入函數的引入。原來KEILC51軟體模擬在不修改startup.a51文件的情況下,缺剩使用64K外部 RAM,它把0000H-FFFFH全部模擬為可讀寫的RAM,而用戶的硬體系統可能沒有用到那麼大的RAM空間,比如只用了8K/16K/32K等,或者用戶把一些地址空間映射給了別的設備,比如8019AS等。在沒有調用OSTaskCreate前,定義為reentrant的函數將用FFE0H做模擬堆棧棧頂指針,而此處在用戶的系統里不是RAM,造成程序跑飛。比如在我的用戶板上,將FE00H-FFFFH空間的一部分分配給8019AS使用,如果把demo程序編譯后直接燒到51上,將不能運行。解決辦法是根據系統RAM配置,修改startup.a51文件,並將其加入項目編譯,如下所示:

XBPSTACK EQU 1 ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 07FFFH+1; set top of stack to highest location+1.

按此修改後,在有32K外部RAM的系統上可以正常運行。用戶可根據自己XRAM的實際配置情況修改startup.a51相關參數,並將其添加到項目里編譯。不必理會KEIL/C51/LIB目錄下的同名文件,此處的startup.a51優先順序高,KEIL將按此處該文件的配置編譯項目。
這也解釋了有些網友問到的,“為什麼加入reentrant關鍵字,在軟體模擬時正確,燒在晶元上就死機,去掉reentrant后兩者都正常”的問題。由於大多數人很少使用重入函數,往往不了解這個細節,特此提請大家注意。

關於uCOS51不能正常工作的原因還可能是因為串口波特率和 OS_TICKS_PER_SEC及TH0、TL0設置不正確引起的。demo程序默認使用22.1184MHz晶體,19200波特率,切換頻率為 50Hz。為此,1。在SERIAL.C中設置“TL1=0xFD;TH1=0xFD;”使波特率為19200;2。在OS_CPU_C.C和 OS_CPU_A.ASM中設置“TH0=0x70;TL0=0x00;”使時鐘節拍tick=50次/秒;3。在OS_CFG.H中設置 OS_TICKS_PER_SEC為50Hz。用戶應根據實際情況,相應地修改這些參數,否則運行不正確。

定時器初值設置:

定時器0用於時鍾節拍發生器
//*****************************************************************************
//初值計算公式:
// (2^16-x)*F=Fosc/12
// 其中:F=時鐘節拍頻率tick;Fosc=晶體或晶振頻率;x=初值;
// 本例中,F=50;Fosc=21.1184MHz;所以x=0x7000。
//*****************************************************************************

定時器1用于波特率發生器
//*****************************************************************************
//初值計算公式:
// TH1=256-(2^SMOD/32*Fosc/12*1/Bound)
// 其中:SMOD=0,1;Fosc=晶體或晶振頻率;Bound=波特率
// 本例中,SMOD=0;Fosc=21.1184MHz;Bound=19200,所以TH1=0xFD。
//*****************************************************************************

demo程序項目中增加按如上方法改寫的startup.a51后,在我的用戶板硬體上運行正確。

為uCOS51增加Shell界面:
uCOSII只提供了操作系統內核,用戶要自己添加文件處理、人機界面、網路介面等重要部分。其中Shell(人機界面)提供了人與機器交互的界面,是機器服務於人的體現,是系統必不可少的重要組成部分。現代的很多OS如UNIX、DOS、VxWorks都提供了友好的命令行界面。Windows更是提供了GUI。大部分人認識OS都是從這裡開始的。uCOS51同樣擁有Shell,它是我從以前寫的前後台程序中移植過來的。

命令行Shell的工作原理比較簡單,主要思路就是單片機接收用戶鍵盤輸入的字元存入命令緩衝區,並回顯到屏幕,當用戶按下回車鍵,觸發軟體狀態機狀態變遷,從輸入態轉移到命令解釋態,然後根據用戶命令調用相關子程序執行相應操作,執行完畢后重新回到輸入態。
我感覺原理很好掌握,程序也不長,但是細節部分要反覆調試多次才能穩定工作。比如:命令行左右邊界的保護、退格鍵的處理、詞表的設計等等。
Shell程序由詞表、取詞子程序、狀態機框架程序(輸入回顯和命令解釋執行)、命令相關子程序組成(詳見源程序清單)。
詞表結構如程序清單所示,由詞數目,左括弧數,右括弧數,每個詞的具體信息(長度,字元串)構成。左右括弧數用於括弧匹配檢查;詞數目用於程序循環;詞的具體信息作為解釋/執行程序的輸入參數。
取詞子程序從命令行語句中提取單詞並存入詞表同時進行匹配檢查和詞法分析。默認字元為:0-9、a-z、A-Z、'.';定界符為:空格、逗號,左/右括弧。建議用戶補充默認字符集(? / \ -)以便實現更靈活的語法。注意:現在版本的Shell只檢查左右括弧數量的匹配,無優先順序和語法含義。
輸入回顯程序循環檢查用戶鍵盤輸入。如果輸入回車,程序狀態轉入解釋執行態;如果輸入退格(8)則回顯退格、空格、退格,模擬刪除字元,同時輸入緩衝區清除相應位元組,清除前先檢查左邊界是否越界。如越界則鳴響報警且不執行清除操作;其他字元輸入直接存入輸入緩衝區並回顯,此前檢查右邊界是否溢出,如果溢出則鳴響報警且拋棄剛輸入的字元。
命令解釋程序調用取詞子程序分析用戶命令行輸入,根據詞表第一個單詞在散轉表中的位置調用相應執行子程序處理命令,如果散轉表中無此單詞,則列印“Bad command!”。取詞子程序返回錯誤指示時也列印此句。
命令解釋程序向相應的命令相關子程序傳入詞表指針,具體執行由用戶自行決定。由命令相關子程序返回后重新回到命令輸入態,完成一次輸入執行全過程。此過程周而復始地循環執行。

Shell界面的命令按功能分為以下幾組:
1。操作系統相關命令:
查看就緒任務lt / 中止任務kill / 恢復任務執行call / CPU利用率usage / 版本查詢ver / 查某個任務信息(TCB、堆棧內容)lt
查看切換次數和時間lts

2。網路相關命令:
顯示配置MAC地址macadr / 顯示配置主機IP地址host / 顯示配置子網掩碼mask / 顯示配置預設網關gateway
顯示網路配置總情況lc / 連通測試命令ping / 用戶數據報發送命令udp / telnet命令tel / 相關應用命令**
顯示ARP高速緩衝區地址對ls / 顯示發送緩衝區信息lti

3。屏幕顯示相關命令:
清屏clr / 幫助help / 功能鍵F3、F7處理 / 組合鍵Ctrl+C、Ctrl+B處理

4。外設(閃盤X5045和I/O口)相關命令:
讀閃盤rdx / 讀I/O口rdp / 寫閃盤wdx

5。安全相關命令:
身份認證密碼許可權usr、pass

6。應用相關命令:
用戶自行定義

用戶命令大小寫不敏感,程序將命令字元串統一成小寫形式。程序中各種參數(如:最大詞長度、詞數量……)定義成宏放在一個頭文件中,隨時可修改配置,很方便。Shell作為一個任務工作於內核之外,佔用一個任務號。

源程序:
詞表
typedef struct{
int Num;
int LeftCurveNum,RightCurveNum;
struct{
int Length;
unsigned char Str[MaxLenWord+1]; /*for '\0'*/
} wt[MaxLenWordTable];
} WORDTABLE;

取詞
bit GetWord(unsigned char *ComBuf,WORDTABLE *WordTable)
{
int i=0; /*ComBuf String pointer*/
int j=0; /*Length of Word */
int k=-1; /*The number of WordTable*/
int StrFlag=0; /*There is "0-9/a-z/A-Z" before " ,()"*/
int SentenceEndFlag=0; /*Sentence end*/
char ch;

WordTable->Num=0;
WordTable->LeftCurveNum=0;
WordTable->RightCurveNum=0;

ch=ComBuf[0];
while(!SentenceEndFlag&&i
if((ch>='0'&&ch<='9')||(ch>='a'&&ch<='z')||(ch>='A'&&ch<='Z')||(ch=='.')){
if(StrFlag==0){
StrFlag=1;k=k+1;j=0;
if(k>=MaxLenWordTable) return 0;
WordTable->wt[k].Str[j]=ch;
WordTable->Num=k+1;
}
else{
j=j+1;
if(j>=MaxLenWord) return 0;
WordTable->wt[k].Str[j]=ch;
}
}
else if(ch==' '||ch==','||ch=='('||ch==')'||ch=='\0'){
if(ch=='(') WordTable->LeftCurveNum++;
if(ch==')') WordTable->RightCurveNum++;
if(StrFlag==1){
StrFlag=0;j=j+1;
WordTable->wt[k].Str[j]='\0';
WordTable->wt[k].Length=j;
}
if(ch=='\0') SentenceEndFlag=1;
}
else{
return 0;
}
i=i+1;
ch=ComBuf[i];
}
if(i
if(WordTable->LeftCurveNum==WordTable->RightCurveNum) return 1;
else return 0;
}
else{
return 0;
}
}

輸入回顯和命令解釋執行
void yyshell(void *yydata) reentrant
{
yydata=yydata;
clrscr();
PrintStr("\t\t***********************************************\n");
PrintStr("\t\t* Welcom to use this program *\n");
PrintStr("\t\t* Author:YangYi 20020715 *\n");
PrintStr("\t\t***********************************************\n\n\n");

/*Login & Password*/

PrintStr("% ");
while(!ShellEnd){

switch(State){
case StatInputCom:{
if(yygetch(&ch)){
if(ch==13) /*Enter return key*/
{
PrintStr("\n");
ComBuf[i+1]='\0';
if(i+1==0) PrintStr("% ");
else
State=StatExeCom;
}
else{
i=i+1;
if((i>=MaxLenComBuf)&&(ch!=8)){
PrintChar(7);
i=MaxLenComBuf-1;
}
else{
if(ch==8){
i=i-2;
if(i<-1) {i=-1;PrintChar(7);}
else{
PrintChar(8);
PrintChar(' ');
PrintChar(8);
}
}
else{
PrintChar(ch);
ComBuf[i]=ch;
}
}
}
break;
}
else{
//OSTimeDly(10);
break;
}
}
case StatExeCom:{
if(GetWord(ComBuf,&WordTable)==1&&WordTable.Num!=0){
yystrlwr(WordTable.wt[0].Str);
for(tem=0;tem
if(yystrcmp(WordTable.wt[0].Str,ComTable[tem])==0) ComMatchFlag=1;
if(ComMatchFlag){
tem--;
switch(tem){
case 0:{DisplayTask(&WordTable);break;}
case 1:{Kill(&WordTable);break;}
case 2:{PingCommand(&WordTable);break;}
case 3:{UDPCommand(&WordTable);break;}
case 4:{CfgHost(&WordTable);break;}
case 5:{CfgMask(&WordTable);break;}
case 6:{CfgGateway(&WordTable);break;}
case 7:{
//ShellEnd=1;
PrintStr("\n\tThis Command is limited!\n\n");
break;
}
case 8:{PrintConfig(&WordTable);break;}
case 9:{clrscr();break;}
case 10:{DisplayHelpMenu(&WordTable);break;}
}
}
else
PrintStr(" Bad command!\n\n");
}
else{
if(WordTable.Num) PrintStr(" Bad command!\n\n");
}

ComMatchFlag=0;
State=StatInputCom;
if(ShellEnd) {PrintStr("\n\n");}
else PrintStr("% ");
i=-1;
break;
}
default:{
//ShellEnd=1;
PrintStr("System fatal error!\n");
PrintChar(7);PrintChar(7);PrintChar(7);
}
}
}
}

以上是我這次移植uCOS51的一些心得,寫出來只是讓準備在 51上運行操作系統的同行們少走彎路並增強使用信心。我強烈推薦大家在自己的51系統中使用uCOS這個簡單實用的自己的操作系統。它的大小應該不是問題,性能上的提高卻是顯著的。但願此文能對朋友們有所幫助,錯誤在所難免,希望讀者指正




[admin via 研發互助社區 ] 在51系列單片機上移植uCOS-II已經有3964次圍觀

http://cocdig.com/docs/show-post-42816.html