当前位置:首页>java>STM32"零代码”编程实现四位数字秒表(硬件定时器)

STM32"零代码”编程实现四位数字秒表(硬件定时器)

  • 2026-10-19 00:27:05
STM32"零代码”编程实现四位数字秒表(硬件定时器)
点击蓝字 | 关注我们

本例子使用硬件定时器实现精确计时(每100ms计数一次,计时精度为0.1s),使用4位数码管(动态扫描算法)实现秒表功能,通过按键控制秒表启停和复位。通过简单的例子学习硬件定时器计时功能使用(定时中断)、继续使用GPIO中断实现两个按键检测。

01
1秒究竟有多长
首先看一段代码:
/* 根据当前模式执行对应流水灯函数 */switch(current_mode){case MODE_WATERFALL_1:LED_Waterfall_1();break;case MODE_WATERFALL_SINGLE:LED_Waterfall();break;case MODE_WATERFALL_GROUP:LED_Waterfall_Group();break;default:current_mode = MODE_WATERFALL_1; // 异常时默认模式1break;}
在该断代码中,使用switch进行多分支判断,根据current_mode实际的值,顺序和MODE_WATERFALL_1、MODE_WATERFALL_SINGLE、MODE_WATERFALL_GROUP三个值比较,如果有相等,则执行对应case下的LED流水灯函数。
将前两个LED流水灯函数放到一起作比较,分析两个函数执行一次,在同样的硬件环境下,用时是否相等?

答案是不相等,在LED_Waterfall()函数中增加了 LED_All_Off()函数和if (i > LED0)...条件判断,这里想说的是,在单片机主循环程序中,由于程序逻辑不同,每一次程序循环用时不尽相同,但是这个时间的差异,大多数情况是可以接受的,比如之前在实现LED流水灯切换时,并不影响程序的执行结果。从视觉观感上,ms级别的时间差,也不影响视觉感受。

但是在一些场合,需要严格控制执行时序,需要按照“精确”时间完成指令的执行,比如,秒表。开始计时后,秒表需要严格按照每一秒计时/计数加1,并同步显示。
在单片机中,可以使用硬件定时器(Timer)来进行精确计时。
AI第一问:stm32f103定时器有哪些功能
STM32F103 定时器的分类与核心功能STM32F103 的定时器主要分为三类:基本定时器(TIM6/TIM7)、通用定时器(TIM2-TIM5)、高级定时器(TIM1/TIM8),不同类型的功能由简到繁,具体如下:基本定时器(TIM6/TIM7)仅具备最基础的定时功能,无 IO 输出 / 输入通道,核心功能:基本定时 / 计数:通过内部时钟(72MHz 分频后)实现精准的时间基准,溢出时产生中断或 DMA 请求。DAC 触发:可作为 DAC(数模转换)的触发源,用于同步 DAC 的输出更新。典型应用:基础延时、DAC 同步触发、系统心跳定时器。通用定时器(TIM2-TIM5)功能最丰富、使用最广泛的定时器,支持输入 / 输出通道,核心功能:(1)核心定时 / 计数模式向上计数:从 0 计数到自动重装值(ARR),溢出后重置为 0 并产生更新事件。向下计数:从自动重装值计数到 0,溢出后重置为 ARR 并产生更新事件。中心对齐计数:先向上计数到 ARR,再向下计数到 0,双向循环(适合对称 PWM)。(2)输入捕获检测外部 IO 引脚的电平变化(上升沿 / 下降沿),记录定时器当前计数值,实现:测量脉冲宽度(如超声波测距的高电平时长)。测量信号频率(如编码器脉冲计数)。捕获外部触发信号的时间戳。(3)输出比较将定时器计数值与预设的比较值(CCR)对比,满足条件时控制 IO 引脚输出:输出电平翻转(如产生固定频率的方波)。输出单脉冲(触发单次外部事件)。强制置位 / 复位输出引脚。(4)PWM 生成通过输出比较的 PWM 模式,生成占空比可调的脉冲宽度调制波:边缘对齐 PWM(常用,如电机调速、LED 调光)。中心对齐 PWM(适合高精度电机控制)。(5)编码器接口对接增量式编码器,自动计数编码器的 A/B 相脉冲,实现:电机转速 / 位置检测。旋转编码器的脉冲计数。(6)外部触发同步可通过外部 IO 引脚的信号触发定时器启动 / 停止 / 重置,或同步多个定时器的计数。高级定时器(TIM1/TIM8)在通用定时器基础上增加了 “死区生成” 和 “刹车功能”,专为电机控制设计:通用定时器的所有功能(定时、PWM、输入捕获、编码器等)。互补 PWM 输出:每路 PWM 对应一路互补 PWM,可设置死区时间(防止电机桥臂直通)。刹车 / 断路功能:检测到故障信号(如过流、过压)时,立即关闭 PWM 输出,保障硬件安全。支持多通道 PWM 同步输出(适合三相电机控制)。

02
原理图设计

元件清单:

序号名称型号数量Lib名称
1单片机stm32f103c81STM32F103C8
2数码管驱动芯片74hc245174hc245
3共阴数码管7SEG-MPX4-CC17SEG-MPX4-CC
4按钮button2button
按下图绘制原理图
图中,4位数码管在显示时使用动态扫描,因此每位数码管的共阴极COM公共端不能直接接地,需要与单片机相连,在运行时,逐次选择每一位数码管,并显示对应的内容,4位逐个循环显示。
03
硬件初始化

在本项目中,需要使用硬件定时器,除了要生成相关初始化、应用层代码,还需要管理HAL库,启用其timer相关功能。所以,本着“零代码”原则,本案例需要先试用STM32CUBEMX生成硬件初始化程序。

启动STM32CUBEMX,选择STM32F103C8T6单片机,新建项目后初始化时钟,使得HCLK为64MHz。
GPIO初始化,将PB0~PB7、PB12~PB15配置为推挽输出,无上拉、无下拉;PA3、PA7配置为中断输入,上拉电阻。
在STM32CUBEMX中,GPIO引脚配置为中断输入时,在对应引脚上,单击鼠标,选择GPIO_EXTIx,不同的引脚,x值和引脚编号一致。
初始化完成后,结果如下:
接下来配置硬件定时器,STM32CUBEMX左侧导航栏中选择Timers,选择其中的一个TIM,这里选择TIM1。
这里,系统时钟64MHz,640分频后,定时器时钟100KHz,计数周期为9999+1=10000,因此,定时器每100ms中断一次,中断10为1秒。
GPIO中断和定时器中断配置:
GPIO和定时器的中断配置可以在配置GPIO和定时器时,分别配置。
也可以配置完成后,集中在NVIC选项中一起使能。
最后,代码生成相关配置:
生成代码后,可以看到在项目数中,GPIO和Timer有各自的.c源文件。
04
编程
将以下提示词丢给豆包:
使用stm32f103c8实现4位共阴数码管计时器:1)有两个按键,连接到PA3和PA7,PA3为开始/结束按键,PA7为复位按键,使用中断方式读取按键输入;2)数码管使用4为共阴数码管,PB0~PB6连接数码管a~g段,PB7连接dp(小数点);PB12~PB15分别连接4位数码管的位选信号,顺序从左到右。3)使用定时器1定时中断进行精确计时;4)使用HAL库已完成按键、数码管使用GPIO的初始化和定时器的初始化;5)开始运行后,计时器不工作,计数值为0,数码管显示0000;此时按下复位键无效6)按下开始按键,启动定时器计数;计数过程中按下复位键无效;计数过程中,再次按下开始键,停止计数,数码管显示最后的计数值;这时按下复位键,计数清零,显示0000。再次按下开始后,重新开始计时...;7)在定时中断中进行计数(定时器初始化中每隔100ms中断一次)累加,当大于9999(4为数码管最大值)后,自动停止计数;定义数码管动态扫描函数,在主循环中调用,实时显示计数值9)因为已经完成时钟、按键、数码管、定时器硬件初始化,只需要实现功能代码和gpio中断回调函数和定时中断回调函数即可。10)所有函数和变量定义都在main.c中完成。
最后使用生成的代码,(经调整后)运行结果如下:
豆包生成的程序基本可以使用,不过这里需要注意的是,豆包在生成按键检测程序时,无论用查询式还是中断方式,都会生成延时函数实现按键消抖。
关于按键消抖:
简单来说,按键消抖是为了避免机械按键物理特性导致的 “虚假触发”,让单片机只识别一次真实的按键操作。一、按键抖动的本质:机械触点的物理特性普通的机械按键内部是金属触点,按下 / 松开时,触点不会瞬间接触 / 断开,而是会因为弹性产生快速、多次的通断震荡(也就是 “抖动”),这个过程持续约5~20ms。用通俗的方式理解:理想状态:按下按键 → 触点 1 次闭合 → 松开 → 触点 1 次断开;实际状态:按下按键 → 触点 “闭合 - 断开 - 闭合 - 断开” 震荡(5~20ms)→ 稳定闭合 → 松开 → 同样震荡 → 稳定断开。
而在中断服务函数中使用延时函数,会出现以下问题:
中断服务函数(ISR)中应尽量避免使用延时函数,我之前在按键中断回调里加HAL_Delay(10)只是为了快速演示消抖逻辑,实际工程中这并不是最优写法。一、为什么中断里用延时函数有问题?HAL_Delay()是基于系统滴答定时器(SysTick)的阻塞式延时函数,在中断里使用它会带来两个核心问题:阻塞中断,导致系统响应变慢 / 卡死中断的核心特性是快速响应、快速退出,而HAL_Delay(10)会让 CPU 在中断里停留 10ms—— 这期间:更高优先级的中断也无法响应(比如定时器 1 的计时中断会被耽误,导致计时不准);主循环的数码管扫描会暂停 10ms,表现为数码管闪烁;若多个中断叠加,甚至会导致系统卡死(SysTick 中断被阻塞,HAL_Delay()无法退出)。2. HAL_Delay()的底层依赖可能失效HAL_Delay()的计时依赖 SysTick 定时器的中断,而 STM32 的中断有优先级:如果 GPIO 按键中断的优先级 ≥ SysTick 中断优先级,那么在按键中断里调用HAL_Delay()时,SysTick 中断无法触发,HAL_Delay()会无限阻塞;即使优先级配置合理,也会破坏 SysTick 的计时精度,影响其他依赖HAL_Delay()的代码。
A*I在生成程序时,有些时候也是自相矛盾的,通过回答来看,就是它明明知道,却还是这么做了。
最后,main.c完成代码如下:
/* Includes ------------------------------------------------------------------*/#include"main.h"#include"tim.h"#include"gpio.h"/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes *//* USER CODE END Includes *//* Private typedef -----------------------------------------------------------*//* USER CODE BEGIN PTD *//* USER CODE END PTD *//* Private define ------------------------------------------------------------*//* USER CODE BEGIN PD *//* USER CODE END PD *//* Private macro -------------------------------------------------------------*//* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */typedef enum {    TIMER_STATE_STOP = 0,    TIMER_STATE_RUNNING} Timer_State;Timer_State g_timer_state = TIMER_STATE_STOP;uint16_t g_count_val = 0;const uint8_t g_seg_code[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/voidSystemClock_Config(void);/* USER CODE BEGIN PFP */voidHAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);voidKey_Process(uint16_t GPIO_Pin);voidDigitTube_Dynamic_Scan(void);/* USER CODE END PFP *//* Private user code ---------------------------------------------------------*//* USER CODE BEGIN 0 *//* USER CODE END 0 *//**  * @brief  The application entry point.  * @retval int  */intmain(void){  /* USER CODE BEGIN 1 */  /* USER CODE END 1 */  HAL_Init();  /* USER CODE BEGIN Init */  /* USER CODE END Init */  SystemClock_Config();  /* USER CODE BEGIN SysInit */  /* USER CODE END SysInit */  MX_GPIO_Init();  MX_TIM1_Init();  /* USER CODE BEGIN 2 */  g_count_val = 0;//  g_timer_state = TIMER_STATE_RUNNING;//  HAL_TIM_Base_Start_IT(&htim1);  /* USER CODE END 2 */  while (1)  {    DigitTube_Dynamic_Scan();    /* USER CODE END WHILE */    /* USER CODE BEGIN 3 */  }  /* USER CODE END 3 */}/**  * @brief System Clock Configuration  * @retval None  */voidSystemClock_Config(void){  RCC_OscInitTypeDef RCC_OscInitStruct = {0};  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;  RCC_OscInitStruct.HSIState = RCC_HSI_ON;  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)  {    Error_Handler();  }  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_DIV2;  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)  {    Error_Handler();  }}/* USER CODE BEGIN 4 */voidHAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){  if (HAL_GPIO_ReadPin(GPIOA, GPIO_Pin) == GPIO_PIN_RESET)  {    Key_Process(GPIO_Pin);  }}voidKey_Process(uint16_t GPIO_Pin){  switch (GPIO_Pin)  {    case GPIO_PIN_3:      if (g_timer_state == TIMER_STATE_STOP)      {        g_timer_state = TIMER_STATE_RUNNING;        HAL_TIM_Base_Start_IT(&htim1);      }      else if (g_timer_state == TIMER_STATE_RUNNING)      {        g_timer_state = TIMER_STATE_STOP;        HAL_TIM_Base_Stop_IT(&htim1);      }      break;    case GPIO_PIN_7:      if (g_timer_state == TIMER_STATE_STOP)      {        g_count_val = 0;      }      break;    default:      break;  }}voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){  if (htim->Instance == TIM1)  {    if (g_timer_state == TIMER_STATE_RUNNING)    {      g_count_val++;      if (g_count_val >= 9999)      {        g_timer_state = TIMER_STATE_STOP;        HAL_TIM_Base_Stop_IT(&htim1);      }    }  }}voidDigitTube_Dynamic_Scan(void){  uint8_t thousand, hundred, ten, unit;  thousand = g_count_val / 1000;  hundred = (g_count_val % 1000) / 100;  ten = (g_count_val % 100) / 10;  unit = g_count_val % 10;  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_SET);  GPIOB->ODR = (GPIOB->ODR & 0xFF00) | g_seg_code[thousand];  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);  HAL_Delay(5);  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_SET);  GPIOB->ODR = (GPIOB->ODR & 0xFF00) | g_seg_code[hundred];  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);  HAL_Delay(5);  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_SET);  GPIOB->ODR = (GPIOB->ODR & 0xFF00) | g_seg_code[ten];  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);  HAL_Delay(5);  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_SET);  GPIOB->ODR = (GPIOB->ODR & 0xFF00) | g_seg_code[unit];  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);  HAL_Delay(5);}/* USER CODE END 4 *//**  * @brief  This function is executed in case of error occurrence.  * @retval None  */voidError_Handler(void){  /* USER CODE BEGIN Error_Handler_Debug */  __disable_irq();  while (1)  {  }  /* USER CODE END Error_Handler_Debug */}#ifdef USE_FULL_ASSERT/**  * @brief  Reports the name of the source file and the source line number  *         where the assert_param error has occurred.  * @param  file: pointer to the source file name  * @param  line: assert_param error line source number  * @retval None  */voidassert_failed(uint8_t *file, uint32_t line){  /* USER CODE BEGIN 6 */  /* USER CODE END 6 */}#endif/* USE_FULL_ASSERT *
END
点击图片链接,查看更多内容

最新文章

随机文章