STM32 使用 OpenOCD+CLion 调试时系统时钟频率不正确的问题解决

使用 CLion 开发 STM32 程序日渐成为一种新时尚。在 CLion 中,我们一般使用 OpenOCD 作为 gdb server 来进行调试。

调试过程中,我发现了一个比较严重的问题:如果点击“调试”按钮来启动程序,并且 CLion 运行配置中选择的启动模式是 reset init,单片机的系统时钟频率就会不正确(始终为 64MHz),并且抖动很大;然而,如果点击“运行”按钮来启动程序,又一切正常。这是为什么呢?

故障分析

我们从 OpenOCD 的启动流程(/usr/share/openocd/scripts/target/stm32f4x.cfg)入手分析。首先,ARM Cortex 内核限制 SWD 时钟频率不能超过内核频率的 1/6,而单片机上电后如果不做任何配置,时钟来源为 HSI 的 16MHz,这样 SWD 频率就最高是 2.666MHz。因此,如果不做时钟上的更改,OpenOCD 默认只能使用 2000kHz 的 adapter speed:

# stm32f4x.cfg line 61-67
# JTAG speed should be <= F_CPU/6. F_CPU after reset is 16MHz, so use F_JTAG = 2MHz
#
# Since we may be running of an RC oscilator, we crank down the speed a
# bit more to be on the safe side. Perhaps superstition, but if are
# running off a crystal, we can run closer to the limit. Note
# that there can be a pretty wide band where things are more or less stable.
adapter speed 2000

而如果让 OpenOCD 启动调试,为了加快 SWD 时钟,OpenOCD 会把 MCU 里面的 PLL 打开并设置到 64MHz,然后将系统时钟来源切换为 PLL:

# stm32f4x.cfg line 99-110
$_TARGETNAME configure -event reset-init {
	# Configure PLL to boost clock to HSI x 4 (64 MHz)
	mww 0x40023804 0x08012008   ;# RCC_PLLCFGR 16 Mhz /8 (M) * 128 (N) /4(P)
	mww 0x40023C00 0x00000102   ;# FLASH_ACR = PRFTBE | 2(Latency)
	mmw 0x40023800 0x01000000 0 ;# RCC_CR |= PLLON
	sleep 10                    ;# Wait for PLL to lock
	mmw 0x40023808 0x00001000 0 ;# RCC_CFGR |= RCC_CFGR_PPRE1_DIV2
	mmw 0x40023808 0x00000002 0 ;# RCC_CFGR |= RCC_CFGR_SW_PLL

	# Boost JTAG frequency
	adapter speed 8000
}

经过上面的配置,我们就可以将 adapter speed 设为最高 8000kHz,从而使调试更加丝滑顺畅。

那么,为什么会出现时钟不正常呢?我们分析 HAL 库设置系统时钟的流程。按照正常流程,HAL 库会启动 HSE,然后配置 PLL 参数,并将 PLL 时钟源设为 HSE,最后将系统时钟来源设置为 PLL。如果单片机上电后不做任何额外的时钟配置,这部分程序是可以正常工作的,单片机能够在正确的时钟上运行。

然而,巧合的是,启用调试器后,在运行用户代码前,系统时钟就处于已经配置 PLL 的状态。HAL 库如果检测到这种状态,则不会对 PLL 配置进行更改,只会检测现有的 PLL 配置与欲应用的配置是否相符,如果不相符,则返回 HAL_ERROR

__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef  *RCC_OscInitStruct) {
  ... // 初始化 HSI,HSE 等,在此省略

  if ((RCC_OscInitStruct->PLL.PLLState) != RCC_PLL_NONE) {
    // 如果 PLL 当前不是系统时钟
    if (__HAL_RCC_GET_SYSCLK_SOURCE() != RCC_CFGR_SWS_PLL) {
      // 正常上电时,__HAL_RCC_GET_SYSCLK_SOURCE 应该返回 RCC_CFGR_SWS_HSI,从而进入这个分支
      ... // 对 PLL 进行配置
    } else {
      // 如果启动调试器,__HAL_RCC_GET_SYSCLK_SOURCE 应该返回 RCC_CFGR_SWS_PLL,就会进入这个分支
      // 对 PLL 配置进行检查,如果与给定的配置不符,就返回 ERROR
      pll_config = RCC->PLLCFGR;
      if((READ_BIT(pll_config, RCC_PLLCFGR_PLLSRC) != RCC_OscInitStruct->PLL.PLLSource) ||
         (READ_BIT(pll_config, RCC_PLLCFGR_PLLM) != RCC_OscInitStruct->PLL.PLLM) ||
         (READ_BIT(pll_config, RCC_PLLCFGR_PLLN) != RCC_OscInitStruct->PLL.PLLN) ||
         (READ_BIT(pll_config, RCC_PLLCFGR_PLLP) != RCC_OscInitStruct->PLL.PLLP) ||
         (READ_BIT(pll_config, RCC_PLLCFGR_PLLQ) != RCC_OscInitStruct->PLL.PLLQ))
      {
          return HAL_ERROR;
      }
    }
  }
}

如果你的程序对 HAL_RCC_OscConfig 函数的返回值没有检查,或者检查后 Error_Handler 没有进行任何处理(STM32Cube 生成的代码中 Error_Handler 默认为空),则容易漏掉这个错误,导致时钟配置不正常。如果你的 Error_Handler 为一个死循环,则会在这里卡死。

解决方法

这个问题的解决方法很简单,只需在单片机进入 main 函数时,强行把系统时钟来源设为 HSI,就会让 HAL 库进入正确的分支:

int main(void)
{
  __HAL_RCC_HSI_ENABLE();
  __HAL_RCC_SYSCLK_CONFIG(RCC_SYSCLKSOURCE_HSI);

  HAL_Init();
  SystemClock_Config();
  // 进行其他初始化
}

LL 库

无独有偶,使用 LL 库也会遇到类似的问题,不过问题的原因不太相同。LL 库没有 HAL 库中的各种检查,配置时钟的过程只是对寄存器的简单操作:

void SystemClock_Config(void) {
    LL_RCC_HSE_Enable();
    while(LL_RCC_HSE_IsReady() != 1);    // Wait till HSE is ready

    LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_8, 168, LL_RCC_PLLP_DIV_4);
    LL_RCC_PLL_Enable();
    while(LL_RCC_PLL_IsReady() != 1);    // Wait till PLL is ready

    LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
}

STM32 的 PLL 有一个特性,在 PLL 处于启动状态时,对 PLL 的所有配置会失效。因此,如果以调试器方式启动,上面代码的 LL_RCC_PLL_ConfigDomain_SYS 语句其实没有任何效果,时钟仍然会从 HSI 给出,并且对 PLL 倍数的调节也不会生效。

因此,在配置 PLL 之前,我们需要将系统时钟来源设为 HSI(HSE 也可),并将 PLL 关闭:

void SystemClock_Config(void) {
    LL_RCC_HSE_Enable();
    while(LL_RCC_HSE_IsReady() != 1);                   // 等待 HSE 就绪

    LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_HSE);   // 将系统时钟来源设回 HSE
    while (LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_HSE);
    
    LL_RCC_PLL_Disable();                               // 关闭 PLL
    LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_8, 168, LL_RCC_PLLP_DIV_4);
    LL_RCC_PLL_Enable();
    while(LL_RCC_PLL_IsReady() != 1);                   // 等待 PLL 就绪

    LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
}

这样,就可以成功设置系统时钟频率。

CC BY-SA 4.0 本作品使用基于以下许可授权:Creative Commons Attribution-ShareAlike 4.0 International License.

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据