该笔记基于B站江科大视频内容结合自己的理解进行编写,如有错误,请指正。视频结合笔记共同食用,效果更佳哦~
TIM(Timer,定时器),是 STM32 中功能最强大、结构最复杂的一个外设,学习内容较多,所以 TIM 部分分为 4 个部分,本文为第 2 部分,围绕 TIM 的输出比较功能进行展开。输出比较这个模块最常见的作用就是产生 PWM 波形,用于驱动电机等设备,所以该部分将学习使用 STM32 输出的 PWM 波形驱动舵机和直流电机。
输出比较简介
输出比较功能主要用来输出 PWM 波形,PWM 波形又是驱动电机的必要条件,如果想用 STM32 实现电机相关项目,比如智能车、机器人等,就需要掌握好这个内容。
输出比较(OC,Output Compare),可以通过比较 CNT(计数器)和 CCR(捕获/比较寄存器)寄存器值的关系,来对输出电平进行置 1、置 0 或翻转的操作,从而输出特定频率和占空比的 PWM 波形。这就是输出比较模块最主要的功能。
CNT 计数自增,给定 CCR 一个值,当 CNT>CCR、CNT<CCR、CNT=CCR 时,输出就会对应的置 1 或 0,实现输出一个电平不断跳变的 PWM 波形。
所有高级定时器和通用定时器均配备 4 个输出比较通道。其中高级定时器的前 3 个通道还支持死区生成和互补输出功能,可以用于驱动三相无刷电机,本章暂时不作为重点内容,主要学习通用定时器的输出比较功能。
PWM 简介
PWM(Pulse Width Modulation,脉冲宽度调制)是一个数字输出信号,由高低电平组成的连续变化电平信号,如下图所示:

PWM 的作用是在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。简而言之,就是使用 PWM 波形等效地实现一个模拟信号的输出,如下图所示:

当高电平时间占比大于低电平,等效的模拟量偏上;当高电平时间占比小于低电平,等效的模拟量偏下。
在 LED 呼吸灯实验中,按理说对于数字输出控制 LED,LED 只能有亮灭两种状态,但通过 PWM 却可以实现亮度调节。PWM 让 LED 亮灭交替,当亮灭的频率足够高时,LED 就不会闪烁了,而是呈现出稳定的中间亮度。通过精确控制亮灭的时间比例,就能实现亮度调节,电机调速也是同样原理。这就是 PWM 的基本思想。
需要注意的是,PWM 的应用必须满足一个前提条件:系统需要具备惯性特性。比如 LED,由于余晖和人眼视觉暂留现象,LED 在断电后不会立即熄灭,而是有一定的惯性,需经过一段短暂的时间才会熄灭。同理,电机在断电时也不会瞬间停止,而是会经历一个逐渐减速的过程。这样具有惯性的系统才适用 PWM。
在使用 PWM 时,需要关注以下几个参数:
频率 = 1 高低电平跳变周期时间 T S 频率 = \frac{1}{高低电平跳变周期时间T_S} 频率=高低电平跳变周期时间TS1
PWM 频率越快,等效的模拟信号就越平稳,但性能开销越大。一般来说 PWM 频率在几千赫到几十千赫,这个频率已经足够快了。
占空比 = 高电平时间 T O N T S 占空比 = \frac{高电平时间T_{ON}}{T_S} 占空比=TS高电平时间TON
占空比=50%,表示高低电平时间相等;占空比=20%,表示高电平占 20%,低电平占 80%;占空比=100%,表示一直维持高电平;占空比=0%,表示一直维持低电平。这就是占空比,决定了 PWM 等效的模拟电压大小。占空比越大,等效的模拟电压越趋于高电平,占空比越小,等效的模拟电压越趋于低电平。这个等效关系一般来说是线性的,比如高电平=5V,低电平=0V,则占空比=50%等效中间电压 2.5V,占空比=20%等效 1V。
分辨率 = 占空比变化步距 分辨率 = 占空比变化步距 分辨率=占空比变化步距
比如有的占空比只能是 1%、2%、3% 这样以 1% 的步距跳变,其分辨率就是 1%;如果占空比可以 1.1%、1.2%、1.3% 这样以 0.1% 的步距跳变,其分辨率就是 0.1%。所以分辨率就是占空比变化的精细程度。分辨率的高低根据项目需求决定。如果既要高频率,又要高分辨率,这就对硬件电路要求比较高。要求不高,1% 的分辨率足够。
以上就是本小节的所有内容。
输出比较模块工作原理
通用定时器

