Альтернативная прошивка набора мастер-кит NM8036

 

Альтернативная прошивка набора мастер-кит NM8036.
Часть первая: Часы-термометр.

Многие из радиолюбителей слышали про таймер-термостат-часы «Мастер-Кит» NM8036 (BM8036) и даже приобрели его. Однако присущий разработке ряд недостатков, а также то, что невозможно разработать универсальное устройство, которое подходило бы всем, я решил начать разработку собственной альтернативной прошивки к набору. Цикл данных статей расскажет читателю о том, как самому разработать программное обеспечение устройства и модернизировать его под собственные нужды. Ведь всем не угодишь… И так начнём…

Для начала нам необходима отладочная плата. Можно воспользоваться набором NM8036, а можно изготовить собственную, например, описанную в [2] (кстати, моей разработки, скачать которую в формате lay можно здесь). Изготовление собственной отладочной платы позволяет в будущем использовать её в готовом устройстве и сэкономить немалую сумму на приобретении. Далее необходим комплект радиодеталей для сборки устройства [1, 2]. Обращаю внимание читателей, что для экспериментов лучше использовать чистый микроконтроллер AT Mega32 (можно с любым буквенным индексом), а не портить оригинальный загрузчик Мастер-Кита.

Начнём, как говориться, с малого. Разработаем основу операционной системы устройства: драйвер датчиков температуры DS18x20, драйвер часов реального времени DS1307, драйвера АЦП, клавиатуры, LCD-дисплея и интерфейс управления устройством.

Определимся с fuse-битами микроконтроллера. Будем исходить из того, что наша операционная система будет универсальной, следовательно, для выполнения широкого круга задач за короткие промежутки времени нам необходимо высокое быстродействие микропроцессора. Поскольку в схеме не предусмотрена установка кварцевого резонатора – ограничимся внутренним RC-генератором микроконтроллера. Установим максимальную частоту его работы – 8 МГц.

Для исключения стирания энергонезависимой памяти eeprom микроконтроллера запрограммируем фуз eesave. Также отключим отладочный интерфейс JTAG, для исключения влияния на работу схемы устройства и освобождении нужных нам выводов портов микроконтроллера. Для исключения зависаний микропроцессора при провалах питающего напряжения задействуем встроенный детектор напряжения питания (BOD) и установим напряжение его срабатывания равным 2,6В. Как должны быть запрограммированы fuse-биты микроконтроллера для среды программирования PonyProg изображено на рис.1.

Для написания AVR-программ используют различные языки программирования.  Широко используются Assembler, Ada, Basic, С, C++, Forth и даже Pascal. Но на практике чаще всего используют Assembler и С. Первый из них предпочтителен для создания критичных во времени процедур, а второй - для быстрой разработки программ. Если учесть, что в архитектуру AVR изначально были заложены принципы оптимизации С-процедур, то альтернативы этому алгоритмическому языку нет.

Поскольку ясно, что объём программы будет значительным, для упрощения написания исходных кодов, используем компилятор языка высокого уровня С++.

Существуют бесплатно распространяемые С-компиляторы, и возможности у них колоссальные. Воспользуемся бесплатным компилятором языка С и средой разработки WinAVR-20100110 [3]. Цифры в названии компилятора указывают на его версию. А точнее на дату опубликования программы. В нашем случае дата выхода компилятора – 10 января 2010 г.

Компилятор WinAVR собран из отдельных разработок, каждая из которых может иметь самостоятельное применение.

Использование бесплатного компилятора WinAVR позволит нам свободно распространять и модернизировать программу, не оглядываясь на какие-либо ограничения.

Основную сложность при разработке программного обеспечения в среде WinAVR составляет отсутствие какого-либо единого запускающего файла для компиляции проекта. Главной оболочкой С-компилятора является Programmers Notepad (PN), автор Simon Steele [4]. Для того чтобы указать параметры компиляции и тип используемого микропроцессора к каждой С-программе в WinAVR прилагается make-файл. Он должен находиться в том же каталоге на жёстком диске компьютера, что и С-программа.

Начнем с драйвера шины 1-wire. Именно по протоколу 1-wire [5] осуществляется взаимодейстие датчиков температуры серии DS18x20 с микроконтроллером.

Сначала немного теории. Интерфейс 1-Wire разработан фирмой Dallas Semiconductor, все права на этот интерфейс принадлежат исключительно этой фирме. Фирменная документация по указанному интерфейсу и устройствах, его поддерживающих, находится по адресу [6]. Чем привлекателен этот интерфейс? Разумеется, малым количеством выводов МК, требующихся для подключения практически неограниченного количества микросхем. В самом деле, двусторонний обмен требует всего 1 линию! Кроме того, ассортимент устройств с этим интерфейсом весьма широк. Наконец, протокол обмена по этому интерфейсу очень прост и легко реализуется программно практически на любых МК.

На рисунке 2 показана упрощенная схема аппаратной реализации интерфейса 1-Wire. Вывод DQ устройства представляет собой вход КМОП-логического элемента, который может быть зашунтирован (замкнут на общий провод) полевым транзистором. Сопротивление канала этого транзистора в открытом состоянии - около 100 Ом. Когда транзистор заперт - имеется небольшой ток утечки (примерно 5 мкА) на общий провод.

Шина 1-Wire должна быть подтянута отдельным резистором к напряжению питания устройств (которое, кстати, может быть от 3 до 5В - уточняется по характеристикам конкретного устройства). Сопротивление этого резистора 4.7 К, однако, это значение рекомендовано только для достаточно коротких линий. Если шина 1-Wire используется для подключения удаленных на большое расстояние устройств, то сопротивление этого резистора следует уменьшить. Чуть позже я коснусь этой проблемы и поясню причины необходимости такого уменьшения сопротивления, а пока скажу, что минимально допустимое его сопротивление - около 300 Ом, а максимальное - около пары-тройки десятков килоом. Данные величины - ориентировочные, вы всегда должны уточнить по характеристикам конкретного устройства 1-Wire его максимальный втекающий ток линии DQ, который, собственно, и определяет минимум внешнего сопротивления.

Подключение шины 1-Wire к МК показано условно в двух вариантах: с использованием 2 отдельных выводов МК (один в качестве выхода, а другой в качестве входа), так и одного, работающего и на ввод и на вывод. Разделение этих способов показано пунктирной линией, условно обозначающей границу корпуса МК. С некоторой натяжкой можно представить себе логическое строение шины 1-Wire как всем известное соединение выводов микросхем с открытым коллектором по схеме "монтажное ИЛИ". Очевидно, что передача какой-либо информации при этом возможна только выдачей низкого уровня в линию, т.е. замыканием ее на общий провод, а в высокий логический уровень линия вернется сама, благодаря наличию внешнего подтягивающего резистора. Так же очевидно, что одновременная передача нескольких устройств обречена на неудачу из-за полного искажения информации (все передаваемые единицы одного устройства будут подавлены передаваемыми нулями от другого устройства).

А теперь о том, как происходит обмен информацией по шине 1-Wire. Основные постулаты.

1. Обмен всегда ведется по инициативе одного ведущего устройства, которое в большинстве случаев является микроконтроллером (МК).

2. Любой обмен информацией начинается с подачи импульса сброса ("Reset Pulse" или просто RESET) в линию 1-Wire ведущим устройством.

3. Для интерфейса 1-Wire в общем случае предусматривается "горячее" подключение и отключение устройств.

4. Любое устройство, подключенное к 1-Wire после получения питания выдает в линию DQ импульс присутствия, называемый "Presence pulse" (далее я буду использовать термин PRESENCE). Этот же импульс устройство всегда выдает в линию, если обнаружит сигнал RESET.

5. Появление в шине 1-Wire импульса PRESENCE после выдачи RESET однозначно свидетельствует о наличии хотя бы одного подключенного устройства.

6. Обмен информации ведется так называемыми тайм-слотами: один тайм-слот служит для обмена одним битом информации.

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

Основные постулаты определяют логический низкоуровневый протокол обмена данными.

На рисунке 3 показана диаграмма сигналов RESET и PRESENCE, с которых всегда начинается любой обмен данными. Кстати, выдача импульса RESET в процессе обмена служит для досрочного завершения процедуры обмена информацией.

Как видим, длительность большинства временных интервалов очень приблизительная и имеет только ограничение только по минимуму (не меньше указанного). Условные обозначения линий, показанные на рис. 3, будут использоваться и далее.

Импульс RESET формирует ведущий МК, переводя в низкий логический уровень шину 1-Wire и удерживая ее в этом состоянии минимум 480 микросекунд. Затем МК должен "отпустить" шину. Через некоторое время, зависящее от емкости линии и сопротивления подтягивающего резистора, в линии установится высокий логический уровень. Протокол 1-Wire ограничивает это время "релаксации" диапазоном от 15 до 60 микросекунд, что и является определяющим для выбора подтягивающего резистора (как правило, емкость линии мы менять существенно не можем, а именно она оказывает существенное влияние на время возврата линии к высокому уровню).

Обнаружив импульс RESET, ведомое устройство приводит свои внутренние узлы в исходное состояние и формирует ответный импульс PRESENCE, как следует из рисунка - не позже 60 микросекунд после завершения импульса RESET. Для этого устройство переводит в низкий уровень линию DQ и удерживает ее в этом состоянии от 60 до 240 микросекунд. Конкретное время удержания зависит от многих параметров, но всегда находится в указанном диапазоне. После этого устройство так же "отпускает" шину.

Но после завершения импульса PRESENCE устройству дается еще некоторое время для завершения внутренних процедур инициализации, таким образом, МК должен приступить к любому обмену с устройством не ранее, чем через 480 микросекунд после завершения импульса RESET.

Итак, процедура инициализации интерфейса, с которой начинается любой обмен данными между устройствами, длится минимум 960 микросекунд, состоит из передачи от МК сигнала RESET и приему от устройства сигнала PRESENCE. Если сигнал PRESENCE не обнаружен - значит на шине 1-Wire нет готовых к обмену устройств.

Теперь рассмотрим процедуры обмена битами информации, которые, как вы помните, осуществляются определенными тайм-слотами. Тайм-слот - это по существу определенная, довольно жестко лимитированная по времени последовательность смены уровней сигнала в линии 1-Wire. Различают 4 типа тайм-слотов (будем использовать термин МК, как синоним "ведущего устройства" и просто "устройство", как синоним "ведомого"): передача "1" от МК, передача "0" от МК, прием "1" от устройства и прием "0" от устройства.

Любой тайм-слот всегда начинает МК путем перевода шины 1-Wire в низкий логический уровень. Длительность любого тайм-слота должна находиться в пределах от 60 до 120 микросекунд. Между отдельными тайм-слотами всегда должен предусматриваться интервал не менее 1 микросекунды (конкретное значение определяется параметрами ведомого устройства).

Тайм-слоты передачи отличаются от тайм-слотов приема поведением МК: при передаче он только формирует сигналы, при приеме, кроме того, еще и опрашивает (т.е. принимает) уровень сигнала в линии 1-Wire. Рисунок 4 демонстрирует временные диаграммы тайм-слотов всех 4-х типов: вверху показаны тайм-слоты передачи от МК, внизу - приема от устройства.

Тайм-слот передачи "0" заключается просто в удержании шины 1-Wire в низком уровне в течение всей длительности тайм-слота. Передача "1" осуществляется путем "отпускания" шины 1-Wire со стороны МК не ранее чем через 1 микросекунду после начала тайм-слота, но не позже чем через 15 микросекунд. Ведомое устройство опрашивает уровень в шине 1-Wire в течение временного интервала, условно показанного в виде серого прямоугольника, т.е. начиная с 15-й микросекунды от начала тайм-слота и заканчивая 60-й микросекундой от начала. Типичный момент ввода уровня в устройство (т.е. характерный для большинства устройств) - около 30-й микросекунды от начала тайм-слота.

Заштрихованная область - это область "нарастания" уровня в шине 1-Wire, которая зависит от емкости линии и сопротивления подтягивающего резистора, она приведена для справки.

Тайм-слоты приема информации отличаются тем, что МК формирует только начало тайм-слота (абсолютно так же, как при передаче "1"), а затем управление уровнем шины 1-Wire берет на себя устройство, а МК осуществляет ввод этого уровня так же в определенной зоне временных интервалов. Зона эта, как видно из рисунка, довольно мала. Как и раньше, заштрихованная область - область неопределенности, поэтому для ввода, собственно говоря, контроллеру остается даже не промежуток, а скорее конкретный момент, когда он должен ввести уровень сигнала из линии. Этот момент времени - 14-я или 15-я микросекунда от начала тайм-слота. Разумеется, если линия имеет малую емкость, а подтягивающий резистор мал, зона опроса несколько расширяется, однако рекомендую ориентироваться на худший вариант (как, кстати, рекомендует и фирма-производитель), чтобы всегда обеспечить надежный обмен данными.

Итак, подведем итоги. МК начинает тайм слот с выдачи в шину 1-Wire "0" в течение 1 микросекунды. Последующий уровень зависит от типа тайм слота: для приема и передачи "1" уровень должен стать высоким, а для передачи "0" - оставаться низким вплоть до конца тайм-слота, т.е. не менее 60 и не более 120 микросекунд. Если МК принимает данные, то опрос уровня в шине он должен сделать на промежутке от 13-й до 15-й микросекунде тайм-слота. МК должен обеспечить интервал между отдельными тайм-слотами не менее 1 микросекунды (лучше - больше, максимальное значение не ограничено).

Важно понимать, что следует очень тщательно подходить к обеспечению в шине 1-Wire требуемых временных интервалов, т.к., например, увеличение длительности тайм-слота вывода "0" свыше рекомендованного значения может привести к ошибочному восприятию этого тайм-слота, как сигнала RESET, и, разумеется, после этого вся процедура обмена пойдет насмарку. Но так же следует учитывать влияние самой линии на длительность фронтов импульсов. Поэтому в общем случае, это не простая задача. Но выполнение несложных рекомендаций позволит ее решить достаточно простыми средствами: во-первых, все сигналы, которые должен формировать МК, следует формировать по принципу необходимого минимума длительности (т.е. немного больше, чем указанная минимальная длительность), а от устройства следует ожидать сигналов по принципу наихудшего (т.е. ориентироваться на самые худшие варианты временных параметров сигнала).

Если вы разрабатываете схему, которая целиком умещается на одной плате вместе со всеми устройствами на шине 1-Wire, то, ориентируясь на рисунок 3, вы получите практически идеальную линию: фронты нарастания высокого уровня в шине будут минимальными - это избавит вас от большинства проблем. Но если Вы подключаете несколько устройств через длинный соединительный шлейф - придется бороться с погонной емкостью линии.

Проверено, что соединение по свитым вручную обычным монтажным проводам при типовом подтягивающем резисторе однозначно возможно на расстоянии до 9 м, а для соединения по очень длинной телефонной "лапше" может потребовать уменьшения подтягивающего резистора до 510 Ом и даже менее.

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

Каждое устройство 1-Wire обладает уникальным идентификационным 64-битным номером, программируемым на этапе производства микросхемы. Уникальным - это значит, что фирма-производитель гарантирует, что не найдется двух микросхем с одинаковым идентификационным номером (по крайней мере в течении нескольких десятков лет при существующих темпах производства).

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

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

Как же осуществляется поиск устройств? Ответ на этот вопрос подробно освещён в апноуте (Application Note) [7]:

1. Итак, поиск начинается с импульса RESET от ведущего устройства и принятия PRESENCE от ведомых;

2. Затем, посылается 1 байт команды: 0xF0 – осуществляется поиск всех устройств на линии;

3. Устройства отправляют первый бит своего уникального номера;

Если несколько устройств передают свой бит одновременно – результирующий бит на линии получится, как результат операции логического И (AND);

4. следующий бит, который отправляют устройства – это дополнение первого бита (если первый бит был 1, то будет 0 и наоборот – если был 0 – теперь будет 1);

На основании этих двух битов – ведущее устройство может сделать вывод о первом бите устройств на линии:

     - оба бита равны 0 – это означает, что на линии присутствует более одного устройства и текущие (последние) биты серийного номера у них различные;

     - первый бит равен 0, а второй 1 – на линии присутствует минимум одно устройство и бит серийного номера устройств или устройства равен 0;

- первый бит равен 1, а второй 0 – на линии присутствует минимум одно устройство и бит серийного номера устройств или устройства равен 1;

- оба бита равны 1 – на шине 1-wire устройств нет.

5. Далее, МК отправляет бит назад. И теперь продолжат работу только те ведомые устройства у которых этот бит установлен. Если же устройство такого бита не имеет – оно должно перейти в режим ожидания до следующего сигнала RESET;

6. Данная «двубитная передача» повторяется для всех следующих 63 бит ROM;

7. Т.о. все устройства на линии, кроме одного перейдут в состояние ожидания, а код ROM этого единственного устройства будет известен :)

Затем ведущий переходит к считыванию следующих бит и этот процесс повторяется до считывания всех 64 бит. В результате ведущий обнаруживает полный 64-разрядный идентификатор. Для поиска других идентификаторов необходимо снова инициировать команду «SEARCH ROM», но в этом случае при возникновении несоответствий сделать другой выбор. Если придерживаться данной последовательности, то в конечном счете можно обнаружить все подчиненные устройства. Обратите внимание, что после выполнения первого поиска все подчиненные, кроме одного, переходят в режим ожидания. Таким образом, алгоритм поиска последовательно исключает все устройства, пока не остается одно последнее – его адрес и определяется в первом цикле поиска [8].

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

Итак, предположим, что мы знаем номера всех устройств 1-Wire на шине. Как же ведется работа с ними? Алгоритм тут следующий. МК посылает, как обычно, импульс RESET, и все имеющиеся устройства выдают PRESENCE. Затем МК посылает в шину команду, которую принимают все устройства. Команд определено несколько общих для всех типов 1-Wire-устройств, а так же могут быть команды, уникальные для отдельных типов. Среди команд датчиков температуры нас, в первую очередь, интересуют следующие (см. таблицу 1).

 Таблица 1 

Команда

Код команды

Описание

SEARCH ROM

0xF0

Поиск адресов - используется при универсальном алгоритме определения количества и адресов подключенных устройств

READ ROM

0x33

Считывание уникального серийного номера устройства

MATCH ROM

0x55

Выбор адреса - используется для обращения к конкретному устройству из множества подключенных

SKIP ROM

0xCC

Игнорировать адрес - используется для обращения к единственному устройству на шине, при этом адрес устройства игнорируется (можно обращаться ко всем устройствам сразу)

ALARM SEARCH

0xEC

Тревожный поиск - действие этой команды идентично действию поиска адресов за исключением того, что ответят только те датчики у которых измеренная температура вышла за  записанные в специальные регистры датчика DS18B20 пределы

CONVERT T

0x44

Начать измерение температуры

WRITE SCRATCHPAD

0x4E

Запись настроек в регистры датчика DS18B20

READ SCRATCHPAD

0xBE

Чтение настроек из регистров датчика DS18B20

COPY SCRATCHPAD

0x48

Копировать настройки из регистров датчика DS18B20 в его энергонезависимую память eeprom

RECALL E2

0xB8

Загрузить настройки из энергонезависимой памяти датчика температуры DS18B20 в его регистры

READ POWER SUPPLY

0xB4

Чтение параметров питания датчика – используется для определения типа подключения датчика: с паразитным или внешним питанием

После того, как МК выдаст команду READ ROM, от устройства поступит 8 байт его собственного уникального адреса - МК должен их принять. Учтите, что любая процедура обмена данными с устройством должна быть завершена полностью либо прервана посылкой сигнала RESET.

Если отправлена команда MATCH ROM, то после нее МК должен передать так же и 8 байт конкретного адреса устройства, с которым будет осуществляться последующий обмен данными. Это равносильно выставлению адреса на параллельной шине в микропроцессорных устройствах. Приняв эту команду, каждое устройство сравнивает передаваемый адрес со своим собственным. Все устройства, адрес которых не совпал, прекращают анализ и выдачу сигналов в линии 1-Wire, а опознавшее адрес устройство продолжает работу. Теперь все данные, передаваемые МК будут попадать только к этому "адресованному" устройству.

Если устройство одно на шине - можно ускорить процесс взаимодействия с ним при помощи команды SKIP ROM. Поучив эту команду, устройство сразу считает адрес совпавшим, хотя никакого адреса за этой командой не следует. Некоторые процедуры не требуют приема от устройства никаких данных, в этом случае команду SKIP ROM можно использовать для передачи какой-то информации сразу всем устройствам. Этот момент можно использовать, например, для одновременного запуска цикла измерения температуры несколькими датчиками-термометрами типа DS18x20.

В завершение обзора интерфейса рассмотрим строение уникального 64-битного номера-адреса устройств 1-Wire. Он состоит фактически из 8 отдельных байт: одного байта идентификатора семейства, шести байт (48 бит) собственно уникального адреса и одного байта контрольной суммы всех предыдущих байтов. Рассмотрение этих составных частей начнем в обратном порядке.

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

Очевидно, что алгоритм подсчета CRC должен быть одинаковым как для МК, так и для любого устройства. Он "стандартизирован" и описан в документации. Однако его понимание требует определенных умственных усилий и, что лично для меня более критично, наличия времени, которого постоянно не хватает. Именно поэтому я не стану описывать сам алгоритм расчета, а просто приведу примеры программной реализации этого алгоритма, которые можно использовать не особо углубляясь в математические дебри. Он содержится в файле «crc8.c» среди исходных кодов устройства [9]. Листинг подпрограммы вычисления контрольной суммы приведён на врезке 1.

 Врезка 1: 

uint8_t         crc8 ( uint8_t *data_in, uint16_t number_of_bytes_to_read )

{

      uint8_t   crc;

      uint16_t loop_count;

      uint8_t  bit_counter;

      uint8_t  data;

      uint8_t  feedback_bit;

     

      crc = CRC8INIT;

 

      for (loop_count = 0; loop_count != number_of_bytes_to_read; loop_count++)

      {

                      data = data_in[loop_count];

                     

                      bit_counter = 8;

                      do {

                                      feedback_bit = (crc ^ data) & 0x01;

     

                                      if ( feedback_bit == 0x01 ) {

                                                     crc = crc ^ CRC8POLY;

                                      }

                                      crc = (crc >> 1) & 0x7F;

                                      if ( feedback_bit == 0x01 ) {

                                                     crc = crc | 0x80;

                                      }

                     

                                      data = data >> 1;

                                      bit_counter--;

                     

                      } while (bit_counter > 0);

      }

     

      return crc;

}

 

Эквивалентное полиноминальное выражение для CRC: X16 + X15 + X2 + 1.

Итак, мы разобрались с алгоритмом расчета контрольной суммы уникального адреса устройства, однако это всего лишь один последний байт из 8 байтов этого адреса, необходимо рассмотреть и остальные. Предыдущие 6 байтов (помните, что мы рассматриваем байты адреса в обратном порядке?), собственно, и есть тот самый уникальный номер-идентификатор. Номер - он и есть номер, рассказывать о нем нечего, переходим к заключительному первому байту - коду семейства.

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

И напоследок, несколько слов о так называемом "паразитном питании" устройств 1-Wire. Спецификация этих устройств допускает их питание от самой линии данных, т.е. микросхема может получать питание по собственной (и единственной) линии данных! Не все устройства поддерживают такой режим питания, но очень многие, например, тот же датчик температуры DS18x20 или ключ-таблетка DS1990A. Теперь вам должно быть понятным, почему низкий уровень сигнала в шине 1-Wire имеет такие жесткие временные рамки - десятки микросекунд. Это связано с необходимостью обеспечивать питанием те устройства, которые получают его от шины. Потребление тока всех устройств 1-Wire такое ничтожное, что емкости встроенных конденсаторов достаточно для поддержания в рабочем состоянии схемы устройства в течение небольшого интервала, когда в шине 1-Wire низкий логический уровень.

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

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

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

