當(dāng)前位置:首頁 > 公眾號精選 > 嵌入式大雜燴
[導(dǎo)讀]推薦語 本次推薦的是魚鷹寫的關(guān)于延時方面專題。文章列舉、分析了多個延時方法的優(yōu)缺點(diǎn)及改進(jìn)方法等,同時也分享了一些使用經(jīng)驗(yàn),帶我們深刻理解單片機(jī)的各種延時功能。另外,魚鷹的這種鉆研、學(xué)習(xí)精神很值得我們學(xué)習(xí) 下轉(zhuǎn)原文(文章較長,可收藏下面慢慢讀~

推薦語

本次推薦的是魚鷹寫的關(guān)于延時方面專題。文章列舉、分析了多個延時方法的優(yōu)缺點(diǎn)及改進(jìn)方法等,同時也分享了一些使用經(jīng)驗(yàn),帶我們深刻理解單片機(jī)的各種延時功能。另外,魚鷹的這種鉆研、學(xué)習(xí)精神很值得我們學(xué)習(xí)

下轉(zhuǎn)原文(文章較長,可收藏下面慢慢讀~):


最強(qiáng)干貨,不僅適用于單片機(jī)應(yīng)用場合,其他任何需要延時的平臺都可以借鑒參考!
在這篇長達(dá)萬字的長文中,魚鷹將通過延時這種剛需功能聊聊 溢出、可重入、編程思想、共享變量保護(hù)等方面內(nèi)容,以延時功能為載體,能更好的理解這些縹緲的知識點(diǎn)。
本篇長文將分成五篇陸續(xù)發(fā)布:概述、V1.0~V1.5、V1.7、V2.0~V2.3、V2.5~V2.7。版本V3.x留給對本公眾號發(fā)展有幫助的道友,在公眾號【魚鷹談單片機(jī)】的后臺回復(fù)關(guān)鍵字 延時即可獲取。
在生活中,時間與我們的生活息息相關(guān),日出而作,日落而息,說的就是利用太陽來大概判斷時間,從而規(guī)劃自己的作息。而在單片機(jī)領(lǐng)域,同樣需要一個時間去控制你的代碼運(yùn)行情況。
玩單片機(jī)的應(yīng)該都了解過晶振,很多初學(xué)者可能會從前輩那里得到這樣一個比喻:單片機(jī)的心臟。從功能的角度來說確實(shí)如此,因?yàn)閱纹瑱C(jī)代碼確實(shí)是依靠晶振執(zhí)行的,比如說晶振輸出一個脈沖,CPU執(zhí)行一條指令,就像心臟跳動一次,你做一個抬手動作。只不過你的心臟可能1秒只能跳動幾十次,而晶振1秒輸出脈沖幾十兆赫茲(1 MHz = 1,000 KHz = 1000,000 Hz),而一般單片機(jī)還會將晶振輸出的頻率進(jìn)行倍頻,倍頻后的頻率才最終用于驅(qū)動CPU的運(yùn)行。
如果說現(xiàn)實(shí)生活中,時間的最小單位是秒(應(yīng)該沒人在生活和工作中去精確到毫秒吧,更多可能是分鐘),而在單片機(jī)領(lǐng)域,常用的時間單位應(yīng)該是毫秒、微秒、納秒,而決定時間精度的就是晶振(更準(zhǔn)確的說是經(jīng)過晶振分頻、倍頻后的系統(tǒng)時鐘,比如說 STM32 的 8 M 晶振分頻成 1 M,然后倍頻成 72 M 作為系統(tǒng)時鐘驅(qū)動 CPU,進(jìn)而執(zhí)行存儲器中的代碼)。
對于應(yīng)用開發(fā)來說,可能他不會去了解晶振頻率多少,系統(tǒng)頻率多少,一條指令運(yùn)行時間又是多少?他們更多的需求是,在多少毫秒、多少秒(微秒很少用)之后這段代碼執(zhí)行一遍,這個界面刷新一遍,然后以這個時間為周期,循環(huán)執(zhí)行。這是利用延時功能去達(dá)到特定代碼周期執(zhí)行的目的。
延時可分為相對延時和絕對延時,相對延時與絕對延時的差別可看下圖理解:
(在后面介紹的幾種延時演進(jìn)版本中,可自行思考采用何種方式延時)
本篇筆記將根據(jù)魚鷹多年編程經(jīng)驗(yàn)介紹自己如何實(shí)現(xiàn)延時功能。說是進(jìn)化論,不如說是魚鷹個人延時功能的使用演進(jìn)過程。
說到延時函數(shù),51單片機(jī)過來的道友腦中應(yīng)該會想起郭天祥老師視頻中的延時函數(shù),而使用原子例程的道友會想起例程中使用systick定時器實(shí)現(xiàn)的延時功能,盡管他們的實(shí)現(xiàn)方式有所不同,但他們都是采用死等的方式達(dá)到延時功能(所謂死等,有個比喻魚鷹覺得挺恰當(dāng)?shù)模后H拉磨,讓CPU一直在一個地方打轉(zhuǎn),時間到了就離開這個地方)。
死等方式確實(shí)很容易理解,也很容易實(shí)現(xiàn),但是它的弊病也很明顯,不說它極大的浪費(fèi)了CPU的資源(在延時過程中,除了能處理中斷,后面的代碼無法處理),更重要的是影響的代碼的執(zhí)行效果,比如說你有一個功能時需要20毫秒執(zhí)行一次,而你的另一個功能卻需要30毫秒執(zhí)行,那你如何采用死等方式處理其中的矛盾呢?
既然是進(jìn)化論,為了筆記的完整性,魚鷹將根據(jù)自己使用過的延時函數(shù)進(jìn)行一一說明。

01.延時實(shí)現(xiàn)V1.x:死等延時

