пятница, 30 августа 2013 г.

Мигаем светодиодом без delay()

или всегда ли официальный примеру учат "хорошему".

Обычно это одна из первых проблем с которой сталкивается навичок в микроконтроллерх. Помигал диодом, запустил стандартный скетч blink(), но как только он него возникает желание что-бы контроллер "занимался чем-то еще" его ждет неприятный сюрприз - тут нет многопоточности. Как только он написали где-то что-то типа delay(1000) - обнаруживается что на это строчке "контроллер остановился" и ничего больше не делает (кнопки не опрашивает, датчики не читает, вторым диодом "помигать" не может).
Новичок лезет с этим вопросом на форум и тут же получает ушат поучений: "отказывайтесь от delay()", учитесь работать с millis() и в прочитайте, в конце концов базовые примеры. В частности Мигаем светодиодом без delay()

Приведу его код:
/* Blink without Delay
 2005
 by David A. Mellis
 modified 8 Feb 2010
 by Paul Stoffregen
 */

const int ledPin =  13;      // номер выхода, подключенного к светодиоду
// Variables will change:
int ledState = LOW;             // этой переменной устанавливаем состояние светодиода 
long previousMillis = 0;        // храним время последнего переключения светодиода

long interval = 1000;           // интервал между включение/выключением светодиода (1 секунда)

void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(ledPin, OUTPUT);      
}

void loop()
{
  // здесь будет код, который будет работать постоянно
  // и который не должен останавливаться на время между переключениями свето
  unsigned long currentMillis = millis();
 
  //проверяем не прошел ли нужный интервал, если прошел то
  if(currentMillis - previousMillis > interval) {
    // сохраняем время последнего переключения
    previousMillis = currentMillis;  

    // если светодиод не горит, то зажигаем, и наоборот
    if (ledState == LOW)
      ledState = HIGH;
    else
      ledState = LOW;

    // устанавливаем состояния выхода, чтобы включить или выключить светодиод
    digitalWrite(ledPin, ledState);
  }
}
  



В принципе отправка к этому примеру - вполне правильна. В нем действительно виден стандартный паттерн как нужно выполнять какое-то переодическое действие (или отложенное):
1. Сохраняем время в какую-то переменную
2. В loop() все время смотрим на разницу "текущие-время - сохраненное"
3. Когда эта разница превысила какое-то значение (нужный нам "интервал переодичности")
4. Выполняем какое-то действие (меняем состояние диода, заново запоминаем "стартовое время и т.п.")

С задачей "объяснить идею" - пример справляется. Но, с моей точки зрения, в нем есть несколько методологических ошибок. Написание скетчек в подобно стиле - рано или поздно приведет к проблемам.

Итак, что же тут не так?

1. Не экономно выбираем тип переменных

Переменная ledPin у нас объявлена как тип int. Зачем?  Разве номер пина может быть очень большим числом? Или может быть отрицательным числом? Зачем под его хранение выделять два байта, когда вполне хватит одного. Тип byte может хранить числа от 0 до 255. Для номера пина - этого вполне хватит.

  const byte ledPIN = 13; //  номер выхода, подключенного к светодиоду

Этого будет вполне достаточно.

2. А зачем нам переменная для малых чисел?

А зачем нам тут вообще переменая? (пусть и объявленная как const). Зачем тратить такты процессора на чтение переменной? И расходовать еще один байт?  Воспользуемся директивой препроцессора #define

#define LED_PIN  13 //  номер выхода, подключенного к светодиоду

Тогда еще на этапе компиляции компилятор просто подставить 13-ть везде где в коде используется LED_PIN и не будет выделять отдельных переменных.

3. Тип int опять выбрали как "первое что в голову пришло"?

И опять спотыкаемся на объявлении следующей же переменной. Почему ledState опять int? Кроме того что снова "два байта там где можно один использовать", так еще и "смысловая нагрузка" теряется. Что у нас хранится в переменной? Состояние светодиода. Включен/выключен. Горит/Не горит. Явно же напрашивается тип boolean. По крайней мере до тех пор, пока светодиод у нас может принимать два состояния.

