编码器的使用

2023-11-07

        首先来看一下增量式编码器的输出信号和它的信号倍频技术。增量式编码器输出的脉冲波形信号形式常见的有两种:

  1. 一种是占空比50%的方波,通道A和B相位差为90°;

  2. 另一种则是正弦波这类模拟信号,通道A和B相位差同样为90°。

        对于第1种形式的方波信号,如果把两个通道组合起来看的话,可以发现A和B各自的上升沿和下降沿都能计数,至少在1/2个原始方波周期内就可以计数一次, 最多1/4个原始方波周期。这样计数频率就是原始方波信号的2倍或4倍,换句话说就是,将编码器的分辨率提高了2到4倍,具体如下图所示。

编码器倍频技术

图中的方波信号如果只看其中一个通道的上升沿,那计数频率就等于这个通道信号的频率。如果在通道A的上升沿和下降沿都进行计数,计数频率就是通道A的两倍,即2倍频。 如果同时对两个通道的上升沿和下降沿都计数,那计数频率就变成了原始信号的4倍,即4倍频。

假设有个增量式编码器它的分辨率是600PPR,能分辨的最小角度是0.6°,对它进行4倍频之后就相当于把分辨率提高到了600*4=2400PPR,此时编码器能够分辨的最小角度为0.15°。 编码器倍频技术还可用来扩展一些测速方法的速度适用范围。例如电机测速通常使用M法进行测量(M法在下节介绍),编码器4倍频后可以扩展M法的速度下限。

1. 常用测速方法简介

        上一节提到了增量式编码器倍频技术可以扩展M法的测量范围,那么现在我们就来讲解下这个M法究竟是怎样测速的,以及简单介绍一些常用的编码器测速方法。 对于电机转速的测量,可以把增量式编码器安装到电机上,用控制器对编码器脉冲进行计数,然后通过特定的方法求出电机转速,常用的编码器测速方法一般有三种:M法、T法和M/T法。

  • M法:又叫做频率测量法。这种方法是在一个固定的定时时间内(以秒为单位),统计这段时间的编码器脉冲数,计算速度值。设编码器单圈总脉冲数为C, 时间T0内,统计到的编码器脉冲数为M0,则转速n的计算公式为:

M法测速公式

公式中的编码器单圈总脉冲数C是常数,所以转速n跟M0成正比。这就使得在高速测量时M0变大,可以获得较好的测量精度和平稳性, 但是如果速度很低,低到每个T0内只有少数几个脉冲,此时算出的速度误差就会比较大,并且很不稳定。也有一些方法可以改善M法在低速测量的准确性, 上一节提到的增量式编码器倍频技术就是其中一种,比如原本捕获到的脉冲M0只有4个,经过4倍频后,相同电机状态M0变成了16个, 也就提升了低速下的测量精度。

  • T法:又叫做周期测量法。这种方法是建立一个已知频率的高频脉冲并对其计数,计数时间由捕获到的编码器相邻两个脉冲的间隔时间TE决定, 计数值为M1。设编码器单圈总脉冲数为C,高频脉冲的频率为F0,则转速n的计算公式为:

T法测速公式

公式中的编码器单圈总脉冲数C和高频脉冲频率F0是常数,所以转速n跟M1成反比。从公式可以看出,在电机高转速的时候, 编码器脉冲间隔时间TE很小,使得测量周期内的高频脉冲计数值M1也变得很少,导致测量误差变大,而在低转速时,TE足够大, 测量周期内的M1也足够多,所以T法和M法刚好相反,更适合测量低速。

  • M/T法:这种方法综合了M法和T法各自的优势,既测量编码器脉冲数又测量一定时间内的高频脉冲数。在一个相对固定的时间内,计数编码器脉冲数M0, 并计数一个已知频率为F0的高频脉冲,计数值为M1,计算速度值。设编码器单圈总脉冲数为C,则转速n的计算公式为:

MT法测速公式

由于M/T法公式中的F0和C是常数,所以转速n就只受M0和M1的影响。电机高速时,M0增大,M1减小,相当于M法, 低速时,M1增大,M0减小,相当于T法。

2. STM32的编码器接口简介

        STM32芯片内部有专门用来采集增量式编码器方波信号的接口,这些接口实际上是STM32定时器的其中一种功能。 不过编码器接口功能只有高级定时器TIM1、TIM8和通用定时器TIM2到TIM5才有。编码器接口用到了定时器的输入捕获部分, 功能框图如下图所示。输入捕获功能在《STM32 HAL库开发指南》中已有详细讲解,所以这部分内容在此就不再赘述了。

我们重点关注编码器接口是如何实现信号采集和倍频的。《STM32F4xx参考手册》给出了的编码器信号与计数器方向和计数位置之间的关系,如下表所示。

计数方向与编码器信号的关系

        这个表格将编码器接口所有可能出现的工作情况全都列了出来,包括它是如何实现方向检测和倍频的。虽然信息很全面但是乍看上去却不容易看懂。 首先需要解释一下,表中的TI1和TI2对应编码器的通道A和通道B,而TI1FP1和TI2FP2则对应反相以后的TI1、TI2。STM32的编码器接口在计数的时候, 并不是单纯采集某一通道信号的上升沿或下降沿,而是需要综合另一个通道信号的电平。表中“相反信号的电平”指的就是在计数的时候所参考的另一个通道信号的电平, 这些电平决定了计数器的计数方向。

        为了便于大家理解STM32编码器接口的计数原理,我们将表中的信息提出转换成一系列图像。首先看下图,下图所展示的信息对应表格中“仅在TI1处计数”。 图中包含TI1、TI2两通道的信号,以及计数器的计数方向,其中TI1比TI2 提前 1/4个周期,以TI1的信号边沿作为有效边沿。 当检测到TI1的上升沿时,TI2为低电平,此时计数器向上计数1次,下一时刻检测到TI1的下降沿时,TI2为高电平,此时计数器仍然向上计数一次,以此类推。 这样就能把TI1的上升沿和下降沿都用来计数,即实现了对原始信号的2倍频。

编码器接口2倍频图解——向上计数

接下来看如下图像,图中同样包含TI1、TI2两通道的信号,以及计数器的计数方向,其中TI1比TI2滞后 1/4个周期,以TI1的信号边沿作为有效边沿。 当检测到TI1的上升沿时,TI2为高电平,此时计数器向下计数1次,下一时刻检测到TI1的下降沿时,TI2为低电平,此时计数器仍然向下计数一次,以此类推。 这样同样是把TI1的上升沿和下降沿都用来计数,同样实现了对原始信号的2倍频,只不过变成向下计数了。

编码器接口2倍频图解——向下计数

以上两幅图像都是只以TI1的信号边沿作为有效边沿,并且根据TI2的电平决定各自的计数方向,然后判断计数方向就能得到编码器的旋转方向,向上计数正向,向下计数反向。 “仅在TI2处计数”也是同样的原理,在这里就不重复讲了。

        最后如下图所示,下图所展示的信息对应表格中“在TI1和TI2处均计数”。这种采样方式可以把两个通道的上升沿和下降沿都用来计数,计数方向也是两个通道同时参考, 相当于原来仅在一个通道处计数的2倍,所以这种就能实现对原始信号的4倍频。

