一直以來,都渴望為開源世界貢獻自己的作品,但總是心有顧慮。首先是之前一直忙碌於公司的項目,沒有時間做自己的東西,公司利益為先,確實也沒法開源;二是總覺得自己的代碼醜陋,不夠優雅、簡潔。怕貼出來後貽笑大方。 最近一段時間沒有那麼忙了,於是有時間把以前的積累沉澱一下。對之前做過的項目會有更多的反思。比如 ...
一直以來,都渴望為開源世界貢獻自己的作品,但總是心有顧慮。首先是之前一直忙碌於公司的項目,沒有時間做自己的東西,公司利益為先,確實也沒法開源;二是總覺得自己的代碼醜陋,不夠優雅、簡潔。怕貼出來後貽笑大方。
最近一段時間沒有那麼忙了,於是有時間把以前的積累沉澱一下。對之前做過的項目會有更多的反思。比如,
- 怎麼做才能把業務邏輯和工具組件分開,使工具組件可以在多個項目中復用;
- 怎麼做才能使整個系統的框架更加簡潔優雅,使其更容易維護、升級;
- 怎樣才能合理的以多人協作的方式開發單片機程式;
帶著這3個問題,回顧之前做過的項目,不敢說完全想清楚,但是有了一個初步的結果,於是就誕生了我的第一個開源項目,我給他取名為SmartTimer。
實際上,目前的SmartTimer,無論從代碼嚴謹性上,還是代碼美學上,和之前我用過的比較知名的開源項目,如RT-Thread、EasyFlash等相比,有相當大的差距。但是開源精神不僅是分享技術,另外很重要的一點是,只有拿出來曬,被各位高手”噴過”,你的項目才會更加完善,對你個人來說,才會有更大的收穫。
1.SmartTimer能幹什麼?
簡單說來,SmartTimer是一個輕量級的基於STM32的定時器調度器,在單片機”裸跑”的情況下,可以很方便的實現非同步編程。
它可以應用在對實時性要求沒那麼高的場合,比如說一個空氣檢測裝置,每200ms收集一次甲醛數據,這個任務顯然對實時性要求沒那麼高,如果時間上相差幾毫秒,甚至幾十毫秒也沒關係,那麼使用SmartTimer非常適合;而如果開發一個四軸飛行器,無論是對陀螺儀數據的採集、計算,以及對4個電機的控制,在時間的控制上都需要非常精確。那麼這種場合下SmartTimer無法勝任,你需要一個帶有搶占優先順序機制的實時系統。
不同的場景,選擇不同的工具和架構才是最合理的,SmartTimer只能做它力所能及的事情。
雖然SmartTimer是基於STM32開發的,但是它可以很方便的移植到其他的單片機上。
2. SmartTimer的一般用法
2.1 Runlater。
在單片機編程中,想實現在”xxx毫秒後調用xxx函數”的功能,一般有3種方法:
- 用阻塞的,非精確的方式,就是用for(i=0;i<0xffff;i++);這種迴圈等待的方式,來非精確的延遲一段時間,然後再順序執行下麵的程式;
- 利用硬體定時器實現非同步的精確延時,把XXX函數在定時器中斷里執行;
- 同樣是利用硬體定時器,但是只在定時器中斷里設置標誌位,在系統的主While迴圈中檢測這個標誌位,當檢測到標誌置位後,去運行XXX函數。
從理論上來說,以上3種方式中,第3種採用定時器設定標誌位的方法最好。因為首先主程式不用阻塞,在等待的時間里,MCU完全可以去做其他的事情,其次在定時器中斷里不用占用太多的時間,節約中斷資源。但這種方式有個缺點,就是實現起來相對麻煩一些。因為如果你要有N個runlater的需求,那麼就得設置N個標誌位,還要考慮定時器的分配、設定。在程式主While迴圈里也會遍佈N個查詢標誌位的if語句。如果N足夠多,其實大於5個,就會比較頭疼。這樣會使主While迴圈看起來很亂。這樣的實現不夠簡潔、優雅。
SmartTimer首先解決的就是這個問題,它可以優雅地延遲調用某函數。
2.2 Runloop
在定時器編程方面還有另一個典型需求,就是“每隔xxx毫秒運行一次XXX函數,一共運行XXX次”。這個實現起來和runlater差不多,就是加一個運行次數的技術標誌。我就不再贅述了。還是那句話:
SmartTimer可以優雅的實現Runloop功能。
2.3 Delay
並不是說非阻塞就一定比阻塞好,因為在某些場景下,必須得用到阻塞,使單片機停下來等待某個事件。那麼SmartTimer也可以提供這個功能。
3. SmartTimer的高級用法
所謂的高級用法,並不是說SmartTimer有隱藏模式,能開啟黑科技。而是說,如果你能轉變思路,舉一反三地話,可以利用SmartTimer提供的簡單功能實現更加優化、合理的系統結構。
傳統的單片機裸跑一般採用狀態機模式,就是在主While迴圈里設定一些標誌位或是設定好程式進行的步驟,根據事件的進程來跳轉程式。簡單的說來,這是一種順序執行的程式結構。其靈活性和實時性並不高,尤其是當需要處理的業務越來越多,越來越複雜時,狀態機會臃腫不堪,一不留神(其實是一定以及肯定)就會深埋bug於其中,調試解決BUG時也會異常痛苦。
如果你能轉換一下思路,不再把業務邏輯中各個模塊的關係看成基於因果(順序),而是基於時間,模塊間如果需要確定次序可以採用標誌位進行同步。那麼恭喜你,你已經有了採用實時系統的思想,可以嘗試使用RT-thread等操作系統來完成你的項目了。但是,使用操作系統有幾個問題,第一是當單片機資源有限的時候,使用操作系統恐怕不太合適;第二是學習操作系統本身有一定的難度,至少你需要花費一定的時間;第三如果你的項目複雜度沒有那麼高,使用操作系統有點大材小用。
那麼,請允許我沒羞沒臊的說一句,其實利用SmartTimer中的Runloop功能可以簡單的實現基於時間的主程式框架。
4.關於Demo
與源碼一起提供的,還有一個Demo程式。這個Demo比較簡單,主要是為了測試SmartTimer的功能。Demo程式基本可以體現Runlater,Runloop,Delay功能。同時也能基本體現基於時間的編程思想(單片機裸跑程式框架)。
5.SmartTimer的使用
SmartTimer.h中聲明的公開函數並不多,總共有8個:
void stim_init ( void );
void stim_tick (void);
void stim_mainloop ( void );
int8_t stim_loop ( uint16_t delayms, void (*callback)(void), uint16_t times);
int8_t stim_runlater ( uint16_t delayms, void (*callback)(void));
void stim_delay ( uint16_t delayms);
void stim_kill_event(int8_t id);
void stim_remove_event(int8_t id);
下麵我將逐一介紹
5.1 必要的前提
SmartTimer能夠工作的必要條件是:
- A. 設置Systick的定時中斷(也可以是其他的硬體定時器TIMx,我選擇的是比較簡單的Systick),我預設設置為1ms中斷一次,使用者可以根據自己的情況來更改。Systick時鐘的設置在stim_init函數中,該函數必須在主程式初始化階段調用一次。
- B. 在定時器中斷函數中調用stim_tick();可以說,這個函數是SmartTimer的引擎,如A步驟所述,預設情況下,每1ms,定時器中斷會調用一次stim_tick();
- C. 在主While迴圈中執行stim_mainloop(),這個函數主要有兩個作用,一是執行定時結束後的回調函數;二是回收使用完畢的timer事件的資源。
5.2 開始使用SmartTimer
做好以上的搭建工作後,就可以開始使用SmartTimer了。
int8_t stim_runlater ( uint16_t delayms, void (*callback)(void));
該函數接受兩個參數,返回定時事件的id。參數delayms傳入延遲多長時間,註意這裡的單位是根據之前A步驟里,你設置的時間滴答來確定的(預設單位是1ms);第二個參數是回調函數的函數指針,目前只支持沒有參數,且無返回值的回調函數,未來會考慮加入帶參數和返回值的回調。
舉例:
timer_runlater(100,ledflash); //100豪秒(100*1ms=100ms)後,執行void ledflash(void)函數
//如果在stim_init()中,設置的時鐘滴答為10ms執行一次,那麼傳入同樣的參數,意義就會改變:
timer_runlater(100,ledflash); //1秒(100*10ms=1000ms=1S)後,執行void ledflash(void)函數
int8_t stim_loop ( uint16_t delayms, void (*callback)(void), uint16_t times);
這個函數的參數意義同runlater差不多,我就不詳細說明瞭。 該函數接���3個參數,delayms為延遲時間,callback為回調函數指針,times是迴圈次數。 舉例(以1ms滴答為例):
timer_runloop(50,ledflash,5); // 每50ms,執行一次ledflash(),總共執行5次
timer_runloop(80,ledflash, TIMER_LOOP_FOREVER); // 每80ms,執行一次ledflash(),無限迴圈。
void timer_delay ( uint16_t delayms); //延遲xx ms
這個函數會阻塞主程式,並延遲一段時間。
void stim_kill_event(int8_t id);
void stim_remove_event(int8_t id);
這兩個函數,可以將之前設定的定時事件取消。比如之前用stim_loop無限迴圈了一個事件,當獲取某個指令後,需要取消這個任務,則可以用這兩個函數取消事件調度。這兩個函數的區別是:
void stim_kill_event(int8_t id); //直接取消事件,忽略未處理完成的調度任務。
void stim_remove_event(int8_t id);//將已經完成計時的調度任務處理完畢之後,再取消事件
5.3 註意事項
SmartTimer可接受的Timer event數量是有上限的,這個上限由smarttimer.h中的巨集定義
#define TIMEREVENT_MAX_SIZE 20
來決定的。預設為20個,你可以根據實際情況增加或減少。但不可多於128個
SmartTimer的源碼和Demo程式下載地址在GitHub上:https://github.com/lmooml/SmartTimer.git