- любой обмен информацией начинается с передачи импульса RESET и приема импульса PRESENCE;

- если импульса PRESENCE не обнаружено - на шине нет устройств;

- МК всегда инициирует обмен, начиная каждый тайм-слот обмена битом информации;

- временные параметры каждого тайм-слота следует соблюдать с максимально возможной точностью;

- для выбора одного из многих устройств на шине 1-Wire МК должен передать в шину команду MATCH ROM и затем 8 байт адреса устройства, последний (8-й) байт этого адреса - есть контрольная сумма предыдущих семи;

- если устройство на шине одно - МК может узнать его адрес путем посылки команды READ ROM, после чего принять от устройства 8 байтов адреса, последний из которых так же будет контрольной суммой первых семи;

- для работы с единственным устройством на шине можно отказаться от указания его адреса, для этого МК должен передать устройству команду SKIP ROM, после чего можно начинать обычный обмен данными;

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

- прервать начатый обмен можно в любой момент путем выдачи импульса RESET в шину 1-Wire (но это может нарушить нормальную работу некоторых устройств) [5].

Чтобы не начинать работу с шиной 1-wire с «нуля», воспользуемся свободно распространяемой библиотекой [9].

В ней содержаться следующие функции для работы с упомянутой шиной:

1.      ow_reset(); - передачи импульса RESET;

2.      ow_bit_io(); - чтение бита при приёме информации;

3.      ow_byte_wr(); - передача байта ведомому устройству;

4.      ow_byte_rd(); - приём байта от ведомого устройства;

5.      ow_rom_search(); - поиск устройств на шине 1-wire;

6.      ow_command(); - передача команды датчику;

7.      ow_parasite_enable(); - установить высокий уровень на выходе порта микроконтроллера для паразитного питания устройств;

8.      ow_parasite_disable(); - установить низкий уровень на выходе порта микроконтроллера для отключения паразитного питания устройств и передачи/приёма информации;

9.      ow_set_bus(); - настройка вывода порта микроконтроллера для работы с шиной 1-wire.

 

Поскольку работать с шиной мы планируем исключительно для целей обслуживания и опроса датчиков температуры серии DS18x20 (DS18B20, DS18S20, DS1820 и аналогичных) создадим на основе [10] библиотеку «ds18x20.c».

Создадим необходимые для работы с датчиками температуры подпрограммы:

1.      DS18X20_find_sensor(); - поиск датчика температуры типа DS18x20;

2.      search_sensors(); - поиск датчиков температуры и сохранение их серийных номеров в массив gSensorIDs;

3.      DS18X20_meas_to_cel(); - преобразование прочитанного значения температуры с датчика в «удобоваримый» формат градусов сельсия;

4.      DS18X20_temp_to_decicel(); - преобразование значения температуры в формат знакового целого числа;

5.      DS18X20_get_power_status(); - определение типа питания датчика – паразитное или внешнее (осуществляется путём чтения соответствующего флага из внутреннего регистра датчика температуры);

6.      DS18X20_start_meas(); - подача команды измерения температуры;

7.      DS18X20_read_meas(); - чтение температуры с датчика;

8.      DS18X20_write_scratchpad(); - запись параметров настройки порогов температуры и разрешения преобразования (9, 10, 11 или 12 бит) в датчик температуры DS18B20;

9.      DS18X20_copy_scratchpad(); - копировать параметры настройки из регистров датчика DS18B20 в его энергонезависимую память.

 

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

Теперь нам необходимо получить доступ к микросхеме часов реального времени (Real Time Clock, далее RTC) DS1307.

Как всегда сначала немного теории.

DS1307 - часы реального времени с последовательным интерфейсом - низкая потребляемая мощность, полный BCD календарь, часы плюс 56 байтов энергонезависимого статического ОЗУ. Адрес и данные передаются последовательно через 2-проводную двунаправленную шину. Часы / календарь хранят следующую информацию: секунды, минуты, часы, день, дату, месяц и год. Конец месяца автоматически подстраивается для месяцев, в которых менее 31 дня, включая поправку для високосного года. Часы работают в 24-часовом или 12-часовом формате с индикатором AM/PM. DS1307 имеет встроенную схему контроля питания, которая обнаруживает пропадание питания и автоматически переключает схему на питание от батареи.

Vbat - вход батареи для любого стандартного 3 вольтового литиевого элемента или другого источника энергии. Для нормальной работы напряжение батареи должно поддерживаться между 2.5 и 3.5 В. Уровень, при котором запрещён доступ к часам реального времени и пользовательскому ОЗУ,  установлен внутренней схемой равным 1.25 x Vbat. Литиевая батарея ёмкостью 35 mAh или больше достаточна для питания DS1307 в течение более чем 10 лет при отсутствии питания.

DS1307 работает как ведомое устройство на последовательной шине. Для доступа к нему надо установить состояние START и передать код идентификации устройства, сопровождаемый адресом регистра. К последующим регистрам можно обращаться последовательно, пока не установлено состояние STOP. Когда Vсс падает ниже 1.25 x Vbat, устройство прекращает связь и сбрасывает адресный счетчик. В это время оно не будет реагировать на входные сигналы, чтобы предотвратить запись ошибочной информации. Когда Vcc падает ниже Vbat, устройство переключается в режим хранения с низким потреблением. При включении питания устройство переключает питание с батареи на Vcc, когда напряжение питания превысит Vbat + 0. 2V, и реагирует на входные сигналы, когда Vcc станет более 1.25 x Vbat. Функциональная схема на рисунке 5 показывает главные элементы часов реального времени с последовательным интерфейсом.

Когда питание находится в пределах нормы, устройство полностью доступно, и данные могут быть записаны и считаны. Когда к устройству подключена трёхвольтовая батарея и Vcc ниже 1.25 x Vbat, чтение и запись запрещены. Однако отсчёт времени при этом работает. Когда Vcc падает ниже Vbat, питание ОЗУ и отсчёта времени переключается на внешнюю батарею 3 В.

SCL (Последовательный Тактовый Вход) - SCL используется, чтобы синхронизировать передачу данных через последовательный интерфейс. SDA (Вход/Выход Последовательных Данных) - SDA - вход / выход данных для 2-проводного последовательного интерфейса. Это выход с открытым стоком, который требует внешнего притягивающего резистора. SQW/OUT (Меандр / Выходной Драйвер) - Когда бит SQWE установлен в 1, на выходе SQW/OUT вырабатываются импульсы в форме меандра одной из четырех частот: 1 Гц., 4 кГц., 8 кГц., 32 кГц. Вывод SQW/OUT - с открытым стоком, требует внешнего притягивающего резистора. X1, X2 - выводы для подключения стандартного кристалла кварца 32.768 кГц. Внутренняя схема генератора рассчитана на работу с кристаллом, имеющим номинальную емкость (CL) 12.5 пФ.

Карта адресов для RTC и регистров ОЗУ DS1307 показана на рисунке 6. Регистры RTC расположены по адресам от 00h до 07h. Регистры ОЗУ расположены по адресам от 08h до 3Fh. Во время многобайтового доступа, когда указатель адреса достигает 3Fh, конца пространства ОЗУ, он возвращается к 00h, началу пространства часов.

Информацию о времени и дате получают, считывая соответствующие регистры. Регистры часов показаны на рисунке 7. Время и календарь устанавливаются или инициализируются путём записи байтов в соответствующие регистры. Содержание регистров времени и календаря хранится в двоично-десятичном (BCD) формате. Бит 7 Регистра 0 - это бит остановки часов (CH). Когда этот бит установлен в 1, генератор остановлен. Когда сброшен в ноль, генератор работает, а часы считают время.

DS1307 может работать в 12-часовом или 24-часовом режиме. Бит 6 регистра часов задаёт один из этих режимов. Когда он равен 1, установлен 12-часовой режим. В 12-часовом режиме высокий уровень бита 5 сообщает о послеполуденном времени. В 24-часовом режиме бит 5 - второй бит 10 часов (20-23 часа).

Регистр управления DS1307 предназначен для управления работой вывода SQW/OUT. Бит OUT -управление выходом. Этот бит управляет выходным уровнем на выводе SQW/OUT, когда генерация меандра запрещена. Если SQWE = 0, логический уровень на выводе SQW/OUT равен 1, если OUT = 1, и 0 - если OUT = 0. SQWE - Разрешение меандра. Когда этот бит установлен в 1, разрешается генерация меандра. Частота меандра зависит от значений битов RS0 и RS1. Эти биты управляют частотой меандра, когда его генерация разрешена. В таблице 2 показаны частоты, которые могут быть заданы RS битами. 

Таблица 2 – Частоты выходного меандра на выходе SQW/OUT DS1307 

RS1

RS0

Частота меандра на выходе SQW/OUT DS1307

0

0

1 Гц.

0

1

4,096 кГц.

1

0

8,192 кГц.

1

1

32,768 кГц.

 

DS1307 поддерживает двунаправленные 2-проводную шину и протокол передачи данных. Устройство, которое посылает данные на шину, называется передатчиком, а устройство, получающее данные - приемником. Устройство, которое управляет передачей, называется ведущим. Устройства, которые управляются ведущим - ведомые. Шина должна управляться ведущим устройством, которое вырабатывает последовательные такты (SCL), управляет доступом к шине, и генерирует состояния СТАРТ и СТОП. DS1307 работает как ведомое на 2-проводной шине. Типичная конфигурация шины, использующая этот 2-проводной протокол, показана на рисунке 9.

Определен следующий протокол шины:

- передача данных может быть начата только тогда, когда шина не занята.

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

Соответственно, были определены следующие состояния шины:

Шина свободна. На обеих линиях: данных и тактов высокий уровень.

Начало передачи данных: переход линии данных от высокого уровня к низкому при высоком уровне на линии тактов определяет условие СТАРТ.

Остановка передачи данных: переход линии данных от низкого уровня к высокому, в то время как на линии тактов высокий уровень, определяет условие СТОП.

Правильные данные: линия считается находящейся в состоянии передачи данных, когда после состояния СТАРТ уровень на линии данных не изменяется во время высокого уровня на линии тактирования. Он должен изменяться только во время низкого уровня тактового сигнала. Имеется один тактовый импульс на бит данных. Каждая передача данных начинается с состояния СТАРТ и заканчивается состоянием СТОП. Число байтов данных, переданных между этими состояниями, не ограничено и определяется ведущим  устройством. Информация передаётся байтами, и каждый приемник посылает подтверждение с девятым битом.

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

На рисунках 10, 11, и 12 подробно показано, как передаются данные по 2-проводной шине. В зависимости от состояния бита R/W при передаче, как показано на рисунках 11 и 12, возможны два типа передачи данных:

1. Режим ведомого приемника (режим записи DS1307): Последовательные данные и такты получены через SDA и SCL. После передачи каждого байта передаётся подтверждающий бит. Состояния СТАРТ и СТОП опознаются как начало и конец последовательной передачи. Распознавание адреса выполняется аппаратно после приема адреса ведомого и бита направления (См. рисунок 11). Байт адреса содержит семибитный адрес DS1307, равный 1101000, сопровождаемым битом направления (R/W), который при записи равен 0. После получения и расшифровки байта адреса DS1307 выдаёт подтверждение на линии SDA. После того, как DS1307 подтверждает адрес ведомого и бит записи, ведущий передает адрес регистра DS1307. Тем самым будет установлен указатель регистра в DS1307. Тогда ведущий начнет передавать байты данных в DS1307, который будет подтверждать каждый полученный байт. По окончании записи ведущий сформирует состояние СТОП.

2. Режим ведомого передатчика (режим чтения из DS1307): Первый байт принимается и обрабатывается как в режиме ведомого приёмника. Однако в этом режиме бит направления укажет, что направление передачи изменено. Последовательные данные передаются по SDA от DS1307, в то время как последовательные такты - по SCL в DS1307. Состояния СТАРТ и СТОП опознаются как начало и конец последовательной передачи (См. рисунок 12). Байт адреса - первый байт, полученный после того, как ведущим сформировано состояние СТАРТ. Байт адреса содержит семибитный адрес DS1307, равный 1101000, сопровождаемым битом направления (R/W), который при чтении равен 1. После получения и расшифровки байта адреса DS1307 выдаёт подтверждение на линии SDA. Тогда DS1307 начинает передавать данные, начинающиеся с адреса регистра, на которые указывает указатель регистра. Если указатель регистра не записан перед инициированием режима чтения, то первый адрес, который читается - это последний адрес, оставшийся в указателе регистра. DS1307 должен получить "Неподтверждение", чтобы закончить чтение [10, 11].

Для работы с 2-проводной шиной I2C в современных микроконтроллерах AVR имеется специальный последовательный двухпроводной интерфейс (TWI).

Модуль TWI состоит из нескольких подмодулей (см. рисунок 13). Все регистры выделенные жирной линией доступны через шину данных микроконтроллера.

Выводы SCL и SDA связывают двухпроводной интерфейс микроконтроллера с остальными устройствами в системе. Драйверы выходов содержат ограничитель скорости изменения фронтов для выполнения требований к TWI. Входные каскады содержат блок подавления помех, задача которого состоит в игнорировании импульсов длительностью менее 50 нс. Обратите внимание, что к каждой из этих линий можно подключить внутренний подтягивающий резистор путем установки разрядов соответствующих разрядов PORTC. Использование встроенных подтягивающих резисторов в ряде случаев позволяет отказаться от применения внешних.

Блок генератора управляет периодом импульсов SCL в режиме ведущего устройства. Период SCL задается регистром скорости TWI (TWBR) и значением бит управления предделителем в регистре состояния TWI (TWSR). В подчиненном режиме значения скорости или установки предделителя не оказывают влияния на работу, но частота синхронизации ЦПУ подчиненного устройства должна быть минимум в 16 раз выше частоты SCL. Обратите внимание, что подчиненные могут продлевать длительность низкого уровня на линии SCL, тем самым уменьшая среднюю частоту синхронизации шины TWI. Частота SCL генерируется в соответствии со следующим выражением:

                                                                                  (1)

где:      TWBR - значение регистра скорости TWI;

            TWPS - значение бит предделителя в регистре состояния TWI.  

Обратите внимание, что значение TWBR должно быть не менее 10, если TWI работает в ведущем режиме. Если TWBR меньше 10, то ведущий может генерировать некорректное состояние на линиях SDA и SCL. Проблема возникает при работе в ведущем режиме при передаче условий СТАРТ+ПОДЧИН_АДР+ ЧТЕНИЕ/ЗАПИСЬ подчиненному.

Блок шинного интерфейса содержит сдвиговый регистр адреса и данных (TWDR), контроллер СТАРТа/СТОПа и схему арбитража. TWDR содержит передаваемый байт адреса или данных, или принятый байт адреса или данных. Помимо 8-разр. регистра TWDR в состав блока шинного интерфейса также входит регистр, хранящий значение передаваемого или принятого бита (НЕТ) ПОДТВ. К данному регистру нет прямого доступа со стороны программного обеспечения. Однако во время приема он может устанавливаться или сбрасываться путем манипуляций с регистром управления TWI (TWCR). В режиме передатчика значение принятого бита (НЕТ) ПОДТВ можно определить по значению регистра TWSR.

Контроллер СТАРТа/СТОПа отвечает за генерацию и детектирование условий СТАРТ, ПОВТОРНЫЙ СТАРТ и СТОП. Контроллер СТАРТа/СТОПа позволяет обнаружить условия СТАРТ и СТОП, даже если микроконтроллер находится в одном из режимов сна. Этим обеспечивается возможность пробуждения микроконтроллера по запросу ведущего шины.

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

Блок обнаружения адреса проверяет равен ли принятый адрес значению 7-разр. адреса из регистра TWAR. Если установлен бит разрешения обнаружения общего вызова TWGCE в регистре TWAR, то все входящие адресные биты будут дополнительно сравниваться с адресом общего вызова. При адресном совпадении подается сигнал блоку управления, что позволяет выполнить ему необходимые действия. В зависимости от установки регистра TWCR подтверждение адреса TWI может происходит, а может и нет. Блок обнаружения адреса способен функционировать даже, когда микроконтроллер переведен в режим сна, тем самым позволяя возобновить нормальную работу микроконтроллера по запросу мастера шины.

Блок управления наблюдает за шиной TWI и генерирует отклики в соответствии с установками регистра управления TWI (TWCR). Если на шине TWI возникает событие, которое требует внимания со стороны программы, то устанавливается флаг прерывания TWINT. Следующим тактом обновляется содержимое регистра статуса TWI - TWSR, в котором будет записан код, идентифицирующий возникшее событие. Даная информация хранится в TWSR только тогда, когда установлен флаг прерывания TWI. Остальное время в регистре TWSR содержится специальный код состояния, который информирует о том, что нет информации о состоянии TWI. До тех пор пока установлен флаг TWINT линия SCL остается в низком состоянии. Этим обеспечивается возможность завершить программе все задачи перед продолжением сеанса связи.

 Флаг TWINT устанавливается в следующих ситуациях:

1.      После передачи условия СТАРТ/ПОВТОРНЫЙ_СТАРТ

2.      После передачи ПОДЧИН_АДР+ЧТЕНИЕ/ЗАПИСЬ

3.      После передачи адресного байта

4.      После потери арбитража

5.      После того как TWI адресован собственным подчиненным адресом или общим вызовом

6.      После приема байта данных

7.      После приема условия СТОП или ПОВТОРНЫЙ_СТАРТ в режиме подчиненной адресации

8.      После возникновения ошибки по причине некорректного условия СТАРТ или СТОП.

 TWI ориентирован на передачу данных в байтном формате с управлением по прерываниям. Прерывания возникают после обнаружения одного из событий на шине, например, прием байта или передача условия СТАРТ. Управление TWI по прерываниям позволяет освободить программное обеспечение на выполнение других задач во время передачи байта данных. Обратите внимание, что установка флага TWINT приводит к генерации запроса на прерывание только в том случае, когда установлен бит разрешения прерывания TWIE в регистре TWCR, а также разрешена работа прерываний установкой бита в регистре SREG. Если бит TWIE сброшен, то состояние TWINT должно отслеживаться программно для оценки ситуации на шине TWI.

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

На рисунке 14 показан простой пример подключения к шине TWI. Здесь предполагается, что мастер желает передать один байт данных подчиненному.

  1. Первым шагом работы TWI является передача условия СТАРТ. Это инициируется путем записи специфического значения в TWCR. О значении, которое необходимо записать будет сказано позже. Однако, необходимо следить, чтобы в записываемом в регистр значении был установлен бит TWINT. Запись лог. 1 в TWINT сбрасывает этот флаг. TWI не начнет работу до тех пор пока будет установлен флаг TWINT в регистре TWCR. Сразу после сброса TWINT начинается передача условия СТАРТ.
  2. После передачи условия СТАРТ устанавливается флаг TWINT в регистре TWCR, а содержимое TWSR обновляется значением кода состояния, индицирующего об успешной передачи условия СТАРТ.
  3. В программе необходимо выполнить проверку значения TWSR, чтобы убедится в том, что условие СТАРТ было успешно передано. Если TWSR индицирует прочую ситуацию, то программа выполняет особые действия, например, вызывает процедуру обработки ошибочных ситуаций. Если код состояния имеет ожидаемое значение, то выполняется загрузка условия ПОДЧИН_АДР + ЗАПИСЬ в TWDR. Необходимо помнить, что TWDR используется для хранения как адреса, так и данных. После загрузки в TWDR желаемого значения ПОДЧИН_АДР + ЗАПИСЬ в регистр TWCR должно быть записано специфическое значение, которое служит командой для передачи значения ПОДЧИН_АДР + ЗАПИСЬ, хранящегося в TWDR. Какое именно значение необходимо записать будет сказано позже. Однако необходимо следить, чтобы в записываемом в регистр значении был установлен бит TWINT. Запись лог. 1 в TWINT приводит к сбросу этого флага. TWI не начнет работу до тех пор пока установлен бит TWINT в регистре TWCR. Сразу после сброса флага TWINT инициируется передача адресного пакета.
  4. После передачи адресного пакета устанавливается флаг TWINT в регистре TWCR, а содержимое регистра TWSR обновляется кодом состояния, индицирующего успешность передачи адресного пакета. В коде состояния также отражается, было ли подтверждение приема адресного пакета со стороны подчиненного или нет.
  5. Выполняется программная проверка значения TWSR, чтобы убедиться в успешности передачи адресного пакета и что бит подтверждения ПОДТВ имеет ожидаемое значение. Если TWSR индицирует иную ситуацию, то при необходимости выполняются особые действия, например, вызывается процедура обработки ошибочных ситуаций. Если же код состояния имеет ожидаемое значение, то программа записывает пакет данных в TWDR. Впоследствии в регистр TWCR записывается специфическое значение, которое служит командой для TWI и вызывает аппаратную передачу данных, записанных в TWDR. Какое именно значение необходимо записать, будет сказано позже. Однако необходимо учесть, что в записываемом значении должен быть установлен бит TWINT. Запись лог. 1 в TWINT приводит к сбросу этого флага. TWI не начнет работу до тех пор, пока будет установлен бит TWINT в регистре TWCR. Сразу после сброса TWINT начинается передача пакета данных.
  6. После передачи пакета данных устанавливается флаг TWINT в регистре TWCR, а содержимое регистра TWSR обновляется значением кода состояния, который сигнализирует об успешной передачи пакета данных. В коде состояния также отражается, было ли принято подтверждение от подчиненного или нет.
  7. Выполняется программная проверка значения в TWSR, чтобы убедиться в успешности передачи пакета данных и в том, что бит ПОДТВ имеет ожидаемое значение. Если TWSR индицирует иную ситуацию, то программа выполняет особые действия, в т.ч. вызывает процедуру обработки прерывания. Если код состояния имеет ожидаемое значение, то выполняется запись специального значения в TWCR, которое служит командой для TWI и инициирует передачу условия СТОП. Какое именно значение необходимо записать, сказано далее. Однако следует учесть, что во время записи должна быть произведена установка бита TWINT. Запись лог. 1 в TWINT приводит к очистке этого флага. TWI не начнет работу до тех пор, пока установлен бит TWINT в регистре TWCR. Сразу после сброса флага TWINT инициируется передача условия СТОП. Обратите внимание, что флаг TWINT НЕ устанавливается по завершении передачи условия СТОП.

Не смотря на простоту изложенного примера, он показывает принципы, положенные в основу любой передачи через TWI.

Из вышеизложенного можно сделать следующие выводы:

По завершении работы TWI устанавливается флаг TWINT и далее ожидается реакция со стороны программы. Линия находится в низком состоянии, пока сброшен флаг TWINT. Если флаг TWINT установлен, то пользователь может обновлять любой из регистров TWI значением, которое относится к следующему этапу работы шины TWI. Например, в TWDR загружается значение, которое необходимо передать на следующем цикле шины. После обновления всех регистров TWI и завершении других задач выполняется запись в TWCR. Во время записи TWCR необходимо, чтобы был установлен бит TWINT. В этом случае запись лог. 1 в TWINT приведет к сбросу данного флага. TWI выполняет действия в соответствии установкой регистра TWCR.

Далее (таблица 3) показан пример на Ассемблере и Си. В примере предполагается, что все символьные обозначения определены в файле описания микроконтроллера.

Таблица 3.

Пример кода на Ассемблере

Пример кода на Си

Комментарий

1

ldi r16,

(1<<TWINT)|(1<<TWSTA)|(1<<TWEN)

out TWCR, r16

TWCR = (1<<TWINT)|

(1<<TWSTA)|(1<<TWEN)

Передача условия СТАРТ

2

wait1:

in r16,TWCR

sbrs r16,TWINT

rjmp wait1

while (!(TWCR & (1<<TWINT)))

;

Ожидание установки флага TWINT. Этим индицируется завершение передачи условия СТАРТ

3

in r16,TWSR

andi r16, 0xF8

cpi r16, START

brne ERROR

if ((TWSR & 0xF8) != START)

ERROR();

Проверка кода состояния TWI. Маскир. бит предделителя. Если код состояния не равен СТАРТ, то переход на ERROR

ldi r16, SLA_W

out TWDR, r16

ldi r16, (1<<TWINT) | (1<<TWEN)

out TWCR, r16

TWDR = SLA_W;

TWCR = (1<<TWINT) | (1<<TWEN);

Загрузка ПОДЧИН_АДР + ЗАПИСЬ в регистр TWDR. Сброс бита TWINT в TWCR для начала передачи адреса

4

wait2:

in r16,TWCR

sbrs r16,TWINT

rjmp wait2

while (!(TWCR & (1<<TWINT)))

;

Ожидание установки флага TWINT. Этим сигнализируется завершение передачи ПОДЧИН_АДР + ЗАПИСЬ и получение/неполучение подтверждения (ПОДТВ/НЕТ ПОДТВ).

5

in r16,TWSR

andi r16, 0xF8

cpi r16, MT_SLA_ACK

brne ERROR

if ((TWSR & 0xF8) != MT_SLA_ACK)

ERROR();

Проверка значения регистра состояния. Маскирование бит предделителя. Если состояние отличается от MT_SLA_ACK, то переход на ERROR

ldi r16, DATA

out TWDR, r16

ldi r16, (1<<TWINT) | (1<<TWEN)

out TWCR, r16

TWDR = DATA;

TWCR = (1<<TWINT) | (1<<TWEN);

Загрузка данных в TWDR. Сброс флага TWINT в TWCR для начала передачи данных

6

wait3:

in r16,TWCR

sbrs r16,TWINT

rjmp wait3

while (!(TWCR & (1<<TWINT)))

;

Ожидание установки флага TWINT. Этим индицируется, что данные были переданы и принято/не принято подтверждение (ПОТДВ/НЕТ ПОДТВ).

7

in r16,TWSR

andi r16, 0xF8

cpi r16, MT_DATA_ACK

brne ERROR

if ((TWSR & 0xF8) != MT_DATA_ACK)

ERROR();

Проверка значения регистра состояния TWI. Маскирование бит предделителя. Если состояние отличается от MT_DATA_ACK, то переход на ERROR

ldi r16, (1<<TWINT)|(1<<TWEN)|

(1<<TWSTO)

out TWCR, r16

TWCR = (1<<TWINT)|(1<<TWEN)|

(1<<TWSTO);

Передача условия СТОП

 

Передача условия СТАРТ инициируется путем записи в TWCR следующего значения:

TWCR

TWINT

TWEA

TWSTA

TWSTO

TWWC

TWEN

-

TWIE

Значение

1

x

1

0

x

1

0

x

Для разрешения работы двухпроводного последовательного интерфейса необходимо установить бит TWEN. Запись лог. 1 в TWSTA инициирует передачу условия СТАРТ, а запись лог. 1 в TWINT приводит к сбросу флага TWINT. После записи данного значения TWI тестирует двухпроводную последовательную шину и генерирует условие СТАРТ сразу после освобождения шины. После передачи условия СТАРТ аппаратно устанавливается флаг INT, а в регистр TWSR помещается код состояния $08 (см. Таблицу 3). Для перевода в режим ведущего передатчика необходимо передать ПОДЧИН_АДР + ЗАПИСЬ. Это выполняется путем записи значения ПОДЧИН_АДР + ЗАПИСЬ в регистр TWDR. После этого необходимо сбросить флаг TWINT (путем записи в него лог. 1) для продолжения сеанса связи. Данное выполняется путем записи следующего значения в TWCR:

TWCR

TWINT

TWEA

TWSTA

TWSTO

TWWC

TWEN

-

TWIE

Значение

1

x

0

0

x

1

0

x

После передачи ПОДЧИН_АДР + ЗАПИСЬ и приема бита подтверждения флаг TWINT снова устанавливается, а в регистр TWSR помещается код состояния, который может иметь несколько значений. В режиме ведущего код состояния может быть $18, $20 или $38. Для каждого из этих кодов состояний необходимо выполнить адекватные действия.

После успешной передачи ПОДЧИН_АДР + ЗАПИСЬ должен быть передан пакет данных. Его передача инициируется записью байта данных в TWDR. Доступ на запись к TWDR разрешен только тогда, когда флаг TWINT равен 1. В противном случае доступ блокируется и устанавливается флаг ошибочной записи TWWC в регистре TWCR. После обновления TWDR необходимо сбросить бит TWINT (путем записи в него лог. 1) для продолжения сеанса связи. Данное можно выполнить путем записи следующего значения в регистр TWCR:

TWCR

TWINT

TWEA

TWSTA

TWSTO

TWWC

TWEN

-

TWIE

Значение

1

x

0

0

x

1

0

x

Данная последовательность повторяется до тех пор, пока не будет передан последний байт. После этого генерируется условие СТОП или ПОВТОРНЫЙ СТАРТ. Условие СТОП генерируется путем записи следующего значения TWCR:

TWCR

TWINT

TWEA

TWSTA

TWSTO

TWWC

TWEN

-

TWIE

Значение

1

x

0

1

x

1

0

x

Условие ПОВТОРНЫЙ СТАРТ генерируется путем записи следующего значения в TWCR:

TWCR

TWINT

TWEA

TWSTA

TWSTO

TWWC

TWEN

-

TWIE

Значение

1

x

1

0

x

1

0

x

После передачи условия ПОВТОРНОГО СТАРТА (состояние $10) двухпроводной последовательный интерфейс может обращаться к тому же подчиненному устройству или же к новому, при этом не требуется передача условия СТОП. Таким образом, повторный СТАРТ полезно использовать для смены подчиненного устройства в режимах ведущий передатчик и ведущий приемник без потери управления шиной.

Теперь переходим к практике.

Для начала напишем подпрограмму инициализации шины I2C (врезка 2). Учитывая, что максимальная частота следования тактовых импульсов для DS1307 100 кГц., подставим данное значение в формулу (1) и решим уравнение. Полученное значение TWBR и запишем в регистр установки скорости передачи.

 

Врезка 2:

 

//TWI initialize

// bit rate:80 (freq: 100Khz)

void twi_init(void)

{

 TWCR= 0x00;          //отключить TWI

 TWBR= 0x20;          //установить битрейт передатчика

 TWSR= 0x00;           //установить предделитель

 TWCR= 0x44;          //включить TWI

}

 

Опишем подпрограмму формирования условия START:

Врезка 3:

 

unsigned char i2c_start(void)

{

 

      TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN); //Запись значения START в регистр

     

    while (!(TWCR & (1<<TWINT)));                     //Ожидание выполнения микроконтроллером этой команды

                                                                                                    //Условие START было сформировано

      if ((TWSR & 0xF8) == START)                                      //Возврат 0, если передача прошла нормально

                      return(0);

else                                                                                       //Возврат 1, если произошла ошибка

                      return(1);

}

 

Подпрограмма формирования повторного старта будет выглядеть практически также (см. файл «i2c_routines.c»).

После формирования условия старт необходимо передать адрес ведомого устройства (функция i2c_sendAddress(); на врезке 4).

Врезка 4:

 unsigned char i2c_sendAddress(unsigned char address)

{

  unsigned char STATUS;

    if((address & 0x01) == 0)

     STATUS = MT_SLA_ACK;

   else

     STATUS = MR_SLA_ACK;

  

   TWDR = address;                                                  //Загрузить адрес ведомого устройства в TWDR-регистр.

   TWCR = (1<<TWINT)|(1<<TWEN); //Очистить TWINT и TWEN биты регистра TWCR

                                                                                     //для начала передачи адреса ведомого устройства

   while (!(TWCR & (1<<TWINT)));                      //Ждать пока адрес будет передан

   if ((TWSR & 0xF8) == STATUS)                        //Прочитать было ли подтверждение верного

//приёма от ведомого устройства

       return(0);

   else

      return(1);

}

 

Следующим шагом должна быть передача байта данных (врезка 5).

Врезка 5:

 

unsigned char i2c_sendData(unsigned char data)

{

   TWDR = data;                                                       //Загрузить байт данных в регистр TWDR

   TWCR = (1<<TWINT) |(1<<TWEN);                //Начать передачу

 

   while (!(TWCR & (1<<TWINT)));                      //Ждать завершения передачи

 

   if ((TWSR & 0xF8) != MT_DATA_ACK)   //Прочитать было ли подтверждение верного

//приёма от ведомого устройства

        return(1);

   else

      return(0);

}

 

Для формирования условия STOP и прекращения обмена информацией необходимо записать в регистр TWCR соответствующее значение.

 

Врезка 6:

void i2c_stop(void)

