TIM 输出比较-stm32入门

1. 输出比较(OC)简介

1.1 基本概念

OC(Output Compare)输出比较
主要作用:用来输出 PWM 波形,PWM 波形又是驱动电机的必要条件。

如果想用 STM32 做一些有电机的项目,比如智能车、机器人等,需要好好掌握输出比较功能。

输出比较可以通过比较 CNT 计数器与 CCR 捕获/比较寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形。

  • CNT:时基单元里面的计数器
  • CCR:捕获/比较寄存器(输入捕获和输出比较共用的。

CNT 计数自增,CCR 是我们给定的一个值,当 CNT > CCR、< CCR 或者 = CCR 时,这里输出就会对应的置 1、置 0、置 1、置 0。这样就可以输出一个电平不断跳变的 PWM 波形了。这就是输出比较的基本功能。

每个高级定时器和通用定时器都拥有4个输出比较通道,可以同时输出四路 PWM 波形。这四个通道有各自的 CCR 寄存器,但是它们共用一个 CNT 计数器的。

  • 基本定时器没有输入捕获和输出比较功能
  • 高级定时器的前3个通道额外拥有死区生成和互补输出的功能,是用于驱动三相无刷电机的。

1.2 PWM 简介

PWM(Pulse Width Modulation)脉冲宽度调制,是一个数字输出信号,由高低电平组成。

基本功能:在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。(使用 PWM 波形,是用来等效的实现一个模拟信号的输出)

比如数字端口实现 LED 呼吸灯:GPIO 口是一个数字输出的端口,输出电平不是高电平就是低电平;按理来说 LED 只有完全亮和完全灭两种状态,怎么能实现 LED 连续的调控亮度呢?这就是 PWM 的功劳。

  • 我们让 LED 不断点亮 、熄灭、点亮、熄灭;当这个点亮、熄灭的频率足够大时,LED 就不会闪烁了,而是呈现一个中等亮度,当我们调控这个点亮和熄灭的时间比例时,就能让 LED 呈现出不同的亮度级别。
  • 对于电机调速也是一样。我们以一个很快的频率给电机通电、断电、通电、断电,那么电机的速度就能维持在一个中等速度,这就是 PWM 的基本思想。

PWM 的应用场景必须是一个惯性系统。就是说 LED 在熄灭的时候,由于余晖和人眼视觉暂留现象,LED 不会立马熄灭,而是有一定的惯性,过一小段时间才会熄灭;电机也是,当电机断电时,电机的转动不会立马停止,而是有一定的惯性,过一会才停。这样具有惯性的系统,才能使用 PWM。

在这里插入图片描述
高低电平跳变的数字信号,可以等效为中间这个虚线所表示的模拟量。当上面电平时间长一点,下面电平时间短一点,等效的模拟量就偏向于上面;当下面电平时间长一点,上面电平时间短一点,等效的模拟量就偏向于下面;

PWM参数

  1. 频率 = 1 / TS :代表一个高低电平变换周期的时间的倒数。

PWM 的频率越快,它等效模拟的信号就越平稳,不过同时性能开销就越大。一般来说 PWM 的频率都在 几K~几十KHz,这个频率就已经足够快了。

  1. 占空比 = TON / TS :高电平的时间相对于整个周期的时间的比例,一般用百分比表示。

占空比决定了 PWM 等效出来的模拟电压的大小。占空比越大,等效的模拟电压就趋近于高电平;占空比越小,等效的模拟电压就趋近于低电平。等效关系一般来说是线性的。

  1. 分辨率 = 占空比变化步距:比如有的占空比只能是 1%、2%、3%等等这样以 1% 的步距跳变,那它的分辨率就是 1%;如果可以以 1.1%、1.2%、1.3% 等等这样以 0.1% 的步距跳变,那它的分辨率就是 0.1%。所以分辨率就是占空比变化的精细程度。

如果你既要高频率,又要高分辨率,这就对硬件电路要求比较高了。一般要求不高的话, 1% 的分辨率就也足够使用了。

1.3 输出比较模块

1.3.1 通用定时器

在这里插入图片描述
输入部分是 CNT 与 CCR 比较的结果,本模块是输出比较电路,最后通过 TIM_CH1 输出到 GPIO 引脚上。

  • 输入部分连接的是 CNT 计数器和 CCR1 第一路的捕获/比较寄存器,当 CNT > 或者 = CCR1 时,就会给这个输出模式控制器传一个信号,然后输出模式控制器就会改变它输出 OC1REF(REF:reference 缩写,意思是参考信号) 的高低电平。

上面的 ETRF 输入是定时器的一个小功能,一般不用,不需要了解。

  • 接着这个 REF 信号可以前往主模式控制器,可以把 REF 映射到主模式的 TRGO 输出上去;不过 REF 的主要去向还是下面这一路,达到极性选择模块,给寄存器写 0,信号就会往上走,就是信号电平不反转;写 1 的话,信号就会往下走,就是信号通过一个非门取反,那输出信号就是输入信号高低电平反转的信号,这就是极性选择,就是选择是不是要把高低电平反转一下。

  • 接着就是输出使能电路了,选择要不要输出,最后就是 OC1 引脚,这个引脚就是 CH1 通道的引脚,在引脚定义表里就可以知道具体是哪个 GPIO 口了。

1.3.2 输出模式控制器

输出比较的 8 种模式:(输入的是 CNT 和 CRR 大小关系,输出的是 REF 的高低电平)

模式描述
冻结CNT=CCR时,REF保持为原状态
匹配时置有效电平CNT=CCR时,REF置有效电平
匹配时置无效电平CNT=CCR时,REF置无效电平
匹配时电平翻转CNT=CCR时,REF电平翻转
强制为无效电平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置无效电平

模式可以通过寄存器来进行配置。

模式作用:

  • 冻结模式:CNT 和 CCR 无效,REF 保持不变,维持上一个状态即可。比如你正在输出 PWM 波,突然想暂停一会输出,就可以设置成这个模式,一旦切换成冻结模式后,输出就暂停了,并且高低电平也维持为暂停时刻的状态,保持不变。
  • 匹配时三个模式:有效电平与无效电平,一般是高级定时器里面的说法,是和关断、刹车这些功能配合表述的,说的比较严谨,在这里为了理解方便,可以直接认为置有效电平就是置高电平,置无效电平就是置低电平。都是在 CNT = CRR 时,执行操作,这些模式就可以用作波形输出,比如相等时电平翻转模式可以方便的输出一个频率可调,占空比始终为 50% 的 PWM 波形。比如你设置 CCR 为 0,那 CNT 每次更新清 0 时,就会产生一次 CNT = CCR 的事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期,并且高电平和低电平的时间始终相等,也就是占空比始终为 50%。当你改变定时器更新频率时,输出波形的频率也会随之改变(输出波形的频率 = 更新频率 / 2),这就是匹配时电平翻转模式的用途。上面两个模式不适合输出连续变化的波形,感觉用处不大,如果你想定时输出一个一次性的信号,可以考虑这两个模式。
  • 强制输出模式,和冻结模式差不多。如果你想暂停波形输出,并且在暂停期间保持低电平或者高电平,就可以设置这两个强制输出模式。
  • PWM1 模式和 PMW2 模式,可以用于输出频率和占空比都可调的 PWM 波形,是我们主要使用的模式(PWM1 模式下一般我们都只使用向上计数)。PWM2 模式实际上就是 PWM1 模式的取反,改变 PWM1 模式和 PMW2 模式,就只是改变了 REF 电平的极性而已。由于 REF 输出之后还有一个极性的配置,所以使用 PWM1 模式的正极性和 PWM2 模式的反极性最终的输出是一样的。

1.3.2 高级定时器

在这里插入图片描述
在它外面通常要接一个这样的电路:上面是正极,接着是大功率开关管(一般是MOS管,就是一种大功率电子开关,然后再来一个MOS管,最后到GND,MOS管左边是控制极,比如说给高电平右边两根线就导通,低电平就断开;下面也一样,有一个控制极,高电平导通,低电平断开,这就是一个最基本的推挽电路,中间是输出。如果上管导通、下管断开,那输出就是高电平;如果下管导通、上管断开,那输出就是低电平;如果上下管都导通,那就是电源短路,是不允许的;如果上下管都断开,那输出就是高阻态,这就是推挽电路工作流程。如果有两个这样的推挽电路,就构成了H桥电路,就可以控制直流电机正反转,如果有三个这样的推挽电路,就可以用于驱动三相无刷电机,这就是这个电路的用途。

如果直接用单片机来控制的话,那就需要两个控制极,并且这两个控制极电平是相反的,也就是互补,因为上管导通、下管就必须断开,下管导通、上管就必须断开。

知道外围电路的需要,再来理解内部电路结构自然就好理解了,首先这个OC1和OC1N就是两个互补的输出端口,分别控制上管和下管的导通和关闭,然后是在切换上下管导通状态时,如果在上管关断的瞬间,下管就立刻打开,那可能会因为器件不理想,上管还没完全关断,下管就已经导通了,出现了短暂的上下管同时导通的现象,这会导致功率损耗,引起器件发热,所以为了避免这个问题,就有了死区生成电路,它会在上管关闭的时候,延迟一小段时间,再导通下管,下管关闭的时候,延时一小段时间,再导通上管,这样就可以避免上下管同时导通的现象。这就是死区生成和互补输出的用途。

1.4 PWM 基本结构

在这里插入图片描述

  1. 时钟源选择、时基单元和运行控制我们还需要继续使用,只不过后面的更新事件的中断申请,我们不需要了。输出 PWM 暂时还不需要中断。配置好时基单元,CNT 就开始不断的自增运行了。
  2. 下面是输出比较单元,总共有四步:最开始是 CCR 捕获/比较寄存器,CCR 是我们自己设定的,CNT 不断自增运行,同时它两还在不断进行比较,后面是输出模式控制器,输出的 REF 就是一个频率可调,占空比也可调的 PWM 波形,最终再经过极性选择,输出使能,最终通向 GPIO 口,这样就能完成 PWM 波形的输出了。

右上角蓝色是CNT,黄色是 ARR,蓝色线从 0 开始自增,一直增到 ARR,也就是 99,之后清 0 继续自增;在这个过程中我们再设置一条红色线 CCR,比如设置为 30,之后再执行输出模式的逻辑,下面的绿色线就是输出。可以看到 REF 的值不断变化,并且它的占空比是受 CCR 值得调控的,如果 CCR 设置高一些,输出的占空比就变大;CCR 设置低一些,输出的占空比就变小,这就是 PWM 的工作流程。

1.5 PWM 参数计算

在这里插入图片描述

  • PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)

PWM 的周期始终对应着计数器的一个溢出更新周期。所以 PWM 的频率等于计数器的更新频率。

  • PWM占空比: Duty = CCR / (ARR + 1)

在等于 30 的瞬间,就已经跳变为低电平了,所以 CNT 是从 0~29 是高电平,总共 30 个数的时间;所以这里占空比就是 30 / 100 = 30%。

  • PWM分辨率(占空比变化的步距): Reso = 1 / (ARR + 1)

CCR 的变化范围取决于 ARR 的值,CCR 越大,对应的分辨率就越大,占空比变化的越细腻,越好。

2. 三个定时器输出比较功能案例

2.1 PWM 驱动 LED 呼吸灯

2.1.1 硬件电路图

在这里插入图片描述
PA0 引脚输出 PWM 波形驱动 LED,并呈现不同的亮度。LED 正极接在 PA0 引脚,负极接 GND 的驱动方法,这样就是高电平点亮、低电平熄灭的正极性驱动方法,这样接的话观察更直观一些,就是占空比越大,LED 越亮,占空比越小,LED 越暗。

2.1.2 定时器输出比较模块初始化

  • 定时器输出比较模块的库函数
//用来配置输出比较(Output Compare)模块的,输出比较模块有 4 个,一个函数配置一个单元。
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);//(选择定时器,结构体)
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);//用来给输出比较结构体赋一个默认值
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);//仅高级定时器使用,在使用高级定时器输出PWM时需要调用,使能主输出,否则 PWM 将不能正常输出
//小功能和运行时更改参数的函数
void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);//用来配置强制输出模式的,如果在运行中想要暂停输出波形并且强制输出高低电平,可以使用,不过一般用的不多,因为强制输出高电平和设置 100% 占空比是一样的,强制输出低电平和设置 0% 占空比是一样的,所以四个函数了解即可,不需要掌握。
void TIM_ForcedOC2Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC3Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC4Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);//用来配置CCR寄存器的预装功能的,这个预装功能就是影子寄存器,一般可以不用,了解即可,不需要掌握。
void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC1FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);//用来配置快速使能,手册里单脉冲模式有介绍,用的不多,不需要掌握
void TIM_OC2FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC3FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC4FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);//手册外部事件时清除REF信号有介绍,不需要掌握
void TIM_ClearOC2Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC3Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC4Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);//用来单独设置输出比较的极性的,带N的就是高级定时器里的互补通道的配置,结构体初始化那个函数里也可以设置极性,作用是一样的,只不过用结构体是一起初始化的,在这里是单独的函数进行修改的
//一般来说,结构体里的参数,都会有一个单独的函数可以进行更改,这里的函数就是用来单独更改输出极性的。
void TIM_OC1NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC2PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC2NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC3PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC3NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx);//单独修改输出使能参数的
void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);
void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);//选择输出比较模式,用来单独更改输出比较模式的函数
//用来单独更改 CCR 寄存器值的函数,这四个函数比较重要,我们在运行的时候,更改占空比,就需要用到这四个函数
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);

  • GPIO 口与 TIM 输出比较(OC)的通道(CH)的复用关系查询引脚定义表。
    在这里插入图片描述
  • 只能在固定引脚输出,不能任意选择引脚输出。
  • STM32 的重映射/重定义功能给予部分复用功能一次更改引脚的机会来避免两个外设引脚的冲突。如果重映射列表里差不多,那么外设复用的 GPIO 就不能挪位置,这就是重映射的功能,配置重映射用 AFIO 来完成的。
  • 这些就是外设引脚和GPIO引脚的复用关系和重映射的介绍,我们在使用外设的引脚时,需要多参考一下引脚定义表。

