大家好,我是痞子衡,是正經搞技術的痞子,今天痞子衡給大家分享的是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主頁、微信公眾號 平臺上,
微信搜索"痞子衡嵌入式"或者掃描下面二維碼,就可以在手機上第一時間看了哦,

轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/1794.html
標籤:嵌入式
上一篇:kvmalloc函式
