文章目录
这篇博文写的时间确实有几天了,主要是想让需要的人更好地运用这一模块,同时将自己的使用经验分享给大家,就像当初迷茫的我,也是CSDN的 大佬们的指点迷津对我有了很大的帮助。
这几阶段,我主要是将一些模块知识的理解与运用,和一些项目的经验,后期打算深入编程语言与嵌入式相关技术。还有那一直想好好深入学习的数据结构,哈哈。
编码器在项目、竞赛中被广泛运用。很多运动控制系统都是一个闭环系统,而在这个闭环系统中,各类型的电机必然是执行器,但只有电机,那这个运动控制只能是开环的,我们需要一个反馈值,用于实现电机控制的闭环结构。编码器被广泛应用于电机测速,实现电机闭环控制。
下篇博文是上次写的关于电机控制的博文
电机控制程序
编码器(encoder)是将信号(如比特流)或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。
编码器把角位移或直线位移转换成电信号,前者称为码盘,后者称为码尺。
按照读出方式,编码器可以分为接触式和非接触式两种。
按照工作原理,编码器可分为增量式和绝对式两类。
我们在通常使用中,常以工作原理的形式进行分类,以供选型使用。
增量式编码器基本原理:
增量式编码器是将位移转换成周期性的电信号,再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。
先上图,留个印象。这是逐飞科技的一款正交编码器(有带方向的),(不是打广告)。
对于增量式的编码器,有几个基本的概念。
1、五根线,三根数据线A线、B线、Z线,还有VCC与GND。
其中,A、B均为数据输出线,输出的是脉冲数,用于数据加工处理。关于A、B线的不同之处,见下图。两者输出的脉冲存在90度的相位差。
Z线一般是零点信号的,就是当编码器旋转到零点位置时,它会发出一个脉冲用于表示,这个位置是生产商固定的,也可以称作机械零位。
对于增量式编码器的工作原理可见这篇博文:
增量式编码器详解

绝对式编码器基本原理:
与增量式通过计数脉冲数和判断脉冲的方向不同,绝对式编码器直接输出数字的传感器,更准确的说,是能给出与每个角位置相对应的完整的数宇量输出 。
所谓的位置信息大致可以这样理解,每个位置是唯一的,编码器旋转过程中,直接读取位置即可。编码器中有光码盘(可理解为下图)。
在它的圆形码盘上沿径向有若干同心码道,每条上由透光和不透光的扇形区相间组成,相邻码道的扇区数目是双倍关系,码盘上的码道数就是它的二进制数码的位数,在码盘的一侧是光源,另一侧对应每一码道有一光敏元件;当码盘处于不同位置时,各光敏元件根据受光照与否转换出相应的电平信号,形成二进制数。
这种编码器的特点是不要计数器,在转轴的任意位置都可读出一个固定的与位置相对应的数字码。显然,码道越多,分辨率就越高,对于一个具有 N 位二进制分辨率的编码器,其码盘必须有N 条码道。
(光码盘)
绝对式编码器类别:单圈绝对编码器和多圈绝对编码器
**1、旋转单圈绝对编码器,**以转动中测量光电码盘各道刻线,以获取唯一的编码,当转动超过360度时,编码又回到原点,这样就不符合绝对编码唯一的原则,这样的编码只能用于旋转范围360度以内的测量,称为单圈绝对值编码器。
如果要测量旋转超过360度范围,就要用到多圈绝对编码器。
2、旋转多圈绝对编码器,编码器生产厂家运用钟表齿轮机械的原理,当中心码盘旋转时,通过齿轮传动另一组码盘(或多组齿轮,多组码盘),在单圈编码的基础上再增加圈数的编码,以扩大编码器的测量范围,这样的绝对编码器就称为多圈式绝对编码器,它同样是由机械位置确定编码,每个位置编码唯一不重复,而无需记忆。
这部分可详细参考这篇博文。绝对式编码器
3、编码器运用实例
先看图,这是我在智能车赛测速用的编码器,WCHF103芯片与四个编码器之间的数据传输依靠的是SPI的“分时复用”,对于分时复用的概念,前几篇博文已经介绍过了。走SPI通信这种办法,确实有一定的优点,少用一个定时器,哈哈。
后来,我又写了这个编码器与32的芯片进行SPI通信的程序,有需要的可以评论区留下邮箱。

