前面我们认识到了GPIO可以进行输入和输出,那么它为什么能够输入和输出呢?接下来我们就来介绍它到底怎么工作的。

时钟

不管是其他外设还是CPU它们的工作都是靠时钟来进行“催促”完成的。CPU不会主动去Flash中读取指令。在单片机中,几乎所有外设也都不能主动工作,都需要时钟来驱动它工作。

这是一个简化的图,假设所有设备都在收到高电平时才会工作一次。这个信号我们叫做时钟信号,发出信号的源头,我们叫做时钟源。

在工作时,我们难道要让所有的设备都要按照时钟信号工作吗,比如我没有用到串口,难道串口也要工作吗?当然不是,所以设计师就加了一个开关,来控制是否使用。​​​​​​​

时钟频率

这个是时钟信号的形式,不同设备触发工作的时机不同,可能有的在上升沿触发,有的在下降沿触发。对于处理速度,我们使用时钟频率来进行衡量。

倍频与分频

比如时钟信号输出的频率为8MHz,可以通过倍频,改为16MHz,也可以通过分频,改为4MHz。

这里分别为倍频和分频。

这样我们就可以给不同外设,输出不同的频率了。

这里是我们的一个外部时钟器。这个叫做无源谐振器,还有一部分内部电路,通过配合输出稳定的频率。还有一种叫做有源振动器,是由MCU内部通过电容来实现。

时钟树

这个图就是时钟树,首先我们要认识到有四个信号源,分别为,HSI,HSE,LSI,LSE。I为内部,E为外部,H为高频率,L为低频率。​​​​​​​

这是外部接的两个外部时钟源。然后还要有分频和倍频功能。

同时,我们还看到了图中的复用器,用来选择使用哪个时钟源。同时最右边的类似与门的东西,称为使能开关,我们前面说,不会全部打开,就是靠这个来进行配置的。

时钟树一共有如下三部分​​​​​​​

二三部分,我们先不讲解,主要讲解第一部分。

简化后就是上面这样。首先树根部分选择出时钟源,然后树干经过选择器,和倍频器,或直接输出,选择出放入预分频器,进行分频到外设。这里给大家强调一点,最后外设接的总线的时钟频率一般≥外设设置的频率,相当于提供给外设较大的能力,但是外设用多少看外设自身配置。

这里介绍一下PCLK1就是36MHz,就是APB1分出去的频率,其余类似。

看上面代码中有一个CSS是干嘛的呢?它的作用是在外部时钟源出现故障后,可以转换时钟源为内部时钟源。​​​​​​​

这个图是自己的理解,可能不够权威,但是可以帮助大家理解时钟源和外设之间的关系,时钟信号就是通过这些总线来和外设进行连接,提供给外设一个活力,让其可以正常工作。外设的开关,就是我说的与门,我们配置一个,然后剩余一个由时钟信号来进行控制从而促使其工作。

CPU操作片上外设的过程

通信的实质是CPU内部寄存器和片上外设寄存器之间的数据拷贝​​​​​​​​​​​​​​

当时钟给写使能,且确定要写后,把数据转移到寄存器当中。

RCC

RCC就是复位与时钟控制器,我们这里先只考虑时钟相关的功能。我们的时钟树,就是要靠RCC来进行配置。

其中最重要的三个部分就是配置时钟源,配置倍频分频,配置外设开关。

时钟源配置

倍频分频配置

外设配置

案例与源码

代码

void CPU_Delay(volatile uint32_t delay)
{
  while(delay--);
}
  while (1)
  {
    HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_8);
    CPU_Delay(654545);
    
  }

代码非常简单,我们主要需要配置不同的时钟频率来实现执行这段代码的时常,从而改变LED灯闪烁时常。

但是volatile是什么呢,我们发现在不加时,显示不出来我们想要的效果,这是因为内部做了优化,会在cpu中的寄存器中存放中间值,所以我们看不到效果,加上之后就保证了内存的可见性,不让其优化。

源码

__HAL_RCC_GPIOF_CLK_ENABLE();

