STM32F767系统内核与时钟设置

 本文介绍如何对STM32F767的系统内核与时钟进行设置。

开发环境

硬件环境

  • 电脑:Windows 10 Home x64
  • Apollo STM32F767开发板(ST-LINK V2仿真器)

软件环境

  • Keil Version 5.24.1 (Pack Installer:Keil.STM32F7xx_DFP.2.9.0.pack)
  • STM32CubeMX Version 4.25.0(Packages Manager:STM32CubeF7)

系统设置

 STM32F767基于强大的Cortex-M7内核,其许多系统特性需要进行设置以发挥最高性能。

设置Flash Interface

 STM32F767的flash支持TCM与AXI接口,而Chrom-Art Accelerator与Instruction Prefetch仅支持在TCM模式下使用。因此,需要首先将flash配置为TCM接口。TCM接口的选择通过BOOT引脚以及Flash选项控制寄存器FLASH_OPTCR1进行设置。

 当BOOT pin=0时,FLASH_OPTCR1寄存器关于BOOT的选项如下所示。

 默认状态下,BOOT_ADD0的值为0x80,即默认以TCM接口启动。

 可以通过读取Flash默认配置以确认Flash接口,程序如下所示。

1
2
3
FLASH_OBProgramInitTypeDef Flash_InitStruct;
HAL_FLASHEx_OBGetConfig(&Flash_InitStruct);

 可以看到系统默认状态下BOOT_ADD0的值为0x80。

使能Chrom-Art Accelerator与Instruction Prefetch

 Chrom-Art Accelerator™ (DMA2D) 是专用于图像处理的专业 DMA,将其开启有利于提升图像相关处理速度。Instruction Prefetch即为指令预读取,ST对其的解释是:

每个 flash 读操作可读取 256 位,可以是 8 行 32 位指令,也可以是 16 行 16 位指令,具体取决于烧写在 Flash 中的程序。因此对于顺序执行的代码,至少需要 8 个 CPU 周期来执行前一指令行的读取操作。在 CPU 对当前指令行进行操作请求时,可使用 ITCM 总线的预取操作读取 Flash 中的下一个连续存放的指令行。可通过将 FLASH_ACR 寄存器中的 PRFTEN位置 1 来使能预取功能。当访问 Flash 至少需要一个等待周期时,此功能非常有用。处理非顺序执行的代码(有分支)时,指令可能并不存在于当前使用的指令行以及预取的指令行中。在这种情况下,CPU 延迟周期数至少等于等待周期数。

 Art Accelerator与Instruction Prefetch的启动在HAL_Init()函数中完成。HAL_Init()函数如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HAL_StatusTypeDef HAL_Init(void)
{
/* Configure Instruction cache through ART accelerator */
#if (ART_ACCLERATOR_ENABLE != 0)
__HAL_FLASH_ART_ENABLE();
#endif /* ART_ACCLERATOR_ENABLE */
/* Configure Flash prefetch */
#if (PREFETCH_ENABLE != 0U)
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif /* PREFETCH_ENABLE */
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
HAL_InitTick(TICK_INT_PRIORITY);
/* Init the low level hardware */
HAL_MspInit();
/* Return function status */
return HAL_OK;
}

 注意到ART_ACCLERATOR_ENABLE与PREFETCH_ENABLE为控制关键字,其定义在stm32f7xx_hal_conf.h文件中,将其设置为1即表示ART Accelerator与Prefetch启用。

1
2
3
4
5
6
7
8
9
/* ########################### System Configuration ######################### */
/**
* @brief This is the HAL system configuration section
*/
#define VDD_VALUE ((uint32_t)3300U) /*!< Value of VDD in mv */
#define TICK_INT_PRIORITY ((uint32_t)0x0FU) /*!< tick interrupt priority */
#define USE_RTOS 0U
#define PREFETCH_ENABLE 1U
#define ART_ACCLERATOR_ENABLE 1U /* To enable instruction cache and prefetch */

使能I Cache与D Cache

 I Cache为指令Cache,D Cache为数据Cache。Cache位于Core与SRAM或者外部RAM之间,Cache的速率较RAM更高,只要保证Cache有足够高的命中率,则可以进一步提高Core与SRAM或者外部RAM之间的数据交互速率,进而提高性能。

 在system.c文件中建立如下函数。

1
2
3
4
5
6
7
8
9
10
11
//enable CPU L1-Cache
void Cache_Enable(void)
{
SCB_InvalidateICache();//enable I Cache.
SCB_EnableICache();
SCB_InvalidateDCache();//enable D Cache.
SCB_EnableDCache();
SCB->CACR|=1<<2; //Enables Force Write-Through. All Cacheable memory regions are treated as Write-Through.
}

 关于Cache的函数说明在Keil官网中CMSIS-Core (Cortex-M)部分,同时安装STM32CubeMX 后CMSIS文件夹下也有相关文档,与官网一致。

 注意到在I/D Cache使能前均使用Invalidate相关函数无效化了Cache中的数据,官网中明确提出系统复位后,必须在Enable函数前首先应用Invalidate函数。可以理解在系统复位后,Cache中的指令\数据是无效的,为了避免dirty指令\数据影响系统运行,则首先将其无效化。

 再有一个重要操作是对SCB->CACR寄存器FORCEWT进行使能,进行强制Write-Through。这里主要是为了解决Cache的一致性问题。关于Cache的一致性问题可以参考文档AN4839: Level 1 cache on STM32F7 Series and STM32H7 Series。根据STM32F7 MPU Cache浅析一文,一致性问题的主要发生在两种情形:

