Синхронное движение между несколькими линейными приводами может иметь жизненно важное значение для успеха некоторых приложений клиентов, одним из которых является два линейных привода, открывающих люк. Для этого мы рекомендуем использовать специальный инструмент Firgelli блок синхронного управления FA-SYNC-2 и FA-SYNC-4. Однако некоторые домашние мастера и хакеры предпочитают свободу, которую предлагает микроконтроллер, такой как Arduino, и предпочитают вместо этого писать свою собственную программу синхронного управления. Цель этого руководства - предоставить обзор того, как этого добиться с помощью Линейный привод Optical Series.
Предисловие
Это руководство не является подробным описанием шагов, необходимых для достижения синхронного управления с Arduino, а скорее представляет собой широкий обзор, который поможет вам написать собственную программу. Это продвинутое руководство предполагает, что вы уже знакомы с аппаратным и программным обеспечением Arduino, а в идеале имеете опыт работы с сигналами широтно-импульсной модуляции (ШИМ), процедурой обслуживания прерываний (ISR), устранением неисправностей датчиков и кодировщиков двигателей. Пример, представленный в этом руководстве, представляет собой примитивный пропорциональный контроллер. Многие улучшения могут быть реализованы на следующем примере, включая, помимо прочего: реализацию контура ПИД-регулирования и масштабирование до более чем двух линейных приводов. Имейте в виду, что у нас нет ресурсов для обеспечения технической поддержки приложений Arduino, и мы не будем отлаживать, редактировать, предоставлять код или схемы подключения за пределами этих общедоступных руководств.
Обзор синхронного управления
Синхронное управление достигается путем сравнения длины двух линейных приводов и пропорциональной регулировки скорости; если один привод начинает двигаться быстрее другого, мы его замедляем. Мы можем считывать положение линейного привода через встроенный оптический энкодер. Оптический энкодер - это небольшой пластиковый диск с 10 отверстиями в нем, который подключен к двигателю постоянного тока, так что, когда двигатель вращается, пластиковый диск тоже. Инфракрасный светодиод направлен на пластиковый диск, так что, когда он вращает свет, он либо проходит через отверстия в оптическом диске, либо блокируется пластиком диска. Инфракрасный датчик на другой стороне диска определяет, когда свет проходит через отверстие, и выдает прямоугольный сигнал. Подсчитав количество импульсов, которые обнаруживает приемник, мы можем вычислить как частоту вращения двигателя, так и расстояние, пройденное линейным приводом. Оптический линейный привод 35 фунтов имеет 50 (+/- 5) оптических импульсов на дюйм хода, в то время как приводы 200 фунтов и 400 фунтов имеют 100 (+/- 5) импульсов на дюйм. Сравнивая, насколько далеко выдвинулся каждый линейный привод, мы можем пропорционально регулировать скорость двух приводов, чтобы они всегда оставались одинаковой длины при выдвижении.
Необходимые компоненты
- Два оптических линейных привода
- Два мотора привода ИБТ-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);
Для достижения наилучших результатов мы настоятельно рекомендуем удалить пропорциональный контроллер и реализовать контур ПИД-регулирования; однако это выходит за рамки этого вводного руководства и намеренно опущено.
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 в синхронном режиме
В дополнение к линейному приводу серии Optical мы также предлагаем два линейных привода со встроенными энкодерами: Bullet 36 Cal. и Bullet 50 Cal, оба из которых имеют внутренний квадратурный энкодер на эффекте Холла. Энкодер на эффекте Холла работает по тому же принципу, что и оптический энкодер, но вместо света он использует магнетизм. Кроме того, будучи квадратурным энкодером, он имеет два сигнальных выхода, каждый смещен по фазе на 90 градусов. Таким образом, вам необходимо использовать плату Arduino с 4 или более контактами прерывания (Arduino Uno имеет только два) и изменить код для обработки ввода от двух сигналов на привод. Кроме того, переменную времени устранения дребезга falsepulseDelay необходимо настроить вместе с K_p.
Советы по написанию собственной программы
Более двух линейных приводов
При использовании двух или более линейных приводов Arduino Uno больше не будет работать, поскольку у него есть только два доступных контакта прерывания. Вам нужно будет использовать плату Arduino с соответствующим количеством доступных контактов прерывания, дополнительная информация: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
Во-вторых, в интересах эффективности рекомендуется векторизовать ваше программирование с использованием массивов и циклов for () для итерации по каждому актуатору.
Debouncing
Как и во многих датчиках, важно осознавать подпрыгивая сигналы. Как и в случае с механическими переключателями, кодеры также могут страдать от подпрыгивания. В приведенном выше примере процесс дебунирования был обработан простой задержкой (определяемой переменной falsepulseDelay), важно обрабатывать это в любых изменениях программного обеспечения, которые вы делаете, или с физически схемой, чтобы отфильтровать подпрыгивая шум.
Обработка перевернуться
Если вы измените код, будьте в курсе опрокидывания при работе с функцией millis(). Оба милли () и последний массивDebounceTime объявлены неподписанными длинными переменными, что означает, что они могут хранить значения до 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/