增量式和位置式编码器的原理讲解以及步进电机测速方案实现

目录

1.编码器详解

1.1. 编码器介绍

1.1.1. 增量式编码器

1.1.2. 绝对式编码器

1.1.3. 混合式绝对式编码器

1.2. 旋转编码器原理

1.2.1. 增量式编码器原理

1.2.2. 绝对式编码器原理

1.2.3. 编码器基本参数

2.增量式编码器倍频原理

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

2.2.1. TIM_Base_InitTypeDef

2.2.2.步进电机测速


1.编码器详解

1.1. 编码器介绍

编码器,是一种用来测量机械旋转或位移的传感器。这种传感器能够测量机械部件在旋转或直线运动时的位移位置或速度等信息, 并将其转换成一系列电信号。编码器是工业中常用的传感器之一,广泛应用于工业生产当中需要对机械系统进行监视或控制的场景, 包括工业控制、机器人、照相机镜头、雷达平台以及部分计算机输入设备例如轨迹球和鼠标滚轮等等。

编码器可以根据不同的方式分出很多种类型。例如根据检测原理,可分为光学式、磁式、感应式和电容式。 根据内部机械结构的运动方式,可分为线性编码器和旋转编码器。根据其刻度实现方法及信号输出形式, 又可分为增量式、绝对式以及混合式三种。编码器种类繁多,本章主要讲解旋转编码器,如下图所示,外形很像一个电机。

旋转编码器

图7-1 旋转编码器

1.1.1. 增量式编码器

增量式旋转编码器是将设备运动时的位移信息变成连续的脉冲信号,脉冲个数表示位移量的大小。只有当设备运动的时候增量式编码器才会输出信号。 编码器一般会把这些信号分为通道A和通道B两组输出,并且这两组信号间有90°的相位差。同时采集这两组信号就可以知道设备的运动和方向。 除了通道A、通道B以外,很多增量式编码器还会设置一个额外的通道Z输出信号,用来表示编码器特定的参考位置,传感器转一圈Z轴信号才会输出一个脉冲。 增量式编码器只输出设备的位置变化和运动方向,不会输出设备的绝对位置。

1.1.2. 绝对式编码器

绝对式旋转编码器是将设备运动时的位移信息通过二进制编码的方式变成数字量直接输出。 这种编码器与增量式编码器的区别主要在内部的码盘。绝对式编码器的码盘利用若干透光和不透光的线槽组成一套二进制编码, 这些二进制码与编码器转轴的每一个不同角度是唯一对应的,读取这些二进制码就能知道设备的绝对位置,所以叫它绝对式编码器。 绝对式编码器一般常用自然二进制、格雷码或者BCD码等编码方式。

1.1.3. 混合式绝对式编码器

混合式绝对式编码器,它输出两组信息:一组信息用于检测磁极位置,带有绝对信息功能;另一组则和增量式编码器的输出信息完全相同。

1.2. 旋转编码器原理

旋转编码器的原理示意图如下图所示。旋转编码器内部大都由码盘、光电检测装置和信号处理电路等部分构成。码盘上刻了若干圈线槽, 线槽等距并且可透光,当码盘旋转时就会周期性的透过和遮挡来自光电检测装置的光线,这样检测装置就会周期性的生成若干电信号。 但是这些电信号通常比较微弱,需要加入一套处理电路对信号进行放大和整形,最后把信号整形为脉冲信号并向外输出。

编码器原理示意图

图7-2 编码器原理示意图

虽然旋转编码器的原理在总体上差不多,但是对于这些原理的具体实现方法却有很大不同。

1.2.1. 增量式编码器原理

首先来看增量式编码器。上节提到过,增量式编码器都有A、B两通道信号输出,这是因为增量式编码器的码盘上有两圈线槽, 两圈线槽的之间会错开一定的角度,这个角度会使得光电检测装置输出的两相信号相差 1/4 周期(90°)。码盘的具体工作方式如下图所示。 图中黑色代表透光,白色代表遮光。当码盘转动时,内圈和外圈的线槽会依次透过光线,光电检测装置检测到光线通断的变化, 就会相应的输出脉冲信号,因为内外圈遮光和透光时候存在时间差,所以也就有了A、B两通道信号的相位差。

增量式编码器码盘运作方式1

图7-3 增量式编码器码盘运作方式1

增量式编码器码盘运作方式2

图7-4 增量式编码器码盘运作方式2

增量式编码器码盘运作方式3

图7-5 增量式编码器码盘运作方式3

根据两相信号变化的先后顺序就可以判断运动方向,记录输出的脉冲个数可以知道位移量的大小,同时通过输出信号的频率就能得到速度。

一些增量式编码器上会有4圈线槽,分别对应A、B、-A、-B四相信号,相邻两相信号间也是差1/4周期,只不过这种编码器会把-A和-B两相信号反相, 然后叠加到A、通道B,用来增强信号。除了通道A、通道B以外,很多增量式编码器还会设置一个额外的通道Z输出信号。通道Z信号也在码盘上有对应的线槽, 不过只有一条,码盘转一圈才会经过一次。通道Z信号一般用做参考零位,指示设备位置或者清除积累量。