TIM2_CH1_ETR 只能使用 PA0 引脚或者重映射的 PA15 引脚,再其他的引脚,就没有机会作为这个通道的输出引脚了。

  • 配置定时器(TIM2 通用定时器)
  1. RCC开启时钟,把我们要用的 TIM 外设和 GPIO 外设的时候都打开
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//TIM2 是 APB1 总线的外设
  1. 配置时基单元,包括前面的时钟源选择
TIM_InternalClockConfig(TIM2);//选择内部时钟
	
TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,向上计数
//定时 1 s(记公式/理解工作流程)
TIMTimeBaseInitStructure.TIM_Period = 100 - 1;//ARR 的值(取值范围0~65535)
TIMTimeBaseInitStructure.TIM_Prescaler = 720 - 1;//PSC 的值(取值范围0~65535),0````````:不需要分频
TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
TIM_TimeBaseInit(TIM2, &TIMTimeBaseInitStructure);
  1. 配置输出比较单元(CRR的值,输出比较模式,极性选择,输出使能),用结构体统一配置
TIM_OCInitTypeDef TIM_OCInitStructure;//有些参数高级定时器才会用到,只列出需要用到的
TIM_OCStructInit(&TIM_OCInitStructure);//给结构体变量赋初始值,函数内部也是手动赋初始值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//输出比较模式,强制输出模式不允许初始化时使用
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); 

