目录
第一个工程:PWM驱动LED呼吸灯
PWM.c
初始化PWM步骤
TIM的库函数
TIM_OCStructInit
TIM_CtrlPWMOutputs
TIM_CCxCmd和TIM_CCxNCmd
TIM_SelectOCxM
四个单独更改CCR寄存器值的函数
四个初始化定时器的通道的函数
给结构体一次性都赋初始值的函数
如何确定PSC、CCR和ARR的值
PWM.h
main.c
如何引脚重映射
第二个代码:PWM驱动舵机
PWM.c
PWM.h
Servo.c
Servo.h
Main.c
第三个代码:PWM驱动直流电机
PWM.c
PWM.h
Motor.c
Motor.h
Main.c
声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186
接下来我们来学习一下TIM输出比较的代码部分
第一个工程:PWM驱动LED呼吸灯
接线图:
注意:这里是将LED的正极接在GPIO上,而不是将负极插在GPIO上,这样做的目的是为了将低电平点亮变成为高电平点亮。
复制工程改名:
PWM.c
初始化PWM步骤
初始化PWM就看这个图:
将这里需要的路线打通
第一步,配置RCC开启时钟,把我们要用的TIM外设和GPIO外设的时钟打开。
第二步,配置时基单元,包括这前面的时钟源选择;
第三步,配置输出比较单元,里面包括这个CCR的值,输出比较模式,极性选择,输出使能这些参数,在库函数里也是用结构体统一来配置的。
第四步,配置GPIO,把PWM对应的GPIO口初始化为复用推挽输出的配置。这个PWM和GPIO的对应关系可以参考一下引脚定义表
第五步,配置运行控制,启动计数器,这样就能输出PWM了。
TIM的库函数
接下来我们来看一下库函数里对应的函数。
之前我们介绍TIM的部分库函数了,现在继续看一下,打开tim.h文件,拖到最后。
先看这四个
这四个函数就是用来配置输出比较模块。OC就是output compare输出比较。
他们是用来配置结构图的这一块的
这里输出比较单元有四个,对应也有四个函数。
它的参数第一个TIMx选择定时器,第二个结构体,就是输出比较的那些参数。
TIM_OCStructInit
这个是用来给输出比较结构体赋一个默认值的。
TIM_CtrlPWMOutputs
仅高级定时器使用,在使用高级定时器输出PWM时需要调用这个函数,使能主输出,否则PWM将不能正常输出。
到这里输出比较的配置,其实就已经可以完成了。接下来就是一些小功能和运行时更改参数的函数了。
这里有四个函数
这个是用来配置强制输出模式的。如果你在运行中想要暂停输出波形,并且强制输出高或低电平。可以用一下这个函数,不过一般用的不多,因为强制输出高电平和设置百分之百占空比是一样的。强制输出低电平和设置百分之零占空比也是一样的。所以这四个函数了解一下就行,不需要掌握。
然后下面这些
这四个函数是用来配置CCR寄存器的预装功能的。这个预装功能就是影子寄存器,之前也介绍过。
这个一般可以不用,也了解一下即可,不需要掌握。
接下来的四个
这四个函数是用来配置快速使能的,这个功能手册里单脉冲模式那一节有小段介绍,用的也不多,不需要掌握
接着是
这个功能在手册里外部事件时清除ref信号一节有介绍,这个也不需要掌握。
刚才看的这些函数都是STM32的一些小功能配置的,用的都不多。感兴趣的话可以看看手册。
这些就是用来单独设置输出比较的极性的。这里带个N的就是高级定时器里互补通道的配置,OC4N没有互补通道,所以就没有OC4N的函数。这里有函数可以设置极性,在结构体初始化的个函数里也可以设置极性。这两个地方设置极限的作用是一样的,只不过是用结构体是一体初始化的,在这里是一个单独的函数进行修改的。一般来说,结构体里的参数都会有一个单独的函数可以进行更改。这里的函数就是用来单独更改输出极限的。
TIM_CCxCmd和TIM_CCxNCmd
这是用来单独修改输出使能参数的。
TIM_SelectOCxM
选择输出比较模式,这个是用来单独更改输出比较模式的函数,然后再往下看。
四个单独更改CCR寄存器值的函数
这四个是用来单独更改CCR寄存器值的函数。这四个函数比较重要,我们在运行的时候更改占空比,就需要用到这四个函数。
到这里有关输出比较的函数就介绍完了。
本节重点说输出比较单元部分。
四个初始化定时器的通道的函数
对应四个输出比较单元,或者说输出比较通道,这都是一个意思。
你需要初始化哪个通道,就调用哪个函数。不同的通道对应的GPIO口也是不一样的,所以这里要按照GPIO口的需求来。
我们使用的是PA0,对应的就是第一个输出比较通道。所以这里就要使用这个函数。
第一个是TIMx,我们给TM2,第二个是用来配置输出比较的结构体,我们转到定义复制一下这个结构体类型名,起个变量名,在下面列一下结构体成员。
这里结构体的成员比较多,而且里面有很多参数是高级定时器才用到的。比如带个N的参数,还有这个IdleState参数都是高级定时器才需要用,所以这里一般我们就只把我们需要用的参数列出来就行了。
对于这个结构体变量来说,它现在是一个局部变量,如果不给它的成员赋初始值,它成员的值就是不确定的。
这可能会导致一些问题,比如当你想把高级定时器当做通用定时器输出PWM时,你自然就会把这里的TIM2改成TIM1,这样的话,这个结构体原来用不到的成员,现在就需要用了。而这些成员你又没给赋值,就会导致高级定时器输出PWM出现一些奇怪的问题。
所以为了避免程序中出现不确定的因素,我们要么就像上面之前一样把结构体所有的成员都配置完整,没有用也都给配置一下。
怎么给结构体一次性都赋初始值?
给结构体一次性都赋初始值的函数
我们就要用到这个函数
它就是用来给结构体赋初始值,把结构体变量的地址传进去,这样就能给结构体赋初始值了。
这个函数的内部是这样一一赋值的
如果你不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改你想改的值就行了。
接下来看一下我们要赋值的成员
TIM_OCMode的取值是这些
这些就是我们上一小节介绍过的几种输出比较模式:
第一个timing就是冻结模式。第二个active,相等时置有效电平。第三个inactive,相等时置无效电平。第四个Toggle相等时电平翻转。下面两个就是PWM模式一和PWM模式二的。
然后下面这里还有
这就是强制输出的两种模式,不过这两个参数它不让我们初始化的时候使用,我们也不用它。
我们主要用的就是这个
TIM_OCPolarity的取值
这里参数第一个是high高极性,就是极性不翻转,ref波形直接输出,或者说是有效电平是高电平,ref有效时输出高电平,都是一个意思。
第二个是low低极性,就是ref电平取反,或者说是有效电平为低电平。这个可以根据你的电路需求来,这里我们就选择高级性这个复制。
TIM_Pulse用来设置CCR寄存器值,这个参数可以是0到FFFF之间的一个值,就是16位的范围。
我们还有一个GPIO没有初始化,最终这个波形肯定是要借用一下GPIO口才能输出。
TIM2的OC1通道是借了哪个GPIO口?
我们可以打开这个引脚定义表
这一列默认复用功能就是片上外设的端口和GPIO的连接关系。
在这里可以看到,TIM2的ETR引脚和通道1(CH1)的引脚都是借用了PA0这个引脚。换句话说,就是TIM2的引脚复用在了PA0引脚上,所以说如果我们要使用TIM2的OC1,也就是CH1通道输出PWM。它就只能在PA0的引脚上输出,而不能任意选择引脚输出。
同样如果使用TIM2的CH2,就只能在PA1端口输出,其他同理。比如我们要使用SPI的的MISO引脚,那就是PA6。如果要使用IIC二的SCL引脚,就是PB10。
这个关系是定死的,不能任意更改。不过虽然它是定死的,STM32,还是给了我们一次更改的机会的,那就是重定义,或者叫重映射。
比如如果你既要用USART的TX引脚,又要用TIM2的CH3通道,它俩冲突了,没办法同时用
我们就可以在这个重映射的列表里找一下。比如这里我们找到了TIM2的CH3就可以从原来的引脚换到这里的引脚,这样就避免了两个外设引脚的冲突。
如果这个重新映射的列表里找不到,外设复用的GPIO就不能挪位置,这就是重新映射的功能。
配置重映射就是用AFIO来完成的,等会儿后面详细讲。
以上就是外设引脚和GPIO引脚的复用关系和重新映射的介绍。
我们在使用外设的引脚时,需要多参考一下这个引脚定义表。
通过这个表就知道TIM2的OC1通道除了可以用PA0引脚外,还可以看到这里有重新映射的位置。
所以如果使用重映射它可以从PA0挪到这个PA15的引脚上,其他的引脚就没有机会作为这个通道的输出引脚了。
这样的话我们就知道该配置哪个GPIO口了,我们回到代码。
在这上面的位置初始化一下PA0。当然这个初始化的位置随意,你可以放在上面,也可以放在下面,只不过我们习惯把GPIO放在上面。
GPIO的输出模式也要更改,配置为复用推挽输出,为什么用这个模式?
对于普通的开漏推广输出,引脚的控制权是来自于输出数据寄存器的,
如果想让定时器来控制引脚,就需要使用复用开漏/推挽输出的模式。
在这里输出数据寄存器将被断开,输出控制权将转移给片上外设
通过刚才看到引脚定义表我们就知道了这里片上外设引脚连接的就是TIM2的CH1通道。所以只有把GPIO设置成复用推挽输出引脚,控制权才能交给片上外设,PWM的波形才能通过引脚输出。
这样输出PWM的GPIO就配置好了。
最后使能定时器,PWM波形就能通过PA0输出了。
如何确定PSC、CCR和ARR的值
最后我们看一下如何确定PSC、CCR和ARR的值
如果现在要产生一个频率为1KHz,占空比为50%,分辨率为1%的PWM波形,怎么确定这三个值呢?
我们利用这个三个公式:
由题代入公式就是:
解得CCR=50,PSC+1=720,ARR+1=100,(72M=72*10^6)
那程序里就是ARR=100-1,PSC=720-1,CCR=50
对应波形图就这样:
手画的间距不是很标准,大家知道占空比是50%,高低电平所用时间一样长就可以了。
再示波器上看到的应该是这样的:
这就是频率为1KHz,占空比为50%的PWM了
总结:
PSC决定了计数器的时钟频率;
ARR决定了计数器的溢出值;
CCR决定了PWM信号的占空比。
符号:
PSC(预分频器)
ARR(自动重装载寄存器)
CCR(捕获/比较寄存器)
CNT(计数器)
请牢牢记住正面的红字总结和符号表示以及上面提到的是三个公式!
上一篇我们说了结论:LED亮的时长比LED灭的时长要久,那么灯看上去就比较亮,也就是高电平占的时间比低电平的时间长,则灯看上去就比较亮,如果高电平比低电平的时间短,则灯上去就比较暗,所以如果我们想要程序呈现呼吸灯的效果,那就要不断地调整CCR的值,因为CCR决定了PWM信号的占空比。
我们怎么让CCR不断变化呢?
我们需要调用一下这个函数
这个函数是用来单独更改通道1的CCR值的,既然我们调用了这个函数来更改CCR值,则结构体里的这个成员就不需要赋值了,直接给0
再写一个函数封装一下
我们只需要在主函数的while循环里不断调用这个函数更改CCR的值就可以了
PWM.c
#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模式1TIM_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的值
}
PWM.h
#ifndef __PWM_H
#define __PWM_Hvoid PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);#endif
main.c
#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}}
}
运行结果:
STM32-PWM驱动LED呼吸灯
这就是呼吸灯的效果,如果你用示波器看的话就能看到占空比再不断地变化。
如何引脚重映射
最后我们再看一下之前说的引脚重映射这个是怎么玩的。
我们从引脚定义表里看到了这个TIM2的CH1可以从PA0挪到PA15引脚上,怎么操作?
我们就需要用到AFIO了,首先要使用AFIO就要开启AFIO的时钟,
然后我们到这个GPIO.h里找这个函数
这个函数是引脚重映射配置
它的第一个参数取值非常多,里面都是重映射的方式,每个方式对应的重映射关系可以看一下手册上AFIO这一节,找到这个表
重新印象就是重映射,里面有四种重新映射的方式。
如果我们想把PA0改到PA15就可以选择部分重映射方式1或者完全重映射。
这个参数的取值里的这些就是部分映射和完全映射
如果都不使用这几个取值,就是没有重映射,对应手册上表里的四种方式。
我们这里选择部分重映射1的参数,然后第二个参数enable
这样就能把PA0换到PA15了。
但是现在还要注意一个问题,PA15上电后已经默认复用为调试端口JTDI,所以如果想让它作为普通的GPIO或者复用定时器的通道,还需要先关闭调试端口的复用,怎么关闭?也是用这个函数
这时,第一个参数的取值的这三个就是用来解除调试端口的复用的。
解释一下这三个参数的意义:
第一个参数,SWJ就是SWD和JTAG这两种调试方式,NoJTRST就是解除JTRST引脚的复用,在引脚定义里就是NJTRST
如果使用这个参数,这个PB4就变为正常的GPIO口了。
然后看第二个参数JTAGDisable,这个就是解除JTAG调试端口的复用。
在引脚定义里就是PA15,PB3,PB4这三个端口变回GPIO。
最后看第三个参数SWJ_Disable ,这个参数就是把SWD和JTAG的调试端口全部解除。
在引脚定义里,就是这五个引脚全部变成普通的GPIO,没有调试功能。所以这个参数千万不要随便调用。一旦你调用这个参数,并且下载程序之后,你的调试端口就没有了。这之后再使用stlink或者DAP就下载不进去程序了。这时就只能使用串口下载一个没有解除调试端口的程序,这样才能把调试端口弄回来。所以使用这个参数要小心一点。
这些参数和GPIO能不能使用的情况,在手册里
如果我们需要用PA15,PB3,PB4这三个引脚,通常就是解除JTAG的复用,保留SWD的复用。
综上,我们想把PA0改到PA15就可以选择部分重映射方式1,则需要先解除JTAG复用,所以第一个参数我们就选这个
然后第二个参数enable,这样就可以正常使用PA15这个引脚了。
总结:
如果你重映射的引脚正好是调试端口,就得在配置GPIO口之前加上这几步:
先打开AFIO时钟;
再用AFIO重映射外设复用的引脚(如果只是想要将调试端口变为普通的GPIO口,就不需要这步);
最后用AFIO将JTAG复用解除掉(如果你重映射的引脚不是调试端口,就不需要这步);
现在有了这三句,我们定时器的通道一就从PA0挪到PA15了。GPIO口配置这里也得改成PA15
最后在面包板上也得将LED挪到PA15引脚。
运行效果省略,可以自己试试。
我们接下来开始写第二个代码。
第二个代码:PWM驱动舵机
接线图:
线的正负极说明:
注意:这里要接5V的电机电源,大家不要把它接在面包板的正极。面包板的正极只有3.3V的电压,而且输出功率不大,带不动电机的。所以我们需要把它接在stlink或者DAP的5V输出引脚,它输出的直接是USB的5V电源,这个功率足够驱动电机的。
复制工程并改名:
PWM.c
驱动舵机的关键就是输出一个这样的PWM波形
只要你的波形能按照这个规定准确的输出,驱动舵机就非常简单了。
在PWM.c里改一下
查引脚图
将通道改成通道2的初始化
这样就能同时使用多个通道来输出PWM。但是要知道因为不同通道是共用一个计数器的,所以对于同一个定时器的不同通道输出的PWM,它们的频率必须是一样的。它们的占空比由各自的CCR决定,所以占空比可以各自设定。由于计数器更新,所有PWM同时跳变,所以他们的相位是同步的。这就是同一个定时器不同通道输出PWM的特点。
如果驱动多个舵机或者直流电机,使用一个定时器不同通道的PWM就可以了。
接下来把这个改成2
然后我们要确定ARR,PSC,CCR的值
回归这三个公式:
PWM频率是舵机要求的频率
我们学习所用的舵机的周期是20ms,所以PWM频率就是1/20ms=50Hz
然后舵机要求高电平时间是0.5ms~2.5ms
PSC和ARR的参数并不是固定的,这个可以自己多尝试几次,找一个比较方便计算的值。
我们设置PSC+1=72,ARR+1=20K,这样满足第一个等式,所以最终频率是50Hz。
CCR设置成500,就是0.5ms,CCR设置成2500,就是2.5ms(注意,占空比表示的是高电平在这个周期内的占比,所以500/20K后还得乘以20ms即一个周期,才是高电平的时长),即CCR的取值范围是200~2500,对应0.5ms~2.5ms。
所以这里改成:
PWM.c
#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模式1TIM_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的值
}
PWM.h
#ifndef __PWM_H
#define __PWM_Hvoid PWM_Init(void);
void PWM_SetCompare2(uint16_t Compare);#endif
接下来封装一下舵机的程序
Servo.c
舵机的初始化函数
在这个模块将PWM初始化一下
然后写一个舵机设置角度的函数
Angle / 180 * 2000 + 500这个式子的函数关系如图:
由此可见映射即线性对应。
Servo.c
#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); //设置占空比//将角度线性变换,对应到舵机要求的占空比范围上
}
Servo.h
#ifndef __SERVO_H
#define __SERVO_Hvoid Servo_Init(void);
void Servo_SetAngle(float Angle);#endif
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(); //OLED初始化Servo_Init(); //舵机初始化Key_Init(); //按键初始化/*显示静态字符串*/OLED_ShowString(1, 1, "Angle:"); //1行1列显示字符串Angle:while (1){KeyNum = Key_GetNum(); //获取按键键码if (KeyNum == 1) //按键1按下{Angle += 30; //角度变量自增30if (Angle > 180) //角度变量超过180后{Angle = 0; //角度变量归零}}Servo_SetAngle(Angle); //设置舵机的角度为角度变量OLED_ShowNum(1, 7, Angle, 3); //OLED显示角度变量}
}
我们要通过按键来调整角度的话即需要调用Key.c文件里面的东西了,并且我们还要借助OLED来显示当前角度值
运行后就可以看到每按一下按键,角度就加30度
第三个代码:PWM驱动直流电机
接线图:
红色的那块小板子就TB6612电机驱动模块,它的第一个引脚VM,电机电源,同样也是接在STLINK或者DAP的5V引脚上。VCC逻辑电源接在面板3.3V正极。
这两根输出线不分正反,如果你对调这两根线,电机旋转的方向就会反过来。
然后右边这里是另一路的驱动,如果你需要接两个电机,就在右边再接一个电机。如果只需要一个电机,就随便选一路,另一路空着就行。
STBY待机控制脚,我们不需要待机,直接接逻辑电源+3.3V
剩下三个是控制引脚,AIN1和AIN2是方向控制,任意接两个GPIO也行。PWMA是速度控制,需要接PWM的输出脚。
复制工程并改名
先改一下GPIO口。
根据引脚表格上的提示,PA2对应的是TIM2的通道3,所以我们需要将初始化通道也改一下
对应的初始化CCR的这个函数也要改成3
这样PWM就初始化好了
PWM.c
#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模式1TIM_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的值
}
PWM.h
#ifndef __PWM_H
#define __PWM_Hvoid PWM_Init(void);
void PWM_SetCompare3(uint16_t Compare);#endif
接下来我们封装一下直流电机的驱动程序
Motor.c
在初始化函数中初始化PWM,
并且还要初始化一下控制方向的两个引脚
初始化结束就要写设置速度的函数。
参数要给一个带符号的速度变量,负数用来表示反转,正数代表正转;
我们把方向控制角设置为一个高电平,一个低电平,哪个为高,哪个为低不重要,就是极性不一样而已。
最后将速度传给设置占空比的函数,这样速度就也设置完成了,电机就能转起来了。
注意:PWM_SetCompare3必须传正数,在speed<0时,speed前面要加一个负号。
Motor.c
#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只能给正数}
}
Motor.h
#ifndef __MOTOR_H
#define __MOTOR_Hvoid Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);#endif
接下来修改一下主函数
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(); //OLED初始化Motor_Init(); //直流电机初始化Key_Init(); //按键初始化/*显示静态字符串*/OLED_ShowString(1, 1, "Speed:"); //1行1列显示字符串Speed:while (1){KeyNum = Key_GetNum(); //获取按键键码if (KeyNum == 1) //按键1按下{Speed += 20; //速度变量自增20if (Speed > 100) //速度变量超过100后{Speed = -100; //速度变量变为-100//此操作会让电机旋转方向突然改变,可能会因供电不足而导致单片机复位//若出现了此现象,则应避免使用这样的操作}}Motor_SetSpeed(Speed); //设置直流电机的速度为速度变量OLED_ShowSignedNum(1, 7, Speed, 3); //OLED显示速度变量}
}
这样代码就全部完成了。
注意:因为电机里面也是线圈和磁铁,所以在PWM的驱动下会发出分鸣器的声音,这是正常现象。如果你不介意的话,可以不管。如果介意的话,怎么避免这个问题?
答案就是加大PWM频率,当PWM频率足够大时,超出人耳的范围,人耳就听不到了。
人能听到声音的频率范围是20Hz到20KHz。我们目前给的PWM频率是1KHz,是能听到的。
加大频率我们可以通过减小预分频器来完成,这样不会影响占空比,因为占空比=CCR/(ARR+1),所以修改预分频PSC不会影响占空比。
因此我们要在PWM.c中修改ARR和PSC的值:
这样频率就是20KHz,人耳就听不到了。
运行结果:
STM32-PWM驱动直流电机
如果你电机的正反转方向和你想要的方向不一样,就是极性反了。
有很多地方可以调换过来,比如最简单的电机的两根输出线反过来接。或者输入的IN1和IN2反过来。在程序里也可以把这里的set和reset反过来,这都可以改变极性。
本节的内容就全部完成了,下节继续。
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