
本例子使用硬件定时器实现精确计时(每100ms计数一次,计时精度为0.1s),使用4位数码管(动态扫描算法)实现秒表功能,通过按键控制秒表启停和复位。通过简单的例子学习硬件定时器计时功能使用(定时中断)、继续使用GPIO中断实现两个按键检测。
/* 根据当前模式执行对应流水灯函数 */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;}

答案是不相等,在LED_Waterfall()函数中增加了 LED_All_Off()函数和if (i > LED0)...条件判断,这里想说的是,在单片机主循环程序中,由于程序逻辑不同,每一次程序循环用时不尽相同,但是这个时间的差异,大多数情况是可以接受的,比如之前在实现LED流水灯切换时,并不影响程序的执行结果。从视觉观感上,ms级别的时间差,也不影响视觉感受。
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 同步输出(适合三相电机控制)。
元件清单:
| 序号 | 名称 | 型号 | 数量 | Lib名称 |
| 1 | 单片机 | stm32f103c8 | 1 | STM32F103C8 |
| 2 | 数码管驱动芯片 | 74hc245 | 1 | 74hc245 |
| 3 | 共阴数码管 | 7SEG-MPX4-CC | 1 | 7SEG-MPX4-CC |
| 4 | 按钮 | button | 2 | button |

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










使用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()的代码。
/* 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 *


