15.步进电机圆弧插补实现¶

xwbar的头像
2025-10-20 03:27:37
/
活动首发

15.2.2.2. 软件分析¶

宏定义

bsp_stepper_init.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

41

42

43

44

45 /*宏定义*/

/*******************************************************/

#define MOTOR_PUL_TIM TIM8

#define MOTOR_PUL_IRQn TIM8_UP_TIM13_IRQn

#define MOTOR_PUL_IRQHandler TIM8_UP_TIM13_IRQHandler

#define MOTOR_PUL_CLK_ENABLE() __TIM8_CLK_ENABLE()

#define MOTOR_PUL_GPIO_AF GPIO_AF3_TIM8

/*********************X轴电机引脚定义*******************/

//Motor 方向

#define X_MOTOR_DIR_PIN GPIO_PIN_1

#define X_MOTOR_DIR_GPIO_PORT GPIOE

#define X_MOTOR_DIR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()

//Motor 使能

#define X_MOTOR_EN_PIN GPIO_PIN_0

#define X_MOTOR_EN_GPIO_PORT GPIOE

#define X_MOTOR_EN_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()

//Motor 脉冲

#define X_MOTOR_PUL_PORT GPIOI

#define X_MOTOR_PUL_PIN GPIO_PIN_5

#define X_MOTOR_PUL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()

//定时器通道

#define X_MOTOR_PUL_CHANNEL TIM_CHANNEL_1

/*********************Y轴电机引脚定义*******************/

//Motor 方向

#define Y_MOTOR_DIR_PIN GPIO_PIN_8

#define Y_MOTOR_DIR_GPIO_PORT GPIOI

#define Y_MOTOR_DIR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()

//Motor 使能

#define Y_MOTOR_EN_PIN GPIO_PIN_4

#define Y_MOTOR_EN_GPIO_PORT GPIOE

#define Y_MOTOR_EN_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()

//Motor 脉冲

#define Y_MOTOR_PUL_PORT GPIOI

#define Y_MOTOR_PUL_PIN GPIO_PIN_6

#define Y_MOTOR_PUL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()

//定时器通道

#define Y_MOTOR_PUL_CHANNEL TIM_CHANNEL_2

由于直线插补需要两个步进电机才能完成,所以在bsp_stepper_init.h中新增第二个步进电机的相关IO口和外设的宏定义。

宏定义的具体内容与其他步进电机控制例程一致,故不再赘述。

步进电机初始化

bsp_stepper_init.h-结构体¶

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17 /* 步进电机结构体 */

typedef struct{

uint16_t pul_pin; //脉冲引脚

uint16_t dir_pin; //方向引脚

uint16_t en_pin; //使能引脚

uint32_t pul_channel; //输出脉冲的定时器通道

GPIO_TypeDef *pul_port; //脉冲引脚端口结构体

GPIO_TypeDef *dir_port; //方向引脚端口结构体

GPIO_TypeDef *en_port; //使能引脚端口结构体

}Stepper_TypeDef;

/* 步进电机结构体数组 bsp_stepper_init.c */

Stepper_TypeDef step_motor[2] =

{

{X_MOTOR_PUL_PIN, X_MOTOR_DIR_PIN, X_MOTOR_EN_PIN, X_MOTOR_PUL_CHANNEL, X_MOTOR_PUL_PORT, X_MOTOR_DIR_GPIO_PORT, X_MOTOR_EN_GPIO_PORT},

{Y_MOTOR_PUL_PIN, Y_MOTOR_DIR_PIN, Y_MOTOR_EN_PIN, Y_MOTOR_PUL_CHANNEL, Y_MOTOR_PUL_PORT, Y_MOTOR_DIR_GPIO_PORT, Y_MOTOR_EN_GPIO_PORT},

};

在bsp_stepper_init.h中定义了一个结构体,内部包括控制步进电机所必须的引脚端口、编号和脉冲输出的定时器通道,

然后在bsp_stepper_init.c中定义了一个Stepper_TypeDef类型的数组,分别管理X、Y轴步进电机。这么做的目的是为了方便后续程序更方便的控制两个步进电机,

如需增加或减少步进电机的数量,直接增加或减少数组元素即可。

bsp_stepper_init.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