编码器接口4倍频图解

3. 编码器接口初始化结构体详解

HAL库函数对定时器外设建立了多个初始化结构体,其中编码器接口用到的有时基初始化结构体 TIM_Base_InitTypeDef ,和编码器初始化配置结构体 TIM_Encoder_InitTypeDef 。初始化结构体成员用于设置定时器工作环境参数,并由定时器相应初始化配置函数调用, 最终这些参数将会写入到定时器相应的寄存器中。

3.1. TIM_Base_InitTypeDef

时基结构体 TIM_Base_InitTypeDef 用于定时器基础参数设置,与 HAL_TIM_Base_Init 函数配合使用完成配置。 这个结构体在《STM32 HAL库开发指南》的定时器章节有详细的讲解,这里我们只简单的提一下。

定时器基本初始化结构体

1
2
3
4
5
6
7
8
9
typedef struct
{
 uint32_t Prescaler;            //预分频器
 uint32_t CounterMode;          //计数模式
 uint32_t Period;               //定时器周期
 uint32_t ClockDivision;        //时钟分频
 uint32_t RepetitionCounter;    //重复计算器
 uint32_t AutoReloadPreload;    //自动重载值
}TIM_Base_InitTypeDef;
  1. Prescaler:定时器预分频器设置;

  2. CounterMode:定时器计数方式;

  3. Period:定时器周期;

  4. ClockDivision:时钟分频;

  5. RepetitionCounter:重复计数器;

  6. AutoReloadPreload:自动重载预装载值。

3.2. TIM_Encoder_InitTypeDef

              编码器初始化配置结构体 TIM_Encoder_InitTypeDef 用于定时器的编码器接口模式,与 HAL_TIM_Encoder_Init 函数配合使用完成初始化配置操作。高级定时器TIM1和TIM8以及通用定时器TIM2到TIM5都带有编码器接口,使用时都必须单独设置。

编码器接口初始化结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct
{
  uint32_t EncoderMode;    //编码器模式
  uint32_t IC1Polarity;    //输入信号极性
  uint32_t IC1Selection;   //输入通道
  uint32_t IC1Prescaler;   //输入捕获预分频器
  uint32_t IC1Filter;      //输入捕获滤波器
  uint32_t IC2Polarity;    //输入信号极性
  uint32_t IC2Selection;   //输入通道
  uint32_t IC2Prescaler;   //输入捕获预分频器
  uint32_t IC2Filter;      //输入捕获滤波器
 }TIM_Encoder_InitTypeDef;
  1. EncoderMode:编码器模式选择,用来设置计数器采集编码器信号的方式,可选通道A计数、通道B计数和双通道计数。 它设定TIMx_SMCR寄存器的SMS[2:0]位。这个成员实际是用来设置编码器接口的倍频数的,当选择通道A或B计数时为2倍频,双通道计数时为4倍频。

  2. ICxPolarity:输入捕获信号极性选择,用于设置定时器通道在编码器模式下的输入信号是否反相。 它设定TIMx_CCER寄存器的CCxNP位和CCxP位。

  3. ICxSelection:输入通道选择,ICx的信号可来自三个输入通道,分别为 TIM_ICSELECTION_DIRECTTI、 TIM_ICSELECTION_INDIRECTTI 或 IM_ICSELECTION_TRC。它设定TIMx_CCMRx寄存器的CCxS[1:0]位的值。 定时器在编码器接口模式下,此成员只能设置为TIM_ICSELECTION_DIRECTTI。

  4. ICxPrescaler:输入捕获通道预分频器,可设置1、2、4、8分频。它设定TIMx_CCMRx寄存器的ICxPSC[1:0]位的值。

  5. ICxFilter:输入捕获滤波器设置,可选设置0x0至0x0F。它设定TIMx_CCMRx寄存器ICxF[3:0]位的值。

4 减速电机编码器测速实验

       本实验讲解如何使用STM32的编码器接口,并利用编码器接口对减速电机进行测速。

4.1. 硬件设计

本实验用到的减速电机与减速电机按键控制例程的相同,所以电机、开发板和驱动板的硬件连接也完全相同,只加上了编码器的连线。

编码器接口部分原理图

上图是我们电机开发板使用的编码器接口原理图,通过连接器与STM32的GPIO相连,一共4个通道,可以同时接入两个编码器。本实验使用PC6和PC7两个引脚,对应TIM3的CH1和CH2。

4.2. 软件设计

       本 编码器测速例程是在减速电机按键控制例程的基础上编写的,这里只讲解跟编码器有关的部分核心代码,有些变量的设置,头文件的包含以及如何驱动电机等并没有涉及到。

4.2.1. 编程要点

  1. 定时器 IO 配置

  2. 定时器时基结构体TIM_HandleTypeDef配置

  3. 编码器接口结构体TIM_Encoder_InitTypeDef配置

  4. 通过编码器接口测量到的数值计算减速电机转速

42.2. 软件分析

  1. 宏定义

bsp_encoder.h-宏定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 /* 定时器选择 */
 #define ENCODER_TIM                         TIM3
 #define ENCODER_TIM_CLK_ENABLE()            __HAL_RCC_TIM3_CLK_ENABLE()
 /* 定时器溢出值 */
 #define ENCODER_TIM_PERIOD                  65535
 /* 定时器预分频值 */
 #define ENCODER_TIM_PRESCALER               0
 /* 定时器中断 */
 #define ENCODER_TIM_IRQn                    TIM3_IRQn
 #define ENCODER_TIM_IRQHandler              TIM3_IRQHandler
 /* 编码器接口引脚 */
 #define ENCODER_TIM_CH1_GPIO_CLK_ENABLE()   __HAL_RCC_GPIOC_CLK_ENABLE()
 #define ENCODER_TIM_CH1_GPIO_PORT           GPIOC
 #define ENCODER_TIM_CH1_PIN                 GPIO_PIN_6
 #define ENCODER_TIM_CH1_GPIO_AF             GPIO_AF2_TIM3
 #define ENCODER_TIM_CH2_GPIO_CLK_ENABLE()   __HAL_RCC_GPIOC_CLK_ENABLE()
 #define ENCODER_TIM_CH2_GPIO_PORT           GPIOC
 #define ENCODER_TIM_CH2_PIN                 GPIO_PIN_7
 #define ENCODER_TIM_CH2_GPIO_AF             GPIO_AF2_TIM3
 /* 编码器接口倍频数 */
 #define ENCODER_MODE                        TIM_ENCODERMODE_TI12
 /* 编码器接口输入捕获通道相位设置 */
 #define ENCODER_IC1_POLARITY                TIM_ICPOLARITY_RISING
 #define ENCODER_IC2_POLARITY                TIM_ICPOLARITY_RISING
 /* 编码器物理分辨率 */
 #define ENCODER_RESOLUTION                  15
 /* 经过倍频之后的总分辨率 */
 #if ((ENCODER_MODE == TIM_ENCODERMODE_TI1) || (ENCODER_MODE == TIM_ENCODERMODE_TI2))
   #define ENCODER_TOTAL_RESOLUTION             (ENCODER_RESOLUTION * 2)  /* 2倍频后的总分辨率 */
 #else
   #define ENCODER_TOTAL_RESOLUTION             (ENCODER_RESOLUTION * 4)  /* 4倍频后的总分辨率 */
 #endif
 /* 减速电机减速比 */
 #define REDUCTION_RATIO                     34