如下图。这个编码器挺好,便宜实用,上面两种有些小贵。霍尔编码器也是增量式编码器中的一种,有A、B相两相输出,与文章开篇的那种不同,但理解起来是一样的道理。
接下来我就讲讲自己对带编码器的直流减速电机的理解。

1)直流因为是直流电。
2)根据公式P=FV,功率相同条件下,力和速度成反比。
2)编码器主要用于测速。
(图片更加形象生动)
在带霍尔传感器的直流电机的条件下,电机转动一圈,通过霍尔传感器的A、B两相输出一定数量的脉冲,我们可以根据一定时间内的脉冲数计算出电机的瞬时速度。
记脉冲数有两种方式:
1) 我们通过定时器的输入捕获或者GPIO引脚的外部中断来检测边沿变化,以此来检测脉冲数,但是会有毛刺,也就是错误的脉冲信号。
2) 我们以stm32芯片作为主控,利用32的定时器外设的输入捕获功能,配置相关输入通道为编码器接口模式,就可以进行脉冲数的计数。
在第二种方式中,错误的脉冲信号会被输入滤波器过滤掉,所以更推荐也更常用。
注意点如下:
1) 通用、高级定时器具有输入捕获功能,基本定时器没有。
2) 在高级、通用定时器中不全都有编码器接口模式,只有TIM1-5、8具有编码器接口模式。(以STM32F103ZET6为例)
3) 一个定时器有四个独立的输入通道CH1-4,但只有CHI,CH2,也就是TI1、TI2,能作为编码器接口的输入通道。所以,用多少个霍尔编码器,就需要多少个定时器。
4) 编码器模式下,定时器可以理解为计数器。
根据上图,编码器的A、B两相通过接线,接到定时器输入通道1、2对应的GPIO引脚上即可,这样,电机转动,就会通过编码器产生连续的脉冲输入到T1,T2中。
接下来需要详细分析一下如何利用编码器接口模式进行测速的。
首先我们应该理解脉冲计数的原理。对于计数,有三种模式,
1) 计数器在T1输出的脉冲的上升沿或下降沿计数。
2) 计数器在T2输出的脉冲的上升沿或下降沿计数。
3) 计数器在T1和T2的输出脉冲的上升沿与下降沿计数。
但我们经常用第三种模式,因为提高了采样精度。具体的解释,可以看这个大佬的解释,好理解且实用博文链接
三种模式以及相关的计数方向如下表。

这里我先对这个表进行分析,便于理解。先分析表格。以仅在T1计数为例,相对应信号的电平就是指TI2是高电平还是低电平。TI1FP1信号指TI1的脉冲,然后脉冲分为上升沿与下降沿两种,同理可理解相应的表中的TI1FP2。
表中第一行,可以理解为在TI2上的脉冲为高电平时,如果TI1的脉冲为上升沿,则计数器+1,;如果TI1的脉冲为下降沿,则计数器-1。对于TI2的脉冲不进行计数,因为已经使用过其高低电平了,并且规定了只对TI1计数了。
其他行亦是这个道理。
下面这个表便是第三种模式下,计数器的工作过程,从图中我们可以知道,可以通过计数器是递增还是递减,来判断电机的正反转。而且可以看到,毛刺没过滤了,计数器碰到没有计数。

