前言
第六章 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_CCMRx 中 OCxM 位的配置(如 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_CCER 的 CCxP 位控制。它决定了"有效电平"对应的是物理上的高电平还是低电平。逻辑关系如下:
| CCxP 配置 | 逻辑关系 | 结果说明 |
|---|---|---|
| 0 | 正极性 (同相) | OCxREF 为高 -> 引脚输出 高电平 |
| 1 | 负极性 (反相) | OCxREF 为高 -> 引脚输出 低电平 |
输出使能(Enable) 由 TIMx_CCER 的 CCxE 位控制。
- CCxE = 1:开启输出开关,信号通过 GPIO 引脚(如 PA0)输出。
- CCxE = 0:关闭输出。
只有在输出使能的情况下,信号才会真正出现在对应的定时器输出引脚 TIMx_CHx 上。例如,TIM2_CH1默认映射到PA0引脚。

总结
将上述流程串联起来,我们可以清晰地看到 PWM 波形的生成路径:
- CNT 在 0 到 ARR 之间循环计数(决定频率)。
- CCR 设定了一个阈值切割点。
- 比较器 根据 CNT 与 CCR 的关系,输出逻辑电平。
- 极性电路 决定最终的高低电平。
最终效果: 随着 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 的硬件架构主要由微型直流电机、减速齿轮组、控制芯片及电位器(位置传感器)构成。如图所示:

其完整控制环路如下:
- 指令接收:单片机通过信号线输入特定脉宽的 PWM 信号,该信号脉宽代表目标预期角度。
- 误差计算:控制芯片内部的基准电路将接收到的 PWM 信号转化为目标参考电压;同时,实时读取与输出轴同轴转动的电位器电压,该电压代表当前实际角度。
- 驱动执行:控制芯片对目标电压与实际电压进行比较运算,产生电压差。电压差的正负号决定电机的旋转方向,电压差的大小影响旋转速度。
- 稳定定位:电机旋转时,通过减速齿轮组带动输出轴和电位器同步转动。当实际角度逼近目标角度时,电压差趋于零,电机随之停转,输出轴即稳定保持在指定位置。
5.1.3 PWM 控制协议与角度映射
为了实现对舵机的精确控制,必须向其信号线输入符合特定时序规范的 PWM(脉冲宽度调制)信号。对于 SG90,其标准控制协议需满足以下要求:
- 信号周期:20ms(对应 PWM 频率为 50Hz)。
- 有效脉宽:高电平的持续时间被严格限制在 0.5ms~2.5ms 之间。
此微秒级的高电平脉宽,与舵机的旋转角度呈现严格的线性映射关系。以标准 180° 舵机为例,其对应关系如下:
| 高电平持续时间 | 对应角度(0°~180° 标定) | 对应角度(-90°~+90° 标定) |
|---|---|---|
| 0.5ms | 0° | -90° |
| 1.0ms | 45° | -45° |
| 1.5ms | 90° (中位) | 0° |
| 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 电机的工作电流需求。若强行驱动,将导致引脚过载损坏。工程实践中必须引入集成化功率驱动芯片(如 TB6612FNG 或 L298N)。驱动芯片通过 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)
-
初始化:依次初始化OLED、舵机、按键模块。OLED用于显示当前角度。
-
显示静态字符串:在OLED第一行显示"Angle:"提示。
-
主循环:
- 调用
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)
- 初始化:OLED、电机模块、按键模块依次初始化。
- 显示静态字符串:在OLED第一行显示"Speed:"。
- 主循环:
- 调用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",此时电机处于静止状态。
操作说明:
- 每次按下PB1按键,速度值以20为增量逐步增加,依次显示20、40、60、80、100,此时电机正向转速随之提升(正值表示正向旋转,数值大小代表转速快慢);
- 再次按键后速度切换为-100,电机立即以最大速度反向旋转(负值表示反向旋转);
- 继续按键时,速度值依次变为-80、-60、-40、-20,最终归零停止;
- OLED屏幕实时更新当前速度值,电机转向与速度值的正负符号保持一致;
技术说明:
- PWM工作频率为20kHz,确保电机运行时无噪声干扰
- 注:在堵转情况下可能出现轻微啸叫声,此为正常现象