返回列表 发新帖我要提问本帖赏金: 200.00元(功能说明)

[ARM入门] 还不知道电机转速?看看电机重点应用:基于GD32的M/T法测速

[复制链接]
 楼主| 呐咯密密 发表于 2022-8-19 16:24 | 显示全部楼层 |阅读模式
<
本帖最后由 呐咯密密 于 2022-8-22 10:07 编辑

[url=home.php?mod=space&uid=760190]@21小跑堂 #申请原创#[/url]
前言

大家好,好久没写原创了,失踪人口回归首秀。带大家玩一玩电机测速。


在有感电机控制中,获取电机转速是非常重要的步骤,转速获取越准确,控制电机时越方便,抛开霍尔不谈,这里讨论电机编码器。

目前常见的电机编码器按种类分为绝对值编码器和增量编码器,绝对值编码器相对较为方便,编码器直接通过通讯接口输出绝对位置信息,MCU接收角度信息便可。而增量编码器就需要MCU测量编码器输出的脉冲信号,通过计算脉冲信号的频率,从而计算实际转速,同时通过A  B两相的相位差获取旋转方向。通过这样的方式测量转速一般有三种方法,下面就这三种方法展开介绍,并分析每个方法的优劣势。

一、测频率法(M法)

M法测速又称“定时测角法”,指在一定周期T内,测量编码器输出的脉冲个数M1,然后计算出转速。


首先已知电机旋转一周,编码器会产生P个脉冲信号,P由编码器线数和倍频数的乘积决定,例如2500线4倍频编码器,则P = 2500*4 = 10000。在一定时间周期T内,测量编码器输出的脉冲个数M1,则编码器输出频率为:

用频率f除以一圈脉冲数p就可以得到电机转速n:



电机转速单位为r/min,所以还需要再乘以60:

电机一圈脉冲数P和周期T一般是已知常数,那电机的转速和单位时间内的脉冲计数M1成正比,试想,当单位时间内捕获到M1个脉冲,刚好未捕捉到M1+1个脉冲,此时误差最大,最大误差为一个脉冲,所以误差率为:

测速误差率与脉冲个数成反比关系,转速越高 M1 值越大,当转速很低时,M1 值很小,误差率会变大,因此 M 法适合高速测量


二、测周期法(T法)

T 法又称“定角测时法”,是测量编码器相邻两个脉冲之间的时间间隔来计算转速,也被称为周期法。实际使用中通过一个高频时钟脉冲的个数 M2 来计算编码器两个脉冲之间的时间间隔。


另高频脉冲频率为f0,两个相邻脉冲之间的时间间隔Tt = M2/f0,电机的转速可以表示为:

T法测速误差来源于高频脉冲数量,最多可能产生一个高频脉冲的误差,当产生了一个脉冲误差时,最大误差率为:

这里可以预料到的是,转速越快,间隔时间Tt越短,高频脉冲个数M2越少,当丢失一个高频脉冲数,就会对转速造成很大影响。当转速越慢,单位时间的M2越多,误差越小。因此T法测速更适合低转速应用


三、M/T法测速

以采样周期为基准,在采样时间T内,同时计算编码器输出的脉冲数量M1和高频脉冲数量M2,同时尽量保持两个计数时间的的严格同步,最大限度减小误差。

设电机旋转一圈编码器输出脉冲数为p,高频脉冲频率为fc,T时间内,高频脉冲计数为M2,编码器输出脉冲数为M1,则此时T时间内电机旋转圈数:

一秒电机旋转圈数为:

每分钟的转速为:

T可通过高频脉冲计数原理求得:

所以最终电机转速为:

此种方式在低速和高速都有很高的分辨率,最适合宽速度检测范围使用。

四、如何在GD32单片机上实现上述过程

这里本人从M法测速开始,发现低转速速度误差太大,于是升级了M/T法测速,这里仅介绍这两种的实现,T法就不做实现,其实M/T能实现,T法就一定不会出问题啦。下文将逐步讲解我的开发历程。