由于结构体变量是局部变量,如果不给它的成员赋初始值,他成员的值是不确定的,这可能会导致一些问题:
比如当你想把高级定时器当作通用定时器输出 PWM 时,那自然会把 TIM2 改成 TIM1,结构体原来用不到的成员现在就需要用了,而这些成员又没有赋初值,那就会导致高级定时器输出 PWM 出现一些奇怪的问题。
为了避免程序中出现不确定的因素,要么把结构体所有的成员配置完整,要么先给结构体成员赋一个初始值,再修改部分的结构体成员。

  1. 配置GPIO,把 PWM 对应的 GPIO 口初始化为复用推挽输出的配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出:输出数据寄存器将被断开,输出控制权转移给片上外设,片上外设引脚连接到 TIM2 的 CH1 通道
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平

只有把 GPIO 设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM 波形才能通过引脚输出

  1. 运行控制,启动计数器,输出 PWM
TIM_Cmd(TIM2, ENABLE);

2.1.3 示例代码

PWM.h 文件

#ifndef __PWM_H
#define __PWM_H

void PWM_Init(void);
void PWM_SetCompare1(uint16_t compare);

#endif

PWM.c 文件

#include "stm32f10x.h"                  // Device header

void PWM_Init(void) {
//1.RCC开启时钟,把我们要用的 TIM 外设和 GPIO 外设的时候都打开
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//TIM2 是 APB1 总线的外设
	
//2.配置时基单元,包括前面的时钟源选择
	TIM_InternalClockConfig(TIM2);//选择内部时钟
	
	TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
	TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
	TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,向上计数
	//定时 1 s(记公式/理解工作流程)
	TIMTimeBaseInitStructure.TIM_Period = 100 - 1;//ARR 的值(取值范围0~65535)
	TIMTimeBaseInitStructure.TIM_Prescaler = 720 - 1;//PSC 的值(取值范围0~65535),0````````:不需要分频
	TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
	TIM_TimeBaseInit(TIM2, &TIMTimeBaseInitStructure);
//3.配置输出比较单元(CRR的值,输出比较模式,极性选择,输出使能),用结构体统一配置
	TIM_OCInitTypeDef TIM_OCInitStructure;//有些参数高级定时器才会用到,只列出需要用到的
	TIM_OCStructInit(&TIM_OCInitStructure);//给结构体变量赋初始值,函数内部也是手动赋初始值
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//输出比较模式,强制输出模式不允许初始化时使用
	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); 
