Часто, когда нужно реализовать поддержку реального времени, разработчики AVR берут модуль часов, что-то типа DS1307, подключают его по I2C, и каждый раз когда надо узнать время, долго обмениваются с внешним модулем, потребляя миллиампер, а то и больше на подтягивающих резисторах, и при этом шина I2C оказывается ограничена по скорости. За сутки часы уходят на десяток секунд а то и больше. А когда надо более-менее точно отмерять границы секунд — подключают ещё внешнее прерывание. Зато хвастаются, что, де, время считается по кристаллу и при пропадании питания счёт времени сохраняется.
Но, господа! А знали ли вы, что микроконтроллеры AVR сами по себе могут работать с часовым кристаллом 32768 Гц, тактируя от него один из таймеров и позволяя реализовать учёт времени. Что? Кто сказал: "А как же энергонезависимость"? Разве я не писал статью про энергосбережение? Но, погодите, сейчас всё расскажу.
Рассказывать буду на примере полюбившегося всем ATmega328P.
Для наглядности я решил накропать небольшой демонстрационный проектик, но немного увлёкся и вот что получилось:
Часы реального времени
Если между пинами TOSC1 и TOSC2 подключить часовой кварцевый резонатор, то при установке бита AS2 в регистре ASSR, таймер 2 будет работать от резонатора.
В этом режиме микроконтроллер включает встроенные конденсаторы, порядка 18пФ на TOSC1 и 8пФ на TOSC2 (см. табличку 9-8 в разделе 9.5 даташита). Этих значений достаточно, чтобы уверенно вести кристалл со значением нагрузочной ёмкости 6пФ и ниже.
Но обычно мы имеем дело с дешёвыми резонаторами на 12,5пФ. В этом случае для стабильной работы требуется установить дополнительные конденсаторы. Общая суммарная ёмкость на каждом из выводов рассчитывается по формуле (см. в том же разделе 9.5):
Cвнеш + Свнутр + Cпараз = 2 * CL,
или
Cвнеш = 2 * CL — Свнутр — Cпараз
где, Cвнеш — необходимая внешняя ёмкость; Свнутр — внутренняя ёмкость, предоставляемая микроконтроллером (18/8пФ для TOSC1/2), и Cпараз — паразитная ёмкость линий, которая типично находится где-то в районе 3-5 пФ. CL — значение нагрузочной ёмкости для конкретного кварцевого резонатора.
Проще говоря, по грубым прикидкам для резонатора с нагрузочной ёмкостью 12,5пФ, TOSC1 можно ничего не вешать, а вот между TOSC2 и землёй подоткнуть что-то в районе 10пФ.
Готово! Теперь таймер2 у нас работает от кварца. Осталось настроить предделитель 1/128:
TCCR2A = (1 << CS22) | (0 << CS21) | (1 << CS20);
и теперь таймер будет переполняться 32768 Гц (частота кварца) / 128 (прескалер) / 256 (диапазон таймера) = ровно 1 раз в секунду.
Красота! Осталось повесить прерывание на переполнение таймера и считать секунды.
Работа с таймером в асинхронном режиме сопряжена с некоторыми особенностями. В частности, что следует помнить, изменения записываемые в регистры TCNT2, OCR2A, OCR2B, TCCR2A, TCCR2B не сразу попадают в таймер а только в определённые моменты работы тактового генератора. Поэтому некоторое время они могут быть заняты и в них лучше ничего нового не записывать. Чтобы узнать что они заняты можно проверить на состояние битов TCN2UB, OCR2AUB, OCR2BUB, TCR2AUB, TCR2BUB в регистре ASSR. Каждый из этих бит соответствует одному из вышеперечисленных регистров, и если бит читается как 1, то соответствующий регистр занят и нужно подождать.
PPM насколько это дофига?
Резонаторы различаются точностью. Например 20ppm, 50ppm, 100ppm. PPM — это parts per million, т.е. "миллионных долей". Иначе говоря кварц 100ppm может работать на 1 десятитысячную быстрее или медленнее чем ожидается.
Но много ли это? Если ваша комната длиной 5 метров, то 20ppm от этой длины — это 0,1 миллиметра. Найдите проволочку 0,1мм и постелите её на пол для сравнения. Да что там! Обойки на стенах меняют размер комнаты на 500ppm а то и больше!
Казалось бы, 20ppm — фигня война! Но не тут то было. В сутках у нас 86400 секунд. Значит таймер, который врёт на 20ppm, за сутки наврёт на 1,728 секунды или почти 52 секунды за месяц! Это что ж, каждые две недели часы подводить?
А уж что говорить про 50 и 100ppm…
"Ха!" — скажет кто-то — "я знаю что такое распределение Гаусса, возьму кварц 50ppm подешевле, а он по теории вероятности скорее всего окажется ближе к нулю…".
Ой не спеши, дорогой товарищ! Не так прост бизнес по изготовлению кварцевых резонаторов. А всё дело в том что отрезают они там лазером кусочки резонатора, но где-то хватят чуть побольше, где-то чуть поменьше. А затем пускают резонатор в тест. Если он показывает частоту в пределах 10ppm, то его кидают в партию 10ppm и продают втридорога, если укладывается в 20ppm — то в партии 20ppm продают подороже обычного, но не настолько. И так далее, в зависимости от того, какие по точности партии предлагает производитель. В итоге получается что 50ppm это, те кварцы, которые не попали в партию подороже, т.е. они гарантированно врут на 30-40ppm.
А ещё бывает брак, больше 100ppm (или какую максимальную допустимую величину установил завод), который скидывают в коробки и относят на мусорку. Но до мусорки они не доходят, потому что предприимчивые китайцы лепят эти кварцы на модули RTC, либо продают на али как "20ppm мамойклянус небитый некрашеный".
Итак, вот у вас в руках оказался этот кварц и вы чешите репку, почему же часы уходят на четверть минуты за день и как это дело вернуть. И тут нам поможет…
Коррекция хода часов
Когда учётом реального времени занимается целый микроконтроллер, можно вертеть как душе угодно. Например, внести периодическую коррекцию хода часов.
Просто через некоторые интервалы добавлять или проглатывать секунду — было бы слишком заметно, да и переключение секунд до момента корректировки происходило бы заметно не синхронно с "эталонными часами".
Поэтому мы, вместо целых секунд, будем корректировать значение счётчика таймера. Увеличивая или уменьшая его на 1, мы тем самым удлиним или укоротим секунду на 1/256.
Но, если мы делаем это в прерывании таймера по переполнению, то значение TCNT2, когда мы входим в прерывание, будет равно нулю, и если мы открутим его ещё на один шаг назад, т.е. сделаем 255, то прерывание снова сработает через 1 отсчёт таймера. В результате секунда вместо того, чтобы стать длиннее на 1/256, станет в 256 раз короче.
Чтобы провернуть фокус, сделаем хитрее: вместо того, чтобы ловить прерывание по переполнению, зарядим OCR2A на значение 255. И повесим прерывание по сравнению. Прерывание по сравнению срабатывает в конце указанного отсчёта таймера, т.е. прерывание стрельнёт в тот момент, когда таймер перекидывается с 255 на 0, в этом отношении для нас ничего не изменится: мы входим в прерывание и видим TCNT2 равным 0. Но у регистров сравнения есть замечательная особенность: если в счётчик TCNT записывается новое значение и после записи значение совпадает с регистром сравнения, то в этом отсчёте таймера прерывание по сравнению будет проигнорировано. Иначе говоря мы смело можем записать в TCNT2 значение 255, не боясь, что прерывание таймера сработает сразу же. Как раз то, что нужно.
Коррекция времени может происходить по следующему сценарию: пусть у нас есть знаковая 16-битная переменная, которая хранит величину коррекции, и такая же 16-битная переменная аккумулятор. При каждом прерывании, то есть один раз в секунду, увеличим (или уменьшим) аккумулятор на значение коррекции. Если аккумулятор переполнился (т.е. сменил знак на отрицательный при увеличении, или на положительный при уменьшении), то соответственно увеличим или уменьшим TCNT2 на один.
Если перевести на язык цифр, то при единичном значении величины коррекции, каждые 65536 секунд будет происходить коррекция на 1/256 секунды. Что равняется 5,1 мс за сутки или 0,06 ppm.
А максимально возможное значение корреции хода (32768) будет соответствовать коррекции на 1/256 секунды каждую вторую секунду, что равно коррекции на секунду за 8 минут 32 секунды, или 2 минутам 48,75 секундам за сутки, или 1953,1 ppm
В прерывании идёт учёт времени: часов, минут, секунд, значения которых хранятся в volatile-переменных. Считывая их мы можем узнать текущее время а считывая значение TCNT2 мы можем узнать доли секунды. Вот только если мы получили значение 255 — неизвестно, то ли это конец текущей секунды, то ли была коррекция хода часов на 1 пункт назад. Для простоты можно было бы тупо подождать перекидывания счётчика таймера, но я придумал хитрее: второй регистр сравнения, OCR2B, я заряжаю на значение 254. Т.е. когда таймер досчитывает до 255, флаг по второму сравнению оказывается установленным. В обработчике прерывания по первому регистру происходит сброс этого флага. Иначе говоря, если мы прочитали значение 255 и флаг OCF2B установлен — значит это конец секунды, а если сброшен — значит было прерывание и была коррекция, на самом деле следует его читать как 0.
Температурная коррекция
Какие бы красивые ppm не были обещаны производителем кристалла, они справедливы только для довольно узкого диапазона температур. На деле же кристалл снижает частоту как при уменьшении, так и при увеличении температуры, по параболе, вершина которой приходится примерно на +25°С, а снижение скорости типично составляет около 0,04 ppm/°С².