1. 试验资料

    a. GD32E230C8T6
    b. LME2500FE   2500线四倍频磁编码器
    c. 纸飞机串口助手
    d. 伺服电机+驱动器

2. M法测速实现及试验结果

在设计初期,使用定时器2的通道0输入捕获来获取编码器的输出脉冲M1,同时使用定时器0作为时间T。

首先将定时器2设置为输入捕获,同时开启通道0的中断,每捕获到一个脉冲则进入一次中断,将标志位+1;定时器0作为普通中断,开启溢出中断,定时检测捕获数量。计算完成存入数组,在主函数转换为速度,通过串口发送到PC进行显示。

  1. void timer2_configuration(void)
  2. {
  3.         gpio_configuration();
  4.         
  5.     timer_ic_parameter_struct timer_icinitpara;
  6.     timer_parameter_struct timer_initpara;
  7.     /* enable the TIMER clock */
  8.     rcu_periph_clock_enable(RCU_TIMER2);

  9.     timer_deinit(TIMER2);
  10.     timer_struct_para_init(&timer_initpara);
  11.     timer_initpara.prescaler         = 0;
  12.     timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
  13.     timer_initpara.counterdirection  = TIMER_COUNTER_UP;
  14.     timer_initpara.period            = 65535;
  15.     timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
  16.     timer_init(TIMER2, &timer_initpara);

  17.     timer_channel_input_struct_para_init(&timer_icinitpara);
  18.     timer_icinitpara.icpolarity  = TIMER_IC_POLARITY_BOTH_EDGE;
  19.     timer_icinitpara.icselection = TIMER_IC_SELECTION_DIRECTTI;
  20.     timer_icinitpara.icprescaler = TIMER_IC_PSC_DIV1;
  21.     timer_icinitpara.icfilter    = 0x0;
  22.     timer_input_pwm_capture_config(TIMER2, TIMER_CH_0, &timer_icinitpara);

  23.     timer_input_trigger_source_select(TIMER2, TIMER_SMCFG_TRGSEL_CI0FE0);
  24.     timer_slave_mode_select(TIMER2, TIMER_SLAVE_MODE_RESTART);

  25.     timer_master_slave_mode_config(TIMER2, TIMER_MASTER_SLAVE_MODE_ENABLE);
  26.     timer_auto_reload_shadow_enable(TIMER2);
  27.     timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_CH0);
  28.     timer_interrupt_enable(TIMER2, TIMER_INT_CH0);
  29.     timer_enable(TIMER2);
  30. }

  31. void timer_configuration(void)
  32. {
  33.     timer_ic_parameter_struct timer_icinitpara;
  34.     timer_parameter_struct timer_initpara;


  35.     rcu_periph_clock_enable(RCU_TIMER0);

  36.     timer_deinit(TIMER0);
  37.     timer_struct_para_init(&timer_initpara);
  38.     timer_initpara.prescaler         = 71;
  39.     timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
  40.     timer_initpara.counterdirection  = TIMER_COUNTER_UP;
  41.     timer_initpara.period            = 1999;//1MS
  42.     timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
  43.         timer_initpara.repetitioncounter = 0;
  44.     timer_init(TIMER0, &timer_initpara);

  45.     timer_auto_reload_shadow_enable(TIMER0);
  46.     timer_interrupt_flag_clear(TIMER0,TIMER_INT_FLAG_UP);
  47.     timer_interrupt_enable(TIMER0,TIMER_INT_FLAG_UP);
  48.     timer_enable(TIMER0);        
  49. }

  50. void TIMER2_IRQHandler(void)
  51. {
  52.     if(SET == timer_interrupt_flag_get(TIMER2, TIMER_INT_FLAG_CH0)){
  53.         timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_CH0);
  54.                                 readvalue2++;
  55.     }
  56. }
  57. void TIMER0_BRK_UP_TRG_COM_IRQHandler(void)
  58. {
  59.         if(SET == timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP))
  60.         {
  61.                 timer_interrupt_flag_clear(TIMER0, TIMER_FLAG_UP);
  62.                 if(readvalue2 > readvalue1){
  63.                                 count = (readvalue2 - readvalue1);
  64.                 }else{
  65.                                 count = ((0xFFFFU - readvalue1) + readvalue2);
  66.                 }
  67.                 TIM_NUM++;
  68.                 readvalue1=readvalue2;
  69.                 if(TIM_NUM>999&TIM_NUM<1999)
  70.                 {
  71.                         Speed_num[TIM_NUM-1000] = count+1;                        
  72.                 }
  73.                 if(TIM_NUM>1999)
  74.                 {
  75.                         timer_disable(TIMER0);
  76.                 }
  77.         }
  78. }