上图对应通用定时器以下电路:

图3-1中,当【CNT>CCR1】或者【CNT=CCR1】时,会给【输出模式控制器】传递信号,然后【输出模式控制器】会改变其输出的【oc1ref】(ref 是 reference 的缩写,意为参考信号)的高低电平。
【输出模式控制器】上还有【ETRF】输入,是定时器的一个小功能,一般不用,暂时不需要了解。
接着【oc1ref】可以前往主模式控制器,即可以把该信号映射到主模式的 TRGO 输出上。

不过【oc1ref】的主要去向还是另一条路,前往极性选择电路。

当给寄存器【CC1P】写 0,信号往上走,电平保持不变,输出与输入一致;写 1,信号往下走,经非门取反后输出,实现电平翻转。这就是极性选择。
信号随后进入【输出使能电路】,由该电路决定是否进行输出。若选择输出,信号将被传送至【OC1】引脚,对应图3-2的 CH1 引脚,具体对应的GPIO端口可在引脚定义表中查询。
接下来看下【输出模式控制器】的执行逻辑,如下表所示:
| 模式 | 描述 | 举例 |
|---|---|---|
| 冻结 | CNT=CCR 时,REF 保持为原状态 | 当遥控车以50%占空比的速度行驶时: 1. 若进入冻结模式后引脚保持高电平,车辆将立即转为全速前进 2. 若引脚变为低电平,车辆将惯性滑行直至完全停止 |
| 匹配时置 有效电平 | CNT=CCR 时,REF 置有效电平 | 不适合输出连续变化的波形 |
| 匹配时置 无效电平 | CNT=CCR 时,REF 置无效电平 | 不适合输出连续变化的波形 |
| 匹配时电平翻转 | CNT=CCR 时,REF 电平翻转 | 当 CCR 设为 0 时,CNT 计数器每次清零都会触发 CNT=CCR 事件,导致输出电平翻转。由于每两次翻转构成一个完整周期,因此高低电平持续时间相等,从而实现 50% 的占空比。需注意,调整定时器更新频率将直接影响 PWM 信号的输出频率。 |
| 强制为无效电平 | 忽略 CNT 与 CCR,REF 强制为无效电平 | 遥控信号丢失时,遥控车停止驱动电机。 |
| 强制为有效电平 | 忽略 CNT 与 CCR,REF 强制为有效电平 | 无人机传感器检测到高度骤降时紧急全速拉升。 |
| PWM 模式 1 | 向上计数:CNT<CCR 时,REF 置有效电平,CNT≥CCR 时,REF 置无效电平 向下计数:CNT>CCR 时,REF 置无效电平,CNT≤CCR 时,REF 置有效电平 | - |
| PWM 模式 2 | 向上计数:CNT<CCR 时,REF 置无效电平,CNT≥CCR 时,REF 置有效电平 向下计数:CNT>CCR 时,REF 置有效电平,CNT≤CCR 时,REF 置无效电平 | - |
【输出模式控制器】的模式可以通过配置寄存器【OC1M[2:0]】进行选择。
“PWM 模式 1” 和 “PWM 模式 2” 只是极性上取反的区别,实际只使用 “PWM 模式 1” + “向上计数” 这种模式即可。那这种模式是如何输出频率和占空比都可调的 PWM 波形呢?