延時實(shí)現(xiàn) V1.0
魚鷹首先使用的延時實(shí)現(xiàn)就是上面所說的死等方式:
類似上面這種CPU運(yùn)行在一個循環(huán)中,直到達(dá)到條件后離開該函數(shù),從而達(dá)到延時功能。
這種實(shí)現(xiàn)方式只要對C語言有所了解,很容易理解,但是在使用過程中,你會發(fā)現(xiàn)延時并不準(zhǔn)確,或者說可能在這個單片機(jī)里面,延時很準(zhǔn),移植到另一個單片機(jī)可能一點(diǎn)就不準(zhǔn)了,所以為了達(dá)到準(zhǔn)確的延時功能,必須借助示波器等儀器調(diào)整參數(shù)以確定真正的延時時間。
并且還有一種情況就是,如果這個函數(shù)被中斷后再執(zhí)行,那么你的延時將不再準(zhǔn)確!這個問題留在V1.5版本討論。
當(dāng)然如果你使用KEIL開發(fā),魚鷹還可以告訴你另一個確定時間的方法:
這個時間在參數(shù)設(shè)置正確的情況下還是非常準(zhǔn)的(M3內(nèi)核以上,在線或者模擬,右下角時間可清零用于重新計(jì)時),當(dāng)然你還是得確定這個時間和現(xiàn)實(shí)時間的換算關(guān)系(當(dāng)你把下面的參數(shù)根據(jù)實(shí)際情況設(shè)置正確后,它們的換算關(guān)系是1:1,這個可以通過示波器確定),當(dāng)你了解了換算關(guān)系,那么你就可以脫離示波器,來知道某些代碼的運(yùn)行時間(精度可達(dá)每條指令運(yùn)行時間,這個時間實(shí)際上用的是DWT,這個后面說)。