使用宏定义非常方便程序升级、移植。如果使用不同的定时器、编码器倍频数、编码器分辨率等,修改这些宏即可。 开发板使用的是TIM3的CH1和CH2,分别连接到编码器的通道A和通道B,对应的引脚为PC6、PC7。

  1. 定时器复用功能引脚初始化

bsp_encoder.c-定时器复用功能引脚初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 /**
   * @brief  编码器接口引脚初始化
   * @param  无
   * @retval 无
   */
 static void Encoder_GPIO_Init(void)
 {
   GPIO_InitTypeDef GPIO_InitStruct = {0};

   /* 定时器通道引脚端口时钟使能 */
   ENCODER_TIM_CH1_GPIO_CLK_ENABLE();
   ENCODER_TIM_CH2_GPIO_CLK_ENABLE();

   /**TIM3 GPIO Configuration
   PC6     ------> TIM3_CH1
   PC7     ------> TIM3_CH2
   */
   /* 设置输入类型 */
   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
   /* 设置上拉 */
   GPIO_InitStruct.Pull = GPIO_PULLUP;
   /* 设置引脚速率 */
   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

   /* 选择要控制的GPIO引脚 */
   GPIO_InitStruct.Pin = ENCODER_TIM_CH1_PIN;
   /* 设置复用 */
   GPIO_InitStruct.Alternate = ENCODER_TIM_CH1_GPIO_AF;
   /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
   HAL_GPIO_Init(ENCODER_TIM_CH1_GPIO_PORT, &GPIO_InitStruct);

   /* 选择要控制的GPIO引脚 */
   GPIO_InitStruct.Pin = ENCODER_TIM_CH2_PIN;
   /* 设置复用 */
   GPIO_InitStruct.Alternate = ENCODER_TIM_CH2_GPIO_AF;
   /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
   HAL_GPIO_Init(ENCODER_TIM_CH2_GPIO_PORT, &GPIO_InitStruct);
 }

定时器通道引脚使用之前必须设定相关参数,这里选择复用功能,并指定到对应的定时器。使用GPIO之前都必须开启相应端口时钟,这个没什么好说的。 唯一要注意的一点,有些编码器的输出电路是不带上拉电阻的,需要在板子上或者芯片GPIO设置中加上上拉电阻。

  1. 编码器接口配置

bsp_encoder.c-编码器接口配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 /**
   * @brief  配置TIMx编码器模式
   * @param  无
   * @retval 无
   */
 static void TIM_Encoder_Init(void)
 {
   TIM_Encoder_InitTypeDef Encoder_ConfigStructure;

   /* 使能编码器接口时钟 */
   ENCODER_TIM_CLK_ENABLE();

   /* 定时器初始化设置 */
   TIM_EncoderHandle.Instance = ENCODER_TIM;
   TIM_EncoderHandle.Init.Prescaler = ENCODER_TIM_PRESCALER;
   TIM_EncoderHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
   TIM_EncoderHandle.Init.Period = ENCODER_TIM_PERIOD;
   TIM_EncoderHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
   TIM_EncoderHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

   /* 设置编码器倍频数 */
   Encoder_ConfigStructure.EncoderMode = ENCODER_MODE;
   /* 编码器接口通道1设置 */
   Encoder_ConfigStructure.IC1Polarity = ENCODER_IC1_POLARITY;
   Encoder_ConfigStructure.IC1Selection = TIM_ICSELECTION_DIRECTTI;
   Encoder_ConfigStructure.IC1Prescaler = TIM_ICPSC_DIV1;
   Encoder_ConfigStructure.IC1Filter = 0;
   /* 编码器接口通道2设置 */
   Encoder_ConfigStructure.IC2Polarity = ENCODER_IC2_POLARITY;
   Encoder_ConfigStructure.IC2Selection = TIM_ICSELECTION_DIRECTTI;
   Encoder_ConfigStructure.IC2Prescaler = TIM_ICPSC_DIV1;
   Encoder_ConfigStructure.IC2Filter = 0;
   /* 初始化编码器接口 */
   HAL_TIM_Encoder_Init(&TIM_EncoderHandle, &Encoder_ConfigStructure);

   /* 清零计数器 */
   __HAL_TIM_SET_COUNTER(&TIM_EncoderHandle, 0);

   /* 清零中断标志位 */
   __HAL_TIM_CLEAR_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
   /* 使能定时器的更新事件中断 */
   __HAL_TIM_ENABLE_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
   /* 设置更新事件请求源为:定时器溢出 */
   __HAL_TIM_URS_ENABLE(&TIM_EncoderHandle);

   /* 设置中断优先级 */
   HAL_NVIC_SetPriority(ENCODER_TIM_IRQn, 5, 1);
   /* 使能定时器中断 */
   HAL_NVIC_EnableIRQ(ENCODER_TIM_IRQn);

   /* 使能编码器接口 */
   HAL_TIM_Encoder_Start(&TIM_EncoderHandle, TIM_CHANNEL_ALL);
 }

编码器接口配置中,主要初始化两个结构体,其中时基初始化结构体TIM_HandleTypeDef很简单,而且在其他应用中都用涉及到,直接看注释理解即可。

重点是编码器接口结构体TIM_Encoder_InitTypeDef的初始化。对于STM32定时器的编码器接口,我们首先需要设置编码器的倍频数,即成员EncoderMode, 它可把编码器接口设置为2倍频或4倍频,根据bsp_encoder.h的宏定义我们将其设置为4倍频,倍频原理在上面已有讲解这里不再赘述。

对于编码器接口输入通道的配置,我们只讲解通道1的配置情况,通道2是一样的。首先是输入信号极性,成员IC1Polarity在输入捕获模式中是用来设置触发边沿的, 但在编码器模式中是用来设置输入信号是否反相的。设置为RISING表示不反相,FALLING表示反相。此成员与编码器的计数触发边沿无关, 只用来匹配编码器和电机的方向,当设定的电机正方向与编码器正方向不一致时不必更改硬件连接,直接在程序中修改IC1Polarity即可。

接下来是成员IC1Selection,这个成员用于选择输入通道,IC1可以是TI1输入的TI1FP1,也可以是从TI2输入的TI2FP1,我们这里选择直连(DIRECTTI),即TI1FP1映射到IC1, 在编码器模式下这个成员只能设置为DIRECTTI,其他可选值都是不起作用的。

