一、PWM驱动舵机
通过按键控制舵机的旋转角度,现象是按一下按键舵机旋转30°,就是通过按键去变化PWM的占空比。
接线图

配置PWM波形
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);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;//自动重装ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;//预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器,高级定时器独有。PSC
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);//清断中断标记,以解决TIM_TimeBaseInit带来的中断
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);
TIM_Cmd(TIM2,ENABLE);
整体流程遵循时钟使能 → 外设引脚初始化 → 定时器基础配置 → PWM 输出通道配置 → 定时器使能的 STM32 外设配置通用逻辑,每一步的作用和关联关系如下:
- 使能 GPIOA 时钟,为 PA1 引脚的功能配置做准备;
- 初始化 GPIOA_Pin1 ,配置为复用推挽输出模式(AF_PP),因为定时器 PWM 属于 GPIO 复用功能,推挽模式保证输出驱动能力,同时配置 50MHz 输出速度;
- 使能 TIM2 定时器时钟(TIM2 挂在 APB1 总线,需单独使能);
- 配置 TIM2 时钟源 ,使用内部时钟(STM32 通用定时器默认也是内部时钟,此句可省略,但显式配置更规范);
- 初始化 TIM2 时基单元 ,配置预分频、自动重装值、计数模式等核心参数,决定 PWM 的频率;
- 清除 TIM2 更新标志位,避免时基初始化时产生的更新中断误触发(若开启了更新中断,此步必须有);
- 初始化 TIM2_OC2(通道 2) ,先通过
TIM_OCStructInit初始化 PWM 结构体默认值,再配置 PWM 模式、输出极性、输出使能、占空比寄存器值,决定 PWM 的占空比和输出特性; - 使能 TIM2 定时器,定时器开始计数,PA1 引脚正式输出 PWM 波。
引脚对应表

快速修改 TIM2 通道 2 的 CCR2 寄存器值
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2,Compare);
}
然后把0~180° 的角度值转换成了舵机对应的 PWM 占空比参数(CCR2 值)

void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
这里的0°就是上面的-90°
CCR/ARR == 占空比
按键控制舵机的旋转角度
通过按键触发舵机角度每次 + 30°,超过 180° 则重置为 0°
while(1)
{
keyNum = Key_GetNum();
if(keyNum == 1)
{
Angle += 30;
if(Angle > 180)
{
Angle = 0;
}
}
Servo_SetAngle(Angle);
OLED_ShowNum(1,7,Angle,3);
}
二、PWM驱动直流电机
和上面一样通过按键控制PWM占空比。
接线图

配置PWM
基本和上面一样,改用TIM2的通道3,对应的GPIO口是PA2,然后是为了方便配置占空比,直接将ARR配置为100。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,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);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;//自动重装ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1;//预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器,高级定时器独有。PSC
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);//清断中断标记,以解决TIM_TimeBaseInit带来的中断
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);
TIM_Cmd(TIM2,ENABLE);
配置驱动模块
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();
把 PA4、PA5 配置成通用推挽输出,作为电机驱动模块的方向控制引脚。将PWM的驱动函数放在最后,调用时就不需要单独调用。
电机转动方向及转速
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_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare3(-Speed);
}
}
- 正数(Speed > 0) :执行正转电平配置(PA4 高、PA5 低),直接用正数设置 PWM 占空比(转速);
- 负数(Speed < 0) :执行反转 电平配置(PA4 低、PA5 高),取负数的绝对值
-Speed设置 PWM 占空比(保证占空比为正); - 零(Speed = 0) :进入正数分支,电平为 PA4 高、PA5 低,但 PWM 占空比为 0,电机停止转动(刹车)。
调用
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_ShowSignedNum(1,7,Speed,3);
}
}
}
三、测量PWM频率、占空比
接线图

