这已经是两年前我的文章草稿了。今天无意间看到了,想了想还是打算补充完整吧。
前言
市场上常见的增量式编码器大多都是A,B,Z相,但例如A+,A-,B+,B-,Z+,Z-这类的编码器其实就是所谓的差分编码,一般出现在有伺服驱动器的场景上用到,或者需要远距离传输时。因为带有对称负信号连接,电流对于电缆贡献的电磁场为0,衰减最小,抗干扰最佳,可传输较远的距离。那么问题来了,在使用定时器编码器模式时,该怎样去接线。这时候就需要用到差分转单端外围电路的实现。
以三路编码接口AB相为实例(如果需要Z相,原理基本一样),附带原理图(亲测),需要封装库的话可以私信我
一、电路实现
IC芯片使用的是TLP2745高速隔离光耦,足以满足速率要求(理论可以满足1MHZ频率)。且支持不超过30V电压输入。
这里3脚接了个1K限流电阻,输入电压以是5V为例。如果需要其他电压的输入,要更改此处的电阻值。输出电压是3V3。附带LED指示灯。
注意:虽然是差分电路,但是频率并不高,阻抗可以不计算。在某些场景超过100MHZ的情况下才需要严格按照规定进行阻抗匹配设计。
二、代码实现
1.硬件设计
根据原理图可以得知
都是使用定时器通道1,2的编码器接口模式(STM32使用编码器模式必须接通道1和通道2)
知道A,B相相位都存在90度的极限位差,可以得到电机此时的正反转状态。
要实现代码,还需知道编码器的分辨率是多少,实际应用中,都要用到四倍频,也就是上升沿下降沿都计算,获得四倍精准度(编码器模式本身就有这个模式,不需要担心四倍频会增加CPU负担,如果不倍频,那就是输入捕获功能了)
如果使用伺服驱动器,在某个参数可以设置编码器线数,以台达伺服为例,如设置500,实际到程序中四倍频后就是2000脉冲,也就是旋转一圈的脉冲数为2000。
如果使用增量式编码器,一般上面都会标注编码器线数。
例如,标注1000P/R,一圈脉冲数为1000,经过四倍频后,得到4000P/R。
注意:脉冲数越高,控制精度越高,可以根据实际设备选择分辨率。
2.软件设计
使用HAL库,以STM32F407为例:
硬件:
编码器输入(3-5V输入):
PA15 → A1 TIM2_CH1
PA1 → B1 TIM2_CH2
PC6 → A2 TIM3_CH1
PC7 → B2 TIM3_CH2
软件:
1. 初始化编码器接口
需要对每个定时器进行初始化,以便它们能够以编码器模式工作。以下是针对一个定时器(例如,TIM2)的示例初始化代码:
/* 宏定义 --------------------------------------------------------------------*/
//A1/B1
// 定义编码器接口使用的定时器,此处为 TIM2
#define ENCODERAB1_TIMx TIM2
// 使能定时器模块的时钟,这里指的是 TIM2
#define ENCODERAB1_TIM_RCC_CLK_ENABLE() __HAL_RCC_TIM2_CLK_ENABLE()
// 禁用定时器模块的时钟,这里指的是 TIM2
#define ENCODERAB1_TIM_RCC_CLK_DISABLE() __HAL_RCC_TIM2_CLK_DISABLE()
// 使能用于编码器信号的 GPIO 端口A的时钟
#define ENCODERAB1_TIM_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
// 定义编码器通道1的引脚和端口,此处为 PA15
#define ENCODERA1_TIM_CH1_PIN GPIO_PIN_15
#define ENCODERA1_TIM_CH1_GPIO GPIOA
// 定义编码器通道2的引脚和端口,此处为 PA1
#define ENCODERB1_TIM_CH2_PIN GPIO_PIN_1
#define ENCODERB1_TIM_CH2_GPIO GPIOA
// 定义 GPIO 引脚的替代功能,这里为 TIM2 的 AF1
#define GPIO_AB1_AFx_TIMx GPIO_AF1_TIM2
// 定义编码器接口模式,这里配置为同时使用 TI1 和 TI2
#define TIM_ENCODERMODE_TIx TIM_ENCODERMODE_TI12
// 定义定时器的 IRQ 号,这里是 TIM2 的 IRQ 号
#define ENCODER1_TIM_IRQn TIM2_IRQn
// 定义定时器的 IRQ 处理函数,这里是 TIM2 的 IRQ 处理函数
#define ENCODER1_TIM_IRQHANDLER TIM2_IRQHandler
//A2/B2
//类似方法
/* 函数体 --------------------------------------------------------------------*/
/**
* @brief 通用定时器初始化并配置通道PWM输出
* @param 无
* @retval 无
* @note 无
*/
void Encoder_TIMx_Init(void){
/* 定时器编码器配置结构声明 */
TIM_Encoder_InitTypeDef sConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* 定时器基本环境配置 */
//A1/B1相
htim2_Encoder.Instance = ENCODERAB1_TIMx; //定时器选择
htim2_Encoder.Init.Prescaler = ENCODER_TIM_PRESCALER; //定时器预分频
htim2_Encoder.Init.CounterMode = TIM_COUNTERMODE_UP; //计数模式模式
htim2_Encoder.Init.Period = ENCODER_TIM32_PERIOD; //定时器周期
htim2_Encoder.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; //时钟分频因子
htim2_Encoder.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; //预装载
sConfig.EncoderMode = TIM_ENCODERMODE_TIx; //信号捕获模式
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; //输入捕获极性
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; //输入捕获选择
sConfig.IC1Prescaler = TIM_ICPSC_DIV1; //输入捕获预分频
sConfig.IC1Filter = 0; //输入捕获滤波器
sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; //输入捕获极性
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; //输入捕获选择
sConfig.IC2Prescaler = TIM_ICPSC_DIV1; //输入捕获预分频
sConfig.IC2Filter = 0; //输入捕获滤波器
if (HAL_TIM_Encoder_Init(&htim2_Encoder, &sConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2_Encoder, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
//A2/B2相
//类似方法
}
对于其它定时器,需要重复类似的初始化过程,只是改变相应的定时器实例和相关引脚配置。
2. 启动 / 停止编码器接口
在主函数或者适当的初始化部分中,启动或停止每个定时器的编码器模式:
/**
* @brief 启动编码器接口
* @param 无
* @retval 无
* @note 无
*/
void Start_Encoder(void){
HAL_TIM_Encoder_Start(&htim2_Encoder,TIM_CHANNEL_ALL);
// 重复此过程以启动TIM3
}
/**
* @brief 停止编码器接口
* @param 无
* @retval 无
* @note 无
*/
void Stop_Encoder(void){
HAL_TIM_Encoder_Stop(&htim2_Encoder,TIM_CHANNEL_ALL);
// 重复此过程以停止TIM3
}
3. 读取编码器值
要读取编码器的值,可以直接读取定时器的计数器:
// 定义定时器周期,当定时器开始计数到ENCODER_TIMx_PERIOD值是更新定时器并生成对应事件和中断
#define ENCODER_TIM16_PERIOD 0xFFFF
#define ENCODER_TIM32_PERIOD 0xFFFFFFFF
/**
* @brief 获取X轴编码器计数值
* @param 无
* @retval Value:返回32位有符号计数值
* @note 无
*/
int32_t XEncoder_GetCounting(void){
#if ( ENCODER_TIM16_PERIOD > 0xFFFF )
/* 32bits 计数器 */
int32_t Value = __HAL_TIM_GET_COUNTER(&htim2_Encoder);
/* 假定定时器不会溢出 */
return Value ;
#else
/* 16bits 定时器 */
uint32_t Value = __HAL_TIM_GET_COUNTER(&htim2_Encoder);
int32_t Period = ENCODER_TIM16_PERIOD + 1;
/* 16bits 定时器容易溢出 */
return Value + XOverflowCount * Period;
#endif
}
// 用类似的方式读取TIM3的值,例如y轴
4. 处理方向和速度
你可以通过比较连续的编码器读数来确定电机的方向和速度。这部分需要结合你的具体应用逻辑来实现。
5. 清零或处理溢出
你可能需要定期清零计数器或者处理溢出情况。
例如对定时器TIM2清零:
__HAL_TIM_SET_COUNTER(&htim2, 0);
在适当的位置调用此类代码,比如在初始化函数中,或者在需要重置计数器的事件处理中
清零操作应谨慎使用,特别是在动态监测编码器值的系统中。在不适当的时机重置计数器可能会导致控制逻辑的错误判断或系统状态的混乱
例如对定时器TIM2处理溢出:
确保在定时器初始化时启用了溢出(更新)中断:
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
然后,在定时器的中断服务例程中,添加处理溢出的代码:
void TIM2_IRQHandler(void){
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
{
if(__HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
// 溢出处理代码
}
}
}
注意事项
确保你的编码器输入信号电平与单片机的I/O口兼容(例如:3-5V输入)。
如果你的编码器信号频率非常高,你可能需要调整定时器预分频器和输入滤波器以获得稳定的读数。
对于伺服驱动器,还需要考虑与驱动器的通信和控制逻辑,例如位置控制、速度控制等。
这是一个基础的程序设计框架,具体细节需要根据你的实际应用和硬件环境进行调整。
文章总结
编码器接口初始化:在软件方面,关键步骤是正确初始化各个定时器的编码器接口模式。这包括设置定时器的计数模式、编码器模式、输入捕获的极性和滤波器设置。
软件设计灵活性:STM32提供的库函数使得编码器接口的实现变得相对简单,同时也为定制化的控制逻辑提供了便利。
可靠性和精确性:由于使用了差分信号,这种设计在抗干扰方面表现良好,特别是在电气噪声较多的工业环境中。
个人结语
在曾经的嵌入式龙卷风,我是那个寻找完美电路设计的猎人。世界上没有现成的解决方案,就像超市里永远买不到成熟的香蕉。所以,我拿起了我的科技魔杖,变身为“逆向科研攻城狮”,开始了我的神秘探索之旅。
在这里,我需要的是一双锐利的眼睛,一只放大镜,和一颗不怕失败的心。就像古老的炼金术士,用他们的烧瓶和坩埚转化铅为金,我用我的放大镜和万用表在电路板上舞动。
每一个电阻,每一个电容,都是我的舞伴。我跟随着电流的节奏,从一个接点跳到另一个接点。在这个过程中,我遇到了无数的挑战,但也正是这些挑战,让我的电路更加完美,更加强大。
有时,我感觉自己就像是一个硬币铸币厂,每一枚硬币都是我辛勤劳动的成果。而当我最终完成我的作品时,那一刻的喜悦,是无法用言语来形容的。我创造了一个不仅稳定性强,而且能抵抗各种干扰的编码器硬件电路。这不仅是一段电路,而是一段艺术,一段由放大镜、万用表和示波器构建的艺术。
工业需要开源,就像马斯克一样!