{

  TWCR =  (1<<TWINT)|(1<<TWEN)|(1<<TWSTO);         //Сформировать условие STOP

 Для того, что бы прочитать время и дату из микросхемы-часов DS1307 необходимо написать подпрограммы чтения данных с шины I2C (врезка 7). Поскольку для завершения получения данных из DS1307 необходимо сформировать условие «нет подтверждения приёма» необходимы две такие функции.

 

Врезка 7:

 

//Подпрограмма приёма байта от ведомого устройства с выдачей сигнала подтверждения

 

unsigned char i2c_receiveData_ACK(void)

{

  unsigned char data;

 

  TWCR = (1<<TWEA)|(1<<TWINT)|(1<<TWEN);

 

  while (!(TWCR & (1<<TWINT)));         

  if ((TWSR & 0xF8) != MR_DATA_ACK

        return(ERROR_CODE);

 

  data = TWDR;

  return(data);

}

//Подпрограмма приёма байта от ведомого устройства с выдачей сигнала «нет подтверждения»

 

unsigned char i2c_receiveData_NACK(void)

{

  unsigned char data;

 

  TWCR = (1<<TWINT)|(1<<TWEN);

 

  while (!(TWCR & (1<<TWINT)));                          //Wait for TWINT flag set. This indicates that the

                                                                                                                                      //data has been received

  if ((TWSR & 0xF8) != MR_DATA_NACK)    //Check value of TWI Status Register

        return(ERROR_CODE);

 

  data = TWDR;

  return(data);

Подпрограммы более высокого уровня (чтение времени и даты из DS1307, запись параметров настройки, даты и времени в RTC) из-за обычной лени подробно описывать не буду. С подробными комментариями они размещены в файле «DS1307.c»:

1.      DS1307_read_time(); - чтение времени и даты с часов DS1307;

2.      DS1307_write_time(); - запись даты и времени в часы DS1307;

3.      DS1307_write_param(); - запись параметров в регистр настройки DS1307.

 

Теперь, имея за плечами богатый теоретический запас знаний, перейдём, наконец, непосредственно к разработке самого устройства – часов-термометра.

Для начала создадим Makefile. Для этого воспользуемся стандартной утилитой MFile  поставляемой с пакетом WinAVR. Запустим её из меню «Пуск» и выберем из выпадающего по нажатию на вкладку «Makefile» меню тип используемого микропроцессора «atmega32», хотя для начальных экспериментов вполне подойдёт и более дешёвый atmega16 (рис.15). Остальные параметры лучше оставить по умолчанию. Сохраним полученный файл в каталог в котором будет размещаться наша программа и откроем его при помощи любого текстового редактора. Далее нам необходимо установить частоту с которой будет работать наш микроконтроллер. Для этого найдём в Makefile строку «F_CPU = XXXXXXX» и изменим значение частоты на 8 000 000 (рис.16), т.е. 8МГц. Сохраним внесённые изменения в файл и закроем его.

 Вторым шагом необходимо создать главный файл программы «main.c» и обслуживающий его заголовочный файл «main.h». Для этого воспользуемся программой Programmers Notepad. Для этого после запуска программы выберем язык компиляции проекта (рис.17) и сохраним файл.

Можно загрузить исходные коды, включая названные файлы с сайта [12], но как говориться лучше самому через всё пройти, поэтому приступим…

Определим максимальное число датчиков температуры (врезка 8) и программ управления (на будущее). Как в оригинальной прошивке набора их у нас будет 32. Обусловлено это число тем, что нам понадобятся различные флаги – однобитные переменные, а без существенного усложнения программы и с учётом экономии драгоценной памяти, в одной четырёхбитной переменной типа long int может храниться как раз 8*4=32 таких флага. Максимальное же число подключаемых датчиков температуры ограничено, вдобавок, ёмкостями линии и самих датчиков. При использовании 32 датчиков и витой пары категории 5E длина линии может достигать 200 метров без особых осложнений (естественно при паразитном питании датчиков температуры это невозможно). Данное явление проверено мной на практике. Для повышения надёжности устройства пришлось, правда, несколько видоизменить схему подключения датчиков к микроконтроллеру (см. рис. 18 и 40).

 

 

Врезка 8:

 //Число датчиков температуры и программ управления

#define        N_PROG                              32

 

Далее подключим файлы описаний, которые потребуются нам для дальнейшей работы и описанные библиотеки для работы с шинами 1-wire и I2C (врезка 9).

 

Врезка 9:

 

#include <inttypes.h>

#include <avr/interrupt.h>

#include <avr/io.h>

#include <stdlib.h>

#include <avr/eeprom.h>

#include <avr/wdt.h>

#include <avr/sleep.h>

 

//i2c шина

#include "i2c_routines.c"

 

//Для подсчёта задержек

#include "delay.c"

 

//Термометр

#include "onewire.c"

#include "ds18x20.c"

 

//Файл описаний

#include "main.h"

 

//Подключаем LCD драйвер

#include "lcddriver.h"

#include "lcddriver.c"

 

//Подключаем драйвер клавиатуры

#include "keydriverADC.c"

 

//Часы

#include "DS1307.c"

#include "clock.c"

 

//Автоматический расчёт дня недели

#include "gregorianDay.c"

 

//Отображение диаграмм

#include "diagramma.c"

 

Для запрета сброса микроконтроллера при возникновении неразрешённых прерываний, такое бывает при ошибках в программе или нестабильном питании, воспользуемся стандартным приёмом – создадим подпрограмму, которая должна выполняться при возникновении такого прерывания. В нашем случае мы ничего делать не будем – просто завершим обработку этого прерывания (врезка 10).

Врезка 10:

 

SIGNAL (__vector_default)   //Запрет сброса при возникновении неразрешённого прерывания

{

return;                                         //просто выход

}

 

В случае критической ошибки в выполнении программы или возникновении нештатной ситуации (например, коротком замыкании шины 1-wire) пусть наша программа перезапускается. Для этого создадим переход по указателю на вектор сброса (врезка 11).

Врезка 11:

 

//Начать программу с вектора сброса

void (*funcptr)(void) = 0x0000;

 

 

Теперь, случись нестандартная ситуация, мы вызываем функцию funcptr(); и микроконтроллер перезагружается.

Приступим к написанию главной части программы – функции «main». Начнём с настройки периферийных устройств микроконтроллера (врезка 12).

Врезка 12:

 

//Запрет всех прерываний

      cli();

                     

      // Инициализация портов/нагрузок                           

      PORTC= 0b00000000;

      DDRC = 0b11111100;

                     

      PORTD= 0b00000000;

      DDRD = 0b01011000;

                     

      PORTA= 0b00000000;    

      DDRA = 0b11110000;

                     

      //Инициализация i2c

      twi_init();

                     

      //Инициализация шины 1-wire

      ow_set_bus(&PIND,&PORTD,&DDRD,PD7);

                     

      //Запись в RTC настроек генерации меандра с частотой 1 Гц.

if(DS1307_write_param(0x10)) funcptr();   //в случае ошибки на шине i2c - перезагружаемся               

                     

      //Инициализация дисплея

      _lcd_Display_Init();

                     

      //Очищаем экран

      _lcd_ClrScr();

                     

      //Контрастность подсветки

      //Настройка шима 0-го таймера

      TCCR0=0x69;

      //Выход ШИМа контрастности на OC0

      DDRB |=(1<<PB3);

 

      //Звук, яркость подсветки

      //настройка шима первого таймера

      TCCR1A=0xA1;

      TCCR1B=0x02;

      OCR1AL=0x7F;

                     

      //Инициализация таймера 2

      TCCR2=0x07;

      TIMSK=0b01000000;

                     

      //Выключить аналоговый компаратор для снижения потребляемой мощности

      ACSR=0x80;

                     

      //Инициализация АЦП

      ADMUX=FIRST_ADC_INPUT | (ADC_VREF_TYPE & 0xff);

      ADCSRA=0xCD;

                     

      // Инициализация внешнего прерывания INT0 по нарастающему фронту импульса от часов DS1307

      GICR|=0x40;

      MCUCR=0x02;

      MCUCSR=0x00;

      GIFR=0x40;

 

      //Разрешение прерываний

sei(); 

 

 

Библиотека LCD-индикатора разработки автора [12] обладает гибкими возможностями конфигурирования выводов микроконтроллера для подключения к ним ЖК-индикатора на базе контроллера Hd44780u или аналогичного. Для конфигурирования выводов микроконтроллера в соответствии с разводкой печатной платы необходимо в файле «lcddriver.c» указать название портов и номера пинов к которым будет подключаться индикатор (врезка 13). В нашем случае индикатор подключается на PORTB.

Врезка 13:

 

//ЛИНИИ ДАННЫХ

#define LCD_DATA_PORT                  PORTB

#define LCD_DATA_PIN                       PINB

#define LCD_DATA_DDR                     DDRB

 

#define D0                                                 4

#define D1                                                 5

#define D2                                                 6

#define D3                                                 7

 

 

//ЛИНИИ УПРАВЛЕНИЯ

#define LCD_PORT                                 PORTB

#define LCD_PIN                                    PINB

#define LCD_DDR                                  DDRB

 

#define IRS                                                               2

#define IWR                                             1

#define IE                                                  0

 

Отмечу, что благодаря особому макросу все строки, выводимые на дисплей, хранятся во flash-памяти микроконтроллера. Для того, чтобы отобразить из flash-памяти на экран надпись " HELLO! " достаточно набрать в программе фрагмент кода: _lcd_Printf (" HELLO! ");.

Для лучшего понимания работы программы следует сначала изучить работу и основные команды контроллера Hd44780, используемого во всех современных LCD модулях со встроенным знакогенератором. Итак, обо всём по порядку

Контроллер HD44780 фирмы Hitachi фактически является промышленным стандартом и широко применяется при производстве алфавитно-цифровых ЖКИ-модулей. Аналоги этого контроллера или совместимые с ним по интерфейсу и командному языку микросхемы, выпускают множество фирм, среди которых: Epson, Toshiba, Sanyo, Samsung, Philips. Еще большее число фирм производят ЖКИ-модули на базе данных контроллеров.

Контроллер HD44780 потенциально может управлять 2-мя строками по 40 символов в каждой (для модулей с 4-мя строками по 40 символов используются два однотипных контроллера), при матрице символа 5 х 7 точек. Контроллер также поддерживает символы с матрицей 5 х 10 точек, но в последние годы ЖКИ-модули с такой матрицей практически не встречаются, поэтому можно считать, что фактически бывают только символы 5 х 7 точек.

Существует несколько различных более-менее стандартных форматов ЖКИ-модулей (символов х строк): 8 х 2, 16 х 1, 16 х 2, 16 х 4, 20 х 1, 20 х 2, 20 х 4, 24 х 2, 40 х 2, 40 х 4. Встречаются и менее распространенные форматы: 8 х 1, 12 х 2, 32 х 2 и др., - принципиальных ограничений на комбинации и количество отображаемых символов контроллер не накладывает - модуль может иметь любое количество символов от 1 до 80, хотя в некоторых комбинациях программная адресация символов может оказаться не очень удобной.

Изучая каталоги различных фирм-производителей ЖКИ-модулей, можно убедиться, что одни форматы и конструктивы являются собственными разработками и не обнаруживают аналогов в номенклатуре остальных фирм, другие являются фактическими стандартами и производятся большинством изготовителей. В качестве примера можно назвать ЖКИ-модуль формата 16 х 2, именуемый PC1602-A у Powertip, ED16200 у EDT, DMC-16227 у Optrex, SC2402A у Bolymin, MDLS-16265 у Varitronix, PVC160202 у Picvue и др., все эти модули имеют одинаковые конструктивные размеры и являются взаимозаменяемыми.

В рамках одного конструктива ЖКИ-модуль может иметь еще ряд модификаций. В частности, могут применяться несколько типов ЖКИ, отличающихся цветом фона и цветом символов, а также по применяемым ЖК-материалам и структуре.

В качестве элементов подсветки в основном применяют светодиоды, что позволяет осуществить довольно простую программную регулировку яркости, путйм применения встроенного в микроконтроллер ШИМа. Светодиодная подсветка не требует высоковольтного источника (прямое падение напряжения составляет 4,2 В) и при использовании в качестве источника тока биполярного транзистора с прямым напряжением 0,8 – 1 В., позволяет обойтись без токоограничительного резистора в цепи анода при питании от источника с напряжением 5 В.

Для соединения ЖКИ-модуля с управляющей системой используется параллельная синхронная шина, насчитывающая 8 или 4 (выбирается программно) линий данных DB0...DB7, линию выбора операции R/W, линию выбора регистра RS и линию стробирования/синхронизации Е. Кроме линий управляющей шины имеются две линии для подачи напряжения питания 5 В - GND и VCC, и линия для подачи напряжения питания драйвера ЖКИ - V0.

Указанные выше названия линий шины являются стандартными, но существует множество различных вариантов расположения контактов у каждого конкретного конструктива ЖКИ-модуля. На самом деле, единственным реально стандартным вариантом расположения контактов является двухрядное 14-ти контактное поле, расположенное вертикально в левой части модуля, а также совпадающее в ним двухрядное 16-ти контактное поле, содержащее дополнительную пару контактов с подключенными к ней выводами питания подсветки. В любом случае, для получения достоверной информации необходимо воспользоваться соответствующей справочной литературой изготовителя модуля.

На начальном этапе необходимо подать питание на ЖКИ-модуль и добиться от него признаков работоспособности. Схема включения модуля, рассчитанного на стандартный диапазон температур, показана на рис.19а. Подстроечный резистор R1 позволяет плавно менять напряжение питания драйвера ЖКИ, что приводит к изменению угла поворота жидких кристаллов. Этим резистором можно отрегулировать фактическую контрастность при некотором преимущественном угле наблюдения (снизу-вверх или сверху-вниз). Включение в данную схему ЖКИ-модуля, рассчитанного на расширенный диапазон температур, не приведет к успеху, так как из-за особенностей применяемых в них ЖК-материалов, эти ЖКИ требуют повышенного напряжения питания и при питании напряжением 5 В изображение либо будет отсутствовать совсем, либо будет слабоконтрастным. Для преодоления этой ситуации необходимо подать на вывод V0 отрицательное напряжение (напряжение на ЖКИ определяется разностью VCC и V0), составляющее в предельном случае - 5 В. Если в схеме отсуствует источник отрицательного напряжения, то не составляет труда собрать простейший преобразователь, например, по схеме на рис. 19б. Следует отметить, что у большинства поставляемых на просторы СНГ ЖКИ дисплеев на плате предусмотрена установка инвертора напряжений (рис.20). Для этого на плате имеется соответствующая разводка печатных проводников. Установив недостающие (рис. 19б) компоненты легко заставить «без тормозов» работать такой ЖК-индикатор при температуре окружающего воздуха до - 250С (проверено!). Забегая вперёд, скажу, что для регулировки контраста мы также будем использовать ШИМ.

Для активизации ЖКИ-модуля подайте напряжение питания и повращайте движок резистора R1 (рис. 19). После окончания цикла внутренней инициализации модуль включается в режим развертки одной верхней строки. При изменении напряжения на выводе V0 сегменты этой строки должны менять свое состояние от прозрачного до непрозрачного, что является свидетельством правильного подключения питания модуля и работоспособности контроллера и драйверов ЖКИ. Установите движок в такое положение, при котором изображение сегментов в верхней строке едва проступает на основном фоне ЖКИ. Теперь ЖКИ-модуль готов к приему и отображению информации. После того, как вы добьетесь отображения на индикаторе какого-либо текста, то сможете более точно отрегулировать изображение в соответствии с необходимой контрастностью и требуемым углом наблюдения.

Для соединения модуля с управляющей системой можно выбрать один из двух вариантов: по 8-ми или 4-х разрядной шине. В первом случае потребуется 11 сигнальных линий, во втором - только 7. Сам процесс обмена также может быть организован двояко. Можно подключить ЖКИ-модуль к системной шине (если таковая имеется) и выполнить обмен в синхронном режиме с максимальной скоростью. Этому широко распространенному в прошлые годы способу присущ ряд недостатков. Во-первых, большинство современных устройств выполняется с применением однокристальных микропроцессоров без использования дополнительной внешней памяти и, как следствие, системная шина у этих устройств просто отсутствует. Во-вторых, в современных системах повышенной сложности и производительности, у которых присутствует дополнительная память и, естественно, системная шина, скорость операций на шине находится за пределами возможностей контроллера HD44780 (2 МГц при питании 5 В и 1 МГц при 3 В). Это может потребовать введения дополнительных схем для замедления скорости работы шины при выполнении операций обмена с ЖКИ-модулем. В-третьих, подключение к системной шине в большинстве случаев потребует вводить схемы дешифрации и формирования сигналов Е и R/W, что опять приведет к дополнительным затратам. Все сказанное выше не означает, что вариант с подключением к шине принципиально неэффективен. В какой-то конкретной системе этот способ, наоборот, может быть самым оптимальным.

Второй вариант - очень простой - обмен с ЖКИ-модулем выполняется чисто программными средствами, через порты ввода-вывода микроконтроллера. Подробным рассмотрением его и займёмся.

На рис. 20а приведена схема подключения ЖКИ-модуля с 8-ми разрядной шиной к некоторому абстрактному микроконтроллеру XYZ. Этот контроллер содержит два порта: 8-ми разрядный двунаправленный РА0...РА7, к которому подключена шина DB0...DB7 ЖКИ-модуля, и 3-х разрядный РВ0...РВ2, к которому подключены линии управляющих сигналов: E, RS,R/W. На рис. 20б можно видеть схему подключения ЖКИ-модуля к этому же контроллеру в 4-х разрядном режиме. Обратите внимание, что для обмена в 4-х разрядном режиме используется старшая тетрада шины данных - DB4...DB7.

В соответствии с временной диаграммой (рис. 21, 22) в исходном состоянии сигнал Е = 0, сигнал R/W = 0, значение сигнала RS - произвольное, шина данных DBO...DB7 в состоянии высокого импеданса (HI). Такое состояние управляющих сигналов (E и R/W) должно поддерживаться все время в промежутках между операциями обмена с ЖКИ-модулем. Шина данных в эти моменты в принципе свободна, и может использоваться в мультиплексном режиме для каких-либо других целей, например, для сканирования матрицы клавиатуры. Естественно, необходимо позаботиться об исключении конфликтов на шине данных в момент совершения операций обмена с ЖКИ-модулем.

Последовательности действий, которые необходимо выполнять управляющей системе при совершении операций записи и чтения для 8-ми и 4-х разрядной шины приведены соответственно в табл. 4...7 и на рис 25 и 24.

 

 

 

 

 

 

 

 

Таблица 4. Операции записи для 8-ми разрядной шины

 

1.Установить значение линии RS

2.Вывести значение байта данных на линии шины DB0...DB7

3.Установить линию E = 1

4.Установить линию E = 0

5.Установить линии шины DB0...DB7 = HI

 

Таблица 5. Операции чтения для 8-ми разрядной шины

1.Установить значение линии RS

2.Установить линию R/W = 1

3.Установить линию Е = 1

4.Считать значение байта данных с линий шины DB0...DB7

5.Установить линию Е = 0

6.Установить линию R/W = 0

 

Таблица 6. Операция записи для 4-х разрядной шины

1.Установить значение линии RS

2.Вывести значение старшей тетрады байта данных на линии шины DB4...DB7

3.Установить линию Е = 1

4.Установить линию Е = 0

5.Вывести значение младшей тетрады байта данных на линии шины DB4...DB7

6.Установить линию Е = 1

7.Установить линию Е = 0

8.Установить линии шины DB4...DB7 = HI

 

Таблица 7. Операция чтения для 4-х разрядной шины

1.Установить значение линии RS

2.Установить линию R/W = 1

З.Установить линию Е = 1

4.Считать значение старшей тетрады байта данных с линий шины DB4...DB7

5.Установить линию Е = 0

6.Установить линию Е = 1

7.Считать значение младшей тетрады байта данных с линий шины DB4...DB7

8.Установить линию Е = 0

9.Установить линию R/W = 0

 

Приведенные в табл. 4...7 операции подразумевают, что время выполнения каждого шага составляет не менее 250 нс. При использовании современных быстродействующих микроконтроллеров это условие может быть легко нарушено, поэтому необходимо тщательно контролировать минимальные значения временных интервалов, чтобы они всегда находились в области допустимых значений, указанных в таблице 8, 9 и при необходимости вводить задержки.

 Таблица 8. Значения временных характеристик. Операция записи 

Параметр

Мин.

Макс.

Един.

Период сигнала Е

500

-

нс

Положительный полупериод сигнала Е

230

-

нс

Фронт/спад сигнала Е

-

20

нс

Установление адреса

40

-

нс

Удержание адреса

10

-

нс

Установление данных

80

-

нс

Удержание данных

10

-

нс

Таблица 9. Значения временных характеристик. Операция чтения 

Параметр

Мин.

Макс.

Един.

Период сигнала Е

500

-

нс

Положительный полупериод сигнала Е

230

-

нс

Фронт/спад сигнала Е

-

20

нс

Установление адреса

40

-

нс

Удержание адреса

10

-

нс

Установление данных

-

160

нс

Удержание данных

5

-

нс

Описанные выше операции записи/чтения байта являются базовыми для осуществления обмена с ЖКИ-модулем. Реализация этих двух операций - единственное, что отличает процесс обмена по 8-ми разрядной шине от обмена по 4-х разрядной шине. На основе этих двух операций, реализованных программно (когда модуль подключен к портам микро-ЭВМ), или аппаратно (когда модуль подключен к системной шине), строятся все виды операций программирования и управления.

Несколько слов о подключении ЖКИ-модулей формата 40 х 4. Эти модели содержат два контроллера HD44780 и фактически представляют собой два модуля 40 х 2. Для сокращения числа соединений все сигналы обоих контроллеров соединены параллельно, исключение составляет только сигнал Е, одновременно выполняющий роль тактового сигнала и сигнала выборки микросхемы CS, поэтому обмен с каждым из контроллеров ведется попеременно.

Перед началом рассмотрения принципов управления ЖКИ-модулем, обратимся к внутренней структуре контроллера HD44780, чтобы понять основные принципы построения ЖКИ-модулей на его основе. Эта информация позволит понять способы организации модулей различных форматов с точки зрения программной модели, а также мотивации конструкторов ЖКИ-модулей.

Упрощенная структурная схема контроллера приведена на рис. 23. Можно сразу выделить основные элементы с которыми приходится взаимодействовать при программном управлении: регистр данных (DR), регистр команд (IR), видеопамять (DDRAM), ОЗУ знакогенератора (CGRAM), счетчик адреса памяти (АС), флаг занятости контроллера.

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

Управление контроллером ведется посредством интерфейса управляющей системы. Основными объектами взаимодействия являются регистры DR и IR. Выбор адресуемого регистра производится линией RS, если RS = 0 - адресуется регистр команд (IR), если RS = 1 - регистр данных (DR).

Данные через регистр DR, в зависимости от текущего режима, могут помещаться (или прочитываться) в видеопамять (DDRAM) или в ОЗУ знакогенератора (CGRAM) по текущему адресу, указываемому счетчиком адреса (АС). Информация, попадающая в регистр IR, интерпретируется устройством выполнения команд как управляющая последовательность. Прочтение регистра IR возвращает в 7-ми младших разрядах текущее значение счетчика АС, а в старшем разряде флаг занятости (BF).

Видеопамять, имеющая общий объем 80 байтов, предназначена для хранения кодов символов, отображаемых на ЖКИ. Видеопамять организована в две строки по 40 символов в каждой. Эта привязка является жесткой и не подлежит изменению. Другими словами, независимо от того, сколько реальных строк будет иметь каждый конкретный ЖКИ-модуль, скажем, 80 х 1 или 20 х 4, адресация видеопамяти всегда производится как к двум строкам по 40 символов.

Будучи устройством с динамической индикацией, контроллер циклически производит обновление информации на ЖКИ. Сам ЖКИ организован как матрица, состоящая в зависимости от режима работы из 8-ми (одна строка символов 5 х 7 точек), 11-ти (одна строка символов 5 х 10 точек) или 16-ти (две строки символов 5 х 7 точек) строк по 200 сегментов (когда строка насчитывает 40 символов) в каждой. Собственный драйвер конроллера HD44780 имеет только 40 выходов (SEG1...SEG40) и самостоятельно может поддерживать только 8-ми символьные ЖКИ. Это означает, что ЖКИ-модули форматов до 8 х 2 реализованы на одной единственной микросхеме HD44780, модули, имеющие большее количество символов, содержат дополнительные микросхемы драйверов, например, HD44100, каждая из которых дополнительно предоставляет управление еще 40-ка сегментами.

Особняком стоят ЖКИ-модули формата 16 х 1. Они также реализованы с помощью одной единственной микросхемы HD44780, но одна 16-ти символьная строка в них фактически составлена из двух 8-ми символьных. И хотя это усложняет программное управление, ведь строка оказывается логически разорванной посередине, тем не менее, экономически это оправдано, ибо позволило создать ЖКИ-модуль, содержащий всего одну микросхему. Другой вариант пространственной адресации встречается в 4-х строчных модулях. Из-за проблем разводки токоведущих дорожек, первая и вторая строки этих модулей являются таковыми как обычно, третья же является продолжением первой строки, а четвертая - второй.

У контроллера HD44780 существует набор внутренних флагов, определяющих режимы работы различных элементов контроллера (таблица 10). В таблице 11 приведены значения управляющих флагов непосредственно после подачи на ЖКИ-модуль напряжения питания. Переопределение значений флагов производится специальными командами, записываемыми в регистр IR, при этом комбинации старших битов определяют группу флагов или команду, а младшие содержат собственно флаги.

 

Таблица 10. Флаги, управляющие работой контроллера HD44780

 

I/D:                режим смещения счетчика адреса АС, 0 - уменьшение, 1 - увеличение.

 

S:                    флаг режима сдвига содержимого экрана. 0 - сдвиг экрана не производится, 1 - после записи в DDRAM очередного кода экран сдвигается в направлении, определяемым флагом I/D: 0 - вправо, 1 - влево. При сдвиге не производится изменение содержимого DDRAM. изменяются только внутренние указатели расположения видимого начала строки в DDRAM.

 

S/C:                               флаг-команда, производящая вместе с флагом R/L операцию сдвига содержимого экрана (так же, как и в предыдущем случае, без изменений в DDRAM) или курсора. Определяет объект смещения: 0 - сдвигается курсор, 1 - сдвигается экран.

 

R/L:               флаг-команда, производящая вместе с флагом S/C операцию сдвига экрана или курсора. Уточняет направление сдвига: 0 - влево, 1 - вправо.

 

D/L:               флаг, определяющий ширину шины данных: 0 - 4 разряда, 1 - 8 разрядов.

 

N:                   режим развертки изображения на ЖКИ: 0 - одна строка, 1 - две строки

 

F:                    размер матрицы символов: 0 - 5 х 8 точек, 1 - 5 х 10 точек.

 

D:                   наличие изображения: 0 - выключено, 1 - включено

 

С:                   курсор в виде подчерка: 0 - выключен, 1 - включен

 

В:                   курсор в виде мерцающего знакоместа: 0 - выключен, 1 - включен. 

 

 

 

 

Таблица 11. Значения управляющих флагов после подачи питания

 

I/D = 1:                   режим увеличения сетчика на 1

 

S = 0:                       без сдвига изображения

 

D/L = 1:                  8-ми разрядная шина данных

 

N = 0:                      режим развертки одной строки

 

F = 0:                       символы с матрицей 5 х 8 точек

 

D = 0:                      отображение выключено

 

С = 0:                      курсор в виде подчерка выключен

 

В = 0:                      курсор в виде мерцающего знакоместа выключен

 

 

Список управляющих комбинаций битов регистра IR и выполняемые ими команды приведены в таблице 12. Так как на момент включения ЖКИ-модуль ничего не отображает (флаг D = 0), то для того, чтобы вывести какой-либо текст необходимо, как минимум, включить отображение, установив флаг D = 1. Вот пример широко распространенной последовательности для инициализации ЖКИ-модуля: $38, $OC, $06 (знак «$» перед числом указывает на шестнадцатеричное основание). $38 устанавливает режим отображения 2-х строк с матрицей 5 х 8 точек и работу с 8-ми разрядной шиной данных; $OC включает отображение на экране ЖКИ-можуля, без отображения курсоров; $06 устанавливает режим автоматического перемещения курсора слева-направо после вывода каждого символа.

Контроллер HD44780 поддерживает как операции записи так и операции чтения. Чтение регистра DR приводит к загрузке содержимого DDRAM или CGRAM, в зависимости от текущего режима, при этом курсор смещается на одну позицию, как и при записи. Чтение регистра IR возвращает 8 значащих разрядов, причем в 7-ми младших содержится текущее значение счетчика АС (7 разрядов, если адресуется DDRAM, и 6 -если CGRAM), а в старшем - флаг занятости BF. Этот флаг имеет значение 1 когда контроллер занят и 0 - когда свободен. Необходимо учитывать, что большинство операций, выполняемых контроллером, занимают значительное время, около 40 мкс, а время выполнения некоторых доходит до единиц миллисекунд, поэтому цикл ожидания снятия флага BF должен обязательно присутствовать в программах драйвера ЖКИ-модуля и предшествовать совершению любой операции, поэтому он весьма удобен для отечественных применений. Это свойство контроллеров фирмы Epson обеспечило им заслуженную популярность, поэтому в последнее время основная масса импортируемых в нашу страну ЖКИ-модулей оснащены именно этим контроллером; в качестве примера в таблице 13 приведен набор символов этого контролера.

Один важный момент! После совершения операции записи или чтения DDRAM и появления после нее признака готовности (BF = 0), прочитанное в этом же цикле (вместе с флагом BF) значение АС скорее всего не будет достоверным. Дело в том, что между появлением признака готовности и вычислением контроллером нового значения АС существует некоторый временной интервал, составляющий около 4 мкс при тактовой частоте контроллера 270 кГц. Поэтому, если необходимо получить истинное значение АС, нужно совершить повторную операцию прочтения IR спустя не менее чем 4 мкс (если контроллер работает на частоте 270 кГц время ожидания необходимо пропорционально увеличить). Вывод на экран символа производится записью его кода в регистр DR. При этом символ размещается в DDRAM по текущему адресу, указываемому АС, а значение АС увеличивается или уменьшается на 1. Чтобы произвести переустановку курсора на нужную позицию, необходимо присвоить АС соответствующее значение (см. таблицу 12). Здесь есть одна тонкость: Когда производится последовательная запись символов и в результате заполняется вся строка, курсор автоматически переходит на вторую строку, но если необходимо принудительно установить курсор, скажем, на начало второй стороки, то будет неверным присвоить АС казалось бы логичное значение $28 (40), правильным является значение $40 (64). Значения адресов DDRAM в диапазоне $28...$3fF (а равно и $68...$7F) являются неопределенными и результаты работы с ними могут быть непредсказуемыми. Необходимо учитывать, что контроллеры, устанавливаемые на ЖКИ-модули, могут иметь различные наборы символов, причем это может зависеть как от производителя контроллера, так и от модификации данной конкретной модели. Например, фирма Powertip выпускает ЖКИ-модули с четырьмя базовыми модификациями наборов символов: японской, европейской, французской и русской.

Более того, существует как минимум два варианта русского набора символов: контроллер фирмы Hitachi (H2 по маркировке фирмы Powertip) и контроллер фирмы Epson (EH по маркировке Powertip). Контроллер фирмы Hitachi обладает существенным недостатком - у него весьма ограниченный набор русских символов, фактически у него имеются только прописные русские буквы, и даже среди них отсутствует символ «Ф». Напротив, контроллер фирмы Epson содержит полный набор русских символов (табл.13) в прописном и строчном вариантах, поэтому он весьма удобен для отечественных применений.

Из допустимых для размещения в DDRAM кодов символы с кодами $00...$07 (и их дубликат с кодами $08...$0F) имеют специальное назначение - это переопределяемые символы, графическое изображение которых может назначить сам потребитель, разместив соответствующую информацию в области CGRAM. Для программирования доступны 8 переопределяемых символов в режиме с матрицей 5 х 7 точек и 4 с матрицей 5 х 10 (в режиме 5 х 10 переопределяемые символы адресуются кодами DDRAM через один: $00, $02, $04, $06). Для кодирования матрицы используются горизонтально «уложенные» байты (рис.26), пять младших битов которых несут информацию о рисунке (причем 1 означает, что сегмент будет включен), 4-й разряд каждого из 8-ми (или 11-ти в режиме 5 х 10) байтов матрицы определяет левую колонку символа, а 0-й - правую. Старшие три бита не используются, равно как и старшие пять байтов, составляющих полную область матрицы символа (16 байтов) в режиме 5 х 10 (обратите внимание, что матрица программируемых символов допускает использование полной высоты строки (8 строчек для режима 5 х 7 и 11 строчек для режима 5 х 10), то есть можно размещать точки в области подчеркивающего курсора). Чтобы определить собственный симол необходимо установить счетчик АС на адрес начала матрицы требуемого символа в CGRAM - $00, $08, $10 и т.д. ($00, $10, $20 для режима 5 х 10 точек) - произвести перезапись всех байтов матрицы, начиная с верхней строки. После этого, записав в DDRAM код запрограммированного символа: $00, $01, $02 ($00, $02, $04 для режима 5 х 10 точек), на экране в соответствующем месте будет отображаться переопределенный символ.

Несколько слов о процессе инициализации ЖКИ-модуля (рис.27). Производитель контроллера рекомендует выполнять следующую последовательность действий для инициализации (рис.26). Выдержать паузу не менее 15 мс между установлением рабочего напряжения питания (> 4,5 В) и выполнением каких-либо операций с контроллером. Первой операцией выполнить команду, выбирающую разрядность шины (это должна быть команда $30 независимо от того, какой разрядности интерфейс вы собираетесь использовать в дальнейшем), причем перед выполнением этой операции не проверять значение флага BF. Далее опять выдержать паузу не менее 4,1 мс и повторить команду выбора разрядности шины, причем перед подачей команды вновь не производить проверку флага BF. Следующим шагом необходимо вновь выдержать паузу, на этот раз 100 мкс, и в третий раз повторить команду установления разрядности шины, вновь без проверки BF. Эти три операции являются инициализирующими и призваны вывести контроллер в исходный режим работы (то есть перевести в режим работы с 8-ми разрядной шиной) из любого состояния. Следом за ними нормальным порядком (без выдерживания пауз, но с проверкой флага BF) выполняется инициализация режимов работы с выдачей инициализирующей последовательности, аналогичной указанной в таблице 10 (содержащей, в том числе команду выбора необходимой разрядности шины).

Поскольку мы будем работать исключительно с с 4-х разрядной шиной, то необходимо помнить, что когда объявлям режим работы, то есть выдаём дисплею команду $20, то делаем это из 8-ми разрядного режима, который устанавливается автоматически после подачи напряжения питания, а значит, не сможете адекватно объявить необходимое значение флагов N и F, располагающихся в младшей тетраде команды установки разрядности шины. Поэтому команду необходимо повторить в уже установившемся 4-х разрядном режиме путем последовательной передачи двух тетрад, то есть для 4-х разрядного режима [13].

 Как вы уже заметили, таблица встроенного знакогенератора в части русских символов не совпадает с кодировкой принятой в windows, поэтому для прямого вывода русских букв из программы потребуется их перекодировка. Проще всего осуществить эту процедуру при помощи специальной таблицы. Для её составления необходимо ознакомиться с таблицей символов ANSI-кириллица, которая в windows имеет номер 1251. Именно в этой кодировке WinAVR сохраняет файлы с расширениями  *.с  и  *.h  нашей программы. На кодовой странице номер 1251 русские буквы (кириллица) размещаются следующим образом:

 Прописные буквы с "А" по "Я" (кроме "Ё") занимают места со 192 по 223.

Строчные буквы с "а" по "я" (также кроме "ё") расположены с 224 по 255.

Для букв "Ё" и "ё" отведены коды 168 и 184.

 

Вот и составим нашу таблицу перекодировки, используя полученные знания. Для этого создадим файл «codetable.h» и занесём в него массив code_tab[], в котором под номером русскоязычного символа за вычетом числа 192 (буква "А") будет расположен код этого самого символа в кодировке LCD Epson (таблица 13). Для экономии оперативной памяти микроконтроллера разместим массив в памяти программ. Для этого используем идентификатор  « __attribute__ ((progmem))».

Врезка 14:

 

//Таблица перекодировки

unsigned  char __attribute__ ((progmem)) code_tab[]=

                      {

                      0x41,0xa0,0x42,0xa1,0xe0,0x45,0xa3,0xa4,0xa5,0xa6,0x4b,0xa7,0x4d,0x48,0x4f,0xa8,

                      0x50,0x43,0x54,0xa9,0xaa,0x58,0xe1,0xab,0xac,0xe2,0xad,0xae,0x62,0xaf,0xb0,0xb1,

                      0x61,0xb2,0xb3,0xb4,0xe3,0x65,0xb6,0xb7,0xb8,0xb9,0xba,0xbb,0xbc,0xbd,0x6f,0xbe,

                      0x70,0x63,0xbf,0x79,0xe4,0x78,0xe5,0xc0,0xc1,0xe6,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,

                      0xa2,0xb5

                      }; 

 

 

Для перекодировки «на лету» текста при выводе его на ЖКИ напишем специальную функцию (врезка 15).

 

 

Врезка 15:

 

unsigned char DatCom;          //выводимый символ

 

if(DatCom)                                //если этот символ не «0», т.е. не является признаком конца строки или командой

      {             

      if (byte==0xA8) _lcd_WriteToDisplayA(pgm_read_byte(&code_tab[64]), DatCom);     //это буква «Ё»?

                                                                                                                                                                   //выводим через таблицу

      else if (byte==0xB8) _lcd_WriteToDisplayA(pgm_read_byte(&code_tab[65]), DatCom);//это буква «ё»?

                                                                                                                                                                   //выводим через таблицу

      else if (byte<0xC0) _lcd_WriteToDisplayA(byte, DatCom);                                                  //это не кириллица ?

                                                                                                                                                                   //выводим символ напрямую

      else _lcd_WriteToDisplayA(pgm_read_byte(&code_tab[byte-0xC0]), DatCom);            //значит это кириллица -

                                                                                                                                                                   //используем таблицу

      }

      else _lcd_WriteToDisplayA(byte, DatCom);               //если это какая-то команда – просто передаём её в LCD

 

 

Поскольку процесс инициализации ЖК-модуля и вывод символов на дисплей подробно описан выше, приведу лишь краткое описание подпрограмм для работы с ЖКИ, которые размещены в файле «lcddriver.c»:

1.      _lcd_Display_Init(); - инициализация дисплея;

2.      _lcd_SetPos (столбец, строка); - установка позиции курсора (счёт начинается с нуля);

3.      _lcd_FreeSpace(столбец, строка, кол-во символов); - очистка области дисплея;

4.      _lcd_WriteToDisplayA (команда/данные, число или символ); - передача команды LCD без применения таблицы перекодировки символов;

5.      _lcd_WriteToDisplay (команда/данные, число или символ); - передача команды LCD через таблицу перекодировки символов;

6.      _lcd_Print (строка); - вывод строки на LCD из ОЗУ;

7.      _lcd_PrintF (строка); - вывод строки на LCD из памяти программ (из flash);

8.      _lcd_Out (строка, кол-во символов); - вывод на LCD буфера;

9.      _lcd_ClrScr(); - очистка всего экрана;

10.  IntToStr(переменная, с какого символа, сколько символов заменять); - замена символов в массиве на числовое значение;

11.  _lcd_LoadGraphSymbols (символ, по какому адресу CGRAM записывать); - загрузить свой символ в LCD;

12.  _lcd_write4_byte (байт); - запись байта в LCD по 4-х разрядному интерфейсу;

13.  _lcd_read4_byte(); - чтение байта из LCD по 4-х разрядному интерфейсу.

 

Для осуществления плавного мигания подсветкой ЖК-дисплея необходимо изменять значение регистра ШИМ таймера 1 OCR1BL (врезка 16).

 

Врезка 16:

 

//Плавное мигание подсветкой в качестве приветствия

      for(n=3;n>0;n--)

      {             

                      while(OCR1BL)

                      {

                      _delay_ms(2);

                      OCR1BL--;

                      };

                                     

                      while(OCR1BL<Bright)

                      {

                      _delay_ms(2);

                      OCR1BL++;

                      }

      };           

 

 

Но сначала нам необходимо его правильно настроить. Для этого обратимся к описанию микроконтроллера AT Mega32 [14, 15].

Укрупненная функциональная схема 16-разр. таймера-счетчика показана на рисунке 28. Регистры ввода-вывода, а также биты или линии ввода-вывода, к которым организован доступ от ЦПУ, выделены жирной линией.

Регистр таймера-счетчика (TCNT1), регистры порогов сравнения (OCR1A/B), a также регистр захвата (ICR1) являются 16-разрядными регистрами. В связи с этим, во время доступа к этим регистрам должна быть соблюдена специальная процедура. Регистры управления таймером (TCCR1A/B) являются 8-разр. регистрами, поэтому, доступ к ним со стороны ЦПУ не связан с какими-либо ограничениями. Все сигналы запросов на прерывание представлены в регистре флагов прерываний таймеров (TIFR). Все прерывания индивидуально маскируются регистром макси прерываний таймеров (TIMSK). Регистры TIFR и TIMSK не представлены на функциональной схеме, т.к. они совместно используются другими таймерами микроконтроллера.

Таймер-счетчик может тактироваться внутренне через предделитель или внешне тактовым источником, подключенным к выводу T1. Блок выбора тактового источника позволяет выбрать тактовый источник и фронт, по которому будет изменяться состояние таймера-счетчика. Если тактовый источник не задан, то таймер-счетчик находится в неактивном состоянии. Сигнал на выходе блока выбора тактового источника является тактовым сигналом таймера (clkT1).

Значение регистров порогов сравнения (OCR1A/B) непрерывно сравнивается со значением счетчика. Результат сравнения может использоваться для генерации прямоугольных импульсов с ШИМ или с переменной частотой на выходах OC1A/B. В случае определения совпадения значений сравниваемых регистров устанавливается соответствующий флаг прерываний (OCF1A/B), который в свою очередь может служить источником прерывания.

Регистр захвата позволяет запомнить состояние таймера-счетчика при возникновении заданного внешнего события (фронт внешнего сигнала) на входе захвата фронта ICP1 или на выводах аналогового компаратора. На входе захвата фронта предусмотрена схема цифровой фильтрации (подавитель шума) для снижения риска срабатывания схемы захвата от помехи. Верхний предел или максимальное значение таймера-счетчика в зависимости от режима работы таймера могут определяться значением в OCR1A, ICR1 или иметь фиксированные значения. Если OCR1A задает верхний предел счета в режиме ШИМ, то он не может использоваться для генерации ШИМ-сигналов. Однако верхний предел в этом случае имеет двойную буферизацию, тем самым допуская изменение его значения в произвольный момент времени. Если верхний предел счета является постоянным значением, то альтернативно можно использовать регистр ICR1, освобождая регистр OCR1A для функции широтно-импульсной модуляции.

Далее договоримся о сокращениях, которые будем использовать применительно к таймерам-счётчикам микроконтроллера:

НП  - нижний предел счёта, которого достигает счётчик – его нулевое значение (0х0000);

МАКС  - максимальное значение, которого достигает счётчик - 0xFFFF (десятичное = 65535);

ВП  - верхний предел – верхний предел счета (вершина счета).

В качестве вершины счета могут выступать фиксированные значения 0x00FF, 0x01FF, 0x03FF или содержимое регистров OCR1A или ICR1.

Регистры TCNT1, OCR1A/B и ICR1 являются 16-разрядными, поэтому, доступ к ним через 8-разр. шину данных AVR ЦПУ может быть осуществлен с помощью двух инструкций чтения или записи. У каждого 16-разр. таймера имеется свой 8-разр. регистр для временного хранения старшего байта данных. Поэтому, во время доступа к 16-разр. регистрам одного таймера используется один и тот же временный регистр. Чтение/запись младшего байта инициирует 16-разр. операцию чтения/записи. Если выполняется запись младшего байта 16-разр. регистра, то за один такт ЦПУ одновременно записываются и младший байт и старший байт из временного регистра. Если выполняется чтение младшего байта 16-разр. регистра, то за один такт ЦПУ параллельно с чтением младшего байта происходит копирование старшего байта 16-регистра во временный регистр.

Не все 16-разрядные регистры используют временный регистр для копирования старшего байта. Чтение 16-разр. регистров OCR1A/B не связано с использованием временного регистра.

Таким образом, чтобы записать данные в 16-разр. регистр, необходимо сначала записать старший байт, а затем младший. А при чтении 16-разр. регистра, наоборот, сначала считывается младший байт, а затем старший.

Таймер-счетчик может использовать как внешний, так и внутренний тактовые сигналы. Источник тактового сигнала выбирается соответствующей схемой микроконтроллера под управлением бит выбора синхронизации (CS12:0), которые находятся в регистре управления таймером-счетчиком (TCCR1B).

Основным элементом 16-разр. таймера-счетчика является программируемый реверсивный 16-разрядный счетчик. На рисунке 29 представлена функциональная схема счетчика и окружающих его элементов.

Содержимое 16-разр. счетчика разбито на две 8-разр. ячейки, расположенных в памяти ввода-вывода: Старший байт счетчика (TCNT1H) , в котором хранятся старшие 8-разрядов счетчика, и младший байт счетчика (TCNT1L), в котором хранятся младшие 8-разрядов. ЦПУ не имеет непосредственного доступа к регистру TCNT1H. Если ЦПУ выполняет доступ к TCNT1H, то фактически обращение происходит к временному регистру. Во временный регистр копируется значение TCNT1H, если выполняется чтение регистра TCNT1L и в TCNT1H копируется содержимое временного регистра, если выполняется запись в TCNTnL. Такой механизм реализован для считывания/записи 16-разр. значения счетчика за один такт ЦПУ в условиях 8-разр. шины данных.

В зависимости от используемого режима работы каждый такт синхронизации таймера clkTn счетчик будет сбрасываться, инкрементироваться или декрементироваться. Сигнал clkTn может быть внешним или внутренним, что задается битами выбора синхронизации (CS12:0). Если тактовый источник не задан (CS12:0 = 0), то таймер останавливается. Однако содержимое TCNT1 остается доступным ЦПУ независимо от наличия синхронизации на clkTn. Если ЦПУ выполняет запись в TCNT1, то тем самым блокируется (запись имеет более высокий приоритет) любое действие счетчика: сброс или счет.

Алгоритм счета определяется значением бит режима работы таймера (WGM13:0), расположенных в регистрах А и В управления таймером-счетчиком (TCCR1A и TCCR1B). Имеется четкая связь между алгоритмом счета счетчика и формой генерируемого на выходах OC1A/B сигнала.

Установка флага переполнения таймера-счетчика (TOV1) происходит в зависимости выбранного с помощью бит WGM13:0 режима работы. Флаг TOV1 может использоваться для генерации прерывания ЦПУ.

При работе счётчика 16-разрядный цифровой компаратор непрерывно сравнивает значение TCNT1 со значением регистров порогов сравнения (OCR1A/B). Если значение TCNT1 равно OCR1x, то компаратор формирует сигнал совпадения (равенства). Следующий за совпадением такт ЦПУ устанавливает флаг сравнения (OCF1x). Если бит OCIE1x = 1, то установка флага сравнения приведет к генерации прерывания по результату сравнения. Флаг OCF1x автоматически сбрасывается после перехода на вектор обработки прерывания. Альтернативно флаг OCF1x сбрасывается программно, если записать в него лог. 1. Сигнал совпадения используется формирователем выходного сигнала, результирующая форма которого зависит от выбранного с помощью бит WGM13:0 режима работы таймера и режима формирования импульсов (биты COM1x1:0). Сигналы ВЕРХНИЙ ПРЕДЕЛ и НИЖНИЙ ПРЕДЕЛ используются формирователем импульсов для отработки особых случаев задания экстремальных значений в некоторых режимах работы. У канала сравнения А имеется своя отличительная особенность, которая позволяет задать верхний предел счета (т.е. разрешающую способность счетчика). В дополнение к разрешающей способности верхний предел определяет период формируемых импульсов.

Если задан любой из 12 режимов широтно-импульсной модуляции (ШИМ), то в этом случае регистр OCR1x содержит двойную буферизацию. Если таймер работает в нормальном режиме или режиме сброса при совпадении (CTC), то двойная буферизация отключается. Двойная буферизация синхронизирует обновление регистра порога сравнения OCR1x по достижении верхнего или нижнего предела счета в зависимости от выбранного режима работы (алгоритма счета). Такая синхронизация предотвращает возможность возникновения несимметричных ШИМ-импульсов нечетной длины, тем самым гарантируя отсутствие сбоев при генерации прямоугольных импульсов.

Доступ к регистру OCR1x хоть и кажется сложным, но выполнен таким образом не случайно. Если двойная буферизация разрешена, то ЦПУ фактически осуществляет доступ к буферному регистру OCR1x. Если же двойная буферизация отключена, то ЦПУ обращается к регистру OCR1x непосредственно. Содержимое регистра OCR1x (в т.ч. и буферного) может измениться только путем непосредственной записи в него (таймер-счетчик не обновляет содержимое данного регистра автоматически аналогично регистрам TCNT1 и ICR1). Таким образом, OCR1x считывается напрямую, а не через временный регистр старшего байта. Запись регистров OCR1x происходит через временный регистр, т.к. все 16 разр. участвуют в сравнении непрерывно. Первым необходимо записать старший байт OCR1xH. Если выполняется запись по адресу старшего байта, то содержимое временного регистра обновляется записываемым значением. Если выполняется запись младшего байта (OCR1xL), то параллельно копируется содержимое временного регистра в старшие 8-разрядов буферного регистра OCRnx или регистра порога сравнения OCR1x, тем самым обновляя все 16 разрядов за один такт ЦПУ.

Поскольку запись в TCNT1 в любом из режимов работы блокирует отработку совпадения на один такт синхронизации таймера, то имеются некоторые опасные ситуации при изменении TCNT1 во время использования любого из каналов сравнения, независимо работает таймер-счетчик или нет. Если в TCNT1 записано значение равное OCR1x, то возникающее совпадение игнорируется, тем самым вызывая некорректную генерацию импульсов. Следует избегать записи в TCNT1 значения равного верхнему пределу в ШИМ-режимах с переменным значением верхнего предела. В этом случае совпадение по достижении верхнего предела игнорируется и счет продолжится до 0xFFFF. Аналогично, следует избегать записи в TCNT1 значения равного нижнему пределу, если счетчик работает как вычитающий (обратный счет). Прежде чем настроить вывод OC1x на вывод в регистре направления данных необходимо выполнить инициализацию регистра OC1x. Самым простым способом решения этой задачи является использование бита принудительной установки результата сравнения (FOC1x) при работе таймера в нормальном режиме. Регистр OC1x сохраняет свое состояние даже при изменении режима работы таймера.

Учтите, что биты COM1x1:0 не содержат схемы двойной буферизации и любые их изменения вступают в силу мгновенно.

Биты задания режима формирования выходного сигнала (COM1x1:0) имеют двойное назначение. С одной стороны биты COM1x1:0 используются формирователем сигнала и определяют какое логическое состояние должно быть на выходе OC1x при возникновении следующего совпадения. С другой стороны, биты COM1x1:0 используются для разрешения/запрета альтернативной функции вывода порта OC1x. На рисунке 30 представлена упрощенная логическая схема, на которую воздействуют биты COM1x1:0. На рисунке показаны только те регистры управления портом ввода-вывода (DDR и PORT), на которые оказывает действие биты COM1x1:0. Если происходит системный сброс, то выход регистра OC1x принимает нулевое состояние.

Функция линии универсального порта ввода-вывода заменяется на функцию выхода формирователя сигнала OC1x, если хотя бы один из бит COM01, COM00 установлен (логика ИЛИ). Однако, управление направлением вывода OC1x (вход или выход) в этом случае остается за соответствующим битом регистра направления данных (DDR). Чтобы значение регистра OC1x присутствовало на выводе OC1x необходимо настроить данную линию на вывод (установить соотв. бит в DDRD). Управление вводом альтернативной функции не зависит от режима работы таймера за некоторыми исключениями. Забегая вперёд скажу, что мы будем использовать данное обстоятельство для включения/отключения звукового сигнала и подсветки LCD-дисплея.

Схемотехника выходной логики позволяет инициализировать состояние регистра OC1x перед разрешением настройки вывода OC1x в качестве выхода.

Установки бит COM1x1:0 оказывают различное влияние в зависимости от выбранного режима работы: нормального, сброса при совпадении и ШИМ. Общим для всех режимов работы является не выполнение каких-либо действий с регистром OC1x при возникновении совпадения, если COM1x1:0 = 0. В таблице 14 описано действие различных установок этих бит для режимов без ШИМ. Аналогичная информация для режима с быстрой ШИМ приведена в таблице 15, а для ШИМ с фазовой и частотной коррекцией в таблице 15.

Изменение состояния бит COMnx1:0 вступает в силу при следующем после их записи совпадении. В режимах без ШИМ воздействовать на генерацию импульсов можно с помощью стробирующего бита принудительной установки результата сравнения FOC1x.

Под режимом работы 16-разр. таймера понимается его алгоритм счета и поведение связанного с ним выхода формирователя импульсов, что определяется комбинацией бит, задающих режим работы таймера (WGM13-10) и режим формирования выходного сигнала (COM1x1:0). При этом биты задания режима формирования выходного сигнала не влияют на алгоритм счета, т.к. алгоритм счета зависит только от состояния бит задания режима работы таймера. В режимах с ШИМ биты COM1x1:0 позволяют включить/отключить инверсию на генерируемом ШИМ-выходе (т.е. выбрать ШИМ с инверсией или ШИМ без инверсии). Для режимов без ШИМ биты COM1x1:0 определяют, какое действие необходимо выполнить при возникновении совпадения: сбросить, установить или инвертировать выход.

Самым простым режимом работы является нормальный режим (WGM13-10 = 0b0000). В данном режиме счетчик работает как суммирующий (инкрементирующий), при этом сброс счетчика не выполняется. Переполнение счетчика происходит при переходе через максимальное 16-разр. значение (0xFFFF) к нижнему пределу счета (0x0000). В нормальном режиме работы флаг переполнения таймера-счетчика TOV1 будет установлен на том же такте синхронизации, когда TCNT1 примет нулевое значение.

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

Блок сравнения может использоваться для генерации прерываний. Не рекомендуется использовать выход OC1x для генерации сигналов в нормальном режиме работы, т.к. в этом случае будет затрачена значительная часть процессорного времени.

В режиме сброса таймера при совпадении (СТС) (WGM01, WGM00 =0b10) регистр OCR1 используется для задания разрешающей способности счетчика. Если задан режим CTC и значение счетчика (TCNT1) совпадает со значением регистра OCR1, то счетчик обнуляется (TCNT1=0). Таким образом, OCR1 задает вершину счета счетчика, а, следовательно, и его разрешающую способность. В данном режиме обеспечивается более широкий диапазон регулировки частоты генерируемых прямоугольных импульсов. Он также упрощает работу счетчика внешних событий.

В режиме сброса таймера при совпадении (WGM13-0 = 0b0100 или 0b1100) разрешающая способность таймера задается регистрами OCR1A или ICR1. В режиме СТС происходит сброс счетчика (TCNT1), если его значение совпадает со значением регистра OCR1A (WGMn3-0 = 0b0100) или с ICR1 (WGMn3-0 = 0b1100). Значение регистра OCR1A или ICR1 определяет верхний предел счета, а, следовательно, и разрешающую способность таймера. В данном режиме обеспечивается более широкий диапазон регулировки частоты генерируемых прямоугольных импульсов. Он также упрощает работу счетчика внешних событий. Временная диаграмма работы таймера в режиме СТС показана на рисунке 31. Счетчик (TCNT1) инкрементирует свое состояние до тех пор, пока не возникнет совпадение со значением OCR1A или ICR1, а затем счетчик (TCNT1) сбрасывается.

По достижении верхнего предела счета может генерироваться прерывание с помощью флагов OCF1A или ICF1, соответствующим используемым регистрам для задания верхнего предела счета. Если прерывание разрешено, то процедура обработки прерывания может использоваться для обновления верхнего предела счета. Однако, задание значения вершины счета близкого к значению нижнего предела счета, когда счетчик работает без предделения или с малым значением предделения, необходимо выполнять с особой осторожностью, т.к. в режиме СТС нет двойной буферизации. Если значение, записанное в OCR1A или ICR1, меньше текущего значения TCNT1, то сброс счетчика по условию совпадения наступит, когда он достигнет максимального значения (0xFFFF), затем перейдет в исходное состояние 0x0000 и достигнет нового значения OCR1A или ICR1. Во многих случаях возникновение такой ситуации не желательно. В качестве альтернативы может выступить режим быстрой ШИМ, где регистр OCR1A определяет верхний предел счета (WGM13-10 = 0b1111), т.к. в этом случае OCR1A имеет двойную буферизацию.

Для генерации сигнала в режиме CTC выход OC1A может использоваться для изменения логического уровня при каждом совпадении, для чего необходимо задать режим переключения (COM1A1, COM1A0 = 0b01). Значение OC1A будет присутствовать на выводе порта, только если для данного вывода задано выходное направление. Максимальная частота генерируемого сигнала равна fOC0 = fclk_I/O/2, если OCR1A = 0x0000. Для других значений OCR1 частоту генерируемого сигнала можно определить по формуле:

,                                                            (2)

 

где переменная N задает коэффициент деления предделителя (1, 8, 32, 64, 128, 256 или 1024).

 

Также как и для нормального режима работы, флаг TOV1 устанавливается на том же такте таймера, когда его значение изменяется с 0xFFFF на 0x0000.

Режим быстрой широтно-импульсной модуляции (ШИМ) (WGM13-10 = 0b0101, 0b0110, 0b0111, 0b1110, 0b1111) предназначен для генерации ШИМ-импульсов повышенной частоты. В отличие от других режимов работы в этом используется однонаправленная работа счетчика. Счет выполняется в направлении от нижнего к верхнему пределу счета.

Если задан неинвертирующий режим выхода, то при совпадении TCNT1 и OCR1x сигнал OC1x устанавливается, а на верхнем пределе счета сбрасывается. Если задан инвертирующий режим, то выход OC1x сбрасывается при совпадении и устанавливается на верхнем пределе счета. За счет однонаправленности счета, рабочая частота для данного режима в два раза выше по сравнению с режимом ШИМ с фазовой коррекцией, где используется двунаправленный счет. Возможность генерации высокочастотных ШИМ сигналов делает использование данного режима полезным в задачах стабилизации питания, выпрямления и цифро-аналогового преобразования. Высокая частота, при этом, позволяет использовать внешние элементы физически малых размеров (индуктивности, конденсаторы), тем самым снижая общую стоимость системы.

Разрешающая способность ШИМ может быть фиксированной 8, 9 или 10 разрядов или задаваться регистром ICR1 или OCR1A, но не менее 2 разрядов (ICR1 или OCR1A = 0x0003) и не более 16 разрядов (ICR1 или OCR1A = 0xFFFF). Разрешающая способность ШИМ при заданном значении верхнего предела (ВП) вычисляется следующим образом:

 

                                                                        (3)

 

В режиме быстрой ШИМ счетчик инкрементируется до совпадения его значения с одним из фиксированных значений 0x00FF, 0x01FF или 0x03FF (если WGMn3:0 = 0b0101, 0b0110 или 0b0111, соответственно), значением в ICR1 (если WGM13:10 = 0b1110) или значением в OCR1A (если WGM13:10 = 0b1111), а затем сбрасывается следующим тактом синхронизации таймера. Временная диаграмма для режима быстрой ШИМ представлена на рисунке 32. На рисунке показан режим быстрой ШИМ, когда для задания верхнего предела используется регистр OCR1A или ICR1. Значение TCNT1 на временной диаграмме показано в виде графика функции для иллюстрации однонаправленности счета. На диаграмме показаны как инвертированный, так и неинвертированный ШИМ-выходы. Короткой горизонтальной линией показаны точки на графике TCNT1, где совпадают значения OCR1x и TCNT1x. Флаг прерывания OC1x устанавливается при возникновении совпадении.

Флаг переполнения таймера-счетчика (TOV1) устанавливается всякий раз, когда счетчик достигает верхнего предела. Дополнительно тем же тактовым импульсом вместе с флагом TOV1 могут установиться флаги OC1A или ICF1, если для задания верхнего предела используется регистр OCR1A или ICR1, соответственно. Если одно из этих прерываний разрешено, то в процедуре обработки прерывания может быть выполнено обновление верхнего предела счета и порогов сравнения.

Если изменяется значение верхнего предела счета, то необходимо соблюдение условия, чтобы записываемое новое значение верхнего предела было больше или равно значений во всех регистрах порога сравнения. В противном случае совпадение между TCNT1 и OCR1x никогда не возникнет. Обратите внимание, что при использовании фиксированных значений верхнего предела во время записи в регистры OCR1x происходит маскирование к 0 неиспользуемых разрядов.

Механизм модификации регистра ICR1 отличается от OCR1A в том случае, если он используется для задания верхнего предела. Регистр ICR1 не имеет двойной буферизации. Это означает, что если в ICR1 записывается малое значение во время работы счетчика с малым предделением или без него, то имеется опасность записи в регистр ICR1 значения, которое окажется меньше текущего значения TCNT1. Как результат, в такой ситуации будет пропущено совпадение на вершине счета. В этом случае счетчик дойдет до максимального значения (0xFFFF), перезапустится со значения 0x0000, а только затем возникнет совпадение. Регистр OCR1A содержит схему двойной буферизации, поэтому, его можно модифицировать в любой момент времени.

Если выполняется запись по адресу OCR1A, то фактически значение помещается в буферный регистр OCR1A. Если же возникает совпадение между TCNT1 и вершиной счета, то следующим тактом синхронизации таймера происходит копирование буферного регистра в регистр порога сравнения OCR1A. Обновление регистра выполняется тем же тактом, что и сброс TCNT1 и установка флага TOV1.

Рекомендуется использовать регистр ICR1 для задания верхнего предела, если верхний предел счета является константой. В этом случае также освобождается регистр OCR1A для генерации ШИМ-сигнала на выходе OC1A. Однако, если частота ШИМ динамически изменяется (за счет изменения верхнего предела), то в этом случае выгоднее использовать регистр OCR1A для задания верхнего предела, т.к. он поддерживает двойную буферизацию.

В режиме быстрой ШИМ блоки сравнения позволяют генерировать ШИМ-сигналы на выводах OC1x. Если COM1x1:0 =0b10, то задается ШИМ без инверсии выхода, а если COM1x1:0 = 0b11, то задается режим ШИМ с инверсией на выходе. Фактическое значение OC1x можно наблюдать на выводе порта, если для него задано выходное направление (DDR_OC1x). ШИМ-сигнал генерируется путем установки (сброса) регистра OC1x при возникновении совпадения между OCR1x и TCNT1, а также путем сброса (установки) регистра OC1x вместе со сбросом счетчика (переход с верхнего предела на нижний предел).

Частота ШИМ выходного сигнала для заданного значения верхнего предела (ВП) определяется выражением:

 

,                                                                          (4)

 

где N – переменная, которая задает значение коэффициента предделения (1, 8, 32, 64, 128, 256 или 1024).

 

Запись предельных значений в регистр OCR1x связана с особыми случаями в генерации ШИМ-импульсов. Если OCR1x установить равным нижнему пределу (0x0000), то на выходе будет возникать короткий импульс каждый (ВП+1)-ый такт синхронизации таймера. Запись в OCR1x значения равного верхнему пределу приведет к установке постоянного уровня лог. 1 или 0 на выходе (зависит от выбранной с помощью бит COM1x1:0 полярности выходного сигнала).

Если требуется генерация меандра (прямоугольные импульсы со скважностью 2 или заполнением 50%) высокой частоты, то необходимо использовать режим быстрой ШИМ с установкой бит COM1A1:0 = 0b01, которая вызывает переключение (инвертирование) логического уровня на выходе OC1A при каждом совпадении. Данное применимо, только если OCR1A используется для задания верхнего предела (WGM13-10 =0b1111). Максимальная генерируемая частота меандра в этом случае fOC1A = fclk_I/O/2, если OCR1A =0x0000.

Режим широтно-импульсной модуляции с фазовой коррекцией (ШИМ ФК) (WGM13-10 = 0b0001, 0b010, 0b0011, 0b1010 или 0b1011) предназначен для генерации ШИМ сигнала с фазовой коррекцией и высокой разрешающей способностью. Режим ШИМ ФК основан на двунаправленной работе таймера-счетчика. Счетчик циклически выполняет счет в направлении от нижнего предела (0x0000) до верхнего предела, а затем обратно от верхнего предела к нижнему пределу. Если задан неинвертирующий режим выхода формирователя импульсов, то выход OC1x сбрасывается/устанавливается при совпадении значений TCNT1 и OCR1x во время прямого/обратного счета. Если задан инвертирующий режим выхода, то, наоборот, во время прямого счета происходит установка, а во время обратного – сброс выхода OC1x. При двунаправленной работе максимальная частота ШИМ-сигнала меньше, чем при однонаправленной работе, однако, за счет такой особенности, как симметричность в режимах ШИМ с двунаправленной работой, данные режимы предпочитают использовать при решении задач управления приводами.

Разрешающая способность ШИМ в данном режиме может быть либо фиксированной (8, 9 или 10 разрядов) либо задаваться с помощью регистра ICR1 или OCR1A. Минимальная разрешающая способность равна 2-м разрядам (ICR1 или OCR1A = 0x0003), а максимальная -16-ти разрядам (ICR1 или OCR1A =0xFFFF). Если задан верхний предел, то разрешающая способность ШИМ в данном режиме определяется следующим образом:

 

.                                                                   (5)

 

В режиме ШИМ ФК счетчик инкрементируется пока не достигнет одного из фиксированных значений 0x00FF, 0x01FF или 0x03FF (соответственно для WGM13-10 = 0b0001, 0b0010 или 0b0011), а также значения равного ICR1 (если WGM13-0 = 0b1010) или OCR1A (если WGM13:10 = 0b1011). Далее, при достижении верхнего предела, счетчик изменяет направление счета. Значение TCNT1 остается равным верхнему пределу в течение одного такта синхронизации таймера. Временная диаграмма для режима ШИМ ФК представлена на рисунке 33. На рисунке показан режим ШИМ ФК с использованием регистра OCR1A или ICR1 для задания верхнего предела. Состояние TCNT1 представлено в виде графика функции для иллюстрации двунаправленности счета. На рисунке представлены, как неинвертированный, так и инвертированный ШИМ-выход. Короткие горизонтальные линии указывают точки на графике изменения TCNT1, где возникает совпадение со значением OCR1x. Флаг прерывания OC1x устанавливается при возникновении совпадения.

Флаг переполнения таймера-счетчика (TOV1) устанавливается всякий раз, когда счетчик достигает нижнего предела. Если для задания верхнего предела используется регистр OCR1A или ICR1, то, соответственно устанавливается флаг OC1A или ICF1 тем же тактовым импульсом, на котором произошло обновление регистра OCR1x из буферного регистра (на вершине счета). Флаги прерывания могут использоваться для генерации прерывания по достижении счетчиком нижнего или верхнего предела.

При изменении значения верхнего предела счета необходимо следить, чтобы оно было больше или равно значениям во всех регистрах сравнения. В противном случае совпадение между TCNT1 и OCR1x никогда не возникнет. Обратите внимание, что при использовании фиксированных значений верхнего предела счета во время записи в регистры OCR1x неиспользуемые разряды обнуляются. Третий период на рисунке 33 иллюстрирует случай, когда динамическое изменение верхнего предела счета приводит к генерации несимметричного импульса. Данная особенность основывается на времени обновления регистра OCR1x. Поскольку, обновление OCR1x возникает на вершине счета, то и период ШИМ начинается и заканчивается на вершине счета. Это подразумевает, что длительность обратного счета определяется предыдущим значением верхнего предела, а прямого – новым значением верхнего предела. Если два этих значения разные, то и длительность прямого и обратного счета будет также отличаться. Различие в длительности приводит несимметричности выходных импульсов.

Если стоит задача изменения верхнего предела при работающем счетчике, то вместо этого режима рекомендуется использовать режим ШИМ ФЧК (фазовая и частотная коррекция). Если используется статическое значение верхнего предела, то между данными режимами практически нет отличий.

В режиме ШИМ ФК блоки сравнения позволяют генерировать ШИМ-сигналы на выводах OC1x. Если установить COM1x1:0 = 0b10, то выход ШИМ будет без инверсии, а если COM1x1:0=0b11, то с инверсией. Фактическое значение OC1x можно наблюдать на выводе порта, если в регистре направления данных для данного вывода порта задано выходное направление (DDR_OC1x). ШИМ-сигнал генерируется путем установки (сброса) регистра OC1x при совпадении значений OCR1x и TCNT1 во время прямого счета, а также путем сброса (установки) регистра OC1x при совпадении между OCR1x и TCNT1 во время обратного счета. Результирующая частота ШИМ-сигнала в режиме ШИМ ФК при заданном верхнем пределе (ВП) может быть вычислена по следующему выражению:

,                                                                    (6)

 

где N – коэффициент деления предделителя (1, 8, 32, 64, 128, 256 или 1024).

 

Запись предельных значений в регистр OCR1x связано с особыми случаями в генерации ШИМ-сигналов в режиме ШИМ ФК. Если задать режим ШИМ без инверсии и OCR1x установить равным нижнему пределу, то на выходе непрерывно будет установлен лог. 0, а если равным верхнему пределу, то на выходе постоянно присутствует лог. 1. Для ШИМ с инверсией указанные уровни необходимо заменить противоположными.

Если задать использование OC1A в качестве верхнего предела (WGM13:10 = 0b1011) и установить COM1A1:0 =0b01, то на выходе OC1A будет генерироваться меандр.

Режим широтно-импульсной модуляции с фазовой и частотной коррекцией (ШИМ ФЧК) (WGM13-10 = 0b1000 или 0b1001) предназначен для генерации ШИМ-импульсов высокой разрешающей способности с фазовой и частотной коррекцией. Также как и режим ШИМ ФК режим ШИМ ФЧК основан на двунаправленной работе счетчика. Счетчик циклически считает от нижнего предела (0x0000) до верхнего предела, а затем обратно от верхнего предела к нижнему пределу. Если задан неинвертирующий режим ШИМ, то выход OC1x сбрасывается, если возникает совпадение между TCNT1 и OCR1x во время прямого счета, и устанавливается, если возникает совпадение во время обратного счета. В инвертирующем режиме работа инверсная. Двунаправленная работа, по сравнению с однонаправленной, связана с генерацией более низких частот. Однако, благодаря симметричности в режимах ШИМ с двунаправленным счетом, их применение предпочтительно в задачах управления приводами.

Основное отличие между режимами ШИМ ФК и ШИМ ФЧК состоит в моменте обновления регистра OCR1x из буферного регистра OCR1x (см. рисунок 33 и рисунок 34).

Разрешающая способность ШИМ в этом режиме может задаваться с помощью регистра ICR1 или OCR1A. Минимальная разрешающая способность равна 2-ум разрядам (ICR1 или OCR1A = 0x0003), а максимальная разрешающая способность - 16-ти разрядам (ICR1 или OCR1A = 0xFFFF). Разрешающая способность ШИМ в разрядах может быть вычислена по следующему выражению:

 

.                                                      (7)

 

В режиме ШИМ ФЧК счетчик инкрементируется до совпадения со значением в ICR1 (WGM13:10 = 0b1000) или в OCR1A (WGM13:10 = 0b1001). Это означает достижение вершины счета, после чего происходит изменение направления счета. Значение TCNT1 остается равным вершине счета в течение одного такта синхронизации таймера. Временная диаграмма для режима ШИМ ФЧК показана на рисунке 34. На рисунке показан режим ШИМ ФЧК, когда вершину счета задает регистр OCR1A или ICR1. Значение TCNT1 показано в виде графика функции для иллюстрации двунаправленности счета. На диаграмме показан как неинвертирующий, так и инвертирующий ШИМ выходы. Короткие горизонтальные линии указывают на точки график TCNT1, где возникает совпадение между OCR1x и TCNT1. Флаг прерывания OC1x устанавливается после возникновения совпадения.

Флаг переполнения таймера-счетчика (TOV1) устанавливается тем же тактом, когда произошло обновление регистров значением из буферного регистра (на нижнем пределе счета). Если для задания верхнего предела используется регистр OCR1A или ICR1, то по достижении счетчиком верхнего предела устанавливается флаг OC1A или ICF1, соответственно. Флаги прерывания могут использоваться для генерации прерывания при достижении счетчиком верхнего или нижнего предела.

При изменении верхнего предела необходимо следить, чтобы новое значение было больше или равно значениям во всех регистрах порога сравнения. В противном случае, если задано значение верхнего предела меньше любого из значений регистров порога сравнения, совпадение между TCNT1 и OCR1x никогда не наступит.

На рисунке 34 показано, что в отличие от режима ШИМ ФК, генерируемый выходной сигнал симметричен на всех периодах. Поскольку, регистры OCR1x обновляются на нижнем пределе счета, то длительности прямого и обратного счетов всегда равны. В результате выходные импульсы имеют симметричную форму, а, следовательно, и откорректированную частоту.

Использование регистра ICR1 для задания верхнего предела рекомендуется, если значение верхнего предела является константой. В этом случае также освобождается регистр OCR1A для широтно-импульсной модуляции импульсов на выводе OC1A. Однако если требуется динамическое изменение частоты ШИМ за счет изменения верхнего предела, то для задания верхнего предела рекомендуется использовать регистр OCR1A за счет наличия у него двойной буферизации.

В режиме ШИМ ФЧК блоки сравнения позволяют генерировать ШИМ-импульсы на выводе OC1x. Если COM1x1:0 = 0b10, то задается неинвертирующий ШИМ выход, а, если COM1x1:0=0b11, то инвертирующий. Значение OC1x будет присутствовать на соответствующем выводе порта только в случае, если для него задано выходное направление. ШИМ сигнал генерируется путем установки (сброса) регистра OC1x при совпадении между OCR1x и TCNT1 во время прямого счета и сброса (установки) регистра OC1x при совпадении между OCR1x и TCNT1 во время обратного счета. Частота ШИМ в данном режиме при заданном верхнем пределе (ВП) счета определяется следующим образом:

,                                                              (8)

 

где N – коэффициент деления предделителя (1, 8, 32, 64, 128, 256 или 1024).

Запись предельных значений в регистр OCR1x связана с особыми случаями в генерации ШИМ-сигналов в данном режиме. Если задать OCR1x равным нижнему пределу (0x0000), то в неинвертирующем режиме на выходе будет постоянного присутствовать низкий логический уровень, а при записи значения равного верхнему пределу на выходе будет длительно присутствовать высокий логический уровень. В инвертирующем режиме приведенные уровни будут противоположными.

Если OCR1A используется для задания верхнего предела (WGM13:10 = 0b1001) и COM1A1:0 = 0b01, то на выходе OC1A будет генерироваться меандр.

Для управления устройством, в том числе и описанными выше функциями, включая поиск датчиков температуры нам понадобиться драйвер клавиатуры. Назовём его «keydriverADC.c», а соответствующий ему заголовочный файл «keydriverADC.h». Поскольку схема клавиатуры представляет собой резисторный делитель напряжения питания, образованный последовательно соединёнными сопротивлениями номиналом 4,7 кОм., для преобразования напряжения в код нажатой клавиши нам понадобиться встроенный в микроконтроллер аналого-цифровой преобразователь (ADC).

Микроконтроллер ATmega32 содержит 10-разр. АЦП последовательного приближения. АЦП связан с 8-канальным аналоговым мультиплексором, 8 однополярных входов которого связаны с линиями порта A.

АЦП содержит УВХ (устройство выборки-хранения), которое поддерживает на постоянном уровне напряжение на входе АЦП во время преобразования. Функциональная схема АЦП показана на рисунке 35.

АЦП имеет отдельный вывод питания AVCC (аналоговое питание). AVCC не должен отличаться более чем на ± 0.3В от VCC.

В качестве внутреннего опорного напряжения может выступать напряжение от внутреннего источника опорного напряжения (ИОН) на 2.56В или напряжение AVCC. Если требуется использование внешнего ИОН, то он должен быть подключен к выводу AREF с подключением к этому выводу блокировочного конденсатора для улучшения шумовых характеристик.

АЦП преобразовывает входное аналоговое напряжение в 10-разрядный код методом последовательных приближений. Минимальное значение соответствует уровню GND, а максимальное уровню AREF минус 1 младший разряд. К выводу AREF опционально может быть подключено напряжение AVCC или внутренний ИОН на 2,56 В. путем записи соответствующих значений в биты REFSn в регистр ADMUX. Несмотря на то, что ИОН на 2,56 В. находится внутри микроконтроллера, к его выходу может быть подключен блокировочный конденсатор для снижения чувствительности к шумам, т.к. он связан с выводом AREF.

Канал аналогового ввода выбираются путем записи бит MUX в регистр ADMUX. В качестве однополярного аналогового входа АЦП может быть выбран один из входов ADC0…ADC7, а также GND и выход фиксированного источника опорного напряжения.

Работа АЦП разрешается путем установки бита ADEN в ADCSRA. Выбор опорного источника и канала преобразования не возможно выполнить до установки ADEN. Если ADEN = 0, то АЦП не потребляет ток, поэтому, при переводе в экономичные режимы сна рекомендуется предварительно отключить АЦП.

АЦП генерирует 10-разрядный результат, который помещается в пару регистров данных АЦП ADCH и ADCL. По умолчанию результат преобразования размещается в младших 10-ти разрядах 16-разр. слова (выравнивание справа), но может быть опционально размещен в старших 10-ти разрядах (выравнивание слева) путем установки бита ADLAR в регистре ADMUX.

Практическая полезность представления результата с выравниванием слева существует, когда достаточно 8-разрядное разрешение, т.к. в этом случае необходимо считать только регистр ADCH. В другом же случае необходимо первым считать содержимое регистра ADCL, а затем ADCH, чем гарантируется, что оба байта являются результатом одного и того же преобразования. Как только выполнено чтение ADCL блокируется доступ к регистрам данных со стороны АЦП. Это означает, что если считан ADCL и преобразование завершается перед чтением регистра ADCH, то ни один из регистров не может модифицироваться и результат преобразования теряется. После чтения ADCH доступ к регистрам ADCH и ADCL со стороны АЦП снова разрешается.

АЦП генерирует собственный запрос на прерывание по завершении преобразования. Если между чтением регистров ADCH и ADCL запрещен доступ к данным для АЦП, то прерывание возникнет, даже если результат преобразования будет потерян.

Одиночное преобразование запускается путем записи лог. 1 в бит запуска преобразования АЦП ADSC. Данный бит остается в высоком состоянии в процессе преобразования и сбрасывается по завершении преобразования. Если в процессе преобразования переключается канал аналогового ввода, то АЦП автоматически завершит текущее преобразование прежде, чем переключит канал.

В режиме автоматического перезапуска АЦП непрерывно оцифровывает аналоговый сигнал и обновляет регистр данных АЦП. Данный режим задается путем записи лог. 1 в бит ADFR регистра ADCSRA. Первое преобразование инициируется путем записи лог. 1 в бит ADSC регистра ADCSRA. В данном режиме АЦП выполняет последовательные преобразования, независимо от того сбрасывается флаг прерывания АЦП ADIF или нет.

Если требуется максимальная разрешающая способность (10 разрядов), то частота на входе схемы последовательного приближения должна быть в диапазоне 50…200 кГц. Если достаточно разрешение менее 10 разрядов, но требуется более высокая частота преобразования, то частота на входе АЦП может быть установлена свыше 200 кГц.

Модуль АЦП содержит предделитель (рис.36), который формирует производные частоты свыше 100 кГц по отношению к частоте синхронизации ЦПУ. Коэффициент деления устанавливается с помощью бит ADPS в регистре ADCSRA. Предделитель начинает счет с момента включения АЦП установкой бита ADEN в регистре ADCSRA. Предделитель работает пока бит ADEN = 1 и сброшен, когда ADEN=0.

Нормальное преобразование требует 13 тактов синхронизации АЦП. Первое преобразование после включения АЦП (установка ADEN в ADCSRA) требует 25 тактов синхронизации АЦП за счет необходимости инициализации аналоговой схемы.

После начала нормального преобразования на выборку-хранение затрачивается 1.5 такта синхронизации АЦП, а после начала первого преобразования – 13,5 тактов. По завершении преобразования результат помещается в регистры данных АЦП и устанавливается флаг ADIF. В режиме одиночного преобразования одновременно сбрасывается бит ADSC. Программно бит ADSC может быть снова установлен и новое преобразование будет инициировано первым нарастающим фронтом тактового сигнала АЦП.

В режиме автоматического перезапуска новое преобразование начинается сразу по завершении предыдущего, при этом ADSC остается в высоком состоянии. Времена преобразования для различных режимов преобразования представлены в таблице 16.

Таблица 16 – Время преобразования АЦП

Тип преобразования

Длительность выборки-хранения (в тактах с момента начала преобразования)

Время преобразования (в тактах)

Первое преобразование

14.5

25

Нормальное однополярное преобразование

1.5

13

Нормальное дифференциальное преобразование

1.5/2.5

13/14

 

Биты MUXn и REFS1:0 в регистре ADMUX поддерживают одноступенчатую буферизацию через временный регистр. Этим гарантируется, что новые настройки канала преобразования и опорного источника вступят в силу в безопасный момент для преобразования. До начала преобразования любые изменения канала и опорного источника вступаю в силу сразу после их модификации. Как только начинается процесс преобразования доступ к изменению канала и опорного источника блокируется, чем гарантируется достаточность времени на преобразование для АЦП. Непрерывность модификации возвращается на последнем такте АЦП перед завершением преобразования (перед установкой флага ADIF в регистре ADCSRA). Обратите внимание, что преобразование начинается следующим нарастающим фронтом тактового сигнала АЦП после записи ADSC. Таким образом, пользователю не рекомендуется записывать новое значение канала или опорного источника в ADMUX до 1-го такта синхронизации АЦП после записи ADSC.

Особые меры необходимо предпринять при изменении дифференциального канала. Как только осуществлен выбор дифференциального канала усилительному каскаду требуется 125 мкс для стабилизации нового значения. Следовательно, в течение первых после переключения дифференциального канала 125 мкс не должно стартовать преобразование. Если же в этот период преобразования все-таки выполнялись, то их результат необходимо игнорировать.

Такую же задержку на установление необходимо ввести при первом дифференциальном преобразовании после изменения опорного источника АЦП (за счет изменения бит REFS1:0 в ADMUX).

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

В режиме одиночного преобразования переключение канала необходимо выполнять перед началом преобразования. Переключение канала может произойти только в течение одного такта синхронизации АЦП после записи лог. 1 в ADSC. Однако самым простым методом является ожидание завершения преобразования перед выбором нового канала.

В режиме автоматического перезапуска канал необходимо выбирать перед началом первого преобразования. Переключение канала происходит аналогично - в течение одного такта синхронизации АЦП после записи лог. 1 в ADSC. Но самым простым методом является ожидание завершения перового преобразования, а затем переключение канала. Поскольку следующее преобразование уже запущено автоматически, то следующий результат будет соответствовать предыдущему каналу. Последующие преобразования отражают результат для нового канала.

Источник опорного напряжения (ИОН) для АЦП (VИОН) определяет диапазон преобразования АЦП. Если уровень однополярного сигнала свыше VИОН, то результатом преобразования будет 0x3FF. В качестве VИОН могут выступать AVCC, внутренний ИОН 2,56В или внешний ИОН, подключенный к выв. AREF. AVCC подключается к АЦП через пассивный ключ. Внутреннее опорное напряжение 2,56В генерируется внутренним эталонным источником VBG, буферизованного внутренним усилителем. В любом случае внешний вывод AREF связан непосредственно с АЦП и, поэтому, можно снизить влияние шумов на опорный источник за счет подключения конденсатора между выводом AREF и общим. Напряжение VИОН также может быть измерено на выводе AREF высокоомным вольтметром. Обратите внимание, что VИОН является высокоомным источником и, поэтому, внешне к нему может быть подключена только емкостная нагрузка.

Если пользователь использует внешний опорный источник, подключенный к выв. AREF, то не допускается использование другой опции опорного источника, т.к. это приведет к шунтированию внешнего опорного напряжения. Если к выв. AREF не приложено напряжение, то пользователь может выбрать AVCC и внутреннее напряжение 2.56 В. качестве опорного источника. Результат первого преобразования после переключения опорного источника может характеризоваться плохой точностью и пользователю рекомендуется его игнорировать.

Схема аналогового входа для однополярных каналов представлена на рисунке 37. Независимо от того, какой канал подключен к АЦП, аналоговый сигнал, подключенный к выв. ADCn, нагружается емкостью вывода и входным сопротивлением утечки. После подключения канала к АЦП аналоговый сигнал будет связан с конденсатором выборки-хранения через последовательный резистор, сопротивление которого эквивалентно всей входной цепи.

АЦП оптимизирован под аналоговые сигналы с выходным сопротивлением не более 10 кОм. Если используется такой источник сигнала, то время выборки незначительно. Если же используется источник с более высоким входным сопротивлением, то время выборки будет определяться временем, которое требуется для зарядки конденсатора выборки-хранения источником аналогового сигнала. Рекомендуется использовать источники только с малым выходным сопротивлением и медленно изменяющимися сигналами, т.к. в этом случае будет достаточно быстрым заряд конденсатора выборки-хранения.

По завершении преобразования (ADIF = 1) результат может быть считан из пары регистров результата преобразования АЦП (ADCL, ADCH). Зависимость между прочитанным значением, опорным и измеренным  напряжениями следующая:

,                                                                         (9)

 

где Vвх – уровень напряжения на подключенном к АЦП входу;

 

VИОН –напряжение выбранного источника опорного напряжения. Код 0x000 соответствует уровню аналоговой земли, а 0x3FF - уровню напряжения ИОН минус 1 шаг квантования по напряжению.

Ура! Теперь мы знаем не только как рассчитать по прочитанному из регистров значению преобразования номер нажатой кнопки клавиатуры. Ещё мы можем измерить напряжение на аналоговых входах IN0 и IN1 набора, и даже напряжение элемента питания (батарейки) часов реального времени DS1307.

Теперь разберемся, за что же отвечает каждый из битов регистров управления АЦП.

Таблица 17 - Регистр управления мультиплексором АЦП– ADMUX

Разряд

7

6

5

4

3

2

1

0

 

 

REFS1

REFS0

ADLAR

MUX4

MUX3

MUX2

MUX1

MUX0

ADMUX

Чтение/запись

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

 

Исх. значение

0

0

0

0

0

0

0

0

 

Разряд 7:6 – REFS1:0: Биты выбора источника опорного напряжения

Данные биты определяют какое напряжение будет использоваться в качестве опорного для АЦП (см. табл. 18). Если изменить значения данных бит в процессе преобразования, то новые установки вступят в силу только по завершении текущего преобразования (т.е. когда установится бит ADIF в регистре ADCSRA). Внутренний ИОН можно не использовать, если к выводу AREF подключен внешний опорный источник.

Таблица 18 – Выбор опорного источника АЦП

REFS1

REFS0

Опорный источник

0

0

AREF, внутренний VИОН отключен

0

1

AVCC с внешним конденсатором на выводе AREF

1

0

Зарезервировано

1

1

Внутренний источник опорного напряжения 2.56В с внешним конденсатором на выводе AREF

Разряд 5 – ADLAR: Бит управления представлением результата преобразования

Бит ADLAR влияет на представление результата преобразования в паре регистров результата преобразования АЦП. Если ADLAR = 1, то результат преобразования будет иметь левосторонний формат, в противном случае - правосторонний. Действие бита ADLAR вступает в силу сразу после изменения, независимо от выполняющегося параллельно преобразования. Полное описание действия данного бита представлено в “Регистры данных АЦП – ADCL и ADCH”.

Разряд 4:0 – MUX4:0: Биты выбора аналогового канала и коэффициента усиления

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

Обратите внимание! Выбор коэффициента усиления для дифференциальных каналов возможен только для микроконтроллеров в  корпусе TQFP. Микросхема AT Mega32 в таком корпусе установлена в наборе NM8036. Используемый же нами микропроцессор в корпусе PDIP, что явно свидетельствует, об отсутствии в микроконтроллере дифференциального усилителя АЦП. По этому то я о нем ничего и не рассказываю. Желающие подробно ознакомиться с данной фишкой могут обратиться к даташиту на микроконтроллер AT Mega32 [14].

Таблица 19 – Выбор входного канала и коэффициента усиления

MUX4..0

Однополярный вход

Неинвертирующий дифференциальный вход

Инвертирующий дифференциальный вход

Коэффициент усиления, Ку

00000

ADC0

Нет

00001

ADC1

00010

ADC2

00011

ADC3

00100

ADC4

00101

ADC5

00110

ADC6

00111

ADC7

01000

Нет

 

ADC0

ADC0

10

01001

ADC1

ADC0

10

01010

ADC0

ADC0

200

01011

ADC1

ADC0

200

01100

ADC2

ADC2

10

01101

ADC3

ADC2

10

01110

ADC2

ADC2

200

01111

ADC3

ADC2

200

10000

ADC0

ADC1

1

10001

ADC1

ADC1

1

10010

ADC2

ADC1

1

10011

ADC3

ADC1

1

10100

ADC4

ADC1

1

10101

ADC5

ADC1

1

10110

ADC6

ADC1

1

10111

ADC7

ADC1

1

11000

ADC0

ADC2

1

11001

ADC1

ADC2

1

11010

ADC2

ADC2

1

11011

ADC3

ADC2

1

11100

ADC4

ADC2

1

11101

ADC5

ADC2

1

 

11110

1.23 В (VBG)

Нет

111110

0 В(GND)

Таблица 20 – Регистр А управления и статуса АЦП – ADCSRA

Разряд

7

6

5

4

3

2

1

0

 

 

ADEN

ADSC

ADFR

ADIF

ADIE

ADPS2

ADPS1

ADPS0

ADCSRA

Чтение/запись

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

Чт./Зп.

 

Исх. значение

0

0

0

0

0

0

0

0

 

Разряд 7 – ADEN: Разрешение работы АЦП

Запись в данный бит лог. 1 разрешает работу АЦП. Если в данный бит записать лог. 0, то АЦП отключается, даже если он находился в процессе преобразования.

Разряд 6 – ADSC: Запуск преобразования АЦП

В режиме одиночного преобразования установка данного бита инициирует старт каждого преобразования. В режиме автоматического перезапуска установкой этого бита инициируется только первое преобразование, а все остальные выполняются автоматически. Первое преобразование после разрешения работы АЦП, инициированное битом ADSC, выполняется по расширенному алгоритму и длится 25 тактов синхронизации АЦП, вместо обычных 13 тактов. Это связано с необходимостью инициализации АЦП.

В процессе преобразования при опросе бита ADSC возвращается лог. 1, а по завершении преобразования – лог. 0. Запись лог. 0 в данный бит не предусмотрено и не оказывает никакого действия.

 

Разряд 5 – ADFR: Выбор режима автоматического перезапуска АЦП

 

Если в данный бит записать лог. 1, то АЦП перейдет в режим автоматического перезапуска. В этом режиме АЦП автоматически выполняет преобразования и модифицирует регистры результата преобразования через фиксированные промежутки времени. Запись лог. 0 в этот бит прекращает работу в данном режиме.

 

Разряд 4 – ADIF: Флаг прерывания АЦП

 

Данный флаг устанавливается после завершения преобразования АЦП и обновления регистров данных. Если установлены биты ADIE и I (регистр SREG), то происходит прерывание по завершении преобразования. Флаг ADIF сбрасывается аппаратно при переходе на соответствующий вектор прерывания. Альтернативно флаг ADIF сбрасывается путем записи лог. 1 в него. Обратите внимание, что при выполнении команды "чтение-модификация-запись" с регистром ADCSRA ожидаемое прерывание может быть отключено. Данное также распространяется на использование инструкций SBI и CBI.

 

Разряд 3 – ADIE: Разрешение прерывания АЦП

 

После записи лог. 1 в этот бит, при условии, что установлен бит I в регистре SREG, разрешается прерывание по завершении преобразования АЦП.

 

Разряды 2:0 – ADPS2:0: Биты управления предделителем АЦП

 

Данные биты определяют на какое значение тактовая частота ЦПУ будет отличаться от частоты входной синхронизации АЦП.

Таблица 21 – Управление предделителем АЦП

ADPS2

ADPS1

ADPS0

Коэффициент деления

0

0

0

2

0

0

1

2

0

1

0

4

0

1

1

8

1

0

0

16

1

0

1

32

1

1

0

64

1

1

1

128

Таблица 22 – Регистры данных АЦП – ADCL и ADCH

ADLAR = 0:

Разряд

15

14

13

12

11

10

9

8

 

 

-

-

-

-

-

-

ADC9

ADC8

ADCH

 

ADC7

ADC6

ADC5

ADC4

ADC3

ADC2

ADC1

ADC0

ADCL

 

7

6

5

4

3

2

1

0

 

Чтение/запись

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

 

 

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

 

Исх. значение

0

0

0

0

0

0

0

0

 

 

0

0

0

0

0

0

0

0

 

ADLAR = 1:

Разряд

15

14

13

12

11

10

9

8

 

 

ADC9

ADC8

ADC7

ADC6

ADC5

ADC4

ADC3

ADC2

ADCH

 

ADC1

ADC0

-

-

-

-

-

-

ADCL

 

7

6

5

4

3

2

1

0

 

Чтение/запись

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

 

 

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

Чт.

 

Исх. значение

0

0

0

0

0

0

0

0

 

 

0

0

0

0

0

0

0

0

 

 

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

Если выполнено чтение ADCL, то доступ к этим регистрам для АЦП будет заблокирован (т.е. АЦП не сможет в дальнейшем модифицировать результат преобразования), пока не будет считан регистр ADCH.

Левосторонний формат представления результата удобно использовать, если достаточно 8 разрядов. В этом случае 8-разрядный результат хранится в регистре ADCH и, следовательно, чтение регистра ADCL можно не выполнять. При правостороннем формате необходимо сначала считать ADCL, а затем ADCH.

 

ADC9:0: Результат преобразования АЦП

 

Данные биты представляют результат преобразования.

 

На основании полученных знаний теперь мы можем правильно настроить АЦП и организовать циклический опрос его входов с целью измерения напряжений и распознавания нажатых кнопок клавиатуры:

 

Врезка 17:

 

// Инициализация АЦП

      // Частота АЦП: 250,000 кГц.

      // Вход опорного напряжения соединён с AVCC pin

      // Автотриггерный режим работы: не используется

 

      ADMUX=FIRST_ADC_INPUT | (ADC_VREF_TYPE & 0xff);

      ADCSRA=0xCD;

 

Прежде чем приступить к составлению подпрограммы обработки клавиатуры необходимо определиться с уровнями напряжений, которые будут поступать на вход АЦП микроконтроллера при нажатии на кнопки управления (рисунок 40). Для этого составим расчётную таблицу в формате MS Excel (таблица 23). Скачать её в готовом виде можно здесь.

 

Таблица 23 – Расчёт значений АЦП.

Название кнопки

Кнопка N

Кол-во Резисто-ров

Сумма сопротивлений резисторов, Ом.

Номиналы резисторов, Ом.

U key

ADC (dec)

Граница ADC (dec)

ENTER

1

6

28200

4700

4,285714

878

951

UP

2

5

23500

4700

4,166667

853

866

LEFT

3

4

18800

4700

4

819

836

RIGHT

4

3

14100

4700

3,75

768

794

DOWN

5

2

9400

4700

3,333333

683

725

MENU

6

1

4700

4700

2,5

512

597

POWER

7

0

0

4700

0

0

256

 

 

 

 

 

 

 

0

 

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

0 – нет нажатий на кнопки;

1 – дребезг контактов кнопки (кнопка мало удерживается в нажатом состоянии);

2 – короткое нажатие на кнопку (более 0,25 секунды);

5 – удержание кнопки в нажатом состоянии более 6 секунд;

15 – удержание кнопки в нажатом состоянии более 12 секунд.

 

Для звукового подтверждения нажатия клавиш составим подпрограмму подачи звукового сигнала (врезка 18) в качестве параметров которой будем передавать длительность звучания сигнала и его тон.

 Врезка 18:

 

void _beep (unsigned char beep_counter, unsigned char ton)

{

      TCCR1B=ton;

      if(beep_on) beep = beep_counter;

}

 

 В подпрограмме обработки прерывания по переполнению таймера-счётчика 2 будем подавать звуковой сигнал, отсчитывать длительность его звучания, запускать преобразование АЦП и отсчитывать интервал времени, необходимый датчикам температуры для преобразования температуры в цифровой код:

Врезка 19:

 

//Обработчик прерывания от таймера 2

SIGNAL (SIG_OVERFLOW2)

{

 static char a;

 

     

      if(flag_temp) flag_temp--;                                              //отсчёт времени для измерения температуры

                     

      ADCSRA|=0x40;                                               //начать новое преобразование АЦП

                     

      if(a) a--;

      else

                      {

                      mig^=0xFF;

                      a = 10;

                      };

                     

      if(beep)

                      {

                      DDRD SET_B(5);

                      beep--;

                      }

      else         DDRD CLR_B(5);

                     

      return;

}

 

 

Тональность звукового сигнала, генерируемого пьезобузером BE1 зависит от частоты работы таймера-счётчика 1 и, следовательно, частоты генерируемой им ШИМ. Она же в свою очередь зависит от числового значения в регистре управления предделителем TCCR1B. Как уже догадался внимательный читатель, изменение значения в этом регистре изменит частоту генерации ШИМ не только в канале B, используемом для управления для бузером, но и для канала A, управляющего яркостью подсветки LCD-дисплея. Но ведь изменение частоты скорее всего останется незаметной для глаза наблюдателя. Попробуем подсчитать частоту следования ШИМ импульсов при различных коэффициентах предделения тактовой частоты микроконтроллера. Для этого вновь составим расчётную таблицу в формате MS Excel. При этом помним, что микропроцессор у нас работает на частоте 8 000 000 Гц., а таймер-счётчик 1 с разрешением 8 бит (таблица 24).

 

Таблица 23 – Расчёт значений АЦП.

 

Значение в регистре TCCR1B

Коффициент предделения

Частота выходного ШИМ, Гц.

0

таймер остановлен

0,0

1

1

31250,0

2

8

3906,3

3

64

488,3

4

256

122,1

5

1024

30,5

 

Как видим дл LCD индикатора теоретически применим любой коэффициент предварительного деления таймера. Даже на частоте 30,5 Гц. мы не должны видеть его мерцания. Верно?!! Ан нет! Как показала практика при малых уровнях яркости, когда длительность генерируемого ШИМ-импульса мала, мерцание подсветки становится заметным уже на частоте 150 Гц. Так что не видать нам последних двух строк в таблице!

Частота 31,25 кГц. явно великовата для нормальной работы пьезобузера, большинство из которых «пищит» с частотой около 4000 кГц. А вот значения 2 и 3 для регистра TCCR1B как раз должны подойти.

Я решил проверить данную гипотезу на практике и написал маленькую тестовую программку, которая выводила на бузер прямоугольный ШИМ-сигнал с указанными в таблице частотами. «Пищалка» хорошо работала только при значениях 2 и 3 в регистре TCCR1B. При этом когда в регистр было записано число 2 – звук пьезодинамика был естественным, как при питании его постоянным напряжением. При TCCR1B = 3 генерируемый звук был другого более низкого тона.

Вот и используем звук более низкого тона для озвучивания клавиши CANCEL, а более высокого – для озвучивания всех остальных кнопок управления.

Запись _beep(2, (key == CANCEL)? 3:2); (врезка 20) как раз это и означает. На самом деле это сокращённая запись условия _beep( 2,  if(key == CANCEL) return 3; else return 2);. Просто так удобнее :-).

Врезка 20:

 

//Коды клавиш

#define OK                                 1

#define UP                                 2

#define LEFT                            3

#define RIGHT                         4

#define DOWN                          5

#define CANCEL                     6

#define POWER                        7

#define NO_KEY                      0

 

 

void scan_key(unsigned int adc_data)                                //опрос клавиатуры АЦП и фильтрация нажатий на кнопки

{

 static unsigned int key_count;                              //переменная подсчёта числа вызовов клавиатуры

 static unsigned char NEW_KEY, OLD_KEY, flag=0;

 

if    (adc_data>866 && adc_data<950)                             NEW_KEY = OK;

else if (adc_data>836 && adc_data<866)         NEW_KEY = UP;

else if (adc_data>794 && adc_data<836)         NEW_KEY = LEFT;

else if (adc_data>725 && adc_data<794)         NEW_KEY = RIGHT;

else if (adc_data>597 && adc_data<725)         NEW_KEY = DOWN;

else if (adc_data>256 && adc_data<597)         NEW_KEY = CANCEL;

else if (adc_data<256)                                            NEW_KEY = POWER;

else                                                                              NEW_KEY = NO_KEY;

 

 

 if(NEW_KEY == NO_KEY)                  //нет нажатых кнопок    

      {

      key = NO_KEY;                                 //передаём в основную программу что нет нажатых кнопок           

      key_count = key_counter = 0;

      flag = 0;                                                //фиксируем, что было отпускание кнопки

      return;

      };

                     

 if(OLD_KEY == NEW_KEY)                //в предыдущем цикле опроса клавиатуры была нажата эта же кнопка

      {

      key_count++;                                     //инкрементируем счётчик длительности удержания

      }

 else

      {

      OLD_KEY = NEW_KEY; //это первое нажатие данной кнопки

      key_count = 0;                                   //сброс счётчика нажатий

      };

                     

 if ((key_count>0)&&(flag==0))            //кнопка была нажата и … …отпущена спустя 0,25 секунды

      {

      key = NEW_KEY;                              //передаём в основную программу код нажатой клавиши

      key_counter = 2;                                //и то, что это было короткое нажатие на кнопку

      flag = 1;                                                //фиксируем, что было короткое нажатие, что бы больше сюда не входить

      _beep(2, (key == CANCEL)? 3:2);//подаём звуковой сигнал разной тональности

      }

 else

      {

      if (key_count>20)                              //кнопка удерживается нажатой, более 2,4 секунды

                      {

                      key_counter = 2;                //передаем в основную программу «выполнить обработку нажатия ещё раз»

                      key = NEW_KEY;              //и код нажатой клавиши

                      if (key_count>50) key_counter = 5;              //кнопка удерживается нажатой более 6 секунды

                                                                                                    //передаем в основную программу «выполнить обработку

//нажатия ещё 4 раза»

                      if (key_count>100) key_counter = 15;         //кнопка удерживается нажатой более 12 секунд

                                                                                                    //передаем в основную программу «выполнить обработку

//нажатия ещё 14 раз»   

                      }

      else                                                        //«мёртвое» время – на нажатия кнопок не реагируем

                      {

                      key_counter = 0;                // передаем в основную программу «нет нажатых кнопок»

                      key = NO_KEY;

                      }

      };

 

 return;                                                        //выход из подпрограммы

}

 

 

Как вы уже заметили, названия кнопок в нашей программе отличаются от  названий в оригинальном устройстве мастер-кит BM8036. Я сделал это умышленно, ведь, по сути, мы разрабатываем новое устройство, используя набор лишь в качестве отладочной платы, следовательно, будем разрабатывать «свою» логику управления и назначение кнопок.

Для того, что бы устройство нормально работало при первом включении, когда в eeprom микроконтроллера содержится «мусор» введём функцию сброса настроек по умолчанию (файл «main.c»):

Врезка 21:

 

if(adc_data[1]>866 && adc_data[1]<950)                                                      //сброс настроек по умолчанию

                      {

                      key = NO_KEY;

                      beep_on = 1;

                      Contrast = 120;

                      Bright = 200;

                      Time_ind = 1;

                      indicirovat = 0;

                                     

                      eeprom_write_byte(&E_indicirovat, indicirovat);

                      eeprom_write_byte(&E_beep_on, beep_on);

                      eeprom_write_byte(&E_Contrast, Contrast);

                      eeprom_write_byte(&E_Bright, Bright);

                      eeprom_write_byte(&E_Time_ind, Time_ind);

                      eeprom_write_byte(&E_nSensors, 0);

                                     

                      for(n=0;n<N_PROG;n++) eeprom_write_block(&str3[3], &E_text_sensor[n][0], 7);                       

                      };

 

 Сброс настроек будет осуществляться, если при включении питания микроконтроллер обнаружит, что результат первого АЦП преобразования будет находиться в интервале значений от 866 до 950, что соответствует нажатой кнопке «OK». Таким образом, если при подаче питания на устройство удерживать в нажатом состоянии кнопку «OK» произойдёт сброс настроек на умолчания, заключающийся в следующем:

- звук включён;

- контрастность дисплея равна 47%;

- яркость подсветки LCD равна 78%;

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

- на LCD отображается только время;

- названия всех датчиков: «датчик:».

Кстати о названиях датчиков. На форуме мастер-кит [20], посвящённому набору BM/NM8036 как то была высказана идея о том, что неплохо бы сделать ввод названий датчиков и изменить индикацию так, что бы эти самые названия отображались на дисплее. И в самом деле, подумал я, неудобно ведь глядя на показания температуры всех 12 датчиков вспоминать или смотреть по шпаргалке, какой из них за что отвечает. Вот и сделал я этот самый ввод названий датчиков. Для этого был создан массив индикации volatile unsigned char str3[15] =  " 0-датчик:___,_"; (файл «main.h»), в котором при смене индицируемых датчиков, из eeprom микроконтроллера читается его название и помещается в массив str3[], начиная с третьего символа (напомню, счёт элементов в массивах на языке С++ всегда начинается с нуля) и так семь элементов подряд.

Вот мы плавно и подошли к описанию индикации и меню в нашей программе. За то, в каком пункте меню мы находимся будет отвечать у нас счетчик меню – переменная volatile unsigned char menu = 0;, которая при включении устройства (начальной инициализации регистров микроконтроллера при сбросе) будет равна нулю.

И так по порядку:

Главное меню – оно же основной экран индикации. Здесь мы будем отображать дату, время и значения температур и названий датчиков с автоматической сменой показаний через заданные переменной значение volatile unsigned char ind; секунд (рисунок 38).

Для удобства восприятия программы будем нумеровать пункты меню устройства не по порядку, а особым образом. Пункт главного меню нумеруем одной цифрой, например, для меню поиска датчиков температуры цифрой 2. Подпункт этого же меню двумя такими же цифрами (22), а дальнейшие «подпунктики» трёхзначным числом (220…225). Это позволит нам иметь до 10 пунктов меню по 10 подпунктов в и ещё множество «подпунктиков» в каждом, используя лишь однобайтное представление числа menu.

Как показал мой опыт эксплуатации наборов 8036, в большинстве случаев индикации температур только первых 12 датчиков явно недостаточно. Устраним этот недочёт: будем показывать температуру и названия первых unsigned char indicirovat; датчиков. Будем записывать любое число от 0 до 32 в эту переменную. В соответствии с ней и покажем на экране устройства температуры датчиков. Как уже было сказано выше, при значении indicirovat = 0 – будем отображать только текущую дату и время. Количество одновременно индицируемых на LCD датчиков у нас ограничено количеством строк используемого ЖКИ-модуля, следовательно, датчики будем отображать по два на один экран индикации (врезка 22).

А что если установить переменную indicirovat равную нечётному числу, ведь в LCD две строки? -  спросит внимательный читатель. Что ж предусмотрим и этот случай и будем вместо названия и температуры последующего датчика выводить пустую строку (16 пробелов).

Врезка 22:

 

switch (menu)

                      {

                                      case 0:

                                      {             

                                      if(ind)    

                                                     {

                                                     i = 2*ind - 2;

                                                     n = 2*ind - 1;

                                                                    

                                                     eeprom_read_block(&str3[3], &E_text_sensor[i][0], 7);

                                                     _lcd_SetPos(0,0);               //Выводим первую строку                                           

                                                     lcd_print_temp(temp[i], i, NO_MIGANIE);//вывод температуры текущего датчика на lsd

                                                                    

                                                     eeprom_read_block(&str3[3], &E_text_sensor[n][0], 7);

                                                     _lcd_SetPos(0,1);               //Выводим вторую строку                                           

                                                     if(n<indicirovat)  lcd_print_temp(temp[n], n, NO_MIGANIE); //вывод температуры текущего

                                                                                                                                                                    //датчика на lsd

                                                     else _lcd_Printf("                ");

                                                     }

                                      else

                                                     {             

                                                     //Чтение времени с часов

                                                     DS1307_read_time();       

                                                                    

                                                     //Индикация времени

                                                     time_to_lcd (date, month, year, day, hour, minute, second);

                                                     };

                                                    

                                                     if(key == OK)

                                                                     {

                                                                     menu = 1;

                                                                     break;                   

                                                                     }                                                            

                                      };

                                      break;

 

 

Подпрограмма индикации температуры lcd_print_temp (температура, номер датчика, мигать ли номером датчика); выглядит вот так:

Врезка 23:

 

//Мигание номером датчика и температурой

#define MIGANIE 1

#define NO_MIGANIE 0

 

//Вывод температуры на lcd в формате "" 0-датчик:-25,2С";"

void lcd_print_temp(const signed int temp, const unsigned char nomer, const unsigned char migat)

{

      str3[0]=str3[10]=str3[11]=' ';

                     

      IntToStr((int)(nomer+1),&str3[0],2);             //номер датчика

                                     

      if(migat&&mig) str3[0]=str3[1]=' ';

                     

      if(temp == NO_SENSOR) str3[11]=str3[12]=str3[14]='-';

      else

                      {

                      IntToStr((signed int)(temp/10),&str3[10],3);               //вывод целой части

                      IntToStr((signed int)(temp%10),&str3[14],1);             //вывод дробной части

                                     

                      if(temp<0)

                                      {

                                      if(temp>-100) str3[11]='-';

                                      else str3[10]='-';                                  

                                      }

                      };

                     

      _lcd_Out(str3,15);             

      _lcd_WriteDataByteToDisplayRAM_A(0x00);

}

 

 

Она использует всё тот же массив индикации str3[];, упомянутый выше. Мигание номером датчика нам понадобиться в дальнейшем при выборе номера датчика для ввода его названия. Как видно из текста подпрограммы (врезка 23) знак «-» (минус) при выводе отрицательных температур у нас «перемещается» по массиву индикации, следуя разрядности отображаемой температуры.

Температуру на ЖКИ и названия датчиков мы вывели. Теперь займёмся собственно поиском самих датчиков (врезка 24).

Врезка 24:

 

case 2:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("  Поиск установ-");

                                                    

                                      _lcd_SetPos(0,1);               //Выводим вторую строку

                                      _lcd_Printf("ленных датчиков ");

                                      };

                                      break;

                                                    

                                      case 22:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf(" Поиск датчиков ");

                                                    

                                      _lcd_SetPos(0,1);               //Выводим вторую строку

                                      _lcd_Printf(" DS18X20 ...    ");

                                                    

                                      for(i=0;i<N_PROG;i++) gSensorIDs[i][0] = 0;              //обнуление массива ID-датчиков

                                      eeprom_write_block(&gSensorIDs, &gSensorIDs_E, (N_PROG*OW_ROMCODE_SIZE));

                                                    

                                      nSensors = search_sensors();

                                                    

                                      eeprom_write_byte(&E_nSensors, nSensors);

                                                    

                                      eeprom_write_byte(&E_indicirovat, 6);

                                                    

                                      for (i=0;i<nSensors;i++)

                                                      {

                                      if(gSensorIDs[i][0] == DS18B20_ID)

                                                     {                                                                                                                                                                                                                                         if(DS18X20_get_power_status(&gSensorIDs[i][0])==DS18X20_POWER_PARASITE) 

                                                                     //если датчик с паразитным питанием

                                                                     {

                                                               if(DS18X20_write_scratchpad(&gSensorIDs[i][0],0,0,DS18B20_9_BIT)!=

                                                               DS18X20_OK || DS18X20_copy_scratchpad(DS18X20_POWER_PARASITE,

                                                               &gSensorIDs[i][0])!=DS18X20_OK)             

//копирование параметров в eeprom датчика                       

                                                                     funcptr();                                             //начать прогу с начала, как при включении МК

                                                                     }

                                                                     else

                                                                     {

                                                                     if(DS18X20_write_scratchpad(&gSensorIDs[i][0],0,0, DS18B20_12_BIT)!=

DS18X20_OK     //запись в датчик порогов температурной сигнализации

//и разрешения преобразования 12 бит

                                                                     ||DS18X20_copy_scratchpad(DS18X20_POWER_EXTERN,

                                                                     &gSensorIDs[i][0])!=DS18X20_OK)  //копирование параметров в eeprom датчика                                                                                   funcptr();              //начать прогу с начала, как при включении МК

                                                                     }

                                                      }

                                                      };

                                                                    

                                      n = z = 0;

                                      menu = 220;

                                      };

                                      break;

                                                    

                                      case 220:

                                      {                            

                                      //Вывод на LCD кол-ва датчиков

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                                    

                                      if(nSensors)

                                      {             

                                                     str2[0] = str2[6] = ' ';

                                                     IntToStr((int)(nSensors),&str2[6],2);                              //количество датчиков

                                                     IntToStr((int)(n+1),&str2[0],2);                                      //номер датчика

                                                     if(mig) str2[0] = str2[1] = ' ';                                                            

                                                     str2[10]=str2[11]=' ';

                                                     IntToStr((signed int)(temp[n]/10),&str2[10],3);          //вывод целой части

                                                     IntToStr((signed int)(temp[n]%10),&str2[14],1);        //вывод дробной части

                                                     if(temp[n]<0)

                                                                     {

                                                                     if(temp[n]>-100) str2[11]='-';

                                                                     else str2[10]='-';                                  

                                                                     };

                                                                                    

                                                     _lcd_Out(str2,15);              //вывод температуры текущего датчика

                                                     _lcd_WriteDataByteToDisplayRAM_A(0x00);//символ градуса                                                      

                                                                    

                                                     _lcd_SetPos(0,1);               //Выводим первую строку

                                                     str0[12] = ' ';

                                                     IntToStr((int)(z+1),&str0[12],2);                     //номер назначения

                                                     if(eeprom_read_byte(&gSensorIDs_E[z][0])) str0[14] = 'M';   //вывод запомнено ли

                                                     else str0[14] = ' ';

                                                     if(mig) str0[12] = str0[13] = str0[14] = ' ';

                                                     _lcd_Out(str0,16);             

                                      }

                                      else

                                      {

                                                     _lcd_Printf("Датчиков не най-");

                                                     _lcd_SetPos(0,1);               //Выводим вторую строку

                                                     _lcd_Printf("дено! Повторить?");                                

                                      }

                                      };

                                      break;

 

 

Теперь датчики найдены. Сопоставление логических датчиков с физическими, как в оригинальной прошивке 8036,  мы произвели в пункте подменю №220. Для читателей плохо разбирающихся в языке программирования С++ поясню как это было сделано:

Перед поиском датчиков в 22-ом подменю мы «стёрли» (заполнили нулями) eeprom микроконтроллера в разделе, отведённом под хранение серийных номеров датчиков gSensorIDs_E [][] и обнулили массив gSensorIDs[][] в оперативной памяти. Далее  выполнили поиск датчиков командой nSensors = search_sensors();. При этом серийные номера всех найденных датчиков были сохранены в массив gSensorIDs[][] начиная с нулевого элемента. Следующим шагом стало сохранение числа найденных датчиков в ееprom. По окончании этой операции каждому из найденных датчиков посылалась команда DS18X20_get_power_status(); для определения типа питания датчика. В случае если датчик включён по трёхпроводной схеме с внешним питанием, он настраивался командой DS18X20_write_scratchpad(); на преобразование температуры с разрядностью 12 бит и в его eeprom сохранялись эти настройки (команда DS18X20_copy_scratchpad();). Аналогично датчики с паразитным питанием переключались в режим преобразования с разрешением 9 бит.

Здесь следовало бы упомянуть несколько особенностей подключения датчиков по схеме с паразитным  питанием. Если читатель решит использовать двухпроводное подключение, то необходимо помнить, что крайне нежелательно подключать более 10 датчиков к устройству используя лишь паразитное питание. Так же нежелательно применять в этом случае провод длинной более 10 метров. О применении не витой пары или экранированного кабеля в этом случае вообще можно забыть. Кроме того, обращаю внимание на необходимость соединения вывода питания датчика Vdd с общим проводом шины 1-wire (GND) при использовании паразитного питания. В случае несоблюдения последней рекомендации (вывод Vdd никуда не подключён) имеют место многократные «перезагрузки» датчиков температуры в результате наведённых на неподключённый вывод помех. Сразу после сброса измеренная датчиком температура составляет +85,0 0С, что и отображается на ЖК-дисплее.

Пороги температурной сигнализации, имеющиеся в датчиках температуры DS18B20, мы не используем, так как в дальнейшем все процедуры сравнения температур будем осуществлять программно. Поэтому запишем в датчик значения порогов равные нулю. Датчики температуры DS1820, DS18S20 не содержат внутренней памяти eeprom и управляющих регистров, поэтому работа с памятью на них не распространяется. Кроме того разрешение преобразования температуры в них строго фиксировано – 9 бит и не подлежит изменению. Именно по этой причине в программе предусмотрена проверка наличия типа датчика DS18B20 (if(gSensorIDs[i][0] == DS18B20_ID)).

Далее осуществляется автоматический переход в пункт меню 220, в котором пользователю, если датчики найдены, предлагается произвести сопоставление физических датчиков логическим. Если датчиков нет – выводится соответствующее сообщение. При сопоставлении номеров физических датчиков логическим можно было воспользоваться массивом соответствия, в котором для каждого логического датчика в ячейке памяти содержался бы номер физического. Но мы так не поступили по двум причинам: Во-первых, это потребовало бы дополнительно 32 байта оперативной памяти. Во-вторых для обращения к конкретному датчику нам пришлось бы использовать ссылку указателя на указатель или массив в массиве, что не только замедлило бы выполнение программы, но и было бы слишком запутанно и невежливо с точки зрения хорошего тона «правильного программирования».

 Для сопоставления датчиков мы просто будем сохранять в ееprom микроконтроллера физические серийные номера датчиков на место, соответствующее логическому датчику (врезка 24). Например, физический датчик №4 мы хотим сделать логическим с № 8 и логическим с № 30. Для этого сохраним его серийный номер в ячейки с номерами 8 и 30 в массиве gSensorIDs_E [][] eeprom. В дальнейшем после окончания сопоставления датчиков и при каждом включении устройства считаем массив серийных номеров из eeprom в оперативную память (eeprom_read_block(&gSensorIDs, &gSensorIDs_E, (N_PROG*OW_ROMCODE_SIZE)); ) и получим возможность прямого обращение к логическим датчикам.

Датчики мы нашли. Физические номера логическим сопоставили. Теперь нужно как-то измерить температуру. Для этого датчикам необходимо подать команду измерения температуры DS18X20_start_meas(DS18X20_POWER_PARASITE, NULL)  и спустя минимум 750 мкс. считать показания датчика. Так как датчиков у нас несколько, то команду запуска измерений подадим сразу всем. Для этого используем широковещательный нулевой адрес (NULL). По окончании отсчёта времени, которое производится в подпрограмме обработки прерывания по переполнению таймера ,2 поочерёдно, каждому из датчиков, используя сохранённый ранее его физический номер, будем подавать команду чтения температуры. Делать будем это до тех пор, пока все 32 датчика не переберем, не зависимо от того есть ли этот датчик на самом деле. В случае отсутствия такового подпрограмма чтения температура будет возвращать значение NO_SENSOR = 0x8000, сигнализирующее том, что такого датчика нет (врезка 25).

 

Врезка 25:

 

if(flag_temp==0)

                                      {

                                      if(sensor<N_PROG-1)       

                                                     {                                            

                                                     i = DS18X20_read_meas(&gSensorIDs[sensor][0],&subzero,&cel,&cel_frac_bits);

//чтение температуры с датчика

                                                     if(i==DS18X20_OK) temp[sensor] = DS18X20_temp_to_decicel(subzero, cel, cel_frac_bits);

                                                     else temp[sensor] = NO_SENSOR;

                                                                    

                                                     sensor++;                              //следующий датчик

                                                                    

                                                     }

                                                     else

                                                     {

                                                     sensor = 0;

                                                     if(DS18X20_start_meas(DS18X20_POWER_PARASITE,NULL)!=DS18X20_OK)

                                                     funcptr();                              //старт измерения температуры для всех датчиков

                                                     flag_temp = 30;

                                                     }

                                      }

 

 

Итак, датчики мы нашли, температуры измерили и вывели на дисплей вместе с названиями датчиков. Теперь вернёмся к меню устройства.

Для регулировки яркости и контрастности ЖКИ достаточно изменять содержимое регистров сравнения таймеров-счётчиков микроконтроллера OCR1BL и OCR0 соответственно.

Для отображения диаграмм уровня регулировок создадим специальную подпрограмму diag (значение, делитель), которую поместим в файл «diagramma.c» (врезка 26).

Врезка 26:

 

//Вывод линейной диаграммы в виде |||||||------

 

void diag (unsigned int t,unsigned char p)

      {

      unsigned char n;

                                     

                      _lcd_SetPos(0,1);

                                     

                      wdt_reset();          //сброс сторожевого таймера

                                     

                      n=0;

                      while(t>(p-1))

                                      {

                                      t-=p;

                                      _lcd_WriteDataByteToDisplayRAM(0x04);//"|||"

                                      n++;

                                      };

                      if(t>((p/3)*2))

                                      {

                                      _lcd_WriteDataByteToDisplayRAM(0x01);//"||-"

                                      n++;

                                      }

                                      else

                                      {

                                                     if(t>(p/3))

                                                     {

                                                     _lcd_WriteDataByteToDisplayRAM(0x02);//"|--"

                                                     n++;

                                                     }

                                      };

                      for(;n<16;n++) _lcd_WriteDataByteToDisplayRAM(0x03);//"---"       

      }

 

 

Здесь в качестве аргумента функции _lcd_WriteDataByteToDisplayRAM(); используется номера ячеек памяти знакогенератора контроллера, в которые мы поместили свои нарисованные символы (рисунок 39). Для того, что бы эти самые символы туда «упаковать» необходимо после инициализации дисплея вызвать определённую последовательность команд записи в CGRAM LCD (врезка 27).

Врезка 27:

 

//Инициализация дисплея

_lcd_Display_Init();

                              

//Очищаем экран

_lcd_ClrScr();

 

//Установка контрастности дисплея

      OCR0 = Contrast;

                     

      sei();

                     

      //Запись в LCD "своих" символов

      _lcd_LoadGraphSymbols(&SymbolCGRAM0, 0);

      _lcd_LoadGraphSymbols(&SymbolCGRAM1, 1);

      _lcd_LoadGraphSymbols(&SymbolCGRAM2, 2);

      _lcd_LoadGraphSymbols(&SymbolCGRAM3, 3);

      _lcd_LoadGraphSymbols(&SymbolCGRAM4, 4);

 

 

 

Теперь можем продолжить описание пунктов меню устройства:

Врезка 27:

 

case 3:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("     Яркость    ");

                                                    

                                      _lcd_SetPos(0,1);               //Выводим вторую строку

                                      _lcd_Printf("  подсветки LCD ");

                                                    

                                      switch (key)

                                                     {

                                                     case OK:

                                                     menu = 33;

                                                     break;

                                                                    

                                                     case CANCEL:

                                                     menu = 0;

                                                     break;

                                                                    

                                                     case RIGHT:

                                                     menu++;

                                                     break;

                                                                    

                                                     case LEFT:

                                                     menu--;

                                                     break;

                                                     }                            

                                      };

                                      break;

                                                    

                                      case 33:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("     Яркость    ");

                                                    

                                      z = Bright+1;

                                      diag(z,16);

                                                    

                                      switch (key)

                                                     {

                                                     case OK:

                                                     eeprom_write_byte(&E_Bright, Bright);                                      //яркость

                                                     menu = 3;

                                                     break;

                                                                    

                                                     case CANCEL:

                                                     Bright = eeprom_read_byte(&E_Bright);                                     //яркость

                                                     OCR1BL = Bright;

                                                     menu = 3;

                                                                    

                                                     case UP:

                                                     for( ;key_counter>1;key_counter--)

                                                                     {

                                                                     if(Bright<255)Bright++;

                                                                     OCR1BL = Bright;

                                                                     };

                                                     break;

                                                                    

                                                     case DOWN:

                                                     for( ;key_counter>1;key_counter--)

                                                                     {

                                                                     if(Bright>0)Bright--;

                                                                     OCR1BL = Bright;

                                                                     };

                                                     break;                                  

                                                     }                            

                                      };

                                      break;

                                                    

                                      case 4:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("  Контрастность ");

                                                    

                                      _lcd_SetPos(0,1);               //Выводим вторую строку

                                      _lcd_Printf("       LCD      ");

                                                    

                                      switch (key)

                                                     {

                                                     case OK:

                                                     menu = 44;

                                                     break;

                                                                    

                                                     case CANCEL:

                                                     menu = 0;

                                                     break;

                                                                    

                                                     case RIGHT:

                                                     menu++;

                                                     break;

                                                                    

                                                     case LEFT:

                                                     menu--;

                                                     break;

                                                     }                            

                                      };

                                      break;

                                                    

                                      case 44:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("  Контрастность ");

                                                    

                                      z = Contrast+1;

                                      diag(z,16);

                                                    

                                      switch (key)

                                                     {

                                                     case OK:

                                                     eeprom_write_byte(&E_Contrast, Contrast);                             //контрастность

                                                      menu = 4;

                                                     break;

                                                                    

                                                     case CANCEL:

                                                     Contrast = eeprom_read_byte(&E_Contrast);                           //контрастность

                                                     OCR0 = Contrast;

                                                     menu = 4;

                                                                    

                                                     case UP:

                                                     for( ;key_counter>1;key_counter--)

                                                                     {

                                                                     if(Contrast<255)Contrast++;

                                                                     OCR0 = Contrast;

                                                                     };

                                                     break;

                                                                    

                                                     case DOWN:

                                                     for( ;key_counter>1;key_counter--)

                                                                     {

                                                                     if(Contrast>0)Contrast--;

                                                                     OCR0 = Contrast;

                                                                     };

                                                     break;

                                                     }                            

                                      };

                                      break;

 

 

 