4. И вновь не верный тип. Теперь уже критично

Наверное уже надоел :)  Но объявление следующей переменной мне опять не нравится. Почему previousMillis как long? В нее  мы сохраняем время  возвращаемое функцией millis(). В документации на нее говорится что она возвращает unsigned long  (кстати currentMillis объявили правильно). Значит и все переменные где хранится время должны быть unsigned long.
Эта ошибка с типом - гораздо более опасная. В отличает от предыдущих где тип был выбран "избыточно", в этом случае он выбран "недостаточно". Функция millis() возвращает unsigned long которая может принимать максимальное значение 4,294,967,295 . А в long у нас может поместится 2,147,483,647 . В два раза меньше.  Примерно на 24-тый день в переменной previousMillis из-за этого начнут образовыватся отрицательный значения. И все логика  (currentMillis - previousMillis) - может поломатся.

Конечно мигать диодом 24-дня непрерывно никто не будет. Но если научившись на этом примере, по его образу и подобию сделают, скажем систему полива сада?  Которая через месяц устроит потоп?

Кстати недавно на форуме, как раз и ловили подобную ошибку. Там вместо "unsigned long" подобная переменная была объявлена вообще как int. Повезло в том, что работали не с миллисекундами, на более мелких интервалах. Вместо millis() использовали micros() . И скетч "зависал" через 32-секунды (это число и натолкнуло на необходимость поиска ошибочного int). А если бы был millis() - все бы висло раз в несколько дней. И догадатся "что не так" - было бы гораздо сложней. Грешили бы на "питание/помехи" и т.п.
Вообщем я думаю вы уже поняли, что небрежное обращения с типами, использование "первого попавшегося с которым заработало" - в итоге выливается в часы жизни проведенные за поиском бага.


5. Мелочный придирки

Ну и парочка придирок которые "не ошибки", но можно было-бы компактней написать. Хотя возможно тут автор примера просто старался написать "попроще для новичков". Но раз мы уже "смотрим скетч под лупой.." :)
Переменную previousMillis можно, с помощью ключевого слово static объявить внутри loop(). Не засорять простраство имен глобальных переменных. Полезная привычка.

От переменное ledState - вообще можно отказаться. Текущие состояние пина можно узнать с помощью digitalRead().  Вообщем-то этот "совет" довольно спорен. С переменой ledState - код более читабелен... просто покажу что можно и без нее.

Да и без переменной currentMillis можно обойтись. Просто два раза вызвать функцию millis() . 

6. Наш ответ Чемберлену

Ну вот, а теперь попытаемся переписать этот пример, учтя все "замечания".

 /* Blink without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/08/delay.html
 */

#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  INTERVAL  1000UL           // интервал между включение/выключением светодиода (1 секунда)

void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
}

void loop()
{
  // здесь будет код, который будет работать постоянно
  // и который не должен останавливаться на время между переключениями свето
  
  static unsigned long previousMillis = 0;        // храним время последнего переключения светодиода
 
  //проверяем не прошел ли нужный интервал, если прошел то
  if(millis() - previousMillis > INTERVAL) {
    // сохраняем время последнего переключения
    previousMillis = millis();  
    
    // меняем состояние выхода светодиода на противоположное текущему. 
    // если горит - тушим, не горит - зажигаем.
    digitalWrite(LED_PIN,!digitalRead(LED_PIN));
  }
}

UPD: Больше примеров вы найдете в следующей статье Делаем несколько дел одновременно-переодически

P.S. Задать вопросы и почитать обсуждение вы можете либо в комментариях ниже, либо на сайте arduino.ru в ветке Еще раз мигаем светодиодом без Delay | Аппаратная платформа Arduino

воскресенье, 11 августа 2013 г.

Еще одни часы на Ардуине. Часть 2. Выводим время на экран

В прошлой части Еще одни часы на Ардуине. Часть 1. Запускаем часы мы запустили модуль часов DS1307 и научились выводить время в Serial. Но пользоваться такими часами, конечно невозможно. Открывать Serial монитор для что-бы узнать "который час" :)
Так что, сегодня, мы

