Занимательная микроэлектроника — страница 91 из 117

  rcall HEX_time  ;имеем hex-секунды в temp

  mov count_sek,temp  ;переписываем в счетчик

           ;далее распаковываем для индикации: часы-минуты

   ldi ZL,Hour  ;распакованные в память

ld temp,Z

                 mov data,temp

                 andi temp,0b00001111  ;младший часов

  ldi ZL,DeH

                 st Z,temp

   andi data, 0b11110000  ;старший часов

swap data  ;старший в младшей тетраде

ldi ZL,DdH

                 st Z,data


                 ldi ZL,Min  ;распакованные в память

  ld temp,Z

                 mov data,temp

andi temp,0b00001111  ;младший минут

  ldi ZL,DeM

                 st Z,temp

                 andi data,0b11110000  ;старший минут

    swap data  ;старший в младшей тетраде

ldi ZL,DdM

                st Z,data

sei

ret


ReadTime:  ;чтения часов из памяти в порядке ЧЧ: ММ ДД. мм. ГГ

cli

           rcall ReadClk  ;сначала читаем из часов

ldi ZH,1  ;старший RAM

ldi ZL,Hour

           ld temp,Z

           rcall out_com  ;hour

ldi ZL,Min

           ld temp,Z;

    rcall out_com  ;min

   ldi ZL,Sek

           ld temp,Z;

rcall out_com  ;sek

    ldi ZL,Date

           ld temp,Z+;

rcall out_com  ;data

ld temp,Z+;

rcall out_com  ;month

  ld temp,Z;

           rcall out_com  ;year

sei

ret


Как видите, довольно длинно получилось, но ничего не поделаешь. Теперь мы находимся в следующей ситуации: часы установлены и идут сами по себе, в памяти МК имеются значения времени, которые туда записали при установке, есть еще регистр count_sek, в котором отдельно хранятся значения секунд в нормальном (а не BCD) цифровом формате. Осталось заставить МК отсчитывать время — сам по себе контроллер никогда не «узнает», который сейчас час.

Для этого мы и припасли прерывание от часов, которое происходит раз в секунду. В принципе мы могли бы каждое это прерывание читать значения времени из часов процедурой ReadClk, но это неудобно, т. к. процедура длинная и будет тормозить индикацию. Даже в ПК так не делали — там время отсчитывается BIOS при включенном компьютере самостоятельно. И нет никакой нужды этим заниматься, если мы можем считать время в МК: синхронизацию значений мы при включении питания или при установке часов делаем, а синхронизация хода часов обеспечена тем, что прерывания управляются от RTC. А считать секунды, минуты и часы совсем нетрудно и много времени не займет. Календарь же нам вести в МК не требуется, мы его правильный отсчет получим при чтении из устройства за счет того, что предварительно обновляем значения в памяти процедурой ReadClk (см. процедуру ReadTime в листинге 16.15).