//4.配置GPIO,把 PWM 对应的 GPIO 口初始化为复用推挽输出的配置
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出:输出数据寄存器将被断开,输出控制权转移给片上外设,片上外设引脚连接的就是 TIM2 的 CH1 通道,PWM 波形通过引脚输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平

//5.运行控制,启动计数器,输出 PWM
	TIM_Cmd(TIM2, ENABLE);
}

void PWM_SetCompare1(uint16_t compare) {
	TIM_SetCompare1(TIM2, compare);
}

main.c 文件

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"

uint8_t i;

int main(void) {
	OLED_Init();
	PWM_Init();
	
	while(1){
		for (i = 0; i <= 100; i++) {
			PWM_SetCompare1(i);//设置 CCR 寄存器的值
			Delay_ms(10);
		}
		for (i = 0; i <= 100; i++) {
			PWM_SetCompare1(100 - i);
			Delay_ms(10);
		}
	} 
}

PWM_SetCompare1函数设置 CCR 寄存器的值,不直接设置占空比,占空比由 CCR 和 ARR + 1 共同决定的,一般情况下都是要和 ARR 共同计算的

2.1.4 引脚重映射

TIM2 的 CH1 可以从 PA0 挪到 PA15 引脚上。

  1. 开启 AFIO 的时钟,使用 AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
  1. 调用函数,用于引脚重映射配置
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);//(重映射的方式,新的状态)