Подключаем экран:

Мне повезло. В случае Arduino Mega подключение экрана сводится к простому "нахлобучиванию" его на плату (главное, при первом втыкании  не торопится и не погнуть пины). У Меги SDA/SLC не закрываются шилдом. А 5V и GND дополнительно выведено возле D23 и D25. Так что LCD шилд ничуть не мешает подключению RTC шилда.

В случае же UNO - придется чуть-чуть взять в руки паяльник.
Вам потребуется:


Гнезда:

  •  PBS-5 - для выведения вверх SDA/SLC
  • PBS-6 - для выведения земли и питания
  • PBS-7 - что-бы вывести цифровые пины (пока мы их не используем, но что-бы два раза не вставать).

Впаиваем их сюда:


Там специально, производителем, именно для этих целей, оставлены дырочки-площадки.

В результате мы получим что-то такое:

[тут-будет-картинка]

Тут для примера впаяна PBD-5 и в которую воткнут проводок.

Заливаем в дуину такой скетч проверки экрана:
 
#include "LiquidCrystal.h" // библиотека экрана

// select the pins used on the LCD panel
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup(){
   lcd.begin(16, 2); // запускаем библиотеку
   lcd.setCursor(0,0); // курсов в левый верхний угол экрана
   lcd.print("Hello from LCD!!");
}
void loop(){}


Видим на экрнае такое:

[тут-будет-картинка]

И радуемся правильно подключенному экрану. Если необходимо, плохо видно буквы или, наоборот сквозь буквы проступают "квадратики" фоновые, берем тонкую плоскую отвертку (или кончиком ножа) и крутим регулятор контрастности (винтик в верхнем левом углу шилда).


Который час?

Теперь у нас все готово к выводу времени на экран.  Подключаем RTC модуль (если мы его отключали), берем финальный скетч из  первой части и добавляем в него инициализацию экрана. Строки 1,4,7 из "скетч проверки экрана" прошлого раздела (выделенные строки).

После чего у нас все готово, что-бы написать функцию вывода времени на экран.

Делаем все практически аналогично тому как в прошлой части выводили в Serial. Только функцию называем printTimeToLCD()

 
void printTimeToLCD(){
    byte static prevSecond=0; // тут будем хранить, сколько секунд было при выводе
    
    if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
       lcd.setCursor(0,0); // устанавливаем позицию курсора
       
      lcd.print(RTC.hour); // часы
    
      lcd.print(":"); // разделитель
      lcd.print(RTC.minute);
    
      lcd.print(":"); 
      lcd.print(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

Добавляем вызов этой функции в loop(), заливаем в дуину и... мы сделали свои, пусть и неказистые, но первые электронные часы!!! :) На экране видно текущие время и "меняющие секунды".

А причесать?

Попробуем добавить чуть-чуть "эстетичности". Что мне не нравится в текущем варианте

  1. Хочется выводить время "по центру", а не в углу
  2. Убрать секунды. Мне они не нужны. Пусть лучше мигает двоеточие между часами и минутами (что-бы было видно что "секунды идут").
  3. Привести время к более привычному формату "16:03",  а не как сейчас "16:3"
  4. Если присмотрется в момент смены секунд видно легкое "помаргивание" надписи, она как-бы "пригасает".
Первый пункт - самый простой. Что-бы подвинуть время, нам достаточно сделать

lcd.setCursor(5,0).

Секунды - просто выкидываем :) А разделитель между часами и минутой показываем либо пробелом либо двоеточием. В зависимости от того четная или не четная у нас секунда

lcd.print( (RTC.second %2 )?" ":":");

Что осталось? Дописать "0" перед минутами если они меньше 10-ти

if(RTC.minute<10)lcd.print(0);

Аналогично для часов. Только добавлять впереди будем пробел, а не "0".  Ноль, вперди часов - выглядит некрасиво, а "что-то дописать" - нужно. Что-бы позиция времени не смещалась влево на малых значениях часа.

Наша функция вывода времени приняла вид:

 

