痞子衡嵌入式:鏈接函數到8字節對齊地址或可進一步提升i.MXRT1xxx內核執行性能

  大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是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主頁、微信公眾號 平台上。

微信搜索”痞子衡嵌入式“或者掃描下面二維碼,就可以在手機上第一時間看了哦。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

您可能也會喜歡…