最后是成员IC1Prescaler和成员IC1Filter,我们需要对编码器的每个脉冲信号都进行捕获,所以设置成不分频。根据STM32编码器接口2倍频或4倍频的原理, 接口在倍频采样的过程中也会对信号抖动进行补偿,所以输入滤波器也很少会用到。

配置完编码器接口结构体后清零计数器,然后开启定时器的更新事件中断,并把更新事件中断源配置为定时器溢出,也就是仅当定时器溢出时才触发更新事件中断。 然后配置定时器的中断优先级并开启中断,最后启动编码器接口。

  1. 定时器溢出次数记录

bsp_encoder.c-定时器溢出次数记录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 /**
   * @brief  定时器更新事件回调函数
   * @param  无
   * @retval 无
   */
 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
 {
   /* 判断当前计数器计数方向 */
   if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle))
     /* 下溢 */
     Encoder_Overflow_Count--;
   else
     /* 上溢 */
     Encoder_Overflow_Count++;
 }

在TIM_Encoder_Init函数中我们配置了仅当定时器计数溢出时才触发更新事件中断,然后在中断回调函数中记录定时器溢出了多少次。首先定义一个全局变量Encoder_Overflow_Count, 用来记录计数器的溢出次数。在定时器更新事件中断回调函数中,使用__HAL_TIM_IS_TIM_COUNTING_DOWN函数判断当前的计数方向,是向上计数还是向下计数, 如果向下计数,Encoder_Overflow_Count减1,反之则加1。这样在计算电机转速和位置的时候就可以把溢出次数也参与在内。

  1. 主函数

main.c-主函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
 /**
   * @brief  主函数
   * @param  无
   * @retval 无
   */
 int main(void)
 {
   __IO uint16_t ChannelPulse = 0;
   uint8_t i = 0;

   /* HAL库初始化*/
   HAL_Init();
   /* 初始化系统时钟为168MHz */
   SystemClock_Config();
   /* 配置1ms时基为SysTick */
   HAL_InitTick(5);
   /* 初始化按键GPIO */
   Key_GPIO_Config();
   /* 初始化USART */
   DEBUG_USART_Config();

   printf("\r\n——————————野火减速电机编码器测速演示程序——————————\r\n");

   /* 通用定时器初始化并配置PWM输出功能 */
   TIMx_Configuration();

   TIM1_SetPWM_pulse(PWM_CHANNEL_1,0);
   TIM1_SetPWM_pulse(PWM_CHANNEL_2,0);

   /* 编码器接口初始化 */
   Encoder_Init();

   while(1)
   {
     /* 扫描KEY1 */
     if( Key_Scan(KEY1_GPIO_PORT, KEY1_PIN) == KEY_ON)
     {
       /* 增大占空比 */
       ChannelPulse += 50;

       if(ChannelPulse > PWM_PERIOD_COUNT)
         ChannelPulse = PWM_PERIOD_COUNT;

       set_motor_speed(ChannelPulse);
     }

     /* 扫描KEY2 */
     if( Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)
     {
       if(ChannelPulse < 50)
         ChannelPulse = 0;
       else
         ChannelPulse -= 50;

       set_motor_speed(ChannelPulse);
     }

     /* 扫描KEY3 */
     if( Key_Scan(KEY3_GPIO_PORT, KEY3_PIN) == KEY_ON)
     {
       /* 转换方向 */
       set_motor_direction( (++i % 2) ? MOTOR_FWD : MOTOR_REV);
     }
   }
 }

本实验的主函数与减速电机按键调速基本相同,只是在一开始初始化了HAL库和配置了SysTick嘀嗒定时器为1ms中断一次, 当然最重要的还是调用Encoder_Init函数,初始化和配置STM32的编码器接口。while循环内容相同,为了不影响到在while循环中调整电机速度, 我们将使用中断进行编码器数据采集和计算。

  1. 数据计算

main.c-数据计算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 /* 电机旋转方向 */
 __IO int8_t Motor_Direction = 0;
 /* 当前时刻总计数值 */
 __IO int32_t Capture_Count = 0;
 /* 上一时刻总计数值 */
 __IO int32_t Last_Count = 0;
 /* 电机转轴转速 */
 __IO float Shaft_Speed = 0.0f;

 /**
   * @brief  SysTick中断回调函数
   * @param  无
   * @retval 无
   */
 void HAL_SYSTICK_Callback(void)
 {
   static uint16_t i = 0;

   i++;
   if(i == 100)/* 100ms计算一次 */
   {
     /* 电机旋转方向 = 计数器计数方向 */
     Motor_Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle);

     /* 当前时刻总计数值 = 计数器值 + 计数溢出次数 * 计数器溢出值  */
     Capture_Count =__HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (Encoder_Overflow_Count * ENCODER_TIM_PERIOD);

     /* 转轴转速 = 单位时间内的计数值 / 编码器总分辨率 * 时间系数  */
     Shaft_Speed = (float)(Capture_Count - Last_Count) / ENCODER_TOTAL_RESOLUTION * 10 ;

     printf("电机方向:%d\r\n", Motor_Direction);
     printf("单位时间内有效计数值:%d\r\n", Capture_Count - Last_Count);/* 单位时间计数值 = 当前时刻总计数值 - 上一时刻总计数值 */
     printf("电机转轴处转速:%.2f 转/秒 \r\n", Shaft_Speed);
     printf("电机输出轴转速:%.2f 转/秒 \r\n", Shaft_Speed/REDUCTION_RATIO);/* 输出轴转速 = 转轴转速 / 减速比 */

     /* 记录当前总计数值,供下一时刻计算使用 */
     Last_Count = Capture_Count;
     i = 0;
   }
 }

如上代码所示,首先定义了一些全局变量,用来保存计算数据和供其他函数使用。在SysTick中断回调函数中每100ms执行一次采集和计算, 先检测电机旋转方向,直接读取当前时刻的计数器计数方向就可获得方向,向上计数为正向,向下计数为反向。

接着是测量当前时刻的总计数值,根据总计数值计算电机转速,在本例程中我们使用M法进行测速,单位时间内的计数值除以编码器总分辨率即可得到单位时间内的电机转速, 代码中单位时间为100ms,单位时间内的计数值由当前时刻总计数值Capture_Count减上一时刻总计数值Last_Count得到,编码器总分辨率由编码器物理分辨率乘倍频数得到, 这里算出来的电机转速单位是转/百毫秒,转到常用的单位还需要乘上一个时间系数,比如转/秒就乘10。不过此时得到的是电机转轴处的转速,并不是减速电机输出轴的转速, 把转轴转速除以减速比即可得到输出轴的转速。

所有数据全部采集和计算完毕后,将电机方向、单位时间内的计数值、电机转轴转速和电机输出轴转速等数据全部通过串口打印到窗口调试助手上, 并将当前的总计数值记录下来方便下次计算使用。

4.3. 下载验证

保证开发板相关硬件连接正确,用USB线连接开发板“USB转串口”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板,串口调试助手会显示程序输出的信息。 我们通过开发板上的三个按键控制电机加减速和方向,在串口调试助手的接收区即可看到电机转速等信息。

减速电机测速实验现象

4.4. 步进电机编码器测速实验

