STM32定时器+TB6612驱动电机
STM32定时器输入捕获和输出比较 + TB6612驱动电机
项目文档下载:
https://gitee.com/strayfatcat/STM32_tb6612_motor_doc
文章思路及部分素材来源:
【铁头山羊stm32 入门教程【新版】】 https://www.bilibili.com/video/BV11X4y1j7si/?p=44&share_source=copy_web&vd_source=b1aaf6b02020581bbc6e9ea7456a4af7
【[铁头山羊stm32入门教程] 7.2. 定时器之输入捕获(上)】 https://www.bilibili.com/video/BV14p421Q7DV/?share_source=copy_web&vd_source=b1aaf6b02020581bbc6e9ea7456a4af7
【STM32入门教程-2023版 细致讲解 中文字幕】 https://www.bilibili.com/video/BV1th411z7sn/?p=19&share_source=copy_web&vd_source=b1aaf6b02020581bbc6e9ea7456a4af7
前置知识:定时器定时
STM32通用定时器框图如下:
图中英文简称解释:
- CK_PSC:定时器时钟源
- PSC:预分频值,16位寄存器最大65535
- CK_CNT:计数器CNT的计数频率
- CNT:定时器计数器
- ARR:自动重装载寄存器
从图中可看出,定时器得到时钟源CK_PSC后,先要经过预分频器PSC的分频,然后得到CNT计数器每秒计数的频率CK_CNT(单位Hz)。此时定时器开始向上或向下计数,当计数器CNT的值与自动重装载寄存器ARR的值相等时会触发中断,可以简单理解为触发定时中断。此时从计数器CNT开始计数到定时器触发中断的时间,就是定时器定时时间。
总计以上可得出定时器定时时间流程:
- 时钟源CK_PSC首先经过预分频PSC
- 预分频后得到计数器CNT计数频率
- 计数频率乘以期望计数时间(单位:秒)可以得出ARR的值
定时器计数频率:
单位:赫兹Hz(次/s)
通过预分频器的值控制,决定定时器计数器CNT计数的速度
Freq = Tclk / (PSC + 1)
定时器溢出时间:
单位:秒 s
由定时器计数器CNT频率控制
Tout = (ARR + 1) / Freq
根据上述两个式子可以推出
定时器定时公式:
Tout = (ARR + 1) * (PSC + 1) / Tclk
Tout:定时时间(单位s)
Tclk:系统主控频率(单位Hz)
- 例如:定时10ms,主控频率72Mhz,求ARR和PSC的值。
72MHz 换算单位得 72 000 000 Hz
带入得
0.01s = (ARR + 1) * (PSC + 1) / 72 000 000Hz
一般选择 (PSC+1) 使分频后计数频率为一个方便的整数(如 1 MHz),便于计算 ARR。这里取7200
则计算得 (ARR + 1) = 100
故可得PSC=7199,ARR=99
定时器10ms定时示例代码:
/**
* 函 数:系统定时器初始化
* 参 数:无
* 返 回 值:无
*/
void timer_init(void)
{
// 打开apb1时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 选择内部时钟作为中断
TIM_InternalClockConfig(TIM2);
// 初始化定时器配置
TIM_TimeBaseInitTypeDef TIM_TimeInitStruceture;
TIM_TimeInitStruceture.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeInitStruceture.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeInitStruceture.TIM_Period = 7200 - 1;
TIM_TimeInitStruceture.TIM_Prescaler = 100 - 1;
TIM_TimeInitStruceture.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeInitStruceture);
/** 清除标志位,解决系统重启后数字从1开始自加 **/
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
// 启动更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// NVIC分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
// 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
/* 定时器中断函数 复制到主函数使用
void TIM2_IRQHandler(void){
//判断标志位
if (TIM_GetITStatus(TIM2,TIM_IT_Update) == SET){
//清除标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
*/
一、定时器输出比较(输出PWM波)
1.是什么PWM?
全称Pulse Width Modulation:脉冲宽度调制(简称脉宽调制,通俗的讲就是调节脉冲的宽度),是电子电力应用中非常重要的一种控制技术。可以通过对一系列脉冲的宽度进行调制,来有效地获得所需要的模拟参量,常应用于电机控速等领域。可以等效实现模拟信号的输出。
应用场景:必须是一个惯性系统
重要参数
- 频率 = 1 / TS。TS指一个高低电平变换周期的时间。频率越快,频率值越小,等效模拟的信号越平稳,性能开销越大。

- 占空比 = TON / TS。TON指高电平时间,TS是周期时间。高电平占整个周期时间的比例。占空比决定了PWM等效出来的模拟电压大小,占空比越大,等效的模拟电压就越趋近于高电平

