大家好,我是痞子衡,是正經(jīng)搞技術(shù)的痞子。今天痞子衡給大家分享的是 i.MXRT 上進一步提升代碼執(zhí)行性能的經(jīng)驗。
今天跟大家聊的這個話題還是跟痞子衡最近這段時間參與的一個基于 i.MXRT1170 的大項目有關(guān),痞子衡在做其中的開機動畫功能,之前寫過一篇文章 《降低刷新率是定位 LCD 花屏顯示問題的第一大法》 介紹了開機動畫功能的實現(xiàn)以及 LCD 顯示注意事項,在此功能上,痞子衡想進一步測試從芯片上電到 LCD 屏顯示第一幅完整圖像的時間,這個時間我們暫且稱為 1st UI 時間,該時間的長短對項目有重要意義。
痞子衡分別測試了代碼在 XIP 執(zhí)行下和在 TCM 里執(zhí)行下的 1st UI 時間,得到的結(jié)果竟然是 XIP 執(zhí)行比 TCM 執(zhí)行還要快 50ms,這是怎么回事?這完全顛覆了我們的理解,i.MXRT 上 TCM 是與內(nèi)核同頻的,F(xiàn)lash 速度遠低于 TCM。如果是 XIP 執(zhí)行,即使有 I-Cache 加速,也最多與 TCM 執(zhí)行一樣快,怎么可能做到比 TCM 執(zhí)行快這么多。于是痞子衡便開始深挖這個奇怪的現(xiàn)象,然后發(fā)現(xiàn)了進一步提升代碼執(zhí)行性能的秘密。
一、引出計時差異問題
痞子衡的開機動畫程序是基于 SDK_2.x.x_MIMXRT1170-EVKboardsevkmimxrt1170jpeg_examplessd_jpeg 例程的,只是去了 SD 卡和 libjpeg 庫相關(guān)代碼。工程有兩個 build,一個是 TCM 里執(zhí)行(即 debug),另一個是 XIP 執(zhí)行(即 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 圖像幀開始和結(jié)束的時間點。翻轉(zhuǎn) 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 圖像幀結(jié)束時間點
????light_led(2,?0);
}
?
上圖是痞子衡抓到的波形(30Hz,XIP),痞子衡一共做了四次測試,分別是 30Hz LCD 刷新率下的 XIP/TCM 以及 60Hz LCD 刷新率下的 XIP/TCM,結(jié)果如下表所示。表中的 Init Time 一欄表示的是開機動畫程序代碼執(zhí)行時間(從 SystemInit()函數(shù)開始執(zhí)行到 APP_InitDisplay()函數(shù)結(jié)束的時間),可以看到 TCM 執(zhí)行比 XIP 執(zhí)行慢近 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 執(zhí)行比 TCM 執(zhí)行快這個結(jié)果,痞子衡是不相信的,于是痞子衡便用二分法逐步查找,發(fā)現(xiàn)時間差異是 BOARD_InitLcdPanel()函數(shù)里的 DelayMs()調(diào)用引起的,這些人為插入的延時是 LCD 屏控制器手冊里的要求,總延時時間應該是 153ms,但是這個函數(shù)的執(zhí)行在 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
????//?...
}
所以現(xiàn)在的問題就是為何在 TCM 里執(zhí)行 DelayMs(153)需要 203ms,而 XIP 執(zhí)行下是精確的。讓我們進一步查看 DelayMs()函數(shù)的原型,這個函數(shù)其實調(diào)用的是 SDK_DelayAtLeastUs()函數(shù),SDK_DelayAtLeastUs()函數(shù)從命名上看就很有意思,AtLeast 即保證軟延時一定能滿足用戶設(shè)置的時間,但也可能超過這個時間。為何是 AtLeast 設(shè)計,其實這里就涉及到 Cortex-M7 內(nèi)核一個很重要的特性 - 指令雙發(fā)射,軟件延時的本質(zhì)是靠 CPU 執(zhí)行指令來消耗時間,但是 CPU 拿指令到底是單發(fā)射還是雙發(fā)射有一定的不確定性,因此無法做到精確,如果以全雙發(fā)射來計算,就能得出最小延時時間。
#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);
}
分析到現(xiàn)在,問題已經(jīng)轉(zhuǎn)化成為何 XIP 下執(zhí)行指令雙發(fā)射概率比 TCM 里執(zhí)行指令雙發(fā)射概率更大,關(guān)于這個現(xiàn)象并沒有在 ARM 官方文檔里查找到相關(guān)信息,DelayLoop()循環(huán)里只是 3 條指令,XIP 下執(zhí)行肯定是在 Cache line 里,這跟在 TCM 里執(zhí)行并沒有什么區(qū)別。讓我們再去看看兩個工程的 map 文件,找到 DelayLoop()函數(shù)鏈接地址,這個函數(shù)在兩個測試工程下鏈接地址對齊不一樣,這意味著測試條件不完全相同,或許這是一個解決問題的線索。
XIP 執(zhí)行工程(flexspi_nor_debug),DelayLoop()函數(shù)地址 8 字節(jié)對齊:
*******************************************************************************
*** ENTRY LIST
***
Entry Address Size Type Object
----- ------- ---- ---- ------
DelayLoop 0x3000'3169 0xa Code Lc fsl_common.o [1]
TCM 執(zhí)行工程(debug 工程),DelayLoop()函數(shù)地址 4 字節(jié)對齊:
*******************************************************************************
*** ENTRY LIST
***
Entry Address Size Type Object
----- ------- ---- ---- ------
DelayLoop 0x314d 0xa Code Lc fsl_common.o [1]
三、找到計時差異本質(zhì)
前面找到 DelayLoop()函數(shù)鏈接地址差異是一個線索,那我們就針對這個線索做測試,不再讓鏈接器自動分配 DelayLoop()函數(shù)地址,改為在鏈接文件里指定地址去鏈接,下面代碼是 IAR 環(huán)境下的示例,我們使用 debug 工程(即在 TCM 執(zhí)行)來做測試。
C 源文件中在 DelayLoop()函數(shù)定義前加#pragma location = ".myFunc",即將該函數(shù)定義為 .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 };
根據(jù)鏈接起始地址 m_text_func_start 的不同,我們得到了不同的結(jié)果,如下表所示。至此真相大白,造成 DelayMs()函數(shù)執(zhí)行時間不同的根本原因不是 XIP/TCM 執(zhí)行差異,而是鏈接地址對齊差異,8 字節(jié)對齊的函數(shù)更容易觸發(fā) CM7 指令雙發(fā)射,相比 4 字節(jié)對齊的函數(shù)在性能上能提升 24.8% 。
m_text_func_start 值 | 鏈接地址對齊 | 函數(shù)調(diào)用語句 | 實際執(zhí)行時間 |
---|---|---|---|
0x00004000 | 8n 字節(jié) | DelayMs(100) | 100ms |
0x00004002 | 2 字節(jié),未能鏈接 | N/A | N/A |
0x00004004 | 4 字節(jié) | DelayMs(100) | 133ms |
0x00004008 | 8 字節(jié) | DelayMs(100) | 100ms |
現(xiàn)在我們得到了一個有趣的結(jié)論,Cortex-M7 上將函數(shù)鏈接到 8 字節(jié)對齊的地址有利于指令雙發(fā)射,這就是進一步提升代碼執(zhí)行性能的秘密。
至此,i.MXRT 上進一步提升代碼執(zhí)行性能的經(jīng)驗痞子衡便介紹完畢了,掌聲在哪里~~~