Как вы уже заметили здесь, для примера, я не только показал вывод надписей и диаграмм на LCD, но и описал обработку событий клавиатуры. Должно же наше устройство как-то реагировать на нажатия кнопок управления, не так ли :).

Отключение звука будем предусматривать или пусть всегда пищит? – спрошу я и большинство читателей хором ответят – БУДЕМ! Для этого создадим пункт меню и дополнительное подменю:

Врезка 28:

                                                    

                                      case 5:

                                      {

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("    Настройка   ");

                                                    

                                      _lcd_SetPos(0,1);               //Выводим вторую строку

                                      _lcd_Printf("      звука     ");

                                                    

                                      switch (key)

                                                     {

                                                     case OK:

                                                     menu = 55;

                                                     break;

                                                                    

                                                     case CANCEL:

                                                     menu = 0;

                                                     break;

                                                                    

                                                     case RIGHT:

                                                     menu++;

                                                     break;

                                                                    

                                                     case LEFT:

                                                     menu--;

                                                     break;                   

                                                     }                            

                                      };

                                      break;

 

case 55:

                                                     {

                                                     _lcd_SetPos(0,0);                                               //Выводим первую строку

                                                     _lcd_Printf("Настройка звука:");//"Настройка звука:"

                                                                    

                                                     _lcd_SetPos(0,1);                                               //Выводим вторую строку

                                                    

                                                     _lcd_Printf("      ");

                                                                    

                                                     if(beep_on)         

                                                     {

                                                    

                                                     _lcd_Printf(" B");//" В"

                                                     }

                                                     else

                                                     {

                                                     _lcd_Printf("BЫ");                             //"ВЫ"

                                                     };

                                                                    

                                                     _lcd_Printf("КЛЮЧЕН  ");                              //"КЛЮЧЕН  "

                                                                    

                                                     switch (key)

                                                                     {

                                                                     case OK:

                                                                     eeprom_write_byte(&E_beep_on, beep_on);

                                                                     menu = 5;

                                                                     break;

                                                                                    

                                                                     case CANCEL:

                                                                     beep_on = eeprom_read_byte(&E_beep_on);

                                                                     menu = 5;

                                                                     break;

                                                                                    

                                                                     case RIGHT:

                                                                     case LEFT:

                                                                     if(beep_on) beep_on = 0;

                                                                     else beep_on = 1;

                                                                     break;

                                                                     }                            

                                                     };

                                      break;

 

 

