Timer auto-reload產生中斷來精確計時,以函式指標(function pointer)切換中斷服務程式的功能
我們上個月在兩篇文章中提過,使用高階語言開發MCU韌體,若要精確計時就要靠MCU內的Timer(計時器)來達成,因為編譯器能保證程式邏輯正確,但不能保證執行時間、效率。
回顧一下這兩篇文章:
消除開關彈跳現象(de-bounce)、Timer delay、功能選單,以N76S003為例
製作隔空感應的引擎轉速表 - N76S003計時器輸入捕捉的應用(Timer input capture)
這是003Clock系列的文章,前幾篇沒跟上的記得回追一下喔!
(一)製作新唐N76E003/N76S003白光LED電子時鐘套件
(二)輕鬆上手:以VS Code, SDCC, Git建立新唐8051編譯環境
(三)新唐N76E003/N76S003白光LED電子時鐘套件韌體技巧說明
Part 1:消除開關彈跳現象(de-bounce)、Timer delay、功能選單
Part 2:Timer auto-reload產生中斷來精確計時,以函式指標(function pointer)切換中斷服務程式的功能
Part 3:以N76S003的SPI控制PT6961白光LED七段顯示器
(四)實戰技巧分享:新唐ISP,不用燒錄器也能更新韌體
今天的主題是要介紹003Clock是怎麼計時的。一樣,完整程式碼請見留言區或GitHub source code。
8051是怎麼執行程式的?
幾乎所有的CPU都會由程式計數器(下稱PC, Program Counter)來控制程式的進程,這個PC可以把它視為一個「指針」指著程式記憶體(Code Memory),告訴CPU現在程式執行到哪裡。而程式記憶體,就是記載著我們燒編好燒進去的機器碼。
同樣地8051的CPU開機時PC會從0x0000開始往上加,逐一讀取出程式記憶體中的內容出來執行。(但是也有一些變種型號因為特殊需要,預設有offset不從0開始)
中斷有什麼好處?
MCU擁有相當多的硬體周邊功能,程式執行的過程也時常是接收某些硬體周邊的資訊,運算過後再經由周邊傳出,而這些硬體周邊的運作速度會比CPU的執行速度慢許多。舉個簡單的例子:「等待ADC轉換完畢,讀取轉換結果,再從UART將數值傳出去。」
那麼要怎麼確認ADC轉換完了沒?UART傳完了沒?有以下兩種方法可以確認:
- 輪詢:軟體不斷去讀取硬體周邊的暫存器確認硬體周邊的狀態
- 中斷:由硬體自動產生事件通知CPU,例如ADC轉換完畢的事件、UART傳送完畢的事件
比起CPU不斷輪流問硬體「好了沒?」,不如讓硬體主動通知CPU「我好了!」,程式執行效率是不是高了許多呢?
「我好了!」執行中斷向量裡頭的中斷服務程式吧!
中斷向量(Interrupt Vector)是中斷服務程式(ISR, Interrupt Service Routine)存放的位址。當中斷發生時,程式就不是逐一執行,而是會將PC移至中斷向量的位址,讓CPU執行對應的中斷服務程式,處理硬體周邊發生的事件。中斷服務程式執行完畢,再將PC移回原先執行到的位址,讓CPU繼續做剛剛沒做完的事情。
另外,如果是使用Cortex-M的讀者要留意,中斷管理是透過查詢中斷向量表來進行。Cortex-M有一個中斷向量表,儲在程序記憶體的特定位址中(一般從位址0開始)。當中斷事件發生時,處理器會根據中斷號碼查詢中斷向量表。中斷向量表中的記載了對應中斷事件ISR的位址,讓PC移動到該位址,以執行對應的ISR。於鏈結時會將ISR的位址填入到中斷向量表中。
N76S003的Timer 3
除了標準8051就有的Timer 0和1之外,新唐N76S003額外提供Timer 2和3,這讓計時、計數方面的應用能有更大的彈性。
在除彈跳/Timer delay的文章中介紹過Timer 0和1,也在引擎轉速表的文章裡面,介紹過了Timer 2的使用方式以及它輸入捕捉的應用,這回要來介紹Timer 3,這下N76S003的4個Timer都被我用上了XD。
N76S003的Timer 3是一個帶有1~1/128的prescaler的16-bit auto reload timer,如同Timer 0, 1和2一樣,也能在溢位時產生中斷事件,並且會在溢位時由硬體自動從RH3, RL3將初值載入Timer 3的計數器。不過Timer 3的計數器沒有映射在特殊功能暫存器中(SFR, Special Function Registers),就不像前述那3個Timer能夠被程式存取,也因為這樣,Timer 3就很適合拿來當一個單純產生計時中斷事件的Timer,也是當初挑它來當計算時間Timer的原因。
以下節錄自N76S003的技術參考手冊,也許當初新唐RD就是考量到有這種需求,設計了Timer 3放進這顆晶片?
Timer 3 Auto reload產生2 Hz的中斷
在003Clock的應用中,要用Timer 3產生一個2 Hz的事件,並透過中斷的方式通知CPU「時間到了!」。為什麼不是1 Hz?不是每1秒把秒針加一就好嗎?其實是為了閃爍七段顯示器的冒號(秒點),才將事件設成2 Hz,也就是每0.5秒鐘去切換一次冒號的狀態。Auto reload是由硬體自動完成,好處就是溢位以後,不用透過軟體重新載入Timer 3的初始值,這完全避免掉了軟體執行時間產生的誤差,達到精確計時的目的。
將prescaler設定成1/128,並將初始值RH3, RL3設定成3036(65536 - 62500),就能產生2 Hz的事件,計算式如下:
16 MHz /
128 / 62500 = 2 Hz
除此之外,要讓事件發生中斷,還別忘了要將中斷致能。EIE1 |= 0x02; EA = 1;
- RH3 = (uint8_t)(3036 >> 8); // 65536 - 62500
- RL3 = (uint8_t)(3036 & 0xFF);
- T3CON = 0x0F; // Timer 3 run, pre-scalar = 1/128
- EIE1 |= 0x02; // Enable timer 3 interrupt
- EA = 1;
Timer 3的中斷服務程式
Timer 3的中斷服務程式內容很簡單,就是透過if ((u8counter & 0x01) == 0)將2 Hz的頻率再除2,變成1 Hz的頻率呼叫計時函式clock()。u8counter & 0x01實際上是一個最佳化過的2餘數除法。真正的餘數除法u8counter % 2得先進行除法運算,再將商數乘上除數與被除數相減,才能獲得餘數。透過一條簡單的AND運算只取u8counter的最低位元,也能做到2餘數的功能,執行速度會快上許多。
- void Timer3_ISR(void) __interrupt (16)
- {
- static uint8_t u8counter;
- if ((u8counter & 0x01) == 0)
- {
- clock(&time, 0);
- clock(&stopwatch, 1);
- }
- u8counter ++;
- (*isrFunction)();
- T3CON &= 0xEF; // Clear TF3
- }
蛤啊?(*isrFunction)();?這是什麼咚咚? … 函式指標的實際應用
剛剛提過,每個中斷事件發生時,都會將PC移動至中斷向量,去執行裡面的內容,而每個事件都只會有一個中斷向量而已,所以原則上一個事件發生時只能夠執行一段固定的程式碼。
003Clock能顯示時間之外,還有顯示秒數、計時器的功能,這些功能都與Timer 3計數息息相關,所以也用Timer 3中斷來更新七段顯示器的內容。之前看過功能選單那篇文章的讀者就知道,我是在main()迴圈內以switch case切換不同顯示功能的,言下之意就是要在main()迴圈中改變中斷服務程式的功能。
還在念大學的時候曾經修過C語言,老師跟我們說C語言的指標除了可以指向變數位址,還能指向函式的位址。當時聽得霧傻傻,想說要執行函式不就去call那個function而已,搞那麼複雜幹嘛。還好當初學的沒還給老師,這回還真的被我用上了「函式指標」。(也是因為寫MCU韌體要實際去操作記憶體,才逐漸了解指標奧妙之處,善用指標真的可以把韌體寫得很好、效率很高。)
在003Clock的應用中,我宣告了一個全域的函式指標變數,然後在main()迴圈內的switch case,依照不同功能將該函式指標指向某個function,並在中斷服務程式執行這個函式指標指到的function,達到切換中斷服務程式執行內容的功能。
重點回顧
- 中斷事件發生時,會去執行中斷服務程式
- Timer Auto-Reload可以避免軟體執行初始值載入的誤差
- 透過改變函式指標指向的函式位址,可以切換中斷服務程式的功能
- 函式指標的宣告方式
void (*isrFunction)(void); - 將函式指標指向函式func1()
isrFunction = &func1; - 執行函式指標指到的函式
(*isrFunction)();
看完了3篇有關Timer的文章,你們說Timer重不重要,N76S003的Timer好不好用!
留言
張貼留言