來源:裸機思維
【說在前面的話】相信很多人都遇到過這樣的情況:在一個Cortex-M嵌入式應用中要實現(xiàn)一個精確的毫秒級延時并不困難——如果你有RTOS,在任務中使用諸如 os_sleep(<休眠時間>) 之類的函數(shù)就可以輕松實現(xiàn);如果你是裸機,也可以使用每個Cortex-M芯片都默認攜帶的SysTick來實現(xiàn)一個,甚至Arm官方的CMSIS都提供了現(xiàn)成的API,即SysTick_Config(<中斷間隔的時鐘周期數(shù)>):
static volatile uint32_t s_wMSCounter = 0;
extern?uint32_t?SystemCoreClock;
/*! \brief initialise platform before main()
*/
__attribute__((constructor(101)))
void platform_init(void)
{
SystemCoreClockUpdate();
/* Generate interrupt each 1 ms */
SysTick_Config(SystemCoreClock / 1000);
}
__attribute__((weak))
void systimer_1ms_handler(void)
{
????/*?default?systimer?1ms?hander?
?????*?you?can?override?it?by?implement?a?non-weak version
?????*/
}
void SysTick_Handler (void)
{
if (s_wMSCounter) {
s_wMSCounter--;
}
systimer_1ms_handler();
}
void delay_ms(uint32_t wMillisecond)
{
s_wMSCounter = wMillisecond;
while( s_wMSCounter > 0 );
}
上述代碼非常典型,唯一需要強調(diào)的是SystemCoreClock是一個定義在啟動文件system_<芯片型號>.c 里的全局變量,負責保存當前處理器的工作頻率——上面的平臺初始化函數(shù) platform_init()?就是借助這一變量把 SysTick 初始化為以“1ms為間隔產(chǎn)生中斷”的。如果要實現(xiàn)一個微秒級延時卻并不那么一帆風順。首先,不同人關于實現(xiàn)方案就有不同的想法,比如:
- 有的人習慣于直接用軟件方法堆積NOP()來實現(xiàn)——這種方法所產(chǎn)生的延時效果“可能”容易受到編譯器優(yōu)化等級的影響——據(jù)說這也是很多人懼怕開啟編譯器的原因之一,因為一開優(yōu)化,很多對時間敏感的硬件時序就因為延時函數(shù)的不穩(wěn)定而一起變得不可捉摸;
extern?uint32_t?SystemCoreClock;
#ifndef DELAY_US_CALIBRATION
/*!?\brief?不要問我為啥是?8, 我也不知道,但在當前這個工程下,8貌似最準
?*!????????你如果不服,就自己測一個,然后定義這個宏……
?*!????????如果你頭鐵改了工程的優(yōu)化等級,請也無比親自測一下……具體怎么
?*! ???????測,我也不知道。如果你也怕麻煩,就不要改優(yōu)化等級。
*/
# define DELAY_US_CALIBRATION 8
#endif
void?delay_us(uint32_t?wUS)
{
????//!?calcluate?how?many?cycles?required?for?1us
????uint32_t?wCyclesPerUS?=?SystemCoreClock?/?1000000ul;
????
????/*!?subtract some cycles from wCyclesPerUS based on the
?????*!?experience?or?actual?measurement?in?current?optimisation
?????*/
????wCyclesPerUS?-=?DELAY_US_CALIBRATION;
????for?(int?i?=?0;?i?
????????for?(j?=?0;?j?< wCyclesPerUS; j ) {
???????? __NOP();
????????}
????}
}
- 有的人提倡使用定時器來實現(xiàn)精確延時,這一方案顯然不太懼怕編譯器優(yōu)化的“血腥巨斧”。想法是沒錯的,但如果要保證這樣寫出來的延時庫有一定的可移植性,就需要保證 delay_us() 函數(shù)實現(xiàn)所依賴的硬件定時器是“通用的”和“普遍存在”的——符合這一要求的第一選擇是SysTick——然而既然SysTick已經(jīng)被 delay_ms() 占用了,又如何能抽的開身呢?
既然 SysTick 被占用了,那有沒有別的符合要求的硬件呢?如果不算Cortex-M0/M0 的話,從某種程度上說還真有——DWT。這是一個系統(tǒng)外設,專門用來為Cortex-M3及其以上芯片提供調(diào)試和追蹤的硬件輔助功能。在【裸機思維】往期轉載的文章中,就有使用DWT實現(xiàn)延時的內(nèi)容。這個方法好是好,但缺點也是非常突出的:
- DWT 根本就不是設計給用戶用的,它是Cortex-M處理器預留給上位機調(diào)試軟件(例如MDK)進行調(diào)試和追蹤的。換句話說,上位機調(diào)試軟件覺得這是自己的私人財產(chǎn),從來沒想過用戶會去使用它——這就導致調(diào)試過程中,IDE會按照自己的意思隨意修改它的配置——啥時候會改呢?這要看IDE的心情。如果你的程序依賴了DWT進行延時,那么調(diào)試的時候,IDE的一個無心之舉可能就會毀了你的時序——這一知識點非常容易忽略掉,從而導致很多人遇到調(diào)試的時候,系統(tǒng)隨機性的功能不正常的坑,從而浪費大把的時間,往往還想不到是DWT導致的——說這一方法是天坑可能一點也不為過。
- DWT 不是所有 Cortex-M 芯片都有……(Cortex-M0/M0 就沒有)
既然 SysTick 被占用、DWT?又是天坑,是不是意味著我們就只能使用芯片的普通定時器了?——這每個廠家都不一樣……每個應用對定時器的使用情況也都不同,那我還怎么做通用的延時庫啊?
別急,今天我們就來介紹一種在完全不影響 SysTick 已有功能的前提下,繼續(xù)把它榨干——提供更多功能的方法。為了避免誤解,我把這種方法的目標需求列舉如下:
- 提供一個精確的 delay_us() 函數(shù);
- 提供一個精確測量任意代碼塊所實際占用系統(tǒng)周期數(shù)的方法;
- 實現(xiàn)一個記錄從進入 main() 函數(shù)以來總共經(jīng)歷了多少個時鐘周期(且在合理的時間范圍內(nèi)不會溢出)的計數(shù)器(時間戳);
- 用戶已有的 SysTick 功能不能受到干擾;
- 比如用戶使用 SysTick 作為RTOS的基準時鐘(非Tickless模式);
- 比如用戶使用 SysTick 作為普通的毫秒級延時(就像前面例子代碼所展示的那樣);
- 用戶不需要修改自己任何已有的 SysTick 代碼。
【部署 perf_counter 庫】要實現(xiàn)上述功能,可以直接借助一個叫做 perf_counter 的庫,這是我基于這幾年在代碼性能分析中總結出來的,我已經(jīng)把它放在 github 上進行開源,其地址為:https://github.com/GorgonMeducer/perf_counter
這個庫目前支持 Arm Compiler 5(armcc) 和 Arm Compiler 6(armclang)。它不僅提供了源代碼,還提供了編譯好的 library (.lib)可供全系列Cortex-M處理器使用。
第一步,下載最新的release:
解壓縮后可以看到如下的內(nèi)容:
如果只是普通的使用,直接拷貝 lib 目錄到你的工程即可。
第二步,將庫加入到已有的 MDK 工程中:
別忘記在工程的頭文件搜尋路勁中包含 perf_counter.h 所在文件夾,例如(具體位置根據(jù)你工程的情況而定,不要死腦經(jīng)):
第三步:編譯并調(diào)整一些工程選項
如果你編譯后很順利,則請?zhí)^下面的內(nèi)容,快進到 0 error 0 warning的圖片之后。
好,下面讓我們來談談你可能遇到的問題,以及對應的解決方案:
問題一:提示找不到 $Super$$SysTick_Handler
.\Out\example.axf: Error: L6218E: Undefined symbol $Super$$SysTick_Handler (referred from systick_wrapper_ual.o).
Not enough information to list image symbols.
Not enough information to list load addresses in the image map.
Finished: 2 information, 0 warning and 1 error messages.
".\Out\example.axf" - 1 Error(s), 0 Warning(s).
perf_counter 庫是一個“附加型”庫——它假設你自己已經(jīng)實現(xiàn)了一個SysTick的中斷處理程序,并開啟了中斷模式——如果你沒有,直接加一個空的就好了:
void SysTick_Handler (void)
{
}
好,問題解決。什么?你的工程也根本沒有用SysTick?好辦,請在進入main后調(diào)用函數(shù)init_cycle_counter()?并傳遞false,例如:int main(void)
{
...
init_cycle_counter(false);
...
}
這樣做的目的是告訴 perf_counter:“請自己玩的開心”。問題二:wchar和enum的尺寸不兼容:需要強調(diào)的是,perf_counter.lib 庫在編譯的時候,開啟了 Short enums/wchar(分別對應命令行的?-fshort-enums -fshort-wchar)。這么做其實沒什么特別的原因,但如果你的工程使用了不同的配置,例如:
下圖的工程配置中,沒有勾選 "Short enums/wchar"
你一定會看到這樣的編譯錯誤:
.\Out\example.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.
既然知道了原因,解決方法就很簡單,要么在工程配置中勾選上這一選項;要么使用源代碼編譯(不使用lib):
也就是圖中所示的:perf_counter.c 和 systick_wrapper_ual.s。
? ? perf_counter.c 依賴了 CMSIS,所以確保你的工程中加入了對CMSIS的支持——推薦的是使用MDK自帶的 CMSIS,在RTE配置界面中勾選:
如果你使用的是工程自帶的CMSIS(很多STM32工程就是這樣),請確保你的CMSIS?是較新的版本(判斷標準就是是否帶有 cmsis_compiler.h)。
此外,這里的 systick_wrapper_ual.s 是一個匯編源程序,使用的是Arm的老語法(Unified Assembly Language),如果你的工程使用的是 Arm Compiler 5(armcc),這里就沒什么需要特別注意的了;如果你的工程使用的是 Arm Compiler 6(armclang),則你需要檢查工程配置,以確保MDK能正確的選擇對應的Assembler:
注意這里的 Assembler Option,根據(jù)你MDK版本的不同,它可能有以下幾個有效選項:
- armclang(Auto Select)——我吐血推薦選這個
- armclang(GNU Syntax)——?這個意思就是使用 GNU的匯編語法,顯然不能選它;
- armclang(Arm Syntax)——這是最新MDK(從5.32開始)才有的選項,選了也行;
- armasm(Arm Syntax)——這就是 Arm Compiler 5里一直使用的老匯編器,選他當然兼容性最好。
做好了以上兩個準備工作,編譯應該就很順利了。是不是覺得有點頭大?頭大就用?.lib 啊……完全不用經(jīng)歷這些痛苦。
至此,我們完成了 perf_counter 庫在工程中的部署。那么它帶給我們哪些功能呢?
【SysTick第一吃:微秒級精確延時】
#include?"perf_counter.h"
...
delay_us(30);????//!
...
再也不用擔心編譯器優(yōu)化導致延時不準啦?。?!
再也不擔心庫不通用啦?。?!再也不用擔心芯片不支持DWT啦?。。。。?!再也不用擔心調(diào)試/追蹤會干擾DWT啦?。。?!
【SysTick第二吃:精確測量代碼的時鐘周期】perf_counter.h 提供了兩個函數(shù),用于精確測量任意代碼片段所消耗的CPU時鐘周期數(shù)(不是us數(shù)哦):
extern void start_cycle_counter(void);
extern int32_t stop_cycle_counter(void);
它們的使用非常簡單直接,例如:start_cycle_counter();
//!?測量?打印??"Hello?World\r\n" 究竟用了多少個時鐘周期
printf("Hello World! \r\n");
int32_t?iCycleUsed?=?stop_cycle_counter(void);
printf("Cycle?Used:?%d",?iCycleUsed);
當然,如果你的工程環(huán)境允許你用printf的話,還可以用 perf_counter.h 自帶的宏將上述代碼簡化一下://!?the?demo?of?__cycleof__()
__cycleof__() {
????printf("Hello?World\r\n");
}
其運行結果為:(以上結果為FVP仿真結果,CPU周期數(shù)值不可以做參考)
我們甚至還可以添加一點注釋性的字符串,幫助我們區(qū)分測試的范圍:
//!?the?demo?of?__cycleof__()
__cycleof__("Print?string")?{
????printf("Hello?World\r\n");
}
我們看到,傳遞給__cycleof__的提示字符串"Print string"被添加到了"total cycle count:..." 的前面,一目了然。實際上,start_cycle_counter() 和 stop_cycle_counter() 的組合還可以用來測量中斷處理程序實際使用的系統(tǒng)周期數(shù)——讀過我【實時性迷思】系列文章的小伙伴,一定知道測量“事件處理函數(shù)所需時間”的意義:
volatile?int32_t?g_nMaxHandlingCycles = 0;
void?USART0_RX_Handler(void)
{
????start_cycle_counter();
????
????//!?你的USART0 接收中斷處理程序實際內(nèi)容
????...
????
????int32_t nCycles = stop_cycle_counter();
????g_nMaxHandlingCycles?=?MAX(nCycles,?g_nMaxHandlingCycles);
}
從此一舉告別“拍腦袋憑感覺”說中斷處理時間要多長的舊世界。
此外,start_cycle_counter()?和 stop_cycle_counter() 還支持類似體育老師所使用的秒表的功能,即,起跑后、可以分別記錄每一個學生所用的時間。具體表現(xiàn)為:
int32_t nCycles = 0;
start_cycle_counter();???? //!
...
nCycles?=?stop_cycle_counter();??//!
...
nCycles?=?stop_cycle_counter();??//!
...
nCycles?=?stop_cycle_counter();??//!< 第三次獲取從開始以來的時間
...
具體什么情況下要用到這樣的方式就見仁見智了,這里就不再繼續(xù)展開。最后,需要強調(diào)一下,雖然 start_cycle_counter() 和 stop_cycle_counter() 有 start 和 stop 的字樣,但這只是邏輯上的,并不會真正的干擾 SysTick 的功能(也就是不會開啟或者關閉 SysTick)。這也是這個庫敢于聲稱自己不會影響用戶已有的 SysTick 功能的原因。
【SysTick第三吃:系統(tǒng)時間戳】閱讀到這里,聰明的你一定已經(jīng)發(fā)現(xiàn)了:無論是 perf_counter(performance counter)庫名的明示,還是 start_cycle_counter() 和 stop_cycle_counter() 的強大功能,都暗示其實這個庫應該不是專門用來提供微秒延時函數(shù) delay_us() 的,實際上,只要你稍微看一眼源代碼就會發(fā)現(xiàn)上述猜想完全沒錯—— delay_us() 其實才是附贈的:
void delay_us(int32_t iUs)
{
iUs *= SystemCoreClock / 1000000ul;
start_cycle_counter();
while(stop_cycle_counter() < iUs);
}
看到真相的你,有沒有意識到,在 start_cycle_counter() 和 stop_cycle_counter() 之間不能調(diào)用 delay_us() 呢?既然 delay_us() 都是“cycle counter”送的,還有啥別的功能是附贈的么?——還真有:系統(tǒng)時間戳。想象一下,既然 start_cycle_counter() 和 stop_cycle_counter() 的組合可以獲得從開始以來的時間,那么如果我在進入main()之前就執(zhí)行 start_cycle_counter() ,然后在需要的時候調(diào)用 stop_cycle_counter() 是不是就可以獲取“從main()開始已經(jīng)執(zhí)行了多少個周期”的系統(tǒng)時間戳呢?
Bingo!答對了,原理上就是這樣,只不過實際上,為了保留 start_cycle_counter() 和 stop_cycle_counter() 給用戶使用,per_counter庫就自己獨立實現(xiàn)了對應的邏輯——用戶可以通過調(diào)用函數(shù)?clock() 來獲取這一信息:
#ifdef __PERF_CNT_USE_LONG_CLOCK__
__attribute__((nothrow))
extern int64_t clock(void);
#endif
熟悉系統(tǒng)庫 extern _ARMABI clock_t clock(void);
而 clock_t 在 Cortex-M環(huán)境下定義如下:typedef unsigned int clock_t; /* cpu time type */
為什么perf_counter.h 要采用不一樣的定義呢?說起來也簡單:clock() 函數(shù)返回的是系統(tǒng)周期數(shù),而不是什么以 us 或者 ms 為單位的時間——考慮到現(xiàn)在處理器頻率動輒幾百兆赫茲,有的甚至達到了1GHz(比如 NXP的RT系列),如果用 int32_t?(哪怕用 uint32_t)也撐不了幾秒鐘。
假設系統(tǒng)頻率為1GHz,使用 uint32_t 來計數(shù),由于32bit整數(shù)取值范圍是0~4G,因此,最多4秒就撐不住了……
那究竟多長才夠呢?
當我們使用 int64_t 的時候,哪怕系統(tǒng)頻率是 4GHz,2G 秒 ≈ 24855 天 ≈ 68年。雖然沒有一萬年那么久,不過多半一個嵌入式設備也沒法用這么久(千年蟲警告),但考慮到大部分Cortex-M嵌入式系統(tǒng)估計沒有4GHz這么夸張,輕松跑個1000多年不溢出應該是沒有問題的。
既然我們鐵了心要用 int64_t 來取代 clock_t 原本的 int32_t,怎么解決這里的沖突呢?——顯然去修改系統(tǒng)頭文件
翻開Arm的隱藏寶典:AAPCS,我們發(fā)現(xiàn)以下的規(guī)則:32位系統(tǒng)下,
- 如果函數(shù)的返回值其大小不超過32bit,則保存在寄存器 r0中;
- 如果函數(shù)的返回值其大小為64bit,則其低 32bit 保存在 r0中、高32bit保存在 r1中。
顯然,當我們實現(xiàn)clock()函數(shù)時返回 int64_t的值與 返回 int32_t其實是兼容的——因為低32bit的內(nèi)容實際上都是保存在 r0 里的,此時如果用戶調(diào)用clock() 的時候:
- 使用的是
里定義的函數(shù)原型,即? clock_t clock(void),則,當函數(shù)返回時,r1里保存的值會被無視,只有r0里的值被視作返回值; - 使用的是我們自己定義的函數(shù)原型,即 int64_t clock(void),則你可以獲得完整的 int64_t 時間戳。
既然原理清楚了,再看 perf_counter.h 里面的定義,我們會發(fā)現(xiàn)clock()的函數(shù)原型被一個宏?__PERF_CNT_USE_LONG_CLOCK__ 保護著:
#ifdef __PERF_CNT_USE_LONG_CLOCK__
__attribute__((nothrow))
extern int64_t clock(void);
#endif
這實際上是告訴我們,如果我們想獲得 int64_t 時間戳時,只要在工程中定義宏??__PERF_CNT_USE_LONG_CLOCK__?就可以了。忙活了半天,有的小伙伴可能會疑惑了:饒了這么一大圈,clock() 究竟有啥用處呢?這玩法就多了,快一鍵三聯(lián)~ 下次我們好好來說說。
【后記】perf_counter(https://github.com/GorgonMeducer/perf_counter)是我在工作中總結和整理出的一個庫,它的特點是在不干擾已有 SysTick 功能的前提下額外為我們提供系統(tǒng)周期測量的功能——并在這基礎上衍生出了 delay_us() 和 系統(tǒng)時間戳的功能——正可謂一鴨三吃,把SysTick榨干到了極致。perf_counter 庫的原理其實很簡單,但其中要處理的 corner case 確實很惱人,我也是歷經(jīng)一年多才真正想明白這里面的彎彎繞。后面如果閱讀量不錯的話,我會考慮專門出一篇介紹 perf_counter 原理的文章。其中,關于如何“不影響現(xiàn)有SysTick中斷處理程序”的功能,已經(jīng)在之前的文章《【嵌入式秘術】手把手教你如何劫持RTOS》中進行了詳細介紹,有興趣的小伙伴可以再回味回味。
在開源的過程中,為了簡化用戶的使用,我做了如下的優(yōu)化:
- 在 Arm Compiler 5(armcc)和 Arm Compiler 6中,不需要用戶手工對庫進行初始化——庫會在進入main()之前“自己做”;
- Lib中的perf_counter.lib適用于包含Cortex-M0在內(nèi)的全系列Cortex-M處理器,做到全覆蓋;
- perf_counter.h 幾乎不依賴
和 之外的庫。使用.lib進行部署,非常簡潔方便。
perf_counter庫的使用當然也存在限制,重要的事情在最后說:
- 如果你原本的 RTOS 使用了 SysTick并開啟了Tickless模式,perf_counter雖然不會干擾原有的 SysTick功能,但自己的計時功能卻會受到 Tickless模式的干擾;
- perf_counter庫假設你原本的SysTick應用會保持一個固定的定時周期——也就是 LOAD寄存器的內(nèi)容是固定的、不會隨著程序的執(zhí)行而經(jīng)常變化。其實RTOS的tickless模式會干擾perf_counter的計數(shù)可靠性也是這個原因。
一般來說,大部分RTOS和普通的周期性定時功能都不會經(jīng)常動態(tài)的去改變SysTick的計數(shù)周期,所以不必太擔心。
原創(chuàng)不易,如果你喜歡我的思維、覺得我的文章對你有所啟發(fā),請務必 “點贊、收藏、轉發(fā)” 三連,這對我很重要!謝謝!
歡迎訂閱 裸機思維