08-SysTick与通用定时器TIM.md 13 KB


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 延时实现(查询方式)

// 微秒级延时(基于 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

#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

#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 部分)

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

#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 部分)

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

#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

#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

#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)

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(中断和回调)

// 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

#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 控制。

// 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
配置预分频 无(AHB 或 AHB/8 固定) TIMx->PSC = value
配置重装载 SysTick->LOAD = value TIMx->ARR = value
计数方向 只减 可增可减
使能中断 `CTRL = TICKINT`
启动 `CTRL = ENABLE`
停止 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。要更长时间需循环累积