草庐IT

【一文读懂】如何用编码器测速

荒野火狐 2023-04-10 原文

文章目录


前言

详解编码器测速原理及实现
参考csdn


一、AB相编码器计数原理

适用于霍尔编码器和带光栅的光电编码器
编码器样子

让遮挡与透过形成高低电平的脉冲

此图为B 提前 A 90° 为反向旋转,相位差相差90°的波形就叫正交波形
当只有一个方波 只能测位置、速度,不能知道方向
而两个方波,就能一个测速,一个测方向
例:
A下降沿触发 并且B是低电平 判断反转
B下降沿触发 并且A是低电平 判断正转

四倍频

还有一种用法 叫四倍频,它是数 A相 和 B相 的上升和下降脉冲 4个信号来测速可以用来提高精度,因为一般只需要考虑A相一个上升或下降沿计数就行,这里数4个,所以叫四倍频。
stm32的编码器接口应该默认是四倍频的:
这里的大佬,说
手动转一圈的脉冲数 n:1560 个;
且用的是13线编码器 和 1:120转速比的电机,那么计算一圈的脉冲数则是
13 *(120)*4 = 1560;其中4应该就是一个周期数4个脉冲沿。这里计算后面会讲。

二、要用到的一些参数

编码器参数

编码器线数:线数就是编码器的分辨率,即转一圈发出的脉冲数
例:光电编码器:500ppr (500线)
霍尔编码器: 13ppr (13线)

电机参数

减速比 :输入转速/输出转速

一般减速比的表示方法是以1为分母,用“:”连接的输入转速和输出转速的比值,如输入转速为1500r/min,输出转速为25r/min,那么其减速比则为:i=60:1。

轮子参数

用直尺 或商家给的参数 得到
轮子直径 :65mm = 0.065m

计算

例 :500线的编码器 和30:1减速比的电机
也就是说 电机实际转30圈 但现实轮子只转一圈
那么轮子转一圈 编码器得到的脉冲数为 500*30 = 15000 (个)

速度计算思路

设 定时器设定时间为t,D为直径,则


e: 由程序得到
PI : 3.1415926
D : 0.065m
n :15000
另定时500ms
t = 0.5s
所以 speed = e * 0.00002722713 (m/s)
为了数据好显示 speed =1000* e * 0.00002722713 (mm/s)

三、代码如下(更契合原理的代码)

没有使用32的编码器接口

Enconder.c:

#include "stm32f10x.h"                  // Device header
//-32768 ~ +32767 编码器范围 定时器是16位
//这里没用stm32自带的编码器接口(TIM_EncoderInterfaceConfig) , 而是用原理写出

int16_t Encoder_Count_Left;//带符号
int16_t Encoder_Count_Right;//带符号
// B接PB1  A接PB0

//A下降沿触发 并且B是低电平 判断反转
//B下降沿触发 并且A是低电平 判断正转

//左编码 E1B E1A
//       PB1 PB0
void Encoder_Init_Left(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//AFIO中断引脚选择
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//第0个线路拨到GPIOB上
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//
	
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断模式  也有事件模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	EXTI_Init(&EXTI_InitStructure);
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
	NVIC_Init(&NVIC_InitStructure);
}

//单位时间内读取编码器计数  变化的值
int16_t Encoder_Get_Left(void)
{
	int16_t Temp;
	Temp = Encoder_Count_Left;
	Encoder_Count_Left = 0;
	return Temp;
}


//如果是9-10 中断函数  只需要将两个if并列的放在一个函数里就行了

//A下降沿触发 并且B是低电平 判断反转
void EXTI0_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line0) == SET)
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
		{
			Encoder_Count_Left --;
		}
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}
//B下降沿 A低电平 判断正转
void EXTI1_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line1) == SET)
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
		{
			Encoder_Count_Left ++;
		}
		EXTI_ClearITPendingBit(EXTI_Line1);//清楚标志位
	}
}

//右编码 E2B  E2A
//       PB11 PB10

void Encoder_Init_Right(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//AFIO中断引脚选择
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource10);//第0个线路拨到GPIOB上
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource11);//
	
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line10 | EXTI_Line11;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断模式  也有事件模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	EXTI_Init(&EXTI_InitStructure);
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
	NVIC_Init(&NVIC_InitStructure);
	
}

//变化的值
int16_t Encoder_Get_Right(void)
{
	int16_t Temp;
	Temp = Encoder_Count_Right;
	Encoder_Count_Right = 0;
	return Temp;
}


//如果是10-15 中断函数  只需要将两个if并列的放在一个函数里就行了

//因为轮子是对称转 所以镜像 相反

//右编码 E2B  E2A
//       PB11 PB10

//A下降沿触发 并且B是低电平 判断正转
void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line10) == SET)
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)
		{
			Encoder_Count_Right ++;
		}
		EXTI_ClearITPendingBit(EXTI_Line10);
	}
	B下降沿 A低电平 判断反转
	if (EXTI_GetITStatus(EXTI_Line11) == SET)
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == 0)
		{
			Encoder_Count_Right --;
		}
		EXTI_ClearITPendingBit(EXTI_Line11);//清楚标志位
	}
}