存在一个小问题:PA15 上电后默认复用为了调试端口 JTDI,所以如果想让他做普通的 GPIO 或者复用定时器的通道,那还需要先关闭调试端口的复用。
调试端口对应的重映射方式:

  • GPIO_Remap_SWJ_NoJTRST:PB4 变回普通 GPIO 口
  • GPIO_Remap_SWJ_JTAGDisable:PA15、PB3、PB4 三个引脚变回普通 GPIO 口
  • GPIO_Remap_SWJ_Disable:PA13、PA14、PA15、PB3、PB4 五个引脚全部变成普通 GPIO 扣,没有调试功能了。这个参数千万不要随便调用,一旦调用并且下载程序之后,那么调试端口就没有了,之后再使用 STLINK 就下载不进去程序了。这时就只能使用串口下载,下载一个新的、没有解除调试端口的程序,这样才能把调试端口弄回来。所以使用这个参数要小心一些。

引脚重映射代码:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//打开 AFIO 时钟
	
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);//PA15 变为 TIM2_CH1 通道
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//PA15、PB3、PB4 三个引脚变回普通 GPIO 口
  • 如果想让 PA15、PB3、PB4 这三个引脚当作 GPIO 口来使用的话,那就加一下第一句和第三局。先打开 AFIO 时钟,再用 AFIO 将 JTAG 复用解除掉,这样就行了。
  • 如果像重映射定时器或者其他外设的复用引脚,那就加一下第一句和第二句。先打开 AFIO 时钟,再用 AFIO 重映射外设复用的引脚,这样就行了。
  • 如果你重映射的引脚正好是调试端口,那这三句都得加上。先打开 AFIO 时钟,再用 AFIO 将 JTAG 复用解除掉,再用 AFIO 重映射外设复用的引脚,这样才行。
  1. 配置GPIO,把 PWM 对应的 GPIO 口初始化为复用推挽输出的配置
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平

