【江科大STM32学习笔记-06】TIM 定时器 - 6.2 定时器的输出比较功能

前言

第六章 TIM 定时器主要包含以下四个部分:

  • **第一部分:**定时器基本定时功能(已学习)
  • **第二部分:**定时器输出比较功能(本篇内容)
  • **第三部分:**定时器输入捕获功能
  • **第四部分:**定时器编码器接口功能

其中,输出比较是本章的核心内容之一,因为它是 STM32 产生 PWM 波形的基础。几乎所有涉及"功率控制"或"模拟量控制"的应用,背后都离不开 PWM 技术。因此,掌握输出比较功能,是使用 STM32 进行电机控制、机器人开发等项目的基础。


目录

前言

[1 输出比较模式](#1 输出比较模式)

[1.1 简介](#1.1 简介)

[1.2 工作原理](#1.2 工作原理)

[第一阶段:时基单元运行(Time Base)------ 决定波形的频率](#第一阶段:时基单元运行(Time Base)—— 决定波形的频率)

[第二阶段:比较单元判定(Comparator)------ 决定波形的占空比](#第二阶段:比较单元判定(Comparator)—— 决定波形的占空比)

[第三阶段:输出模式控制(Mode Controller)------ 生成中间参考信号 OCxREF](#第三阶段:输出模式控制(Mode Controller)—— 生成中间参考信号 OCxREF)

[第四阶段:输出控制电路(Output Stage)------ 极性选择与引脚驱动](#第四阶段:输出控制电路(Output Stage)—— 极性选择与引脚驱动)

总结

[1.3 输出比较的8种模式](#1.3 输出比较的8种模式)

[1.4 关键寄存器介绍](#1.4 关键寄存器介绍)

[3 PWM 简介](#3 PWM 简介)

[3.1 什么是PWM?](#3.1 什么是PWM?)

[3.2 PWM 波形的形成机制(STM32 实现原理)](#3.2 PWM 波形的形成机制(STM32 实现原理))

[3.3 PWM 的关键参数与计算](#3.3 PWM 的关键参数与计算)

[3.3.1 频率 (Frequency)](#3.3.1 频率 (Frequency))

[3.3.2 占空比 (Duty Cycle)](#3.3.2 占空比 (Duty Cycle))

[3.3.3 分辨率 (Resolution)](#3.3.3 分辨率 (Resolution))

[3.3.4 参数配置实例:确定相关寄存器的值](#3.3.4 参数配置实例:确定相关寄存器的值)

[3.4 PWM在惯性系统中的应用](#3.4 PWM在惯性系统中的应用)

[4 PWM 输出的配置与实现](#4 PWM 输出的配置与实现)

[4.1 参数选型:锁定寄存器配置值](#4.1 参数选型:锁定寄存器配置值)

[4.2 PWM初始化流程](#4.2 PWM初始化流程)

[4.2.1 开启时钟](#4.2.1 开启时钟)

[4.2.2 配置 GPIO (复用推挽输出)](#4.2.2 配置 GPIO (复用推挽输出))

[4.2.3 配置时基单元](#4.2.3 配置时基单元)

[4.2.4 配置输出比较单元](#4.2.4 配置输出比较单元)

[4.2.5 启动定时器](#4.2.5 启动定时器)

[4.3 动态调整占空比(实现呼吸灯)](#4.3 动态调整占空比(实现呼吸灯))

[4.3.1 预装载与影子寄存器机制](#4.3.1 预装载与影子寄存器机制)

[4.4 PWM引脚重映射(Remap)](#4.4 PWM引脚重映射(Remap))

[4.4.1 映射方案选择与引脚分析](#4.4.1 映射方案选择与引脚分析)

[4.4.2 资源冲突深度解析:PA15 与 JTAG](#4.4.2 资源冲突深度解析:PA15 与 JTAG)

[4.4.3 重映射配置步骤](#4.4.3 重映射配置步骤)

[4.4.4 完整代码示例](#4.4.4 完整代码示例)

[4.4.5 配置要点与排错指南](#4.4.5 配置要点与排错指南)

[5 相关元器件简介](#5 相关元器件简介)

[5.1 SG90 9g 舵机(伺服电机)](#5.1 SG90 9g 舵机(伺服电机))

[5.1.1 简介](#5.1.1 简介)

[5.1.2 内部闭环控制原理](#5.1.2 内部闭环控制原理)

[5.1.3 PWM 控制协议与角度映射](#5.1.3 PWM 控制协议与角度映射)

[5.1.4 基于 STM32 的 PWM 参数计算](#5.1.4 基于 STM32 的 PWM 参数计算)

[5.1.5 角度与 CCR 值的线性转换逻辑](#5.1.5 角度与 CCR 值的线性转换逻辑)

[5.1.6 进阶探讨:舵机抖动原因与多路控制策略](#5.1.6 进阶探讨:舵机抖动原因与多路控制策略)

[5.2 130电机马达](#5.2 130电机马达)

[5.2.1 物理架构与电磁转换机理](#5.2.1 物理架构与电磁转换机理)

[5.2.2 机电特性参数分析](#5.2.2 机电特性参数分析)

[5.2.3 硬件接口约束与保护逻辑](#5.2.3 硬件接口约束与保护逻辑)

[5.2.3.1 功率放大与驱动隔离](#5.2.3.1 功率放大与驱动隔离)

[5.2.3.2 反电动势及其抑制](#5.2.3.2 反电动势及其抑制)

[5.2.3.3 电源噪声抑制](#5.2.3.3 电源噪声抑制)

[5.3 TB6612FNG电机驱动模块](#5.3 TB6612FNG电机驱动模块)

[5.3.1 模块架构与技术优势](#5.3.1 模块架构与技术优势)

[5.3.2 硬件接口与引脚定义](#5.3.2 硬件接口与引脚定义)

[5.3.3 控制逻辑真值表与状态解析](#5.3.3 控制逻辑真值表与状态解析)

[5.3.4 实现结论与工程建议](#5.3.4 实现结论与工程建议)

[6 本章节实验](#6 本章节实验)

[6.1 PWM驱动LED呼吸灯](#6.1 PWM驱动LED呼吸灯)

[6.1.1 实验目标](#6.1.1 实验目标)

[6.1.2 硬件设计](#6.1.2 硬件设计)

[6.1.3 软件设计](#6.1.3 软件设计)

[6.1.4 实验现象](#6.1.4 实验现象)

[6.2 PWM驱动舵机](#6.2 PWM驱动舵机)

[6.2.1 实验目标](#6.2.1 实验目标)

[6.2.2 硬件设计](#6.2.2 硬件设计)

[6.2.3 软件设计](#6.2.3 软件设计)

[6.2.4 实验现象](#6.2.4 实验现象)

[6.3 PWM驱动直流电机](#6.3 PWM驱动直流电机)

[6.3.1 实验目标](#6.3.1 实验目标)

[6.3.2 硬件设计](#6.3.2 硬件设计)

[6.3.3 软件设计](#6.3.3 软件设计)

[6.3.4 实验现象](#6.3.4 实验现象)


1 输出比较模式

1.1 简介

**输出比较(Output Compare,简称OC)**是STM32定时器的一项核心功能,广泛应用于PWM波形生成、精确延时控制、外部事件触发等场景。其基本原理可以概括为:将定时器的计数器(CNT)与预先设定的捕获/比较寄存器(CCR)进行比较,当两者满足特定关系(如相等、大于或小于)时,通过硬件自动翻转 GPIO 引脚的电平,从而生成精确的波形。

通俗地说,输出比较就像给定时器设置了一个"闹钟":当计数器走到我设定的数值(CCR)时,引脚就自动执行一个动作(比如翻转电平、输出高电平或低电平)。这种自动化的硬件机制,使得生成精确的PWM波形变得简单高效。

STM32 通用定时器(如 TIM2-TIM5)的结构框图如下所示,图中标示的"输出比较部分"即为通用定时器的输出比较模块电路,包含 4 个独立的输出比较通道,它们共用同一个计数器,但每个通道都有独立的 CCR 寄存器和输出引脚。


1.2 工作原理

输出比较的本质,是利用 CNT 与 CCR 的数值关系,通过一系列硬件电路的处理,最终在 GPIO 上输出特定的电平。如图所示:

我们可以将这个过程拆解为以下四个核心阶段:

第一阶段:时基单元运行(Time Base)------ 决定波形的频率

定时器启动后,计数器 CNT 在时钟驱动下开始运行。CNT 的计数模式决定了波形的基础周期,常见的模式有:

  • 向上计数模式:CNT 从 0 递增至自动重装载值(ARR),发生溢出后清零,循环往复。这是生成 PWM 最常用的模式。如下图所示:
  • 向下计数模式:CNT 从 ARR 递减至 0,然后重新加载 ARR 再次递减;
  • 中心对齐计数模式:CNT 先从 0 递增至 ARR,再从 ARR 递减至 0,循环往复;

关键点:ARR(自动重装载寄存器)的值决定了计数器的溢出周期,也就是最终输出波形的周期(频率)。

第二阶段:比较单元判定(Comparator)------ 决定波形的占空比

在 CNT 运行过程中,硬件电路会实时比较 CNT (当前计数值)与 CCR(预设比较值)的大小。比较结果通常分为三种情况:

  • CNT < CCR
  • CNT = CCR
  • CNT > CCR

这个比较结果是生成 PWM 信号的原始依据。CCR 的值决定了电平翻转的时间点,也就是最终波形的占空比

第三阶段:输出模式控制(Mode Controller)------ 生成中间参考信号 OCxREF

比较结果出来后,并不是直接输出到引脚,而是先送入"输出模式控制器"。根据寄存器 TIMx_CCMRxOCxM 位的配置(如 PWM 模式 1、PWM 模式 2、翻转模式等),生成一个中间参考信号 OCxREF (Output Compare Reference)。如图所示:

OCxREF 是一个逻辑信号(高电平/低电平),代表了当前通道在逻辑上是"有效"还是"无效"。例如,在向上计数模式下,PWM模式1的规则如下:

  • CNT < CCR 时:OCxREF = 1(有效状态)
  • CNT ≥ CCR 时:OCxREF = 0(无效状态)

第四阶段:输出控制电路(Output Stage)------ 极性选择与引脚驱动

OCxREF 信号还需要经过最后一道关卡才能到达 GPIO 引脚,这部分电路主要负责极性调整输出使能

极性选择(Polarity) 由寄存器 TIMx_CCERCCxP 位控制。它决定了"有效电平"对应的是物理上的高电平还是低电平。逻辑关系如下:

CCxP 配置 逻辑关系 结果说明
0 正极性 (同相) OCxREF 为高 -> 引脚输出 高电平
1 负极性 (反相) OCxREF 为高 -> 引脚输出 低电平

输出使能(Enable)TIMx_CCERCCxE 位控制。

  • CCxE = 1:开启输出开关,信号通过 GPIO 引脚(如 PA0)输出。
  • CCxE = 0:关闭输出。

只有在输出使能的情况下,信号才会真正出现在对应的定时器输出引脚 TIMx_CHx 上。例如,TIM2_CH1默认映射到PA0引脚。

总结

将上述流程串联起来,我们可以清晰地看到 PWM 波形的生成路径:

  1. CNT0ARR 之间循环计数(决定频率)。
  2. CCR 设定了一个阈值切割点。
  3. 比较器 根据 CNT 与 CCR 的关系,输出逻辑电平。
  4. 极性电路 决定最终的高低电平。

最终效果: 随着 CNT 的周期性变化,输出引脚 TIMx_CHx 上会产生连续的脉冲波形。我们只需要通过软件修改 CCR 寄存器的值,就可以在不停止定时器的情况下,平滑地改变输出波形的占空比,实现对电机速度或 LED 亮度的精准控制。如图所示:


1.3 输出比较的8种模式

输出比较模式的核心在于:当CNT与CCR的比较结果满足条件时,如何控制OCxREF的电平。这些模式通过配置捕获/比较模式寄存器(TIMx_CCMRx)中的OCxM[2:0]位来实现,共有8种模式。下表详细列出了这8种模式及其典型应用:

OCxM[2:0]配置 模式名称 功能描述 典型应用
000 冻结 比较匹配时,OCxREF保持原状态,不受比较结果影响。 暂停波形输出,维持当前电平
001 匹配时置有效电平 当CNT = CCR时,OCxREF强制为有效电平(高电平)。 产生固定宽度的正脉冲
010 匹配时置无效电平 当CNT = CCR时,OCxREF强制为无效电平(低电平)。 产生固定宽度的负脉冲
011 匹配时翻转 当CNT = CCR时,OCxREF电平翻转。 生成50%占空比的方波信号
100 强制为无效电平 无视比较结果,强制OCxREF为无效电平(低电平)。 强制关闭输出
101 强制为有效电平 无视比较结果,强制OCxREF为有效电平(高电平)。 强制开启输出
110 PWM模式1 向上计数时:CNT < CCR时输出有效电平,CNT ≥ CCR时输出无效电平。 最常用模式,生成频率和占空比可调的PWM波形
111 PWM模式2 向上计数时:CNT < CCR时输出无效电平,CNT ≥ CCR时输出有效电平。 生成与PWM1极性相反的PWM波形

模式说明:

  • 冻结模式:当你正在输出PWM波,想暂停输出时,可以临时切换为冻结模式,输出将维持在暂停时刻的电平状态。
  • 匹配时翻转模式:可以方便地输出一个频率可调、占空比始终为50%的PWM波形。例如设置CCR=0,则每次CNT更新清零时都会产生比较匹配,电平翻转一次。每更新两次,输出为一个周期,且高低电平时间相等。
  • PWM模式1和PWM模式2:两者本质上是互补关系------它们产生的波形正好极性相反。在实际应用中,通常只需掌握PWM模式1,配合极性选择功能即可灵活实现各种需求。我们后续的实验也将主要使用PWM模式1。

1.4 关键寄存器介绍

配置和使用输出比较功能,主要涉及以下几个关键寄存器:

寄存器 名称 作用描述
TIMx_CNT 计数器 核心计数器,不断递增或递减,其值反映时间流逝。
TIMx_CCR 捕获/比较寄存器 存放用户设定的比较值,是决定输出波形特征(如PWM占空比)的关键。每个通道有自己的CCR寄存器。
TIMx_ARR 自动重装载寄存器 决定计数器的计数周期,即PWM波形的频率。计数器计数到ARR后溢出重装。
TIMx_PSC 预分频器 对定时器的时钟源进行分频,用于调整计数频率。
TIMx_CCMRx 捕获/比较模式寄存器 配置通道为输出模式(CCxS位);选择上述8种输出比较模式之一(OCxM位);配置输出预装载功能(OCxPE位)等。
TIMx_CCER 捕获/比较使能寄存器 使能输出通道(CCxE位);配置输出极性(CCxP位),决定有效电平对应高电平还是低电平。
TIMx_CR1 控制寄存器1 使能定时器计数器(CEN位);设置计数方向(DIR位)等。
TIMx_SR 状态寄存器 当比较匹配发生时,相应的CCxIF标志位会被置1。
TIMx_DIER DMA/中断使能寄存器 使能比较匹配中断(CCxIE位)或DMA请求(CCxDE位)。

关于CCR预装载功能:TIMx_CCRx寄存器有影子寄存器,可以通过OCxPE位控制是否启用预装载功能。当启用时,写入CCR的新值不会立即生效,而是在下一次更新事件(UEV)时才加载到影子寄存器,这样可以避免在周期中间突然改变比较值而导致波形异常。


3 PWM 简介

3.1 什么是PWM?

PWM (Pulse Width Modulation),全称为脉冲宽度调制 ,是一种利用微处理器的数字输出信号实现模拟量控制的技术。简而言之,PWM 是一种将模拟信号电平进行数字化编码的方法。

单片机的 IO 口在任意时刻只能输出两种离散状态:高电平(如 3.3V)或低电平(0V),属于典型的数字信号,无法直接输出连续变化的模拟电压。然而,如果让 IO 口按照固定周期不断输出高低电平,并通过调节高电平在周期中的持续时间比例(占空比),就可以改变信号的平均电压值,使其在整体效果上等效为一个介于 0V 和 3.3V 之间的连续模拟量。

从原理上看,这种等效关系可以用面积等效法来解释:在一个固定周期内,PWM 波形的电压-时间面积,与目标模拟电压在同一周期内的面积相等,因此两者具有相同的平均电压效果。如下图所示:PWM 对应模拟信号的等效图。

为了更直观地理解这一点,可以引入惯性系统的概念。所谓惯性系统,是指其响应无法跟随输入信号的高速变化,而是表现为对输入信号的平均响应。例如,控制一个 LED 以极高频率(如每秒 1000 次)快速开关:

  • 如果 LED 在 50% 的时间内点亮,50% 的时间内熄灭;
  • 由于人眼存在视觉暂留效应(视觉惯性),无法分辨这种高速闪烁;
  • 最终看到的不是闪烁,而是一个亮度约为全亮一半的稳定光。

从能量等效角度来看,这相当于给 LED 施加了一个等效电压:3.3V×50%=1.65V

同样的原理也适用于其他具有惯性的系统,例如:

  • 电机的转动惯量,使其转速反映平均电压;
  • 电容的充放电特性,使输出电压趋于平滑;
  • 低通滤波电路,可将 PWM 转换为稳定的模拟电压。

因此,只要 PWM 的开关频率足够高,负载就无法跟随其瞬时变化,而只表现出平均效果,从而实现用数字信号输出模拟量的目的。

提示:上文中的"惯性"与电路中的惯性系统的 关系

这里的"惯性"其实是一个广义的低通滤波概念。例如:

  • 物理/机械惯性:如电机的转子质量,它无法随高频信号瞬间启停,只能响应能量的平均值。
  • 生物惯性:如人眼的视觉暂留,本质上是神经感官对光线变化的滞后反应。
  • 电路惯性:也就是我们在电路书籍中常看到的电容(电压不能突变)和电感(电流不能突变)。

它们本质上都是在扮演"储能缓冲"的角色。正是因为有了这些惯性(滤波效应),PWM的高频方波才能被平滑成稳定的模拟量。如果没有惯性,PWM就只是无意义的快速开关切换。


3.2 PWM 波形的形成机制(STM32 实现原理)

这里与输出比较的工作原理一节高度重合,因为都是以产生PWM波形为目的去解释其工作原理的,所以这里只大致介绍一下。

在 STM32 中,PWM 波形的产生依赖于定时器内部两个核心寄存器的配合:计数器 (CNT)捕获/比较寄存器 (CCR)

工作流程大致分以下三个阶段:

(1) 计数阶段:定时器启动后,计数器 CNT 在时钟驱动下从 0 开始不断累加,直到达到自动重装载值 ARR,然后清零重新开始。这个周期性的计数过程形成了一个"锯齿波"或"三角波"的时间基准。

(2) 比较阶段:在 CNT 计数的过程中,硬件电路会实时将 CNT(当前值) 与 CCR(预设比较值) 进行比较:

  • 当 CNT < CCR 时:输出有效电平(例如高电平)。
  • 当 CNT ≥ CCR 时:输出无效电平(例如低电平)。

(3) 波形输出:随着 CNT 的周期性变化,输出引脚上就会自动产生出一连串脉宽固定的方波。如图所示:

  • 改变 ARR:改变了计数周期的长短,即调整了 PWM 的频率。
  • 改变 CCR:改变了电平翻转的阈值点,即调整了 PWM 的占空比。

这种机制完全由硬件自动完成,无需 CPU 干预,因此效率极高。


3.3 PWM 的关键参数与计算

描述一个 PWM 信号,离不开三个核心参数:频率占空比分辨率。在 STM32 开发中,它们与硬件寄存器(PSC、ARR、CCR)存在严格的数学对应关系。

3.3.1 频率 (Frequency)

指 1 秒钟内 PWM 信号周期的个数。频率越高,等效输出的模拟信号越平滑,纹波越小,但系统性能开销也越大。

物理定义:

计算公式:

  • CK_PSC:定时器的输入时钟频率(通常为 72MHz)。
  • PSC:预分频系数。
  • ARR:自动重装载值(决定周期长度)。

3.3.2 占空比 (Duty Cycle)

指一个周期内,有效电平(高电平)持续时间占整个周期的比例。它直接决定了等效输出的模拟电压的大小。占空比越大,平均电压越高。

物理定义:

计算公式:

3.3.3 分辨率 (Resolution)

指占空比可调节的最小步长,即 PWM 对输出控制的"细腻程度"。分辨率越高,对电压或速度的控制就越细腻。

计算公式:

所以,ARR 值越大,分辨率越高(步长越小),控制越精细。

3.3.4 参数配置实例:确定相关寄存器的值

在 STM32 中生成特定的 PWM 波形,本质上是一个逆向推导的过程。我们需要根据应用需求,利用 PWM 的三大核心公式,依次锁定硬件中三个关键寄存器的数值:PSC(预分频器)ARR(自动重装载寄存器)CCR(捕获/比较寄存器)

  • PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
  • PWM占空比: Duty = CCR / (ARR + 1)
  • PWM分辨率: Reso = 1 / (ARR + 1)

核心逻辑说明:

  • 确定 ARR:由分辨率(步进精度)决定。
  • 确定 PSC:由目标频率与已确定的 ARR 共同决定。
  • 确定 CCR:由目标占空比与已确定的 ARR 共同决定。

具体示例如下:

配置需求:利用 STM32(主频 72MHz)产生一个频率为 1kHz 、占空比 50% (可任意调节)、分辨率为**1%**的 PWM 波形。计算步骤如下:

(1)确定 ARR (由分辨率决定)

分辨率定义为占空比变化的最小步长。要求 1% 的分辨率,意味着需要将一个完整的周期等分成 100 份。

(2)确定确定 PSC (由目标频率决定)

目标 1kHz,且ARR = 99,代入频率公式:

解得:

(3)确定 CCR (由目标占空比决定)

假设我们需要初始输出 50% 占空比的波形。已知: ARR = 99,PSC = 719。代入占空比公式:

最终的硬件参数设置如下:

寄存器名称 设定值 对应物理含义
PSC 719 将 72MHz 时钟进行 720 分频,产生 100kHz 的计数频率
ARR 99 每计满 100 个数溢出一次,产生 1kHz 的 PWM 周期
CCR 0 ~ 100 调节该值即可在 0% ~100% 范围内改变占空比

通过这三个值的配合,就能锁定PWM波形的频率和分辨率,并确定其占空比。


3.4 PWM在惯性系统中的应用

PWM 技术因其高效率和低成本,广泛应用于各类惯性系统中。所谓惯性系统,是指对输入信号变化反应有滞后、能起到"低通滤波"作用的系统。例如:

  • LED 亮度控制(调光) 利用人眼的视觉暂留效应。当 PWM 频率大于 50Hz 时,人眼无法分辨闪烁,只能感知到亮度的变化。这是手机屏幕亮度调节和呼吸灯的核心原理。
  • 直流电机调速 利用电机的机械惯性(转子转动惯量)。电机转速取决于电枢两端的平均电压。相比于传统的电阻分压调速,PWM 方式发热少、能效比极高。
  • 占空比**↑**------> 平均电压↑ ------> 转速加快
  • 占空比↓ ------> 平均电压↓ ------> 转速减慢
  • 开关电源 (SMPS) 通过控制功率管(MOSFET)的导通时间(占空比)来精确控制输出电压。这是所有现代电子设备供电的基础。
  • 音频信号生成 (DAC) 通过高速 PWM 配合 RC 低通滤波器,可以将数字音频流还原为模拟声波,常用于简单的语音播放。

PWM技术之所以应用如此广泛,得益于其显著的优点:

  • 抗干扰能力强:信号始终在全高或全低电平工作,数字传输不易受模拟噪声影响。
  • 能效高:控制元件(如三极管/MOS管)工作在开关状态,导通时压降小,截止时电流小,显著降低了功耗和发热。
  • 实现简单:现代微控制器(MCU)内部均集成了专用的硬件 PWM 模块,无需复杂的外部电路。

4 PWM 输出的配置与实现

在掌握了 PWM 的理论基础后,本节进入代码实战环节。我们将以 STM32F103 为例,详细拆解如何配置定时器以输出 PWM 波形,并实现占空比的动态调整。

​​​​​​​在本节的演示中,我们的目标是生成一个具有以下特征的 PWM 波形:

  • 输出引脚:使用 TIM2 的通道 1(默认引脚为 PA0)。
  • 波形规格:频率为 1kHz,占空比 0%~100% 可调,且具有 1% 的调节分辨率。
  • 应用实例:最终通过动态修改占空比,在 PA0 引脚上实现一个平滑的呼吸灯效果。

4.1 参数选型:锁定寄存器配置值

​​​​​​​根据实验要求的1kHz频率和1%分辨率,需要先计算出对应的寄存器参数值。基于 3.3.4 节的推导逻辑,我们确定了以下三个关键配置参数,这些参数与STM32硬件寄存器的对应关系如下表所示:

目标需求 对应寄存器 参数值
调节精度 (1%) ARR (自动重装载值) ARR = 100 - 1
波形频率 (1kHz) PSC (预分频系数) PSC = 720 - 1
初始占空比 (0%) CCR (比较值) CCR = 0

​​​​​​​通过这组参数配置,定时器的计数频率被设定为 100kHz,每计满 100 个数产生一个 PWM 周期。此时,只需在程序中将 CCR 的值在 0 ~100 之间调整,即可实现精度为 1% 的全范围占空比控制。如图所示:


4.2 PWM初始化流程

​​​​​​​配置 PWM 输出通常遵循一套固定的"五步法"。为了便于演示,我们将 TIM2 的通道 1 (CH1) 配置为 PWM 输出,该通道默认映射在 PA0 引脚。

4.2.1 开启时钟

​​​​​​​任何外设在使用前都需要先开启其时钟。我们需要分别开启定时器和 GPIO 的时钟。注意它们挂载的总线可能不同。

cpp 复制代码
// 开启TIM2的时钟。TIM2是APB1总线上的外设。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

// 开启GPIOA的时钟,因为我们要使用PA0引脚。GPIOA是APB2总线上的外设。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

4.2.2 配置 GPIO (复用推挽输出)

​​​​​​​关键点:要使定时器能够控制GPIO引脚的电平,引脚模式必须配置为 复用推挽输出 (GPIO_Mode_AF_PP)

  • 普通推挽:引脚电平由输出数据寄存器(ODR)控制(即 CPU 软件控制)。

  • 复用推挽 :引脚控制权移交给片上外设(定时器),由定时器内部信号直接驱动引脚。如图所示:

cpp 复制代码
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;         // 选择要配置的引脚,TIM2_CH1 默认在 PA0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;   // 设置为复用推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置输出速度为50MHz,这是一个常见的速度选择

GPIO_Init(GPIOA, &GPIO_InitStructure);            // 将配置应用到GPIOA端口

4.2.3 配置时基单元

​​​​​​​这一步决定了 PWM 的频率。我们通过TIM_TimeBaseInitTypeDef结构体来配置,主要涉及以下两个成员:

  • TIM_Prescaler:设定预分频器值 PSC;
  • TIM_Period:设定自动重装载值 ARR;
cpp 复制代码
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

// 配置时基单元结构体
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     // 时钟分频,一般不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数器向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;                 // 设置自动重装载值ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;              // 设置预分频值PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            // 重复计数器,高级定时器才用到,此处填0

// 将配置写入TIM2
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

4.2.4 配置输出比较单元

​​​​​​​这一步决定了 PWM 的模式极性初始占空比。我们使用TIM_OCInitTypeDef结构体进行配置。

cpp 复制代码
TIM_OCInitTypeDef TIM_OCInitStructure;

// 给结构体赋一个初始值,这是一个好习惯,可以避免因未赋值成员导致的意外错误
TIM_OCStructInit(&TIM_OCInitStructure);

// 设置输出比较模式为PWM模式1
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
// 设置输出极性为高,即有效电平为高电平。如果设置为低,则输出波形会取反
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 使能输出
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
// 设置初始CCR值,此值决定初始占空比。对于1kHz、ARR=99的PWM,CCR=50对应50%占空比
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比设为0%

// 将配置应用到TIM2的通道1。不同的通道对应不同的函数:TIM_OC1Init、TIM_OC2Init等
TIM_OC1Init(TIM2, &TIM_OCInitStructure);

// 使能TIM2在CCR1上的预装载寄存器。这样,在运行时修改CCR值,会等到下一个更新事件才生效,避免波形异常
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);

注意:先调用 TIM_OCStructInit 给结构体赋默认值,防止未知错误。

4.2.5 启动定时器

​​​​​​​最后一步,开启定时器。

cpp 复制代码
TIM_Cmd(TIM2, ENABLE);

​​​​​​​至此,计数器开始计数,PA0 引脚上将输出一个 1kHz,初始占空比为 0% 的PWM信号。


4.3 动态调整占空比(实现呼吸灯)

​​​​​​​PWM 的核心价值在于其动态控制能力。在程序运行过程中,动态地修改PWM占空比,可以实现呼吸灯效果、电机调速等关键应用。STM32 标准库为此提供了 TIM_SetComparex 系列函数(x 代表通道号,取值为 1-4),这些函数通过直接操作定时器的 CCR 寄存器来实现功能。

​​​​​​​​​​​​​​代码封装示例:

cpp 复制代码
/**
  * @brief  设置 TIM2 通道 1 的占空比
  * @param  Compare: CCR 数值 (0~100)
  */
void PWM_SetCompare1(uint16_t Compare)
{
    TIM_SetCompare1(TIM2, Compare);
}

​​​​​​​​​​​​​​在呼吸灯实验中,我们在主循环中不断改变这个Compare值,并调用PWM_SetCompare1,改变PWM占空比,即可以观察到LED亮度的周期性变化。呼吸灯主循环逻辑如下所示:

cpp 复制代码
while (1)
{
    // 渐亮
    for (int i = 0; i <= 100; i++)
    {
        PWM_SetCompare1(i);
        Delay_ms(10);
    }
    // 渐暗
    for (int i = 0; i <= 100; i++)
    {
        PWM_SetCompare1(100 - i);
        Delay_ms(10);
    }
}

拓展:修改PWM频率:除了占空比,如果需要动态修改PWM的频率,可以使用TIM_SetAutoreload函数来修改ARR寄存器的值。例如,TIM_SetAutoreload(TIM2, NewARR_Value);。同样地,为了使新的ARR值平滑生效,建议在配置时基单元时启用ARR的预装载功能。

4.3.1 预装载与影子寄存器机制

​​​​​​​​​​​​​​在动态调整 PWM 占空比(如实现呼吸灯或电机调速)时,常频繁调用 TIM_SetComparex 函数。为确保输出波形的连续性,防止在计数周期中途修改CCR而导致波形畸变,STM32 硬件设计采用了预装载(Preload) 与**影子寄存器(Shadow Register)**机制。

1. 双缓冲寄存器结构:

​​​​​​​​​​​​​​在硬件底层,捕获/比较寄存器(CCR)并非由单一寄存器组成,而是构成了一套双缓冲结构:

  • 预装载寄存器(Preload Register) :作为软件层面的操作接口,存放由 TIM_SetComparex 函数写入的新数值。
  • 影子寄存器(Shadow Register):作为硬件层面的执行核心,其数值直接与计数器(CNT)进行实时比较,控制引脚电平。

2. 占空比更新的执行逻辑

​​​​​​​​​​​​​​当通过 TIM_OCxPreloadConfig 函数使能预装载功能后,CCR 的更新过程受硬件严格同步:

  • 缓冲存储 :在计数周期运行过程中调用 TIM_SetComparex,新数值仅存储于预装载寄存器中。此时影子寄存器仍持有旧值,继续控制当前周期的 PWM 逻辑。
  • 同步加载:新数值的正式生效被延迟至当前计数周期结束。当触发**更新事件(UEV)**时,硬件自动将预装载寄存器中的内容拷贝至影子寄存器。
  • 波形连续性:该机制确保了比较阈值的切换始终发生在周期的起始边界,有效规避了因中途修改而可能产生的非法窄脉冲或电平闪变,保证了 PWM 信号在动态调节过程中的完整性与相位一致性。

4.4 PWM引脚重映射(Remap)

​​​​​​​​​​​​​​在实际的项目开发中,GPIO 引脚冲突是常见问题,同一个引脚常被多个外设功能争抢使用。以 TIM2 的通道 1(CH1)为例,其默认输出引脚为 PA0,但 PA0 同时也常被用作串口通信或 ADC 采集引脚。为了解决这种硬件资源冲突,STM32 提供了 **引脚重映射(Remap)**功能。

​​​​​​​​​​​​​​通过 **AFIO(Alternate Function I/O,复用功能 I/O)**模块,我们可以将外设的特定功能从默认引脚转移到备用引脚上。本节将以 TIM2_CH1 从 PA0 重映射到 PA15 为例,详细拆解重映射的配置逻辑与实现步骤。

4.4.1 映射方案选择与引脚分析

​​​​​​​​​​​​​​首先,我们需要查阅芯片手册,确认 TIM2 支持哪些重映射方案。在 STM32F103C8T6 中,TIM2 的 4 个通道支持以下映射方式:

映射方式 CH1 引脚 CH2 引脚 CH3 引脚 CH4 引脚
无重映射(默认) PA0 PA1 PA2 PA3
部分重映射 1 PA15 PB3 PA2 PA3
部分重映射 2 PA0 PA1 PB10 PB11
完全重映射 PA15 PB3 PB10 PB11

​​​​​​​​​​​​​​从上表可知,若要将 TIM2_CH1 转移到 PA15,必须选择部分重映射 1完全重映射

​​​​​​​​​​​​​​接着,我们需要对比源引脚(PA0)与目标引脚(PA15)的硬件属性:

引脚号 引脚名称 类型 I/O 电平 主功能 (复位后默认) 默认复用功能 重定义功能 (Remap)
10 PA0-WKUP I/O - PA0 (普通 IO) WKUP、 USART2_CTS 、 ADC12_IN0、TIM2_CH1_ETR -
38 PA15 I/O FT JTDI (调试接口) - TIM2_CH1_ETR、 PA15、 SPI1_NSS

4.4.2 资源冲突深度解析:PA15 与 JTAG

​​​​​​​​​​​​​​根据引脚属性表,PA15 存在一个极易踩坑的关键问题:它与 JTAG 调试接口存在功能冲突。

​​​​​​​​​​​​​​在 STM32 复位后,PA15 的默认主功能并非普通 GPIO,而是 JTAG 调试接口的 JTDI 信号。在硬件电路上,调试接口的优先级高于普通复用外设。若未经特殊配置,直接将 PA15 初始化为 PWM 输出通道,引脚电平将受控于调试模块,PWM 波形无法正常输出。

​​​​​​​​​​​​​​为释放 PA15 的控制权,必须调整调试端口的配置。核心操作准则如下:

  • 禁用 JTAG 功能:通过配置 AFIO 寄存器,关闭 JTAG 调试接口,释放 PA15、PB3、PB4 等引脚。
  • 保留 SWD 功能:在禁用 JTAG 的同时,务必确保 SWD(Serial Wire Debug)接口(PA13/JTMS, PA14/JTCK)保持使能状态,以保证后续程序下载与调试不受影响。

4.4.3 重映射配置步骤

​​​​​​​​​​​​​​基于以上分析,配置引脚重映射需要比标准 PWM 初始化多出三个关键步骤:

(1)开启 AFIO 时钟

​​​​​​​​​​​​​​重映射功能由 AFIO 模块管理。在配置任何重映射寄存器之前,必须先使能其时钟。

cpp 复制代码
// 开启 AFIO 时钟(重映射的前提条件)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

(2)配置引脚重映射方式

​​​​​​​​​​​​​​调用库函数选择对应的重映射模式。

cpp 复制代码
// 选择 TIM2 的部分重映射 1 方式 (CH1 从 PA0 转移到 PA15)
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);

(3)处理调试端口复用冲突

​​​​​​​​​​​​​​精确禁用 JTAG 释放目标引脚。

cpp 复制代码
// 禁用 JTAG 功能,保留 SWD 功能,释放 PA15 为普通 IO
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);

4.4.4 完整代码示例

​​​​​​​​​​​​​​结合标准 PWM 的初始化流程,将 TIM2_CH1 重映射到 PA15 的完整代码如下:

cpp 复制代码
void PWM_Remap_Init(void)
{
    /* 1. 开启时钟 (增加 AFIO 时钟) */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // ❗重映射必须开启 AFIO
    
    /* 2. 配置重映射 & 解除调试端口占用 */
    GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);     // 部分重映射1: CH1 -> PA15
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);  // ❗关闭 JTAG,保留 SWD,释放 PA15
    
    /* 3. 配置 GPIO (注意改为 PA15) */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;       // 目标引脚改为 PA15
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 必须为复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    /* 4. 时基单元与输出比较配置 (与普通配置完全一致) */
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    // ... [省略常规的 PSC、ARR 配置代码] ...
    
    TIM_OCInitTypeDef TIM_OCInitStructure;
    // ... [省略常规的 PWM 模式、CCR 配置代码] ...
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);         // 依然使用通道 1 的初始化函数
    
    /* 5. 启动定时器 */
    TIM_Cmd(TIM2, ENABLE);
}

4.4.5 配置要点与排错指南

​​​​​​​​​​​​​​重映射过程的细节较多,稍有不慎即会导致配置失效。以下是核心步骤的总结与排错清单:

步骤 关键操作代码 核心目的与注意事项
1. AFIO 时钟 RCC_APB2PeriphClockCmd(...,AFIO,ENABLE) 必须开启,否则后续重映射指令无效。
2. 重映射方式 GPIO_PartialRemap1_TIM2 根据需求查表选择,确保映射逻辑正确。
3. 调试端口释放 GPIO_Remap_SWJ_JTAGDisable 禁用 JTAG 释放 PA15,保留 SWD 保证烧录
4. GPIO 初始化 改为 GPIO_Pin_15 引脚号必须与重映射后的物理引脚对应。
5. 通道配置 TIM_OC1Init 无论引脚如何变化,逻辑上仍属于 TIM2 的 CH1。

警告:谨防芯片"变砖"

​​​​​​​​​​​​​​在处理调试端口释放时,千万不要使用 GPIO_Remap_SWJ_Disable 参数。该参数会同时彻底禁用 SWD 和 JTAG 接口。一旦执行,后续将无法通过 ST-Link 或 J-Link 下载程序(俗称锁死变砖),只能通过 BOOT0 切换启动模式使用串口 ISP 工具强行擦除芯片来恢复。


5 相关元器件简介

5.1 SG90 9g 舵机(伺服电机)

​​​​​​​​​​​​​​在机器人开发、航模控制及各类自动化项目中,SG90 9g 舵机是一种应用极广的小型执行机构。无论是机械臂关节的运动定位,还是遥控模型的转向控制,均依赖于这种精密组件来实现。实物如图所示:

5.1.1 简介

​​​​​​​​​​​​​​舵机(Servo Motor) ,严格意义上称为位置伺服电机。与通电后持续旋转的常规直流电机不同,舵机的核心特性在于精确的角度定位。它的输出轴通常被限制在一个固定的物理角度范围内(例如 0°~180°),并能够根据接收到的控制信号,精准旋转并悬停在范围内的任意指定角度上。

​​​​​​​​​​​​​​从硬件接口上看,SG90 通常引出三根线,其功能定义如下表所示:

线色(常见) 接口定义 连接说明
棕色 GND 接系统电源负极(必须与控制板共地)
红色 VCC 接电源正极(SG90 通常需使用 5V 独立供电)
橙色 Signal 信号线,接收来自单片机的 PWM 控制信号

5.1.2 内部闭环控制原理

​​​​​​​​​​​​​​舵机实现精准角度锁定的基础,在于其内部构建的微型闭环控制系统。SG90 的硬件架构主要由微型直流电机、减速齿轮组、控制芯片及电位器(位置传感器)构成。如图所示:

​​​​​​​​​​​​​​其完整控制环路如下:

  1. 指令接收:单片机通过信号线输入特定脉宽的 PWM 信号,该信号脉宽代表目标预期角度。
  2. 误差计算:控制芯片内部的基准电路将接收到的 PWM 信号转化为目标参考电压;同时,实时读取与输出轴同轴转动的电位器电压,该电压代表当前实际角度。
  3. 驱动执行:控制芯片对目标电压与实际电压进行比较运算,产生电压差。电压差的正负号决定电机的旋转方向,电压差的大小影响旋转速度。
  4. 稳定定位:电机旋转时,通过减速齿轮组带动输出轴和电位器同步转动。当实际角度逼近目标角度时,电压差趋于零,电机随之停转,输出轴即稳定保持在指定位置。

5.1.3 PWM 控制协议与角度映射

​​​​​​​​​​​​​​为了实现对舵机的精确控制,必须向其信号线输入符合特定时序规范的 PWM(脉冲宽度调制)信号。对于 SG90,其标准控制协议需满足以下要求:

  • 信号周期:20ms(对应 PWM 频率为 50Hz)。
  • 有效脉宽:高电平的持续时间被严格限制在 0.5ms~2.5ms 之间。

​​​​​​​​​​​​​​此微秒级的高电平脉宽,与舵机的旋转角度呈现严格的线性映射关系。以标准 180° 舵机为例,其对应关系如下:

高电平持续时间 对应角度(0°~180° 标定) 对应角度(-90°~+90° 标定)
0.5ms -90°
1.0ms 45° -45°
1.5ms 90° (中位)
2.0ms 135° +45°
2.5ms 180° +90°

​​​​​​​​​​​​​​例如:高电平持续时间1.0ms + 标准 180° 舵机。

注意:除了180°舵机外,还有360°连续旋转舵机。此类舵机移除了内部的电位器反馈机制,无法实现角度定位。其 PWM 信号(1.5ms 此时为停止阈值)仅能控制电机的旋转方向和速度。在系统设计与采购时需严格区分。

5.1.4 基于 STM32 的 PWM 参数计算

​​​​​​​​​​​​​​在明确控制协议后,需将理论参数转化为微控制器的硬件寄存器配置。以主频为 72MHz 的 STM32 为例,需合理配置定时器的预分频器(PSC)和自动重装载值(ARR),以生成周期为 20ms (50Hz)的PWM信号。

1. 确定 ARR 与 PSC :

​​​​​​​​​​​​​​为保证角度调节的平滑度,需提供较高的计数分辨率。设定 ARR 为 19999(即计数 20000 次构成一个完整周期):

​​​​​​​​​​​​​​将 ARR = 19999 代入上述公式,解得:

2. 计算 CCR:

​​​​​​​​​​​​​​在上述配置下,定时器单次计数耗时为 1us,分辨率为 0.005%。随后根据高电平要求的持续时间,计算捕获/比较寄存器(CCR)的配置值值:

  • 0.5ms(起点):
  • 2.5ms(终点):

结论:在代码初始化配置 PSC=71,ARR=19999 后,通过调用 TIM_SetCompareX(TIMx, CCR) 函数,使 CCR 的值在 500~2500 之间线性变化,即可实现舵机 0°~180° 的平滑与精确控制。​​​​​​​​​​​​​​

5.1.5 角度与 CCR 值的线性转换逻辑

为了实现对舵机输出轴角度的精确控制,必须在目标角度 Angle 与定时器捕获/比较寄存器数值 CCR 之间建立明确的数学映射关系。本节将说明驱动函数中舵机角度设置接口的角度-CCR线性转换原理。

cpp 复制代码
/**
  * 函    数:舵机设置角度
  * 参    数:Angle 要设置的舵机角度,范围:0~180
  * 返 回 值:无
  */
void Servo_SetAngle(float Angle)
{
	PWM_SetCompare2(Angle / 180 * 2000 + 500);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}

针对 SG90 舵机,控制协议规定 0.5ms 至 2.5ms 的脉宽对应 0° 至 180° 的转角。在定时器配置为 20ms 周期、计数分辨率为 1us(即 ARR=19999)的条件下,对应的 CCR 取值范围为 500 至 2500。

该映射关系表现为标准的一元一次线性方程(Linear Equation):y = Kx + b。设定自变量为目标角度 x(Angle),因变量为寄存器值 y(CCR),其推导过程如下:

1. 确定坐标点:

已知该线性函数经过以下两个边界坐标点:

  • 起点:(x1, y1) = (0, 500)
  • 终点:(x2, y2) = (180, 2500)

2. 求解斜率 k:

斜率表示单位角度变化所引起的 CCR 数值增量:

3. 求解截距 b:

将起点坐标 (0, 500) 代入线性方程 y = kx + b:

4. 建立最终映射方程:

5. 代码实现转换:

在 Servo_SetAngle 函数中,通过表达式 Angle / 180 * 2000 + 500 直接计算目标 CCR 值。该算法确保了在 [0, 180] 连续角度区间内,输出脉宽能够平滑地在 [0.5ms, 2.5ms] 之间线性切换,从而实现伺服系统的精准定位。

5.1.6 进阶探讨:舵机抖动原因与多路控制策略

在实际应用(如多足机器人系统的开发)中,常会遇到舵机抖动的问题。 当供电电压稳定且负载在许可范围内时,此类现象的产生主要源于PWM 控制信号的微小波动。舵机的死区通常在微秒级别(约 2us 左右),若单片机产生的脉宽发生微量抖动,舵机的闭环系统即会不断校正位置,产生高频震颤。

高精度控制策略对比:

  • 现代方案(硬件 PWM 生成):如上文所述的 STM32 平台,利用片上定时器的硬件比较输出(OC)功能直接生成 PWM。该方法完全依赖底层硬件寄存器运行,不占用 CPU 资源,输出脉宽精度极高,是消除舵机抖动的最佳设计实践。

  • 传统/妥协方案(软件定时器中断模拟) :在早期的低算力微控制器(如 51 单片机)或硬件 PWM 通道受限的系统中,开发者常利用定时器中断(设定 20ms 周期,中断服务程序中翻转 IO 电平并通过 delay 控制高电平持续时间)或外挂外部计数器芯片(如 8253)来模拟 PWM。尽管该方法能够在引脚资源匮乏时实现多路舵机控制,但受限于软件中断响应固有的时间不确定性(即抖动,Jitter),极易引起脉宽误差积累,进而导致舵机微抖。

因此,在设计具有高精度伺服需求的系统架构时,优先配置并分配硬件定时器的 PWM 通道,是确保系统伺服稳定性的核心原则。


5.2 130电机马达

130型直流电机是一种广泛应用于微型机电系统、消费电子及小型移动机器人领域的执行机构。因其结构精简、控制逻辑直观且具备较高的转速功率比,该型号电机常作为实验平台及工业原型开发中的基础动力单元。实物如图所示:

5.2.1 物理架构与电磁转换机理

130直流电机属于永久磁铁直流有刷电机(PMDC)。其运行遵循安培力定律:当外部直流电压施加于电机引脚时,电流通过内部换向器进入转子绕组。由于转子处于定子永久磁场中,受磁场力作用产生转矩,带动输出轴旋转。

针对直流电机的运动状态控制,通常分为速度调节与方向切换两个维度:

  • 速度调节(Speed Regulation)

电机的旋转速度与其引脚两端的平均输入电压成正比。在嵌入式开发中,基于脉冲宽度调制(PWM)技术,通过改变信号的占空比(Duty Cycle)来实现等效平均电压的调节。

其中, 为等效输出电压, 为电源电压。

  • 方向控制(Direction Control)

电机的转向取决于输入电流的极性。改变施加于引脚两端电压的正负极性,即可通过改变转子磁场方向实现输出轴的正向或反向旋转。

5.2.2 机电特性参数分析

在进行电路驱动设计与系统稳定性评估时,需严格参考 130 型电机的电学参数。以下为典型实验环境下该类电机的额定参数表:

参数名称 典型取值范围 物理意义与工程影响
空载转速 11000 ~ 15000 rpm(转/分钟) 电机在无负载状态下的最高转速
空载电流 60 ~ 100 mA 克服内部摩擦与阻力所需的维持电流
启动/堵转电流 0.7 ~ 1.2 A 电机启动瞬间或负载过重停止时的峰值电流

堵转电流(Stall Current) 是系统设计的核心约束指标。由于电机在静止瞬间或发生堵转时,转子无法切割磁感线产生反电动势(Back-EMF),此时回路阻抗仅由微小的线圈电阻组成,根据欧姆定律,瞬时电流会激增。这一特性决定了单片机的 GPIO 引脚无法直接驱动电机,且电源系统需具备足够的瞬态响应能力。

5.2.3 硬件接口约束与保护逻辑

直流电机作为感性功率器件,其电气特性对逻辑电路具备较强的干扰性与破坏性,因此在硬件接口设计中必须遵循以下准则:

5.2.3.1 功率放大与驱动隔离

单片机(如 STM32)的 GPIO 典型输出电流通常被限制在 20mA 左右,远低于130 电机的工作电流需求。若强行驱动,将导致引脚过载损坏。工程实践中必须引入集成化功率驱动芯片(如 TB6612FNGL298N)。驱动芯片通过 H 桥电路结构实现大电流的输出控制,并完成弱电信号与强电功率的物理/电气隔离。

5.2.3.2 反电动势及其抑制

电机本质上是一个感性负载,根据电磁感应定律,当电机断电或快速换向时,线圈电流的突变()会产生极高的反向瞬变电压:

尖峰电压(Spike Voltage) 若反馈至逻辑电路或驱动电路,可能导致半导体元器件发生击穿损坏。为保护系统,必须在驱动电路中配置续流二极管(Flyback Diode)。在 H 桥驱动芯片内部,通常已集成了此类保护二极管,用于在关断瞬间提供泄放路径,消耗感性电势产生的多余能量。

5.2.3.3 电源噪声抑制

电机运行过程中,碳刷换向会产生高频电磁干扰(EMI)。为降低噪声对微控制器系统的影响,通常建议在电机引脚两端并联一颗 0.1uF 的陶瓷电容,并在电机电源输入端配置大容量电解电容,以平滑电流波动并吸收高频干扰信号。


5.3 TB6612FNG电机驱动模块

在嵌入式机电控制系统中,微控制器(MCU)的 GPIO 引脚因电流驱动能力受限,无法直接驱动直流电机等感性负载。TB6612FNG 是一款双通道 MOSFET 型 H 桥驱动芯片,凭借其高效率、低发热量及小型化封装的特性,已成为替代传统 L298N 驱动器的主流工业方案,尤其适用于 130 型等微型直流电机的精密控制。

5.3.1 模块架构与技术优势

TB6612FNG 内部集成了两套完全独立的 H 桥(H-Bridge) 驱动电路。H 桥是一种典型的电路拓扑,通过四个受控开关管的导通与截止组合,实现对感性负载电流方向及大小的控制。其结构如图所示:

相比于基于双极性晶体管(BJT)设计的 L298N,TB6612FNG 采用 功率 MOSFET 作为输出级。其核心技术优势表现在:

  • 低导通电阻:由于 MOSFET 的导通压降远低于 BJT,芯片在额定电流范围内功率损耗极小,转换效率更高,基本无需额外安装散热片。
  • 高开关频率:支持高达 100kHz 的 PWM 输入频率,能够实现平滑的转速控制并有效抑制电机运行时的电磁噪声。
  • 多重保护机制:芯片内置过热保护(Thermal Shutdown)及低电压检测电路,提升了系统的可靠性。

5.3.2 硬件接口与引脚定义

TB6612FNG 模块的引脚分布可分为电源接口、逻辑控制接口与功率输出接口三部分。具体定义如下表所示:

引脚分类 引脚名称 功能描述 工程连接说明
电源接口 VM 电机驱动电源正极 连接外部电源(如电池、稳压模块)的正极,为电机旋转提供能量。电压范围一般为2.5V ~ 12V。
电源接口 VCC 逻辑供电正极 连接 MCU 的 3.3V 或 5V 输出,为内部逻辑电路供电。
电源接口 GND 公共参考地 连接电源负极,必须与单片机系统实现共地。
逻辑输入 AIN1 / AIN2 A 路方向控制 连接 MCU 的 GPIO 引脚。通过电平组合控制 A 路电机转向。
逻辑输入 BIN1 / BIN2 B 路方向控制 功能同 A 路,用于控制 B 路电机。
逻辑输入 PWMA / PWMB 调速控制引脚 连接 MCU 的 PWM 输出引脚。通过调节占空比改变平均电压,实现调速。
逻辑输入 STBY 待机控制 高电平时芯片正常工作;低电平时进入低功耗模式,输出关闭。
功率输出 AO1 / AO2 A 路功率输出 直接连接至 A 路电机的两个输入端。
功率输出 BO1 / BO2 B 路功率输出 直接连接至 B 路电机的两个输入端。

5.3.3 控制逻辑真值表与状态解析

TB6612FNG 的逻辑控制具有高度的确定性。以 A 通道为例,其输出状态由 STBY、AIN1、AIN2 以及 PWMA 四个信号共同决定,如图所示:

TB6612FNG 控制逻辑真值表:

STBY AIN1 AIN2 PWMA 输出状态 (AO1, AO2) 电机运动状态解析
0 X X X 高阻态 (Hi-Z) 待机模式:所有输出关闭,进入节能状态。
1 1 0 PWM 正转:转速由 PWM 信号的占空比决定。
1 0 1 PWM 反转:转速由 PWM 信号的占空比决定。
1 1 1 X 低电平 (Low) 短接制动(Brake):电机绕组短路,产生反电动势实现强制刹车。
1 0 0 X 高阻态 (Hi-Z) 停止:电机依靠摩擦力自由滑行至停止。

表达的是等效平均电压与占空比之间的正比关系。

  • :平均电压(Average Voltage)。指在一个周期内,脉冲信号折算后的等效直流电压。
  • :正比符号。表示左侧变量随右侧变量的线性增加而增加。
  • :占空比(Duty Cycle)。指在一个脉冲周期内,高电平(有效电平)持续时间占整个周期时间的百分比,数值范围通常为 0\\% \\sim 100\\%(或 0 \\sim 1)。

5.3.4 实现结论与工程建议

TB6612FNG 通过逻辑信号与功率输出的解耦,实现了对直流电机的精密驱动。在实际工程部署中,应注意以下几点:

  • 待机管脚管理:STBY 引脚不可悬空。若无需低功耗控制,可直接将其连接至 VCC;若需受控,则需连接 MCU 引脚并在初始化时置高。
  • PWM 频率选择:虽然芯片支持 100kHz,但过高的频率会增加开关损耗。通常建议将 PWM 频率设定在 10kHz ~ 20kHz 之间,以平衡控制精度与转换效率。
  • 电源去耦:在 VM 接口附近应放置大容量电解电容(如 100uF ~ 470uF),以吸收电机换向产生的感性浪涌电流,确保系统电压的稳定性。

通过对 TB6612FNG 逻辑与电气特性的准确配置,开发者能够实现对直流电机在方向、速度及制动性能上的全方位控制。


6 本章节实验

6.1 PWM驱动LED呼吸灯

6.1.1 实验目标

本实验旨在通过 STM32 定时器的输出比较(Output Compare)功能生成脉冲宽度调制(PWM)波形,驱动 LED 实现呼吸灯效果。实验的核心技术要求包括:

  • 硬件资源配置:掌握 STM32 定时器时基单元、输出比较单元及 GPIO 复用功能的初始化流程。
  • 参数计算与映射:理解 PWM 频率、占空比与硬件寄存器(ARR、PSC、CCR)之间的数学映射关系。
  • 动态占空比调节:实现在程序运行中通过动态修改 CCR 寄存器值来控制输出等效电压。
  • 引脚重映射应用:掌握 AFIO 时钟配置及引脚重映射技术,实现输出通道在不同 GPIO 引脚间的灵活切换。

6.1.2 硬件设计

6.1.3 软件设计

本实验的软件逻辑主要分为 PWM 模块初始化占空比动态控制循环两大部分。

1. PWM 模块初始化配置

初始化流程严格遵循硬件时钟树与外设配置协议:

  • 时钟使能:开启 APB1 总线上的 TIM2 时钟以及 APB2 总线上的 GPIOA 时钟。若涉及引脚重映射,需额外开启 AFIO 复用功能时钟。
  • GPIO 模式设置:输出引脚需配置为复用推挽输出(GPIO_Mode_AF_PP),以确保引脚控制权交由定时器外设而非 CPU 数据寄存器。
  • 时基单元初始化:通过设定预分频器(PSC)与自动重装载寄存器(ARR)来确定 PWM 的基准频率。
  • 输出比较单元配置:设定为 PWM 模式 1,配置输出极性为高。初始捕获/比较寄存器(CCR)设为 0,即初始占空比为 0%。

2. 主程序控制算法

主循环通过双重循环结构实现 LED 亮度的平滑过渡。程序不断修改 CCR 寄存器的值,改变 PWM 的高电平持续时间,从而改变 LED 的平均工作电流。每个步进周期辅以微秒级延时,以匹配人眼的视觉暂留特性,最终呈现周期性的呼吸视觉效果。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"

uint8_t i;			//定义for循环的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	PWM_Init();			//PWM初始化
	
	while (1)
	{
		for (i = 0; i <= 100; i++)
		{
			PWM_SetCompare1(i);			//依次将定时器的CCR寄存器设置为0~100,PWM占空比逐渐增大,LED逐渐变亮
			Delay_ms(10);				//延时10ms
		}
		for (i = 0; i <= 100; i++)
		{
			PWM_SetCompare1(100 - i);	//依次将定时器的CCR寄存器设置为100~0,PWM占空比逐渐减小,LED逐渐变暗
			Delay_ms(10);				//延时10ms
		}
	}
}

PWM.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO重映射*/
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);			//开启AFIO的时钟,重映射必须先开启AFIO的时钟
//	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);			//将TIM2的引脚部分重映射,具体的映射方案需查看参考手册
//	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);		//将JTAG引脚失能,作为普通GPIO引脚使用
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		//GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA0引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式		
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;					//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);							//结构体初始化,若结构体没有完整赋值
																	//则最好执行此函数,给结构体所有成员都赋一个默认值
																	//避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;				//输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;		//输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;	//输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);						//将结构体变量交给TIM_OC1Init,配置TIM2的输出比较通道1
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare1(uint16_t Compare)
{
	TIM_SetCompare1(TIM2, Compare);		//设置CCR1的值
}

6.1.4 实验现象

将程序下载至开发板后,连接在PA0(或重映射后的PA15)上的LED会呈现呼吸灯效果:亮度从暗逐渐变亮,再逐渐变暗,如此循环反复。使用示波器测量对应引脚,可观察到频率为1kHz的PWM波形,其占空比在0%至100%之间周期性变化。


6.2 PWM驱动舵机

6.2.1 实验目标

本实验旨在通过STM32的PWM输出功能驱动舵机,实现按键控制舵机旋转至指定角度。通过本实验,您将学习到:

  • 掌握如何根据舵机的控制要求(周期20ms,高电平0.5ms~2.5ms对应-90°~+90°或0°~180°)配置PWM参数(频率、占空比范围)。
  • 学习将角度值线性映射到PWM占空比(CCR值)的方法,使舵机能够精确转动到目标角度。
  • 理解多模块协同编程思想:将PWM底层驱动封装为独立模块,上层应用模块(舵机控制)调用底层接口,实现代码分层与复用。
  • 掌握按键输入检测与消抖处理,结合OLED显示实现人机交互。

6.2.2 硬件设计

6.2.3 软件设计

本实验的软件结构分为三层:底层硬件驱动(PWM、按键)、中间层舵机控制模块、上层应用逻辑(主循环)。各模块分工明确,便于维护和扩展。

1. PWM模块配置(PWM.c)和 按键模块(Key.c)

PWM模块负责输出符合舵机要求的波形,使用TIM2的通道2,引脚为PA1。关键配置如下:

  • 时钟使能:开启TIM2和GPIOA时钟。
  • GPIO初始化 :将PA1设置为复用推挽输出(GPIO_Mode_AF_PP),由TIM2控制引脚电平。
  • 时基单元
  • 预分频器PSC = 72 - 1,自动重装值ARR = 20000 - 1。
  • 计算得PWM频率 = 72MHz / (72 × 20000) = 50Hz,周期20ms,符合舵机控制要求。
  • 计数器模式为向上计数。
  • 输出比较
  • 选择PWM模式1(TIM_OCMode_PWM1),极性高有效(TIM_OCPolarity_High)。
  • 使能输出,初始CCR值设为0。
  • 使用TIM_OC2Init配置通道2。
  • 使能定时器TIM_Cmd启动TIM2。

同时提供PWM_SetCompare2函数,用于运行时修改CCR2的值,从而改变占空比。

按键模块则使用PB1和PB11两个引脚,配置为上拉输入。Key_GetNum函数通过查询方式检测按键按下,包含消抖和松手检测,返回键码(1或2)。本实验中仅使用按键1(PB1)。

2. 舵机控制模块(Servo.c)

舵机模块封装了PWM底层,对外提供简单易用的角度设置接口:

  • Servo_Init:调用PWM_Init初始化PWM。
  • Servo_SetAngle:接收角度参数(0~180°),通过线性映射公式 CCR = Angle / 180 × 2000 + 500 计算出对应的CCR值(500~2500),再调用PWM_SetCompare2设置占空比。该公式实现了角度与脉宽的线性对应:0°→500(0.5ms),180°→2500(2.5ms)。

3. 主程序逻辑(main.c)

  1. 初始化:依次初始化OLED、舵机、按键模块。OLED用于显示当前角度。

  2. 显示静态字符串:在OLED第一行显示"Angle:"提示。

  3. 主循环

  • 调用Key_GetNum获取按键状态。
  • 若按键1按下,角度变量Angle自增30°,超过180°则归零。
  • 调用Servo_SetAngle将当前角度写入舵机,驱动舵机旋转。
  • 更新OLED显示角度值,便于观察。

程序通过简单的按键触发和角度映射,实现了舵机的步进控制,展示了PWM在电机控制中的典型应用。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Servo.h"
#include "Key.h"

uint8_t KeyNum;			//定义用于接收键码的变量
float Angle;			//定义角度变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Servo_Init();		//舵机初始化
	Key_Init();			//按键初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Angle:");	//1行1列显示字符串Angle:
	
	while (1)
	{
		KeyNum = Key_GetNum();			//获取按键键码
		if (KeyNum == 1)				//按键1按下
		{
			Angle += 30;				//角度变量自增30
			if (Angle > 180)			//角度变量超过180后
			{
				Angle = 0;				//角度变量归零
			}
		}
		Servo_SetAngle(Angle);			//设置舵机的角度为角度变量
		OLED_ShowNum(1, 7, Angle, 3);	//OLED显示角度变量
	}
}

PWM.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA1引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;				//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/ 
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);                         //结构体初始化,若结构体没有完整赋值
	                                                                //则最好执行此函数,给结构体所有成员都赋一个默认值
	                                                                //避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;               //输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       //输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   //输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC2Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC2Init,配置TIM2的输出比较通道2
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare2(uint16_t Compare)
{
	TIM_SetCompare2(TIM2, Compare);		//设置CCR2的值
}

Servo.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "PWM.h"

/**
  * 函    数:舵机初始化
  * 参    数:无
  * 返 回 值:无
  */
void Servo_Init(void)
{
	PWM_Init();									//初始化舵机的底层PWM
}

/**
  * 函    数:舵机设置角度
  * 参    数:Angle 要设置的舵机角度,范围:0~180
  * 返 回 值:无
  */
void Servo_SetAngle(float Angle)
{
	PWM_SetCompare2(Angle / 180 * 2000 + 500);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}

Key.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/**
  * 函    数:按键初始化
  * 参    数:无
  * 返 回 值:无
  */
void Key_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB1和PB11引脚初始化为上拉输入
}

/**
  * 函    数:按键获取键码
  * 参    数:无
  * 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
  * 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
  */
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;		//定义变量,默认键码值为0
	
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)			//读PB1输入寄存器的状态,如果为0,则代表按键1按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 1;												//置键码为1
	}
	
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)			//读PB11输入寄存器的状态,如果为0,则代表按键2按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 2;												//置键码为2
	}
	
	return KeyNum;			//返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}

6.2.4 实验现象

下载程序后,OLED屏幕显示"Angle:0"。按下连接在PB1上的按键,舵机输出轴会旋转30°,OLED显示的角度值同步增加30°。每按一次按键,角度增加30°,依次为30°、60°、90°、120°、150°、180°,再次按键角度归零,舵机回到0°位置。

使用示波器测量PA1引脚,可观察到周期为20ms的PWM波形,高电平时间随角度变化在0.5ms~2.5ms之间步进。


6.3 PWM驱动直流电机

6.3.1 实验目标

本实验旨在通过STM32的PWM输出功能配合电机驱动芯片(TB6612)控制直流电机的转速和方向,实现按键步进调速与正反转切换。通过本实验,将学习到:

  • 掌握直流电机驱动的基本原理:通过H桥电路控制电机正反转,通过PWM占空比调节转速。
  • 学习如何将PWM模块与GPIO方向控制相结合,封装出简洁的电机控制接口(速度值-100~100)。
  • 理解PWM频率对电机运行的影响,学会选择合适频率以避免可闻噪音(本实验选择20kHz)。
  • 进一步熟悉模块化编程思想,将底层PWM、按键、OLED与电机控制逻辑分层实现。

6.3.2 硬件设计

6.3.3 软件设计

本实验软件由底层PWM驱动、电机控制模块、按键模块和主程序组成。电机模块封装了方向控制和速度设置,对外提供直观的带符号速度接口。

1. PWM模块配置(PWM.c)

PWM模块使用TIM2的通道3,引脚为PA2,输出20kHz的PWM波形。关键配置如下:

  • 时钟使能:开启TIM2和GPIOA时钟。
  • GPIO初始化 :PA2设置为复用推挽输出(GPIO_Mode_AF_PP)。
  • 时基单元:预分频器PSC = 36 - 1,自动重装值ARR = 100 - 1,计数器向上计数。PWM频率 = 72MHz / (36 × 100) = 20kHz,高于人耳听觉上限,避免电机产生可闻噪音。
  • 输出比较 :配置为PWM模式1,极性高有效,初始CCR=0,使能输出,使用TIM_OC3Init配置通道3。
  • 使能定时器:启动TIM2。

同时提供PWM_SetCompare3函数,用于运行时修改CCR3值,控制占空比(0~100)。

2. 电机控制模块(Motor.c)

电机模块管理两个部分:直流电机初始化和设置速度。

  • 直流电机初始化
  • 开启GPIOA时钟。
  • 将PA4、PA5配置为推挽输出(GPIO_Mode_Out_PP),用于控制TB6612的AIN1、AIN2引脚。
  • 调用PWM_Init初始化PWM。
  • 速度设置函数 Motor_SetSpeed(int8_t Speed)
  • 输入参数Speed范围-100~100,正值表示正转,负值表示反转。
  • 根据Speed符号设置方向引脚:正转:PA4高电平,PA5低电平。反转:PA4低电平,PA5高电平。
  • 调用PWM_SetCompare3设置PWM占空比:正转时直接传入Speed,反转时传入-Speed(因为PWM值需为正数)。
  • 这种设计实现了用单一有符号变量同时控制方向和转速。

3. 按键模块(Key.c)

与舵机实验相同,使用PB1和PB11两个上拉输入按键。Key_GetNum返回键码,本实验仅使用按键1(PB1)。

4. 主程序逻辑(main.c)

  1. 初始化:OLED、电机模块、按键模块依次初始化。
  2. 显示静态字符串:在OLED第一行显示"Speed:"。
  3. 主循环:
  • 调用Key_GetNum获取按键状态。
  • 若按键1按下,速度变量Speed增加20;若超过100,则跳转至-100(实现正反转循环切换)。
  • 调用Motor_SetSpeed将当前速度写入电机。
  • 更新OLED显示当前速度值(带符号,三位数)。

程序通过按键触发速度步进,直观展示了PWM调速和方向控制的效果。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Motor.h"
#include "Key.h"

uint8_t KeyNum;		//定义用于接收按键键码的变量
int8_t Speed;		//定义速度变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Motor_Init();		//直流电机初始化
	Key_Init();			//按键初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Speed:");		//1行1列显示字符串Speed:
	
	while (1)
	{
		KeyNum = Key_GetNum();				//获取按键键码
		if (KeyNum == 1)					//按键1按下
		{
			Speed += 20;					//速度变量自增20
			if (Speed > 100)				//速度变量超过100后
			{
				Speed = -100;				//速度变量变为-100
											//此操作会让电机旋转方向突然改变,可能会因供电不足而导致单片机复位
											//若出现了此现象,则应避免使用这样的操作
			}
		}
		Motor_SetSpeed(Speed);				//设置直流电机的速度为速度变量
		OLED_ShowSignedNum(1, 7, Speed, 3);	//OLED显示速度变量
	}
}

PWM.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA2引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;                 //计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1;               //预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/ 
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);                         //结构体初始化,若结构体没有完整赋值
	                                                                //则最好执行此函数,给结构体所有成员都赋一个默认值
	                                                                //避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;               //输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       //输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   //输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC3Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC3Init,配置TIM2的输出比较通道3
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare3(uint16_t Compare)
{
	TIM_SetCompare3(TIM2, Compare);		//设置CCR3的值
}

Motor.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "PWM.h"

/**
  * 函    数:直流电机初始化
  * 参    数:无
  * 返 回 值:无
  */
void Motor_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		//开启GPIOA的时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);						//将PA4和PA5引脚初始化为推挽输出	
	
	PWM_Init();													//初始化直流电机的底层PWM
}

/**
  * 函    数:直流电机设置速度
  * 参    数:Speed 要设置的速度,范围:-100~100
  * 返 回 值:无
  */
void Motor_SetSpeed(int8_t Speed)
{
	if (Speed >= 0)							//如果设置正转的速度值
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_4);	//PA4置高电平
		GPIO_ResetBits(GPIOA, GPIO_Pin_5);	//PA5置低电平,设置方向为正转
		PWM_SetCompare3(Speed);				//PWM设置为速度值
	}
	else									//否则,即设置反转的速度值
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);	//PA4置低电平
		GPIO_SetBits(GPIOA, GPIO_Pin_5);	//PA5置高电平,设置方向为反转
		PWM_SetCompare3(-Speed);			//PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
	}
}

Key.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/**
  * 函    数:按键初始化
  * 参    数:无
  * 返 回 值:无
  */
void Key_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB1和PB11引脚初始化为上拉输入
}

/**
  * 函    数:按键获取键码
  * 参    数:无
  * 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
  * 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
  */
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;		//定义变量,默认键码值为0
	
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)			//读PB1输入寄存器的状态,如果为0,则代表按键1按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 1;												//置键码为1
	}
	
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)			//读PB11输入寄存器的状态,如果为0,则代表按键2按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 2;												//置键码为2
	}
	
	return KeyNum;			//返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}

6.3.4 实验现象

下载程序后,OLED屏幕初始显示"Speed:0",此时电机处于静止状态。

操作说明:

  1. 每次按下PB1按键,速度值以20为增量逐步增加,依次显示20、40、60、80、100,此时电机正向转速随之提升(正值表示正向旋转,数值大小代表转速快慢);
  2. 再次按键后速度切换为-100,电机立即以最大速度反向旋转(负值表示反向旋转);
  3. 继续按键时,速度值依次变为-80、-60、-40、-20,最终归零停止;
  4. OLED屏幕实时更新当前速度值,电机转向与速度值的正负符号保持一致;

技术说明:

  • PWM工作频率为20kHz,确保电机运行时无噪声干扰
  • 注:在堵转情况下可能出现轻微啸叫声,此为正常现象
相关推荐
babe小鑫2 小时前
大专数据可视化技术专业学习数据分析的价值
学习·信息可视化·数据分析
bill_man2 小时前
RHI学习笔记(2)-Qt6的RHI结构
笔记
Non importa2 小时前
二分法:算法新手第三道坎
c语言·c++·笔记·qt·学习·算法·leetcode
山岚的运维笔记2 小时前
SQL Server笔记 -- 第74章:权限或许可 第75章:SQLCMD 第76章:资源调控器
数据库·笔记·sql·microsoft·oracle·sqlserver
舟舟亢亢2 小时前
Redis知识复习笔记(上)
数据库·redis·笔记
一 乐2 小时前
英语学习平台系统|基于springboot + vue英语学习平台系统(源码+数据库+文档)
java·vue.js·spring boot·学习·论文·毕设·英语学习平台系统
2023自学中4 小时前
笔记本电脑 连接 手机WIFI,开发板网线连接笔记本,开发板 和 虚拟机 同时上网
linux·单片机·嵌入式硬件·tcp/ip
宇木灵10 小时前
C语言基础学习-二、运算符
c语言·开发语言·学习
uran10 小时前
从电磁兼容到代码优化:STM32 GPIO速度与EMI的隐秘关联
stm32·gpio·emc·嵌入式优化