- 分辨率 = 占空比变化步距。有的步距只能是1%,2%,3%这样以1%的步距跳变,则分辨率就是1%。也就是占空比的精细程度。例如LED的分辨率为1%即说明LED的亮度是1%亮,2%亮这样一直到100%亮
2.PWM参数计算
PWM频率:
Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:
Duty = CCR / (ARR + 1) * 100%
PWM分辨率:
Reso = 1 / (ARR + 1)
例如:系统时钟 72 MHz,设定 PWM 频率 10 kHz,分辨率 ≥ 0.05%。
问:
- PSC、ARR 各取多少合适?
10 000 = 72 000 000 / (PSC+1)/(ARR+1) 化简可得 (PSC+1)(ARR+1) = 7200
因为分辨率≥0.05%,即1 / (ARR+1) ≥ 0.0005 可得 ARR+1≥2000
一般选择 (PSC+1) 使分频后计数频率为一个方便的整数(如 1 MHz),便于计算 ARR。这里取PSC=2(不固定,但要求整数)
带入得出ARR=2399
所以ARR = 2399,PSC = 2
- 对应的 CCR 若要输出 60% 占空比,应为多少?
0.6 = CCR / (2399+1) 得CCR = 1440
3.定时器输出比较配置解析
以下是定时器配置生成PWM的基本流程,这里我们使用PWM生成模式1

图解CNT(ARR)、CCR和PWM输出的关系:

上图中,第一个坐标系为CNT与时间的关系,其中横坐标为CNT的值(设置为99,CCR的值为30);第二个坐标系为输出电平与时间的关系。因为我们使用PWM生成模式1,所以当CNT的值小于CCR的值时,GPIO口输出高电平;同理当CNT的值大于CCR时,GPIO口输出低电平。当CNT的值累加到99时,会触发定时器的自动重装载,会自动将ARR中的值赋给CNT,并重新开始新一轮的生成。所以对应输出电平与时间坐标系,可以明显看出PWM的一个周期,图中时30%的高电平时间,70%低电平时间。
所以我们可以得出以下结论:
PWM的本质就是通过定时器控制输出高低电平的时间。高电平时间占一个周期里的比重越大,LED灯(或者电机)就越亮(转速越快)
以下是定时器生成pwm代码:
pwm.c
#include "stm32f10x.h"
#include "pwm.h"
/**
* 函 数:PWM初始化函数
* 参 数:无
* 返 回 值:无
* 说 明:初始化pwm,PSC=35,ARR=999
* pwm频率:20KHz,pwm占空比:0-100%,pwm分辨率:1%
*/
void pwm_init(void)
{
/* 开启定时器1和GPIO口时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 开启TIM1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟
/* 初始化PWM引脚 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // PA8引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 初始化定时器结构体 */
TIM_TimeBaseInitTypeDef TIM_InitStructure; // 定义初始化结构体
TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频 不分频
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数模式 向上计数
TIM_InitStructure.TIM_Period = 100 - 1; // Arr 自动重装载值 与占空比有关
TIM_InitStructure.TIM_Prescaler = 36 - 1; // PSC 预分频周期
TIM_InitStructure.TIM_RepetitionCounter = 0; // 重复计数器 高级定时器功能
TIM_TimeBaseInit(TIM1, &TIM_InitStructure);
/* 初始化输出比较 */
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); // 初始化结构体变量
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 输出比较模式,选择PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性,选择为高,若选择极性为低,则输出高低电平取反
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 输出使能
TIM_OCInitStructure.TIM_Pulse = 0; // 初始的CCR值
TIM_OC1Init(TIM1, &TIM_OCInitStructure); // 配置TIM1的输出比较通道1
/* 高级定时器使能主输出 */
TIM_CtrlPWMOutputs(TIM1, ENABLE);
/* TIM使能 */
TIM_Cmd(TIM1, ENABLE);
}
/**
* 函 数:PWM设置CCR
* 参 数:Compare 要写入的CCR的值,范围:0~100
* 返 回 值:无
* 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
* 占空比Duty = CCR / (ARR + 1)
*/
void pwm_set_compare1(int8_t Compare)
{
TIM_SetCompare1(TIM1, Compare); // 设置CCR
}
pwm.h
#ifndef __PWM_H__
#define __PWM_H__
void pwm_init(void);
void pwm_set_compare1(int8_t Compare);
#endif
二、定时器输入捕获(读取编码器)
输入捕获详解
定时器输入捕获部分结构详解,图片来自bilibili铁头山羊