这个代码我们前面讲解GPIO初始化的时候见过,不过我们当时没讲,现在我们来讲解一下,这个代码就是打开GPIOF针脚的意思。

#define __HAL_RCC_GPIOF_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg; \
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPFEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPFEN);\
                                        UNUSED(tmpreg); \
                                      } while(0U)

首先我们要知道和GPIO相同RCC还是一个结构体,里面存放着自己的寄存器。

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))

#define RCC_APB2ENR_IOPFEN_Pos               (7U)                              
#define RCC_APB2ENR_IOPFEN_Msk               (0x1UL << RCC_APB2ENR_IOPFEN_Pos)  /*!< 0x00000080 */
#define RCC_APB2ENR_IOPFEN                   RCC_APB2ENR_IOPFEN_Msk  

代码中,SET_BIT把APB2ENR的第七位设置为1

这个就代表把外部使能打开了,写入了寄存器,在下面的代码就相当于检查,我们不管。

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL2;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses 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_DIV4;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}

在开始初始化的时候,这个代码就是用来配置时钟源和倍频分频的。

  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

我们挑出这一个来讲一下

上半部分主要配置时钟源

      /* Set the new HSE configuration ---------------------------------------*/
      __HAL_RCC_HSE_CONFIG(RCC_OscInitStruct->HSEState);
#define RCC_CR_HSEON_Pos                     (16U)                             
#define RCC_CR_HSEON_Msk                     (0x1UL << RCC_CR_HSEON_Pos)        /*!< 0x00010000 */
#define RCC_CR_HSEON                         RCC_CR_HSEON_Msk   
#define __HAL_RCC_HSE_CONFIG(__STATE__)                                     \
                    do{                                                     \
                      if ((__STATE__) == RCC_HSE_ON)                        \
                      {                                                     \
                        SET_BIT(RCC->CR, RCC_CR_HSEON);                     \
                      }                                                     \
                      else if ((__STATE__) == RCC_HSE_OFF)                  \
                      {                                                     \
                        CLEAR_BIT(RCC->CR, RCC_CR_HSEON);                   \
                        CLEAR_BIT(RCC->CR, RCC_CR_HSEBYP);                  \
                      }                                                     \
                      else if ((__STATE__) == RCC_HSE_BYPASS)               \
                      {                                                     \
                        SET_BIT(RCC->CR, RCC_CR_HSEBYP);                    \
                        SET_BIT(RCC->CR, RCC_CR_HSEON);                     \
                      }                                                     \
                      else                                                  \
                      {                                                     \
                        CLEAR_BIT(RCC->CR, RCC_CR_HSEON);                   \
                        CLEAR_BIT(RCC->CR, RCC_CR_HSEBYP);                  \
                      }                                                     \
                    }while(0U)

相当于把第16位设置为1

接下来,下半部分,就是设置分频的

  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV4;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

其实这里我们从语义上完全可以理解,后面同样把这些写入寄存器当中,就完成了对时钟树的配置。

/*-------------------------- PCLK1 Configuration ---------------------------*/
if (((RCC_ClkInitStruct->ClockType) & RCC_CLOCKTYPE_PCLK1) == RCC_CLOCKTYPE_PCLK1)
  {
    assert_param(IS_RCC_PCLK(RCC_ClkInitStruct->APB1CLKDivider));
    MODIFY_REG(RCC->CFGR, RCC_CFGR_PPRE1, RCC_ClkInitStruct->APB1CLKDivider);
  }
#define MODIFY_REG(REG, CLEARMASK, SETMASK)  WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
#define RCC_CFGR_PPRE1_DIV2                  0x00000400U  
#define RCC_CFGR_PPRE1_Pos                   (8U)                              
#define RCC_CFGR_PPRE1_Msk                   (0x7UL << RCC_CFGR_PPRE1_Pos)      /*!< 0x00000700 */
#define RCC_CFGR_PPRE1                       RCC_CFGR_PPRE1_Msk   

同时在代码中,以PCLK1来举例,这里把100写入8-10位寄存器

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