在定时器0的中断响应函数中,对TIMER0的当前捕获值和上一次捕捉只进行运算,得出单位时间T内的脉冲数量,并将值保存进Speed_num数组,保存到1000个TIMER0失能中断,主函数开始处理数据并发送到上位机。

  1.     while(1){
  2.                 if(TIM_NUM>1999)
  3.                 {                        
  4.                         for(uint16_t p=0;p<999;p++)
  5.                         {
  6.                                 kalman_height = Speed_num[p]*60/10000*0.02;
  7.         //                                                kalman_height=kalmanFilter(&KFP_height,kalman_height);
  8.                                 printf("{Speed:%f}\n",kalman_height);                                       
  9.                         }
  10.                         TIM_NUM = 0;
  11.                         timer_enable(TIMER0);        
  12.                 }
  13.                         
  14.     }

其中,M1 = Speed_num[p],P = 2500*4 = 10000,T = 20ms =0.02s。

开启伺服电机驱动器,设置速度3000转,先使用驱动器读取一下速度波动,如下图一所示:

图一:伺服驱动读取速度波动-3000rp/m

再通过串口助手获取单片机M法测速速度波动,如下图二所示:

图二:M法测速测得速度波动-3000rp/m

因为两张图的采样频率不同,显示的效果也会不同,而且驱动器会进行数据滤波,但是还是可以看出速度分布在3000附近,下面我们将速度数据卡尔曼滤波(后文所有的图均默认进行滤波处理),如下图三所示。

图三:M法卡尔曼滤波速度-3000rp/m

由上图三和图一进行比较,可以发现我们使用GD32的M法测速在3000转时表现较好,基本和驱动器测得数据较为吻合,说明方法是没问题的,于是我们降低转速,继续对比。


图四:伺服驱动读取速度波动-2000rp/m

图五:M法卡尔曼滤波速度-2000rp/m

图六:伺服驱动读取速度波动-1000rp/m

图七:M法卡尔曼滤波速度-1000rp/m

从图四-图七,对比就会发现,在2000rp/m时,速度已经发生了偏移,速度点以2002rp/m为中心分布,在1000rp/m时,速度点以2004rp/m为中心分布,速度越低,产生的速度误差越大。


3. M/T法测速实现及试验结果

初期试验:

一开始我使用三个定时器,

定时器2继续使用M法的捕获功能,捕获M1的值,使用定时器14产生高频脉冲,每当计数器到达就触发中断,获取M2的值,最后在定时器0中定时获取M1和M2的值,在主函数打印。此方法在3500rp/m以下均无问题,但是速度再提高,编码器输出的频率增高,导致中断处理不过来了,M1不再随着转速的提高而提高,速度显示也就无法提高了。后来想到这块MCU的定时器是有正交译码器的。如此便不会受中断的影响。


GD32E230之正交译码器的M/T法测速。


