轉自:http://www.zg4o1577.cn/bbs/dpj-59966-1.html
作者: 朱有田
本文僅討論筆者在實踐分時系統過程中的方法和思想,不拘泥于編程語言工具及其技巧,不用程序語言描述,全文采用敘事的方法,引出工程實踐過程中解決一些問題的過程和思路。
第一部分: 單CPU計算機運行的模型
計算機最基本模型是圖靈模型(相關知識自行腦補),計算機將所有要處理的復雜任務分解到有限的基本的操作(指令),這個操作的集合就是指令集,指令集被設計固化到硬件(CPU或處理器)中。程序是為了解決特定問題而編制的一個指令序列,計算機的運行就是一個在時間上串行的一個指令流。
如果有2個程序需要在一臺計算機上運行,常見的場景是先運行其中一個程序,運行結束后,再運行第二個程序。
第二部分: 單CPU計算機中多個程序并發執行
多個程序并行的概念出現的很早,早期為了共享昂貴的計算機資源,人們試圖使一個計算機為多個用戶同時提供服務。多個程序并發執行,采取分時的方法來實現,稱分時系統。
分時即將時間視為資源進行分配,將時間切分為人們感知上較小的一個單位,比如20毫秒(0.02秒),稱為一個時間片。若程序1和程序2都要被執行,每個程序輪流被執行一個時間片。從宏觀(感官)上來看,程序1和程序2是同時一起執行的,就像兩臺計算機在同步工作一樣。
若程序1和程序2同步運行,計算機是這樣進行的:先運行程序1,20毫秒后,切換到程序2,20毫秒后又切換到程序1,20毫秒后再次切換到程序2,如此反復… 一秒鐘切換了50次,程序1和程序2都在運行、暫停、運行、暫停這樣的狀態中進行,由于切換時間夠快,人的感官認為程序1和程序2是同步運行的。
第三部分: 中斷和定時器的作用
要實現分時的機制,離不開定時器和中斷機制。定時器就是定時發出中斷信號讓計算機能夠進入切換程序;中斷機制是指計算機的硬件要能夠支持在執行過程中被中斷,跳轉到指定的中斷程序中去運行,并在運行結束后能夠返回到被中斷的點繼續運行原來的程序。如果計算機硬件沒有中斷機制,則無法實現分時。
對于信號的輸入處理,程序總是以順序的方式進行的。比如按下一個按鍵,程序并不是立即就知道有按鍵被按下,而是要等到程序指針運行到按鍵處理程序時才會被發現。按鍵響應的速度取決于程序指針本身的運行速度和按鍵處理程序的長短及間隔距離,前者依賴于硬件主頻,后者依賴于程序設計的水平。這種依賴程序主動去發現的信號輸入方式一般稱查詢方式,與查詢方式相對應的是中斷方式。
中斷方式是設計在硬件級別的,假如按下一個按鍵采用中斷的方式處理,那么當按鍵按下時觸發中斷,系統執行完當前指令后保存當前的程序指針位置(返回時候用),然后將程序指針跳轉到指定的點,程序設計時在這個點編寫好按鍵處理程序,程序結束后返回到被中斷的點繼續運行。很顯然,中斷方式的響應時間很小且容易估算,程序也不需要“定時的去查詢”。中斷機制依賴于硬件提供的中斷資源(可接受的中斷源數量),往往比較有限;其次當多個中斷信號發生時,要考慮優先次序和中斷嵌套(中斷過程中再響應中斷)的問題。
事實上,中斷的實現也是一種查詢方式,當中斷信號發生時,計算機并不是真正的立即響應,而是要等待當前的指令被執行完,硬件才會去查詢是否有中斷信號存在。中斷也可以理解為在微指令級別(一個指令由一段微指令組成)下進行查詢處理的,其顆粒度是一條指令。
定時器是一種二進制計數器,其硬件上原理非常簡單:用一些邊沿觸發器串接起來就能構成一個二進制計數器,只要輸入一個方波,就能使計數器的數值加一。當計數器加到溢出時,將溢出信號引出到中斷信號,就能起到定時的作用。我們通過給定時器設置一個初始值來設置定時器時間的長短。
比如在程序中要等待一秒鐘的延時,如不用定時器,則必須讓程序指針在一個循環體中空轉N次,達到延時的作用。這種方式有兩個弊端:一是程序空轉浪費計算機資源,在這個延時過程中,程序啥也干不了,無法響應按鍵,無法刷新顯示,如同死機一樣;二是延時精度難控制,由于程序指令本身執行時間有長短之分,要達到精確的延時,程序設計要反復調教循環體空轉的次數。
如采用定時器進行一秒延時,只要在定時器內設置好初始值(使初始值到溢出的時間剛好為1秒),開啟定時器后,程序可以繼續執行其他工作,如按鍵掃描等。當1秒時間到達,定時器溢出發出中斷信號,使程序指針跳轉到中斷響應程序,程序在此進行延時后的處理。由此可以發現,定時器相當于硬件級別并行的設備,程序指針的跑動和定時器內計數值的增加是并行的。
言歸正傳,定時器和中斷是實現分時系統的基礎,其中中斷機制尤為重要,70年代流行的PDP系列小型機上,分時系統的定時中斷是由一個從工頻電源上獲取的50-60Hz的定時中斷外設產生,稱之為電源時鐘,令人腦洞大開。現代操作系統的祖宗UNIX就是在這種機型上產生的一種分時操作系統,UNIX最初的用戶是貝爾實驗室的文員們,他們用它來處理專利文書,由于多個終端上可以同步進行操作,又能方便進行共享和調用,得到當時女職員們的好評。
摘要:UNIX是用于DEC PDP-11及Interdata 8/32計算機的一個通用的交互作用的分時操作系統。從1971年開始以來,使用日趨廣泛。UNIX所論及的內容有以下領域: 1.文件結構:一個統一的、可隨機尋址的字節序列。取消“記錄”的概念。文件尋址的效率。2.文件系統設備的結構:目錄與文件。3.I/O設備與文件系統的一體化。4.用戶接口:外殼程序原理,I/O重新定向以及管道。5.進程環境:系統調用、信號、以及地址空間。6.可靠性:癱瘓,文件的丟失。7.安全性:損壞與檢查時數據保護;阻塞情況下的系統保護。8.一個高級語言的使用—收益與代價。9.UNIX系統沒有一般意義上的“實時”、進程間通訊、異步的I/O等功能。
UNIX內核由10,000行左右的C語言代碼和1,000行左右的匯編語言代碼所組成。匯編語言代碼又可進一步分成兩部份:一部份為200行,包括為提高系統效率而設計的那些代碼(可以用C語言來寫);另一部份為800行,包括不能用C語言寫的、執行硬件功能的那些代碼。
第四部分: 分時的意義
從程序設計角度,程序是一個單線順序的指令流(語句流),在程序里,指令(語句)一個接一個被執行,從開始到結束,或條件轉移,或反復循環。程序員安排好這一切前后處理的次序。在應用角度,用戶打開程序,運行程序,直到結束退出,再打開另一個程序。在DOS時代,人們就是這么做的,那真是一個單純的時代。
在分時系統下,多個程序并行,程序員設計程序時可以設計多個線程,比如一個線程負責顯示,一個線程負責鍵盤輸入,一個線程負責數據處理。每一個線程都是單獨的程序,最終他們將同步運行,程序員除了要考慮單個線程的運行,還要考慮線程之間的同步、通信和互斥。這改變了傳統的程序設計思維,使軟件的表現和可能性更加豐富多彩,程序設計手段也更加多樣。
在應用角度,當你在編輯文檔時,同時又在下載電影,耳機里又能播放著音樂。這些當下看起來稀松平常的事情,在計算機還是單任務時代真是難以想象的。
第五部分: 在80c52單片機上實踐分時系統
分時系統并不高不可攀,了解以上的概念后,任何有經驗的程序員都能對它進行實踐,筆者就是在最便宜和古老的51單片機上實踐了分時系統。
80c52是intel的一款增強型mcs-51單片機,51系列8位單片機自80年代開始至今仍被廣泛使用在各種場景,這種芯片很便宜,也很容易就能搞到。后來Intel將mcs-51授權給了多個芯片制造商。以ATMEL生產的AT89S52為例,它支持總計111條的51指令集,256字節RAM,8k字節程序閃存(可反復刷寫),3個16位計時器,5個中斷源,1個串口,4個8位IO口。要使她運行起來非常的方便:只需在XTAL管腳之間加一個晶振和2個27pF電容(晶振的頻率決定運行速度),在VCC腳加5V電源,然后把RESET管腳對地短路一下(復位),計算機便開始從程序閃存的0地址開始取指令執行..
沒玩過51單片機的讀者一定好奇程序是怎么編制到芯片的閃存里面去的。單片機程序的開發主要依賴個人電腦,首先通過文本編輯器編寫源程序,然后通過51的編譯器程序編譯成目標文件,最后通過芯片燒錄器將目標文件復制(燒入)到51芯片內。然后你的程序就可以在單片機跑動了。
事實上,無論是偉福,還是uvsion,都提供了包括編輯器、編譯器、仿真和調試為一體的開發環境,程序在編制完成后,直接在開發環境里面仿真調試,經過排錯后,將問題降到最低再燒入到單片機運行,這比反復燒寫芯片驗證程序效率要高的多。
最要緊的事情是:AT89S52具備實現分時系統的必要條件:1支持中斷,2具備計時器,3勉強夠用的RAM空間。
第六部分: 時間片的分配
實現分時系統的第一要考慮怎么進行時間片的分配,也就是程序之間的切換問題。這個很簡單,可利用一個定時器產生一個固定的延時,當延時到達,進入定時器中斷,將這個中斷的入口跳轉到切換程序。注意:程序的切換不是簡單的跳轉,而是要先保存當前程序的執行狀態、被中斷的地址,不至于下次回來繼續當前程序時變量、狀態都被改變,那就會亂套了。然后再選擇下一個要運行的程序,將被選中程序的狀態恢復到當前的各狀態寄存器當中,恢復到這個程序上次被打斷的點繼續運行,等待下一個時間片用完,進中斷,重復以上過程。
既然每個程序都要保存和恢復它的運行狀態,那么問題來了。一是要保存哪些信息,稱之為保護的范圍;二是需要多少內存,能夠支撐多少個程序進行分時。
第一個問題,保護范圍取決于用戶程序的斷點地址、狀態字、寄存器組,以及堆棧空間。第二個問題,支撐多少個程序進行分時取決于內存的大小,89C52的256字節支撐8個線程后,留給應用程序的內存只有36個字節,因此80c51的128個字節RAM是不夠的。
在此,為了區別于系統程序,改稱以上所謂被分時的程序為任務或線程。
第七部分: 內存組織的方法
我們能設想到最原始的方法是通過數組(數據表)的方式,將每一個線程的保護內容固定的保存起來。這種方法的缺點是不靈活,要增補被保護的內容就要重新定義數組,改動操作數據的代碼。當然還有更值得推薦的方法:用堆棧來組織內存。
堆棧是一種數據結構,數據如被壓入沖鋒步槍AK47彈匣的子彈,最先被壓進去的子彈,最后才被彈出到槍膛。在計算機內一般設有一個SP寄存器,存放堆棧指針,如果執行PUSH指令,SP先向前推一格,數據被寫入SP所指向的內存;如果執行POP指令,數據從SP所指向的內存讀出,SP再向后退一格。這種先入后出的結構能夠很好的支撐子程序嵌套調用時的數據存儲。舉一個栗子:
主程序運行時調用子程序A,當前的程序指針PC被壓入堆棧(必須留下跳轉點的腳印,否則子程序完成后返回不到原來被跳過去的點),接著PC跳轉到子程序A中開始運行,在子程序A里面又有調用子程序B,此刻的程序指針PC也被壓入堆棧,接著跳到子程序B中,子程序B完成后返回,將堆棧中最近保存的PC彈出到PC寄存器,程序返回到子程序A,子程序A完成后返回,從堆棧彈出最早壓入的PC,程序返回到主程序。
系統在線程之間切換時,將當前線程的所有需要保護的內容全部壓入堆棧,最后僅需要保存好它的堆棧指針(就像把東西全扔箱子里,只需保管好鑰匙)。將下一個線程的堆棧指針取出來,從堆棧彈出所有要恢復的數據,然后返回到新線程運行下一個時間片。
第八部分: 關于線程調度
當一個時間片用完,定時器發生中斷后,程序指針跳轉到線程切換程序,首先是保護現場。然后是選擇下一個線程。那么問題又來了,我們打算怎么來選擇下一個線程?這就是所謂的線程調度,操作系統的教課書里叫進程調度。
最原始粗暴的方法就是順序循環調度,所有線程先來后到排個隊,挨個輪著,實行平均主義的制度。在一些實時系統里面,比如μCOS,是按優先級來確定調度權的:永遠只選擇優先級最高的線程,也就是說,如果線程一直處于最高優先級,那么它就一直占著CPU不放,就不會被搶占。如果要讓線程立即得到執行,就必須設置最高優先級。這種方法能夠提供手段讓用戶設計的任務滿足實時的要求(所謂實時,就是對響應時間可評估,可控制,不是字面理解上的實時。)
除了優先級方式,還可以設定一種搶占的機制,比如在順序調度的過程中來一個半道插隊方式的搶占,也是一種可以滿足實時要求的方法。半路殺出個陳咬金,需要提前設置一個信號通知給調度程序,表示當前有某個線程需要插隊搶占。這里要考慮到原有的順序調度不能被打亂,或者被搶占的線程正好又是被輪到的線程,那么下次調度應繼續往后調度。
調度算法有很多,應按工程實際需求來設計,讀者不妨也可以設想更有創新意義的方法。
第九部分: 系統效率和延遲
分時總是要付出一些代價的,假如一顆CPU全力運行一個單程序,那么CPU利用率是100%。如果這顆CPU要分時運行兩個程序,那么每個程序對CPU的利用率都不足50%,因為切換要耗費CPU時間,切換次數越多越耗費,“切換可是要上稅的”。切換次數和時間片的設置相關,一般來說時間片設置范圍在1ms到20ms之間,時間片太小,浪費了大量CPU時間在上下文切換上,時間片太大,線程被輪到的時間就長,響應變差。一個線程的運行延遲在分時系統里很難被準確的估算,它與已就緒的線程數量、CPU主頻、時間片長短、中斷響應等各種因素相關,這些因素動態影響著單個線程的運行延遲。
分時系統中單個線程性能的一種簡單的估算方法:假如一個CPU工作在30MHz的主頻下,有3個已就緒的線程被平均調度,時間片相等,那么單個線程的運行性能相當于它獨占了一個10MHz不到的CPU的效果,相當于這個CPU被拆分成了3個10MHz不到的CPU在同步運行。由于現場就緒與否是動態變化的,因此很難完全準確估算。
總而言之,分時盡管耗費了一些CPU時間,但相對它所帶來驚奇效果和并行思維給程序設計帶來更多的可能性而言,微不足道。分時就像變魔術一樣,能把一顆CPU拆分成N個弱小的CPU同步運行不同的程序。從宏觀上,分時能夠充分挖掘CPU資源,當一個線程需要等待或有目的的延時,完全可以把CPU時間讓給其他線程,而不是白白的空等。
第十部分: 線程的休眠和就緒
當一個線程需要等待或延時,或者索性暫停掉,可以通過狀態標記,使調度程序跳過對它的調度,那么這個線程就是被休眠了,或稱該線程處于休眠態。反過來,如果線程等著被調度,稱該線程處于就緒態。這個功能在調度程序內做一個判斷不難實現。
如果要使一個線程休眠,系統可以提供一個函數(子程序)來修改線程的休眠標志(變量)。任何線程,包括線程自己都可以進行休眠操作。所謂想睡就睡,但不是想醒就醒,喚醒一定是其他線程或時鐘服務來幫助喚醒你。
除了休眠,還可以設置一個殺死的操作,所謂殺死線程,就是讓線程的狀態信息恢復初始化,下次再喚醒該線程,相當于要從頭開始執行。殺死線程,或線程自殺,可以理解為線程的復位并休眠。
可以設想一種簡單的設計場景:用線程1來處理用戶輸入,線程2、3、4為不同功能的獨立任務,線程1可通過用戶輸入選擇喚醒或休眠線程2、3、4來調取不同的功能。對于任務的開發來說,確實提供了與往常不同的手段,用的好,絕對順手。
第十一部分: 線程之間通信和互斥
感官上,線程是獨立運行的,微觀上,線程跑著跑著不知道什么時候就咔的被喊停,CPU被強行切換到別的線程去運行。也就是說,線程的運行是隨時隨刻隨地都可能被中斷的。
線程和線程之間要通信,一般通過全局變量、全局的數據結構(內存的一塊空間)來共享信息。比如線程A往全局變量內寫信息,線程B從該變量讀信息。以達到讓線程A的信息傳達給線程B的目的。那么問題又來了,如果線程A對全局變量寫了一半,咔,時間片到了,輪到線程B運行,這時候線程B從該變量讀到的信息就是一個錯誤的信息。
先來分析這個問題:
中斷是在指令和指令之間響應的,也就是其最小顆粒度是單條指令,如果寫變量是一條指令搞定的,由于指令在執行過程中不會被中斷,所以不會發生以上情況。單條指令的操作也稱為原子操作。
可惜的是,一條指令至多可寫一個字。對于沒有接觸過匯編的程序員來說,一條高級語言的語句很容易被認為是一個原子操作,其實不然,經過編譯后,一條高級語言的語句往往都由若干條指令組成。
思考解決問題:
要解決寫變量時候不被破壞的問題,就要排斥其他線程對這塊變量的操作,或防止在寫的過程中被中斷。方法有幾種:
最簡單粗暴的方法是開始寫共享變量之前直接把中斷給關了,寫完以后再開。還有更流氓的做法,就是在寫共享變量之前直接讓計時器停擺,定住,結束后再繼續往前計數。方法雖然簡單,但是這么做會讓整個系統短暫停滯,明顯有副作用。
推薦的方法是讓調度程序帶上鎖功能,如果處于上鎖狀態,調度程序不執行切換。線程在寫共享變量之前先加鎖,結束后再解鎖。在加鎖期間,雖然中斷和計時都不受影響,但是其他所有線程都被禁止了,還是有些缺憾。
更好的辦法是采用信號量,相當于給共享變量加一個紅綠燈。設置紅綠燈的操作必須是原子操作,線程在操作共享變量時先查一下是不是綠燈,如果是綠燈就把它變為紅燈,然后操作變量,結束后再設為綠燈;如果是紅燈就排隊等待,直到綠燈來了再進行操作。
名詞摘要:在教材中,把對需要互斥的共享變量和設備進行獨占操作,叫臨界區操作,結束后要退出臨界區。
第十二部分: 掛鐘和鬧鈴叫醒服務
線程在運行過程中經常會用到等待相對精確的延時、以及超時判斷等和時間相關的操作。傳統的單線程程序,經常用最粗暴的空操作指令循環來獲得一定的延時,這無可厚非,單線程中一段程序的運行時間能夠被準確的估算出來。多線程環境就不同了,一段程序的運行時間長短是不可準確估算的,取決于就緒線程的數量和時間片的長短,這些都是變化的。
需求場景1:線程需要就地等待1分鐘。
需求場景2:線程進入一個循環等待某個信號,當時間超過5秒鐘判斷為超時,退出循環。
以下是一種設計方案:
設計一個系統掛鐘,讓他一直以單位時間往前走。好比在墻上掛一個鐘頭,讓所有線程都能看時間。為每個線程提供一個鬧鈴指針和喚醒服務標志,如果某個線程需要喚醒服務,就把鬧鈴指針撥到目標時間,然后開啟喚醒服務。系統掛鐘每走一個刻度都要判斷當前時間和需要喚醒服務線程的鬧鈴時間是否一致,一致的話就喚醒這個線程。
當某個線程需要就地等待1分鐘就可以這樣做:線程先看一下當前的時間,然后把時間往前推1分鐘設置一個鬧鈴,然后告訴系統時鐘:鬧鈴到了記得叫醒我!接著線程就自我休眠了,等著1分鐘后系統時鐘的喚醒并繼續運行。
實踐的栗子:啟用硬件另一個計時器,每10ms發生一次中斷。系統中設置一個16位的字,每過10ms將這個字加一,這個16位字相當于一個量程為65536的掛鐘。設置一個數組用于存儲每個線程的鬧鈴時間,設置一個狀態字,用于標志線程的喚醒服務。當時鐘往前走一個刻度時,同時進行一個喚醒的判斷,將當前時間和鬧鈴時間一致的線程喚醒。
時鐘的最小刻度設定為10ms,也就意味著延時設定的最小單位是10ms,那么問題又來了,若需要延時1ms或更小的時間刻度該怎么解決?
如果把時鐘刻度直接調至1ms,會導致時鐘中斷次數增加了10倍,時鐘和鬧鈴的程序段的執行次數也就增加了10倍,這嚴重消耗了CPU時間,降低了整個系統的性能。如何解決這個問題留給讀者去思考。
第十三部分: 公用的函數(或子程序)
如果一個函數僅僅使用堆棧和局部變量,那么這個函數就能天然的被多個線程同時調用。因為堆棧和局部變量(或CPU寄存器)都是受保護的,假使2個線程同時調用一個函數,在線程切換的情況下,雖然都是運行在相同的函數代碼段,但各自線程里的局部變量和堆棧內容不同,所以運行結果也是各自分開的。這個函數就像使了分身之術一樣,在不同的線程里同時運行著。這種函數也叫作可重入函數。
如果一個函數使用了全局變量,那么假使2個線程同時調用了它,那么在線程A里面所定義的全局變量和線程B里面所定義的全局變量指向同一個位置,就會發生問題。這種不能被多個線程同時調用的函數,叫不可重入函數。
第十四部分: 后記
事實上,分時系統是現代操作系統的核心技術內容,它并不過時。本文所描述的內容涉及到操作系統的部分原理知識,當然描述不夠專業,沒有準確引用教材中的各種名詞和術語,不能完整的討論操作系統。但筆者希望本文能夠成為讀者對操作系統知識的一種指引,如果覺得意猶未盡,可以去翻一翻《操作系統》相關教材,看看專業的角度這些概念是怎么被描述的。很多程序員總是以為軟件能夠解決一切問題,其實不然,有些想法或算法不得不依賴硬件的支持。事實上,操作系統的各種思想也深刻影響了硬件的設計,CPU設計者在設計新的CPU時,充分考慮并納入了好多為操作系統考慮的事情。
現代操作系統往往鑒于UNIX這樣的通用操作系統來描述的,其內容除了講分時調度以外,還包括內存管理,外存管理(也就是文件系統),系統調用,設備驅動等內容。硬件往往基于個人計算機(intel的架構)來描述,頭緒非常多,讓學習者覺得課程就像是空中閣樓,可遠觀而不可觸碰。
用單片機做實踐是最能摸到計算機本質的路徑,試想,在一臺裸機上讓你的程序最接近硬件的跑動起來,是多么踏實的趕腳(好吧,牽強了)。通用計算機和單片機原理是一樣的,只不過通用計算機規模更大,主頻更高,為了達到更大的規模,架構起來就要用更多的手段和技巧。
比如在通用計算機內,程序指令并不是固化在ROM或閃存內的,而是存放在低速容量更大的外存(硬盤、軟盤、光驅、U盤)中,需要執行程序時,把程序文件成塊的復制到內存中,CPU從內存中取指令執行。那么問題來了,程序文件必須被裝載在指定內存地址段,(跳轉指令中包含了具體的地址),如果地址和指令錯位,那么程序跳轉將找不到北。要解決這個問題,CPU的設計者從早期的內存分段管理,發展到現在的虛擬地址空間轉換,可謂費盡心思。隨著程序規模越做越大,當單個程序的長度超出內存的總容量時,問題又來了。為了解決這個問題,又引入了分頁和虛擬內存的方法,分頁允許程序不用一次性全部調入內存,而是將要被執行部分的頁面調入內存,剩余的還是存在硬盤里,當程序執行的指令已經不在內存時,CPU將會發生一個缺頁中斷,將硬盤中的目標頁調入內存,將不用的頁調回到硬盤。這些都是內存管理單元MMU做的事情,而MMU基本是硬件實現(做在CPU內部),但它需要軟件配合才能用起來。所有的這些機制都是為了解決過程中碰到的問題而提出的解決方案,要搞清楚源頭。
討論操作系統離不開信息安全的話題,黑客和病毒總是存在。理論上,用戶程序如果要想搞破壞是非常容易做到的,單靠軟件防止黑客或病毒的攻擊理論上是做不到的,因為理想狀態下所有指令都是公平對等的,你不能用程序指令去阻止用于惡意破壞的程序指令,比如故意改寫數據(破壞正確的數據)。所以,計算機安全必須依靠硬件提供的機制,例如CPU設計者將處理器的運行分成多個狀態,低權級狀態下只能執行部分安全的指令,高權級狀態下才可以執行全部指令,用戶程序只能運行在低權級,需要進行讀寫操作時須通過調用系統提供的功能調用來實現,系統是工作在特權級下的,這樣就為計算機安全提供了可控手段。
性能是計算機永恒的話題。分時系統上就緒的線程數量越多,單個線程的性能就越低(可視為CPU的性能被線程瓜分了,線程越多,單個線程瓜分到的性能就少)。為了提升計算機性能,除了設置流水線、多級緩存等方式外,單個CPU主要靠提升主頻來實現性能的數量級提升。除此之外,引入多個CPU并行工作,通過增加系統的并行度也是提高性能的主要手段。多個CPU使得線程從物理上真正的并行了,一個多核的CPU在同一時刻可以同時運行多個線程,多CPU在硬件組織上又是一件費盡心機的活(不要簡單的理解成多插幾個CPU就完事那么簡單)。超級計算機(用于氣象、原子能、航天等領域的測算)往往以集群方式組織起來的高度并行的結構,這也要求軟件設計必須要被分割為高度的并行化才能發揮超級計算機的性能。如果一個問題的算法必須至始至終前后串行,無法被分割成多個同步運行的部分,那么這種程序放在超級計算機上運行,縱使這臺超算有千萬個CPU,程序也只能在其中一個CPU上跑完,剩下的CPU等于閑著沒事干。同樣的道理,多核CPU體現的也是并行性能,只有在線程數量多的分時系統內才能體現其性能。如果用一個8核的CPU僅僅只運行一個線程,那么剩下7個核心就閑著沒事做。
備注:本文一直采用線程來描述并發執行的程序,其實線程是專有名詞,有更清晰的概念。操作系統中還有進程的概念,進程也是并發的程序,一個進程還能再分多個線程。在實時操作系統中,線程又被稱為任務。
寫在最后
通過以上敘述,我想讀者一定對分時、多線程、并發執行有了概念,同時也一定留下了一堆的疑問和不解,這可是好現象,進步都是在疑問和不解中開始的。
還有2個應用例程可以從這里下載:http://www.zg4o1577.cn/bbs/dpj-59966-1.html
豆豆的打開關上
需求:假設有一個指針,可以作圓周運動,通過在鍵盤上輸入具體的角度,使指針指向該角度。
輸入:數字鍵盤,按3位數字后回車生效。范圍0-999,表示圓周角度,對360度取模。
顯示:3位段式液晶數碼顯示。
執行:減速步進電機,按輸入的角度作圓周運動。
結構:木結構為主,在電機上安裝臂桿,做成類停車桿的結構。可360度自由轉動。
技術上要考慮的問題:
1、液晶驅動的問題,擬采用HT1621芯片驅動;
2、歸零問題,擬采用12V電池+5V模塊的供電方式,設置斷電繼電器和斷電歸零程序;
3、轉動角度問題,單步判斷逼近方法,or距離計算一次到位方法;擬采用前者;
操作模型:
1、打開開關,液晶顯示---;臂桿角度為0;(開關處并聯的繼電器吸合,開關的觸點留一組去cpu)
2、輸入0-360之間的一個整數表示圓周的絕對角度,每輸入一個數字,顯示到液晶屏;其他輸入作無效處理;
3、回車后,液晶屏內的數字被作為目標值,執行電機轉到該角度;
4、轉動過程中,可以按暫停鍵,暫停或繼續;
5、轉動過程中,可以繼續輸入新的角度值,按回車后變更新的目標值執行。
6、執行完成后,電機去電待命。
7、開關關閉,cpu檢測到關機指令,執行歸零程序,完成后,繼電器斷開。(歸零程序不可被中斷,歸零后須再確認關機指令,若還是關機,則繼電器斷開。)
實施步驟:
1、整體方案構思;資料消化;
2、硬件草圖;
3、電機驅動試驗,正反向,速度和角度;
4、液晶顯示試驗;
5、按鍵掃描模擬和試驗;
6、軟件構思;
7、編碼和模塊測試;
8、最終調試。
全局變量:當前值,目標值,暫停標志,鍵值,顯示數字,ram區,8拍數組;
按鍵掃描獲取鍵值(任務2)
鍵值改變數字or啟動執行(轉換為目標值)or暫停標志變更,無效值;新的數字來了,要標志,用于判斷回車鍵的作用;數字 轉換 成ram區;ram區寫入HT1621的ram;(任務3)
暫停標志沒有時and當前值!=目標值時執行:目標值在左側還是在右側:前進一步或后退一步,延時,返回;(任務4)
[一個8拍數組;前進一步,當前值(0-4096變更);后退一步,當前值(0-4096變更);]
不停的查開關標志,若關機,改變目標為0,執行,等待執行完成,再判斷是否關機,關斷電源繼電器。否則返回。(任務0)
- ;kernel: sys51 r0.99
- ;project name: doudou's up & down
- ;designer: ut
- ;version: 1.0
- ;date: 2016/11/1
- ;=============================================================================================
- ; TIME-SHARING SYSTEM FOR MCS51 RELEASE 0.99
- ; UT.ZUZU
- ; COPYRIGHT(2012/5/10-2016/11/--)
- ;=============================================================================================
- ;詳細請查看手冊
- ;硬件要求
- ;1、52系列兼容的51單片機,內存256字節或以上。本程序在AT89S52運行,24.576MHZ晶振,改變晶振需調整計數器值。晶振頻率越高,控制器性能越好。
- ;2、256字節內存中,系統使用了大部分高地址部分,0-47由用戶支配,具體請看內存分配說明。
- ;3、一共8個線程:TASK0到TASK2為3個主線程,其余為次線程;主線程對9個寄存器和PSW、AB、DPTR進行保護,并預留堆棧最大嵌套調用為7級;次線程僅保護PSW,AB,R0-R3,最大嵌套調用為2級。
- ;4、系統可以在調度程序中喂看門狗,時間片不可過大,超過4MS不喂狗看門狗發出系統復位信號。看門狗功能可以在配置定義中取消。
- ;5、分時過程通過定時器0進行,其初值定義在T0_VALUE中,目前設置的是5MS時間片。
- ;6、系統時間記錄在SYS_TIME變量中,通過定時器2進行,目前設置是10MS加1.
- ;任務操作說明
- ;0、任務的邊界應該是循環。不建議跳出邊界。盡可能的使用系統提供的調用。
- ;1、不同任務可以調用同一個子程序,注意子程序內受保護的范圍。
- ;2、主任務擁有獨立的R0-R7、ACC、B、PSW寄存器、DPTR指針。次任務僅保護7個寄存器。
- ;3、任務之間可以通過內存變量來傳遞信息,注意在寫內存時必須占用系統,寫完后再釋放系統,建議使用加鎖和解鎖調用。
- ;4、系統初始化后所有任務都是睡眠的,系統會喚醒任務0和任務7,其他任務的喚醒由用戶操作。任務7為伺服任務,不建議休眠它或在該任務中使用系統延時調用。(有一種風險:所有任務處于休眠態,會進入待機)
- ;5、系統的性能與晶振頻率、喚醒的任務數量、任務占用的時間片有關系。
- ;6、任務有權殺死或休眠任何任務,如果系統所有任務都被殺死或休眠,系統會進入節電POWER_DOWN模式,等待復位激活。
- ;7、系統提供10MS刻度的16位系統時間,由TIMER2來完成。任務可以根據自己需要來完成延時功能,其性能優于普通的空等待DELAY子程序。
- ;8、任務不可以操作TIMER0和TIMER2這兩個定時器,需要時,可以使用TIMER1. 建議不要設置為高優先級,可能導致系統時間停走。
- ;注:殺死和休眠的區別:任務被殺死后再次喚醒從頭開始運行,任務被休眠后再次喚醒是從原來休眠的地方繼續運行(就像暫停)。
- ;用戶使用注意:
- ;1.總計8個任務,單個線程是死循環,所有線程并發執行,可以有限調整每個線程的時間片,默認5MS時間片,合理使用可以滿足實時要求。
- ;2.任務0到2是主線程,線程內寄存器A和B,R0-R7,DPTR都受保護,子程序嵌套調用最大達8級。
- ;3.任務3到7是次線程,線程內寄存器A和B,R0-R3受保護,子程序嵌套調用最大2級。注意這個限制條件。嵌套調用超限將導致堆棧過界破壞,使系統崩潰。
- ;4.用戶只能使用0-59之間的內存空間。
- ;5.用戶無需考慮堆棧的分配,禁止任務程序修改堆棧指針SP。
- ;6.中斷響應程序中要注意保護現場和恢復現場。
- ;2012-5-22 R0.91 占用系統和釋放系統改用停止和開啟計時器的方式實現。UNDEBUG
- ;2012-5-23 R0.92 喂狗簡化到CPL指令 UNDEBUG
- ;使用時注意:所有中斷程序內要用到PSW,A,B,R0~R7,DPTR,必須事先暫存,返回前恢復,注意它們不受保護
- ;2016-11-08 確認BUG和注釋。可以作為穩定版。
- ;2016-11-08 R0.99
- ;為了增強實用性,擬重新布局內存,改用堆棧方式保護現場,保證3個主線程,每個線程分配29字節,增加對DPTR的保護,;增加對PSW的保護
- ;可以最大嵌套29-2(PC)-2(DPTR)-10(AB,RG)-1(PSW)=7個CALL;閹割剩余5個次線程,每個線程分配13字節:AB,R0-R3,PC,PSW,最大嵌套2個CALL。
- ;總體256字節的內存:3個主線程:29*3=87;5個次線程:13*5=65;8個線程狀態(優先級、SP、鬧鈴H、鬧鈴L)=32;
- ;系統變量:10;系統堆棧:14;R0-R7:8個除去。 剩余用戶可用的內存區:40字節
- ;真可謂:螺螄殼里做道場。
- ;20161110
- ;備忘:調度程序要增加加鎖功能(不切換,好處是總是能進系統區做一些系統要做的事,比如喂狗)
- ;延時誤差太大,最大誤差是一個單位(不累計),考慮系統(放在時鐘程序內)來負責高精度大跨度的計時,和喚醒服務,需要額外16字節用于8個線程的鬧鐘記錄。
- ;看門狗使用指南:時間片調節的太大就會觸發看門狗,應能根據需要關閉看門狗。13位,每一個機器周期+1
- ;時鐘要方便配置
- ;注意中斷嵌套的影響 ;注意測量 系統服務的時間,及其與中斷時間的比重,比重和效率成正比 ;中斷響應前后次序的關系分析,用戶怎么用中斷
- ;喚醒服務要注意的是:必須留一個伺服線程,該線程始終保持就緒(不能使用系統延時)。否則有一種風險:所有任務同時調用系統延時而休眠,調度程序將轉入節電模式。要復位或外部中斷才能恢復。
- ;20161111 r0.99基本調試完成
- ;0.99版本比較0.92版本特點如下:
- ;1、充分利用堆棧的特點布局內存,使得保護內容的調整變的靈活。
- ;2、不改變8個任務的總數,但集中資源到3個主任務上,增加對psw、dptr寄存器的保護(原來沒考慮周全,如psw是必須保護的)。使主任務不再有束縛。
- ;3、取消原有的延時服務,增加系統時鐘的定時喚醒服務功能,每個任務可以設置自己的延時時間,然后進入休眠態等待,時間到了系統時鐘會喚醒你。
- ;4、改變了殺、休眠、喚醒的方式,采用位表示殺死信號、就緒態、喚醒服務,可以用邏輯的方法快速操作。
- ;5、增加了調度程序的加鎖功能,加鎖狀態下,調度程序不進行任務切換,但繼續執行其他系統功能。
- ;6、看門狗、初始時間片可配置。
- ;7、任務7作為伺服線程,可以做一些簡單的脈搏動作。伺服線程必須始終就緒,否則有任務全部休眠的風險。
- ;0.99的篇幅反而比0.92下降了7%,除了更加實用以外,顯得更加優美。
- ;實際應用達到3個以上時,修復一些潛在的bug之后,可以升為r1.0版本,并出一份《51多任務內核的應用手冊》
- ;內存地圖規劃
- ;0-47 用戶
- ;48-63 鬧鐘數組-每個任務2個字節,用于指示鬧鐘時間 /30H
- ;64-73 系統變量 /40H
- ;74-87 系統堆棧 7個CALL 包括中斷 SP_SYS:73 /49H 再壓縮至6個call
- ;88-95 任務優先狀態字節 /58H
- ;96-103 任務SP指針 /60H
- ;104-132 任務0堆棧 SP0:103 /67H
- ;133-161 任務1堆棧 SP1:132 /84H
- ;162-190 任務2堆棧 SP2:161 /A1H
- ;191-203 任務3堆棧 SP3:190 /BEH
- ;204-216 任務4堆棧 SP4:203 /CBH
- ;217-229 任務5堆棧 SP5:216 /D8H
- ;230-242 任務6堆棧 SP6:229 /E5H
- ;243-255 任務7堆棧 SP7:242 /F2H
- ;NOTE:OPRATING SFR OR RAM WHERE HAVE THE SAME ADDRESS WITH EACH OTHER WILL BE ATTENTED! CARE <DATASHEET OF AT89S52>
- ;-------------------------------------------------------------------------------
- ;標號定義
- PRI_BYTE EQU 0D8H ;INIT PRIORITY OF EVERY TASK ;時間片;0.5MS:0FCH,1MS:0F8H,2MS:0F0H,5MS:D8H,10MS:B0H,20MS:60H 這里是參考值,初始化時間片請定義在PRI_BYTE
- SYS_SP EQU 4bH ;SYSTEM STACK HEAD
- START_TASK_SP EQU 67H
- TAB_PRI EQU 58H ;基址 見內存分配規劃
- TAB_SP EQU 60H ;基址
- TAB_CLK EQU 30H ;BASE
- WDT_PIE EQU 00H ;設置為1E,看門狗開啟,其他值則關閉看門狗 NO TEST 13位計時器1FFF復位,合計4MS :意味著啟用看門狗時,時間片必須小于4MS,占用系統時也要注意這個問題,建議用加鎖功能代替占用系統
- ;系統全局變量定義
- sys_bit_byte equ 2fh ;留給系統的8個標志位 位地址78-7fh
- TMP_A EQU 40H
- TASK_CURT_P EQU 41H ;當前的任務指針
- task_sch_p equ 4ah ;調度任務指針
- CLK_ALARM EQU 42H ;鬧鐘字節 從左到右每一位依次標志任務0到7的鬧鈴請求,1為有鬧鈴請求
- DEAD_SIG EQU 43H ;從左到右每一位依次標志任務0到7的殺死請求,1為有殺死請求
- READY_BYTE EQU 44H ;從左到右每一位依次標志任務0到7的就緒狀態,1為就緒
- LOCK_BYTE EQU 45H ;5A表示加鎖,其他值表示解鎖
- TMP_SP EQU 46H
- WDT_BYTE EQU 47H ;狗盆子
- SYS_TIME_H EQU 48H ;系統時鐘高8位
- SYS_TIME_L EQU 49H ;系統時鐘低8位
- nouse equ 4bh ;預留
- preempt_bit bit 78h ;是否搶占
- delay_sv_bit bit 79h ;定時器1中斷服務 標志 用于小刻度的延時需求
- preempt_task EQU 2eh ;搶占任務號 僅0-7有效,搶占后作廢,用于調度程序切換到指定的任務去。
- delay_times equ 2dh ;用于timer1計時刻度的次數
- ;系統晶振:24.576MHZ
- T0_VALUE_H EQU 0D8H ;時間片;0.5MS:0FCH,1MS:0F8H,2MS:0F0H,5MS:D8H,10MS:B0H,20MS:60H 這里是初始賦值,初始化時間片請定義在PRI_BYTE
- T0_VALUE_L EQU 00H
- T2_VALUE_H EQU 0B0H ;時鐘刻度 參考上面
- T2_VALUE_L EQU 00H
- T1_VALUE_H EQU 0fcH ;時鐘刻度 參考上面 500us
- T1_VALUE_L EQU 00h ;
- ;------------------------------規劃程序入口
- ORG 00H
- JMP SYS_START
- ORG 03H
- ;LJMP INT_INT0 ;(INT0)
- RETI
- ORG 0BH
- ;LJMP INT_T0 ;(IF0)
- LJMP SHARE_SYS
- RETI
- ORG 13H
- ;LJMP INT_INT1 ;(INT1)
- RETI
- ORG 1BH
- ;LJMP INT_T1 ;(IF1)
- JMP sys_ms_svrs
- RETI
- ORG 23H
- ;LJMP INT_RTX ;(RI,TI)
- RETI
- ORG 2BH
- ;LJMP INT_T2 ;(IF2)
- JMP SYS_TIME_RUN
- RETI
- ;標記中斷返回:如果意外中斷,直接返回,不至于跳飛;-)
- ;以下是任務的入口,應和表格中定義一致
- ORG 30H
- LJMP TASK_0
- ORG 38H
- LJMP TASK_1
- ORG 40H
- LJMP TASK_2
- ORG 48H
- LJMP TASK_3
- ORG 50H
- LJMP TASK_4
- ORG 58H
- LJMP TASK_5
- ORG 60H
- LJMP TASK_6
- ORG 68H
- LJMP TASK_7
- ;;開機,從00H跳過來*******************************************
- SYS_START:
- MOV SP,#SYS_SP ;SYSTEM STACK
- MOV WDT_BYTE,#WDT_PIE ;準備好狗糧
- clr preempt_bit
- clr delay_sv_bit
- mov preempt_task,#0
- CALL INIT_RAM ;初始化系統內存
- CALL INIT_TIMER ;初始化定時器
- CALL USER_INIT ;用戶初始化程序
- CALL SYS_TIMER_START ;啟動系統定時器
-
- MOV DEAD_SIG,#0 ;清空殺手信號
- MOV R1,#0F8H;
- MOV R0,#7;
- CALL SET_PRIBYTE ;任務7時間片設置為1MS
- MOV READY_BYTE,#10000001B ;任務0就緒,任務7當作伺服線程,如果沒有一個線程就緒,會進待機
- MOV TASK_CURT_P,#0
- MOV TASK_sch_P,#0
- MOV TAB_PRI,#PRI_BYTE ;任務正常運行的要素:不被殺,就緒,優先級(時間片)不要太長(看門狗會叫),SP狀態
- MOV SP,#START_TASK_SP
- LJMP TASK_0 ;進入任務0,啟動分時,START SHARE
- ;;上面用到的子程序:任務1到7依次初始化各自內存空間-----------------------------
- INIT_RAM:
- MOV R0,#7 ;以此對各任務進行內存初始化賦值
- ITR0:
- CALL TASKRAM_INIT
- DJNZ R0,ITR0 ;TASK0任務作為系統啟動的入口,可以不用初始,其內容會在第一個時間片中斷后調度程序會給予。
- ;CALL TASKRAM_INIT ;JUST FOR TEST TASK0 RAM INIT
- RET
- ;以下表格用于初始化內存用
- TAB_1:
- DB 067H,084H,0A1H,0BEH,0CBH,0D8H,0E5H,0F2H,00H ;任務棧頂地址
- TAB_2:
- DB 030H,038H,040H,048H,050H,058H,060H,068H,00H ;任務入口地址 和ORG 30H.. 對應
- ;上面用到的子程序:開機初始化任務內存操作:1、根據任務號查表得棧頂位置、入口位置;2、在棧頂壓入:入口、現場;3、將SP存到SP_I; 4、清就緒態
- ;初始化任務內存分主次 ;任務號先存R0
- TASKRAM_INIT:
- MOV A,R0
- MOV DPTR,#TAB_1
- MOVC A,@A+DPTR ;查表得初始SP
- MOV TMP_SP,SP
- MOV SP,A ;開始壓棧
- MOV A,R0
- MOV DPTR,#TAB_2
- MOVC A,@A+DPTR ;查表得初始PC
-
- MOV 02H,A
- PUSH 02H ;PUSH PC_L
- MOV 02H,#0
- PUSH 02H ;PUSH PC_H PC是16位的
- PUSH 02H ;PSW,AB,R0-R7,DPTR
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- ;區分主次任務
- ;任務號大于2則跳過以下步驟
- CLR C
- MOV A,#2
- SUBB A,R0
- JC TKI00
- PUSH 02H ;R4 R5 R6 R7 DPL DPH
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- TKI00:
- ;保存SP到數組,SP--> SP_I
- MOV A,#TAB_SP
- ADD A,R0
- MOV R1,A ;這個是指針變量,指向當前SP的存放地址
- MOV @R1,SP ;記錄SP
- MOV SP,TMP_SP ;壓棧完成,恢復SP
- ;優先級字節賦值初始值
- MOV A,#TAB_PRI
- ADD A,R0
- MOV R1,A
- MOV @R1,#PRI_BYTE
- ;清就緒態
- CALL CLR_READY_BIT
- RET
- ;子程序:以下初始化系統定時器 TIMER2 DEBUGED 120516 --------------------------
- INIT_TIMER:
- ;TIMER2 SETUP
- MOV 0C8H,#00H ;MOV T2CON,#00H
- MOV 0C9H,#00H ;MOV T2MOD,#00H
- MOV 0CCH,#T2_VALUE_L ;MOV TL2,#T2_VALUE_L
- MOV 0CDH,#T2_VALUE_H ;MOV TH2,#T2_VALUE_H
- MOV 0CAH,0CCH ;MOV RCAP2L,TL2
- MOV 0CBH,0CDH ;MOV RCAP2H,TH2
- ;TIMER0 SETUP
- ANL 88H,#11101111B;TCON CLR TR0 : STOP TIMER0
- ANL 89H,#11110000B ;TMOD(SET TIMER0)
- ORL 89H,#00000001B ;TMOD(SET TIMER0) MODE:01 16BIT COUNT UP
- MOV 8AH,#T0_VALUE_L ;TL0
- MOV 8CH,#T0_VALUE_H ;TH0
- ;TIMER1 SETUP
- ANL 88H,#10111111B;TCON CLR TR0 : STOP TIMER1
- ANL 89H,#00001111B ;TMOD(SET TIMER0)
- ORL 89H,#00010000B ;TMOD(SET TIMER0)MODE:01 16BIT COUNT UP MODE:02 8BIT autoCOUNT UP
- MOV 8bH,#T1_VALUE_L ;TL0
- MOV 8dH,#T1_VALUE_H ;TH0
- RET
- ;子程序:啟動TIMER0和TIMER2 ;DEBUGED 120516---------------------------------
- SYS_TIMER_START:
- MOV SYS_TIME_H,#00
- MOV SYS_TIME_L,#00
- MOV IP,#00000000B ;SET PRIORITY
- MOV IE,#10101010B ;SETB EA ;SETB ET2 ;SETB ET0 ET1 TO ENABLE INTERUPT OF TIMER2 AND TIMER0 AND TIMER1
- ORL 88H,#01010000B ;TCON SETB TR0,TR1 START TIMER0 TIMER1
- ORL 0C8H,#00000100B ;ORL T2CON,#00000100B ;SETB TR2 TO START TIMER2
- RET
- ;;系統時間處理,在TIMER2中斷后跳進來
- ;系統時間處理有2大內容:1、比較各鬧鐘的目標時間是否到達,到達并且該任務有喚醒服務,就執行喚醒;2、時鐘刻度加一。
- SYS_TIME_RUN:
- CLR EA
- MOV TMP_SP,SP ;保存A 保護現場
- MOV SP,#SYS_SP ;--------------------------------界面,以下系統區
- MOV TMP_A,PSW
- PUSH TMP_A
- MOV TMP_A,A ;PUSH A
- PUSH TMP_A
- MOV TMP_A,B ;PUSH B
- PUSH TMP_A
- PUSH 00H ;PUSH R0
- PUSH 01H ;PUSH R1
- PUSH 02H ;PUSH R2
- PUSH 03H ;PUSH R3
- ;處理CLK_ALARM字節、TAB_CLK數組
- MOV A,CLK_ALARM
- JZ STR00 ;沒有服務時跳過
- MOV R0,#TAB_CLK
- MOV R3,SYS_TIME_L
- CALL PROC_CMP_BYTE ;低8位比較
- MOV R1,A
- MOV R0,#TAB_CLK
- DEC R0
- MOV R3,SYS_TIME_H
- CALL PROC_CMP_BYTE ;高8位比較,對比結果保存到A 1表示相等 0表示不等
- ANL A,R1 ;H和L的比較結果合并
- MOV R1,A
- MOV A,CLK_ALARM
- ANL A,R1 ;與喚醒服務合并
- ORL READY_BYTE,A ;執行喚醒
- MOV A,R1
- CPL A
- ANL CLK_ALARM,A ;清喚醒標志,表示完成喚醒
- STR00:
- ;16位系統時鐘+1 放在后面處理,延時00時可立即生效
- MOV A,SYS_TIME_L
- INC SYS_TIME_L
- INC A
- JNZ $+4
- INC SYS_TIME_H
- ANL 0C8H,#01111111B ;ANL T2CON,#01111111B ;CLEAR TF2 清TIMER2中斷標志
- POP 03H ;POP R3
- POP 02H ;POP R2
- POP 01H ;POP R1
- POP 00H ;POP R0
- POP TMP_A
- MOV B,TMP_A ;POP B
- POP TMP_A
- MOV A,TMP_A ;POP A ;恢復現場
- POP TMP_A
- MOV PSW,TMP_A
- MOV SP,TMP_SP ;---------------------------------------------界面,以上系統區
- SETB EA
- RETI
- ;;;;;;;中斷返回
- ;用于刻度為500us,次數255的等待服務。只提供一個線程使用,出于系統消耗的考慮,500us中斷必須篇幅足夠小。
- ;定時器1中斷服務:500us中斷一次,無服務直接返回。有服務:次數(time_us字節)為0則讓waiting_task_p任務搶占(標志完成)。不為0時,減一。
- ;系統需要用一個字節的標志位2fh,用戶要避開。
- ;preempt_bit bit 78h ;是否搶占
- ;delay_sv_bit bit 79h ;定時器1中斷服務 標志 用于小刻度的延時需求
- ;preempt_task EQU 3fh ;搶占任務號 僅0-7有效,搶占后作廢,用于調度程序切換到指定的任務去。
- ;delay_times equ 3eh ;用于timer1計時刻度的次數
- sys_ms_svrs:
- jb delay_sv_bit,smsv0 ;無服務直接返回
- MOV 8bH,#T1_VALUE_L ;TL1
- MOV 8dH,#T1_VALUE_H ;TH1
- reti
- smsv0:
- ;保護現場
- mov tmp_a,a
-
- ;查delay_times次數:等于0時,置搶占任務preempt_bit
- mov a,delay_times
- jnz smsv1
- setb preempt_bit ;置搶占位
- clr delay_sv_bit ;清服務位
- ORL 88H,#00100000B ;SETB TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- ;中斷不嵌套,本次中斷返回后進入系統中斷
- MOV 8bH,#T1_VALUE_L ;TL1
- MOV 8dH,#T1_VALUE_H ;TH1
- mov tmp_a,a
- reti
- ;次數減一
- smsv1: dec delay_times
- ;恢復現場
- mov a,tmp_a
- MOV 8bH,#T1_VALUE_L ;TL1
- MOV 8dH,#T1_VALUE_H ;TH1
- reti
- ;;上面要用到
- ;;子程序:基地址存R0(間隔1個字節的8個數組),與系統時鐘(H或L字節)R3進行比較,8次,結果存放在ACC對應的位里面,1表示相等
- PROC_CMP_BYTE:
- MOV B,#0
- MOV R2,#8
- MOV A,#15
- ADD A,R0
- MOV R0,A
- PCB00:
- MOV A,@R0
- CJNE A,03H,PCB01
- MOV A,B
- SETB C
- RRC A
- JMP PCB02
- PCB01:
- MOV A,B
- CLR C
- RRC A
- PCB02: MOV B,A
- DEC R0
- DEC R0 ;間隔1字節的指針,從右到左
-
- DJNZ R2,PCB00
- MOV A,B
- RET
- ;;調度程序,在timer0中斷后跳過來。
- ;;調度程序的內容:1、保護現場;2、存sp;3、喂狗、執行任務死刑、判斷加鎖;4、切換下一個就緒的任務指針;5、調取新任務的時間片設置到定時器;6、調取新sp;7、恢復現場;8、返回到新任務。
- SHARE_SYS: ;保護現場先
- MOV TMP_A,PSW ;PUSH PSW
- PUSH TMP_A
- MOV TMP_A,A ;PUSH A
- PUSH TMP_A
- MOV TMP_A,B ;PUSH B
- PUSH TMP_A
- PUSH 00H ;PUSH R0
- PUSH 01H ;PUSH R1
- PUSH 02H ;PUSH R2
- PUSH 03H ;PUSH R3
- ;區分主次任務
- ;TASK_CURT_P 大于2則跳過以下步驟
- CLR C
- MOV A,#2
- SUBB A,TASK_CURT_P
- JC SS00
- PUSH 04H ;PUSH R4
- PUSH 05H ;PUSH R5
- PUSH 06H ;PUSH R6
- PUSH 07H ;PUSH R7
- PUSH DPL
- PUSH DPH
- SS00: ;存SP到數組SP
- MOV A,#TAB_SP
- ADD A,TASK_CURT_P
- MOV R0,A ;這個是指針變量,指向當前SP的存放地址
- MOV @R0,SP ;記錄SP
-
- ;切換SP,以下進入系統區----------------------------------------------------------------INTERFACE
- MOV SP,#SYS_SP ;SP指向系統SP
- CALL WDT ;喂狗
- CALL KILL_TASK ;根據DEAD_SIG字節,執行任務的死刑 ;-*
- ;是否上鎖,如果上鎖 LOCK_BYTE= 5AH 則不執行任務切換
- MOV A,LOCK_BYTE
- CJNE A,#5AH,SS04
- JMP SS05
- SS04:
- mov r1,task_sch_p ;暫存
- MOV R6,#10
- SELECT_P: ;選擇下一個任務
- DJNZ R6,SS01 ;選擇次數計時,如果連續選擇超10次就得進節電模式了
- MOV P1,#0FFH
- ORL 87H,#02H ;INTO POWER-DOWN MODE
- LJMP SYS_START ;醒來的話就重新開機咯
- ;切換任務指針(0-7) 全局變量TASK_sch_P 任務指針,僅此進行寫操作
- SS01:
- INC TASK_sch_P
- MOV R0,TASK_sch_P
- CJNE R0,#8,SS02 ;超限
- MOV TASK_sch_P,#0
- ;判就緒位,不在就緒態就跳回 SELECT_P,重復以上步驟
- SS02:
- MOV R0,TASK_sch_P
- CALL GET_READY_BIT
- JNC SELECT_P
- ;調度結束,新的指針在task_sch_p
- ;是否有搶占信號
- jnb preempt_bit,ss06
- mov a,preempt_task
- clr c
- subb a,#8
- jnc ss06 ;搶占任務號無效(大于7)
- mov a,preempt_task
- cjne a,task_sch_p,ss07 ;如果搶占任務和本次應該調度的任務相同,則下一次不要再調這個任務了。(本次調度生效,否則退回上一次調度指針)。
- jmp ss08
- ss07:
- mov task_sch_p,r1 ;恢復調度指針
- ss08:
- mov r0,preempt_task
- call set_ready_bit ;搶占任務就緒位
- mov task_curt_p,preempt_task ;直接指定任務號,切換
- clr preempt_bit
- jmp ss05
- ss06: mov task_curt_p,task_sch_p ;調度盤指針 確定調度指針和實際任務指針分離,解決搶占后調度不公平問題
- SS05:
- ;取優先字節地址
- MOV A,#TAB_PRI
- ADD A,TASK_CURT_P
- MOV R0,A
- ;時間片賦值 ;RESET THE TIMER0
- MOV 8AH,#T0_VALUE_L ;TL0
- MOV 8CH,@R0 ;TH0 ;MOV TH0,@R0;選中后,優先級設置到時間片
- ;取SP_I --> SP
- MOV A,#TAB_SP
- ADD A,TASK_CURT_P
- MOV R0,A
- MOV SP,@R0
- ;以下退出系統態,回到新的任務態,恢復現場-------------------------------------------INTERFACE
- ;區分主次任務
- ;TASK_CURT_P 大于2則跳過以下步驟
- CLR C
- MOV A,#2
- SUBB A,TASK_CURT_P
- JC SS03
- POP DPH
- POP DPL
- POP 07H ;POP R7
- POP 06H ;POP R6
- POP 05H ;POP R5
- POP 04H ;POP R4
- SS03:
- POP 03H ;POP R3
- POP 02H ;POP R2
- POP 01H ;POP R1
- POP 00H ;POP R0
- POP TMP_A
- MOV B,TMP_A ;POP B
- POP TMP_A
- MOV A,TMP_A ;POP A
- POP TMP_A
- MOV PSW,TMP_A
- ;此時堆棧內當前應是中斷返回時的PC值,RETI可以返回。
- ;ANL 88H,#11011111B ;CLR TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- RETI
- ;;子程序:根據被殺任務信號字節8位,從左到右每一位代表任務0-7是否要殺掉,1為殺死,0為不殺 來執行死刑
- ;執行內容:將該任務的內存區重新初始化(初始化后為休眠態),下次再輪到時,從頭開始。
- KILL_TASK:
- MOV R3,#8
- KTA00:
- MOV A,DEAD_SIG
- RRC A
- MOV DEAD_SIG,A
- JNC KTA01
- MOV a,R3
- DEC a
- mov r0,a
- CALL TASKRAM_INIT
- KTA01:
- DJNZ R3,KTA00
- MOV DEAD_SIG,#0 ;清掉所有DEAD信息
- RET
- ;;喂狗子程序
- WDT:
- MOV 0A6H,WDT_BYTE ;MOV WDTRST,WDT_BYTE WDT_BYTE= 1EH OR E1H
- MOV A,WDT_BYTE
- CPL A ;取反
- MOV WDT_BYTE,A
- RET
- ;;獲取就緒位:在調度程序中用到
- GET_READY_BIT: ;任務號R0, 執行結束后,結果的位在C
- MOV B,R0
- INC B
- MOV A,READY_BYTE
- GRB00: RLC A
- DJNZ B,GRB00
- RET
- ;提供的系統調用
- ;-----------------------------------------------------------------------------------------------
- ;子程序:修改任務的時間片,任務號在R0,優先字節(時間片)在R1,將優先字節寫入到數組
- SET_PRIBYTE:
- MOV A,#TAB_PRI
- ADD A,R0
- MOV R0,A
- MOV A,R1
- MOV @R0,A
- RET
- ;子程序:回到調度程序 DEBUGED 120516
- WAITING:
- NOP ;留給中斷響應的間隙
- ORL 88H,#00100000B ;SETB TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- RET
- ;子程序:占用系統,任務在讀寫的時候不允許系統中斷,和frees配套使用
- OCCUPY:
- ;ORL IE,#00000010B ;ENABLE INTERRUPT OF TIMER0 方法1:關閉timer0的中斷
- ANL 88H,#11101111B ;TCON CLR TR0, STOP TIMER0 方法2:關閉timer0的計時
- RET
- ;子程序:釋放系統 和occupy配套使用,任務占用系統后應及時釋放
- FREES:
- ;ANL IE,#11111101B ;DISABLE INTERUPT OF TIMER0
- ORL 88H,#00010000B ;TCON SETB TR0, START TIMER0
- RET
- ;注意:occupy和free要配套用,他們之間就是臨界區,然而occupy會導致不進調度程序,不建議使用。建議用加鎖和解鎖來實現臨界區的操作。
- LOCK_SYS:
- MOV LOCK_BYTE,#5AH
- RET
- UNLOCK_SYS:
- MOV LOCK_BYTE,#0EEH
- RET
- ;精確的系統延時-將16位的延時數,每一位為一個時刻,存放在DPTR,計算目標時間,設置喚醒任務,休眠自己。等待系統時鐘在時間到了再喚醒你,誤差為一個調度周期。
- ;任務號為全局變量指針TASK_CURT_P
- ;延時步驟:1、將dptr個刻度和當前時間相加得到目標時間,存入到鬧鈴數組當前任務位置;2、設置本任務的喚醒服務位,當目標時間到達,系統時鐘會喚醒你;3、進入休眠態;
- DELAY_SYS:
- ;計算目標16位目標值,存放在TAB_CLK對應的位置
- MOV A,SYS_TIME_L
- ADD A,DPL
- MOV DPL,A
- MOV A,SYS_TIME_H
- ADDC A,DPH ;帶進位
- MOV DPH,A
- MOV A,#TAB_CLK
- MOV R0,TASK_CURT_P
-
- ADD A,R0
- ADD A,R0
- ;雙字節指針
- MOV R0,A
- MOV @R0,DPH
- INC R0
- MOV @R0,DPL
- ;設置喚醒位,在CLK_ALARM字節,8個位標志8個任務的喚醒服務,1為有服務。
- MOV R0,TASK_CURT_P
- CALL SET_ALARM_BIT
- ;清就緒位,在READY_BYTE
- MOV R0,TASK_CURT_P
- CALL CLR_READY_BIT
- ;回調度
- ORL 88H,#00100000B
- RET
- ;上面用到的子程序:設置喚醒服務的位,任務號預先放在R0
- SET_ALARM_BIT:
- MOV B,R0
- MOV A,#10000000B
- INC B ;最小任務號為1
- SAB00: DJNZ B,SAB01 ;循環左移
- ORL CLK_ALARM,A
- JMP SAB02
- SAB01: RR A
- JMP SAB00
- SAB02:
- RET
- ;子程序:任務自殺
- KILL_SELF:
- MOV B,TASK_CURT_P
- MOV A,#10000000B
- INC B ;最小任務號為1
- KSF00: DJNZ B,KSF01 ;循環左移
- ORL DEAD_SIG,A
- JMP KSF02
- KSF01: RR A
- JMP KSF00
- KSF02:
- RET
- ;子程序:殺死,任務號存R0
- KILL_TASK_CALL:
- MOV B,R0
- MOV A,#10000000B
- INC B ;最小任務號為1
- KTSK00: DJNZ B,KTSK01 ;循環左移
- ORL DEAD_SIG,A
- JMP KTSK02
- KTSK01: RR A
- JMP KTSK00
- KTSK02:
- RET
- ;子程序:清就緒位,就緒態字節 8位 從左到右每一位分別代表任務0-7是否就緒,1為就緒,0為休眠
- ;任務號存在R0
- CLR_READY_BIT:
- MOV B,R0
- MOV A,#01111111B
- INC B ;最小任務號為1
- CRB00: DJNZ B,CRB01 ;循環左移
- ANL READY_BYTE,A
- JMP CRB02
- CRB01: RR A
- JMP CRB00
- CRB02:
- RET
- ;子程序:置就緒位,上面的相反操作 ;任務號存在R0
- SET_READY_BIT:
- MOV B,R0
- MOV A,#10000000B
- INC B ;最小任務號為1
- SRB00: DJNZ B,SRB01 ;循環左移
- ORL READY_BYTE,A
- JMP SRB02
- SRB01: RR A
- JMP SRB00
- SRB02:
- RET
- ;子程序:小刻度的延時功能(通過定時器1和搶占機制完成),次數放在r0
- delay_sys_us:
- mov delay_times,r0
- mov preempt_task,task_curt_p ;占用的任務號預存
- mov r0,task_curt_p
- call clr_ready_bit ;延時期間要休眠
- setb delay_sv_bit ;開啟延時服務
- ORL 88H,#00100000B ;SETB TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- nop
- nop ;中斷響應
- ret
- ;;;;;;;;;;;;;;;;;;伺服線程:任務7
- task_7:
- mov r2,#77h
- mov r3,#77h
- tk700:
- mov r0,#01h
- mov r1,#0eh
- call delay16b
- setb p1.7 ;led 熄滅100ms
- mov r0,#0dh
- mov r1,#0bdh
- call delay16b
- clr p1.7 ;led 點亮900ms
- jmp tk700
- jmp task_7
- ;r0:h r1:l 16位數的nop延時 一個周期為10.25us(全速) ,高8位放在r0,低8位放在r1
- delay16b:
- dll00:
- mov a,r1
- clr c
- subb a,#1
- mov r1,a
- mov a,r0
- subb a,#0 ;進位 16位數減一
- mov r0,a
- div ab ;純粹為了延時
- div ab
- nop
- nop
- mov a,r0
- orl a,r1
- jnz dll00
- ret
- ;注意:以上僅做了關于殺死、休眠、喚醒任務的調用,僅為了使用方便,實際使用時推薦使用更高效的邏輯方法:
- ;比如:要殺任務3和6,可以將dead_sig ORL 00010010 即可
- ;要休眠任務2和4,可以將ready_byte ANL 11010111 即可
- ;要喚醒任務1和7,可以將ready_byte ORL 10000001 即可
- ;SYSTEM END==============================================================line number of r0.92 is 750
- ;User's code
- ;*********************************************************************
- ;project name: 360度指示器 豆豆的打開關上
- ;designer: ut
- ;version: 1.0
- ;date: 16-11-16
- ;*********************************************************************
- ;用戶在此定義自己的變量地址 及 標號 0-46d 0-2cH :一共45個字節,除去0-7,可以用8-2cH這37個字節
- ;需求:
- ;1\ 16位數字鍵,0-9 abcd * #
- ; 2\ 3位段碼LCD顯示
- ; 3\ 按下數字,插入到LCD的左側。
- ; 4\ 按下D(回車),LCD的數字作為角度值,電機轉動到指定角度,三位數字范圍0-999,對360取模,執行完畢后再輸入數字時LCD清零再插入。
- ; 5\ 按下A電機向上微調,按下B電機向下微調
- ; 6\ 按下C,LCD清零。
- ; 7\ 關機時,壁板歸位至270度位置,再切斷電源
- ; 8\ 開機時,壁板開啟到0度位置。
- ;task0: 主線程,開機初始化,啟動其他任務,主循環是不停的取鍵、取鍵值成功后處理鍵值。
- ;task1: 電機移動,始終試圖將當前位置靠近目標位置,直到達到為止。
- ;task2: 按鍵掃描,轉換為鍵值存入到緩沖區。
- ;處理鍵值:0-9,執行循環插入 BCD 數組,如果有清屏標志,則先清屏再插入。
- ;處理鍵值:A-B, 執行電機走12拍,約1度,A為正方向,B為反方向。
- ;處理鍵值:C, 將BCD全部設置為0
- ;處理鍵值:D,將BCD轉成一個16位數,再mod360運算,將結果寫到電機目標值。設置清屏標志。
- ;關于顯示:在主線程的循環中,涉及到BCD變化時,才會觸發顯示,顯示過程:將BCD碼轉換成段碼,將段碼輸出到HT1621驅動器。
- WR_1621 BIT P3.6
- ;RD_1621 BIT P3.7
- DATA_1621 BIT P3.5
- CS_1621 BIT P3.7
- BIAS EQU 52H; //0B1000 0101 0010 1/3DUTY 4COM
- SYSDIS EQU 0; //0B1000 0000 0000 關振系統蕩器和LCD偏壓發生器
- SYSEN EQU 02H; //0B1000 0000 0010 打開系統振蕩器
- LCDOFF EQU 04H; //0B1000 0000 0100 關LCD偏壓
- LCDON EQU 06H; //0B1000 0000 0110 打開LCD偏壓
- XTAL EQU 28H; //0B1000 0010 1000 外部接時鐘
- RC256 EQU 30H; //0B1000 0011 0000 內部時鐘
- TONEON EQU 12H; //0B1000 0001 0010 打開聲音輸出
- TONEOFF EQU 10H; //0B1000 0001 0000 關閉聲音輸出
- WDTDIS EQU 0AH; //0B1000 0000 1010 禁止看門狗
- ;;;內存變量,范圍(8-2cH)
- nouse1 equ 20h ;預留給可尋址的位
- nouse2 equ 21h
- pool_key equ 21h ;22,23,24;類似堆棧指針,前推一位
- p_key equ 25h
- pool_bcd equ 26h ;26,27,28;存放bcd碼 循環覆蓋
- p_bcd equ 29h ;存放bcd指針,0-2 循環
- pool_print equ 8h ;8 9 10 存放3位數碼管的段碼
- key_value equ 0bh
- p_moto_H equ 0ch
- p_moto_L equ 0dh
- targ_moto_H equ 0eh
- targ_moto_L equ 0fh
- p_step equ 10h ;表的指針
- p_deg equ 11h
- ;定義位
- key_catched bit 00h ;獲取到一個按鍵后置位
- bcd_ready bit 01h ;bcd插入新值時置位
- moto_dir bit 02h ;電機方向
- tmp_dir bit 03h
- reset_bcd bit 04h ;重置bcd
- ;;用戶在這里寫初始化程序,在系統開機初始化時,被調用,注意:此處不可進行系統功能的調用
- user_init:
- mov p1,#0f0h ;關電機
- mov 08h,#0 ;段碼區
- mov 09h,#0
- mov 0ah,#0
- mov 26h,#0 ;bcd區
- mov 27h,#0
- mov 28h,#0
-
- mov p_key,#0 ;表示無按鍵
- mov p_bcd,#0
- mov p_step,#0
- mov p_deg,#0
- mov p_moto_h,#0
- mov p_moto_l,#0
- mov targ_moto_h,#0
- mov targ_moto_l,#0
- clr reset_BCD
- clr p2.2 ;開啟關機繼電器
-
- ret
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;;;;;;;任務0 主線程 ;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;主線程,1、從鍵盤緩沖池取一個按鍵; 2、處理該按鍵(數字鍵插入bcd區)其他鍵(步數增減、清零、回車)3、bcd轉換為段碼,隱去尾部0
- ;4、段碼輸出
- ;;;其他按鍵處理:步數增加一個幅度,不改變參數,步數減少一個幅度,不改變參數,bcd歸零,bcd轉換為目標值;;;;;;;;;;
- ;;-----------------------------------任務0
- task_0:
- CALL Ht1621_Init;() ; 上電初始化LCD驅動芯片
- mov dptr,#tab_ht1621
- mov r3,#0
- mov r5,#16
- call Ht1621WrAllData;(0,Ht1621Tab,16) ;清除1621寄存器數據,清屏
- mov dptr,#tab_ht1621_dou
- mov r3,#0
- mov r5,#3
- call Ht1621WrAllData;(0,Ht1621Tab,3);顯示 ;logo
- ;LCD的掃描是不需要延時的
- orl ready_byte,#01100000b ;開啟任務2:鍵盤掃描程序 ;開啟任務1:電機驅動:實際值逼近目標值
-
- ;臂板垂直向下270度為初始態(壓縮狀態,方便包裝和移動)在電氣驅動里面初始化。
- tsk0tv0:
- jb p2.1,tsk04 ;關機信號判斷
- clr key_catched
- call catch_a_key
- jnb key_catched,tsk0tv0 ;沒有獲取到鍵值
- clr bcd_ready
- call key_proc
- jnb bcd_ready,tsk0tv0 ;不涉及到bcd變化
-
- call bcd2print ;bcd 轉換為段碼
- call hide_zero ;消隱尾部的0
- mov r3,#0
- mov r4,#pool_print
- mov r5,#3
- ; mov lock_byte,#5ah ;加鎖
- call print ;輸出到LCD
- ; mov lock_byte,#11h
-
- jmp tsk0tv0
- ;;關機流程,回到270度位置
- tsk04:
- mov dptr,#tab_ht1621_off
- mov r3,#0
- mov r5,#3
- call Ht1621WrAllData;(0,Ht1621Tab,3);顯示off
- mov targ_moto_l,#0eh
- mov targ_moto_h,#01h ;電機目標為270
- ;等待電機到點
- tsk040: mov a,P_moto_l
- cjne a,targ_moto_l,tsk040
- mov a,p_moto_h
- cjne a,targ_moto_h,tsk040
- jnb p2.1,tsk0tv0 ;最后確認是否關機
- setb p2.2 ;關機
- ORL 87H,#02H ;INTO POWER-DOWN MODE
- LJMP SYS_START ;醒來的話就重新開機咯
- ;步驟:1、指針為0則無按鍵值,2、取鍵值,指針-1,(臨界區);3處理鍵值;
- ;print_LCD: 函數 將3個字節的內容顯示到LCD 0-f 都能顯示 步奏:1、字節數轉換到 段碼字節 2發送給ht1621
- ;如何循環顯示,三個字節要構成單向環,abcabcabc,始終顯示指針后3位數字,加入新的字節時指針往前推。
- ;將上面的三個字節,轉為一個整數<=999,占2個字節。
- ;設為目標值
- ;當前值與目標值比較,不等于則靠近,等于則關閉。
- ;關機線程
- ;正或反,步數n個。函數
- jmp task_0
- ;--------task0 end----------
- ;任務0子程序:從pool_key,p_key取一個鍵值,存放在key_value,并置位key_catched
- catch_a_key:
- ;clr key_catched
- mov a,p_key
- jnz cak00
- ret ;p_key 為0表示鍵值池空
- cak00:
- ;臨界區:取一個鍵值
- mov lock_byte,#5ah
- mov a,#pool_key
- add a,p_key
- mov r0,a
- mov a,@r0
- dec p_key
- mov lock_byte,#11;臨界區
- mov key_value,a ;鍵值
- setb key_catched
- ret
- ;任務0子程序:處理當前鍵值,小于10放到循環的bcd池里(pool_bcd,p_bcd),大于10則調用相關功能
- key_proc:
- mov a,key_value
- clr c
- subb a,#10
- jc kpc00
- mov a,key_value
- cjne a,#0ah,kpc01
- ;0a鍵功能
- call up_a_bit
- kpc01: cjne a,#0bh,kpc02
- ;0b鍵功能
- call down_a_bit
- kpc02: cjne a,#0ch,kpc03
- ;0c鍵功能
- call clr_bcd
- kpc03: cjne a,#0dh,kpc04
- ;0d鍵功能
- setb reset_BCD ;回車后前面的數據在下次按鍵輸入后,清掉
- call set_target
- ret
- kpc00: call keyv2bcd
- kpc04:
- ret
- ;;;;;;第二層子程序
- clr_bcd:
- mov r0,#pool_bcd
- mov @r0,#0
- inc r0
- mov @r0,#0
- inc r0
- mov @r0,#0
- setb bcd_ready
- ret
- set_target: ;將bcd里的3位數字轉換16位數字,并存入到targ_moto_L, targ_moto_H 中
-
- mov r1,p_bcd ;0-2范圍
- mov r3,#0
- mov r4,#0
- ;;個位數
- mov a,#pool_bcd
- add a,r1
-
- mov r0,a
- mov a,@r0 ; 取到bcd值 從個位數查起
- mov r4,a ;r3存H,r4存L 100* + 10* + L
- ;;;重復
- dec r1
- mov a,r1
- cjne a,#0ffh,ste00;
- mov r1,#2 ;過界處理
- ste00:
- mov a,#pool_bcd
- add a,r1
- mov r0,a
- mov a,@r0
- mov b,#10
- mul ab ;十位數
- clr c ;16位加法
- addc a,r4
- mov r4,a
- mov a,b
- addc a,r3
- mov r3,a
- ;;;重復以上
- dec r1
- mov a,r1
- cjne a,#0ffh,ste01;
- mov r1,#2 ;過界處理
- ste01:
- mov a,#pool_bcd
- add a,r1
- mov r0,a
- mov a,@r0
- mov b,#100
- mul ab ;百位數
- clr c ;16位加法
- addc a,r4
- mov r4,a
- mov a,b
- addc a,r3
- mov r3,a
- call targ_mod360
- mov lock_byte,#5ah
- mov targ_moto_L,r4
- mov targ_moto_H,r3
- mov lock_byte,#11h
- ret
- ;目標值在r3,r4(HL),結果調整后還是在R3 R4
- targ_mod360: ;輸入的bcd值(0-999)轉換為0-360度范圍,與360取模:求余
- mov dph,r3 ;暫存
- mov dpl,r4
- mov a,r4 ;360d=0168h
- clr c
- subb a,#68h
- mov r4,a
- mov a,r3
- subb a,#01h
- mov r3,a
- jc tmd00;表示過頭了
- jmp targ_mod360
- tmd00:
- mov r3,dph
- mov r4,dpl
- ret
- up_a_bit: ;電機向上微調一個距離 ;臨界區處理,禁止其他控制電機的操作
- anl ready_byte,#10111111b ;休眠電機任務(task1)
- mov r3,#12 ;走12拍
-
- uab00:
- dec p_step
- mov a,p_step
- cpl a
- jnz uab03 ;過0則回到7
- mov p_step,#7
- uab03:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
- anl P1,#11110000b ;驅動電機
- orl P1,a
- call waiting
-
- djnz r3,uab00
- anl p1,#11110000b ;關電機
- orl ready_byte,#01000000b ;喚醒電機任務
- ret
- down_a_bit: ;電機向下微調一個距離 ;臨界區處理,禁止其他控制電機的操作
- anl ready_byte,#10111111b ;休眠電機任務(task1)
- mov r3,#12 ;走12拍
- dab00:
- inc p_step
- mov a,p_step
- cjne a,#8,dab03 ;過7則回到0
- mov p_step,#0
- dab03:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
- anl P1,#11110000b ;驅動電機
- orl P1,a
-
- call waiting
- djnz r3,dab00
- anl p1,#11110000b ;關電機
- orl ready_byte,#01000000b ;喚醒電機任務
- ret
- ;任務0子程序:鍵值key_value放到循環的bcd池里(pool_bcd,p_bcd),置位bcd_ready *****orig
- keyv2bcd:
- jnb reset_bcd,k2b10
- call clr_bcd
- clr reset_bcd
- k2b10: mov b,key_value ;鍵值
- ;存放在pool_bcd
- mov a,p_bcd ;容錯處理:p_bcd只能0-2,超范圍就置0
- clr c
- subb a,#3
- jnc k2b01
- ;先推指針
- inc p_bcd
- mov a,p_bcd
- cjne a,#3,k2b02 ;0-2循環處理
- k2b01: mov p_bcd,#0
- k2b02:
- mov a,#pool_bcd
- add a,p_bcd
- mov r0,a
- mov @r0,b ;再存鍵值
- setb bcd_ready
- ret
- ;任務0子程序:從循環的bcd池里取最近3個值(pool_bcd,p_bcd),查表轉換成段碼,存放到段碼數組(pool_print);
- bcd2print:
- mov r1,p_bcd ;0-2范圍
- mov r2,#3 ;依次取3個數
- b2p00: mov a,#pool_bcd
- add a,r1
- mov r0,a
- mov a,@r0 ; 取到bcd值然后 查表獲取段碼
- mov dptr,#tab_ht1621_seg
- movc a,@a+dptr
- mov r3,a
- mov a,r2
- dec a
- add a,#pool_print
- mov r0,a
- mov a,r3
- mov @r0,a
- dec r1
- mov a,r1
- cpl a
- jnz b2p01
- mov r1,#2
- b2p01:
- djnz r2,b2p00
- ret
- ;任務0子程序:將段碼數組pool_print的3個字節前面的0隱去
- hide_zero:
- mov r3,#2 ;2次,個位數不管
- mov r0,#pool_print
- hzo00:
- mov a,@r0
- cjne a,#5fh,hzo01 ; 5f 為0的段碼
- mov @r0,#0
- inc r0
- djnz r3,hzo00
- hzo01:
- ret
- ;任務0子程序:將段碼數組pool_print的3個字節輸出到ht1621
- print: ;(uchar Addr,uchar *p,uchar cnt) R3:ADDR r4:P R5:CNT
- CLR CS_1621;
- MOV A,#0A0H
- MOV R0,#3
- CALL Ht1621Wr_Data ;(0xa0,3); // - - 寫入數據標志101
- MOV A,R3
- RLC A
- RLC A ;Ht1621Wr_Data(Addr<<2,6); // - - 寫入地址數據
- MOV R0,#6
- CALL Ht1621Wr_Data
-
- prt00:
- mov a,r4
- mov r0,a
- MOV A,@r0 ;取段碼
- MOV R0,#8
- CALL Ht1621Wr_Data ;Ht1621Wr_Data(*p,8); // - - 寫入數據
- INC r4
- DJNZ R5,prt00
- SETB CS_1621
- CALL delay_a_while
- RET
- ;任務0清零表
- TAB_HT1621:
- DB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0;
- TAB_HT1621_off:
- DB 033h,078h,078h,055h,055h,055h,000h,05h,05h,05h,0,0,0,0,0,0;
- TAB_HT1621_dou:
- DB 0b7h,0b3h,093h,055h,055h,055h,000h,05h,05h,05h,0,0,0,0,0,0;
- ;任務0段碼表,依據硬件線序確定
- tab_ht1621_seg:
- db 5fh; 0
- db 06h; 1
- db 3dh; 2
- db 2fh; 3
- db 66h; 4
- db 6bh; 5
- db 7bh; 6
- db 0eh; 7
- db 7fh; 8
- db 6fh; 9
- db 7eh; A
- db 73h; b
- db 31h; c
- db 37h; d
- db 79h; E
- db 78h; F
- db 33h; o
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;;;;任務1:電機驅動,實際值逼近目標值 ;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;實際值:p_moto_L、p_moto_H、目標值targ_moto_L、targ_moto_H 最大360度
- ;步驟:目標值-實際值,記錄符號(正負)到moto_dir, 結果等于0時,關閉電機,返回;結果小于180時,moto_dir反置(尋找最短路徑)
- ;讓電機逼近一步,實際值+1或-1。返回。
- ;;------------------------------------------任務1
- task_1:
- mov p_moto_L,#0eh
- mov p_moto_H,#01h ; 0-359范圍
- mov targ_moto_L,#0
- mov targ_moto_H,#0 ; 0-999范圍 ;要mod360處理,變為0-359范圍
-
- ;臂板垂直向下270度為初始態(壓縮狀態,方便包裝和移動)在電氣驅動里面初始化。
- tsk100:
- ;16位減法 目標值-當前值,默認為+
- clr moto_dir ;默認電機方向
- clr c
- mov a,targ_moto_L
- subb a,P_moto_L
- mov b,a
- mov a,targ_moto_H
- subb a,P_moto_H ;結果高位在a,低位在b
- jnc tsk105 ;結果為負數的話 被減數+360,再減
- clr c
- mov a,targ_moto_L
- addc a,#68h
- mov r0,a
- mov a,#01h
- addc a,targ_moto_h
- mov r1,a
- clr c ;重新算一次
- mov a,r0
- subb a,P_moto_L
- mov b,a
- mov a,r1
- subb a,P_moto_H ;結果高位在a,低位在b
- tsk105:
- mov r1,a ;H
- mov r0,b ;L 暫存結果(正偏差:0-359)
- jnz tsk104
- mov a,b
- jnz tsk104
- ;結果為0 關閉電機 并返回
- anl p1,#11110000b ;驅動電機
- orl ready_byte,#11100001b ;開啟其他任務
- jmp tsk100
- tsk104: ;偏差如果大于180,電機方向反向
- anl ready_byte,#11011111b ;暫停鍵盤線程
- clr c
- mov a,r0
- subb a,#180
- mov b,a
- mov a,r1
- subb a,#0 ;16位減去180
- jc tsk101 ;
- cpl moto_dir
- tsk101:
- call moto_move
- ;實際位置指針調整一位
- jb moto_dir,tsk102
- clr c
- mov a,p_moto_L
- addc a,#1
- mov p_moto_L,a
- clr a
- addc a,p_moto_H
- mov p_moto_H,a
- cjne a,#01h,tsk100 ;如果等于360則歸零
- mov a,p_moto_L
- cjne a,#68h,tsk100
- mov p_moto_L,#0
- mov p_moto_H,#0
- jmp tsk100
- tsk102:
- clr c
- mov a,p_moto_L
- subb a,#1
- mov p_moto_L,a
- mov a,p_moto_H
- subb a,#0
- mov p_moto_H,a
- cpl a ;如果等于ffff,則改為359
- jnz tsk100
- mov a,p_moto_L
- cpl a
- jnz tsk100
- mov p_moto_L,#67h
- mov p_moto_H,#01h
- jmp tsk100
- jmp task_1
- ;----------task1 end-------}}}}--
- ;任務1子程序:電機走一度,方向在moto_dir,
- ;涉及2張表:表1,45度折合512拍表,tab_deg,p_deg(0-44), 表2,8拍表tab_step,p_step(0-7)
- ;步驟:1根據方向調整度數指針,取一個度數拍數 2根據方向走N拍并更新拍數指針;
- moto_move:
- mov c,moto_dir ;防止過程中改變方向
- mov tmp_dir,c
- mmv00:
- jb tmp_dir,mmv01 ;正方向
- dec p_deg
- mov a,p_deg
- cpl a
- jnz mmv03 ;過0則回到44
- mov p_deg,#44
- mmv03:
- mov a,p_deg
- mov dptr,#tab_deg
- movc a,@a+dptr
- jmp mmv02
- mmv01:
- inc p_deg ;反方向
- mov a,p_deg
- cjne a,#45,mmv04 ;過44則回到0
- mov p_deg,#0
- mmv04:
- mov a,p_deg
- mov dptr,#tab_deg
- movc a,@a+dptr
- mmv02:
- mov r3,a
- call move_n_step
- ret
- move_n_step:;方向在tmp_dir,步數在r3, 表2,8拍表tab_step,p_step(0-7)
- mns00:
- jb tmp_dir,msn01 ;正方向
- dec p_step
- mov a,p_step
- cpl a
- jnz msn03 ;過0則回到7
- mov p_step,#7
- msn03:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
- jmp msn02
- msn01:
- inc p_step ;反方向
- mov a,p_step
- cjne a,#8,msn04 ;過7則回到0
- mov p_step,#0
- msn04:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
-
- msn02:
- anl p1,#11110000b ;驅動電機
- orl p1,a
- ;至此,電機走動了一拍,下面是延時:需要2ms,采用不可重入的delay_sys_us完成
- ;CALL delay_a_step
- ;call waiting
- ;mov dptr,#10
- ;call delay_sys
-
- mov r0,#3
- call delay_sys_us
-
- djnz r3,mns00 ;走r3步數
- ret
- tab_step: ;步進電機8拍表,循環使用
- DB 1001B,0001B,0011B,0010B,0110B,0100B,1100B,1000B
- tab_deg: ;45度折合512拍表,循環使用
- db 11,11,12,11,11,12,11,11,12,11,12,12,11,11,12
- db 11,11,12,11,11,12,11,11,12,11,11,12,11,11,12
- db 11,11,12,12,11,12,11,11,12,11,11,12,11,11,12
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ; ;
- ;;;;;任務2:按鍵掃描 ;
- ;;標準的4*4按鍵掃描程序,鍵值為0-fh,設置3個字節的緩沖pool_key,設置一個緩沖指針p_key(0-3),當緩沖區滿,丟棄新的按鍵
- task_2:
- ;初始化
- mov p_key,#0 ;0表示緩沖區空,3表示滿了,類似堆棧指針,注意定義時往前推一格
- scan_key:
- mov a,p_key
- cjne a,#3,tk301 ;緩沖區滿了
- jmp scan_key
- tk301:
- anl p0,#00001111b ;p0.7 p0.6 p0.5 p0.4 為豎線 從左到右
- mov a,p2 ;p2.4 p2.5 p2.6 p2.7 為橫線 從上到下
- orl a,#00001111b
- cpl a
- jz scan_key ;快速判斷,無任何按鍵時不要去挨個掃了,這樣響應更快
- mov r3,#4 ;豎線循環4次
- mov a,#01111111b
- tk300:
- orl p0,#11110000b
- anl p0,a
- mov r2,a ;暫存
- jb p2.4,tk303
- mov r0,#0
- call take_keyv
- tk303:
- jb p2.5,tk304
- mov r0,#1
- call take_keyv
- tk304:
- jb p2.6,tk305
- mov r0,#2
- call take_keyv
- tk305:
- jb p2.7,tk302
- mov r0,#3
- call take_keyv
- tk302:
- mov a,r2
- rr a ;下一個豎線
- djnz r3,tk300
- jmp scan_key
-
- jmp task_2
- ;----------------------------task2 end---}}}}}-----
- ;
- tab_key16: ;二位數組的4*4鍵值表
- db 0ah,0bh,0ch,0dh
- db 03,06,09,0fh
- db 02,05,08,0
- db 01,04,07,0eh
- ;子程序:查表取鍵值,r3:行號,r0:列號
- take_keyv:
- mov a,r3 ;1-4 轉為 0-3
- dec a
-
- mov dptr,#tab_key16
- mov b,#4
- mul ab ;調整基地址
- add a,dpl
- mov dpl,a
- clr a
- addc a,dph ;進位考慮
- mov dph,a
- mov a,r0
- movc a,@a+dptr ;查表取到對應的鍵值 在b
- mov b,a
- ;存到緩沖池
- mov a,p_key
- cjne a,#3,tkv00 ;緩沖區滿了
- ret
- tkv00:
-
- mov lock_byte,#5ah ;;臨界區,加鎖
- inc p_key
- mov a,p_key
- add a,#pool_key
- mov r1,a
- mov @r1,b ;存緩沖
- mov lock_byte,#11h ;;;;退臨界區,解鎖
- mov a,#30
- add a,sys_time_l ;設置300ms時限
- mov r1,a
- tkv01: ;按鍵釋放時立即返回,連續按住時要間隔延時
- ;超時退出
- mov a,sys_time_l
- clr c
- subb a,r1
- clr c
- subb a,#5 ;時間模糊處理,只要接近目標時間50ms以內,就算超時,擔心有錯過時鐘刻度的考慮
- jc tkv02
- ;anl p0,#00001111b ;p0.7 p0.6 p0.5 p0.4 為豎線 改變p0可能會擾亂豎線掃描
- mov a,p2 ;p2.4 p2.5 p2.6 p2.7 為橫線 從上到下
- orl a,#00001111b
- cpl a
- jnz tkv01 ;判斷是否釋放(范圍為本行)
- tkv02:
- ret
- ;;--------------任務3-----次任務,注意保護范圍:psw\a\b\r0-r3 以及最大嵌套2個call
- ;;-----------------------------------任務3
- ;;步進電機每走一步的延時喚醒線程
- task_3:
- jmp task_3
- ;-----------------------------------------------task3 end--------------
- ;
- ;;-----------------------------------任務4
- task_4:
- jmp task_4
- ;-----------------------------------------------task5 end--------------
- ;
- ;;-----------------------------------任務5
- task_5:
- jmp task_5
- ;-----------------------------------------------task5 end--------------
- ;
- ;;-----------------------------------任務6
- task_6:
- jmp task_6
- ;-----------------------------------------------task6 end--------------
- ;
- ;
- ;--------------------------------------------------------------------------------------
- ;以下用戶子程序區
- ;;ht1621b driver RD WR DATA CS
- ;/********************************************************
- ;函數名稱:void Ht1621_Init(void)
- ;功能描述: HT1621初始化
- Ht1621_Init:
- SETB CS_1621;
- SETB WR_1621;
- SETB DATA_1621;
- MOV dptr,#5;
- CALL delay_sys; // - - 延時使LCD工作電壓穩定
- MOV R1,#BIAS
- CALL Ht1621WrCmd;
- MOV R1,#RC256
- CALL Ht1621WrCmd; // - - 使用內部振蕩器
- MOV R1,#SYSDIS
- CALL Ht1621WrCmd; // - - 關振系統蕩器和LCD偏壓發生器
- MOV R1,#WDTDIS
- CALL Ht1621WrCmd;; // - - 禁止看門狗
- MOV R1,#SYSEN; // - - 打開系統振蕩器
- CALL Ht1621WrCmd;
- MOV R1,#LCDON; // - - 打開聲音輸出
- CALL Ht1621WrCmd;
- ret
- ;**寫數據到ht1621,數據存A,發送位數存R0*****************************************************/
- Ht1621Wr_Data:;(uchar Data,uchar cnt) A:DATA R0:number of send-bit
- CLR WR_1621;
- CALL delay_a_while
- RLC A;
- MOV DATA_1621,C;
- CALL delay_a_while
- SETB WR_1621;
- CALL delay_a_while
- DJNZ R0,Ht1621Wr_Data
- ret
- ;****寫命令給HT1621****************************************************
- Ht1621WrCmd: ;(uchar Cmd) cmd byte store in R1
- CLR CS_1621
- CALL delay_a_while
- MOV A,#80H
- MOV R0,#4
- CALL Ht1621Wr_Data; // - - 寫入命令標志1000
- MOV A,R1
- MOV R0,#8
- CALL Ht1621Wr_Data; // - - 寫入命令數據
- SETB CS_1621
- CALL delay_a_while
- RET
- ;*******************************************************
- ;函數名稱:void Ht1621WrOneData(uchar Addr,uchar Data)
- ;功能描述: HT1621在指定地址寫入數據函數
- ;全局變量:無
- ;參數說明:Addr為寫入初始地址,Data為寫入數據
- ;返回說明:無
- ;說 明:因為HT1621的數據位4位,所以實際寫入數據為參數的后4位
- ;********************************************************/
- Ht1621WrOneData:;(uchar Addr,uchar Data) R2,R3
- CLR CS_1621;
- MOV A,#0A0H
- MOV R0,#3
- CALL Ht1621Wr_Data;(0xa0,3); // - - 寫入數據標志101
- MOV A,R2
- RLC A
- RLC A
- MOV R0,#6
- CALL Ht1621Wr_Data;(Addr<<2,6); // - - 寫入地址數據
- MOV A,R3
- RLC A
- RLC A
- RLC A
- RLC A
- MOV R0,#4
- CALL Ht1621Wr_Data;(Data<<4,4); // - - 寫入數據
- SETB CS_1621
- CALL delay_a_while
- RET
- ;*********函數名稱:void Ht1621WrAllData(uchar Addr,uchar *p,uchar cnt)
- ;功能描述: HT1621連續寫入方式函數
- ;參數說明:Addr為寫入初始地址,*p為連續寫入數據指針,
- ;cnt為寫入數據總數
- ;返回說明:無
- ;說 明:HT1621的數據位4位,此處每次數據為8位,寫入數據
- ;總數按8位計算
- ;********************************************************/
- Ht1621WrAllData:;(uchar Addr,uchar *p,uchar cnt) R3:ADDR DPTR:P R5:CNT
- CLR CS_1621;
- MOV A,#0A0H
- MOV R0,#3
- CALL Ht1621Wr_Data ;(0xa0,3); // - - 寫入數據標志101
- MOV A,R3
- RLC A
- RLC A ;Ht1621Wr_Data(Addr<<2,6); // - - 寫入地址數據
- MOV R0,#6
- CALL Ht1621Wr_Data
-
- hwd00:
- CLR A
- MOVC A,@A+DPTR
- MOV R0,#8
- CALL Ht1621Wr_Data ;Ht1621Wr_Data(*p,8); // - - 寫入數據
- INC DPTR
- DJNZ R5,hwd00
- SETB CS_1621
- CALL delay_a_while
- RET
- ;;;----------------
- delay_a_while:
- mov r3,#50
- daw00:
- nop
- djnz r3,daw00
- ret
- delay_a_step:
- mov r6,#0ffh
- dast00:
- nop
- nop
- nop
- djnz r6,dast00
- ret
- ;---------------------------------------以下用戶數據表區------------------
- ;用戶可以在此定義所需要的數據表
- end
- ;*******************************************the end**********************
復制代碼
|