本实验讲解如何使用STM32的编码器接口,并利用编码器接口对步进电机进行测速。学习本小节内容时,请打开配套的“步进电机编码器测速”工程配合阅读。

4.4.1. 硬件设计

本实验用到的步进电机与步进电机按键控制例程的相同,所以电机、开发板和驱动板的硬件连接也完全相同,只加上了编码器的连线。 关于编码器接口部分原理图及其说明与减速电机编码器相同,可以查看 减速电机编码器测速实验 章节相关内容。

4.4.2. 软件设计

本编码器测速例程是在步进电机按键控制例程的基础上编写的,这里只讲解跟编码器有关的部分核心代码,有些变量的设置,头文件的包含以及如何驱动步进电机等并没有涉及到, 完整的代码请参考本章配套的工程。我们创建了两个文件:bsp_encoder.c 和 bsp_encoder.h 文件用来存放编码器接口驱动程序及相关宏定义。

4.4.2.1. 编程要点

  1. 定时器 IO 配置

  2. 定时器时基结构体TIM_HandleTypeDef配置

  3. 编码器接口结构体TIM_Encoder_InitTypeDef配置

  4. 通过编码器接口测量到的数值计算步进电机转速

  5. M法: n(转速 r/s)= M0(编码器单位时间内的脉冲数) / (T0(单位时间)* C(单圈的脉冲数))

  6. 即 单位时间内的脉冲数 / 单圈的脉冲数
    推导方法:
    M0 / T0 = 1秒的总脉冲数 M1
    M1 / C = 1秒转过多少圈(转速)

4.6.2.2. 软件分析

  1. 宏定义

bsp_encoder.h-宏定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 /* 定时器选择 */
 #define ENCODER_TIM                            TIM3
 #define ENCODER_TIM_CLK_ENABLE()                                 __HAL_RCC_TIM3_CLK_ENABLE()

 /* 定时器溢出值 */
 #define ENCODER_TIM_PERIOD                     65535
 /* 定时器预分频值 */
 #define ENCODER_TIM_PRESCALER                  0

 /* 定时器中断 */
 #define ENCODER_TIM_IRQn                       TIM3_IRQn
 #define ENCODER_TIM_IRQHandler                 TIM3_IRQHandler

 /* 编码器接口引脚 */
 #define ENCODER_TIM_CH1_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOC_CLK_ENABLE()
 #define ENCODER_TIM_CH1_GPIO_PORT              GPIOC
 #define ENCODER_TIM_CH1_PIN                    GPIO_PIN_6
 #define ENCODER_TIM_CH1_GPIO_AF                GPIO_AF2_TIM3

 #define ENCODER_TIM_CH2_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOC_CLK_ENABLE()
 #define ENCODER_TIM_CH2_GPIO_PORT              GPIOC
 #define ENCODER_TIM_CH2_PIN                    GPIO_PIN_7
 #define ENCODER_TIM_CH2_GPIO_AF                GPIO_AF2_TIM3

 /* 编码器接口倍频数 */
 #define ENCODER_MODE                           TIM_ENCODERMODE_TI12

 /* 编码器接口输入捕获通道相位设置 */
 #define ENCODER_IC1_POLARITY                   TIM_ICPOLARITY_RISING
 #define ENCODER_IC2_POLARITY                   TIM_ICPOLARITY_RISING

 /* 编码器物理分辨率 */
 #define ENCODER_RESOLUTION                     600

 /* 经过倍频之后的总分辨率 */
 #if ((ENCODER_MODE == TIM_ENCODERMODE_TI1) || (ENCODER_MODE == TIM_ENCODERMODE_TI2))
   #define ENCODER_TOTAL_RESOLUTION             (ENCODER_RESOLUTION * 2)  /* 2倍频后的总分辨率 */
 #else
   #define ENCODER_TOTAL_RESOLUTION             (ENCODER_RESOLUTION * 4)  /* 4倍频后的总分辨率 */
 #endif

宏定义的说明与减速电机章节相同,此处不再赘述。

  1. 定时器复用功能引脚初始化

bsp_encoder.c-定时器复用功能引脚初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 /**
   * @brief  编码器接口引脚初始化
   * @param  无
   * @retval 无
   */
 static void Encoder_GPIO_Init(void)
 {
   GPIO_InitTypeDef GPIO_InitStruct = {0};

   /* 定时器通道引脚端口时钟使能 */
   ENCODER_TIM_CH1_GPIO_CLK_ENABLE();
   ENCODER_TIM_CH2_GPIO_CLK_ENABLE();

   /**TIM3 GPIO Configuration
   PC6     ------> TIM3_CH1
   PC7     ------> TIM3_CH2
   */
   /* 设置输入类型 */
   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
   /* 设置上拉 */
   GPIO_InitStruct.Pull = GPIO_PULLUP;
   /* 设置引脚速率 */
   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

   /* 选择要控制的GPIO引脚 */
   GPIO_InitStruct.Pin = ENCODER_TIM_CH1_PIN;
   /* 设置复用 */
   GPIO_InitStruct.Alternate = ENCODER_TIM_CH1_GPIO_AF;
   /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
   HAL_GPIO_Init(ENCODER_TIM_CH1_GPIO_PORT, &GPIO_InitStruct);

   /* 选择要控制的GPIO引脚 */
   GPIO_InitStruct.Pin = ENCODER_TIM_CH2_PIN;
   /* 设置复用 */
   GPIO_InitStruct.Alternate = ENCODER_TIM_CH2_GPIO_AF;
   /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
   HAL_GPIO_Init(ENCODER_TIM_CH2_GPIO_PORT, &GPIO_InitStruct);
 }

定时器复用功能引脚初始化的说明与减速电机章节相同,此处不再赘述。

  1. 编码器接口配置