介于本篇文章篇幅已经很长了, 就不介绍定时器的正交译码器功能了,该功能网络上资料很多,这里不赘述,我们从代码入手:

  1. int rmp;
  2. int m1,m2;
  3. void gpio_configuration(void)
  4. {
  5.     /* enable the GPIOA clock */
  6.     rcu_periph_clock_enable(RCU_GPIOA);
  7.     /*configure PA6(TIMER2 CH0) as alternate function*/
  8.     gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
  9.     gpio_af_set(GPIOA, GPIO_AF_1, GPIO_PIN_6);        
  10.         gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7);
  11.          gpio_af_set(GPIOA, GPIO_AF_1, GPIO_PIN_7);
  12. //        gpio_mode_set(GPIOB, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
  13. }
  14. /**定时器2  用于捕获编码器输出脉冲数*/
  15. void timer2_configuration(void)
  16. {
  17.         gpio_configuration();
  18.         
  19.     timer_ic_parameter_struct timer_icinitpara;
  20.     timer_parameter_struct timer_initpara;
  21.     /* enable the TIMER clock */
  22.     rcu_periph_clock_enable(RCU_TIMER2);

  23.     /* deinit a TIMER */
  24.     timer_deinit(TIMER2);
  25.     /* initialize TIMER init parameter struct */
  26.     timer_struct_para_init(&timer_initpara);
  27.     /* TIMER2 configuration */
  28.     timer_initpara.prescaler         = 0;
  29.     timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
  30.     timer_initpara.counterdirection  = TIMER_COUNTER_UP;
  31.     timer_initpara.period            = 65535;
  32.     timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
  33.     timer_init(TIMER2, &timer_initpara);

  34.     /* TIMER2 configuration */
  35.     /* initialize TIMER channel input parameter struct */
  36.     timer_channel_input_struct_para_init(&timer_icinitpara);
  37.     /* TIMER2 CH0 PWM input capture configuration */
  38.     timer_icinitpara.icpolarity  = TIMER_IC_POLARITY_RISING;
  39.     timer_icinitpara.icselection = TIMER_IC_SELECTION_DIRECTTI;
  40.     timer_icinitpara.icprescaler = TIMER_IC_PSC_DIV1;
  41.     timer_icinitpara.icfilter    = 0x0;
  42.     timer_input_capture_config(TIMER2, TIMER_CH_0, &timer_icinitpara);
  43.         timer_input_capture_config(TIMER2, TIMER_CH_1, &timer_icinitpara);
  44.                
  45.          timer_quadrature_decoder_mode_config(TIMER2, TIMER_ENCODER_MODE2, TIMER_IC_POLARITY_BOTH_EDGE, TIMER_IC_POLARITY_BOTH_EDGE);

  46.     timer_slave_mode_select(TIMER2, TIMER_ENCODER_MODE2);
  47.     /* auto-reload preload enable */
  48.     timer_auto_reload_shadow_enable(TIMER2);
  49.     /* clear channel 0 interrupt bit */
  50.     timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_UP);
  51. //    /* channel 0 interrupt enable */
  52.     timer_interrupt_enable(TIMER2, TIMER_INT_UP);
  53.     /* TIMER2 counter enable */
  54.     timer_enable(TIMER2);
  55. }
  56. /**定时器0  用于周期获取脉冲数和高频时钟脉冲***/
  57. void timer_configuration(void)
  58. {
  59.     timer_ic_parameter_struct timer_icinitpara;
  60.     timer_parameter_struct timer_initpara;

  61.     /* enable the TIMER clock */
  62.     rcu_periph_clock_enable(RCU_TIMER0);
  63.     /* disable a TIMER */
  64.     timer_deinit(TIMER0);
  65.     /* initialize TIMER init parameter struct */
  66.     timer_struct_para_init(&timer_initpara);
  67.     /* TIMER2 configuration */
  68.     timer_initpara.prescaler         = 7199;
  69.     timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
  70.     timer_initpara.counterdirection  = TIMER_COUNTER_UP;
  71.     timer_initpara.period            = 9;//1S
  72.     timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
  73.         timer_initpara.repetitioncounter = 0;
  74.     timer_init(TIMER0, &timer_initpara);

  75.     /* auto-reload preload enable */
  76.     timer_auto_reload_shadow_enable(TIMER0);
  77.     /* clear channel 0 interrupt bit */
  78.     timer_interrupt_flag_clear(TIMER0,TIMER_INT_FLAG_UP);
  79.     /* channel 0 interrupt enable */
  80.     timer_interrupt_enable(TIMER0,TIMER_INT_FLAG_UP);
  81.     /* TIMER2 counter enable */
  82.     timer_enable(TIMER0);        
  83. }

  84. /****定时器14  用于产生高频时钟脉冲****/
  85. void timer14_configuration(void)
  86. {
  87.     timer_ic_parameter_struct timer_icinitpara;
  88.     timer_parameter_struct timer_initpara;

  89.     /* enable the TIMER clock */
  90.     rcu_periph_clock_enable(RCU_TIMER14);
  91.     /* disable a TIMER */
  92.     timer_deinit(TIMER14);
  93.     /* initialize TIMER init parameter struct */
  94.     timer_struct_para_init(&timer_initpara);
  95.     /* TIMER2 configuration */
  96.     timer_initpara.prescaler         = 1;
  97.     timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
  98.     timer_initpara.counterdirection  = TIMER_COUNTER_UP;
  99.     timer_initpara.period            = 359;//624us采样一次
  100.     timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
  101.         timer_initpara.repetitioncounter = 0;
  102.     timer_init(TIMER14, &timer_initpara);

  103.     /* auto-reload preload enable */
  104.     timer_auto_reload_shadow_enable(TIMER14);
  105.     /* clear channel 0 interrupt bit */
  106.     timer_interrupt_flag_clear(TIMER14,TIMER_INT_FLAG_UP);
  107.     /* channel 0 interrupt enable */
  108.     timer_interrupt_enable(TIMER14,TIMER_INT_FLAG_UP);
  109.     /* TIMER2 counter enable */
  110.     timer_enable(TIMER14);        
  111. }

  112. void TIMER2_IRQHandler(void)
  113. {
  114.     if(SET == timer_interrupt_flag_get(TIMER2, TIMER_INT_FLAG_UP)){
  115.         timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_UP);
  116.     }
  117. }
  118. void TIMER14_IRQHandler(void)
  119. {
  120.         if(SET == timer_interrupt_flag_get(TIMER14, TIMER_INT_FLAG_UP))
  121.         {
  122.                 timer_interrupt_flag_clear(TIMER14, TIMER_FLAG_UP);
  123.                 m2++;
  124.         }
  125. }
  126. int m1_data[point_num],m2_data[point_num];
  127. void TIMER0_BRK_UP_TRG_COM_IRQHandler(void)
  128. {
  129.         if(SET == timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP))
  130.         {
  131.                 timer_interrupt_flag_clear(TIMER0, TIMER_FLAG_UP);
  132.                 m1 = timer_counter_read(TIMER2);
  133.                 if(TIM_NUM<point_num)
  134.                 {
  135.                         m1_data[TIM_NUM] = m1;
  136.                         m2_data[TIM_NUM] = m2;
  137.                         TIM_NUM++;
  138.                                         m1 = 0;
  139.                                 m2 = 0;
  140.                 }else
  141.                 {
  142.                         timer_disable(TIMER0);        
  143.                         timer_disable(TIMER2);        
  144.                         timer_disable(TIMER14);               
  145.                 }

  146.         }
  147. }

