本次课程采用单片机型号为STM32F103C8T6。
课程链接:江科大自化协 STM32入门教程
往期笔记链接:
STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出
STM32学习笔记(二)丨STM32程序调试丨OLED的使用
STM32学习笔记(三)丨中断系统丨EXTI外部中断
STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)
如果上一篇笔记的内容为史诗级副本,本篇文章的内容我愿称之为传说级副本(二)。
PWM技术,全称Pulse Width Modulation,即脉冲宽度调制技术,是一种对模拟电平信号进行数字编码的方法。在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。
通过高分辨率计数器的使用,方波的占空比被调制用来对一个具体模拟信号的电平进行编码(即可以用数字信号来等效地表达模拟信号)。PWM信号仍然是数字的,在给定的任何时刻,满幅值的直流供电要么完全有(ON),要么完全无(OFF)。电压或电流源是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用PWM进行编码。
以LED为例:GPIO的输出信号只能是数字信号,如果想通过数字信号输出模拟量,可以通过以下的方法实现:让LED不断点亮、熄灭、点亮、熄灭,当点亮、熄灭的频率足够大时,由于LED的余晖和人眼的视觉暂留效应,LED就会呈现出一个中等亮度。当调控点亮和熄灭的时间比例时就能让LED呈现出不同的亮度级别。
对于电机调速也类似:在高频率下不断让电机交替通断,由于电机断电后不会立刻停止,而是由于惯性转动后停下,电机的速度就能维持在一个中等速度。
PWM的秘诀是:天下武功,唯快不破! 需要注意的是:只有在具有惯性的系统中,才能用PWM对模拟信号进行编码。
在使用PWM对模拟量进行编码时,以下几个参数尤其重要:
对于PWM频率而言,频率越快,它等效模拟的信号就约平稳,不过同时性能开销就越大。一般来说PWM的频率在几kHz到几十kHz之间。
占空比是高电平时间相对于整个周期时间的比例,一般用百分比表示。占空比决定了PWM等效出的模拟电压的大小。
从下图可以看出,高低电平跳变的数字信号可以被等效地表示为中间虚线所表示的模拟量,且占空比越大,等效的模拟量就越趋近于数字量的高电平;占空比越小,等效的模拟量就越趋近于数字量的低电平,且这个等效关系一般而言是线性一一对应的。
对于分辨率,即占空比的变化步距,即 占空比最小能以百分之多少的精度变化,它的值可以是1%、0.1%。分辨率的大小要看实际项目的需求定。如果既要高频率,又要高分辨率,就需要硬件电路要有足够的性能。要求不高的情况下,1%的分辨率就足够使用了。
输出比较,英文全称Output Compare,简称OC。它最主要的功能是 可以通过比较计数器CNT和捕获/比较寄存器(Capture/Compare Register)CCR值的关系,来输出电平进行置1、置0的翻转操作,用于输出一定频率和占空比的PWM波形。
每个高级定时器和通用定时器都拥有4个输出比较的通道,可以同时输出4路PWM波形,且高级定时器的前3个通道额外拥有死区生成电路和互补输出的功能。4个输出比较通道都有独立的CCR寄存器,但是它们共用同一个CNT计数器。
通用定时器的输出比较部分电路如下图所示:
如上图所示,当CNT = CCR1或者CNT > CCR1时,输出模式控制器就会收到一个信号,输出模式控制器就会改变它输出的OC1REF的高低电平。REF是Reference的缩写,意为参考信号。
接下来OC1REF信号兵分两路:一路通向主模式控制器,即可以将REF信号映射到主模式的TRGO上,去触发其他外设的功能;另一路通往一个极性选择电路,通过控制TIMx_CCER寄存器的值(0或1),可以选择是否将REF信号翻转,之后通往输出使能电路,可以控制是否输出,最后通往OC1引脚,即TIMx_CH1通道的引脚(在引脚定义表中即可找到具体的GPIO口)。
输出比较拥有8种工作模式 ,其对应了输出模式控制器种的执行逻辑,8种输出模式可以通过TIM_CCMR1k寄存器进行配置。输出模式控制器的执行逻辑如下表所示:
有效电平和无效电平是高级定时器中的表述,与关断、刹车等功能配合表述的,这里表述的比较严谨。在这里为了理解方便,可以直接认为有效电平就是高电平,无效电平就是低电平。
以PWM模式1、向上计数模式为例,PWM波形产生原理如下图所示:
在上图中,时基单元之前的时钟源选择部分省略。在这里我们不需要使用更新事件的中断申请。上图中,ARR = 99,CCR = 30,需要注意的是:当CNT = CCR时电路已经置为低电平,故REF为高电平的时间为CNT从0变到29(30个数)的时间。 设置的CCR值越接近ARR,输出的PWM波形的占空比就越大。
参数计算公式如下所示:
舵机是小型直流伺服电机的一种,是一种根据输入PWM信号占空比来控制输出角度的装置。它有三根输入线,其中两根是电源线,一根是PWM信号输入线。
“伺服”—词源于希腊语“奴隶”的意思,英文为Servo。人们想把某一个结构或系统当作一个得心应手的驯服工具,服从控制信号的要求而动作。伺服的主要任务是按照控制命令的要求,对输出信号和输出功率进行放大、变换与调控等处理,使驱动装置输出的力矩、速度和位置控制得非常灵活方便。由于它的“伺服”性能,因此而得名——伺服系统。它的优势在于:可以非常灵活地控制输出装置的力矩、速度和位置等物理参量。
交流伺服电机和直流伺服电机的共同点是:利用传感器(编码器)对转子的位置、转速、力矩、转向进行检测,斌且将得到的信号经由伺服驱动器反馈给伺服控制器,从而达到调节转子位置、转速、力矩、转向的目的; 二者的不同点在于,一般而言,交流伺服电机相较于直流伺服电机对转子有更高的控制精度。
拆开一个舵机,可以发现它是由一个直流电机、一个减速齿轮组、一个电位器(电压编码器)和一个控制板 4部分组成的整体。舵机不是一种单独的电机,它的内部是由直流电机驱动的。内部的控制电路板是一个电机的控制系统,整个舵机内部形成了一个闭环的控制系统。博主在这里没有找到SG90内部电路板的电路图,不过经过了解得知其大概的执行逻辑是:PWM信号输入到控制板后首先进行解调,获得一个直流偏置电压,该偏置电压与电位器的电压相比较,将获得的电压差输出给下一级电机驱动集成电路,以驱动电机正反转。电机转动时会控制电位器转动,直到PWM解调得到的直流偏置电压和电位器的电压差为0时,电机停止转动。
舵机对输入的PWM信号的要求如下:周期为20ms(对应50Hz),高电平宽度为0.5 ~ 2.5ms。且转动角度为0° ~ 180°,中间的对应关系都是线性一一对应的。这里的PWM波形实际上是作为一个通信协议来使用的,与用PWM波形等效出一个模拟输出的关系不大。
直流电机的特点是通电就会旋转,且转速随模拟电压的增大而增大。博主通过搜索资料,猜测仅通过PWM波形控制舵机只能实现控制舵机旋转到固定位置,在旋转过程中的速度是不能调节的(内部没有转速反馈)。 要想实现调节旋转速度,一种方案是可以采用360°舵机的方案,即普通舵机去掉编码器。在360°舵机中,0.5~2.5ms的高电平时间对应的不再是电机的旋转角度,而是电机的旋转方向和速度。猜测原理应该是PWM解调得到的直流偏置电压直接输出给直流电机,这时的电机只是一个减速电机而已。另一种方案是采用步进电机来实现想实现的功能。
在这里还需要注意一点,舵机、直流电机等都属于大功率设备,使用GPIO口是无法直接驱动的。最好可以实现单独的大功率电源实现单独供电,如果不能实现,供电电源要和STM32的负极共地,如下图所示。对于课程使用的套件,可以直接从STLINK的5V输出引脚作为电机的电源驱动引脚。
直流电机是一种能将电能转换为机械能的装置,有两个电极。有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。上图所示的电机是130直流电机。直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作。本课程使用TB6612电机驱动芯片来驱动电机。
TB6612是一款双路H桥型的直流电机驱动芯片,其中有两个驱动电路,可以独立地驱动两个直流电机并且控制其转速和方向。当H桥地左上和右下两个MOS管导通,电流就从O1流向O2,电机就正转;当右上和坐下两个MOS管导通,电流就从O2流向O1,电机就反转。
电机驱动电路同样也是一个研究课题,市面上也有很多的电机驱动可供选择,常见的电机驱动芯片有TB6612、DRV8833、L9110、L298N等,另外还有用分立元件MOS管搭建的驱动电路,它可以实现更大的驱动功率。当然也可以自己用MOS管设计电路。
该模块使用时的一些引脚使用细节:
- VM驱动电压输入端,一般和电机的额定电压保持一致。
- VCC逻辑电平输入端,一般和控制器的电源保持一致。
- STBY意为Stand By,为待机控制引脚。如果不需要控制可以直接接到逻辑高电平VCC(3.3V),如果需要控制可以接入任意一个GPIO口进行控制。
// 配置输出比较模块
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);
TIM_SetComparex
函数最重要,其他的了解即可)// 使用高级定时器输出PWM波形时使能主输出,否则PWM波形不能正常输出
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
// 单独设置输出比较的输出极性(带N的是高级定时器中互补通道的配置)
// 在这里可以设置输出极性,在OC初始化函数中也可以用结构体设置输出极性,这里相当于将单独修改结构体中的某一参数封装到一个函数中
void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
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);
// 配置强制输出模式(运行中暂停输出波形且强制输出高/低电平)
// 强制输出高电平和设置100%占空比等效,强制输出低电平和设置0%占空比等效
void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
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);
// 配置CCR寄存器的预装功能(影子寄存器)
void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
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);
// 清除REF信号(手册中在“外部事件时清除REF信号”一节有介绍)
void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
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);
拓展:AFIO与引脚重映射功能
在STM32的引脚定义表中,定义了片上外设和GPIO口的连接关系。在这里,TIM2的CH1引脚复用在了PA0引脚上。部分外设和GPIO口的连接关系如下图所示:
如果要选择确定的外设输出,就必须在对应的GPIO口输出,这个对应关系是硬性确定的,不可更改。但是STM32依然给予了使用者一次“更改”的机会,这就是重定义,或者称为重映射。如果在默认复用功能中要使用的两个功能在同一个引脚冲突,就可以使用这个方法将其中一个外设的引脚重映射到另一个端口上去。配置重映射是用AFIO完成的。
由上图可知,TIM2的CH1可以从PA0挪动到PA15引脚上,下面是使用AFIO进行这一操作的操作流程:
- 使用RCC开启AFIO时钟
- 使用函数
GPIO_PinRemapConfig
函数进行引脚重映射配置,其中第一个参数可给的参数值选项非常多,它们代表重映射的方式,每个方式与重映射的对应关系可以参考手册(这里以TIM2复用功能重映射表为例):
如果想把TIM2_CH1从PA0重映射到PA15,就可以使用部分重映射模式1,或者完全重映射模式。在参数列表中可以找到,部分重映射模式1,部分重映射模式2,完全重映射三种方式(如果都不使用,就是没有重映射):
- 如果原端口上电后存在默认复用功能,需要根据实际需要关闭原来的复用功能。(如果不是这一步省略)
在这里PA15上电后默认作为调试端口JTDI,故我们需要关闭PA15的调试功能。关闭调制功能同样需要用到GPIO_PinRemapConfig
函数,它的参数中以下三个是用来解除调试端口的复用的:
第一个参数GPIO_Remap_SWJ_NoJTRST
中的SWJ指SWD和JTAG两种调试方式。NoJTRST意为接触JTRST引脚的复用,在引脚定义表中可以找到,PB4上电后默认复用为NJTRST,如果在GPIO_PinRemapConfig
函数中使用该参数,那么PB4就可以作为正常的GPIO口使用,其余的四个仍然为调试端口,不能当作GPIO口使用。
第二个参数GPIO_Remap_SWJ_JTAGDisable
,用来接触JTAG调试端口的复用,使用后PA15、PB3、PB4变回正常的GPIO,而PA13和PA14仍为SWD的调试端口。
第三个参数GPIO_Remap_SWJ_Disable
,用来接触所有SWD和JTAG的调试端口的调试功能,使用后PA13、PA14、PA15、PB3、PB4全部变成普通的GPIO,STM32将失去调试功能(非常危险)。一旦调用这个参数并且下载程序之后,这之后使用STLINK将无法下载程序。这时就需要用串口下载一个新的没有接触调试端口的程序,才可以将调试端口重新使能。
调试端口的映像也可以在手册中找到:
这里博主使用的芯片有可能存在bug,PA15引脚重映射功能无法实现。
PWM.c
#include "stm32f10x.h" // Device header
/**
* @brief PWM输出初始化函数
* @param 无
* @retval 无
*/
void PWM_Init(void)
{
// 1.配置时基单元
// 用RCC外设时钟控制打开定时器的基准时钟和外设GPIO的工作时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 引脚重映射配置,可以将TIM1_CH1的输出信号从PA0重映射到PA15
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
// 选择时基单元的时钟源(这里使用内部时钟)
TIM_InternalClockConfig(TIM2);
// 配置时基单元
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);
// 2.配置GPIO输出端为复用推挽输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出模式,通过这样将引脚电平的控制权交给片上外设
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3.初始化输出比较单元(通道)
// 这里使用PA0口,对应第一个输出比较通道
TIM_OCInitTypeDef TIM_OCInitStruct; // 在这个结构体中有部分是高级定时器才拥有的成员
TIM_OCStructInit(&TIM_OCInitStruct); // 如果不对结构体成员赋初始值,那么它的值将不确定,这样可能会导致一些奇怪的问题
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // OC输出模式 配置为PWM模式1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // OC输出极性
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // OC输出使能
TIM_OCInitStruct.TIM_Pulse = 0; // Pulse的值会被加载到CCR寄存器中
TIM_OC1Init(TIM2, &TIM_OCInitStruct);
// 4.配置运行控制,打开定时器
TIM_Cmd(TIM2, ENABLE);
}
/**
* @brief 更改比较/捕获寄存器的值CCR(当 ARR + 1 == 100 时 CCR 即为占空比)
* @param Compare 无符号16位整型数,注意:它只能是正数
* @retval 无
*/
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()
{
OLED_Init();
PWM_Init();
while(1)
{
for(i = 0; i < 100; i ++)
{
PWM_SetCompare1(i);
Delay_ms(10);
}
for(i = 0; i < 100; i ++)
{
PWM_SetCompare1(100 - i);
Delay_ms(10);
}
}
}
接线图和程序源码如下所示。笔者使用的开发板似乎存在Bug,当把PA2(TIM2_CH2)作PWM输出时,似乎不能正确输出,读者也可以自行实验验证。
PWM.c
#include "stm32f10x.h" // Device header
/**
* @brief PWM输出初始化函数
* @param 无
* @retval 无
*/
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 1.配置时基单元
TIM_InternalClockConfig(TIM2);
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);
// 2.配置GPIO输出端PA1为复用推挽输出模式
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);
// 3.初始化输出比较单元(通道)
// 这里使用PA1口,对应第二个输出比较通道
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // OC输出模式 配置为PWM模式1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // OC输出极性
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // OC输出使能
TIM_OCInitStruct.TIM_Pulse = 0; // CCR:500~2500
TIM_OC2Init(TIM2, &TIM_OCInitStruct);
// 4.配置运行控制
TIM_Cmd(TIM2, ENABLE);
}
/**
* @brief 更改比较/捕获寄存器的值CCR
* @param Compare 无符号16位整型数,注意:它只能是正数,范围是500~2500
* @retval 无
*/
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare);
}
Servo.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/**
* @brief 舵机初始化(模块化封装底层函数)
* @param 无
* @retval 无
*/
void Servo_Init(void)
{
PWM_Init();
}
/**
* @brief 设置舵机的旋转角度
* @param Angle,设置的角度,它的值可以是 0 ~ 180
* @retval 无
*/
void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500); // Angle通过换算得到对应的CCR的值
}
Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* @brief 按键初始化函数,初始化PB0
* @param 无
* @retval 无
*/
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief 返回按下按键的值,若不按下按键默认返回0
* @param 无
* @retval KeyNum 按键对应的值
*/
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) // 读取PB0端口的值
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0); // 如果不松手,程序将在此等待
Delay_ms(20);
KeyNum = 1;
}
return KeyNum;
}
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()
{
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);
}
}
接线图和程序源码如下所示(由于博主使用的单片机芯片有bug,TIM2的CH3无法输出PWM波形,故电机驱动的PWM输入接单片机的PA3口,使用TIM2的第四个输出比较通道。博主在PB11口多加装了一个按键用来切换电机转向):
PWM.c
#include "stm32f10x.h" // Device header
/**
* @brief PWM输出初始化函数
* @param 无
* @retval 无
*/
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 1.配置时基单元
// 选择时基单元的时钟源(这里使用内部时钟)
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
// 提高PWM波的频率可以解决直流电机发出类似蜂鸣器的声音的问题
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; // ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; // PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 2.配置GPIO输出端为复用推挽输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3.初始化输出比较单元(通道)
// 这里使用PA3口,对应第四个输出比较通道
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // OC输出模式 配置为PWM模式1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // OC输出极性
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // OC输出使能
TIM_OCInitStruct.TIM_Pulse = 0; // CCR
TIM_OC4Init(TIM2, &TIM_OCInitStruct);
// 4.配置运行控制
TIM_Cmd(TIM2, ENABLE);
}
/**
* @brief 更改比较/捕获寄存器CCR的值
* @param Compare 无符号16位整型数,注意:它只能是正数
* @retval 无
*/
void PWM_SetCompare4(uint16_t Compare)
{
TIM_SetCompare4(TIM2, Compare);
}
Morto.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/**
* @brief 直流电机初始化函数
* @param 无
* @retval 无
*/
void Motor_Init(void)
{
PWM_Init();
// 初始化电机方向控制脚PA4、PA5
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);
}
/**
* @brief 控制电机的转向和速度函数
* @param Speed 电机速度,可以为正也可以为负
* @retval 无
*/
void Motor_SetSpeed(int8_t Speed)
{
if (Speed >= 0) // 正传
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare4(Speed);
}
else // 反转
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare4(- Speed); // Speed 此时为负数,但是SetCompare的参数必须为正,故在前添加负号
}
}
Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* @brief 按键初始化函数
* @param 无
* @retval 无
*/
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
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的输出速度,在输入模式下这个参数选择没有用处
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief 返回按下按键的值,若不按下按键默认返回0
* @param 无
* @retval KeyNum 按键对应的值
*/
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // 读取1端口的值
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); // 如果不松手,程序将在此等待
Delay_ms(20);
KeyNum = 1;
}
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) // 读取11端口的值
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); // 如果不松手,程序将在此等待
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
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()
{
OLED_Init();
Key_Init();
Motor_Init();
OLED_ShowString(1, 1, "Speed:");
while(1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
Speed += 20;
if (Speed > 100)
{
Speed = -100;
}
}
if (KeyNum == 2) // 按下第二个按键电机反向旋转
{
Speed = -Speed;
}
Motor_SetSpeed(Speed);
OLED_ShowSignedNum(1, 7, Speed, 3);
}
}
原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记~