大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是 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-EVKboardsevkmimxrt1170jpeg_examplessd_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 上进一步提升代码执行性能的经验痞子衡便介绍完毕了,掌声在哪里~~~