TIMER2作为高级定时器,拥有正交译码器的功能,我们将其通道0和通道1接到编码器的A和B相,初始化TIMER2为译码器模式

  1.   timer_quadrature_decoder_mode_config(TIMER2, TIMER_ENCODER_MODE2, TIMER_IC_POLARITY_BOTH_EDGE, TIMER_IC_POLARITY_BOTH_EDGE);

  2.     timer_slave_mode_select(TIMER2, TIMER_ENCODER_MODE2);

主要就是这两个函数,其他的几乎和普通定时器设置差不多。

TIMER14用于产生一个100K的高频时钟脉冲,并使能溢出中断,每产生一个脉冲则将M2的值累加1;

       定时器0  用于周期获取脉冲数和高频时钟脉冲数,并使能溢出中断,中断到达后读取TIMER2的计数值,该值的变化量与编码器输出脉冲相同,计算两次之间的差值即为每次采样脉冲间隔的编码器脉冲数。中断中将M1和M2的值存入数组,在主函数进行速度计算并输出:
  1.     while(1){
  2.         if(TIM_NUM>point_num-2)
  3.         {                        
  4.                 for(uint16_t p=5;p<point_num-2;p++)
  5.                 {
  6.                         m1calc = m1_data[p]-m1_data[p+1];
  7.                         if(m1calc<0)
  8.                         {
  9.                                 m1calc = m1calc+65535;                                                
  10.                         }
  11.                         Speed_num[p] = (float)(60*m1calc*100000)/(m2_data[p]*10000);
  12.                         kalman_height = kalmanFilter(&KFP_height,Speed_num[p]);
  13. //                                                printf("{Speed:%d,%d,%d}\n",(int)(Speed_num[p]+0.5),m1calc,m2_data[p]);
  14.                         printf("{Speed:%d}\n",(int)(kalman_height+0.5));                                       
  15.                 }
  16.                 TIM_NUM = 0;
  17.                 start_flag = 0;
  18.                 timer_enable(TIMER0);        
  19.                 timer_enable(TIMER2);        
  20.                 timer_enable(TIMER14);        
  21.         }
  22.                         
  23.     }

