目录
一、前言
最近在网上查找资料学习STM32如何将代码搬到RAM中去运行,于是查看了一些前人的博客,介绍了KEIL的分散加载文件(sct文件)和将特定的函数定义到RAM地址的方法,然后又顺便提了下在RAM中运行程序速度会有所提升,当然我一开始也是满满的赞同,学习嘛,总是要跟着前人的资料去做对应的实验,才能更有助于自己的学习和消化,于是就跟着实验一步一步来做,在实验中发现了一些问题,于是引出了本文的一些疑问和思考,有不对的地方还望批评指正。
二、ROM和RAM程序运行速度实验
总体思路是,设置一个在ROM或RAM中的while死循环程序,在这个循环中不断进行特定的计算,每隔100毫秒通过串口打印一次循环运行的次数。
1、STM32F103ZET6平台,内核频率72MHz。
(1)ROM中运行
初始化一个100毫秒中断一次的定时器
-
unsigned
int dwSpeedTestLastCnt =
0;
-
unsigned
int dwSpeedTestCnt =
0;
-
-
void time_init()
-
{
-
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
-
-
NVIC_InitTypeDef NVIC_InitStructure;
-
-
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
-
-
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
-
TIM_TimeBaseInitStructure.TIM_Period =
200;
-
TIM_TimeBaseInitStructure.TIM_Prescaler =
35999;
-
TIM_TimeBaseInitStructure.TIM_ClockDivision =
0;
-
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
-
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
-
TIM_Cmd(TIM3,ENABLE);
-
-
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
-
-
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
-
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;
-
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=
0;
-
NVIC_InitStructure.NVIC_IRQChannelSubPriority=
1;
-
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
-
NVIC_Init(&NVIC_InitStructure);
-
}
-
-
void SpeedTest(void)
-
{
-
while(
1)
-
{
-
dwSpeedTestCnt++;
-
}
-
}
-
-
void TIM3_IRQHandler(void)
-
{
-
if(
TIM_GetITStatus(TIM3, TIM_IT_Update))
-
{
-
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
-
-
printf(
"\r\nCycle = %d", dwSpeedTestCnt - dwSpeedTestLastCnt);
-
dwSpeedTestLastCnt = dwSpeedTestCnt;
-
-
GPIOC->ODR ^=
0x01;
//闪个LED
-
-
TIM3->CNT =
0;
-
}
-
}
-
-
int main(void)
-
{
-
USART1_Config();
-
LED_Config();
-
time_init();
-
SpeedTest();
-
-
return
0;
-
}
工程编译成功后从map文件中看到SpeedTest函数地址如下:
SpeedTest 0x08003805 Thumb Code 14 main.o(.text)
取打印的10个典型值如下:即每100ms循环了31万多次。
-
Cycle
=
313572
-
Cycle
=
313768
-
Cycle
=
313638
-
Cycle
=
313833
-
Cycle
=
313702
-
Cycle
=
313572
-
Cycle
=
313768
-
Cycle
=
313638
-
Cycle
=
313833
-
Cycle
=
313702
(2)RAM中运行
在RAM中运行程序需要修改sct文件(在工程的Obj目录里),在RAM地址区域添加一个名为“RAMCODE ”的Section段,如下:
刚刚的程序只需要改掉一个地方就可以了,就是把测试函数用一对标签包住,修改如下:
-
#pragma arm section code = "RAMCODE"
-
void SpeedTest(void)
-
{
-
while(
1)
-
{
-
dwSpeedTestCnt++;
-
}
-
}
-
#pragma arm section
工程编译成功后从map文件中看到SpeedTest函数地址如下:
SpeedTest 0x20000001 Thumb Code 14 main.o(RAMCODE)
取打印的10个典型值如下:即每100ms循环了55万多次。
-
Cycle
=
555242
-
Cycle
=
555011
-
Cycle
=
554780
-
Cycle
=
555125
-
Cycle
=
554897
-
Cycle
=
555242
-
Cycle
=
555011
-
Cycle
=
554780
-
Cycle
=
555125
-
Cycle
=
554897
(3)总结
对于这个简单的测试来说,在STM32F103ZET6这个平台下,内核频率72MHz,RAM中程序跑的比ROM要快,快了大概
77%((555011 - 313572) / 313572 * 100% = 77%)。
2、STM32F429BIT6平台,内核频率168MHz。
由于好奇心,又在这个平台进行了一下测试,还是一样的套路,100ms下看能循环多少次。
(1)ROM中运行
工程编译成功后从map文件中看到SpeedTest函数地址如下:
SpeedTest 0x080011c5 Thumb Code 14 main.o(i.SpeedTest)
取打印的10个典型值如下:即每100ms循环了167万多次。
-
Cycle
=
1675472
-
Cycle
=
1675468
-
Cycle
=
1675472
-
Cycle
=
1675468
-
Cycle
=
1675472
-
Cycle
=
1675468
-
Cycle
=
1675472
-
Cycle
=
1675468
-
Cycle
=
1675472
-
Cycle
=
1675468
(2)RAM中运行
工程编译成功后从map文件中看到SpeedTest函数地址如下:
SpeedTest 0x20000001 Thumb Code 14 main.o(RAMCODE)
取打印的10个典型值如下:即每100ms循环了128万多次。
-
Cycle
=
1288825
-
Cycle
=
1288824
-
Cycle
=
1288825
-
Cycle
=
1288825
-
Cycle
=
1288825
-
Cycle
=
1288824
-
Cycle
=
1288825
-
Cycle
=
1288824
-
Cycle
=
1288825
-
Cycle
=
1288824
(3)总结
对于这个简单的测试来说,在STM32F429BIT6这个平台下,内核频率168MHz,ROM中程序跑的比RAM要快,快了大概
30%((1675472 - 1288825) / 1288825 * 100% = 30%)。
那么问题来了,跑的测试程序都是一样的为什么得出的结论不一样?按理来说RAM的读写速度肯定比内嵌的Flash快的多啊,为什么在F429这里,程序在RAM里跑不过ROM了?
带着这些疑问,于是又在网上各种查找资料。
三、预取指令技术
STM32F103xx系列的CPU时钟频率可以达到72MHz,但是内部Flash的时钟频率跑不了太高只有24MHz,所以当CPU直接访问Flash存储器时必须插入等待周期以得到正确的结果。但是还有个要注意的问题,Flash可以每次读出64位的数据,而CPU每次取指令最多为32位的字,如果是每次取指令都从Flash直接读取的话,势必造成至少32位从Flash里面读出来的数据的读取浪费(读出来了却没用上),所以STM32F103xx专门有两个64位“预取缓冲区”来保存每次读出的指令,CPU到这个缓冲区里去拿指令,当“预取缓冲区”为空时再次从Flash读出指令。于是Flash的数据位宽得到了完全的使用,CPU在执行指令同时“预取缓冲区”也在读取Flash数据,减少了CPU的等待周期从而提高了CPU运行效率。下面从几个方面来了解。
1、STM32F1的内部Flash和系统框架
下面是从《STM32F10xxx中文参考手册》中摘抄来的关于内部Flash的介绍:
-
2.3.3 嵌入式闪存
-
高性能的闪存模块有以下的主要特性:
-
● 高达512K字节闪存存储器结构:闪存存储器有主存储块和信息块组成:
-
─ 主存储块容量:
-
小容量产品主存储块最大为4K× 64位,每个存储块划分为32个1K字节的页(见表2)。
-
中容量产品主存储块最大为16K× 64位,每个存储块划分为128个1K字节的页(见表3)。
-
大容量产品主存储块最大为64K× 64位,每个存储块划分为256个2K字节的页(见表4)。
-
互联型产品主存储块最大为32K× 64位,每个存储块划分为128个2K字节的页(见表5)。
-
─ 信息块容量:
-
互联型产品有2360× 64位(见表5)。
-
其它产品有258× 64位(见表2、 表3、 表4)。
-
闪存存储器接口的特性为:
-
● 带预取缓冲器的读接口(每字为2× 64位)
-
● 选择字节加载器
-
● 闪存编程/擦除操作
-
● 访问/写保护
-
-
……………………………………
-
闪存读取
-
闪存的指令和数据访问是通过AHB总线完成的。预取模块是用于通过ICode总线读取指令的。仲
-
裁是作用在闪存接口,并且DCode总线上的数据访问优先。
-
读访问可以有以下配置选项:
-
● 等待时间:可以随时更改的用于读取操作的等待状态的数量。
-
● 预取缓冲区(2个64位):在每一次复位以后被自动打开,由于每个缓冲区的大小(64位)与闪
-
存的带宽相同,因此只通过需一次读闪存的操作即可更新整个缓冲区的内容。由于预取缓
-
冲区的存在, CPU可以工作在更高的主频。 CPU每次取指最多为32位的字,取一条指令
-
时,下一条指令已经在缓冲区中等待。
-
● 半周期:用于功耗优化。
-
注: 1. 这些选项应与闪存存储器的访问时间一起使用。等待周期体现了系统时钟(SYSCLK)频率与闪
-
存访问时间的关系:
-
0等待周期,当 0 < SYSCLK < 24MHz
-
1等待周期,当 24MHz < SYSCLK ≤ 48MHz
-
2等待周期,当 48MHz < SYSCLK ≤ 72MHz
-
2 . 半周期配置不能与使用了预分频器的AHB一起使用,时钟系统应该等于HCLK时钟。该特性
-
只能用在时钟频率为8MHz或低于8MHz时,可以直接使用的内部RC振荡器(HSI),或者是主振
-
荡器(HSE),但不能用PLL。
-
3. 当AHB预分频系数不为1时,必须置预取缓冲区处于开启状态。
-
4. 只有在系统时钟(SYSCLK)小于24MHz并且没有打开AHB的预分频器(即HCLK必须等于
-
SYSHCLK)时,才能执行预取缓冲器的打开和关闭操作。一般而言,在初始化过程中执行预取
-
缓冲器的打开和关闭操作,这时微控制器的时钟由8MHz的内部RC振荡器(HSI)提供。
-
5. 使用DMA: DMA在DCode总线上访问闪存存储器,它的优先级比ICode上的取指高。 DMA在
-
每次传送完成后具有一个空余的周期。有些指令可以和DMA传输一起执行。
下面是F1系列(非互联网产品)的系统结构图:
ICode总线:该总线将Cortex™-M3内核的指令总线与闪存指令接口相连接。指令预取在此总线上完成。
DCode总线:该总线将Cortex™-M3内核的DCode总线与闪存存储器的数据接口相连接(常量加载和调试访问)。
系统总线:此总线连接Cortex™-M3内核的系统总线(外设总线)到总线矩阵,总线矩阵协调着内核和DMA间的访问。
DMA总线:此总线将DMA的AHB主控接口与总线矩阵相联,总线矩阵协调着CPU的DCode和DMA到SRAM、闪存和外设的访问。
在STM32库函数“system_stm32f10x.c”文件中有个叫SetSysClockTo72的函数,这个函数将系统时钟配置到72MHz,如下:
-
/**
-
* @brief Sets System clock frequency to 72MHz and configure HCLK, PCLK2
-
* and PCLK1 prescalers.
-
* @note This function should be used only after reset.
-
* @param None
-
* @retval None
-
*/
-
static void SetSysClockTo72(void)
-
{
-
__IO
uint32_t StartUpCounter =
0, HSEStatus =
0;
-
-
/* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/
-
/* Enable HSE */
-
RCC->CR |= ((
uint32_t)RCC_CR_HSEON);
-
-
/* Wait till HSE is ready and if Time out is reached exit */
-
do
-
{
-
HSEStatus = RCC->CR & RCC_CR_HSERDY;
-
StartUpCounter++;
-
}
while((HSEStatus ==
0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
-
-
if ((RCC->CR & RCC_CR_HSERDY) != RESET)
-
{
-
HSEStatus = (
uint32_t)
0x01;
-
}
-
else
-
{
-
HSEStatus = (
uint32_t)
0x00;
-
}
-
-
if (HSEStatus == (
uint32_t)
0x01)
-
{
-
/* Enable Prefetch Buffer */
-
FLASH->ACR |= FLASH_ACR_PRFTBE;
-
-
/* Flash 2 wait state */
-
FLASH->ACR &= (
uint32_t)((
uint32_t)~FLASH_ACR_LATENCY);
-
FLASH->ACR |= (
uint32_t)FLASH_ACR_LATENCY_2;
-
-
-
/* HCLK = SYSCLK */
-
RCC->CFGR |= (
uint32_t)RCC_CFGR_HPRE_DIV1;
-
-
/* PCLK2 = HCLK */
-
RCC->CFGR |= (
uint32_t)RCC_CFGR_PPRE2_DIV1;
-
-
/* PCLK1 = HCLK */
-
RCC->CFGR |= (
uint32_t)RCC_CFGR_PPRE1_DIV2;
-
-
#ifdef STM32F10X_CL
-
/* Configure PLLs ------------------------------------------------------*/
-
/* PLL2 configuration: PLL2CLK = (HSE / 5) * 8 = 40 MHz */
-
/* PREDIV1 configuration: PREDIV1CLK = PLL2 / 5 = 8 MHz */
-
-
RCC->CFGR2 &= (
uint32_t)~(RCC_CFGR2_PREDIV2 | RCC_CFGR2_PLL2MUL |
-
RCC_CFGR2_PREDIV1 | RCC_CFGR2_PREDIV1SRC);
-
RCC->CFGR2 |= (
uint32_t)(RCC_CFGR2_PREDIV2_DIV5 | RCC_CFGR2_PLL2MUL8 |
-
RCC_CFGR2_PREDIV1SRC_PLL2 | RCC_CFGR2_PREDIV1_DIV5);
-
-
/* Enable PLL2 */
-
RCC->CR |= RCC_CR_PLL2ON;
-
/* Wait till PLL2 is ready */
-
while((RCC->CR & RCC_CR_PLL2RDY) ==
0)
-
{
-
}
-
-
-
/* PLL configuration: PLLCLK = PREDIV1 * 9 = 72 MHz */
-
RCC->CFGR &= (
uint32_t)~(RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL);
-
RCC->CFGR |= (
uint32_t)(RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLSRC_PREDIV1 |
-
RCC_CFGR_PLLMULL9);
-
#else
-
/* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
-
RCC->CFGR &= (
uint32_t)((
uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
-
RCC_CFGR_PLLMULL));
-
RCC->CFGR |= (
uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
-
#endif /* STM32F10X_CL */
-
-
/* Enable PLL */
-
RCC->CR |= RCC_CR_PLLON;
-
-
/* Wait till PLL is ready */
-
while((RCC->CR & RCC_CR_PLLRDY) ==
0)
-
{
-
}
-
-
/* Select PLL as system clock source */
-
RCC->CFGR &= (
uint32_t)((
uint32_t)~(RCC_CFGR_SW));
-
RCC->CFGR |= (
uint32_t)RCC_CFGR_SW_PLL;
-
-
/* Wait till PLL is used as system clock source */
-
while ((RCC->CFGR & (
uint32_t)RCC_CFGR_SWS) != (
uint32_t)
0x08)
-
{
-
}
-
}
-
else
-
{
/* If HSE fails to start-up, the application will have wrong clock
-
configuration. User can add here some code to deal with this error */
-
}
-
}
其中这两句使能“预取缓冲区”,然后设置等待周期为2个等待周期,如下:
-
/* Enable Prefetch Buffer */
-
FLASH->ACR |= FLASH_ACR_PRFTBE;
-
-
/* Flash 2 wait state */
-
FLASH->ACR &= (
uint32_t)((
uint32_t)~FLASH_ACR_LATENCY);
-
FLASH->ACR |= (
uint32_t)FLASH_ACR_LATENCY_2;
为什么设置2个等待周期呢?因为72MHz/24MHz = 3。等2个周期后,下一个周期可以读走数据。
2、“预取缓冲区”工作方式
这个没看到手册上面有很详细的说具体怎么工作的,倒是在网上搜到了一篇文章感觉讲的挺好的,如下引用一段(因为是个文档,不知道出处是哪里,就贴不上原文链接了):
首先,STM32的内部Flash是组织成64位宽度,即每次可以读出64位;在Flash与CPU的取指队列之间有两个缓冲器,用于暂存Flash中取出的指令,见下图。
其次,STM32的指令有16位的也有32位的,指令是从图中绿色的缓冲器取出;当绿色缓冲器变空时,黄色缓冲器中的内容会被复制到绿色缓冲器中;这样取指与读取Flash互不干扰。
正因为STM32的指令有不同长度,所以程序执行的等待周期与程序的内容有关。图一是假定所有指令都是16位的指令:
(1)时刻t0时黄色缓冲器和绿色缓冲器都为空,此时CPU等待3个周期后,到时刻t1时才能读到指令;
(2)时刻t1时绿色缓冲器被填满,黄色缓冲器仍为空,Flash控制器继续读取后续指令;
(3)时刻t2时绿色缓冲器还有两个字节,黄色缓冲器被填满;此时因为两个缓冲器都有数据,读取Flash的操作暂停(图一中的绿色虚线框所示);
(4)当绿色缓冲器变空时,黄色缓冲器被复制到绿色缓冲器,同时恢复读取Flash的操作;
(5)时刻t3时缓冲器的状态又变为上述第(3)步的状态。
从以上分析可以看出,CPU的指令执行是没有等待周期的。但当执行跳转指令时,Flash缓冲器中的内容作废,系统回到了上述第(1)步的状态。
图二是假定每三条指令中有两条16位的指令和一条32位的指令。这种情况下,如图所示,CPU的指令执行也是没有等待周期的。
图三是假定所有指令都是32位的指令,从图中可看出,CPU每执行两条指令,要插入一个等待周期。
上面的分析只是针对每个CPU周期都有取指操作的情况,而实际的操作中情况并没有这么简单,因为Cortex-M3的指令不都是单周期指令。实际的程序执行情况是受很多因素影响的,单纯静态的分析也是不现实的。
从以上可以了解到虽然Flash速度跟不上CPU,但是在理想情况下,CPU读取指令达到了“0”等待的效果。
3、STM32F4的内部Flash和系统框架
说完了F1的咱再来说说F4的,F4是M4的内核,CPU时钟可以达到180MHz,既然比M3快了那么多,那么肯定在内部Flash和指令读取机制上会有一定的提升,下面摘抄《STM32F429中文参考手册》里介绍Flash的一段:
-
3.3 嵌入式 Flash
-
Flash 具有以下主要特性:
-
● 对于 STM32F40x 和 STM32F41x,容量高达 1 MB;对于 STM32F42x 和 STM32F43x,
-
容量高达 2 MB
-
● 128 位宽数据读取
-
● 字节、半字、字和双字数据写入
-
● 扇区擦除与全部擦除
-
● 存储器组织结构
-
Flash 结构如下:
-
— 主存储器块,分为 4 个 16 KB 扇区、 1 个 64 KB 扇区和 7 个 128 KB 扇区
-
— 系统存储器,器件在系统存储器自举模式下从该存储器启动
-
— 512 字节 OTP(一次性可编程),用于存储用户数据
-
OTP 区域还有 16 个额外字节,用于锁定对应的 OTP 数据块。
-
— 选项字节,用于配置读写保护、 BOR 级别、软件/硬件看门狗以及器件处于待机或
-
停止模式下的复位。
-
● 低功耗模式(有关详细信息,请参见参考手册的“电源控制 (PWR)”部分)
-
-
3.4 读接口
-
3.4.1 CPU 时钟频率与 Flash 读取时间之间的关系
-
为了准确读取 Flash 数据,必须根据 CPU 时钟 (HCLK) 频率和器件电源电压在 Flash 存取控
-
制寄存器 (FLASH_ACR) 中正确地编程等待周期数 (LATENCY)。
-
当电源电压低于 2.1 V 时,必须关闭预取缓冲器。 表 7 所示为 Flash 等待周期与 CPU 时钟
-
频率之间的对应关系。
-
-
注意: STM32F405xx/07xx 和 STM32F415xx/17xx 器件:
-
- 当 VOS =“0”时, fHCLK 最大值 = 144 MHz。
-
- 当 VOS =“1”时, fHCLK 最大值 = 168 MHz。
-
STM32F42xxx 和 STM32F43xxx 器件:
-
- 当 VOS[1:0] =“0x01”时, fHCLK 最大值为 120 MHz。
-
- 当 VOS[1:0] =“0x10”时, fHCLK 最大值为 144 MHz。
-
- 当 VOS[1:0] =“0x11”时, fHCLK 最大值为 168 MHz。
从以上信息知道F4的Flash数据位宽增加到了128位,然后电压的不同也会相应的影响等待周期。
接下来还专门介绍了“ART加速器”,如下:
-
3.4
.2 自适应实时存储器加速器 (ART Accelerator™)
-
专有的自适应实时 (ART) 存储器加速器面向 STM32 工业标准 ARM® Cortex™-M4F 处理器
-
进行了优化。该加速器很好地体现了 ARM Cortex M4F 的固有性能优势,克服了通常条件
-
下,高速的处理器在运行中需要经常等待 FLASH 读取的情况。
-
为了发挥处理器的全部性能,该加速器将实施指令预取队列和分支缓存,从而提高了
128 位
-
Flash 的程序执行速度。根据 CoreMark 基准测试,凭借 ART 加速器所获得的性能相当于
-
Flash 在 CPU 频率高达
168 MHz 时以
0 个等待周期执行程序。
-
指令预取
-
每个 Flash 读操作可读取
128 位,可以是
4 行
32 位指令,也可以是
8 行
16 位指令,具体
-
取决于烧写在 Flash 中的程序。因此对于顺序执行的代码,至少需要
4 个 CPU 周期来执行
-
前一次读取的
128 位指令行。在 CPU 请求当前指令行时,可使用
I-
Code 总线的预取操作读
-
取 Flash 中的下一个连续存放的
128 位指令行。可将 FLASH_ACR 寄存器中的 PRFTEN 位
-
置
1,来使能预取功能。当访问 Flash 至少需要一个等待周期时,此功能非常有用。
-
图
4 所示为需要
3 WS(
3 个等待周期)访问 Flash 时连续
32 位指令的执行过程,图中分别
-
介绍了使用和不使用预取操作两种情况。
![](https://i-blog.csdnimg.cn/blog_migrate/235b8c5c709f3d285da28f518c2d4bab.png)
![](https://i-blog.csdnimg.cn/blog_migrate/35f46dfa31dfc0b8f2ed0232d00e1a6c.png)
处理非顺序执行的代码(有分支)时,指令可能并不存在于当前使用的或预取的指令行中。这种情况下, CPU 等待时间至少等于等待周期数。
-
指令缓存存储器
-
为了减少因指令跳转而产生的时间损耗,可将
64 行
128 位的指令保存到指令缓存存储器
-
中。可将 FLASH_ACR 寄存器中的指令缓存使能 (ICEN) 位置
1,来使能这一特性。每当出
-
现指令缺失(即请求的指令未存在于当前使用的指令行、预取指令行或指令缓存存储器中)
-
时,系统会将新读取的行复制到指令缓存存储器中。如果 CPU 请求的指令已存在于指令缓
-
存区中,则无需任何延时即可立即获取。指令缓存存储器存满后,可采用 LRU(最近最少使
-
用)策略确定指令缓存存储器中待替换的指令行。此特性非常适用于包含循环的代码。
-
数据管理
-
在 CPU 流水线执行阶段,将通过 D-
Code 总线访问 Flash 中的数据缓冲池。因此,直到提
-
供了请求的数据后, CPU 流水线才会继续执行。为了减少因此而产生的时间损耗,通过
-
AHB 数据总线 D-
Code 进行的访问优先于通过 AHB 指令总线
I-
Code 进行的访问。
-
如果频繁使用某些数据,可将 FLASH_ACR 寄存器中的数据缓存使能 (DCEN) 位置
1,来使
-
能数据缓存存储器。此特性的工作原理与指令缓存存储器类似,但保留的数据大小限制在
8
-
行
128 位/行以内。
-
注意: 用户配置扇区中的数据无法缓存。
“ART 加速器”这个东西在F1的手册里面没看到说过,F4里介绍说实现了“指令预取队列”和“分支缓存”,这一部分介绍看起来比F1的更牛逼,在F4的系统框图里也专门画了这么一个东西。
“分支缓存”是什么意思?我们先来想个问题,在程序里有个循环体,那么到达循环体尾部的时候有条跳转指令指示程序地址跳回到之前的某个地方,因为预取缓存里还有很多个字节是存了这条跳转语句之后的一些东西,这些都用不着了,要重新到跳转的地址去读指令了,导致了CPU需要等待这一部分的读取时间,循环次数越多,等待的时间越多。于是引入了“分支缓存”,它可以记录下最近的跳转情况,当再次遇到相同的跳转语句情景时,机智地去将要跳转的地址去预取指令,这样在指令跳转时CPU就不需要等待了。这里讲的比较粗略,想更详细了解的可以参考文末贴出的一篇文章《CPU分支指令预测技术》。
四、实验分析
1、分析
经过以上资料的学习,其实可以大致明白是什么原因了。在没有对程序优化的情况下,因为SpeedTest这个测试函数里的while循环只有一条变量自加语句,导致运行时程序疯狂跳转,而F1只有预取缓存,F4既有预取缓存又有分支缓存,所以F1在处理循环跳转时会有CPU的等待周期,而F4在多次循环时可以说基本没有。SpeedTest里循环的汇编代码如下,这是从调试窗口上拷贝下来的,程序一直在这6条指令里循环跑。
-
0
x08003806
486
A
LDR
r0,
[pc,#424] ; @
0
x080039B0
-
0
x08003808
6800
LDR
r0,
[r0,#0x00]
-
0
x0800380A
1
C40
ADDS
r0,
r0,
#1
-
0
x0800380C
4968
LDR
r1,
[pc,#416] ; @
0
x080039B0
-
0
x0800380E
6008
STR
r0,
[r1,#0x00]
-
0
x08003810
E7F9
B
0
x08003806
对于F1来说,在这种情况下RAM跑程序的速度优势强于ROM跑程序的跳转等待周期缺陷,于是看到的现象是RAM跑程序更快。对于F4来说,已经处理了ROM跑程序的跳转等待周期缺陷,所以RAM已经不具优势了,于是看到的现象是ROM跑程序更快。
2、再次实验验证
为了验证上面说的这些,下面再对F1做个实验,把循环体里的语句复制成很多的单条语句,让循环体成为一大段连续的程序区,这样的话对F1做出来的实验现象会是ROM比RAM更快吗?
更改测试函数如下:
-
void SpeedTest(void)
-
{
-
while(
1)
-
{
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
dwSpeedTestCnt++;
-
}
-
}
(1)ROM中运行
取打印的10个典型值如下:即每100ms循环了113万多次。
-
Cycle
=
1138258
-
Cycle
=
1138491
-
Cycle
=
1138730
-
Cycle
=
1138966
-
Cycle
=
1139205
-
Cycle
=
1138255
-
Cycle
=
1138491
-
Cycle
=
1138733
-
Cycle
=
1138966
-
Cycle
=
1139206
(2)RAM中运行
取打印的10个典型值如下:即每100ms循环了101万多次。
-
Cycle
=
1012125
-
Cycle
=
1012333
-
Cycle
=
1012542
-
Cycle
=
1012755
-
Cycle
=
1012964
-
Cycle
=
1012125
-
Cycle
=
1012333
-
Cycle
=
1012541
-
Cycle
=
1012756
-
Cycle
=
1012963
(3)结论
从实验数据可以看出,经过改动测试函数,循环的跳转频繁程度明显降低,ROM上跑程序的速度实现了反超。
五、结论
由于STM32与其他单片机一样,采用哈佛总线结构,即取指总线和数据总线是分开的,所以取指令和取数据是可以同时进行的,用ROM跑程序的话正好可以利用这一优良特性。但是用RAM跑程序的话由于取指令和取数据都需要占用数据总线,虽然RAM是极快的,对单片机CPU来说数据读写永远是0等待周期,就算这样也需要多花时钟周期去读数据,因为读指令和读数据不可以同时进行了,这是一种性能的损失。刚好“预取缓存”和“分支缓存”技术又将RAM的速度优势给平衡掉了。所以我个人认为,在F4平台下,通常情况下将程序代码放在RAM中跑并不会具有速度优势。对于F1平台而言,还要看具体的程序结构才能确定。
六、参考资料
文章:
《STM32 进阶教程 11 - RAM中运行程序》
https://blog.csdn.net/zhanglifu3601881/article/details/95040782
《CPU分支指令预测技术》
http://blog.sina.com.cn/s/blog_58942aff01009w0z.html
资料:
《STM32F10xxx中文参考手册_V10》
《Cortex-M3权威指南》
《STM32F407, 429参考手册(中文)》
《STM32F429_数据手册(中文)》
链接:https://pan.baidu.com/s/12ZU83ET5z7JPxkU9BAYurQ 提取码:cbom