现在定时器通道 1 就已经成功地挪到了 PA15 上来了。这就是重映射的功能,可以更改复用的引脚。

2.2 PWM 驱动舵机

2.2.1 硬件电路图

在这里插入图片描述
棕色线:GND,接在面包板的 GND
红色线:5V 正极,接 5V 的电机电源,不要把它接在面包板的正极,这个正极只有 3.3V 电压,而且输出功率不大,带不动电机的,所以我们需要把它记在 STLINK 的 5V 输出引脚。从引脚标号来看,最下面的两个引脚都是 5V 输出脚,接哪个都行。这里输出的直接是 USB 的 5V 电源,这个功率足够驱动电机的。
橙色线:PWM 信号,接在 PA1 引脚上,用的是通道 2。
最后,再在 PB1 接一个按键,用来控制舵机。

舵机要求

  1. 周期 20ms
  2. 高电平时间 0.5ms ~ 2.5ms (0° ~ 180°)

2.2.2 示例代码

PWM.h 文件

#ifndef __PWM_H
#define __PWM_H

void PWM_Init(void);
void PWM_SetCompare1(uint16_t compare);

#endif

PWM.c 文件

#include "stm32f10x.h"                  // Device header

void PWM_Init(void) {
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//TIM2 是 APB1 总线的外设
	
	TIM_InternalClockConfig(TIM2);//选择内部时钟
	
	TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
	TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
	TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,向上计数
	//定时 1 s(记公式/理解工作流程)
	TIMTimeBaseInitStructure.TIM_Period = 20000 - 1;//ARR 的值(取值范围0~65535)
	TIMTimeBaseInitStructure.TIM_Prescaler = 72 - 1;//PSC 的值(取值范围0~65535),0````````:不需要分频
	TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
	TIM_TimeBaseInit(TIM2, &TIMTimeBaseInitStructure);

	TIM_OCInitTypeDef TIM_OCInitStructure;//有些参数高级定时器才会用到,只列出需要用到的
	TIM_OCStructInit(&TIM_OCInitStructure);//给结构体变量赋初始值,函数内部也是手动赋初始值
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//输出比较模式,强制输出模式不允许初始化时使用
	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); 

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	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);//GPIO 配置好默认低电平

	TIM_Cmd(TIM2, ENABLE);
}