void printTimeToLCD(){
    byte static prevSecond=0; // тут будем хранить, сколько секунд было при выводе
    
    if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
       lcd.setCursor(5,0); // устанавливаем позицию курсора
       
      if(RTC.hour<10)lcd.print(" ");
      lcd.print(RTC.hour); // часы
    
      lcd.print( (RTC.second % 2)?" ":":"); // разделитель моргает
      
      if(RTC.minute<10)lcd.print(0); // лидирующий ноль, если нужен
      lcd.print(RTC.minute);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

В принципе, 4-тый пункт, "помаргивание" - уже не виден на глаз. Так что усложнять код, для борьбы с ним - мы пока не будем.

Сегодняшний наш финальный скетч таков:

 

// Author: alxarduino@gmail.com
// Sample Clock App for http://alxarduino.blogspot.com

// Библиотеки необходимые для работы модуля часов
#include "Wire.h"
#include "DS1307new.h"

// библиотека экрана
#include "LiquidCrystal.h" 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup(){
  Serial.begin(9600);
  if(RTC.isPresent()){ // обнаружен ли модуль?
    Serial.println("RTC Ready"); // все хорошо
  } else {
    Serial.println("Error!!!! RTC Module not found"); // сообщаем о проблеме
    while(1); // и останавливаем скетч
  }
  
  lcd.begin(16, 2); // запускаем библиотеку экрана
}

void loop(){
  RTC.getTime();// получить время от модуля
  printTimeToSerial(); // выводим полученное время в лог
  printTimeToLCD();; // выводи время на экран
  doSerialCommands(); // слушаем и выполняем команды из Serial
}

