大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT上進一步提升程式碼執行效能的經驗。
今天跟大家聊的這個話題還是跟痞子衡最近這段時間參與的一個基於i.MXRT1170的大專案有關,痞子衡在做其中的開機動畫功能,之前寫過一篇文章 《降低重新整理率是定位LCD花屏顯示問題的第一大法》 介紹了開機動畫功能的實現以及LCD顯示注意事項,在此功能上,痞子衡想進一步測試從晶片上電到LCD屏顯示第一幅完整影像的時間,這個時間我們暫且稱為1st UI時間,該時間的長短對專案有重要意義。
痞子衡分別測試了程式碼在XIP執行下和在TCM裡執行下的1st UI時間,得到的結果竟然是XIP執行比TCM執行還要快50ms,這是怎麼回事?這完全顛覆了我們的理解,i.MXRT上TCM是與核心同頻的,Flash速度遠低於TCM。如果是XIP執行,即使有I-Cache加速,也最多與TCM執行一樣快,怎麼可能做到比TCM執行快這麼多。於是痞子衡便開始深挖這個奇怪的現象,然後發現了進一步提升程式碼執行效能的祕密。
一、引出計時差異問題
痞子衡的開機動畫程式是基於 \SDK_2.x.x_MIMXRT1170-EVK\boards\evkmimxrt1170\jpeg_examples\sd_jpeg 例程的,只是去了SD卡和libjpeg庫相關程式碼。工程有兩個build,一個是TCM裡執行(即debug),另一個是XIP執行(即flexspi_nor_debug)。
專案板上的Flash型號是MX25UW51345G,痞子衡將其配成Octal mode, DDR, 166MHz用於啟動。專案板上還有兩個LED燈,痞子衡在LED燈上飛了兩根線,連同POR引腳一起連上示波器,用於精確測量1st UI各部分時間組成。
示波器通道1連線POR引腳,表明1st UI時間起點;通道2連線LED1 GPIO,表明ROM啟動時間(進入使用者APP的時間點);通道3連線LED2 GPIO,做兩次電平變化,分別是1st影像幀開始和結束的時間點。翻轉LED GPIO程式碼位置如下:
void light_led(uint32_t ledIdx, uint8_t ledVal);
void SystemInit (void) {
// 將LED1置1,標示ROM啟動時間
light_led(1, 1);
SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2));
// ...
}
void APP_InitDisplay(void)
{
// ...
g_dc.ops->enableLayer(&g_dc, 0);
// 將LED2置1,標示1st影像幀開始時間點
light_led(2, 1);
}
int main(void)
{
BOARD_ConfigMPU();
BOARD_InitBootPins();
BOARD_BootClockRUN();
BOARD_ResetDisplayMix();
APP_InitDisplay();
while (1)
{
// ...
}
}
static void APP_BufferSwitchOffCallback(void *param, void *switchOffBuffer)
{
s_newFrameShown = true;
// 將LED2置0,標示1st影像幀結束時間點
light_led(2, 0);
}
上圖是痞子衡抓到的波形(30Hz,XIP),痞子衡一共做了四次測試,分別是30Hz LCD重新整理率下的XIP/TCM以及60Hz LCD重新整理率下的XIP/TCM,結果如下表所示。表中的Init Time一欄表示的是開機動畫程式程式碼執行時間(從SystemInit()函式開始執行到APP_InitDisplay()函式結束的時間),可以看到TCM執行比XIP執行慢近50ms,這便是奇怪問題所在。
程式碼位置 | LCD重新整理率 | POR Time | Boot Time | Init Time | Launch Time |
---|---|---|---|---|---|
XIP | 30Hz | 3.414ms | 10.082ms | 34.167ms + 153ms | 32.358ms |
TCM | 30Hz | 3.414ms | 10.854ms | 33.852ms + 203ms | 32.384ms |
XIP | 60Hz | 3.414ms | 9.972ms | 18.142ms + 153ms | 16.166ms |
TCM | 60Hz | 3.414ms | 10.92ms | 17.92ms + 203ms | 16.104ms |
二、定位計時差異問題
對於開機動畫程式碼,XIP執行比TCM執行快這個結果,痞子衡是不相信的,於是痞子衡便用二分法逐步查詢,發現時間差異是BOARD_InitLcdPanel()函式裡的DelayMs()呼叫引起的,這些人為插入的延時是LCD屏控制器手冊裡的要求,總延時時間應該是153ms,但是這個函式的執行在XIP下(153ms)和TCM裡(203ms)時間不同。
static void BOARD_InitLcdPanel(void)
{
// ...
#if (DEMO_PANEL == DEMO_PANEL_TM103XDKP13)
// ...
/* Power LCD on */
GPIO_PinWrite(LCD_RESET_GPIO, LCD_RESET_GPIO_PIN, 1);
DelayMs(2);
GPIO_PinWrite(LCD_RESET_GPIO, LCD_RESET_GPIO_PIN, 0);
DelayMs(5);
GPIO_PinWrite(LCD_RESET_GPIO, LCD_RESET_GPIO_PIN, 1);
DelayMs(6);
GPIO_PinWrite(LCD_STBYB_GPIO, LCD_STBYB_GPIO_PIN, 1);
DelayMs(140);
#endif
// ...
}
所以現在的問題就是為何在TCM裡執行DelayMs(153)需要203ms,而XIP執行下是精確的。讓我們進一步檢視DelayMs()函式的原型,這個函式其實呼叫的是SDK_DelayAtLeastUs()函式,SDK_DelayAtLeastUs()函式從命名上看就很有意思,AtLeast即保證軟延時一定能滿足使用者設定的時間,但也可能超過這個時間。為何是AtLeast設計,其實這裡就涉及到Cortex-M7核心一個很重要的特性 - 指令雙發射,軟體延時的本質是靠CPU執行指令來消耗時間,但是CPU拿指令到底是單發射還是雙發射有一定的不確定性,因此無法做到精確,如果以全雙發射來計算,就能得出最小延時時間。
#define DelayMs VIDEO_DelayMs
#if defined(__ICCARM__)
static void DelayLoop(uint32_t count)
{
__ASM volatile(" MOV R0, %0" : : "r"(count));
__ASM volatile(
"loop: \n"
" SUBS R0, R0, #1 \n"
" CMP R0, #0 \n"
" BNE loop \n");
}
#endif
void SDK_DelayAtLeastUs(uint32_t delay_us, uint32_t coreClock_Hz)
{
assert(0U != delay_us);
uint64_t count = USEC_TO_COUNT(delay_us, coreClock_Hz);
assert(count <= UINT32_MAX);
#if (__CORTEX_M == 7)
count = count / 3U * 2U;
#else
count = count / 4;
#endif
DelayLoop(count);
}
void VIDEO_DelayMs(uint32_t ms)
{
SDK_DelayAtLeastUs(ms * 1000U, SystemCoreClock);
}
分析到現在,問題已經轉化成為何XIP下執行指令雙發射概率比TCM裡執行指令雙發射概率更大,關於這個現象並沒有在ARM官方文件裡查詢到相關資訊,DelayLoop()迴圈裡只是3條指令,XIP下執行肯定是在Cache line裡,這跟在TCM裡執行並沒有什麼區別。讓我們再去看看兩個工程的map檔案,找到DelayLoop()函式連結地址,這個函式在兩個測試工程下連結地址對齊不一樣,這意味著測試條件不完全相同,或許這是一個解決問題的線索。
XIP執行工程(flexspi_nor_debug),DelayLoop()函式地址8位元組對齊:
*******************************************************************************
*** ENTRY LIST
***
Entry Address Size Type Object
----- ------- ---- ---- ------
DelayLoop 0x3000'3169 0xa Code Lc fsl_common.o [1]
TCM執行工程(debug工程),DelayLoop()函式地址4位元組對齊:
*******************************************************************************
*** ENTRY LIST
***
Entry Address Size Type Object
----- ------- ---- ---- ------
DelayLoop 0x314d 0xa Code Lc fsl_common.o [1]
三、找到計時差異本質
前面找到DelayLoop()函式連結地址差異是一個線索,那我們就針對這個線索做測試,不再讓連結器自動分配DelayLoop()函式地址,改為在連結檔案裡指定地址去連結,下面程式碼是IAR環境下的示例,我們使用debug工程(即在TCM執行)來做測試。
C原始檔中在DelayLoop()函式定義前加#pragma location = ".myFunc",即將該函式定義為.myFunc的段,然後在連結檔案icf中用place at語句指定.myFunc段到固定地址m_text_func_start處開始連結:
#if defined(__ICCARM__)
#pragma location = ".myFunc"
static void DelayLoop(uint32_t count)
{
// ...
}
#endif
define symbol m_text_func_start = 0x00004000;
place at address mem: m_text_func_start { readonly section .myFunc };
define symbol m_text_start = 0x00002400;
define symbol m_text_end = 0x0003FFFF;
place in TEXT_region { readonly };
根據連結起始地址m_text_func_start的不同,我們得到了不同的結果,如下表所示。至此真相大白,造成DelayMs()函式執行時間不同的根本原因不是XIP/TCM執行差異,而是連結地址對齊差異,8位元組對齊的函式更容易觸發CM7指令雙發射,相比4位元組對齊的函式在效能上能提升24.8% 。
m_text_func_start值 | 連結地址對齊 | 函式呼叫語句 | 實際執行時間 |
---|---|---|---|
0x00004000 | 8n位元組 | DelayMs(100) | 100ms |
0x00004002 | 2位元組,未能連結 | N/A | N/A |
0x00004004 | 4位元組 | DelayMs(100) | 133ms |
0x00004008 | 8位元組 | DelayMs(100) | 100ms |
現在我們得到了一個有趣的結論,Cortex-M7上將函式連結到8位元組對齊的地址有利於指令雙發射,這就是進一步提升程式碼執行效能的祕密。
至此,i.MXRT上進一步提升程式碼執行效能的經驗痞子衡便介紹完畢了,掌聲在哪裡~~~
歡迎訂閱
文章會同時釋出到我的 部落格園主頁、CSDN主頁、微信公眾號 平臺上。
微信搜尋"痞子衡嵌入式"或者掃描下面二維碼,就可以在手機上第一時間看了哦。