void PWM_SetCompare1(uint16_t compare) {
	TIM_SetCompare2(TIM2, compare);
}

ARR 与 PSC 的值确定 舵机周期,CCR 的值确定高电平时间,同时也是设置舵机的角度,使用 TIM_SetCompare2 函数更改。

如果你通道 1 和通道 2 都想要用的话,那直接在初始化时将通道 1 和通道 2 都初始化了
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
这样就能同时使用两个通道来输出两个 PWM 了,同理,通道 3 和通道 4 也是可以使用的,那对于同一个定时器的不同通道输出的 PWM,因为不同通道是共用一个计数器的,所以它们的频率必须是一样的,它们的占空比,由各自的 CCR 决定,所以占空比可以各自设定,还有就是它们的相位,由于计数器更新,所有的 PWM 同时跳变,所以它们的相位是同步的,这就是同一个定时器不同通道输出 PWM 的特点。如果驱动多个舵机或者直流电机,那使用同一个定时器不同通道的 PWM,就完全可以了。

Servo.h 文件

#ifndef __SERVO_H
#define __SERVO_H

void Servo_Init(void);
void Servo_SetAngle(float Angle);

#endif

Servo.c 文件

#include "stm32f10x.h"                  // Device header
#include "PWM.h"

void Servo_Init(void) {
	PWM_Init();
}

void Servo_SetAngle(float Angle) {
	PWM_SetCompare1(Angle / 180 * 2000 + 500);
}

封装舵机设置角度的函数,参数是 0 ~ 180°,调用一下就能变为对应的角度,这样才直观方便。而不是设置 CCR 的值,参数是 500~2500,这参数不直观,不方便。

main.c 文件

#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();
	Servo_Init();
	Key_Init();
	
	OLED_ShowString(1, 1, "Angle:");
	
	while(1){
		KeyNum = Key_GetNum();
		if (KeyNum == 1) {
			Angle += 30;
			if (Angle > 180) {
				Angle = 0;
			}
		}
		Servo_SetAngle(Angle);
		OLED_ShowNum(1, 7, Angle, 3);
	} 
}

2.3 PWM 驱动直流电机

2.2.1 硬件电路图

在这里插入图片描述

红色的模块是 TB6612 电机驱动模块:

  1. 第一个引脚 VM,电机电源,接在 STLINK 的 5V 引脚上
  2. 第二个 VCC,逻辑电源,接在面包板 3.3V 正极
  3. 第三个 GND,电源负极,接在面包板的负极
  4. AO1、AO2:电机输出端,接电机的两根线,这个接线不分正反,如果对调这两根线,那电机旋转方向就会反过来。
  5. 右边是另一路的驱动,如果需要接两个电机,那就在右边再接一个电机,如果只需要一个电机,那就随便选一路,另一路空着就行。
  6. STBY:待机控制脚,不需要待机,直接接逻辑电源 3.3V
  7. 剩下三个控制引脚:AIN1 和 AIN2 是方向控制,任意接两个 GPIO 口就行,这里接的 PA4 和 PA5 两个脚,PWMA 是速度控制,需要接 PWM 的输出脚,这里接的是 PA2 这个引脚,对应着 TIM2 的通道 3

还是接一个按键,在 PB1 口,用于控制。

注意:TB6612 电机驱动模块不要插反,要不然容易烧坏驱动

2.2.2 示例代码

PWM.h 文件

#ifndef __PWM_H
#define __PWM_H

void PWM_Init(void);
void PWM_SetCompare1(uint16_t compare);

#endif

PWM.c 文件

#include "stm32f10x.h"                  // Device header

