• ARM开发初级-STM32时钟系统以及如何正确使用HAL_Delay-学习笔记08


    1. STM32时钟系统概述

    1.1 时钟系统的概念及意义

    • 概念:时钟系统是由振荡器(信号源)、定时唤醒器、分频器等组成的电路。常用的信号源有晶体振荡器和RC振荡器

    • 意义:时钟是嵌入式系统的脉搏,处理器内核在时钟驱动下完成指令执行,状态变换等动作,外设部件在时钟的驱动下完成各种工作,比如串口数据的发送、A/D转换、定时器计数等等。因此时钟对于计算机系统是至关重要的,通常时钟系统出现问题也是致命的,比如振荡器不起振、振荡不稳、停振等。

    在这里插入图片描述
    首先,通过晶体振荡器产生一个时钟信号,然后跟着一个开关,不开这个时钟信号就没有办法传递出去。通常来说晶体振荡器的频率是比较低的,而CPU可以承受的频率是比较大的。比如振荡器只能有几兆或几十兆的时钟信号产生,而CPU需要的时钟信号是上百兆。因此,我们需要倍频器,使输出信号频率等于输入信号频率整数倍的电路,供给CPU使用。然而我们有很多内外设,他们的需要的频率没有CPU那么高,因此我们又需要分频器,将高速频率降下来,适合各个部件的工作频率。

    1.2 常见振荡器介绍

    • 概念:振荡器是用来产生重复电子讯号的电子元件。其构成的电路叫振荡电路,能将直流电转换为具有一定频率交流信号输出的电子电路或装置。

    • 分类:振荡器主要分为RC,LC振荡器和晶体振荡器。RC振荡器是采用RC网络作为选频移相网络的振荡器。LC振荡器是采用LC振荡回路作为移相和选频网络的正反馈振荡器。晶体振荡器的振荡频率受石英晶体控制。

    • RC振荡器:RC振荡器是由电阻电容构成的振荡电路,能将直流电转换为具有一定频率交流信号输出的电子电路或装置

      • 优点:实现的成本比较低
      • 缺点:由于电阻电容的精度问题所以RC振荡器的震荡频率会有误差,同时受到温度、湿度的影响
        在这里插入图片描述
    • 晶体振荡器:石英晶体振荡器是高精度和高稳定度的振荡器,被广泛应用于彩电、计算机、遥控器等各类振荡电路中,以及通信系统中用于频率发生器、为数据处理设备产生时钟信号和为特定系统提供基准信号

      • 优点:是相对来说震荡频率一般都比较稳定,同时精度也较高
      • 缺点:价格稍高,用晶体振荡器一般还需要接两个15-33pF起振电容
        在这里插入图片描述
    • STM32 中主要有四个时钟源: 可以分为高速和低速两种,高速一般几十MHz,低速一般几十kHz;还可以分为内部和外部两种,内部指在STM32内已经集成了,外部指可以在管脚外部接一个晶振。当我们开机刚上电的时候,我们需要给CPU提供时钟信号,但是外部管脚还没有被初始化,这时候我们就需要使用高速内部时钟来给CPU提供时钟信号。如果出现外部时钟出现故障,这个时候也可以调用内部时钟继续工作

      • HSI:高速内部时钟,RC振荡器,频率为16MHz;
      • HSE:高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~26MHz
      • LSI:低速内部时钟,RC 振荡器,频率为 32kHz 左右。供独立看门狗和自动唤醒单元使用。
      • LSE:低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源

    1.3 时钟树分析

    在这里插入图片描述

    STM32中所有的设备都是需要时钟的,那么怎么时钟源给每一个设备分配适合它的时钟信号都会体现在时钟树中。从上往下看,低速内部时钟主要控制看门狗;低速外部时钟控制RTC;高速内部时钟、高速外部时钟都可以直接选择作为系统时钟,也可以让他们通过倍频器将频率提高到最高168MHz作为系统时钟。当确定好选择哪一个时钟后可以通过AHB PRESC给Cortex系统等高速内核使用,也可以在通过APB PRESC给低速外设使用。

    2. STM32时钟配置实例

    配置STM32F407的时钟,并对比STM32时钟配置前(16 MHz)后(168 MHz)LED外设闪烁的快慢。需要注意的是闪烁的时候需要用到delay函数,然而HAL_Delay不管设置的时钟是多少都是按照毫秒来计算的,因此我们需要自己写一个delay函数,比较简单代码如下。

    void delay(uint32_t time)
    {
    	uint32_t i, j;
    	for (i = 0; i < time; i++)
    	{
    		for (j = 0; j < 5000; j++);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过电路原理图,我的板子上的LED用的是PE2、PF8、PF9 和 PF10,因此要设置他们为输出模式。

    在这里插入图片描述
    然后我们看一下时钟配置,默认情况下用的是HSI,也就是高速内部时钟(16 MHz),导出工程后,在main函数的while里写如下代码。然后下载到板子里,可以观察到板子上的LED每次闪烁在一秒左右。

    while (1)
      {
    	HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_RESET);
    	HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10, GPIO_PIN_RESET);
    	
    	delay(1000);
    	
    	HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_SET);
    	HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10, GPIO_PIN_SET);
    	
    	delay(1000);
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后,我们按照下图重新设置一下时钟(先设置能使用外部晶振,在配置倍频器),这样能把时钟拉满。算是STM32F407能接受的最快速度。这个时候,我们重新导出工程,下载到板子里,可以观察到闪烁速度快了将近十倍。

    在这里插入图片描述

    在这里插入图片描述

    3. SysTick定时器讲解

    3.1 SysTick 定时器介绍

    • 概念
      能够定时、计数的器件称为定时器。SysTick,(系统滴答定时器)是一个定时设备,位于Cortex-M4内核中,可以对输入的时钟进行计数,当然,如果时钟信号是周期性的,计数也就是计时。系统定时器一般用于操作系统,用于产生时基,维持操作系统的心跳。根据这个中断,系统就可以实现时间片的计算从而切换进程。
    • 工作原理
      滴答定时器是一个24位定时器,也就是最多能计数 2 24 2^{24} 224。在使用的时候,我们一般给计数器送一个初始的计数值,计数器向下计数,每来一个时钟信号,计数初值就减一,计数值减到0的时候,就会发出一次中断。然后重新从计数初值再减一计数,循环不断

    3.2 SysTick定时器工作原理分析

    在这里插入图片描述
    在上图中,我们假设AHB是 168 MHz,经过/8的分频器,那么SysClock的时钟就是 21 MHz。然后这个时钟信号作为输入,给到定时器,定时器读取重载数值寄存器,假设其值为val,定时器每次val--,直到等于0的时候重新加载数值寄存器。这个重载数值寄存器的值是可以设置的,假设我们要设置它到0的时候,时间经过1ms,那么我们要设置的值是多少?
    1 21 M H z ( s ) ∗ v a l = 1 ( m s ) ⇒ v a l = 21000 < 2 24 \frac1{21 MHz}(s) * val = 1 (ms) \Rightarrow val = 21000 < 2^{24} 21MHz1(s)val=1(ms)val=21000<224
    在这里插入图片描述

    3.3 SysTick寄存器

    在这里插入图片描述
    在这里插入图片描述
    上面两张图很好的解释了和SysTick有关的寄存器,都比较直白,没啥好解释的

    3.4 SysTick定时器使用实例

    我用的环境是CubeMx和Keil 5,这次要利用SysTick定时器实现1s打印一个字符串。新建一个CubeMx项目,设置USART1串口,然后导出项目。
    首先,我们先了解下程序是如何初始化Systick的,打开main.c可以看到main函数里的第一行HAL_Init在这里初始化了Systick

    int main(void)
    {
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART1_UART_Init();
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    打开HAL_Init的定义,HAL_InitTick(TICK_INT_PRIORITY)初始化了Systick,输入的参数是Systick的中断优先级

    HAL_StatusTypeDef HAL_Init(void)
    {
      /* Configure Flash prefetch, Instruction cache, Data cache */ 
    #if (INSTRUCTION_CACHE_ENABLE != 0U)
      __HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
    #endif /* INSTRUCTION_CACHE_ENABLE */
    
    #if (DATA_CACHE_ENABLE != 0U)
      __HAL_FLASH_DATA_CACHE_ENABLE();
    #endif /* DATA_CACHE_ENABLE */
    
    #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;
    }
    
    • 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

    继续追查HAL_InitTick(TICK_INT_PRIORITY)的定义,在这里定义了HAL_SYSTICK_Config,设置了systick需要每1ms中断一次

    /**
      * @brief This function configures the source of the time base.
      *        The time source is configured  to have 1ms time base with a dedicated 
      *        Tick interrupt priority.
      * @note This function is called  automatically at the beginning of program after
      *       reset by HAL_Init() or at any time when clock is reconfigured  by HAL_RCC_ClockConfig().
      * @note In the default implementation, SysTick timer is the source of time base. 
      *       It is used to generate interrupts at regular time intervals. 
      *       Care must be taken if HAL_Delay() is called from a peripheral ISR process, 
      *       The SysTick interrupt must have higher priority (numerically lower)
      *       than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
      *       The function is declared as __weak  to be overwritten  in case of other
      *       implementation  in user file.
      * @param TickPriority Tick interrupt priority.
      * @retval HAL status
      */
    __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
    {
      /* Configure the SysTick to have interrupt in 1ms time basis*/
      if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
      {
        return HAL_ERROR;
      }
    
      /* Configure the SysTick IRQ priority */
      if (TickPriority < (1UL << __NVIC_PRIO_BITS))
      {
        HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
        uwTickPrio = TickPriority;
      }
      else
      {
        return HAL_ERROR;
      }
    
      /* Return function status */
      return HAL_OK;
    }
    
    • 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

    追查HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)的定义,发现还需要继续看SysTick_Config的定义

    /**
      * @brief  Initializes the System Timer and its interrupt, and starts the System Tick Timer.
      *         Counter is in free running mode to generate periodic interrupts.
      * @param  TicksNumb Specifies the ticks Number of ticks between two interrupts.
      * @retval status:  - 0  Function succeeded.
      *                  - 1  Function failed.
      */
    uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
    {
       return SysTick_Config(TicksNumb);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    定义如下,时间要设置成ticks-1,防止溢出,之后就是操作底层的寄存器。同样的道理我们也可以追查Systick优先级的定义,这里就不继续看了。

    /**
      \brief   System Tick Configuration
      \details Initializes the System Timer and its interrupt, and starts the System Tick Timer.
               Counter is in free running mode to generate periodic interrupts.
      \param [in]  ticks  Number of ticks between two interrupts.
      \return          0  Function succeeded.
      \return          1  Function failed.
      \note    When the variable __Vendor_SysTickConfig is set to 1, then the
               function SysTick_Config is not included. In this case, the file device.h
               must contain a vendor-specific implementation of this function.
     */
    __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
    {
      if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
      {
        return (1UL);                                                   /* Reload value impossible */
      }
    
      SysTick->LOAD  = (uint32_t)(ticks - 1UL);                         /* set reload register */
      NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
      SysTick->VAL   = 0UL;                                             /* Load the SysTick Counter Value */
      SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                       SysTick_CTRL_TICKINT_Msk   |
                       SysTick_CTRL_ENABLE_Msk;                         /* Enable SysTick IRQ and SysTick Timer */
      return (0UL);                                                     /* Function successful */
    }
    
    • 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

    如果我们要设置1s打印一个字符串,那么我们要设置每1ms后重装载值寄存器清零后的中断回调函数。要找到这个中断回调函数,我们需要查中断向量表

    __Vectors       DCD     __initial_sp               ; Top of Stack
                    DCD     Reset_Handler              ; Reset Handler
                    DCD     NMI_Handler                ; NMI Handler
                    DCD     HardFault_Handler          ; Hard Fault Handler
                    DCD     MemManage_Handler          ; MPU Fault Handler
                    DCD     BusFault_Handler           ; Bus Fault Handler
                    DCD     UsageFault_Handler         ; Usage Fault Handler
                    DCD     0                          ; Reserved
                    DCD     0                          ; Reserved
                    DCD     0                          ; Reserved
                    DCD     0                          ; Reserved
                    DCD     SVC_Handler                ; SVCall Handler
                    DCD     DebugMon_Handler           ; Debug Monitor Handler
                    DCD     0                          ; Reserved
                    DCD     PendSV_Handler             ; PendSV Handler
                    DCD     SysTick_Handler            ; SysTick Handler
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们需要查看SysTick_Handler的定义

    void SysTick_Handler(void)
    {
      /* USER CODE BEGIN SysTick_IRQn 0 */
    
      /* USER CODE END SysTick_IRQn 0 */
      HAL_IncTick();
      HAL_SYSTICK_IRQHandler();
      /* USER CODE BEGIN SysTick_IRQn 1 */
    
      /* USER CODE END SysTick_IRQn 1 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    查看HAL_SYSTICK_IRQHandler的定义。

    /**
      * @brief  This function handles SYSTICK interrupt request.
      * @retval None
      */
    void HAL_SYSTICK_IRQHandler(void)
    {
      HAL_SYSTICK_Callback();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    HAL_SYSTICK_Callback就是我们需要写的函数,写的代码如下,因为每1ms中断一次,那么我们1s就需要1000次才能打印一句话。printf的实现可以通过覆盖fputc来实现。

    /* USER CODE BEGIN 1 */
    void HAL_SYSTICK_Callback(void)
    {
    	static uint32_t i = 0;
    	i++;
    	if (i == 1000)
    	{
    		printf("1s \n");
    		i = 0;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    /* USER CODE BEGIN PFP */
    int fputc(int ch, FILE *p)
    {
    	while (!(USART1->SR & (1<<7)));
    	USART1->DR = ch;
    	return ch;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    至此上传到板子,就可以实现目标功能。

    4. HAL_Delay()函数的实现

    HAL_Delay是依赖SysTick来实现的,虽然常用,但是如果不正确使用会导致死机。首先我们先看下他的源码

    /**
      * @brief This function provides minimum delay (in milliseconds) based 
      *        on variable incremented.
      * @note In the default implementation , SysTick timer is the source of time base.
      *       It is used to generate interrupts at regular time intervals where uwTick
      *       is incremented.
      * @note This function is declared as __weak to be overwritten in case of other
      *       implementations in user file.
      * @param Delay specifies the delay time length, in milliseconds.
      * @retval None
      */
    __weak void HAL_Delay(uint32_t Delay)
    {
      uint32_t tickstart = HAL_GetTick();
      uint32_t wait = Delay;
    
      /* Add a freq to guarantee minimum wait */
      if (wait < HAL_MAX_DELAY) //防止我们写delay的时间是0,加一个最小值
      {
        wait += (uint32_t)(uwTickFreq);
      }
    
      while((HAL_GetTick() - tickstart) < wait) // 主要看这里,延时多久主要看while的条件
      											// 也就是当前时间-开始时间>wait退出
      {
      }
    }
    
    • 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

    我们可以继续看HAL_GetTick是怎么写的。它返回了uwTick

    /**
      * @brief Provides a tick value in millisecond.
      * @note This function is declared as __weak to be overwritten in case of other 
      *       implementations in user file.
      * @retval tick value
      */
    __weak uint32_t HAL_GetTick(void)
    {
      return uwTick;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    uwTick是怎么变化的呢,经过查找我们发现它首先是一个uint32_t,以及一个对uwTick的加法操作,加了什么,我们经过几次查找找到了它是1u,简单说就是每次+1

    __IO uint32_t uwTick;
    uwTickFreq = 1U,
    /**
      * @brief This function is called to increment  a global variable "uwTick"
      *        used as application time base.
      * @note In the default implementation, this variable is incremented each 1ms
      *       in SysTick ISR.
     * @note This function is declared as __weak to be overwritten in case of other 
      *      implementations in user file.
      * @retval None
      */
    __weak void HAL_IncTick(void)
    {
      uwTick += uwTickFreq;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    那这个HAL_IncTick()是在哪里被调用的呢?
    其实我们之前已经见过了,这也就是说我们每毫秒会调动这个变量一次,每一次都会+1。

    void SysTick_Handler(void)
    {
      /* USER CODE BEGIN SysTick_IRQn 0 */
    
      /* USER CODE END SysTick_IRQn 0 */
      HAL_IncTick();
      HAL_SYSTICK_IRQHandler();
      /* USER CODE BEGIN SysTick_IRQn 1 */
    
      /* USER CODE END SysTick_IRQn 1 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    最后我们返回HAL_Delay的定义

    __weak void HAL_Delay(uint32_t Delay)
    {
      uint32_t tickstart = HAL_GetTick(); // HAL_GetTick每毫秒会+1
      uint32_t wait = Delay;
    
      /* Add a freq to guarantee minimum wait */
      if (wait < HAL_MAX_DELAY) //防止我们写delay的时间是0,加一个最小值
      {
        wait += (uint32_t)(uwTickFreq);
      }
    
      while((HAL_GetTick() - tickstart) < wait) // 主要看这里,延时多久主要看while的条件
      											// 也就是当前时间-开始时间>wait退出
      {
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    总的来说,HAL_Delay是通过SysTick每毫秒调用的一个回调函数而实现的。如果这个回调函数没有办法被调用,我们的程序就会卡在while里不更新,也退不出去,因此造成了死机。那么要保证SysTICK_IRQHandler要顺利执行,那就需要有较高的抢占优先级。如果一个被调用的中断的优先级是 0 0,那么这个时候再调用HAL_Delay,由于它的优先级也是0 0,这就导致了SysTICK_IRQHandler无法抢占之前的中断,无法抢断,那么uwTick就无法+1,最后导致卡在死循环,死机。这也是为什么我们平时设置GPIO或者其他中断的抢占优先级调低一点的原因。

  • 相关阅读:
    Rust开发问题汇总
    【爬虫知识】比lxml和bs4都要好?解析器parsel的使用
    双向交错CCM图腾柱无桥单相PFC学习仿真与实现(4)一些优化总结
    Go网络编程 Conn接口
    C语言内联汇编(详细)介绍附实例快速掌握
    小米为什么造不出芯片
    《中国工业经济》企业数字化转型与供应链配置—集中化还是多元化
    标准流布局
    平衡优化算法在特征选择中的应用(Matlab代码实现)
    Linux系统IO和标准IO的接口函数
  • 原文地址:https://blog.csdn.net/baidu_41924187/article/details/126128217