與上述方式類似的實(shí)現(xiàn)是采用nop指令(空指令,即除了浪費(fèi)CPU沒有任何功能的指令),但魚鷹 不建議使用 nop 指令實(shí)現(xiàn)微秒級別以上的延時,因?yàn)轸~鷹不覺得nop指令能比上述實(shí)現(xiàn)方式優(yōu)勢更大:
1、都要采用某種方式確定實(shí)際延時時間(比如示波器,當(dāng)然還有魚鷹上面的方式,通過對比示波器你會發(fā)現(xiàn),準(zhǔn)確到讓你懷疑人生?。?;
2、當(dāng)優(yōu)化級別提高時,有可能出現(xiàn)延時不一致的情況;
有人會說可以通過nop指令的執(zhí)行時間,進(jìn)而確定延時時間,但是又引入以下問題:
1、去哪查找nop指令運(yùn)行時間?上網(wǎng),沒錯,但好像這種資料比較難找,就算你找到了你怎么就能確定這個資料是對的,你還是需要通過示波器(還有上述方式,再次強(qiáng)調(diào)?。┲惖拇_定。
2、不同平臺下的nop指令執(zhí)行時間不一致,比如M3內(nèi)核和M4內(nèi)核nop指令執(zhí)行周期不一致,即使相同平臺下,如果哪天心血來潮,改變了系統(tǒng)時鐘頻率,那么參數(shù)你還是得重新確定。
3、nop匯編移植性不是很好,單片機(jī)中,大多數(shù)時候采用C語言編寫,你要在C中嵌入?yún)R編需要折騰一下。
以上就是魚鷹不建議采用nop指令的原因,既然能用C語言解決的,干嘛需要匯編指令,這種方式并沒有比上述代碼有更多優(yōu)勢,反而缺點(diǎn)不少。
那么真的如上面所說,nop指令沒有一點(diǎn)優(yōu)勢嗎?
有,精確延時(這里的精確延時指的是微小延時情況下的精確)!當(dāng)芯片手冊告訴你某些功能需要延時多少個 系統(tǒng)時鐘周期時,采用nop指令無疑是最好的方式,因?yàn)檫@是能達(dá)到最小延時(指不會額外浪費(fèi)CPU)的最佳方式,這樣你也不用考慮 進(jìn)入、退出函數(shù)時額外消耗的時間了(當(dāng)延時足夠長時,進(jìn)入、退出函數(shù)消耗的時間可以忽略不計(jì),而延時很短情況下需要考慮)。
延時實(shí)現(xiàn) V1.5
前面介紹了版本1.0有一個很大的弊端,那就是在更改優(yōu)化級別的情況下,可能影響延時效果。所以有必要找到更好的方式去實(shí)現(xiàn)延時效果。
事實(shí)上,魚鷹在很長時間都是采用V1.0進(jìn)行延時的,比如流水燈、按鍵消抖、數(shù)碼管顯示等。直到看到正點(diǎn)原子的延時函數(shù):
注釋很清晰,簡單來講就是設(shè)置一個初始值,然后由硬件負(fù)責(zé)遞減這個值,當(dāng)減到零后設(shè)置標(biāo)志位,循環(huán)中只要不停地查詢這個標(biāo)志位是否置位即可,一旦置位,即代表延時時間達(dá)到了。
通過代碼和注釋可以知道,最大延時時間1864毫秒,1秒多點(diǎn),對于單片機(jī)來說,時間很長了。
現(xiàn)在我們來分析這種實(shí)現(xiàn)方式的優(yōu)勢:
1、延時時間 相對精確,也就是說,只要配置正確,精度可達(dá)systick時鐘精度(當(dāng)然如果延時在微秒級別時,誤差較大,除了進(jìn)出函數(shù)消耗外,還有循環(huán)體外語句和判斷語句的執(zhí)行時間,這些很難避免)。
2、即使代碼采用最高級別進(jìn)行優(yōu)化編譯,對于毫秒級別的延時影響也非常小。
3、即使在延時過程中被中斷了(裸機(jī)環(huán)境下被硬件中斷,系統(tǒng)環(huán)境下被硬件和其他任務(wù)中斷),延時時間在 絕大多數(shù)情況下是準(zhǔn)確的,但是 V1.0版本采用純軟件的方式總是 將被中斷的時間包含在延時時間內(nèi)。
雖說軟件版或硬件版都可能存在延時不準(zhǔn)確的問題,但事實(shí)上在軟件中多延時幾個毫秒是沒有多大問題的(可通過關(guān)中斷確保延時準(zhǔn)確),所以這種超過延時的情況不必太糾結(jié),但是如果你的延時函數(shù)的延時時間可能 比需要延時時間更短,那么就要引起注意了!
分析了好處,咱們說說缺點(diǎn):
1、就像上面所說的,延時時間最大1秒多點(diǎn),對于有些需求來說,延時有點(diǎn)短了(有些人可能會說這是雞蛋里挑骨頭,誰沒事延時那么久,就算需要延時很久,多延時幾次就行了,嗯,算你過關(guān))。
2、占用systick時鐘。用過嵌入式系統(tǒng)的都知道,大多數(shù)操作系統(tǒng)都會采用systick作為系統(tǒng)的心跳時鐘,也就是說,如果將來你的裸機(jī)代碼需要移植到系統(tǒng)中執(zhí)行,必須重新實(shí)現(xiàn)延時功能(可能你會說,我就在裸機(jī)上開發(fā),不上系統(tǒng)行不行,OK)。
3、函數(shù) 非可重入 這一點(diǎn)很多人可能都沒有意識到,在寫這段話之前,魚鷹也沒有意識到(延時這么簡單的功能,誰會想那么多,魚鷹亦是如此。但魚鷹在思考它的缺點(diǎn)時,也以為在裸機(jī)環(huán)境下不需要考慮可重入和不可重入問題,因?yàn)槁銠C(jī)就一個大循環(huán),肯定順序執(zhí)行,也就不需要考慮這種問題( 為什么順序執(zhí)行就不需要考慮可重入問題? ),但是卻突然想到了硬件中斷可打斷主循環(huán)的情況,而在中斷中執(zhí)行微秒級別的延時是有可能的)。
當(dāng)你在主循環(huán)中延時 秒級別時,突然中斷來了,開始延時 秒級別的代碼,那么必然修改systick寄存器,導(dǎo)致返回主循環(huán)時快速退出延時,最終達(dá)不到預(yù)期的延時效果!這是很可怕的事情,比如模擬I2C通信時,出現(xiàn)了這種情況……
通過上述分析,你應(yīng)該知道使用這種實(shí)現(xiàn)方式有多大風(fēng)險了吧!
那么該如何改進(jìn)呢?
延時實(shí)現(xiàn) V1.7
(事實(shí)上以下實(shí)現(xiàn)方式應(yīng)該是魚鷹在使用 V2.7 版本很久后才采用的方式,但因?yàn)閮?nèi)容的相關(guān)性,換個順序介紹)
是不是很簡單,簡單到讓你懷疑它的功能!
這里不再使用systick,而是使用DWT(關(guān)于這個模塊,魚鷹后期可能專門寫一個小節(jié)介紹它,歡迎關(guān)注 魚鷹談 單片機(jī)),為什么使用它呢?
1、不占用操作系統(tǒng)的心跳時鐘;
2、精度非常高,系統(tǒng)時鐘的精度,也就是說,即使你多執(zhí)行了一條指令,它也能發(fā)現(xiàn)!
3、延時足夠長,168 M頻率下可延時 25 秒多(0xFFFF FFFF / 168),這對于大多數(shù)需求都足夠了,只要你的執(zhí)行周期或者延時時間在此之內(nèi)的都沒有問題。
4、屬于不用白不用的資源(cortex-m3、m4都有這個模塊,像魚鷹這么節(jié)約的人,肯定要用上的)。
采用上述實(shí)現(xiàn)方式有什么好處:
1、解決了可重入問題。
2、函數(shù)內(nèi)盡可能的減少不必要的語句執(zhí)行時間。
3、精度高,延時長(采用DWT的優(yōu)勢,而不是實(shí)現(xiàn)方式的優(yōu)勢)。
4、因?yàn)橛? 硬件修改寄存器,所以可用于 任何中斷中,即使這個中斷優(yōu)先級非常高(有些時候,如果時間寄存器由軟件修改,那么比它高的中斷就不能正常運(yùn)行)。但不建議在中斷中延時毫秒級別以上的延時,微秒級別可以考慮。
現(xiàn)在我們來看看這段代碼如何實(shí)現(xiàn):
微秒和168相乘,是為了換算168M主頻下的 延時時鐘數(shù)(1 MHz = 1 us)。
接著獲取當(dāng)前時鐘,作為開始計(jì)時的 時刻,最后 當(dāng)前時間 (由硬件更改該值)與計(jì)時時刻比較,當(dāng)發(fā)現(xiàn)時間增加到大于 延時時鐘數(shù)時,即可跳出循環(huán),此時即達(dá)到了延時目的。
以上代碼似乎不難理解,但是有經(jīng)驗(yàn)的道友可能會問,你的變量大小是有限的(在這里是4字節(jié)),你不怕溢出嗎?溢出了之后, 計(jì)時時刻可能會比 當(dāng)前時刻更大,那么使用減法會不會有問題?
在大一的時候魚鷹就在思考這個問題了,兩個 無符號的數(shù)相減,如果前者比后者 ,會發(fā)生什么問題?這樣是否就達(dá)不到準(zhǔn)確延時的目的了?是否需要考慮溢出的情況?
有人會說這只需要一個判斷語句就能輕松搞定了,當(dāng)前者比后者小的時候特殊處理即可,比如4和5相減,特殊處理即可。
但魚鷹一直覺得應(yīng)該有一種比較好的方式去解決,直到大四實(shí)習(xí)看到 FIFO 的源碼,魚鷹才豁然開朗,終于找到了(對 FIFO 感興趣的可以去看魚鷹的另一篇筆記,很詳細(xì)的介紹了一個非常有意思的公式,而魚鷹也在那篇筆記中說到可以利用這種方式去做延時,只是里面寫的有點(diǎn)bug,事實(shí)上不算bug,只是有種脫褲子放屁的感覺,各位道友可以去看看當(dāng)時的實(shí)現(xiàn)方式,而對于 FIFO, 魚鷹目前也有了更多的經(jīng)驗(yàn),發(fā)現(xiàn)那篇筆記的實(shí)現(xiàn)方式采用 % 取余運(yùn)算效率較低,還有一種更高效的方式,而且建議能用判斷語句,就別用 % 處理)!
那么目前這種 看似沒有處理溢出的方式是否真的能夠適應(yīng)溢出的情況,答案是肯定的,那么原理何在?
我們經(jīng)??梢钥吹界姳恚ǚ菙?shù)字手表),指針從1一直轉(zhuǎn)到12,然后又從1開始,周而復(fù)始。當(dāng)其從1轉(zhuǎn)回到1時,即經(jīng)歷了 12個小時,但是如果你在 超過 天時間 來查看鐘表時,雖然你之前看到的是1,現(xiàn)在看到的還是1,但實(shí)際上已經(jīng)過去了 24小時了!
同理,計(jì)算機(jī)的世界亦是如此,如果說一個字節(jié)的最大值是255,那么這里的255就是鐘表里的12,字節(jié)溢出后變?yōu)?,而鐘表溢出后就是1,這種特性是由計(jì)算機(jī)和鐘表本身決定的,不隨外界變化而變化,當(dāng)我們能夠利用這種特性是,你會發(fā)現(xiàn)能簡化很多東西(這個函數(shù)需要靠自己去悟,別人很難說清)。
DWT計(jì)數(shù)器變量大小為4個字節(jié),也就是說最大值為0xFFFF FFFF,那么我們來思考以下幾個問題:
1、這個函數(shù)的最大延時是多少?
2、這個函數(shù)的使用是否真的沒有一點(diǎn)隱患?
3、它憑什么是可重入函數(shù)?
4、是否適用于所有定時器?
第一個問題,最大延時,前面魚鷹已經(jīng)計(jì)算過了,25秒多,那么精確的時間是多少呢?
(0xFFFF FFFF / 168) us,那么為什么不是((0xFFFF FFFF + 1)/ 168) us?這個留給道友去思考。
第二個問題,使用隱患問題,這個問題其實(shí)在說明鐘表例子時已經(jīng)說明了,如果你在超過它最大延時的時候再回來 查詢這個值,你會發(fā)現(xiàn) 最終延時遠(yuǎn)遠(yuǎn)超出了。事實(shí)上,你的延時函數(shù)不可能被打斷25秒多(如果真的打斷這么長時間,你就要好好考慮了),但是你不得不考慮這個問題,因?yàn)槟阍谙麓问褂眠^這種方式時,你不能確定是否真的能使用DWT這種超級延時外設(shè),有可能你的最大延時是1 毫秒 (比如一個定時器被你設(shè)定 1 毫秒溢出),那么你的延時函數(shù)被打斷 1 毫秒后再回來執(zhí)行是很可能的,所以你除了要考慮它的最大延時時間,還要考慮它 最大被打斷時間(DWT不用考慮這么多,因?yàn)?5秒對于單片機(jī)來說實(shí)在是太長了)。
事實(shí)上,除了這個隱患還有另一個,這個留在V2.7版本討論。
第三、所謂可重入,簡單來說,就是這個函數(shù)能否當(dāng)成兩個函數(shù)執(zhí)行,而不影響他們的功能,更實(shí)際一點(diǎn)的話就是,當(dāng)在主函數(shù)和中斷函數(shù)同時 調(diào)用(注意用詞,執(zhí)行不恰當(dāng))這個函數(shù)時是否會造成功能紊亂(在這里表現(xiàn)為延時不準(zhǔn))。在這個函數(shù)中,這里的共享資源是DWT,按理說共享資源都需要進(jìn)行保護(hù),這樣才能可重入,但是因?yàn)檫@個函數(shù)只對DWT進(jìn)行 讀取操作,而不進(jìn)行寫操作(寫操作由硬件自動完成),所以不存在修改共享資源的情況,也就是它可重入的原因,那么為什么那些參數(shù)、局部變量也是可重入的?這個基礎(chǔ)扎實(shí)的話應(yīng)該能懂,如果不懂就在評論區(qū)留言好了。
第四個問題,思想可適用于所有定時器,但注意只是思想,當(dāng)你的定時器是 遞減的,比如systick,那么需要做一點(diǎn)點(diǎn)修改,謹(jǐn)記(當(dāng)然168這個數(shù)也得正確設(shè)置,如果不知道怎么修改,可留言)!
事實(shí)上這種實(shí)現(xiàn)方式魚鷹最近(2020-01)在安富萊電子(強(qiáng)烈建議工作的道友參考安富萊例程和文檔,因?yàn)檫@些代碼應(yīng)該都是由一位大佬寫的,非常專業(yè),而正點(diǎn)原子更適合初學(xué)者)和RT-Thread文檔中都有看到相關(guān)描述,只是在此之前魚鷹并沒有看到過類似代碼,而是由FIFO源碼深入思考受到啟發(fā),進(jìn)而實(shí)現(xiàn)了以上代碼。而當(dāng)看到安富萊延時實(shí)現(xiàn)時才發(fā)現(xiàn),實(shí)現(xiàn)代碼驚人相似(實(shí)際上在此之前魚鷹一直采用非死等方式實(shí)現(xiàn)延時的,但后來發(fā)現(xiàn)死等方式也很有必要,比如模擬I2C總線,所以利用非死等V2.7的實(shí)現(xiàn)思想實(shí)現(xiàn)了死等)。


