前段时间为了测试实验器材的阻尼,需要去开发一套装置来测试。提出用Arduino单片机来控制电动推杆(Linear Actuator)来制造相应速度的运动,搭配上测力计,从而根据来测得阻尼,在这里简单记录一下全过程。这款电动推杆是带AB相增量式磁编码器的,但是我们也可以选择不利用编码器,即将其当作一个普通电机进行使用。本文代码可以直接拷贝使用。
目录
一、基本原理
二、不带编码器(PWM)接线
1、L289N接线
2、Arduino接线
三、带编码器接线
四、Arduino代码(不带编码器控制电动推杆速度)
五、Arduino代码(带编码器控制电动推杆速度)
六、拓展:MATLAB与Arduino连接及代码
(28条消息) 关于电机编码器的知识汇总,都在这里了!_张巧龙的博客-CSDN博客https://blog.csdn.net/best_xiaolong/article/details/114274883?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%B8%A6%E7%BC%96%E7%A0%81%E5%99%A8%E7%94%B5%E6%9C%BA&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-3-114274883.nonecase&spm=1018.2226.3001.4187一、PWM:
简单来说就是通过带一定频率的开关动作来实现调节平均电压的方法。这里通过发送数字信号(即满电压1或无电压0),这里利用方波的占空比被调制的方法对一个具体模拟信号的电平进行编码。
二、编码器:
编码器在电动推杆中是一种检测线性位移的传感器,其由电动机来驱动,可以将线性位移进行编码。
三、带编码器和不带编码器之间的区别:
不带编码器指的就是开环控制(Open-loop control),想调节电机的速度只取决于单片机给到电机的PWM信号(即脉冲信号数量和之间的间隔)。其优点是系统简单,缺点是运行效率很低,因为其总是在最大电流和0之间切换以防止失步。并且实际发现占空比设置得较小的时候,推杆速度将会不稳定。另外通过人工去测量电动推杆的运行速度,误差很大。
带编码器指的是闭环控制(Closed-loop control),也指的是PID控制。加入了AB相磁通量式编码器来作为监测器,其能够将位移转换成周期性的电信号,再将电信号转换为计数脉冲,有AB两个相可以正反向计数。单片机能够通过读取编码器输出的脉冲的上升沿和脉冲个数来测得电动推杆的转动方向和实际速度,再根据PI控制器可以算出一个将电动推杆调整到目标速度的PWM值,最后将PWM值输出给电机。这个[对比输入信号和实际信号,进行一个反馈,修正输入信号]的过程会一直循环,会形成一个闭环系统。对比不带编码器的情况,其可以不必保持最大电流,降低系统整体功耗,而且控制也会更精确,输出的速度更加稳定。
PID控制分为增量式PID和位置式PID,在本文中,指的是增量式PID(市面上大多数编码器电机),可以用于让速度动态维持在一个目标值。PID对比PWM方法去控制电机转速,其优势是能够使速度更稳定(如实际操作中,转速很难达到并保持166.5 RPM,有可能为150、170 RPM,反正就不会保持166.5 RPM,此时就需要用到一个闭环的算法(增量式PID))。并且抗干扰能力强,这也是闭环系统对比开环系统的优势。
(26条消息) STM32f4日记5之AB相编码器测速实验(TIM定时器的编码器模式使用)_@SHAWN_shawn的博客-CSDN博客_ab相霍尔编码器https://blog.csdn.net/qq_51564898/article/details/113359450
我这里采用了Arduino uno+L289N+电动推杆(电动机+编码器)的组合。接线图如下:
用Arduino和L298N控制直流电机的正反转和调速_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1eE411F7HP/?spm_id_from=autoNext&vd_source=1fcf4febef247ec78f783870e62a07d6以上这个视频从电压的角度讲了接线的问题以及功率的问题。接线顺序由左至右:
接线注释:
接线注释:
由于Arduino UNO上没有stm32上那种编码器模式,因此这里利用了MsTimer2.h的定时中断库。接线方式基本和不带编码器接线时的一致,如图所示:
接线注释:
这里有两个子程序,要用哪个,就把另外一个注释掉就好。对比stm32,类似analogWrite是封装了很多隐藏库的,因此只需要一条语句即可进行输出,无须像stm32一样配置寄存器库。
#define ENA 9 //以下三个都是输出
#define IN1 8
#define IN2 7
void setup() {
pinMode(ENA,OUTPUT);
pinMode(IN1,OUTPUT);
pinMode(IN2,OUTPUT);
Serial.begin(9600);
}
void loop() {
Pos_NegRotation(); //以下有两个子程序
PWMcontrol(); //脉宽调制控制速度从小到大
}
void Pos_NegRotation() //这个是控制电机正、反转、刹车的
{
analogWrite(ENA,80); //这个80/255即指的是占空比
digitalWrite(IN1,HIGH); //高电平
digitalWrite(IN2,LOW); //低电平
delay(2000); //暂停2秒
digitalWrite(IN1,HIGH); //高电平
digitalWrite(IN2,HIGH); //低电平
delay(2000); //暂停2秒
digitalWrite(IN1,LOW); //高电平
digitalWrite(IN2,HIGH); //低电平
delay(2000); //暂停2秒
digitalWrite(IN1,LOW); //高电平
digitalWrite(IN2,LOW); //低电平
delay(2000); //暂停2秒
}
void Pos_NegRotation() //这个是控制电机速度从小到大的
{
int i; //预定义
digitalWrite(IN1,HIGH); //预定义电机反转
digitalWrite(IN2,LOW);
for(i=0;i<=255;i++) //即占空比由小到大
{
Serial.print("Value_i ="); //以下两行为串口打印数值
Serial.println(i);
analogWrite(ENA,i); //把i写入使能口=D9
delay(20); //延时20 ms
}
digitalWrite(IN1,HIGH); //停2秒
digitalWrite(IN2,HIGH); //
delay(2000);
}
(15条消息) Arduino 让小车走直线的秘密 增量式PID 直流减速编码电机_浩浩的科研笔记的博客-CSDN博客_pid控制小车跑直线程序https://blog.csdn.net/chrnhao/article/details/112639533?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-1.pc_relevant_baidujshouduan&spm=1001.2101.3001.42421.这里参考的是上述这位博主所提供的代码,适当补上了注释,并针对UNO板子做出了调整。另外我后面尝试了一下,并没有选择引用外部的定时器库,因为发现引用了之后总是报错。
PID参数遵循:先调I,再调P。速度闭环不使用D微分项。
06_Arduino_PID教程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1xB4y1v72z/?spm_id_from=333.337.search-card.all.click&vd_source=1fcf4febef247ec78f783870e62a07d6
这个是来自清华大学电机系分享的PID教程,代码讲得很清楚。
#define ENCODER_A 2 //编码器A相引脚,在uno板子中,中断0指的是引脚2,
#define ENCODER_B 3 //编码器B相引脚,在uno板子中,中断1指的是引脚3,其可以输入外部中断信号
const unsigned int L298N_IN1=7; //IN1
const unsigned int L298N_IN2=8; //IN2
const unsigned int Motor_PWM=9; //ENA
int value; //用于存储通过PI控制器计算得到的用于调整电机转速的PWM值的整形变量
#include //定时中断头文件库
String Target_Value; //串口获取的速度字符串变量
int Velocity,Count=0; //Count计数变量 Velocity存储设定时间内A相上升沿和下降沿的个数
float Velocity_KP =7.2, Velocity_KI =0.68,Target=0;//Velocity_KP,Velocity_KI.PI参数 Target目标值
/*
Kp是让实际值保持在一个固定值左右的浮动
Ki是让固定值为目标值,即消除稳态误差
*/
int startPWM=30; //输出的PWM小于30的时候电机不转,设置初始值为30
int PWM_Restrict=255; //PWM数不能超过255
/*
*(1)计数器方向:商家给出的那一幅图的信息是:A相上升沿触发时,B相是低电平,跟A相下降沿触发时B是高电平是一个方向。将这种将count累计为正;
*(2)电机输出方向(控制电机转速方向的接线是正着接还是反着接)
*(3)PI 控制器 里面的误差(Basi)运算是目标值减当前值(Target-Encoder),还是当前值减目标值(Encoder-Target)
*三个方向只有对应上才会有效果否则你接上就是使劲的朝着一个方向(一般来说是反方向)满速旋转
*/
void READ_ENCODER_A()
{
if (digitalRead(ENCODER_A) == HIGH)
{
if (digitalRead(ENCODER_B) == LOW)
Count++; //根据另外一相电平判定方向
else
Count--;
}
else
{
if (digitalRead(ENCODER_B) == LOW)
Count--; //即两个都是LOW,根据另外一相电平判定方向
else
Count++;
}
}
void setup()
{
Serial.begin(9600); //打开串口
Serial.println("/*****开始驱动*****/");
pinMode(ENCODER_A,INPUT); //设置AB相为输入模式
pinMode(ENCODER_B,INPUT);
pinMode(L298N_IN1,OUTPUT); //设置两个驱动引脚为输出模式
pinMode(L298N_IN2,OUTPUT);
pinMode(Motor_PWM,OUTPUT); //设置使能引脚为输出模式
MsTimer2::set(5, control); //每5ms进入一个中断服务程序 control()
MsTimer2::start (); //开始计时
attachInterrupt(0, READ_ENCODER_A, CHANGE); //开启对应5号引脚的0号外部中断(在Arduino UNO中,中断0是指D2引脚,中断1是指D3引脚。其他板子并不同)
//触发方式为CHANGE,表示电平变化就会触发,即上升沿和下降沿都触发,触发的中断函数为 READ_ENCODER_A
}
void loop()
{
while(Serial.available()>0) //检测串口是否接收到了数据
{
Target_Value=Serial.readString(); //读取串口字符串
Target=Target_Value.toFloat(); //将字符串转换为浮点型,并将其赋给目标值
Serial.print("目标转速频率:"); //串口打印出设定的目标转速
Serial.println(Target);
}
Serial.println(Velocity); //可以在串口监视器里面看速度是否匀速
}
/****定时器中断触发函数****/
void control()
{
Velocity=Count; //把采用周期(内部定时中断周期)所累计的脉冲上升沿和下降沿的个数,赋值给速度
Count=0; //清零
value=Incremental_PI_A(Velocity,Target);//通过目标值和当前值在这个函数下算出我们需要调整用的PWM值
Set_PWM(value); //将算好的值输出给电机
}
/****PI控制器****/
int Incremental_PI_A (int Encoder,int Target)
{
float Bias; //定义全局浮点型变量Bias(本次偏差)
static float PWM=0,Last_bias=0; //定义全局静态浮点型变量 PWM并预设为0,Last_bias(上次偏差)并预设为0
Bias=Target-Encoder; //计算偏差,目标值减去当前值
PWM+=Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias; //增量式PI控制器计算
if(PWM>PWM_Restrict)PWM=PWM_Restrict; //限幅
if(PWM<-PWM_Restrict)PWM=-PWM_Restrict; //限幅
Last_bias=Bias; //保存上一次偏差
return PWM; //增量输出
}
/****实际控制函数****/
void Set_PWM(int motora)
{
if (motora > 0) //如果算出来的PWM为正
{
digitalWrite(L298N_IN1,HIGH); //让PWM在所设定的正转方向上调整
digitalWrite(L298N_IN2,LOW);
analogWrite(Motor_PWM, motora+startPWM);
}
else if(motora == 0) //PWM=0就停车
{
digitalWrite(L298N_IN1,LOW);
digitalWrite(L298N_IN2,LOW);
}
else if (motora < 0) //如果算出来的PWM为负
{
digitalWrite(L298N_IN1,LOW); //让PWM在所设定的反转方向上调整
digitalWrite(L298N_IN2,HIGH);
analogWrite(Motor_PWM, -motora+startPWM);
}
}
经发现,上面这个代码运行有问题,(原因应该是loop函数并不正确,一直报错exit 1,相当于缺少了loop)自己结合各个资料重新编写了一下,并且重新理解了电动推杆速度的计算方法,即在通常计算出的圈/秒速度后需要乘以导程(导程S:螺旋线绕圆柱体转一圈,沿圆柱体轴线方向移动的距离)才为电动推杆的速度。
#define ENCODER_A 2 //编码器A相引脚,在uno板子中,中断号0指的是引脚2,其可以输入外部中断信号
#define ENCODER_B 3 //编码器B相引脚,在uno板子中,中断号1指的是引脚3,其可以输入外部中断信号
#define L298N_IN1 7 //IN1
#define L298N_IN2 8 //IN2
#define Motor_PWM 9 //ENA
volatile float motor=0;//中断变量,子脉冲计数
float V=0; //速度 单位cm/s
float Target_V=20; //目标速度,单位cm/s
int PWM=0; //用于存储通过PI控制器计算得到的用于调整电机转速的PWM值的整形变量
float Velocity_KP =7.2, Velocity_KI =0.68,Target=0; //Velocity_KP,Velocity_KI.PI参数 Target目标值,这里是PI控制
/*
* Arduino初始化函数
*/
void setup()
{
Motor_Init(); //电机端口初始化
Serial.begin(9600); //打开串口
Serial.println("/*****开始驱动*****/");
}
void loop() {
Read_Motor_V();//读取脉冲计算速度ok
PWM= Incremental_PI(V,Target_V);//PI运算ok
Serial.println(V); //直接用串口绘图画出速度曲线ok
Set_Pwm(1,PWM); //设置方向,设置速度(两个方向都可以试试,最好能用之前那个方法,自动调整一下方向)ok
}
void Set_Pwm(int mode,int speed){
if(mode==1){
//正转模式,如果方向有问题,就换另外一个模式。ok
digitalWrite(L298N_IN1,LOW);
digitalWrite(L298N_IN2,HIGH);
analogWrite(Motor_PWM,speed);
}
else if(mode==2){
//反转模式
digitalWrite(L298N_IN1,HIGH);
digitalWrite(L298N_IN2,LOW);
analogWrite(Motor_PWM,speed);
}
}
/*
* 电机端口初始化函数,控制芯片引脚全部拉低
*/
void Motor_Init(){
//电动推杆
pinMode(ENCODER_A,INPUT); //左轮编码器A引脚,设置为输入模式
pinMode(ENCODER_B,INPUT); //左轮编码器B引脚,设置为输入模式
pinMode(L298N_IN1,OUTPUT); //设置两个驱动引脚为输出模式
pinMode(L298N_IN2,OUTPUT); //
pinMode(Motor_PWM,OUTPUT); //设置使能引脚为输出模式
//驱动芯片控制引脚全部拉低
digitalWrite(L298N_IN1,LOW);
digitalWrite(L298N_IN2,LOW);
digitalWrite(Motor_PWM,LOW);
}
/*********************************************************
* 增量式调速算法的C语言表达
* 函数功能:增量式PI控制器
* 入口参数:当前速度(编码器测量值),目标速度
* 返回值:电机PWM
* 参考资料:
* 增量式离散PID公式:
* Pwm-=Kp*[e(k)-e(k-1)]+Ki*e(k)+Kd*[e(k)-2e(k-1)+e(k-2)]
* e(k):本次偏差
* e(k-1):上一次偏差
* e(k-2):上上次偏差
* Pwm:代表增量输出
* 在速度闭环控制系统里面我们只使用PI控制,因此对PID公式可简化为:
* Pwm-=Kp*[e(k)-e(k-1)]+Ki*e(k)
* e(k):本次偏差
* e(k-1):上一次偏差
* Pwm:代表增量输出
* Kp是让实际值保持在一个固定值左右的浮动
* Ki是让固定值为目标值,即消除稳态误差
* 注意增量式PID先调I,再调P,最后再调D
/****PI控制器****/
int Incremental_PI(float current,float Target)
{
static float pwm,Bias,Last_bias; //定义全局静态浮点型变量 PWM并预设为0,Last_bias(上次偏差)并预设为0
Bias=Target-current; //计算偏差,目标值减去当前值
pwm+=Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias; //增量式PI控制器计算
if(pwm>250)pwm=250; //限幅250,最高为255
if(pwm<-250)pwm=-250; //限幅
Last_bias=Bias; //保存上一次偏差
return pwm; //增量输出
}
/***********************************
* 电机实际速度计算:
* 公式:
* 已知参数:
* 车轮直径65mm,
* 左边轮子一圈:360脉冲(RISING),
* 右边轮子一圈:390脉冲(RISING),
* 单位时间读两个轮子脉冲读取两个轮子脉冲
* 采用M法测速:n=M0/(C*T0),
* C为商家提供的单圈总脉冲数(商家提到的是直流电机主轴旋转一圈,在霍尔传感器每个引脚是有16个脉冲信号输出)
* T0为自己设定的时间
* M0为设定时间内的脉冲数(测量得)
* n单位为圈/每秒(这里求得的是角速度,要乘以半径才等于电动推杆的速度,半径商家给的是10mm?)
*
* 外部中断触发模式:
* LOW:低电平触发;
* CHANGE:电平变化触发;
* RISING :上升沿触发(由LOW变为HIGH);
* FALLING:下降沿触发(由HIGH变为LOW);
* HIGH:高电平触发(该中断模式仅适用于Arduino due);
*
***********************************/
void Read_Motor(){
motor++; //中断函数,读脉冲。感觉这里可以根据继续修改,即根据电平来判断是否加或者减。
}
void Read_Motor_V(){
unsigned long nowtime=0;
motor=0;
nowtime=millis()+50;//读50毫秒
attachInterrupt(digitalPinToInterrupt(ENCODER_A),Read_Motor,RISING);(改为只触发上升沿)触发方式为CHANGE,表示电平变化就会触发,即上升沿和下降沿都触发,触发的中断函数为Read_Moto_L 。ok
while(millis()
这里如果仍然跟上述代码报相同的错误,那需要关闭应用程序重新打开进行编译一下。
编译成功!
由于自己的工作开展主要都是在Matlab上,因此在其上的工作会比较顺手,而且Matlab功能强大,算力强大,若将其与单片机结合起来,将能够实现更多的工作。