stm32单片机使用tb6612驱动编码器电机并测速的驱动代码详解—详细参考开发手册(可移植+开发手册)
目录
前言:
1 开发用到的器件
1.1 stm32f103c8t6开发板
1.2带编码器的直流减速电机
1.3电源
1.4 tb6612fng电机驱动板
2 使用tb6612驱动电机
2.1 tb6612引脚介绍
2.2 tb6612驱动电机完整接线图
2.3 tb6612驱动电机代码
3 使用编码器测速
3.1 霍尔编码器
3.2 基础脉冲数
3.3 四倍频技术
3.4 编码器数据采集及测速程序实现
3.4.1 初始化配置
3.4.2 重要公式解析
3.5 定时器CNT寄存器溢出问题
3.5.1 解决中断溢出问题
3.6 完整驱动代码
3.7 效果展示
4 遇到的问题
4.1 电平不匹配
前言:
这篇文章为学习pid算发,pid项目开发提供必要的知识讲解,全文代码为基于hal库编写。本文通过开发手册详细介绍了编码器测速连线,编程时需要注意的细节。
1 开发用到的器件
1.1 stm32f103c8t6开发板
本项目采用的主控芯片为市面上流行的开发板stm32f103c8t6,对于参数细节不过多介绍。
1.2带编码器的直流减速电机
电机型号为jgb37-520没有找到具体厂家,但是问客服找到了接线图。
电机工作原理我们也不用去纠结,只需要知道电机输入两端M1,M2的输入电压差来驱动电机远动。接线我们在编程的时候介绍。不过编码器的一些参数是十分重要的,如信号类型,基础脉冲数,具体用法也在编程中介绍。
1.3电源
单片机的供电系统不足以驱动该电机,于是我使用了3个3.7v的锂电池串联。
1.4 tb6612fng电机驱动板
TB6612FNG是东芝公司推出的一款双通道H桥电机驱动芯片,能够同时驱动两个直流电机或一个步进电机,具有低功耗和高效率的特点。
2 使用tb6612驱动电机
首先我们不急于实现测速功能,下先实现流畅的控制电机。
2.1 tb6612引脚介绍
电源引脚
VM
:电机电源输入(典型范围:2.5V ~ 13.5V),为电机提供动力电源。
VCC
:逻辑电源输入(典型值:2.7V ~ 5.5V),为内部逻辑电路供电。
GND
:接地引脚,需与外部公共地连接
控制引脚(每通道一组)
IN1/AIN2
(通道A输入引脚):控制通道A的转向和停止。
BIN1/BIN2
(通道B输入引脚):控制通道B的转向和停止。
PWMA/PWMB
(通道A/B的PWM输入引脚):接收MCU的PWM信号,控制电机速度。
STBY
(待机模式控制引脚):高电平时芯片正常工作,低电平时所有输出关闭(电机自由停止)。
输出引脚
A01/A02
(通道A输出引脚):连接直流电机A的两端。
B01/B02
(通道B输出引脚):连接直流电机B的两端。
组合效果:
TB6612FNG的每个通道(A或B)通过IN1
、IN2
和PWM
三个引脚控制。以下以单个通道为例说明不同组合的效果:
IN1 | IN2 | PWM | 输出效果 | 说明 |
---|---|---|---|---|
0 | 0 | 任意 | 短路制动 | 电机快速停止 |
1 | 0 | 高 | 正转 | 电机正向转动,速度由PWM占空比决定 |
0 | 1 | 高 | 反转 | 电机反向转动,速度由PWM占空比决定 |
1 | 1 | 任意 | 停止 | 电机自由停止(无制动) |
2.2 tb6612驱动电机完整接线图
tb6612由两个通道(A,B)可以同时驱动两电机,在本项目中我们只是用到了通道A驱动一个电机,在开发手册中有一个推荐的典型应用图,可以清晰的看到外围电路与内部线路走向。tb6122只需要4个简单的外部器件就可以使用。通过下面的应用图我们可以自己打板测试。
实际接线图如下
2.3 tb6612驱动电机代码
因为是hal库编写我这里给出在cubemx中的定时器配置。选择通道一生成pwm波,要注意的是pwm波频率建议设置为1khz~100khz。这样的频率。可以更细分的控制电机,且频率太低会导致电机噪音大(人耳可闻),频率太高会导致MOS管开关损耗增加,芯片发热。
引脚初始化不在多说
下面代码就是根据tb6612的引脚功能去实现的代码
/** @brief通过stby引脚将tb6612使能* @param* @return* */
void Motor_Enable(void) //开启电机
{HAL_GPIO_WritePin(MOTOR_STBY_PORT, MOTOR_STBY_PIN, GPIO_PIN_SET);
}void Motor_Disable(void) //关闭电机
{HAL_GPIO_WritePin(MOTOR_STBY_PORT, MOTOR_STBY_PIN, GPIO_PIN_RESET);
}
/** @brief通过ain1,ain2引脚控制电机的正反转* @param 参数direction标志电机正反转* @return* */
void motor_Direction(uint8_t direction)
{if(direction==0)//正转{HAL_GPIO_WritePin(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN,GPIO_PIN_RESET);HAL_GPIO_WritePin(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN, GPIO_PIN_SET);}else{HAL_GPIO_WritePin(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN,GPIO_PIN_SET);HAL_GPIO_WritePin(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN, GPIO_PIN_RESET);}
}
/** @brief通过调节占空比调节速度* @param 参数speed决定电机速度* @return* */
void motor_SetSpeed(uint16_t speed)
{if(speed>1000)__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,speed);__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,speed);
}
一些宏定义
#define MOTOR_AIN1_PIN GPIO_PIN_3
#define MOTOR_AIN1_PORT GPIOA
#define MOTOR_AIN2_PIN GPIO_PIN_4
#define MOTOR_AIN2_PORT GPIOA
#define MOTOR_STBY_PIN GPIO_PIN_5
#define MOTOR_STBY_PORT GPIOA
3 使用编码器测速
3.1 霍尔编码器
编码器是一种将角位移或者直线位移转换成一连串电数字脉冲的一种传感 器。我们可以通过编码器测量电机转动的位移或者速度信息。 编码器按照工作原理,可以分为增量式编码器和绝对式编码器。绝对式编码 器的每一个位置对应一个确定的数字码,因此它的示值只与测量的终止位置有关, 而与测量的中间过程无关。增量式编码器是将位移转换成周期性的电信号,再把 这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。我们常用的编码器 为增量式编码器。(这一段是我从开发手册上复制过来的编码器工作原理,下面我用经行图文结合的演示)
从编码器检测原理上来分,还可以分为光学式、磁式、感应式、电容式。常 见的是光电编码器(光学式)和霍尔编码器(磁式)。一般来说光电编码器是霍 尔编码器精度的几十倍。这里我们使用的是,AB双向增量式磁性霍尔编码器。
霍尔编码器是由多对极的磁环和霍尔元件组成。多对极的磁 环是在一定直径的圆板上等分地布置有不同的磁极。
多对极的磁环与电动机同轴,电动机旋转时,磁环会跟着一起旋转,霍尔元件则固定不动,霍尔元件检测磁环的磁极,根据N或者S级分别输出高电平或者低电平, 当电机持续转动时,电机轴上的磁环持续转动,每转动一圈,就会生成一个周期的方波。
这是其中A霍尔元件生成的波形,B霍尔元件会生成一个慢1/4周期的波形(因为霍尔元件相间90°)通过判断A、B两相的先后顺序,可以确定旋转方向。
3.2 基础脉冲数
霍尔编码器中有一个重要的参数是我们编程必须要用到的所以这里特意单独开一小节,基础脉冲数,这个参数是由多极磁环上NS极的对数决定的,我的编码器上的多极磁环上有11对NS极,所以基础脉冲数为11。
这个参数通常不是2的n次方。这种设计通常是为了与电机本身的极对数匹配,例如无刷直流(BLDC)电机。如果BLDC电机有11对极(22个磁极),那么使用11 PPR的编码器可以实现每圈一个电周期的对应关系,极大简化了电机换相(Commutation)的控制逻辑。
3.3 四倍频技术
四倍频是通过程序提升我们的编码器的精度的一种数据处理方法,可以有效 的最大化我们的编码器的精度和测量精度。举个例子,通过基础脉冲数为11代表这一个圆被对称分成11分,现在使用四倍频我们可以将他分成44份,那位置是不是就更精确了呢。下面讲解具体是如何实现的。
正常我们一般的处理方式是通过A相去 计数,A相的上升沿计数或者下降 沿计数,B相去判断目前的转动方向(通过判断A相上升沿或下降沿时B相的电平状态)。假设上升沿计数,对于下面这段波形我们只能计数四次
四倍频则是同时计算AB两相的每个跳边沿,这样子原本在A相计数的一 个脉冲周期内就实现了四次计数,从而实现了精度的提升。对于同样的一段波形四倍频可以计数16次!
3.4 编码器数据采集及测速程序实现
因为编码器输出的是标准的正交方波,所以我们可以使用单片机(STM32 STM8 51 等)直接读取。在软件中的处理方法是分两种,自带编码器接口的单片 机如STM32,可以直接使用编码器硬件接口(定时器CH1&CH2)计数,然后从对 应的寄存器读出相应的数据即可。没有编码器硬件接口的单片机,可以通过外部 中断接口测量,比如把编码器A相输出接到单片机的外部中断接口,这样就可以 通过跳边沿触发中断,然后在对应的中断服务函数里面,通过B相的电平来确定 正反转。如当A相来一个上升沿或者是下降沿的时候,如果B相是高电平就认为 是正转,低电平就认为是反转。
3.4.1 初始化配置
stm32采集正交方波比较简单我们只需要初始化定时器为编码器模式即可,通过两个通道同时采集A,B两相的上升沿和下降沿,实现四倍频。下面给出在cubemx的配置。
接线图如下
绿线(编码器A相),白线(编码器B相)分别连接定时器采集的ch1,ch2。
3.4.2 重要公式解析
我们先给出我写的速度计算代码,通过代码结合公式解析。
/** @brief计算电机转速* @param* @return motor_rpm电机转速 转/分钟* */
float Calculate_RPM(void) {uint32_t current_time = HAL_GetTick(); //获取从芯片启动到行在运行的时间单位msuint32_t time_elapsed = current_time - last_capture_time; //上次计算速度到这次计算速度的时间差用于计算这一期间的速度if(time_elapsed >= 100) {int32_t current_count_extended = Read_Encoder_Extended(); //获取当前的脉冲速int32_t pulse_diff = current_count_extended - last_encoder_count; //与上一次计算的脉冲数的差值motor_rpm = (float)pulse_diff / TOTAL_PULSES_PER_REV * (60000.0f / time_elapsed);last_encoder_count = current_count_extended;last_capture_time = current_time;UART_Printf(&huart1, "speed: %.2f\r\n",motor_rpm); //打印转速测试UART_Printf(&huart1, "pulses_count: %d\r\n",pulse_diff); //打印与上一次计算的脉冲数的差值测试UART_Printf(&huart1, "last_count:%d\r\n",last_encoder_count); //打印最后一次记录的脉冲数测试是否溢出}return motor_rpm;
}
宏定义
#define GEAR_RATIO 30.0f // 减速比30:1
#define PULSES_PER_REV 11.0f // 电机轴每转脉冲数
#define TOTAL_PULSES_PER_REV (GEAR_RATIO * PULSES_PER_REV * 4) // 输出轴每转总脉冲数(4倍频)
固定时间内捕获的脉冲数计算
上面给出的是开发手册的计算公式,如果你跟我一使用stm32的编码器模式那么我们可以直接从CNT寄存中读出当前编码器数值,用当前数值减去上一次记录的数值就是“固定时间读取的编码器数值”所以我们得到固定时间内捕获的脉冲数的公式为:
*** p=固定时间读取的编码器数值/4
我们把固定时间内捕获的脉冲数转化为固定时间内的圈数就是:
* n = 固定时间读取的编码器数值/(减速比*电机轴每转脉冲数*4)
然后转化为转/分钟就是
r = 固定时间读取的编码器数值/(减速比*电机轴每转脉冲数*4)*固定时间
这里的固定时间由自己决定
3.5 定时器CNT寄存器溢出问题
如果你跟我一样使用stm32单片机进行驱动,那么这个问题你一定要注意!STM32硬件定时器中的CNT寄存器:肯定会溢出。
STM32的定时器CNT寄存器是一个16位的寄存器(对于基本和通用定时器而言)。这意味着它能存储的最大值是 2^16 - 1 = 65535
。
-
向上计数时:当CNT从0开始增加,达到65535后,再来一个计数脉冲,它会变成0,并产生一个溢出(更新)中断。
-
向下计数时:当CNT从65535开始减少,达到0后,再减一次,它会变成65535,并同样产生溢出中断。
会有什么影响?
假设CNT当前值是10。它正转65536个脉冲,CNT的值又回到了10。如果你的代码只做一次采样:current_count = __HAL_TIM_GET_COUNTER(&htim3); // 结果是10
你看起来像是完全没有动过,但实际上它已经转了整整一圈(65536个脉冲)甚至更多圈。就是“溢出”导致的数据错误,这样随之而来的时速度计算错误
3.5.1 解决中断溢出问题
解决方法也很简单我们在中断回调函数中做回调处理,在发生中断溢出时我们使用变量tim3_overflow_count结合当前编码器数值进行计算。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if (htim->Instance == TIM3) { // 判断是哪个定时器产生的溢出if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3)) {// 如果当前是向下计数(反转),溢出意味着从0翻到65535,总脉冲数应该减65536tim3_overflow_count--;} else {// 如果当前是向上计数(正转),溢出意味着从65535翻到0,总脉冲数应该加65536tim3_overflow_count++;}}
}
3.6 完整驱动代码
/** tb6612.c** Created on: Aug 26, 2025* Author: ccc*/
#include"head.h"
/** @brief通过stby引脚将tb6612使能* @param* @return* */
void Motor_Enable(void) //开启电机
{HAL_GPIO_WritePin(MOTOR_STBY_PORT, MOTOR_STBY_PIN, GPIO_PIN_SET);
}void Motor_Disable(void) //关闭电机
{HAL_GPIO_WritePin(MOTOR_STBY_PORT, MOTOR_STBY_PIN, GPIO_PIN_RESET);
}
/** @brief通过ain1,ain2引脚控制电机的正反转* @param 参数direction标志电机正反转* @return* */
void motor_Direction(uint8_t direction)
{if(direction==0)//正转{HAL_GPIO_WritePin(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN,GPIO_PIN_RESET);HAL_GPIO_WritePin(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN, GPIO_PIN_SET);}else{HAL_GPIO_WritePin(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN,GPIO_PIN_SET);HAL_GPIO_WritePin(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN, GPIO_PIN_RESET);}
}
/** @brief通过调节占空比调节速度* @param 参数speed决定电机速度* @return* */
void motor_SetSpeed(uint16_t speed)
{if(speed>1000)__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,speed);__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,speed);
}// 全局变量
int32_t encoder_count = 0;
int32_t last_encoder_count = 0;
float motor_rpm = 0.0f;
uint32_t last_capture_time = 0;/** @brief编码器初始化* @param* @return* */
void Encoder_Init(void) {HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);__HAL_TIM_SET_COUNTER(&htim3, 0); // CNT清零HAL_TIM_Base_Start_IT(&htim3); // 启动定时器溢出中断tim3_overflow_count = 0;last_encoder_count = 0;
}/** @brief获取脉冲数* @param* @return* */
int32_t Read_Encoder_Extended(void) {// 核心公式:总脉冲数 = 溢出次数 * 65536 + 当前的CNT值return (tim3_overflow_count * 65536) + __HAL_TIM_GET_COUNTER(&htim3);
}/** @brief计算电机转速* @param* @return motor_rpm电机转速 转/分钟* */
float Calculate_RPM(void) {uint32_t current_time = HAL_GetTick(); //获取从芯片启动到行在运行的时间单位msuint32_t time_elapsed = current_time - last_capture_time; //上次计算速度到这次计算速度的时间差用于计算这一期间的速度if(time_elapsed >= 100) {int32_t current_count_extended = Read_Encoder_Extended(); //获取当前的脉冲速int32_t pulse_diff = current_count_extended - last_encoder_count; //与上一次计算的脉冲数的差值motor_rpm = (float)pulse_diff / TOTAL_PULSES_PER_REV * (60000.0f / time_elapsed);last_encoder_count = current_count_extended;last_capture_time = current_time;UART_Printf(&huart1, "speed: %.2f\r\n",motor_rpm); //打印转速测试UART_Printf(&huart1, "pulses_count: %d\r\n",pulse_diff); //打印与上一次计算的脉冲数的差值测试UART_Printf(&huart1, "last_count:%d\r\n",last_encoder_count); //打印最后一次记录的脉冲数测试是否溢出}return motor_rpm;
}
3.7 效果展示
在主函数中调用
void Motor_Run(void)
{Motor_Enable();motor_Direction(0);motor_SetSpeed(500);Calculate_RPM();
}
电机
可以看到数据正常,并且当数据过大时也不会产生溢出错误。
4 遇到的问题
最后讲一讲遇到的问题,希望对大家有帮助
4.1 电平不匹配
一开始我使用电机驱动板上的5v供电给编码器供电,发现CNT寄存器中的计数值不确。一下是对问题的解析:
1. 电平不匹配问题
1.1 信号电平标准
-
5V TTL电平:高电平≥2.0V,低电平≤0.8V
-
3.3V CMOS电平:高电平≥2.0V,低电平≤0.8V(与5V TTL兼容)
-
STM32 GPIO:通常工作在3.3V,输入高电平阈值约0.7×VDD≈2.31V
1.2 问题根源
当编码器使用5V供电时:
-
编码器输出的高电平接近5V
-
STM32的GPIO引脚最大耐受电压通常是3.6V(有些型号有5V容忍引脚,但需要特殊配置)
-
5V信号直接输入到3.3V的GPIO可能超过其最大耐受电压,导致:
-
信号失真
-
输入保护二极管导通,将电压钳位在约3.6V
-
长期使用可能损坏GPIO引脚
-
2. 信号完整性问题
2.1 电压不匹配导致的信号问题
当5V信号输入到3.3V系统时:
-
过冲和振铃:信号边沿可能产生过冲,导致多次逻辑转换
-
反射:阻抗不匹配引起信号反射
-
阈值不确定性:信号在逻辑阈值附近徘徊,导致不可靠的检测
还有就是定时器溢出的问题,在上面已经讲过了。