void Get_Motor_Speed(int *LSpeed,int *RSpeed)
{
	static int LWheelEncoderNow   = 0;
	static int RWheelEncoderNow   = 0;
	
	static int LWheelEncoderLast  = 0;
	static int RWheelEncoderLast  = 0;	

	
	//记录本次左右编码器数据
	LWheelEncoderNow += Encoder_Get_Left();
	RWheelEncoderNow += Encoder_Get_Right();

	
	//500ms测速    (	*1000 )单位改为mm/s  变化显示更清楚
	*LSpeed  = (LWheelEncoderNow - LWheelEncoderLast)* 1000*0.00002722713;  
	*RSpeed  = (RWheelEncoderNow - RWheelEncoderLast)* 1000*0.00002722713;


	//记录上次编码器数据
	LWheelEncoderLast = LWheelEncoderNow;                    
	RWheelEncoderLast = RWheelEncoderNow;  

}

/*
static int 不管在函数内还是函数外,都作为一个全局变量可以保存它被修改以后的值。

而 int 则没有这一功能,只有作为全局变量时能保存修改。放在函数内部时,每次调用都用的是一个新的数。
*/


Encoder.h

#ifndef __ENCODER_H
#define __ENCODER_H

//左编码 E1B E1A
//       PB1 PB0
void Encoder_Init_Left(void);
int16_t Encoder_Get_Left(void);

//右编码 E2B E1A
//       PB11 PB10
void Encoder_Init_Right(void);
int16_t Encoder_Get_Right(void);

void Get_Motor_Speed(int *LSpeed,int *RSpeed);


#endif

Timer.c

#include "stm32f10x.h"                  // Device header

//extern uint16_t Num; //引用其他文件(主函数)的Num变量

void TIM4_Timer_Init(u16 arr,u16 psc)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//TIM2 APB1总线上的外设
	
	TIM_InternalClockConfig(TIM4); //选择内部时钟
	
	//CK_CNT_OV = CK_PSC/(PSC+1)/(ARR+1)
	//预分频给少点 自动重装给多点 以一个比较高的频率计比较多的数
	//预分频给多点 自动重装给少点 以一个低的频率计比较少的数
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //配置时基单元
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频 /1分频 影响不大
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
	TIM_TimeBaseInitStructure.TIM_Period = arr; //周期 ARR自动重装器的值  10000 - 1
	TIM_TimeBaseInitStructure.TIM_Prescaler = psc;  //PSC预分频器的值 10k的计数频率 计10000的数  7200 - 1
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值   //高级定时器才有 这里通用计时器 不用
	TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStructure);
	
	
	TIM_ClearFlag(TIM4, TIM_FLAG_Update);//手动把更新中断标志位清除一下 防止 reset直接到1
	TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //使能中断 update 更新中断
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); //优先级分组
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; //3通道 //f10x.h 255行
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_Cmd(TIM4, ENABLE); //启动定时器
}

/*
void TIM2_IRQHandler(void) //定时器2 的中断函数
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//看更新中断标志位
	{
		Num++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}
*/

Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void TIM4_Timer_Init(u16 arr,u16 psc);

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Motor.h"
#include "Encoder.h"
#include "Timer.h"

uint8_t KeyNum;
int8_t Speed; 
int16_t NumL;
int16_t NumR;
int LSpeedNow = 0;
int RSpeedNow = 0;

int main(void)
{
	OLED_Init();
	Motor_Init_Left();
	Motor_Init_Right();
	Encoder_Init_Left();
	Encoder_Init_Right();
	TIM4_Timer_Init(5000-1,7200-1);//500ms 定时
	
	OLED_ShowString(1, 1, "VLeft:");
	OLED_ShowString(2, 1, "VRight:");
	OLED_ShowString(3, 1, "LSpd:");
	OLED_ShowString(4, 1, "RSpd:");
	
	while (1)
	{
		//KeyNum = Key_GetNum();
		/*
		Delay_ms(1000);
		KeyNum = 1;
		if (KeyNum == 1)
		{
			Speed += 20;
			if (Speed > 100)
			{
				Speed = -100;
			}
		}
		*/
		Speed = 10;
		Motor_SetSpeed_Left(Speed);
		Motor_SetSpeed_Right(Speed);
		
		//NumL += Encoder_Get_Left();//调用这个函数的间隙里,旋转编码器产生的正负脉冲数
		OLED_ShowSignedNum(3, 5, LSpeedNow, 3);
		
		//NumR += Encoder_Get_Right();//调用这个函数的间隙里,旋转编码器产生的正负脉冲数
		OLED_ShowSignedNum(4, 5, RSpeedNow, 3);
		
		OLED_ShowSignedNum(1, 8, Speed, 3);
		OLED_ShowSignedNum(2, 8, Speed, 3);
		
		OLED_ShowSignedNum(4, 10, NumL, 2);
	}
}