Итак, вычеркнем опять из начального запуска процедуру инициализации Timer 1 (всю секцию Set Timer 1, вернув вместо нее ldi temp, (1< и out TIMSK, temp для инициализации только Timer 0, см. первоначальный текст в Приложении 5), уберем из текста обработчик прерывания TIM1_COMPA и вместо ссылки rjmp TIM1_COMPA в секции прерываний опять поставим команду reti.

Вместо этого в секции прерываний для внешнего прерывания INTO (во второй строке, сразу после rjmp RESET) заменим reti на rjmp EXT_INTO, а в начальную загрузку впишем инициализацию внешнего прерывания INTO:

;====== внешнее прерывание INTO

ldi temp,(1<  ;прерывание. INTO по спаду

out MCUCR,temp

ldi temp,(1<  ;разрешение. INTO

out GICR,temp

ldi temp,$FF  ;на всякий случай сбросить все флаги прерываний

out GIFR,temp


Теперь, если часы работают, у нас каждую секунду будет происходить прерывание INTO. В нем мы сначала займемся счетом времени, а потом записью во внешнюю flash каждые три часа. Для этого нам придется организовать довольно громоздкую процедуру сравнения времени с заданным. В нашем измерителе мы будем писать с т. н. метеорологическим интервалом (каждые три часа, начиная с 0 часов).

Но писать в память в определенные моменты времени — это еще не все. Метеоданные имеют смысл только, если они привязаны к абсолютному времени. Если же мы будем просто писать в память, как сейчас, то при чтении данных мы никогда не узнаем, когда именно была произведена первая запись. Но даже если мы запишем время включения прибора на бумажке (точно зная интервал, остальные кадры нетрудно привязать к абсолютному времени), то учесть отключения питания мы все равно не сможем. Зачем тогда было придумывать такой хитрый механизм сохранения адреса при сбоях?

Но и писать в память время каждого измерения нецелесообразно — оно займет минимум 5 байт, в нашем случае больше, чем сами данные. Потому мы поступим следующим образом: при начальной загрузке устанавливаем некий флаг (назовем его «флаг первичной записи»), который покажет, что это первая запись после включения питания. Если этот флаг установлен, то мы будем писать время в виде отдельного кадра, а точнее — двух кадров, потому что в один 4-байтовый кадр время + дата у нас не уместится. Можно в принципе и сэкономить, но сделать размер вспомогательного кадра времени кратным кадру данных удобно с точки зрения отсчета адресов во flash. Два кадра займут 8 байт, пять из них есть значение времени, а оставшиеся три мы используем так: будем придавать самым первым двум определенное значение ($FA). Тогда считывающая программа, встретив два $FA подряд, будет «знать», что перед ней кадры времени, а не данных, и их нужно интерпретировать соответствующим образом.

Тут мы учитываем тот факт, что ни данные (10-битовые), ни значения времени не могут содержать байтов, имеющих величину, когда старшая тетрада равна $F. Так что в принципе хватило бы и одного такого байта, но для надежности мы их вставим два подряд (благо их количество позволяет), и у нас даже еще один байт останется в запасе. И его мы также используем: будем писать в него значение регистра MCUCSR, в котором содержатся сведения о том, откуда ранее пришла команда на сброс. Отдельные биты в этом байте сбоев (БС) означают следующее:

• Bit 3 — Watchdog Reset Flag (БС = 08) устанавливается, если сброс был от сторожевого таймера;

• Bit 2 — Brown-out Reset Flag (БС = 04) устанавливается, если был сброс от снижения питания ниже 4 В;

• Bit 1 — External Reset Flag (БС = 02) устанавливается, если сброс был от внешнего сигнала Reset (характерно для перепрограммирования);

• Bit 0 — Power-on Reset Flag (БС = 01) устанавливается, если было включение питания МК.

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

Таким образом, после каждого включения МК у нас будет записываться кадр времени, и мы всегда сможем привязать данные к абсолютному времени и дате, даже если в записи был длительный перерыв. Это немного уменьшит полезный объем памяти, но т. к. сбои происходят относительно редко, подобное уменьшение можно не принимать во внимание.

Есть еще один момент, который связан с процедурой чтения данных из flash-памяти: коли мы считаем время в МК отдельно, то при такой длительной процедуре счет неизбежно собьется. Чтобы это исправить, нам требуется в самом конце процедуры ReadFullFlash инициализировать часы заново:

rcall RclockIni  ;заново инициализируем часы

Окончательный вариант программы измерителя с часами, суммирующий все, описанное ранее, довольно велик по объему (он содержит порядка 1300 строк, без учета включаемого файла i2c.prg), потому я его в книге не привожу. Его можно воссоздать, если последовательно делать в исходной программе измерителя из Приложения 5 (раздел «Измеритель температуры и давления на AVR», листинг П5.2) все рекомендованные мной изменения, начиная с листинга 15.9. Не доверяющие своей внимательности и просто ленивые могут его скачать с моей домашней странички по адресу http://revich.lib.ru/AVR/ctp.zip. В архиве содержатся оба необходимых файла: собственно программа ctp.asm и файл с процедурами I2С, который называется i2c.prg и полностью совпадает с тем текстом, что приведен в Приложении 5 и обсуждался ранее. Распакуйте их в одну папку и не забудьте еще приложить фирменный файл макроопределений m8535def.inc, после чего программу можно компилировать.

Заметки на полях

В этой программе есть потенциальная ошибка, хотя и не очень серьезная: если мы обратимся к какой-либо «длительной» процедуре (в данном случае это чтение содержимого flash), то при совпадении ее по времени с необходимостью записи данных последняя осуществлена не будет, и данные пропадут. Чтобы такое исключить полностью, надо отслеживать время и вблизи значения часа, кратного трем, запрещать такие процедуры. Можно поступить еще проще — устанавливать при чтении флаг первичной записи, и тогда пропущенная запись не приведет к сбою при анализе информации. Но здесь я не стал в такие моменты углубляться, т. к. совпадение все же крайне маловероятно (чтение занимает максимум полминуты, можно и вручную отследить момент), да и навредить оно вряд ли сможет, т. к. обычно после чтения оператором счетчик обнуляется и запись начинается заново.

Схема измерителя совпадает с приведенной на рис. 15.2, если добавить к ней изменения, представленные на рис. 16.5 и индикацию. Для индикации часов в программе предусмотрены незанятые выводы порта А (с