Работа с Arduino

в 7:39, , рубрики: arduino, прерывания, таймер, метки: , ,

Как это было?

Когда у меня возникло желание вести разработку под Arduino, я столкнулся с несколькими проблемами:

  • Выбор модели из списка доступных
  • Попытки понять, чего мне понадобится кроме самой платформы
  • Установка и настройка среды разработки
  • Поиск и разбор тестовых примеров
  • «Разборки» с экраном
  • «Разборки» с процессором

Для решения этих проблем мною было просмотрено и прочитано довольно много разных источников и в этой статье я постараюсь сделать обзор найденных мною решений и методов их поиска.

Выбор платформы

Перед началом программирования под железяку требуется в начале ее купить. И тут я столкнулся с первой проблемой: оказалось, что разных *дуин довольно много. Тут есть и широкая линейка Arduino и примерно такая же широкая Freeduino и другие аналоги. Как оказалось, большой разницы, что именно брать, нет. То есть одни из этих устройств чуть быстрее, другие чуть медленнее, одни дешевле, другие — дороже, но основные принципы работы практически не отличаются. Отличия появляются практически только при работе с регистрами процессора и то я далее объясню, как по возможности избежать проблем.

Я выбрал платформу Arduino Leonardo как самую доступную по цене и имеющуюся на тот момент в Интернет магазине, в котором я всё и заказывал. Отличается она от остальной линейки тем, что у нее на борту установлен только один контроллер, который занимается и работой с USB-портом и выполнением тех самых задач, которые мы на наше устройство повесим. У этого есть свои плюсы и минусы, но напороться на них при первоначальном изучении не получится, поэтому забудем о них пока. Оказалось, что она подключается к компьютеру через micro-USB, а не USB-B, как вроде бы большинство других. Это меня несколько удивило, но и обрадовало, потому что я, как владелец современного устройства на Android'е без этого кабеля вообще из дома не выхожу.
Да, питается почти любая *дуино-совместимая железяка несколькими способами, в том числе, от того же кабеля, через который программируется. Также один светодиод почти у всех плат размещен прямо на плате контроллера, что позволяет начать работу с устройством сразу после покупки, даже не имея в руках вообще ничего, кроме совместимого кабеля.

Спектр задач

Я думаю, что прежде, чем взяться за как таковое написание чего-то под железяку, интересно понять, что на ней можно реализовать. С Ардуино реализовать получится почти что угодно. Системы автоматизации, идеи для «умного дома», контроллеры управления чем-нибудь полезным, «мозги» роботов… Вариантов просто уйма. И сильно помогает в этом направлении довольно широкий набор модулей расширения, чрезвычайно просто подключаемых к плате контроллера. Список их довольно длинный и многообещающий, и ищутся они в Интернете по слову shield. Из всех этих устройств я для себя посчитал самым полезным LCD экран с базовым набором кнопок, без которого по моему скромному мнению заниматься какими бы то ни было тренировочными проектами совершенно неинтересно. Экран брался отсюда, еще там есть его описание, а также с приведенной страницы ведут ссылки на официальный сайт производителя.

Постановка задачи

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

Первые шаги

Вот мне наконец приехали все купленные компоненты и я их собрал. Разъем экрана подключился к плате контроллера как родной, плата была подключена к компьютеру… И тут мне сильно помогла вот эта статья. Повторять то же самое я не буду.

Скрытый текст

Единственное скажу, что вспомнив молодость (а точнее первый «проект», собранный во время изучения радиоэлектроники во Дворце пионеров — мультивибратор с двумя светодиодами), я нашел 2 светодиода и поправил приведенный в статье пример и начал мигать ими :).

«Вторые шаги»

Следующим закономерным вопросом для меня стало «как работать с LCD экраном?». Официальная страница устройства любезно предоставила мне ссылки на архив, в котором оказалось 2 библиотеки с замечательными примерами. Только не сказала, что с этим всем делать. Оказалось, что содержимое нужно просто распаковать в папку libraries среды разработки.

После этого можно открывать пример GuessTheNumber.pde и заливать в плату аналогично примеру с мигающим светодиодом. Однако лично у меня после прошивки экран остался равномерно светящимся и без единой буквы. После недолгих поисков проблемы оказалось, что надо было просто подкрутить отверткой единственный имеющийся на плате экрана потенциометр чтобы задать нормальное значение контрастности.

Использованного в примере набора команд в принципе достаточно для простой работы с экраном, но если захочется чего-то большего, то можно открыть исходный текст библиотек LCDKeypad и LiquidCrystal и посмотреть что там есть еще.

Архитектура программы

