4. SPI总线协议核心原理

定时器,说白了就是STM32的“心跳节拍器”。我刚开始学STM32那会儿,总觉得定时器就是个计数的玩意儿,没啥技术含量。直到有一次做电机控制项目,PWM频率抖了一下,电机直接啸叫起来……嗯,从那以后我再也不敢小看定时器了。

这一章,咱们把STM32的定时器家族捋一遍,重点搞定基本定时器和PWM输出。你想想看,无论是LED呼吸灯、舵机控制,还是后面I2C/SPI通信中的时钟生成,都离不开定时器。

4.1 STM32定时器资源概览

STM32的定时器分三类:基本定时器、通用定时器、高级定时器。它们的关系,我用一张图给你讲清楚。

核心观点:三类定时器是“层层叠加”的关系——高级定时器包含通用定时器的全部功能,通用定时器又包含基本定时器的全部功能。

STM32定时器家族结构图 基本定时器(TIM6/TIM7) 16位向上计数器 | 自动重装载 | 产生中断/DMA 无外部引脚 | 无PWM | 无输入捕获 通用定时器(TIM2~TIM5) 16/32位计数器 | 向上/向下/中央对齐计数 4路独立PWM | 输入捕获 | 输出比较 | 编码器接口 高级定时器(TIM1/TIM8) 含通用定时器全部功能 + 互补PWM + 死区插入 + 刹车功能 F1系列共14个定时器 F4系列最多17个

我个人习惯这样记:基本定时器就是“纯计时器”,通用定时器是“多功能瑞士军刀”,高级定时器是“电机控制专用核弹”。

特性 基本定时器 通用定时器 高级定时器
计数器位数 16位 16/32位 16位
计数模式 仅向上 向上/向下/中央对齐 向上/向下/中央对齐
PWM通道数 0 4 4(含互补输出)
输入捕获 无 有 有
死区插入 无 无 有
典型应用 定时中断、时基 PWM、脉冲测量 电机控制、逆变器

选型建议:做I2C/SPI通信的时钟延时,用基本定时器就够了。做LED呼吸灯或舵机控制,用通用定时器。做无刷电机FOC控制,才需要上高级定时器。别杀鸡用牛刀。

4.2 基本定时器配置实战

基本定时器,说白了就是一个“自动加1的计数器”。它从0开始,每来一个时钟脉冲就加1,加到预设值就归零,同时触发中断。我在项目中常用它来做“精确延时”和“周期性任务调度”。

4.2.1 基本定时器的工作原理

核心公式就一个:

定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率

举个例子:STM32F103的APB1时钟是36MHz,我想产生1ms的定时中断。

// 目标:1ms中断一次
// 时钟:36MHz = 36,000,000 Hz
// 预分频:36000-1 → 计数频率 = 36MHz / 36000 = 1KHz = 1ms
// 自动重装载:1000-1 → 计数1000次 = 1ms

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

TIM_TimeBaseStructure.TIM_Prescaler = 36000 - 1;     // 预分频
TIM_TimeBaseStructure.TIM_Period = 1000 - 1;          // 自动重装载
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);

TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);            // 使能更新中断
TIM_Cmd(TIM6, ENABLE);                                // 启动定时器

我曾经踩过的坑:预分频值和自动重装载值都要减1!因为寄存器是从0开始计数的。我第一次写的时候忘了减1,结果定时时间整整多了一个时钟周期,排查了半小时才找到原因。

4.2.2 CubeMX配置步骤

用CubeMX配置基本定时器,其实就三步:

  1. 选择定时器:在Pinout视图的Timers里勾选TIM6或TIM7
  2. 设置参数:配置Prescaler和Counter Period
  3. 使能中断:在NVIC Settings里勾选TIM6 global interrupt

我个人习惯把预分频值设成“时钟频率/10000”,这样计数频率就是10KHz,自动重装载值直接写“定时时间(ms)×10”,算起来特别方便。

4.3 PWM输出配置与占空比调节

PWM,说白了就是“快速开关信号”。通过调节高电平占一个周期的时间比例(占空比),就能控制LED亮度、电机转速、舵机角度。我做过一个智能灯项目,用PWM调光,效果比模拟调压好太多了。

4.3.1 PWM的工作原理

通用定时器的PWM模式,核心是“比较匹配”机制:

  • 计数器从0开始向上计数
  • 当计数值 < 比较值(CCR)时,输出高电平
  • 当计数值 ≥ 比较值(CCR)时,输出低电平
  • 计数值达到自动重装载值(ARR)时,归零重新开始

你想想看,这不就是一个“锯齿波+阈值比较”的过程吗?

PWM生成原理示意图 CCR比较值 高电平 低电平 一个PWM周期(ARR+1个计数时钟) 占空比 = 高电平时间 / 周期 = CCR / (ARR + 1) 频率 = 时钟 / (PSC+1) / (ARR+1)

4.3.2 CubeMX配置PWM输出

以TIM2的通道1(PA0)输出50Hz、50%占空比的PWM为例:

  1. 选择定时器通道:在Pinout视图里找到TIM2,Channel1选PWM Generation CH1
  2. 配置参数:
    • Prescaler: 720-1(72MHz/720=100KHz)
    • Counter Period: 2000-1(100KHz/2000=50Hz)
    • Pulse: 1000(50%占空比)
  3. 生成代码:CubeMX会自动生成初始化代码

生成的HAL代码长这样:

// 初始化代码(CubeMX自动生成)
TIM_HandleTypeDef htim2;

void MX_TIM2_Init(void)
{
    TIM_OC_InitTypeDef sConfigOC = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 720 - 1;
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 2000 - 1;
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim2);

    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 1000;          // 初始占空比50%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}

// 启动PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

4.3.3 动态调节占空比

运行时调节占空比,其实就是修改CCR寄存器的值。HAL库提供了现成的函数:

// 设置占空比为75%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500);

// 或者用宏定义更清晰
#define PWM_PERIOD  2000
#define SET_DUTY(x) __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (x))

// 呼吸灯效果示例
void breath_led(void)
{
    for(int duty = 0; duty < PWM_PERIOD; duty += 10)
    {
        SET_DUTY(duty);
        HAL_Delay(5);
    }
    for(int duty = PWM_PERIOD; duty > 0; duty -= 10)
    {
        SET_DUTY(duty);
        HAL_Delay(5);
    }
}

经验之谈:调节占空比时,注意不要超过ARR值。我曾经在调试时不小心把CCR设成了2500(ARR是1999),结果PWM波形直接乱掉了。建议加个限幅:duty = (duty > PWM_PERIOD) ? PWM_PERIOD : duty;

4.4 常见问题与避坑指南

问题1:PWM输出没波形

  • 检查GPIO是否配置为复用功能(AF)
  • 检查定时器时钟是否使能
  • 检查是否调用了HAL_TIM_PWM_Start()

问题2:定时时间不准

  • 确认系统时钟配置是否正确
  • 预分频值和自动重装载值是否减1
  • 中断优先级是否被更高优先级中断抢占

问题3:占空比调节不线性

  • 检查PWM模式是PWM1还是PWM2(两者极性相反)
  • 确认OCPolarity设置是否正确
  • 如果负载是LED,人眼对亮度感知是对数的,需要做Gamma校正

嗯,定时器这块内容就这么多。说白了就是三个参数:预分频器决定计数速度,自动重装载决定周期,比较值决定占空比。掌握了这三个旋钮,你就能玩转STM32的定时器了。