到这里想必对原理已经有较为清楚的理解了。
接下来,我们需要理解在编码过程中两个重要的参数。很重要!!!
1)重装载值
2)预分频系数
这两个系数在定时、PWM输出中都被运用到,且具有不同的含义,但这里我就详细讲一下在编码器计数中如何设定。
重装载值:
这里我们需要明白一点,电机转一圈输出的脉冲数是固定的,一般为360,具体的看情况,速度快了,也就可以理解为相同时间,转的圈数增多了。
遇到一个脉冲,计数器计数一次,当计数达到重装载值,就会产生溢出中断,计数器并清零重新计数,所以重装载值就是计数器脉冲计数的最大值。
预分频系数
原先定时器用作定时或者PWM输出,我们的预分频系数分的是32芯片的内部时钟,就是这个公式:CK_CNT=TIMxCLK/(PSC+1),这里的PSC就是预分频系数。但定时器用作编码器测速,这时时钟是外部时钟,也就是编码器采集的电机产生的脉冲。
预分频分的就是外部时钟的频率。
举个例子,如果你预分频系数为2,假设电机旋转一圈产生100个脉冲,则此时你单片机只能记录50个脉冲。
这两个因素应该理解的差不多了。
1、那我们如何计算转速呢,设重装载值为ARR,预分频系数置PSC,电机线数为360
那么电机转一圈,编码器采集到360*4 / PSC 个脉冲,因为预分频系数为PSC。再用定时器定时m秒,记录m秒内溢出的次数为n次,得到速度为 v=( n * ARR+当前的计数值) / (360 * 4 / PSC )/ m 。
总的来说,就是算每秒转了几圈。
2、预分频系数设置为 0 ,自动重装载值设为65535。然后在周期中断里每次去读取TIMx->CNT的值,并清零,以便于下一次读取。之所以在周期中断中,是因为我们读取的CNT的值相当于当前编码器的角度信息,以5ms的时间周期去读取,就可以看做相应时间编码器的变化,类似于测的效果。
下面这篇这篇博文可以作为借鉴。
博文链接
第一种测速方式为例。
这段测速的思路引自这位大佬,具体文章和代码见下面链接。
测速方式一原文
int Encoder_Timer_Overflow; //编码器溢出次数(每389*4溢出一次)
u16 Previous_Count; //上次TIM3->CNT的值
void TIM3_Encode_init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //GPIOA6和GPIOA7
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //速度100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //浮空
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA6和PA7
GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_TIM3); //GPIOA6复用为定时器3通道1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource7,GPIO_AF_TIM3); //GPIOA7复用为定时器3通道2
TIM_TimeBaseStructure.TIM_Period = arr; //(编码器线数-1)*4 四倍频原理
TIM_TimeBaseStructure.TIM_Prescaler=psc; //定时器分频
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分频因子,不分频
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure); //初始化TIM3
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1; //选择输入端IC1映射到TI1上
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising; //上升沿捕获
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI; //映射到TI1上
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM_ICInitStructure.TIM_ICFilter =6; //配置输入滤波器
TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel=TIM_Channel_2; //选择输入端IC2映射到TI2上
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising; //上升沿捕获
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI; //映射到TI2上
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM_ICInitStructure.TIM_ICFilter=6; //配置输入滤波器
TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising );//编码器配置(定时器、编码模式、上升沿、上升沿)
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断分组配置
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; //使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority =0x02; //响应优先级2
NVIC_Init(&NVIC_InitStructure); //配置定时器3
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断
TIM_Cmd(TIM3,ENABLE); //使能定时器3
}
void TIM3_IRQHandler(void) //定时器3中断服务函数
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
Encoder_Timer_Overflow++;
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
u32 Read_Encoder(void)
{
u32 Count; //一段时间内转过的脉冲数
u16 Current_Count; //当前TIM3->CNT的值
u16 Enc_Timer_Overflow_one;
Enc_Timer_Overflow_one=Encoder_Timer_Overflow;
Current_Count = TIM_GetCounter(TIM3); //获得当前TIM3->CNT的值
Encoder_Timer_Overflow=0; //清零,方便下次计算
if((TIM3->CR1&0x0010) == 0x0010) //如果反转
Count = (u32)((Enc_Timer_Overflow_one)* -1*(4*ENCODER_PPR) - (Current_Count - Previous_Count)); //计算出一个时间转过的脉冲数
else //如果正转
Count = (u32)(Current_Count - Previous_Count + (Enc_Timer_Overflow_one) * (4*ENCODER_PPR)); //计算出一个时间转过的脉冲数
Previous_Count = Current_Count;
return(Count);
}
//中间省略定时5的配置
void TIM5_IRQHandler(void) //定时器5中断服务函数
{
if(TIM_GetITStatus(TIM5,TIM_IT_Update)==SET) //溢出中断
{
encode=Read_Encoder(); //获得编码器的值
printf("编码器脉冲数为:%d\r\n",encode);
speed=encode/390.0f/4.0f/0.05f;
printf("电机转速为:%f\r\n",speed);
}
TIM_ClearITPendingBit(TIM5,TIM_IT_Update); //清除中断标志位
}
文章有些冗长,笔者我想尽量将各方面解释具体到位,不妥之处或者有疑问,可以在评论区留言,我会及时回复的。
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
我正在使用ruby1.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.\"\
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru