Глава 16 Некоторые последовательные интерфейсы МК

Звонок в офис интернет-провайдера:

— Алло! Это Интернет?

— Да, слушаем Вас!

— Соедините с www.yahoo.com.

smeshok.com

Все современные интерфейсы, предназначенные для обмена данными между некоторыми устройствами (USB, FireWare, Serial АТА, Ethernet и т. п.), — последовательные. Исключение составляют до сих пор распространенные IDЕ (АТА) — интерфейсы жестких дисков и совершенно уже устаревший, но еще «живой» интерфейс LPT, который когда-то использовался не только для подсоединения принтеров, но даже цифровых камер и сканеров.

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

о скоростях, превышающих единицы Мбайт/с (десятки Мбит/с), преимущества параллельной передачи становятся вовсе не столь однозначными. Ведь в параллельной линии отдельные проводники всегда немного разные, отчего при увеличении длины кабеля и скорости передачи биты, передаваемые по разным проводам, начинают «разъезжаться» по времени: одни приходят чуть раньше, другие чуть позднее. По научному это называется фазовым сдвигом. Этот самый сдвиг сказывается при достаточно высоких скоростях уже на очень небольших расстояниях, например, при стандартной ныне тактовой частоте системной шины ПК 533 МГц (и тем более при 1066 МГц), материнскую плату приходится проектировать гак, чтобы проводники, связывающие процессор и память, были строго параллельными и имели одинаковую длину. Учитывая, что число одних только линий данных доходит до 128, можно себе представить, какая головоломная задача встает перед конструкторами. Несравненно проще повышать частоту последовательного канала, ведь там за каждый такт передается всего один бит, и сам такт мы теоретически можем сделать сколь угодно коротким, т. к. все зависит только от быстродействия оборудования. Оказывается выгоднее заложить максимум функциональности в микросхемы, чем иметь дело с толстенными «шлангами» с сотней проводов внутри.

В микроконтроллерных устройствах с нашими объемами данных, конечно, скорость передачи нас волнует в последнюю очередь, но вот количество соединительных проводов — очень критичный фактор. Поэтому все внешние устройства, с которыми мы будем иметь дело в этой книге, будут иметь последовательные интерфейсы. Из всех доступных мы рассмотрим два, которых достаточно для удовлетворения первостепенных нужд: это классический интерфейс UART и универсальный I2С.

UART и RS-232

Сначала разберемся в терминах, которые имеют отношение к предмету разговора. В компьютерах есть COM-порт (в противном случае его всегда можно эмулировать через USB, как мы увидим в главе 18), часто ошибочно называемый портом RS-232. Правильно сказать так: COM-порт передает данные, основываясь на стандарте последовательного интерфейса RS-232. Последний, кроме собственно протокола передачи, стандартизирует также и электрические параметры и даже всем знакомые разъемы DB-9 и DB-25. UART (Universal Asynchronous Receiver-Transmitter, «универсальный асинхронный приемопередатчик») есть основная часть любого устройства, поддерживающего RS-232, но и не только его (недаром он «универсальный»), например, стандарты RS-485 и RS-422 также реализовываются через UART. Мы будем здесь рассматривать только RS-232, как самый простой.

Кроме UART, в состав RS-232 (в том числе в COM-порт ПК) входит схема преобразования логических уровней в уровни RS-232, где биты передаются разнополярными уровнями напряжения, притом инвертированными относительно UART. В UART действует положительная логика с обычными логическими уровнями, где логической единице соответствует высокий уровень (+3 или +5 В), а логическому нулю — низкий (О В). У RS-232 наоборот, логическая единица есть отрицательный уровень от -3 до -12 В, а логический ноль — положительный уровень от +3 до +12 В. Преобразователь уровня в МК, естественно, не входит, так что для состыковки с компьютером придется его «изобретать». Этот вопрос мы в подробностях рассмотрим в главе 18, поэтому схемы в этой главе будут лишены узла сопряжения с ПК.

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

Перед тем как обсуждать UART, рассмотрим подробнее, как, собственно, происходит обмен. Стандарт RS-232 — один из самых первых протоколов передачи данных между устройствами, он был утвержден еще в 1969 году, и к компьютерам (тем более ПК) тогда еще не имел никакого отношения. Идея этого интерфейса заключается в передаче целого байта по одному проводу в виде последовательных импульсов, каждый из которых может быть «0» или «1». Если в определенные моменты времени считывать состояние линии, то можно восстановить то, что было послано.

Однако эта простая идея натыкается на определенные трудности. Для приемника и передатчика, связанных между собой тремя проводами («земля» и два сигнальных провода «туда» и «обратно»), приходится задавать скорость передачи и приема, которая должна быть одинакова для устройств на обеих концах линии. Эти скорости стандартизированы, и выбираются из ряда 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 56000, 57600, 115200, 128000, 256000 (более медленные скорости я опустил). Число это обозначает количество передаваемых/принимаемых бит в секунду. Отметим, что стандарт RS-232E устанавливает максимальную скорость передачи 115200, однако функции Windows позволяют установить и более высокую скорость. Но не все схемы преобразования уровней могут пропустить через себя такие сигналы, и это следует учитывать при проектировании.

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

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

Такая же идея лежит в основе всех последовательных интерфейсов, они различаются только способами синхронизации. Например, в интерфейсе SPI, в том числе в его варианте для программирования МК, синхронизирующие импульсы передаются по отдельной, специально выделенной линии. Это облегчает задачу синхронизации, но требует большего количества проводов (не менее четырех, включая «землю»). А модемы или, к примеру, устройства Ethernet могут работать вообще всего по двум проводам, благодаря довольно сложному протоколу. Интерфейсы UART и 1C, которые мы будем изучать, требуют трехпроводного соединения (включая «землю»), однако различаются по назначению линий.

В RS-232 передача каждого байта всегда сопровождается начальным (стартовым) битом, который служит для синхронизации. После него идут восемь (или девять — при проверке на четность) информационных битов, а затем стоповые биты, которых может быть один, два и более, но это уже не имеет принципиального значения (почему — мы сейчас увидим).

Общая диаграмма передачи таких последовательностей показана на рис. 16.1.

Рис. 16.1. Диаграмма передачи данных по последовательному интерфейсу RS-232 в формате 8n2

Хитрость заключается в том, что состояния линии передачи, соответствующие стартовому и стоповому битам, имеют разные уровни: стартовый бит передается положительным уровнем напряжения (логическим нулем), а столовый — отрицательным уровнем (логической единицей), потому фронт стартового бита всегда однозначно распознается.

Подробности

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

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

Обычный формат данных, по которому работает львиная доля всех устройств, обозначается 8n1, что читается так: 8 информационных бит, no parity, столовый бит. «No parity» означает, что проверка на четность не выполняется. На диаграмме рис. 16.1 показана передача некоего произвольного кода, а также передача байтов, состоящих из всех единиц и из всех нулей в формате (для наглядности) 8n2. А в каком случае это важно, два стоповых бита передается или один (ведь, по сути, в состоянии линии при этом ничего не меняется)?

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

Из описанного алгоритма работы понятно, что погрешность несовпадения скоростей обмена может быть такой, чтобы фронты не «разъезжались» за время передачи/приема всех десяти-двенадцати бит бóлее чем на полпериода, т. е. в принципе фактическая разница частот тактовых импульсов может достигать 4–5 %. На практике их стараются все же сделать как можно ближе к стандартным величинам, но это не всегда возможно.

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

А какова надежность при передаче таким способом? Приемник RS-232 дополнительно снабжают схемой, которая фиксирует уровень не один раз за период действия бита, а трижды, при этом за окончательный результат принимается уровень двух одинаковых из трех полученных состояний линии, таким образом удается избежать случайных помех. Дополнительная проверка целостности данных (контроль четности) и/или программные способы (вычисление контрольных сумм и т. п.) нам не потребуется, т. к. скорости обмена малы и ошибки маловероятны. Данные о разновидностях соединительных кабелей вы найдете в главе 18.

Для работы в обе стороны нужны две линии, которые у каждого приемопередатчика обозначаются RxD (приемная) и TxD (передающая). В каждый момент времени может работать только одна из линий, т. е. приемопередатчик либо передает, либо принимает данные, но не одновременно (это т. н. полудуплексный режим — так сделано потому, что у UART-микросхем традиционно один регистр и на прием, и на передачу).

Замечание

Для AVR на самом деле это не так — UART может одновременно принимать и передавать данные. Но адрес регистра данных для приема и передачи один и тот же, потому со стороны выглядит, как будто регистры приема и передачи есть один регистр. В самых первых микросхемах UART это действительно так и было.

Кроме RxD и TxD, в разъемах RS-232 присутствуют также и другие линии,

о чем подробнее мы поговорим в главе 18. Отметим, что специально устанавливать состояния выводов порта (на вход или на выход), которые используются, как TxD и RxD, не требуется, как только вы «заведете» UART, они автоматически сконфигурируются, как надо. Только, в отличие от выводов программирования, их не рекомендуется задействовать еще для каких-то функций.

В AVR семейства Tuny (кроме модели 2313, которая все же, если позволительно так выразиться, «не совсем» Tuny) UART отсутствует, а в большинстве моделей семейства Mega этот порт реализован в виде более функционального USART («синхронно-асинхронного»), в некоторых моделях их даже несколько. USART полностью совместим с UART (кроме наименований некоторых регистров), и отличается от UART тем, что, во-первых, может самостоятельно обрабатывать девятибитовые посылки с контролем четности (не требуя программной реализации этого контроля), во-вторых, может иметь длину слова от 5 до 9 бит (UART только 8 или 9). Самое же главное его отличие (из-за которого он и получил свое название) в том, что его можно использовать в синхронном режиме, передавая по специальной дополнительной линии ХСК тактовые импульсы (в результате чего USART почти перестает отличаться от SPI, кроме того, что последний может работать значительно быстрее). Еще одна особенность USART — возможность работы в режиме мультипроцессорного обмена. Мы все эти режимы применять не будем, потому в дальнейшем будем вести речь только о UART, т. е. о работе в асинхронном режиме.

Прием и передача данных через UART

Перед работой с UART его следует установить в нужный режим, а также задать скорость обмена. Делается это, например, таким образом:

;для семейства Classic при частоте 4 МГц

       ldi temp,25  ;скорость передачи 9600 при 4 МГц

       out UBRR,temp  ;устанавливаем

       ldi temp, (1<<RXEN|1<<TXEN|1<<RXB8|1<<ТХВ8)

       out UCR,temp  ;разрешение приема/передачи 8 бит

Число BAUD для делителя частоты (в данном случае 25) можно определить из таблиц, которые имеются в каждом описании соответствующего контроллера (там же приводится и ошибка для выбранного значения частоты), или рассчитать по формуле: BAUD = fpeз/16(UBRR+1). Для семейства Mega процедура несколько усложняется, потому что регистров в USART больше:

;для семейства Меда при частоте 16 МГц

        ldi temp,103  ;9600 при 16 МГц

        out UBRRL,temp

        ldi temp,(1<<RXEN)|(1<<TXEN)  ;разрешение приема/передачи

        out UCSRB,temp

        ldi temp,(1<<URSEL)|(3<<UCSZ0)  ;формат 8n1

        out UCSRC,temp

Чем выше тактовая частота МК /рез, тем точнее может быть установлена скорость. При частоте кварца 4 МГц мы с приемлемой точностью можем получить скорости обмена не более 28 800 бод. Правда, при выборе специального кварца (например, 3,6864 МГц) можно получить с нулевой ошибкой весь набор скоростей вплоть до 115 200, но зато для других целей такие частоты неудобны. Для получения скоростей передачи выше указанных (стандартно COM-порт позволяет установить скорости, как указано ранее до 256 кбод) придется увеличивать частоту. Так, при кварце 8 МГц и общем коэффициенте деления, равном единице, мы получим скорость 250 000, что отличается от стандартных 256 000 на приемлемые 2,4 %.

Теперь перейдем к собственно процессу приему/передачи данных. В общем случае наша задача формулируется так: контроллер все время ожидает команды от компьютера, и при получении ее должен послать в ответ несколько байтов. Такой процесс можно организовать различным образом.

С UART связаны прерывания, причем в силу важности предмета тут их аж целых три: «передача завершена» (ТХ Complete), «регистр передатчика пуст» (ТХ UDR Empty) и «прием завершен» (RX Complete). Для их использования можно поступить следующим образом (примеры приведены для семейства Classic). Сначала вы инициализируете прерывание «прием закончен» (для чего надо установить бит RXCIE в регистре UCR). Возникновение этого прерывания означает, что в регистре данных udr имеется принятый байт. Процедуру обработки этого прерывания иллюстрирует листинг 16.1.

Листинг 16.1

UART_RXC:

    in temp,UDR  ;принятый байт — в переменной temp

     cbi UCR,RXCIE  ;запрещаем прерывание «прием закончен»

             <анализируем команду, если это не та команда — опять разрешаем прерывание «прием закончен» и выходим из процедуры

      sbi UCR,RXCIE

      reti

             В противном случае готовим данные, самый первый посылаемый байт должен быть в переменной temp>

      sbi UCR,UDRIE  ;разрешение прерывания «регистр данных пуст»

reti

Далее у нас почти немедленно возникает прерывание «регистр данных пуст». Обработчик этого прерывания состоит в том, что мы посылаем байт, содержащийся в переменной temp, и готовим данные для следующей посылки (листинг 16.2).

Листинг 16.2

UART_DRE:

    out UDR,temp  ;посылаем байт

    cbi UCR,UDRIE  ;запрещаем прерывание «регистр данных пуст»

          <готовим данные, следующий байт — в temp. Если же был отправлен последний нужный байт, то опять разрешаем прерывание «прием закончен» и далее выходим из процедуры, иначе выполняем следующий оператор:>

    sbi UCR,UDRIE  ;разрешаем прерывание «регистр данных пуст»

reti

Для семейства Mega (USART) вместо UCR в текст примеров надо подставить UCSRB. Обратим внимание на то, что после обработки первого прерывания переменная temp здесь может содержать подготовленный для отправки байт, и не должна в промежутках между прерываниями использоваться еще где-то. В противном случае ее надо сохранять, например, в стеке, или все же отвести для этого дела специальный регистр. Как видите, все довольно сложно.

Однако, как и в случае записи в EEPROM (см. главу 15), поскольку эти события (прием и передача) происходят относительно редко, на практике мы не будем использовать прерывания, и процедуры резко упростятся (на примере USART — листинг 16.3).

Листинг 16.3

Out_com:  ;посылка байта из temp с ожиданием готовности

           sbis UCSRA,UDRE  ;ждем готовности буфера передатчика

           rjmp out_com

           out UDR,temp  ;собственно посылка байта

ret  ;возврат из процедуры Out_com

In_com:  ;прием байта в temp с ожиданием готовности

           sbis UCSRA,RXC  ;ждем готовности буфера приемника

           rjmp in_com

           in temp,UDR  ;собственно прием байта

ret  ;возврат из процедуры In_com

Для семейства Classic надо заменить все UCSRA на USR. Для сформулированной ранее задачи непрерывного ожидания внешних команд обращение к процедуре In_com при этом вставляется в пустой цикл в конце программы:

Cykle:

        rcall In_com

          <анализируем полученный в temp байт, и что-то с ним делаем, например, посылаем ответ через процедуру Out_com>

rjmp Cykle  ;зацикливаем программу

При таком способе контроллер большую часть времени ожидает приема, непрерывно выполняя проверку бита RXC (в процедуре In_com), этот процесс прерывается только на время выполнения «настоящих» прерываний. Прерывания все равно должны выполняться много быстрее, чем байт, в случае, если он пришел, успевает в UDR смениться следующим (пауза составляет около 1 мс при скорости 9600, и за это время успеет выполниться порядка нескольких тысяч команд), так что мы ничего не потеряем. А процедура посылки Out_com сама по себе может выполняться долго (как и в случае с записью EEPROM, кроме самого первого обращения: задержка будет, если посылать несколько байт подряд). Но для программиста процедура также в основном будет заключаться в том, что контроллер будет ожидать очистки UDR, и т. к. это не прерывание, то ожидание в любой момент может быть прервано реальным прерыванием, и мы ничего не теряем (даже если длительность прерывания превысит время посылки байта, то это лишь вызовет небольшую паузу в передаче).

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

В общем и целом все эти нюансы следует иметь в виду, но на практике они почти не доставляют сложностей, за исключением одного момента, который мы еще обсудим в главе 17: если вам необходим переход в режим энергосбережения, то его объявление останавливает МК немедленно, как только встретится соответствующая команда. Если при этом в регистре данных UART оставался недоотправленный байт, то он так и не будет отправлен. Простыми задержками (например, выполнением пустого цикла) перед остановкой МК с этим явлением бороться неудобно (как мы говорили, нужно выполнить несколько тысяч команд). Лучше всего в таких случаях дождаться момента, когда регистр передатчика вновь окажется пуст (выполнением того же цикла непрерывной проверки UDRE, как в процедуре Out_com), и только тогда переходить к объявлению режима энергосбережения.

Отладка программ с помощью UART

Эти процедуры помогут мне выполнить давнее обещание и рассказать о том, как любую схему превратить в отладочный стенд. Вы просто временно расставляете в нужных местах программы контрольные точки в виде пар операторов:

move temp,RegX

rcall Out_com

Здесь RegX — регистр, значение которого хочется отследить в реальном времени. Если это регистр ввода/вывода, то вместо move надо использовать инструкцию in. Подсоединив схему к компьютеру (см. главу 18), вы будете получать на ПК значения требуемого регистра при каждом прохождении программой этой контрольной точки. Иногда это может нарушить нормальную работу программы, как мы говорили ранее, но даже с учетом этого обстоятельства такой способ много нагляднее, быстрее и дешевле, чем использование дорогих отладочных модулей в совокупности с AVR Studio.