02延時實(shí)現(xiàn)V2.x:非死等延時


延時實(shí)現(xiàn) V2.0
前面V1.x版本的演進(jìn),很好的解決了延時問題,但有經(jīng)驗(yàn)的你會發(fā)現(xiàn),上述實(shí)現(xiàn)方式有一個硬傷,那就是都是采用死等方式實(shí)現(xiàn),在等待延時內(nèi)單片機(jī)除了 能響應(yīng)中斷外,什么也干不了,如果說 延時時間很短(微秒級別)或者必須采用死等方式處理外,其他情況我們應(yīng)該盡可能讓延時和其他任務(wù)同時處理。
假設(shè)我們有這樣一個需求,4個按鍵檢測、1個LCD屏幕顯示。按鍵濾波10毫秒,LCD屏幕 最低30毫秒刷新一次,執(zhí)行時間1毫秒(為啥不說按鍵執(zhí)行時間,因?yàn)閳?zhí)行時間太短了,這里忽略不計(jì)了)。
如果在裸機(jī)環(huán)境下開發(fā),入門級的會這樣處理(大一時的魚鷹):
主循環(huán)不停執(zhí)行按鍵掃描,因?yàn)橐獮V波,所以使用郭天祥老師那種方式濾波10毫秒后等待按鍵電平穩(wěn)定,然后檢測,并且在主循環(huán)不停刷新LCD,反正這里說刷新周期為30毫秒,而我CPU任務(wù)不多,有時間就刷新唄。
按鍵濾波10毫秒,LCD屏幕執(zhí)行時間1毫秒,這樣在按鍵按下情況,LCD屏幕刷新周期可達(dá)到11毫秒,可達(dá)到要求(如果按鍵沒有按下,刷新周期1毫秒)。
后來隨著需求增加,比如增加串口通信處理,增加大量浮點(diǎn)數(shù)據(jù)處理,執(zhí)行時間20毫秒,要求這兩個任務(wù)能在100毫秒內(nèi)執(zhí)行完畢,并且數(shù)據(jù)處理過程可被外部中斷而不影響。
假如把這個串口和數(shù)據(jù)處理繼續(xù)放在主函數(shù),先前最差情況下(按鍵按下),執(zhí)行周期為11毫秒,增加新任務(wù)后,最差情況下31毫秒可以完成串口和數(shù)據(jù)處理任務(wù),小于100毫秒。
但是當(dāng)你增加新任務(wù)后,你會發(fā)現(xiàn)LCD刷新要求達(dá)不到了(最少30毫秒),那該怎么辦?
通過分析你可以很容易的發(fā)現(xiàn),按下按鍵那一刻有10毫秒白白浪費(fèi)了CPU,如果這段時間能用于數(shù)據(jù)處理就好了。
問題點(diǎn)找到了,但是該怎么處理呢?
我們很容易想到把按鍵檢測功能放到中斷中去處理,為什么呢?因?yàn)樗膱?zhí)行很短,只是濾波耗時,如果我們能把 按鍵檢測和濾波分開執(zhí)行就好了。
于是我們想到了使用兩個變量將按鍵和檢測分開(關(guān)于中斷下按鍵檢測可參考藍(lán)橋杯代碼,看似很難理解,但理解后你會發(fā)現(xiàn)這是一種非常高效的實(shí)現(xiàn)方式):
一個變量用于記錄按鍵電平狀態(tài),一個變量用于計(jì)時,定時器1ms中斷一次,當(dāng)按鍵按下后(定時中斷中檢測是否按下),記錄電平狀態(tài),并且初始化計(jì)時變量為10,在定時中斷中發(fā)現(xiàn)計(jì)時變量不為0,遞減變量,直到為零后再次檢測按鍵狀態(tài)和當(dāng)前狀態(tài)是否一致即可(關(guān)于按鍵檢測魚鷹可能會在后期分享一篇筆記進(jìn)行深入分析,實(shí)現(xiàn)單擊、雙擊、長按等功能,關(guān)注魚鷹即可,在這里描述的這種實(shí)現(xiàn)方式魚鷹感覺還是復(fù)雜了點(diǎn))。
因上述代碼執(zhí)行時間很短,所以在解決了按鍵問題后,就可以達(dá)到任務(wù)要求了。
由此我們可以得到V2.0版本基本實(shí)現(xiàn)思想:
使用一個變量,專門用于計(jì)時。
思想很簡單,但是實(shí)現(xiàn)起來有點(diǎn)麻煩:
第一:你需要設(shè)置初始計(jì)時值
第二:你需要在特定的地方遞增或遞減這個計(jì)時值,比如定時中斷函數(shù)。
第三:計(jì)時時間達(dá)到后進(jìn)行相關(guān)處理。
以上第二點(diǎn)是一個很麻煩的事情,在 特定的地方修改值不是很大麻煩(也算一個麻煩,因?yàn)橐坏┠愕挠?jì)時任務(wù)增多,你的定時中斷任務(wù)復(fù)雜度必然會增加),最大問題在于修改計(jì)數(shù)值本身,修改變量會導(dǎo)致一些可重入問題,這個問題留待后面解決。
即使經(jīng)過上述方式優(yōu)化代碼結(jié)構(gòu)后,如果在后期再增加新任務(wù)后,你可能會發(fā)現(xiàn)LCD屏幕刷新周期可能又不符合要求,每次增加任務(wù)后都可能會導(dǎo)致刷新頻率不符合要求(因?yàn)槟壳熬褪侨蝿?wù)空閑即刷新,并沒有固定刷新時間),反反復(fù)復(fù)修改代碼誰也受不了,難道這就是所謂的碼農(nóng),難道就沒有更好的解決辦法了嗎?
延時實(shí)現(xiàn) V2.1
借用上述V2.0的思想,我們可以將其擴(kuò)展成V2.1,使得任務(wù)中對時間要求較高的功能進(jìn)行精確控制。
前面介紹的任務(wù)可以細(xì)分好幾個小任務(wù),按鍵檢測任務(wù)、LCD刷新任務(wù)、計(jì)算任務(wù)、串口任務(wù)。
上述任務(wù)中,LCD刷新任務(wù)有30 ms的硬性要求(保證屏幕不閃爍)。所以借用V2.0思想,使用計(jì)時變量對它精確控制,但是又不能像按鍵處理那樣將LCD刷新任務(wù)放到中斷任務(wù)中處理,因?yàn)長CD執(zhí)行時間長達(dá)1 ms。
很容易的,我們可以安排一個變量,在主循環(huán)中設(shè)置初始值,并執(zhí)行刷新任務(wù),而中斷中檢測這個值是否大于零,大于零即遞減。
代碼如下所示:
這樣一來,只要單片機(jī)不是滿負(fù)荷運(yùn)行,應(yīng)該就能達(dá)到刷新要求了(這里最終延時應(yīng)該是31ms,可自行測試一下)。
但是有經(jīng)驗(yàn)的你會發(fā)現(xiàn),計(jì)時變量在中斷和主函數(shù)都進(jìn)行了修改操作,這就導(dǎo)致了一個問題:是否需要對這個變量保護(hù)?
原則上需要,但是在這里卻不需要。為什么?
通過認(rèn)真思考,你會發(fā)現(xiàn)兩次修改都是有條件的:中斷下修改需要變量大于0,而主函數(shù)下修改需要變量等于零。這樣一來,你會發(fā)現(xiàn)變量修改是 互斥的,也就是不存在同時修改的可能!?。?
這也是為什么你沒有從別人類似的代碼中看到對共享資源保護(hù)的原因!
但是有些道友對共享變量不進(jìn)行保護(hù),在使用上存在顧慮,所以就會思考是否存在一種更好的方式去實(shí)現(xiàn)上述方法,由此V2.2現(xiàn)世。
延時實(shí)現(xiàn) V2.2
    V2.1存在的問題在于兩個地方同時修改了變量,導(dǎo)致變量成為了 偽共享變量(為什么是偽共享變量已經(jīng)在前面解釋了)。
    有沒有一種方法,讓變量只在中斷被修改,而主函數(shù)可以根據(jù)變量當(dāng)前的值判斷延時時間是否達(dá)到呢?
    這里介紹一種常規(guī)的方法,就是讓變量的初始化賦值直接在中斷進(jìn)行:
    