48static void TIM_PWMOUTPUT_Config(void)

{

TIM_OC_InitTypeDef TIM_OCInitStructure;

/* 获取数组元素个数 */

uint8_t member_count = sizeof(step_motor)/sizeof(Stepper_TypeDef);

/*使能定时器*/

MOTOR_PUL_CLK_ENABLE();

TIM_StepperHandle.Instance = MOTOR_PUL_TIM;

/* 累计 TIM_Period个后产生一个更新或者中断*/

//当定时器从0计数到TIM_PERIOD,即为TIM_PERIOD次,为一个定时周期

TIM_StepperHandle.Init.Period = TIM_PERIOD;

// 通用控制定时器时钟源TIMxCLK = HCLK = 168MHz

// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=28MHz

TIM_StepperHandle.Init.Prescaler = TIM_PRESCALER-1;

/*计数方式*/

TIM_StepperHandle.Init.CounterMode = TIM_COUNTERMODE_UP;

/*采样时钟分频*/

TIM_StepperHandle.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;

TIM_StepperHandle.Init.RepetitionCounter = 0;

/*初始化定时器为输出比较模式*/

HAL_TIM_Base_Init(&TIM_StepperHandle);

/*PWM模式配置--这里配置为PWM模式2*/

TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM2;

/*比较输出的计数值*/

TIM_OCInitStructure.Pulse = TIM_PERIOD;

/*当定时器计数值小于CCR1_Val时为高电平*/

TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;

/*设置互补通道输出的极性*/

TIM_OCInitStructure.OCNPolarity = TIM_OCNPOLARITY_HIGH;

/*快速模式设置*/

TIM_OCInitStructure.OCFastMode = TIM_OCFAST_DISABLE;

/*空闲电平*/

TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_RESET;

/*互补通道设置*/

TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;

for(uint8_t i = 0; i < member_count; i++)

{

/* 配置输出比较通道 */

HAL_TIM_OC_ConfigChannel(&TIM_StepperHandle, &TIM_OCInitStructure, step_motor[i].pul_channel);

TIM_CCxChannelCmd(MOTOR_PUL_TIM, step_motor[i].pul_channel, TIM_CCx_DISABLE);

}

}

接下来比较重要的是控制步进电机的定时器初始化,在上述代码中我们使用PMW模式2输出脉冲,具体原因已在第一象限直线插补实验中说明。

上述代码中,分别使用TIM8的通道1和通道2控制X、Y两轴步进电机,使用一个for循环配置所有用到的通道,初始化定时器配置好输出通道之后并没有立刻启动定时器输出。

第一象限逆时针圆弧插补相关参数

bsp_circular_interpolation.h-圆弧插补变量定义¶

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19 /* 坐标轴枚举 */

typedef enum{

x_axis = 0U,

y_axis

}Axis_TypeDef;

/* 圆弧插补参数结构体 */

typedef struct{

__IO int32_t startpoint_x; //起点坐标X

__IO int32_t startpoint_y; //起点坐标Y

__IO int32_t endpoint_x; //终点坐标X

__IO int32_t endpoint_y; //终点坐标Y

__IO uint32_t endpoint_pulse; //到达终点位置需要的脉冲数

__IO uint32_t active_axis; //当前运动的轴

__IO int32_t deviation; //偏差参数

__IO uint8_t motionstatus : 1; //插补运动状态

__IO uint8_t dir_x : 1; //X轴运动方向

__IO uint8_t dir_y : 1; //Y轴运动方向

}CircularInterpolation_TypeDef;

上述代码中分别定义了一个枚举和结构体,用于管理第一象限逆时针圆弧插补的相关数据参数。CircularInterpolation_TypeDef结构体的成员中,

定义了圆弧起点和终点的坐标,endpoint_pulse记录运动到终点需要多少脉冲,active_axis记录当前活动的轴,

也就是当前进给的轴,deviation就是上面讲到的Fi,用于偏差值的记录和计算。X、Y轴的运动方向和整个插补运动的状态则由一个结构体位域定义。

第一象限逆时针圆弧插补运动