Т.е. при температуре +15°С или +35°С отклонение составит всего 4ppm, что примерно равно 0,35 секунды за сутки. Иначе говоря, при работе на комнатных температурах на это можно забить.
Однако, если мы, например, делаем часики для машины и температура бывает, скажем, 0 градусов по осени, и +50 на солнышке, то тут уже отклонение составит 25 ppm, или 2,1 секунду за сутки и более минуты за месяц. Здесь уже нужно обеспокоиться тем, чтобы скомпенсировать уход.
Но тут всё просто: нужно замерить температуру, вычислить поправку и обновить нашу величину коррекции таймера. Большая точность измерения температуры не нужна, плюс-минус пару градусов — и нормально.
Исходя из предположения, что микроконтроллер и кварц собраны в одной коробке и даже нагреваясь, микроконтроллер нагревает и кварц, а значит их температуры примерно равны, то нам сойдёт даже внутренний термодатчик, который есть в ATmega328P. Пусть он замеряет плюс-минус километр, если усреднить 4096 замеров, то точность оказывается на приемлемом уровне, нужно только подкорректировать его показания, чтобы замеренные 25 градусов на нём соответствовали 25 градусам в комнате.
Сохранение счёта и работа от батарейки
Сделать питание от батарейки дело не хитрое — два диода, вот и вся любовь. Батарейка-"таблетка" CR2032 новенькая выдаёт 3,2 вольта, а разряженная может давать 2,5, т.е. после диода у нас остаётся 1,8 — 2,5 вольта.
Хитрость состоит в том, чтобы максимально быстро и точно проинформировать микроконтроллер о том, что внешние питание-то уже тю-тю, и, скорее всего, всё внешнее оборудование уже либо перестаёт отвечать, либо вот-вот начнёт бредить от обесточивания — т.е. продолжать с ним общаться уже смысла нет никакого.
При этом, информационный сигнал не должен как-либо разряжать батарейку, которая обеспечивает резервное питание микроконтроллеру.
Для такой цели подойдёт схема на супервизоре питания на 3,15-3,3 В с открытым коллектором (напр. PST529G, MCP120-315 и т.п.):