這樣主函數(shù)就只需要做判斷即可,而不需要修改變量。
但是這種處理有一個隱患,你必須在1ms內(nèi)(假設(shè)中斷1ms)查詢到超時時間,一旦主函數(shù)有任務(wù)運(yùn)行超過此時間,延時時間必然不準(zhǔn)。
所以以上解決方式不建議采用。那么是否有更好一點(diǎn)的解決方式呢?
再說一個更好的方式之前,再介紹一種方式。
延時實(shí)現(xiàn) V2.3
計(jì)時變量遞增,但不是30時清零,而是240時清零,即0~239循環(huán)(為什么不是增長到255后自然溢出?)。
這種實(shí)現(xiàn)方式好處就是減少了重新賦值計(jì)時變量的次數(shù),但不可避免的是,仍然存在1ms查詢頻率的限制,一旦超出,延時不再準(zhǔn)確!
是否存在一種沒有查詢頻率限制的實(shí)現(xiàn)方法呢?換句話說,我們不是根據(jù) 時刻判定延時時間的到達(dá),而是通過 判斷呢?即1~2倍超時時間內(nèi)都可以認(rèn)為超時時間到,而不是剛好就是超時時間呢?
延時實(shí)現(xiàn) V2.5
    當(dāng)然有,這里介紹一種有意思的公式(這個公式的妙處可看魚鷹FIFO相關(guān)筆記):