bsp_circular_interpolation.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 /**

* @brief 第一象限逆圆插补运动

* @param start_x:圆弧起点相对于圆心的坐标X,增量

* @param start_y:圆弧起点相对于圆心的坐标Y,增量

* @param stop_x:圆弧终点相对于圆心的坐标X,增量

* @param stop_y:圆弧终点相对于圆心的坐标Y,增量

* @param speed:进给速度

* @retval 无

*/

void Circular_InterPolation_CCW(int32_t start_x, int32_t start_y, int32_t stop_x, int32_t stop_y, uint16_t speed)

{

/* 判断当前是否正在做插补运动 */

if(interpolation_para.motionstatus != 0)

return;

/* 检查起点、终点坐标是否在同一个圆上 */

if(((start_x * start_x) + (start_y * start_y)) != ((stop_x * stop_x) + (stop_y * stop_y)))

return;

/* 偏差清零 */

interpolation_para.deviation = 0;

/* 起点坐标 */

interpolation_para.startpoint_x = start_x;

interpolation_para.startpoint_y = start_y;

/* 终点坐标 */

interpolation_para.endpoint_x = stop_x;

interpolation_para.endpoint_y = stop_y;

/* 所需脉冲数是从起点到终点的脉冲数之和 */

interpolation_para.endpoint_pulse = abs(stop_x - start_x) + abs(stop_y - start_y);

/* 第一象限逆圆,x轴逆转,y轴正转 */

interpolation_para.dir_x = CCW;

interpolation_para.dir_y = CW;

MOTOR_DIR(step_motor[x_axis].dir_port, step_motor[x_axis].dir_pin, CCW);

MOTOR_DIR(step_motor[y_axis].dir_port, step_motor[y_axis].dir_pin, CW);

/* 起点坐标y=0,说明起点在x轴上,直接向y轴进给可减小误差 */

if(interpolation_para.startpoint_y == 0)

{

/* 第一步活动轴为Y轴 */

interpolation_para.active_axis = y_axis;

/* 计算偏差 */

interpolation_para.deviation += (2 * interpolation_para.startpoint_y + 1);

}

else

{

/* 第一步活动轴为X轴 */

interpolation_para.active_axis = x_axis;

/* 计算偏差 */

interpolation_para.deviation -= (2 * interpolation_para.startpoint_x + 1);

}

/* 设置速度 */

__HAL_TIM_SET_COMPARE(&TIM_StepperHandle, step_motor[x_axis].pul_channel, speed);

__HAL_TIM_SET_COMPARE(&TIM_StepperHandle, step_motor[y_axis].pul_channel, speed);

__HAL_TIM_SET_AUTORELOAD(&TIM_StepperHandle, speed * 2);

/* 使能主输出 */

__HAL_TIM_MOE_ENABLE(&TIM_StepperHandle);

/* 开启活动轴比较通道输出 */

TIM_CCxChannelCmd(MOTOR_PUL_TIM, step_motor[interpolation_para.active_axis].pul_channel, TIM_CCx_ENABLE);

HAL_TIM_Base_Start_IT(&TIM_StepperHandle);

interpolation_para.motionstatus = 1;

}

上述代码用来开启圆弧插补运动,并在开始运动前,对起点终点坐标、XY轴的进给方向和进给速度进行处理。

第11行:判断当前是否有插补正在运行,如果有则跳出;

第15行:判断当前传入的起点终点坐标是否在同一个圆上,如不在则跳出;

第21~28行:记录起点终点坐标,计算并记录总共需要的插补步数;

第31~34行:由于是第一象限逆时针圆弧插补,所以在插补未开始时,将XY轴的步进电机方向设置到符合第一象限逆时针;

第37~50行:判断起点坐标是否在X轴上,如果在X轴则第一步向+Y方向进给,并计算对应的偏差值,如果不在X轴则第一步向-X方向进给;

第53~55行:设置圆弧插补的进给速度;

第53~65行:开启定时器输出,开始执行插补运动。

bsp_circular_interpolation.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 /**

* @brief 定时器比较中断回调函数

* @param htim:定时器句柄指针

* @note 无

* @retval 无

*/

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