Далее предлагаю сделать настройку индикации. В этом пункте меню будем устанавливать количество отображаемых датчиков температуры и интервал индикации – время, через которое будут сменяться показания на дисплее. Судя по отзывам на форуме мастер-кит большинство людей, которые приобрели набор NM/BM8036, устанавливают одинаковое время индикации как для времени и даты, так и для каждой четвёрки датчиков. Так зачем за зачастую ненужную опцию расплачиваться усложнением программного обеспечения и драгоценными байтами ОЗУ.

Врезка 29:

                                                   

                                      case 6:

                                      {

                                     _lcd_SetPos(0,0);               //Выводим первую строку

                                      _lcd_Printf("   Настройка    ");

                                                    

                                      _lcd_SetPos(0,1);               //Выводим вторую строку

                                      _lcd_Printf("   индикации    ");

                                                    

                                      switch (key)

                                                     {

                                                     case OK:

                                                     menu = 66;

                                                     break;

                                                                    

                                                     case CANCEL:

                                                     menu = 0;

                                                     break;

                                                                    

                                                     case RIGHT:

                                                     menu++;

                                                     break;

                                                                    

                                                     case LEFT:

                                                     menu--;

                                                     break;   

                                                     }                            

                                      };

                                      break;

                                                    

                                      case 66:

                                      {             

                                      _lcd_SetPos(0,0);               //Выводим первую строку

                                                    

                                      str5[8] = str6[7] = ' ';

                                      IntToStr((int)(indicirovat),&str5[8],2);          //количество индицируемых датчиков     

                                      if(mig) str5[8] = str5[9] = ' ';

                                      _lcd_Out(str5,16);              //вывод температуры текущего датчика                                 

                                                                    

                                      _lcd_SetPos(0,1);               //Выводим первую строку

                                      IntToStr((int)(Time_ind),&str6[7],2);                            //время индикации

                                      if(mig) str6[7] = str6[8] = ' ';

                                      _lcd_Out(str6,16);

                                                                    

                                      switch (key)

                                                     {

                                                     case UP:

                                                     if(indicirovat<N_PROG) indicirovat++;

                                                     else indicirovat = 0;

                                                     break;

                                                                    

                                                     case DOWN:

                                                     if(indicirovat>0) indicirovat--;

                                                     else indicirovat = N_PROG;

                                                     break;

                                                                    

                                                     case RIGHT:

                                                     if(Time_ind<99) Time_ind++;

                                                     else Time_ind = 0;

                                                     break;

                                                                    

                                                     case LEFT:

                                                     if(Time_ind>0) Time_ind--;

                                                     else Time_ind = 99;

                                                     break;

                                                                    

                                                     case OK:

                                                     eeprom_write_byte(&E_Time_ind, Time_ind);

                                                     eeprom_write_byte(&E_indicirovat, indicirovat);     

                                                     time = 0;

                                                     menu = 6;

                                                     break;

                                                                    

                                    &n