目录
编码器
编码器简介
编码器:一种将直线位移、角位移数据转换为脉冲信号、二进制编码的设备。常用于测量物体运动的位置、角度或者速度。
编码器分类
编码器可以按照检测原理、编码类型进行分类。
检测原理可分为:光电式、磁电式。
编码类型可分为:绝对式、增量式。
主要以下四大类:
光电+绝对式
光电+增量式
磁电+绝对式
磁电+增量式
编码器原理
磁电+增量式
利用霍尔效应(磁跟电的关系,磁的变化影响电的变化),将位移转换成计数脉冲,用脉冲个数计算位移和速度。
光电+增量式
利用光电系统,将位移转换成计数脉冲,用脉冲个数计算位移和速度。
光电+绝对式
不同位置、角度时,光敏元件根据受光转换出相应电平信号,形成二进制数。
磁电+绝对式
复杂,略。
编码器参数
分辨率:编码器可以测量的最小距离。对于增量式编码器,分辨率即转轴每旋转一圈所输出的脉冲数(PPR,线)。
精度:编码器输出的信号数据与实际位置之间的误差,常用角分、角秒表示。
最大响应频率(PPS):编码器每秒能输出的最大脉冲数,单位Hz。
最大转速:指编码器机械系统所能承受的最高转速。
STM32编码器接口
编码器接口简介
STM32定时器的编码器接口模式相当于带有方向选择的外部时钟,即外部输入的脉冲信号可以作为计数器的时钟,而计数的方向则是由脉冲相位的先后所决定。
编码器接口框图
A、B两相脉冲信号从TIMx_CH1和TIMx_CH2这两个通道输入,经过滤波器和边沿检测器(可以设置滤波和反相)的处理,进入到编码器接口控制器中。
Tips:TIMx_CH1和TIMx_CH2这两个通道才支持编码器功能,TIMx_CH3和TIMx_CH4这两个通道不支持。
编码器接口计数原理
编码器接口可以利用输入脉冲的边沿进行计数,通过计数值的变化量可以算出输入脉冲信号的变化量,也就可以计算出电机的转速。
TIMx从模式控制寄存器(TIMx_SMCR)的0~2位:SMS[2:0]为从模式选择,控制边沿检测的方式。
选择外部信号时,触发信号(TRGI)的有效边沿与外部输入上所选的极性相关。
001:编码器模式1-计数器根据TI1FP1电平在TI2FP2边沿递增/递减计数。
010:编码器模式2-计数器根据TI2FP2电平在TI1FP1边沿递增/递减计数。
011:编码器模式3-计数器根据TI1FP1和TI2FP2的边沿计数,计数的方向取决于另外一个信号的电平。
Tips:
选择仅在TI1或TI2处计数,就相当于对脉冲信号进行了2倍频(上身沿和下降沿),如果编码器输出11个脉冲信号,那么就会计数22次。
选择在TI1和TI2处均计数,就相当于对脉冲信号进行了4倍频(上身沿和下降沿),如果编码器输出11个脉冲信号,那么就会计数44次。
因此可以我们通过计数次数来计算电机速度时,需要除以相应的倍频系数。至此,A、B两相脉冲信号的变化就转换成了定时器的计数变化。
可以通过一分钟内计数的变化量来计算电机速度:
电机转速 = 一分钟内计数的变化量 / 倍频系数 / 编码器线数 / 减速比
编码器模式1:仅在TI1处计数
计数器根据TI1FP1电平在TI2FP2边沿递增/递减计数。
假设把A相接在CH1(TI1),B相接在CH2(TI2),选择仅在TI1处计数(仅检测A相边沿)。脉冲信号会有两种,当编码器正转时A相在前、当编码器反转时B相在前。
正转:当A相上身沿时,B相低电平,表格中对应的是递增计数。当A相下降沿时,B相高电平,表格中对应的是递增计数。即编码器正转对应的计数方向是递增计数。
反转:当A相上身沿时,B相高电平,表格中对应的是递减计数。当A相下降沿时,B相低电平,表格中对应的是递减计数。即编码器反转对应的计数方向是递减计数。
编码器模式2:仅在TI2处计数
计数器根据TI2FP2电平在TI1FP1边沿递增/递减计数。
假设把A相接在CH1(TI1),B相接在CH2(TI2),选择仅在TI2处计数(仅检测B相边沿)。脉冲信号会有两种,当编码器正转时A相在前、当编码器反转时B相在前。
正转:当B相上身沿时,A相高电平,表格中对应的是递增计数。当B相下降沿时,A相低电平,表格中对应的是递增计数。即编码器正转对应的计数方向是递增计数。
反转:当B相上身沿时,A相低电平,表格中对应的是递减计数。当B相下降沿时,A相高电平,表格中对应的是递减计数。即编码器反转对应的计数方向是递减计数。
编码器模式3:在TI1和TI2处均计数
计数器根据TI1FP1和TI2FP2的边沿计数,计数的方向取决于另外一个信号的电平。
假设把A相接在CH1(TI1),B相接在CH2(TI2),选择仅在TI2处计数(仅检测B相边沿)。脉冲信号会有两种,当编码器正转时A相在前、当编码器反转时B相在前。
正转:当A相上身沿时,B相低电平,表格中对应的是递增计数。当B相上身沿时,A相高电平,表格中对应的是递增计数。当A相下降沿时,B相高电平,表格中对应的是递增计数。当B相下降沿时,A相低电平,表格中对应的是递增计数。即编码器正转对应的计数方向是递增计数。
反转:当B相上身沿时,A相低电平,表格中对应的是递减计数。当A相上身沿时,B相高电平,表格中对应的是递减计数。当B相下降沿时,A相高电平,表格中对应的是递减计数。当A相下降沿时,B相低电平,表格中对应的是递减计数。即编码器反转对应的计数方向是递减计数。
计数溢出处理
编码器在电机运行时会一直旋转并输出脉冲信号,如果时间较长,那么定时器计数就会溢出,我们必须对溢出进行处理,否则电机速度的计算结果就不准了。
当前总计数值 = 计数器当前值 + 溢出次数(可在溢出中断中记录) * 65536。
溢出时读取TIMx控制寄存器1(TIMx_CR1)的位4(DIR,计数方向),以计算总的计数次数变化量。
0:计数器递增计数
1:计数器递减计数
作用:在计数溢出时,读取到溢出方向,后续才能计算总的计数变化量。
编码器测速
编码器测试相关参数
以下参考针对正点原子的直流有刷电机。
编码器分辨率 编码器供电 电机减速比 11PPR DC 5V 30:1 编码器分辨率对于增量式编码器来说,转轴每旋转一圈所输出的脉冲数。即表示每旋转一周能输出11个脉冲数。如果用STM32的编码器接口去检测这个脉冲,假如采用四倍频,则这个编码器旋转一周所对应的脉冲数是11*4,即44个脉冲数。STM32知道了脉冲数,就可以反推旋转速度。
知道了旋转速度,也就是电机原始输出轴(转子的转速),最终经过电机减速比得到输出转速。
编码器接口HAL库函数
函数 | 主要功能 |
HAL_TIM_Encoder_Init() | 初始化定时器基础参数及编码器接口 |
HAL_TIM_Encoder_Start() | 开启编码器接口通道 |
HAL_TIM_PeriodElapsedCallback() | 定时器更新中断回调函数 |
__HAL_TIM_IS_TIM_COUNTING_DOWN() | 读取DIR位,判断计数方向 |
关键结构体介绍
typedef struct
{
uint32_t EncoderMode; /* 编码器模式 */
uint32_t IC11Polarity; /* 输入极性(边沿检测器) */
uint32_t IC1Selection; /* 输入通道选择 */
uint32_t IClPrescaler; /* 时钟分频因子 */
uint32_t IClFilter; /* 滤波器 */
uint32_t IC21Polarity;
uint32_t IC2Selection;
uint32_t IC2Prescaler;
uint32_t IC2Filter;
}TIM_Encoder_InitTypeDef;
课堂代码
代码功能:
1、利用TIM3的CH1(PC6)、CH2(PC7)作为编码器脉冲信号输入接口。
2、实现简单的电机测速。
主要硬件资源:
TIM1(PA8) 正常输出通道
TIM1(PB13) 互补输出通道
TIM3(PC6) 编码器A相输入通道
TIM3(PC7) 编码器B相输入通道
使用TIM6定时计算电机速度,配置溢出时间为1ms,即1ms进入一次定时器6的更新中断。
ADC1_CH9(PB1) 电压
ADC1_CH0(PA0) 温度
ADC1_CH9(PB0) 电流
SD(PF10) 刹车信号输出
重要函数:
1、定时器初始化函数
初始化定时器通道IO,配置定时器、编码器接口等,增加TIM3更新中断。
2、更新中断回调函数
处理溢出中断,判断计数方向,记录溢出次数(通过TIMx_CR1:DIR判断。向上溢出就加一,向下溢出就减一),计算电机速度。
3、编码器计数函数
计算总的计数值,总计数值 = 计数器当前值 + 溢出次数 * 65536
4、电机速度计算函数
计算单位时间内计数变化量,然后计算电机的转速。
计算电机转速:
1、计算ms毫秒内计数变化量
2、计算1min内计数变化量:g_encode.speed * ((1000 / ms) * 60 ,
3、除以编码器旋转一圈的计数次数(倍频倍数 * 编码器分辨率)
4、除以减速比即可得出电机转速
直流有刷电机选择磁电增量式编码器,分辨率11线。
编码器配置参数介绍:
IC1Polarity:在编码器模式下用于设置输入信号是否反相,它设定的是TIMx_CCER寄存器的CCxNP位和CCxP位。本实验不需要输入信号反相。
IC1Selection:用来设置通道映射关系,他设定TIMx_CCMRx寄存器的CCxS[1:0]位的值。
/******************************* 第二部分 电机编码器测速 ****************************************************/
/********************************* 1 通用定时器 编码器程序 *************************************/
TIM_HandleTypeDef g_timx_encode_chy_handle; /* 定时器x句柄 */
TIM_Encoder_InitTypeDef g_timx_encoder_chy_handle; /* 定时器编码器句柄 */
/**
* @brief 通用定时器TIMX 通道Y 编码器接口模式 初始化函数
* @note
* 通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
* 通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void gtim_timx_encoder_chy_init(uint16_t arr, uint16_t psc)
{
/* 定时器x配置 */
g_timx_encode_chy_handle.Instance = GTIM_TIMX_ENCODER; /* 定时器x */
g_timx_encode_chy_handle.Init.Prescaler = psc; /* 定时器分频 */
g_timx_encode_chy_handle.Init.Period = arr; /* 自动重装载值 */
g_timx_encode_chy_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 时钟分频因子 */
/* 定时器x编码器配置 */
g_timx_encoder_chy_handle.EncoderMode = TIM_ENCODERMODE_TI12; /* TI1、TI2都检测,4倍频 */
g_timx_encoder_chy_handle.IC1Polarity = TIM_ICPOLARITY_RISING; /* 输入极性,非反向 */
g_timx_encoder_chy_handle.IC1Selection = TIM_ICSELECTION_DIRECTTI; /* 输入通道选择 */
g_timx_encoder_chy_handle.IC1Prescaler = TIM_ICPSC_DIV1; /* 不分频 */
g_timx_encoder_chy_handle.IC1Filter = 10; /* 滤波器设置 */
g_timx_encoder_chy_handle.IC2Polarity = TIM_ICPOLARITY_RISING; /* 输入极性,非反向 */
g_timx_encoder_chy_handle.IC2Selection = TIM_ICSELECTION_DIRECTTI; /* 输入通道选择 */
g_timx_encoder_chy_handle.IC2Prescaler = TIM_ICPSC_DIV1; /* 不分频 */
g_timx_encoder_chy_handle.IC2Filter = 10; /* 滤波器设置 */
HAL_TIM_Encoder_Init(&g_timx_encode_chy_handle, &g_timx_encoder_chy_handle);/* 初始化定时器x编码器 */
HAL_TIM_Encoder_Start(&g_timx_encode_chy_handle,GTIM_TIMX_ENCODER_CH1); /* 使能编码器通道1 */
HAL_TIM_Encoder_Start(&g_timx_encode_chy_handle,GTIM_TIMX_ENCODER_CH2); /* 使能编码器通道2 */
__HAL_TIM_ENABLE_IT(&g_timx_encode_chy_handle,TIM_IT_UPDATE); /* 使能更新中断 */
__HAL_TIM_CLEAR_FLAG(&g_timx_encode_chy_handle,TIM_IT_UPDATE); /* 清除更新中断标志位 */
}
/**
* @brief 定时器底层驱动,时钟使能,引脚配置
此函数会被HAL_TIM_Encoder_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == GTIM_TIMX_ENCODER)
{
GPIO_InitTypeDef gpio_init_struct;
GTIM_TIMX_ENCODER_CH1_GPIO_CLK_ENABLE(); /* 开启通道y的GPIO时钟 */
GTIM_TIMX_ENCODER_CH2_GPIO_CLK_ENABLE();
GTIM_TIMX_ENCODER_CH1_CLK_ENABLE(); /* 开启定时器时钟 */
GTIM_TIMX_ENCODER_CH2_CLK_ENABLE();
gpio_init_struct.Pin = GTIM_TIMX_ENCODER_CH1_GPIO_PIN; /* 通道y的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = GTIM_TIMX_ENCODERCH1_GPIO_AF; /* 端口复用 */
HAL_GPIO_Init(GTIM_TIMX_ENCODER_CH1_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = GTIM_TIMX_ENCODER_CH2_GPIO_PIN; /* 通道y的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = GTIM_TIMX_ENCODERCH2_GPIO_AF; /* 端口复用 */
HAL_GPIO_Init(GTIM_TIMX_ENCODER_CH2_GPIO_PORT, &gpio_init_struct);
HAL_NVIC_SetPriority(GTIM_TIMX_ENCODER_INT_IRQn, 2, 0); /* 中断优先级设置 */
HAL_NVIC_EnableIRQ(GTIM_TIMX_ENCODER_INT_IRQn); /* 开启中断 */
}
}
/**
* @brief 定时器中断服务函数
* @param 无
* @retval 无
*/
void GTIM_TIMX_ENCODER_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_timx_encode_chy_handle);
}
/******************************** 2 基本定时器 编码器程序 ************************************/
TIM_HandleTypeDef timx_handler; /* 定时器参数句柄 */
/**
* @brief 基本定时器TIMX定时中断初始化函数
* @note
* 基本定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
* 基本定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
timx_handler.Instance = BTIM_TIMX_INT; /* 基本定时器X */
timx_handler.Init.Prescaler = psc; /* 设置预分频器 */
timx_handler.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数器 */
timx_handler.Init.Period = arr; /* 自动装载值 */
timx_handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 时钟分频因子 */
HAL_TIM_Base_Init(&timx_handler);
HAL_TIM_Base_Start_IT(&timx_handler); /* 使能基本定时器x和及其更新中断:TIM_IT_UPDATE */
__HAL_TIM_CLEAR_IT(&timx_handler,TIM_IT_UPDATE); /* 清除更新中断标志位 */
}
/**
* @brief 定时器底册驱动,开启时钟,设置中断优先级
此函数会被HAL_TIM_Base_Init()函数调用
* @param 无
* @retval 无
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIMX_INT)
{
BTIM_TIMX_INT_CLK_ENABLE(); /*使能TIM时钟*/
HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 1, 3); /* 抢占1,子优先级3,组2 */
HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn); /*开启ITM3中断*/
}
}
/**
* @brief 基本定时器TIMX中断服务函数
* @param 无
* @retval 无
*/
void BTIM_TIMX_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&timx_handler); /*定时器回调函数*/
}
/******************************** 3 公用部分 编码器程序 ************************************/
volatile int g_timx_encode_count = 0; /* 溢出次数 */
/**
* @brief 定时器更新中断回调函数
* @param htim:定时器句柄指针
* @note 此函数会被定时器中断函数共同调用的
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle)) /* 判断CR1的DIR位 */
{
g_timx_encode_count--; /* DIR位为1,也就是递减计数 */
}
else
{
g_timx_encode_count++; /* DIR位为0,也就是递增计数 */
}
}
else if (htim->Instance == TIM6)
{
int Encode_now = gtim_get_encode(); /* 获取编码器值,用于计算速度 */
speed_computer(Encode_now, 50); /* 中位平均值滤除编码器抖动数据,50ms计算一次速度*/
}
}
/**
* @brief 获取编码器的值
* @param 无
* @retval 编码器值
*/
int gtim_get_encode(void)
{
return ( int32_t )__HAL_TIM_GET_COUNTER(&g_timx_encode_chy_handle) + g_timx_encode_count * 65536; /* 当前计数值+之前累计编码器的值=总的编码器值 */
}
冒泡排序:略
一阶低通滤波:适用于对电机速度进行滤波。
公式为:Y(n)= qX(n) + (1-q)Y(n-1)
其中X(n)为本次采样值;Y(n-1)为上次滤波输出值;Y(n)为本次滤波输出值,q为滤波系数
q值越小则上一次输出对本次输出影响越大,整体曲线越平稳,但是对于速度变化的响应也会越慢
通过累计计算10次电机速度,然后进行冒泡排序,把10次电机速度值从小到大排序,接着将中间的6次速度值累加求平均值,最后再进行一阶低通滤波。
一阶低通滤波公式:Y(n) = q * X(n) + (1-q) * Y(n-1)。
Y(n):本次滤波输出值
q:滤波系数(0~1)(q越大,响应越快,但曲线不平滑;q越小,曲线越平滑,但响应越慢)
X(n):本次采样值
Y(n-1):上次滤波输出值
/************************************* 第三部分 编码器测速 ****************************************************/
Motor_TypeDef g_motor_data; /*电机参数变量*/
ENCODE_TypeDef g_encode; /*编码器参数变量*/
/**
* @brief 电机速度计算
* @param encode_now:当前编码器总的计数值
* ms:计算速度的间隔,中断1ms进入一次,例如ms = 5即5ms计算一次速度
* @retval 无
*/
void speed_computer(int32_t encode_now, uint8_t ms)
{
uint8_t i = 0, j = 0;
float temp = 0.0;
static uint8_t sp_count = 0, k = 0;
static float speed_arr[10] = {0.0}; /* 存储速度进行滤波运算 */
if (sp_count == ms) /* 计算一次速度 */
{
/* 计算电机转速
第一步 :计算ms毫秒内计数变化量
第二步 ;计算1min内计数变化量:g_encode.speed * ((1000 / ms) * 60 ,
第三步 :除以编码器旋转一圈的计数次数(倍频倍数 * 编码器分辨率)
第四步 :除以减速比即可得出电机转速
*/
g_encode.encode_now = encode_now; /* 取出编码器当前计数值 */
g_encode.speed = (g_encode.encode_now - g_encode.encode_old); /* 计算编码器计数值的变化量 */
speed_arr[k++] = (float)(g_encode.speed * ((1000 / ms) * 60.0) / REDUCTION_RATIO / ROTO_RATIO ); /* 保存电机转速 */
g_encode.encode_old = g_encode.encode_now; /* 保存当前编码器的值 */
/* 累计10次速度值,后续进行滤波*/
if (k == 10)
{
for (i = 10; i >= 1; i--) /* 冒泡排序*/
{
for (j = 0; j < (i - 1); j++)
{
if (speed_arr[j] > speed_arr[j + 1]) /* 数值比较 */
{
temp = speed_arr[j]; /* 数值换位 */
speed_arr[j] = speed_arr[j + 1];
speed_arr[j + 1] = temp;
}
}
}
temp = 0.0;
for (i = 2; i < 8; i++) /* 去除两边高低数据 */
{
temp += speed_arr[i]; /* 将中间数值累加 */
}
temp = (float)(temp / 6); /*求速度平均值*/
/* 一阶低通滤波
* 公式为:Y(n)= qX(n) + (1-q)Y(n-1)
* 其中X(n)为本次采样值;Y(n-1)为上次滤波输出值;Y(n)为本次滤波输出值,q为滤波系数
* q值越小则上一次输出对本次输出影响越大,整体曲线越平稳,但是对于速度变化的响应也会越慢
*/
g_motor_data.speed = (float)( ((float)0.48 * temp) + (g_motor_data.speed * (float)0.52) );
k = 0;
}
sp_count = 0;
}
sp_count ++;
}