bsp_encoder.c-编码器接口配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 /**
   * @brief  配置TIMx编码器模式
   * @param  无
   * @retval 无
   */
 static void TIM_Encoder_Init(void)
 {
   TIM_Encoder_InitTypeDef Encoder_ConfigStructure;

   /* 使能编码器接口时钟 */
   ENCODER_TIM_CLK_ENABLE();

   /* 定时器初始化设置 */
   TIM_EncoderHandle.Instance = ENCODER_TIM;
   TIM_EncoderHandle.Init.Prescaler = ENCODER_TIM_PRESCALER;
   TIM_EncoderHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
   TIM_EncoderHandle.Init.Period = ENCODER_TIM_PERIOD;
   TIM_EncoderHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
   TIM_EncoderHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

   /* 设置编码器倍频数 */
   Encoder_ConfigStructure.EncoderMode = ENCODER_MODE;
   /* 编码器接口通道1设置 */
   Encoder_ConfigStructure.IC1Polarity = ENCODER_IC1_POLARITY;
   Encoder_ConfigStructure.IC1Selection = TIM_ICSELECTION_DIRECTTI;
   Encoder_ConfigStructure.IC1Prescaler = TIM_ICPSC_DIV1;
   Encoder_ConfigStructure.IC1Filter = 0;
   /* 编码器接口通道2设置 */
   Encoder_ConfigStructure.IC2Polarity = ENCODER_IC2_POLARITY;
   Encoder_ConfigStructure.IC2Selection = TIM_ICSELECTION_DIRECTTI;
   Encoder_ConfigStructure.IC2Prescaler = TIM_ICPSC_DIV1;
   Encoder_ConfigStructure.IC2Filter = 0;
   /* 初始化编码器接口 */
   HAL_TIM_Encoder_Init(&TIM_EncoderHandle, &Encoder_ConfigStructure);

   /* 清零计数器 */
   __HAL_TIM_SET_COUNTER(&TIM_EncoderHandle, 0);

   /* 清零中断标志位 */
   __HAL_TIM_CLEAR_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
   /* 使能定时器的更新事件中断 */
   __HAL_TIM_ENABLE_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
   /* 设置更新事件请求源为:定时器溢出 */
   __HAL_TIM_URS_ENABLE(&TIM_EncoderHandle);

   /* 设置中断优先级 */
   HAL_NVIC_SetPriority(ENCODER_TIM_IRQn, 5, 1);
   /* 使能定时器中断 */
   HAL_NVIC_EnableIRQ(ENCODER_TIM_IRQn);

   /* 使能编码器接口 */
   HAL_TIM_Encoder_Start(&TIM_EncoderHandle, TIM_CHANNEL_ALL);
 }

有关编码器接口配置的内容在减速电机章节已经说明,这里再重复说明一下编码器接口结构体TIM_Encoder_InitTypeDef的初始化。

首先需要设置编码器的倍频数,即成员EncoderMode,它可把编码器接口设置为2倍频或4倍频,我们将其设置为4倍频。

接下来对编码器接口输入通道进行配置,通道1的配置和通道2是一样的。

成员IC1Polarity在编码器模式中是用来设置输入信号是否反相的。设置为RISING表示不反相,FALLING表示反相。此成员与编码器的计数触发边沿无关, 只用来匹配编码器和电机的方向,当设定的电机正方向与编码器正方向不一致时不必更改硬件连接,直接在程序中修改IC1Polarity即可。

成员IC1Selection,用于选择输入通道,IC1可以是TI1输入的TI1FP1,也可以是从TI2输入的TI2FP1,我们这里选择直连(DIRECTTI),即TI1FP1映射到IC1, 在编码器模式下这个成员只能设置为DIRECTTI,其他可选值都是不起作用的。

成员IC1Prescaler和成员IC1Filter,我们需要对编码器的每个脉冲信号都进行捕获,所以设置成不分频。根据STM32编码器接口2倍频或4倍频的原理, 接口在倍频采样的过程中也会对信号抖动进行补偿,所以输入滤波器也很少会用到。

配置完编码器接口结构体后清零计数器,然后开启定时器的更新事件中断,并把更新事件中断源配置为定时器溢出,也就是仅当定时器溢出时才触发更新事件中断。 然后配置定时器的中断优先级并开启中断,最后启动编码器接口。

  1. 定时器溢出次数记录

bsp_encoder.c-定时器溢出次数记录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 /**
   * @brief  定时器更新事件回调函数
   * @param  无
   * @retval 无
   */
 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
 {
   /* 判断当前计数器计数方向 */
   if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle))
     /* 下溢 */
     Encoder_Overflow_Count--;
   else
     /* 上溢 */
     Encoder_Overflow_Count++;
 }

定时器溢出次数记录的说明与减速电机章节相同,此处不再赘述。

  1. 主函数

main.c-主函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
 /* 电机旋转方向 */
 __IO int8_t motor_direction = 0;
 /* 当前时刻总计数值 */
 __IO int32_t capture_count = 0;
 /* 上一时刻总计数值 */
 __IO int32_t last_count = 0;
 /* 单位时间内总计数值 */
 __IO int32_t count_per_unit = 0;
 /* 电机转轴转速 */
 __IO float shaft_speed = 0.0f;
 /* 累积圈数 */
 __IO float number_of_rotations = 0.0f;

 /**
   * @brief  主函数
   * @param  无
   * @retval 无
   */
 int main(void)
 {
   int i = 0;

   /* 初始化系统时钟为168MHz */
   SystemClock_Config();
   /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
   DEBUG_USART_Config();
   printf("欢迎使用野火 电机开发板 步进电机 编码器测速 例程\r\n");
   printf("按下按键1启动电机、按键2停止、按键3改变方向\r\n");
   /* 初始化时间戳 */
   HAL_InitTick(5);
   /*按键初始化*/
   Key_GPIO_Config();
   /*led初始化*/
   LED_GPIO_Config();
   /*步进电机初始化*/
   stepper_Init();
   /* 上电默认停止电机,按键1启动 */
   MOTOR_EN(OFF);
   /* 编码器接口初始化 */
   Encoder_Init();

   while(1)
   {
     /* 扫描KEY1,启动电机 */
     if(Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON)
     {
       MOTOR_EN(ON);
     }
     /* 扫描KEY2,停止电机 */
     if(Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON)
     {
       MOTOR_EN(OFF);
     }
     /* 扫描KEY3,改变方向 */
     if(Key_Scan(KEY3_GPIO_PORT,KEY3_PIN) == KEY_ON)
     {
       static int j = 0;
       j > 0 ? MOTOR_DIR(CCW) : MOTOR_DIR(CW);
       j=!j;
     }

     /* 20ms计算一次 */
     /* 电机旋转方向 = 计数器计数方向 */
     motor_direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle);

     /* 当前时刻总计数值 = 计数器值 + 计数溢出次数 * ENCODER_TIM_PERIOD  */
     capture_count =__HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (Encoder_Overflow_Count * ENCODER_TIM_PERIOD);

     /* 单位时间内总计数值 = 当前时刻总计数值 - 上一时刻总计数值 */
     count_per_unit = capture_count - last_count;

     /* 转轴转速 = 单位时间内的计数值 / 编码器总分辨率 * 时间系数  */
     shaft_speed = (float)count_per_unit / ENCODER_TOTAL_RESOLUTION * 50 ;

     /* 累积圈数 = 当前时刻总计数值 / 编码器总分辨率  */
     number_of_rotations = (float)capture_count / ENCODER_TOTAL_RESOLUTION;

     /* 记录当前总计数值,供下一时刻计算使用 */
     last_count = capture_count;

     if(i == 50)/* 1s报告一次 */
     {
       printf("\r\n电机方向:%d\r\n", motor_direction);
       printf("单位时间内有效计数值:%d\r\n", (count_per_unit<0 ? abs(count_per_unit) : count_per_unit));
       printf("步进电机转速:%.2f 转/秒\r\n", shaft_speed);
       printf("累计圈数:%.2f 圈\r\n", number_of_rotations);
       i = 0;
     }
     delay_ms(20);
     i++;
   }
 }

本实验的主函数与步进电机按键调速基本相同,在初始化的时候调用Encoder_Init函数,初始化和配置STM32的编码器接口。