图中可以看出,配置输入捕获功能所需的五个寄存器,分别是:通道选择、输入滤波、边沿检测、信号选择和分频系数
分别对应库函数的TIM_ICInitTypeDef结构体里的参数
typedef struct
{
uint16_t TIM_Channel; /* 通道选择 */
uint16_t TIM_ICPolarity; /* 边沿检测 */
uint16_t TIM_ICSelection; /* 信号选择 */
uint16_t TIM_ICPrescaler; /* 分频系数 */
uint16_t TIM_ICFilter; /* 输入滤波 值范围:0x0 - 0xF */
} TIM_ICInitTypeDef;
对应寄存器功能的解释:
通道选择:选择外部信号输入的通道,可选通道有CH1-CH4,其中CH1和CH2为一组,CH3和CH4为一组通道。
输入滤波:对输入信号进行滤波,滤除电平信号中的毛刺信号,保证输入信号的纯净。
边沿检测:该寄存器会检测输入信号的上下边沿信号并输出对应的信号。
信号选择:选择由哪个边沿检测寄存器作为输入信号,直接:从对应当前通道直接输入;间接:从通道组的另一个通道输入
分频系数:选择边沿触发的频率,例如:选择1分频,当出现一个上升/下降沿就会触发事件,选择2分频,当累计出现两个上升/下降沿就会触发事件
- pwm频率计算:
测周法:两个上升沿内,以标准频率fc计次,得到N ,则频率fx=fc / N
- pwm占空比计算:
CCR2寄存器的值 * 100 / CCR1寄存器的值
CCR2寄存器中记录的是每一个下降沿到来时CNT中的计数值
CCR1寄存器中记录的是每一个上升沿到来时CNT中的计数值
以下代码是输入捕获读取第一节的pwm频率代码
ic_pwm.c
#include "stm32f10x.h"
#include "ic_pwm.h"
/**
* 函 数:输入捕获初始化函数
* 参 数:无
* 返 回 值:无
* 说 明:无
*/
void ic_pwm_init(void)
{
/* 开启 TIM3 和 GPIOA 时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 开启TIM3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟
/* 初始化PWM引脚 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // PA6 引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 初始化定时器时基单元结构体 */
TIM_TimeBaseInitTypeDef TIM_InitStructure; // 定义初始化结构体
TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频 不分频
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数模式 向上计数
TIM_InitStructure.TIM_Period = 65536 - 1; // Arr 自动重装载值
TIM_InitStructure.TIM_Prescaler = 72 - 1; // PSC 预分频周期
TIM_InitStructure.TIM_RepetitionCounter = 0; // 重复计数器 高级定时器功能
TIM_TimeBaseInit(TIM3, &TIM_InitStructure);
/* 初始化输入捕获 */
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 输入通道 通道一
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤波器
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 信号交叉选择 直接输入
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 极性选择 上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 捕获分配器 不分频
/* 使用PWM模式初始化,这个函数会自动配置好CH1和CH2 */
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure); // 此函数同时会把另一个通道配置为相反的配置,实现PWMI模式 当然也可以手动配置
/*选择触发源及从模式*/
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); // 触发源选择TI1FP1
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); // 从模式选择复位 即TI1产生上升沿时,会触发CNT归零
/* TIM使能 */
TIM_Cmd(TIM3, ENABLE);
}
/**
* 函 数:获取输入捕获的频率
* 参 数:无
* 返 回 值:捕获得到的频率
*/
uint32_t ic_pwm_get_freq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); // 测周法得到频率fx = fc / N,这里不执行+1的操作也可
}
/**
* 函 数:获取输入捕获的占空比
* 参 数:无
* 返 回 值:捕获得到的占空比
*/
uint32_t ic_pwm_get_duty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1); // 占空比Duty = CCR2 / CCR1 * 100,这里不执行+1的操作也可
}
ic_pwm.h
#ifndef __IC_PWM_H__
#define __IC_PWM_H__
void ic_pwm_init(void);
uint32_t ic_pwm_get_freq(void);
uint32_t ic_pwm_get_duty(void);
#endif
测试结果:
三、TB6612驱动电机
TB6612介绍