案例中:m1calc为M1,100000为fc,即编码器输出脉冲数。m2_data[p]为M2,即高频脉冲计数。kalmanFilter()函数为卡尔曼滤波器,此处不便公开。


最终效果如下:

在3000RP/M表现基本无差异:


                                                                                      

图八:M/T法卡尔曼滤波速度-3000rp/m

在低速表现优异


图九:M/T法卡尔曼滤波速度-1000rp/m


图十:伺服驱动读取速度波动-100rp/m

十一M/T法卡尔曼滤波速度-100rp/m

高速表现同样没问题


图十二:伺服驱动读取速度波动-5000rp/m

图十三:M/T法卡尔曼滤波速度-5000rp/m



总结

M/T法测速在增量型编码器中还是很常用的测速方式,再结合相应的滤波算法,可以得到非常精准的速度信息,同时MCU的译码器模式可同时获取到点击的旋转方向,可自行探索。上问中的编码器未经校准,精度表现一般,于本次实验结果无关。







本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

打赏榜单

21小跑堂 打赏了 200.00 元 2022-08-22
理由:恭喜通过原创文章审核!请多多加油哦!

LEDyyds 发表于 2022-8-22 10:16 | 显示全部楼层
这个厉害啊,大佬这整体文章排版就很舒适,不过这个串口助手好爽啊。
klbyf 发表于 2022-8-22 18:20 | 显示全部楼层
wuliangu 发表于 2022-8-31 10:25 | 显示全部楼层
学习了, 谢谢分享!
鸡蛋鸭蛋荷包蛋 发表于 2023-3-29 14:42 | 显示全部楼层
震惊了,大佬真的强
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:苏州澜宭自动化科技嵌入式工程师
简介:本人从事磁编码器研发工作,负责开发2500线增量式磁编码器以及17位、23位绝对值式磁编码器,拥有多年嵌入式开发经验,精通STM32、GD32、N32等多种品牌单片机,熟练使用单片机各种外设。

567

主题

4081

帖子

56

粉丝
快速回复 在线客服 返回列表 返回顶部