воскресенье, 19 мая 2013 г.

Умная Серва. Часть 1

Про серву написано много. Примеры и как ее подключать находятся в гугле легко.
Поэтому я хочу рассмотреть скорее типично-практические задачи возникающие при работе с ней. Для опытных это все "тривиально", но надеюсь кому-то будет полезно.


Типичной задачей при использовании сервы является

"нужно повернутся в позицию XXX, после чего выполнить какое-то действие".

Предположим у нас стоит задача "серва стоит в 0, нужно ее  повернуть в позицию 90-то градусов и включить светодиод".

Раннее зажигание

На первый взгляд тут все просто.
Подключаем серву как стандартном примере из библиотеки


и пытаемся ее крутить:
#include "Servo.h" // подключаем библиотеку сервы

#define LED_PIN 13  // используем встроенный светодиод
#define SERVO_PIN 9  // подключаем серву на 9-ты pin
Servo myservo;

void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву
  
  myservo.write(90); // поворачиваем серву
  digitalWrite(LED_PIN,HIGH); // включаем светик
  
}

void loop(){
}


Тут нас ждет первое разочарование: светодиод загорелся одновременно с тем как серва начала крутится. А не когда он стала в заданную позицию.
Это произошло от того, что метод Servo.write является "не блокирующим". Он не останавливает скетч на время своего выполнения. По сути его вызов означает не "поворачиваем серву", а "сказали серве повернутся". Выполнение этой команды, занимает у сервы какое-то время. А скетч , в этот момент выполняется дальше. И включает светодиод светодиод.

Что же делать?  Первое что приходит в голову - нужно как-то узнавать текущие положение сервы и включать светодиод только когда она займет нужную позицию.

К сожалению, для того что-бы узнать физическое положение сервы, нужно дополнительное оборудование (или вносить модификацию в саму серву, или искать дорогую серву обратной связью).
К счастью, если нам не нужна сверх-высокая точность, когда достаточно что-бы светодиод загорелся ПОСЛЕ того как серва займет положение (а не "точно в этот момент").

Для этого мы просто остановим скетч и подождем пока серва прокрутится

...
 myservo.write(90); // поворачиваем серву
 delay(2000); // ждем пока серва повернется
 digitalWrite(LED_PIN,HIGH); // включаем светик
...

Откуда взялась цифра 2000?  Я просто "прикинул" что двух секунд будет достаточно что-бы любая популярная серва успела повернутся на 90-то градусов.

Теперь у нас светодиод загорится не раньше чем серва станет в 90-то градусов.

Кстати сейчас мы можем поправить еще один наш "косяк". Скетч замечательно крутит серву в 90-то градусов, но его работу не удобно наблюдать если серва уже стоит в 90-то. Нужно выключать Ардуину, крутить руками серву в 0 (с риском ее сломать) просто что-бы еще раз увидеть как отрабатывает скетч.

Исправим в это. Будем при старте ее выкручивать в ноль, а уж потом ставить в 90-то и зажигать светодиод. Естественно не забудем дать ей время на "выкрутится в ноль".

void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(2000); // ждем пока станет в 0
  
  // крутим в 90-то и зажигаем светик
  myservo.write(90); // поворачиваем серву
  delay(2000); // даем время на поворот
  digitalWrite(LED_PIN,HIGH); // включаем светик
  
}


Позднее зажигание

Но теперь у нас нас имеется противоположный эффект - светодиод загорается с опозданием.
Как это побороть?  Да просто. Опытным путем подбираем значение вот этой задержки delay(), после myservo.write . Не слишком маленькое что-бы серва успевала повернутся и не слишком большое, что-бы пауза между "серва перестала крутится" и "светодиод загорелся" - не слишком раздражала.

Умное зажигание.

А что будет если завтра, мы решим поворачивать не на 90, а на, скажем 135-ть? Или таких поворотов нам нужно будет сделать сотню, на разные углы?  Для каждого подбирать время задержки?

Естественно - нет. Мы будем его "высчитывать". Возьмем в руки секундомер,  в предыдущем скетче будем ставить серву не в 90, а в 180-градусов и засечем секундомером сколько времени это занимает. Ну либо, опять-таки подберем delay методом "перелет-недолет" (в следующем посте расскажу как самой Ардуиной можно точнее замерить этот интервал).

Предположим мы замеряли, что полный поворот сервы (180-градусов), занимает 1200 миллисекунд. Отсюда мы можем узнать время поворота на один градус 1200ms/180=6.66ms
Значит поворот на 135-градусов требует задержки 6.66*135=900 ms.

