在51系列單片機(jī)上移植uCOS-II
關(guān)鍵詞:嵌入式實(shí)時(shí)多任務(wù)操作系統(tǒng)、uC/OS-II、C51
引言:隨著各種應(yīng)用電子系統(tǒng)的復(fù)雜化和系統(tǒng)實(shí)時(shí)性需求的提高,并伴隨應(yīng)用軟件朝著系統(tǒng)化方向發(fā)展的加速,在16位/32位單片機(jī)中廣泛使用了嵌入式實(shí)時(shí)操作系統(tǒng)。然而實(shí)際使用中卻存在著大量8位單片機(jī),從經(jīng)濟(jì)性考慮,對某些應(yīng)用場合,在8位MCU上使用操作系統(tǒng)是可行的。從學(xué)習(xí)操作系統(tǒng)角度,uC/OS-II for 51即簡單又全面,學(xué)習(xí)成本低廉,值得推廣。
結(jié)語:μC/OS-II具有免費(fèi)、簡單、可靠性高、實(shí)時(shí)性好等優(yōu)點(diǎn),但也有缺乏便利開發(fā)環(huán)境等缺點(diǎn),尤其不像商用嵌入式系統(tǒng)那樣得到廣泛使用和持續(xù)的研究更新。但開放性又使得開發(fā)人員可以自行裁減和添加所需的功能,在許多應(yīng)用領(lǐng)域發(fā)揮著獨(dú)特的作用。當(dāng)然,是否在單片機(jī)系統(tǒng)中嵌入μC/OS-II應(yīng)視所開發(fā)的項(xiàng)目而定,對于一些簡單的、低成本的項(xiàng)目來說,就沒必要使用嵌入式操作系統(tǒng)了。
uC/OS-II原理:
uCOSII包括任務(wù)調(diào)度、時(shí)間管理、內(nèi)存管理、資源管理(信號量、郵箱、消息隊(duì)列)四大部分,沒有文件系統(tǒng)、網(wǎng)絡(luò)接口、輸入輸出界面。它的移植只與4個(gè)文件相關(guān):匯編文件(OS_CPU_A.ASM)、處理器相關(guān)C文件(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有64個(gè)優(yōu)先級,系統(tǒng)占用8個(gè),用戶可創(chuàng)建56個(gè)任務(wù),不支持時(shí)間片輪轉(zhuǎn)。它的基本思路就是 “近似地每時(shí)每刻總是讓優(yōu)先級最高的就緒任務(wù)處于運(yùn)行狀態(tài)” 。為了保證這一點(diǎn),它在調(diào)用系統(tǒng)API函數(shù)、中斷結(jié)束、定時(shí)中斷結(jié)束時(shí)總是執(zhí)行調(diào)度算法。原作者通過事先計(jì)算好數(shù)據(jù),簡化了運(yùn)算量,通過精心設(shè)計(jì)就緒表結(jié)構(gòu),使得延時(shí)可預(yù)知。任務(wù)的切換是通過模擬一次中斷實(shí)現(xiàn)的。
uCOSII工作核心原理是:近似地讓最高優(yōu)先級的就緒任務(wù)處于運(yùn)行狀態(tài)。
操作系統(tǒng)將在下面情況中進(jìn)行任務(wù)調(diào)度:調(diào)用API函數(shù)(用戶主動(dòng)調(diào)用),中斷(系統(tǒng)占用的時(shí)間片中斷OsTimeTick(),用戶使用的中斷)。
調(diào)度算法書上講得很清楚,我主要講一下整體思路。
(1)在調(diào)用API函數(shù)時(shí),有可能引起阻塞,如果系統(tǒng)API函數(shù)察覺到運(yùn)行條件不滿足,需要切換就調(diào)用OSSched()調(diào)度函數(shù),這個(gè)過程是系統(tǒng)自動(dòng)完成的,用戶沒有參與。OSSched()判斷是否切換,如果需要切換,則此函數(shù)調(diào)用OS_TASK_SW()。這個(gè)函數(shù)模擬一次中斷(在51里沒有軟中斷,我用子程序調(diào)用模擬,效果相同),好象程序被中斷打斷了,其實(shí)是OS故意制造的假象,目的是為了任務(wù)切換。既然是中斷,那么返回地址(即緊鄰OS_TASK_SW()的下一條匯編指令的PC地址)就被自動(dòng)壓入堆棧,接著在中斷程序里保存CPU寄存器(PUSHALL)……。堆棧結(jié)構(gòu)不是任意的,而是嚴(yán)格按照uCOSII規(guī)范處理。OS每次切換都會(huì)保存和恢復(fù)全部現(xiàn)場信息(POPALL),然后用RETI回到任務(wù)斷點(diǎn)繼續(xù)執(zhí)行。這個(gè)斷點(diǎn)就是OSSched()函數(shù)里的緊鄰OS_TASK_SW()的下一條匯編指令的PC地址。切換的整個(gè)過程就是,用戶任務(wù)程序調(diào)用系統(tǒng)API函數(shù),API調(diào)用OSSched(),OSSched()調(diào)用軟中斷OS_TASK_SW()即OSCtxSw,返回地址(PC值)壓棧,進(jìn)入OSCtxSw中斷處理子程序內(nèi)部。反之,切換程序調(diào)用RETI返回緊鄰OS_TASK_SW()的下一條匯編指令的PC地址,進(jìn)而返回OSSched()下一句,再返回API下一句,即用戶程序斷點(diǎn)。因此,如果任務(wù)從運(yùn)行到就緒再到運(yùn)行,它是從調(diào)度前的斷點(diǎn)處運(yùn)行。
(2)中斷會(huì)引發(fā)條件變化,在退出前必須進(jìn)行任務(wù)調(diào)度。uCOSII要求中斷的堆棧結(jié)構(gòu)符合規(guī)范,以便正確協(xié)調(diào)中斷退出和任務(wù)切換。前面已經(jīng)說到任務(wù)切換實(shí)際是模擬一次中斷事件,而在真正的中斷里省去了模擬(本身就是中斷嘛)。只要規(guī)定中斷堆棧結(jié)構(gòu)和uCOSII模擬的堆棧結(jié)構(gòu)一樣,就能保證在中斷里進(jìn)行正確的切換。任務(wù)切換發(fā)生在中斷退出前,此時(shí)還沒有返回中斷斷點(diǎn)。仔細(xì)觀察中斷程序和切換程序最后兩句,它們是一模一樣的,POPALL+RETI。即要么直接從中斷程序退出,返回?cái)帱c(diǎn);要么先保存現(xiàn)場到TCB,等到恢復(fù)現(xiàn)場時(shí)再從切換函數(shù)返回原來的中斷斷點(diǎn)(由于中斷和切換函數(shù)遵循共同的堆棧結(jié)構(gòu),所以退出操作相同,效果也相同)。用戶編寫的中斷子程序必須按照uCOSII規(guī)范書寫。任務(wù)調(diào)度發(fā)生在中斷退出前,是非常及時(shí)的,不會(huì)等到下一時(shí)間片才處理。OSIntCtxSw()函數(shù)對堆棧指針做了簡單調(diào)整,以保證所有掛起任務(wù)的棧結(jié)構(gòu)看起來是一樣的。
(3)在uCOSII里,任務(wù)必須寫成兩種形式之一(《uCOSII中文版》p99頁)。在有些RTOS開發(fā)環(huán)境里沒有要求顯式調(diào)用OSTaskDel(),這是因?yàn)殚_發(fā)環(huán)境自動(dòng)做了處理,實(shí)際原理都是一樣的。uCOSII的開發(fā)依賴于編譯器,目前沒有專用開發(fā)環(huán)境,所以出現(xiàn)這些不便之處是可以理解的。
移植過程:
(1)拷貝書后附贈(zèng)光盤sourcecode目錄下的內(nèi)容到C:YY下,刪除不必要的文件和EX1L.C,只剩下p187(《uCOSII》)上列出的文件。
(2)改寫最簡單的OS_CPU.H
數(shù)據(jù)類型的設(shè)定見C51.PDF第176頁。注意BOOLEAN要定義成unsigned char 類型,因?yàn)閎it類型為C51特有,不能用在結(jié)構(gòu)體里。
EA=0關(guān)中斷;EA=1開中斷。這樣定義即減少了程序行數(shù),又避免了退出臨界區(qū)后關(guān)中斷造成的死機(jī)。
MCS-51堆棧從下往上增長(1=向下,0=向上),OS_STK_GROWTH定義為0
#define OS_TASK_SW() OSCtxSw() 因?yàn)镸CS-51沒有軟中斷指令,所以用程序調(diào)用代替。兩者的堆棧格式相同,RETI指令復(fù)位中斷系統(tǒng),RET則沒有。實(shí)踐表明,對于MCS-51,用子程序調(diào)用入棧,用中斷返回指令RETI出棧是沒有問題的,反之中斷入棧RET出棧則不行??傊瑢τ谌霔?,子程序調(diào)用與中斷調(diào)用效果是一樣的,可以混用。在沒有中斷發(fā)生的情況下復(fù)位中斷系統(tǒng)也不會(huì)影響系統(tǒng)正常運(yùn)行。詳見《uC/OS-II》第八章193頁第12行
(3)改寫OS_CPU_C.C
我設(shè)計(jì)的堆棧結(jié)構(gòu)如下圖所示:
TCB結(jié)構(gòu)體中OSTCBStkPtr總是指向用戶堆棧最低地址,該地址空間內(nèi)存放用戶堆棧長度,其上空間存放系統(tǒng)堆棧映像,即:用戶堆??臻g大小=系統(tǒng)堆棧空間大小+1。
SP總是先加1再存數(shù)據(jù),因此,SP初始時(shí)指向系統(tǒng)堆棧起始地址(OSStack)減1處(OSStkStart)。很明顯系統(tǒng)堆棧存儲(chǔ)空間大小=SP-OSStkStart。
任務(wù)切換時(shí),先保存當(dāng)前任務(wù)堆棧內(nèi)容。方法是:用SP-OSStkStart得出保存字節(jié)數(shù),將其寫入用戶堆棧最低地址內(nèi),以用戶堆棧最低地址為起址,以O(shè)SStkStart為系統(tǒng)堆棧起址,由系統(tǒng)棧向用戶??截悢?shù)據(jù),循環(huán)SP-OSStkStart次,每次拷貝前先將各自棧指針增1。
其次,恢復(fù)最高優(yōu)先級任務(wù)系統(tǒng)堆棧。方法是:獲得最高優(yōu)先級任務(wù)用戶堆棧最低地址,從中取出“長度”,以最高優(yōu)先級任務(wù)用戶堆棧最低地址為起址,以O(shè)SStkStart為系統(tǒng)堆棧起址,由用戶棧向系統(tǒng)棧拷貝數(shù)據(jù),循環(huán)“長度”數(shù)值指示的次數(shù),每次拷貝前先將各自棧指針增1。
用戶堆棧初始化時(shí)從下向上依次保存:用戶堆棧長度(15),PCL,PCH,PSW,ACC,B,DPL,DPH,R0,R1,R2,R3,R4,R5,R6,R7。不保存SP,任務(wù)切換時(shí)根據(jù)用戶堆棧長度計(jì)算得出。
OSTaskStkInit函數(shù)總是返回用戶棧最低地址。
操作系統(tǒng)tick時(shí)鐘我使用了51單片機(jī)的T0定時(shí)器,它的初始化代碼用C寫在了本文件中。
最后還有幾點(diǎn)必須注意的事項(xiàng)。本來原則上我們不用修改與處理器無關(guān)的代碼,但是由于KEIL編譯器的特殊性,這些代碼仍要多處改動(dòng)。因?yàn)镵EIL缺省情況下編譯的代碼不可重入,而多任務(wù)系統(tǒng)要求并發(fā)操作導(dǎo)致重入,所以要在每個(gè)C函數(shù)及其聲明后標(biāo)注reentrant關(guān)鍵字。另外,“pdata”、“data”在uCOS中用做一些函數(shù)的形參,但它同時(shí)又是KEIL的關(guān)鍵字,會(huì)導(dǎo)致編譯錯(cuò)誤,我通過把“pdata”改成“ppdata”,“data”改成“ddata”解決了此問題。OSTCBCur、OSTCBHighRdy、OSRunning、OSPrioCur、OSPrioHighRdy這幾個(gè)變量在匯編程序中用到了,為了使用Ri訪問而不用DPTR,應(yīng)該用KEIL擴(kuò)展關(guān)鍵字IDATA將它們定義在內(nèi)部RAM中。
(4)重寫OS_CPU_A.ASM
A51宏匯編的大致結(jié)構(gòu)如下:
NAME 模塊名 ;與文件名無關(guān)
;定義重定位段 必須按照C51格式定義,匯編遵守C51規(guī)范。段名格式為:?PR?函數(shù)名?模塊名
;聲明引用全局變量和外部子程序 注意關(guān)鍵字為“EXTRN”沒有‘E’
全局變量名直接引用
無參數(shù)/無寄存器參數(shù)函數(shù) FUNC
帶寄存器參數(shù)函數(shù) _FUNC
重入函數(shù) _?FUNC
;分配堆??臻g
只關(guān)心大小,堆棧起點(diǎn)由keil決定,通過標(biāo)號可以獲得keil分配的SP起點(diǎn)。切莫自己分配堆棧起點(diǎn),只要用DS通知KEIL預(yù)留堆??臻g即可。
?STACK段名與STARTUP.A51中的段名相同,這意味著KEIL在LINK時(shí)將把兩個(gè)同名段拼在一起,我預(yù)留了40H個(gè)字節(jié),STARTUP.A51預(yù)留了1個(gè)字節(jié),LINK完成后堆棧段總長為41H。查看yy.m51知KEIL將堆棧起點(diǎn)定在21H,長度41H,處于內(nèi)部RAM中。
;定義宏
宏名 MACRO 實(shí)體 ENDM
;子程序
OSStartHighRdy
OSCtxSw
OSIntCtxSw
OSTickISR
SerialISR
END ;聲明匯編源文件結(jié)束
一般指針占3字節(jié)。+0類型+1高8位數(shù)據(jù)+2低8位數(shù)據(jù) 詳見C51.PDF第178頁
低位地址存高8位值,高位地址存低8位值。例如0x1234,基址+0:0x12 基址+1:0x34
(5)移植串口驅(qū)動(dòng)程序
在此之前我寫過基于中斷的串口驅(qū)動(dòng)程序,包括打印字節(jié)/字/長字/字符串,讀串口,初始化串口/緩沖區(qū)。把它改成重入函數(shù)即可直接使用。
系統(tǒng)提供的顯示函數(shù)是并發(fā)的,它不是直接顯示到串口,而是先輸出到顯存,用戶不必?fù)?dān)心IO慢速操作影響程序運(yùn)行。串口輸入也采用了同樣的技術(shù),他使得用戶在CPU忙于處理其他任務(wù)時(shí)照樣可以盲打輸入命令。
(6)編寫測試程序Demo(YY.C)
Demo程序創(chuàng)建了3個(gè)任務(wù)A、B、C優(yōu)先級分別為2、3、4,A每秒顯示一次,B每3秒顯示一次,C每6秒顯示一次。從顯示結(jié)果看,顯示3個(gè)A后顯示1個(gè)B,顯示6個(gè)A和2個(gè)B后顯示1個(gè)C,結(jié)果顯然正確。
顯示結(jié)果如下:
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程序經(jīng)Keil701編譯后,代碼量為7-8K,可直接在KeilC51上仿真運(yùn)行。
編譯時(shí)要將OS_CPU_C.C、UCOS_II.C、OS_CPU_A.ASM、YY.C加入項(xiàng)目
文件名 : 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個(gè)不可重入函數(shù)
PUBLIC OSStartHighRdy
PUBLIC OSCtxSw
PUBLIC OSIntCtxSw
PUBLIC OSTickISR
;PUBLIC SerialISR
;分配堆棧空間。只關(guān)心大小,堆棧起點(diǎn)由keil決定,通過標(biāo)號可以獲得keil分配的SP起點(diǎn)。
?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,任務(wù)切換時(shí)由相應(yīng)程序調(diào)整
ENDM
POPALL MACRO
;POP ACC ;不必保存SP,任務(wù)切換時(shí)由相應(yīng)程序調(diào)整
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自動(dòng)關(guān)中斷,此處不必用CLR EA指令,因?yàn)榈酱颂庍€未開中斷,本程序退出后,開中斷。
LCALL _?OSTaskSwHook
OSCtxSw_in:
;OSTCBCur ===> DPTR 獲得當(dāng)前TCB指針,詳見C51.PDF第178頁
MOV R0,#LOW (OSTCBCur) ;獲得OSTCBCur指針低地址,指針占3字節(jié)。+0類型+1高8位數(shù)據(jù)+2低8位數(shù)據(jù)
INC R0
MOV DPH,@R0 ;全局變量OSTCBCur在IDATA中
INC R0
MOV DPL,@R0
;OSTCBCur->OSTCBStkPtr ===> DPTR 獲得用戶堆棧指針
INC DPTR ;指針占3字節(jié)。+0類型+1高8位數(shù)據(jù)+2低8位數(shù)據(jù)
MOVX A,@DPTR ;.OSTCBStkPtr是void指針
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1
;*UserStkPtr ===> R5 用戶堆棧起始地址內(nèi)容(即用戶堆棧長度放在此處) 詳見文檔說明 指針用法詳見C51.PDF第178頁
MOVX A,@DPTR ;用戶堆棧中是unsigned char類型數(shù)據(jù)
MOV R5,A ;R5=用戶堆棧長度
;恢復(fù)現(xiàn)場堆棧內(nèi)容
MOV R0,#OSStkStart
restore_stack:
INC DPTR
INC R0
MOVX A,@DPTR
MOV @R0,A
DJNZ R5,restore_stack
;恢復(fù)堆棧指針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 獲得當(dāng)前TCB指針,詳見C51.PDF第178頁
MOV R0,#LOW (OSTCBCur) ;獲得OSTCBCur指針低地址,指針占3字節(jié)。+0類型+1高8位數(shù)據(jù)+2低8位數(shù)據(jù)
INC R0
MOV DPH,@R0 ;全局變量OSTCBCur在IDATA中
INC R0
MOV DPL,@R0
;OSTCBCur->OSTCBStkPtr ===> DPTR 獲得用戶堆棧指針
INC DPTR ;指針占3字節(jié)。+0類型+1高8位數(shù)據(jù)+2低8位數(shù)據(jù)
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
;調(diào)用用戶程序
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 使用這兩個(gè)變量主要目的是為了使指針比較變?yōu)樽止?jié)比較,以便節(jié)省時(shí)間。
MOV R0,#OSPrioCur
MOV R1,#OSPrioHighRdy
MOV A,@R1
MOV @R0,A
LJMP OSCtxSw_in
;-------------------------------------------------------------------------
RSEG ?PR?OSIntCtxSw?OS_CPU_A
OSIntCtxSw:
;調(diào)整SP指針去掉在調(diào)用OSIntExit(),OSIntCtxSw()過程中壓入堆棧的多余內(nèi)容
;SP=SP-4
MOV A,SP
CLR C
SUBB A,#4
MOV SP,A
LJMP OSIntCtxSw_in
;-------------------------------------------------------------------------
CSEG AT 000BH ;OSTickISR
LJMP OSTickISR ;使用定時(shí)器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 ;工作于系統(tǒng)態(tài),無任務(wù)切換。
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沒被用到,保留此語句防止告警產(chǎn)生
stk = (OS_STK *)ptos; //用戶堆棧最低有效地址
*stk++ = 15; //用戶堆棧長度
*stk++ = (INT16U)task & 0xFF; //任務(wù)地址低8位
*stk++ = (INT16U)task >> 8; //任務(wù)地址高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,任務(wù)切換時(shí)根據(jù)用戶堆棧長度計(jì)算得出。
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
//初始化定時(shí)器0
void InitTimer0(void) reentrant
{
TMOD=TMOD&0xF0;
TMOD=TMOD|0x01; //模式1(16位定時(shí)器),僅受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文件中設(shè)置?STACK空間為40H即64,不要超出范圍。
OS_STK TaskStartStkyyb[MAX_STK_SIZE+1];//用戶棧多一個(gè)字節(jié)存長度
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("ntt*******************************n");
PrintStr("tt* Hello! The world. *n");
PrintStr("tt*******************************nnn");
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);
}
}
重入問題的解決:
任務(wù)函數(shù)中帶有形參和局部變量時(shí)若使用reentrant關(guān)鍵字會(huì)引起重入,從C51.PDF 129-131頁的內(nèi)容知:為了函數(shù)重入,形參和局部變量必須保存在堆棧里,由于51硬件堆棧太小,KEIL將根據(jù)內(nèi)存模式在相應(yīng)內(nèi)存空間仿真堆棧(生長方向由上向下,與硬件棧相反)。對于大模式編譯,函數(shù)返回地址保存在硬件堆棧里,形參和局部變量放在仿真堆棧中,棧指針為?C_XBP,XBPSTACK=1時(shí),起始值在startup.a51中初始化為FFFFH+1。仿真堆棧效率低下,KEIL建議盡量不用,但為了重入操作必須使用。KEIL可以混合使用3種仿真堆棧(大、中、小模式),為了提高效率,針對51推薦統(tǒng)一使用大模式編譯。
為了支持重入,重新設(shè)計(jì)了堆棧結(jié)構(gòu)(如下圖)。增加了保存仿真堆棧指針?C_XBP和堆棧內(nèi)容的數(shù)據(jù)結(jié)構(gòu)。相應(yīng)改變的文件有:OS_CPU_A.ASM、OS_CPU_C.C、OS_CPU.H、YY.C。由圖可知,用戶棧中保存的仿真棧與硬件棧相向生長,中間為空閑間隔,顯然uCOSII的堆棧檢測函數(shù)失效。硬件棧的保存恢復(fù)詳見上節(jié),仿真堆棧的保存與8086移植中的一樣,OS只提供堆??臻g和只操作堆棧指針,不進(jìn)行內(nèi)存拷貝,效率相對很高。
建議使用統(tǒng)一的固定大小的堆??臻g,盡管uCOSII原作者把不同任務(wù)使用不同空間看成是優(yōu)點(diǎn),但為了在51上有效實(shí)現(xiàn)任務(wù)重入,針對51筆者還是堅(jiān)持不使用這個(gè)優(yōu)點(diǎn)。
用戶堆??臻g的大小是可以精確計(jì)算出來的。用戶堆??臻g=硬件堆棧空間+仿真堆??臻g。硬件棧占用內(nèi)部RAM,內(nèi)部RAM執(zhí)行效率高,如果堆??臻g過大,會(huì)影響KEIL編譯的程序性能。如果堆??臻g小,在中斷嵌套和程序調(diào)用時(shí)會(huì)造成系統(tǒng)崩潰。綜合考慮,我把硬件堆棧空間大小定成了64字節(jié),用戶根據(jù)實(shí)際情況可以自行設(shè)定。仿真堆棧大小取決于形參和局部變量的類型及數(shù)量,可以精確算出。因?yàn)樗杏脩魲J褂孟嗤臻g大小,所以取占用空間最大的任務(wù)函數(shù)的空間大小為仿真堆棧空間大小。這樣用戶堆??臻g大小就唯一確定了。我將用戶堆??臻g大小用宏定義在OS_CFG.H文件中,宏名為MaxStkSize。
51的SP只有8位,無法在64K空間中自由移動(dòng),只好采用拷貝全部硬件堆棧內(nèi)容的笨辦法。51 本來就弱,這么一來缺點(diǎn)更明顯了。其實(shí),引入OS必然要付出代價(jià),一般OS要占用CPU10%-20%的負(fù)荷能力,請權(quán)衡利弊決定。切換頻率決定了CPU的耗費(fèi),頻率越高耗費(fèi)越大,大到一定程度就該換更強(qiáng)的CPU了。我選了50Hz的切換頻率,不高也不低,用戶可以根據(jù)需要自行定奪。在耗費(fèi)無法避免的情況下,我采取了幾個(gè)措施來提高效率:1。ret和reti混用減少代碼;2。IE、SP不入出棧,通過另外方式解決;3。用IDATA關(guān)鍵字聲明在匯編中用到的全局變量,變DPTR操作為Ri操作;4。設(shè)計(jì)堆棧結(jié)構(gòu),簡化算法;5。讓串口輸入輸出工作在系統(tǒng)態(tài),不占用任務(wù)TCB和優(yōu)先級,增加彈性緩沖區(qū),減少等待。
在51單片機(jī)上硬件仿真uCOS51的說明:
zyware網(wǎng)友2002/11/22來信詢問uCOS51在單片機(jī)上的硬件仿真問題,具體情況是“在51上用uCOS51核,以及一些構(gòu)件,keilc上仿真通過,用wave接硬件仿真程序亂飛,wave仿真以前的程序沒有問題,不知是何緣故”。
由于我的OS程序已經(jīng)在KEIL軟件仿真和硬件上實(shí)際測試過,所以不可能是程序錯(cuò)??赡艿脑蛑荒苁怯布抡孳浖O(shè)置問題。本人用的是Medwin軟件,在Insight上調(diào)試,使用uCOS51編譯測試程序一樣跑飛。即使添加修改后的startup.a51(詳見《在51單片機(jī)上固化uCOS51的說明》)也不正常。我發(fā)現(xiàn)Medwin似乎沒有編譯startup.a51,因?yàn)樗言撐募釉诹薿ther Files目錄下而不是source Files目錄,于是我猜測只有放在source Files目錄下的文件才被編譯。由觀察知,以.c和.asm做后綴的文件均被放在此目錄下且被編譯。于是我立即將startup.a51改成startup.asm并加入項(xiàng)目編譯,結(jié)果測試正常。不必?fù)?dān)心startup改名造成沖突,KEIL在鏈接目標(biāo)文件時(shí)會(huì)自動(dòng)處理重名段,本目錄的文件優(yōu)先級高(我是這么理解的,具體原理不清楚,這只是根據(jù)實(shí)踐得到的結(jié)論,希望了解此處理過程的朋友能告之,不勝感激。)。
具體做法如下:
1。按《在51單片機(jī)上固化uCOS51的說明》一文修改startup.a51,并將其更名為startup.asm。
2。將startup.asm、yy1.c、os_cpu_c.c、ucos_ii.c、os_cpu_a.asm五個(gè)文件加入項(xiàng)目編譯。
3。運(yùn)行
在51單片機(jī)上固化uCOS51的說明:
近來,收到多位網(wǎng)友來信詢問uCOS51在51單片機(jī)上的固化問題,歸納其焦點(diǎn)就是:為什么OS在KeilC51上模擬可以正常運(yùn)行,但把它燒錄在CPU上卻不能工作?理論上,程序在軟件仿真通過測試后,將其燒錄在硬件上,硬件調(diào)試應(yīng)該一次成功。許多網(wǎng)友也有這個(gè)經(jīng)驗(yàn),可為什么在調(diào)試uCOS51時(shí)失效了呢?難道操作系統(tǒng)調(diào)試很特殊嗎?
其實(shí)問題出在重入函數(shù)的引入。原來KEILC51軟件仿真在不修改startup.a51文件的情況下,缺剩使用64K外部RAM,它把0000H-FFFFH全部仿真為可讀寫的RAM,而用戶的硬件系統(tǒng)可能沒有用到那么大的RAM空間,比如只用了8K/16K/32K等,或者用戶把一些地址空間映射給了別的設(shè)備,比如8019AS等。在沒有調(diào)用OSTaskCreate前,定義為reentrant的函數(shù)將用FFE0H做仿真堆棧棧頂指針,而此處在用戶的系統(tǒng)里不是RAM,造成程序跑飛。比如在我的用戶板上,將FE00H-FFFFH空間的一部分分配給8019AS使用,如果把demo程序編譯后直接燒到51上,將不能運(yùn)行。解決辦法是根據(jù)系統(tǒng)RAM配置,修改startup.a51文件,并將其加入項(xiàng)目編譯,如下所示:
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的系統(tǒng)上可以正常運(yùn)行。用戶可根據(jù)自己XRAM的實(shí)際配置情況修改startup.a51相關(guān)參數(shù),并將其添加到項(xiàng)目里編譯。不必理會(huì)KEIL/C51/LIB目錄下的同名文件,此處的startup.a51優(yōu)先級高,KEIL將按此處該文件的配置編譯項(xiàng)目。
這也解釋了有些網(wǎng)友問到的,“為什么加入reentrant關(guān)鍵字,在軟件仿真時(shí)正確,燒在芯片上就死機(jī),去掉reentrant后兩者都正?!钡膯栴}。由于大多數(shù)人很少使用重入函數(shù),往往不了解這個(gè)細(xì)節(jié),特此提請大家注意。
關(guān)于uCOS51不能正常工作的原因還可能是因?yàn)榇诓ㄌ芈屎蚈S_TICKS_PER_SEC及TH0、TL0設(shè)置不正確引起的。demo程序默認(rèn)使用22.1184MHz晶體,19200波特率,切換頻率為50Hz。為此,1。在SERIAL.C中設(shè)置“TL1=0xFD;TH1=0xFD;”使波特率為19200;2。在OS_CPU_C.C和OS_CPU_A.ASM中設(shè)置“TH0=0x70;TL0=0x00;”使時(shí)鐘節(jié)拍tick=50次/秒;3。在OS_CFG.H中設(shè)置OS_TICKS_PER_SEC為50Hz。用戶應(yīng)根據(jù)實(shí)際情況,相應(yīng)地修改這些參數(shù),否則運(yùn)行不正確。
定時(shí)器初值設(shè)置:
定時(shí)器0用于時(shí)鐘節(jié)拍發(fā)生器
//*****************************************************************************
//初值計(jì)算公式:
// (2^16-x)*F=Fosc/12
// 其中:F=時(shí)鐘節(jié)拍頻率tick;Fosc=晶體或晶振頻率;x=初值;
// 本例中,F(xiàn)=50;Fosc=21.1184MHz;所以x=0x7000。
//*****************************************************************************
定時(shí)器1用于波特率發(fā)生器
//*****************************************************************************
//初值計(jì)算公式:
// TH1=256-(2^SMOD/32*Fosc/12*1/Bound)
// 其中:SMOD=0,1;Fosc=晶體或晶振頻率;Bound=波特率
// 本例中,SMOD=0;Fosc=21.1184MHz;Bound=19200,所以TH1=0xFD。
//*****************************************************************************
demo程序項(xiàng)目中增加按如上方法改寫的startup.a51后,在我的用戶板硬件上運(yùn)行正確。
為uCOS51增加Shell界面:
uCOSII只提供了操作系統(tǒng)內(nèi)核,用戶要自己添加文件處理、人機(jī)界面、網(wǎng)絡(luò)接口等重要部分。其中Shell(人機(jī)界面)提供了人與機(jī)器交互的界面,是機(jī)器服務(wù)于人的體現(xiàn),是系統(tǒng)必不可少的重要組成部分?,F(xiàn)代的很多OS如UNIX、DOS、VxWorks都提供了友好的命令行界面。Windows更是提供了GUI。大部分人認(rèn)識OS都是從這里開始的。uCOS51同樣擁有Shell,它是我從以前寫的前后臺(tái)程序中移植過來的。
命令行Shell的工作原理比較簡單,主要思路就是單片機(jī)接收用戶鍵盤輸入的字符存入命令緩沖區(qū),并回顯到屏幕,當(dāng)用戶按下回車鍵,觸發(fā)軟件狀態(tài)機(jī)狀態(tài)變遷,從輸入態(tài)轉(zhuǎn)移到命令解釋態(tài),然后根據(jù)用戶命令調(diào)用相關(guān)子程序執(zhí)行相應(yīng)操作,執(zhí)行完畢后重新回到輸入態(tài)。
我感覺原理很好掌握,程序也不長,但是細(xì)節(jié)部分要反復(fù)調(diào)試多次才能穩(wěn)定工作。比如:命令行左右邊界的保護(hù)、退格鍵的處理、詞表的設(shè)計(jì)等等。
Shell程序由詞表、取詞子程序、狀態(tài)機(jī)框架程序(輸入回顯和命令解釋執(zhí)行)、命令相關(guān)子程序組成(詳見源程序清單)。
詞表結(jié)構(gòu)如程序清單所示,由詞數(shù)目,左括號數(shù),右括號數(shù),每個(gè)詞的具體信息(長度,字符串)構(gòu)成。左右括號數(shù)用于括號匹配檢查;詞數(shù)目用于程序循環(huán);詞的具體信息作為解釋/執(zhí)行程序的輸入?yún)?shù)。
取詞子程序從命令行語句中提取單詞并存入詞表同時(shí)進(jìn)行匹配檢查和詞法分析。默認(rèn)字符為:0-9、a-z、A-Z、'.';定界符為:空格、逗號,左/右括號。建議用戶補(bǔ)充默認(rèn)字符集(? / -)以便實(shí)現(xiàn)更靈活的語法。注意:現(xiàn)在版本的Shell只檢查左右括號數(shù)量的匹配,無優(yōu)先級和語法含義。
輸入回顯程序循環(huán)檢查用戶鍵盤輸入。如果輸入回車,程序狀態(tài)轉(zhuǎn)入解釋執(zhí)行態(tài);如果輸入退格(8)則回顯退格、空格、退格,模擬刪除字符,同時(shí)輸入緩沖區(qū)清除相應(yīng)字節(jié),清除前先檢查左邊界是否越界。如越界則鳴響報(bào)警且不執(zhí)行清除操作;其他字符輸入直接存入輸入緩沖區(qū)并回顯,此前檢查右邊界是否溢出,如果溢出則鳴響報(bào)警且拋棄剛輸入的字符。
命令解釋程序調(diào)用取詞子程序分析用戶命令行輸入,根據(jù)詞表第一個(gè)單詞在散轉(zhuǎn)表中的位置調(diào)用相應(yīng)執(zhí)行子程序處理命令,如果散轉(zhuǎn)表中無此單詞,則打印“Bad command!”。取詞子程序返回錯(cuò)誤指示時(shí)也打印此句。
命令解釋程序向相應(yīng)的命令相關(guān)子程序傳入詞表指針,具體執(zhí)行由用戶自行決定。由命令相關(guān)子程序返回后重新回到命令輸入態(tài),完成一次輸入執(zhí)行全過程。此過程周而復(fù)始地循環(huán)執(zhí)行。
Shell界面的命令按功能分為以下幾組:
1。操作系統(tǒng)相關(guān)命令:
查看就緒任務(wù)lt / 中止任務(wù)kill / 恢復(fù)任務(wù)執(zhí)行call / CPU利用率usage / 版本查詢ver / 查某個(gè)任務(wù)信息(TCB、堆棧內(nèi)容)lt
查看切換次數(shù)和時(shí)間lts
2。網(wǎng)絡(luò)相關(guān)命令:
顯示配置MAC地址macadr / 顯示配置主機(jī)IP地址host / 顯示配置子網(wǎng)掩碼mask / 顯示配置缺省網(wǎng)關(guān)gateway
顯示網(wǎng)絡(luò)配置總情況lc / 連通測試命令ping / 用戶數(shù)據(jù)報(bào)發(fā)送命令udp / telnet命令tel / 相關(guān)應(yīng)用命令**
顯示ARP高速緩沖區(qū)地址對ls / 顯示發(fā)送緩沖區(qū)信息lti
3。屏幕顯示相關(guān)命令:
清屏clr / 幫助help / 功能鍵F3、F7處理 / 組合鍵Ctrl+C、Ctrl+B處理
4。外設(shè)(閃盤X5045和I/O口)相關(guān)命令:
讀閃盤rdx / 讀I/O口rdp / 寫閃盤wdx
5。安全相關(guān)命令:
身份認(rèn)證密碼權(quán)限usr、pass
6。應(yīng)用相關(guān)命令:
用戶自行定義
用戶命令大小寫不敏感,程序?qū)⒚钭址y(tǒng)一成小寫形式。程序中各種參數(shù)(如:最大詞長度、詞數(shù)量……)定義成宏放在一個(gè)頭文件中,隨時(shí)可修改配置,很方便。Shell作為一個(gè)任務(wù)工作于內(nèi)核之外,占用一個(gè)任務(wù)號。
源程序:
詞表
typedef struct{
int Num;
int LeftCurveNum,RightCurveNum;
struct{
int Length;
unsigned char Str[MaxLenWord+1]; /*for '