本文尝试用Arduino开发版控制42步进电机,搭配通用的步进电机驱动器,实现对步进电机的转速控制和方向控制。
原材料:
- Arduino开发板及附件
- 42步进电机和配套驱动器
- 电源
- 接线方式:共阴
总览
2、42步进电机
可以看到这个是四线步进电机,内部两两短接,可以通过万用表测出,相同相的线随意接入驱动器的A+,A-和B+,B-即可。
3、驱动器
驱动器侧面有一排按钮,往上拨为OFF,往下为ON,其中看驱动器界面标识可见, SW1-SW3:为步进电机驱动器电流控制按钮,电流越大步进电机就越有劲,不容易丢步。
SW4:是控制限时电流按钮,即电机不转时是否给电机电流按钮,据说开启OFF可以更好的保护电机,但是为了更好地控制步进电机建议打开。
SW5-SW8:是控制电机精度的,Pulsw/rev表示的是多少个脉冲为一圈,数字越小即转的越快,数字越高越精确转的越慢。
PUL+,PUL-:步数控制,输入脉冲信号,一个脉冲(一高一低)走一步。
DIR+,DIR-:方向控制,接入高电平即正转,接入低电平则反转。
ENA+,ENA-:使能控制,一般情况悬空(都不接)即可,接入高电平步数控制失效,可以手动转动,当接入低电平恢复原有的步数控制,此时无法手动转动。
4、程序代码
保证线路连接正确的情况下,以下程序就能简单控制步进电机的运动,如果要精确控制等更精确的控制,或者用库控制的话需要更深入的研究。
代码如下:
#define STEPPIN 9
#define DIRPIN 8
//方向位为8,脉冲位为9
void setup() {
pinMode(STEPPIN, OUTPUT);
pinMode(DIRPIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
// Enables the motor to move in a particular direction
Serial.println("Forward Begins");
digitalWrite(DIRPIN, HIGH);
// 正向转1圈(200脉冲)
for (int x = 0; x < 200; x ++) {
digitalWrite(STEPPIN, HIGH);
delayMicroseconds(500);
digitalWrite(STEPPIN, LOW);
delayMicroseconds(500);
}
Serial.println("Forward Ends");
delay(1000); // Delay for one second
// Changes the rotation direction or rotates in opposite direction
Serial.println("Backward Begins");
digitalWrite(DIRPIN, LOW);
// 反向转3圈(600脉冲)
for (int x = 0; x < 600; x ++) {
digitalWrite(STEPPIN, HIGH);
delayMicroseconds(500);
digitalWrite(STEPPIN, LOW);
delayMicroseconds(500);
}
Serial.println("Backward Ends");
delay(2000); //Delay for two seconds
}
PID调速代码:
// 导入PWM库,此库可自定义PWM的频率和占空比(除了timer0相关的端口,因为timer0是负责dealay等函数的,因此不能去改动timer0)
#include <PWM.h>
// 导入 PID 库的头文件
#include <PID_v1.h>
// 导入 定时中断 库的头文件,这个库用的是timer2,因此这个程序的其他地方不能对timer2进行修改,否则会影响这个库的功能
#include <MsTimer2.h>
// ↓↓↓↓↓↓↓↓↓ 变量定义与初始化:PID 控制部分 ↓↓↓↓↓↓↓↓↓//
// 定义三个变量,分别代表 PID 控制器的期望输入值、实际输入值、控制量
double Setpoint = 6; // 希望电机的转速为 6rad/s
double Input;
double Output;
// 初始化 PID 的三个参数
double kp = 2, ki =15, kd = 1;
//double kp = 11.46, ki =98.22, kd = 0.33;
// 创建一个 PID 控制器的实例
PID myPID(&Input, &Output, &Setpoint, kp, ki, kd, DIRECT);
// ↑↑↑↑↑↑↑↑↑ 变量定义与初始化:PID 控制部分 ↑↑↑↑↑↑↑↑↑//
// ↓↓↓↓↓↓↓↓↓ 变量定义与初始化:PID 自整定部分 ↓↓↓↓↓↓↓↓↓//
double OutputHigh,OutputLow;
double outputStep = 30 ; //必须是正值
double OutputUsedinPIDAutotune = 0;
double inputHistory[20];
int caculateFlag = 1;
int inputHighAll = 0;
int inputHighNum = 0;
int inputLowAll = 0;
int inputLowNum = 0;
int atemp, btemp;
int if_inputHistory_or_not = 0; // 决定是否开始PID自整定过程中的对Input的历史记录
// ↑↑↑↑↑↑↑↑↑ 变量定义与初始化:PID 自整定部分 ↑↑↑↑↑↑↑↑↑//
// ↓↓↓↓↓↓↓↓↓ 变量定义与初始化:电机测速部分 ↓↓↓↓↓↓↓↓↓//
int count = 0; // 记录在指定时间段内,编码器码盘脉冲值
double omega = 0; //电机转速 rad/s
unsigned long old_time = 0; // 时间标记
double measure_sample_time = 100; // 测速周期,单位:毫秒
// ↑↑↑↑↑↑↑↑↑ 变量定义与初始化:电机测速部分 ↑↑↑↑↑↑↑↑↑//
// ↓↓↓↓↓↓↓↓↓ 变量定义与初始化:MATLAB绘图部分 ↓↓↓↓↓↓↓↓↓//
// 当这个变量为0的时候,串口会显示最详细的数据,此时不宜用MATLAB绘图
// 当这个变量不为0的时候,串口会只显示一些关键的数据,共1列,每4个数据为1组,第一个是当前的实际转速(rad/s),第二个是要求的转速(rad/s),第三个是当前实际的占空比,第四个是当前的时间(毫秒),此时宜用MATLAB绘图
int Use_MATLAB_to_Draw_or_not = 0;
// ↑↑↑↑↑↑↑↑↑ 变量定义与初始化:MATLAB绘图部分 ↑↑↑↑↑↑↑↑↑//
// ↓↓↓↓↓↓↓↓↓ 变量定义与初始化:XXX部分 ↓↓↓↓↓↓↓↓↓//
// ↑↑↑↑↑↑↑↑↑ 变量定义与初始化:XXX部分 ↑↑↑↑↑↑↑↑↑//
// 自定义函数:外部中断起作用时的中断处理函数,(当端口收到测速信号线(FG线)的下降沿信号时,会有一个外部中断,而中断处理函数就在此定义) ↓↓↓↓↓↓↓↓↓
void Code()
{
//每中断一次,记录一次
count += 1; // 编码器码盘计数加一
}
// 自定义函数:定时中断起作用时的中断处理函数,用于计算电机转速 ↓↓↓↓↓↓↓↓↓
void omega_measure()
{
detachInterrupt(0); // 关闭外部中断0
// Serial.print("time: ");Serial.print(millis());Serial.println("毫秒");
// Serial.print("old_time: ");Serial.print(old_time);Serial.println("毫秒");
//把measure_sample_time毫秒内编码器码盘计得的脉冲数,换算为当前转速值
//转速单位 rad/s。这个编码器码盘为2044个空洞。 (2044这个数字是标定过的 我自己标定的)
//电机转速,其中 count/2044 计算的是在measure_sample_time毫秒之内,转了多少圈,“2*3.141592654”是为了转换为rad制
omega =(double)count/2044*2*3.141592654*1000/measure_sample_time;
// 记录被调量Input(即 omega)的历史值,20个
if (if_inputHistory_or_not == 1)
{
for (int i = 1; i < 20; i++)
{
inputHistory[20 - i] = inputHistory[20 - i - 1];
}
inputHistory[0] = omega;
}
//在串口监视器中输出一些结果
if (Use_MATLAB_to_Draw_or_not == 0)
{
// 输出详细数据
Serial.println(" ");
Serial.print("期望转速: ");Serial.print(Setpoint);Serial.println("rad/s");
Serial.print("实际转速: ");Serial.print(omega);Serial.println("rad/s");
Serial.print("PID自整定震荡前或者后的占空比值: ");Serial.println(Output);
Serial.print("PID自整定震荡中的占空比值: ");Serial.println(OutputUsedinPIDAutotune);
Serial.print("当前系统时间:");Serial.print(millis());Serial.println("毫秒");
Serial.println(" ");
}
else
{
// 输出简单数据
Serial.println(omega);
Serial.println(Setpoint);
Serial.println(Output);
Serial.println(millis());
}
//恢复到编码器测速的初始状态
count = 0; //把脉冲计数值清零,以便计算下一个measure_sample_time毫秒的脉冲计数
old_time= millis(); // 记录每次测速时的时间节点
attachInterrupt(0, Code,FALLING); // 重新开放外部中断0
}
void setup() {
// put your setup code here, to run once:
// 2口接FG信号测速线,另外,2口是外部中断口,中断接口是in.0(Ardunio mega2560 自带的特性)
pinMode(2, INPUT);
//电机的编码器脉冲中断函数, 当编码器码盘的脉冲信号下跳沿的时候,中断一次(FALLING),中断函数是Code,中断接口是in.0,对应pin2口
attachInterrupt(0, Code, FALLING);
// 对定时中断进行设置,每 measure_sample_time毫秒 进入一次定时中断,并调用测速函数(即:每measure_sample_time毫秒测一次速)
MsTimer2::set(measure_sample_time, omega_measure);
// 开启定时中断
MsTimer2::start();
// ↓↓↓↓↓↓↓↓↓设置PWM的频率,与pin口 ↓↓↓↓↓↓↓↓↓
InitTimersSafe();
// 在 mega2560 上,12口属于timer1,不会与定时中断库“MsTimer2”冲突
bool success = SetPinFrequencySafe(12, 10000);
if(success)
{
// PWM 信号是从 12 口输出的
pinMode(12, OUTPUT);
}
// ↑↑↑↑↑↑↑↑↑设置PWM的频率,与pin口 ↑↑↑↑↑↑↑↑↑
// 打开串口监视器
Serial.begin(9600); //串口波特率为9600
//打开 PID 控制器
myPID.SetMode(AUTOMATIC);
// 设置控制量的范围,在此应用中,控制量是占空比,范围是0~255
myPID.SetOutputLimits(0,255);
// 设置计算采样周期
myPID.SetSampleTime(1);
// 初始化 inputHistory 数组
for (int i = 0; i < 20; i++)
{
inputHistory = 0;
}
}
void loop()
{
// 主程序,将重复运行:
// loop()每执行一次就将当前速度传给Input
Input = omega;
if (Use_MATLAB_to_Draw_or_not == 0)
{ // 做详细计算、PID自整定等
if (millis() < 25000)
{
// 当运行时间小于25秒,则进行普通的 PID 控制,并且 以PID控制器计算得到的占空比运转 (稳定为先)
myPID.Compute();
pwmWrite(12, Output);
}
// 当运行时间大于25秒,且小于30秒,则 PID 进行自整定 (强行震荡)
else if(millis() < 30000)
{ if_inputHistory_or_not = 1; // 打开“记录被调量Input(即 omega)的历史值”
// Output包含的是稳定值
OutputLow = Output - outputStep;
OutputHigh = Output + outputStep;
if (Input < Setpoint)
{
OutputUsedinPIDAutotune = OutputHigh;
if (int(OutputUsedinPIDAutotune) > 255) {OutputUsedinPIDAutotune = 255;}
}
else if (Input > Setpoint)
{
OutputUsedinPIDAutotune = OutputLow;
if (int(OutputUsedinPIDAutotune) < 0) {OutputUsedinPIDAutotune = 0;}
}
pwmWrite(12, OutputUsedinPIDAutotune);
}
// 震荡结束后,继续 PID 自整定 (分析波形 (即数组inputHistory[]的波形) 计算自整定后的PID参数)
else if(millis() < 35000)
{ // 先关闭电机
pwmWrite(12, 0);
// 关闭“记录被调量Input(即 omega)的历史值”
if_inputHistory_or_not = 0;
while (caculateFlag == 1)
{
for (int i = 1; i < 19; i++)
{
//如果是波峰
if (inputHistory > inputHistory[i - 1] && inputHistory > inputHistory[i + 1])
{
inputHighAll += inputHistory; //波峰值总数增加
inputHighNum++; //波峰个数计数增加
if (inputHighNum == 1) atemp = i; //当产生第一个波峰的时候,atemp记录下此时是第几个数据(每个数据相隔100ms)
btemp = i; //当对20个历史数据分析完,btemp则可记录下最后一个波峰对应的是第几个数据
}
//如果是波谷,类似上面
else if (inputHistory < inputHistory[i - 1] && inputHistory < inputHistory[i + 1])
{
inputLowAll += inputHistory;
inputLowNum++;
}
}
double autoTuneA = (inputHighAll / inputHighNum) - (inputLowAll / inputLowNum); //峰峰值(即波峰值 - 波谷值)的平均数A
double autoTunePu = (btemp - atemp) * measure_sample_time/1000 / (inputHighNum - 1); //两个波峰之间的时间间隔Pu(s)
double Ku = 4 * outputStep / (autoTuneA * 3.14159);
double Kp_self = 0.6 * Ku;
double Ki_self = 1.2 * Ku / autoTunePu;
double Kd_self = 0.075*Ku*autoTunePu;
// 暂时关闭定时中断
MsTimer2::stop();
// 在串口监视器输出自整定的最终结果
Serial.println(" ");
Serial.println("******************************");
Serial.println("PID 自整定过程完成!");
Serial.println("自整定的PID参数为: ");
Serial.print("Kp_self: ");Serial.println(Kp_self);
Serial.print("Ki_self: ");Serial.println(Ki_self);
Serial.print("Kd_self: ");Serial.println(Kd_self);
Serial.println(" ");
Serial.print("震荡过程中的峰峰值 A: ");Serial.print(autoTuneA);Serial.println("rad/s");
Serial.print("两个波峰之间的时间间隔Pu: ");Serial.print(autoTunePu);Serial.println("秒");
Serial.println("******************************");
Serial.println(" ");
// 上述消息显示5秒
delay(5000);
// 把自整定的PID参数导入PID控制器中
myPID.SetTunings(Kp_self, Ki_self, Kd_self);
//
Serial.println("已经将自整定的PID参数导入PID控制器中");
Serial.println("即将以自整定的PID参数进行全新的控制,请等待10秒......");
// 在延时10秒
delay(10000);
caculateFlag = 0;
// 重新开启定时中断
MsTimer2::start();
}
}
// 从速度0开始,以自整定的PID参数进行全新的控制
else
{
if (caculateFlag = 0)
{
Output = 255;
Input = 0;
myPID.Compute();
pwmWrite(12, Output);
omega = 0;
caculateFlag = 2;
old_time= millis();
}
else
{
myPID.Compute();
pwmWrite(12, Output);
}
}
}
else
{
// 只在一组PID参数下运算
myPID.Compute();
pwmWrite(12, Output);
}
}
//转角θ=-ANcos(wt),转速V=ANwsin(wt)
float w=3;
int N=100; //N是半个周期的脉冲数,正比于正弦函数的振幅
//如果乘积Nw太大,步进电机就会丢步
float dt[400]={0}; //脉冲的时间间隔
int k;
const byte pinSPEED=5;
const byte pinDIREC=6;
void setup() {
pinMode(pinSPEED,OUTPUT); // 5号引脚发送PULSE(控制速度)
pinMode(pinDIREC,OUTPUT); // 6号引脚指定SIGN (控制方向)
int dtMAX=30;
float t=0;
for(k=1;k<=N;k++)
{dt[k]=(1.0F/sin(k*PI/(N+1))> dtMAX ? dtMAX : 1.0F/sin(k*PI/(N+1)));
//如果两个脉冲的时间间隔超过预设的dtMAX,就认为它是dtMAX
//dtMAX的值可以根据需要自行修改
t=t+dt[k];}
//for循环结束后,t代表数组dt的前N项的和
for(k=1;k<=N;k++)
{dt[k]=PI*dt[k]/(w*t);}
}
void loop() {
digitalWrite(pinDIREC,HIGH); //一个方向运动
for(k=1;k<=N;k++)
{digitalWrite(pinSPEED,HIGH);
digitalWrite(pinSPEED,LOW);
delayMicroseconds(1E6*dt[k]);}
digitalWrite(pinDIREC,LOW); //反方向运动
for(k=1;k<=N;k++)
{digitalWrite(pinSPEED,HIGH);
digitalWrite(pinSPEED,LOW);
delayMicroseconds(1E6*dt[k]);}
}