摘要:本文系统讲解 STM32 定时器与 PWM 的实战应用。文章从时钟信号基础入手,依次介绍 SysTick 系统滴答定时器的原理与配置、TIM 通用定时器的核心组件与定时算法,再到 PWM 脉冲宽度调制的核心参数与生成原理,并通过 LED 闪烁、蜂鸣器控制、串口重定向等实验演示具体用法。随后讲解 SG90 舵机的硬件参数、控制协议与驱动实验,最后介绍硬件定时器通道不足时使用基本定时器中断模拟 PWM 的高级技巧,并给出注意事项。

1. 时钟信号基础

时钟是单片机的心脏,决定了系统的运行节奏。

  • 频率 (f):时钟信号在一秒钟内的重复次数。
  • 单位:赫兹 (Hz)
  • 公式:f = 1/T
  • 周期 (T):一个时钟信号持续的时间。
  • 单位:秒 (s)
  • 公式:T = 1/f

2. SysTick 系统滴答定时器

2.1 简介与特性

  • 定义:Cortex-M 系列内核集成的 24 位递减计数器(异常编号 15)。
  • 作用:
    • 裸机开发:用于产生精准延时。
    • 操作系统:作为时基单元(Time Base),维持系统心跳(如 FreeRTOS、uCOS)。
  • 时钟源:
    1. 内部时钟:FCLK(自由运行时钟,通常等于系统主频 HCLK)。
    2. 外部时钟:STCLK(通常为 HCLK 的 8 分频)。

2.2 工作原理

  1. 设定初值(重装载值)并使能。
  2. 每来一个时钟脉冲,计数值减 1。
  3. 计数减到 0 时,触发异常(中断),计数器自动重装初值并继续循环。
  4. 最大计数值:2^24 = 16,777,216。

2.3 定时计算与配置实例

计算公式:

定时时间 = (ARR + 1) × (PSC + 1) / 输入时钟频率

(注:SysTick 通常只有重装载值,无独立 PSC 寄存器,若使用外部 8 分频源需考虑分频系数)

表格

输入频率目标时间计算过程配置建议
32 kHz200 ms32000×0.2=6400计数值:6400
160 MHz2 s160,000,000×2=320,000,000(溢出)需配合分频器使用

2.4 STM32F411 默认行为分析

  • 默认配置:
  • 输入频率:16 MHz
  • 重载值 (Reload Value):16,000 - 1

  • 中断状态:默认开启
  • 结果推导:

T=16000/16,000,000=0.001s=1ms

  • HAL库机制:
  • SysTick每1ms触发一次中断,全局变量 uwTick 自增。
  • HAL_Delay(ms) 函数通过轮询 uwTick 的差值来实现延时。

2.5 ⚠️ HAL_Delay 在中断中卡死问题

  • 现象:在外部中断服务函数中调用 HAL_Delay,程序卡死。
  • 原因(死锁):
    1. HAL_Delay 等待 uwTick 增加。
    2. uwTick 增加依赖于 SysTick 中断。
    3. SysTick 优先级通常最低。
    4. 当前外部中断正在执行,阻塞了低优先级的 SysTick 中断,导致 uwTick 永远不增加,死循环等待。
  • 解决方案:
    • 推荐:中断服务函数应“快进快出”,严禁在中断中使用延时函数。
    • 不推荐:提高 SysTick 的抢占优先级(会破坏系统实时性逻辑)。


3. TIM 通用定时器

分为基本定时器、通用定时器、高级定时器

定时器类似于我们生活中的闹钟,可以设定一个时间来提醒我们。定时器可以作为基本的定时器/计数器,

用于生成周期性的定时中断或作为计数器来计数外部事件的脉冲数。例如1秒钟进入定时器中断干一件事,定时器可以配置为产生脉冲宽度调制(PWM)信号,用于控制电机速度、LED亮度调节、风扇风速调节等应用。

3.1 核心组件

定时器类似于可编程闹钟,主要包含三个关键寄存器:

预分频器 (PSC):对输入时钟进行分频。

自动重装寄存器 (ARR):决定计数的周期(决定频率)。

计数器 (CNT):实际进行加减计数的寄存器。

3.2 定时算法训练

公式: fout=fclk(PSC+1)(ARR+1)