最大的差别就是在while循环中加入了数据计算的部分。如上代码所示,首先定义了一些全局变量,用来保存计算数据和供其他函数使用。 在while循环中加入了一个20ms的延时,每执行一次while循环就采集数据和计算, 由于使用了在while循环中延时的方法,单位时间会有波动,不过波动很小,不影响本实验的结果。 如果很在意数据稳定性,可以使用减速电机 减速电机编码器测速实验数据计算 章节在SysTick中断回调函数中采集和计算数据的方式。

在采集数据和计算时,先检测电机旋转方向,直接读取当前时刻的计数器计数方向就可获得方向,向上计数为正向,向下计数为反向。

接着是测量当前时刻的总计数值,根据总计数值计算电机转速,在本例程中我们使用M法进行测速,单位时间内的计数值除以编码器总分辨率即可得到单位时间内的电机转速, 代码中单位时间为20ms,单位时间内的计数值由当前时刻总计数值count_per_unit减上一时刻总计数值last_Count得到,编码器总分辨率由编码器物理分辨率乘倍频数得到, 这里算出来的电机转速单位是转/百毫秒,转到常用的单位还需要乘上一个时间系数,比如转/秒就乘50,此时计算出的速度就是步进电机的速度。

所有数据全部采集和计算完毕后,将电机方向、单位时间内的计数值、电机转轴转速和电机输出轴转速等数据全部通过串口打印到窗口调试助手上, 并将当前的总计数值记录下来方便下次计算使用。

4.6.3. 下载验证

保证开发板相关硬件连接正确,用USB线连接开发板“USB转串口”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板,串口调试助手会显示程序输出的信息。 我们通过开发板上的三个按键控制电机加减速和方向,在串口调试助手的接收区即可看到电机转速等信息。

步进电机测速实验现象

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

编码器的使用 的相关文章

  • 队列的概念及结构(内有成型代码可供CV工程师参考)

    目录 前言以及队列全部代码 CV工程师点这里 一 队列的概念 二 队列的实现 三 代码实现以及详细解释 1 初步介绍 2 定义结构体 以及栈内数据类型 3 初始化队列 4 队列的销毁 5 队列插入元素 尾插 6 删除队头元素 7 返回队头元
  • 51单片机:定时器/计数器TMOD设定

    51单片机 定时器 计数器TMOD设定 TMOD格式 上图为TMOD格式 8位分为两组 高四位控制T1 低四位控制T0 TMOD各位的说明 GATE 0 仅由运行控制位TRx x 0 1 来控制定时器 计数器运行 1 用外中断引脚 INT0
  • 【linux】Linux cp命令拷贝 不覆盖原有的文件

    文章目录 print和printf输出重定向 print items gt output file 保存items到文件 如分别保存用户和家目录 awk F print 1 gt username print 6 gt home etc p
  • MyBatisPlus(三)基础Service接口:增删改查

    MyBatisPlus 基础Service接口 增删改查 使用 MyBatisPlus 的 Service 接口 实现基础的增删改查功能 创建Service 创建Service 继承自MyBatisPlus提供的Service接口 代码 p
  • js手动触发事件dispatchEvent()

    创建并触发事件 有两种方法可以实现手动触发事件 一种是如下所示的过时的繁琐的方式创建事件对象并触发事件 create init dispatch 还有一种 不兼容IE 但是建议使用 参考另一篇JavaScript原生实现事件监听及手动触发
  • 关于航模的几点积累:(一)关于机型

    关于机型 1 固定翼飞行器分类按外观 像真机 非像真机按主翼位置 上单翼 中单翼 下单翼按动力来源 电动 油动按控制系统 遥控 线控 自由飞行按螺旋桨位置 前拉 背推 腰推 尾推按起落架位置 前三点 后三点 此处还要有尾推式 背推式 腰推式
  • 关于LPC824Lite开发板下载程序时提示"Invalid ROM Table"

    在使用LPC824Lite开发板的时候经常会出现程序下不进去的qing情况 特别是使用功耗模式下经常会出现 提示一下错误 解决方法 在Debug里面吧connect改成with Pre reset Reset改成SYSRESETREQ 并在
  • 老顽固在13英寸苹果笔记本MacBook Air上开发C#.NET程序的感受浅谈

    为什么80 的码农都做不了架构师 gt gt gt 换新笔记本出发点 1 老婆说今年表现不错 没招花惹草 发点儿奖励 看我喜欢写程序就换个崭新笔记本 2 从第一次看超薄的苹果电脑后 我就深深地爱上了她 只是一直不舍得购买 手上的笔记本也很好
  • 迁移学习 & 凯明初始化

    前言 这一章其实就是之前没做完的事 来补一下 两者其实没啥关系 迁移学习 以下内容学习自迁移学习 斯坦福21秋季 实用机器学习中文版 迁移学习包括什么 feature extraction train a model on a relate
  • linux opensuse安装xmind

    opensuse yast中默认是没有xmind的 可以上官网 上面会有安装包及安装方法 suse版本是15 2 利用snap来安装xmind 一安装snap 1 添加源 sudo zypper addrepo refresh https
  • 【Python】如何做代码静态分析?

    文章目录 静态分析简介 什么是静态分析 为什么要做代码静态分析 Bug 引入 Bug 发现 Bug 修复成本 测试左移 静态分析特点 常用的静态分析技术 静态分析简介 中文名 程序静态分析 外文名 Program Static Analys
  • [VS2010] 编译64位静态库lib提示fatal error LNK1112: module machine type 'x64' conflicts等错误的解决方案

    环境 Win7 x64 Visual Studio 2010 程序写完 本想导出dll x64 lib x64 在动态链接库dll下编译成功 但是在编译静态库lib时 出错了 报错如下 1 gt Build started Project
  • npm WARN config global `--global`, `--local` are deprecated. Use `--location=global` instead. chang

    npm安装typescript报错 提示 这里简述项目相关背景 在vscode新建一个项目 想写ts 于是先准备安装typescript 不料 报错 原因分析 提示 这里填写问题的分析 主要原因发现应该是npm版本号的原因 解决方案 提示
  • html的block标签,html标签的block、inline分类总结分享

    块元素 block element m address 地址 blockquote 块引用 center 举中对齐块 dir 目录列表 p 常用块级容易 也是css layout的主要标签 dl 定义列表 fieldset form控制组
  • 【SEER数据库简介】

    一 SEER数据库简介 全称 Surveillance Epidemiology and End Results Program 网址 https seer cancer gov SEER 数据库所涉及的肿瘤被划分为9类 乳腺肿瘤 结肠与直
  • PieCloudDB Database:云原生分布式虚拟数仓的诞生之旅

    杭州拓数派科技发展有限公司 OpenPie 的旗舰产品 PieCloudDB Database 是一款云原生分布式虚拟数仓 PieCloudDB 通过多种创新性技术将物理数仓整合到云原生数据计算平台 PieCloudDB 可以动态创建虚拟数
  • VTK相机类vtkCamera原理及用法

    vtk是著名的开源三维渲染库 在三维渲染过程中的一个非常重要的内容就是相机即vtkCamera类的设置 在VTK中 相机的实质是一个观测点 VTK的官方Doc对vtkCamera写的十分简略 暗坑很多 在学习和使用vtkCamera的过程中
  • PostgreSQL报pg_dump: no matching tables were found错误

    刚导出表时 发现找不到这个表 但是表是存在的 所以找了一圈 发现是要加 才行 例如 T TABLES
  • python中input()和raw_input()的区别

    两者均是python的内置函数 通过读取控制台的输入与用户实现交互 raw input 将所有输入作为字符串看待 不管用户输入什么类型的都会转变成字符串 raw的含义就是 生的 未加工的 gt gt gt s1 raw input abc
  • Cadence学习篇(1) Cadence原理图工程以及原理图库的创建

    文章目录 前言 一 创建原理图库 1 1新建工程 1 2 设置原理图板框 1 3 设置原理图栅格 二 添加多个原理图 2 1 原理图重命名 2 2 原理图编页码 三 放置元器件 3 1 添加库 3 2 连线 四 保存工程文件 4 1 新建原