Естественно каждый раз вычислять руками эти задержки - совершенно "не по программески". Напишем вспомогательную функцию, и в будущем вместо myservo.write/delay будем вызвать ее. Пусть она делает все что нам нужно. 
#define DELAY_180 1200 // время, в миллисекундах, за которое серва поворачивается на 180 градусов

// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
void setServo(int newPos){
  // вычисляем нужную задержку
  unsigned long servoDelay=(DELAY_180 /180)*newPos;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
}
Теперь чуть-чуть изменим на setup() на использование нашей новой функции
  void setup(){
  ...

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(DELAY_180); // ждем пока станет в 0
  
  // крутим в 90-то и зажигаем светик
  setServo(90);
  digitalWrite(LED_PIN,HIGH); // включаем светик
  
}

Как видите, теперь в коде с "бизнес логикой" исчез delay(). Тем что-бы про него "не забыть" и его вычислением - занимается вспомогательная функция.
Но в строках 5 и 6-ть, я "по старинке" использую write/delay. Это сделано потому что при включении контроллера я еще не знаю где стоит серва. Следовательно функция setServo - не может вычислить необходимое время поворота. Поэтому я просто дал максимально возможную задержку (где-бы серва не стояла,она успеет провернутся до нуля).

Запоминаем позицию

Прежде чем двигаться дальше, чуть-чуть структурируем ("причешем") наш код (но оставим его логику неизменной). Вынесем работу с сервой в две отдельные функции (и будем их вызывать из setup()
init_servo() - все что относится с инициализации сервы.
run_servo_logic() - сюда поместим всю нашу "бизнес логику". Вращения, мигания и т.п.

Весь наш код, целиком, теперь выглядит так:
  void setup(){
#include "Servo.h" // подключаем библиотеку сервы

#define LED_PIN 13  // используем встроенный светодиод
#define SERVO_PIN 9  // подключаем серву на 9-ты pin

Servo myservo;


#define DELAY_180 1200 // время, в миллисекундах, за которое серва поворачивается на 180 градусов


void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву

  init_servo();
  
  run_servo_logic();
  
}

void loop(){
}

void init_servo(){
  myservo.attach(SERVO_PIN); // подключаем серву

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(DELAY_180); // ждем пока станет в 0
}


void run_servo_logic(){
   // крутим в 90-то и зажигаем светик
  setServo(90);
  digitalWrite(LED_PIN,HIGH); // включаем светик
}


// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
void setServo(int newPos){
  // вычисляем нужную задержку
  unsigned long servoDelay=(DELAY_180 /180)*newPos;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
}

Теперь мы можем немного усложнить нашу задачу. Будем выполнять не один поворот. Пусть наша серва должна, по очереди устанавливаться в следующий позиции 90, 45, 135,90, 180
И по достижении каждой позиции включать/выключать светик.

Для этого, мы введем еще одну вспомогательную функцию toggleLed(), которая будет переключать светик в противоположное значение, и попробую "в лоб", задать наши повороты

void run_servo_logic(){
   // крутим в 90-то и зажигаем светик
  setServo(90);
  toggleLed(); // включаем светик
  
  setServo(45);
  toggleLed(); // выключаем
  
  setServo(135);
  toggleLed(); 
  
  setServo(90);
  toggleLed(); 
  
  setServo(180);
  toggleLed(); 
}


// переключает светик в противоположное состояние
void toggleLed(){
  digitalWrite(LED_PIN,!digitalRead(LED_PIN));
}

Пробуем выполнить, и замечаем, что первый setServo(90) - у нас выполняется нормально, второй setServo(45) - тоже (на самом деле - просто повезло), а вот все остальные - явно ждут дольше чем нужно. Опять наблюдаем задержку между "серва стала в позицию" и "светодиод поменял свое состояние".  Где-то (в функции setServo()) у нас ошибка с вычислением времени задержки.
Внимательно всматриваемся в setServo() и понимаем, что, при вычислении, мы неявно предполагаем что серва, перед поворотом, стоит в нуле. Скажем после  вызова setServo(90) , следует вызов setServo(180). Нужно "довернуть" серву всего на 90-то градусов, а время задержки мы рассчитали как для полного поворота на 180.
Что-бы скорректировать эту ошибку, будем передавать в функцию setServo еще один параметр - текущие положение сервы. И вычислять время поворота исходя из разницы "старого" и"нового положения". При этом, воспользуемся встроенной функцией abs(X), которая возвращает модуль числа. Что-бы при вращении против-часовой стрелки, не получать отрицательные задержки
void run_servo_logic(){
   // крутим в 90-то и зажигаем светик
  setServo(0,90);
  toggleLed(); // включаем светик
  
  setServo(90,45);
  toggleLed(); // выключаем
  
  setServo(45,135);
  toggleLed(); 
  
  setServo(135,90);
  toggleLed(); 
  
  setServo(90,180);
  toggleLed(); 
}


// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
// для этого используют oldPos - старое положение сервы
void setServo(int oldPos,int newPos){
  // вычисляем нужную задержку
  int angleDiff=abs(newPos-oldPos);
  unsigned long servoDelay=(DELAY_180 /180)*angleDiff;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
}

Теперь все работает "как нужно".
Но такое решение "некрасиво". Оно заставляет нас постоянно ,руками указывать прошлое положение сервы. Что с одной стороны - просто неприятная работа, с другой - увеличивает шанс ошибки. Если мы, скажем, переставим местами поворот на 45 и 135, то на нужно будет не забыть поправить прошлое положение в трех местах.

Давайте исправим это, и сделаем что-бы функция setServo - опять принимала только один параметр newPos - новое положение. А "прошлое" - сама запоминала в какую-то глобальную переменную.



byte prevServoPos=0; // сюда будем запоминать, куда мы установили серву. изначально - стоит в нуле.

// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
// для этого используют  глобальную prevServoPos - старое положение сервы
void setServo(byte newPos){
  // вычисляем нужную задержку
  int angleDiff=abs(newPos-prevServoPos);
  unsigned long servoDelay=(DELAY_180 /180)*angleDiff;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
  
  prevServoPos=newPos;// запомнили где теперь стоит серва
}


Теперь мы можем выкинуть из головы заботу о "где сейчас серва" и опять указывать только "куда нужно повернутся" setServo(НОВАЯ_ПОЗИЦИЯ);

Осталось исправить последнюю "некрасивость". Вот эта череда setServo(...);toggleLed(); - раздражает.  Много одинаковых строчек, которые отличаются только одной цифрой. А если углов будет сотня? Набирать их как мартышка? А если нужно будет внести какое-то изменения, опять в менять в сотне мест?
Конечно нет. Тут явно напрашивается поместить в массив наши углы, и проходить по нему циклом for.

В итоге наш скетч примет такой вид:

#include "Servo.h" // подключаем библиотеку сервы

#define LED_PIN 13  // используем встроенный светодиод
#define SERVO_PIN 9  // подключаем серву на 9-ты pin
#define DELAY_180 1200 // время, в миллисекундах, за которое серва поворачивается на 180 градусов

Servo myservo;
byte prevServoPos=0; // сюда будем запоминать, куда мы установили серву. изначально - стоит в нуле.

void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву

  init_servo();
  
  run_servo_logic();
  
}

