Как это было?
Когда у меня возникло желание вести разработку под Arduino, я столкнулся с несколькими проблемами:
- Выбор модели из списка доступных
- Попытки понять, чего мне понадобится кроме самой платформы
- Установка и настройка среды разработки
- Поиск и разбор тестовых примеров
- «Разборки» с экраном
- «Разборки» с процессором
Для решения этих проблем мною было просмотрено и прочитано довольно много разных источников и в этой статье я постараюсь сделать обзор найденных мною решений и методов их поиска.
Выбор платформы
Перед началом программирования под железяку требуется в начале ее купить. И тут я столкнулся с первой проблемой: оказалось, что разных *дуин довольно много. Тут есть и широкая линейка Arduino и примерно такая же широкая Freeduino и другие аналоги. Как оказалось, большой разницы, что именно брать, нет. То есть одни из этих устройств чуть быстрее, другие чуть медленнее, одни дешевле, другие — дороже, но основные принципы работы практически не отличаются. Отличия появляются практически только при работе с регистрами процессора и то я далее объясню, как по возможности избежать проблем.
Я выбрал платформу Arduino Leonardo как самую доступную по цене и имеющуюся на тот момент в Интернет магазине, в котором я всё и заказывал. Отличается она от остальной линейки тем, что у нее на борту установлен только один контроллер, который занимается и работой с USB-портом и выполнением тех самых задач, которые мы на наше устройство повесим. У этого есть свои плюсы и минусы, но напороться на них при первоначальном изучении не получится, поэтому забудем о них пока. Оказалось, что она подключается к компьютеру через micro-USB, а не USB-B, как вроде бы большинство других. Это меня несколько удивило, но и обрадовало, потому что я, как владелец современного устройства на Android'е без этого кабеля вообще из дома не выхожу.
Да, питается почти любая *дуино-совместимая железяка несколькими способами, в том числе, от того же кабеля, через который программируется. Также один светодиод почти у всех плат размещен прямо на плате контроллера, что позволяет начать работу с устройством сразу после покупки, даже не имея в руках вообще ничего, кроме совместимого кабеля.
Спектр задач
Я думаю, что прежде, чем взяться за как таковое написание чего-то под железяку, интересно понять, что на ней можно реализовать. С Ардуино реализовать получится почти что угодно. Системы автоматизации, идеи для «умного дома», контроллеры управления чем-нибудь полезным, «мозги» роботов… Вариантов просто уйма. И сильно помогает в этом направлении довольно широкий набор модулей расширения, чрезвычайно просто подключаемых к плате контроллера. Список их довольно длинный и многообещающий, и ищутся они в Интернете по слову shield. Из всех этих устройств я для себя посчитал самым полезным LCD экран с базовым набором кнопок, без которого по моему скромному мнению заниматься какими бы то ни было тренировочными проектами совершенно неинтересно. Экран брался отсюда, еще там есть его описание, а также с приведенной страницы ведут ссылки на официальный сайт производителя.
Постановка задачи
Я как-то привык при получении в свои руки нового инструмента сразу ставить себе какую-нибудь в меру сложную и абсолютно ненужную задачу, браво ее решать, после чего откладывать исходник в сторонку и только потом браться за что-нибудь по настоящему сложное и полезное. Сейчас у меня под рукой был очень похожий на составную часть мины из какого-нибудь голливудского кинофильма экран со всеми необходимыми кнопками, с которым надо было научиться работать, а также очень хотелось освоить работу с прерываниями (иначе в чем смысл использования контроллера?) поэтому первым же, что пришло в голову, оказалось написать часы. А поскольку размер экрана позволял, то еще и с датой.
Первые шаги
Вот мне наконец приехали все купленные компоненты и я их собрал. Разъем экрана подключился к плате контроллера как родной, плата была подключена к компьютеру… И тут мне сильно помогла вот эта статья. Повторять то же самое я не буду.
«Вторые шаги»
Следующим закономерным вопросом для меня стало «как работать с 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