Если вы исследуете программу, в которой работа с UART не предусмотрена, то ничего не стоит вставить его инициализацию туда временно, и также временно вывести проводочками выводы RxD и TxD на небольшой отладочный стендик, состоящий из одного-единственного преобразователя уровней UART/RS-232. Единственное неудобство — при перестановке контрольных точек программу придется каждый раз перекомпилировать и заново записывать ее в МК, но это все равно потребуется при ее правке. По этим причинам я стараюсь иметь компьютеры с двумя COM-портами: к одному из них подключается программатор, к другому — выход схемы. Если у вас есть редактор текста, позволяющий запускать компиляцию прямо из него, как описывалось в главе 13, то процесс отладки микропрограммы становится не сложнее, чем работа в среде Turbo Pascal, Delphi или Visual Basic. Правда, многое еще зависит от удобства программы, которая принимает данные в ПК. Этот вопрос мы также обсудим в главе 18.

Запись констант через UART

Научившись таким образом принимать и передавать данные через UART, мы можем внести изменения в нашу программу измерителя с тем, чтобы загружать коэффициенты в EEPROM без перепрограммирования системы. Для начала придется изменить схему самого измерителя, добавив к ней модуль преобразователя UART/RS-232, который будет подсоединяться к выводам 14 (RxD) и 15 (TxD) контроллера ATmega8535 (см. рис. 15.2). Саму схему мы рисовать пока не будем, различные варианты ее построения мы подробно обсудим в главе 18.

Инициализацию UART удобно производить в той же секции начальной загрузки, например, сразу после инициализации таймеров. Вставьте туда фрагмент задания скорости и режима, приведенный ранее (естественно, в варианте для семейства Mega, но с коэффициентом 25, а не 103, как в примере, т. к. у нас кварц 4 МГц), потом процедуры Out_com и In_com (например, в начало текста, сразу после векторов прерываний), а затем вместо пустого цикла в конце программы впишите следующий код:

Gcykle:

        cpi temp,0xE0  ;записать коэффициенты + 8 байт

        breq ргос_Е0

        cpi temp,0хЕ2  ;читать коэффициенты 8 байт

        breq ргос_Е2

rjmp Gcykle

Работает этот кусок программы так, как описано ранее: программа в основном непрерывно опрашивает бит RXC, «отвлекаясь» только на выполнение настоящего прерывания (Timer 0 в данном случае, см. главу 15). И только если через UART был принят какой-то байт (он окажется в temp), программа переходит к его последовательной проверке на одно из заданных значений.

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

В оформлении процедуры заложен один потенциальный баг: между выполнением приема байта (процедура in_com) и его анализом (cpi temp…) может «вклиниться» прерывание, и содержимое temp будет, скорее всего, испорчено. Чтобы избежать этой ошибки, можно поступить двояко: либо запретить на время все прерывания (по крайней мере, пока идет анализ), либо каждый обработчик прерывания начинать с команды push temp и заканчивать командой pop temp (что в больших программах может быть довольно сложно осуществить на практике). Между тем, учитывая относительную редкость обращения через UART, вероятность такого совпадения чрезвычайно мала (в данном случае ее можно оценить, как отношение среднего времени выполнения цикла анализа к промежутку между прерываниями Timer 0, что составит величину порядка 0,1 % — один шанс из 1000). И за все время работы по подобной схеме автор ни разу не получил такого сбоя. Потому я не стал усложнять программу, но, строго говоря, это неправильно, и грамотный преподаватель программирования обязательно сделал бы замечание. Чтобы соблюсти все правила, можно, например, в процедуру in_com вставить команду запрещения прерываний cli сразу по выходу из цикла опроса (перед оператором out udr, temp), а команду разрешения прерываний sei — в текст основной программы сразу после метки Gcykle. Только в этом случае следует помнить, что использование процедуры in_com (например, для отладки) всегда должно сопровождаться командой sei, иначе МК просто «сдохнет» в какой-то момент (зависнет). Кроме того, прерывания будут тогда запрещены и при выполнении команд, поступающих с компьютера, а это в общем случае не всегда желательно. Чтобы освободить себя от подобных размышлений, я и отказался от этой возможности. В крайнем случае команда с компьютера пропадет, ничего страшного — то же самое может быть при простом сбое обмена. Тем не менее помнить о том, что подобные баги возможны, следует всегда, в других случаях это может оказаться очень критичным.

В данном случае мы договариваемся, что значение $Е0 означает команду на перезапись коэффициентов в памяти, а значение $Е2 — чтение ранее записанных значений. Естественно, в первом случае программа обязана ожидать «сверху» дополнительно еще 8 байт значений, а во втором — наоборот, прочесть записанные в EEPROM коэффициенты и выдать их «наверх». Если же принятый байт не равен ни одной из этих величин, то программа спокойно возвращается по метке Gcykle и продолжает опрос бита RXC до следующего отправленного ей байта.

Для единообразия записи текста процедуры приема и отправки не вызываются напрямую, а дополнительно структурированы. Процедура приема организуется так:

рrос_Е0:  ;записать коэффициенты +8 байт

         rcall WriteKoeff

rjmp Gcykle

Метка proc_E0, как и метка ргос_Е2 далее, должны располагаться сразу после основного цикла Gcykle (потому что команда rjmp имеет ограниченное пространство действия, см. главу 13). Далее, где-то (в конце программы, например) записываем, наконец, собственно процедуру WriteKoeff приема коэффициентов и записи их в память. В ней мы учтем, что коэффициенты нельзя писать сразу в EEPROM, так как запись байта длится дольше, чем его прием через UART, и во избежание их потери необходим некий буфер. Но нам и не нужно его специально изобретать, т. к. коэффициенты все равно дублируются в SRAM, куда мы их первоначально и запишем. Если бы мы этого не сделали, то пришлось бы перезапускать контроллер после записи[15]. Сказанное иллюстрирует листинг 16.4.

Листинг 16.4.

WriteKoeff:  ;записать коэффициенты +8 байт

cli  ;запрещаем прерывания

        ldi ZH,1

        ldi ZL, tZH  ;начальный адрес SRAM

LoopWR:

        rcall in_com  ;принимаем следующий байт

        st Z+,temp  ;сложили в SRAM

        cpi ZL,pKL+1  ;до адреса pKL ровно 8 байт, см. листинг в Приложении 5

        brne LoopWR

;теперь коэффициенты находятся в SRAM, складываем в EEPROM clr ZH

        clr ZL  ;адрес EEPROM = 0:0

        ldi YH,1

        ldi YL,tZH  ;начальный адрес SRAM

LoopWE:

        ld temp,Y+  ;забираем из SRAM

        rcall WriteEEP  ;переписываем в EEPROM

        inc ZL  ;следующий адрес

        cpi ZL,8

        brne LoopWE

        ldi temp,$AA  ;все Ok, посылаем ответ

        rcall out_com

sei  ;разрешаем прерывания

ret

Если все благополучно, по окончании процедуры в компьютер будет послан байт со значением $АА. Если такой байт не получен, значит, что-то, например, потерялось по дороге, или произошел еще какой-то сбой.

Процедура чтения коэффициентов вызывается так:

ргос_Е2:  ;читать все коэффициенты 8 байт из EEPROM

          rcall ReadKoeff

rjmp Gcykle

А собственно процедура чтения (листинг 16.5) будет гораздо короче, т. к. не требуется спешить с приемом байтов и, соответственно, обращаться к SRAM (вообще-то нам безразлично, откуда получать коэффициенты, так что будем читать из оригинала — из EEPROM).

Листинг 16.5

ReadKoeff:  ;читать коэффициенты 8 байт из EEPROM

cli

           clr ZH

           clr ZL

LoopRE:

           rcall ReadEEP

           rcall out_com

           inc ZL

           cpi ZL,8  ;счетчик до 8

           brne LoopRE

sei

ret

Разобранный нами последовательный порт UART хорош своей изумительной простотой. UART в той или иной форме содержат практически все современные контроллеры, кроме самых простых, вроде семейства Tuny. Эта простота, однако, оборачивается и некоторыми недостатками. Во-первых, UART может работать только с заранее оговоренной скоростью обмена. Это неудобно, когда вы заблаговременно не знаете характеристики линии: при соединении с компьютером на столе можно задавать скорость и 115 200, а при необходимости передачи по километровому кабелю и скорость 9600, которую мы тут выбрали, окажется чересчур высокой (подробнее об этом см. главу 18).

Если не брать во внимание способ синхронизации по отдельной линии, как в USART (лишний провод нам ни к чему), то в принципе можно придумать способ, когда устройства соединяются на самой быстрой скорости, на которой это возможно: так, например, работает модем, который со стороны компьютера есть тот же UART, отличающийся только формой передачи сигнала по линии (в нем единицы и нули передаются не уровнями напряжения, а посылками различной частоты или фазы), отчего может работать на значительно больших расстояниях. Не так уж сложно решить и задачу автоопределения на одной стороне скорости обмена, заданной на другой: нужно попробовать соединиться на различных скоростях, и когда заранее оговоренный байт (или их последовательность) будет принят верно, значит, мы достигли нужной скорости.

Можно ли, однако, решить задачу так, чтобы скорость передачи задавалась с одного конца, как в синхронном обмене, но при этом избежать лишнего провода? Оказывается, вполне возможно, если работать на небольших скоростях. Но UART имеет еще один капитальный недостаток: он предназначен только для соединения не более чем двух устройств между собой. Режим мультипроцессорного обмена USART, о котором мы упоминали, есть попытка решить эту проблему, но лучше выстроить сразу такой протокол, при котором несколько устройств могут быть соединены между собой, и без помех обмениваться данными в нужном направлении. Все последовательные интерфейсы, кроме «чистого» UART, построены именно таким образом, включая и SPI, и USB и многие другие. В том числе и протокол I2С, который мы сейчас и разберем.

Последовательный интерфейс I2С

Собственно термин I2С принадлежит фирме Philips, которая придумала этот интерфейс, а в описаниях AVR «местный» вариант I2С называют TWI (от two-wire, «двухпроводной»). Мы не будем вдаваться в тонкости различий этих протоколов, потому что они нам, по большому счету, безразличны — главное, что они полностью совместимы, и все внешние устройства, имеющие интерфейс I2С, будут работать с AVR. Потому во избежание путаницы мы всегда будем употреблять более распространенный термин I2С.

Этот интерфейс использует два сигнальных провода, как и UART (плюс, конечно, «землю», поэтому физически это трехпроводной интерфейс, а не двухпроводной, как его часто называют), только по одному из них (SCL) всегда передаются синхронизирующие импульсы, а собственно информация — по второму (SDA). Информация в каждый данный момент времени передается только одним устройством и только в одну сторону. С помощью I2С может быть (теоретически) соединено до 128 устройств, так, как показано на рис. 16.2. «Подтягивающие» резисторы должны иметь номинал порядка единиц или десятков килоом (чем выше скорость передачи, тем меньше). В качестве их можно использовать встроенные резисторы выходных линий портов AVR, но автор не рекомендует это делать, поскольку их номинал слишком велик для обычных скоростей передачи (см. далее).

Рис. 16.2. Соединение устройств по интерфейсу I2С (общий провод не показан)

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

Разумеется, чтобы различить несколько устройств, каждое из них обязано иметь индивидуальный адрес. Он задается 7-битным кодом (восьмой бит байта адреса служит для других целей, как мы увидим), потому-то всего таких устройств на одной линии может быть 128. Адрес этот часто задается еще изготовителем, хотя в самом AVR он может быть, разумеется, задан программно, но для наших целей это не потребуется, и вот почему.

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

Типовой вариант обмена информацией по интерфейсу I2С показан на рис. 16.3.

Рис. 16.3. Обмен информацией по интерфейсу I2С

Кратко расшифруем эту диаграмму. Любой сеанс передачи по протоколу I2С начинается с состояния линии, именуемого Start (когда состояние линии SDA меняется с логической единицы на нуль при высоком уровне на линии SCL). Start может выдаваться неоднократно (тогда он называется «повторный старт»). Заканчивается сеанс сигналом Stop (состояние линии SDA меняется с логического нуля на единицу при высоком уровне на линии SCL). Между этими сигналами линия считается занятой, и только ведущий (тот, который выдал сигнал Start) может управлять ей (подробнее см. в [2]). Сама информация передается уровнями на линии SDA (в обычной положительной логике), причем смена состояний может происходить только при низком уровне на SCL, при высоком уровне на ней происходит считывание значения бита. Любая смена уровней SDA при высоком уровне SCL будет воспринята либо как Start, либо как Stop.

Процесс обмена всегда начинается с передачи ведущим байта, содержащего 7-битовый адрес (начиная со старшего разряда). Восьмой (младший!) бит называется R/W и несет информацию о направлении обмена: если он равен «0», то далее ведущий будет передавать информацию (W), если равен «1» — читать (R), т. е. ожидать данные от ведомого. Все посылки (и адресные, и содержащие данные) всегда сопровождаются девятым битом, который носит название «бит квитирования». Во время действия этого девятого тактового импульса адресуемое устройство (т. е. ведомый, который имеет нужный адрес при передаче адреса, или ведущий, если данные направлены к нему, и т. п.) обязан сформировать ответ (АСК) низким уровнем на линии SDA. Если такого ответа нет (NACK), то можно считать, что данные не приняты, и фиксировать сбой на линии. Иногда устройства не требуют отсылки бита АСК, и это учтено в процедурах, которые рассмотрены далее.

Заметим, что сигналы SCL совершенно необязательно должны представлять собой равномерный меандр со скважностью 2 — период их следования в принципе ничем не ограничен, кроме «терпения» приемника, который, естественно, ждет сигнала какое-то ограниченное время (иначе при нарушении протокола программа может зависнуть). Более подробно мы разбирать протокол не будем, так как вы легко можете найти его изложение в описании любого устройства, которое этот протокол поддерживает (в том числе и в описаниях AVR, изложенных по-русски в книге [2]).

Как видим, организовать обмен по протоколу I2С непросто, но это есть цена за универсальность и простоту электрической схемы. Большинство современных устройств с интерфейсом I2С могут работать с тактовой частотой до 400 кГц, но в силу не слишком высокой помехоустойчивости такой линии максимальные частоты лучше использовать только тогда, когда микросхемы установлены на одной плате недалеко друг от друга. При соединении проводами (например, МК с каким-нибудь датчиком) лучше ограничиться частотами до 100 кГц, а при длинных линиях связи (провода в полметра длиной и более) частоту обмена стоит снижать до 10–30 кГц.

Организовать обмен по интерфейсу I2С можно различными способами, и еще недавно это была исключительно программная эмуляция протокола. AVR семейства Mega (и только этого семейства) имеют I2С (TWI), реализованный аппаратно. Реализация эта, впрочем, не очень удобна, потому что не избавляет от необходимости «ручного» отслеживания различных этапов обработки сигнала, в результате чего программа получается не менее громоздкой, чем при программной эмуляции. Еще один способ — использование прерывания, которое связано с TWI, тогда можно разгрузить контроллер от многочисленных задержек (передача одного байта длится примерно 0,1 мс). В дальнейшем, чтобы не распыляться, мы будем применять более универсальную программную эмуляцию, которая имеет и некоторые преимущества: позволяет произвольно выбирать выводы для соединения (какие удобно, а не какие заданы аппаратной реализацией) и годится для абсолютно всех МК AVR, а не только для тех Mega, что имеют встроенный TWI. Единственное, чего мы лишимся — возможности «будить» контроллер, находящийся в «спящем» режиме (см. главу 17) обращением к нему через TWI, но нам это будет не нужно, т. к. контроллер всегда у нас находится в режиме ведущего. В фирменных Application Notes есть изложение процедуры программной эмуляции, но, как водится, с ошибкой в реализации.

Программная эмуляция протокола I2С

Наша задача будет формулироваться так: есть контроллер, и есть некое (одно или более) внешнее устройство. Нам надо прочесть/записать данные. Контроллер тут всегда будет выступать, как Master, а устройство — как Slave. Для того чтобы программно эмулировать протокол I2С, нам тогда придется сначала решить вопрос о том, как формировать тактирующую последовательность на линии SCL.

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

Для формирования импульса воспользуемся счетчиком cnt (пусть это будет регистр из числа старших — от r16 до r31). Пустой цикл, повторенный NN раз, тогда запишется так:

           ldi cnt,NN

cyk_delay: dec cnt

           brne cyk_delay

Посчитаем, чему должно равняться число NN. Пусть мы хотим обеспечить скорость передачи для I2С около 100 кГц, тогда длительность одного импульса (полпериода тактовой частоты) должна равняться примерно 5 мкс. Сам цикл занимает 3 такта (команда dec 1 такт + команда brne с переходом — 2 такта), т. е., например, при частоте кварцевого генератора 4 МГц он будет длиться 0,75 мкс. Итого, чтобы получить при этой частоте импульс в 5 мкс, нам надо повторить цикл 6–7 раз. Точно подогнать частоту не удастся, но нам это, как мы говорили, и не требуется: опыт показывает, что при ошибке даже в два-три раза работоспособность I2С практически не нарушается.

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

delay:  ;~5 мкс (кварц 4 МГц)

           push cnt

           ldi cnt,6

cyk_delay: dec cnt

           brne cyk_delay

           pop cnt

ret

Используя эту процедуру, можно сформировать весь протокол. Чтобы не загромождать текст этой главы, я вынес полный текст процедур обмена по I2С в Приложение 5 (раздел «Процедуры обмена по интерфейсу I2С», листинг П5.3). Подробно расшифровывать я его не буду, т. к. он полностью соответствует описанию протокола.

Указанный в Приложении 5 текст, кроме общих процедур посылки и приема байта (бесхитростно названных write и read), содержит процедуры для двух конкретных устройств: энергонезависимой памяти с интерфейсом I2С (типа АТ24) и часов реального времени (RTC) с таким же интерфейсом DS1307. Эти микросхемы имеют заданные I2С — адреса — при записи $А0 (10100000) у памяти и $D0 (11010000) у часов (соответственно, $А1 и $D1 при чтении, подробности см. далее). Сейчас мы займемся проектированием устройства, использующего эти возможности.

Как и сказано в Приложении 5, текст приведенной в листинге П5.3 программы следует скопировать и сохранить в виде отдельного подключаемого файла. Мы будем предполагать, что такой файл называется i2c.prg. Директиву. include "i2с. рrg" следует включать в текст программы обязательно после таблицы векторов прерываний, т. к., в отличие от файла макроопределений (m8535def.inc в данном случае), наш включаемый файл содержит команды, а не только инструкции компилятору. В принципе можно просто вставить текст из файла в основную программу (это и делает компилятор, когда встречает директиву include), только программа тогда станет совсем «нечитаемой».

Запись данных во внешнюю flash-пэмять