void PWM_Init(void) {
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//TIM2 是 APB1 总线的外设

	TIM_InternalClockConfig(TIM2);//选择内部时钟
	
	TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
	TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
	TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,向上计数
	//定时 1 s(记公式/理解工作流程)
	TIMTimeBaseInitStructure.TIM_Period = 100 - 1;//ARR 的值(取值范围0~65535)
	TIMTimeBaseInitStructure.TIM_Prescaler = 720 - 1;//PSC 的值(取值范围0~65535),0````````:不需要分频
	//TIMTimeBaseInitStructure.TIM_Prescaler = 36 - 1;//去除蜂鸣器噪音
	TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
	TIM_TimeBaseInit(TIM2, &TIMTimeBaseInitStructure);

	TIM_OCInitTypeDef TIM_OCInitStructure;//有些参数高级定时器才会用到,只列出需要用到的
	TIM_OCStructInit(&TIM_OCInitStructure);//给结构体变量赋初始值,函数内部也是手动赋初始值
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//输出比较模式,强制输出模式不允许初始化时使用
	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); 

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出:输出数据寄存器将被断开,输出控制权转移给片上外设,这里片上外设引脚连接的就是 TIM2 的 CH1 通道,PWM 波形通过引脚输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平

	TIM_Cmd(TIM2, ENABLE);
}

void PWM_SetCompare1(uint16_t compare) {
	TIM_SetCompare3(TIM2, compare);
}

因为电机里面也是线圈和磁铁,所以在 PWM 的驱动下,会发出蜂鸣器的声音,这是正常现象。如果介意的话,那怎么避免这个问题呢?

  • 答案就是加大 PWM 频率,当 PWM 频率足够大时,超出人耳的范围,人耳就听不到了。经过查询,人耳听到声音的频率范围是 20 Hz~20 KHz,我们目前给的是 1KHz,人耳是能听到的,所以在这里,我们可以加大频率,加大频率可以通过减小预分频器来完成,这样不会影响占空比,所以我们给这个预分频器去掉一个 0,现在就是 10 KHz 了,当然还不够,我们再把 72 变成 36,变为一半,现在就是 20 KHz 了。

Motor.h 文件

#ifndef __MOTOR_H
#define __MOTOR_H

void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);

#endif

Motor.c 文件

  • 在电机的模块里,要初始化电机方向控制的那两个脚
#include "stm32f10x.h"                  // Device header
#include "PWM.h"

void Motor_Init(void) {
	//在电机的模块里,要初始化电机方向控制的那两个脚
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	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);//GPIO 配置好默认低电平
	
	PWM_Init();
}

void Motor_SetSpeed(int8_t Speed) {
	if (Speed >= 0) {//正转
		GPIO_SetBits(GPIOA, GPIO_Pin_4);
		GPIO_ResetBits(GPIOA, GPIO_Pin_5);
		PWM_SetCompare1(Speed);
	}
	else {
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);
		GPIO_SetBits(GPIOA, GPIO_Pin_5);
		PWM_SetCompare1(-Speed);//SetCompare 必须传正数
	}
}

Motor_SetSpeed 函数的参数要带符号,负数用来表示反转,这里速度值定义为 -100 ~ 100(取决于 ARR)

main.c 文件

#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();
	Motor_Init();
	Key_Init();

	OLED_ShowString(1, 1, "Speed:");
	
	while(1){
		KeyNum = Key_GetNum();
		if (KeyNum == 1) {
			Speed += 20;
			if (Speed > 100) {
				Speed = -100;
			}
		}
		Motor_SetSpeed(Speed);
		OLED_ShowNum(1, 7, Speed, 3);
	} 
}

如果你电机的正反转方向和你想要的方向不一样,就是极性反了,这个有很多地方可以调换过来

  1. 电机两根线反过来接
  2. 输入的 IN1 和 IN2 反过来接
  3. 程序里 Set 和 Reset 反过来
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值