文章大纲
- 前言:写作背景、硬件清单(WHEELTEC MS42CG 步进电机与 D36A 驱动板)、阅读说明
- 一、步进电机驱动的基本原理:脉冲数决定转角、脉冲频率决定转速、主控 STM32F407VET6 与通用驱动思路
- 二、关键参数:步距角与细分数:选型参数、拨码开关设置 16 细分、1.8° 步距角换算示例
- 三、接线与供电:动力线四线两相绕组、EN/DIR/ST/GND 控制线、可调直流稳压电源与电流限制
- 四、STM32CubeMX 工程配置:芯片与时钟树、定时器 OC 翻转模式、使能与方向 IO、IO 复用冲突处理
- 五、代码解析:方向与使能控制、启动与停止脉冲、比较输出中断回调、motor.c 与 motor.h 源码
- 总结:开环驱动完整链路回顾、常见坑点、后续闭环控制规划
哈喽!本人小白一个,心血来潮买了个步进电机和驱动板,打算从零开始,先让步进电机开环转起来,后续再学习用编码器读取脉冲数,做成闭环控制。其实有很多知识点想拓展的去说,但是又怕偏离了主题,所以很多地方不会太详细去介绍,后续会出其他的文章来介绍这些内容。我自己也是个小白也是在学习阶段,我觉得写文章更有利于自己的学习,欢迎各位大佬在评论区指出我的不足之处。
先介绍一下今天用到的电机:我在淘宝买的WHEELTEC MS42CG步进电机(图1),还有D36A驱动板(图2)。
(图1)
(图2)
一、步进电机驱动的基本原理:
主控我用的是STM32F407VET6单片机,不过用什么单片机其实都没关系,重要的是驱动的思路。
在我看来,要驱动步进电机,首先得明白它的工作原理。对我们这种小白来说,不需要研究得太深入,但一定要先了解个大概,有个初步的认识,这样才有利于理解后面的代码。网上有很多网课所以这里不过多介绍。
大致的思路是:单片机发送的脉冲数决定步进电机转过的角度,脉冲发送的频率决定转速。
二、关键参数:步距角与细分数:
首先来看选步进电机需要了解哪些参数。电机方面要了解步距角、相数、额定电流、保持转矩、最高空载转速等;驱动器方面要了解细分数等。这样才能适配自己的应用场景。着来说什么是细分数、什么是步距角,以及两者的关系。步距角可以通过这个公式计算。步距角就是不细分时,一个脉冲让步进电机转过的角度;细分数可以通过驱动板上的拨码开关设置(见图3),这里我设置为16细分。也就是说,把一个步距角分成16份,每个脉冲只走步距角的十六分之一。假设步距角是 1.8°,细分数是16,那么每个脉冲就走1.8°÷16 = 0.1125°。这样转一圈需要200×16 = 3200个脉冲(200 是不细分时转一圈的步数,即 360° ÷ 1.8°),刚好对应 360°。可见,细分数就相当于分辨率。
(图3)
接下来是单片机控制部分。思路是使用定时器的输出比较(OC)翻转模式来生成脉冲,再通过控制脉冲输出的频率来控制步进电机的转速。再配置电机使能和方向的IO引脚。值得注意的是一个完整的脉冲信号应该是一个方波,也就是说先输出高电平一段时间后再输出低电平,相当于两次翻转才是一个完整的脉冲信号,比如我要输出2000个脉冲数,那么我的翻转次数一个是脉冲数N*2。这点不能忽略。这里定时器的思路就是定时器计数触发中断然后翻电平,翻转电平是由我们的硬件自行翻转不需要软件再去参与了,我们只需要在回调函数里面去计数,判断是否满足我们所需要的脉冲数。满足脉冲数就结束这个停止计数,不然就执行__HAL_TIM_SET_COMPARE比较输出去重新手动装载计数。
通俗易懂的来讲就是计数器数到规定计数时产生一次中断硬件自动翻转电平,手动装填脉冲数。大概就是这么一个过程。具体代码实现如图4。
(图4)
后续我会把源码一起附上。
首先是接线图我们先说步进电机的动力缆,这个为什么是四根线呢?可以这样理解,首先这种步进电机运行的方式并不是像直流电机那样通电就能转的,而是驱动器通过交替改变两相绕组电流,驱动电机转子运转。
(图5)
然后是控制器的和单片机的接线,可以看到上面是一个2X5P的端子,我们目前只需要用到几根线,分别是EN、DIR、ST、GND这四根线,其他的线我们本次文章并不会使用,暂时空着。EN代表使能信号,DIR代表电机旋转的方向,ST是接收脉冲数的引脚。如图6
(图6)
接着就是电机的电压线了,本人本次使用的供电方案是,淘宝买的可调直流稳压电源,最大可调电压30V,电流10A。可以看到Vin就代表电源输入,Vout的话代表电源输出。输入我们是供12V的电压,电流设置根据你的拨码开关来设置。官方给的文件里没有给出具体的电流设置,但是我们可以把开关电源的电流限制再1.68A以下这样就不怕烧坏板子了。图7是电源输入输出示意图。图8为AI搜索出来的电流设置表格。
(图7)
(图8)
四、STM32CubeMX 工程配置:
接着就是使用STM32CUBEMX软件来配置整个工程
第一步我们先打开STM32CUBEMX选择我们的芯片,我这里用的时STM32F407的MCU然后选择外部高速晶振,这里的的master clock output主要是时钟输出,用来把芯片内部时钟引到IO引脚,示波器测时钟用。另外一个时跟音频有关。我们用不到所以暂时不需要深入了解。具体操作如图9
(图9)
第二步我们配置sys,debug选择serial wire我这里用的是stlink和Dlink所以选择serial wire就可以了,然后你需要用到 JTAGE的话也可以,这些都不是最主要的。具体操作如图10。
(图10)
第三步配置时钟树,时钟树的配置这里就不详细展开了。没有什么特殊要求就正常得选择外部高速晶振,然后通过倍频和分频输出168Mhz的频率。至于为什么要有时钟树这个东西,可以这么理解,时钟相当于单片机得心脏,你需要不同的频率去供给不同的外设,这样才能外设才能正常工作。具体操作如图11,图12。
(图11)
(图12)
第四步配置定时器输出脉冲,我这里选择定时器3,当然不一定非得是定时器3,时钟源选择internal clock内部时钟。然后通道1选择compare output CH1就可以了,通道2我是预留出来的打算之后控制两个步进电机。PSC和ARR我设置成83和65535,大家可以自行设置,PSC的意思就是预分频系数,像这里我们的TIM3挂载再APB1 Timer上APB1 Timer最大的时钟频率是84Mhz所以们设置为83就相当于把定时器3使用的时钟频率进行降频,变成了每1微妙进行一次计数,ARR的值就代表计数上限,这里选择计数最大值 65535,用来拓宽计数范围,降低比较值溢出的风险,我们这里使用的是OC中断输出,所以我们可以自行设置计数中断输出时间所以ARR的值因人而异,通道配置成Toggle on match模式然后极性是低电平。一定要记得打开中断。为什么要补充那么多PSC,ARR的说明呢,就是因为希望大家能更通透的去理解这些概念。只有真正理解了概念你就会明白这个为什么要这样用。以及我能不能填其他的数,会不会有影响?做到举一反三。具体操作如下图13—15。
(图13)
(图14)
(图15)
第五步配置使能和方向的IO输出控制。这一步很简单只需要先择自己喜欢的一个GPIO PIN配置为推挽输出就行了。具体操作如下图16。
(图16)
第六步保存自己的工程文件所在位置以及工程名字还有IDE,其他保持默认即可。最后点击generate code生成代码。具体操作如下图17。
(图17)
这里做一些说明补充给我一位挚友,回答一些他之前问过的问题,希望对他和大家都有一些帮助。就是在分配IO资源的时候会出现一些问题,比如我们使用cubexm的时候可以看到USART1_RX和TIM1_CH3这个通道占用了同一个IO。假设我需要同时使用到串口1和定时器3的通道在官方的datasheet里其实可以看到PE13也有定时器通道3的复用功能。这样在IO分配资源的时候我们就可以避免资源冲突的这么一个问题。假设你在使用标准库的时候我建议你可以下一个cubexm工具来先做一IO资源分配,避免重复。如果不想下载的话可以问一下AI或者自己查官方的datasheet。总之这个就是这么方便怎么来。
还有一个知识点:STM32的IO引脚有主功能和复用功能。引脚上电默认的第一功能是通用 GPIO。串口、定时器、SPI 这些都属于引脚的复用功能。很多引脚支持多路复用,可以映射给不同外设通道。比如PA9,它既可以配置成串口1的发送引脚,也可以配置成定时器1的通道引脚。我们在CubeMX选择外设的时候,CubeMX会自动帮我们选定对应的复用配置;我们把引脚从普通 GPIO 切换给定时器或者串口外设使用,这个就叫复用功能。具体操作如下图18—20。
(图18)
(图19)
(图20)
只需要先选中PE13作为定时器1通道3的输出,你在配置定时器的时候通道3会自动选中你标黄的部分。具体操作如下图21。
(图21)
五、代码解析:
接下来是代码解析首先是方向和使能GPIO控制所图22所示
(图22)
首先我们函数里面的三个参数定义分别是电机序号,要走多少步,以及运行的速度,这里运行的逻辑就是假设motor标志位等于1,计数器就清零 motor1_toggle_cnt = 0;
然后设置要走的总脉冲所需要的总计数器目标值motor1_target_toggle = (uint32_t)step_num * 2U;设置运行速度motor1_ccr = speed_ccr;
定时器CNT数值清零__HAL_TIM_SET_COUNTER(&htim3, 0); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, motor1_ccr)设置当前的速度;
电机工作标志位至1 motor1_busy = 1;定时器开始计数标志位至1 tim3_ch1_running = 1;
然后开始定时器比较输出中断HAL_TIM_OC_Start_IT(&htim3, TIM_CHANNEL_1);图下图23所示
(图23)
停止脉冲的函数图下图24所示
(图24)
定时器比较输出的中断回调函数,意思就是每次计数器计数到设定值以后就会翻转电平,产生一个中断回调函数,我们只需要在里计数然后判断是否以及输出足够的脉冲数。如果没有满足就继续输出。图下图25所示
(图25)
motor.c和motor.h代码:
#include "motor.h" static volatile uint32_t motor1_toggle_cnt = 0; static volatile uint32_t motor1_target_toggle = 0; static volatile uint16_t motor1_ccr = 1000; static volatile uint8_t motor1_busy = 0; static volatile uint16_t motor2_toggle_cnt = 0; static volatile uint16_t motor2_target_toggle = 0; static volatile uint16_t motor2_ccr = 1000; static uint8_t tim3_ch1_running = 0; //static uint8_t tim3_ch2_running = 0; #define circle 3200 void Motor_SetDir(uint8_t motor, uint8_t dir) { if(motor == 1) { HAL_GPIO_WritePin(MOTOR1_DIR_PORT, MOTOR1_DIR_PIN, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); } else if(motor == 2) { HAL_GPIO_WritePin(MOTOR2_DIR_PORT, MOTOR2_DIR_PIN, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); } } void Motor_SetEnable(uint8_t motor, uint8_t en) { if(motor == 1) { HAL_GPIO_WritePin(MOTOR1_EN_PORT, MOTOR1_EN_PIN, en ? GPIO_PIN_SET : GPIO_PIN_RESET); } else if(motor == 2) { HAL_GPIO_WritePin(MOTOR2_EN_PORT, MOTOR2_EN_PIN, en ? GPIO_PIN_SET : GPIO_PIN_RESET); } } uint8_t Motor_IsBusy(uint8_t motor) { if(motor == 1) { return motor1_busy; } return 0; } void Motor_StartRun(uint8_t motor, uint16_t step_num, uint16_t speed_ccr) { if(motor == 1) { if(motor1_busy != 0) { return; } motor1_toggle_cnt = 0; motor1_target_toggle = (uint32_t)step_num * 2U; motor1_ccr = speed_ccr; //====新增这一行!!定时器计数器清零,消除CNT随机值带来的溢出问题==== __HAL_TIM_SET_COUNTER(&htim3, 0); //现在CNT=0,设置第一次比较点 = 0 + motor1_ccr __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, motor1_ccr); motor1_busy = 1; tim3_ch1_running = 1; HAL_TIM_OC_Start_IT(&htim3, TIM_CHANNEL_1); } } void Motor_Stop(uint8_t motor) { if(motor == 1) { HAL_TIM_OC_Stop_IT(&htim3, TIM_CHANNEL_1); motor1_busy = 0; tim3_ch1_running = 0; } else if(motor == 2) { //HAL_TIM_OC_Stop_IT(&htim3, TIM_CHANNEL_2); //motor2_busy = 0; //tim3_ch2_running = 0; } } void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance != TIM3) { return; } if(tim3_ch1_running) { motor1_toggle_cnt ++; if(motor1_toggle_cnt >= motor1_target_toggle) { HAL_TIM_OC_Stop_IT(&htim3, TIM_CHANNEL_1); motor1_busy = 0; tim3_ch1_running = 0; } else { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, motor1_ccr + __HAL_TIM_GET_COUNTER(&htim3)); } } /* 电机2,需要使用再取消注释 if(tim3_ch2_running) { motor2_toggle_cnt ++; if(motor2_toggle_cnt >= motor2_target_toggle) { HAL_TIM_OC_Stop_IT(&htim3, TIM_CHANNEL_2); motor2_busy = 0; tim3_ch2_running = 0; } else { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, motor2_ccr + __HAL_TIM_GET_COUNTER(&htim3)); } } */ }#ifndef __MOTOR_H #define __MOTOR_H #include "stm32f4xx_hal.h" #define MOTOR1_DIR_PIN GPIO_PIN_10 #define MOTOR1_DIR_PORT GPIOD #define MOTOR1_EN_PIN GPIO_PIN_9 #define MOTOR1_EN_PORT GPIOD #define MOTOR2_DIR_PIN GPIO_PIN_12 #define MOTOR2_DIR_PORT GPIOB #define MOTOR2_EN_PIN GPIO_PIN_13 #define MOTOR2_EN_PORT GPIOB extern TIM_HandleTypeDef htim3; #define MICRO_DIV 16 // 细分数,拨码是多少改这里 #define MAX_SPEED_RPM 400.0f //最大转速限制 void Motor_SetDir(uint8_t motor, uint8_t dir); void Motor_SetEnable(uint8_t motor, uint8_t en); uint8_t Motor_IsBusy(uint8_t motor); void Motor_StartRun(uint8_t motor, uint16_t step_num,uint16_t speed_ccr); uint16_t Speed_Rpm2Ccr(float speed_rpm, uint16_t micro); void motor_set_angle(uint16_t angle, uint16_t speed_rpm, uint8_t DIR); void Motor_Stop(uint8_t motor); #endif总结:
最后是对这一篇文章的总结,本次文章是小班在上班的时候一边上班一边写出来的,耗时一周才写完的,后续也会分享更多的学习记录。感谢大家的观看。