Задача, которую мы сейчас будем решать, формулируется так: предположим, мы хотим, чтобы данные с нашего измерителя температуры и давления не терялись, а каждые три часа записывались в энергонезависимую память. Разумеется, встроенной памяти нам надолго не хватит, потому придется прибегнуть к внешней. Схему измерителя (см. рис. 15.2) придется минимально доработать — так, как показано на рис. 16.4.

Рис. 16.4. Присоединение внешней EEPROM к измерителю температуры и давления

Здесь применяется энергонезависимая память типа АТ24С256. Она имеет структуру EEPROM (т. е. с индивидуальной адресацией каждого байта), но чтобы отличить ее от встроенной EEPROM, в дальнейшем мы будем внешнюю память называть flash (хотя это и не совсем корректно). Последнее число в обозначении означает объем памяти в килобитах, в данном случае это 256 Кбит или 32 768 байтовых ячеек (32 кбайт). Объем памяти в 32 кбайт кажется смешным в сравнении с современными разновидностями flash, которые уже достигают объемов 8 Гбайт, но, во-первых, для наших целей, как вы увидите, этого будет достаточно. Во-вторых, память принципиально больших объемов с интерфейсом ГС не выпускают — слишком он медленный.

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

Чтобы прочесть 32 кбайта со скоростями I2С, потребуется примерно 0,5 мс на каждый байт, т. е. около 16 с. Потому максимальный объем памяти с таким интерфейсом фирмы Atmel, к примеру, составляет 1 Мбит (АТ24С1024). Память с большими объемами представляет собой, во-первых, действительно flash-память (с блочным доступом), во-вторых, выпускается с интерфейсами побыстрее (как, к примеру, AT26DF321 объемом 4 Мбайта с 66-мегагерцевым интерфейсом SPI). Максимальный объем одного кристалла flash-пэмяти, достигнутый на момент написания этих строк — 8 Гбит (Samsung), более емкие устройства (flash-кэрты) представляют собой модули, собранные из нескольких подобных микросхем. Микросхемы с параллельным интерфейсом имеют стандартную разводку выводов и совместимы по выводам с любыми другими микросхемами памяти.

Кстати, найти в продаже микросхемы EEPROM с последовательным доступом (в том числе и использованную в нашей схеме АТ24С256) в корпусе DIP довольно сложно. АТ24С256 (как и упоминающаяся далее АТ24С512 и другие) чаще встречается в миниатюрном корпусе SOIC с 8 выводами. Так как присоединять в простейшем случае приходится всего 4 вывода (2 питания и 2 сигнальных), то даже при ручной разводке это не доставляет больших сложностей.

Время чтения можно сократить, если попробовать «выжать» из I2С все, на что он способен, но непринципиально. Да нам это и не требуется, потому что все равно эти данные мы будем передавать через UART примерно с такими же по порядку величины скоростями.

Адресация тут двухбайтовая, потому под адрес задействуется два регистра (AddrL и AddrH). Мы выбираем r24 и r25 (см. текст процедур в Приложении 5) — почему именно эти, вы увидите далее. Записываемые данные будут храниться в регистре DATA. Эти регистры являются входными переменными как для процедуры записи, так и чтения.

Теперь давайте определимся, что именно мы будем записывать, и на сколько нам хватит этой памяти. Базовый кадр данных у нас будет состоять из четырех байтов значений давления и температуры. Мы можем, конечно, писать и в распакованном BCD-виде, взяв подготовленные для индикации значения, но зачем загромождать память (кадр тогда состоял бы из 6 байтов, не четырех), если коэффициенты пересчета мы знаем (они у нас хранятся в EEPROM), и пересчитать всегда сможем. Если договориться на четырех байтах, то в наши 32 кбайта мы сможем вместить 8192 измерения (на самом деле чуть меньше, как мы увидим, но это несущественно), то есть при трехчасовом цикле (8 измерений в сутки) памяти нам хватит на 1024 суток, или почти на 3 года записей!

Как видите, даже такой объем вполне приемлемый. Если хотите увеличить еще в два раза — возьмите память АТ24С512, ее можно поставить сюда без изменений в схеме (и в программе, кроме задания максимального адреса). Схемотехника серии АТ24 предполагает возможность установки параллельно четырех или восьми таких микросхем (с заданием индивидуального I2С-адреса для каждой), так что при желании объем можно увеличить еще в четыре-восемь раз. Причем использовать, например, две АТ24С512 целесообразнее, чем одну АТС1024, так как для последней адресация усложняется (адрес для объема 128 кбайт содержит 17 бит и выходит за рамки 2-байтового).

Подробности

Микросхемы серии АТ24 имеют два (или три для микросхем с буквой В в конце обозначения, например, АТ24С256В) специальных вывода А0 и А1 (выводы 1 и 2 для 8-выводных корпусов), которые задают индивидуальный I2С-адрес. Если эти выводы ни к чему не подсоединять (как в нашей схеме), то считается, что они подсоединены к логическому нулю. Тогда I2С-адрес микросхемы при записи будет 10100000 в двоичной форме или $А0 в шестнадцатеричной (см. листинг процедур I2С в Приложении 5). Если на указанные выводы адреса подавать сигналы, то старшие 7 бит адреса такой микросхемы будут определяться формулой 10100А1А0. Таким образом, переходом от одной микросхемы к другой можно управлять, если подавать на эти выводы сигнал по дополнительным линиям, которые фактически будут 17-м и 18-м битами адреса.

Для того чтобы записывать исходные значения температуры и давления, нам их придется где-то хранить отдельно, отведя для этого специальные ячейки в SRAM. Сама запись производится очень просто: с каждым байтом мы увеличиваем на единицу содержимое счетчика адресов AddrH: AddrL (командой adiw — именно для этого и выбирались регистры r24 и r25, чтобы ее можно было использовать), «забиваем» нужный байт в регистр DATA, и вызываем процедуру WriteFlash.

Но тут встает две проблемы. Прежде всего, нужно решить, что делать, когда память закончится. Тогда следует либо обнулять ячейки и начинать запись заново, поверх младших адресов, либо, что гораздо красивее, остановить запись, пока содержимое ее не будет прочитано и адрес принудительно не будет обнулен. Поэтому потребуется какой-то флаг, сигнализирующий о том, что настал конец памяти. Причем отвести для этого флага, например, бит в регистре Flag, будет недостаточно: а что будет при сбое питания? Нам придется хранить где-то во встроенной EEPROM и этот флаг и, главное, текущий адрес памяти, иначе данные будут пропадать после каждого отключения питания. А для прибора, который может писать три года подряд, это «несолидно».

А как отсчитывать время, когда производить запись? Для того чтобы метеоданные были полноценными, их нужно привязать ко времени. И тут мы неизбежно приходим к тому, чтобы объединить часы с нашим измерителем. Этим мы займемся чуть далее, потому что использовать сам контроллер в качестве часов, как мы это делали в главе 14, здесь нецелесообразно, слишком много он всего делает такого, что может вызвать сбой в отсчете времени. Придется задействовать внешние часы, но подключение RTC заметно сложнее, чем памяти, потому мы рассмотрим этот вопрос позднее.

А пока, чтобы отработать процедуры обмена по I2С, договоримся, что запись в память у нас будет производиться по прерыванию Timer 1, который больше все равно в измерителе ничем не занят. При 4 МГц тактовой частоты и максимально возможном коэффициенте ее деления 1024, можно заставить Timer 1 срабатывать каждые, например, 15 с, для чего в регистр сравнения придется записать число 58 594 (проверьте!). С такой частотой память, конечно, заполнится очень быстро (32 кбайта — менее чем за 1,5 суток), но это, наоборот, удобно, если стоит задача проверить все наши процедуры.

Итак, записываем в секции определений программы измерителя, там, где адреса SRAM:

;Нех-данные — «сырые», без пересчета

.equ Thex = 0х0А  ;0А,0В — старший и младший байты температуры

.equ Phex = 0х0С  ;0C,0D — старший и младший байты давления

.equ FEnRAM = $0Е  ;флаг, если равен $FF, то писать во flash

Отдельно запишем адреса в EEPROM (первые восемь у нас заняты коэффициентами):

.equ FEnEE = 0x10  ;флаг если равен $FF, то писать во flash

.equ EaddrL = 0x11  ;младший байт тек. адреса

.equ EaddrH = 0x12  ;старший байт тек. адреса

Обратите внимание, что запись во flash разрешена, если байт FEnEE равен $FF, т. е. в самом начале, когда EEPROM еще пуста, запись по умолчанию разрешается. В процедуре обработки данных дописываем процедуры сохранения «сырых» значений температуры и давления по указанным адресам. Они у нас содержатся в регистрах AregH: AregL. В начале обработки данных по температуре, после имеющегося оператора rjmp prs дописываем:

     ldi ZL,Thex ;запоминаем температуру

     st Z+,AregH

     st Z,AregL

А там, где начинается расчет давления, после оператора rjmp contPT записываем:

     ldi ZL,Phex  ;запоминаем давление

     st Z+,AregH

     st Z,AregL

Теперь инициализируем таймер. В загрузочную секцию вместо строк инициализации Timer 0 (ldi temp, (1<<TOIE0) и out TIMSK, temp) добавляем:

;++++++++Set Timer 1

     ldi temp,high(58594)

    out OCR1АН, temp

    ldi temp,low(58594)

    out OCR1AL,temp

    ldi temp,0b01000000

    out TCCR1A,temp  ;переключающий режим для вывода PD5-0C1A

    ldi temp,0b00001101

    out TCCR1B,temp  ;1/1024 очистить после совпадения

    ldi temp, (1<<TOIEO)|(1<<0CIE1A)  ;разреш. прерывания

    ;по совпадению для Timer 1 и переполнению Timer 0

    out TIMSK,temp

К выводу OC1A (вывод 19 для ATmega8535) можно присоединить светодиод, который будет попеременно гореть и гаснуть с периодом 30 с, показывая, что запись работает.

Далее в секции начальной загрузки инициализируем регистры адреса. Получится довольно сложная процедура (листинг 16.6), которая должна проверять значения адреса в EEPROM, и если он есть (т. е. память не пуста и там не записаны все единицы), то еще и сравнивать его с последним возможным адресом (32767 или 7FFFh).

Листинг 16.6

:=======инициализация адреса flash

       clr ZH  ;старший EEPROM

       ldi ZL,EaddrL  ;младший EEPROM

       rcall ReadEEP

       mov AddrL,temp

       ldi ZL,EaddrH

       rcall ReadEEP

       mov AddrH,temp ;теперь в AddrH:AddrL адрес из EEPROM

       ldi temp,0xFF ;если все FF, то память была пуста

       ср AddrL,temp

       ldi temp,0xFF

       cpc AddrH, temp

       brne cont_1

       clr AddrH  ;если пуста, то присваиваем адрес = 0

       clr AddrL

       clr ZH  ;старший EEPROM

       ldi ZL,EaddrL  ;младший EEPROM

       mov temp,AddrL

       rcall WriteEEP  ;и записываем его опять в EEPROM

       inc ZL

       mov temp,AddrH

       rcall WriteEEP

cont_1:  ;теперь проверку на последний адрес $7FFF

       ldi temp,0xFF

       cp AddrL,temp

       ldi temp,0x7F

       cpc AddrH, temp

       brne cont_2

       sbr Flag,4  ;4 бит регистра Flag = конец памяти

cont_2:  ;загрузка байта разрешения записи flash

       clr ZH  ;старший EEPROM

       ldi ZL,FEnEE

       rcall ReadEEP

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

       ldi ZL,FEnRAM  ;младший RAM

       st Z,temp  ;сохраняем значение флага

Отдельный бит «конец памяти» в регистре Flag (бит 2, т. е. устанавливается он командой sbr Flag, 4, см. главу 13) нам понадобится позднее, для того, чтобы можно было временно запретить запись во flash внешней командой, не сбрасывая значения адреса и независимо от того, достигнут конец памяти или нет.

Теперь в секции прерываний заменим reti на rjmp TIM1_COMPA в строке для прерывания Timer1 Compare А (шестое сверху, не считая RESET), и напишем его обработчик (листинг 16.7).

Листинг 16.7

TIM1_COMPA:  ;15 секунд

;проверять разрешение записи во flash

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

          ldi ZL,FEnRAM

          ld temp,Z

          cpi temp,$FF

          breq flag_WF

          reti  ;если запрещено, то выходим из прерывания

flag_WF:

          ldi ZL,Thex  ;адрес значения в SRAM

          ld DATA,Z+  ;старший T

          rcall WriteFlash  ;пишем во flash

          adiw AddrL,1

          ld DATA,Z+  ;младший T

          rcall WriteFlash

          adiw AddrL,1

          ld DATA,Z+  ;старший Р

          rcall WriteFlash

          adiw AddrL,1

          ld DATA,Z+  ;младший P

          rcall WriteFlash

;проверяем адрес на 7FFF

          ldi temp,0xFF

          cp AddrL,temp

          ldi temp,0x7F

          cpc AddrH, temp

          breq clr_FE ;если равен, на clr_FE

          adiw AddrL,1  ; иначе сохраняем след, адрес:

          clr ZH

          ldi ZL,EaddrL  ;в EEPROM

          mov temp,AddrL

          rcall WriteEEP

          inc ZL

          mov temp,AddrH

          rcall WriteEEP

reti  ;выход из прерывания

clr_FE  ;если конец памяти:

          clr temp

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

          ldi ZL,FEnRAM

          st Z,temp  ;сбрасываем разрешение записи

          clr ZH  ;старший EEPR

          ldi ZL,FEnEE

          rcall WriteEEP

          sbr Flag,4  ;бит «конец памяти»

          clr temp

          out TCCR1A,temp  ;отмена переключающего режима для вывода PD5-OC1A

reti  ;на выход из прерывания

Как мы видим, здесь каждые 15 с идет запись в EEPROM текущего адреса (того, по которому должна производиться следующая запись), т. е. если в какой-то момент питание пропадет, то при следующей загрузке запись все равно начнется с текущего адреса. При достижении конца памяти отключится переключающий режим для вывода OC1A, и светодиод перестанет мигать, сигнализируя о конце памяти.

Отметим, что запись идет через каждые четыре байта, т. е. для заполнения 32 кбайтов внешней памяти придется 8192 раза обновить содержимое ячеек. Таким образом, для достижения теоретического предела по количеству циклов записи в EEPROM (100 тыс.) нужно как минимум двенадцать раз заполнить внешнюю память. На самом же деле число допустимых циклов еще намного больше (автор специально запускал запись каждые три секунды на пару месяцев, но сбоев так и не добился), потому можно не опасаться, что мы исчерпаем ресурс встроенной EEPROM.

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

Обратите внимание, что в процедурах I2С имеются псевдонимы: DATA и ClkA есть те же регистры, что и temp1 и temp2 в основной программе. Я предостерегал вас от такой практики, но в данном случае ничего страшного не произойдет, т. к. использование этих регистров разнесено во времени — обращение к I2С никогда не сможет произойти во время расчетов, где задействованы temp1 и temp2. В дальнейшем эти регистры нам где-нибудь еще пригодятся.

Чтение данных из памяти через UART

Теперь займемся процедурами чтения содержимого внешней памяти и осуществления установок разрешения-запрещения записи. Естественно, это придется делать через компьютер (а куда еще читать?), и мы используем уже инициализированный нами UART. Нам потребуется реализовать четыре процедуры:

• Запретить запись во flash.

• Разрешить запись во flash.

• Прочесть содержимое flash.

• Обнулить адрес flash, чтобы начать запись с начала.

Добавим в текст программы, в основной цикл (по метке Gcykle, обязательно после команды rcall in_com) следующие строки:

cpi temp,0xF0  ;запись во flash разрешить breq proc_F0

cpi temp,0xF1  ;запись во flash запретить breq proc_F1

cpi temp,0xF2  ;читать flash breq proc_F2

cpi temp,0xF8  ;запись во flash с начала, обнулить адрес

breq proc_F8

Посылая соответствующие команды ($F0 и т. д.) с компьютера, мы будем вызывать соответствующую процедуру. Проще всего оформить процедуры разрешения и запрещения так, как в листинге 16.8.

Листинг 16.8

proc_F0:  ;F0 запись flash разрешить

        rcall EnableFlash

rjmp Gcykle

proc_F1:  ;F1 запись flash запретить

        rcall DisFlash

rjmp Gcykle

EnableFlash:

cli

;сначала проверяем бит «конец памяти»

        sbrc Flag,2

        rjmp exit_FE

;проверяем байт разрешения, если нет, пишем его в память

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

        ldi ZL,FEnRAM

        ld temp,Z

        cpi temp,$FF

        breq exit_FA

        ldi temp,$FF

        st Z,temp

        clr ZH  ;старший EEPR

        ldi ZL,FEnEE

        rcall WriteEEP

        ldi temp,$AA  ;все Ok

        rcall out_com

sei

ret

exi t_FA:

       ldi temp,$FA  ;ответ в комп. — уже разрешен

       rcall out_com

sei

ret

exit_FE:

       ldi temp,$FE  ;ответ в комп. — конец памяти

       rcall out_com

sei

ret

DisFlash:

cli

       clr temp

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

       ldi ZL,FEnRAM

       st Z,temp

       clr ZH  ;старший EEPR

       ldi ZL,FEnEE

       rcall WriteEEP

       ldi temp,$AA  ;ответ в комп. — все Ok

       rcall out_com

sei

ret

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

Листинг 16.9

proc_F2:  ;F2 читать flash

        rcall ReadFullFlash

rjmp Gcykle

ReadFullFlash:

cli

        mov YH,AddrH  ;сохраняем текущий адрес в Y

        mov YL,AddrL

        clr AddrL  ;чтение начнем с начала памяти

        clr AddrH

loopRF:

        cp AddrL, YL  ;не дошли ли до текущего

        срс AddrH, YH

        breq end_RF  ;если дошли, то конец чтения

        rcall ReadFlash  ;собственно чтение

        mov temp,DATA  ;данные из DATA в temp

        rcall out_com  ;передаем наружу

        adiw AddrL,1  ;следующий адрес

        rjmp loopRF

end_RF:

        mov AddrH,YH  ;восстанавливаем текущий адрес

        mov AddrL,YL

sei

ret

Процедура эта будет долгой, если записан сколько-нибудь существенный кусок в памяти (для передачи 32 кбайт со скоростью 9600 потребуется порядка полминуты, да еще и чтение по I2С), и на все это время прерывания будут запрещены. Для нашего измерителя это выльется только в исчезновение на это время индикации, но могут быть ситуации, когда следует предотвратить выключение контроллера на такое время — например, чтобы не потерять данные, когда настанет момент очередной записи. В дальнейшем мы учтем этот момент (хотя это, как вы увидите, сильно усложнит программу). А пока

Листинг 16.10

proc_F8:  ;F8 clear address

        rcall ClearAddr

rjmp Gcykle

ClearAddr:

        cbr Flag,4  ;обнуляем бит конец памяти

        clr AddrH  ;обнуляем адрес

        clr AddrL

        clr ZH  ;и записываем его в EEPROM

        ldi ZL,EaddrL

        mov temp,AddrL  ;можно и просто clr temp

        rcall WriteEEP

        inc ZL

        mov temp,AddrH

        rcall WriteEEP

        ldi temp,$AA  ;ответ в комп. все Ok

        rcall out_com

sei

ret

Теперь у нас есть измеритель температуры и давления, записывающий данные во внешнюю память, откуда они могут быть прочитаны в любой момент. В главе 18 мы займемся вопросом приема данных через компьютер. А здесь нам осталось только объединить измеритель с часами, чтобы можно было задавать точный и достаточно длительный интервал измерения. И для этой цели нам также пригодится интерфейс I2С.

Часы с интерфейсом I2С

Моделей микросхем RTC, о которых мы говорили в начале главы 14, существует множество. Все они внутри устроены примерно одинаково, и имеют встроенный счет времени и календарь, а также функции будильника и/или таймера. Подавляющее большинство RTC имеют возможность автономной работы от батарейки в течение длительного времени, без потери однажды установленного времени. Такие часы обычно снабжены кварцем на 32 768 Гц, иногда даже встроенным в микросхему. Кроме этого, значительная часть моделей имеет дополнительный выход (иногда и не один), на котором формируется некая частота, задаваемая программно. Этот выход можно использовать для управления прерыванием микроконтроллера, и таким образом организовать счет времени и его индикацию.

Еще одна особенность микросхем RTC — величины времени в них традиционно представлены в десятичном виде (т. е. в упакованном BCD-формате). Именно так выдаются значения времени в RTC, встроенных в ПК. Например, число минут, равное 59, так и выдается, как байт со значением 59, но, как мы уже говорили, это не $59, что в десятичной системе есть 89! Соответствующее шестнадцатеричное число записалось бы как $ЗВ. BCD-представление удобно для непосредственной индикации, но при выполнении арифметических операций со временем (или, например, операций сравнения) его приходится преобразовывать к обычному двоичному виду. На самом деле это почти не доставляет неудобств, скорее наоборот.

Для наших целей выберем модель RTC под названием DS1307. Это простейшие часы с I2С-интерфейсом, в 8-выводном корпусе с внешним резонатором на 32 768 Гц, 5-вольтовым питанием и возможностью подключения резервной батарейки на 3 В (т. е. обычной «таблетки»). Схема переключения питания на батарейку встроенная и не требует внешних элементов. Имеется вывод для прерывания МК, который может программироваться с различным коэффициентом деления частоты кварца. Мы его запрограммируем на выдачу импульсов с периодом 1 с, по внешнему прерыванию от этих импульсов в МК будем считать секунды, обновлять значение времени и производить всякие другие полезные действия — точно так же, как мы это делали в часах из главы 14, только там отсчет времени производился внутренним таймером. Но здесь мы можем быть уверены, что при любых сбоях в МК время у нас будет отсчитываться верно.

Схема подсоединения DS1307 к нашему измерителю приведена на рис. 16.5. Обратите внимание, что выводы интерфейса I2С (5 и 6) здесь те же самые, что и для памяти. Выход программируемой частоты SQW у нас подсоединен к выводу внешнего прерывания МК. SQW мы должны запрограммировать на выдачу сигнала с периодом 1 с.

Рис. 16.5. Присоединение часов DS1307 к измерителю температуры и давления

Основное неудобство обращения с часами DS1307 — отсутствие состояния «по умолчанию», поэтому внутренние регистры могут при включении питания иметь произвольные значения. В частности, в этих часах в одном из регистров (том же, что хранит значения секунд) предусмотрен бит СН, который может погружать часы в «спячку» — если он установлен в единицу, то не работает генератор и даже невозможно определить правильность подключения. Есть и бит (в регистре управления), который отключает выход частоты на прерывания МК. По этим причинам после первого включения (если батарейка подсоединена — то только после первого), часы приходится инициализировать. Логика разработчиков проста: зачем кому-то нужны часы, которые не установлены на правильное время? Ну а если их устанавливать, то нетрудно и установить эти биты.

Так что сначала нам придется написать процедуру инициализации часов. Для этого в регистре управления DS1307 (он имеет номер 7) нужно установить бит 4, который разрешает выход частоты для прерывания, и обнулить младшие два бита в этом регистре, что означает частоту на этом выходе 1 Гц (подробности см. в описании DS1307, которое можно скачать с сайта maxim-ic.com). Но это еще не все: ранее мы говорили, что необходимо вообще завести часы, установив бит, который отвечает за работу задающего генератора. Это бит номер 7 в регистре секунд — здесь используется тот факт, что максимальное значение секунд равно 59 (напомним, что оно в BCD-форме, потому это равносильно значению $59), и старший бит всегда будет равен нулю. А если мы его установим, то часы стоят, и значение секунд не имеет значения. Потому мы совместим сброс этого бита с установкой секунд в нужное значение (соответствующий регистр самый первый, и имеет адрес $00). Сказанное иллюстрирует листинг 16.11.

Листинг 16.11

IniSek: ;секунды — в temp, если бит 7=1

           ;то остановить, иначе завести часы

           sbis PinC,pSDA  ;линия занята

           rcall err_i2c

     ldi ClkA,0  ;адрес регистра секунд

     mov DATA,temp

     rcall write_i2c

     brcs stopW

     ldi temp,$AA  ;все отлично

     rcall out_com

ret

IniClk:  ;установить выход SQW

     ldi ClkA,7  ;адрес регистра управления

     ldi DATA,0b00010000  ;выход SQW с частотой 1 Гц

     rcall write_i2c

     brcs stopW

     ldi temp,$AA  ;все отлично

     rcall out_com

ret

stopW:

             ldi temp,$ЕЕ  ;подтверждение не получено

             rcall out_com

ret

err_i2c:

             ldi temp,$AE  ;линия занята

             rcall out_com

sei

ret

Напомню, ЧТО процедура write_i2c (как и использующаяся далее read_i2c) для доступа к часам уже имеется в файле i2c.prg (см. Приложение 5). Процедурой IniSek мы можем при желании и остановить, и запустить часы. Если нужно остановить, то следует temp придать значение, большее 127. Если temp меньше 128, то в часы запишется значение секунд, и они пойдут. При обнаружении ошибок в компьютер (без запроса с его стороны) выдается определенный код: $АЕ, если линия занята, и $ЕЕ, если подтверждение со стороны часов (АСК) не получено. Если все в порядке, то выдается код $АА. Те же самые вызовы для выдачи кодов у нас будут в других процедурах обращения к часам.

Раздельные процедуры нам понадобились потому, что иногда часы идут, а выход на прерывание МК у них может оказаться отключенным. Тогда нам надо только его подключить, а время сбивать не следует. А когда вызывать эти процедуры? Прежде всего, при включении контроллера: мы помним, что при самом первом запуске часы следует заводить обязательно. Но могут быть и сбои при перебоях с питанием (на практике бывало так, что выход SQW при работе от батарейки самопроизвольно отключался). Для того чтобы правильно организовать процедуру, нам следует сначала выяснить, в каком состоянии часы находятся (листинг 16.12).

Листинг 16.12

ReadSet:

            sbis PinC,pSDA  ;линия занята

            rcall err_i2c

       ldi ClkA,7  ;адрес регистра управления

       rcall read_i2c

       mov temp,data  ;в temp значение регистра управления

       ldi СlкА,0  ;адрес регистра секунд

       rcall read_i2c  ;в data значение регистра секунд

brcs stopW

ret

Записав все эти процедуры в любом месте программы (но поблизости друг от друга, чтобы обеспечить беспроблемный переход на метку stopw), мы включаем в процедуру начального запуска такой фрагмент (листинг 16.13).

Листинг 16.13

;======инициализация часов =======

           rcall ReadSet  ;прочли установочные байты

                                 ;в temp регистр установок, в data секунды

          cpi DATA,$80  ;если больше или равно 128

          brsh setsek  ;то завести часы

          cpi temp,$10  ;если выход не установлен

          brne st_clk  ;тогда только его установка

          rjmp setRAM

setsek:

          clr temp

          rcall IniSek  ;устанавливаем секунды = 0

st_clk:

          rcall IniClk  ;установка выхода

setRAM:

          rcall Rclocklni  ;в любом случае чтение часов в память

...

