你們好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給你們分享的是i.MXRT上進一步提高代碼執行性能的經驗。html
今天跟你們聊的這個話題仍是跟痞子衡最近這段時間參與的一個基於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)。oop
項目板上的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,這即是奇怪問題所在。flex
代碼位置 | 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拿指令究竟是單發射仍是雙發射有必定的不肯定性,所以沒法作到精確,若是以全雙發射來計算,就能得出最小延時時間。ui
#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()函數連接地址,這個函數在兩個測試工程下連接地址對齊不同,這意味着測試條件不徹底相同,或許這是一個解決問題的線索。.net
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主頁、微信公衆號 平臺上。
微信搜索"痞子衡嵌入式"或者掃描下面二維碼,就能夠在手機上第一時間看了哦。