大家好,我是驚覺。失蹤了三個(gè)月,我回來了。給大家?guī)硪粋€(gè)好消息和一個(gè)壞消息。壞消息是,我尚未滿血復(fù)活,Ardupilot第四篇將繼續(xù)延期。好消息是,公眾號(hào)恢復(fù)更新,先出一系列提升編碼能力的文章。 全國電賽在即,昨天母校老師聯(lián)系我,想讓我給學(xué)弟們做下賽前培訓(xùn)。我做過很多年的培訓(xùn),很早就發(fā)現(xiàn)了一個(gè)問題:同學(xué)們?cè)跒楸荣愖鰷?zhǔn)備時(shí),往往只注重去學(xué)習(xí)使用各種各樣的傳感器,自動(dòng)控制算法,各種驅(qū)動(dòng)。同學(xué)們只關(guān)注如何去實(shí)現(xiàn)功能,而忽視了如何把代碼寫得更好,更健壯,更易擴(kuò)展和維護(hù)。如果在比賽之前,先準(zhǔn)備好高質(zhì)量的代碼框架,基礎(chǔ)模塊,熟練掌握調(diào)度技巧,將極大提高賽時(shí)的開發(fā)和調(diào)試效率。 所謂高質(zhì)量,涉及到很多方面,比如:
筆者不打算一一講解這些設(shè)計(jì)原則,而是介紹一些實(shí)際的基礎(chǔ)模塊,講解它們的設(shè)計(jì)思路,注意事項(xiàng)和編程技巧,并在此過程中讓大家理解相關(guān)的設(shè)計(jì)原則。 毫秒級(jí)定時(shí)模塊作為系列開篇,本文先介紹一個(gè)非?;A(chǔ)的模塊:毫秒級(jí)定時(shí)模塊。 友情提醒,本模塊比較基礎(chǔ),可能有的同學(xué)對(duì)此非常熟悉,不過文末還有一個(gè)重要的小技巧噢。有基礎(chǔ)的同學(xué),可直接往后翻,跳到“再看comm_delay”一節(jié)。 此模塊提供基礎(chǔ)的定時(shí)功能,可細(xì)分為兩種:
可能有的同學(xué)會(huì)想,直接用單片機(jī)的定時(shí)器嘛,一個(gè)任務(wù)用一個(gè)定時(shí)器,有啥好講的。其實(shí)不然。定時(shí)模塊肯定要依賴于硬件定時(shí)器,但是一個(gè)任務(wù)用一個(gè)定時(shí)器的話,會(huì)有如下問題:
因此,我們需要一個(gè)統(tǒng)一的,可移植性強(qiáng)的定時(shí)模塊。 我們?cè)倩仡^看下兩個(gè)基礎(chǔ)的功能,延時(shí)和定時(shí)。 延時(shí)示例,1秒打印1次hello。comm_delay實(shí)現(xiàn)毫秒極延時(shí)。 void comm_delay(uint32_t ms); 定時(shí)示例,1秒打印1次hello。comm_get_ms返回當(dāng)前系統(tǒng)時(shí)間,即系統(tǒng)從啟動(dòng)到現(xiàn)在經(jīng)過了多少毫秒。
可能有的同學(xué)覺得上述兩項(xiàng)功能差不多,而定時(shí)比延時(shí)的代碼要復(fù)雜。定時(shí)的代碼確實(shí)多一些,不過它具有并發(fā)能力,即支持多個(gè)定時(shí)任務(wù)同時(shí)進(jìn)行。 定時(shí)示例,1秒打印1次you,2秒打印1次me。 static void show_you(void) 小結(jié):
即:
其實(shí)延時(shí)函數(shù)很簡(jiǎn)單,因?yàn)樗部梢钥闯墒且粋€(gè)定時(shí)任務(wù): void comm_delay(uint32_t ms) 系統(tǒng)時(shí)間實(shí)現(xiàn)comm_get_ms,即記錄系統(tǒng)時(shí)間,自然要靠硬件定時(shí)器啦。大家用的單片機(jī),無論是TI,STM32,NXP等,大部分都是cortex-m的內(nèi)核,該內(nèi)核有一個(gè)專門干這事情的定時(shí)器:SysTick timer。其名稱為系統(tǒng)滴答定時(shí)器,只有簡(jiǎn)單的定時(shí)功能。配置好reload計(jì)數(shù)并使能后,其由reload值遞減至0,觸發(fā)中斷,再從reload遞減。如果根據(jù)其時(shí)鐘配置相應(yīng)的reload值,實(shí)現(xiàn)每1ms觸發(fā)1次中斷,那就可以記錄毫秒 級(jí)的系統(tǒng)時(shí)間。 其配置函數(shù)位于單片機(jī)驅(qū)動(dòng)庫的CMSIS組件中,一般的工程都包含了這個(gè)組件,比如筆者使用TRUEStudio創(chuàng)建的工程: 下面是stm32的示例。SystemCoreClock為單片機(jī)的主頻,這也是SysTick的輸入時(shí)鐘。SystemCoreClock / 1000即為1ms的定時(shí)計(jì)數(shù),將reload配置為此值即可實(shí)現(xiàn)每1ms觸發(fā)1次定時(shí)中斷。其他單片機(jī)方法類似。
用法非常簡(jiǎn)單,單片機(jī)啟動(dòng)時(shí)調(diào)用sys_tick_init配置并使能SysTick,每1ms觸發(fā)1次SysTick_Handler,其內(nèi)對(duì)當(dāng)前時(shí)間sys_tick進(jìn)行加1操作。應(yīng)用層通過sys_tick_get獲取當(dāng)前時(shí)間。 SysTick_Handler在中斷向量表中指定,大家根據(jù)具體的MCU對(duì)號(hào)入座。 comm_get_ms只需對(duì)sys_tick_get進(jìn)行簡(jiǎn)單的封裝: uint32_t comm_get_ms(void) 再看comm_delay我們?cè)倏匆幌潞撩霕O延時(shí)的實(shí)現(xiàn),大家覺得它有問題嗎?
對(duì)于只需要進(jìn)行幾分鐘演示的電賽來說,它沒有問題。不過,電賽只是同學(xué)們實(shí)踐所學(xué)的一條途徑。正兒八經(jīng)的產(chǎn)品,需要具有足夠的健壯性,可長期穩(wěn)定地運(yùn)行。上述代碼能長期運(yùn)行嗎? static uint32_t sys_tick = 0; comm_get_ms是對(duì)sys_tick_get的簡(jiǎn)單封裝,而sys_tick_get返回的是一個(gè)32位無符號(hào)整型變量,它記錄的是系統(tǒng)從啟動(dòng)到現(xiàn)在所經(jīng)過的毫秒數(shù)。32位無符號(hào)整型變量最大能表示多長的時(shí)間呢?
其可記錄49天。在49天后,sys_tick將會(huì)溢出,從零重新開始累加。為了方便描述,要使用到一個(gè)宏UINT32_MAX。UINT32_MAX表示32位無符號(hào)整型變量的最大值,即0xffffffff。 假設(shè)我們要延時(shí)1分鐘,即60000ms。當(dāng)前時(shí)間為UINT32_MAX - 59999,下面計(jì)算timeout。 uint32_t timeout = comm_get_ms() + ms; timeout發(fā)生溢出,計(jì)算結(jié)果為0。
那么下面的等待循環(huán)將立刻退出,而不需要等待1分鐘。 while(comm_get_ms() < timeout); 在接下來的1分鐘內(nèi),comm_get_ms(60000)都是失效的,每1分鐘執(zhí)行1次的任務(wù)將不停地執(zhí)行。還有其他溢出的場(chǎng)景,這里不再一一描述。我們只要明確一點(diǎn)就好:comm_delay不能長期運(yùn)行。 健壯的comm_delay怎么修改comm_delay以解決溢出問題呢?其實(shí)很簡(jiǎn)單,直接給出答案:
我們簡(jiǎn)單地驗(yàn)證幾個(gè)場(chǎng)景: 當(dāng)前時(shí)間為10ms,延時(shí)2ms。先計(jì)算timeout: 下面看看從現(xiàn)在開始,什么時(shí)候
此種場(chǎng)景,成功實(shí)現(xiàn)延時(shí)2ms。 當(dāng)前時(shí)間為(UINT32_MAX - 1)ms,延時(shí)2ms。之所以定成UINT32_MAX - 1,是想測(cè)試時(shí)間溢出的場(chǎng)景,2ms后時(shí)間溢出。 先計(jì)算timeout,timeout在加2時(shí)溢出,最終結(jié)果為0。 下面看看從現(xiàn)在開始,什么時(shí)候
此種場(chǎng)景,成功實(shí)現(xiàn)延時(shí)2ms。 總結(jié)經(jīng)過兩種情況的測(cè)試,我們發(fā)現(xiàn),無論計(jì)算過程中時(shí)間有無溢出,改進(jìn)后的comm_delay都圓滿完成延時(shí)。 這種實(shí)現(xiàn)的原理是什么呢?原理很重要噢,否則大家在使用時(shí),可能把大于和小于關(guān)系搞反,或者是把被減數(shù)與減數(shù)的關(guān)系搞反。至于原理是什么呢,今天來不及講了,請(qǐng)待下回分解。劇透一下,下篇的名字叫:張三與李四誰跑的快。 |
|