В прошлой части статьи, мы остановились на этапе изготовления шилда.
На сегодняшний день, этот шилд уже выполнил несколько своих функций 🙂 По крайне мере, как подарок на День рождения! Признаюсь, что в софтовой части я много чего из задуманного не сделал (в силу тех или иных причин). Скажу так, на сегодняшний день, часы и будильник работают и радуют своего нового хозяина. Наверное, чуть-чуть станет полегче на работе, сделаю еще одну такую плату, чтобы продолжить работу над программной частью. А может, кто-то из моих читателей подтянется и поможет наполнить проект другими фичами 🙂
Эту часть статьи я бы хотел расписать помодульно. Т.е., рассмотреть работу с каждым отдельным элементом шилда. Возможно, некоторые идеи и реализации могут кому-то (мне-то, точно!) пригодиться в дальнейшем.
Спешу заметить, что эта часть оказалось ОЧЕНЬ объемной! Кому не интересно, листайте в конец, там ссылка на скачивание готового материала по данному проекту.
Просто, я очень хотел рассказать о тех моментах, которые для меня оказались новыми и важными. Дело в том, что этот шилд (в программной части) превосходит все, что я когда-либо делал до этого. И я очень благодарен и признателен своему корешу Александру за его терпение и полезные советы и ГЕРОИЧЕСКИЕ усилия по подталкиванию меня в нужном направлении!!!
ВАЖНО!!! Я постарался по максимуму не использовать готовые библиотеки и не копировать куски кода из интернета, без попытки понять, что же там происходит. Почему так? Одной из причин для такого шага — готовый код я планирую использовать в следующем проекте. А еще я планирую, использовать в дальнейшем этот скетч на самой ходовой и распространенной Atmega8.
Напомню из каких модулей состоят наши часы:
-> Буззер (пищалка)
-> светодиод (Alarm On/Off)
-> RTC микросхема (DS1307)
-> индикация
-> кнопки
Ну, не думаю, что есть необходимость в описании работы с буззером (пищалкой) — можно почитать тут.
И со светодиодом (digitalWrite Вам в помощь :)).
А вот о работе с выделенными (жирненьким и подчеркнуто) модулями хотелось бы сосредоточиться:
Итак, по порядку.
Ох, как же Ардуинщики любят эту микросхему! Для работы с ней уже написано несколько библиотек. (Навскидку, DS1307.h, RTClib.h и еще куча других… Не ленитесь, поищите). Но я решил их не использовать. «Мы все сделаем сами!!» :). Тем более, как оказалось, ничего в этом сложного нет.
Как оказалось, фактически, нам нужно только читать и писать в нужные ячейки (в нужном формате) нужные цифры 🙂
(Как однажды заметил один мой приятель, наблюдая как я пытаюсь играть на гитаре. «Так в игре на гитаре ничего сложного не вижу. Нужно просто в нужное время дергать нужную струну, зажатую на нужном ладу». Тогда меня эта фраза ОЧЕНЬ и ОЧЕНЬ повеселила).
Еще раз напомню, про карту ячеек DS1307:
— Для включения часов следует установить бит CH в ноль, это следует сделать принудительно, т.к. часы после включения по умолчанию выключены.
— Часы хранят информацию в двоично-десятичном виде
— 7 регистр отвечает за выходной тактовый генератор часов, SQW вывод.
Тестовый скетч:
//подключаем библиотеку i2c #include "Wire.h" //все устройства, поддерживающие i2c имеют свой адрес. Для DS1307- это 0x68 #define DS1307_ADDRESS 0x68 //это описание переменных, в которых будем хранить текущее время и дату byte second = 0; //секунды byte minute = 11; //минуты byte hour = 15; //часы byte dayOfWeek = 4; //дни недели byte dayOfMonth = 21; //день byte month = 4; //месяц byte year = 14; //последние две цифры года //------------------------------------- // Процедура конвертации из двоично-десятичной системы в десятичную //------------------------------------- byte bcdToDec(byte val) { return ( (val/16*10) + (val%16) );} //----------------------------------------------- // Ну и соответственно наоборот byte decToBcd(byte val){return ( (val/10*16) + (val%10) );} //------------------------------------- //------------------------------------- //процедура получения времени из DS1307 //с использованием указателей на соответ. переменные //------------------------------------- void getDateDs1307(byte *second, byte *minute, byte *hour, byte *dayOfWeek, byte *dayOfMonth, byte *month, byte *year) { // Указываем начальный адрес в DS1307 равным 0 Wire.beginTransmission(DS1307_ADDRESS); Wire.write(0); Wire.endTransmission(); Wire.requestFrom(DS1307_ADDRESS, 7); //запрашиваем подряд 7 байт *second = bcdToDec(Wire.read() & 0x7f); //чтобы бит CH не мешал *minute = bcdToDec(Wire.read()); *hour = bcdToDec(Wire.read() & 0x3f); // *dayOfWeek = bcdToDec(Wire.read()); *dayOfMonth = bcdToDec(Wire.read()); *month = bcdToDec(Wire.read()); *year = bcdToDec(Wire.read()); } //------------------------- //------------------------------------------------------ //Процедура записи байта в DS1307 (указываем адрес и значение) void writeByteDS1307(byte addr, byte toRecByte) { Wire.beginTransmission(DS1307_ADDRESS); Wire.write(addr); Wire.write(decToBcd(toRecByte)); Wire.endTransmission(); }// end Процедуры записи байта в DS1307 //------------------------------------------------------ void setup(){ Wire.begin(); //инициализируем протокол i2c Serial.begin(9600); //инициализируем СОМ-порт writeByteDS1307(0,0); // Запускаем часы (помните про бит CH?) writeByteDS1307(7,bcdToDec(0x010)); // Включаем режим генерации импульсов DS1307 с частотой 1Hz. } void loop(){ // считываем данные из DS1307 и выводим их в СОМ-порт getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year); Serial.print(hour); Serial.print(":"); Serial.print(minute); Serial.print(":"); Serial.println(second); Serial.print(dayOfMonth); Serial.print("/"); Serial.print(month); Serial.print("/"); Serial.print("20"); Serial.println(year); delay(500); Serial.println("-------------------------"); } //конец основного цикла
В этом «тестовом» скетче, мы реализовали возможность считывать данные из DS1307, сохранять данные в нужные ячейки и конвертировать данные в нужные форматы. Более чем достаточно.
А что же с импульсами на выводе SQW? Я их решил использовать для своей «внутренней» синхронизации. Дело в том, что чтение из RTC (по протоколу i2c) относительно медленное, а это значит, что постоянно считывать данные — это не самое рациональное решение. Поэтому время будем считывать только при старте программы, и затем каждый час. А в промежутке между синхронизацией время будем считать программно. (Т.к. у RTC и Ардуино разные тактовые генераторы, то время может незначительно расходится. Поэтому, для того чтобы время всегда было синхронно с DS1307, то подсчет «внутренних секунд» будем вести по тактовым импульсам с DS1307). Для этого будем использовать обработку внешнего прерывания.
attachInterrupt(1, blink, CHANGE);
Тут все просто. Используем внешнее прерывание 1 (Digital Pin №3). И в случае изменения состояния [CHANGE] (HIGH-> LOW или LOW->HIGH) переходим к выполнению функции «blink»
Далее я занялся вопросом вывода информации на экран.
Напомню наш вариант подключения индикаторов. Каждый сегмент индикатора подключен к выходам сдвигового регистра 74HC595. Для вывода нужной информации, мы должны:а) выставить на выходах 74HC595 нужную комбинацию уровней (1-если нужно что бы горел сегмент), и b) «посадить» катод на землю. С одним индикатором все просто. В случае вывода информации на четыре индикатора, эту процедуру нужно повторить для каждого разряда. В нашей схеме в единицу времени реально светиться может только ОДИН разряд! Значит «зажигать» разряды нужно достаточно быстро, чтобы создать иллюзию «статической картинки».
Теоретически подковались, переходим к программированию. В первом приближении это выглядело так:
byte lckPin=9; //подключение 74HC595 byte clockPin=10; //подключение 74HC595 byte dataPin=8; //подключение 74HC595 byte showDisp[4]; //это массив, который будет отображаться на индикаторах byte segmNum[4]={6,4,5,3}; //массив пинов, к которым подключены катоды индикаторов, 1сегм. - pin 6, 2 сегм. - pin 4 и т.д. // 0 1 2 3 4 5 6 7 8 9 byte Encode7seg[]={252,192, 181, 213, 201, 93, 125, 196, 253,221}; byte cur_digit=0; //глобальная переменная, хранящая номер сегмента, который будет отображаться void setup(){ pinMode(lckPin, OUTPUT); pinMode(clockPin, OUTPUT); pinMode(dataPin, OUTPUT); for (byte z=0;z<4;z++) { pinMode(segmNum[z],OUTPUT); } } void loop(){ byte timeDelay=2;//время, в течении которого индикатор будет включен for(byte i=0;i<99;i++){ pushDataToDisp(i,99-i); //заполняем буфер for(byte j=0;j<3;j++){ //для отображения информации, четыре раза подряд вызываем процедуру отображения сегмента showDisplay(); delay(timeDelay); }//конец цикла с j }//конец цикла с i }//конец основного цикла //------------------------------------------------ //---- процедура заполнения экранного буфера void pushDataToDisp(byte firstInd, byte secondInd) { showDisp[0]=Encode7seg[(firstInd/10)]; showDisp[1]=Encode7seg[(firstInd%10)]; showDisp[2]=Encode7seg[(secondInd/10)]; showDisp[3]=Encode7seg[(secondInd%10)]; }//end pushDisp //---- процедура вывода содержимого экранного буфера void showDisplay(){ digitalWrite(segmNum[(cur_digit-1) & 3],LOW); //выключаем все сегменты digitalWrite(lckPin,LOW); shiftOut(dataPin,clockPin,MSBFIRST,showDisp[cur_digit]); //заносим данные в сдвиговый регистр digitalWrite(lckPin,HIGH); digitalWrite(segmNum[cur_digit],HIGH); //включаем нужный сегмент cur_digit++; //увеличиваем счетчик сегментов if (cur_digit>3){ //если вышли из диапазона, сбрасываем значение cur_digit=0; } }//конец процедуры showDisplay
В таком виде все работает. Однако, такую важную процедуру лучше вызывать через равные промежутки времени, а то очень неприятно, когда изображение «мельтешит» или «тупит» или наблюдаются «фантомные» отображения.
И такая задача имеет свое решение — прерывания по таймеру. Хм, давайте разбираться. Погуглим на тему Arduino и таймеры…
Timer0 is a 8bit timer. In the Arduino world timer0 is been used for the timer functions, like delay(), millis() and micros(). If you change timer0 registers, this may influence the Arduino timer function. So you should know what you are doing. Timer1 is a 16bit timer. In the Arduino world the Servo library uses timer1 on Arduino Uno (timer5 on Arduino Mega). Timer2 is a 8bit timer like timer0. In the Arduino work the tone() function uses timer2.
Очень хорошо. Правда, выбор не особо богат.
Timer0 — мы трогать не будем, так как этот счетчик используется самой Ардуино.
Timer2 — Нужен для корректной работы функции генерации звука (tone()). Тоже не трогаем.
Остается Timer1. (Мы, вроде как, не собираемся использовать библиотеку работы с сервомашинками).
Важно!!! Я не утверждаю, что досконально разобрался с этим вопросом, но для небольшого, даже не «погружения», а так, «зайти по пояс» — вполне достаточно. Для желающих «погрузиться с головой» — читайте либо даташиты на контроллер, либо тут).
(Все изложенное ниже — мое субъективное понимание принципов работы таймер-счетчика).
Итак, что такое таймер? Таймер — аппаратный счетчик.
Представьте себе, строгого вахтера (Таймер-счетчик), который тупо считает проходящих мимо людей (тактовые импульсы) и при этом пишет на доске мелом цифру (TCNT1). А считать наш вахтер умеет только до 65535 (сказывается ЕГЭ, наверное). Вот так и сидит, считает. Старую цифру стирает, новую записывает. А как досчитал до последнего значения — вахтер может подать определенный сигнал 🙂 Подал сигнал, стер все с доски и опять считать. Сидит себе считает, никому не мешает 🙂 Следуя далее нашей абстракции, мимо вахтера может проходить не каждый человек (попробуйте представить себе чудо-проход в заборе, который может пропускать к вахтеру каждого восьмого, или тридцать второго человека). Такая избирательность называется предделителем.
А что вахтер (ну, он же счетчик)? Вахтеру нужно указать- нужно считать людей или нет (включить/выключить счетчик). Ну и конечно же вахтер может считать до 65535 (т.е., до предельного значения), либо считать до определенного числа (записанного на листочке- OCR1A). Первое — это называется «прерывание по переполнению», а второе — «прерывание по сравнению». И по каждому такому событию вахтер может подавать нужный сигнал обработчикам прерываний. Да, вахтеры бывают двух типов: одни умеют считать до 255 (это восьмибитный счетчик), а другие — до 65535. 🙂
Итак, первым делом нужно ПРАВИЛЬНО настроить таймер, и вторым — указать что делать в случае появления соответствующего прерывания.
Обработчики прерываний — это кусок кода, которому передается управление.
Для прерывания по переполнению свой код нужно размещать
ISR(TIMER1_OVF_vect){//код обработки прерывания по переполнению}
а для режима сравнения:
ISR(TIMER1_COMPA_vect){//код обработки прерывания по сравнению}
Ниже пример Blink, в котором светодиод меняет свое состояние 1 раз в секунду
//Пин, к которому подключен светодиод #define ledPin 12 byte ledState=0; //тут будем хранить состояние светодиода void setup(){ pinMode (ledPin,OUTPUT); //настраиваем пин на ВЫХОД //start --------- настраиваем прерывание по таймеру -------------- TCCR1A = 0; //сбрасываем регистры таймер счетчика TCCR1B = 0; TCNT1 = 0; TCCR1B |= (1 << CS12);// устанавливаем значение предделителя =256 //Т.е., за 1 секунду наш счетчик с таким предделителем насчитает //до 16000000/256=62500. OCR1A = 62500; // Указываем значение, по достижении которого сработает прерывание TCCR1B |= (1 << WGM12); // ставим режим работы CTC mode TIMSK1 |= (1 << OCIE1A); // разрешаем прерывание по сравнению //end ----------- настраиваем прерывание по таймеру -------------- } // end setup void loop(){ // Ваш полезный код } //end loop // А это процедура обработки прерывания ISR(TIMER1_COMPA_vect) { if (ledState == LOW) ledState = HIGH; else ledState = LOW; // пишем значение ledState в пин digitalWrite(ledPin, ledState); }//end ISR
ВАЖНО!!! Счетчик таймера "тикает" абсолютно никого не напрягая, т.е., не занимая процессорного времени. А вот обработчики прерывания - к сожалению, требуют вычислительных мощностей 🙂
Все? Нет не все!!! Как говорил один знакомый программист: "НЕЛЬЗЯ ДОЛГО ВИСЕТЬ В ПРЕРЫВАНИИ". Так, что давайте попробуем немного оптимизировать наш обработчик прерывания. Идея взята отсюда. А именно, прямая работа с битами порта. Поясняющая картинка:
(Естественно, нужный порт должен быть заранее настроен на OUTPUT). Пробуем и получаем такой вариант:
Как вы понимаете, тут еще остался небольшой потенциал 🙂
Я (по аналогии со светодиодом, где это прокатывало) наивно предполагал, что обновление всех сегментов должно происходить с частотой 60Hz. Т.е., каждый отдельный сегмент должен обновляться с частотой 60*4=240Hz. А это значит, что прерывание нужно вызывать 240*2=480 раз в секунду. Делаем расчет. 16000000/480=33333. Таким образом, при достижении счетчиком значения в 33333 и нужно вызывать прерывание. Однако, тут математика не прокатила. При таком варианте цифры смотрелись хорошо, а вот секундная точка вела себя очень не уверено (дрожала, как осиновый лист). Короче говоря, в итоге, я подбирал частоту сработки прерывания экспериментально. Добившись в конце концов и "нормальной" индикации, и "отзывчивости" на нажатие кнопок при значении 28000. Итого, вызов процедуры отображения одного сегмента индикатора будет происходить (16000000/28000=571 раз в секунду).
На таком варианте вывода изображения я и остановился.
Теперь займемся кнопками. Работа с кнопками реализована на конечном автомате (или по умному, Finite State Machine.). За алгоритм, еще раз огромное спасибо Александру. 🙂
(Чуть позже, я нашел у DiHalt-a (достаточно известная личность) очень похожее решение).
Вкратце, идея такова.
Для определения нажатия какой-либо клавиши, считываем значение с АЦП, к которому подключена наша клавиатура. По значению (точнее некому зазору значений) определяем номер нажатой клавиши.
Однако, кроме простого определения состояния кнопки (типа, НАЖАТА/НЕ НАЖАТА) нам нужно различать следующие варианты нажатия кнопки: НАЖАТА КОРОТКО, НАЖАТА И ОТПУЩЕНА, НАЖАТА И УДЕРЖИВАЕТСЯ и, допустим, долго удерживается, при этом срабатывает АВТОПОВТОР. Значит, нам нужно еще и контролировать время нахождения кнопки в нажатом состоянии. А еще, учитывая тот факт, что у нас в задумке устройство РЕАЛЬНОГО ВРЕМЕНИ (тут можно улыбнуться), мы не можем "тупить" (это я про всякие delay()) и тратить драгоценное время на сра.. несчастную кнопку. Итак, наш обработчик кнопок, при каждом своем вызове должен выполнять некие действия, учитывая результаты своей предыдущей работы.
Вот, как мог нарисовал блок-схему (рисовал и одновременно разбирался с Google-вской Он-лайн софтинкой):
Приведенная блок-схема - это вариант для обработки одной кнопки. В случае нескольких, нам еще придется анализировать и этот момент. Слава богу, это оказалось не сложным. Итак, в программе-обработчике нам фактически нужно "ловить" значение переменных НАЖАТАЯ КНОПКА и ФЛАГ.
if((pressBut==alarmButt)&&(buttonFlag==bf_short_release))
После обработки нажатия кнопок, сбрасываем переменную ФЛАГ.
Что бы не загромождать статью исходниками, в архиве есть все примеры.
Итак, наш основной скетч. (Обычно, все знакомые ПРОФЕССИОНАЛЬНЫЕ программисты говорят: "Лучше один раз увидеть исходник, чем сто раз слушать его описание". Но я все же, кратко опишу.)
Идея конечного автомата (которую мы применили в функции обработки кнопок) мне очень понравилась и я решил использовать этот же принцип и в основной программе.
Упрощенная блок-схема:
При первом запуске программы (setup):
- Настраиваем соответствующие пины на выход
- на DS1307 устанавливаем режим генерации импульсов с частотой 1 Hz
- считываем время установки будильника
- настраиваем все нужные прерывания
Прерывания. (у нас их два).
- 1) Прерывание по таймеру.
В этот прерывании происходит вывод на индикаторы содержимого "видео буфера" [showDisp].
- 2) По внешнему прерыванию (тактовые импульсы с DS1307), идет подсчет "внутренних" секунд, минут и часов!!! Остальные данные (дни, месяцы, год) мы ежечасно запрашиваем из RTC. Так же в этом прерывании происходит контроль необходимости подачи звукового сигнала будильника и необходимость запроса данных из RTC.
Основной цикл (loop):
- При необходимости запрашиваем текущее время с DS1307
- проверяем необходимость подачи звукового сигнала
- опрашиваем клавиши
- Переходим в нужный режим работы
Каждый режим работы (ВРЕМЯ, РЕДАКТИРОВАНИЕ, БУДИЛЬНИК и т.д.) - это состояния нашего конечного автомата. В которых выполняются ТОЛЬКО свои функции и обрабатывает нажатые кнопки (свойственные этому режиму). При необходимости указывается (согласно логики работы) следующий режим.
Приведенная блок-схема, весьма упрощенная. По идее, такие же схемы нужно рисовать и для каждого прерывания.
Ниже, я попытался (как смог) нарисовать диаграмму состояний (или граф переходов).
Немного поясню. Названия СОСТОЯНИЙ я использовал такие же как и в скетче (надеюсь, они получились "говорящими"). СОСТОЯНИЕ [EDIT_SEG] (которое на графе нарисовано 7 раз) - это одно состояние, в которое можно попасть из разных мест.
Честно говоря, я до недавнего времени считал, что в программировании, понятия "КОНЕЧНЫЙ АВТОМАТ" и "ФЛАГОВЫЙ АВТОМАТ" - одно и тоже 🙁 Ан, нет. Не все так просто. И вообще, теме "КОНЕЧНЫЙ АВТОМАТ" или "FINITE-STATE MASHINE" можно будет в дальнейшем посвятить целую статью.
Фу, честно говоря, притомился. Опишу теперь функционал часов, с точки зрения пользователя.
(Немного уточню. Это описание функционала программной части, который реализован на данный момент.)
Отображение на индикаторе времени в виде ЧЧММ. При этом раз в секунду моргает "разделительная" точка между часами и минутами. (В классических часах, обычно это двоеточие, но как говорится, что было под рукой).
Короткое нажатие Кнопки "+" переключает режимы отображения (ЧАСЫ (ЧЧММ)-> ДАТА (ДДММ)->ГОД (ГГГГ)).
По прошествии определенного времени (timeReturnMode=5 секунд), часы автоматически возвращаются в свой основной режим (отображение времени).
Длинное нажатие кнопки "SET" - переводит в режим корректировки текущей информации (т.е., находясь в режиме отображения ДАТА, корректировке будет подлежать сперва ДЕНЬ, затем МЕСЯЦ). Соответствующее корректируемое значение будет "мигать".
Корректировка (только инкрементальная) осуществляется нажатием клавиши "+".
(К сожалению, на данный момент не реализована проверка соответствия МЕСЯЦ -> макс. количество дней в этом месяце).
Сохранение изменений - длинное нажатие кнопки "SET".
Кнопка "ALARM" - короткое нажатие включает/выключает режим будильника. При включении режима будильника, значение установленного времени срабатывания будильника "мигает" и загорается светодиод, режим будильника включен.
Длинное нажатие кнопки "ALARM" позволяет установить время срабатывания будильника.
Сперва устанавливаем часы, затем минуты времени установки будильника (корректируемое значение будет "мигать").
Корректировка осуществляется нажатием клавиши "+".
Запись изменений - длинное нажатие кнопки "SET". Установленное время записывается в энергонезависимую память DS1307.
При срабатывании будильника, на индикаторе будет "мигать" установленное время срабатывания будильника (в течении 1 минуты) и при этом будет подаваться звуковой сигнал. Для выключения звукового сигнала достаточно коротко нажать кнопку "ALARM". При этом, режим будильника (ВКЛЮЧЕН) не изменяется, а только прекращается подача звукового сигнала.
Запрос времени с DS1307 происходит в момент инициализации, и каждый час.
Все! Алес! Как говорил генерал Михалыч в х/ф "Особенности национальной охоты", "Ну - всё, что знал, рассказал!!!" 🙂
Все материалы по данному проекту (фото, скетчи, схемы и т.п.) можно забрать ТУТ.
Небольшой P.S.
В ходе эксплуатации (а всего-то прошло две недели), у нового хозяина появилось несколько новых "хотелок". Возможно, когда нибудь что-то из этого я попробую добавить 🙂 Если дойдут руки...
- Возможность настройки режима "Подача звукового сигнала каждый час"
- Установка будильника на срабатывание в зависимости от дня недели (типа, контроля будние дни и выходные)
- Запись текущего состояния будильника (ВКЛ/ВЫКЛ) (при его изменении) в DS1307
- Отображение на индикаторе режима ДЕНЬ НЕДЕЛИ (вообще-то, пацан высказал пожелание чтобы это было реализовано линейкой светодиодов: пять зеленых и два красных. Но это ТОЧНО вопрос ОЧЕНЬ далекой перспективы)
- "Чтобы звуковой сигнал был поинтереснее" (т.е., проигрывал мелодии)
- Некая индикация важных дат и вывод соответствующего напоминания в COM-порт. (типа вбил в скетч дни рождения близких людей, а часы тебе про них напоминают)