4.5.2.2. 软件分析¶
宏定义
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。
定时器复用功能引脚初始化
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设置中加上上拉电阻。
编码器接口配置
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倍频的原理,
接口在倍频采样的过程中也会对信号抖动进行补偿,所以输入滤波器也很少会用到。
配置完编码器接口结构体后清零计数器,然后开启定时器的更新事件中断,并把更新事件中断源配置为定时器溢出,也就是仅当定时器溢出时才触发更新事件中断。
然后配置定时器的中断优先级并开启中断,最后启动编码器接口。
定时器溢出次数记录
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。这样在计算电机转速和位置的时候就可以把溢出次数也参与在内。
主函数
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循环中调整电机速度,
我们将使用中断进行编码器数据采集和计算。
数据计算
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。不过此时得到的是电机转轴处的转速,并不是减速电机输出轴的转速,
把转轴转速除以减速比即可得到输出轴的转速。
所有数据全部采集和计算完毕后,将电机方向、单位时间内的计数值、电机转轴转速和电机输出轴转速等数据全部通过串口打印到窗口调试助手上,
并将当前的总计数值记录下来方便下次计算使用。