官术网_书友最值得收藏!

第3章 中斷編程與代碼管理

雖然目前已經順利實現LED閃爍的功能,但是在實際產品中,一塊單片機通常總是會同時實現多個功能,最常見的應用就是:在控制顯示的同時還要檢測按鍵(以處理用戶的輸入交互信息)。由于LED閃爍是由延時函數實現的,它就是一系列(除延時外無實際意義的)累加操作,也就是說,單片機在延時期間無法處理其他事情。如果恰好在1s的延時期間按下按鍵(這是非??赡艿模瑔纹瑱C自然檢測不到按下狀態,因為它在任一時刻只能處理一件事情(順序執行)。反過來,當單片機在執行其他非常耗時的任務時,LED閃爍行為也就會被迫改變(例如,閃爍速度變慢了),對不對?

為了凸顯延時函數帶來的困擾,我們稍微更改一下任務:按鍵第一次按下可以使LED閃爍,第二次按下將停止閃爍,第三次按一下LED繼續閃爍,依此類推。下面看一下相應的源代碼,如清單3.1所示。

清單3.1 源代碼

在源代碼的開始,我們使用關鍵字sbit分別給控制LED與讀取按鍵狀態的引腳都定義了一個別名。在main函數內,我們使用關鍵字bit定義了一個位變量flicker_flag標記LED是否執行閃爍功能(與關鍵字sbit不同,bit定義的是變量,而不是別名),為0則停止,1則運行。flicker_flag初始狀態為0表示默認不閃爍。

然后檢測按鍵是否被按下,即讀取引腳P3.2(KEY)的電平是否等于0。如果答案是肯定的,就把flicker_flag取反。這里特別要注意語句while(!KEY),它用來檢測按鍵是否松開,如果省略這條語句,LED閃爍電路也可以正常啟動運行,但要使它停下來幾乎不可能。因為判斷按鍵是否被按下的語句也就只有幾微秒,一旦LED閃爍開始,執行閃爍功能的時間會比判斷按鍵是否按下的時間要長得多,換句話說,當按鍵被按下時,單片機有很大的概率還在延時函數里執行,所以按鍵按下的狀態也就沒有被檢測到。

那么,長時間按下按鍵總可以吧?然而一旦按鍵被檢測到按下了(KEY為低電平),假設flicker_flag現在被設置為0,理論上LED閃爍確實會停止(后面執行LED閃爍的if語句不會執行),但是這下好了,程序又會重新檢測,以迅雷不及掩耳之勢將flicker_flag又設置為1了,LED閃爍功能又打開了,如圖3.1所示。

圖3.1 沒有按鍵松開檢測時的執行流程

也就是說,要想使LED閃爍功能停止,只有一種可能:在檢測到按鍵后幾微秒內準確停止,也就是第1步開始后在第2步馬上松開按鍵。但是,按鍵動作持續的時間一般都是毫秒級的,這意味著不太可能做到如此短而準確地按鍵操作。while(!KEY)就表示當按鍵被按下后,如果一直按著按鍵,它就總會執行這條while循環語句(感嘆號為“邏輯非”運算,0為假,非0為真),直到松開按鍵之后(KEY為高電平),單片機才會跳出while循環語句執行后面的if語句,這樣可以防止單片機檢測到一次按鍵被按下狀態后,就立即重復檢測相同的一次按鍵狀態。

實際的按鍵讀取代碼通常還會進行消抖(消除抖動)操作,它在檢測到按鍵按下后延時一段時間(通常是十幾毫秒左右)再讀取按鍵是否仍然被按下。因為按鍵按下的那一瞬間,單片機讀到的電平狀態并不是穩定的,按鍵消抖可以防止按鍵被誤觸發。當然,這已經不是我們關注的主要內容,大家了解一下即可。

盡管如此,清單3.1所示代碼還是有點小問題,大多數時候必須長時間按下按鍵才能使LED閃爍停止,因為前面已經提過,單片機大部分時間還是在運行LED閃爍代碼,必須按下按鍵直到它運行到按鍵檢測部分才能再次修改flicker_flag標記,繼而達到停止LED閃爍的目的。而我們卻希望在任意時刻只要按一下按鍵就會立刻停止LED閃爍,該怎么辦呢?比較好的解決方案就是使用中斷(Interrupt)編程。

什么是中斷呢?咱們舉個例子,假如我正在教室講課,門外有人敲門:外面有個陌生人找。我當然會氣定神閑地慢悠悠回復道:你讓他在會議室等一下,下課后我再過去找他。然后繼續上課,外面又有人敲門說:校長有事找!事關薪資福利職稱,必須馬上得走一趟,刻不容緩!這時我會急匆匆地對學生們說:(你們先自己)預習(一下),(處理完事我再過來)。隨即快速奪門絕塵而去。

我這個老師的形象實在是不好,開個玩笑,大家不要學習。在這個例子中,“我正在教室講課”相當于單片機在順序執行語句,當陌生人到來時,我對此處理的方式是押后(先把手頭的事干完再處理其他事情),在單片機編程中稱為順序編程。當校長有事找時,我馬上出去應答(得先去見校長再回來講課,決計拖延不得呀),在單片機編程中稱為中斷編程。也就是說,“校長有事找”相當于一個中斷信號,它中斷了我正在進行的講課動作。很明顯,中斷編程一般應用在對實時性要求比較高的場合,如果不馬上處理就會后悔莫及。

在按鍵控制LED閃爍的簡單任務中,可以把按鍵按下事件作為中斷信號,這樣無論延時代碼是否正在執行,單片機都可以實時響應,對不對?還有一種思路就是:影響按鍵無法實時檢測的根本原因在于延時語句的執行時間太長了,只要我們能縮短其執行的時間,一樣可以讓單片機實時響應按鍵狀態。

由于使用循環語句延時1s的代碼實在太缺乏效率了,所以我們決定使用中斷編程來優化它,具體的思路是:使用一個定時器設置定時時長為1s,每當1s時間到來時就發送一個中斷信號,單片機根據中斷信號進行LED狀態轉換的控制,而在1s內(中斷信號未到來之前),我們不需要再做LED閃爍功能相關的延時控制,也就可以把更多的時間用于檢測按鍵,相應的代碼執行流程對比如圖3.2所示。

圖3.2 順序與中斷編程執行流程對比

中斷編程的關鍵在于中斷信號的產生,這可以通過定時器來實現。定時器是個什么東西呢?看過警匪片的讀者都會知道,有些反派會使用定時炸彈,先設置一個時間,開啟計時后數字就會不斷地減小,數字減小到了全0就會爆炸。定時炸彈就是定時器的一個典型應用。一般單片機內部都有定時器,根據型號的不同,可以是加法或減法類型。由于定時器是一個硬件電路,它與軟件指令是同時運行的(并行),我們只需要控制它何時產生中斷信號即可,具體來說包括設置定時時長(初始值)、使能中斷(允許計數器產生中斷信號)、開啟計數,這樣當計數完成后就會產生一個中斷信號。

我們使用中斷方式來實現LED閃爍功能,相應的源代碼如清單3.2所示。

清單3.2 中斷編程源代碼

首先定義了一個全局變量count并初始化為0,因為51單片機定時器的定時時長達不到1s,所以只能借助另一個變量來實現。例如,把定時時長設置為20ms,定時器每中斷一次就將count加1,當count為50時,就意味著1s的時間到了。所以在main函數中,我們使用了“判斷count是否大于49”的if語句。在if語句中先將count清零,這樣就可以開始下一個50次20ms的計數,然后將LED的狀態取反即可。

注意中斷服務函數(Interrupt Service Routine,ISR)timer0_isr的形式,它使用了關鍵字interrupt,后面跟了一個中斷號1,這是51單片機中斷服務函數的固定模式,雖然它看起來像一個函數,但卻不能由其他函數調用(main函數也不可以),只能在指定的中斷產生時自動調用,這是中斷服務函數與一般函數的主要區別。

另外需要特別注意的是,我們沒有在中斷服務函數中編寫過多代碼。事實上,也不應該在中斷服務函數中編寫過多代碼,因為中斷的最大特點就是實時性。如果在其中編寫大量代碼,單片機在運行中斷服務函數時又產生了另一個中斷怎么辦呢?除非新產生的中斷優先級更高,否則它就無法及時得到運行,也就失去了中斷實時性的特點。

請務必牢記:中斷服務函數中只做必要操作!我們只是把TH0與TL0設置初始值后,再將count累加就退出了。雖然將LED電平取反的功能放到中斷服務函數中也可以實現相同的功能,但這種編程方式是不妥當的(通俗來講,這不是單片機工程師的專業編程)。

TH0與TL0是什么東西呢?main函數中賦值的TMOD、ET0、EA、ETR又是什么呢?這涉及51單片機的定時器結構,如圖3.3所示。

圖3.3 51單片機的定時器0結構(工作模式1)

從圖可以看到,定時器結構中有很多網絡或模塊被取了名稱(例如C/T、TR0、GATE、TL0、TH0、TF0),其實它們都是圖2.4所示TMOD與TCON寄存器中的某些位,并且在reg51.h頭文件中進行了標識符定義,我們直接通過它們即可控制定時器的運行狀態,如圖3.4所示。

圖3.4 定時器0相關的控制位

51單片機中的兩個定時器被命名為T0(Timer0)與T1,圖3.4中我們僅標記了與T0相關的控制位,因為AT89C1051僅有這一個定時器。每個定時器都有4種工作模式,為簡化討論過程,我們使用比較常用的模式1(M1M0=01)。

定時器有兩種工作方式,即定時計數(本質上都是計數),它們的唯一區別是:定時是對單片機內部固定頻率(對于圖2.3所示電路就是12MHz)時鐘進行計數,而計數是對外部引腳T0(P3.4)的脈沖進行計數。例如,我們要實現測量引腳P3.4的脈沖個數,此時應該使用計數而不是定時。這兩種工作方式的切換由C/T位來決定,我們當然使用定時工作方式(C/T=0)。

接下來確定是否開始啟動定時器計數,它由一個與門的輸出電平控制(為1則開始計數,為0則停止計數)。為了啟動計數過程,我們首先應該將TR0(Timer0 Run)置1,同時還應該使或門的輸出也為1。或門用來選擇引腳(P3.2)的電平是否也參與啟動計數的控制,我們只需要軟件控制計數,將GATE置0即可(注意GATE輸入有一個非門),所以將TMOD寄存器初始化為0x1(高4位無效)。

在工作模式1下,TH0與TL0組成了一個16位加法計數器。當開啟計數后,計數器就會從設置的初始值開始累加,一旦累加到最大值就會產生一個溢出標志位TF0(Timer0 overflow,可以理解為進位),此時就可以產生中斷信號。所以現在關鍵的問題在于:我們應該將定時器初始值設置多少呢?假設我們需要定時20ms,則相應的初始值應為216-20ms×12MHz/12=45536(0xB1E0)。也就是說,我們需要將TH0與TL0分別初始化為0xB1與0xE0。(計算時已經將12MHz除以12,是因為從圖3.3可以看到,振蕩時鐘經過了12分頻)

計數器溢出后是否產生中斷還取決于單片機是否允許中斷。為了允許定時器0產生中斷信號,我們首先應該開啟總中斷允許標記位EA(Enable All),它是單片機中所有中斷允許的總控制位,然后再開啟定時器0的專用允許標記位ET0(Enable timer0)即可。

最后請注意:當定時器0產生中斷后,計數器的初始值是不會重載的(計數器不會繼續累加),所以在進入中斷服務函數后,我們對TH0與TL0重新設置了初始值。

定時器需要配置的位比較多,對于51單片機不熟悉的讀者可能會覺得有些煩瑣,當然,我們討論定時器中斷編程的主要目標是為了理解這種編程思想,對于具體編程不感興趣的讀者可以跳過。在實際產品開發過程中,中斷是一種非常重要的處理方式。換句話說,可以不去了解51單片機內部定時器具體是如何控制的,因為不同廠家的單片機操作方式并不一樣(當然,原理相通),本書其他地方也并未涉及具體的中斷編程,但是不能不理解中斷編程思路。

在稍微復雜點的項目中,通常會將代碼進行分塊管理,例如,我們可以將清單2.4所示源代碼分解為六個文件,它們之間的關系如圖3.5所示。

圖3.5 分塊管理的代碼包含關系

圖3.5中的箭頭表示文件包含關系,例如simple_led_driver.c包含了led.h,led.c包含了led.h、consts.h、delay.h。各文件相應的源代碼如清單3.3~3.8所示。

清單3.3 源文件simple_led_key_driver.c

清單3.4 頭文件led.h

清單3.5 源文件led.c

清單3.6 頭文件delay.h

清單3.7 源文件delay.c

清單3.8 頭文件consts.h

擴展名為.c的文件稱為源文件(Souce File),擴展名為.h的文件稱為頭文件(Header File)。頭文件通常僅包含變量或函數的聲明(不是定義)、宏定義、特殊功能寄存器定義,它通常被源文件使用#include預處理器指令包含。

可以看到,我們把源代碼劃分后放在不同的文件中,每一個文件只包含與某一項功能相關的代碼。例如,led.c僅包含與LED控制相關的代碼,delay.c僅包含時間延時相關的代碼。后續如果有更多的功能,則可以添加到對應的文件當中。如果項目中添加了全新的功能模塊,則可以再新建源文件與頭文件負責處理。

除main函數所在的源文件simple_led_key_driver.c外,其他源文件都包含了同名的頭文件。頭文件只做了函數原型的聲明,而函數的具體定義則放在同名源文件中。每一個頭文件都會首先使用條件編譯指令#ifndef定義一個唯一的標識符,它的命名規則一般由前后加下劃線的頭文件名全大寫(這只是慣例,并非必須這么做)組成,這樣可以防止頭文件被重復包含,在大型工程中可以提升編譯效率。

條件編譯指令#ifndef所起的作用是:如果當前標識符沒有定義過,就定義它并編譯該頭文件,如果同一個項目中的另一個文件中也包含了相同的頭文件,它會首先判斷標識符是否定義過,由于剛才已經定義過,所以就會略過相同的頭文件。

很明顯,模塊化后的源代碼更多且顯得更煩瑣了,簡單的源代碼當然沒有這樣做的必要,但是項目越復雜,進行模塊劃分后會更容易管理。本書為了使代碼更簡潔,不使用這種模塊劃分方式,這里讀者主要了解一下設計思想即可。

主站蜘蛛池模板: 建宁县| 文山县| 布拖县| 蒲江县| 南澳县| 翁源县| 海盐县| 临汾市| 棋牌| 双江| 六盘水市| 永善县| 察哈| 富川| 简阳市| 新巴尔虎左旗| 玉环县| 沁源县| 嘉峪关市| 扶余县| 会同县| 故城县| 潮州市| 宣城市| 慈利县| 西畴县| 门头沟区| 陆河县| 贵定县| 出国| 施秉县| 安福县| 嘉义县| 南部县| 盐源县| 金沙县| 岚皋县| 太白县| 峨山| 深水埗区| 多伦县|