Про серву написано много. Примеры и как ее подключать находятся в гугле легко.
Поэтому я хочу рассмотреть скорее типично-практические задачи возникающие при работе с ней. Для опытных это все "тривиально", но надеюсь кому-то будет полезно.
Типичной задачей при использовании сервы является
"нужно повернутся в позицию XXX, после чего выполнить какое-то действие".
Предположим у нас стоит задача "серва стоит в 0, нужно ее повернуть в позицию 90-то градусов и включить светодиод".
Тут нас ждет первое разочарование: светодиод загорелся одновременно с тем как серва начала крутится. А не когда он стала в заданную позицию.
Это произошло от того, что метод Servo.write является "не блокирующим". Он не останавливает скетч на время своего выполнения. По сути его вызов означает не "поворачиваем серву", а "сказали серве повернутся". Выполнение этой команды, занимает у сервы какое-то время. А скетч , в этот момент выполняется дальше. И включает светодиод светодиод.
Что же делать? Первое что приходит в голову - нужно как-то узнавать текущие положение сервы и включать светодиод только когда она займет нужную позицию.
К сожалению, для того что-бы узнать физическое положение сервы, нужно дополнительное оборудование (или вносить модификацию в саму серву, или искать дорогую серву обратной связью).
К счастью, если нам не нужна сверх-высокая точность, когда достаточно что-бы светодиод загорелся ПОСЛЕ того как серва займет положение (а не "точно в этот момент").
Для этого мы просто остановим скетч и подождем пока серва прокрутится
Откуда взялась цифра 2000? Я просто "прикинул" что двух секунд будет достаточно что-бы любая популярная серва успела повернутся на 90-то градусов.
Теперь у нас светодиод загорится не раньше чем серва станет в 90-то градусов.
Кстати сейчас мы можем поправить еще один наш "косяк". Скетч замечательно крутит серву в 90-то градусов, но его работу не удобно наблюдать если серва уже стоит в 90-то. Нужно выключать Ардуину, крутить руками серву в 0 (с риском ее сломать) просто что-бы еще раз увидеть как отрабатывает скетч.
Исправим в это. Будем при старте ее выкручивать в ноль, а уж потом ставить в 90-то и зажигать светодиод. Естественно не забудем дать ей время на "выкрутится в ноль".
Теперь мы можем немного усложнить нашу задачу. Будем выполнять не один поворот. Пусть наша серва должна, по очереди устанавливаться в следующий позиции 90, 45, 135,90, 180
И по достижении каждой позиции включать/выключать светик.
Для этого, мы введем еще одну вспомогательную функцию toggleLed(), которая будет переключать светик в противоположное значение, и попробую "в лоб", задать наши повороты
Теперь все работает "как нужно".
Но такое решение "некрасиво". Оно заставляет нас постоянно ,руками указывать прошлое положение сервы. Что с одной стороны - просто неприятная работа, с другой - увеличивает шанс ошибки. Если мы, скажем, переставим местами поворот на 45 и 135, то на нужно будет не забыть поправить прошлое положение в трех местах.
Давайте исправим это, и сделаем что-бы функция setServo - опять принимала только один параметр newPos - новое положение. А "прошлое" - сама запоминала в какую-то глобальную переменную.
Теперь мы можем выкинуть из головы заботу о "где сейчас серва" и опять указывать только "куда нужно повернутся" setServo(НОВАЯ_ПОЗИЦИЯ);
Осталось исправить последнюю "некрасивость". Вот эта череда setServo(...);toggleLed(); - раздражает. Много одинаковых строчек, которые отличаются только одной цифрой. А если углов будет сотня? Набирать их как мартышка? А если нужно будет внести какое-то изменения, опять в менять в сотне мест?
Конечно нет. Тут явно напрашивается поместить в массив наши углы, и проходить по нему циклом for.
В итоге наш скетч примет такой вид:
Поэтому я хочу рассмотреть скорее типично-практические задачи возникающие при работе с ней. Для опытных это все "тривиально", но надеюсь кому-то будет полезно.
Типичной задачей при использовании сервы является
"нужно повернутся в позицию XXX, после чего выполнить какое-то действие".
Раннее зажигание
На первый взгляд тут все просто.
Подключаем серву как стандартном примере из библиотеки
и пытаемся ее крутить:
Подключаем серву как стандартном примере из библиотеки
и пытаемся ее крутить:
#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 . Не слишком маленькое что-бы серва успевала повернутся и не слишком большое, что-бы пауза между "серва перестала крутится" и "светодиод загорелся" - не слишком раздражала.
Как это побороть? Да просто. Опытным путем подбираем значение вот этой задержки 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 - не может вычислить необходимое время поворота. Поэтому я просто дал максимально возможную задержку (где-бы серва не стояла,она успеет провернутся до нуля).
Но в строках 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), которая возвращает модуль числа. Что-бы при вращении против-часовой стрелки, не получать отрицательные задержки
Внимательно всматриваемся в 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(...);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 секунду.