上图中,蓝线是【CNT 计数器】的值,黄线是【ARR 自动重装器】的值。蓝线从 0 开始递增,达到黄线数值后自动归零,从而开启新的计数周期。红线是【CCR 捕获/比较器】的值,绿线则显示 PWM 波形的实际输出。
观察得到,蓝线小于红线时(CNT<CCR),绿线处于高电平;蓝线大于红线时(CNT≥CCR),绿线处于低电平。
由此可见,绿线 PWM 的占空比是受红线 CCR 控制,CCR 越大,占空比越大,反之越小。黄线 ARR 越大,绿线 PWM 频率越小,ARR 越小,PWM 频率越大。
由此证明【REF】是一个频率和占空比均可调制的 PWM 波形。最终再经过【极性选择输出使能】输出到【GPIO】。这样就完成 PWM 波形的输出了。
图3-5中可以获得参数:
P
W
M
频率
F
r
e
q
=
计数器时钟
C
K
_
C
N
T
=
C
K
_
P
S
C
P
S
C
+
1
A
R
R
+
1
=
72
,
000
,
000
99
+
1
=
720
k
H
z
P
W
M
占空比
D
u
t
y
=
C
C
R
A
R
R
+
1
=
30
99
+
1
=
30
%
P
W
M
分辨率
R
e
s
o
=
1
A
R
R
+
1
=
1
99
+
1
=
1
%
\begin{aligned} PWM 频率Freq = 计数器时钟 CK\_CNT &= \frac{\frac{CK\_PSC}{PSC+1}}{ARR+1} \\ &= \frac{72,000,000}{99+1} \\ &= 720kHz \end{aligned} \\ PWM 占空比 Duty = \frac{CCR}{ARR+1} = \frac{30}{99+1}=30\% \\ PWM 分辨率 Reso = \frac{1}{ARR+1} = \frac{1}{99+1}=1\%
PWM频率Freq=计数器时钟CK_CNT=ARR+1PSC+1CK_PSC=99+172,000,000=720kHzPWM占空比Duty=ARR+1CCR=99+130=30%PWM分辨率Reso=ARR+11=99+11=1%
PWM 分辨率是指占空比可调节的最小单位(步距)。因此 CCR 的取值范围应该在 [0, ARR+1]。
CCR=ARR+1 时,占空比正好=100%,此时若继续增大 CCR 值也不会改变占空比,没有意义。因此,ARR 值决定了 CCR 的最大取值,ARR 越大则 CCR 调节范围越广,相应的 PWM 分辨率也越高。
高级定时器(可选)

虽然视频教程说明这部分电路仅需了解即可,无需掌握,但我仍决定记录下来,以备后续可能的使用需求。
与图3-1进行对比可见,高级定时器的输出比较部分仅额外增加了如下电路:

要理解上图所选电路的功能,需结合下图外部电路综合分析:

上图【P-MOS】和【N-MOS】左边是控制极,若【OC1】输入高电平,则【P-MOS】导通,若【OC1】输入低电平,则【P-MOS】断开,【N-MOS】同理。这就是一个基本的推挽电路。
若【P-MOS】导通,【N-MOS】断开,则输出高电平;若【P-MOS】断开,【N-MOS】导通,则输出低电平;若【P-MOS】【N-MOS】都导通,则电源短路,这是不允许的;若【P-MOS】【N-MOS】都断开,则输出就是高阻态。这就是推挽电路的工作流程。
采用两个图3-8所示的推挽电路即可构成H桥电路,实现对直流电机的正反转控制。若增加到三个推挽电路,则可用于驱动三相无刷电机。
若需要使用单片机控制推挽电路,就需要 2 个控制极,且这 2 个控制极需保持互补电平(即电平相反)。理解了推挽电路的这个基本要求后,图3-7中的电路原理就很容易掌握了。
【OC1】【OC1N】是一对互补的输出,在切换【P-MOS】【N-MOS】状态时,若【P-MOS】断开瞬间【N-MOS】导通,由于器件的不理想,可能【P-MOS】未完全断开,而【N-MOS】已经导通,出现了短暂的短路,这种短路会产生额外功率损耗并导致器件发热。
为解决这一问题,设计了死区生成电路。它会在【P-MOS】断开时延迟一段时间后再导通【N-MOS】,【N-MOS】断开时延迟一段时间后再导通【P-MOS】。通过这种时序控制,可确保两个MOS管不会同时导通。
以上就是死区生成和互补输出的内容,了解一下即可。
舵机简介
舵机是一种根据输入的 PWM 信号占空比来控制输出角度的装置。