输入频率目标时间/频率PSC 配置ARR 配置备注
10 kHz计时 5s10-150000-1需较大ARR
55 MHz计时 5s55000-15000-1先分频至1kHz
5 MHz计时 14ms500-1140-1/
45 MHz计时 127ms45000-1127-1/
24 kHz计时 12s24-112000-1/
33 MHz计时 5s33000-15000-1/

3.3 实验:LED闪烁 (1s间隔)

实验要求:LED4每隔1s切换一次状态

1、打开一个定时器

2、配置定时器产生1ms的定时周期

3、在NVIC中打开中断

4、打开LED4引脚为OUTPUT

5、开启定时器

6、重写回调函数,在定时器回调函数中切换LED4的状态

TIM1对应APB2总线

配置思路 (16MHz输入):

  • 目标:1s (1000ms)
  • PSC: 16000-1 →→ 分频后频率 1kHz (1ms/次)
  • ARR: 1000-1 →→ 计数1000次 = 1s

MX配置:

    代码逻辑:

    // 1. 开启定时器中断
    HAL_TIM_Base_Start_IT(&htim1);
    // 2. 重写回调函数
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    if (htim->Instance == TIM1) {
    // 切换LED状态
    HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
    }
    }

    4. PWM 脉冲宽度调制

    PWM,全称为脉冲宽度调制(Pulse Width Modulation),是一种调节信号的方法。简单来说,PWM 就是通过改变信号的高电平和低电平的时间比例来控制输出的平均电压或功率。

    4.1 核心参数

    • 周期 ( T): T=1/f。如果频率为50Hz ,也就是说一个周期是20ms,那么一秒钟就有50次PWM周期。-->1000 ms / 50 = 20 ms
    • 占空比 (Duty Cycle):高电平时间占整个周期的比例 (0%∼100% )。
    • 分辨率:由 ARR 决定。精度越高,ARR 越大。

    4.2PWM生成原理

    PWM的频率就是在输入频率的基础上进行了两次分频

    一次PSC 另一次ARR

    4.2 蜂鸣器控制实验

    • 需求:16MHz输入,PWM10kHz频率,1%精度,占空比 0。
    • 计算:
      • 频率 10kHz →→ 周期 0.1ms。
      • 1%精度 →→ 将周期分为100份。
      • 16,000,000/10,000=1600(总分频数)。
      • PSC: 16-1 (分频后1MHz)。
      • ARR: 100-1 (1MHz / 100 = 10kHz)。
      • CCR: 控制占空比 (0~100)。

    4.3 串口重定向 (printf)

    为了调试方便,通常重定向 fputc 到 USART1(注意添加头文件:#include "stdio.h"):

    int fputc(int ch, FILE *p)
    {
        while(!(USART1->SR & (1<<7))); // 等待发送缓冲区空 (TXE)
        USART1->DR = ch;
        return ch;
    }

    4.4PWM编程

    打开定时器PWM通道

    HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);

    按键检测调整占空比

    if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
    {
            HAL_Delay(20);
            while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0);
            HAL_Delay(20);
    }
    

    5. SG90 舵机控制

    5.1 硬件参数

    • 电压:4.8V - 6V。
    • 信号线:橙色线,接收PWM信号。
    • 角度范围:0° ~ 180°。

    工作电压 4.8V - 6V(典型5V)

    电流消耗 100mA(空闲),250mA(运行)

    扭矩 1.2kg·cm(4.8V),1.6kg·cm(6V)

    转速 0.12秒/60°(4.8V),0.10秒/60°(6V)

    重量 9克

    尺寸 22.2mm × 11.8mm × 31mm

    转动角度 180°(部分型号支持360°)

    齿轮材质 塑料(部分型号为金属齿轮)

    接口类型 3针(信号、电源、地)

    5.2 控制协议

    舵机通过高电平持续时间来控制角度,标准周期为 20ms (50Hz)。

    高电平脉宽对应角度备注
    0.5 ms0°最小脉宽
    1.5 ms90°中位
    2.5 ms180°最大脉宽
    • 脉冲宽度:0.5ms - 2.5ms,对应角度0° - 180°。

    控制舵机的信号是周期为20ms的PWM,一个周期的高电平的范围是0.5~2.5ms,其他时间为低电平。

    我们可以用另一种方法来看:前2.5ms是可变化的高低电平,后17.5ms是固定的低电平,只需要控制前2.5ms的电平变化即可。

    例如:

    需要输出0.5ms高电平,那么就是0.5ms高电平 + 2.0ms低电平 + 17.5ms固定低电平;

    需要输出1.0ms高电平,那么就是1.0ms高电平 + 2.5ms低电平 + 17.5ms固定低电平;

    如此类推,实际我们只需要控制前2.5ms的高低电平分配,其他时间都保持低电平即可。

    只要改变占空比就能让舵机转动,0~180°

    注意:180°舵机与360°舵机底层原理并不相同,所以编程时我们应注意舵机种类。

    5.3舵机控制实验

    保证PWM周期的时长为20ms 频率50HZ--->定时器的定时时间为20ms

    16Mhz的输入频率,预分频 1600-1 ARR:200-1

    16Mhz-->50HZ

    高电平时长调节的最小单位可达 0.1ms 可实现二十档,满足普通实验需求。

    int main(void)
    {
      /* USER CODE BEGIN 1 */
      /* USER CODE END 1 */
      /* MCU Configuration--------------------------------------------------------*/
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
      /* USER CODE BEGIN Init */
      /* USER CODE END Init */
      /* Configure the system clock */
      SystemClock_Config();
      /* USER CODE BEGIN SysInit */
      /* USER CODE END SysInit */
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_TIM5_Init();
      MX_USART1_UART_Init();
      /* USER CODE BEGIN 2 */
      HAL_TIM_PWM_Start(&htim5,TIM_CHANNEL_1);
      int angle=5;
      /* USER CODE END 2 */
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
        if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_2)==0)
        {
          HAL_Delay(20);
          while(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_2)==0);
          angle+=1;
          if(angle > 25)
          {
            angle=25;
          }
          __HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, angle);
          HAL_Delay(20);
        }
      }
      /* USER CODE END 3 */
    }

    6. 软件模拟 PWM (高级技巧)

    当硬件定时器通道不足时,可使用基本定时器中断模拟PWM。

    外部晶振使用

    因为最小步距要求是1%调节,而单个周期的长度为1ms,定时时间不能高于最小步距10us要求,但是可以更小。

    以此为例,定时器进行模拟时,最大的定时时间为10us,最小定时时间可以比10us低,比如调整1us

    此处我们将定时器设置为1us的定时时间

    基于当前要求,10us的定时时间可实现1%的调节精度

    当前选用TIM10基本定时器实现定时,可知其输入频率为25Mhz

    计时10us PSC=1 ARR 25

    ⚠️ 注意事项:软件模拟PWM非常消耗CPU资源。如果定时时间过短(如1us),会导致主程序无法运行。建议模拟PWM的频率不要太高(周期建议在100us以上)。

    定时器10会每隔10us触发一次中断回调

    按键使能

    主程序打开中断

            HAL_TIM_Base_Start_IT(&htim10);
    int duty_beep=0;
    int BEEP_DUTY=30;
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
            if(htim == &htim10)
            {
        }
    }
    //按键消抖
               if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_2)==0)
                {
                        HAL_Delay(20);
                        while(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_2)==0);
                        HAL_Delay(20);
                }

    整体架构

    /* USER CODE BEGIN 0 */
    int duty_beep=0;
    int BEEP_DUTY=30;
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
      if(htim == &htim10)
      {
        duty_beep++;
        if(duty_beep >= 100)
          duty_beep=0;
        if(duty_beep < BEEP_DUTY)
          HAL_GPIO_WritePin(GPIOC,GPIO_PIN_6,1);
        else
          HAL_GPIO_WritePin(GPIOC,GPIO_PIN_6,0);
      }
    }
    /* USER CODE END 0 */
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
      /* USER CODE END 1 */
      /* MCU Configuration--------------------------------------------------------*/
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
      /* USER CODE BEGIN Init */
      /* USER CODE END Init */
      /* Configure the system clock */
      SystemClock_Config();
      /* USER CODE BEGIN SysInit */
      /* USER CODE END SysInit */
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_TIM10_Init();
      MX_USART1_UART_Init();
      /* USER CODE BEGIN 2 */
      HAL_TIM_Base_Start_IT(&htim10);
      /* USER CODE END 2 */
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
        if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
        {
          HAL_Delay(20);
          while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0);
          BEEP_DUTY+=10;
          if(BEEP_DUTY> 100)
          {
            BEEP_DUTY=100;
          }
          HAL_Delay(20);
        }
      }
      /* USER CODE END 3 */
    }

    Logo

    openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

    更多推荐