随机推荐

  • Larave5.7使用Mailable发送邮件

    现在很多网站都有发送邮件验证身份的功能 所以介绍一下Laravel中邮件发送的方法 Laravel框架中为我们绑定了Mailable服务 我们只需要配置好参数 然后使用该服务即可 配置邮件服务器 我们发送邮件需要有一个stmp服务器 现在有
  • Sublime实现自动排版

    sublime功能很强大 但是使用sublime就可以实现代码自动重新缩进 使代码缩进重排 方法 Ctrl A选中全部内容 然后在菜单中选择Edit gt Line gt Reindent
  • 苹果发布AirTag新固件更新:增加了反跟踪增强功能

    Apple今天发布了专为AirTags设计的1 0 27 固件的新版本 这是对 6 月份提供的更新的修订 新的 AirTags 1 0 276 固件的内部版本号为 1A287b 而旧固件的内部版本号为 1A276d 6 月份发布的 1 0
  • k8s dashboard 报错 Error: 'dial tcp 172.168.56.2:9090: getsockopt: connection refused'

    访问web http 192 168 56 101 8080 ui Error dial tcp 172 17 26 2 9090 getsockopt connection refused 排查方法 1 需要检查apiserver的地址设
  • hive多窗口遇到java.sql.SQLException 异常

    hive多窗口遇到java sql SQLException 异常 多打开一个客户端窗口启动 hive 会产生 java sql SQLException 异常 文章目录 hive多窗口遇到java sql SQLException 异常
  • 【沉浸式腾讯云服务器部署安装docker】

    重置密码 sudo passwd root lighthouse VM 12 2 centos sudo passwd root Changing password for user root New password Retype new
  • asp.net zero 8.2 学习-11-Metronic替换google字体,加速网页加载速度

    asp net zero 8 2使用的前端模板是Metronic6 0以上版本 官网的Metronic下载下来 打开很慢主要是加载googole字体耗费时间 这是我之前写的如何在Metronic中替换google字体 Metronic是一款
  • 使用STM32F4XX自带数学库“arm_math.h“

    使用STM32F4XX自带数学库 arm math h STM32 F4属于Cortex M4F构架 这与M0 M3的最大不同就是具有FPU 浮点运算单元 支持浮点指令集 因此在处理数学运算时能比M0 M3高出数十倍甚至上百倍的性能 但是要
  • 什么是低信噪比图像及处理方法

    信号处理领域的信噪比即SNR Singal to Noise Ration 又称讯噪比 即放大器的输出信号的电压与同时输出的噪声电压的比 常常用分贝数表示 设备的信噪比越高表明它产生的杂音越少 一般来说 信噪比越大 说明混在信号里的噪声越小
  • python (一维、二维)列表的初始化

    一维列表的初始化 初始一个长度为5的列表 方式1 a 0 5 0 0 0 0 0 方式2 a 0 for in range 5 0 0 0 0 0 二维列表的初始化 初始一个2 5的列表 方式1 b 0 5 for in range 2 0
  • Hibernate环境搭建(小实例)

    Hibernate是一个开源的对象关系映射框架 在学习之前 首先让我们先了解一下Hibernate环境是如何搭建的 废话不多说 直接进入正题 建项目 引Jar包 首先 我们需要创建一个Java项目 创建好项目之后 就需要引入与Hiberna
  • unity的HDR效果

    http blog csdn net wolf96 article details 44057915 文章开始先放两组效果 文章结尾再放两组效果 本文测试场景资源来自浅墨大神 shader效果为本文效果 HDR 人们有限的视觉系统 只支持1
  • 【DS】单链表@线性表 —— 增删查改

    目录 0 引 1 链表的概念和结构 2 链表的分类 3 链表的实现 3 1 打印 申请新节点 销毁 3 1 1 打印 3 1 2 申请新节点 3 1 3 销毁 3 2 尾插 尾删 3 2 1 尾插 3 2 2 尾删 3 3 头插 头删 3
  • 物联网场景中,我们如何选择时序数据库 ?

    如今时序数据的应用场景十分广泛 许多类型的数据都是时间序列数据 金融市场交易 传感器测量 水冷 高温 地震 服务器监控 CPU 内存 磁盘 资源消耗 能源 电力 人体健康 心率 血氧浓度 网络访问 通过保留数据固有的时间序列性质 我们可以记
  • mysql:ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: Incorrect string value:

    发现某个组件的表单输入报错 Error ER TRUNCATED WRONG VALUE FOR FIELD Incorrect string value xE6 x88 x91 xE4 xBB xAC for column content
  • 【算法与数据结构】235、LeetCode二叉搜索树的最近公共祖先

    文章目录 一 题目 二 解法 三 完整代码 所有的LeetCode题解索引 可以看这篇文章 算法和数据结构 LeetCode题解 一 题目 二 解法 思路分析 本题和这道题类似 算法与数据结构 236 LeetCode二叉树的最近公共祖先
  • Linux下Samba的配置

    参考 http www cnblogs com mchina archive 2012 12 18 2816717 html 前言 为了实现windows 和 Linux以及其他操作系统之间的资源共享 软件商推出nfs 和samba两种解决
  • Air101

    目录 1 合宙Air101 固件编译可参考 PinOut V2 1092400 管脚映射表 PinOut V2 1091800 2 Air103 最新固件下载 固件编译可参考 PinOut V3 21112201 管脚映射表 资料链接 Pi
  • Solid JS基础

    Solid js 用于构建用户界面的声明式 高效且灵活的 JavaScript 库 您可以在 官方教程 中尝试下面提到的部分例子 本文引用并简化了官方教程中的部分例子 本文讲述部分 solid 主要内容 更多详细内容 移步 Solid AP
  • 编码器的使用

    首先来看一下增量式编码器的输出信号和它的信号倍频技术 增量式编码器输出的脉冲波形信号形式常见的有两种 一种是占空比50 的方波 通道A和B相位差为90 另一种则是正弦波这类模拟信号 通道A和B相位差同样为90 对于第1种形式的方波信号 如果