STM32
直播中

李晓鹏

7年用户 1260经验值
私信 关注
[问答]

如何去制作一个基于Stm32CubeMx的一秒定时器呢

为什么叫基本定时器呢?
如何去制作一个基于STM32CubeMx的一秒定时器呢?

回帖(1)

陈思阳

2021-11-23 12:01:57
  小明妈妈跟小明说:“10分钟后,你再不给我去做作业我就揍你!”,接着,小明妈妈看着手表,1秒钟数1下,0,1,2,3,……,599。看看小明有没有做作业,根据情况判断要不要揍他。
  接着,小明妈妈又从0数起,到599,继续看看小明有没有做作业…… ……再数数…… ……
  于是,小明妈妈就是一个每隔10分钟监视一下小明有没有做作业的定时器,手表,就是小明妈妈定时器的时钟,小明妈妈数600个周期后(0~599),触发定时器中断,定时器中断里,判断小明有没有去做作业,没有,就揍他,有,就啥事也不做。
  STM32上的定时器,也是跟小明妈妈一样,是通过数数计时的,比如,数1个数是1秒,从0数到599,就是600秒(10分钟)了。
  STM32上的数数,可以正着数,从0~599,也可以倒着数,从599~0,还可以先正着数后倒着数,当然,小明妈妈也是可以的。
  那能不能不数600个数?数别的?可以的,只需要设置就行了。
  还能不能不1秒数一次,长一点或者短一点?也是可以的,只需要设置就行了。
  RM0033告诉我们,STM32F207有8个小明妈妈,TIM1~TIM8,资源非常丰富,当然,它也不仅仅只能用于数数,功能非常强大。
  为什么叫基本定时器呢?因为它们不仅用来计时,还有其他非常强大的功能,如PWM,脉宽测量等等。定时器,只是它的基本功能。
  我们今天,只讲数数功能,数到一定数后,就触发定时器中断,处理事件。换句话说,每隔一段特定的时间,触发特定的事件。
  我们就来做一个一秒的定时器吧,每隔1秒打印定时器开启时间。
  先打开Stm32CubeMx,新建一个工程
  建完之后,看TIMx(x=1,2,3……),这就是定时器。
  这里面有各种需要设置的地方,Slave Mode、Trigger Source、Clock Source、等等等。
  这是啥?看不懂,怎么办(黑人问号)???
  
  没关系,问一下RM0033,找到相关的部分,看介绍。
  它告诉我们,定时器包含16Bit自动重载计数器,由可编程预分频器组成。抱歉,翻译不是我的特长,大家大概看得懂意思就行啦。它还告诉我们,这些定时器有很多作用,像量输入信号脉冲宽度或者输出波形等等。还讲了脉宽测量的范围,从微秒级到毫秒级的。还说这几个小明妈妈是完全独立的,不过她们可以同步,具体的要参考一下Section 13.3.20
  
  点到13.3.20,定时器可以连在一块,具体参考14.3.15。
  
  点到14.3.15,当一个定时器配置成Master Mode时,它能够复位,开始,停止,或者作为另外一个被配置成Slave Mode的定时器的时钟。
  由此可见,Slave Mode,主要用在两个以上的定时器上,具体有兴趣的朋友可以继续往下看资料,这里就不截图出来。
  它有Reset Mode、GateMode、Trigger Mode、External Clock Mode几种模式。
  我们这章节,不需要用到Slave Mode,所以,就选择 Disable。
  
  Trigger Source 选择,里面有ITR0、1、2、3等项目,怎么选择呢?
  ClockSource 选择,里面有Internal Clock,ETR,怎么选择呢?
  Channel1、2、3、4,以及以下的,如何设置呢?
  先来看一个图:
  图中标的1、2、3,就是Timer的ClockSource。
  图中标的4、5,是定时器的其它功能,如Pwm,脉宽测量等功能,我们这节课暂时不需要关注它。
  
  再来看看 Clock selection 的说明:
  
  我这板子呢,也没有外部引脚的时钟源,也没有用到Slave Mode,所以,
  ClockSource,就选 Internal Clock,Trigger Clock,就选Disable。
  Channel1、2、3、4、及以下的呢,我们全都Disable掉,不需要选。
  设置完的定时器如下图:
  
  基本设置完成,再先择顶上的标签页“Configuration”,再点TIM1,旁边有个时钟标志,弹出菜单:
  
  这里密密麻麻的一大垞,怎么设置呢?
  小明妈妈跟小明说:“10分钟后,你再不给我去做作业我就揍你!”,接着,小明妈妈看着手表,1秒钟数1下,0,1,2,3,……,599。
  小明妈妈的ClockSource就是手表,周期是1秒钟,频率就是1Hz。
  小明妈妈觉得1秒钟数一下,10分钟得数600下,这时,小明妈妈想,那我2秒数一下,我只需要数300下,我3秒数一下,我只需要数200下,……
  这个“x秒数一下”,就是预分频(Prescalar),作用是,把超高超高的频率降低下来。
  预分频为0,分频后的频率就是1Hz/(0+1)=1Hz,要数600下,Count Preiod=600-1;
  预分频为1,分频后的频率就是1Hz/(1+1)=0.5Hz,要数300下,Count Preiod=300-1;
  预分频为2,分频后的频率就是1Hz/(2+1)=1/3Hz,要数200下,Count Preiod=200-1;
  …… ……
  预分频为n,分频后的频率就是1Hz/(n+1) Hz,…………自个算去;
  小明妈妈想计时10分钟,她的ClockSource=手表,手表频率是1Hz,
  Prescaler=0,Count Preiod=599;
  Prescaler=1,Count Preiod=299;
  Prescaler=2,Count Preiod=199;
  说完小明妈妈,我们来看一下Stm32,Stm32 ClockSource我们选的是Internal Clock,那么,我们这个Internal Clock的频率是多少呢?
  首先,得了解一下TIM1的Internal Clock从何而来?
  看一下DataSheet里面的系统总框图,TIM1挂在APB2总线上,于是,它的Clock由APB2提供;
  
  再回忆一下我们刚用Stm32时,设置Clock的时候是怎么设置的?
  打开Clock Configuration标签页:
  看到没,这个APB2 timer clock = 120Mhz = 120,000,000Hz。
  
  120,000,000Hz,也就是计时1秒,要数1.2亿次啊,那Count Preiod设置 120,000,000-1可以不?
  那是不行的,因为设置不下去,Count Period是16bit的,所以它的最大值就是 65535(2的16次方-1)。
  不用担心,我们有Prescaler,这个东西它也能把Internal Clock分频,它的范围是 0~65535,所以,它能把clock的频率变成 120MHz/(0+1)~120MHz/(65535+1),也就是120,000,000Hz~1831Hz之间,我们不要选得这么极端,我们选择个11999,也就是把频率分成 120,000,000/(11999+1) = 10000Hz,计时一秒,只需要10000次就够了。
  那么,我们的Prescaler设置为120000-1=119999,Count Period设置为10000-1=9999
  Counter Mode,管正着数,还是倒着数,还是先正着数,再倒着数。
  正着数:从0~9999,数1000次,产生上溢事件。
  倒着数:从9999~0,数1000次,产生下溢事件。
  先正着数,再倒着数:从0~9998,数9999次,再从9999~1,数9999次。
  请详细阅读RM0033第13.3.2,在这里,我们就设置个Up吧。
  其他的,在基本定时器中不需要用到,所以就不用设置,这样,一个1秒钟的定时器就完成了!如图所示:
  
  每次计数器溢出(不管上溢还是下溢)时,会产生一个update event(UEV)。
  所以,记得把NVIC Settings里面的中断钩上,我们后续会对这个事件进行处理。
  
  接下来,生成代码后,看看它生成了什么东西,我们把这些东西,挪到上一篇的@命令的项目中
  在一个项目中,有时候要增加外设,但项目已经完成了,我们不可能用Stm32CubeMx重新走一遍配置,再把我们自己写的代码挪过去,这样太麻烦了。
  比较简便的办法,就是用Stm32CubeMx生成配置,然后把这些配置文件及代码,移植到原有的项目中。
  对STM32比较熟悉的同学还可以直接用HAL库直接写配置,或者直接写寄存器。
  所以后面我们就比较少用Stm32CubeMx直接生成代码直接用了,我们更多用它,来生成一些配置,把这些配置移植到我们原有的项目中,以及结合文档,理解STM32的寄存器。
  Stm32CubeMx生成了tim.c和tim.h文件,在main()里面用MX_TIM1_Init();进行初始化,以及在stm32f2xx_it.c里面,生成void TIM1_UP_TIM10_IRQHandler(void)中断服务例程。
  好了,依葫芦画瓢,把它挪过来吧。
  tim.c/tim.h,直接复制到对应的文件夹下,tim.c要加入工程编译。
  
  在main.c里面,增加初始化MX_TIM1_Init();,记得把tim.h #include进来。
  #include “main.h”
  #include “stm32f2xx_hal.h”
  #include “dma.h”
  #include “i2c.h”
  #include “tim.h” // 包含的tim.h文件
  #include “usart.h”
  #include “gpio.h”
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_UART4_Init();
  MX_TIM1_Init(); // 初始化
  /* USER CODE BEGIN 2 */
  USR_UartInit();
  /* USER CODE END 2 */
  在stm32f2xx_it.c里面,增加中断服务例程,因为用到htim1这个变量,所以还要把这个变量extern进来
  /* External variables --------------------------------------------------------*/
  extern TIM_HandleTypeDef htim1; // 就是这个变量
  extern DMA_HandleTypeDef hdma_uart4_rx;
  extern UART_HandleTypeDef huart4;
  /**
  * @brief This function handles TIM1 update interrupt and TIM10 global interrupt.
  */
  void TIM1_UP_TIM10_IRQHandler(void)
  {
  /* USER CODE BEGIN TIM1_UP_TIM10_IRQn 0 */
  /* USER CODE END TIM1_UP_TIM10_IRQn 0 */
  HAL_TIM_IRQHandler(&htim1);
  /* USER CODE BEGIN TIM1_UP_TIM10_IRQn 1 */
  /* USER CODE END TIM1_UP_TIM10_IRQn 1 */
  }
  还有记得在stm32f2xx_hal_conf.h里面,把#define HAL_TIM_MODULE_ENABLED打开,之前是注释掉的。
  /*#define HAL_MMC_MODULE_ENABLED */
  /*#define HAL_SPI_MODULE_ENABLED */
  #define HAL_TIM_MODULE_ENABLED // 一定要打开喔 ^_^
  #define HAL_UART_MODULE_ENABLED
  /*#define HAL_USART_MODULE_ENABLED */
  /*#define HAL_IRDA_MODULE_ENABLED */
  /*#define HAL_SMARTCARD_MODULE_ENABLED */
  挪好了,那接下来,我们要做什么?
  开启TIM中断,处理UEV;
  我们就在初始化后开启TIM中断。
  void MX_TIM1_Init(void)
  {
  TIM_ClockConfigTypeDef sClockSourceConfig;
  TIM_MasterConfigTypeDef sMasterConfig;
  htim1.Instance = TIM1; // Tim1
  htim1.Init.Prescaler = 29999; // 预分频
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP; // 正着数
  htim1.Init.Period = 3999; // 数多少个周期
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim1.Init.RepetitionCounter = 0;
  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
  {
  _Error_Handler(__FILE__, __LINE__);
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
  {
  _Error_Handler(__FILE__, __LINE__);
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
  {
  _Error_Handler(__FILE__, __LINE__);
  }
  HAL_TIM_Base_Start_IT(&htim1); // 在这里,开开开!^_^
  }
  我们看一下中断服务例程,
  上面讲过,每次计数器溢出(不管上溢还是下溢)时,会产生一个update event(UEV)。
  也就是说,每隔1秒钟(我们设置的),会产生一次中断。
  // 简单地理解,定时器每计1秒会进一次这个函数喔^_^~~~
  void TIM1_UP_TIM10_IRQHandler(void)
  {
  /* USER CODE BEGIN TIM1_UP_TIM10_IRQn 0 */
  /* USER CODE END TIM1_UP_TIM10_IRQn 0 */
  HAL_TIM_IRQHandler(&htim1);
  /* USER CODE BEGIN TIM1_UP_TIM10_IRQn 1 */
  /* USER CODE END TIM1_UP_TIM10_IRQn 1 */
  }
  点进去,看一下HAL_TIM_IRQHandler(&htim1);这个函数做了什么。
  里面有各种不同的事件处理,但我们只关注 Update Event 就行了。
  /* TIM Update event */
  if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET)
  {
  if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) !=RESET)
  {
  __HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);
  HAL_TIM_PeriodElapsedCallback(htim); // 就是你了!实现它!
  }
  }
  再看一下回调函数。
  它的NOTE告诉我们,这个函数不能改,如果需要,就在重写它就行了。
  我们现在就来重写它。
  /**
  * @brief Period elapsed callback in non blocking mode
  * @param htim pointer to a TIM_HandleTypeDef structure that contains
  * the configuration information for TIM module.
  * @retval None
  */
  __weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  {
  /* Prevent unused argument(s) compilation warning */
  UNUSED(htim);
  /* NOTE : This function Should not be modified, when the callback is needed,
  the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
  */
  }
  既然我们需要打印定时器启动后的秒数,那我们首先需要这个秒数的变量。
  变量名为 tim1StartCntS
  变量类型用 unsigned int,能计时 2的32位 秒,也就是 4294967296 秒 = 1193046小时 = 49710天 = 136年,系统一般运行不了这么长时间,所以 unsigned int 就足够了。
  /* Includes ------------------------------------------------------------------*/
  #include “tim.h”
  /* USER CODE BEGIN 0 */
  volatile unsigned int tim1StartCntS; // 这个够定时器运行 136 年啊^_^
  /* USER CODE END 0 */
  TIM_HandleTypeDef htim1;
  这个函数,就是重写的回调函数。
  void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  {
  /* Prevent unused argument(s) compilation warning */
  if(htim-》Instance==htim1.Instance)
  {
  tim1StartCntS++; // 每一秒钟自加。
  }
  /* NOTE : This function Should not be modified, when the callback is needed,
  the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
  */
  }
  接下来,能不能直接在回调函数里面打印一下 tim1StartCnS?
  最好不要,
  记住中断服务例程设计原则其中之一:中断尽量简短,请不要在中断服务例程里执行比较耗费时间的事,比如,printf,延时等需要大量时间的事。
  那么,我们就设计一个 Handler,在main()主循环里面,打印定时器启动时间吧。
  每隔一秒钟,tim1StartCntS会增加1,而tim1CntTmp不变,所以,每一秒钟,tim1StartCntS 和tim1CntTmp的值是不同的,每次不同,都要更新一下tim1CntTmp的值,然后打印。
  这样,就每一秒都打印出 tim1StartCntS 的值了。
  /* USER CODE BEGIN 1 */
  void TIM1_Handler(void)
  {
  static unsigned int tim1CntTmp = 0;
  if(tim1CntTmp!=tim1StartCntS)
  {
  tim1CntTmp = tim1StartCntS;
  printf(“Tim1 Start %d seconds.rn”, tim1CntTmp);
  }
  }
  记得,这个TIM1_Handler(),要在tim.h里面声明一下,要在main()主循环里面调用一下。
  // 在tim.h里面声明一下
  extern void _Error_Handler(char *, int);
  void MX_TIM1_Init(void);
  void TIM1_Handler(void);
  // 在 main()的主环里调用
  while (1)
  {
  USR_LedHandler();
  SERDEB_Handler();
  TIM1_Handler(); // 就这了!
  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */
  }
  编译,运行,烧录,来看一下运行结果:这个,运行很久了喔。
  
举报

更多回帖

发帖
×
20
完善资料,
赚取积分