Значение $10 регистр установок должен иметь, если мы ранее уже устанавливали часы. Процедура чтения значений часов RclockIni в память у нас отсутствует, и мы поспешим исправить это, включив в текст туда же, где находятся остальные процедуры для часов, еще две: ReadClk для чтения BCD-значений и RclockIni для преобразования их в распакованный формат (листинг 16.14). Предварительно зададим место в SRAM, куда мы будем складывать значения всех разрядов времени (включая календарь), и отдельно только часы и минуты, но распакованные (они могут пригодиться для индикации).

Листинг 16.14

;SRAM старший байт адреса SRAM=0x01

.equ Sek = 0x10  ;текущие секунды BCD-значение

.equ Min = 0x11  ;текущие минуты

.equ Hour = 0x12  ;текущие часы

.equ Date = 0x13  ;текущая дата

.equ Month = 0x14  ;текущий месяц

.equ Year = 0x15  ;текущий год

;распакованные часы

.equ DdH = 0x16  ;часы старш. дес.

.equ DeH = 0x17  ;часы младший дес.

.equ DdM = 0x18  ;мин старш. дес.

.equ DeM = 0x19  ;мин младш. дес.

;<начиная с адреса $20 у нас хранятся коэффициенты>

Rciockini:  ;инициализация часов

        rcall ReadClk  ;сложили часы в память

        ldi ZH,0x01;

        ldi ZL,Sek  ;адрес секунд в памяти

        ld temp,Z  ;извлекаем из памяти упакованные Sek

        mov count_sek,temp

        andi temp,0b11110000  ;распаковываем — старший

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

        ldi data,10

        mov mult10,data  ;в mult10 всегда будет 10

        mul temp,mult10  ;умножаем на 10 в r1:r0 результат умножения

        andi count_sek,0b00001111  ;младший

        add count_sek,r0  ;получили hex-секунды

                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

ret

ReadClk:  ;чтение часов

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

           ldi ZL,Sek  ;адрес секунд в памяти

           ldi ClkA,0  ;адрес секунд в часах

           sbis PinC,pSDA

           rcall err_i2c

           rcall start

           ldi DATA,0b11010000  ;I2С-адрес часов+запись

           rcall write

           brcs stopR  ;C=1 если ошибка

           mov DATA,ClkA  ;адрес регистра секунд

           rcall write

           brcs stopR  ;С=1 если ошибка

           rcall start

           ldi DATA,0b11010001  ; адрес часов+чтение

           rcall write

           brcs stopR  ;C=1 если ошибка

           set;CK

           rcall read  ;читаем секунды

           brcs stopR  ;C=1 если ошибка

           st Z +,DATA  ;записываем секунды в память

           rcall read  ;читаем минуты

           brcs stopR  ;С=1 если ошибка

           st Z+,DATA  ;записываем минуты

           rcall read  ;читаем часы

           brcs stopR  ;С=1 если ошибка

           st Z +,DATA  ;пишем часы в память

           rcall read  ;день недели читаем, но никуда не пишем

           brcs stopR  ;С=1 если ошибка

           rcall read  ;дата — читаем

           brcs stopR  ; С=1 если ошибка

           st Z +,DATA  ;дату записываем

           rcall read  ;месяц читаем

           brcs stopR  ;С=1 если ошибка

           st Z+,DATA  ;месяц записываем

           clt  ;НЕ давать АСК — конец чтения

           rcall read  ;год читаем

           brcs stopR  ;С=1 если ошибка

           st Z+,DATA  ;год записываем

           rcall stop

ret

stopR:

           ldi temp,$EE  ;подтверждение не получено

           rcall out_com

ret

Здесь нам пришлось оформить процедуру чтения из часов отдельно, прямым обращением к процедурам чтения через I2С, т. к. часы имеют специальный и очень удобный протокол. Если вы им один раз даете команду на чтение (значение адреса 0b11010001), то они начинают выдавать последовательно все значения регистров, начиная с того, к которому было последнее обращение прошлый раз. Здесь мы начинаем с регистра секунд и заканчиваем регистром года. Чтобы остановить выдачу, надо в последнем чтении и не выдавать подтверждение (АСК).

Прочитанные значения складываются в память (в исходном BCD-виде) и отдельно, в процедуре RclockIni, распаковываются для индикации. Об индикации мы тут подробно говорить не будем, вы уже знаете, как ее организовать (для этого надо добавить еще четыре разряда ЧЧ:ММ в обработчик прерывания по таймеру TIM0, см. окончательный вариант измерителя в конце этой главы), остановимся на применении полученных значений времени для наших целей своевременной записи температуры и давления.

Сначала нам еще надо обеспечить ход времени в МК (в память МК должны все время попадать текущие значения времени) и научиться устанавливать часы: пока мы их только «заводили» и устанавливали секунды. Для счета времени установим отдельный регистр-счетчик секунд (не читать же каждую секунду значения часов) и запомним, что его нельзя трогать:

def count_sek = r26 ;счетчик секунд

Теперь начнем с последней задачи: как установить нужное время? Для этого напишем процедуру (листинг 16.15), которая будет вызываться из компьютера по команде $A1. А по команде $А2 будем читать значение часов.

Листинг 16.15

Gcykle

        cpi temp,0xA1  ;установить RTC + 6 байт BCD, начиная с секунд

        breq ргос_А1

        cpi temp,0xA2  ;читать часы в комп.

        breq ргос_А2

rjmp Gcykle

proc_A1:  ;А1 установка часов

              rcall SetTime

rjmp Gcykle

proc_A2:  ;А2 читать часы в комп. из памяти

              rcall ReadTime;

rjmp Gcykle

              ;Процедура преобразования BCD в HEX, специально для времени HEX_time:

;на входе в ZL адрес секунды, часы или минуты на выходе в temp hex-значение,

                     ld temp,Z;

                     andi temp,0b11110000  ;распаковываем — старший

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

                     mul temp,mult10  ; умножаем на 10 в r0 результат

                     ld temp,Z;

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

                     add temp,r0  ;получили hex

ret

Sclock:  ;получить из компьютера 6 байт и записать в память

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

             ldi ZL,Sek  ;Ram

         rcall in_com

         st Z+,temp  ;sek

         rcall in_com

         st Z+,temp  ;min

         rcall in_com

         st Z+,temp  ;hour

         rcall in_com

         st Z+,temp  ;data

         rcall in_com

         st Z+,temp  ;month

         rcall in_com

         st Z,temp  ;year

push cnt  ;сохраняем cnt на всякий случай

              rcall SetClk  ;переписываем в часы

pop cnt

ret

Setclk:  ;установить часы

              sbis PinC,pSDA  ;линия занята

              rcall err_i2c

              ldi ZH,0x01

              ldi ZL,Sek  ;адрес секунд в памяти

          ldi ClkA,0  ;регистр секунд ld DATA,Z+ ;извлекаем секунды

          rcall write_i2c  ;секунды записываем

          brcs stops

          ldi ClkA,1  ;регистр минут

          ld DATA,Z+  ;извлекаем минуты

          rcall write_i2c  ;минуты записываем

          brcs stopS

          ldi ClkA,2  ;регистр часов

          ld DATA,Z+

          rcall write_i2c  ;записываем часы

          brcs stopS

          ldi ClkA,4  ;регистр даты (день недели пропускаем)

          ld DATA,Z+

          rcall write_i2c  ;записываем дату

          brcs stopS

          ldi ClkA,5  ;регистр месяца

          ld DATA,Z+

          rcall write_i2c  ;месяц записываем

          brcs stopS

          ldi ClkA,6  ;регистр года

          ld DATA,Z

          rcall write_i2c  ;год записываем

          brcs stopS

          ldi ClkA,7  ;регистр установок — на всякий случай

          ldi DATA, 0Ь00010000

          rcall write_i2c

          brcs stopS

          ldi temp,$AA  ;все отлично

          rcall out_com

   ret

stopS:

                 ldi temp,$EE  ;подтверждение не получено

                 rcall out_com

ret

SetTime:  ;установка текущих значений в МК

cli

           rcall Sclock  ;записали из компьютера BCD-значения

                 ldi ZL,Sek  ;упакованные секунды

                 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<<TOIE0) и out TIMSK, temp для инициализации только Timer 0, см. первоначальный текст в Приложении 5), уберем из текста обработчик прерывания TIM1_COMPA и вместо ссылки rjmp TIM1_COMPA в секции прерываний опять поставим команду reti.

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

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

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

out MCUCR,temp

ldi temp,(1<<INT0)  ;разрешение. 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 и индикацию. Для индикации часов в программе предусмотрены незанятые выводы порта А (с 4 по 7). Их следует присоединить к индикаторам по схеме, аналогичной остальным (как в часах из главы 14). Если вы не хотите использовать индикацию часов, то лучше вернуть процедуру по прерыванию Timer 0 в то состояние, которое она имеет в программе измерителя без часов (см. Приложение 5), тогда на каждый разряд будет приходиться относительно большая часть времени индикации. Еще один момент в схеме, который мы пока не обсуждали полностью — это соединение с компьютером, напомню, что его мы будем разбирать в главе 18.

Обращаю ваше внимание также, что в тексте программы ctp.asm, процедуре записи во flash (строка 496), есть закомментированный оператор rjmp mmi. Если его раскомментировать, то запись будет происходить каждую минуту, что позволит выполнить проверку записи во flash, в том числе отследить корректность последнего адреса.