配置PWM
让输出的PWM的频率为1MHZ。
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;//自动重装ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;//预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器,高级定时器独有。PSC
使用TIM_PrescalerConfig函数去修改PSC,以改变PWM的频率。
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2,Prescaler,TIM_PSCReloadMode_Immediate);
}
配置输入捕获
void IC_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;//自动重装ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;//预分频psc
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器,高级定时器独有。PSC
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0XF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_PWMIConfig(TIM3,&TIM_ICInitStructure); //此函数会自动把剩下的一个通道初始化为相反的配置,此函数仅支持通道1和通道2
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
TIM_Cmd(TIM3, ENABLE);
}
选择内部时钟,用TIM3。将PSC配置为72,将计数频率配置为1MHZ。
为 TIM3 通道 1 设置捕获规则,所有配置围绕「精准、抗干扰捕获外部上升沿」设计;
关键:TIM_ICFilter=0XF是最大滤波,适合工业环境 / 杂波多的场景,若外部 PWM 信号干净,可减小滤波值(如 0x4)提升响应速度。
PWMI 模式开启(代码灵魂,自动配置双通道)
TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);
如果是只测量频率,不用进行这一步。这个的作用是基于通道 1 的配置,自动初始化通道 2 为相反捕获配置,无需手动写通道 2 的代码:
- 通道 1(TIM3_Channel1):保持配置 → 捕获上升沿(记录 PWM 周期的起始 / 结束);
- 通道 2(TIM3_Channel2):自动配置 → 捕获下降沿(记录 PWM 高电平的结束);
- 硬件特性:两个通道的捕获值会自动存入CCR1 和CCR2寄存器,CPU 只需按需读取,无需干预;
- 限制:STM32 硬件仅支持通道 1/2组成 PWMI 配对,通道 3/4 不支持。
从模式
TIM_TS_TI1FP1:指定「TIM3 通道 1 滤波后的上升沿」作为定时器的触发源;
TIM_SlaveMode_Reset:复位从模式 → 每当检测到上述触发源(上升沿),定时器的计数器 CNT 会被硬件自动复位为 0,并同时将当前 CNT 值存入 CCR1 寄存器
转换逻辑
这个使用的是测周法,计数频率是1MHZ,除以CNT1就是PWM的频率。
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
占空比 =(CNT2 + 1)/(CNT2 + 1)
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1)*100 / (TIM_GetCapture1(TIM3) + 1);
}
调用
int main(void)
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
OLED_ShowString(2,1,"Duty:00%");
PWM_SetPrescaler(72 - 1); //Freq = 72M / (psc + 1) / 100
PWM_SetCompare1(20); //Duty = ccr / 100
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
OLED_ShowNum(2,6,IC_GetDuty(),2);
}
}
四、旋转编码器测速
接线图

配置GPIO
将PA6、PA7配置为上拉输入。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM3 时钟使能 + 时基单元配置
将PSC配置为0,不要分频。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;//自动重装ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;//预分频psc
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器,高级定时器独有。PSC
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
TIM3 双通道输入捕获配置
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0XF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter = 0XF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3,&TIM_ICInitStructure);
- 核心:为 TIM3 的两个通道分别配置输入捕获规则,作为编码器模式的硬件基础(编码器模式本质是输入捕获的特殊应用);
- 关键:
TIM_ICStructInit(&TIM_ICInitStructure):初始化捕获结构体为默认值,避免未赋值的成员出现脏数据,导致配置失败; - 滤波 & 极性:两个通道均设最大滤波(抗编码器杂波)、上升沿捕获,后续编码器模式会基于此极性实现边沿检测。
编码器接口模式配置(代码灵魂,开启硬件计数)
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
这个的作用是将 TIM3 从普通定时器转换为编码器专用计数器
TIM_EncoderMode_TI12编码器模式 1同时检测 TI1(A 相)和 TI2(B 相)的边沿,A/B 相每一个有效边沿都会触发计数器加 / 减 1,计数精度最高(编码器最常用模式)
使能 TIM3,开始编码器计数
TIM_Cmd(TIM3, ENABLE)
读取CNT
将读取到的CNT值放在Temp,然后将CNT清零,最后将Temp返回。
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3,0);
return Temp;
}
调用
将读取CNT的函数放在定时中断函数里,定时去更新CNT的数值,就是在1内旋转编码器旋转了多少,这个数值和速度相同。
int main(void)
{
OLED_Init();
Encoder_Init();
Timer_Init();
OLED_ShowString(1, 1, "Speed:");
while(1)
{
OLED_ShowSignedNum(1,7,Speed,5);
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
Speed = Encoder_Get();
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}