( 尾-頭+ 表長 ) % 表長
    這個公式是用來計(jì)算隊(duì)列中的元素個數(shù),受它啟發(fā),我們可以利用這個公式計(jì)算時長:
     Duration= (CurrentTime – Time + 256) % 256(注意不是255)
    CurrentTime為隨時間不斷遞增的變量,Time為記錄的一個時刻點(diǎn)。
    因?yàn)镃urrentTime遞增到256后自動清零的特性,可簡化公式如下:
     Duration= CurrentTime – Time
    由此我們得到了和V1.7類似的代碼:
    
    這里特別注意需要強(qiáng)制轉(zhuǎn)換成8位無符號整數(shù),這是因?yàn)橄鄿p的時候默認(rèn)采用有符號計(jì)算,我們需要讓結(jié)果強(qiáng)制變成無符號8位整數(shù)(如果變量類型為無符號32位整數(shù)則不需要如此),還有判斷條件是大于29,而不是30。
    可以看看延時結(jié)果:
我們關(guān)注的重點(diǎn)在于變量溢出后是否還能準(zhǔn)確延時30 ms。
事實(shí)證明,即使溢出,延時仍然準(zhǔn)確無誤。

延時實(shí)現(xiàn) V2.7
上述延時版本的實(shí)現(xiàn)是一種飛躍,極大的簡化延時代碼的實(shí)現(xiàn),但如果沒有接下來這個版本的演進(jìn),你可能會如此實(shí)現(xiàn)各種功能的延時:
每一個延時功能,都使用兩個變量按上述V2.5實(shí)現(xiàn)各種延時。
如果延時任務(wù)不多,確實(shí)沒有問題,但是如果延時任務(wù)很多,會帶來什么問題?
第一:中斷函數(shù)處理的延時變量很多,增加中斷負(fù)擔(dān);
第二:需要的延時變量很多。
那么是否有更好解決方式呢?
利用V2.5實(shí)現(xiàn)兩種以上延時功能后,你會發(fā)現(xiàn),中斷中的延時變量完全可以由一個變量承擔(dān)計(jì)時任務(wù)。
比如延時200ms任務(wù)和LCD刷新任務(wù):
可以看到,通過上述代碼可以實(shí)現(xiàn)兩種延時任務(wù),互不干擾。
而以上代碼才是魚鷹這篇長文的核心!也是魚鷹愿意花大量筆墨去寫這樣一個通用功能的原因所在,也是魚鷹特別希望各位道友掌握的一個延時技能(到此全文筆記已接近尾聲,V3.x版本更多的是用于提高關(guān)于延時使用的認(rèn)知)。
那么現(xiàn)在來分析這樣的代碼實(shí)現(xiàn)有什么好處:
1、可重入,也就是說你在任何函數(shù)中采用這種延時方式,原則上不會影響延時的準(zhǔn)確性(最終延時時間 大于等于需要延時時間)。
2、中斷執(zhí)行代碼極少,各個延時功能幾乎沒有耦合關(guān)系。
3、延時精度高,在這里延時精度為1 ms。
4、可實(shí)現(xiàn)超長延時,如果把計(jì)時變量改成32位整數(shù),延時時間超長(這里還是建議使用DWT,用于精確延時)。
5、變量少,一個延時任務(wù)只需要一個變量記錄上一次延時到達(dá)的時刻即可。
6、延時變量類型可根據(jù)延時長度自行選擇。比如說延時100 ms 使用8位整數(shù),延時1 s 使用16位整數(shù),延時 1天使用32位整數(shù)(當(dāng)然判斷代碼需要根據(jù)情況修正才能準(zhǔn)確延時,不懂的話可以留言討論)。
7、非阻塞式延時,執(zhí)行效率高。
討論了這么多優(yōu)點(diǎn),難道就沒有任何缺點(diǎn)嗎?
一番思考下,魚鷹想到了一個缺點(diǎn),或者說另一個使用隱患。
我們想象這樣一個場景,8位的計(jì)時變量,延時250 ms,上一次記錄的超時時刻是0,原本應(yīng)該在250這個時刻判斷為超時,但有一個任務(wù)執(zhí)行時間10 ms,導(dǎo)致在即將到達(dá)判斷條件前執(zhí)行了這個10 ms任務(wù)(假設(shè)在249這個時刻執(zhí)行了這個任務(wù),249 + 10 = 259,溢出變成3,查詢時3 – 0 > 249不成立,但是250~255這些值是成立的),那么最終導(dǎo)致下一個250才是超時時刻,也就是說最終延時250 * 2 ms(如果不湊巧下一次又在249附近這里運(yùn)行了這個任務(wù),那么后果……),這肯定不是我們想看到的。
那么從這個例子中我們可以總結(jié)一個防止延時時間錯誤的方法:
延時時間 + 最大查詢時間 < 最大可延時時間
在上面例子中,延時時間為250,查詢時間暫且認(rèn)為是10(不包含其他任務(wù)情況下),最大可延時時間255。代入公式發(fā)現(xiàn)不滿足條件,這也就是為什么會出現(xiàn)延時錯誤(延時錯誤指的是超出延時時間兩倍以上)的原因。
延時實(shí)現(xiàn) V2.8 :單次延時
還好沒有把版本號提的太高,不然就尷尬了。
因?yàn)轸~鷹的需求一直是周期延時,就沒往單次延時方向考慮,后來將筆記發(fā)布到知乎之后,有網(wǎng)友由此受到啟發(fā),想改進(jìn)他的延時功能(當(dāng)時他使用的方法類似 V2.1)。
一開始魚鷹很不明白,為什么明知這個版本的兩大缺點(diǎn),還選擇這種方式呢?
1、查詢頻率限制
2、如果延時任務(wù)多,中斷處理變得復(fù)雜
后來慢慢討論,終于知道為什么了,這是當(dāng)時的討論:
第一,該網(wǎng)友使用的場景是單次延時,而V2.7是周期延時,如果沒有好的策略處理是不能實(shí)現(xiàn)單次延時的;第二,雖然該網(wǎng)友的中斷變量數(shù)會增加,但他巧妙的避免了查詢頻率這一關(guān)鍵缺陷,所以還是很有參考價值的(希望公眾號的道友也能如這位網(wǎng)友一般,把自己的見解留言在文章下,這樣的交流對技術(shù)的提高是很有幫助的)。
相信很多道友在讀前面幾篇筆記時,有看到魚鷹重點(diǎn)強(qiáng)調(diào)了“ 查詢頻率”,但又有多少道友理解了這一詞呢?查詢頻率,換句話說就是 代碼的執(zhí)行周期,因?yàn)槭窃谘訒r判斷環(huán)境下,所以魚鷹稱之為“查詢頻率”。今天這篇筆記測試環(huán)境不再是裸機(jī),而是 RT-Thread 操作系統(tǒng),通過修改線程延時,能讓各位道友更深刻理解“查詢頻率”一詞。
延時實(shí)現(xiàn) V2.8.0
現(xiàn)在先來看看這位網(wǎng)友是如何解決查詢頻率限制的吧:
這是網(wǎng)友在魚鷹前面的思路下回復(fù)的內(nèi)容,當(dāng)時魚鷹那篇文章采用圖片形式,根本無法復(fù)制粘貼,所以這位網(wǎng)友能夠敲出這些代碼進(jìn)行回復(fù)也算是有心了。
當(dāng)時初看代碼時,還以為和魚鷹寫的版本一樣,所以一直在和這位 網(wǎng)友現(xiàn)在晉升為魚鷹的道友了,后面就以道友相稱了)強(qiáng)調(diào)查詢頻率限制,后面經(jīng)過不斷地討論后,他理解了魚鷹的查詢頻率的意思,魚鷹也理解了這段代碼和魚鷹寫的是不一樣的。
我們可以看一下中斷處理,發(fā)現(xiàn)它并 不是直接將變量遞減至零,而是留下了一個1,這個1就是用來做最后的延時超時時的處理工作,而一旦處理完成,完成清零。
也就是說,一個變量,被這位道友分成兩部分用,前部分用來延時,后一個1用來做超時時間達(dá)到的標(biāo)志位,還有一點(diǎn)就是最終延時時間不需要再減1了。
因?yàn)闀r間遞減到1之后,中斷不再對其遞減,所以 這個 1 一直保留,直到判斷超時代碼執(zhí)行(查詢)完成,才完成最終的清零操作,輕松實(shí)現(xiàn)單次延時功能,而且也不存在查詢頻率的限制。
確實(shí)是相當(dāng)不錯的策略,一個變量就解決一個延時問題,沒啥副作用。
如果延時不多,這個策略確實(shí)很不錯,但是一旦延時增多,中斷的負(fù)擔(dān)就會增加,遠(yuǎn)不如V2.7版本的一條代碼高效(中斷代碼要盡可能的少,執(zhí)行時間盡可能的短)。
由此我們可以思考,是否能對 V2.7 版本進(jìn)行改進(jìn),達(dá)到單次延時的效果呢。
一番思考下,魚鷹終于找到了一個很好的策略去實(shí)現(xiàn)它。
延時實(shí)現(xiàn) V2.8.5
為了實(shí)現(xiàn)單次延時,魚鷹增加了一個延時變量,也就是說,在魚鷹這種策略下,如果要實(shí)現(xiàn)一次單次延時,必須使用兩個變量,這是這個方法的一點(diǎn)缺陷,所以對于內(nèi)存不足的情況,可以使用那位道友的 V2.8.0(這個版本號是隨便編的,方便魚鷹說明)。
現(xiàn)在貼上代碼(RT-Thread):
由于魚鷹懶得找systick處理函數(shù)在哪,也不想修改系統(tǒng)的代碼,就使用了一個線程增加時間(實(shí)際上該系統(tǒng)有一個函數(shù)rt_tick_get() 函數(shù)可以使用,但怕有些讀者不熟悉,所以自己弄了一個變量替代)。
關(guān)鍵性的東西已經(jīng)注釋好了,現(xiàn)在分析一下。
為了使時間變量準(zhǔn)確增加,遞增變量的線程task優(yōu)先級必須比延時線程main更高(這里的main函數(shù)也是一個線程)。
首先分析一下為什么這個策略可以實(shí)現(xiàn)單次延時?關(guān)鍵點(diǎn)就在于延時時間delay被初始化為最大值,而每次延時完畢也會將其設(shè)置為最大值,而判斷條件是 大于號,也就是說只要delay設(shè)置為最大值,那么這個條件永遠(yuǎn)也滿足不了,里面的代碼也永遠(yuǎn)不會執(zhí)行,這樣一來就實(shí)現(xiàn)了單次延時的效果,而且這個延時時間也可以任意指定(也有限制條件,可回看V2.7)
現(xiàn)在進(jìn)行第一次測試:
設(shè)置兩個線程執(zhí)行頻率1 ms,在這里的超時代碼查詢頻率也可以認(rèn)為是1 ms,但由于打印函數(shù)比較耗時,所以執(zhí)行時間較長,好在打印函數(shù)也是有條件限制的,影響不大。
這里用了一個flag來表示觸發(fā)條件,這個flag通過一個系統(tǒng)的shell命令led設(shè)置,
現(xiàn)在輸入命令,觸發(fā)標(biāo)志位:
可以看到,延時時間非常準(zhǔn)確,說延時20 ms,就延時20 ms,絕不含糊。
現(xiàn)在將main線程執(zhí)行頻率設(shè)置為 6 ms,task線程執(zhí)行頻率還是 1 ms。
測試結(jié)果如下:
可以看到,魚鷹只改變了main線程的執(zhí)行頻率,就導(dǎo)致延時時間超過了20毫秒,達(dá)到25毫秒,很不準(zhǔn)確。這是因?yàn)殡m然超時時間到了,但是因?yàn)榇a還沒有執(zhí)行到,所以導(dǎo)致執(zhí)行到代碼時,已經(jīng)超過延時時間很久了。
所以,對于精確延時,必須注意查詢頻率。
這里還有要注意的一點(diǎn)是,因?yàn)閐elay變量在多個地方調(diào)用,所以注意互斥保護(hù),因?yàn)橐坏┥洗窝訒r沒有達(dá)到,你再次修改延時時間,那么必然影響上次延時效果,這是V2.8兩個版本都要考慮的問題,切記!


推薦閱讀:

為什么說你一定要掌握 KEIL 調(diào)試方法?


-THE END-



如果覺得文章對你有幫助,歡迎轉(zhuǎn)發(fā)、分享給朋友,感謝你的支持!


如果對本文有問題,歡迎留言!即使沒有問題也可以留下走心評論。


如需轉(zhuǎn)載請聯(lián)系我。


微信公眾號「魚鷹談單片機(jī)

每周一更單片機(jī)知識

長按后識別圖中二維碼關(guān)注

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