Схема удерживает низкий уровень на входе PB0 микроконтроллера, когда напряжение внешнего питания недостаточно. Тут следует быть внимательным с электрическим уровнем на входе микроконтроллера, он не должен превышать 0,5 вольт от напряжения питания самого микроконтроллера, которое будет из-за диода на 0,7 вольт ниже напряжения в питающей цепи. Потому для указанной схемы важно использовать супервизор с открытым коллектором. Если использовать push-pull супервизор, то при установке верхнего уровня, напряжение на ножке МК превысит допустимые пределы. Для такого случая к выходу супервизора нужно подключить делитель.
Как вариант, можно собрать эквивалентную схему-супервизор на стабилитроне и компараторе с открытым коллектором — т.е. повторить то, что собрано внутри дискретного супервизора:

Или обойтись вместо компаратора простым транзистором, в этом случае схема будет работать наоборот: притягивать резистором вход микроконтроллера к земле, а транзистор будет притягивать его к верхнему уровню, когда питание в норме:

С учётом падения напряжения на базе транзистора около 0,7 вольт и примерно 0,1 вольта на резисторе, стабилитрон на 2,7 вольт сделает своё дело. Как только напряжение вырастет более 3,4 вольта, ток пойдёт через базу транзистора, и он притянет вверх сигнальную линию, сигнализируя что есть внешнее питание, и, наоборот, при недостаточном напряжении уровень будет низким.
Кстати, вместо стабилитрона вполне зайдёт обычный белый светодиод в прямом включении — т.к. он как раз начинает пропускать ток где-то при 2,5-2,7 вольт
В программной части, при пропадании питания нужно как-то завершить выполнение всех функций и перевести микроконтроллер в режим энергосбережения, а при появлении питания — начинать инициализацию с нуля.
Сначала я хотел сделать механизмом пуллинга: какая-нибудь периодически вызываемая функция проверяет: не пропало ли питание? Если пропало — возвращет 0, вызвавшая её функция проверяет, и тоже возвращает 0, и так далее до самого верха.
Но такой подход подразумевает слишком громоздкий код, кучу постоянных проверок. Поэтому я решил сделать немножко по-хакерски, назвал я подход "процедура ноль" или "routine zero":

Суть состоит в следующем: функция main делает необходимые инициализации, производит запуск подсистемы менеджера питания, тот проверяет — если питание есть, то хакерским методом стек очищается и управление передаётся на некую routine_zero, которая объявлена рядышком. При этом настраивается прерывание по изменению уровня на мониторящем питание пине, которое следит: если вдруг уровень стал низким, т.е. питание пропало — тут же, не выходя из прерывания, вызывает terminate_when_power_loss() и переводит микроконтроллер в режим power save. terminate_when_power_loss() — процедура также описанная где-то в главном коде, которая экстренно завершает работу всяких USARTов, SPIев, TWIев, освобождает порты, блокирует прерывания, кроме часов и т.д. — т.е. делает всё то, что необходимо исходя из предположения, что питание снаружи пропало и контроллер будет влачить жалкое существование на несчастной батареечке.
В режиме power save происходит учёт времени и сохраняется вся память, потребление находится на уровне примерно 1 мкА, микроконтроллер просыпается лишь раз в секунду, отработать учёт времени и при изменении уровня на сигнальном пине. Как только появляется внешнее питание, снова происходит хакерским методом сброс стека и вызов routine_zero().
Попробую объяснить всё то же, но чуть иначе: весь код приложения написан в процедуре routine_zero(), когда она выполняется, неважно чем занимается код в данный момент, если включены прерывания и питание пропало, выполнение кода обрывается прям посредине, при этом память сохраняет своё состояние. Вызывается terminate_when_power_loss() и затем происходит переход в режим энергосбережения, а как только питание появляется снова, routine_zero() запускается с самого начала, но вся память при этом имеет то же самое состояние что и до отрубания питания, и счёт часов продолжается.
Иными словами, можно просто писать routine_zero() не заботясь ни о чём, только сбросить все нужные глобальные переменные к начальным значениям ручками, потому как они, в случае перезапуска после пропадания питания, будут хранить прошлые значения.
Единственный минус, в реализованном примере в режиме энергосбережения не работает АЦП, поэтому замер температуры и перерасчёт корректирующего значения не происходит. Но корректировка времени продолжает выполняется в соответствии с последним вычисленным корректирующим значением.
Счёт времени при обновлении прошивки
Чтобы время не терялось при обновлении прошивки, я немного модифицировал свой проект загрузчика
Теперь при вызове загрузчика, приложение пакует показание часов в 32-битную переменную, показывающее количество секунд с 1.01.2000 00:00:00 и засовывает это значение в регистры OCR2B:OCR2A:GPIOR2:GPIOR1. А в загрузчике, при появлении флага переполнения таймера, это значение увеличивается на 1. При возврате из загрузчика происходит обратное преобразование значения к дате и времени. Таким образом на период работы загрузчика, счёт времени не прекращается, единственный минус — не происходит коррекции времени, но загрузчик работает обычно всего несколько секунд, так что потеря не большая.
Калибровка внутреннего тактового генератора
Минусом использования часового кварца для таймера на ATmega328P является то, что он задействует те же ножки, что используются для подключения системного кварца, т.е. работа возможна только от внутреннего RC-генератора, который калибруется с завода в пределах ±10%. Этого может быть недостаточно для работы UART. Но зато, сравнивая скорость изменения таймера 2, работающего от часового кварца, и таймера 1, работающего от системного RC-генератора, можно вычислить действительную скорость процессора. Затем, изменяя значение регистра OSCCAL можно подогнать скорость с достаточно высокой точностью. OSCCAL изменяясь на примерно 120 пунктов экпоненциально калибрует скорость примерно в двукратном диапазоне, т.е. позволяет установить требуемую частоту с точностью примерно 0,5%. Этого более чем достаточно.
В демонстрационном примере системный предделитель меняется программно и система работает на скорости 4МГц в активном режиме — это позволяет безопасно работать даже при падении напряжения до 1,8 вольт. При переходе в режим энергосбережения скорость роняется до 1 МГц, чтобы прерывание таймера гарантировано было длинее 1 такта осциллятора таймера, иначе есть риск повторного пробуждения, а также чтобы меньше циклов проводить в ожидании освобождения регистров таймера.
Демнострационный проект
Исходник на гитхабе: github.com/AterLux/RTC/
Проект собран для работы с OLED дисплеем разрешением 128x32 на базе контроллера SSD1306, подключенным по шине I2C (см.схему). Можно использовать дисплеи с SPI подключением, немного поменяв настройки в коде (см.исходники). Разрешения кроме 128х32 не поддерживаются.

Проект активно использует генерируемый исходный код посредством php.
Настроив php у себя, вы легко сможете добавить новый шрифт цифр, просто редактируя картинку.
Кстати, если посмотреть на шрифт цифр, то можно заметить что часто некоторые области цифр схожи. Например у 0 3 8 9 — одинаковая закруглённая верхушка, 0 и 6 — делят меж собой низ, и т.д. Поэтому вместо хранения цифр целиком хранятся отдельно горизонтальные блоки высотой 8 пикселей, и для каждой цифры список таких используемых блоков. Такой подход позволил уменьшить объём занимаемой цифрами памяти почти на треть и сэкономить более килобайта.
Другая забавная вещица в этом проекте — это локализация.
В проекте забиты два шрифта. Таблица символов использует ASCII символы начиная с кода 32 (пробела) по 126й, затем, начиная с 128го идут символы А.Яа.яЁёÄäЇїÖöÜüЎўЄєҐґß°²³
Формирование текстовых строк и перевод кодировки также автоматизированы в php-скрипте convert_strings.php.
Вы можете добавить поддержку нового языка в прошивку. Для этого в директории lang возьмите один из имеющихся файлов за основу. Назовите новый файл lang-<код языка>.txt, например, вы хотите добавить украинский язык, назовите файл, например, lang-ua.txt.
В первых строчках файла укажите:
NAME: название языка, например Українська
DEFAULT: код языка, из которого будут браться строки, если они отсутствуют в этом файле. Например ru
Затем, просто переведите строки после знака = и перекомпилируйте проект. Если php настроен правильно, то при сборке будет вызван скрипт, который преобразует строки из файла в исходный код, а в приложении после запуска, в меню выбора появится новый язык.
Напряжение питания должно быть больше напряжения на батарейке, т.е. минимум 3,3 вольта, но не более 5,5. Чем меньше напряжение, тем меньше потребление тока микроконтроллером. Рекомендуемое напряжение 3,6 вольт, тогда потребление всей схемы, вместе с дисплеем на средней яркости будет в районе 5 мА. При использовании в автомобильной цепи рекомендую задействовать импульсный преобразователь (на основе AP3211, ACT4060, TPS54331, ST1S10, и т.п. с рабочим током 1мА и ниже) тогда ток на входе преобразователя будет в районе 2-3мА и можно будет не беспокоиться о круглосуточной работе часиков от аккумулятора
В случае использования модуля дисплея с SPI (код нужно будет подправить и перекомпилировать, см display/displayhw.h) напряжение на микроконтроллере не должно более чем на 0,5 вольт быть больше напряжения на контроллере дисплея (3,3 В), т.е. общее напряжение питания, с учётом диода, не должно быть больше 4,3 вольт.
Программка для синхронизации времени и обновления прошивки: aterlux.ru/files/RTCsetup.zip

