【開篇就白給?】
對大家熟悉的Cortex-M處理起來說,無論是強調極致資源和低功耗的Cortex-M0、還是頻率達到上GHz且能與某些應用處理器掰一掰手腕的Cortex-M7,都不會缺席了SysTick的身影。 正因為SysTick是官方欽定的“不可或缺”的“基礎設施”,無論是RTOS系統(tǒng)還是裸機應用,幾乎所有的嵌入式固件都會用到它。在這一背景下,如果我告訴你,有一個基于C語言的模塊,提供以下功能:
精確測量系統(tǒng)性能
精確測量函數(shù)執(zhí)行時間
精確測量中斷響應延遲
提供精確到us級的阻塞或非阻塞的延時服務
改善偽隨機數(shù)的隨機數(shù)特性
提供系統(tǒng)時間戳
……
使用了SysTick卻不會占用SysTick;
或者說提供以上功能的同時,用戶的原有的SysTick應用(比如RTOS調度器或是普通的應用延時)絲毫不會受到影響;
再直白點說:以上功能都是白送的,每個Cortex-M處理器都能立即享有,且不受到芯片型號的影響,
你是不是要直呼:“真就白給?”
是的!作為一個在Github上開源的C語言模塊,它真就白給!
【請張嘴……啊~】
perf_counter版本一路進化,從加入對GCC、IAR的支持、通過Library簡化用戶部署以來,從版本1.6.1開始更是把模塊的部署做到了極致的簡化: ? 這次,你只要從下面的鏈接“一次性”的下載 CMSIS-Pack,就可以在MDK環(huán)境中實現(xiàn)傻瓜式的部署: ? https://raw.githubusercontent.com/GorgonMeducer/perf_counter/CMSIS-Pack/cmsis-pack/GorgonMeducer.perf_counter.1.9.9.pack ? 也可以向公眾號發(fā)送?perf_counter?進行下載。 ? 下載后,雙擊pack文件進行無腦安裝:
在確認了開原許可Apache 2.0的條款后,一路Next,直到單擊Finish,完成整個安裝過程:
一般來說,部署會非常順利,但如果出現(xiàn)了安裝錯誤,比如下面這種:
則很可能是您所使用的MDK版本太低導致的——是時候更新下MDK啦。
當我們想在任何已有工程中部署perf_counter時,只需要單擊MDK工具欄上如下圖所示的圖標:
打開RTE配置窗口:
我們會注意到,在列表的末端出現(xiàn)了一個Utilities條目,依次展開后勾選Performance下的perf_counter——默認情況下系統(tǒng)會自動選擇以庫的形式來實現(xiàn)模塊的部署——這也是我吐血推薦的方式,因為它省去了不必要的編譯麻煩。
單擊OK按鈕后,我們會發(fā)現(xiàn)Utilities已經被加入到了工程管理器中,而perf_counter.lib也已經成功的部署到了目標工程中:
看過我往期文章《【教程】如何用GCC“零匯編”白嫖MDK》的小伙伴一定知道,MDK也可以使用GCC作為編譯器。perf_counter也將這種情況考慮在內——當用戶實際使用的是GCC時,對應的 libperf_counter_gcc.a(而不是arm compiler 5或arm compielr 6下的perf_counter.lib)會被加入到工程中。
GCC環(huán)境下使用perf_counter略微有一些注意事項,由于在文章《【教程】如何用GCC“零匯編”白嫖MDK》的末尾已經有過非常詳盡的介紹,這里就不再贅述了。
當我們在MDK環(huán)境下使用Arm Compiler 6作為編譯器時,需要打開對GNU擴展和C99(極其以上)語言標準的支持,具體方法如下圖所示:在Language C標準下拉列表中選擇帶有gnu前綴的選項——如果沒有什么特別的顧慮,推薦直接拉滿——使用gnu11即可。
如果你使用的是較老的Arm Compiler 5,則應該同時勾選C99 Mode和GNU extensions兩個選項。找不到上述兩個選項的小伙伴,應該認真考慮升級你們的MDK版本了。
【一鍵終結甜咸之爭?】
雖然在RTE中,perf_counter推薦并默認使用library的方式來部署, ? ? 但考慮到總有小伙伴對黑盒子有莫名的恐懼:
“……往往要親眼看著黃酒從罎子裏舀出,看過壺子底裏有水沒有,又親看將壺子放在熱水裏,然後放心……” https://zh.m.wikisource.org/zh/%E5%AD%94%E4%B9%99%E5%B7%B1
因此,perf_counter貼心的提供了以Source源代碼來進行部署的方式:
此時,相關的perf_counter.c和systick_wrapper_ual.s會取代原本的庫文件加入到編譯中:
當然,使用Source方式來編譯也是有代價的,即perf_counter的源代碼對CMSIS有依賴——當你的工程中并未在RTE配置界面中勾選CMSIS-CORE,就會出現(xiàn)類似下圖所示的黃色警告信息:“Additional software components required”。
此時,你可以簡單的單擊Resolve按鈕來解決問題——你會發(fā)現(xiàn),所謂的解決方案就是RTE自動把Source模式依賴的CMSIS-CORE幫你勾選上了而已:
如果你對此并無異議,則問題圓滿解決;如果你的系統(tǒng)中存在別的版本的CMSIS(比如很多正點原子、野火、以及CubeMX生成的工程都很可能會攜帶不經由RTE配置界面來管理的CMSIS),則很有可能出現(xiàn)CMSIS版本沖突——而關于沖突的解決方案,則稍微復雜一些——你可以參考我的文章《CMSIS玩家的“陰間成就”指南》來實現(xiàn)某種取舍……又或者……
還是推薦你繼續(xù)使用Library模式吧,畢竟它對CMSIS沒有任何依賴。
【一鍵更新的……嵌入式軟件模塊?】
一旦你安裝了perf_counter任何一個版本的pack,都會在MDK的pack-installer中留下痕跡:
此時,只要通過菜單 Pcak->Check For Update,我們就能實時的查詢?perf_counter是否存在最新版本:
如果Pack-Installer真的從github上發(fā)現(xiàn)了更新,就會以黃色Update的圖標來告知我們:
此時,單擊Update按鈕,即可安裝最新版本:
那么,如何才能鼓勵博主多多更新、加入更多更好的功能呢?
當然還是要靠有能力科學上網的小伙伴多多Star呀!
https://github.com/GorgonMeducer/perf_counter.git
【庫的初始化和注意事項】
關于頭文件
在任何需要使用perf_counter的C語言源文件中,我們需要首先加入對頭文件的引用:
#include?"perf_counter.h"
需要注意的是,通過RTE方式部署的perf_counter并不會在工程管理器中引入?perf_counter.h?以供用戶查看。想要查看perf_counter.h查找可用API的小伙伴,可以“簡單的”在上述代碼行上單擊右鍵,從彈出菜單中選擇“Open document 'perf_counter'”?來實現(xiàn)對頭文件的訪問。
關于庫的初始化
一般來說,用戶會在某一個地方,比如?main()?函數(shù)內完成對CPU工作頻率的配置,我們應該在完成這一工作之后確保全局變量?SystemCoreClock?被正確的更新——保存當前CPU的工作頻率,比如:
extern?uint32_t?SystemCoreClock; void?main(void) { ????SystemCoreClockUpdate();????//!?更新CPU工作頻率 ????SystemCoreClock?=?72000000ul?//!?假設更新后的系統(tǒng)頻率是 72MHz ????... }
一般來說,你的芯片工程如果本身都是基于較新的CMSIS框架而創(chuàng)建的,你的啟動文件中已經為你定義好了全局變量?SystemCoreClock——當然,凡事都有例外,如果你在編譯的時候報告找不到變量?SystemCoreClock?或者說“Undefined symbol __SystemCoreClock” 之類的,你自己定義一下就好了,比如:
uint32_t?SystemCoreClock; void?main(void) { ????SystemCoreClockUpdate();????//!?更新CPU工作頻率 ????SystemCoreClock?=?72000000ul?//!?假設更新后的系統(tǒng)頻率是 72MHz ????... }
在這以后,我們需要對?perf_counter?庫進行初始化。這里分兩種情況:
1、用戶自己的應用里完全沒有使用SysTick。此時,在編譯時,我們多半會看到類似如下的錯誤提示:
Error: L6218E: Undefined symbol $Super$$SysTick_Handler (referred from systick_wrapper_ual.o).
對于這種情況,我們需要在任意的C文件中添加一個SysTick中斷處理程序:
#include "perf_counter.h" ... __attribute__((used))????//!然后我們在?main()?函數(shù)里初始化?perf_counter?服務:
#include?... void?main(void) { ????SystemCoreClock?=?72000000ul?//!?假設更新后的系統(tǒng)頻率是 72MHz ????init_cycle_counter(false); ????... } 需要特別注意的是:由于用戶并沒有自己初始化?SysTick,因此我們需要將這一情況告知?perf_counter?庫——由它來完成對?SysTick?的初始化——這里傳遞?false?給函數(shù)?init_cycle_counter()?就是這個功能。如果由perf_counter 庫自己來初始化SysTick,它會為了自己功能更可靠將 SysTick的溢出值(LOAD寄存器)設置為最大值(0x00FFFFFF)。
2、用戶自己的應用里使用了SysTick,擁有自己的初始化過程。對于這種情況,我們需要確保一件事情:即,SysTick的CTRL寄存器的 BIT2(SysTick_CTRL_CLKSOURCE_Msk)是否被置位了——如果其值是1,說明SysTick使用了跟CPU一樣的工作頻率,那么SysTick的測量結果就是CPU的周期數(shù);如果其值是0,說明SysTick使用了來自于別處的時鐘源,這個時鐘源具體頻率是多少就只能看芯片手冊了(比如STM32就喜歡將系統(tǒng)頻率做 1/8 分頻后提供給SysTick作為時鐘源),此時SysTick測量出來的結果就不是CPU的周期數(shù)。
在確保了?CTRL?寄存器的?BIT2?被正確置位,并且SysTick中斷被使能(置位?BIT1,SysTick_CTRL_TICKINT_Msk?)后,我們可以簡單的通過?init_cycle_counter()?函數(shù)告訴perf_counter模塊:SysTick?被用戶占用了——這里傳遞 true 就實現(xiàn)這一功能。
#include... void main(void) { SystemCoreClock = 72000000ul //! 假設更新后的系統(tǒng)頻率是 72MHz init_cycle_counter(true); ... } 關于Library的匹配問題
perf_counter.lib?庫在編譯的時候,開啟了?Short enums/wchar(分別對應命令行的?-fshort-enums -fshort-wchar)。這么做其實沒什么特別的原因,但如果你的工程使用了不同的配置,例如:
下圖的工程配置中,沒有勾選 "Short enums/wchar"
你一定會看到這樣的編譯錯誤:
.Outexample.axf: Error: L6242E: Cannot link object perf_counter.o as its attributes are incompatible with the image attributes.
... wchart-16 clashes with wchart-32. ... packed-enum clashes with enum_is_int.既然知道了原因,解決方法就很簡單,要么在工程配置中勾選上這一選項;要么使用源代碼編譯的模式。
【時間類服務】
微秒級阻塞延時
perf_counter提供了一個us級阻塞延時函數(shù)?delay_us(),它的函數(shù)原型如下:
extern void delay_us(int32_t iUs);實際上,由于函數(shù)調用的開銷,delay_us在時間判斷上會存在一個“不積累”的誤差——根據優(yōu)化等級的不同,其具體CPU周期數(shù)存在差異,如果我們以Library方式進行部署時,這一誤差大約在+/-25個CPU周期左右——這一信息實際上告訴我們:
在使用Library的情況下,當你的CPU頻率超過50MHz時,delay_us()?可以提供最小<1us的延時誤差;
當你的系統(tǒng)頻率不滿足上述條件時,以系統(tǒng)頻率 12MHz為參考,則可以認為delay_us誤差為不積累的 +/- 2us。
具體評估方法,請參考我往期的文章《【實時性迷思】CPU究竟跑的有多快?》,這里就不做贅述。
全局系統(tǒng)時間
perf_counter提供了API函數(shù)get_system_ticks(),用于方便用戶獲取自?SysTick啟動以來系統(tǒng)已經經歷過的總周期數(shù),其函數(shù)原型如下:
__attribute__((nothrow)) extern int64_t get_system_ticks(void);可以看到,其返回值是一個 64位的有符號整數(shù),即便拋開符號位,也基本可以確信:無論芯片頻率如何,在人類滅絕之前,不會發(fā)生溢出問題。
此外,從版本v1.9.9開始,我們還可以從 perf_counter 中找到另外兩個有用的函數(shù):get_system_us() 和 get_system_ms()。 比如,這里的?get_system_ms()?可以告訴我們從SysTick啟動以來(一般大約可以等效為從系統(tǒng)復位開始)已經過去了多少毫秒。是不是特別方便?
非阻塞式多重延時
在狀態(tài)機中,非阻塞式的延時往往是必不可少的功能,包括但不限于:
機構控制的延時;
電路的時序控制;
通信協(xié)議的超時處理;
下圖就是一個支持多實例的非阻塞延時的狀態(tài)機,即便你沒有看過我的狀態(tài)機系列文章,對應的邏輯應該也算是淺顯易懂。
這里的核心思想是:
在延時的開始時刻,通過??get_system_ms()?來獲取當前的系統(tǒng)時間戳;
計算目標時刻的系統(tǒng)時間戳并保存在狀態(tài)機類中(保存在 iTargetTime里);
在隨后的狀態(tài)中以非阻塞的方式輪詢?get_system_ms()?以檢查約定的時間是否已經到來。
下面的狀態(tài)圖展示了如何在執(zhí)行某些動作(或者子狀態(tài)機)的同時,進行超時判斷:
這里值得注意的細節(jié)是:
在延時的開始時刻,通過?get_system_ms()?來獲取當前的系統(tǒng)時間戳;
計算目標時刻的系統(tǒng)時間戳并保存在狀態(tài)機類中(保存在 iTargetTime里);
在讀取字符失敗時,通過對比當前的系統(tǒng)時間戳來判斷是否超時。
具體狀態(tài)圖的解讀和翻譯方式,還不熟悉的小伙伴可以單擊這里來閱讀狀態(tài)機系列文章,這里就不再贅述。
隨機數(shù)發(fā)生
幾乎所有的C語言教程都在介紹過隨機數(shù)的發(fā)生,比如:
#include?由于rand()實際上是一個偽隨機數(shù)發(fā)生器,因此為了達到理想的效果,無一例外的,所有教材都推薦使用時間作為隨機數(shù)的種子。借助 get_system_ticks() 的幫助,我們的偽隨機發(fā)生函數(shù)幾乎可以肯定的是距離真正的隨機數(shù)發(fā)生器更近了一步:#include? int?main?(void)? { int i, n; time_t t; n = 5; /* Intializes random number generator */ srand((unsigned) time(&t)); /* Print 5 random numbers from 0 to 49 */ for( i = 0 ; i < n ; i++ ) { printf("%d ", rand() % 50); } ???return(0); } #include?"perf_counter.h" #include?srand((unsigned)get_system_ticks()); 【嵌入式C語言擴展】
perf_counter除了提供一些與系統(tǒng)性能測量和時間有關的服務歪,還額外對嵌入C語言做了一定的擴展,有興趣的小伙伴不妨試一試。
全局中斷屏蔽 __IRQ_SAFE
perf_counter?提供了一個關鍵字?__IRQ_SAFE,它可以在執(zhí)行緊隨其后一條語句、或是緊隨其后的花括號內的代碼片斷時,暫時性的關閉全局中斷響應,并在完成對應操作后恢復原樣。??
//!?執(zhí)行?緊隨其后的printf語句時,暫時性的屏蔽全局中斷 __IRQ_SAFE?printf("hellow world!"); //!?執(zhí)行花括號內的代碼時,暫時性的屏蔽全局中斷 __IRQ_SAFE { ... } //! 想提前結束時,可以用continue; __IRQ_SAFE?{ ????... ????if?(某些條件) { ????????//!?我們需要提前結束 ????????continue; ????} ????//!?條件性跳過的操作 ????... }__IRQ_SAFE在使用時,有以下注意事項:
它只能用于函數(shù)內部,不可以用來修飾函數(shù)或者變量;
它支持嵌套
編譯器類型檢測
有些小伙伴在進行軟件開發(fā)時,可能會因為這樣或者那樣的原因,需要能夠穩(wěn)定可靠的檢測出當前所使用的編譯器,比如?Arm Compiler 5、Arm Compiler 6、GCC等等。
perf_counter?提供了一系列統(tǒng)一格式的宏,有效的解決了上述問題。它們是:
__IS_COMPILER_ARM_COMPILER_5__ __IS_COMPILER_ARM_COMPILER_6__ __IS_COMPILER_GCC__ __IS_COMPILER_LLVM__ __IS_COMPILER_IAR__這些宏僅會在檢測到對應編譯器時被定義。一個典型的用法如下:
#if defined(__IS_COMPILER_IAR__) __attribute__((constructor)) #else __attribute__((constructor(255))) #endif void __perf_counter_init(void) { init_cycle_counter(true); }這里,__attribute__((constructor))?的作用在于告訴編譯器“請在執(zhí)行main函數(shù)前執(zhí)行被它修飾的函數(shù)”。這是一個GCC擴展,為大部分編譯器廣泛接受和支持,但由于IAR的在語法上并不支持存在多個函數(shù)時排隊用的序號,因此需要與其它編譯器區(qū)別處理。
預編譯膠水宏
很多場景下,我們需要在預編譯時刻對多個“文本片斷”進行“粘合”,以生成新的名稱,比如宏、枚舉、變量名、函數(shù)名等等。 ? 一般來說,我們都見過類似如下的做法:
#define?TPASTE(a,b)????a##b但這里其實存在一些問題,這類問題在我的文章《【為宏正名】本應寫入教科書的“世界設定”》中有詳細講解,這里就不再贅述。單純從功能上來講,TPASTE只能完成2個名稱的“粘合”,如果是多個呢?如果要粘合的名稱數(shù)量不去定呢?perf_counter就提供了這樣一個解決方案?CONNECT(),并具有以下優(yōu)勢:
黏合的數(shù)量可以是變化的
最大支持黏合9個片斷
比如,我們想生成一個安全的臨時名稱,則可以試著將代碼所在行號__LINE__、下劃線以及用戶指定的后綴黏合在一起:
#define?SAFE_NAME(__NAME)???? ????CONNECT(__,__LINE__,_,__NAME)比如,下面的代碼:
#define?measure_time(...)??????????????????????????????????? ????({ ????????int64_t?SAFE_NAME(StartTime)?=?get_system_ticks(); ????????__VA_ARGS__; ????????get_system_ticks()?-?SAFE_NAME(StartTime);???????? ???? }) ???? int32_t?iCycleUsed = measure_time( ????printf("Hello world! "); ????);假設measure_time所在的行號為123,則實際對應的代碼為:
int32_t iCycleUsed = ????({?????????????????????????????????????????????????????? ????????int64_t?__123_StartTime?=?get_system_ticks(); ????????__VA_ARGS__;???????????????????????????????????????? ????????get_system_ticks()?-?__123_StartTime;???????? });該代碼的作用是測量?measure_time()的圓括號內的代碼塊所用時間,并作為表達式的值返回。這里用到了GCC的一個被稱為“Statements and Declarations in Expressions”的語法擴展,感興趣的小伙伴可以參考下面的鏈接:
https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html#Statement-Exprs
數(shù)組元素枚舉器
C語言中,我們時常要使用for語句來實現(xiàn)對數(shù)據元素的訪問,比如,下面的代碼:
static volatile int16_t s_iADCBuffer[ADC_BUFFER_SIZE]; int16_t?get_average_voltage(void) { ????int32_t?nTotal?= 0; ????for?(int32_t?n?=?0;?n?在這個簡單的例子中,for循環(huán)的作用就是枚舉數(shù)組?s_iADCBuffer?中的每一個元素。很多高級語言(甚至是Linux內核代碼),都引入了專門的?foreach?關鍵字來實現(xiàn)這樣的數(shù)據枚舉功能,perf_counter也不能免俗,其語法為:
foreach?(<數(shù)組元素的類型>,<數(shù)組名稱>) { ????... }借助?foreach?的幫助,上述代碼可以被簡化為:
static volatile int16_t s_iADCBuffer[ADC_BUFFER_SIZE]; int16_t get_average_voltage(void) { int32_t nTotal = 0; foreach (volatile int16_t, s_iADCBuffer) { ????????nTotal?+=?*_; } return nTotal / ADC_BUFFER_SIZE; }注意,這里"_"是一個指向枚舉過程中當前元素的指針,簡單說,它等效于前面代碼中的 "(s_iADCBuffer+n)",由于是指針,因此在使用是需要用 “*_”來獲取元素的內容。使用"_"來指代循環(huán)體內的當前元素,借鑒于腳本語言perl。如果你非常討厭這種用法,覺得不知所云,那么也可以使用下面的方法:static volatile int16_t s_iADCBuffer[ADC_BUFFER_SIZE]; int16_t get_average_voltage(void) { int32_t nTotal = 0; ????foreach?(volatile?int16_t,?s_iADCBuffer,?piItem)?{ ????????nTotal?+=?*piItem; } return nTotal / ADC_BUFFER_SIZE; }注意到:foreach在原有2個參數(shù)的基礎上引入了第三個參數(shù)?piItem,這里的用法實際為:foreach?(<數(shù)組元素的類型>,<數(shù)組名稱>,<枚舉元素名稱>) { ????... }換句話說,用戶可以通過第三個參數(shù)指定枚舉元素的變量名稱了,是不是一下就清晰了很多?
【如何測量代碼片斷占用了多少CPU資源】
支持嵌套的 __cycleof__()
很多時候,我們會關心某一段代碼或者函數(shù)究竟用了多少CPU周期,比如,我們寫了一個算法,你很擔心“這個算法究竟使用了多少CPU資源”,為了解決這個問題,我們需要用到如下的公式: ? CPU資源占用(百分比) =? ????(函數(shù)運行所需的時間)?(算法運行間隔的最小值) ???? 100% ? 對于【函數(shù)運行所需的時間】和【算法運行間隔的最小值】來說,雖然它們都是時間單位,但考慮到CPU的頻率是給定的(不變的),因此,這里的時間單位在乘以CPU的工作頻率后都可以被換算為CPU的周期數(shù)。舉例來說,假如【算法運行間隔的最小值】是 20ms、CPU的頻率是72MHz,那么對應的周期數(shù)就是 72000000?* (20ms / 1000ms) = 1440000 個周期??磥砩鲜龉街形ㄒ恍枰覀儗嶋H測量的就是【函數(shù)運行所需的周期數(shù)】了。 ?
perf_counter?提供了一個非常簡單的運算符:__cycleof__()。假設我們要測量的代碼片斷如下:
my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); ...則我們可以輕松的通過__cycleof__()運算來測量結果:... __cycleof__("my?algorithm")?{ my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); } ...如果你的系統(tǒng)支持?printf(),則可以看到類似如下的輸出結果:??
帶入上述公式: 525139 /?14400000 * 100% ≈?36.5% ? 就計算出這個算法占用了大約?36.5%?的CPU資源,值得說明的是,從原理上看,這一方式對裸機和RTOS同樣有效哦。 ?
有的小伙伴很快會說,我的系統(tǒng)并不允許我調用printf,那我還可以使用?__cycleof__()?么?當然了!就繼續(xù)以上述代碼為例子:
int32_t?nCycleUsed?=?0; ... __cycleof__("my algorithm", { nCycleUsed = _; }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); } ...這里的代碼所實現(xiàn)的功能是:測量了用戶函數(shù)?my_algorithm_step_xxx()?所使用的周期數(shù):
測量的結果被轉存到了一個叫做?nCycleUsed?的變量中;
__cycleof__()?將不會調用?printf()?進行任何內容輸出。
我相信很多小伙伴會揉了揉眼睛、仔細看了又看,然后回過頭來滿頭問號: ? ? ? 這是C語言? 這是什么語法? ? 不要懷疑,這就是C語言,只不過使用了一點GCC的語法擴展(感興趣的小伙伴可以復制這里的連接?https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html#Statement-Exprs),考慮到本文只介紹?perf_counter?如何使用,而對其如何實現(xiàn)的并不關心,我們不妨略過GCC擴展語法的部分,專門來看看上述代碼的使用細節(jié): ?
首先,為了方便大家觀察,我們先忽略圓括號內的部分:
__cycleof__(...)?{ my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); } ...可以發(fā)現(xiàn),這里跟此前并沒有什么不同:花括號包圍的部分就是我們要測量的代碼片斷; ?接下來,我們專門來看__cycleof__()?圓括號中的部分:
int32_t?nCycleUsed?=?0; __cycleof__("my algorithm", { nCycleUsed = _; ????}) { ... } ...容易發(fā)現(xiàn),如果以“,” 為分隔符,那么實際傳遞給?__cycleof__()?的是兩個部分: ? 1、標注測量名稱的字符串"my algorithm"2、一段用花括號括起來的代碼片斷:{nCycleUsed = _;}其中,nCycleUsed?是一個事先已經初始化好的變量。 ? 這里,對于表示測量名稱的字符串"my algorithm",在這一用法下在最終的編譯結果里并不會占用任何RAM或者是ROM,但作為語法結構是必須的。 ? 對于花括號所囊括的代碼片段來說,實際上在這個花括號里,你幾乎可以為所欲為:你可以寫任意數(shù)量的代碼
你可以調用函數(shù)
你可以定義變量(當然這里定義變量肯定就是局部變量了)
但我們一般要做的事情其實是通過__cycleof__()?所定義的一個局部變量"_"來獲取測量結果——這也是下面代碼的本意:
nCycleUsed = _;需要說明的是,這個局部變量"_"生命周期僅限于這個花括號中,因此不會影響 __cycleof__() 整個結構之外的部分——或者說,下述代碼是沒有意義的: ?int32_t nCycleUsed = 0; ... __cycleof__("my algorithm", { nCycleUsed = _; }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); } printf("Cycle Used %d", _);編譯器會毫不客氣的告訴你 "_" 是一個未定義的變量,反之如果你這么做:int32_t nCycleUsed = 0; ... __cycleof__("my algorithm", { nCycleUsed = _; printf("Cycle Used %d", _); }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); }則會看到你心怡的輸出結果:
系統(tǒng)時間戳 get_system_ticks()?
如果你對上述例子的等效形式(展開形式)感到非常好奇,其實大可不必,上述代碼在“邏輯上等效”于如下的形式: ?
int32_t nCycleUsed = 0;
do { ????int64_t?_?=?get_system_ticks(); { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); } _ = get_system_ticks() - _; ????//!?我們添加的代碼 nCycleUsed = _; printf("Cycle Used %d", _); }?while(0);是不是突然就沒有那么神秘了?通過“邏輯等效”的形式展開,我們很容易發(fā)現(xiàn)一些有趣的內容: ?起核心作用的是一個叫做?get_system_ticks()?的函數(shù)。實際上它返回的是從復位后 SysTick被使能至今所經歷的 CPU 周期數(shù)——由于它是int64_t 的類型,因此不用擔心超過 SysTick 24位計數(shù)器的量程,也不用擔心人類歷史范圍內會發(fā)生溢出的可能。?知道這一點后,聰明的小伙伴就可以自己整活兒了。
由于 "_"?是一個局部變量,因此可以判斷?__cycleof__() 是支持嵌套的。
需要特別說明的是,get_system_tick()?函數(shù)自己也是有CPU時鐘開銷的,所以如果要獲得較為精確的結果,推薦通過下面的方法來獲取校準值:
static?int64_t?s_lPerfCalib; void?calib_perf_counter(void)?{ int64_t lTemp = get_system_tick(); s_lPerfCalib = get_system_tick() - lTemp; } int64_t?get_perf_counter_calib(void) { ????return s_lPerfCalib; }具體如何使用,這里就不再贅述了。連續(xù)計時模式
為了方便某些特殊場合的測試需求,perf_counter還通過start_cycle_counter()?和?stop_cycle_counter()?的組合提供了類似體育老師所使用秒表的連續(xù)計時功能,即:起跑后可以分別記錄每一個學生所用的時間。具體表現(xiàn)為
int32_t nCycles = 0; start_cycle_counter();???? //!值得強調的是雖然?start_cycle_counter()?和?stop_cycle_counter()?有?start?和?stop?的字樣,但這只有邏輯上的意義而并不會真正的干擾?SysTick?的功能(也就是不會開啟或者關閉?SysTick)。這也是這個庫敢于聲稱自己不會影響用戶已有的?SysTick?功能的原因。
【對RTOS的支持】
雖然 perf_counter 本身可以直接在大部分RTOS環(huán)境下直接使用,但在額外插件的加持下,perf_counter 還可以提供額外的功能,比如:
刨去任務調度的干擾,測量線程內指定代碼所用的時間周期數(shù)。(詳情請參考文章《實時性迷思(5)——實戰(zhàn)RTOS多任務性能分析》)
對跨越多個線程的算法進行精確的性能測量(詳情請參考文章《實時性迷(6)——如何進行跨任務性能分析》)
要獲得上述功能,在MDK環(huán)境下只需要勾選對應補丁即可(一些具體注意事項請參考文章《實時性迷思(5)——實戰(zhàn)RTOS多任務性能分析》),非常方便。
如果你是RT-Thread的用戶,還可以通過官方的包管理器直接獲取最新的版本:
【說在后面的話】
perf_counter 最初誕生于我的日常工作——當我發(fā)現(xiàn)我需要重復的在不同工程間復制性能測試相關的代碼時,制作一個模塊來節(jié)省我的時間就成了偷懶的最好理由。 中文互聯(lián)網上,在嵌入式項目中對系統(tǒng)性能進行測量其實并不是什么熱門話題,在日常應用開發(fā)中,相比定量分析,大家可能更喜歡一拍腦袋的純憑感覺來評價系統(tǒng)的性能。我在文章《【實時性迷思】CPU究竟跑的有多快?》已經對此做了吐槽。相比之下,如何實現(xiàn) us 級別的延時則更為流行一些。看到不少人用DWT之類限定于某幾個處理器的不推薦用戶使用的調試類外設作為延時,看到手中明明有更好的、且通用的方案,我實在不敢獨享——這也成了perf_counter成為github上一個開源項目的契機。 ? 相比最初那簡陋的代碼,到現(xiàn)在最新的 v1.9.9版,我很難抑制內心的那種自我感動。感謝大家的支持——是你們的Star支撐著我一路對項目的持續(xù)更新。謝謝! ? ? ?
編輯:黃飛
?
評論