void loop(){
}

void init_servo(){
  myservo.attach(SERVO_PIN); // подключаем серву

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(DELAY_180); // ждем пока станет в 0
}


void run_servo_logic(){
  byte angles[]={90,45,135,90,180}; // углы в которые нужно ставить серву
  for(byte i=0;i<5;i++){
    setServo(angles[i]); // ставим серву в очередной угол
    toggleLed(); // переключаем состояние светика
  }
}

// переключает светик в противоположное состояние
void toggleLed(){
  digitalWrite(LED_PIN,!digitalRead(LED_PIN));
}

// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
// для этого используют  глобальную prevServoPos - старое положение сервы
void setServo(byte newPos){
  // вычисляем нужную задержку
  int angleDiff=abs(newPos-prevServoPos);
  unsigned long servoDelay=(DELAY_180 /180)*angleDiff;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
  
  prevServoPos=newPos;// запомнили где теперь стоит серва
}
Конечно, это "итоговый" скетч только для данной статьи. Наши игры с сервой - мы еще не закончили. Названию "умная серва" - еще не соотвествуем, но... В следующих статьях мы рассмотрим как наш код опять сделать "не блокирующим" (избавимся от delay()), но при этом сохранить возможность зажигать светик "синхронизировано" с положением сервы. Это нам может потребоваться, если мы захотим, одновременно управлять несколькими сервами, опрашивать, во время вращения, кнопки/датчики и т.п.
Ну и на закуску видео как все это работает. Единственное отличие от кода приведенного выше, для того что-бы на видео было легче уследить за сервой и диодом, после toggleLed(), я добавил delay(1000); Что-бы в каждой позиции серва останавливалась на 1 секунду.