上图舵机有 3 根输入线,2 根电源线,1 根信号线,PWM 信号就是通过信号线控制舵机角度。

从上图可以看出,舵机内部是由【直流电机】驱动的,【舵机内部电板】是电机的控制系统,大概的执行逻辑是,PWM 信号输入【舵机内部电板】后设定目标角度,然后【电位器】检测输出轴的当前角度,若大于目标角度,电机反转,若小于目标角度,电机正转,最终使输出轴固定在指定角度。
输入 PWM 信号的要求:
- 周期为 20ms,即 1 0.02 = 50 H z \frac{1}{0.02} = 50Hz 0.021=50Hz。
- 高电平宽度为 0.5ms 到 2.5ms,即占空比范围,占空比对应的角度如下图:

舵机中 PWM 波形其实是当作一个通信协议来使用的,和前文提到的用 PWM 波形等效模拟输出关系不大。


电机通常属于大功率设备,因此需要配备相应的大功率驱动电源。建议采用图4-5所示的独立【电机电源】供电方案。若无法单独供电,则需确保电源功率满足要求。
当使用独立供电时,需注意:
- 【电机电源】的负极必须与STM32共地
- 【电机电源】的正极连接至【5V】供电引脚。由于该舵机内置驱动电路,可直接与STM32引脚相连。
以上就是舵机的所有内容。
直流电机及驱动简介

直流电机是一种将电能转换为机械能的装置,有 2 个电极,当电极正接时,电机正转,当电极反接时,电机反转。
直流电机属于大功率器件,无法通过 GPIO 口直接驱动,需要配合电机驱动电路来操作。图5-1中的直流电机就是一个单独的电机,没有内置驱动电路,因此需要外接驱动电路进行控制。市场上有多种成熟的驱动方案可供选择,包括 TB6612、DRV8833、L9110 和 L298N 等常见的驱动芯片。此外,也可采用分离 MOS 管搭建驱动电路,这种方式不仅能实现更大的驱动功率,同时也为用户提供了自主搭建电路的选择。

TB6612 是一款采用双路H桥结构的直流电机驱动芯片,可同时驱动两个直流电机并精确控制其转速和转向。得益于 H 桥设计中每路配备的 4 个开关管,该芯片能够实现电机的正反转控制。相比之下,ULN2003 等单开关管结构的驱动芯片仅能实现单向电机控制。

上图展示的是 H 桥电路的基本结构,由左右两组推挽电路构成。其中【O1】和【O2】端子用于连接电机。当左上和右下开关导通时,电流路径为:左上→【O1】→电机→【O2】→右下;当左下和右上开关导通时,电流方向变为:右上→【O2】→电机→【O1】→左下。通过切换导通状态可改变电流流向,从而实现电机的正反转控制。


图5-4展示了TB6612电机驱动模块的硬件电路连接方式:
- 【VM】接【电机电源】正极,需使用独立的大电流电源(与舵机供电方式相同)。如图5-5所示,【VM】为驱动电压输入端,支持4.5V-10V输入,建议与电机额定电压匹配(例如5V电机接5V电源)。
- 【VCC】是逻辑电平输入端,范围是 2.7V 到 5.5V,这个要和控制器的电源保持一致,比如 STM32 是 3.3V 器件,【VCC】就接 3.3V。若是 51 单片机,是 5V 的器件,则【VCC】接 5V。
- 所有【GND】端子内部互通,连接系统负极时只需接一个即可。
- 【AO1】【AO2】接电机两极。
- 【PWMA】【AIN1】【AIN2】直接接 STM32 GPIO 口即可,【PWMA】要接 PWM 输出端,这三个引脚提供低功率控制信号,驱动电路将从【VM】获取电流驱动电机,实现小信号控制大功率设备。
B 路同理。 - 【STBY】是待机控制引脚,若接 GND,则驱动芯片不工作,处于待机状态,若接逻辑电源 VCC,则芯片正常工作。无需待机功能时,建议直接连接VCC。需要待机控制时,可连接任意GPIO引脚,通过输出高低电平来切换工作/待机状态。