TB6612FNG 是东芝半导体公司生产的一款直流电机驱动器件,它具有大电流 MOSFET-H 桥结构,双通道电路输出,可同时驱动 2 个电机。它具有很高的集成度,同时能提供足够的输出能力,运行性能和能耗方面也具有优势,因此在集成化、小型化的电机控制系统中,它可以作为理想的电机驱动器件。
驱动原理如下图
由上图总结可以得出:
一块TB6612可以同时驱动两路电机并读取他们的编码器。
STBY通过输出高低电平用于控制电机启动和停止;AIN1、AIN2口通过输入高低电平来控制电机转动的方向;PWMA口通过输入占空比不同的PWM信号来控制电机转动速度;AO1、AO2用于读取电机编码器,通过编码器数值测速(电机有编码器才能用)
本人采用趣轮科技的D153B电机驱动模块和MG513减速直流电机(霍尔编码器)实现,以下是电机相关代码
PWM频率:20KHz,PWM占空比0~100%,PWM分辨率:1%
pwm采用第一节的代码
motor.c
#include "stm32f10x.h"
#include "motor.h"
/**
* 函 数:电机初始化函数
* 参 数:无
* 返 回 值:无
* 说 明:初始化电机引脚
*/
void motor_init(void)
{
/* 开启GPIOA时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* 初始化电机引脚 用于控制电机转动方向 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; // PA9和PA10
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 初始化PWM 用于控制电机旋转速度 */
pwm_init();
}
/**
* 函 数:直流电机设置速度
* 参 数:Speed 要设置的速度,范围:-100~100
* 返 回 值:无
* 说 明:规定电机正转为逆时针方向,反转为顺时针方向
*/
void motor_set_speed(int8_t Speed)
{
if (Speed >= 0) // 正转速度
{
GPIO_SetBits(GPIOA, GPIO_Pin_9); // PA9高电平
GPIO_ResetBits(GPIOA, GPIO_Pin_10); // PA10低电平
pwm_set_compare1(Speed); // PWM设置
}
else if (Speed < 0) // 反转速度
{
GPIO_ResetBits(GPIOA, GPIO_Pin_9); // PA9低电平
GPIO_SetBits(GPIOA, GPIO_Pin_10); // PA10高电平
pwm_set_compare1(-Speed); // PWM设置 PWM只能给正数 负方向由GPIO的高低电平设置
}
}
motor.h
#ifndef __MOTOR_H__
#define __MOTOR_H__
#include "pwm.h"
void motor_init(void);
void motor_set_speed(int8_t Speed);
#endif
三、读取电机编码器测速
使用定时器输入捕获功能的编码器模式,对应图中圈出的部分。当使用定时器的编码器模式时,有以下注意事项:
- 使用编码器功能,定时器会接管部分寄存器,所以有些寄存器不需要手动配置
- 每个定时器的编码器功能通道只能使用CH1和CH2,CH3和CH4不能作为编码器通道

配置代码如下:
encoder.c
#include "stm32f10x.h"
/**
* 函 数:编码器功能初始化
* 参 数:无
* 返 回 值:无
* 说 明:无
*/
void encoder_init(void)
{
/* 开启GPIOA 和 TIM3 时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
/* GPIO初始化 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // PA6 PA7
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* TIM3 时基单元初始化 */
TIM_TimeBaseInitTypeDef TIM_BaseInitStructure;
TIM_BaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频 不分频
TIM_BaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数模式 当选择定时器编码器模式时,该配置无效
TIM_BaseInitStructure.TIM_Period = 65536 - 1; // ARR自动重装载值 设置最大值
TIM_BaseInitStructure.TIM_Prescaler = 1 - 1; // PSC分频系数 不分频
TIM_BaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器 高级定时器功能
TIM_TimeBaseInit(TIM3, &TIM_BaseInitStructure);
/* TIM3 输入捕获模块初始化 */
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure); // 结构体初始化参数
/* 通道一 */
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 输入捕获通道选择 通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤波器配置 最大
TIM_ICInit(TIM3, &TIM_ICInitStructure);
/* 通道二 */
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 输入捕获通道选择 通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤波器配置 最大
TIM_ICInit(TIM3, &TIM_ICInitStructure);
/* 以下参数不会被使用
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 输入模式(直接/间接)
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 极性选择
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 分频系数
*/
/* 编码器模式配置 */
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); // 计数模式 TI12同时计数 此时极性选择代表两个通道极性不反转
/* 启动TIM3 */
TIM_Cmd(TIM3, ENABLE);
}
/**
* 函 数:读取编码器CNT值并清零
* 参 数:无
* 返 回 值:编码器的CNT值
* 说 明:无
*/
int16_t encoder_get(void)
{
int16_t temp; // 临时变量
temp = TIM_GetCounter(TIM3); // 获取CNT的值
TIM_SetCounter(TIM3, 0); // 清零CNT
return temp;
}
encoder.h
#ifndef __ENCODER_H__
#define __ENCODER_H__
void encoder_init(void);
int16_t encoder_get(void);
#endif
效果如下:
更多推荐

所有评论(0)