写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做!
本文写于:2025.04.05
STM32开发板学习——第16节: [6-4] PWM驱动LED呼吸灯&PWM驱动舵机&PWM驱动直流电机
- 前言
- 开发板说明
- 引用
- 解答和科普
- 一、PWM驱动呼吸灯
- 二、PWM驱动舵机
- 问题
- 三、PWM驱动直流电机
- 总结
前言
本次笔记是用来记录我的学习过程,同时把我需要的困难和思考记下来,有助于我的学习,同时也作为一种习惯,可以督促我学习,是一个激励自己的过程,让我们开始32单片机的学习之路。
欢迎大家给我提意见,能给我的嵌入式之旅提供方向和路线,现在作为小白,我就先学习32单片机了,就跟着B站上的江协科技开始学习了.
在这里会记录下江协科技32单片机开发板的配套视频教程所作的实验和学习笔记内容,因为我之前有一个开发板,我大概率会用我的板子模仿着来做.让我们一起加油!
另外为了增强我的学习效果:每次笔记把我不知道或者问题在后面提出来,再下一篇开头作为解答!
开发板说明
本人采用的是慧净的开发板,因为这个板子是我N年前就买的板子,索性就拿来用了。另外我也购买了江科大的学习套间。
原理图如下
1、开发板原理图
2、STM32F103C6和51对比
3、STM32F103C6核心板
视频中的都用这个开发板来实现,如果有资源就利用起来。另外也计划实现江协科技的套件。
下图是实物图
引用
【STM32入门教程-2023版 细致讲解 中文字幕】
还参考了下图中的书籍:
STM32库开发实战指南:基于STM32F103(第2版)
数据手册
解答和科普
一、PWM驱动呼吸灯
PA0插入一个LED,PA0引脚输出一个PWM波,用于驱动LED,并且呈现不同的亮度,这个LED正极接在PA0引脚,负极接在GND的驱动方法,这样就是高电平点亮,低电平熄灭,这是正极性驱动的方法,更直观一些。占空比越大,LED越亮,占空比越小,LED就越暗,
1、 RCC开启时种,把我们需要的TIM外设和GPIO外设的时钟打开
2、 配置时基单元,包括前面的时钟源选择和时基单元
3、 配置输出比较单元,包括这个CCR的值,输出比较模式、极性选择、输出使能这些参数
4、 配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置
5、 运行控制,启动计数器,这样就能输出PWM
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
给输出比较结构体赋一个默认值的;到这里输出比较就配置完成了;
用的不多
那这里有函数可以设置极性。在结构体初始化的那个函数里也可以设置极性, 这两个地方设置极性的作用是一样的。只不过是用结构体是一起初始化的, 这里是一个单独的函数进行修改的, 一般来说结结构体里的参数,都会有一个单独的函数可以进行更改。
void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
单独更改输出比较模式的函数
总结一下:
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_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);
代码配置
void PWM_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,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=7200-1; //ARRTIM_TimeBaseInitStructure.TIM_Prescaler=10000-1; //PSCTIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCInitStructure.TIM_OCMode=;TIM_OCInitStructure.TIM_OCPolarity=;TIM_OCInitStructure.TIM_OutputState=;TIM_OCInitStructure.TIM_Pulse=;TIM_OC1Init(TIM2,&TIM_OCInitStructure);TIM_Cmd(TIM2,ENABLE);
}
因为有些参数是高级定时器才有的,我们只配置了我们需要用的,
在这里我们这个结构体现在并没有给所有的成员赋值对吧, 对结构体来说是局部变量,如果不给它的成员赋初始值。它成员的值就是不确定的,这可能会导致一些问题:比如当你想把高级定时器当做通用定时器输出PWM时,那你自然就会把这里的TIM2改为TIM1对吧,这样的话,这个结构体原本用不到的成员,现在就需要用了,而这些成员你又没给赋值,那就会导致高级定时器输出PWM会出现一些奇怪的问题。
让高级定时器输出4路PWM,若果我把初始化函数放在程序的第一行,那就没问题。如果初始化函数之前出现了其他的代码,那4路PWM就会有3路不能输出,奇怪,能不能输出PWM,竟然和初始化函数在哪一行有关,要么配置完整,每个结构体成员都要配置一下,不管有没有用,要么先给结构体都赋初始值,再修改部分的结构体成员。
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
这个函数用来给结构体赋初始值的,如果你不想给每个结构体成员都赋值,你可以先用这个函数赋给结构体初始值,然后再更改你想设置的结构体成员;
/*配置比较单元*/TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCStructInit(&TIM_OCInitStructure);TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1 ; //输出模式PWM1TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High; // 极性选择TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable; //输出使能 TIM_OCInitStructure.TIM_Pulse=; //CCRTIM_OC1Init(TIM2,&TIM_OCInitStructure); //比较通道初始化完成
TIM2的引脚复用在了PA0引脚上,如果我们要使用TIM2的OC1也就是CH1通道,输出PWM,那它就只能在PA0的引脚上输出,而不能任意选择引脚输出,同样如果使用TIM2的CH2,那就只能在PA1端口输出,其他的外设也是同理,关系是定死的;
虽然是定死的,STM32还是给我们一次更改的机会的,这就是重定义,或者叫重映射,比如你既要用USART2的TX引脚,又要用TIM2的CH3通道,它俩冲突了,没法同时用,那我们就可以在这个重映射的列表里找一下,比如这里我们找到了TIM2的CH3,那么TIM2-CH3就可以换到这里,避免了两个外设引脚的冲突,如果这个重映射的列表找不到,那外设复用的GOIO就不能挪动位置,这就是重映射功能,配置重映射是用AFIO来完成的。
因此我吗可以配置PA0或者重映射到PA15的引脚上,其他的引脚就没有机会作为这个通道的输出引脚了。初始化引脚的位置可以随意,你可以放在上面也可以放在下面,只不过我们习惯GOIO放在上面,
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
为什么选择这个模式
对应普通的开漏/推挽输出,引脚的控制权是来自于输出数据寄存器的;
如果想让定时器控制引脚,就需要使用复用开漏.推挽输出的模式,这这里输出数据寄存器将被断开,输出控制权转移给片上外设,所以这里的片上外设连接的就是TIM2的CH1通道,所以只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM的波形才能通过引脚输出。
配置GPIO
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);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);
如果现在想要产生一个频率为1Khz,占空比为50%,分辨率为1%的PWM波形,
72M/(PSC+1)/(ARR+1)=1000;
CCR/(ARR+1)=50%;
1/(ARR+1)=1%; ARR=100-1,CCR=50,PSC=720-1;
#include "stm32f10x.h" // Device headervoid PWM_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);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); 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);/*配置比较单元*/TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCStructInit(&TIM_OCInitStructure);TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1 ; //输出模式PWM1TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High; // 极性选择TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable; //输出使能 TIM_OCInitStructure.TIM_Pulse=50; /* CCR*/TIM_OC1Init(TIM2,&TIM_OCInitStructure); //比较通道初始化完成TIM_Cmd(TIM2,ENABLE); //启动定时器
}
如果现在想要产生一个频率为1Khz,占空比为50%,分辨率为1%的PWM波形,
72M/(PSC+1)/(ARR+1)=1000;
CCR/(ARR+1)=50%;
1/(ARR+1)=1%; ARR=100-1,CCR=50,PSC=720-1;
实验现象
频率1Khz,占空比50%
占空比10%
LED呈现呼吸灯
我们想让LED呈现呼吸灯的效果,那就是不断更改CCR的值就行了,在运行工程更改CCR,需要用下面的函数;
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
这个函数是用来单独更改通道1的CCR的值的,把它封装为函数,我们就可以调用这个函数来改变PWM中CCR的值;
oid PWM_SetCompare1(uint16_t Compare)
{TIM_SetCompare1(TIM2,Compare);
}
我们只需要在while(1)主循环里,不断调用PWM_SetCompare1函数更改CCR的值,这样就能完成LED呼吸灯的效果了。
uint8_t i;for(i=0; i<=100;i++) //占空比从0到100{PWM_SetCompare1(i); //设置CCR的值,并不是直接设置占空比,占空比是CCR/(ARR+1)决定Delay_ms(10);}for(i=0; i<=100;i++) //占空比从100到0{PWM_SetCompare1(100-i);Delay_ms(10);}
设置CCR的值,并不是直接设置占空比,占空比是CCR/(ARR+1)决定;
实验现象
LED呼吸灯
PWM占空比变化
占空比变化和呼吸灯效果
引脚重映射
这个TIM2的CH1可以从PA0挪到PA15引脚上,这里需要用到AFIO了
那首先使用AFIO,就要开启AFIO的时钟,
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
引脚重映射配置
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
部分重映射1、部分重映射2、和完全重映射,如果都不使用,那就是没有重映射;对应表里4中情况;
如果我们想把PAO改到PA15,就可以选择这个部分重映射方式1,或者完全重映射,这都可以。
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);
PA15没有加粗,因为它上电默认复用为了调试端口JTDI,所以如果想让他作为普通的GPIO或者复用定时器的通道,那还需要先关闭调试端口的复用,怎么关闭:也是用这个
GPIO_PinRemapConfig();
这三个参数是用来解除调速端口复用的,SWJ就是SWD和JTAG这两种调试方式,第一个SWJ_NoJTRST,就是解除JTRST引脚的复用,在引脚定义里看一下,就是这个NJTREST也就是PB4
如果使用这个参数,那么这个PB4就变为正常的GPIO口了,其他的四个端口仍然是调试端口。不能当做GPIO来使用;
SWJ_JTAGDisable :这个就是解除JTAG调试端口的复用,在引脚定义里就是,PA15、PB3、PB4、这三个端口变回GPIO,上面的PA13和PA14,仍然为SWD的调试端口;
SWJ_Disable :这个参数就是把SWD和JTAG的调试端口全部解除,在引脚定义里就是这5个引脚全部变为普通GPIO口了,没有调速功能了,所以这个参数千万不要随便调用,一但调用这个参数并且下载到程序之后,那么你的调试端口就没有了,这之后再使用STLINK就下载不进去程序了,这时就只能使用串口下载,下载一个新的、没有解除调试端口的程序,这样才能把调试端口能回来。
如果我们需要使用PA15、PB3、PB4这三个引脚,那通常就是解除JATG的复用,保留SWD的复用,所以这里,参数就选择
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
这样就可以正常使用PA15这个引脚了;
总结一下,如果你想让PA15、PB3、PB4、这三个引脚当做GPIO来使用的话,那就加一下这里的第一句和第三局,先打开AFIO时钟,再用GPIO_PinRemapConfig,用AFIO将JATG复用解除掉了,这样就行了。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
如果你想重映射定时器或者其他外设的复用引脚,那就加一下,第一句和第二句,先打开AFIO时钟,再用AFIO重映射外设复用的引脚,这样就行了,如果你重映射的引脚又正好是调试端口,那这三句都得加上:打开AFIO时钟,重映射引脚,解除调速端口,这样才行;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
有了这三句,我们定时器的通道1,就从PA0挪到PA15了,所以下面GPIO初始话这里就,GPIO_Pin_0也得改成GPIO_Pin_15,
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_15;
那现在重映射的代码写好了。
呼吸灯代码
main
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "PWM.h"
#include "OLED.h"uint8_t i;int main(void)
{OLED_Init();OLED_ShowString(1,2,"Hello STM32 MCU");PWM_Init();while(1){for(i=0; i<=100;i++) //占空比从0到100{PWM_SetCompare1(i); //设置CCR的值,并不是直接设置占空比,占空比是CCR/(ARR+1)决定Delay_ms(10);}for(i=0; i<=100;i++) //占空比从100到0{PWM_SetCompare1(100-i);Delay_ms(10);}}
}
PWM.CH
#include "stm32f10x.h" // Device headervoid PWM_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);/*重映射设置*/
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);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); 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);/*配置比较单元*/TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCStructInit(&TIM_OCInitStructure);TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1 ; //输出模式PWM1TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High; // 极性选择TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable; //输出使能 TIM_OCInitStructure.TIM_Pulse=90; /* CCR*/TIM_OC1Init(TIM2,&TIM_OCInitStructure); //比较通道初始化完成TIM_Cmd(TIM2,ENABLE); //启动定时器
}void PWM_SetCompare1(uint16_t Compare)
{TIM_SetCompare1(TIM2,Compare);}
#ifndef __PWM_H
#define __PWM_Hvoid PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);#endif
实验现象
重映射
重映射TIM2_CH1从PA0到PA15
二、PWM驱动舵机
所以要改为OC2lnit,这样这个结构体参数就会配置到通道2了
TIM_OC2Init(TIM2,&TIM_OCInitStructure);
当然如果你通道1和通道2都想要用,那就再这里加两行代码,通道1和2都初始化,这样就能同时使用两个通道来输出两个PWM了,同理通道3和通道4也是可以使用的,那对于同一个定时器的不同通道输出的PWM,它们的频率,因为不同通道是共用一个计数器的,所以它们的频率必须是一样的,它们的占空比,由各自的CCR决定,所以占空比可以各自设定,还有就是它们的相位,由于计数器更新,所有PWM同时跳变,所以它们的相位是同步的,这就是同一个定时器不同通道输出PWM的特点,,如果驱动多个舵机或者直流电机,那使用一个定时器不同通道的PWM,就完全可以了。
舵机要求的周期是20ms,那频率就是1/20ms=50Hz;
占空比这里,舵机要求高电平时间是0.5ms ~ 2.5ms ,
PSC和ARR的值不是固定的,可以尝试几次,找一个方便计算的值;
PSC+1=72,ARR+1=20K;这样的话,满足第一个等式,所以最终频率为50HZ,同时20k对应20ms,那CCR设置500,就是0.5ms;CCR设置2500,那就是2.5ms;
PWM_SetCompare2(500);
这里就是1.5ms也就0度的位置;
改为2500对应180°
PWM_SetCompare2(2500);
改为1500对应90°
首先,给舵机建一个模块,我们想要的函数是,舵机设置角度,参数是0到180度,调用一下,就能变为对应的角度,这样才直观方便。而不是去设置PWM中的CCR,参数是500到2500。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Servo.h"
#include "OLED.h"
#include "Key.h"uint8_t KeyNum;
float Angle;int main(void)
{OLED_Init();OLED_ShowString(1,2,"Hello STM32 MCU");OLED_ShowString(2,1,"Angle:");Key_Init();Servo_Init();while(1){Servo_SetAngle(0);KeyNum=Key_GetNum();if(KeyNum==1){Angle +=30;if(Angle>180){Angle=0;}}Servo_SetAngle(Angle);OLED_ShowNum(2,7,Angle,3);}
}
Servo.ch
#include "stm32f10x.h" // Device header
#include "PWM.h"void Servo_Init(void) //底层初始化
{PWM_Init();
}/*
0 500
180 2500*/
void Servo_SetAngle(float Angle)
{PWM_SetCompare2(Angle/180*2000+500); //完成映射
}
#ifndef __SERVO_H
#define __SERVO_Hvoid Servo_Init(void);
void Servo_SetAngle(float Angle);
#endif
PWM.ch
#include "stm32f10x.h" // Device headervoid PWM_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);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); 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);/*配置比较单元*/TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCStructInit(&TIM_OCInitStructure);TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1 ; //输出模式PWM1TIM_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_Cmd(TIM2,ENABLE); //启动定时器
}void PWM_SetCompare2(uint16_t Compare)
{TIM_SetCompare2(TIM2,Compare);}
#ifndef __PWM_H
#define __PWM_Hvoid PWM_Init(void);
void PWM_SetCompare2(uint16_t Compare);#endif
Key,ch
#include "stm32f10x.h" // Device header
#include "Delay.h"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_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);}uint8_t Key_GetNum(void)
{uint8_t KeyNum =0;if (GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){Delay_ms(20);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);Delay_ms(20);KeyNum=1;}if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_11)==0){Delay_ms(20);while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_11)==0);Delay_ms(20);KeyNum=2;}return KeyNum;
}
#ifndef __KEY_H
#define __KEY_Hvoid Key_Init(void);
uint8_t Key_GetNum(void);#endif
实验现象
舵机控制角度失败版
0度
30度
60度
控制不住
问题
1、白色背景的话:需要打开反相
三、PWM驱动直流电机
红色是TB6612电机驱动模块,它的第一个引脚VM,电机电源,同样的,也是接在STlink的5V引脚,第二个VCC,逻辑电源,接在面包板3.3V正极,第三个GND,电源负极,接在面包板的负极,之后AO1、AO2、电机输出端,接电机的两根线,这个接线不分正反,如果你对调这两根线,那电机旋转的方向就会反过来,然后右边是另一路的驱动,如果你需要驱动两个电机,就接两路,上面是STBY,待机控制脚,不需要待机,直接接逻辑电源正3.3V,剩下的三个是控制引脚,AN1和AN2是方向控制,任意接两个GPIO就行了,这里接的是PA4和PA5两个引脚,最后一个PWMA是速度控制,需要接PWM的输出脚,这里接的是PA2这个引脚,PA2对应的是TIM2的通道3,到时候初始化通道3就行了,另外接了一个按键,在PB1口,用于控制。
在转动时候会有响声,如何避免这个问题呢,可以通过加大PWM频率,当PWM 频率足够大时,超出人耳的范围就听不到了,20hz-20khz,我们目前是1KHZ,是能听到的;
加大频率可以通过减小预分频器来完成,这样不会影响占空比,所以我们给预分频器去掉一个0,现在就是10kHZ了,还可以再把76改成36,变为一半,现在就是20Khz了,
实验现象
直流电机调速
代码
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Motor.h"
#include "OLED.h"
#include "Key.h"uint8_t KeyNum;
int8_t Speed;int main(void)
{OLED_Init();OLED_ShowString(1,2,"Hello STM32 MCU");OLED_ShowString(2,1,"Speed:");Motor_Init();Key_Init();while(1){KeyNum=Key_GetNum();if(KeyNum==1){Speed+=20;if(Speed>100){Speed=-100;}}Motor_SetSpeed(Speed);OLED_ShowSignedNum(2,7,Speed,3);}
}
PWM.C H
#include "stm32f10x.h" // Device headervoid PWM_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);/*重映射设置*/
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);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); 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=36-1; /* PSC*/TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);/*配置比较单元*/TIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCStructInit(&TIM_OCInitStructure);TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1 ; //输出模式PWM1TIM_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_Cmd(TIM2,ENABLE); //启动定时器
}void PWM_SetCompare3(uint16_t Compare)
{TIM_SetCompare3(TIM2,Compare);}
#ifndef __PWM_H
#define __PWM_Hvoid PWM_Init(void);
void PWM_SetCompare3(uint16_t Compare);#endif
Motor.Ch
#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); PWM_Init();
}void Motor_SetSpeed(int8_t Speed)
{if(Speed>=0){GPIO_SetBits(GPIOA,GPIO_Pin_4);GPIO_ResetBits(GPIOA,GPIO_Pin_5);PWM_SetCompare3(Speed);}else{GPIO_SetBits(GPIOA,GPIO_Pin_5);GPIO_ResetBits(GPIOA,GPIO_Pin_4);PWM_SetCompare3(-Speed);}}
#ifndef __MOTOR_H
#define __MOTOR_Hvoid Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
Key.CH(主要初始化了PB1)
#include "stm32f10x.h" // Device header
#include "Delay.h"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_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);}uint8_t Key_GetNum(void)
{uint8_t KeyNum =0;if (GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){Delay_ms(20);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);Delay_ms(20);KeyNum=1;}if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_11)==0){Delay_ms(20);while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_11)==0);Delay_ms(20);KeyNum=2;}return KeyNum;
}
#ifndef __KEY_H
#define __KEY_Hvoid Key_Init(void);
uint8_t Key_GetNum(void);#endif
OLED.CH
#include "stm32f10x.h"
#include "OLED_Font.h"/*引脚配置*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))/*引脚初始化*/
void OLED_I2C_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;GPIO_Init(GPIOB, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_Init(GPIOB, &GPIO_InitStructure);OLED_W_SCL(1);OLED_W_SDA(1);
}/*** @brief I2C开始* @param 无* @retval 无*/
void OLED_I2C_Start(void)
{OLED_W_SDA(1);OLED_W_SCL(1);OLED_W_SDA(0);OLED_W_SCL(0);
}/*** @brief I2C停止* @param 无* @retval 无*/
void OLED_I2C_Stop(void)
{OLED_W_SDA(0);OLED_W_SCL(1);OLED_W_SDA(1);
}/*** @brief I2C发送一个字节* @param Byte 要发送的一个字节* @retval 无*/
void OLED_I2C_SendByte(uint8_t Byte)
{uint8_t i;for (i = 0; i < 8; i++){OLED_W_SDA(!!(Byte & (0x80 >> i)));OLED_W_SCL(1);OLED_W_SCL(0);}OLED_W_SCL(1); //额外的一个时钟,不处理应答信号OLED_W_SCL(0);
}/*** @brief OLED写命令* @param Command 要写入的命令* @retval 无*/
void OLED_WriteCommand(uint8_t Command)
{OLED_I2C_Start();OLED_I2C_SendByte(0x78); //从机地址OLED_I2C_SendByte(0x00); //写命令OLED_I2C_SendByte(Command); OLED_I2C_Stop();
}/*** @brief OLED写数据* @param Data 要写入的数据* @retval 无*/
void OLED_WriteData(uint8_t Data)
{OLED_I2C_Start();OLED_I2C_SendByte(0x78); //从机地址OLED_I2C_SendByte(0x40); //写数据OLED_I2C_SendByte(Data);OLED_I2C_Stop();
}/*** @brief OLED设置光标位置* @param Y 以左上角为原点,向下方向的坐标,范围:0~7* @param X 以左上角为原点,向右方向的坐标,范围:0~127* @retval 无*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{OLED_WriteCommand(0xB0 | Y); //设置Y位置OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}/*** @brief OLED清屏* @param 无* @retval 无*/
void OLED_Clear(void)
{ uint8_t i, j;for (j = 0; j < 8; j++){OLED_SetCursor(j, 0);for(i = 0; i < 128; i++){OLED_WriteData(0x00);}}
}/*** @brief OLED显示一个字符* @param Line 行位置,范围:1~4* @param Column 列位置,范围:1~16* @param Char 要显示的一个字符,范围:ASCII可见字符* @retval 无*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{ uint8_t i;OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分for (i = 0; i < 8; i++){OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容}OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分for (i = 0; i < 8; i++){OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容}
}/*** @brief OLED显示字符串* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param String 要显示的字符串,范围:ASCII可见字符* @retval 无*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i++){OLED_ShowChar(Line, Column + i, String[i]);}
}/*** @brief OLED次方函数* @retval 返回值等于X的Y次方*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1;while (Y--){Result *= X;}return Result;
}/*** @brief OLED显示数字(十进制,正数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~4294967295* @param Length 要显示数字的长度,范围:1~10* @retval 无*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i++) {OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');}
}/*** @brief OLED显示数字(十进制,带符号数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:-2147483648~2147483647* @param Length 要显示数字的长度,范围:1~10* @retval 无*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{uint8_t i;uint32_t Number1;if (Number >= 0){OLED_ShowChar(Line, Column, '+');Number1 = Number;}else{OLED_ShowChar(Line, Column, '-');Number1 = -Number;}for (i = 0; i < Length; i++) {OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');}
}/*** @brief OLED显示数字(十六进制,正数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~0xFFFFFFFF* @param Length 要显示数字的长度,范围:1~8* @retval 无*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{uint8_t i, SingleNumber;for (i = 0; i < Length; i++) {SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;if (SingleNumber < 10){OLED_ShowChar(Line, Column + i, SingleNumber + '0');}else{OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');}}
}/*** @brief OLED显示数字(二进制,正数)* @param Line 起始行位置,范围:1~4* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~1111 1111 1111 1111* @param Length 要显示数字的长度,范围:1~16* @retval 无*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i++) {OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');}
}/*** @brief OLED初始化* @param 无* @retval 无*/
void OLED_Init(void)
{uint32_t i, j;for (i = 0; i < 1000; i++) //上电延时{for (j = 0; j < 1000; j++);}OLED_I2C_Init(); //端口初始化OLED_WriteCommand(0xAE); //关闭显示OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率OLED_WriteCommand(0x80);OLED_WriteCommand(0xA8); //设置多路复用率OLED_WriteCommand(0x3F);OLED_WriteCommand(0xD3); //设置显示偏移OLED_WriteCommand(0x00);OLED_WriteCommand(0x40); //设置显示开始行OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置OLED_WriteCommand(0xDA); //设置COM引脚硬件配置OLED_WriteCommand(0x12);OLED_WriteCommand(0x81); //设置对比度控制OLED_WriteCommand(0xCF);OLED_WriteCommand(0xD9); //设置预充电周期OLED_WriteCommand(0xF1);OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别OLED_WriteCommand(0x30);OLED_WriteCommand(0xA4); //设置整个显示打开/关闭OLED_WriteCommand(0xA6); //设置正常/倒转显示OLED_WriteCommand(0x8D); //设置充电泵OLED_WriteCommand(0x14);OLED_WriteCommand(0xAF); //开启显示OLED_Clear(); //OLED清屏
}
#ifndef __OLED_H
#define __OLED_Hvoid OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char);
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String);
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length);
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);#endif
总结
本节课主要是进行了定时器比较的功能,把上一节课定时器比较进行了实现,不仅要对时基进行配置,还要对比较单元进行配置,后来这就是真个流程,后来把PWM模块封装起来,之后的都是用PWM做底层代码实现其他的功能,
1、如果现在想要产生一个频率为1Khz,占空比为50%,分辨率为1%的PWM波72M/(PSC+1)/(ARR+1)=1000;
CCR/(ARR+1)=50%;1/(ARR+1)=1%; ARR=100-1,CCR=50,PSC=720-1;还学会了引脚重映射,
2、如果你想重映射定时器或者其他外设的复用引脚,那就加一下,第一句和第二句,先打开AFIO时钟,再用AFIO重映射外设复用的引脚,这样就行了,如果你重映射的引脚又正好是调试端口,那这三句都得加上:打开AFIO时钟,重映射引脚,解除调速端口,这样才行;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
有了这三句,我们定时器的通道1,就从PA0挪到PA15了。
3、用PWM为底层,完成了LED呼吸灯、PWM驱动舵机、PWM驱动直流电机,舵机实现的不太好,不知道什么原因,是不是供电的问题有待于考虑
4、学会了基本的示波器的操作,感觉用起来很好玩。