上图中:
- 当【IN1】【IN2】为高电平,则【O1】【O2】输出低电平,【O1】【O2】没有电压差,电机不工作。
- 当【IN1】【IN2】为低电平,则【O1】【O2】关闭,电机不工作。
由此可见,只有当【IN1】和【IN2】电平不同时(一高一低),电机才可能运转。此时电机的运行状态完全取决于【PWM】信号电平,这正是利用PWM技术实现模拟信号等效控制的关键所在。
以上就是直流电机的所有内容。
实验:PWM 驱动 LED 呼吸灯
实验材料
- STM32F103C8T6
- 0.96 寸 4 脚OLED 显示屏
- LED 灯
- ST-LINK 仿真器
- 面包板

需要注意的是,这里 LED 的正极接在 PA0 引脚,负极接在 GND,与之前的连接方式相反,所以现在高电平点亮,低电平熄灭。
实验目标
通过配置 TIM2 通用定时器产生 1kHz 频率、1% 分辨率的 PWM 信号输出至 PA0 引脚,驱动 LED 实现呼吸效果。TIM2 采用内部时钟源作为驱动时钟。
在 OLED 中显示 CCR 值。(这是我自己加上的)
实验原理
已知:
P
W
M
频率
=
C
K
_
C
N
T
A
R
R
+
1
=
C
K
_
P
S
C
P
S
C
+
1
A
R
R
+
1
P
W
M
分辨率
=
1
A
R
R
+
1
PWM频率 = \frac{CK\_CNT}{ARR + 1} = \frac{\frac{CK\_PSC}{PSC+1}}{ARR+1} \\ PWM分辨率 = \frac{1}{ARR+1}
PWM频率=ARR+1CK_CNT=ARR+1PSC+1CK_PSCPWM分辨率=ARR+11
代入数值得:
A
R
R
+
1
=
100
P
S
C
+
1
=
C
K
_
P
S
C
(
A
R
R
+
1
)
×
P
W
M
频率
=
72
,
000
,
000
100
×
1000
=
720
\begin{aligned} &ARR + 1 = 100 \\ &PSC + 1 = \frac{CK\_PSC}{(ARR+1) \times PWM频率} = \frac{72,000,000}{100 \times 1000} = 720 \end{aligned}
ARR+1=100PSC+1=(ARR+1)×PWM频率CK_PSC=100×100072,000,000=720
核心代码
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#define ARR (100 - 1)
#define PSC (720 - 1)
uint8_t pwm_resolutoin; // PWM 分辨率
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "CCR:");
/* 1.开启 TIM、GPIO 时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启通用定时器TIM2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟
/* 2.配置时基单元 */
TIM_InternalClockConfig(TIM2); // 通用定时器TIM2的时钟源设置为内部时钟(定时器上电默认选择内部时钟,所以该行代码可以不写,但为了步骤完整还是协商比较好)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 设置外部时钟的输入滤波器采样频率,不影响计数周期,本次实验可随便选
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数器选择向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = ARR; // 自动重装器值 ARR 设置为为 99,即计数 100 次
TIM_TimeBaseInitStructure.TIM_Prescaler = PSC; // 预分频器值 PSC 设置为 719,即分频系数为 720,将内部时钟从 72,000,000Hz 降到 100000Hz,即 100kHz,此为 CK_CNT
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器值设置为 0,因为TIM2是通用定时器,没有该模块
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); // 初始化时基单元
/* 3.配置输出比较单元 */
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); // 给配置结构体赋值默认值,因为部分配置项不需要在通用定时器中使用,为避免出现错误,可先赋值默认值,再修改需要使用的配置项
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 设置输出比较模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 设置输出比较匹配时的极性,TIM_OCPolarity_High 表示比较匹配时置高电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 设置输出使能状态,决定能否输出 PWM 到引脚
TIM_OCInitStructure.TIM_Pulse = 0; // 设置捕获/比较寄存器 CCR 的初始值为 0,即占空比为 0%,LED 是熄灭状态
TIM_OC1Init(TIM2,&TIM_OCInitStructure); // 设置 TIM2 使用 OC1 通道输出 PWM 信号
/* 4.配置 GPIO */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // TIM2 使用 OC1 通道输出信号需要引脚具备 TIM2_CH1 的功能,查阅手册可以得知 PA0 符合条件
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 选择复用推挽输出模式。开漏/推挽输出是由输出数据寄存器控制的,若想让定时器控制引脚,就需要使用复用开漏/推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 5.运行控制 */
TIM_Cmd(TIM2, ENABLE); // 启动 TIM2 通用定时器,这样 PWM 波形就可以通过 PA0 引脚输出了
while(1)
{
for(pwm_resolutoin = 0; pwm_resolutoin < ARR; pwm_resolutoin++)
{
OLED_ShowNum(1, 5, pwm_resolutoin, 3);
TIM_SetCompare1(TIM2, pwm_resolutoin); // 设置 CCR
Delay_ms(10);
}
for(pwm_resolutoin = ARR; pwm_resolutoin > 0; pwm_resolutoin--)
{
OLED_ShowNum(1, 5, pwm_resolutoin, 3);
TIM_SetCompare1(TIM2, pwm_resolutoin); // 设置 CCR
Delay_ms(10);
}
}
}
实验:PWM 驱动舵机
实验材料
- STM32F103C8T6
- 0.96 寸 4 脚OLED 显示屏
- SG90 舵机
- 直插 2 脚轻触开关
- ST-LINK 仿真器
- 面包板

