يمكن أن تكون الحركة المتزامنة بين المشغلات الخطية المتعددة أمرًا حيويًا لنجاح بعض تطبيقات العملاء ، أحدها المشترك هو مشغلان خطيان يفتحان بابًا. من أجل تحقيق ذلك نوصي باستخدام Firgelli المخصص صندوق التحكم المتزامن FA-SYNC-2 و FA-SYNC-4. ومع ذلك ، يفضل بعض DIYers والمتسللين الحرية التي يوفرها متحكم دقيق مثل Arduino ويفضلون بدلاً من ذلك كتابة برنامج التحكم المتزامن الخاص بهم. يهدف هذا البرنامج التعليمي إلى تقديم نظرة عامة حول كيفية تحقيق ذلك باستخدام سلسلة المحرك الخطي البصري.
مقدمة
هذا البرنامج التعليمي ليس معالجة صارمة للخطوات المطلوبة لتحقيق تحكم متزامن مع Arduino ، بل نظرة عامة واسعة لمساعدتك في كتابة برنامجك المخصص. هذا البرنامج التعليمي متقدم ويفترض أنك على دراية بأجهزة Arduino وبرامجها ، ولديك خبرة مثالية في إشارات تعديل عرض النبض (PWM) وروتين خدمة المقاطعة (ISR) وإلغاء أجهزة الاستشعار وأجهزة ترميز المحركات. المثال المقدم في هذا البرنامج التعليمي هو وحدة تحكم تناسبية بدائية. يمكن تنفيذ العديد من التحسينات في المثال التالي بما في ذلك ، على سبيل المثال لا الحصر: تنفيذ حلقة تحكم PID والقياس إلى أكثر من مشغلين خطيين. يرجى العلم أنه ليس لدينا الموارد اللازمة لتقديم الدعم الفني لتطبيقات Arduino ولن نقوم بتصحيح أو تحرير أو توفير تعليمات برمجية أو مخططات الأسلاك خارج هذه البرامج التعليمية المتاحة للجمهور.
نظرة عامة التحكم المتزامن
يتم تحقيق التحكم المتزامن من خلال مقارنة طول مشغلين خطيين وضبط السرعة بشكل متناسب ؛ إذا بدأ أحد المشغلات في التحرك بشكل أسرع من الآخر ، فسنبطئه. يمكننا قراءة موضع المشغل الخطي عبر المشفر البصري المدمج. المشفر البصري عبارة عن قرص بلاستيكي صغير به 10 فتحات متصل بمحرك التيار المستمر بحيث يعمل القرص البلاستيكي أيضًا عندما يدور المحرك. يتم توجيه مؤشر LED بالأشعة تحت الحمراء نحو القرص البلاستيكي بحيث ينتقل الضوء أثناء دورانه إما من خلال الفتحات الموجودة في القرص البصري أو حجبه بواسطة بلاستيك القرص. يكتشف مستشعر الأشعة تحت الحمراء الموجود على الجانب الآخر من القرص عندما ينتقل الضوء عبر الفتحة ويخرج إشارة موجة مربعة. من خلال حساب عدد النبضات التي يكتشفها المستقبل ، يمكننا حساب RPM للمحرك والمسافة التي قطعها المشغل الخطي. يحتوي المحرك الخطي البصري 35 رطلاً على 50 (+/- 5) نبضة بصرية لكل بوصة من السفر بينما يحتوي كلا المشغلين 200 رطل و 400 رطل على 100 (+/- 5) نبضة في البوصة. بمقارنة المسافة التي امتدها كل مشغل خطي ، يمكننا ضبط سرعة المشغلين بشكل متناسب بحيث يظلان دائمًا على نفس الطول أثناء التمديد.
المكونات المطلوبة
- اثنين من المحركات الخطية البصرية
- اثنان من سائقي المحركات IBT-2
- اردوينو اونو
- مصدر طاقة 12 فولت
- 3 أزرار مؤقتة (لا تباع من قبل Firgelli)
- أسلاك إضافية
الاسلاك الرسم البياني
قم بعمل توصيلات الأسلاك أعلاه. تحقق دائمًا من ألوان الأسلاك الخارجة من المشغل الخطي حيث قد تتغير اتفاقية التلوين عما هو موضح في الرسم التخطيطي أعلاه.
برنامج تعليمي سريع
إذا كنت ترغب فقط في تحريك المشغلين الخطيين بشكل متزامن ، فما عليك سوى اتباع الخطوات التالية:
- قم بإجراء التوصيلات كما هو موضح في مخطط الأسلاك.
- قم بتحميل وتشغيل البرنامج الأول أدناه.
- انسخ ناتج القيمتين بواسطة هذا البرنامج في السطر 23 من البرنامج الثاني أدناه.
- قم بتحميل وتشغيل البرنامج الثاني.
- قم بضبط النظام الخاص بك عن طريق تغيير المتغير K_p (السطر 37 ، البرنامج الثاني). يتم القيام بذلك بسهولة عن طريق إرفاق مقياس الجهد بالدبوس التناظري A0 وتعديل الشيء في الكود لقراءة مقياس الجهد واستخدام وظيفة map (): K_p = map (analogRead (A0) ، 0 ، 1023 ، 0 ، 20000) ؛
سيتناول الجزء المتبقي من هذا البرنامج التعليمي بمزيد من التفصيل بعض الميزات الرئيسية للبرامج. نكرر مرة أخرى أن هذا ليس برنامجًا تعليميًا شاملاً ، بل نظرة عامة على الأشياء التي يجب مراعاتها عند إنشاء برنامجك الخاص.
نظرة عامة على برنامج المعايرة
قبل أن يتم تحقيق التحكم المتزامن ، يجب علينا أولاً معايرة النظام. يتضمن ذلك حساب عدد النبضات لكل دورة تشغيل لأنه كما هو مذكور في مواصفات المنتج ، هناك تفاوت (+/- 5) نبضات لكل بوصة من السفر. قم بتحميل وتشغيل البرنامج أدناه. سيقوم هذا البرنامج بسحب المشغلات بالكامل (السطر 53) وضبط متغير عداد النبض البصري على الصفر ، ثم يتم تمديده بالكامل وسحبه بالكامل (الخط 63 و 74 ، على التوالي) خلال دورة التشغيل هذه ، سيتم حساب عدد النبضات بواسطة روتين خدمة المقاطعة (ISR) ، السطر 153 و 166. بمجرد اكتمال دورة التشغيل ، سيتم إخراج متوسط عدد النبضات ، السطر 88 ، قم بتدوين هذه القيم في وقت لاحق.
https://gist.github.com/Will-Firgelli/89978da2585a747ef5ff988b2fa53904
/* Written by Firgelli Automations
* Limited or no support: we do not have the resources for Arduino code support
* This code exists in the public domain
*
* Program requires two (or more) of our supported linear actuators:
* FA-OS-35-12-XX
* FA-OS-240-12-XX
* FA-OS-400-12-XX
* Products available for purchase at https://www.firgelliauto.com/collections/linear-actuators/products/optical-sensor-actuators
*/
#include <elapsedMillis.h>
elapsedMillis timeElapsed;
#define numberOfActuators 2
int RPWM[numberOfActuators]={6, 11}; //PWM signal right side
int LPWM[numberOfActuators]={5,10};
int opticalPins[numberOfActuators]={2,3}; //connect optical pins to interrupt pins on Arduino. More information: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
volatile long lastDebounceTime_0=0; //timer for when interrupt was triggered
volatile long lastDebounceTime_1=0;
int Speed = 255; //choose any speed in the range [0, 255]
#define falsepulseDelay 20 //noise pulse time, if too high, ISR will miss pulses.
volatile int counter[numberOfActuators]={};
volatile int prevCounter[numberOfActuators]={};
int Direction; //-1 = retracting
// 0 = stopped
// 1 = extending
int extensionCount[numberOfActuators] = {};
int retractionCount[numberOfActuators] = {};
int pulseTotal[numberOfActuators]={}; //stores number of pulses in one full extension/actuation
void setup(){
for(int i=0; i<numberOfActuators; i++){
pinMode(RPWM[i],OUTPUT);
pinMode(LPWM[i], OUTPUT);
pinMode(opticalPins[i], INPUT_PULLUP);
counter[i]=0; //initialize variables as array of zeros
prevCounter[i]=0;
extensionCount[i] = 0;
retractionCount[i] = 0;
pulseTotal[i] = 0;
}
attachInterrupt(digitalPinToInterrupt(opticalPins[0]), count_0, RISING);
attachInterrupt(digitalPinToInterrupt(opticalPins[1]), count_1, RISING);
Serial.begin(9600);
Serial.println("Initializing calibration");
Serial.println("Actuator retracting...");
Direction = -1;
moveTillLimit(Direction, 255);
Serial.println("Actuator fully retracted");
delay(1000);
for(int i=0; i<numberOfActuators; i++){
Serial.print("\t\t\t\tActuator ");
Serial.print(i);
}
Direction = 1;
moveTillLimit(Direction, 255); //extend fully and count pulses
Serial.print("\nExtension Count:");
for(int i=0; i<numberOfActuators; i++){
extensionCount[i]=counter[i];
Serial.print("\t\t");
Serial.print(extensionCount[i]);
Serial.print("\t\t\t");
}
delay(1000);
Direction = -1;
moveTillLimit(Direction, 255); //retract fully and count pulses
Serial.print("\nRetraction Count:");
for(int i=0; i<numberOfActuators; i++){
retractionCount[i]=counter[i];
Serial.print("\t\t");
Serial.print(abs(retractionCount[i]));
Serial.print("\t\t\t");
}
Serial.print("\n");
for(int i=0; i<numberOfActuators; i++){
Serial.print("\nActuator ");
Serial.print(i);
Serial.print(" average pulses: ");
pulseTotal[i]=(extensionCount[i]+abs(retractionCount[i]))/2; //takes the average of measurements
Serial.print(pulseTotal[i]);
}
Serial.println("\n\nEnter these values in the synchronous control progam.");
}
void loop() {
}
void moveTillLimit(int Direction, int Speed){
//this function moves the actuator to one of its limits
for(int i = 0; i < numberOfActuators; i++){
counter[i] = 0; //reset counter variables
prevCounter[i] = 0;
}
do {
for(int i = 0; i < numberOfActuators; i++) {
prevCounter[i] = counter[i];
}
timeElapsed = 0;
while(timeElapsed < 200){ //keep moving until counter remains the same for a short duration of time
for(int i = 0; i < numberOfActuators; i++) {
driveActuator(i, Direction, Speed);
}
}
} while(compareCounter(prevCounter, counter)); //loop until all counts remain the same
}
bool compareCounter(volatile int prevCounter[], volatile int counter[]){
//compares two arrays and returns false when every element of one array is the same as its corresponding indexed element in the other array
bool areUnequal = true;
for(int i = 0; i < numberOfActuators; i++){
if(prevCounter[i] == counter[i]){
areUnequal = false;
}
else{ //if even one pair of elements are unequal the entire function returns true
areUnequal = true;
break;
}
}
return areUnequal;
}
void driveActuator(int Actuator, int Direction, int Speed){
int rightPWM=RPWM[Actuator];
int leftPWM=LPWM[Actuator];
switch(Direction){
case 1: //extension
analogWrite(rightPWM, Speed);
analogWrite(leftPWM, 0);
break;
case 0: //stopping
analogWrite(rightPWM, 0);
analogWrite(leftPWM, 0);
break;
case -1: //retraction
analogWrite(rightPWM, 0);
analogWrite(leftPWM, Speed);
break;
}
}
void count_0(){
//This interrupt function increments a counter corresponding to changes in the optical pin status
if ((millis() - lastDebounceTime_0) > falsepulseDelay) { //reduce noise by debouncing IR signal
lastDebounceTime_0 = millis();
if(Direction==1){
counter[0]++;
}
if(Direction==-1){
counter[0]--;
}
}
}
void count_1(){
if ((millis() - lastDebounceTime_1) > falsepulseDelay) {
lastDebounceTime_1 = millis();
if(Direction==1){
counter[1]++;
}
if(Direction==-1){
counter[1]--;
}
}
}
نظرة عامة على البرنامج المتزامن
قبل تحميل برنامج التحكم المتزامن ، يجب عليك أولاً نسخ القيم الناتجة بواسطة برنامج المعايرة في السطر 23 واستبدال المصفوفة الحالية: {908 ، 906} بقيمك الخاصة. بالإضافة إلى ذلك ، إذا كنت تستخدم المشغل الخطي 35 رطلاً ، فستحتاج إلى تغيير قيمة المتغير في السطر 29 من 20 مللي ثانية إلى 8 مللي ثانية.
بعد التراجع الكامل مرة واحدة (لتحديد الأصل) ، يمكنك تحريك كلا المشغلين الخطيين بشكل متزامن بالضغط على الأزرار الثلاثة المقابلة لأوامر التمديد والسحب والإيقاف. ستبقى المشغلات متزامنة حتى في ظل الأحمال غير المتساوية من خلال مقارنة عدادات النبض النسبية الخاصة بها وضبط السرعة بينها لتبقى متزامنة دائمًا. يرجى العلم أن البرنامج الحالي يطبق وحدة تحكم تناسبية بسيطة ، السطر 93 ، على هذا النحو يخضع للتجاوز والتذبذب حول التوازن. يمكنك ضبط هذا بتغيير المتغير K_p المحدد في السطر 37. يتم القيام بذلك بسهولة عن طريق إرفاق مقياس الجهد بالدبوس التناظري A0 وتعديل الشيء في الكود لقراءة مقياس الجهد واستخدام وظيفة map (): K_p = map (analogRead (A0) ، 0 ، 1023 ، 0 ، 20000) ؛
للحصول على أفضل النتائج ، نقترح بشدة إزالة وحدة التحكم التناسبية وتنفيذ حلقة تحكم PID ؛ لكن هذا خارج نطاق هذا البرنامج التعليمي التمهيدي وقد تم حذفه عمدًا.
https://gist.github.com/Will-Firgelli/44a14a4f3cac3209164efe8abe3285b6
/* Written by Firgelli Automations
* Limited or no support: we do not have the resources for Arduino code support
* This code exists in the public domain
*
*/
#include <elapsedMillis.h>
elapsedMillis timeElapsed;
#define numberOfActuators 2
int downPin = 7;
int stopPin = 8;
int upPin = 9;
int RPWM[numberOfActuators]={6, 11}; //PWM signal right side
int LPWM[numberOfActuators]={5,10};
int opticalPins[numberOfActuators]={2,3}; //connect optical pins to interrupt pins on Arduino. More information: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
volatile unsigned long lastDebounceTime[numberOfActuators]={0,0}; //timer for when interrupt is triggered
int pulseTotal[numberOfActuators]={908, 906}; //values found experimentally by first running two-optical-actuators-sync-calibration.ino
int desiredSpeed=255;
int adjustedSpeed;
int Speed[numberOfActuators]={};
#define falsepulseDelay 20 //noise pulse time, if too high, ISR will miss pulses. If using 35lb actuator, set to 8ms
volatile int counter[numberOfActuators]={};
volatile int prevCounter[numberOfActuators]={};
volatile float normalizedPulseCount[numberOfActuators]={};
int Direction; //-1 = retracting
// 0 = stopped
// 1 = extending
float error;
int K_p=12000; //optimized experimentally. adjust this to fine tune your system
int laggingIndex, leadingIndex; //index of the slowest/fastest actuator
void setup(){
pinMode(stopPin, INPUT_PULLUP);
pinMode(downPin, INPUT_PULLUP);
pinMode(upPin, INPUT_PULLUP);
for(int i=0; i<numberOfActuators; i++){
pinMode(RPWM[i],OUTPUT);
pinMode(LPWM[i], OUTPUT);
pinMode(opticalPins[i], INPUT_PULLUP);
Speed[i]=desiredSpeed;
}
attachInterrupt(digitalPinToInterrupt(opticalPins[0]), count_0, RISING);
attachInterrupt(digitalPinToInterrupt(opticalPins[1]), count_1, RISING);
Serial.begin(9600);
Serial.println("Calibrating the origin");
Serial.println("Actuator retracting...");
Direction = -1;
moveTillLimit(Direction, 255);
for(int i=0; i<numberOfActuators; i++){
counter[i]=0; //reset variables
prevCounter[i]=0;
normalizedPulseCount[i] = 0;
}
delay(1000);
Serial.println("Actuator fully retracted");
}
void loop() {
checkButtons();
if(Direction==1){ //based on direction of motion identify the leading and lagging actuator by comparing pulse counts
if(normalizedPulseCount[0] < normalizedPulseCount[1]){
laggingIndex = 0;
leadingIndex = 1;
}
else{
laggingIndex = 1;
leadingIndex = 0;
}
}
else if(Direction==-1){
if(normalizedPulseCount[0] > normalizedPulseCount[1]){
laggingIndex = 0;
leadingIndex = 1;
}
else{
laggingIndex = 1;
leadingIndex = 0;
}
}
error=abs(normalizedPulseCount[laggingIndex]-normalizedPulseCount[leadingIndex]);
if(Direction!=0){
adjustedSpeed=desiredSpeed-int(error*K_p);
Speed[leadingIndex]=constrain(adjustedSpeed, 0, 255); //slow down fastest actuator
Speed[laggingIndex]=desiredSpeed;
}
for(int i=0; i<numberOfActuators; i++){
Serial.print(" ");
Serial.print(Speed[i]);
Serial.print(" ");
Serial.print(normalizedPulseCount[i]*1000);
driveActuator(i, Direction, Speed[i]);
}
Serial.println();
}
void checkButtons(){
//latching buttons: direction remains the same when let go
if(digitalRead(upPin)==LOW){ Direction=1; } //check if extension button is pressed
if(digitalRead(downPin)==LOW){ Direction=-1; }
if(digitalRead(stopPin)==LOW){ Direction=0; }
}
void moveTillLimit(int Direction, int Speed){
//function moves the actuator to one of its limits
for(int i = 0; i < numberOfActuators; i++){
counter[i] = 0; //reset counter variables
prevCounter[i] = 0;
}
do {
for(int i = 0; i < numberOfActuators; i++) {
prevCounter[i] = counter[i];
}
timeElapsed = 0;
while(timeElapsed < 200){ //keep moving until counter remains the same for a short duration of time
for(int i = 0; i < numberOfActuators; i++) {
driveActuator(i, Direction, Speed);
}
}
} while(compareCounter(prevCounter, counter)); //loop until all counters remain the same
}
bool compareCounter(volatile int prevCounter[], volatile int counter[]){
//compares two arrays and returns false when every element of one array is the same as its corresponding indexed element in the other array
bool areUnequal = true;
for(int i = 0; i < numberOfActuators; i++){
if(prevCounter[i] == counter[i]){
areUnequal = false;
}
else{ //if even one pair of elements are unequal the entire function returns true
areUnequal = true;
break;
}
}
return areUnequal;
}
void driveActuator(int Actuator, int Direction, int Speed){
int rightPWM=RPWM[Actuator];
int leftPWM=LPWM[Actuator];
switch(Direction){
case 1: //extension
analogWrite(rightPWM, Speed);
analogWrite(leftPWM, 0);
break;
case 0: //stopping
analogWrite(rightPWM, 0);
analogWrite(leftPWM, 0);
break;
case -1: //retraction
analogWrite(rightPWM, 0);
analogWrite(leftPWM, Speed);
break;
}
}
void count_0(){
//This interrupt function increments a counter corresponding to changes in the optical pin status
if ((millis() - lastDebounceTime[0]) > falsepulseDelay) { //reduce noise by debouncing IR signal with a delay
lastDebounceTime[0] = millis();
if(Direction==1){
counter[0]++;
}
if(Direction==-1){
counter[0]--;
}
normalizedPulseCount[0]=float(counter[0])/float(pulseTotal[0]);
}
}
void count_1(){
if ((millis() - lastDebounceTime[1]) > falsepulseDelay) {
lastDebounceTime[1] = millis();
if(Direction==1){
counter[1]++;
}
if(Direction==-1){
counter[1]--;
}
normalizedPulseCount[1]=float(counter[1])/float(pulseTotal[1]);
}
}
استخدام مشغلات Bullet 36 و Bullet 50 بشكل متزامن
بالإضافة إلى المشغل الخطي من السلسلة الضوئية ، نقدم أيضًا مشغلين خطيين معروضين مع مشفرات داخلية: Bullet 36 Cal. و Bullet 50 Cal ، وكلاهما لهما مشفر Hall Effect الداخلي التربيعي. يعمل مشفر Hall Effect على نفس مبدأ المشفر البصري ولكن بدلاً من استخدام الضوء ، فإنه يستخدم المغناطيسية. علاوة على ذلك ، لكونه جهاز تشفير تربيعي ، فإنه يحتوي على مخرجي إشارة ، كل منهما خارج المرحلة بمقدار 90 درجة. على هذا النحو ، تحتاج إلى استخدام لوحة Arduino مع 4 أو أكثر من دبابيس المقاطعة (يحتوي Arduino Uno على اثنين فقط) وتعديل الكود لمعالجة الإدخال من إشارتين لكل مشغل. علاوة على ذلك ، يجب ضبط متغير وقت الارتداد falsepulseDelay مع K_p.
نصائح لكتابة البرنامج الخاص بك
أكثر من اثنين من المحركات الخطية
عند استخدام اثنين أو أكثر من المشغلات الخطية ، لن يعمل Arduino Uno بعد الآن حيث لا يتوفر سوى دبابيس للمقاطعة. ستحتاج إلى استخدام لوحة Arduino مع توفر العدد المناسب من دبابيس المقاطعة ، مزيد من المعلومات: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
ثانيًا ، من أجل الكفاءة ، يُنصح بتوجيه البرمجة باستخدام المصفوفات و () الحلقات للتكرار على كل مشغل.
شجب
كما هو الحال مع العديد من أجهزة الاستشعار من المهم أن يكون الإدراك من الإشارات كذاب. كما هو الحال مع مفاتيح الميكانيكية ، يمكن أن تعاني أيضا من الترميز كذاب. في المثال أعلاه تم التعامل مع عملية debouncing بتأخير بسيط (محددة من قبل متغير falsepulseDelay) ، من المهم التعامل مع هذا في أي تغييرات البرنامج الذي تقوم به أو مع الدوائر المادية لتصفية الضوضاء كذاب.
التعامل مع التدحرج
إذا قمت بتعديل التعليمات البرمجية يكون على علم من التمديد عند التعامل مع الدالة millis(). يتم تعريف كل من millis() و array lastDebounceTime كمتغيرات طويلة غير موقعة بمعنى أنها يمكن تخزين القيم حتى 4,294,967,295 (32^2-1). وهذا يترجم إلى فترة التمديد تقريبا من 49.7 يوما. تم تصميم البرنامج الحالي لمعالجة التمديد في وظائف ISR (خدمة المقاطعة) وظائف: count_0 & count_1، ولكن إذا قمت بتعديل هذا البرنامج تأكد من التعامل بشكل صحيح مع متغير التمديد، وإلا سوف تعطل البرنامج الخاص بك بعد ~ 49.7 أيام من الاستخدام المستمر. لمزيد من المعلومات، يرجى الرجوع إلى: https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/