// Выводит текущие время в Serial
void printTimeToSerial(){

  byte static prevSecond=0; // тут будем хранить, сколько секунд было при прошлом отчете
  
  if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
    Serial.print(RTC.hour); // часы
  
    Serial.print(":"); // разделитель
    Serial.print(RTC.minute);
  
    Serial.print(":"); 
    Serial.println(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

// Выводит текущие время на LCD

void printTimeToLCD(){
    byte static prevSecond=0; // тут будем хранить, сколько секунд было при выводе
    
    if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
       lcd.setCursor(5,0); // устанавливаем позицию курсора
       
      if(RTC.hour<10)lcd.print(" ");
      lcd.print(RTC.hour); // часы
    
      lcd.print( (RTC.second % 2)?" ":":"); // разделитель моргает
      
      if(RTC.minute<10)lcd.print(0); // лидирующий ноль, если нужен
      lcd.print(RTC.minute);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

// устанавливает часы модуля на какое-то заранее определенное время
void setSomeTime(){
  RTC.stopClock();// останавливаем часы
  RTC.fillByHMS(19,15,0); // "подкручиваем стрелки на 19:15:00
  RTC.setTime();// отправляем "подкрученное время" самому модулю
  RTC.startClock(); // и опять запускаем часы
}

// слушает из Serial команды и выполняет их. Каждая команда - один символ.
// доступны команды:
//  s - установить время указанное в функции setSomeTime()
void doSerialCommands(){
  if(Serial.available()){ // что-нибудь пришло?
    char ch=Serial.read(); // читаем что пришло
    
    switch(ch){
      case 's': // команда установки времени
           setSomeTime(); // устанавливаем
           break;
           
       // тут, в будущем, мы можем добавлять дополнительные команды
      default:;
           // на неизвестную команду - ничего не делаем
    };
  }
}

На LCD Это выглядит примерно так:
В В реальности, конечно все симпатичней, использовать фотоаппарат я умею только "в автоматическом режиме".

В следующей части мы будем учится работать с кнопками шилда. С учетом дребезга, настройкой чувствительности, отличать "короткое" и "длинное" нажатие и т.п.

суббота, 10 августа 2013 г.

Еще одни часы на Ардуине. Часть 1. Запускаем часы


Для следующего проекта, попались мне в руки два таких шилда:

Arduino LCD KeyPad Shield (SKU: DFR0009) - Robot Wiki
Текстовый экран 16x2 плюс четыре кнопки с помощью делителя напряжения заведенные на пин A0
И шилд часов реального времени: Real Time Clock Module (DS1307) V1.1 (SKU:DFR0151) - Robot Wiki


Тут могла быть ваша реклама вашего магазина шилдов :)

Руки зачесались...
Понятно, что это 1001 проект  "часы на ардуине" и можно просто взять что-то готовое. Но у меня две цели: поиграться с модулями и показать как "вырастает проект". Правильный, с моей точки зрения, процесс. В противовес тому, как многие новички пытаются "накупить железо", составить 1000 "хотелок" и запустить все это сразу.

Итак, раз мы будем идти мелкими шагами - откладываем пока наш экран в сторону. Для начала освоимся с часами.

Железная составляющая


Подключаем их в соотвествии с wiki-производителя:
Линию DS - можно, пока, не подключать (пунктирная линия). С ней будем разбиратся позже.

Но у меня плата - Arduino Mega. У нее шина I2C (по ней передает данные модуль часов) расположены не на A4,A5, а на D20,D21
Поэтому я подключал SDA->D20 ,  SLC->D21

Если у вас другая плата - можете сверится с этой табличкой:
ПлатаI2C / TWI pins
Uno, EthernetA4 (SDA), A5 (SCL)
Mega256020 (SDA), 21 (SCL)
Leonardo2 (SDA), 3 (SCL)
Due20 (SDA), 21 (SCL)

Щупаем програмно

Что-бы комфортно работать с часами, на закапываясь в даташиты нам потребуется скачать и установить библиотеку для DS1307. Производитель модуля - предоставляет ее, но мне больше понравилась альтернативная. Вот эта: ds1307new - DS1307 RTC Library with NV-RAM support - Google Project Hosting
Скачиваем ее, распаковываем, забрасываем в папку libraries и пишем первый наш код, проверяющий видит ли ардуина наши часы:
   // Библиотеки необходимые для работы модуля часов
#include <Wire.h>
#include <DS1307new.h>

void setup(){
  Serial.begin(9600);
  if(RTC.isPresent()){ // обнаружен ли модуль?
    Serial.println("RTC Ready"); // все хорошо
  } else {
    Serial.println("Error!!!! RTC Module not found"); // сообщаем о проблеме
    while(1); // и останавливаем скетч
  }
}

void loop(){
}

Заливаем, открываем Serial монитор и надпись:


Для перепроверки, отсединяем, скажем, линю SLC. Закрываем, открываем монитор (перезапускаем скетч) и видим что надпись у нас сменилась на "Error!!!! RTC Module not found".
Возвращаем проводок на место и опять видим "RTC Ready". Наш "детектор проблем" - работает.

А который час?

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

Ну что-же давай попробуем вывести время. Что-бы сразу привыкать к "хорошим манерам", не будем сильно захламлять loop(), вынесем вывод времени в отдельную функцию и назовем ее printTimeToSerial()  и будем ее вызвать из loop()

void loop(){
  RTC.getTime();// получить время от модуля
  printTimeToSerial(); // выводим полученное время
}
 
void printTimeToSerial(){
  Serial.print(RTC.hour); // часы
  
  Serial.print(":"); // разделитель
  Serial.print(RTC.minute);
  
  Serial.print(":"); 
  Serial.println(RTC.second);
}
В логе (сериал мониторе видим) что-то типа:
17:53:35
17:53:35
17:53:35
17:53:35
17:53:35

Много, много раз...
C одной стороны - хорошо. Модуль посылает нам время и мы научились отправлять его в Serial. С другой... он старается делать это как можно чаще. Насколько хватает пропускной способности Serial. А нам - это не нужно :) Столько дублирования. Поэтому давайте отправлять время в Serial только если, скажем секунды, изменились с прошлого вызова функции.
// Выводит текущие время в Serial
void printTimeToSerial(){

  byte static prevSecond=0; // тут будем хранить, сколько секунд было при прошлом отчете
  
  if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
    Serial.print(RTC.hour); // часы
  
    Serial.print(":"); // разделитель
    Serial.print(RTC.minute);
  
    Serial.print(":"); 
    Serial.println(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}


Теперь наш лог, примет такой вид:

RTC Ready
18:0:26
18:0:27
18:0:28
18:0:29

Каждая надпись - появляется раз в секунду. Можно дествительно смотреть как "часы идут".

 Кстати, возможно вы заметили что время выглядит "не очень красиво", вмето привычных 18:00:26, у нас получилось 18:0:26. Но так как работа с Serial у нас "вспомогательная", мы не будет отвлекатся на решение этой проблемы. Но вернемся к ней и будем "наводить красоту", когда время будет показываться уже на LCD экране.


А точнее?

Получать время - это хорошо. Но его же нужно еще уметь устанавливать. В конечном итоге - будем делать это с помощью кнопок, но пока... сделаем временное решение. Самое простейшие: забъем нужное нам время прямо в скетч. И будем его посылать модулю, если из Serial нам пришел символ 's'

Функция установки времени:

void setSomeTime(){
    RTC.stopClock();// останавливаем часы
    RTC.fillByHMs(19,15,0); // "подкручиваем стрелки на 19:15:00
    RTC.setTime();// отправляем "подкрученное время" самому модулю
    RTC.startClock(); // и опять запускаем часы
}

Функция которая слушает команды из Serial

 void doSerialCommands(){
  if(Serial.available()){ // что-нибудь пришло?
    char ch=Serial.read(); // читаем что пришло
    
    switch(ch){
      case 's': // команда устновки времени
           setSomeTime(); // устанавливаем
           break;
           
       // тут, в будущем, мы можем добавлять дополнительные команды
      default:;
           // на неизвестну команду - ничего не делаем
    };
  }
} 

Все. Нам осталось только вставить вызов doSerialCommand() в loop()  для регулярного опроса команда из Serial. После чего наш скетч принимает вид:

// Author: alxarduino@gmail.com
// Sample Clock App for http://alxarduino.blogspot.com

// Библиотеки необходимые для работы модуля часов
#include "Wire.h"
#include "DS1307new.h"

void setup(){
  Serial.begin(9600);
  if(RTC.isPresent()){ // обнаружен ли модуль?
    Serial.println("RTC Ready"); // все хорошо
  } else {
    Serial.println("Error!!!! RTC Module not found"); // сообщаем о проблеме
    while(1); // и останавливаем скетч
  }
}

void loop(){
  RTC.getTime();// получить время от модуля
  printTimeToSerial(); // выводим полученное время
  doSerialCommands(); // слушаем и выполняем команды из Serial
}

// Выводит текущие время в Serial
void printTimeToSerial(){

  byte static prevSecond=0; // тут будем хранить, сколько секунд было при прошлом отчете
  
  if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
    Serial.print(RTC.hour); // часы
  
    Serial.print(":"); // разделитель
    Serial.print(RTC.minute);
  
    Serial.print(":"); 
    Serial.println(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

// устанавливает часы модуля на какое-то заранее определенное время
void setSomeTime(){
  RTC.stopClock();// останавливаем часы
  RTC.fillByHMS(19,15,0); // "подкручиваем стрелки на 19:15:00
  RTC.setTime();// отправляем "подкрученное время" самому модулю
  RTC.startClock(); // и опять запускаем часы
}

// слушает из Serial команды и выполняет их. Каждая команда - один символ.
// доступны команды:
//  s - установить время указанное в функции setSomeTime()
void doSerialCommands(){
  if(Serial.available()){ // что-нибудь пришло?
    char ch=Serial.read(); // читаем что пришло
    
    switch(ch){
      case 's': // команда установки времени
           setSomeTime(); // устанавливаем
           break;
           
       // тут, в будущем, мы можем добавлять дополнительные команды
      default:;
           // на неизвестную команду - ничего не делаем
    };
  }
}


Заливаем.Открываем Serial Монитор. Набираем s (в нижнем регистре) и нажимает "отправить"

Итого:

  • Мы подключили модуль часов DS1307. 
  • Убедились что Ардуина "его видит" (и сообщает нам если не видит). 
  • Научились получать от модуля время и отправлять его в Serial.
  • Научились устанавливать время модуля по команде из Serial

В следующей части: мы подключим уже LCD и выведем время на него

воскресенье, 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 секунду.

суббота, 12 января 2013 г.

Тахометр для мотоцикла. Часть 1. Знакомство с задачей

Тахометр для мотоцикла. Часть 1 . Знакомство с  задачей

На дня, ко мне обратился знакомый с просьбой оказать помощь сделать тахометр для мотоцикла. Кроме "самого тахометра" его интересовало "познакомится с Ардуиной", которой я ему все уши прожужжал :)

В постановке задачи тахометр должен быть "стрелочный", а обороты двигателя приходить Ардуине в виде "импульсов".   Так же, конечной целью было желание что-бы стрелка могла пробегать весь диапазон за секунду максимум (или намного быстрее :)), но при этом не было видно "ее шагов". Все должно было быть "плавно". При этом - не должно сбиваться/дрожать от внешних вибраций (на мотоцикле их намного больше чем в машине).

И вишенкой на всем этом пироге "хотелок" было "положено" - недорого, из подручного хлама. То есть о покупке качественных, дорогих приводов, микросхем и проч. - речи не идет (как и покупке готового). В случае удачи - опыт будет распространен на другие приборы. В итоге будет новая приборная панель для Viper F5.

Привод стрелки планировалось сделать на шаговике (он уже был в наличии, из старого FDD). Это и предопределило мое согласие. В живую, держа в руках, я с шаговыми двигателями еще не игрался (хотя, конечно уже был знаком с ними "удаленно"), мне это было интересно.

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


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

Дробим на подзадачи:


Обычная архитектура для скетчей более сложных чем "мигаем диодом" является три блока/слоя:

Типичный блоки
  1. Блок чтения данных (кнопки, датчики)
  2. Блок принимающий решения основываясь на данных полученных на предыдущем шаге. Основная "логика" программы.
  3. Управление исполнительными устройствами. Исполнение того что "решил предыдущий блок". Включение моторов, реле, серв и т.п.

В случае тахометра "принимать решение" нам не нужно. Что прочитали, то и показали. Соответственно одна часть у нас "выпала" и остались две:
  1. Замер оборотов
  2. Управление стрелкой

Измерение оборотов

Мой товарищ был готов был сделать внешнюю обвязку которая будет "импульсы" конвертировать в "напряжение", которое очень просто прочитать Ардуиной через analogRead  (в отличие от меня - "схемотехники" у него опыта больше, и "железные решения" - не пугают), но я его отговорил. По старой программистской привычке: если что-то можно сделать программно (относительно просто) - будем делать программно, а не "железно". Не может сломаться та деталь которой нет :)
Итак решено: будем считать импульсы от датчика холла.

Управление стрелочкой

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

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

Заменой серве был выбран шаговый двигатель. Он менее удобен так как не умеет "помнить свою позицию", придется самому об этом заботится. Но нас этим не испугать :) . Шаговый двигатель был выдран из старого 3.5' дисковода (он отвечал за перемещение карретки) и....мы приступили (продолжение будет...)




Зачем мы здесь?

Зачем этот блог?

Этот блог возник как результат моей активности на форуме http://arduino.ru/forum под именем leshak.

Приходится раз за разом отвечать на одни и те же вопросы, бороться с одними и теми же типичными "ошибками не понимания". Надоело писать одно и тоже, с минимальными вариациями

Захотелось иметь место, куда можно "послать..." ;)

Ну и банально иметь "личное место" где можно поделится опытом. Рассказать о каких-то мелких успехах/поделках.

Сам я програмист с 15-ти летним стажем. Правда мой основной язык - C# (.net)

Ардуино и C++ - скорее хобби. Которое, со временем, стало не совсем хобби  :)
За два года, чуть-чуть начитался статей по схемотехнике, вспомнил что такое закон Ома, перестал путать за какой конец братся паяльник (да. да.. пару раз хватал паяльник не за тот конец).

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

OK. Не буду больше "растекатся по древу", начнем, а там посмотрим что выйдет. Как показывает практика планы и "что вышло" слишком часто оказываются разными субстанциями, так что "не загадывая в даль...."