舵机正极接 5V 电压,STM32 无法提供足够电压,无法驱动舵机,所以接在 ST-LINK 仿真器的 5V 输出,由 USB 提供。
实验目标

利用 TIM2-CH2 输出 50Hz PWM(0.5 - 2.5ms 脉宽对应 0~180°),每按一次 PB1 按键角度增加 30°,超 180° 归零,并在 OLED 屏上显示当前角度值。
实验原理
F
P
W
M
=
1
s
20
m
s
=
1
0.02
=
50
H
z
F_{PWM} = \frac{1s}{20ms} = \frac{1}{0.02} = 50Hz
FPWM=20ms1s=0.021=50Hz
F P W M = C K _ C N T A R R + 1 = C K _ P S C P S C + 1 A R R + 1 = 72 , 000 , 000 P S C + 1 A R R + 1 = 50 F_{PWM} = \frac{CK\_CNT}{ARR + 1} = \frac{\frac{CK\_PSC}{PSC+1}}{ARR+1} = \frac{\frac{72,000,000}{PSC+1}}{ARR+1} = 50 FPWM=ARR+1CK_CNT=ARR+1PSC+1CK_PSC=ARR+1PSC+172,000,000=50
PSC 和 ARR 的值并不是唯一的,可以自己多次尝试,这里直接使用:
P
S
C
+
1
=
72
A
R
R
+
1
=
20
,
000
PSC + 1 = 72 \\ ARR + 1 = 20,000
PSC+1=72ARR+1=20,000
故:
−
90
°
:
C
C
R
A
R
R
+
1
=
0.5
m
s
20
m
s
,则
C
C
R
=
0.5
×
20000
20
=
500
-90°:\frac{CCR}{ARR+1} = \frac{0.5ms}{20ms},则CCR = \frac{0.5 \times 20000}{20} = 500
−90°:ARR+1CCR=20ms0.5ms,则CCR=200.5×20000=500
90 ° : C C R A R R + 1 = 2.5 m s 20 m s ,则 C C R = 2.5 × 20000 20 = 2500 90°:\frac{CCR}{ARR+1} = \frac{2.5ms}{20ms},则CCR = \frac{2.5 \times 20000}{20} = 2500 90°:ARR+1CCR=20ms2.5ms,则CCR=202.5×20000=2500
得到 CCR 的取值范围为 500 到 2500。
核心代码
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#define ARR (20000 - 1)
#define PSC (72 - 1)
uint8_t Angle; // 舵机旋转轴角度
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "Angle:");
OLED_ShowNum(1, 7, Angle, 3);
/* 1.开启 TIM、GPIO 时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启通用定时器 TIM2 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启 GPIOB 时钟
/* 2.配置时基单元 */
TIM_InternalClockConfig(TIM2); // 通用定时器TIM2的时钟源设置为内部时钟(定时器上电默认选择内部时钟,所以该行代码可以不写,但为了步骤完整还是协商比较好)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 设置外部时钟的输入滤波器采样频率,不影响计数周期,本次实验可随便选
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数器选择向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = ARR; // 自动重装器值 ARR 设置为为 19999,即计数 20000 次
TIM_TimeBaseInitStructure.TIM_Prescaler = PSC; // 预分频器值 PSC 设置为 71,即分频系数为 72,将内部时钟从 72,000,000Hz 降到 1,000,000Hz,即 1MHz,此为 CK_CNT
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器值设置为 0,因为TIM2是通用定时器,没有该模块
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); // 初始化时基单元
/* 3.配置输出比较单元 */
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); // 给配置结构体赋值默认值,因为部分配置项不需要在通用定时器中使用,为避免出现错误,可先赋值默认值,再修改需要使用的配置项
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 设置输出比较模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 设置输出比较匹配时的极性,TIM_OCPolarity_High 表示比较匹配时置高电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 设置输出使能状态,决定能否输出 PWM 到引脚
TIM_OCInitStructure.TIM_Pulse = 0; // 设置捕获/比较寄存器 CCR 的初始值为 0,即占空比为 0%,LED 是熄灭状态
TIM_OC2Init(TIM2,&TIM_OCInitStructure); // 设置 TIM2 使用 OC2 通道输出 PWM 信号
/* 4.配置 GPIO */
GPIO_InitTypeDef GPIO_InitStructure_PWM;
GPIO_InitStructure_PWM.GPIO_Pin = GPIO_Pin_1; // TIM2 使用 OC2 通道输出信号需要引脚具备 TIM2_CH2 的功能,查阅手册可以得知 PA1 符合条件
GPIO_InitStructure_PWM.GPIO_Mode = GPIO_Mode_AF_PP; // 选择复用推挽输出模式。开漏/推挽输出是由输出数据寄存器控制的,若想让定时器控制引脚,就需要使用复用开漏/推挽输出模式
GPIO_InitStructure_PWM.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure_PWM);
GPIO_InitTypeDef GPIO_InitStructure_Button;
GPIO_InitStructure_Button.GPIO_Mode = GPIO_Mode_IPU; // 选择上拉输入模式,默认高电平
GPIO_InitStructure_Button.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure_Button.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure_Button);
/* 5.运行控制 */
TIM_Cmd(TIM2, ENABLE); // 启动 TIM2 通用定时器,这样 PWM 波形就可以通过 PA0 引脚输出了
TIM_SetCompare2(TIM2, 500); // 舵机旋转轴角度归零(复位)
while(1)
{
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
// 消除按键抖动
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);
Angle += 30; // 每次按键旋转30°
// 超过舵机旋转角度则复位
if(Angle > 180)
{
Angle = 0;
}
TIM_SetCompare2(TIM2, Angle / 180.0 * 2000 + 500); // 将角度转换为 CCR
OLED_ShowNum(1, 7, Angle, 3);
}
}
}
实验:PWM 驱动直流电机
实验材料
- STM32F103C8T6
- 0.96 寸 4 脚OLED 显示屏
- 直流电机
- TB6612 电机驱动模块
- ST-LINK 仿真器
- 面包板