Основная задача часов — считать время. И делать это они должны точно. Естественно, что без использования механизма прерываний никто не сможет гарантировать, что время считается с достаточной точностью. Поэтому вычисление времени точно нужно оставить им. Всё остальное можно вынести в тело основной программы. А этого самого «остального» у нас довольно много — вся работа с интерфейсом. Можно было бы поступить иначе, создать стек событий, создаваемый в том числе и механизмом обработки прерываний, а обрабатываемый внутри основного приложения, это позволило бы например заниматься обновлением экрана не чаще, чем раз в пол секунды (или по нажатию кнопки) но я посчитал это лишним для такой простой задачи, поскольку кроме перерисовки экрана процессору всё равно заняться нечем. Поэтому всё свободное время программа перечитывает состояние кнопок и перерисовывает экран.

Проблемы, связанные с таким подходом

Периодические изменения экрана

Очень хотелось сделать мигающие двоеточия между часами, минутами и секундами, чтобы как в классических часах они пол секунды горели, а пол — нет. Но поскольку экран перерисовывается всё время, надо было как-то определять в какую половину секунды их рисовать, а в какую — нет. Самым простым оказалось сделать 120 секунд в минуте и рисовать двоеточия каждую нечетную секунду.

Мелькания

При постоянной перерисовки экрана становятся заметны мелькания. Чтобы этого не возникало, имеет смысл не очищать экран, а рисовать новый текст поверх старого. Если сам текст при этом не меняется, то мелькания на экране не будет. Тогда функция перерисовки времени будет выглядеть вот так:

LCDKeypad lcd;
void showTime(){
  lcd.home();
  if (hour<10) lcd.print("0"); // Случай разной длины приходится обрабатывать руками
  lcd.print(hour,DEC); // английские буквы и цифры ОНО пишет само, русские буквы нужно определять программисту
  if (second %2) lcd.print(" "); else lcd.print(":"); // вот они где используются, мои 120 секунд в минуте
  if (minute<10) lcd.print("0");
  lcd.print(minute,DEC);
  if (second %2) lcd.print(" "); else lcd.print(":");
  if (second<20) lcd.print("0");
  lcd.print(second / 2,DEC);
  lcd.print(" "); 
  lcd.setCursor(0,1); // переходим в координаты x=0, y=1 то есть в начало второй строки
  lcd.print("    ");
  lcd.print(day,DEC);
  lcd.print(months[month-1]); // месяцы мне было приятнее нумеровать от 1 до 12, а массив с названиями от 0 до 11
  lcd.print(year,DEC);
}
Работа с кнопками

Похожая ситуация с кнопками. Нажатая кнопка числится нажатой при каждом прогоне программы, поэтому за одно нажатие может обработаться любое количество раз. Приходится заставлять программу ждать «отжимания» отдельно. Начнем основную программу так:

int lb=0; // переменная хранит старое значение кнопки
void loop(){
 // main program
  int cb,rb; // определим 2 переменные, для реально нажатой кнопки и для той, которую будет считать нажатой программа
  cb=rb=lcd.button(); // в начале можно считать, что это одна и та же кнопка
  if (rb!=KEYPAD_NONE) showval=1; // переменная указывает, что пока нажата кнопка не надо мигать тому, что настраивается
  if (cb!=lb) lb=cb; // если состояние кнопки изменилось, запоминаем новое, 
    else cb=KEYPAD_NONE; // иначе говорим программе, что все кнопки давно отпущены.

Работа с таймером

Собственно, чтобы вся работа с таймером состоит из двух важных компонентов:

  • Инициализации механизма прерываний от таймера в удобном для нас режиме
  • Собственно, обработки прерывания

Инициализация таймера

Для того, чтобы начать получать нужные нам прерывания, нужно настроить процессор таким образом, чтобы он начал их генерировать. Для этого нужно установить нужные нам регистры в нужные значения. Какие именно регистры и в какие именно значения нужно устанавливать, нужно смотреть в… даташите на процессор :(. Честно говоря, сильно надеялся, что эту информацию можно будет найти в документации на саму Arduino, но нет, это было бы слишком просто. Мало того, для разных процессоров серии номера битов могут отличаться. И я сам лично натолкнулся на то, что попытка установки битов в соответствие с даташитом на соседний процессор привели к плачевным результатам… Но тем не менее, всё не настолько печально, как может показаться, поскольку для этих битов есть еще и имена, они более-менее общие для разных процессоров. Поэтому использовать цифровые значения мы не будем, только имена.

Для начала вспомним, что в микроконтроллерах AVR таймеров несколько. Нулевой используется для вычисления значений delay() и тому подобных вещей, поэтому его мы использовать не будем. Соответственно, используем первый. Поэтому далее в обозначении регистров часто будет проскакивать единичка, для настройки скажем второго таймера нужно там же поставить двойку.

Вся инициализация таймера должна происходить в процедуре setup(). Состоит она из помещения значений в 4 регистра, TCCR1A, TCCR1B, TIMSK1, OCR1A. Первые 2 из них называются «регистрами A и B управления таймера-счетчика 1». Третий — «регистр маски прерываний таймера/счетчика 1», и последний — «регистр сравнения A счетчика 1».

Команды для установки битов принято использовать следующие (понятное дело, что вариантов много, но чаще всего используются именно эти):
BITE |= (1 << POSITION)
то есть вдвигаем «1» на POSITION бит справо налево и проводим логическое «или» между целевым и полученным байтами. При включении контроллера значения всех этих регистров содержат 0, поэтому о нулях мы просто забываем. Таким образом, после выполнения следующего кода

A=0;
A |= (1 << 3)

значение A станет 4.

Вариантов настройки таймера уйма, но нам нужно добиться от таймера следующего:

  • Чтобы таймер перешел в режим работы CTC (то есть в режим счета со сбросом после совпадения, «Clear Timer on Compare match»), судя по даташиту это достигается установкой битов WGM12:0 = 2, что само по себе означает установить биты со второго по нулевой в значение «2», то есть, «010», команда TCCR1B |= (1 << WGM12);
  • Поскольку 16МГц (а именно такая частота у кварцевого резонатора на моей плате) это много, выбрать максимально возможный делитель, 1024 (то есть только каждый 1024-ый такт будет доходить до нашего счетчика), CS12:0=5
  • Сделать так, чтобы прерывание приходило при совпадении с регистром A, для данного счетчика TIMSK1 |= (1 << OCIE1A)
  • Указать при достижении какого именно значения вызывать обработку прерывания, это значение помещается в тот самый регистр A счетчика 1 (целиком его название OCR1A), прерывание по совпадении с которым мы включали предыдущим пунктом.

Как посчитать, до скольки нам нужно проводить вычисления? — Легко, если тактовая частота кварцевого резонатора 16МГц, то при достижении счетчиком значения 16000 прошла бы секунда, если бы коэффициент деления был 1. Так как он 1024, то мы получаем 16000000/1024=15625 в секунду. И всё бы хорошо, но нам нужно получать значения каждые пол секунды, а 15625 на 2 не делится. Значит мы до этого ошиблись и придется взять коэффициент деления поменьше. А следующий по уменьшению у нас 256, что дает 62500 тиков в секунду или 31250 за пол секунды. Счетчик у нас 16-тибитный, поэтому может досчитать до 65536. Иными словами, нам его хватает и на пол секунды и на секунду. Лезем в даташит, потом в исходник и исправляем на CS12:0=4, а после этого OCR1A = 31249; (как я понял, один такт уходит то ли на на сброс, то ли еще куда, поэтому встречаются советы сбросить еще единичку с полученной цифры).

Обработка прерывания

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

Собственно, сейчас оно состоит из зарезервированного слова ISR и указания конкретного прерывания, которое эта функция обрабатывает в скобках. А внутри у этой функции как видите нет ничего фантастического. Даже обязательное RETI как видите за нас автоматом вставляет компилятор.

ISR(TIMER1_COMPA_vect) {
  digitalWrite(LEDPIN, !digitalRead(LEDPIN)); // LEDPIN=13. Эта строка мигает светодиодом на плате. Удобно и прикольно :)
  second++;
  if ((second %2) && lastshowval) { // эта и следующие 7 строк нужны только для того, 
    lastshowval = 0;            // чтобы можно было добиться этого забавного эффекта, как на аппаратных часах,
    showval = 0;       // когда в режиме настройки скажем минут, значение настраиваемого параметра мигает
  }
  if (!(second %2) && !lastshowval){   // только при отпущенных кнопках, а пока кнопки нажаты, оно просто горит.
    lastshowval = 1;
    showval = 1;
  }
  if (second>=120) {  // опять мои 120 секунд в минуте. Ну а кому сейчас легко?
    second-=120;
    minute++;
    if (minute>=60){
      minute-=60;
      hour++;
      if (hour>=24){
        hour-=24;
        day++;
        if ( 
          daylarge(day,month,year) // возвращает true если значение дня 
  // больше максимально возможного для этого месяца этого года. 
        ) {
          day=1;
          month++;
          if (month>12){
            month = 1;
            year++;
          }
        }        
      }
    }
  }
}

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

Автор: nioliz

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js