{

uint32_t last_axis = 0;

/* 记录上一步的进给活动轴 */

last_axis = interpolation_para.active_axis;

/* 根据进给方向刷新坐标 */

switch(last_axis)

{

case x_axis:

switch(interpolation_para.dir_x)

{

case CCW: interpolation_para.startpoint_x--; break;

case CW: interpolation_para.startpoint_x++; break;

}

break;

case y_axis:

switch(interpolation_para.dir_y)

{

case CCW: interpolation_para.startpoint_y--; break;

case CW: interpolation_para.startpoint_y++; break;

}

break;

}

/* 根据上一步的偏差,判断的进给方向,并计算下一步的偏差 */

if(interpolation_para.deviation >= 0)

{

/* 偏差>=0,在圆弧外侧,应向圆内进给,计算偏差 */

interpolation_para.active_axis = x_axis;

interpolation_para.deviation -= (2 * interpolation_para.startpoint_x + 1);

}

else

{

/* 偏差<0,在圆弧内侧,应向圆外进给,计算偏差 */

interpolation_para.active_axis = y_axis;

interpolation_para.deviation += (2 * interpolation_para.startpoint_y + 1);

}

/* 下一步的活动轴与上一步的不一致时,需要换轴 */

if(last_axis != interpolation_para.active_axis)

{

TIM_CCxChannelCmd(htim->Instance, step_motor[last_axis].pul_channel, TIM_CCx_DISABLE);

TIM_CCxChannelCmd(htim->Instance, step_motor[interpolation_para.active_axis].pul_channel, TIM_CCx_ENABLE);

}

/* 进给总步数减1 */

interpolation_para.endpoint_pulse--;

/* 判断是否完成插补 */

if(interpolation_para.endpoint_pulse == 0)

{

/* 关闭定时器 */

TIM_CCxChannelCmd(htim->Instance, step_motor[last_axis].pul_channel, TIM_CCx_DISABLE);

TIM_CCxChannelCmd(htim->Instance, step_motor[interpolation_para.active_axis].pul_channel, TIM_CCx_DISABLE);

__HAL_TIM_MOE_DISABLE(htim);

HAL_TIM_Base_Stop_IT(htim);

interpolation_para.motionstatus = 0;

}

}

开启定时器中断后,后续的处理都直接放到中断回调函数中进行。

第12行:记录上一步进给的活动轴;

第15~30行:这一部分用来计算上一步加工动点的坐标,先判断活动轴是X还是Y,如果是X轴就继续判断上一步是正转还是反转,

正转+1,反转-1,Y轴同理;

第34~44行:这一部分代码根据上一步的动点坐标和偏差值计算新的偏差值;

第48~51行:比较上一步进给的轴和下一步进给的轴是否一致,如果不一致,需要切换PWM输出的通道;

第55行:完成一次插补,总的进给步数就减一,这里使用了总步长法进行终点判别;

第58~65行:如果总的进给步数为0,则表示插补走到终点,关闭定时器结束插补。

整个第一象限逆时针圆弧插补算法的核心内容就在这个定时器中断回调函数中实现了,算法全部的4个步骤都在其中有所体现。

main函数

main.c-main函数¶

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 /**

* @brief 主函数

* @param 无

* @retval 无

*/

int main(void)

{

HAL_InitTick(0);

/* 初始化系统时钟为168MHz */

SystemClock_Config();

/*初始化USART 配置模式为 115200 8-N-1,中断接收*/

DEBUG_USART_Config();

printf("欢迎使用野火 电机开发板 步进电机 第一象限圆弧插补 例程\r\n");

/* LED初始化 */

LED_GPIO_Config();

/* 按键初始化 */

Key_GPIO_Config();

/*步进电机初始化*/

stepper_Init();

while(1)

{

/* 逆时针圆弧 */

if(Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)

{

Circular_InterPolation_CCW(6400 * 10, 0, 0, 6400 * 10, 1000);

}

}

}

main函数中主要就是一些外设的初始化,包括步进电机的定时器初始化。然后在while循环中轮询按键,通过按键控制步进电机做第一象限逆圆插补。

需要注意的是,由于逐点比较法的圆弧插补进给一次是一个步进脉冲,所以最后插补出来的圆弧大小跟步进电机的细分直接相关,在本实验中默认步进电机32细分,

在main函数中输入的终点坐标参数中也对应步进电机的32细分,6400个脉冲步进电机旋转一圈。

从16 万公里的领跑里程读懂国铁加速度
膀胱的位置、形态、结构及功能