实验目标
在 STM32 上利用 PWM 驱动直流电机,通过按键循环调速:每次按下按键(消抖后)速度 +20,超过 100 后自动归零并立即反转方向;转速值始终保持在 0-100 之间;OLED 实时显示带符号的转速值(-100到100)。
核心代码
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#define ARR (200 - 1)
#define PSC (720 - 1)
uint8_t Speed; // 直流电机转速,速度范围规定为 0 ~ 100
int8_t Direaction = 1; // 旋转方向,1 代表正转,-1 代表反转
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "Speed:");
OLED_ShowSignedNum(1, 7, Speed, 3);
/* 1.开启 TIM、GPIO 时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启通用定时器 TIM2 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启 GPIOB 时钟
/* 2.配置时基单元 */
TIM_InternalClockConfig(TIM2); // 通用定时器TIM2的时钟源设置为内部时钟(定时器上电默认选择内部时钟,所以该行代码可以不写,但为了步骤完整还是协商比较好)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 设置外部时钟的输入滤波器采样频率,不影响计数周期,本次实验可随便选
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数器选择向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = ARR; // 自动重装器值 ARR 设置为为 99,即计数 100 次
TIM_TimeBaseInitStructure.TIM_Prescaler = PSC; // 预分频器值 PSC 设置为 719,即分频系数为 720,将内部时钟从 72,000,000Hz 降到 100000Hz,即 100kHz,此为 CK_CNT
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器值设置为 0,因为TIM2是通用定时器,没有该模块
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); // 初始化时基单元
/* 3.配置输出比较单元 */
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); // 给配置结构体赋值默认值,因为部分配置项不需要在通用定时器中使用,为避免出现错误,可先赋值默认值,再修改需要使用的配置项
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 设置输出比较模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 设置输出比较匹配时的极性,TIM_OCPolarity_High 表示比较匹配时置高电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 设置输出使能状态,决定能否输出 PWM 到引脚
TIM_OCInitStructure.TIM_Pulse = 0; // 设置捕获/比较寄存器 CCR 的初始值为 0,即占空比为 0%,LED 是熄灭状态
TIM_OC3Init(TIM2,&TIM_OCInitStructure); // 设置 TIM2 使用 OC3 通道输出 PWM 信号
/* 4.配置 GPIO */
GPIO_InitTypeDef GPIO_InitStructure_PWM;
GPIO_InitStructure_PWM.GPIO_Pin = GPIO_Pin_2; // TIM2 使用 OC3 通道输出信号需要引脚具备 TIM2_CH3 的功能,查阅手册可以得知 PA2 符合条件
GPIO_InitStructure_PWM.GPIO_Mode = GPIO_Mode_AF_PP; // 选择复用推挽输出模式。开漏/推挽输出是由输出数据寄存器控制的,若想让定时器控制引脚,就需要使用复用开漏/推挽输出模式
GPIO_InitStructure_PWM.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure_PWM); // 配置 PWM 输出引脚
GPIO_InitTypeDef GPIO_InitStructure_Motor;
GPIO_InitStructure_Motor.GPIO_Mode = GPIO_Mode_Out_PP; // 选择推挽输出模式
GPIO_InitStructure_Motor.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure_Motor.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure_Motor); // 配置直流电机方向控制引脚
GPIO_InitTypeDef GPIO_InitStructure_Button;
GPIO_InitStructure_Button.GPIO_Mode = GPIO_Mode_IPU; // 选择上拉输入模式,默认高电平
GPIO_InitStructure_Button.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure_Button.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure_Button); // 配置按钮引脚
/* 5.运行控制 */
TIM_Cmd(TIM2, ENABLE); // 启动 TIM2 通用定时器,这样 PWM 波形就可以通过 PA2 引脚输出了
// TIM_SetCompare3(TIM2, 0); // 舵机旋转轴角度归零(复位)
while(1)
{
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
// 消除按键抖动
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);
Speed += 20;
if(Speed > 100) // 速度超过设置范围时归零且切换旋转方向
{
Speed = 0;
Direaction = -Direaction;
}
if(Direaction == 1)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
}
TIM_SetCompare3(TIM2, Speed); // 设置 CCR 控制旋转速度
OLED_ShowSignedNum(1, 7, Speed * Direaction, 3);
}
}
}
STM32 PWM输出比较应用详解
1915

被折叠的 条评论
为什么被折叠?