void TIM4_IRQHandler(void) //定时器4 的中断函数
{
	if (TIM_GetITStatus(TIM4, TIM_IT_Update) == SET)//看更新中断标志位
	{
		Get_Motor_Speed(&LSpeedNow,&RSpeedNow);
		TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
	}
}


四倍频的代码(更新)

在原来基础上修改,这里写的四倍频不管向前还是向后转后会加加,理论上按照00 ,01 ,10,11 四倍频也能有方向,但觉得写的太烦,没写,下面的四倍频已实操过可以用来调pid,在本来的脉冲数上*4就行。

//四倍频

void EXTI0_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line0) == SET)
	{
		//if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
		//{
			Encoder_Count_Left++;
		//}
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}
//B下降沿 A低电平 判断正转
void EXTI1_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line1) == SET)
	{
		//if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
		//{
			Encoder_Count_Left++;
		//}
		EXTI_ClearITPendingBit(EXTI_Line1);//清楚标志位
	}
}

void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line10) == SET)
	{
		//if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)
		//{
			Encoder_Count_Right++;
		//}
		EXTI_ClearITPendingBit(EXTI_Line10);
	}
	B下降沿 A低电平 判断反转
	if (EXTI_GetITStatus(EXTI_Line11) == SET)
	{
		//if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == 0)
		//{
			Encoder_Count_Right++;
		//}
		EXTI_ClearITPendingBit(EXTI_Line11);//清楚标志位
	}
}

至此就可以在oled上显示速度了。

有关【一文读懂】如何用编码器测速的更多相关文章

  1. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  2. ruby - 用逗号、双引号和编码解析 csv - 2

    我正在使用ruby​​1.9解析以下带有MacRoman字符的csv文件#encoding:ISO-8859-1#csv_parse.csvName,main-dialogue"Marceu","Giveittohimóhe,hiswife."我做了以下解析。require'csv'input_string=File.read("../csv_parse.rb").force_encoding("ISO-8859-1").encode("UTF-8")#=>"Name,main-dialogue\r\n\"Marceu\",\"Giveittohim\x97he,hiswife.\"\

  3. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  4. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

  5. c - Ruby - 源代码 - 编码风格 - 2

    查看Ruby代码,它具有以下proc_arity:staticVALUEproc_arity(VALUEself){intarity=rb_proc_arity(self);returnINT2FIX(arity);}更多的是C编码风格问题,但为什么staticVALUE在单独的一行而不是像这样的:staticVALUEproc_arity(VALUEself) 最佳答案 它来自UNIX世界,因为它有助于轻松grep函数的定义:$grep-n'^proc_arity'*.c或使用vim:/^proc_arity

  6. ruby-on-rails - 如何用不同的用户运行nginx主进程 - 2

    A/ctohttp://wiki.nginx.org/CoreModule#usermaster进程曾经以root用户运行,是否可以以不同的用户运行nginxmaster进程? 最佳答案 只需以非root身份运行init脚本(即/etc/init.d/nginxstart),就可以用不同的用户运行nginxmaster进程。如果这真的是你想要做的,你将需要确保日志和pid目录(通常是/var/log/nginx&/var/run/nginx.pid)对该用户是可写的,并且您所有的listen调用都是针对大于1024的端口(因为绑定(

  7. ruby - 如何以编程方式删除实例上的 "singleton information"以使其编码(marshal)? - 2

    我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。

  8. ruby - 如何用递增的值填充数组 Ruby - 2

    我正在尝试解决http://projecteuler.net/problem=1.我想创建一个方法,它接受一个整数,然后创建一个包含它前面的所有整数的数组,并将整数本身作为数组中的值。以下是我目前所拥有的。代码不起作用。defmake_array(num)numbers=Array.newnumcount=1numbers.eachdo|number|numbers 最佳答案 (1..num).to_a是您在Ruby中需要做的全部。1..num将创建一个Range对象,以1开始并以任意值num结束是。Range对象有to_a方法通过

  9. ruby - 如何用 Nokogiri 解析连续的标签? - 2

    我有这样的HTML代码:Label1Value1Label2Value2...我的代码不起作用。doc.css("first").eachdo|item|label=item.css("dt")value=item.css("dd")end显示所有首先标记,然后标记标签,我需要“标签:值” 最佳答案 首先,您的HTML应该有和中的元素:Label1Value1Label2Value2...但这不会改变您解析它的方式。你想找到s并遍历它们,然后在每个你可以使用next_element得到;像这样:doc=Nokogiri::HTML(

  10. ruby - 更改字符编码 - 2

    我在使用Ruby1.9.2p290更改文本文件的编码时遇到问题。我收到错误消息invalidbytesequenceinUTF-8(ArgumentError)。问题(我认为)在于字符集似乎是未知的。如果我执行以下操作,则从命令行:$filetest.txt我得到:Non-ISOextended-ASCIIEnglishtext,withCRLFlineterminators或者,或者,如果我这样做:$file-itest.txt我得到:test.txt:text/plain;charset=unknown但是,如果我这样做,在Ruby中:data=File.open("test.tx

随机推荐