--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — 第1章:定时器 + 配套代码16~19" author: "尚硅谷研究院" date: 2026-07-15 created: 2026-07-15 --- # SysTick与通用定时器TIM > **用生活理解**:SysTick 就像厨房里的倒计时秒表——设好时间(LOAD 值),它倒数到 0 就"叮"一声(中断)。TIM 通用定时器就像高级可编程定时器——可以预分频(时间压缩/扩展)、自动重装(倒计时到 0 自动重新开始)、还能输出 PWM 或测量外部脉冲。SysTick 是内核自带的"简单秒表",TIM 是片上外设的"多功能定时器"。 --- ## SysTick 系统定时器 SysTick 是 **Cortex-M3 内核自带的 24 位递减计数器**,属于内核外设,与具体芯片厂商无关。**在所有 Cortex-M3 产品间处理方式相同**。 > **参考**:Cortex-M3 权威指南 §8(SysTick 定时器) ### SysTick 寄存器 | 寄存器 | 地址偏移 | 功能 | | -------- | -------- | ----------------------------- | | **CTRL** | 0x00 | 控制与状态寄存器 | | **LOAD** | 0x04 | 重装载数值寄存器(24 位有效) | | **VAL** | 0x08 | 当前数值寄存器 | | CALIB | 0x0C | 校准寄存器(一般不用) | **CTRL 寄存器位说明**: | 位 | 名称 | 值含义 | 说明 | | --- | ------------- | --------------------------- | --------------------------------------------- | | 0 | **ENABLE** | 1=启动, 0=停止 | 计数器使能 | | 1 | **TICKINT** | 1=允许中断, 0=禁止 | 计数到 0 时是否产生 SysTick 异常 | | 2 | **CLKSOURCE** | 1=AHB(72MHz), 0=AHB/8(9MHz) | 时钟源选择 | | 16 | **COUNTFLAG** | 自动置 1 | 计数到 0 时硬件置 1,读后或写 CTRL 时自动清 0 | **LOAD 寄存器**:24 位有效(最大值 16,777,215)。 **VAL 寄存器**:读返回当前计数值;写任意值清 0 并清除 COUNTFLAG。 ### SysTick 的两种用法 | 用法 | 配置 | 特点 | | -------------- | -------------------------- | ---------------------------------- | | **查询延时** | CLKSOURCE=AHB, TICKINT=0 | 轮询 COUNTFLAG 标志,阻塞式 | | **周期性中断** | CLKSOURCE=AHB/8, TICKINT=1 | 产生 1ms 定时中断(FreeRTOS 时基) | ### SysTick 延时实现(查询方式) ```c // 微秒级延时(基于 SysTick 查询,AHB=72MHz) void Delay_us(uint32_t us) { // LOAD: 72MHz 下每 1us 需要计数 72 次 // LOAD 最大 24 位 = 16,777,215,单次最大延时 ≈ 233ms SysTick->LOAD = 72 * us; // VAL: 清当前值(写任何值都清 0,同时清除 COUNTFLAG) SysTick->VAL = 0; // CTRL: ENABLE=1, CLKSOURCE=1(AHB), TICKINT=0(无中断) // 0x05 = 二进制 0000 0101 SysTick->CTRL = 0x05; // 轮询等待 COUNTFLAG 置 1(计数到 0) // SysTick_CTRL_COUNTFLAG = (1UL << 16) while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG)) { } // 关闭定时器(写 0 到 ENABLE 位) SysTick->CTRL &= ~SysTick_CTRL_ENABLE; // &=~: ENABLE位清0 } // 毫秒级延时(累加 1000 次微秒) void Delay_ms(uint32_t ms) { while (ms--) { Delay_us(1000); // 1ms = 1000us } } void Delay_s(uint32_t s) { while (s--) { Delay_ms(1000); } } ``` --- ## 通用定时器 TIM STM32F103ZET6 有 7 个定时器: | 定时器 | 类型 | 位数 | 主要特性 | | ---------- | ---- | ---- | ----------------------------------------------- | | **TIM1** | 高级 | 16 | 带互补 PWM 输出、死区插入、刹车功能(电机控制) | | **TIM2~5** | 通用 | 16 | 时基、PWM、捕获/比较(最常用) | | **TIM6~7** | 基本 | 16 | 仅时基功能,无外部 I/O | ### TIM 时基单元结构 ``` APBx 时钟 → PSC(预分频器) → CK_CNT → CNT(计数器) → ARR(自动重装) ↓ ↓ 控制计数器时钟 溢出/更新事件(UEV) ``` **关键寄存器**: | 寄存器 | 说明 | 范围 | | --------------------- | ----------------------------------------------------------------------------- | ---------------- | | **PSC** (预分频器) | 对时钟分频,产生 CK_CNT | 16 位(0~65535) | | **CNT** (计数器) | 当前计数值 | 16 位(0~65535) | | **ARR** (自动重装载) | 计数到该值时产生溢出 | 16 位(0~65535) | | **CR1** (控制寄存器1) | CEN(使能)、UDIS(更新禁止)、URS(更新请求源)、OPM(单脉冲)、ARPE(自动重装预装载) | — | | **DIER** (中断使能) | UIE(更新中断)、CCxIE(捕获比较中断) | — | | **SR** (状态寄存器) | UIF(更新标志)、CCxIF(捕获比较标志) | — | ### 计数器时钟频率计算 ``` CK_CNT = CK_PSC / (PSC + 1) ``` 其中: - CK_PSC = APB1 时钟(TIM2~7)或 APB2 时钟(TIM1) - **注意**:如果 APB1 分频系数 ≠ 1,则 TIM2~7 的时钟 = APB1×2 **示例**(APB1=36MHz,分频=2 → TIM 时钟=72MHz): ``` PSC = 7199 → CK_CNT = 72MHz / 7200 = 10KHz (0.1ms/计数) PSC = 71999 → CK_CNT = 72MHz / 72000 = 1KHz (1ms/计数) ``` ### 溢出时间公式 ``` Tout = (ARR + 1) × (PSC + 1) / CK_PSC CK_PSC = TIM 时钟频率(通常是 72MHz) 所以: Tout = (ARR + 1) × (PSC + 1) / 72,000,000 ``` | ARR | PSC | 定时时长 | 分辨率 | | ----- | ----- | ------------------------ | ------ | | 999 | 7199 | 1000×7200/72M = 100ms | 0.1ms | | 9999 | 7199 | 10000×7200/72M = 1s | 0.1ms | | 999 | 71999 | 1000×72000/72M = 1s | 1ms | | 65535 | 65535 | 65536×65536/72M = ~59.6s | 0.91μs | > **参考**:参考手册 §14(通用定时器寄存器描述) ### CR1 寄存器关键位 | 位 | 名称 | 功能说明 | | --- | -------- | ---------------------------------------------------------------------- | | 0 | **CEN** | 计数器使能(0=停止,1=启动) | | 4 | **DIR** | 计数方向(0=向上计数,1=向下计数) 仅 TIM1/8 等可配 | | 7 | **ARPE** | Auto-Reload Preload Enable(0=ARR立即更新,1=ARR在下次更新事件时更新) | | 5 | **OPM** | 单脉冲模式(1=计数到 0 后自动停止) | --- ## 实验:SysTick/TIM 定时控制 LED 闪烁 ### 软件设计(SysTick 中断版) **项目路径**:`stm32/16_systick_led_twinkle_register` **文件:`stm32/16_systick_led_twinkle_register/User/systick.h`** ```c #ifndef __SYSTICK_H #define __SYSTICK_H #include "stm32f10x.h" #include "led.h" void Systick_Init(void); #endif ``` **文件:`stm32/16_systick_led_twinkle_register/User/systick.c`** ```c #include "systick.h" void Systick_Init(void) { // 1. 设置初始装载的值,1ms 产生一次中断,72000 时钟周期 SysTick->LOAD = 72000 - 1; // 2. 设置时钟源 SysTick->CTRL |= SysTick_CTRL_CLKSOURCE; // 3. 使能中断 SysTick->CTRL |= SysTick_CTRL_TICKINT; // 4. 启动定时器 SysTick->CTRL |= SysTick_CTRL_ENABLE; } // 全局变量作为中断计数 uint16_t count = 0; void SysTick_Handler(void) { count++; // 计数到 1000 次中断,即是1s,翻转LED1 if (count == 1000) { LED_Toggle(LED1); count = 0; } } ``` ### 软件设计(SysTick HAL 库版) **项目路径**:`stm32/17_led_twinkle_systick_hal` SysTick HAL 版不需要额外配置 SysTick 寄存器,CubeMX 生成的 `HAL_Init()` 会自动配置 SysTick 为 1ms 中断,直接利用 `HAL_IncTick()` 和 `uwTick` 全局变量计数即可。 **文件:`stm32/17_led_twinkle_systick_hal/Core/Src/stm32f1xx_it.c`**(SysTick_Handler 部分) ```c void SysTick_Handler(void) { HAL_IncTick(); if (uwTick % 1000 == 0) { HAL_GPIO_TogglePin(GPIOA, LED1_Pin); } } ``` **文件:`stm32/17_led_twinkle_systick_hal/Core/Src/main.c`** ```c #include "main.h" #include "gpio.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while (1) { } } ``` **文件:`stm32/17_led_twinkle_systick_hal/Core/Src/gpio.c`**(MX_GPIO_Init 部分) ```c void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); GPIO_InitStruct.Pin = LED1_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct); } ``` ### 软件设计(TIM 中断版) **项目路径**:`stm32/18_tim_led_twinkle_register` **文件:`stm32/18_tim_led_twinkle_register/Hardware/TIM/tim6.h`** ```c #ifndef __TIM6_H #define __TIM6_H #include "stm32f10x.h" #include "led.h" void TIM6_Init(void); #endif ``` **文件:`stm32/18_tim_led_twinkle_register/Hardware/TIM/tim6.c`** ```c #include "tim6.h" void TIM6_Init(void) { // 1. 使能定时器 RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // 2. 设置预分频值 7199,表示7200分频得到10000Hz(100us) TIM6->PSC = 7199; // 3. 设置自动装载值 99,表示 100 次计数得到 1s TIM6->ARR = 9999; // 4. 打开更新中断使能 TIM6->DIER |= TIM_DIER_UIE; // 5. NVIC配置 NVIC_SetPriorityGrouping(3); NVIC_SetPriority(TIM6_IRQn, 2); NVIC_EnableIRQ(TIM6_IRQn); // 6. 使能计数器 TIM6->CR1 |= TIM_CR1_CEN; } // 中断服务函数 void TIM6_IRQHandler(void) { // 清除中断标志位 TIM6->SR &= ~TIM_SR_UIF; // 每1s翻转一次LED2 LED_Toggle(LED2); } ``` **文件:`stm32/18_tim_led_twinkle_register/User/main.c`** ```c #include "usart.h" #include "delay.h" #include "led.h" #include "tim6.h" int main(void) { // 初始化 USART_Init(); LED_Init(); TIM6_Init(); printf("Hello world!\n"); while (1) { } } ``` ### 软件设计(TIM HAL 库版) **项目路径**:`stm32/19_led_twinkle_tim_hal` **文件:`stm32/19_led_twinkle_tim_hal/Core/Src/tim.c`**(MX_TIM6_Init) ```c TIM_HandleTypeDef htim6; void MX_TIM6_Init(void) { htim6.Instance = TIM6; htim6.Init.Prescaler = 7199; htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 9999; htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim6) != HAL_OK) { Error_Handler(); } } void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle) { if(tim_baseHandle->Instance==TIM6) { __HAL_RCC_TIM6_CLK_ENABLE(); HAL_NVIC_SetPriority(TIM6_IRQn, 2, 0); HAL_NVIC_EnableIRQ(TIM6_IRQn); } } ``` **文件:`stm32/19_led_twinkle_tim_hal/Core/Src/stm32f1xx_it.c`**(中断和回调) ```c // TIM6 中断入口 void TIM6_IRQHandler(void) { HAL_TIM_IRQHandler(&htim6); } // 用户回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { HAL_GPIO_TogglePin(GPIOA, LED2_Pin); } } ``` **文件:`stm32/19_led_twinkle_tim_hal/Core/Src/main.c`** ```c #include "main.h" #include "tim.h" #include "gpio.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM6_Init(); HAL_TIM_Base_Start_IT(&htim6); while (1) { } } ``` --- ## HAL 库版定时器 HAL 库使用 `TIM_HandleTypeDef` 管理定时器,CubeMX 生成初始化代码后通过宏或 API 控制。 ```c // CubeMX 生成: tim.c 中的 MX_TIMx_Init() TIM_HandleTypeDef htim6; void MX_TIM6_Init(void) { htim6.Instance = TIM6; htim6.Init.Prescaler = 7199; // PSC htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 9999; // ARR → (PSC+1)*(ARR+1)/72M = 7200*10000/72M = 1s htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; HAL_TIM_Base_Init(&htim6); } // HAL 库启动定时器中断 HAL_TIM_Base_Start_IT(&htim6); // 不使用中断: HAL_TIM_Base_Start(&htim6); // HAL 库定时器中断回调(用户重写此函数) void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { static uint16_t tick = 0; if (++tick >= 1000) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); tick = 0; } } } ``` ## SysTick vs TIM 对比 | 对比项 | SysTick | TIM 通用定时器 | | ---------- | ------------------------- | ------------------------- | | **属于** | Cortex-M3 内核 | 芯片厂家外设 | | **位数** | 24 位 | 16 位 | | **预分频** | 固定 AHB 或 AHB/8 | 可编程 PSC (0~65535) | | **多通道** | 无 | 4 通道(捕获/比较/PWM) | | **中断** | 仅更新中断 | 更新 + 多通道捕获比较中断 | | **精度** | 一般 | 高 | | **用途** | 时基、裸机延时、RTOS 心跳 | PWM、捕获、精确计时 | | **数量** | 1 个(全芯片唯一) | 7 个(多定时器可并行) | --- ## 核心速查表 | 操作 | SysTick | TIM 通用定时器 | | ---------- | ----------------------- | -------------------- | | 开启时钟 | 无需开启(内核自带) | `RCC->APB1ENR | = TIMxEN` | | 配置预分频 | 无(AHB 或 AHB/8 固定) | `TIMx->PSC = value` | | 配置重装载 | `SysTick->LOAD = value` | `TIMx->ARR = value` | | 计数方向 | 只减 | 可增可减 | | 使能中断 | `CTRL | = TICKINT` | `DIER | = UIE`+`NVIC_EnableIRQ()` | | 启动 | `CTRL | = ENABLE` | `CR1 | = CEN` | | 停止 | `CTRL &= ~ENABLE` | `CR1 &= ~CEN` | | 清中断标志 | 自动(读 CTRL) | `SR &= ~UIF`(写 0) | | 当前计数值 | 读 `VAL` | 读 `CNT` | ## 常见问题与避坑 1. **SysTick 中断不触发** → 检查 CTRL.TICKINT 是否置 1、NVIC 优先级配置(SysTick 属于异常,优先级在 SHPR3 寄存器) 2. **TIM 定时时间不对** → 检查 APB1 分频系数:如果 APB1 预分频 ≠1,则 TIM 时钟 = APB1×2(不要直接按 APB1 频率算 CK_PSC) 3. **中断只进一次** → ISR 中忘了清除 SR.UIF 标志(SR 是写 0 清除) 4. **修改 ARR 后定时时间不变** → CR1.ARPE=0(ARR 立即更新)或 ARPE=1(等待下次更新事件) 5. **TIM 与 SysTick 中断冲突** → 注意优先级配置,SysTick 常用最低优先级 6. **Delay_us 最大延时限制** → LOAD 最大 0xFFFFFF(16,777,215),72MHz 下单次最大 ≈ 233ms。要更长时间需循环累积