另一种较为常用的增量式编码器是霍尔编码器。霍尔增量式编码器在结构上和光电式几乎相同,只不过检测原理变成了霍尔效应。 内部元件也稍有不同,霍尔编码器的码盘上不是线槽,而是不同的磁极,或者有些直接把电机的旋转磁场当作码盘, 然后检测装置换成了霍尔传感器。输出和光电式相同,仍然是相位差1/4周期的A、B两通道信号。

增量式编码器计数起点任意设定,可实现多圈无限累加和测量。需要提高分辨率时,可触发A、B两通道信号的上升沿和下降沿对原脉冲数进行倍频。 但是当接收设备停机重启后,增量式编码器需要重新寻找参考零点。

1.2.2. 绝对式编码器原理

接着是绝对式编码器。绝对式编码器在总体结构上与增量式比较类似,都是由码盘、检测装置和放大整形电路构成,但是具体的码盘结构和输出信号含义不同。 绝对式编码器的码盘上有很多圈线槽,被称为码道,每一条码道内部线槽数量和长度都不同。它们共同组成一套二进制编码, 一条码道对应二进制数的其中一个位,通常是码盘最外侧的码道表示最低位,最内侧的码道表示最高位。码道的数量决定了二进制编码的位数, 一个绝对式编码器有 N 条码道,它就能输出 N 位二进制数,且输出二进制数的总个数是 2N 个。 这些二进制数与转轴的机械位置是固定的,和编码器外部因素无关,所以叫做绝对式编码器。在接收设备断电重启后绝对式编码器无需寻找参考零点。

下图是一个简化版的绝对式编码器码盘,其中白色块透光表示0,黑色块不透光表示1。码盘上的二进制数逆时针依次增大。

绝对式编码器码盘(自然二进制)

图7-6 绝对式编码器码盘(自然二进制)

图中码盘有3条码道,一共可表示23=8个二进制数,所以整个码盘被分成了8个扇区,每个扇区表示一个3位二进制数, 每个二进制数对应一个转轴的位置信息。码盘采用自然二进制编码,自然二进制编码的优点是很方便直观,但是受编码器制造和安装精度的影响, 实际应用中二进制数的每一位不可能同时改变,或者出现码盘停在两个扇区中间,这些情况都很容易造成读数错误。

为了避免出现读数错误,可以使用格雷码来解决。下图是一个使用格雷码的码盘,同样的,白色块透光表示0,黑色块不透光表示1。码盘上的二进制数逆时针依次增大。

绝对式编码器码盘(格雷码)

图7-7 绝对式编码器码盘(格雷码)

图中码盘的码道数与上面的自然二进制码盘完全一致,也能表示8个3位二进制数,只不过将编码方式换成了格雷码。 利用任意相邻的二进制格雷码数都只有一位不同的特性,采用这种编码的码盘在一定程度上克服了自然二进制码盘容易产生读数错误的问题。

绝对式编码器还分为单圈绝对式编码器和多圈绝对式编码器,上面举的两个例子都是针对单圈也就是360°以内的情况,当码盘转动超过360°, 输出的编码会重复,这样不符合绝对式编码器数据唯一的要求,所以就出现了多圈绝对式编码器。多圈绝对式编码器的量程可以超过360°,并且通常超出很多, 其内部结构也比单圈的复杂,但是基本原理都是一样的。

1.2.3. 编码器基本参数

  • 分辨率:指编码器能够分辨的最小单位。对于增量式编码器,其分辨率表示为编码器转轴旋转一圈所产生的脉冲数, 即脉冲数/转(Pulse Per Revolution或PPR)。码盘上透光线槽的数目其实就等于分辨率,也叫多少线,较为常见的有5-6000线。 对于绝对式编码器,内部码盘所用的位数就是它的分辨率,单位是位(bit),具体还分单圈分辨率和多圈分辨率。

  • 精度:首先明确一点,精度与分辨率是两个不同的概念。精度是指编码器每个读数与转轴实际位置间的最大误差,通常用角度、角分或角秒来表示。 例如有些绝对式编码器参数表里会写±20′′,这个就表示编码器输出的读数与转轴实际位置之间存在正负20角秒的误差,精度由码盘刻线加工精度、 转轴同心度、材料的温度特性、电路的响应时间等各方面因素共同决定。

  • 最大响应频率:指编码器每秒输出的脉冲数,单位是Hz。计算公式:最大响应频率 = 分辨率 * 轴转速/60。

  • 信号输出形式:对于增量式编码器,每个通道的信号独立输出,输出电路形式通常有集电极开路输出、推挽输出、差分输出等。 对于绝对式编码器,由于是直接输出几十位的二进制数,为了确保传输速率和信号质量,一般采用串行输出或总线型输出, 例如同步串行接口(SSI)、RS485、CANopen或EtherCAT等,也有一部分是并行输出,输出电路形式与增量式编码器相同。

2.增量式编码器倍频原理

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

  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法的速度下限。

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

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

2.2.1. TIM_Base_InitTypeDef

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

编码器结构体定义

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]位的值。

2.2.2.步进电机测速

encoder.h-宏定义

/* 定时器选择 */
 #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

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

/**
   * @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);
 }

编码器接口配置

 /**
   * @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);
 }

定时器溢出次数记录

/* @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++;
 }

main.c主函数

/* 电机旋转方向 */
 __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("按下按键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++;
   }
 }

  • 21
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

驽马匠人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值