Чтобы залить прошивку в контроллер установите фьюзы следующим образом: Low — 0x62; High — 0xDE; Extended — 0xFE. Залейте программатором бутлоадер, затем подключите через UART и через программку залейте прошивку.
Внимание! Бутлоадер и прошивка читают значение из EEPROM, и обновляют значение OSCCAL, поэтому, если заливаете не в новый контроллер, убедитесь что EEPROM очищена.
Если частота контроллера с завода сильно отличается от номинальной, то связь через UART может быть нестабильной и прошивку загрузить не получится. Если так, то через программатор залейте прошивку, в прошивке выполните калибровку скорости ЦП (это запишет калибровочный байт в EEPROM), проверьте что связь по UART есть — получается синхронизировать время через UART. Затем установите фьюзы: Low — 0x62; High — 0xD6; Extended — 0xFE (это отключит очистку EEPROM) и залейте бутлоадер и снова попробуйте перепрошить через UART.
Для синхронизации времени сначала синхронизируйте время компьютера с интернетом, затем жмите кнопку "синхронизировать часы". Через день операцию можно повторить, и тогда, исходя из замеренного ухода часов, вместе со временем будет обновлено значение калибровки.
Проект свободный для некоммерческого использования. Если у кого получится собрать из этого часики в машину, или ещё какой осмысленный прибор — присылайте, размещу ссылку ниже.


Комментарии 9
При всем при этом замерял ток потребления с батарейки? На ATmega128, в моем текущем проекте, стоит кварц 16 МГц и поставлена батарейка CR2032, и работает нестабильно, а с BOD вообще не работает. Добавил вторую батарейку, скинул напряжение диодом. Вроде все работает, но потребление в режиме сна 1 мА, что много
Как сам кварц и осциллятор, так и микроконтроллер требуют большой ток на 16Мгц — несколько мА. CR2032 может просто не обеспечивать такой ток. Нужно переводить МК в режим глубокого сна и использовать кварц 32768, тогда потребляемый ток будет в районе единиц микроампер.
Когда-нибудь подключал кварц на 32768 гц как основной тактир?
В одном из проектов на ATmega128 были подключены и 16 мгц, и 32768 гц. Так вот, переключиться на 32768 нельзя никак, он может только таймер 0 тикать там. Потребление таки удалось оптимизировать — в схеме было много утечек по току, и даже удавалось от ионистора 1.5Ф сохранить ход часов псевдореального времени. Но тут лотерея с выходом из спящего режима, а BOD сводит пользу pRTC на нет, ибо напряжение в моменте сбрасывается до 1.7В, что триггерит сброс. В новой версии проекта поставим нормальные часы реального времени, но это уже другая история.
на шотки диодах падение меньше 0,2В
дружище, а можно просто собрать схему и залить HEX.если можно подскажи какие фьюзы ставить. а то уж больно сложно расписал.
спасибо.
Основательно и очень познавательно и доходчиво написано.
Ну вот, на неделю чтиво есть )))
Основательно, черт подери. Кстати, у атмела был аппноут про управление симмистором, так там для обнаружения перехода через 0 на ножку контроллера подавали всё те же 220ac через мегаом.
Если открыть схемы стиральных машин,
то увидите, что везде сделали так же,
в том числе и в тех машинах, что
собраны на атмегах.