第一种情况是当有写物理内存的指令时,core会先去更新相应的cache-line(Write-back策略),在没有clean的情况下,会导致其对应的实际物理内存中的数据并没有被更新,如果这个时候有其它的Host(如DMA)访问这段内存时,就会出现问题(由于实际物理内存并未被更新,和D-cache中的不一致),这就是所谓的cache一致性的问题!

第二种情况是DMA更新了某段物理内存(DMA和cache直接没有直接通道),而这个时候Core再读取这段内存的时候,由于相对应地址的cache-line没有被invalidate,导致Core读到的是cache-line中的数据,而非被DMA更新过的实际物理内存的数据!

 针对Cache一致性问题,以上提到的两篇文章也给出了相应的解决方法。

  • MPU配置的代码,将属性改为MPU_ACCESS_BUFFERABLE,即使用write-though策略而非默认的write-back策略;
  • 通过Cache控制寄存器SCB->CACR,将所有cacheable的空间全部强制write-though;
  • 在对cacheable内存区域写数据时,进行cache maintenance操作,即CleanCache操作,将dirty data更新到真实的物理地址中;
  • 修改MPU SRAM1区域到共享区域,可以避免默认状态下SRAM1可被D Cache缓存。

 当前程序中使用第2种方法,实际上这对性能是有损失的,后续将尝试使用更高效方法。直觉上方法1与方法2都一定程序抑制了Cache的性能,方法4则直接将SRAM1设置为不缓存,方法3应该是对性能影响最低但是操作需要异常小心的方法。

时钟设置

时钟树

 STM32F767的时钟系统比较复杂,时钟树总体如图所示。

时钟源

 STM32F767的时钟源有5个,分别列举说明如下。

  • LSI:Low Speed Internal,内部低速时钟,为RC振荡器构成,频率约为32KHz;
  • LSE:Low Speed External,外部低速时钟,由外部晶体提供,当前硬件此时钟为32.768kHz;
  • HSI:High Speed Internal,内部高速时钟,为RC振荡器构成,频率约为16MHz;
  • HSE:High Speed External,外部高速时钟,由外部晶体提供,当前硬件此时钟为25MHz;
  • PLLCLK:锁相环输出时钟。

SYSCLK设置

 SYSCLK时钟的设置可以基于STM32CubeMX 生成代码,STM32CubeMX 的设置原理如下图所示。

 使用STM32CubeMX 设置时钟非常方便,图中基本的设置为:启用HSE时钟,时钟为25MHz,将其作为PLL时钟源。PLLM=25,PLLN=432,PLLP=2,最后得到的PLLCLK为:

$$PLLCLK=HSE\cdot \frac{1}{25}\cdot 432\cdot \frac{1}{2}=216MHz$$

 SYSCLK的时钟源选择PLLCLK,即为216MHz,这是系统允许的最高时钟。

 当SYSCLK设置完毕后,分别设置SYSTICK,APB1与APB2分频系数,可以得到如图所示的各自时钟频率。

RTC时钟设置

 RTC时钟需要精准的时钟源,而外部时钟的精度通常高于内部时钟,因此RTC时钟源需要选择外部晶体。RTC的使用需要额外设置,后续章节再进行详细介绍。

代码实现

 时钟初始化的代码实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct;
RCC_ClkInitTypeDef RCC_ClkInitStruct;
/**Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/**Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 432;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
/**Activate the Over-Drive mode
*/
if (HAL_PWREx_EnableOverDrive() != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
/**Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_7) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
/**Configure the Systick interrupt time
*/
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
/**Configure the Systick
*/
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}

 对代码的几个重点进行补充说明。

__HAL_RCC_PWR_CLK_ENABLE()

 该函数用于使能APB1电源接口时钟。

__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1)

 该函数用于设置内部调压器输出电压输出电压,即调节PWR_CR1寄存器VOS位,设置为SCALE1即为同时打开HAL_PWREx_EnableOverDrive()才能将HCLK设置为216MHz。具体说明参见SMT32F767用户手册3.3.3节。

时钟初始化

 对时钟的初始化为常规调用,参数与本文说明吻合,不再继续说明。

HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_7)

 该函数为flash延迟设置,当系统供电电压与HCLK明确后,按照下表设置flash延迟数。

SYSTICK初始化

 SYSTICK初始化将其周期设置为1ms,时钟源为HCLK,同时设置为最高优先级。

文章目录
  1. 1. 开发环境
    1. 1.1. 硬件环境
    2. 1.2. 软件环境
  2. 2. 系统设置
    1. 2.1. 设置Flash Interface
    2. 2.2. 使能Chrom-Art Accelerator与Instruction Prefetch
    3. 2.3. 使能I Cache与D Cache
  3. 3. 时钟设置
    1. 3.1. 时钟树
    2. 3.2. 时钟源
    3. 3.3. SYSCLK设置
    4. 3.4. RTC时钟设置
    5. 3.5. 代码实现
      1. 3.5.1. __HAL_RCC_PWR_CLK_ENABLE()
      2. 3.5.2. __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1)
      3. 3.5.3. 时钟初始化
      4. 3.5.4. HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_7)
      5. 3.5.5. SYSTICK初始化
|