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