Занимательная микроэлектроника

Ревич Юрий

Часть II

МИКРОКОНТРОЛЛЕРЫ

 

 

Глава 11

Анатомия микроконтроллера

Цифровые электронные устройства могут выполнять в автоматическом режиме довольно сложные функции. Устройства управления военной техникой в 40–50 годы XX века так и делали, для них строили специальные схемы на каждый раз, для каждой конкретной задачи, иногда очень «навороченные», и весьма остроумно придуманные. Эти схемы объединяли цифровые и аналоговые узлы, реализовывавшие различные функции, вплоть до решения в реальном времени сложнейших дифференциальных уравнений. Вы только представьте сложность задачи управления межконтинентальной баллистической ракетой, которая даже в те времена, когда не было ни спутников наведения, ни систем глобального позиционирования, обеспечивала точность попадания в радиусе нескольких десятков метров на расстоянии в тысячи километров!

Характерная черта таких устройств — они построены в принципе из одних и тех же основных элементов. Особенно это касается цифровой техники — со времен Клода Шеннона известно, что любая цифровая функция может быть реализована всего на нескольких базовых «кирпичиках», и мы видели в предыдущих главах, как на основе таких «кирпичиков» — логических элементов— строятся последовательно все более сложные устройства, вплоть до сумматоров и многофункциональных счетчиков, которые затем уже могут комбинироваться в схемы любой степени сложности. Возникает естественная мысль: а нельзя ли соорудить универсальное устройство, которое бы могло выполнять любые подобные функции, раз в какой-то глубинной основе своей они похожи?

К этой мысли человечество двигалось двумя совершенно разными путями. Один из них связан с никогда не покидавшей человечество мечтой о построении искусственного разума. Через арифмометр Паскаля, аналитическую машину Бэббиджа, математическую логику Буля, теоретические построения Тьюринга и Шеннона, через первые электромеханические компьютеры Конрада Цузе, Эйкена и Атанасова этот путь воплотился в ЭНИАКе — построенной в 1946 году электронной вычислительной машине, которая стала символом начала компьютерной эпохи (хотя, добавим, была не самой первой, и не единственной даже в те времена).

Ученые сразу поняли, каковы потенциальные возможности этого устройства — зародилось направление «искусственного интеллекта», стали обсуждаться проблемы автоматического перевода, шахматного компьютера, распознавания образов — в общем-то, многие из них не решены и до сих пор, несмотря на то, что мощность компьютеров возросла в миллионы раз, и вряд ли будут решены в ближайшее время. А вот другая сторона этой революции до времени не обсуждалась столь широко: ведь по сути компьютер и есть то самое универсальное электронное устройство, которое может выполнить любую задачу— от наведения баллистической ракеты на цель до банального переключения режимов стиральной машины, нужно только иметь соответствующую программу. Но обсуждавшийся во введении принцип эквивалентности «железа» и программ, благодаря диссертации Шеннона понятный ученым и инженерам еще задолго до эпохи всеобщей компьютеризации, дошел до практики далеко не сразу, поскольку «железо» резко отставало от нужд практики. Как подумаешь, что вопрос «может ли машина мыслить» обсуждался во времена ЭВМ, по вычислительной мощности эквивалентных сегодняшнему двухдолларовому микроконтроллеру…

Более того, в этом вопросе движение было скорее с обратной стороны — нужды компьютерной отрасли вызвали бурное развитие электроники. И это логично, ведь первые ЭВМ были огромными, потребляли энергии, как небольшой завод, и требовали непрерывного обслуживания (плановое ежесуточное время работы первых советских ЭВМ — 16 часов, остальное — ремонт). Кому в те времена могла прийти мысль даже о том, чтобы дать компьютер каждому в персональное пользование, не то что управлять с его помощью стиральной машиной, правда? Революция произошла лишь с изобретением микропроцессора в фирме Intel в 1971 году. С этого момента инженерам-электронщикам пришлось учить программирование.

Рис. 11.1. Микропроцессор Intel 4004

Первоначально корпорация Intel не помышляла ни о каких процессорах и занималась разработкой и продажами микросхем памяти, на которые тогда как раз начиналось увеличение спроса. В 1969 г. в Intel появились несколько человек из Busicom — молодой японской компании, занимающейся производством калькуляторов. Им требовался набор из 12 интегральных схем в качестве основного элемента нового дешевого настольного калькулятора. Проект был разработан Масатоши Шима, который и представлял японскую сторону. Тед Хофф (Marcian Е. «Ted» Hoff, p. 1937), руководитель отдела, занимавшегося разработкой применений для продукции Intel, ознакомившись с проектом, понял, что вместо того, чтобы создать калькулятор с некоторыми возможностями программирования, можно сделать наоборот, компьютер, программируемый для работы в качестве калькулятора. Развивая идею, в течение осени 1969 г. Хофф определился с архитектурой будущего микропроцессора. Весной в отдел Хоффа пришел (все из той же уже известной нам Fairchild) новый сотрудник Фредерик Фэггин (Federico Faggin), который и придумал название для всей системы: «семейство 4000». Семейство состояло из четырех 16-выводных микросхем: 4001 содержал ROM на 2 килобайта; 4002 — RAM с 4-битным выходным портом для загрузки программ; 4003 представлял собой 10-битный расширитель ввода/вывода с последовательным вводом и параллельным выводом для связи с клавиатурой, индикатором и другими внешними устройствами; наконец, 4004 был 4-битным ЦПУ (центральным процессорным устройством). Он состоял из 2300 транзисторов и работал с тактовой частотой 108 кГц. 15 ноября 1971 г. было объявлено о создании первого микропроцессора. Busicom приобрела разработку, заплатив Intel $60 000. Но в Intel решили возвратить Busicom эти деньги, чтобы вернуть себе права на микропроцессор. i4004 обладал вычислительной мощностью, сравнимой с первым электронным компьютером ЭНИАК. Свое первое практическое применение 4004-й нашел в таких системах, как устройства управления дорожными светофорами и анализаторы крови. Он использован в бортовой аппаратуре межпланетного зонда Pioneer-10, который поставил рекорд долгожительства среди подобных аппаратов: он был запущен NASA в 1972 г., а к 1 сентября 2001 г. Pioneer-10 удалился от Земли на 11,78 млрд км и все еще работал.

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

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

Легкостью программирования современных электронных узлов стали широко пользоваться производители оборудования, особенно в последние годы, чтобы оптимизировать свои прибыли. Делается это примерно так: разрабатывается некое устройство (видеокарта, пишущий привод и т. п.) с некими новыми возможностями. Естественно, оно продается несколько дороже аналогичных более примитивных устройств. Спустя некоторое время конкуренты догоняют, тогда это устройство снимается с продажи, а на рынок выбрасывается аналогичная модель с еще более расширенной функциональностью — по той же цене или еще несколько дороже. Другой вариант той же политики — одновременно выпустить линейку похожих устройств разного класса (например, по быстродействию) с ценой, отличающейся иногда в разы (характерно, например, для видеокарт). И простодушному покупателю невдомек, что разработан был лишь самый дорогой и «продвинутый» вариант, а все более дешевые отличаются лишь программой — «прошивкой» (ну, иногда еще отсутствием некоторых микросхем на плате). Выпуская модели с искусственно урезанной функциональностью, производитель имеет значительно большую суммарную прибыль, т. к. фактически одно и то же устройство может продаваться годами без значительных инвестиций в его доработку, а также охватить все секторы рынка, от самых простых до эксклюзивных. Во многих случаях пользователь сам может превратить дешевое устройство в более дорогое, сменив лишь микропрограмму — классическим примером сезона 2005/2006 годов стала линейка пишущих приводов NEC 3540, 3550, 4550 и др.

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

Ну а теперь перейдем к рассмотрению того, как работает микропроцессор «вообще».

Как работает микропроцессор

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

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

МП и МК

Кстати, а почему мы все время говорим так: то микропроцессоры, то микроконтроллеры? Микроконтроллер (МК) отличается от микропроцессора тем, что он предназначен для управления другими устройствами, и поэтому имеет встроенную развитую систему ввода/вывода, но, как правило, относительно более слабое арифметико-логическое устройство (АЛУ). Микроконтроллерам очень хорошо подходит термин, который в советское время имел, правда, несколько иное значение— «микроЭВМ», еще точнее звучит английское «computer-on-chip». В самом деле, для построения простейшего вычислительного устройства, которое могло бы выполнять что-то полезное, обычный микропроцессор, от i4004 до Pentium и Core Duo, приходится дополнять памятью, BIOS, устройствами ввода/вывода, контроллером прерываний, тактовым генератором с таймерами и т. п. — всем тем, что сейчас стало объединяться в т. н. «чипсеты». «Голый» МП способен выполнить только одно: правильно включиться, ему даже программу загрузки неоткуда взять.

В то же время для МК микропроцессор — это только ядро, даже не самая большая часть кристалла. Для построения законченной системы на типовом МК не требуется вообще ничего, кроме источника питания и периферийных исполняющих устройств, которые позволяли бы человеку определить, что система работает. Обычный МК может без дополнительных компонентов общаться с другими МК, внешней памятью, специальными микросхемами (вроде часов реального времени или флэш-памяти), с компьютером, управлять небольшими (а иногда — и большими) матричными панелями, к нему можно напрямую подключать датчики физических величин (в том числе— чисто аналоговые, АЦП тоже входят в МК), кнопки, клавиатуры, светодиоды и индикаторы, короче — в микроконтроллерах сделано все, чтобы как можно меньше приходилось паять и задумываться над подбором элементов. За это приходится расплачиваться пониженным быстродействием (которое, впрочем, не так и требуется в типовых задачах для МК) и некоторым ограничением в отдельных функциях — по сравнению с универсальными, но в сотни раз более дорогими и громоздкими системами на «настоящих» МП. Вы можете мне не поверить, но процессоры для ПК, о которых мы столько слышим, занимают в общем количестве выпускаемых процессоров лишь 6 %, — остальные 94 % составляют микроконтроллеры различного назначения.

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

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

Блок-схема типичного микропроцессора показана на рис. 11.2.

Рис. 11.2. Блок-схема типичного микропроцессора

Здесь мы включили в состав процессора память программ, которая у ПК-процессоров находится всегда отдельно — сами знаете, программы какого объема бывают в персональных компьютерах. В то же время в большинстве современных микроконтроллеров постоянное запоминающее устройство (ПЗУ) для программ входит в состав чипа и обычно составляет от 1 до 8 кбайт (но есть модели и со 256 кбайтами встроенной памяти!), чего для подавляющего большинства применений вполне достаточно, а если вдруг не хватит, то всегда можно подключить внешнюю память. Впрочем, внутреннее оперативное запоминающее устройство (ОЗУ) того или иного объема имеется во всех современных процессорах, у процессоров для ПК это называется кэш-памятью (иногда — нескольких уровней). Типичный размер ОЗУ данных у микроконтроллеров — от 256 байт до 1 кбайта.

Подробности

В первых моделях микропроцессоров (включая и lntel-процессоры для ПК — от 8086 до 386) процессор выполнял команды строго последовательно: загрузить команду, определить, что ей нужны операнды, загрузить эти операнды (по адресу регистров, которые их должны содержать; адреса эти, как правило, хранятся сразу после собственно кода команды, или определены заранее), потом проделать нужные действия, складировать результаты… До нашего времени дошла архитектура суперпопулярных еще недавно микроконтроллеров 8051, выпускающихся и по сей день различными фирмами (Atmel, Philips), которые выполняли одну команду аж за 12 тактов (в современных системах, впрочем, это число несколько меньше). Для ускорения работы стали делить такты на части (например, срабатывать по переднему и заднему фронтам), но действительный прорыв произошел с внедрением конвейера. Со времен Генри Форда известно, что производительность конвейера зависит только от времени выполнения самой длинной операции — если поделить команды на этапы и выполнять их одновременно разными аппаратными узлами, то можно добиться существенного ускорения (хотя и не во всех случаях). Так, в рассматриваемых далее Atmel AVR конвейер двухступенчатый: когда очередная команда загружается и декодируется, предыдущая уже выполняется и пишет результаты. В AVR это позволило выполнять большинство команд за один такт (кроме команд ветвления, о чем подробнее будет рассказываться в главе 13 ).

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

Под разрядностью МП обычно понимают разрядность чисел, с которыми работает АЛУ, соответственно, такую же разрядность имеют и рабочие регистры. Например, все ПК-процессоры от i386 до последних инкарнаций Pentium были 32-разрядными, последние модели от Intel и AMD стали 64-разрядными.

Большинство микроконтроллеров общего назначения 8-разрядные, но есть и 16- и 32-разрядные. При этом внутренняя шина данных может иметь и больше разрядов, например, чтобы одновременно передавать и адреса и данные.

Распределение рынка МК в первые годы тысячелетия было таким: немного меньше половины выпускаемых изделий составляют 8-разрядные кристаллы, а вторую половину поделили между собой 16- и 32-разрядные, причем доля последних неуклонно растет за счет 16-разрядных. Выпускаются даже 4-разрядные, потомки первого i4004, которые занимают не более 10 % рынка, но что любопытно, эта доля снижается очень медленно.

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

Обычно тактовая частота универсальных МК невелика (хотя типичному инженеру 1980-х, когда ПК работали на частотах не выше 6 МГц, она показалась бы огромной) — порядка 8—16 МГц, иногда до 24 МГц или несколько более. И это всех устраивает: дело в том, что обычные МК и не предназначены для разработки быстродействующих схем. Если требуется быстродействие, то используется другой класс интегральных схем — ПЛИС, «программируемые логические интегральные схемы». Простейшая ПЛИС представляет собой набор никак не связанных между собой логических элементов (более сложные могут включать в себя и некоторые законченные узлы, вроде триггеров и генераторов), которые в процессе программирования такого чипа соединяются в нужную схему. Комбинационная логика работает гораздо быстрее тактируемых контроллеров, и для построения различных логических схем в настоящее время применяют только ПЛИС, от проектирования на «рассыпухе» в массовых масштабах уже давно отказались. Еще одно преимущество ПЛИС — статическое потребление энергии для некоторых серий составляет единицы микроватт, в отличие от МК, которые во включенном состоянии потребляют всегда (если не находятся в режиме энергосбережения). В совокупности с более универсальными и значительно более простыми в обращении, но менее быстрыми и экономичными микроконтроллерами, ПЛИС составляют основу большинства массовых электронных изделий, которые вы видите на прилавках. В этой книге мы, конечно, рассматривать ПЛИС не будем — в любительской практике, в основном из-за дороговизны соответствующего инструментария и высокого порога его освоения, они не используются, и для конструирования одиночных экземпляров приборов даже для профессиональных применений нецелесообразны. А вот если вы закажете разработку некоего прибора профессиональной «конторе», имеющей нужные инструменты и разработчиков с соответствующей квалификацией— почти всегда получите что-нибудь на базе ПЛИС, потому что в конечном итоге так оказывается дешевле.

Если подробности внутреннего функционирования МП нас не очень волнуют (достаточно иметь общее представление о структуре микропроцессорного ядра, чтобы понимать, что именно происходит при выполнении команд), то обмен с внешней средой нас как раз интересует во всех деталях. Для этого служат порты ввода/вывода (I/0-port, от Input/Output). В этом термине имеется некоторая неопределенность, т. к. «порт ввода/вывода» в МК с точки зрения его внутреннего устройства обозначает прежде всего некий регистр для доступа к компонентам, внешним по отношению к вычислительному ядру. А это все узлы, которыми непосредственно управляет пользователь (от таймеров и последовательных портов до регистра флагов и управления прерываниями). Кроме разве что ОЗУ, доступ к которой обеспечивается специальными командами, все остальное в контроллере управляется через порты ввода/вывода.

Однако точно так же называются и внешние порты ввода/вывода, для обмена с «окружающей средой» (управляются они, естественно, внутренними портами ввода/вывода). На схеме рис. 11.2 они показаны в количестве трех (А, В и С). В разных МП их может быть и больше, и меньше. Еще важнее число выводов этих портов, которое чаще всего совпадает с разрядностью процессора (но не всегда, как это было у 8086, который имел внутреннюю 16-разрядную структуру, а внешне выглядел 8-разрядным). Если мы заставим 8-разрядные порты «общаться», например, с внешней памятью, то на двух из них можно выставить 16-разрядный адрес, а на оставшемся— принимать данные. А как быть, если портов два или вообще один? (К примеру, в микроконтроллере Atmel AVR 2313 портов формально два, но один усеченный, так что общее число линий составляет 15.) Для этого все внешние порты в МП всегда двунаправленные. Скажем, если портов два, то можно сначала выставить адрес, а затем переключить их на вход и принимать данные. Естественно, для этого порты должны позволять работу на общую шину, т. е. либо иметь третье состояние, либо выход с общим коллектором для объединения в «монтажное ИЛИ».

Варианты для обоих случаев организации выходной линии порта показаны на рис. 11.3, где приведены упрощенные схемы выходных линий микроконтроллеров семейства 8048 — широко когда-то использовавшегося предшественника популярного МК 8051 (например, 8048 был выбран в качестве контроллера клавиатуры в IBM PC). В самом 8051 построение портов несколько сложнее (в частности, вместо резистора там полевой транзистор), но для уяснения принципов работы это несущественно.

По первому варианту (рис. 11.3, а) в МК 8048 построены порты 1, 2 (всего там три порта). Когда в порт производится запись, то логический уровень поступает с прямого выхода защелки на статическом D-триггере на вход схемы «И», а с инверсного — на затвор транзистора VT2. Если этот уровень равен логическому нулю, то транзистор VT1 заперт, a VT2 открыт, на выходе также логический нуль. Если уровень равен логической единице, то на время действия импульса «Запись» транзистор VT1 открывается, а транзистор VT2 запирается (они одинаковой полярности). Если на выходе присутствует емкость (а она всегда имеется в виде распределенной емкости проводников и емкости входов других компонентов), то через VT1 протекает достаточно большой ток заряда этой емкости, позволяющий сформировать хороший фронт перехода из «0» в «1». Как только импульс «Запись» заканчивается, оба транзистора отключаются, и логическая единица на выходе поддерживается резистором R1. Выходное сопротивление открытого транзистора VT1 примерно 5 кОм, а резистора — 50 кОм. Любое другое устройство, подключенное к этой шине, при работе на выход может лишь либо поддержать логическую единицу, включив свой подобный резистор параллельно R1, либо занять линию своим логическим нулем — это, как видите, и есть схема «монтажное ИЛИ». При работе на вход состояние линии просто считывается со входного буфера (элемент «В» на рис. 11.3, а).

Рис. 11.3. Упрощенные схемы портов ввода/вывода МК 8048 :

а — портов 1 и 2; б — порта 0

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

Лечение амнезии

В 1965 г. в Иллинойском университете был запущен один из самых передовых компьютеров по тому времени — ILLIAC–IV. Это был первый компьютер, в котором была применена быстрая память на микросхемах — каждый чип (производства Fairchild Semiconductor) имел емкость 256 бит, а всего было набрано 1 Мбайт. Стоимость этой памяти составила ощутимую часть от всей стоимости устройства, обошедшегося заказчику— NASA— в $31 млн. Через 10 лет один из первых персональных компьютеров Altair 8800 (1975 г.), продававшийся в виде набора «сделай сам», при стоимости порядка $500 имел всего 256 байт (именно байт, а не килобайт) памяти. В том же году для распространения языка Basic for Altair Биллом Гейтсом и Полом Алленом была создана фирма, получившая первоначальное название Micro-Soft. Одна из самых серьезных проблем, которую пришлось решать — нехватка памяти, потому что созданный ими интерпретатор Basic требовал аж 4 кбайтов!

Проблема объемов памяти и ее дороговизна преследовала разработчиков до самого последнего времени, еще в конце 90-х стоимость памяти для ПК можно было смело прикидывать на уровне $ 1/Мбайт, что при требовавшихся уже тогда для комфортной работы объемах ОЗУ порядка 128–256 Мбайт могло составлять значительную часть стоимости устройства. Сейчас гигабайтом памяти в настольном ПК и даже ноутбуке никого не удивишь, к тому же все современные ОС «умеют» автоматически дополнять недостающий объем ОЗУ за счет дискового пространства. Это привело, в частности, к кардинальным изменениям в самом подходе к программированию: если еще при программировании под DOS о компактности программ и экономии памяти в процессе работы нужно было специально заботиться, то теперь это практически не требуется.

Но в программировании для микроконтроллеров это все еще не так. Хотя гейтсовский интерпретатор Basic «влезет» в большинство современных однокристальных МК, но экономная программа легче отлаживается (а значит, содержит меньше ошибок) и быстрее выполняется. Три-четыре такта, потерянных на вызове процедуры, могут стать причиной какой-нибудь трудновылавливаемой ошибки времени выполнения, например, если за это время произойдет вызов прерывания. Поэтому память в МК стоит экономить, даже если вы располагаете заведомо достаточным ее объемом.

Согласно упоминавшимся принципам фон Неймана, которые до сих пор являются основополагающими для разработчиков компьютерных систем, память должна быть организована иерархически, от памяти малого объема, но с высоким быстродействием, до медленной памяти большой емкости. Именно так и проектируют современные компьютеры, которые содержат очень быструю кэш-память на одном кристалле с процессором (а иногда два или три уровня такой памяти), потом идет быстродействующее ОЗУ (оперативное запоминающее устройство, та самая память, которая указывается в характеристиках ПК), а затем более медленные устройства типа жестких дисков или CD- и DVD-ROM.

Далее мы рассмотрим основные разновидности памяти, используемые как в составе микроконтроллеров, так и во внешних узлах. И начнем с того, что попробуем сами сконструировать устройство долговременной памяти — ПЗУ (постоянное запоминающее устройство). Как мы увидим, любая память в принципе есть не что иное, как преобразователь кодов.

Изобретаем простейшую ROM

Всем известное благодаря распространению оптических дисков сокращение ROM — Read Only Memory — и есть не что иное, как «западное» название ПЗУ. На самом деле это название («память только для чтения») не очень точно характеризует суть дела, отечественный термин «постоянное запоминающее устройство» более корректен, самое же правильное называть такую память «энергонезависимой». ПЗУ отличается от других типов памяти не тем, что его можно только читать, а записывать нельзя, а тем, что информация в нем не пропадает при выключении питания. Сама по себе невозможность записи теперь нехарактерна даже для компакт-дисков, однако название ROM сохранилось.

Тем не менее первыми разновидностями ПЗУ, изобретенными еще в 1956 году, были именно нестираемые кристаллы, которые носят наименование OTP ROM (One-Time Programmable ROM, «однократно программируемое ПЗУ»). До недавнего времени на них делали память программ МК для удешевления серийных устройств. Вы отлаживаете программу на перезаписываемой памяти, а в серию пускаете приборы с «прожигаемой» OTP ROM. И лишь в последние годы «прожигаемая» память стала постепенно вытесняться более удобной flash-памятью, когда последняя подешевела настолько, что смысл в использовании одноразовых кристаллов пропал. Они продолжают выпускаться лишь за счет инерции производства: в 2007 году доля однократно программируемых и масочных (т. е. программируемых прямо на производстве) микроконтроллеров составит, по прогнозам, не более 1/4 всех выпускаемых чипов.

Мы сконструируем подобие «прожигаемого» ПЗУ с помощью диодов. Простейший вариант такого ПЗУ представлен на рис. 11.4. В данном случае он представляет собой не что иное, как преобразователь из десятичного кода в семисегментный.

Рис. 11.4. Простейшее ПЗУ — преобразователь кода

Если на входе поставить дешифратор типа 561ИД1, то мы получим аналог микросхемы 561ИД5. Представьте себе, что первоначально на всех пересечениях между строками и столбцами диоды присутствовали — это вариант незаполненной памяти, в которой записаны все единицы. Затем мы взяли и каким-то образом (например, подачей высокого напряжения) разрушили те диоды, которые нам не нужны, в результате чего получили нужную конфигурацию. Эта схема не содержит активных элементов и потому возможности ее ограничены, например, выходы устройства, подающего активный высокий уровень по входным линиям, должны «тащить» всю нагрузку по зажиганию сегментов. Обычная микросхема ПЗУ построена на транзисторных ячейках и поэтому без всяких хитростей принимает и выдает обычные логические уровни. К тому же она включает в себя и дешифрирующую логику, поэтому на вход подается двоичный, а не десятичный код.

Постойте, а при чем тут ПЗУ вообще? Дело в том, что любое ПЗУ можно представить, как универсальный преобразователь кодов, если рассматривать входной код, как адрес ячейки, а код, получающийся на выходе — как содержимое этой ячейки. Причем удобство состоит в том, что изначально в ПЗУ не записано ничего (одни нули или единицы), и мы можем реализовать на нем любую логическую функцию, все зависит только от емкости. В том числе, такую простую, как преобразователь кодов. Или такую сложную, как операционная система Windows. Последнее мы каждый раз и делаем, когда устанавливаем Windows на компьютер, причем в качестве ПЗУ выступает жесткий диск. Из этого примера отчетливо видно, что каким бы сложным ни был алгоритм, он все равно в конечном итоге сводится к совокупности однозначных логических уравнений, которые можно реализовать как через ПЗУ, так и с помощью устройства памяти любого другого типа.

Общее устройство памяти

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

Рис. 11.5. Общее устройство ЗУ с однобитным выходом

Из нее видно, что память всегда имеет матричную структуру. В данном случае матрица имеет 8 х 8 = 64 ячейки. На рис. 11.5 показано, как производится вывод и загрузка информации в память с помощью мультиплексоров/демультиплексоров (вроде 561КП2, см. главу 9). Код, поступающий на мультиплексор слева (х3 - х5) подключает к строке с номером, соответствующим этому коду, активирующий уровень напряжения (это может быть логическая единица или ноль, неважно). Код на верхнем мультиплексоре (х0 - х2) выбирает аналогичным образом столбец, в результате к выходу этого мультиплексора подключается ячейка, стоящая на пересечении выбранных строки и столбца.

Легко заметить, что сама по себе организация матрицы при таком однобитном доступе для внешнего мира не имеет значения. Если она будет выглядеть, как 4x16 или 32x2 или даже 64x1 — в любом случае код доступа (он называется адресным кодом) будет 6-разрядным, а выход один-единственный. Поэтому всем таким ЗУ приписывается организация Nx1 бит, где N — общее число битов. Для того чтобы получить байтную организацию, надо просто взять 8 таких микросхем и добавить к адресной линии еще три разряда, которыми через отдельный мультиплексор можно управлять выборкой одной из этих микросхем (для этого каждая такая микросхема имеет специальный вывод, называемый «выбор кристалла» — chip select, или CS). В данном примере мы получим в сумме 9 адресных разрядов, что соответствует емкости памяти (64 х 8 = 512 бит или 29). Один из битов можно использовать при этом для контроля четности, так что у нас получается хорошая модель типового модуля емкостью 256 байт, вроде тех, что были в упомянутом ILLIAC–IV. Большинство выпускаемых интегральных ЗУ также сложены из таких отдельных однобитных модулей (только в наше время уже значительно большей емкости) и имеют 8 или 16 параллельных выходов, но бывают кристаллы и с последовательным (побитным) доступом.

В качестве примера можно привести, скажем, ПЗУ с организацией 64Кх16 типа АТ27С1024 фирмы Atmel. Это однократно программируемое КМОП ПЗУ с напряжением питания 5 В и емкостью 1024 Мбита, что составляет 128 кбайт или 64 К двухбайтных слов (как мы увидим, такая организация очень удобна в качестве внешней памяти программ в контроллерах той же Atmel). Следует отметить, что в области микросхем памяти сложилась счастливая ситуация, когда все они, независимо от производителя и даже технологии, совпадают по выводам, разводка которых зависит только от организации матрицы (даже, как правило, не от объема!) и, соответственно, от применяемого корпуса (в данном случае — DIP-40). Для разных типов (RAM, ROM, EEPROM и т. д.) различается разводка выводов, управляющих процессом программирования, но можно спокойно заменять одну микросхему на другую (с той же организацией и, соответственно, в таком же корпусе) без переделки платы. Разводка выводов АТ27С1024 показана на рис. 11.6.

Рис. 11.6. Разводка выводов АТ27С1024

RAM

Традиционное название энергозависимых типов памяти, как и в случае ROM, следует признать довольно неудачным. RAM значит Random Access Memory, т. е. «память с произвольным доступом», по-русски это звучит как ЗУПВ — «запоминающее устройство с произвольной выборкой». Главным же признаком класса является не «произвольная выборка», а то, что при выключении питания память стирается. EEPROM (о которой далее), к примеру, тоже позволяет произвольную выборку и при записи, и при чтении. Но так сложилось исторически, и не нам нарушать традиции.

Устройства RAM делятся на две больших разновидности — статические и динамические ЗУПВ. Простейшее статическое ЗУПВ (SRAM, от слова «static») — это обычный триггер. И «защелки» из микросхемы 561ТМЗ, и регистры типа 561ИР2, и даже счетчик с предзагрузкой типа ИЕ11 (см. главу 9), — все это статические ЗУПВ с различными дополнительными функциями или без них. Регистры и доступная пользователю область ОЗУ (оперативного запоминающего устройства) микроконтроллеров, — все они также относятся к классу SRAM, и мы с ними еще познакомимся довольно близко.

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

Рис. 11.7. Устройство ячейки DRAM :

а — схематическое устройство;  б — микрофотография среза кристалла DRAM (вытянутые вниз структуры — накопительные конденсаторы)

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

На практике регенерация в первых IBM PC и заключалась в осуществлении «фиктивной» операции чтения данных каждые 15 мкс с помощью системного таймера. Естественно, в таком решении было много подводных камней. Во-первых, регенерация всей памяти занимает много времени, в течение которого ПК неработоспособен. Потому-то сигнал на регенерацию и подавался с такой большой частотой, ведь каждый раз проверялась всего 1/256 памяти, так что полный цикл восстановления занимал около 3,8 мс. Во-вторых, такое решение потенциально опасно: любая зловредная программа спокойно может попросту остановить системный таймер, отчего компьютер уже через несколько миллисекунд обязан впасть в полный «ступор». И все современные микросхемы DRAM занимаются восстановлением данных самостоятельно, да еще и так, чтобы не мешать основной задаче — процессам чтения/записи.

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

Огромный плюс DRAM — простота и дешевизна. В отличие от нее, ячейка SRAM, как вы знаете, представляет собой D-триггер, и содержит много логических элементов, занимая большую площадь кристалла. Потому SRAM много дороже, но зато не требует никакой регенерации. Фирма Dallas (ныне объединенная с MAXIM) одно время выпускала микросхемы энергонезависимой памяти (и некоторые другие устройства на их основе), представлявшие собой обычную SRAM со встроенной прямо в чип литиевой батарейкой.

EPROM, EEPROM и Flash

На заре возникновения памяти, сохраняющей данные при отключении питания (EPROM Erasable Programmable ROM — «стираемая/программируемая ROM». По-русски иногда называют ППЗУ — «программируемое ПЗУ»), основным типом ее была память, стираемая ультрафиолетом: UV-EPROM (Ultra-Violet EPROM, УФ-ППЗУ). Причем часто приставку UV опускали, т. к. всем было понятно, о чем речь — альтернативой УФ-ППЗУ были фактически только однократно программируемые кристаллы OTP ROM, которые имелись обычно в виду под сокращениями ROM (или ПЗУ) просто, без добавлений. Микроконтроллеры с УФ-памятью программ были распространены еще в середине 1990-х. В рабочих образцах подобных устройств кварцевое окошечко заклеивали кусочком черной ленты, т. к. информация в UV-EPROM медленно разрушалась на солнечном свету.

Рис. 11.8. Первая микросхема UV-EPROM фирмы Intel (1971) позволяла записать 256 байт информации

На рис. 11.9 показано устройство элементарной ячейки подобной EPROM, которая лежит в основе всех современных типов энергонезависимой памяти. Если исключить из нее то, что обозначено надписью «плавающий затвор», то мы получим самый обычный полевой транзистор — точно такой же, как тот, что входит в ячейку DRAM на рис. 11.7. Если подать на управляющий затвор такого транзистора положительное напряжение, то он откроется, и через него потечет ток (это считается состоянием «логической единицы»). На рис. 11.9 вверху изображен как раз такой случай, когда плавающий затвор не оказывает никакого влияния на работу ячейки, например, такое состояние характерно для чистой flash-памяти, в которую еще ни разу ничего не записывали.

Рис. 11.9. Устройство элементарной ячейки EPROM

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

Замечание

Строго говоря, в NAND-чипах (о которых далее) логика обязана быть обратной: если в обычной EPROM запрограммированную ячейку вы не можете открыть подачей считывающего напряжения, то там наоборот— ее нельзя запереть снятием напряжения. Поэтому, в частности, чистая NAND-память выдает все нули, а не единицы, как EPROM. Но это нюансы, которые не меняют суть дела.

Так как плавающий затвор потому так и называется, что он «плавает» в толще изолятора (двуокиси кремния, SiО2), то сообщенные ему однажды заряды в покое никуда деваться не могут. И записанная таким образом информация может храниться десятилетиями (до последнего времени производители обычно давали гарантию на 10 лет, но на практике в обычных условиях время хранения значительно больше).

Осталось всего ничего — придумать, как размещать заряды на изолированном от всех внешних влияний плавающем затворе. И не только размещать — ведь иногда память и стирать приходится, потому должен существовать способ их извлекать оттуда. В UV-EPROM слой окисла между плавающим затвором и подложкой был достаточно толстым (если величину 50 нм можно охарактеризовать словом «толстый», конечно), и работало все это довольно «грубо». При записи на управляющий затвор подавали достаточно высокое положительное напряжение — иногда до 36–40 В, а на сток транзистора — небольшое положительное. При этом электроны, которые двигались от истока к стоку, настолько ускорялись полем управляющего электрода, что просто «перепрыгивали» барьер в виде изолятора между подложкой и плавающим затвором. Такой процесс называется еще «инжекцией горячих электронов».

Ток заряда при этом достигал миллиампера— можете себе представить, каково было потребление всей схемы, если в ней одновременно заряжать хотя бы несколько тысяч ячеек. И хотя такой ток требовался на достаточно короткое время (впрочем, с точки зрения быстродействия схемы не такое уж и короткое — миллисекунды), но это было крупнейшим недостатком всех старых образцов подобной EPROM-памяти. Еще хуже другое, то, что и изолятор, и сам плавающий затвор такого «издевательства» долго не выдерживали, и постепенно деградировали, отчего число циклов стирания/записи было ограничено нескольким сотнями, максимум — тысячами. Во многих образцах flash-памяти даже более позднего времени была предусмотрена специальная схема для хранения карты «битых» ячеек — в точности так, как это делается для жестких дисков. В современных моделях с миллионами ячеек такая карта, кстати, тоже, как правило, имеется, однако число циклов стирания/записи теперь возросло до соген тысяч. Как этого удалось добиться?

Сначала посмотрим, как осуществлялось в этой схеме стирание. В UV-EPROM при облучении ультрафиолетом фотоны высокой энергии сообщали электронам на плавающем затворе достаточный импульс для того, чтобы они «прыгали» обратно на подложку самостоятельно, без каких-либо электрических воздействий. Первые образцы электрически стираемой памяти (EEPROM, Electrically Erasable Programmable ROM — «электрически стираемое перепрограммируемое ПЗУ», ЭСППЗУ) были созданы в компании Intel в конце 1970-х при непосредственном участии будущего основателя Atmel Джорджа Перлегоса. Он использовал «квантовый эффект туннелирования Фаулера — Нордхейма» (Fowler — Nordheim). За этим непонятным названием кроется довольно простое по сути (но очень сложное с физической точки зрения) явление: при достаточно тонкой пленке изолятора (ее толщину пришлось уменьшить с 50 до 10 нм) электроны, если их слегка «подтолкнуть» подачей не слишком высокого напряжения в нужном направлении, могут просачиваться через барьер, не «перепрыгивая» его. Сам процесс показан на рис. 11.10 вверху (обратите внимание на знак напряжения на управляющем электроде).

Рис. 11.10. Процесс стирания в элементарной ячейке EEPROM

Старые образцы EEPROM именно так и работали: запись производилась «горячей инжекцией», а стирание — «квантовым туннелированием». Оттого они были довольно сложны в эксплуатации — разработчики со стажем помнят, что первые микросхемы EEPROM требовали два, а то и три питающих напряжения, причем подавать их при записи и стирании требовалось в определенной последовательности. Мало того, цена таких чипов была в свете нынешних тенденций почти запредельной. Автор этих строк сам покупал в середине 1990-х полумегабитную (т. е. 64-килобайтную) энергонезависимую память по цене 20 долл. за микросхему. Не забудьте еще про «битые» ячейки, возникновение которых в процессе эксплуатации приходилось все время отслеживать. Неудивительно, что на этом фоне разработчики предпочитали более дешевую, удобную, скоростную и надежную статическую память (SRAM), пристраивая к ней резервное питание от литиевых батареек, которые к тому времени уже достаточно подешевели.

Превращение EEPROM во Flash происходило по трем разным направлениям. В первую очередь — в направлении совершенствования конструкции самой ячейки. Для начала избавились от самой «противной» стадии — «горячей инжекции». Вместо нее запись стали осуществлять «квантовым туннелированием», как и при стирании. На рис. 11.10 внизу показан этот процесс: если при открытом транзисторе подать на управляющий затвор достаточно высокое (но значительно меньшее, чем при «горячей инжекции») напряжение, то часть электронов, двигающихся через открытый транзистор от истока к стоку, «просочится» через изолятор и окажется на плавающем затворе. Потребление тока при записи снизилось на несколько порядков. Изолятор, правда, пришлось сделать еще тоньше, что обусловило довольно большие трудности с внедрением этой технологии в производство.

Второе направление — ячейку сделали несколько сложнее, пристроив к ней второй транзистор (обычный, не двухзатворный), который разделил вывод стока и считывающую шину всей микросхемы. Благодаря этому (вместе с отказом от «горячей инжекции») удалось добиться значительного повышения долговечности — до сотен тысяч, а в настоящее время и до миллионов циклов записи/стирания (правда, последнее — при наличии схем коррекции ошибок, которые замедляют работу памяти). Кроме того, схемы формирования высокого напряжения и соответствующие генераторы импульсов записи/стирания перенесли внутрь микросхемы, отчего пользоваться этими типами памяти стало несравненно удобнее, т. к. они стали питаться от одного напряжения (5 или 3,3 В).

И, наконец, третье, чуть ли не самое главное усовершенствование заключалось в изменении организации доступа к ячейкам на кристалле, вследствие чего этот тип памяти и заслужил наименование — flash («молния»), ныне известное каждому владельцу цифровой камеры или карманного МРЗ-плеера. Так в середине 1980-х назвали разновидность EEPROM, в которой стирание и запись производились сразу целыми блоками — страницами. Процедура чтения из произвольной ячейки, впрочем, по понятным причинам замедлилась — для его ускорения приходится на кристаллах flash-памяти располагать промежуточную (буферную) SRAM. Для flash-накопителей это не имеет особого значения, т. к. там все равно данные читаются и пишутся сразу большими массивами, но для микроконтроллеров может оказаться неудобным.

Тем более там неудобен самый быстродействующий вариант технологии Flash— т. н. память типа NAND (от наименования логической функции «И-НЕ»), где читать и записывать память в принципе возможно только блоками по 512 байт (это обычная величина сектора на жестком диске, также читаемого и записываемого целиком за один раз— отсюда можно понять основное назначение NAND).

В МК обычно применяют традиционную (типа NOR) flash-пэмять программ, в которой страницы относительно невелики по размерам (порядка 64—256 байт). Впрочем, если пользователь сам не взялся за изобретение программатора для такой микросхемы, он может о страничном характере памяти и не догадываться. А для пользовательских данных применяют EEPROM либо с возможностью чтения произвольного байта, либо секционированную на очень маленькие блоки (например, по 4 байта), что также для пользователя значения не имеет.

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

 

Глава 12

Знакомство с микроконтроллером

Общее число существующих семейств микроконтроллеров оценивается приблизительно в 100 с лишним, причем ежегодно появляются все новые и новые. Каждое из этих семейств может включать десятки разных моделей. В 2002–2003 гг. в мире выпускалось ежегодно 3,2 млрд штук микроконтроллеров. Сравните — объем выпуска микропроцессоров для ПК в 2005–2006 гг. можно оценить в 200 млн единиц в год, т. е. всего около 6 % рынка. В то время как в финансовом исчислении, по данным Ассоциации полупроводниковой промышленности США, мировой объем рынка процессоров для ПК в 2006 году равнялся 33 млрд долларов, а микроконтроллеров — всего 12 млрд. Типичная цена рядового МК — порядка 2–5 долл., отдельные их представители могут стоить как существенно меньше, так и больше, но в любом случае их цена не достигает сотен долларов, как для отдельных моделей микропроцессоров от Intel и AMD.

Если говорить о ведущих компаниях, выпускающих микроконтроллеры, то первое место среди производителей 8-разрядных чипов традиционно принадлежит Motorola. Компания Microchip с очень популярным среди радиолюбителей семейством PIC занимает третье место, a Atmel, о продукции которой мы будем говорить далее — лишь шестое. Тем не менее эта формальная статистика еще ни о чем не говорит. Так, среди МК со встроенной flash-памятью Atrfiel принадлежит треть мирового рынка (и в 1995 году она была первой, кто вообще выпустил МК такой категории на рынок), при этом надо учитывать, что среди лидеров рассматриваемое семейство AVR-контроллеров самое молодое — первый Atmel AVR был выпущен в 1997 году. Так как рынок МК весьма консервативен (в рекламу на ТВ не вставишь что-нибудь вроде «в данном пылесосе используется последняя модель RISC-микроконтроллера с 32-разрядной шиной данных», поэтому мода тут играет далеко не ведущую роль), и шестое место в мировом масштабе можно рассматривать, как огромный успех.

Кроме 8-разрядных МК AVR, Atmel выпускает еще несколько их разновидностей, в том числе относящихся к таким популярным семействам, как ARM-процессоры и заметно модифицированные наследники старинного 8051, для которого в мире накоплен огромный объем программного обеспечения. Мы здесь ограничимся лишь 8-разрядными AVR, как одними из самых удобных в радиолюбительской и полупрофессиональной практике (например, для изготовления научного, производственного и другого спецоборудования в единичных экземплярах) — на этой почве они конкурируют лишь с упомянутыми PIC, однако для начального освоения опытные конструкторы рекомендуют именно AVR.

По вычислительной мощности ядро AVR-контроллеров, выполненное по RISC-архитектуре (Reduced Instruction System Command — «система с сокращенным набором команд»), заметно превышает 8-разрядные процессоры первых персональных компьютеров 80-х годов (i8086 в первых моделях IBM PC и 6502 в персоналках Apple), и сравнимо с производительностью 16-разрядного процессора i286, поскольку большинство инструкций выполняются за один такт, а рабочие частоты более высокие (обычно до 16 МГц, за небольшими исключениями, а у некоторых моделей — до 20 МГц). В AVR имеется 32 регистра общего назначения (часть из которых может также выполнять специализированные функции). Это позволяет в простых программах вообще не обращаться к памяти, что особенно удобно для начинающих.

Classic, Mega и Tuny

Линейка AVR делится на три семейства: Classic, Mega и Tuny. МК семейства Classic (они именовались, как AT90S) ныне уже не производятся, однако все еще распространены, т. к. их еще много на складах торгующих фирм и для них наработано значительное количество программ. Чтобы все это ПО пользователям не пришлось переписывать, фирма Atmel позаботилась о преемственности — все МК семейства Classic (за исключением разве что самого первого и не очень удачного AT90S1200) имеют функциональные аналоги в семействе Mega, например, AT90S8515 — ATmega8515, AT90S8535— ATmega8535 и т. п. (только AT90S2313 имеет аналог в семействе Tuny— ATtuny2313). На рис. 12.1 приведены варианты корпусов микроконтроллеров Atmel AVR и панельки для них.

Рис. 12.1. Различные корпуса микроконтроллеров Atmel AVR и панельки для них

Полная совместимость обеспечивается специальным установочным битом (из набора т. н. Fuse-битов), при программировании которого Mega-процессор начинает функционировать как Classic (подробнее об этом — в главе 13). Для вновь разрабатываемых устройств обычно никакого смысла в режиме совместимости нет, однако такой прием в ряде случаев может оказаться полезным для начинающих, т. к. МК Classic устроены проще и не заставляют пользователя отвлекаться на ненужные подробности, не имеющие отношения к делу. Поэтому часть примеров в этой книге, особенно начального уровня, будут ориентированы на семейство Classic.

Семейство Tuny (что в буквальном переводе означает «легко запоминающийся») предназначено для наиболее простых устройств, часть таких МК не имеет возможности программирования по последовательному интерфейсу (а АТ-tuny10 даже является однократно программируемым), и потому мы их не будем рассматривать в этой книге (за исключением ATtuny2313, точнее его «классического» аналога AT90S2313, оба они очень удобны для проектирования небольших, но функциональных устройств, и к тому же могут работать с частотой до 20 МГц). Это не значит, что Tuny следует избегать, среди них есть очень удобные и функциональные микросхемы. Но нельзя объять необъятное — здесь в основном мы будем рассматривать Mega.

Перечислим отличительные особенности Mega.

• Flash-пэмять программ от 8 до 128 кбайт (у семейства Classic 2–8 кбайт).

• Статическое ОЗУ (SRAM) от 512 байт до 4 кбайт (для Classic 128–512 байт).

• EEPROM данных от 512 байт до 4 кбайт (для Classic 128–512 байт).

• Различные способы тактирования: от встроенного RC-генератора, внешней RC-цепочки, внешнего кварцевого резонатора, внешнего сигнала (у Classic — только от кварцевого резонатора или внешнего сигнала). Удобная возможность для удешевления и упрощения схем, хотя и усложняет начальное программирование кристалла, причем у некоторых старших моделей имеется возможность программного снижения частоты.

• Расширенные режимы пониженного энергопотребления.

• Наличие встроенного детектора снижения напряжения питания (Brown-Out Detector, BOD).

• Усовершенствованный полнодуплексный последовательный синхронноасинхронный порт USART (на практике автором использовался исключительно в режиме обычного UART).

• Последовательный двухпроводной интерфейс TWI (по другому, I2С — на практике автор никогда не мог его заставить работать лучше, чем программный имитатор I2 С, пригодный практически для всех моделей AVR).

• Инструкции аппаратного умножения 8-разрядных чисел (в семействе Classic отсутствуют).

Структура МК AVR

Для ознакомления с тем, как устроены МК AVR, возьмем «классический» AT90S8515. Он включает в себя все существенные узлы моделей МК AVR из середины линейки, т. е. как младших Mega, так и старших Tuny, за исключением имеющегося в некоторых моделях АЦП (как, например, в его близком родственнике AT90S8535/ATmega8535, который мы будем широко использовать). Выбор именно Classic обусловлен тем, что блок-схема AYR громоздкая и без того, а в Mega присутствует еще много компонентов, которые для нас останутся второстепенными. Как и АЦП, эти компоненты (Brown-Out Detector, Fuse-биты, USART, дополнительные таймеры и т. п.) мы рассмотрим по ходу дела в дальнейшем.

На рис. 12.2 показана внутренняя структура МК AT90S8515. Нумерация выводов приведена для корпуса DIP-40, кроме этого, процессор выпускается в 44-выводных корпусах PLCC и TQFP (см. рис. 12.1). Даже беглого взгляда на рисунок достаточно, чтобы понять, что для детального рассмотрения структуры этого МК здесь просто не хватит места. Поэтому мы не будем переписывать фирменное описание (с некоторыми подробностями мы познакомимся по ходу дальнейшего изложения), а рассмотрим только некоторые ключевые узлы этой структуры и особенности их функционирования.

Рис. 12.2. Структура AT90S8515

Параллельные порты ввода/вывода

Начнем с внешних портов. В этой модели их четыре, и если подсчитать необходимые выводы (8x4 = 32), прибавить к ним обязательные выводы питания (контакты 20 и 40), тактового генератора (контакты 18 и 19) и вывод Reset (контакт 9), также присутствующий во всех моделях МК без исключения, то получится, что на все остальное остается три вывода. Это, конечно, недостаточно, поэтому почти все выводы портов, кроме своей основной функции (двунаправленного ввода/вывода) несут также и дополнительную. А как они (функции) разбираются между собой? А никак. Никакого специального переключения выводов портов не требуется, просто, если вы, к примеру, в своей программе инициализируете последовательный порт UART, то соответствующие выводы порта D (PD0 и PD1, выводы 10 и 11 микросхемы) будут работать именно в альтернативной функции, как ввод и вывод UART. При этом в промежутках между этим режимом выводов они могут выполнять функцию обычных двунаправленных выводов (хотя на практике это неудобно, потому что приходится применять схемотехнические меры для изоляции функций друг от друга, но иногда к этому прибегают). Аналогичная ситуация со всеми остальными альтернативными функциями — они начинают работать, когда в программе инициализированы соответствующие устройства МК.

По умолчанию все дополнительные устройства отключены, а порты работают на вход, причем находятся в состоянии с высоким импедансом (т. е. высоким входным сопротивлением). Работа на выход требует специального указания, для чего в программе нужно установить соответствующий нужному выводу бит в регистре направления данных (обозначается DDRx, где х — буква, обозначающая конкретный порт, например для порта А это будет DDRA). Если бит сброшен (т. е. равен логическому нулю), то вывод работает на вход (установка по умолчанию), если установлен (т. е. равен логической единице) — то на выход. Причем для установки выхода в состояние «1» нужно отдельно установить соответствующий бит в регистре данных порта (обозначается PORTx), а для установки в «0»— сбросить этот бит. Направление работы вывода (вход-выход, регистр DDRх), и его состояние (0–1, PORTх) путать не следует.

Регистр данных PORTх фактически есть просто выходной буфер, все, что в него записывается, тут же оказывается на выходе. Но если установить вывод порта на вход (т. е. записать в регистр направления логический ноль), как это сделано по умолчанию, то регистр данных DDRх будет играть несколько иную роль — установка его в «0» (так по умолчанию) означает, что вход находится в третьем состоянии с высоким сопротивлением, а установка в «1» подключит к выводу «подтягивающий» (pull-up) резистор сопротивлением около 35 кОм (подобно рис. 11.3, а; вернитесь к описанию работы подобного выходного каскада на «общую шину» в главе 11 — и вы поймете, зачем он нужен).

Сразу отметим, что встроенный pull-up-резистор лучше употреблять при работе на «общую шину» в пределах одной платы, но в большинстве случаев, когда требуется такой резистор (например, если вы подключаете ко входу выносную кнопку с двумя выводами, которая коммутируется на «землю», или при работе на «общую шину» с удаленными устройствами), лучше устанавливать дополнительный внешний резистор параллельно этому внутреннему, с сопротивлением от 1 до 5 кОм (в критичных для потребления случаях его величину можно увеличить до 20–30 кОм). В ответственных устройствах такой резистор следует устанавливать на выводе 9 (Reset) и на выводах 6, 7 и 8, которые служат для программирования и подключены к программирующему разъему ISP (см. главу 13), поскольку их также следует «подтягивать» к напряжению питания (тем более что довольно часто они также используются в качестве обычных входных/выходных линий, что процессу программирования, как правило, не мешает).

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

Установка дополнительного резистора к питанию (как и конденсатора — к «земле») по входу Reset для более надежного сброса рекомендовалась для самых первых моделей AVR, однако в описаниях современных типов вы такого не встретите. Но даже для старых моделей не оговаривалась установка внешних резисторов по другим выводам. Однако для более надежной работы устройства в условиях помех автор настоятельно советует следовать изложенным рекомендациям — хуже от этого не будет. Сопротивление «родного» резистора (который на самом деле представляет собой, разумеется, полевой транзистор) слишком велико для того, чтобы электромагнитные помехи («наводки») на нем эффективно «садились», и у вас могут происходить ложные срабатывания, перезапуски системы, а при очень мощных помехах— даже порча программы в памяти программ. Это, конечно, не касается ситуации, когда порт в режиме входа подсоединен к низкоомному выходу другой микросхемы или, скажем, транзисторного ключа (см. главу 12 ), т. к. у них на выходе всегда имеется какой-то сигнал, и резистор тогда не потребуется вообще, лучше всего ставить вывод микросхемы в третье состояние. Не забывайте только об этих резисторах при работе в режимах экономии питания — нужно тщательно проанализировать схему на предмет того, чтобы исключить ситуации, при котором через эти резисторы протекает ток.

Добавим еще, что, как вы, очевидно, уже поняли из изложенного, выводы портов в достаточной степени автономны, и их режим может устанавливаться независимо друг от друга. Кстати, а как прочесть уровень на выводе порта, если он находится в состоянии работы на вход? Возникает искушение прочесть данные из регистра данных PORTх, но это ничего не даст — вы прочтете только то, что там записано вами же. А для чтения того, что действительно имеется на входе (непосредственно на выводе микросхемы), предусмотрена другая возможность. Чтение происходит из буферного элемента, который в первом приближении соответствует элементу В на схемах по рис. 11.3, т. е. для чтения нужно обратиться к некоторому массиву, который обозначается PINх. Обращение осуществляется так же, как и к отдельным битам обычных регистров (см. главу 13), но PINх не есть регистр, это просто некий диапазон адресов, чтение по которым предоставляет доступ к информации из буферных элементов на входе порта. Записывать что-то по адресам PINx, естественно, нельзя.

Прерывания

Как и в ПК, прерывания (interrupts) в микроконтроллерах бывают двух видов. Но если в ПК прерывания делятся на аппаратные (например, от таймера или клавиатуры) и программные (фактически не прерывания, а подпрограммы, записанные в BIOS — с распространением Windows это понятие почти исчезло из программистской практики), то в МК, естественно, все прерывания являются аппаратными, а делятся они на внутренние и внешние. Любое прерывание, а также вообще возможность их возникновения требует отдельно предварительного специального разрешения. Учтите, что для инициализации прерываний в программе необходимо сделать два действия: разрешить соответствующее прерывание (предполагаем, все прерывания вообще уже разрешены) и установить для него один из доступных режимов. И, конечно, написать подпрограмму-обработчик, иначе все это будет происходить вхолостую (подробнее см. главу 13).

Внутренние прерывания могут возникать от любого устройства, которое является дополнительным по отношению к ядру системы: от таймеров, от аналогового компаратора, от последовательного порта и т. д. Внутреннее прерывание— это событие, которое возникает в системе и прерывает выполнение основной программы. Система внутренних прерываний в AVR довольно раз ветвленная и представляет собой основной механизм взаимодействия устройств с ядром системы, и мы еще к этому вопросу будем неоднократно возвращаться.

На внешних прерываниях мы остановимся здесь подробнее. Внешних прерываний у МК типа AT90S8515 два, INTO и INT1 (а вот в ATmega128 — целых восемь!), естественно, они могут возникать независимо друг от друга. Внешнее прерывание — событие, которое возникает при появлении сигнала на одном из входов, специально предназначенных для этого (в данном случае контакт 12 или 13). Различаются три вида событий, вызывающих прерывание, и их можно устанавливать в программе: это может быть низкий уровень напряжения, а также положительный или отрицательный фронт на соответствующем выводе. Любопытно, что прерывания по всем этим событиям выполняются, даже если соответствующий вывод порта сконфигурирован на выход.

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

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

Подробности

У внимательного читателя возникает законный вопрос— а зачем вообще нужен режим внешнего прерывания по уровню? Дело в том, что оно во всех моделях выполняется асинхронно, т. е. в тот момент, когда низкий уровень появился на выводе МК. Конечно, обнаружение прерывания может произойти только по окончании текущей команды, так что очень короткие импульсы могут «пропасть». Но прерывания INT0 и INT1 в режиме управления по фронту у большинства моделей определяются наоборот, только синхронно, т. е. в момент перепада уровней тактового сигнала контроллера, поэтому их длительность не должна быть короче одного периода тактового сигнала. Но это не самое главное: по большому счету разницы в этих режимах никакой бы не было, если бы не то обстоятельство, что синхронный режим требует непременно наличия этого самого тактового сигнала. И асинхронное внешнее прерывание, соответственно, может «разбудить» контроллер, находящийся в одном из режимов глубокого энергосбережения, когда тактовый генератор не работает, а синхронное— нет. И обычные МК, вроде разбираемого AT90S8515 семейства Classic (но не его Меда-аналога!), выводиться из глубокого «сна» могут только внешним прерыванием по уровню, которое не всегда удобно использовать. У большинства же моделей семейства Меда (из младших моделей — кроме АТmega8), имеется еще одно прерывание INT2, которое происходит только по фронтам (а не по уровню), и, в отличие от INT0 и INT1, только асинхронно. Это значительно повышает удобство работы с семейством Меда в режиме энергосбережения (на эту тему см. главу 17 ).

Таймеры-счетчики

В МК 8515 два таймера-счетчика: один 8-разрядный (Timer 0), второй — 16-разрядный (Timer 1). Оба счетчика представляют собой счетчики с предзагрузкой (вроде 561ИЕ11/14) и могут работать от тактовой частоты процессора непосредственно, или поделенной на 8, 64, 256 или 1024, представляя в таком режиме собой именно таймеры, т. е. устройства для отсчета времени.

Могут они работать и как обычные счетчики внешних импульсов с выводов Т0 (вывод 1) для Timer 0 и Т1 (вывод 2) для Timer 1 (по спаду или по фронту). Частота подсчитываемых импульсов не должна превышать половины частоты тактового генератора МК (причем при несимметричном меандре инструкция рекомендует и еще меньшее значение предельной частоты — 0,4 от тактовой), иначе счетчик вам посчитает «погоду на Марсе» — это сильное ограничение, поэтому, например, использовать МК для создания универсального частотомера неудобно. Поэтому все быстродействующие логические схемы конструируют не на микроконтроллерах, а на комбинационной логике — на ПЛИС.

При наступлении переполнения счетчика возникает событие, которое может вызывать соответствующее прерывание. Счетчик Timer 0 фактически этим и ограничивается. 16-разрядный счетчик Timer 1 — более «продвинутая» штука, и может вызывать прерывания по совпадению с определенным заранее заданным числом (точнее, даже два отдельных прерывания с двумя разными числами А и В), при этом счетчик может обнуляться или продолжать счет, и на специальных выводах OSC1A (вывод 15) или OSC1B (вывод 29) при этом могут генерироваться импульсы (аппаратно, без участия программы). Этот режим нам понадобится, чтобы задавать определенную частоту прерываний — например, в часах для отсчета секунд (см. главу 14).

Также возможно прерывание по внешнему событию на специальном выводе ICP (вывод 31), при этом содержимое счетчика помещается в некий регистр ICR1, а счетчик может обнуляться и начинать счет заново или просто продолжать счет (режим измерения периода внешнего сигнала). Источником таких событий может быть встроенный аналоговый компаратор— это удобно для того, чтобы построить на основе МК частотомер, но, как мы видели ранее— только низкочастотный, не более 5–7 МГц. Некоторые подробности о прерываниях и таймерах вы узнаете также из главы 17, когда мы будем разбирать режимы энергосбережения.

В AT90S8535 и во всех Mega (кроме Mega8515) есть как минимум еще один, третий таймер, который также будучи, как и Timer0, 8-разрядным, может выполнять более интересные вещи (например, работать автономно от остальной системы, когда весь процессор находится в режиме экономии, что позволяет этот таймер использовать в качестве часов реального времени, RTC). Но и это не все: описанные таймеры могут работать в т. н. PWM-режиме, специально предназначенном для их работы в качестве широтно-импульсных модуляторов (ШИМ). Этот режим мы кратко рассмотрим в главе 19 в связи с голосовой сигнализацией.

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

Остальные узлы МК семейства AVR мы рассмотрим по ходу изложения конкретных схем — так будет нагляднее. А тому, как состыковать последовательный интерфейс UART с компьютером, будет посвящена даже отдельная глава 18. Здесь же мы закончим краткое знакомство с микроконтроллером и перейдем к вопросу о том, как его программировать.

 

Глава 13

Персональный компьютер вместо паяльника

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

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

Отмечу, что каждый, кто хочет всерьез научиться программировать (неважно для чего, хоть для Веба, хоть для микроконтроллеров), должен начинать с изучения фундаментального труда Д. Кнута [10]. К сожалению, Кнут не практикующий программист, а профессор Стэндфордского университета, и как все профессоры, начинает с анализа, доказательств теорем и тому подобной «чепухи», зачем-то очень нужной математикам. Читатель, конечно, понимает, что в этих высказываниях есть значительная доля шутки, но правда заключается в том, что математик потребует доказательства там, где инженеру достаточно заверения авторитета, что такая-то формула справедлива, а доказательства и обоснования со ссылками на древнегреческих авторов его только отвлекают от главного. И все же я очень советую изучить по крайней мере те главы первого тома, которые посвящены машинным языкам, и алгоритмы арифметических действий из второго тома (всего тома три, но я ссылаюсь только на два, так как третий посвящен исключительно алгоритмам сортировки, которые лежат за пределами любительской программистской практики).

Конечно, продираться через многочисленные математические изыски Кнута необязательна, если вы согласны ограничиться готовыми программными модулями, которые с течением времени научитесь править и подстраивать под свои нужды. Материал в этой книге не рассчитан на «крутых» профессионалов, и для его освоения и повторения приведенных здесь конструкций не надо обладать особыми знаниями. Но после ознакомления с работой Д. Кнута во многих вещах вам будет значительно проще разобраться.

Как программируются микроконтроллеры

В МК AVR встроенная память программ энергонезависимая, и называется «flash-памятью программ», так ее можно отличить от также энергонезависимой памяти для хранения пользовательских данных, которая именуется EEPROM. Если строго следовать терминологии, то flash — это, как вы знаете, разновидность EEPROM, но во всяком случае, встроенная память программ для AVR-контроллеров делается именно по flash-технологии, что позволяет значительно ускорить процесс программирования. Первоначально такое разделение на EEPROM и flash для, по сути дела, одинаковых по функционированию разделов памяти (и то и другое призвано хранить данные при отсутствии питания) приводило еще и к тому, что память программ допускала лишь около 1000 циклов перезаписи, в то время как EEPROM намного больше (порядка 100 000). Так было в семействе Classic и первоначальных выпусках семейств Tuny и Mega. И хотя на практике и того, и другого более чем достаточно, современные МК AVR допускают уже 10 000 циклов перезаписи для памяти программ.

Подробности

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

С другой стороны, со временем разница между памятью программ и пользовательской EEPROM, совершенно очевидно, стирается: так, в семействе Tuny flash-память программируется побайтно, а не страницами, а в старших моделях семейства Mega, наоборот, EEPROM программируется страницами (правда, небольшого размера, всего в 4 байта). Для конечного пользователя все эти нюансы не имеют ровным счетом никакого значения.

Встроенная память программ имеет объем от 1 кбайта у ATtunyll до 128 кбайт у ATmega128 (как вы догадались, число в наименовании модели как раз и означает объем памяти программ). Если у пользователей системы Windows такие объемы вызовут лишь снисходительную улыбку, то советую им не «задаваться», поскольку Андрес Хейлсберг, автор Turbo Pascal, умудрился в первой версии этого продукта (1982) уложить в 33 280 байт интегрированную среду разработки, встроенный редактор и библиотеку времени выполнения. Так что возможностей AVR достаточно, чтобы построить неслабый персональный компьютер, который мог бы существенно превышать по возможностям модели IBM PC АТ на процессоре i286 (напомним, что последний выполнял инструкцию в среднем за 2–4 такта, a AVR — за 1–2). Правда, 1286 мог адресовать до 16 Мбайт динамического ОЗУ (у процессоров для ПК, напомним, нет встроенной памяти, кроме операционных регистров). Но для применений, которые требуют объемного программного кода, и AVR (кроме Tuny) могут выполнять программы из внешней памяти объемом до 4 Мбайт в отдельных моделях, причем скорость выборки команд зависит только от параметров ОЗУ и тактовой частоты AVR. Мы в данной книге эту возможность не рассматриваем — автору еще ни разу не удалось превысить лимит встроенной памяти выбранного МК.

Программаторы

Для того чтобы загрузить программу во flash-память, служат специальные программаторы, которые делятся на последовательные и параллельные (существуют еще стандартизированный согласно IEEE Std 1149.1-1990 интерфейс JTAG для отладки и тестирования микроконтроллерных устройств. С его помощью, например, выполняется всем известная «перешивка» готовых устройств. Но мы не будем здесь на этом останавливаться, т. к. в любительских конструкциях, да и во многих профессиональных, он никогда не употребляется).

Последовательные программаторы еще называют ISP (In-System Programmer, что переводится как «внутрисистемный программатор»), потому что они обычно не предполагают специального устройства для подсоединения и питания программируемой микросхемы, а заканчиваются обычным плоским кабелем с двухрядным штырьковым разъемом типа IDC или PLD (таким же, какие служат для подсоединения периферии к материнским платам ПК), который подсоединяется к специально предусмотренной штыревой части, располагаемой прямо на плате вашего устройства (подробнее см. главу 14). Питание на программатор при этом поступает от самой схемы.

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

Подробности

Последовательное программирование AVR есть, по сути, использование стандартного последовательного интерфейса SPI в специфических целях, причем следует учитывать, что в некоторых моделях (ATmega64 и АТтеда128) выводы собственно SPI с выводами программирования не совпадают. Процесс последовательного программирования начинается с того, что на вывод SCK подается напряжение уровня «земли», после этого на Reset поступает короткий положительный импульс, и затем последний также оставляют в состоянии низкого уровня не менее, чем на 20 мс (можно и просто предварительно подключить эти выводы к низкому уровню, а затем включить питание контроллера). При этом микросхема переходит в режим ожидания команд программирования. Отсюда понятно, почему снижается помехоустойчивость: такое сочетание уровней вполне может возникнуть в процессе эксплуатации из-за наводок на выводы разъема программирования. У автора этих строк в контроллере, расположенном в цехе со множеством работающих механизмов, при срабатывании мощного пускателя в нескольких метрах от устройства стиралась память программ! Правда, исправить ситуацию оказалось довольно просто установкой дополнительных «подтягивающих» резисторов.

Последовательное программирование поддерживают не все контроллеры AVR, из семейства Tuny так можно программировать только ATtuny12 и ATtuny15. В отличие от последовательного, параллельный способ программирования применяется для кристалла вне схемы. Программаторы такого рода, как правило, в той или иной степени универсальны и подходят для множества различных программируемых чипов, начиная от микросхем ПЗУ (в том числе и компьютерной BIOS) и заканчивая большинством МК.

Например, показанный на рис. 13.1 популярный AutoProg поддерживает около 4000 типов микросхем. Такие программаторы, естественно, относительно дороги (не самый дорогой в своем классе AutoProg стоит почти 9000 руб.), что и понятно — представьте, сколько труда вложено в программное обеспечение с поддержкой такого количества чипов. Экономически целесообразны они для тех, кто много работает с различными семействами микросхем, а также при изготовлении серийной продукции с отлаженной программой, в которой разъемы ISP-систем будут только мешать. Для отладки универсальные программаторы неудобны, т. к. приходится часто вытаскивать и вставлять микросхему в панельку с риском ее повредить, а для некоторых типов корпусов это может быть вообще исключено, если нет соответствующего переходника. Если вы работаете только с одним типом микросхем, но хотите тиражировать отлаженное изделие без лишних разъемов на плате, то вам ничего не стоит изготовить отдельный программатор на основе ISP-системы. Правда, «оживить» чипы AVR, в которых по ошибке были неправильно запрограммированы fuse-биты, отвечающие за источник тактового сигнала (об этом см. в конце главы), можно только с помощью параллельного программатора.

Рис. 13.1. Параллельный программатор AutoProg

Надо сказать, что и тот и другой способ программирования не составляет никакого секрета и подробно излагается в описании любого AVR. Так что в принципе программатор может быть изготовлен самостоятельно как в виде аппаратного устройства, обычно подключаемого через COM-порт, так и в чисто программном виде, с подключением кристалла через LPT-порт. Причем Atmel даже приводит в своих рекомендациях (Application Notes, которые наши электронщики обычно ласково зовут «аппнотами», доступны на сайте Atmel по адресу: ) типовую схему простейшего ISP-программатора (см. «аппноту» AVR910, которая в PDF-формате содержит схему, а asm-файлы включают исходные коды «прошивки»). Программу для ПК, впрочем, придется писать самому, на основе описания процесса программирования (или доставать где-нибудь готовую, например, извлекать из AVR Studio), поскольку программатор лишь транслирует команды.

Конечно, изготовление такого программатора самостоятельно — это занятие очень «на любителя», потому что количество затраченных усилий отнюдь не соответствует результату. Программатор можно заказать в самой Atmel, хотя это и нецелесообразно по ряду причин, т. к. долго и дорого (зато поддержка AVR Studio, о которой далее, обеспечена). Дешевенькое «наколеночное» устройство такого типа можно приобрести рублей за 300 через Интернет, а цена «более фирменных» ISP-систем не выходит за пределы примерно 30 долл. Автор этих строк много лет пользуется программатором AS-2 фирмы Argussoft, который непосредственно подсоединяется к COM-порту (на рис. 13.2 он показан без соединительного кабеля).

Рис. 13.2. Программатор AS-2 фирмы Argussoft в разобранном виде

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

Рис. 13.3. Программа-загрузчик к программатору AS-2

Программирующий разъем — десятиконтактный игольчатый IDС или PLD (подробнее про наименования разъемов такого типа см. главу 14) и именно на него будут ориентированы все схемы, приведенные в этой книге. Это не значит, что я вас призываю покупать именно AS, разъем такого типа стал стандартом и им снабжены многие ISP, кроме упоминавшегося простейшего ISP от самой Atmel, где разъем минимальный— шестиконтактный (три линии собственно SPI, а также два питания и Reset). Впрочем, при необходимости всегда можно изготовить переходник, т. к. все последовательные программаторы работают по одним и тем же линиям (в десятиконтактном разъеме «лишние» контакты служат для разделения сигнальных линий «земляными», так же, как в соединительном кабеле IDE-интерфейса жестких дисков).

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

Рис. 13.4. Пример расположения программирующего разъема PLD-10 на макетной плате и его разводка выводов

С или ассемблер ?

Ядро и система команд МК AVR с самого начала создавались в сотрудничестве с фирмой IAR Systems— производителем компиляторов для языков программирования C/C++. Поэтому структура контроллера максимально оптимизирована для того, чтобы можно было писать программы на языках высокого уровня. Так утверждает реклама, но верить подобным утверждениям стоит с некоторой оглядкой, т. к. все в конечном счете зависит от компилятора. Ведь задача перед ним стоит очень непростая: перевести строки на языке высокого уровня в команды контроллера, что для AVR ничуть не проще, чем для «настоящих» микропроцессоров, и потери тут неизбежны как в части компактности кода, так и времени его выполнения. Но это еще не самое главное.

Фирма IAR Systems в настоящее время предлагает серию пакетов Embedded Workbench для более чем двадцати типов МК различных фирм (под девизом «различные архитектуры — одно решение»). Здесь все рассчитано на то, чтобы человек, владеющий языком С, с минимальными потерями времени смог «пересесть» с одного на другой тип МК. В этом монструозном инструменте все было бы, возможно, и здорово, если бы не цена, которая измеряется в тысячах американских «президентов». Конечно, кое-кто в нашей стране может не придать этому значения (и многие не придают), но сначала стоит рассмотреть саму целесообразность такого подхода, а она вызывает обоснованные сомнения у многих специалистов.

Упомянем также, что IAR Systems — не единственный разработчик компиляторов с языка С для AVR. Есть куда более изящные и не столь «навороченные» инструменты (например, ICC for AVR от фирмы ImageCraft, CodeVisionAVR от HP Infotech), но в любом случае реальная цена их начинается от сотен евро. И это оправданно, поскольку рабочие инструменты должны быть качественными, а потому дешевыми быть не могут. Но для полноты картины стоит упомянуть и бесплатный WinAVR (AVRGCC, winavr.sourceforge.net), который создан на основе компилятора GNU GCC и, соответственно, распространяется по лицензии GPL (вместе с исходными кодами), обладающий всеми недостатками и достоинствами «свободных» продуктов, на чем мы здесь не будем останавливаться. Этот продукт поддерживается AVR Studio, о которой далее.

С другой стороны, фирменный ассемблер (в том числе и вариант, работающий в графическом режиме под Windows) абсолютно бесплатен. Даже если вы пойдете на поводу у тех, кто не придает никакого значения факту наличия у программных продуктов некоторой стоимости или рискнете обратиться к WinAVR, то вам необходимо еще принять во внимание следующие соображения. Язык С надо специально и отдельно учить. И в любом случае никто вас также и не освободит от подробного изучения аппаратной части МК. Для начинающего все это в совокупности может оказаться слишком сложным.

А что, спросите вы, язык ассемблера учить не надо? Практически не надо, потому что ассемблер— это, вообще говоря, не язык, а просто несколько правил оформления последовательности команд МК в текстовом представлении в «понятную» компилятору программу. Основные правила изложены в этой главе далее. Фирменное описание ассемблера для AVR занимает несколько страничек (сравните с фолиантами для изучения С) и входит в AVR Studio, отдельно его можно скачать с сайта Atmel. Функциональность команд наизусть заучивать отнюдь не требуется, т. к. даже профессионалы во время работы кладут перед собой открытый справочник по командам. Причем для каждого типа МК и правила написания, и мнемоника одинаковых по содержанию команд может быть различной, но в принципе все ассемблеры похожи и переход от одного к другому не настолько трудоемок, как это стараются представить разработчики «многокилобаксовых» компиляторов с языка С. Куда больше времени занимает изучение самой структуры контроллеров и работы их отдельных узлов.

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

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

«Программирование без GOTO» — лозунг знаменитого Дейкстры, классика процедурного программирования, к ассемблеру, к сожалению, абсолютно неприложим. Дейкстра имел в виду, что большое количество операторов перехода на метку очень сильно усложняет чтение программы и ведет к лишним ошибкам (подобные программы Дейкстра называл «спагетти» — из-за многочисленных линий переходов, сопровождающих графическое представление такой программы в виде блок-схемы). Но после компиляции все эти хорошо знакомые знатокам Pascal циклы while, do или repeat, until , равно как и оператор выбора (case), и даже — с некоторыми нюансами — обращение к процедурам, все равно превращаются в набор условных или безусловных переходов на определенные адреса в памяти программ. В ассемблере это приходится делать, что называется, «ручками». Разумеется, считать адреса (как при программировании в машинных кодах в начале 1950-х) вручную не нужно, в программе просто ставится метка, как и в языках высокого уровня (только в ассемблере еще проще, чем даже в каноническом Бейсике — в случае процедуры-подпрограммы метка заодно служит ее именем). Тем не менее сложности тут могут быть, и все зависит от склада вашего ума, некоторым (как автору этих строк) в хитросплетениях условных и безусловных переходов разобраться не составляет особенного труда, и иногда такое представление кажется даже более наглядным. Другие от этого «стонут и плачут», и им, конечно, следует обращаться к языку С.

При первоначальном изучении микроконтроллеров я бы решительно рекомендовал ассемблер. Как писал упоминавшийся в начале главы классик программирования Дональд Кнут, «каждый, кто всерьез интересуется компьютерами, должен рано или поздно изучить по крайней мере один машинный язык». Ассемблер — это первично, а все остальное вторично. Но автор этих строк вовсе не является упрямым снобом и прекрасно понимает, что сколько-нибудь крупные проекты (когда число строк кода начинает измеряться тысячами и более) на ассемблере будет создавать весьма затруднительно, по крайней мере, создавать «с нуля», не используя уже готовые модули. Но даже если вы в конце концов перейдете на С, советую эту книгу все же изучить до конца, чтобы познакомиться с AVR, а сам язык в приложении к AVR неплохо изложен в [4]Конструкция опубликована автором в журнале «Радио», 2004, № 9.
.

В этой книге вы встретите только ассемблерные программы. Мы ведь не будем писать для AVR операционные системы, и к тому же максимально попытаемся использовать готовые модули. А разобраться в работе этих модулей, когда они представлены в виде «голых» команд процессора, значительно проще. Не будем мы здесь и разбирать работу в AVR Studio — бесплатном пакете от Atmel, предназначенном для отладки программ (впрочем, программы на ассемблере в нем можно и создавать, а вот на С — только отлаживать, если нет дополнительного компилятора из перечисленных ранее). Вообще-то этот пакет в некоторых случаях оказывается очень целесообразен, особенно в совокупности с различными отладочными модулями (т. н. «китами», Kit’s, которые, в отличие от самого пакета, отнюдь не бесплатны). В нем удобно отлаживать сложные линейные (т. е. не использующие прерываний) процедуры, например, какое-нибудь деление четырехбайтных чисел. И если у вас хватит терпения и усидчивости, чтобы разобраться с еще одной «прослойкой» между вами и микросхемой, то вы ничего не потеряете. Но я не буду загромождать книгу не столь обязательными описаниями промежуточного софта, созданного исключительно для удобства, а покажу, как можно любую вашу схему превратить в отладочный модуль без лишних сложностей. Если вы заинтересовались AVR Studio, то вам сюда [3]Конструкция опубликована автором в журнале «Радио». 2002, № 8.
.

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

Автор этих строк — приверженец стиля разработки программ, который подозрительно напоминает широко известную в программистской среде «разработку, управляемую тестированием». Индустрия ПО приучает программистов к иному стилю: условно говоря, когда все создается «на бумаге». Сначала утверждается общий проект (для чего даже есть специальные люди — программные архитекторы), рисуются блок-схемы, разные программисты пишут отдельные куски кода (отлаживая их, конечно, но только «теоретически», без привязки к реальной рабочей среде), потом это объединяется в некий «проект», наконец, собирается альфа-версия и начинается тестирование готовой программы. Это привлекательный способ, особенно в случае разработки больших проектов, потому что он хорошо управляется в рамках стандартных бизнес-процессов. Именно в расчете на этот способ поточного производства программ создаются компиляторы со строжайшей проверкой типов данных и инструменты вроде AVR Studio, чтобы отловить мелкие ошибки по максимуму еще на начальной стадии. Но общие ошибки на уровне алгоритмов все равно отловить сразу не удается, и потом не удивляйтесь, откуда в конечном продукте низкоуровневые баги, которые следовало бы исправить еще на стадии блок-схемы. Хороший пример дает скандал с гибелью американской станции Mars Climate Orbiter, произошедшей из-за нестыковки в единицах измерения длины различных модулей бортовой навигационной программы.

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

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

Обустройство ассемблера

Если у вас еще нет AVR-ассемблера (т. е. самой программы-компилятора), то, возможно, AVR Studio вам придется установить, т. к. это самый надежный способ добыть последнюю версию ассемблера. Скачайте AVR Studio с сайта Atmel (например, отсюда , к сожалению, последние версии пакета стали довольно «монструозными» — более 40 Мбайт), установите его, разыщите файл avrasm32.exe или более современный avrasm2.exe (он находится в папке…\Atmel\AVR Tools\AvrAssembler) и скопируйте его в подготовленную заранее другую папку. Оттуда же целиком целесообразно скопировать папку Appnotes, во-первых, из-за собственно «аппнот», которые представляют собой рекомендации по реализации отдельных функций и выгодно отличаются от представленных на сайте Atmel форматом ASM. По сути, это готовые модули для встраивания в вашу программу (на сайте они находятся в формате PDF, а исходные коды приходится скачивать отдельно). Учтите, что в них могут быть ошибки, о чем мы еще будем упоминать, и применять их надо с оглядкой.

Во-вторых, в этой папке находятся файлы макроопределений (с расширением inc) для всех моделей AVR, поддерживаемых данной версией ассемблера, даже для тех, что уже не выпускаются. Если не скопировать папку, их пришлось бы скачивать с сайта Atmel поодиночке, а для старых типов искать где-то на стороне. Эти файлы необходимы, иначе вы не сможете откомпилировать ни одной программы, поскольку сам ассемблер абсолютно не подозревает о существовании таких вещей, как PortA или регистр DDRC, а «знает» только числовые адреса соответствующих регистров. Соответствие между этими мнемоническими обозначениями и адресами и устанавливается с помощью inc-файлов, причем для разных моделей эти адреса могут различаться. Можно на всякий случай извлечь также описание AVR-ассемблера в формате СНМ (на английском, естественно). После копирования саму AVR Studio можно удалить.

Ранее существовал и довольно примитивный ассемблер под Windows (wavrasm.exe), но затем, видимо, в корпорации решили его не развивать, обойдясь AVR Studio. Так что нам, сермяжным, придется обустроить среду для создания программ самостоятельно — это делается один раз, и потом вы вообще сможете забыть про существование avrasm.exe. Для этого потребуется еще как минимум текстовый редактор. Можно обойтись Блокнотом или многочисленными его более функциональными заменителями, вроде Edit Plus (MS Word не подойдет решительно), но тогда нам придется отдельно каждый раз запускать процесс компиляции из командной строки. Обычно это делается с помощью FAR или других клонов Norton Commander, что неудобно и сильно замедляет процесс, Потому лучше использовать специальный редактор, который позволит запускать процесс компиляции прямо из своего окна, и к тому же имеет возможность «подсветки» синтаксиса. Так как специально для AVR редакторов никто не делает, они чаще всего по умолчанию «заточены» под Intel-ассемблер, но «подсветку» нередко можно настроить «под себя».

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

Редакторов для написания ассемблерного кода довольно много, как правило, они в той или иной степени «самодеятельные» и бесплатные (исключение — очень профессиональный и известный с давних времен, но платный MultiEdit). Тут важно только выбрать самый удобный, иначе можно попасть в ситуацию, когда будет еще хуже, чем с Блокнотом. Например, широко разрекламированный на множестве ресурсов ASMEdit (некоего Матвеева), первое, что у меня сделал — еще в процессе инсталляции «повесил» Windows 98 [11] до полной неработоспособности, а когда я все же «достучался» до исполняемого файла, то запустить его оказалось невозможным, т. к. окно свернулось в углу экрана в маленький квадратик и распахиваться не желало. Я разыскал более старую версию (ASMEdit 1.2), она установилась нормально (если не считать грамматических ошибок в инсталляторе), и тут выяснилось, что: а) настройка запуска компиляции из командной строки настолько сложна, что требует чуть ли не написания отдельной программы; б) настройка «подсветки» синтаксиса крайне примитивна. К тому же программа без спроса ассоциирует с собой расширение asm и «замусоривает» перечень ассоциаций еще полудюжиной расширений для неведомых целей, которые потом приходится вычищать вручную. Я это так подробно рассказываю потому, что у этого редактора есть одно удобное свойство: сообщения компилятора перенаправляются в окно редактора, как это было в wavrasm, и не требуется просматривать отдельные консольные окна. Если вам удастся «справиться» с ASMEdit, то вам сильно повезло.

Я перебрал в свое время несколько программ, но ни одна меня не устроила в такой степени, как творение некоего Анатолия Вознюка, которое я использую с 1999 года. Сам Анатолий знаменит также своим шедевром под названием Small CD Writer, давно скрывается инкогнито, сайт его больше не откликается, а программы не развиваются (в частности, Small CD Writer так и «не умеет» корректно писать на DVD). Но в разбираемом редакторе под названием ASM Editor (последняя версия 2.2), собственно, больше и развивать нечего. Доступен он на множестве «софтоотстойников» в Интернете* только не перепутайте его с упомянутым ASMEdit.

Для начала работы нам предварительно надо создать командный файл. Предположим, у вас файл avrasm32.exe находится в созданной вами папке c: \avrtools. Запустите Блокнот и введите в него следующий текст (соответственно измените путь, если папка другая): c: \avrtools\avrasm32 — fI %1.asm

Сохраните этот файл под именем, например, asm32.bat в той же папке, что и avrasm32.exe. Теперь запустите ASM Editor, выберите в меню пункт Service|Properties и в появившемся окне найдите вкладку Project, а в ней — строку Assemble ASM file (рис. 13.5). В эту строку, как и показано на рисунке, внесите путь к нашему bat-файлу. Больше ничего настраивать не требуется, остальные пункты можно оставить, как есть, или очистить — по желанию. Теперь по нажатию сочетания клавиш +, вне зависимости от того, где находится файл с редактируемой программой, он скомпилируется, и результат (т. н. hex-файл, о чем далее), если нет ошибок, окажется в той же папке, где и исходный текст.

Рис. 13.5. Настройка редактора ASM Editor

Одновременно с компиляцией отдельно возникает консольное (с белыми надписями на черном фоне) окно с сообщениями компилятора. Сразу смотрите на последние строки, где должна быть надпись «Assembly complete with no errors» («ассемблирование выполнено без ошибок»). Если такая надпись есть, ищите готовый hex-файл в папке с исходным текстом. В противном случае hex-файла не окажется (а предыдущий его вариант, если он был, будет стерт), а в этом окне появятся номера строк в исходном тексте с ошибками и примерным описанием проблемы в данной строке (например, сообщение «Undefined variable reference» — «ссылка на неопределенную переменную», означает, что у вас в данной строке идентификатор, который нигде не определен).

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

Структура программы AVR

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

Естественно, программу сначала нужно в память записать, причем записать совершенно определенным образом, так, чтобы МК «знал», откуда начинать при включении питания или после подачи импульса Reset. Это его «знание» в случае современных МК AVR также программируется, однако для простоты будем считать, что программа всегда начинает читаться с самой первой ячейки памяти программ, т. е. с нулевого адреса. (А вот для классического процессора семейства i86 это не так. По умолчанию он всегда начинал с команды, расположенной за 16 байт до конца первого мегабайта памяти, т. е. по адресу FFFFOh. К тому же, адрес этот можно изменять, но мы этим здесь заниматься не будем.) Исходя из этих обстоятельств, программа должна иметь определенную структуру.

По начальному (нулевому) адресу всегда располагается одна и та же команда безусловного перехода (по-английски jump — «прыжок»), которая записывается так:

rjmp RESET

Или так:

jmp RESET

Отметьте себе на память, что AVR-ассемблер, в отличие от канонического С, регистры букв не различает (одинаково правильной будет форма записи jmp, JMP и jmp, так же как Reset, RESET и reset).

Форма написания (jmp или rjmp) зависит от типа контроллера: если в нем объем памяти программ меньше или равен 8 кбайт, то всегда (и не только в этом случае) используется команда rjmp (relative jump, т. е. «относительный безусловный переход»). Она занимает в памяти два байта, как и практически все остальные команды AVR, о чем мы еще поговорим. Код самой команды в этих двух байтах занимает старшую тетраду старшего байта (т. е. четыре бита), остальные 12 бит представляют собой адрес, куда переходить. В данном случае компилятор подставит адрес команды, следующей сразу за меткой RESET, с которой и начнется собственно выполнение программы. Метка с этим именем, естественно, всегда должна присутствовать, но может быть расположена уже в любом другом удобном месте программы, за исключением еще нескольких первых адресов, о назначении которых далее. Метка, кстати, может называться и не RESET, а любым другим именем, просто так принято для удобства чтения: хотите найти в любой программе ее начало — ищите метку RESET.

Вернемся к форме записи команды. 12 бит адреса могут представлять 4096 различных адресов. Так как единицей объема памяти программ служит слово из двух байтов (а не отдельный байт), как раз из-за того, что любая команда занимает не менее чем два байта, то общий объем адресуемой таким образом памяти и составит 8 кбайт. А вот если памяти больше, то приходится использовать команду jmp (абсолютный безусловный переход). Она состоит из четырех байт, в которых адрес займет 22 бита, и потому с ее помощью можно адресовать до 4 М (8 Мбайт) памяти. Мы условимся, что старшие модели семейства Mega применять не будем, и потому ограничимся командой rjmp.

Подведем некоторый итог: мы уже узнали многое о структуре типовой программы для AVR. Она должна начинаться с безусловного перехода на метку RESET, а само начало этой программы будет располагаться сразу после этой метки где-то в другом месте программы. А почему так странно? Нельзя ли начать прямо сразу с нулевого адреса, строка за строкой, зачем нужны какие-то переходы? Можно, отвечают нам авторы описаний AVR. Простейшая программа может начинаться с нулевого адреса без переходов, но только в одном случае: если мы обязываемся отказаться от прерываний. А без них контроллер теряет 90 % своей функциональности. Конечно, можно придумать пример программы, которая бы делала что-нибудь полезное, но при этом работала совсем без прерываний. Но на практике без прерываний обходятся только тогда, когда штатных прерываний хватает (например, для отслеживания состояния большого количества кнопок), или когда без них программа получается компактнее (такие случаи мы еще встретим).

Обработка прерываний

AVR по умолчанию ожидает, что сразу после первой команды с адресом $0000 идет таблица т. н. векторов прерываний. Вектор — это просто отсылка по нужному адресу с помощью команды rjmp. Адрес обозначается меткой, может располагаться в любом месте программы, и содержит начало процедуры обработки прерывания. Первый вектор располагается по адресу $0001 (а для МК с памятью более 8 К — по адресу $0002, потому что по адресу $0001 находится вторая половина более длинной команды jmp), причем адрес этот означает номер двухбайтного слова в памяти, а не отдельного байта (оно в два раза меньше количества байт). Попутно заметим, адреса в SRAM (ОЗУ) именно байтовые, а не пословные (см. далее). На самом деле в режиме по умолчанию нам вообще не нужно думать про абсолютные адреса и их нумерацию: первая команда программы (rjmp RESET), которая еще называется вектором сброса (или вектором начальной загрузки), автоматически расположится по нулевому адресу, вторая — по адресу $0001 и т. д. Найдя какую-нибудь команду перехода по метке, компилятор автоматически подставит абсолютные адреса.

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

Листинг 13.1.

;=======прерывания=======

rjmp RESET   ;Reset Handler

rjmp EXT_INT0   ;IRQ0 Handler

rjmp EXT_INT1   ;IRQ1 Handler

rjmp TIM2_COMP   ;Timer2 Compare Handler

rjmp TIM2_OVF   ;Timer2 Overflow Handler

rjmp TIM1_CAPT   ;Timer1 Capture Handler

rjmp TIM1_COMPA   ;Timer 1 CompareA Handler

rjmp TIM1_COMPB   ;Timer1 CompareB Handler

rjmp TIM1_OVF   ;Timer1 Overflow Handler

rjmp TIM0_OVF   ;Timer0 Overflow Handler

rjmp SPI_STC   ;SPI Transfer Complete Handler

rjmp USART_RXC   ;USART RX Complete Handler

rjmp USART_UDRE   ;UDR Empty Handler

rjmp USART_TXC   ;USART TX Complete Handler

rjmp ADC   ;ADC Conversion Complete Handler

rjmp EE_RDY   ;EEPROM Ready Handler

rjmp ANA_COMP   ;Analog Comparator Handler

rjmp TWSI   ;Two-wire Serial Interface Handler

rjmp SPM_RDY   ;Store Program Memory Ready Handler

;========================

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

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

Но постойте, мы что, обязаны использовать все прерывания? Конечно, нет. Для неиспользуемых прерываний вместо команды rjmp <метка> следует поставить reti — выход из прерывания («return interrupt»). На самом деле здесь возможна и команда пор — пустая операция («по operation»). Я пробовал из любопытства, — все отлично работает. Но руководство рекомендует именно reti, т. к. при случайной инициализации прерывания оно все равно не будет выполняться, а команда пор в этом случае, очень вероятно, привела бы к «зависанию» программы либо выполнению совсем других, не предусмотренных нами, операций.

Лично я оформляю начало программы следующим образом: копирую таблицу в указанном ранее виде, а затем в начале каждой строки (кроме первой) автоматически меняю с помощью функции поиска-замены редактора «rjmp» на «reti; rjmp», вот так (старую команду я закомментировал):

rjmp RESET   ;Reset Handler

reti; rjmp EXT_INT0   ;IRQ0 Handler

reti; rjmp EXT_INT1   ;IRQ1 Handler

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

rjmp RESET   ;Reset Handler

rjmp EXT_INT0   ;IRQ0 Handler

EXT_INT0:   ;процедура обработки прерывания INTO

reti  ;окончание процедуры обработки прерывания INTO

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

Процедура RESET

Теперь обратимся к процедуре RESET, т. е. к истинному началу программы, что там должно быть? Когда контроллер доходит до команды вызова подпрограммы (call или rcall, см. далее), или в нем происходит прерывание, он должен сохранить состояние программного счетчика с тем, чтобы потом «знать», куда вернуться. Это происходит в специальной области памяти, которая называется стеком (stack). Потому в любой программе на AVR-ассемблере для семейств Mega и Classic первыми после метки reset должны идти следующие строки:

RESET:

          ldi temp, low(RAMEND)   ;загрузка указателя стека

          out SPL, temp

          ldi temp, high(RAMEND)   ;загрузка указателя стека

             out SPH, temp

Этими операторами вы указываете, где компилятору расположить программный стек, а именно в конце SRAM (что обозначается константой RAMEND, объявленной в соответствующем inc-файле, загляните). Служебные слова low и high означают, что от ramend нужно взять, соответственно, младший и старший байты. Для тех моделей, у которых объем SRAM не превышает 256 байт (из доступных в настоящее время это только модель 2313 во всех ее версиях), запись сокращается:

RESET:

        ldi temp, RAMEND   ;загрузка указателя стека

         out SPL, temp

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

У многих представителей семейства Tuny (кроме, насколько я знаю, Tuny2313), в том числе тех, у которых объем SRAM также не превышает 256 байт, стек аппаратный, а не программный, потому там его расположение специально указывать не требуется (отметим, что в [2] в этой части допущена ошибка). По этой причине, в частности, для семейства Tuny не определены команды push и pop, которые для остальных моделей позволяют временно сохранять в стеке значение какого-либо регистра (а для некоторых процессоров, вроде i8086, в которых регистров общего назначения мало, это вообще одни из самых употребительных команд). Размер же аппаратного стека строго фиксирован, и в нем умещается только адрес возврата из подпрограммы или обработчика прерывания.

После задания стека желательно поставить следующие строки:

ldi temp,1<<ACD

out ACSR,temp   ;выкл. аналог, компаратор

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

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

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

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

STOP:

rjmp STOP

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

Определения переменных, констант и подключение внешних файлов

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

….

mov  r16,r11

cpi    r1l6,$11

и

mov  temp,counter

cpi    temp,max_value

И та, и другая запись верна, но вторая значительно более «читабельна», правда? Одно дело иметь программу, в которой фигурируют регистры (еще слава богу, что не их абсолютные адреса) и числа, совсем другое — работать с «говорящими» именами переменных и констант. Идентификатором temp (от слова «temporary» — временный) обычно обозначают рабочую переменную, которая не предназначена для долговременного хранения данных, counter — в переводе «счетчик», max_vaiue — «максимальное значение». Но если команды компилятор «знает» (мнемоника команд фиксирована и их смысл мы расшифруем немного далее), то как ему узнать, что мы имели имели в виду под этими самыми temp и counter?

Для этого служит секция определений имен переменных и констант, которую чаще всего располагают в самом начале текста программы, еще до векторов прерываний, или в отдельном внешнем файле, если определений много, и они очень загромождают текст. Именно такие макроопределения и содержат те самые файлы с расширением inc, которые мы копировали из AVR Studio, — для каждого контроллера свои. В результате чего мы можем писать mov GIMSK, temp вместо mov $3F,r16.

Для того чтобы компилятор нашел эти определения, inc-файл нужно подключить к нашей программе. Это делается с помощью директивы include, как в языке С, например (обратите внимание на точку перед include):

.include «2313def.inc»

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

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

Но имени temp (не говоря уж о counter) вы в inc-файлах не найдете — это нестандартное определение и относится уже к нашей «епархии». Создать свои определения можно с помощью директив def (definitions) для переменных и equ (equvalent) для констант вот так:

.equ max_value = $11  ;максимальное значение = 17

.def temp = r16   ;регистр r16 есть переменная temp

.def counter = r05 ;регистр r05 есть переменная counter

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

.def temp = r16   ;регистр r16 есть переменная temp

.def counter = r16   ;регистр r16 есть переменная counter

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

С помощью директивы equ, вообще говоря, можно определять довольно сложные выражения, но этим пользуются редко, гораздо чаще ее применяют для определения переменных, которые располагаются не в регистрах, а в области SRAM. Например, следующая последовательность директив и команд запишет содержимое регистра counter в SRAM по адресу $60 (для большинства моделей, кроме старших Mega, это первый свободный адрес после занятых адресами регистров):

.equ memory_address = $60

clr ZH

ldi ZL,memory_address

st Z,counter

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

Система команд AVR

С некоторыми командами мы уже познакомились, но еще не знаем толком, что они означают. Теперь я постараюсь разобрать основные команды и их использование. Подчеркну, что речь идет только о части самых употребительных и концептуально важных команд, потому что всего команд для AVR от 90 до 133 (в зависимости от контроллера), и только их подробное описание, например в [2]Префикс «К» в названии отечественных микросхем, обозначающий их принадлежность к бытовому/коммерческому диапазону температур, мы будем в этой книге опускать, подробнее см. главу 8 .
, занимает почти 70 страниц, гораздо больше, чем описание собственно AVR-ассемблера. С теми командами, которые выпадут из рассмотрения здесь, мы познакомимся по ходу дела в дальнейшем. Выборочный перечень базовых команд с их описанием, достаточный для составления большинства законченных программ, вы также найдете в Приложении 4. Однако в любом случае при написании собственных программ вам будет необходимо иметь справочник по командам, в роли которого может выступать как [1, 2], так и англоязычный документ PDF с описанием конкретного контроллера, который легко добывается с сайта Atmel. Учтите, что в неофициальных пособиях могут быть ошибки, так в [1, 2] они встречаются в достаточном количестве, хотя в последних изданиях их, возможно, и исправили.

Формат команды

Прежде всего, заметим, что команды (инструкции, операции — мы будем употреблять эти слова как синонимы) контроллера записываются в определенном формате. Сначала идет собственно мнемоническое название команды, которое может состоять из двух (ld, st), трех (mov, ldi, sei, add) или четырех (breq, brne, sbiw) символов. Затем после пробела (любого их числа, допустимы также знаки табуляции), если это предусмотрено форматом команды, идут операнды: имена регистров или константы (а также выражения), над которыми производится операция (на самом деле это не имена, а адреса, но мы не будем забывать про включение inc-файлов и создание собственных определений, и оперировать только с именами, так будет гораздо меньше ошибок).

Если операндов два (больше не бывает), то они перечисляются через запятую (с любым числом пробелов до или после нее, или вообще без них). Причем тут действует важное правило: во всех ассемблерах второй операнд является первым по смыслу действия (т. е. источником), а первый — приемником (результатом). Например, команда ldi temp, 1 расшифровывается, как «загрузить единицу в регистр temp», а операция sub temp, counter вычитает counter из temp, а не наоборот, и результат помещает также в temp.

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

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

Выходные файлы

Есть, конечно, команды, которые вообще не требуют операндов (как уже знакомые нам пор или sei), или используют всего один операнд (cir), но вне зависимости от этого каждая команда AVR будет занимать в памяти ровно два байта (кроме немногих команд, вроде упоминавшейся jmp, которые работают с операндами большого размера, оттого занимают четыре байта). Такое готовое представление — командное слово, или код операции (КОП) — и записывается в виде числа компилятором в выходной файл, имеющий расширение hex, который необходим программатору для дальнейшей записи в контроллер. Кроме HEX-файлов, есть еще и другие форматы записи готовых программ (самый известный — бинарный), но hex-формат для микроконтроллеров самый распространенный, и мы будем рассматривать только его.

Рассматриваемый НЕХ-формат придуман фирмой Intel (есть и другие «гексы»). Отличается такой формат тем, что содержит числа в текстовом представлении (в шестнадцатеричной записи). Поэтому в случае чего его можно даже править в обычном текстовом редакторе (что исключено для бинарных файлов, содержащих числа, а не символы). Кстати, точно в таком же формате осуществляется запись констант в EEPROM, если это требуется.

Рассмотрим НЕХ-формат подробнее. На рис. 13.6 представлен файл короткой программы, открытый в обычном Блокноте. На первый взгляд тут «сам черт ногу сломит», но на самом деле все достаточно просто, хотя чтение затрудняется тем, что строки не поделены на отдельные байты. Разбираться будет проще, если вы скопируете этот файл под другим именем и расставите в нем пробелы после каждой пары символов. Мы же рассмотрим данный файл как есть.

Рис. 13.6. Файл формата HEX в Блокноте

Основную часть файла занимают информационные строки, содержащие непосредственно КОП. Они состоят из ряда служебных полей и собственно данных. Каждая строка начинается двоеточием, после которой идет число байт в строке. Кроме первой и последней, везде стоит, как видите, число 10 (десятичное 16), т. е. в каждой строке будет ровно 16 информационных байт (исключая служебные). После количества байт идет два байта адреса памяти, указывающие куда писать (в первой строке 0000, во второй это будет, естественно, 0010, т. е. предыдущий адрес плюс 16, и т. д.). Наконец, после адреса идет еще один служебный байт, обозначающий тип данных, который в информационных строках равен 00 (а в первой и последней — 02 и 01, о чем далее). Только после этого начинаются собственно байты данных, которые означают соответствующие КОП, записанные пословно (КОП для AVR, напоминаю, занимают в основном два байта, и память в этих МК также организована пословно), причем так, что младший байт идет первым. Таким образом, запись в первой информационной строке «ЗАС0» в привычном нам «арабском» порядке, когда самый старший разряд располагается слева, должна выглядеть, как СОЗА (если помните, я в главе 7 упоминал об этой несуразице).

В первой строке служебный байт типа данных равен 02, и это означает, что данные в ней представляют сегмент памяти, с которого должна начинаться запись (в данном случае 0000). Заканчивается hex-файл всегда строкой «:00000001ff» — значение типа данных 01 означает конец записи, и данных больше не ожидается. А что означает FF?

Самым последним байтом в каждой строке идет контрольная сумма (дополнение до 2) для всех остальных байт строки, включая служебные. Алгоритм ее вычисления очень простой: нужно вычесть из числа 256 значения всех байтов строки (можно не обращая внимание на перенос), и взять младший байт результата. Соответственно, проверка целостности строки еще проще: нужно сложить значения всех байтов (включая контрольную сумму), и младший байт результата должен равняться нулю. Так, в первой строке число информационных байтов всего два (первое значение в строке), плюс служебный байт типа данных, равный также двум, итого контрольная сумма всегда равна 256 — 2–2 = 252 = $FC. В последней строке одни нули, кроме типа данных, равного 1, соответственно, контрольная сумма равна 256 — 1 = 255 = $FF.

Теперь попробуем немного расшифровать данные. Первое слово в первой информационной строке, как мы выяснили, равно $С03А. Если мы возьмем фирменное описание команд, то обнаружим, что значению старшей тетрады в КОП, равной $С (1100 в двоичной системе), соответствует команда rjmp, но ведь так и должно быть, мы же договорились, что любая программа начинается с безусловного перехода на метку RESET. Теперь очевидно, что остальные биты в этом значении ($03А) представляют абсолютный адрес в программе, где в ее тексте стояла метка reset. Попробуем его найти, для этого вспомним, что адреса отсчитываются по словам, а не по байтам, т. е. число $ЗА (58) надо умножить на 2 (получится 116 = $74), и искать в этой области. Разыщем строку с адресом $0070, отсчитаем три пары байт от начала данных, и найдем там фрагмент «F894», который в нормальной записи будет выглядеть, как $94F8, а это, как легко убедиться по справочнику, есть код команды cli, запрещающей прерывания (которая в начале программы лишняя, т. к. они все равно запрещены, но, видимо, поставлена на всякий случай). Следующая команда будет начинаться с байта $Е5, и первая тетрада в ней обозначает код команды ldi (1110 — проверьте!), а пятерка, очевидно, есть фрагмент адреса конца памяти (RAMEND), который в силу довольно сложного формата записи получается на самом деле равным $025F (см. значение младшего байта, равное $2F). Такое значение соответствует значению ramend, определенному в inc-файлах для МК с 512 байтами встроенного ОЗУ. Все, как и должно быть. Если мы обратим внимание опять на первую-вторую строки с данными, то увидим повторяющийся фрагмент «1895», который, как легко догадаться из материала предыдущего раздела, должен быть командой reti (если проверите по справочнику, то так оно и окажется).

Как видите, разобраться довольно сложно, но при некотором навыке и наличии под рукой таблицы двоичных кодов команд вполне возможно. Именно так работает программа, которая превращает код обратно в текст — дизассемблер (он входит в AVR Studio). А зачем это может понадобиться на практике? Дело в том, что в памяти программ часто хранят те константы, которые предположительно не будут изменяться в процессе эксплуатации, например, устанавливаемые по умолчанию значения какой-то величины. Но, разумеется, по истечении некоторого времени эти константы обязательно захочется изменить. И если у вас текст программы по каким-то причинам отсутствует (например, программа взята из публикации в журнале или скачана с радиолюбительского сайта), а загрузочный hex-файл имеется, то всегда можно «хакнуть» исходный код, и немного подправить его под свои нужды.

Команды перехода (передачи управления)

В языках высокого уровня была всего одна команда перехода на метку (GOTO), и то Дейкстра на нее «набросился». А в ассемблере AVR таких команд пруд пруди, целых 33 шутки! Зачем? На самом деле без доброй половины из них, если не больше, можно обойтись во всех жизненных случаях, т. к. они в значительной степени взаимозаменяемы. Разнообразие это, если угодно, дань памяти великому программисту — для повышения читаемости программ. Мы рассмотрим только ключевые команды из этого перечня.

С командами безусловного перехода rjmp и jmp мы уже познакомились достаточно подробно, так что сразу перейдем к вызову процедур rcall и call (официальный язык фирменных руководств Atmel предпочитает вместо процедуры упоминавшийся нами консервативный термин — подпрограмма, subroutine). Синтаксис у них точно такой же, как у команд безусловного перехода, и, по сути, это тот же самый переход по метке. И разница между этими двумя командами аналогичная: call работает в пределах 64 К адресов памяти (или до 8 М в соответствующем контроллере, поддерживающем такой объем адресного пространства), занимает 4 байта и выполняется за 4 цикла, a rcall годится только для МК с объемом памяти не более 8 кбайт, но занимает 2 байта и выполняется за 3 цикла. Мы в дальнейшем будем пользоваться только командой rcall.

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

Заметим, что стек — одно из самых употребительных понятий в программировании. Наличие программного стека позволяет, например, организовать привычное для языков высокого уровня разделение переменных на локальные и глобальные. Во всех последующих программах в этой книге мы будем пользоваться только глобальными переменными, и при нехватке регистров общего назначения для них мы будем использовать ячейки SRAM. (Привычку употреблять преимущественно глобальные переменные автор даже перенес в свой стиль программирования на Delphi, как вы увидите из главы 18.) Однако в больших проектах локальные переменные зачастую бывает просто необходимы (например, когда одна и та же процедура вызывается в разных местах с исходными данными, хранящимися в различных регистрах). Механизм организации локальных переменных с помощью стека приведен в листинге 13.2 (он в точности такой же, как это происходит при компиляции в «настоящих» языках программирования).

Листинг 13.2:

push var_1   ;переменная var_1 в программе помещается в стек

push var_2   ;переменная var_2 в программе помещается в стек

rcall procedure   ;вызывается процедура

pop var_2   ;после нее результат извлекается из стека

pop var_1   ;второй результат извлекается из стека

procedure:   ;в процедуре извлекается локальная

pop var_loc2   ;переменная var_loc2 со значением var_2

pop var_loc1  ;и переменная var_loc1 со значением var_1

…  ;расчеты, расчеты…

push var_loc1   ;результат — в стек

push var_loc2   ;результат — в стек

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

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

Аналогично происходит обработка прерываний, только специальной команды, как вы знаете, там нет, вызов производится обычным переходом rjmp или jmp, но поскольку он осуществляется с определенного адреса (там, где стоит вектор прерывания), то контроллер делает то же самое: сохраняет в стеке адрес командного счетчика, на котором выполнение основной программы было грубо нарушено, начинает выполнять прерывание и ожидает команды возврата, здесь она записывается как reti (return interrupt).

Еще одна важнейшая группа команд ветвления программы — команды перехода по состоянию отдельного бита в указанном регистре (sbrs, sbic и т. п.). Они очень удобны для организации процедур, аналогичных оператору выбора CASE в языках высокого уровня, но, к сожалению, обладают непривычной логикой: «пропустить следующую команду, если условие выполняется» и для новичка могут показаться слишком заумными. В качестве примера приведу довольно сложную по логике работы, но характерную для микроконтроллерной техники процедуру, в которой задача формулируется так: при наступлении некоторого условия мигать попеременно зеленым и красным светодиодами (СД).

Предположим, что условие мигания задается состоянием бита 3 в некоем рабочем регистре, который назовем регистром флагов — Flag (не путать с «официальным» регистром флагов SREG, о котором далее). Если бит 3 регистра Flag равен единице (установлен), надо мигать, если нет (сброшен) — оба СД погашены.

Красный СД подсоединен к выходу порта В номер 5, а зеленый — к выходу порта С номер 7 (разумеется, это могут быть любые другие выводы других портов).

Текущее состояние СД задается битом 4 в том же регистре флагов Flag.

Алгоритм работы такой программы на языке Pascal (листинг 13.3) описывается типичным вложенным оператором выбора.

Листинг 13.3

case <бит 3 per. Flag> of

   0: <погасить оба СД>

   1: case <бит 4 per. Flag> of

       1: <устанавливаем Port С, вых. 7> ; //горим зеленым <сбрасываем

Port В, вых. 5>; //гасим красный

            <сбрасываем бит 4 Flag>; { следующий раз горим красным }

        0: устанавливаем Port В, вых. 5>; //горим красным

             <сбрасываем Port С, вых. 7>; //гасим зеленый

             <устанавливаем бит 4 Flag>; // следующий раз горим зеленым

        end;

end;

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

Листинг 13.4

         sbrs    Flag,3   ;если флаг 3 стоит, будем мигать

         rjmp   dark   ;иначе будем гасить

         sbrs    Flag,4   ;если флаг 4 стоит, будем гореть зеленым

         rjmp   set4   ;иначе красным

        cbr     Flag, 0Ь00010000   ;следующий раз горим красным

         cbi     PortB,5   ;гасим зеленый

        sbi     PortC,7   ;горим зеленым

         rjmp continue   ;все готово

set4:           ;если флаг 4 не стоит, будем гореть красным

        sbr     Flag, 0Ь00010000   ;следующий раз горим зеленым

        cbi     PortC,7   ;гасим красный

        sbi      PortB,5   ;горим красным

         rjmp   continue   ;все готово

dark:

        cbi     PortC,7   ;гасим оба

         cbi     PortB,5

continue:

С используемыми здесь командами установки и сброса отдельных бит (sbi, sbr и т. п.) мы плотнее познакомимся чуть позже, а сейчас задержимся на ключевой команде всего алгоритма — sbrs, что расшифровывается, как Skip if Bit in Register is Set («пропустить, если бит в регистре установлен»). Имеется в виду, что по состоянию бита (если он установлен в единицу) пропустить нужно следующую команду. В качестве последней обычно также выступает одна из команд ветвления, как здесь, но далеко не всегда. Удобно, например, организовывать выход из прерывания или подпрограммы по какому-то условию, если поставить следующей после sbrs команду reti или, соответственно, ret (примеры мы еще встретим).

Противоположная по логике процедура записывается, как sbrc (Skip if Bit in Register is Cleared — «пропустить, если бит в регистре очищен», т. е. равен нулю). Наконец, есть еще пара аналогичных команд — sbis и sbic, которые применяются, когда нужно отследить состояние бита в регистре ввода/вывода (I/O), а не в регистре общего назначения. Все эти команды мы будем активно применять в дальнейшем.

Наконец, в самой обширной группе команд ветвления имена начинаются с букв Ьг (от слова branch — «ветка»). Это команды условного перехода, которые считаются одними из самых главных в любой системе программирования, т. к. позволяют организовывать циклы — базовое понятие, программистских наук. По смыслу все эти команды сводятся к банальному if… then («если… то»). Мы будем пользоваться лишь частью этих команд, потому что они во многом взаимозаменяемы, и здесь подробно разберем только одну пару команд. Смысл остальных вам будет понятен по ходу дела.

Это наиболее часто употребляемая пара brne (Branch if Not Equal, «перейти, если не равно») и breq (Branch if Equal, «перейти, если равно»). Уже из смысла этих команд понятно, как они пишутся: после команды следует метка, на которую нужно перейти. Вопрос только такой: откуда здесь берется собственно условие? Для этого все команды ветвления обязательно употребляют в паре с одной из команд, устанавливающих флаг нуля Z в регистре флагов SREG. Обычно (и это наиболее наглядно) для этой цели служит команда ср (от compare— «сравнить»), которая сравнивает регистры, или cpi («сравнить с непосредственным значением»). Например, вот так будет вы глядеть простейший цикл, в котором переменная temp последовательно принимает значения от 1 до 10:

      clr temp   ;обнулить temp

back_loop:

     inc temp   ;увеличиваем temp на 1

     <что-то делаем, необязательно с помощью temp >

     cpi temp,10

      brne back_loop

Обратите внимание: если надо, чтобы temp начинала с нулевого значения, то фрагмент «что-то делаем» следует вставить до команды inc, но тогда последним рабочим значением temp в цикле будет 9, а не 10, а после выхода из процедуры — все равно 10.

Другой нюанс заключается в том, что удобная команда сравнения с непосредственным числом cpi работает только для регистров с номерами от 16 до 31 (как и многие другие команды работы с непосредственными значениями, например, ldi). По этой причине рабочие переменные (temp, счетчики) всегда желательно выбирать из этой половины регистрового файла (в примерах в этой книге, как и в «аппнотах», кстати, обычно temp — это r16, хотя и не всегда). Положение осложняется тем, что регистры из старшей половины наиболее дефицитны: последние шесть из них объединены в пары для работы с памятью (см. далее) и некоторых других операций, r24 и r25 задействованы в команде adiw и т. п. Если переменных не хватает, то регистр из первой половины регистрового файла (допустим, это r15) в аналогичном цикле приходится использовать с парой команд:

ldi   temp,10

ср  r15,temp

Иногда проще построить декрементный цикл, в котором переменная уменьшается от заданного значения до нуля:

     ldi temp,10   ;загружаем 10 в temp

back_loop:

     dec temp  ;уменьшаем temp на 1

     <что-то делаем с помощью temp >

      brne back_loop

Как видите, здесь вообще ничего сравнивать не требуется, потому что команда dec при достижении нуля сама установит флаг Z (то же относится к команде tst temp, которая эквивалентна команде cpi temp, 0). И если даже выбрать регистр из первой половины, то лишняя команда понадобится не в каждом цикле, а только один раз для загрузки предварительного значения:

ldi temp,10   ;загружаем 10 в temp

mov r15,temp   ;загружаем 10 в r15 и далее его используем

;в команде dec

Один важный нюанс в работе всех команд перехода заключается в том, что они могут занимать непредсказуемое количество циклов (1 или 2), в зависимости от того, выполняется условие или нет. В AVR реализован конвейер команд, который «тупо» полагает, что следующей будет выполняться команда сразу после команды перехода. Естественно, если ветвление необходимо (в большинстве случаев), конвейер останавливается на один лишний такт, в течение которого происходит выборка адреса перехода. Однако этот недостаток с лихвой компенсируется тем, что за счет конвейера почти все остальные команды выполняются за один такт— недостижимая мечта для многих других типов контроллеров (например, в популярном до сих пор семействе 8051, выпущенном еще в начале 80-х, команда выполняется как минимум за 12 тактов, хотя некоторые современные клоны этого семейства могут работать и быстрее).

Арифметика и логика в интерпретации AVR

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

Не вызывают никаких возражений только очевидные операции: add R1,R2 (сложить два регистра, записать результат в первый) и sub ri,r2 (вычесть второй из первого, записать результат в первый). Но если вдуматься, то вопросов возникает множество: а что будет, если сумма превышает 255? Или разность меньше нуля? Куда девать остатки? Оказывается, все продумано: для учета переноса есть специальные команды adc и sbc соответственно. Корректная операция сложения двух 16-разрядных чисел будет занимать две команды:

add RL1,RL2

adc RH1,RH2

Здесь RL1 и RL2 содержат младшие (low) байты слагаемых, a RH1 и RH2 — старшие (high). Если при первой операции результат превысит 255, то перенос запишется в специальный флаг переноса (в регистре флагов sreg обозначается буквой С) и учтется при второй операции. Аналогично выглядит операция вычитания.

Постойте, но мы же вовсе не хотели складывать 16-разрядные числа! Мы хотели всего лишь сделать так, чтобы в результате сложения 8-разрядных чисел получился правильный результат, пусть он займет два байта. На что нам тогда старший байт второго слагаемого, если его вообще в природе не существует? Конечно, можно сделать его фиктивным, загрузив в некий регистр нулевое значение, но это только кажется, что регистров у AVR много (аж 32 штуки), на самом деле они довольно быстро расходуются под переменные и разные другие надобности, и занимать целый регистр под фиктивную операцию, пусть даже на один раз, как-то некрасиво. Потому «экономная» операция сложения 8-разрядных чисел будет выглядеть таким образом:

add RL1,R2

brcc add_8

inc RH1

add_8:

Исходные слагаемые находятся в R1 и R2, а результат будет в RH1:RH2. Отметим, что в старшем разряде (RH1) в результате может оказаться только либо 0, либо 1, т. к. сумма двух восьмиразрядных чисел не может превысить число 510 (255 + 255), именно потому флаг переноса С представляет собой один-единственный бит в регистре флагов. В этой процедуре команда brcc представляет собой операцию условного перехода на метку add_8 по условию, что флаг переноса равен нулю (BRanch if Carry Cleared). Таким образом, если флаг не равен нулю, выполнится операция увеличения значения регистра RH1 на единицу inc RH1, в противном случае она будет пропущена.

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

Заметим, что во всех случаях процедура разрастается во времени: было две команды, стало четыре, причем тут имеется еще и неявное замедление, поскольку все представленные арифметические команды выполняются за один такт, а команда ветвления brcc может занимать такт (если С = 1), а может и два (если С = 0). Итого мы выиграли один регистр, а потеряли два или три такта. И так всегда: есть процедуры, оптимизированные по времени, есть — по количеству команд, а могут быть и по количеству занимаемых регистров. Идеала, как и везде в жизни, тут не добиться, приходится идти на компромисс.

Если вы посмотрите таблицу команд в Приложении 4, то можете обратить внимание, что из восьмибитовых арифметических операций с константой доступно только вычитание (SUBI, SUCI), напрашивающейся операции сложения с константой нет. Это не упущение автора при формировании выборочной таблицы, а действительная особенность системы команд AVR. Это можно обойти при желании вычитанием отрицательного числа, но на практике не очень-то требуется, потому что разработчики семейства AVR решили облегчить жизнь пользователям, добавив в перечень арифметических команд две очень удобные команды adiw и sbiw, которые, по сути, делают то же самое, что пары команд add/adc (sub/sbc), только за одну операцию, и притом с константой, а не с регистром. Единственный их недостаток— работают они только с определенными парами регистров: с четырьмя парами, начиная с r25-r24. Три старших пары (r26-27, r28-29 и r30-31) носят еще название X, Y и Z, и мы их будем «проходить» далее в этой главе, они задействованы в операциях обмена данными с SRAM. Но, к счастью, точно так же работает и пара r24-r25, которая более нигде не употребляется в объединенном качестве, и это очень удобно. Независимо от используемой пары, старшим считается регистр с большим номером, а операцию нужно проводить с младшим, при этом перенос учтется автоматически. Например, в результате выполнения последовательности команд

clr r25

ldi r24,100

adiw r24,200

в регистрах r25:r24 окажется записано число 300 (в r25 будет записано $01, что эквивалентно 256 для 16-разрядного числа, а в r24 окажется $2С = 44). Аналогично работает и процедура вычитания константы sbiw.

С арифметикой многоразрядных чисел мы познакомимся в главе 15, там же попробуем освоить умножение и деление. Заметим, что все эти операции корректно работают с числами со знаком (см. главу 7), но вы удивитесь, узнав, насколько редко такая задача возникает на практике. А с дробными числами (с «плавающей запятой») AVR работать напрямую не «умеют», и тут приходится хитрить, о чем мы подробнее поговорим в главе 15.

Кроме собственно арифметических, к этой группе команд еще иногда относят некоторые логические операции, например логический сдвиг, хотя чаще их помещают в группу инструкций для операций с битами. Самая простая такая операция — сдвиг всех разрядов регистра влево (lsl) или вправо (lsr) на одну позицию. Сдвиги приходится применять довольно часто, потому что, как вы знаете из главы 7, такая операция равносильна умножению (соответственно, делению) на 2. Для того чтобы разряды не терялись при сдвиге, предусмотрены разновидности этих операций: rol и rоr. Они учитывают все тот же флаг переноса С, и его можно перенести в другой регистр. Например, в результате выполнения последовательности команд

lsl  R1

rol R2

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

Здесь же имеет смысл рассмотреть инструкции установки отдельных битов. Команды, устанавливающие значения битов в регистрах общего назначения (sbr и cbr) иногда относят к группе арифметических операций, а команды, устанавливающие биты в регистрах ввода/вывода (sbi и cbi) — к группе битовых операций. Правда, в некоторых пособиях их относят к одной группе операций с битами, что, конечно, логичней. Но почему такой разнобой, если они, по сути, делают одно и то же?

Механизм работы этих команд существенно различается. Очевиднее всего устроены sbi и cbi: так, уже знакомая нам команда sbi PortB,5 установит в единичное состояние разряд номер 5 порта В. Если этот разряд порта сконфигурирован на выход, то единица появится непосредственно на соответствующем выводе микросхемы (например, для микросхемы ATmega8 это вывод 19, для 8515 или 8535 любого семейства это вывод 6 и т. п.), если на вход, то эта операция управляет подключением «подтягивающего» резистора.

Гораздо сложнее устроены команды установки бит в регистрах общего назначения. Взгляните на фрагмент ранее приведенного кода, где вы встретите команду sbr Flag, 0b00010000, устанавливающую бит номер 4 в единицу. Почему так сложно, да еще и в двоичной системе? Дело в том, что команды эти на самом деле не устанавливают никаких битов, а просто осуществляют некую логическую операцию между значением регистра и заданным байтом, который в этом случае называют битовой маской. Указанная команда расшифровывается как Flag OR ob00010000. Если вы вернетесь к главе 7, то сообразите, что при такой операции четвертый бит (нумерация начинается с нуля) установится в единицу, какое бы значение он ни имел ранее, а все остальные останутся в старом состоянии. Понятно также, почему я предпочел использовать двоичное представление маски: так легче отсчитывать биты. Так, разумеется, можно за один раз установить хоть все биты в регистре — в этом отличие команды sbr от sbi, которая устанавливает только по одному биту.

Аналогично работает команда сброса бита cbr Flag, 0b000010000. Только для того, чтобы ее можно было записывать в точности в том же виде, что и sbr, логическая операция, которую она осуществляет, сложнее: Flag AND (NOT 0b000100000). Здесь сначала в маске инвертируются все биты (реально это производится вычитанием ее из числа $FF, т. е. нахождением дополнения до 1), а затем полученное число и значение регистра комбинируются операцией логического умножения (а не сложения, как в предыдущем случае). В результате четвертый бит обнулится обязательно, а все остальные останутся в неприкосновенности. Кстати, обнулять все биты некоего регистра R удобнее и нагляднее не командой cbr R, $FF, а с помощью инструкций clr R или ldi R,0.

Призываю вас об этой разнице между регистрами общего назначения и регистрами ввода/вывода не забывать. Я и сам до сих пор «попадаюсь» на том, что в случае необходимости обнуления бита 1 в рабочем регистре temp записываю cbr temp, 1 (аналогично верной команде cbi portB,1), хотя такая операция обнулит не первый, а нулевой бит в temp. А операция cbr R, 0 (как и операция sbr R, 0) вообще ничего не делает, и такая запись бессмысленна.

Команды переноса данных

Последнее, что мы рассмотрим в этом разделе — команды, которые употребляются для переноса данных из одной области памяти в другую (здесь память рассматривается в широком смысле этого слова, и в нее включаются также и регистры). Некоторые команды из этой группы нам уже знакомы — это ldi и mov. Первая загружает в регистр непосредственное число (но действует только для регистров, начиная с r16), а вторая — значение другого регистра. Отметим еще, что для ldi очень часто применяется форма записи, которую можно пояснить следующим практическим примером:

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

Смысл этого выражения в том, что мы устанавливаем в единицы для регистра temp только биты с указанными именами (естественно, последние должны быть где-то определены, в данном случае это сделано в inc-файлах). Обратите внимание, что в отличие от команды sbi, остальные биты будут одновременно обнулены, а не останутся при своих значениях. Такая форма очень удобна для задания поименованных битов в процессе инициализации служебных регистров (при использовании «законной» команды sbi нужные биты пришлось бы указывать в числах, и предварительно обнулять регистр). В данном случае мы хотим инициализировать последовательный порт UART, и приведенная форма записи означает, что устанавливается разрешение приема (RXEN) и передачи (TXEN), причем и для того и для другого устанавливается 8-битный режим (RXB8 и TXB8). Подробнее мы в этом разберемся в главе 16, когда будем «проходить» программирование UART, а пока заметим, что еще. не все доделали: установили биты в регистре temp, но он-то какое отношение имеет к последовательному порту?

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

out UCR,temp

Замечу, что указанные команды по отношению к UART справедливы для семейства Classic, для Mega это делается несколько сложнее (a Tuny, кроме модели 2313, вообще UART не имеют), но сейчас это неважно. В паре к команде out идет симметричная команда in (чтения данных из I/О-регистра).

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

Здесь уместно обратить внимание читателей на то, что в момент подобного рода установок надо тщательно следить за тем, чтобы значение рабочего регистра temp не могло измениться между командами его установки и переносом значения в I/O-регистр. А как это может случиться, спросите вы? Да запросто, — если разрешены прерывания, в обработчиках которых наверняка также будет присутствовать temp , то они могут «вклиниться» между командами. Пусть вероятность такого события крайне мала, и оно может не происходить годами, но программист обязан об этом помнить, и грамотно составленная программа во время таких установок прерывания запрещает. В секции RESET это обеспечивается тем, что по умолчанию прерывания запрещены, а команда на их общее разрешение ( sei ) ставится после всех установок. Куда меньше проблем вызывают сами обработчики, т. к. по умолчанию во время обработки прерывания другое, произошедшее позднее, ожидает своей очереди (автоматически прерывания разрешатся только на выходе из текущего обработчика по команде reti ). Но если вам зачем-то нужно разрешить критичным прерываниям «вклиниваться» во время обработки некритичных (для этого следует явно разрешить прерывания внутри обработчика данного прерывания командой sei ), то такая проблема снова возникает. Именно поэтому, в частности, не рекомендуется употреблять синонимы для имен рабочих регистров: слишком легко забыть, что temp и counter , к примеру, указывают на один регистр, и в разных процедурах, вложенных одна в другую, они будут произвольно менять свое значение. Если же все-таки запретить прерывания нельзя или не хочется (например, во время выполнения длинного цикла), то уберечься от ошибок можно, если в начале обработчика прерывания сохранять критичные регистры в стеке командой push , а в конце — извлекать их командой pop . Но тут также возникает немало возможностей наделать трудновылавливаемые ошибки в программе, поскольку отследить содержимое стека тоже не всегда просто.

Следующими по важности командами переноса данных будут команды загрузки и чтения SRAM— id и st. С этими командами связаны такие «жуткие» понятия, как «прямая» и «косвенная» адресации, а также «относительная косвенная адресация» и прочая подобная «лабуда» из арсенала разработчиков микросхем и теоретиков программирования. Эти понятия на практике абсолютно не требуются, и только затуманивают мозги, потому что зачастую относятся к совершенно разным командам и узлам кристалла (это единственный раздел в описаниях МК, который спокойно можно пропускать при чтении, все остальные изучать очень полезно). Мы постараемся от термина «адресация» вообще отказаться и разберем здесь три основных режима чтения/записи SRAM: простой, а также с преддекрементом и постинкрементом. Все три употребляются очень часто, хотя два последних режима работают не во всех типах AVR.

Во всех случаях в чтении и записи SRAM используются регистры X, Y и Z — т. е. пары r27:r26, r29:r28 и r31:r30 соответственно, которые по отдельности еще именуют ХН, XL, YH, YL, ZH, ZL в том же порядке (т. е. старшим в каждой паре служит регистр с большим номером). Если обмен данными производится между памятью и другим регистром общего назначения, то достаточно только одной из этих пар (любой), если же между областями памяти — целесообразно задействовать две. Независимо от того, какую из пар мы используем, чтение и запись происходят по идентичным схемам, меняются только имена регистров.

Покажем основной порядок действий при чтении из памяти для регистра Z (r31:r30). Чтение одной ячейки с заданным адресом Address, коррекция ее значения и последующая запись выполняются так:

ldi ZH,High(Address)   ;старший байт адреса RAM

ldi ZL,Low(Address)   ;младший байт адреса RAM

ld temp,Z   ;читаем ячейку в temp

inc temp   ;например, увеличиваем значение на 1

st Z,temp   ;и снова записываем

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

При всех подобных манипуляциях нужно внимательно следить за двумя вещами: во-первых, за тем, чтобы не залезть в несуществующую область памяти (если объем SRAM составляет 512 байт, как в большинстве моделей, которые мы будем использовать, то ZH в данном примере может иметь значения только 0 или 1). Во-вторых, не забыть, что младшие адреса SRAM заняты регистрами (в большинстве моделей под это зарезервированы первые 96 адресов от $00 до $5F). И запись, например, по адресу $0000 (ZH=0, ZL=0) равносильна записи в регистр r0 . Во избежание коллизий я по возможности поступаю следующим образом: резервирую пару ZH, ZL только под запись/чтение в память, и устанавливаю с самого начала регистр ZH в единицу. Тогда при любом значении ZL мы будем «шарить» только в старших 256 байтах из 512, чего для любых практических нужд обычно достаточно (а если недостаточно, то, скорее всего, все равно придется задействовать внешнюю память), а случайно пересечься с регистрами нет никакой возможности. Обязательно нужно также помнить, что и последние адреса памяти также нельзя занимать: мы сами дали в начале программы команду задействовать их под стек.

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

st -Z,temp   ;с преддекрементом, запись в ячейку с адресом Z-1, после выполнения команды Z = Z-1

st Z+,temp   ;с постинкрементом, запись в ячейку с адресом Z, после выполнения команды Z = Z+1

Аналогично выглядят команды чтения:

ld temp, -Z   ;с преддекрементом, чтение из ячейки с адресом Z-1, после выполнения команды Z = Z-1

ld temp,Z+   ;с постинкрементом, чтение из ячейки с адресом Z, после выполнения команды Z = Z+1

Вот как можно в цикле записать 16 ячеек памяти подряд одним и тем же значением из temp, начиная с нулевого адреса старших 256 байтов памяти:

            ldi ZH,1

           clr ZL

LoopW:

            st Z+,temp   ;сложили в память

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

           brne LoopW

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

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

N_mask: ;маски цифр на семисегментном индикаторе

.db 0b000111111, 0b0000001110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111

Заметим, что таким образом можно формировать hex-файлы для предварительной записи констант в EEPROM, хотя мы будем пользоваться иным, более корректным методом (см. главу 15). Теперь можно в нужном месте использовать команду lpm следующим хитрым образом:

ldi ZH,High(N_mask*2)   ;загружаем адрес начала маски

ldi ZL,Low(N_mask*2)

add ZL,5   ;адрес маски цифры «5»

lpm  ;маска окажется в регистре r0

Надо сказать, что современные МК семейства Mega поддерживают и более простой формат команды lpm, аналогичный обычной id, но универсальности ради я привык к традиционному формату, который поддерживают все МК AVR. Здесь в регистр Z (и только Z!) заносится адрес начала массива констант, причем учитывается, что память программ имеет двухбайтную организацию, а в Z надо заносить побайтные адреса, отчего появляется множитель 2. По этому адресу, как мы договорились, располагается маска цифры «0». Для загрузки цифры «5» прибавляем к адресу это значение и вызываем команду lpm. Полученное значение маски окажется в самом первом регистре общего назначения го, так что при таких действиях его не следует занимать под какие-то переменные.

О Fuse-битах

Это «несчастье» свалилось на нашу голову с появлением семейств Tuny и Mega и привело к многочисленным проклятиям на голову фирмы Atmel со стороны армии любителей, которые стали один за другим «запарывать» кристаллы при программировании. Теперь уже все привыкли и обзавелись соответствующим софтом, а сначала было довольно трудно. Положение усугублялось тем, что в описании этих сущностей действовала извращенная логика: как мы знаем, любая чистая EEPROM (по принципу ее устройства) содержит единицы, и слово «запрограммированный» по отношению к такой ячейке означает, что в нее записали нули. Поэтому разработчики программатора AS-2 даже специально написали в окне программирования конфигурационных ячеек (такое название более правильное, чем fuse-бит, буквально означающее «предохранительный бит») памятку на этот счет (рис. 13.7).

Рис. 13.7. Окно типового состояния конфигурационных ячеек в нормальном режиме работы ATmega8535

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

Переписывать фирменные руководства я не буду, остановлюсь только на самых необходимых моментах. По умолчанию любая микросхема семейств Mega или Tuny запрограммирована на работу от внутренней RC-цепочки, за что разработчикам большое спасибо, иначе было бы невозможным первичное программирование по SPI, а только через параллельный программатор.

Для работы с обычным кварцем, присоединенным по типовой схеме, требуется установить все ячейки CKSEL0—3 в единицы, что согласно логике контроллера означает незапрограммированное их состояние. Это и ведет к критической ошибке. Решив при поверхностном чтении очень невнятно написанного, и к тому же по-английски, руководства, что установка всех единиц означает запрограммировать все ячейки, пользователь смело устанавливает их на самом деле в нули, отчего микросхема переходит в состояние работы от внешнего генератора и разбудить ее через SPI-интерфейс уже невозможно. Легче всего в этом случае переустановить fuse-биты с помощью параллельного программатора, либо за неимением такового, попробовать-таки подключить внешний генератор (его можно собрать по одной из схем из главы 9), как описано в руководстве.

Это самая крупная ошибка, которую можно допустить, но есть и помельче, правда, легче исправляемые. Ячейка SPIEN разрешает/запрещает последовательное программирование по SPI и должна оставаться в нулевом состоянии.

Ячейка S8535C (в других моделях она, соответственно, будет иметь другое название, или вовсе отсутствовать) очень важна, она определяет режим совместимости с семейством Classic (в данном случае с AT90S8535). Если ее установить в нулевое состояние, то МК семейства Mega (а также и единственный представитель Tuny — модель 2313) перейдет в режим совместимости, про все «навороты» можно забыть (кроме, конечно, самих конфигурационных ячеек), и без изменений использовать наработанные старые программы. В режиме совместимости следует учесть, что состояния МК нельзя перемешивать: если fuse-бит совместимости запрограммирован, то программа компилируется полностью, как для семейства Classic (в том числе с помощью соответствующего inc-файла), иначе она может не заработать. Например, AT90S8535 имеет 17 прерываний, a ATmega8535 — 21, и те же самые прерывания могут оказаться на других местах.

Еще одна важная ячейка — EESAVE, которая на рис. 13.7 установлена в единицу (режим по умолчанию), но ее целесообразно перевести в нулевое состояние, тогда при программировании памяти программ не будет стираться содержимое EEPROM. Ячейки SUT определяют длительность задержки сброса, и в большинстве случаев принципиального значения их состояние не имеет.

Наконец, для нас будет иметь значение состояние ячеек BODEN и BODLEVEL. Первая, будучи установлена в ноль, разрешает работу т. н. схемы BOD (Brown-out Detection), которая сбрасывает контроллер при снижении питания ниже допустимого порога. Ячейка BODLEVEL и определяет этот самый порог: при установленной в ноль ячейке он равен 4 В, при единице — 2,7 В. При питании 5 В надо выбирать первое значение, при 3,3 В — второе. Это предохраняет контроллер от «недопустимых операций» при выключении питания, но для обеспечения полной сохранности содержимого EEPROM таких мер оказывается недостаточно и приходится принимать дополнительные.

Ячейки, название которых начинается с BOOT, определяют режим начальной загрузки. Как я уже упоминал, в современных AVR можно изменять начальный адрес программы и расположение векторов прерываний. Эти ячейки, как и все остальные, следует оставить в исходном состоянии. В том числе это касается и битов защиты программы, которые на практике никакой защиты не дают, т. к. при необходимости легко обходятся. Зато неприятностей могут доставить массу, поскольку раз запрограммировав их, исправить что-то уже будет очень трудно, а для любителя почти невозможно.

 

Глава 14

Проба пера: настольные часы

Сконструировать свои первые настольные часы «для дома, для семьи» меня заставила судьба. ЖК-индикаторы в работающих от сети стационарных конструкциях я полагаю неуместными — они «слепые» даже днем, а надо, чтобы часы было видно и ночью. Между тем, пытаясь в конце 1990-х приобрести настольные часы в связи с переездом на новую квартиру, я попал в какой-то неудачный момент, когда во всей Москве не было часов со светящимися индикаторами: старинные советские изделия, в которых малюсенькие голубенькие циферки были еле видны за густой сеткой анода, уже исчезли из продажи, а импортные на светодиодах, как говорится, «не завезли». В результате пришлось делать самому такие, какие нравится.

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

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

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

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

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

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

По этим причинам в более серьезных устройствах (например, когда в дальнейшем мы попробуем объединить часы с различными датчиками) «велосипедов» лучше не изобретать, а выбрать одну из широко распространенных неспециализированных микросхем часов, называемых еще RTC (Real Time Clock), которые включают календарь и функции будильника (а иногда и нескольких), таймера, могут выдавать во внешний мир определенную частоту, потребляют очень мало (типичная величина — 0,8 мкА), иногда обладают встроенным прямо в чип часовым кварцем (и даже с возможностью подстройки). Еще один плюс такой конструкции— часовые кварцы 32 768 Гц, как правило, точнее обычных, тех, что служат для тактирования МК. Выпускаются RTC с самыми разнообразными интерфейсами: от параллельного до I 2 С. Именно такие микросхемы применяются, например, в компьютерах. Особенно преуспели в этом деле две фирмы: Dallas (он же MAXIM) и бывшая Seiko (ныне Epson). Далее мы разберем конструкции на основе таких микросхем.

Ну а теперь перейдем непосредственно к конструкции простейших часов, и начнем с выбора подходящего для этой цели МК.

Выбор микроконтроллера и общее построение схемы

Для выбора МК из предлагаемых фирмой Atmel просто подсчитаем, сколько нам требуется выводов. Во-первых, надо управлять четырьмя разрядами индикации (ЧЧ: ММ). Это мы будем делать в режиме динамической индикации, когда в каждый отдельный момент времени напряжение питания подается только на один разряд индикаторов. В это же время на сегменты, которые все соединены между собой параллельно, подается код, соответствующий именно этому разряду. В следующем такте код меняется, а напряжение подается на следующий разряд, и так далее. При четырех разрядах непосредственное управление предполагает 7 х 4 = 28 задействованных выводов, а динамическое— всего 7 + 4 = 11. Чтобы мигание было незаметно для глаза, полный цикл смены разрядов должен повторяться с частотой не менее 70—100 Гц.

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

По тем временам я остановился на МК AT90S2313 — он выпускается в 20-выводном корпусе (см. рис. 12.1 вверху), в котором минимум 5 выводов должно быть занято под системные нужды (два питания, Reset и два вывода для подключения кварца). Итого нам остается на все про все 15 выводов, что нас устраивает. Мы даже вроде бы получаем один резервный вывод, но далее увидим, что на самом деле под все желательные дополнительные функции выводов нам будет не хватать и придется изворачиваться (сейчас я бы, скорее всего, остановился на ATmega8, у которого 28 выводов корпуса, чтобы не экономить, но для изучения особенностей AVR дефицит даже полезнее). Естественно, если вы захотите повторить схему, и не достанете 2313 Classic, то придется заменить его на ATtuny2313, соответствующим образом установив fuse-бит совместимости (см. главу 13). Так как корпус у него тот же самый, то конструкция ничем отличаться не будет.

Теперь общая схема. Выбираем индикаторы большого размера (высота цифр — 1" или 25,4 мм), с общим анодом, т. е. типа SA10, если брать продукцию Kingbright. Лично я предпочитаю желтого свечения (например, SC10-21Y), но это не имеет значения. Так как падение напряжения у них может достигать 4 В, то от того же источника, что требует МК (5 В), питать их нельзя.

Следовательно нам потребуется два напряжения питания: стабилизированное +5 В и нестабилизированное (пусть будет +12 В). Управлять разрядами индикаторов мы будем от транзисторных ключей с преобразованием уровня (когда на выходе МК уровень +5 В, ключ подает +12 В на анод индикатора), а сегменты от простых транзисторных ключей — при уровне +5 В вывод сегмента коммутируется на «землю» (так как питание индикаторов повышенное, то, к сожалению, управлять прямо от выводов процессора не получится). В обоих случаях управление получается в положительной логике: включенному индикатору и сегменту соответствует логическая единица (что совершенно не принципиально, но удобно для простоты понимания работы схемы). Резисторы в управлении сегментами примем равными 470 Ом, тогда пиковый ток через сегмент составит примерно 20 мА, а средний — 5 мА (при динамическом управлении 4-мя разрядами). Всех «восьмерок» у нас быть не может, максимальное число одновременно горящих разрядов равно 24 («20:08»), потому общее максимальное потребление схемы составит 24 х 5 = 120 мА, плюс -10 мА схема управления, итого 130 мА.

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

Режим энергосбережения с глубоким «засыпанием» МК не подходит, поскольку тогда все «замирает» и его применение обессмысливается, ведь нам нужно, чтобы часы не просто сохраняли значение времени, а продолжали идти и при отключении от сети. При питании в пределах 4–5 В МК типа 2313 потребляет около 5 мА, так что можно рассчитывать на непрерывную работу от щелочной («алкалайновой») батарейки типа АА с емкостью порядка 2 Ач в течение не менее 2–3 недель. Для обеспечения работы понадобятся три таких элемента, соединенных последовательно, тогда их общее напряжение составит 4,5 В.

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

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

Для обеспечения такого режима нам понадобится монитор питания — схема, которая отслеживает наличие входного напряжения, и переключатель с сетевого питания на батарейки. Чтобы сделать схему совсем «юзабельной», добавим также небольшой узел для сигнализации о необходимости замены резервной батарейки — пусть это будет наше ноу-хау, т. к. в подобных сетевых приборах такого почти ни у кого нет. Хотя есть специальные микросхемы, которые «мониторят» питание, и мы будем их в дальнейшем использовать, здесь в целях максимального упрощения схемы мы без них обойдемся. Схему такого узла удобно реализовать, «не отходя от процессора», на встроенном компараторе. Но тогда нужно задействовать аж 18 выводов (12 под индикацию, 2 кнопки, 2 входа компаратора, 1 для его выхода и еще 1 для монитора питания), а ставить процессор большего размера только для этой цели не хочется. И еще больше не хочется добавлять какие-то внешние схемы — все только потому, что мы захотели контролировать батарейку, которая, может быть, сядет этак лет через пять?

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

Теперь поглядим на схему разводки выводов AT90S2313 (рис. 14.1) и выберем, что и к чему мы будем коммутировать.

Рис. 14.1. Разводка выводов МК AT90S2313 (функции показаны применительно к нашей задаче)

Ко входу внешнего прерывания INT1 (7) удобно подключить кнопку, которая будет вводить часы в режим установки. От порта D (портов А и С в этом микроконтроллере нет) осталось шесть разрядов, четыре из которых мы задействуем под управление разрядами индикаторов: PD0 (2), PD1 (3), PD2 (6) и PD4 (8). Из восьми выводов порта В два заняты под входы компаратора AIN+ (выв. 12 — к нему мы подсоединим опорный источник для контроля батареи и также с него будем снимать информацию о состоянии питающего напряжения и второй кнопки) и AIN- (выв. 13 — к нему подключим батарейку). Для управления миганием разделительного двоеточия удобно использовать вывод ОС1 (15), который управляется автоматически от таймера (см. главу 12). Под управление сегментами мы задействуем оставшиеся выводы: PD5 (9), PD6 (11), РВ2 (14) и РВ4—РВ7 (16–19). То, что выводы для управления индикаторами расположены не по порядку — это, конечно, не здорово, нам фактически придется управлять каждым разрядом по отдельности, но обойдемся.

Схема

Вот, собственно, и все предварительные наметки, можно рисовать схему платы управления (рис. 14.2). Схема проста, правда, некоторую громоздкость ей придают ключи управления индикаторами, однако все равно ее можно без труда уместить на плату примерно 70x100 мм, а при некоторых усилиях — и на меньшую.

Рис. 14.2. Схема часов на МК AT90S2313 (плата управления)

Игольчатый разъем XI типа IDС с 10 контактами— программирующий (в главе 13 мы договаривались, что я буду разводить его в соответствии с 10-контактным ISP-программатором, там же подобный разъем описан подробнее, см. рис. 13.4). Все остальные внешние соединения, кроме питания, — через такой же разъем, но с 16 контактами, два из которых— «земля» и питание.

Подробности

Так как игольчатые разъемы типа IDС с шагом 2,54 мм встречаются в практике изготовления микроэлектронных устройств довольно часто, то стоит разобраться в их маркировке. Начнем с того, что наименование IDC в случае штыревых разъемов для установки на плату относится только к разъемам в кожухе с ключом (именно такие используются для подсоединения жесткого диска в ПК). Бескорпусные подобные разъемы носят название PLD для двухрядных (или PLS для однорядных) типов и более удобны в радиолюбительской практике, т. к. длинные разъемы легко «ломаются» в нужном месте, обеспечивая необходимое число выводов (правда, при этом приходится как-то обозначать на плате первый вывод, чтобы не перепутать ориентацию при включении, см. рис. 13.4). Разметка на плате для обоих типов разъемов (с кожухом и без) одинакова, т. к. все равно приходится учитывать место, которое займет кабельная розетка при ее подсоединении, и мы в этой книге для определенности остановимся на IDC-типе. Разумеется, розетка для установки на плоский кабель (с использованием соответствующего инструмента), может иметь только фиксированное число контактов (из ряда 6, 10, 14, 16, 20, 22, 24, 26, 30, 34, 36, 40, 44, 50, 60…), что нужно учитывать при проектировании.

Цифра после обозначения разъема (IDC-10 или PLD-10), естественно, обозначает число контактов разъема, а следующая буква символизирует его конфигурацию: М (male, «папа») для штыревой части, и F (female, «мама») — для гнездовой. Далее может следовать еще одна буква, которая обозначает ориентацию: S для прямых выводов (разъем перпендикулярен плате), R для повернутых под углом 90° (разъем параллелен плате). Таким образом, приведенное на схеме рис. 14.2 обозначение IDC-10MS означает штыревой («папа») разъем в кожухе с ключом, с 10 прямыми выводами. Соответствующая этому разъему кабельная часть обозначится, как IDC-10F. Бескорпусные PLD-разъемы бывают, естественно, только штыревые, потому для них буквы М и F не указываются (а повернутые под углом 90° дополняются буквой R).

Обратите внимание, что программирующие выводы (кроме Reset) здесь работают в двояком режиме. В нормальном режиме эти выводы работают как выходы на достаточно низкоомную (5,1 кОм) нагрузку. Не помешает ли это процессу программирования? Нет, не помешает— такая нагрузка для программатора вполне приемлема. Более того, «чистые» (нигде не задействованные) выводы программирования все равно следует нагружать «подтягивающими» резисторами, иначе не исключены сбои (об этом мы говорили в главе 12). Здесь же роль гасящей помехи нагрузки играют базовые резисторы ключей управления транзисторами, и дополнительные меры не требуются.

Плату индикации делаем отдельно (рис. 14.3). На ней мы располагаем четыре индикатора и две управляющих кнопки (о них далее), а также в точности такой же разъем IDC-16, как и на плате контроллера, причем он должен находиться на стороне платы, противоположной индикаторам. Разводка у него также должна быть идентичной. Эти разъемы мы соединим плоским кабелем.

Рис. 14.3. Схема часов на МК АТ90S2313 (плата индикации)

Изготовить такой плоский кабель с разъемами IDC-16F самостоятельно без специального инструмента практически невозможно, потому либо придется такой инструмент приобрести, либо попросить вам установить разъемы на кабель в любой фирме, которая занимается сборкой и ремонтом компьютеров. Можно употребить и готовый кабель даже с большим числом линий, если на плате установить разъемы PLD (т. е. при отсутствии кожуха). Это решение не очень красивое, т. к. при этом кабельная часть разъема будет выходить за пределы ответной на плате, и это нужно предусмотреть в разводке, иначе большой разъем может во что-нибудь упереться.

Разберем немного работу схемы. При включении питания цепочка R1C1 формирует надежный сигнал Reset. Напомню (см. главу 12), что ставить эту цепочку необязательно— производитель МК гарантирует нормальный Reset и без каких-либо внешних элементов, однако для лучшей защиты от помех это не повредит, ведь часы у нас должны работать по идее годами в круглосуточном режиме. После установления питания диод VD2 «запрет» батарею, которая имеет напряжение заведомо ниже, чем на выходе стабилизатора. Оба диода с переходом Шоттки, падение напряжения на них не превышает 0,2–0,4 В.

Теперь разберемся с нашими компараторными «примочками». В нормальном режиме кнопка Кн2 разомкнута и на работу схемы не влияет. Напряжение батареи фактически напрямую (делитель R4/R5 делит сигнал в отношении 300/301 и эта ошибка не имеет значения) попадает на инвертирующий вход компаратора. Это напряжение сравнивается с напряжением на стабилитроне VD3, равном примерно 3,9 В (стабилитрон обязательно должен быть маломощный, типа КС139Г в стеклянном корпусе, или соответствующий импортный, в другом случае сопротивление резистора R35 надо снизить примерно в два-три раза). Когда напряжение батареи упадет ниже этого уровня (выбранного с некоторым запасом, поскольку при 3 В МК еще может нормально работать, но часть напряжения батареи упадет на диоде VD2, кроме того, следует учитывать, смена батарейки может произойти не сразу), то компаратор перебросится в состояние логической единицы по выходу.

Программа (см. далее) это зарегистрирует и разделительная точка (пара светодиодов VD1 и VD2, рис. 14.3) перестанет мигать и будет гореть постоянно. Восстановление произойдет сразу, как только батарею сменят на свежую. Та же реакция будет, если просто отключить батарею тумблером «Бат» (S1 на рис. 14.2) или удалить ее. Для того, чтобы в этих случаях вход компаратора не оказывался «висящим в воздухе», и предназначен резистор R5. Ток через него настолько мал (около 1,5 мкА), что на разряд батареи это не оказывает влияния. С8 защищает вход от наведенных на этом резисторе помех.

При пропадании внешнего питания диод VD1 запирается, a VD2 открывается и напряжение батареи поступает на питание МК. Резистор R6 вместе с развязывающим конденсатором С2 предназначены для большей устойчивости работы МК в момент перепада напряжений при переключении питания, для той же цели служит конденсатор С7, установленный параллельно кнопке Кн1 (иначе при перепадах напряжения может спонтанно возникать прерывание, и часы войдут в режим установки, о котором см. далее). Одновременно с переключением питания становится равным нулю напряжение на стабилитроне, а т. к. при этом стабилитрон представляет собой обрыв в цепи, то установлен резистор R36, который служит тем же целям, что и R5. Компаратор работать перестает (точнее, он всегда будет показывать «нормальную» батарею), но нас это не волнует, т. к. индикации все равно нет. Тумблер «Бат» нужен для отключения батареи в случае, если вы хотите остановить часы надолго, а вот тумблер для включения сетевого питания тут совершенно не требуется (разве что на время отладки).

Программа

Полный текст программы часов приведен в Приложении 5 (раздел «Программа для часов», листинг П5.1). Все подробности даны в виде комментариев к тексту программы, здесь мы разберем только общее построение и принцип работы.

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

Timer 0 у нас будет по событию переполнения управлять разрядами в режиме динамической индикации. При заданной частоте на входе Timer 0, равной 1/8 от тактовой (4 МГц), частота управления разрядами получится равной 4 МГц/8 = 500 кГц/256 (емкость счетчика), т. е. чуть меньше 2 кГц, а сами разряды (4 шт.) будут изменяться с частотой почти 500 Гц, что однозначно превышает порог заметности мигания.

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

Заметим, что при проектировании питания подобных устройств следует учитывать еще одно обстоятельство: в динамическом режиме для питания индикаторов пульсирующее напряжение использовать нельзя (как в схеме со статической индикацией вроде термометра из главы 10 ), т. к. обязательно возникнут биения между частотами питающего напряжения и переключения разрядов, и яркость свечения будет пульсировать. Потому напряжение +12 В необязательно должно быть стабилизированным, но совершенно необходим сглаживающий фильтр (некоторая неизбежно возникающая — см. главу 4 — величина пульсаций, конечно, может присутствовать). На самом деле в данной конструкции это условие соблюдается автоматически, т. к. те же +12 В подаются и на вход стабилизатора +5 В, но в дальнейшем мы встретим конструкции, в которых питание индикаторов осуществляется от отдельной обмотки трансформатора, и там об этом забывать не следует.

16-разрядный Timer 1 у нас будет управлять собственно отсчетом времени по прерыванию сравнения. Для этого в регистры сравнения загружается число 62 500, а предварительный коэффициент деления задается равным 1/64, тогда прерывание таймера будет возникать с частотой 4 МГц /64/62500 = 1 Гц. На практике число для сравнения подгоняется под конкретный кварц, и обычно оказывается почему-то меньше теоретической величины 62 500 (так, в моем случае оно было равно 62 486).

Подробности

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

Таким образом вы получите величину ухода часов. Пусть, например, она составляет 200 с месяц в сторону отставания. Это значит, что у нас секундный интервал длиннее необходимого на 200/2592000 = 7,7 10-5 часть, т. е. на 77 мкс (число 2 592 000 есть число секунд за 30 дней, проверьте). Эту же величину мы можем получить и с помощью частотомера. Настолько следует повысить частоту «тиков» таймера, для чего нужно уменьшить наш коэффициент деления на величину 62500∙7,7∙10 -5 = 5, т. е. в регистры таймера необходимо записать число 62 495. Отметьте, что, несмотря на кажущуюся достаточно высокую величину коэффициента деления 62 500, изменение его всего на единицу изменит ход часов на целых 40 секунд в месяц, т. е. более чем на секунду в сутки — это является следствием использования 16-разрядных счетчиков-таймеров, и крупнейшим недостатком применения МК для отсчета времени. Далее мы увидим, что для более тонкой подстройки нам придется изощряться, придумывая всякие хитрости (см. главу 19 ).

Кроме этого, в процедуре инициализации разрешается прерывание от кнопки Кн1 (INT1). Для кнопки Кн2 (объединенной с одним из входов компаратора) отдельного прерывания не требуется, ее состояние отслеживается непосредственно в процессе установки (см. далее). По окончании установок разрешаются прерывания (команда sei), и далее программа переходит к выполнению бесконечного цикла, во время которого производится мониторинг состояния определенных узлов.

Основная логика работы часов следующая. Каждую секунду, когда происходит прерывание Timer 1, счетчик секунд sek увеличивается на единицу (см. процедуру обработки прерывания TIM1 по метке mtime). Если его значение не равно 60, то больше ничего не происходит, если равно, то регистр sek обнуляется, и далее по цепочке обновляются значения текущего времени, хранящиеся в регистрах emin, dmin, ehh и dhh (см. их определения в начале программы).

Прерывание по переполнению Timer 0 для управления разрядами происходит независимо от прерывания Timer 1 и использует установленные в последнем значения часов. По Timer 0 обнуляются все выходы всех портов, управляющие индикацией, затем проверяется значение счетчика POS, отсчитывающего последовательные номера разрядов (от 0 до 3). Чтобы не тратить время на всякие проверки и обнуления, для организации счетчика до четырех здесь учитывается тот факт, что число 4 совпадает с числом комбинаций первых двух бит. Тогда для последовательного непрерывного счета (0–1-2-3–0–1…) достаточно каждый раз увеличивать счетчик на единицу (см. команду inc pos в конце процедуры), а в начале ее лишь обнулять старшие шесть бит (команда andi pos, 3). Далее в зависимости от значения счетчика (cpi POS….) устанавливаем питание нужного индикатора (sbi PortD,) и вызываем процедуру установки маски сегментов SET_SEG, причем устанавливаемая маска определяется значением данного разряда в часах.

В процедуре SEG_SET и собственно процедурах установки маски (OUT_х) я предлагаю вам разобраться самостоятельно. Единственный вопрос, который у вас здесь может возникнуть— почему не применить удобный способ непосредственного задания маски рисунков цифр через загрузку констант командой lpm, как я писал в главе 13 — вместо многочисленных «тупых» процедур отдельно для каждой цифры? Дело в том, что маску удобно использовать, если у вас выводы управления разрядами идут подряд (к примеру, когда биты PortD 0–7 соответствуют битам маски 0–7). Тогда маску достаточно «приложить» к регистру порта, и программа резко сокращается. А здесь это сделать нельзя, т. к. «перестраивание» маски под выводы различных портов займет не меньше места, чем простая и понятная прямая установка выводов.

Процедура установки часов работает следующим образом. При коротком нажатии на Кн1 возникает прерывание INT1 (процедура по метке INTT1), в котором первым делом проверяется, есть ли сетевое питание (бит 1 регистра Flag, см. далее), иначе и сама установка не требуется. Далее, как обычно, запрещается само прерывание INT1 во избежание дребезга. Разрешается оно в прерывании Timer 1 (см. начало текста процедуры TIM1), которое, как мы уже знаем, происходит каждую секунду. Таким образом время нечувствительности, в течение которого можно отпустить кнопку без последствий (без перескока на произвольный разряд), составляет случайную величину от 0 до 1 с. На самом это не совсем верное решение, и сделано так только для простоты — следовало бы пропустить одну секунду, и только потом разрешать, иначе вероятность дребезга все-таки остается большой.

Далее в прерывании INT1 устанавливается отдельный счетчик разрядов set_up, который будет считать от 1 до 4 (если он больше, то выходим из режима установки), и признак режима установки (бит 0 регистра Flag). Если этот признак установлен, то разряд, соответствующий установленному номеру в счетчике set_up, будет мигать. Это достигается с помощью вспомогательного счетчика count (см. процедуру TIM1 по метке CONT_1). В этом же месте программы отслеживается состояние Кн2: если она нажата и удерживается, то каждую секунду происходит увеличение значения выбранного разряда на единицу, в тех пределах, в которых это допускается (для единиц минут — от 0 до 9, для десятков минут — от 0 до 5, для десятков часов — от 0 до 2, причем предел единиц часов зависит от значения десятков), далее значение опять обращается в ноль. Отпустив кнопку Кн2, вы фиксируете установленное значение, а нажав кратковременно на Кн1, переходите к следующему разряду. После прохождения всех разрядов, при последнем (пятом) нажатии Кн1, режим установки отменяется, т. е. бит 0 регистра Flag сбрасывается (см. процедуру по прерыванию INT1).

Немаловажная особенность этой конструкции — то, что во время установки счет времени прекращается (команды rjmp END_TIM1 в процедуре TIM1, это было сделано по неопытности, на самом деле там можно было поставить просто reti, но для наглядности я ничего менять не стал), а при выходе из режима установки счетчик секунд устанавливается в состояние 59 (команда ldi sek, 59), т. е. счет сразу же начинается с новой минуты. Окончание установки — довольно важный момент, который можно организовать по-разному, но данный способ самый удобный, т. к. вам достаточно дождаться окончания текущей минуты по образцовым часам, и в этот момент сделать последнее нажатие, выйдя из режима установки, чтобы довольно точно синхронизировать время. Сравните, например, как неудачно выполнена ручная установка часов в Windows, где часы продолжают идти и во время установки. А если бы мы обнуляли счетчик секунд, вместо его установки в максимальное значение, то нам пришлось каждый раз устанавливать число минут на единицу большее, что неудобно.

Теперь об обеспечении режима автономной работы. Программа контроллера в непрерывном цикле опрашивает значение логического уровня на выводе номер 12 PinB,0, он же AIN+ (3,9 В эквивалентно логической единице), и когда оно становится равным нулю, принимает меры к снижению потребления, в первую очередь за счет отключения внешних портов (см. процедуру Disable). Как только внешнее питание восстанавливается, автоматически возобновляется нормальный режим работы (Restore).

При перебрасывании компаратора в любою сторону происходит прерывание ACOMPI. В нем вывод 15 (ОС1) отключается от таймера Timer 1, и устанавливается навсегда в логическую единицу, если состояние компаратора есть логическая единица (когда истощается или отключается батарейка). Тогда двоеточие горит постоянно. И наоборот, вывод этот опять подключается к автоматическому миганию, когда компаратор перебрасывается обратно в ноль.

Детали и конструкция

Для блока питания используем «внутренности» блока со встроенной вилкой, с номинальным напряжением питания 10 В и током не менее 500 мА (такие продаются для некоторых игровых консолей). Напряжение на холостом ходу у него будет составлять примерно 13–14 В, под нагрузкой 130 мА оно сядет как раз примерно до 11–12 В.

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

Рис. 14.4. Кнопка SM5 с лапкой-рычагом

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

Под индикаторы указанного типоразмера 1" подойдут светодиоды диаметром 3 мм, обычные 5-миллиметровые будут слишком выделяться, а под меньшие индикаторы потребуются светодиоды с еще меньшим диаметром. «Суперяркие» светодиоды сюда решительно не годятся. Светодиод при этом желательно иметь с диффузным рассеиванием, чтобы его было одинаково видно со всех углов зрения. Так что вопрос подбора LED может оказаться непростым — в упомянутой конструкции мне так и не удалось сначала подобрать «диффузный» тип, удовлетворяющий всем перечисленным требованиям (они оказались чересчур «лимонного» оттенка), и пришлось устанавливать светодиоды в прозрачном корпусе. Для каждого типа светодиодов придется подобрать резистор R34 (см. рис. 14.2) согласно необходимой яркости (для прозрачных номинал его будет больше, для диффузных— меньше). Устанавливать эту пару диодов следует не прямо друг над другом, а с некоторым наклоном, соответственно наклону цифры индикатора. Неплохо будут также выглядеть прямоугольные (5x2 мм) светодиоды, также под наклоном, только их боковые грани придется закрасить густой черной краской или аккуратно обернуть их качественной липкой лентой.

Я останавливаюсь на всех этих подробностях потому, что они имеют решающее значение для того, будет ли ваша конструкция выглядеть фирменно, или напоминать продукт творчества членов кружка юных техников из деревни Гадюкино. Затрачивать столько сил и средств на конструирование и пренебречь при этом нюансами внешнего вида просто не имеет смысла — если вы, конечно, конструируете бытовой прибор, а не утилитарную схему для рабочих нужд. Но и в последнем случае гораздо приятнее брать в руки аккуратную и удобную в работе конструкцию, а не «голую» плату с болтающимися проводами.

Когда мы соединим платы управления и индикации кабелем и подключим питание, схема заработать сразу не может, потому что нужно запрограммировать МК. Для этого вы должны подключить к разъему XI программатор и загрузить hex-файл с программой. Часы должны «затикать» светодиодами и показать все нули на индикаторах. Потом можно браться за установку времени.

Без сомнения, вы легко сможете доделать эту конструкцию, добавив в нее, к примеру, функции будильника. Причем это можно сделать в принципе даже без переделки схемы, если «повесить» функции установки и включения будильника на те же кнопки, разделив их с простой установкой за счет отсчета времени удержания кнопки (т. е. между нажатием и отпусканием). Сложнее, правда, будет обеспечить выход на «пищалку», но ее можно «повесить» на тот же вывод «мигалки» управления разделительными светодиодами, если при срабатывании будильника заполнять включенное состояние мигалки частотой 2 кГц, предназначенной для управления разрядами — например, переключая с этой частотой вывод ОС1 то на вход, то на выход (при этом в обычном режиме «пищалка» будет еле слышно тикать). Но, разумеется, никто вас не заставляет применять именно МК 2313— возьмите модель 8515, где выводов гораздо больше, и все окажется куда проще. Тем более, что в этом случае можно придумать и еще что-то, например, добавить маленькие разряды секундомера в углу передней панели, а будильник дополнить «милицейской» мигалкой, переключая красный и синий светодиоды попеременно.

 

Глава 15

Вычисления в МК и использование АЦП

Обычные операции сложения и вычитания для 8-разрядных чисел (которые на поверку все равно оказываются 16-разрядными операциями) мы уже «проходили» в главе 13. Здесь мы попытаемся понять, как в 8-разрядном МК можно работать с многоразрядными и дробными величинами, в том числе осуществлять операции деления и умножения.

Подумаем сначала, — ас какими числами приходится работать на практике? Если говорить о целых числах, то большинство реальных нужд вполне укладывается в трехбайтовое значение (224 или 16 777 216). Этот же диапазон дает достаточное для практики значение точности (7 десятичных разрядов), большие числа обычно округляют и записывают в виде «мантисса — порядок», с добавкой степени 10. То же касается и разрядности дробных чисел. При этом следует учесть, что любая арифметическая операция дает погрешности округления, которые могут накапливаться. Углубляться в этот достаточно сложный вопрос мы не будем, нам достаточно того факта, что оперируя с трехбайтовыми числами, как результатом обычных операций деления и умножения, мы не выходим за пределы погрешности в шестом знаке, что значительно превышает разрешение рядовых 10-разрядных АЦП — с числом градаций 1024, означающем ошибку уже в третьем, максимум (в реальности так не бывает) в четвертом знаке.

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

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

Чтобы более четко разделить задачи с различной разрядностью, приводимые в «аппнотах» процедуры для наших целей придется творчески переработать, тем более, что в них имеются ошибки. Ошибки эти мы разбирать подробно не будем, да и вообще не будем останавливаться на внутреннем механизме работы таких процедур — к чему углубляться в теорию вычислений? Желающих я отсылаю к упоминавшемуся фундаментальному труду [10]Есть и еще более мощная штука — Algorithm Builder, который, в соответствии с новыми веяниями, позволяет проектировать программу в виде блок-схемы.
. Здесь мы займемся лишь практическими алгоритмами.

На рис. 15.1 приведена блок-схема алгоритма перемножения двух 16-разрядных беззнаковых чисел (MPY16U), скопированная из pdf-файла Application note AVR200. Жирным выделено необходимое исправление, то же следует сделать в алгоритме перемножения двух 8-разрядных чисел (MPY8U).

Ассемблерный текст процедур умножения можно найти в той же «аппноте», только представленной в виде asm-файла (его можно скачать с сайта Atmel по адресу, приведенному в главе 13, если в таблице с перечнем Application notes щелкнуть по значку диска, а не PDF-документа). Для внесения изменений найдите в тексте процедуру mpy16u, переставьте метку m16u_1 вместо команды brcc noad8, перед которой она стоит, двумя командами ранее — перед командой lsr mp16uH. Аналогичную манипуляцию надо проделать в процедуре mpy8u с меткой m8u_1, если вы желаете воспользоваться 8-разрядным умножением.

Рассмотрите внимательно текст процедуры 16-разрядного умножения в «аппноте» 200 и обратите внимание на две особенности. Во-первых, для представления результата в общем случае требуется 4 байта (регистра), т. е. результат будет 32-разрядным числом, что понятно. Поэтому, во-вторых, разработчики используют в целях экономии одни и те же регистры для множителя и младших байтов результата, но называют их разными именами. Такой прием мы уже обсуждали, и ясно, для чего он применяется — для большей внятности текста процедуры. И тем не менее, вообще говоря, это недопустимо — слишком легко забыть про то, что под разными именами кроется один и тот же регистр, и использовать его еще где-то (не будете же вы, в самом деле, держать неиспользуемыми аж целых 7 регистров только для того, чтобы один раз где-то что-то перемножить, правда?). Потому мы в дальнейшем обойдемся без таких фокусов — лучше написать внятные комментарии, а имена оставить уникальные, даже если они и не окажутся «говорящими».

Рис. 15.1. Процедура перемножения двух 16-разрядных чисел из pdf-файла Atmel Application note AVR200 с исправленной ошибкой

На практике, как мы говорили ранее, 32-разрядное число (максимум 4 294 967 296, т. е. более 9 десятичных разрядов) с точки зрения точности в большинстве случаев избыточно. Если мы ограничимся 24 разрядами результата, то нам придется пожертвовать частью диапазона исходных чисел так, чтобы сумма двоичных разрядов сомножителей не превышала 24. Например, можно перемножать два 12-разрядных числа (в пределах 0—4095 каждое) или 10-разрядное (скажем, результат измерения АЦП) на 14-разрядный коэффициент (до 16 383). Так как при умножении точность не теряется, то этого оказывается более чем достаточным, чтобы обработать большинство практических величин.

Процедура перемножения двух таких величин в исходном 16-разрядном виде, с представлением результата в трехбайтовой форме может быть легко получена из исправленной нами процедуры MPY16U по «аппноте» 200, но я решил воспользоваться тем обстоятельством, что для контроллеров семейства Mega определены аппаратные операции умножения (в Приложении 4 я их не привожу). Тогда алгоритм сильно упрощается, причем он легко модифицируется как для 32-, так и для 24-разрядного результата. Таким образом, для Tuny и Classic по-прежнему следует пользоваться обычными процедурами из «аппноты» (исправленными), а алгоритм для Mega приведен в листинге 15.1 (в названиях исходных переменных отражен факт основного назначения такой процедуры — для умножения неких данных на некий коэффициент). Сокращения LSB и MSB, которые нам еще встретятся не раз, означают least (most) significant bit — младший (старший) значащий разряд, по-русски МЗР и СЗР соответственно.

Листинг 15.1

.def dataL = r4   ;multiplicand low byte

.def dataH = r5   ;multiplicand high byte

.def KoeffL = r2  ;multiplier low byte

.def koeffH = r3  ;multiplier high byte

.def temp = r16   ;result byte 0 (LSB — младший разряд)

.def temp2 = r17   ;result byte 1

.def temp3 = r18   ;result byte 2 (MSB — старший разряд)

;**********

;умножение двух 16-разрядных величин, только для Меда

;исходные величины dataH: dataL и KoeffH: KoeffL

;результат 3 байта temp2:temp1:temp;

;**********

Mu1616:

clr temp2   ;очистить старший

mul dataL,KoeffL   ;умножаем младшие

mov temp,r0   ;в r0 младший результата операции mu1

mov tempi,r1   ;в r01 старший результата операции mu1

mul dataH,KoeffL   ;умножаем старший на младший

add temp1,r1   ;в r0 младший результата операции mu1

adc temp2,r1   ;в r01 старший результата операции mu1

mul dataL,KoeffH   ;умножаем младший на старший

add temp1,r0   ;в r0 младший результата операции mu1

adc temp2,r01   ;в r01 старший результата операции mu1

mul dataH,KoeffH   ;умножаем старший на старший

add temp2,r0   ;4-й разряд нам тут не требуется, но он — в r01

ret

;**********

Как видите, эта процедура легко модифицируется под любую разрядность результата, если нужно получить полный 32-разрядный диапазон, просто добавьте еще один регистр для старшего разряда (temp3, к примеру) и одну строку кода перед командой ret:

adc temp3,r01

Естественно, можно просто обозначить r01 через temp3, тогда и добавлять ничего не придется.

Процедуры деления для многобайтовых чисел

Деление — значительно более громоздкая процедура, чем умножение, требует больше регистров и занимает больше времени (MPY16U из «аппноты» занимает 153 такта, по уверению разработчиков, а аналогичная операция деления двух 16-разрядных чисел — от 235 до 251 тактов). Операции деления двух чисел (и для 8-, и для 16-разрядных) приведены в той же «аппноте» 200, и на этот раз без ошибок, но они не всегда удобны на практике: часто нам приходится делить результат какой-то ранее проведенной операции умножения или сложения, а он нередко выходит за пределы двух байтов.

Потому пришлось разрабатывать свои операции. Например, часто встречается необходимость вычислить среднее значение для уточнения результата по сумме отдельных измерений. Если даже само измерение укладывается в 16 разрядов, то сумма нескольких таких результатов уже должна занимать 3 байта. В то же время делитель — число измерений — может быть и относительно небольшим, и укладываться в один байт. В листинге 15.2 я привожу процедуру деления 32-разрядных чисел (на всякий случай) на однобайтное число, которая представляет собой модификацию оригинальной процедуры из Application notes 200. Как и ранее, названия переменных отражают назначение процедуры — деление состояния некоего 4-байтового счетчика на число циклов счета (определения регистров-переменных не приводятся, комментарии сохранены из оригинального текста «аппноты», они соответствуют блок-схеме алгоритма, размещенной в pdf-файле).

Листинг 15.2

;********

;div32x8» — 32/8 деление беззнаковых чисел

;Делимое и результат в count_HH (старший), countTH,

;countTM, countTL (младший)

;делитель в cikle

;требуется четыре временных регистра dremL — dremHH

;из диапазона r16-r31

;для хранения остатка

;********

div32x8:

        clr dremL   ;clear remainder Low byte

        clr dremM   ;clear remainder

         clr dremH   ;clear remainder

         sub dremHH,dremHH   ;clear remainder High byte and carry

         ldi cnt,33   ;init loop counter

d_1:  rol countTL   ;shift left dividend

        rol countTM

        rol countTH

        rol count_HH

        dec cnt   ;decrement counter

        brne d_2   ;if done

         ret   ;return

d_2:  rol dremL   ;shift dividend into remainder

         rol dremM

        rol dremH

        rol dremHH

        sub dremL,cikle   ;remainder = remainder — divisor

        sbci dremM,0

        sbci dremH,0

        sbci dremHH,0

        brcc d_3   ;if result negative

         add dremL,cikle  ;restore remainder

        clr temp

        adc dremM,temp

        adc dremH,temp

        adc dremHH,temp

       clc   ;clear carry to be shifted into result

        rjmp d_1   ;else

d_3: sec   ;set carry to be shifted into result rjmp d_1

;******** конец 32x8

Многие подобные задачи на деление удается решить значительно более простым и менее громоздким методом, если заранее подгадать так, чтобы делитель оказался кратным степени двойки. Тогда все деление сводится, как мы знаем, к сдвигу разрядов вправо столько раз, какова степень двойки. Для примера предположим, что мы некую величину измерили 64 раза, и хотим узнать среднее. Пусть сумма укладывается в 2 байта, тогда вся процедура деления будет такой:

;деление на 64

       clr count_data   ;счетчик до 6

div64L:

       lsr dataH   ;сдвинули старший

        ror dataL   ;сдвинули младший с переносом

        inc count_data

       cpi count_data,6

       brne div64L

He правда ли, гораздо изящнее и понятнее? Попробуем от радости решить задачку, которая на первый взгляд требует, по крайней мере, знания высшей алгебры — умножить некое число на дробный коэффициент (вещественное число с «плавающей запятой»). Теоретически для этого требуется представить исходные числа в виде «мантисса — порядок», сложить порядки и перемножить мантиссы (см. [10]Есть и еще более мощная штука — Algorithm Builder, который, в соответствии с новыми веяниями, позволяет проектировать программу в виде блок-схемы.
). Нам же неохота возиться с этим представлением, т. к. мы не проектируем универсальный компьютер, и в подавляющем большинстве реальных задач все конечные результаты у нас представляют собой целые числа.

На самом деле эта задача решается очень просто, если ее свести к последовательному умножению и делению целых чисел, представив реальное число в виде целой дроби с оговоренной точностью. Например, число 0,48576 можно представить как 48 576/100 000. И если нам требуется на такой коэффициент умножить, к примеру, результат какого-то измерения, равный 976, то тогда можно действовать, не выходя за рамки диапазона целых чисел: сначала умножить 976 на 48 576 (получится заведомо целое число 47 410 176), а потом поделить результат на 105, чисто механически перенеся запятую на пять разрядов. Получится 474,10176 или, если отбросить дробную часть, 474. Большая точность нам и не требуется, т. к. и исходное число было трехразрядным.

Улавливаете, к чему я клоню? С числами в десятичном виде хорошо работать руками, просто отсчитывая разряды. Нам же делить на сто тысяч в 8-разрядном МК крайне неудобно — представляете, насколько громоздкая процедура получится? Наше ноу-хау будет состоять в том, что мы для того, чтобы «вогнать» дробное число в целый диапазон, будем использовать не десятичную дробь, а двоичную — деление тогда сведется к описанной «механической» процедуре сдвига, аналогичной переносу запятой в десятичном виде.

Итак, чтобы умножить 976 на коэффициент 0,48576, следует сначала последний вручную умножить, например, на 216 = 65 536, и тем самым получить числитель соответствующей двоичной дроби (у которой знаменатель равен 65 536) — он будет равен 31834,76736, или, с округлением до целого 31 835. Такой точности хватит, если исходные числа не выходят, как у нас, за пределы трех-четырех десятичных разрядов. Теперь мы в контроллере должны умножить исходную величину 976 на константу 31 835 и полученное число 31 070 960 (оно оказывается 4-байтовым — S01DA1AF0, потому нашу Mui6i6 придется чуть модифицировать, как сказано при ее описании ранее) сдвигаем на 16 разрядов вправо:

;в ddHH: ddH: ddM: ddL число $01DA1AF0,

;его надо сдвинуть на 16 разрядов

         сlrг cnt

div16L:   ;деление на 65536

         lsr ddHH   ;сдвинули старший

         ror ddH   ;сдвинули 3-й

        rоr ddM   ;сдвинули 2-й

         rоr ddL   ;сдвинули младший

         inc cnt

        cpi cnt,16

        brne divl6L   ;сдвинули-поделили на 2 в 16

В результате, как вы можете легко проверить, старшие байты будут нулевыми, а в ddM:ddL окажется число 474 — тот же самый результат. Но и это еще не все, такая процедура приведена скорее для иллюстрации общего принципа. Ее можно еще больше упростить, если обратить внимание на то, что сдвиг на восемь разрядов есть просто перенос значения одного байта в соседний (в старший, если сдвиг влево, и в младший — если вправо). Итого получится, что для сдвига на 16 разрядов вправо нам надо всего-навсего отбросить два младших байта, и взять из исходного числа два старших ddHH:ddH — это и будет результат. Проверьте — S01DA и есть 474. Никаких других действий вообще не требуется!

Если степень знаменателя дроби, как в данном случае, кратна 8, то действительно никакого деления, даже в виде сдвига, не требуется, но чаще всего это не так. Однако и тут приведенный принцип может помочь: например, при делении на 215 (что может потребоваться, если, например, в нашем примере константа больше единицы) вместо пятнадцати кратного сдвига вправо результат можно сдвинуть на один разряд влево (фактически умножив число на два), а потом уже выделить из него старшие два байта. Итого процедура будет состоять из четырех операций сдвига и займет четыре такта. А в виде циклического сдвига на 15, как ранее, нам требуется в каждом цикле сделать четыре операции сдвига и одну увеличения счетчика: 15 х 5 = 75 простых однотактных операций, и еще 15 операций сравнения, из которых 14 займут два такта — итого 104 такта. А решение «в лоб» на основе операций целочисленного деления в несколько раз превышало бы и эту величину. Существенная разница, правда? Вот такая специальная арифметика в МК.

Операции с числами в формате BCD

Это важная группа операций, ведь значительная часть устройств на основе МК предназначена для демонстрации чисел в том или ином виде. Это, естественно, можно делать только в десятичном формате, в то время как внутреннее представление чисел в регистрах двоичное. В некоторых микропроцессорных системах (в их число входит семейство x51 от Intel и, кстати, x86) даже имеется специальная инструкция для т. н. двоично-десятичной коррекции, которая позволяет получить верный результат при сложении двоично-десятичных чисел в упакованном формате (о BCD-форматах см. главу 7). Но в системе команд AVR такой инструкции нет, и, в общем-то, она все равно не очень-то полезна, т. к. математические операции в любом случае удобнее выполнять в «родной» двоичной форме, а для представления на дисплее числа так или иначе приходится «распаковывать». В ПК этим незаметно для пользователя занимаются процедуры на языках высокого уровня (да так успешно, что приходится скорее озадачиваться обратной проблемой — представлением десятичных чисел в двоичной/шестнадцатеричной форме), ну а на уровне ассемблера десятичные преобразования приходится делать, что называется, ручками.

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

Традиционная область использования команд двоично-десятичной коррекции, в том числе и в процессорах х 86 — манипуляции со значением времени, полученным из микросхем RTC, в которых часы, минуты и секунды всегда хранятся в упакованном BCD-формате. Как вы увидите далее, такой формат хранения довольно удобен на практике. Однако область применения микроконтроллерных систем далеко не исчерпывается подсчетом и демонстрацией времени, потому нам придется выйти за рамки однобайтовых кодов, для которых, собственно, инструкция коррекции и создавалась. Уже для двухбайтовых чисел ее применение вызывает только лишние сложности.

В области BCD-преобразований есть три основные задачи:

• Преобразование двоичного/шестнадцатеричного числа в упакованный BCD-формат.

• Распаковка упакованного BCD-формата для непосредственного представления десятичных чисел с целью их вывода на дисплей.

• Обратное преобразование упакованного BCD-формата в двоичный/шестнадцатеричный для выполнения над ним, например, арифметических действий.

Некоторые процедуры для этой цели приведены в фирменной Application notes 204. При их использовании нужно учесть ряд моментов. Так, процедура bin2BCD8 для преобразования однобайтового числа в BCD работает только для чисел от 0 до 99 (для больших чисел нужен еще один байт, точнее, тетрада — в ней будет храниться старший разряд). В «аппноте» процедура представлена в универсальном виде, пригодном (при небольшой модификации) и для получения упакованного BCD, и для изначально распакованного (результат в двух отдельных байтах). Чтобы не путаться, приведу здесь ее вариант (листинг 15.3), который заодно более экономичный по количеству используемых регистров. Исходное hex-число содержится в регистре temp, распакованный результат — в temp1: temp. Как и в предыдущих случаях, комментарии сохранены из исходного текста.

Листинг 15.3

;преобразование 8-разрядного hex в неупакованный BCD

;вход hex= temp, выход BCD temp1-старший; temp — младший

;эта процедура работает только для исходного hex от 0 до 99

bin2bcd8:

        clr temp1   ;clear result MSD

bBCD8_1: subi temp,10   ;input = input — 10

        brcs bBCD8_2   ;abort if carry set

        inc temp1   ;inc MSD

        rjmp bBCD8_1   ;loop again

bBCD8_2:subi temp, — 10   ;compensate extra subtraction

ret

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

Листинг 15.4

;на входе в temp упакованное BCD-значение

;на выходе в temp hex-значение

;temp1 — вспомогательный регистр для промежуточного хранения temp

;действительна только для семейства Меgа

HEX_BCD:

         mov temp1, temp

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

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

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

          mov temp,temp1   ;возвращаемся к исходному

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

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

ret

Более громоздкая задача — преобразование многоразрядных чисел. Преобразовывать BCD-числа, состоящие более чем из одного байта, обратно в НЕХ-формат приходится крайне редко, зато задача прямого преобразования возникает на каждом шагу. Я здесь приведу отсутствующую в «аппноте» 204 процедуру конвертации чисел, выходящих за рамки 16-разрядного диапазона. Например, такая задача может возникнуть при конструировании многоразрядных счетчиков. Ограничимся диапазоном в 7 десятичных знаков (9 999 999), тогда исходное число будет укладываться в 3 байта (24 разряда). В целях универсальности в процедуре, которая приводится далее в листинге 15.5, на выходе получается отдельно неупакованный (сразу для индикации) и упакованный десятичный формат. Сократить число необходимых регистров можно, если большую часть результатов сразу записывать в SRAM — в дальнейшем мы так и будем поступать, а здесь для наглядности работаем только с регистрами.

Отметим, что процедура bin2BCD24 сделана на основе «фирменной» bin2BCD16 и, как и последняя, использует хитрый прием с записью значений в регистры по адресам памяти: так можно производить над адресами разные манипуляции, меняя регистры (аналогично адресной арифметике в языке С). Как и в других случаях, сохранена часть оригинальных комментариев из исходной «фирменной» процедуры.

Листинг 15.5

;процедура преобразования 3-байтового hex в упакованный (4 регистра)

;и неупакованный (7 регистров) BCD

;исходное значение в регистрах

.def Count0 = r25

.def Count1 = r26

.def Count2 = r27

;на выходе упакованный BCD в регистрах

. def tBCD0 =r13   ;BCD value digits 1 and 0

.def tBCD1 =r14   ;BCD value digits 3 and 2

.def tBCD2 =r15   ;BCD value digit 4,5

.def tBCD3 =r16   ;BCD value digit 6

; на выходе неупакованный BCD в регистрах

.def N1 =r1   ;младший

. def N2 =r2

.def N3 =r3

.def N4 =r4

.def N5 =r5

.def N6 =r6

.def N7 =r7  ;старший

;вспомогательные регистры

.def cnt16a =r18   ;счетчик цикла

.def tmp16a =r19   ;временное значение

;адреса регистров в памяти

.equ AtBCD0 =13   ;address of tBCD0

.equ AtBCD3 =16   ;address of tBCD3

bin2BCD24:

         ldi cnt16a,24   ;Init loop counter clr tBCD3

          clr tBCD2   ;clear result (4 bytes)

          clr tBCD1

         clr tBCD0

         clr ZH   ;clear ZH (not needed for AT90Sxx0x)

bBCDx_1: lsl Count0   ;shift input value

          rol Count1   ;through all bytes

          rol Count2   ;through all bytes

          rol tBCD0

         rol tBCD1

         rol tBCD2

         rol tBCD3

         dec cnt16a  ;decrement loop counter

         brne bBCDx _2 ;if counter not zero

;распаковка

          ldi temp,0b00001111

         mov N1,tBCD0

         and N1,temp

         mov N2,tBCD0

         swap N2

         and N2,temp

         mov N3,tBCD1

         and N3,temp

         mov N4,tBCD1

         swap N4

         and N4,temp

         mov N5,tBCD2

         and N5,temp

         mov N6,tBCD2

         swap N6

         and N6,temp

         mov N7,tBCD3

         ret; return

bBCDx_2:ldi r30;AtBCD3+1   ;Z points to result MSB + 1

bBCDx_3:

          ld tmp16a, — Z   ;get (Z) with pre-decrement

           subi tmp16a, — $03   ;add 0x03

           sbrc tmp16a,3   ;if bit 3 not clear

           st Z,tmp16a   ;store back

          ld tmp16a,Z   ;get (Z)

           subi tmp16a, — $30   ;add 0x30

           sbrc tmp16a,7   ;if bit 7 not clear

           st Z,tmp16a   ;store back

           cpi ZL,AtBCD0   ;done all three?

           brne bBCDx_3   ;loop again if not

           rjmp bBCDx_1

Использование встроенного АЦП

Встроенный АЦП последовательного приближения входит в состав почти всех МК семейства Mega и большинства МК семейства Tuny, кроме простейших младших моделей. В семействе Classic был только один тип МК со встроенным АЦП — AT90S8535 — несколько доработанный вариант популярного AT90S8515. На примере его Mega-версии под названием ATmega8535 мы в дальнейшем и разберем работу встроенного АЦП, но сначала стоит сделать несколько общих замечаний.

Все встроенные АЦП многоканальные и 10-разрядные (за небольшим исключением — например, в ATmega8 из 6 каналов только четыре имеют разрешение 10 разрядов, а оставшиеся два — 8). Многоканальность означает, что имеется только одно ядро преобразователя, которое по желанию программиста может подключаться к одному из входов через аналоговый мультиплексор, наподобие разобранного в главе 8 561КП2. Если вы, как чаще всего и бывает, задействуете лишь часть входов, то остальные могут использоваться как обычные порты ввода/вывода. В разных моделях число каналов колеблется от 4 до 16, причем в некоторых из них выводы АЦП можно коммутировать попарно так, чтобы получить АЦП с дифференциальным входом (тогда измеряется разность напряжений между этими входами, а не абсолютное значение относительно «земли»). Добавим еще, что в некоторых моделях все или часть входов в дифференциальном режиме могут иметь добавочный коэффициент усиления (10, 20 или 200).

Все эти «примочки» дополнительно снижают и без того не слишком высокую точность АЦП, которая номинально составляет для несимметричного (недифференциального) входа ±2 LSB, плюс еще 0,5 LSB за счет нелинейности по всей шкале. Фактически такой АЦП с точки зрения абсолютной точности соответствует 8-разрядному. При соблюдении всех условий эту точность, впрочем, можно повысить, правда, условия довольно жесткие и включают в себя как «правильную» разводку выводов АЦП, так и, например, требование остановки цифровых узлов на время измерения, чтобы исключить наводки (специальный режим ADC Noise Reduction). В дифференциальном режиме есть свои специальные приемы повышения точности. В общем, как и всегда в таких случаях, для получения хорошего результата аналого-цифрового преобразования требуются определенные усилия и некоторый опыт.

Чтобы не углубляться в детали этого процесса и не загромождать программу, мы в дальнейшем поступим проще — предпримем ряд мер, чтобы обеспечить стабильность результата, а абсолютную ошибку скомпенсируем за счет калибровки, которая все равно потребуется. Для начала давайте посчитаем, какие, собственно, ошибки нас могут устроить. Максимально достижимая точность с помощью 10-разрядного преобразователя составляет 0,1 % (1/1024, или ±0,5 LSB) приведенной погрешности (т. е. погрешности от всей шкалы измерения). Для бытовых измерений это достаточно высокая величина, например, большинство портативных мультиметров имеют точность раз в пять хуже, обладая погрешностью порядка 0,5 %. АЦП в 10 разрядов может, например, обеспечить точность измерения температуры 0,1° для стоградусной шкалы (от -50 до +50°).

На самом деле нам такая точность не требуется — все равно термометр, подвешенный за окном или на стенке комнаты, никогда не покажет точную температуру, насколько бы он ни был точным сам по себе. На него будут влиять сквозняки, солнечные лучи, осветительные приборы, конвекция воздуха по нагретой стенке, тепловое излучение от оконных проемов — одним словом, все то, что определяет т. н. методическую погрешность. И для большинства бытовых измерений абсолютной точности в 8 разрядов (~0,4 %) хватает, как говорится, «выше крыши». Это относится не только к температуре, но и к подавляющему большинству других бытовых измерений. В большинстве случаев нам важно обеспечить не абсолютную точность, а, во-первых, стабильность показаний (чтобы в одинаковых условиях прибор показывал Одно и то же, и показания можно было бы сравнивать между собой), и, во-вторых, достаточную разрешающую способность, т. е. оптимальную цену деления прибора.

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

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

После такого экскурса в теорию измерений мы можем сделать вывод, что погрешности встроенного АЦП нам в большинстве случаев хватит и без особых ухищрений, важно только, чтобы показания не «дребезжали». Цифровые помехи со стороны ядра МК, как показывает опыт, имеют значительно меньшее влияние на результат, чем внешние, потому режим Noise Reduction нам не потребуется. Уменьшение дребезга почти до нуля достигается тем, что, во-первых, на входе канала ставится фильтр низкой частоты для устранения неизбежных в совмещенных аналого-цифровых схемах наводок на внешние цепи. Обычно достаточно керамического конденсатора порядка 0,1–1 мкФ, хотя в критичных случаях фирменное руководство рекомендует еще последовательно с ним включать индуктивность (порядка 10 мкГн), которую, добавим, для простоты можно заменить на резистор (несколько единиц или десятков килоом). Во-вторых, мы будем измерять несколько раз, и значения отдельных измерений усреднять — это самый эффективный способ повышения стабильности показаний, который я рекомендую для всех случаев, даже и тогда, когда соблюдены все фирменные рекомендации по повышению точности измерений (и в этом случае — особенно!). Это хоть и загромождает программу, но полученный эффект оправдывает такое усложнение.

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

Встроенным источником опорного напряжения 2,56 В я пользоваться не рекомендую, прежде всего потому, что его величина может «гулять» в значительных пределах (до ±0,3 В), и зависит к тому же от напряжения питания, что в достаточной степени обессмысливает его использование. Единственным аргументом «за» является сама величина 2,56 В, что позволяет без сложных арифметических преобразований получать на выходе число измеряемых милливольт. Выходное значение АЦП (для несимметричного входа) выражается формулой:

N = 1024∙(Uвх/Uon).

Поэтому при Uon = 256 мВ, выходная величина N будет представлять учетверенное значение входного напряжения в милливольтах. Его легко привести к целому числу милливольт, просто сдвинув результат на два разряда вправо.

Однако такое измерение будет достаточно неточным и с искусственно пониженным разрешением (мы «легким движением руки» зачем-то превращаем 10-разрядный АЦП в 8-разрядный). Поэтому во всех случаях, когда требуется обеспечить абсолютную точность (например, при работе АЦП в составе мультиметра, где нас интересуют именно абсолютные значения в вольтах), следует использовать внешний точный источник опорного напряжения, тем более что они вполне доступны, хотя и не всегда дешевы (так, один из самых дорогих — прецизионный МАХ873 с напряжением 2,5 В имеет разброс напряжения 1,5–3 мВ при температурной стабильности 2,5–7 мВ во всем диапазоне температур, и стоит порядка 10 долл.). Важным преимуществом такого способа служит возможность выбора опорного напряжения из более удобных величин (например, 2,048 В), что позволит не терять разрешение встроенного АЦП.

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

Пару слов о самой организации измерений. АЦП последовательного приближения должен управляться определенной тактовой частотой, для чего в его состав входит делитель тактовой частоты самого МК, подобный предварительному делителю у таймеров. Устанавливать максимально возможную частоту (которая равна половине от тактовой) не рекомендуется, а лучше подбирать коэффициент деления так, чтобы тактовая частота АЦП укладывалась в промежуток от 50 до 200 кГц. Например, для тактовой частоты МК 4 МГц подойдет коэффициент деления 32, тогда частота АЦП составит 125 кГц. Преобразование может идти в непрерывном режиме (после окончания преобразования сразу начинается следующее), запускаться автоматически по некоторым прерываниям (не для всех типов AVR), или каждый раз запускаться по команде. Мы будем применять только последний «ручной» режим, т. к. нам для осреднения результатов тогда удобно точно отсчитывать число преобразований. В таком режиме на одно преобразование уходит 14 тактов, поэтому для приведенного примера с частотой 125 кГц время преобразования составит приблизительно 9 мс.

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

Измеритель температуры и давления на AVR

Для иллюстрации практического использования встроенного АЦП мы сконструируем измеритель температуры и атмосферного давления. Для измерения температуры мы заимствуем аналоговую часть схемы термометра из главы 10, перенеся ее сюда практически без изменений, за исключением того, что здесь мы запитаем схему от двуполярного источника ±5 В, чтобы обеспечить более удобный нам диапазон входных напряжений АЦП, начинающийся от 0 В в положительную сторону. Это позволит нам включить АЦП в несимметричном режиме, а не в дифференциальном, что упрощает схему и обеспечивает максимальное разрешение.

С датчиком атмосферного давления все еще проще — ряд фирм выпускают готовые датчики давления. Мы выберем барометрический датчик МРХ4115 фирмы Motorola, питающийся от напряжения 5 В и имеющий удобный диапазон выхода примерно от 0,2 до 4,6 В. Крупный недостаток таких датчиков с нашей точки зрения — то, что погрешность привязана к абсолютной шкале (в данном случае от 15 до 115 кПа, что составляет примерно 11 и 860 мм рт. ст. соответственно) и составляет не менее 1,5 %. Это без учета заводского разброса (устраняется калибровкой) и зависимости выходного напряжения от напряжения питания (устраняется путем относительных измерений — питанием АЦП и датчика от одного источника). Но даже при этих условиях 1,5 % от всей шкалы в 850 мм рт. ст. составит более 12 мм рт. ст. Это, конечно, недопустимо высокая погрешность для измерения атмосферного давления, которое на практике меняется в десятикратно меньших пределах — для большей части России, кроме горных местностей, можно выбирать диапазон от 700 до 800 мм рт. ст., даже с запасом. На самом деле это не должно нас пугать — как показал опыт, такой диапазон нас устраивает с точки зрения разрешения (одному мм рт. ст. будет соответствовать около одного разряда АЦП), а стабильность датчика оказывается вполне на высоте и обеспечивает при надлежащей калибровке разброс в пределах ±1 мм рт. ст.

При этом учтем, что большая абсолютная точность нам не требуется, как и в случае температуры — для небольших высот над уровнем моря можно считать, что при изменении высоты на каждые 10 м давление меняется примерно на 1 мм рт. ст., так что в пределах такого города, как Москва, с естественными перепадами высот 50 и более метров, оно само по себе будет «гулять» в пределах 5 мм рт. ст., даже без учета этажности зданий. И нам все равно целесообразно будет подогнать результат «по месту» так, чтобы не иметь крупных расхождений с прогнозом погоды по телевизору, иначе от показаний прибора будет мало пользы.

Схема

Схема такого прибора будет выглядеть так, как показано на рис. 15.2.

Рис. 15.2. Схема измерителя температуры и давления на МК ATmega8535

Чтобы не загромождать схему, здесь не показан узел индикации, т. к. он аналогичен тому, что используется в часах из главы 14, за исключением того, что должен содержать не четыре, а шесть разрядов (показания в формате «33,3»° и «760» мм рт. ст.). К ним можно добавить постоянно горящие индикаторы, показывающие единицы измерения (см. рис. 15.3, где они изготовлены на основе шестнадцатисегментных индикаторов типа PSA-05).

Рис. 15.3. Расположение индикаторов измерителя температуры и давления

Так как здесь выводов портов хватает, то можно назначить для управления разряды подряд (например, разряды порта С от РC0 до РС6 для управления сегментами и порта В от РВ0 до РВ5 для управления разрядами) и использовать для вывода цифры прием с формированием маски в виде констант (см. главу 13), что заметно сократит программу. Кроме того, надо не забыть знак температуры, который удобно изготовить из отдельного плоского светодиода. В остальном принцип индикации точно такой же, как в часах, и мы остановимся на подробностях чуть далее, когда будем разбирать программу.

Не показан на схеме и программирующий разъем, который полностью одинаков для любой схемы на AVR и показан на рис. 13.4 и 15.2 (соответствующие выводы для ATmega8535 подписаны на схеме рис. 15.2). То, что вывод MOSI (вывод 6) совпадает с выводом индикации единиц давления, вас смущать уже не должно. Однако незадействованные в других функциях выводы программирования (в данном случае MISO и SLK, выводы 7 и 8) следует подсоединить к питанию +5 Вц «подтягивающими» резисторами номиналом от 1 до 10 кОм (на схеме не показаны), так же, как и вывод Reset, только, естественно, без каких-либо конденсаторов (на схеме для вывода Reset указан номинал резистора 5,1 кОм). Как и RC-цепочка для Reset, «подтягивающие» резисторы для выводов программирования в принципе не требуются, однако их следует устанавливать. В тех случаях, когда схема представляет собой временный макет, без этих деталей можно обойтись, однако в работающей схеме без них могут быть неприятности, о чем мы уже говорили в главе 12. Если разъем программирования вообще не предусматривается, то устанавливать резисторы к выводам программирования не нужно.

Схема источника питания показана на рис. 15.4.

Рис. 15.4. Схема источника питания для измерителя температуры и давления

Измеритель имеет четыре питания (+5 Вц, ±5 Ва и +12 В для индикации) и три «земли», причем обычным значком «» здесь обозначена аналоговая «земля» CNDa. Линия цифровой «земли» обозначена GNDц, кроме этого, имеется еще общий провод индикаторов GNDи. Все три «земли» соединяются только на плате источника питания. Отмечу, что готовый трансформатор с характеристиками, указанными на схеме, вы можете не найти. Поэтому смело выбирайте тороидальный трансформатор мощностью порядка 10–15 Вт на напряжение вторичной обмотки 10–14 В (для индикаторов), измерьте на нем число витков на вольт (как описано в главе 4), и домотайте три одинаковых обмотки на 7–8 В каждая поверх существующих, проводом не меньше, чем 0,3 мм в диаметре. Удобнее всего их мотать одновременно сложенным втрое проводом заранее рассчитанной длины.

Теперь немного разберемся с температурой. Сопротивление датчика составляет 760 Ом при 0 °C (~=610 Ом при -50°) и имеет крутизну примерно 3 Ом/° (о датчике см. главу 10). Величины резисторов в аналоговой части измерителя подогнаны так, чтобы обеспечить ток через датчик 1,3 мА. Таким образом напряжение на датчике в диапазоне температур от -50° до +50 °C будет меняться на 400 мВ, т. е. на выходе дифференциального усилителя (с учетом его коэффициента усиления около 12) диапазон напряжений составит примерно 4,9 В. Таким образом мы будем использовать весь диапазон АЦП (от 0 до Uon) в полной мере с некоторым запасом. Резистор R4 устанавливает нижнюю границу диапазона, и здесь его нужно выбирать равным не сопротивлению датчика при 0°, как в схеме по рис. 10.8, а его сопротивлению при минимальной требуемой температуре. При указанных на схеме номиналах нижняя граница диапазона температур будет около -47°, а верхняя — около 55 °C. Для медного датчика с другим сопротивлением следует пересчитать коэффициент усиления усилителя (соответствующая формула приведена в главе 6, см. рис. 6.8). Это можно делать приблизительно — окончательную калибровку под реальный датчик мы будем производить путем изменения коэффициентов пересчета в программе МК.

Программа

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

Это несложно сделать, если представить формулу пересчета значений температуры в виде уравнения:

N = К∙(х — Z),

где N — число на индикаторе, х — текущий код АЦП, Z — код АЦП, соответствующий нулю градусов Цельсия (при наших установках он должен соответствовать примерно середине диапазона).

Чтобы величина по данной формуле всегда получалась положительная, нам придется сначала определять, что больше — х или Z, и вычитать из большего меньшее. Заодно при этой операции сравнения мы определяем значение знака. Если мы предположим, что в регистрах AregH: AregL содержится значение текущего кода АЦП х, а в регистрах KoeffH: KoeffL значение коэффициента Z, то алгоритм будет выглядеть так, как иллюстрирует листинг 15.6.

Листинг 15.6

;вычисление знака:

          ср AregL,KoeffL   ;сравниваем х и Z

          срс AregH,KoeffH

         brsh Ь0

         ;если х меньше Z

          sub KoeffL,AregL

         sbc КоеffH,AregH

         mov AregL,KoeffL  ;меняем местами, чтобы температура

          mov AregH,KoeffH   ;оказалась опять в AregH: AregL

         sbi PortD,7   ;знак -

          rjmp m0

Ь0:   ;если x больше Z

         sub AregL,KoeffL

         sbc AregH,KоеffH

         cbi PortD,7  ;знак +

m0:

<умножение на коэффициент К>

Здесь разряд 7 порта D (вывод 21) управляет плоским светодиодом, который горит, если температура меньше нуля, и погашен в противном случае.

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

N = К∙(х + Z),

все величины будут находиться в положительной области.

Физический смысл коэффициента К — крутизна характеристики датчиков в координатах «входной код АЦП — число на индикаторах». Умножение на К мы будем производить описанным методом — через представление его в виде двоичной дроби (за основу берется 210 = 1024, этого будет достаточно). Вычисление ориентировочных значений коэффициентов К и Z поясняется далее, при описании процедуры калибровки.

Теперь можно окинуть взглядом собственно программу, которая целиком приведена в Приложении 5 (раздел «Программа измерителя температуры и давления»). Как вы видите из таблицы прерываний, здесь используется всего один, самый простой Timer 0, который срабатывает с частотой около 2000 раз в секунду. В его обработчике по метке TIM0 и заключена большая часть функциональности.

В каждом цикле сначала проверяется счетчик cRazr, который отсчитывает разряды индикаторов (от 0 до 5). В соответствии с его значением происходит формирование кода индицируемого знака (по алгоритму вызова константы-маски знака, описанному в главе 13) и затем на нужный разряд подается питание.

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

Как видите, здесь формирование знака реализовано не очень красиво, и довольно громоздким способом — просто передачей управления на нужную процедуру в зависимости от значения счетчика. Сами же процедуры структурно одинаковы (меняются лишь адреса в памяти, из которых считываются значения разрядов индикатора и номера разрядов порта управления PortB ). Программу в этой части можно слегка сократить (если просто вывести одинаковые операторы в отдельную процедуру, и задействовать локальные переменные), но я не стал этого делать, т. к. принципиально это ничего не изменит: места в памяти у нас достаточно, а программа, на мой взгляд, тем лучше читается, чем в ней меньше структурных блоков. (Упоминавшийся в главе 13 Дейкстра, несомненно, схватился бы за сердце, услышав такое, но тем не менее это чистая правда — весь алгоритм окинуть взглядом легче, когда он максимально структурирован, но каждый отдельный фрагмент его проще понять, если не приходится «рыскать» по всему листингу.)

Интерес же представляет другой момент во всем этом — а нельзя ли было бы кардинально решить проблему, учитывая тот факт, что разряды считаются подряд (от нулевого до пятого), в регистре PortB они также расположены подряд, и ячейки SRAM, содержащие значения цифр, также идут подряд (начиная с TdH , см. секцию констант и определений)? Очень хочется как-то «свернуть» все шесть повторяющихся фрагментов в один, т. к. все равно все увязано со значением счетчика cRazr . Отсчитывать адрес, где хранится текущая цифра, несложно, просто прибавляя к начальному адресу ( TdH) значение счетчика. В основном же это естественное желание упирается в тот факт, что невозможно простым способом перевести двоичное значение некоего регистра (в данном случае cRazr ) в номер устанавливаемого бита в регистре PortB . Чтобы заменить «ручное» задание нужного бита в регистре PortB (см. пару команд ldi temp, i << RazrPdL/ out portb, temp ) на автоматическое в соответствии со значением cRazr , понадобится двоично-десятичный дешифратор, подобный по функциям микросхеме 561ИД1 (см. главу 8 ), программная реализация которого будет еще более громоздкой, чем данный алгоритм. Мы еще вернемся к этому вопросу в главе 17 , когда нам понадобится управлять большим количеством индикаторов.

После формирования цифры программа переходит к довольно запутанному, на первый взгляд, алгоритму работы АЦП. На самом деле он не так уж и сложен. Управляют этим процессом две переменных: счетчик циклов countCyk и счетчик преобразований count. Первый из них увеличивается на единицу каждый раз, когда происходит прерывание таймера. Когда его величина достигает 32 (т. е. когда устанавливается единица в бите 5, см. команду sbrs countCyk, 5), то значение счетчика сбрасывается для следующего цикла, и происходит запуск преобразования АЦП, причем для канала, соответствующего значению бита в регистре Flag, указывающего, что именно мы измеряем сейчас: температуру или давление. Таким образом измерения равномерно распределяются по времени.

Сами преобразования отсчитываются счетчиком count до 64 (т. е. цикл одного измерения занимает чуть более секунды: 32 х 64 = 2048 прерываний таймера, а в секунду их происходит примерно 1953). Когда это значение достигается, то мы переходим к обработке результатов по описанным ранее алгоритмам: сумма измерений делится на 64 (т. о. мы получаем среднее за секунду), затем вычитается или прибавляется значение «подставки», т. е. коэффициента Z, и полученная величина умножается на коэффициент К, точнее — на его целый эквивалент, полученный умножением на 1024. Произведение делится на это число и преобразуется к распакованному двоично-десятичному виду, отдельные цифры которого размещаются в памяти для последующей индикации. Как только очередной такой цикл заканчивается, меняется значение бита в регистре Flag, и таким образом давление и температура измеряются попеременно. В целом выходит, что значение каждой из величин меняется примерно раз в две секунды, и представляет собой среднее за половину этого периода.

Собственно результат измерения читается в прерывании АЦП (процедура по метке readADc), которое происходит автоматически по окончании каждого преобразования. В нем увеличивается значение счетчика count, извлекается из памяти предыдущее значение суммы показаний (в зависимости от регистра Flag — температуры или давления), считываются значения АЦП, суммируются и записываются обратно в память. Практически весь алгоритм мы описали — осталось только понять, как получить значения коэффициентов преобразования К и Z и затем выполнить точную калибровку.

Калибровка

Для того чтобы прибор заработал, в него необходимо ввести предварительные значения коэффициентов преобразования К и Z, причем желательно такие, чтобы они были достаточно близки к настоящим, и измеритель не показал бы нам «погоду на Марсе». В программе «зашиты» некие значения коэффициентов (см. процедуру Reset, секцию «Запись коэффициентов» в самом конце программы), которые подойдут вам, если вы не меняли характеристики схемы по рис. 15.2 и использовали тот же самый датчик давления. Как они получены?

Схема датчика температуры должна выдавать, как мы говорили, значение от 0 до 5 В в диапазоне температур примерно от -47 до 55 градусов. Следовательно, на 102 градуса у нас приходится 1024 градации АЦП, и крутизна характеристики составит 1020/1024 = 0,996 десятых градуса на единицу кода АЦП. Для вычислений в МК эту величину мы хотим умножить на 1024, так что можно было бы и не делить, ориентировочное значение коэффициента К так и будет 1020.

Величину Z, соответствующую 0 °C, вычислить также несложно. Мы полагаем, что нулю показаний соответствует температура -47°, тогда значение кода в нуле должно составить величину 470, поделенную на крутизну: 470/0,996 = 471.

Теперь разберемся с давлением. «Если повар нам не врет», то диапазон датчика, соответствующий изменению напряжения на его выходе от 0 до 4,6 В, составляет примерно 850 мм рт. ст. Это будет соответствовать изменению кодов примерно от 0 до 940 единиц, т. е. крутизна К равна 850/950 = 0,895 мм рт. ст. на единицу кода. В приведенном для наших расчетов виде это составит 0,895 х 1024 = 916. «Подставка» Z есть значение кода на нижней границе диапазона датчика, которая равна 11 мм рт. ст., соответственно, Z = 11/0,895 = 12 единиц. Полученные величины и «зашиваем» в программу.

После этого нужно сразу провести калибровку по температуре. Для этого следует запустить прибор и поместить датчик температуры в воду, записав для двух значений температур (как можно ближе к 0°, но не ниже его, и около 30–35 °C) показания датчика (/) и реальные значения температуры по образцовому термометру (О* Они, естественно, будут различаться. Для расчета новых (правильных) значений коэффициентов К' и Z' достаточно решить относительно них систему уравнений:

t ' 1 = К '( х 1 — Z ');

t 1 = К ( х 1 — Z );

t ' 2 = К '( х 2 — Z ');

t 2 = К ( х 2 — Z ).

Здесь величины со штрихами относятся к правильным (новым) значениям, а без штрихов — к старым, причем значение коэффициента К нужно подставлять в изначальной форме (а не умноженным на 1024). Система четырех уравнений содержит четыре неизвестных, два из которых (величины кодов х1 и х2) вспомогательные. Если вы забыли, как решаются такие простые системы, купите любой справочник по математике для средней школы (или книжку по использованию Excel в алгебраических расчетах). Вычисленные значения (не забудьте К умножить на 1024) «забейте» в программу и перепрограммируйте контроллер.

Аналогично калибруется канал давления, только коэффициент Z в уравнениях не вычитается, а прибавляется к х. Но самое сложное здесь — получить действительные значения давления. Далеко не все научные лаборатории располагают образцовыми манометрами для измерения столь малых давлений с необходимой точностью. Поэтому самый простой, хотя и долгий метод — сравнивать показания датчика с данными по давлению, которые публикуются в Интернете. Есть сайты, которые публикуют погоду каждые 3 часа (это т. н. метеорологический интервал). Лучшие и наиболее популярные из них — weather.yandex.ru и gismeteo.ru. Причем лучше не ограничиваться данными одной какой-то службы, а обращаться к нескольким, отбрасывая явные ошибки и усредняя правдоподобные данные, с учетом того, что они публикуются с некоторым запаздыванием (отметьте показания прибора, например, в 9:00, а в Интернет лезьте примерно в 11:00). Данные радио и телевидения использовать нежелательно, т. к. текущие значения могут сообщаться с опозданием на полсуток, либо вообще отсутствовать, а по завтрашнему прогнозу, естественно, вы ничего не откалибруете.

Для получения двух точек дождитесь, пока давление на улице не станет достаточно низким, а затем, наоборот, высоким — экстремальные значения давления в регионе Москвы составляют примерно 720 и 770 мм рт. ст. Чем дальше будут отстоять друг от друга значения, тем точнее калибровка. Для повышения точности можно усреднить коэффициенты, рассчитанные по нескольким парам значений давления, но это стоит делать только, если у вас хватит терпения вести наблюдения в течение нескольких месяцев, когда будет пройдено несколько минимумов и максимумов. Средние значения давления при калибровке лучше не учитывать, т. к. ошибка ее из-за узкого интервала и так достаточно велика.

Хранение констант в EEPROM

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

Логично придумать способ хранения констант, которые могут быть изменены в процессе эксплуатации, отдельно от программы. Для этой цели и служит энергонезависимая память данных, называемая EEPROM. Большинство МК семейства AVR имеют не менее 512 байт такой памяти, а младшие модели семейства Tuny — 64—128 байт. Для подавляющего большинства применений этого более чем достаточно. Не задерживаясь сейчас на вопросе, как осуществлять перезапись констант в процессе эксплуатации (этому вопросу будет посвящена глава 16), рассмотрим детали обращения с EEPROM.

Сохранность данных в EEPROM

Как мы уже говорили (см. главу 11), EEPROM и flash-память программ принципиально не отличаются, и предназначены для хранения данных в отсутствие питания. Однако между ними есть кардинальное различие: EEPROM может быть перезаписана в любой момент программой самого МК. В этом слабость всей системы: при снижении питания ниже определенных величин МК начинает совершать непредсказуемые операции, и EEPROM с большой вероятностью может быть повреждена. Для защиты от этой «напасти» (и вообще от выполнения каких-то операций, которые иногда могут навредить внешним устройствам) в AVR предусмотрена система BOD (см. главу 13), которая при снижении напряжения питания ниже определенного порога (4 или 2,7 В) «загоняет» МК в состояние сброса. Это помогает, но, как показывает опыт, для абсолютной защищенности данных в EEPROM, к сожалению, встроенной системы BOD недостаточно. Возможно, она недостаточно быстродействующая или в ней не слишком надежно фиксируется момент срабатывания, но факты свидетельствуют, что даже при включенной BOD данные все же могут быть повреждены.

Не исключено, что система BOD все время совершенствуется, но автор предпочитает не экспериментировать и использует самый надежный и проверенный способ с внешним монитором питания. Это небольшая микросхема (как правило, трехвыводная), которая при снижении питания ниже допустимого закорачивает свой выход на «землю». Если питание в пределах нормы, то выход находится в состоянии «разрыва» и никак не влияет на работу схемы. Присоединив этот выход к выводу Reset, мы получаем надежный предохранитель (рис. 15.5).

Рис. 15.5. Подсоединение внешнего монитора питания МС34064 к МК (схема из руководства фирмы Motorola)

Для напряжений питания 5 В один из самых популярных мониторов питания — микросхема МС34064, которая имеет встроенный порог срабатывания 4,6 В, выпускается в корпусе ТО-92 с гибкими выводами и обладает достаточно малым собственным потреблением (менее 0,5 мА). Время срабатывания у нее составляет при плавном снижении напряжения порядка 200 не, что достаточно для предотвращения выполнения «вредных» команд.

Если у вас питание автономное (от батарей), то к выбору монитора питания следует подходить довольно тщательно — так, чтобы не приводить схему в неработоспособное состояние тогда, когда батареи еще не исчерпали свой ресурс. При напряжении питания схемы 3,3 В пригодны приборы DS1816-10, MAX809S, при напряжении питания 3,0 В — DS1816-20 или MAX803R, а также некоторые другие.

Отметим, что рекомендуемый в фирменных описаниях способ защиты EEPROM вводом МК в состояние пониженного энергопотребления (см. главу 17) довольно сложно осуществить на практике. Если же вы все же умудритесь его задействовать, то следует учитывать, особенно в случае батарейного питания, что при резком снижении потребления напряжение источника немедленно повысится, что при относительно малом значении гистерезиса монитора питания (для МС34064 — 20 мВ) обязательно вызовет «дребезг» схемы. Увеличить гистерезис можно включением еще нескольких резисторов, но лучше обойтись вводом в режим сброса, как более простым и надежным способом.

Запись и чтение EEPROM

Запись и чтение данных в EEPROM осуществляется через специальные регистры ввода/вывода: регистр данных EEDR, регистр адреса EEAR (если объем EEPROM более 256 байт, то он делится на два — EEARH и EEARL) и регистр управления EECR. Основная особенность этого процесса — медленность процедуры записи, которая для разных моделей AVR может длиться от 2 до 9 мс, в тысячи раз дольше, чем выполнение обычных команд (обратим внимание, что в отличие от записи чтение осуществляется всего за один машинный цикл, даже быстрее, чем из обычной SRAM).

Для удобства проведения всех подобных процедур, которые могут длиться достаточно долго, в AVR предусмотрено соответствующее прерывание. В данном случае прерывание EEPROM может генерироваться по окончании очередного цикла записи, когда память свободна. Использовать его удобно, если нам требуется производить запись достаточно больших массивов: например, для 100 байтов запись может длиться почти секунду, и тормозить МК на весь этот период было бы неразумно. Тогда основная схема действий будет такой: разрешить прерывание EEPROM, и «внутри него» произвести запись очередного байта. Когда массив заканчивается, прерывания EEPROM запрещаются.

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

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

Листинг 15.7

WriteEEP:   ;в ZH:ZL — адрес EEPROM куда писать

;в temp — записываемый байт

        sbic EECR,EEWE   ;ждем очистки бита

       rjmp WriteEEP   ;разрешения записи EEWE

       out EEARH,ZH   ;старший адреса

       out EEARL,ZL   ;младший адреса

        out EEDR,temp   ;данные

            sbi EECR,EEMWE   ;установить флаг разрешения записи

           sbi EECR,EEWE   ;установить бит разрешения

ret  ;(конец WriteEEP)

Установленный нами бит разрешения EEWE в регистре управления сбросится автоматически, когда запись закончится — этого сброса мы и ожидаем в начале процедуры. Естественно, в самый первый раз никакого ожидания на самом деле не потребуется. На всякий случай то же самое рекомендуется делать и при чтении, но практически всегда (если только мы не читаем непосредственно после записи), это не будет задерживать программу дольше, чем на время выполнения команды sbic, т. е. на два машинных цикла. Так как при чтении не требуется устанавливать никаких флагов, то процедура получается несколько короче (листинг 15.8).

Листинг 15.8

ReadEEP:   ;в ZH: ZL — адрес откуда читать

;возврат temp — прочтенный байт

         sbic EECR,EEWE   ;ожидание очистки флага записи

         rjmp ReadEEP

        out EEARH,ZH   ;старший адреса

        out EEARL,ZL   ;младший адреса

        sbi EECR,EERE   ;бит чтения

        in temp,EEDR   ;чтение

ret   ;конец ReadEEP

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

Первичная запись констант в EEPROM

В принципе можно избежать процедуры записи вообще, если просто записать в EEPROM необходимые константы в процессе программирования. Это нужно делать отдельно от записи программы во Flash, с помощью специально подготовленного hex-файла. Но это ничем не будет отличаться от ситуации, когда константы хранятся в тексте программы, только программировать МК придется значительно дольше, особенно при отладке. Гораздо грамотнее будет не пожалеть труда и составить программу так, чтобы она сама записывала нужные константы «по умолчанию». Как это правильно сделать?

Разумеется, это следует сделать при запуске МК, в процедуре Reset. Но записывать константы каждый раз при включении питания не только не имеет смысла (тогда проще их опять же хранить в тексте), но и еще более неудобно для пользователя, чем установка часов, о которой шла речь в главе 14 — в дальнейшем мы научимся отдельно от программы записывать коэффициенты, не меняя текст программы, и хочется, чтобы это не требовалось делать после каждого сбоя питания. Тогда при удаче (если схема спроектирована верно и EEPROM надежно защищена от сбоев) автоматическая запись будет производиться один-единственный раз: при первом запуске контроллера, сразу после загрузки в его память программы, которую мы сейчас создадим.

Для этого нам потребуется как-то узнавать, есть ли уже в EEPROM какие-то данные, или нет, и правильно ли они записаны. Можно учесть тот факт, что в пустой EEPROM всегда записаны одни единицы (любой считанный байт будет равен $FF), но в общем случае это ненадежно. Наиболее универсальный способ — выделить для этого один какой-то байт в EEPROM, и всегда придавать ему определенное значение, а при загрузке МК его проверять. Это не гарантирует 100 %-ной надежности при сбоях (т. к. данные в незащищенной EEPROM могут меняться произвольно, в том числе и с сохранением значения отдельных байтов), но мы будем считать, что от сбоев защищены «двойной броней» (из внешнего монитора питания и встроенной схемы BOD), и нам важно только распознать ситуацию, когда требуется первичная запись в еще не заполненную память. Приборы, которые я проектировал таким образом, работали, не выключаясь годами, без единого сбоя загруженных констант.

Итак, общая схема алгоритма такая: читаем контрольный байт из EEPROM, если он равен заданной величине (обычно я выбираю чередование единиц и нулей: $АА), то это значит, что коэффициенты уже записаны. Если же нет, то записывает значения «по умолчанию», в том числе и значение этого контрольного байта. Далее в любом случае переходим к процедуре чтения из EEPROM и перегрузки записанных констант в SRAM, откуда они при необходимости извлекаются точно так же, как ранее в процедурах расчета физических величин. Так мы сможем ничего не менять в основной программе, описанной ранее в этой главе, а лишь дописать некий текст в секции начальной загрузки.

Пусть значения коэффициентов записываются в EEPROM с самого начала (с адреса 0:0, в том же порядке, в котором они расположены в SRAM), а по адресу $10 записывается контрольный байт, равный $АА. Тогда в программе, приведенной в Приложении 5, в конце процедуры начальной загрузки по метке reset вместо всего фрагмента, начинающегося с заголовка «запись коэффициентов» до команды sei (обязательно перед ней, а не после) добавляется текст листинга 15.9.

Листинг 15.9

;чтение коэффициентов из EEPROM =====

       clr ZH   ;ст. адрес =0

        ldi ZL,$10   ;адрес контрольного байта

        rcall ReadEEP

       cpi temp,$AA   ;если он равен $AA

        breq mm_RK   ;то на чтение в ОЗУ

rcall ZapisK   ;иначе запись значений по умолчанию

mm_RK:   ;извлечение коэфф. из EEPROM в SRAM

        clr ZL   ;начальный адрес EEPROM 0:0

          ldi YL,tZH   ;начальный адрес SRAM, см. основной текст

LoopRK:

        rcall ReadEEP   ;читаем байт

         st Y+,temp   ;складываем в ОЗУ

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

        cpi ZL,8   ; всего 4 коэффициента, 8 байт

         brne LoopRK

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

Листинг 15.10

ZapisK:

;запись предварительных коэффициентов по умолчанию

        clr ZH   ;с нулевого адреса в EEPR

       clr ZL

; Z tempr=471

        ldi temp,High(471)   ;ст.

        rcall WriteEEP

       inc ZL

       ldi temp,Low(471)   ;мл.

        rcall WriteEEP

       inc ZL

; К tempr=1020

        ldi temp,High(1020)   ;ст.

        rcall WriteEEP

       inc ZL

       ldi temp,Low(1020)   ;мл.

       rcall WriteEEP

       inc ZL

; Z press=12

       ldi temp,0x00   ;ст.

       rcall WriteEEP

       inc ZL

       ldi temp,12   ;мл.

        rcall WriteEEP

       inc ZL

; К prs=916

        ldi temp,High(916)   ;ct.

       rcall WriteEEP

       inc ZL

       ldi temp,Low(916)   ;мл.

       rcall WriteEEP

       ldi ZL,$10

       ldi temp,$AA   ;все Ok, записываем

        rcall WriteEEP   ;контрольный байт

ret

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

Конечно, иногда может понадобиться запись какой-то константы по ходу работы программы: например, если вы делаете электронный регулятор уровня какой-то величины (громкости, освещения, яркости свечения), то будет очень правильно записывать текущее значение в EEPROM, чтобы при следующем включении восстанавливалось установленное состояние, и пользователю не приходилось бы делать регулировку заново. Только при этом следует учесть, что EEPROM все же не RAM, и запись в нее, во-первых, имеет ограниченное (хотя и большое — до 100 000) число циклов, во-вторых, протекает на много порядков медленнее, а в-третьих, ведет к повышенному расходу энергии. Потому использовать EEPROM как ОЗУ, конечно, не стоит.

Кроме записи констант, наиболее часто EEPROM служит для хранения, например, заводского номера и названия прибора, фамилии конструктора-программиста или названия фирмы-изготовителя, и всякой другой полезной информации (ср. данные, которые извлекает операционная система ПК при подсоединении устройства plug&play, например, через USB). Можно заполнять различные поля, вплоть до серийного номера, и вести базу выпущенных экземпляров. Несложно сделать и так, чтобы эта информация выдавалась «наверх» автоматически при подсоединении прибора к компьютеру с загруженной программой, и текущие значения параметров выводились в отдельном окне — тогда можно обойтись без громоздкой и «прожорливой» индикации и получить компактный компьютерный «прибамбас». Только вот как все эти данные извлекать и при необходимости изменять, не затрагивая самой программы? Для этого существуют последовательные интерфейсы, к рассмотрению которых мы сейчас и перейдем.

 

Глава 16

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

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

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

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

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

UART и RS-232

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

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

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

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

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

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

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

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

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

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

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

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

Подробности

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

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

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

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

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

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

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

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

Замечание

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        out UBRRL,temp

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

         out UCSRB,temp

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

         out UCSRC,temp

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

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

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

Листинг 16.1

UART_RXC:

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

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

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

      sbi UCR,RXCIE

      reti

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

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

reti

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

Листинг 16.2

UART_DRE:

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

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

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

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

reti

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

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

Листинг 16.3

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

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

           rjmp out_com

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

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

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

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

            rjmp in_com

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

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

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

Cykle:

        rcall In_com

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

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

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

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

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

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

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

move temp,RegX

rcall Out_com

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

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

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

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

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

Gcykle:

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

        breq ргос_Е0

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

         breq ргос_Е2

rjmp Gcykle

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

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

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

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

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

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

          rcall WriteKoeff

rjmp Gcykle

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

Листинг 16.4.

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

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

        ldi ZH,1

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

LoopWR:

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

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

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

        brne LoopWR

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

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

        ldi YH,1

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

LoopWE:

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

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

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

        cpi ZL,8

        brne LoopWE

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

         rcall out_com

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

ret

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

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

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

          rcall ReadKoeff

rjmp Gcykle

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

Листинг 16.5

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

cli

           clr ZH

           clr ZL

LoopRE:

           rcall ReadEEP

           rcall out_com

           inc ZL

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

            brne LoopRE

sei

ret

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

           ldi cnt,NN

cyk_delay: dec cnt

           brne cyk_delay

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

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

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

            push cnt

           ldi cnt,6

cyk_delay: dec cnt

           brne cyk_delay

           pop cnt

ret

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подробности

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

     st Z+,AregH

     st Z,AregL

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

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

     st Z+,AregH

     st Z,AregL

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

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

      ldi temp,high(58594)

    out OCR1АН, temp

    ldi temp,low(58594)

    out OCR1AL,temp

    ldi temp,0b01000000

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

    ldi temp,0b00001101

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

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

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

    out TIMSK,temp

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

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

Листинг 16.6

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

        clr ZH   ;старший EEPROM

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

        rcall ReadEEP

       mov AddrL,temp

       ldi ZL,EaddrH

       rcall ReadEEP

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

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

        ср AddrL,temp

       ldi temp,0xFF

       cpc AddrH, temp

       brne cont_1

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

        clr AddrL

       clr ZH   ;старший EEPROM

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

        mov temp,AddrL

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

        inc ZL

       mov temp,AddrH

       rcall WriteEEP

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

        ldi temp,0xFF

       cp AddrL,temp

       ldi temp,0x7F

       cpc AddrH, temp

       brne cont_2

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

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

        clr ZH   ;старший EEPROM

        ldi ZL,FEnEE

       rcall ReadEEP

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

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

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

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

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

Листинг 16.7

TIM1_COMPA:   ;15 секунд

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

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

          ldi ZL,FEnRAM

          ld temp,Z

          cpi temp,$FF

          breq flag_WF

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

flag_WF:

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

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

          rcall WriteFlash   ;пишем во flash

          adiw AddrL,1

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

           rcall WriteFlash

          adiw AddrL,1

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

           rcall WriteFlash

          adiw AddrL,1

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

           rcall WriteFlash

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

           ldi temp,0xFF

          cp AddrL,temp

          ldi temp,0x7F

          cpc AddrH, temp

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

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

           clr ZH

          ldi ZL,EaddrL   ;в EEPROM

           mov temp,AddrL

          rcall WriteEEP

          inc ZL

          mov temp,AddrH

          rcall WriteEEP

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

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

          clr temp

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

           ldi ZL,FEnRAM

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

          clr ZH   ;старший EEPR

           ldi ZL,FEnEE

          rcall WriteEEP

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

           clr temp

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

breq proc_F8

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

Листинг 16.8

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

         rcall EnableFlash

rjmp Gcykle

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

         rcall DisFlash

rjmp Gcykle

EnableFlash:

cli

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

        sbrc Flag,2

        rjmp exit_FE

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

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

         ldi ZL,FEnRAM

        ld temp,Z

        cpi temp,$FF

        breq exit_FA

        ldi temp,$FF

        st Z,temp

        clr ZH   ;старший EEPR

         ldi ZL,FEnEE

        rcall WriteEEP

        ldi temp,$AA   ;все Ok

        rcall out_com

sei

ret

exi t_FA:

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

       rcall out_com

sei

ret

exit_FE:

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

        rcall out_com

sei

ret

DisFlash:

cli

       clr temp

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

        ldi ZL,FEnRAM

       st Z,temp

       clr ZH   ;старший EEPR

        ldi ZL,FEnEE

       rcall WriteEEP

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

       rcall out_com

sei

ret

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

Листинг 16.9

proc_F2:   ;F2 читать flash

         rcall ReadFullFlash

rjmp Gcykle

ReadFullFlash:

cli

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

        mov YL,AddrL

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

        clr AddrH

loopRF:

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

         срс AddrH, YH

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

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

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

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

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

         rjmp loopRF

end_RF:

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

        mov AddrL,YL

sei

ret

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

Листинг 16.10

proc_F8:   ;F8 clear address

        rcall ClearAddr

rjmp Gcykle

ClearAddr:

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

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

         clr AddrL

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

        ldi ZL,EaddrL

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

         rcall WriteEEP

        inc ZL

        mov temp,AddrH

        rcall WriteEEP

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

        rcall out_com

sei

ret

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

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

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

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

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

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

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

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

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

Листинг 16.11

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

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

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

           rcall err_i2c

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

      mov DATA,temp

     rcall write_i2c

     brcs stopW

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

      rcall out_com

ret

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

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

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

      rcall write_i2c

     brcs stopW

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

     rcall out_com

ret

stopW:

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

             rcall out_com

ret

err_i2c:

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

              rcall out_com

sei

ret

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

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

Листинг 16.12

ReadSet:

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

             rcall err_i2c

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

        rcall read_i2c

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

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

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

brcs stopW

ret

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

Листинг 16.13

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

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

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

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

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

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

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

           rjmp setRAM

setsek:

          clr temp

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

st_clk:

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

setRAM:

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

...

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

Листинг 16.14

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        ldi ZH,0x01;

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

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

        mov count_sek,temp

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

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

         ldi data,10

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

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

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

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

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

                 ld temp,Z

                mov data,temp

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

                ldi ZL,DeH

                st Z,temp

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

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

                 ldi ZL,DdH

                st Z,data

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

                 ld temp,Z

                mov data,temp

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

                ldi ZL,DeM

                st Z,temp

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

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

                ldi ZL,DdM

                st Z,data

ret

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

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

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

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

           sbis PinC,pSDA

           rcall err_i2c

           rcall start

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

            rcall write

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

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

            rcall write

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

            rcall start

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

            rcall write

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

           set;CK

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

           rcall stop

ret

stopR:

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

            rcall out_com

ret

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

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

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

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

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

Листинг 16.15

Gcykle

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

         breq ргос_А1

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

         breq ргос_А2

rjmp Gcykle

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

              rcall SetTime

rjmp Gcykle

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

              rcall ReadTime;

rjmp Gcykle

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

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

                      ld temp,Z;

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

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

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

                      ld temp,Z;

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

                      add temp,r 0  ;получили hex

ret

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

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

             ldi ZL,Sek   ;Ram

         rcall in_com

         st Z+,temp   ;sek

          rcall in_com

         st Z+,temp   ;min

         rcall in_com

         st Z+,temp   ;hour

         rcall in_com

         st Z+,temp   ;data

         rcall in_com

         st Z+,temp   ;month

         rcall in_com

         st Z,temp   ;year

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

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

pop cnt

ret

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

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

               rcall err_i2c

              ldi ZH,0x01

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

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

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

           brcs stops

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

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

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

           brcs stopS

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

          ld DATA,Z+

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

           brcs stopS

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

          ld DATA,Z+

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

          brcs stopS

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

          ld DATA,Z+

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

           brcs stopS

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

          ld DATA,Z

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

           brcs stopS

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

           ldi DATA, 0Ь00010000

          rcall write_i2c

          brcs stopS

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

          rcall out_com

   ret

stopS:

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

                 rcall out_com

ret

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

cli

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

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

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

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

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

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

                 ld temp,Z

                 mov data,temp

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

                  ldi ZL,DeH

                 st Z,temp

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

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

                 ldi ZL,DdH

                 st Z,data

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

                  ld temp,Z

                 mov data,temp

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

                  ldi ZL,DeM

                 st Z,temp

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

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

                ldi ZL,DdM

                st Z,data

sei

ret

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

cli

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

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

           ldi ZL,Hour

           ld temp,Z

           rcall out_com   ;hour

           ldi ZL,Min

           ld temp,Z ;

            rcall out_com   ;min

            ldi ZL,Sek

           ld temp,Z ;

           rcall out_com   ;sek

            ldi ZL,Date

           ld temp,Z+ ;

           rcall out_com   ;data

           ld temp,Z+ ;

           rcall out_com   ;month

            ld temp,Z;

           rcall out_com   ;year

sei

ret

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

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

Итак, вычеркнем опять из начального запуска процедуру инициализации Timer 1 (всю секцию Set Timer 1, вернув вместо нее ldi temp, (1<

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

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

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

out MCUCR,temp

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

out GICR,temp

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

out GIFR,temp

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Схема измерителя совпадает с приведенной на рис. 15.2, если добавить к ней изменения, представленные на рис. 16.5 и индикацию. Для индикации часов в программе предусмотрены незанятые выводы порта А (с 4 по 7). Их следует присоединить к индикаторам по схеме, аналогичной остальным (как в часах из главы 14). Если вы не хотите использовать индикацию часов, то лучше вернуть процедуру по прерыванию Timer 0 в то состояние, которое она имеет в программе измерителя без часов (см. Приложение 5), тогда на каждый разряд будет приходиться относительно большая часть времени индикации. Еще один момент в схеме, который мы пока не обсуждали полностью — это соединение с компьютером, напомню, что его мы будем разбирать в главе 18.

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

 

Глава 17

«Зеленые» микросхемы

Глупо тратить энергию только на поддержание работоспособности МК: как и ПК, основное время современные контроллеры тратят на ожидание прерывания или внешних команд. Даже самая громоздкая процедура (не считая медленных по самому принципу действия алгоритмов передачи данных к внешним устройствам и записи в EEPROM), состоящая, к примеру, из тысячи команд процессора, будет выполнена в МК с весьма невысокой тактовой частотой 4 МГц примерно за треть миллисекунды, а то и быстрее. Остальное время контроллер будет простаивать, но потреблять практически столько же, сколько в рабочем режиме. Хочется придумать механизм, который бы позволял «будить» процессор только тогда, когда это требуется. Оказывается, это очень непросто.

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

И на практике режимы энергосбережения в большинстве случаев целесообразны только тогда, когда ваша схема работает от автономного источника. Посчитайте: типичный AVR семейства Mega потребляет около 15 мА при напряжении питания 5 В. Таким образом, щелочные батарейки самого распространенного для таких устройств типа АА (см. Приложение 2) при емкости примерно 2000 мА ч проработают всего 133 часа или чуть больше 5,5 суток — это не считая потребления внешних устройств.

При использовании режима энергосбережения следует очень тщательно продумывать и схему, и саму процедуру перехода в этот режим. Может случиться, что какие-то внешние устройства у вас потребляют больше, чем сам контроллер, тогда все ваши усилия пойдут насмарку. Например, в схеме часов из главы 14 (там энергосбережение нецелесообразно, но я использую его схему, как наглядный пример) разряды порта D и В управляют транзисторными ключами. Если мы введем МК в режим энергосбережения, то процессор остановится, но состояния портов останутся такими, какими они были к моменту остановки. И если вдруг они случайно оказались в состоянии высокого уровня, то на каждый вывод придется немного менее чем по 1 мА вытекающего тока. При всех включенных выводах это около 10 мА, что сравнимо с потреблением самого контроллера. Какое уж тут энергосбережение…

О режимах энергосбережения AVR

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

Что касается самих режимов, то принципиально отличаются от остальных лишь режим Idle (ждущий) и ADC Noise reduction. В первом из них отключение касается лишь центрального процессора, а все остальные устройства продолжают функционировать. При этом потребление микросхемы уменьшается всего лишь на треть, и на фоне общего расхода энергии всей схемы с учетом внешних устройств такая экономия теряется. Режим Idle удобен тем, что «просыпание» происходит мгновенно по любому прерыванию. В режиме ADC Noise reduction (подавления шумов АЦП — мы его не задействовали), кроме отключения процессорного ядра, отключаются также порты ввода/ вывода, в остальном он аналогичен Idle. Его используют только по прямому назначению, т. к. питание также практически не экономится, а выводить МК из этого режима сложнее, чем в случае Idle.

С точки зрения энергосбережения более интересны остальные режимы, которые не очень сильно отличаются друг от друга. Во всех моделях AVR без исключения имеется наиболее универсальный режим Power Down, при котором отключаются все внутренние тактовые сигналы. Потребление МК при этом снижается до нескольких микроампер. Вывести МК из такого состояния можно лишь внешним прерыванием (и то не всяким, о чем далее) или сбросом, т. е. выключением/включением питания, подачей внешнего сигнала Reset или сигналом от сторожевого таймера (последним мы займемся в конце этой главы). Вывод из состояния «сна» в режиме Power Down занимает довольно много времени, и это время еще требуется контролировать с помощью конфигурационных битов.

Потому мы здесь будем рассматривать его модификацию под названием Standbye. В этом режиме выключается все, кроме тактового генератора, что несколько повышает потребление (до десятков микроампер), зато вывод из «сна» занимает всего 6 машинных тактов. Так как Standbye имеется не во всех AVR семейства Mega, а в семействах Classic и Tuny его нет вообще, то для этих МК можно без переделок задавать обычный режим Power Down, не забывая только, что пробуждение тогда может длиться до более чем 10 мс (это важно, например, если МК управляется случайными сигналами и промежуток между ними может быть короче времени «пробуждения» — сигнал попросту потеряется).

Внешние прерывания, которыми можно вывести МК из режима Standbye, — это прерывания INTO, INTI и INT2 (если последнее имеется в данной модели). Однако сначала требуется тщательно изучить их режимы, т. к. они делятся на синхронные и асинхронные. Мы уже знаем из главы 12, что синхронные прерывания — это те, которые обнаруживаются лишь в момент прихода фронта тактового импульса. Их преимущество в практически мгновенном обнаружении сигнала на прерывание, но в режиме «сна», когда тактовые сигналы отключены, они, естественно, не могут быть обнаружены. К синхронным относятся прерывания по фронту и по спаду INTO и INT1. Режим прерывания по низкому уровню асинхронный, потому он пригоден для «пробуждения» МК. Кроме этого, очень удобно, если МК поддерживает прерывание INT2, поскольку оно имеет режимы только по фронту или по спаду, которые обнаруживаются асинхронно, в отличие от INTO и INT1 (из основных представителей семейства Mega INT2 нет только у ATmega8).

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

Измеритель давления и температуры в автономном режиме

Покажем возможную схему действий на примере нашего измерителя температуры и давления из главы 15 (без часов и записи в память). Схема его была приведена на рис. 15.2, и мы помним, что там не показана индикация (она такая же, как у часов на рис. 14.2, только используются другие разряды портов). В принципе можно было бы использовать ЖК-индикаторы, и тогда срок службы батарей сильно увеличивается (и их количество можно уменьшить, см. далее), но это потребует заметного изменения системы управления индикаторами (на ЖК, напомню, надо подавать переменное напряжение). К тому же подобрать удобный и эстетичный ЖК-индикатор для нашей конкретной цели — тоже отдельная и непростая задача. Чтобы не уходить в сторону от темы, я решил не останавливаться на этом вопросе: оставим LED-индикаторы, как более яркую иллюстрацию особенностей программирования режима энергосбережения.

Не забудем, что аналоговая часть потребляет сама по себе не менее 3 мА, потому ее придется отключать принудительно. Вариант батарейного источника питания для измерителя в автономном режиме, сделанный с учетом этого обстоятельства, показан на рис. 17.1. Один набор батарей служит здесь для обеспечения всех напряжений, причем напряжение аналоговой части (положительное и отрицательное) формируется с помощью инвертора, входящего в состав микросхемы P6BU-0505Z фирмы PEAK Electronic. Входное напряжение для этого DC/DC-преобразователя берется от стабилизатора цифровой части схемы.

Рис. 17.1. Батарейный источник питания для измерителя температуры и давления

Микросхема P6BU-0505Z может быть заменена на аналогичные изделия других фирм, только следите за характеристиками: так, преобразователи TMR-3 фирмы TRACO имеют встроенную возможность отключения, могут работать от нестабилизированного входного напряжения (т. е. прямо от батарей), но если присмотреться внимательно, то окажется, что они плохо работают при малых токах потребления (менее 20 % от номинала) и даже в режиме StandBye потребляют до 10 мА, что, конечно, неприемлемо в нашем случае.

Выбранная микросхема потребляет на холостом ходу гораздо меньше (порядка 1 мА), но встроенной возможности отключения не имеет, потому приходится вводить отдельный ключ (электронное реле КР293КП5В с контактами на замыкание), отключающий аналоговую часть при переходе в режим «сна». Для включения/отключения используем разряд 6 порта D (вывод 20 микросхемы ATmega8535), который придется устанавливать на выход и с нулевым уровнем, иначе схема не заработает.

Если пренебречь разделением аналогового и цифрового положительного питания, то P6BU-0505Z можно заменить на доступный и дешевый (порядка 1 долл.) преобразователь напряжения +5 В в одно напряжение -5 В под названием ICL7660 (он же МАХ1044, он же 1168ЕП1), который включается примерно по той же схеме, но требует дополнительно еще двух конденсаторов. Главный недостаток этой микросхемы — невысокая стабильность выходного напряжения, из-за чего точность измерений заметно снизится.

Индикаторы питаются нестабилизированным напряжением от 6 элементов питания, т. к. минимальное напряжение, при котором индикаторы еще горят, равно приблизительно 6 В. Среднее же напряжение во время эксплуатации будет около 8 В, соответственно ограничительные резисторы тока сегментов (R27—R34 на рис. 14.2), которые в сетевой схеме были равны 470 Ом, следует уменьшить как минимум вдвое — примерно до 200–220 Ом.

Для щелочных батареек типа АА общее время работы до исчерпания их ресурса составит около 10 часов (потребление тока шестью одновременно включенными индикаторами по схеме рис. 14.2, с учетом уменьшения сопротивлений составит максимум 200 мА), что для наших целей приемлемо. Если хотите увеличить время работы, и габариты позволяют, то следует выбрать более емкие батареи — так, со щелочными батареями типа D (особенно модели Duracell Ultra) наш прибор проработает около недели.

Задачу сформулируем так: пусть по первому нажатию внешней кнопки контроллер «просыпается», а по второму — «засыпает». Кроме этого, введем режим автоматического «засыпания» по истечении некоторого промежутка времени. Чтобы пользоваться прибором было удобно, этот промежуток должен быть достаточно большим: не менее минуты, так что нам придется потрудиться (встроенный таймер, как нам известно, обеспечит лишь интервал порядка 16 с). Разумеется, при выключении контроллера должна отключаться и индикация.

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

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

Использование режима энергосбережения

В случае использования прерывания по низкому уровню, как мы уже знаем из главы 12, при первом же возникновении его следует запретить, иначе оно будет происходить непрерывно, пока действует низкий уровень. Это не очень удобно, когда прерывания управляются от внешних сигналов в форме меандра (как от часов из главы 16), потому что возникает вопрос — а когда разрешать его снова? Если перед уходом в «сон», то, в силу длительности состояния низкого уровня на выводе прерывания, оно тут же произойдет опять, и МК вообще никогда (в течение текущего полупериода) не «заснет». И управление «засыпанием» и прерываниями очень сильно усложняется. В таких случаях лучше будет задействовать прерывание INT2, которое не нужно запрещать (дребезг в таких случаях исключен), и соответствующую модель контроллера.

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

Кнопка (без фиксации) подсоединяется к выводу INTO (PD2, вывод 16 для ATmega8535). Если кнопка имеет перекидной контакт (т. е. три вывода), она подсоединяется к «земле» и питанию так, как показано на рис. 8.3, б (вместо элемента «исключающее ИЛИ» выступает, естественно, наш МК). Чаще встречаются кнопки с двумя выводами, тогда их подсоединяют так, как показано на рис. 17.2. В обоих случаях при ненажатой кнопке на выводе должен быть потенциал питания, т. е. высокий уровень, а при нажимании вывод коммутируется на «землю».

Рис. 17.2. Подсоединение кнопки с двумя выводами к МК АТтеда8535

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

Еще раз напомню (см. главу 12 ), что в принципе от резистора на схеме рис. 17.2 можно отказаться, поскольку специально для таких случаев в AVR предусмотрено подключение внутреннего «подтягивающего» резистора. Однако для надежности ставить его все же следует (так же, как и по выводам программирования, см. описание схемы к рис. 14.2 в главе 14 ), т. к. номинал встроенного «подтягивающего» резистора достаточно велик (минимум 35 кОм, согласно руководству), то на нем могут возникать наводки, которые приведут к ложным срабатываниям кнопки. Вы можете попробовать исключить резистор из схемы и убедиться в этом сами — ложные срабатывания появятся обязательно. И уж наверняка ложное прерывание будет возникать в ситуации, описанной в главе 14 для кнопки Кн1, когда питание внезапно переключается с сети на батареи. Так как мы тут такой режим не используем, то конденсатор параллельно кнопке (см. схему подключения Кн1 на рис. 14.2) ставить необязательно.

Доработка программы

Доработку программы измерителя (напоминаю, что за основу берем листинг П5.2, приведенный в Приложении 5, в разделе «Программа измерителя температуры и давления») начнем с того, что, во-первых, включим питание аналоговой части. Для этого в разделе начальной загрузки, там, где устанавливаются порты («установка портов вход/выход»), вместо команды ldi temp, 0b10000000 (перед out DDRD, temp) запишем ldi temp, 0b11000000. Теперь вывод 20 сконфигурирован на выход и для надежности следует добавить еще оператор cbi PortD, 6 (т. к. включается ключ низким уровнем).

Во-вторых, инициализируем таймер (Timer 1). В пробной программе записи во flash из главы 16 мы его настраивали ровно на 15 с, а здесь выберем вдвое меньший интервал — 7,5 с. В загрузочную секцию вместо строк инициализации Timer 0 (ldi temp, (i<

Листинг 17.1

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

        ldi temp,high(292 97)

       out OCR1AH,temp

       ldi temp,low(2 9297)

       out OCR1AL,temp

       ldi temp,0b00001101

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

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

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

        out TIMSK,temp

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

def count_min = r23   ;счетчик 7,5-секундных интервалов

А в секции начальных установок его не забудем обнулить:

clr count_min

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

Листинг 17.2

Gcykle:

    sbrs Flag,7   ;если бит 7 установлен, то засыпаем

гjmp Gcykle   ;иначе бесконечный цикл

cli   ;на всякий случай запрещаем прерывания; все порты на вход, и нули в разряды, кроме PortD,6

     clr temp

    out DDRB,temp

    out DDRC,temp

    out DDRD,temp

    out PortB,temp

    out PortC,temp

    ldi temp,0b01000000  ;выключение питания на всякий случай

     out PortD,temp

    ldi temp,0b11100000   ;разрешение Sleep, режим Standby прерывание по уровню

     out MCUCR,temp

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

     out GICR,temp

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

Sleep   ;наконец, спим

     cbr Flag, $80   ;по выходу из сна сбрасываем флаг sleep

     clr count_min   ;отсчет времени

;сначала установка портов вход-выход обратно

cli  ;на всякий случай запрещаем прерывания

     ldi temp,0b00111111   ; разряды out DDRB,temp

     ldi temp,0b01111111   ; сегменты out DDRC,temp

     ldi temp,0b11000000   ; знак минус и питание

     out DDRD,temp

    clr temp

    out PortD,temp   ;включить аналоговое питание

     out MCUCR,temp   ;запрещаем режим Sleep

    out TCNT1H,temp   ;очищаем счетные регистры таймера

     out TCNT1L,temp

     ldi temp,0b00001101   ; запускаем таймер

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

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

rjmp Gcykle   ;бесконечный цикл

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

Листинг 17.3

ТIМ1_СОМРА:

   inc count_min

   cpi count_min,1   ;через 7,5 с разрешаем INTO

   brne schet_time

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

   out GICR,temp   ;GIMSK и GIGR — синонимы

schet_time:   ;здесь отсчет времени

    sbrs count_min,3   ;если бит 3=1, то прошло 8 интервалов =1 мин

reti  ;иначе выходим

   clr count_min   ;в след, раз — сначала

   sbr Flag,$80   ;устанавливаем бит sleep

   clr temp   ;останавливаем таймер

   out TCCR1B,temp

reti   ;выходим

Если бы мы не связывались с кнопкой, то на этом можно было бы закончить — через 1 минуту после включения у нас измеритель уходит в «сон», из которого его можно вывести только выключением-включением питания или подачей сигнала Reset. Но мы хотим все делать грамотно, потому используем кнопку. Прерывание INTO тогда будет выглядеть так, как показано в листинге 17.4 (также не забудьте заменить в таблице прерываний во второй строке reti на rjmp EXT_INT0).

Листинг 17.4

EXT_INT0:

    clr temp

    out GICR,temp   ;запрещаем внешние прерывания

sbrs Flag,7  ;если были во сне, то больше ничего не делаем

reti

     clr temp   ;иначе готовимся ко сну

     out TCCR1B,temp   ;останавливаем таймер

      clr temp   ;чистим счетные регистры таймера

     out TCNT1H,temp

     out TCNT1L,temp

     ldi temp,0b00001101   ;заново запускаем

      out TCCR1B,temp

ldi count_min,7   ;на интервал 7,5 с

reti

Теперь у нас измеритель будет работать как задумывали: после включения через 1 мин. он уходит в режим энергосбережения, когда индикаторы гаснут, и потребление минимизируется. Если нажать на кнопку, то МК «проснется» и выполнит все процедуры после команды Sleep. Если ничего не делать, то через минуту измеритель опять «заснет». Если нажать до истечения этого срока на кнопку (но не ранее, чем через 7,5 с), то он «заснет» через 7,5 с после нажатия. В принципе таймер можно было бы и не останавливать перед засыпанием, но так мы более уверены, что он начнет отсчет с самого начала.

Время задержки вы легко можете регулировать, просто меняя число, которое загружается в регистры OCR1AH/L. Оно рассчитывается исходя из формулы: время задержки в секундах равно частоте кварца в герцах, деленному на коэффициент предварительного деления (1024) и на это число. Например, если вместо 29 297 загрузить 11 719, то пауза до засыпания по нажатию кнопки станет равной 3 с, а время работы сократится менее чем до полуминуты. Чтобы увеличить время «бодрствования» вдвое, команду sbrs count_min,3 в прерывании таймера нужно заменить на sbrs count_min,4.

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

В прерывании кнопки для исключения случайного попадания в конец отсчитываемого интервала времени используется очистка регистров таймера. Тогда таймер начнет считать сначала, пока не достигнет заданного числа. Если вам потребуется задать отсчитываемый интервал очень точно, то следует почистить также счетчик предделителя (в семействе Classic такой возможности не было). Для этого в регистре SFIOR нужно записать единицу (не ноль!) в бит, соответствующий таймеру: для Timer 1 (а также Timer 0, так как у них предделитель общий) это будет бит 0 под названием PSR10 . Этого не нужно делать при установленном счете напрямую (с коэффициентом деления тактовой частоты 1/1), в остальных случаях при запуске таймера в произвольный момент времени счетчик предделителя начнет со случайного числа (за исключением момента начального запуска при включении питания). Чем меньше коэффициент деления и чем больше число, отсчитываемое таймером, тем меньше относительная погрешность, но абсолютная ошибка всегда может достигнуть величины интервала между отчетами таймера (при коэффициенте 1/1 она попросту неустранима, только и всего).

Вторая особенность таймеров семейства Меда — наличие асинхронного режима работы для одного из таймеров (в большинстве моделей это Timer 2). Тогда его можно завести от независимого источника импульсов (от внешней частоты или низкочастотного кварца, в том числе часового 32 768 Гц), и он может считать независимо от работы всей остальной схемы. Теоретически эту функцию можно использовать как RTC, но на практике это неудобно (нет ни счета времени, ни календаря), зато ее очень удобно применять для вывода МК из «сна» по времени. В режиме энергосбережения под названием Power Save МК будет «просыпаться» каждое прерывание от Timer 2, и подсчетом этих прерываний его можно окончательно «разбудить» автоматически через нужный промежуток времени.

Использование сторожевого таймера

Сторожевой (watchdog) таймер — одно из самых полезных устройств в составе микроконтроллеров AVR. Причем это неочевидно: в нормальном режиме работы, когда все настроено идеально, он вовсе не нужен. Но представьте себе такую ситуацию: МК настроен на прием данных от компьютера, причем по простейшей схеме из главы 16, с непрерывным опросом бита UDRE. К примеру, он ожидает шесть байтов, как в нашей программе, но на пятом байте ПК внезапно ломается (кто-то прошел и ногой выдернул провод из COM-порта). Что будет с контроллером? Естественно, он повиснет в ожидании байта, и из этого состояния его не удастся вывести никаким способом, кроме полного сброса. Еще более опасны в этом отношении наши процедуры опроса линии по интерфейсу I2С — при программировании для ПК нам бы «голову оторвали» за такую организацию процесса.

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

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

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

Для примера разберем такой случай возможного использования сторожевого таймера. В измерителе с часами, который был описан в главе 16, опасная ситуация может возникнуть, если по каким-то причинам не «придет» прерывание от часов. Это может произойти, например, если сами часы «повисли» (у них ведь тоже достаточно сложный алгоритм) или просто «потерялся» фронт очередного импульса. А так как мы настраиваемся на круглогодичную работу этого прибора, то подобную ситуацию следует рассматривать как вероятную. В конце концов, космическая частица может прилететь и все нарушить — согласно некоторым исследованиям, сбои такого рода неизбежны с вероятностью примерно 1 сбой на 1000 часов работы (правда, автор этих строк лично ничего такого не наблюдал, но и специально вопрос не исследовал). В этом случае неплохо выполнить процедуру включения прибора заново: в начале программы мы в том числе инициализируем и часы, а уж если они совсем сломались, тут ничего не поделаешь.

Для этого перед началом основного цикла инициализируем сторожевой таймер:

;запускаем WDT на 2 сек:

wdr  ;команда на сброс — так рекомендуется

ldi temp,(1<<WDCE)|(1<<WDE)

out WDTCR,temp

ldi temp,(1<<WDP0)|(1<<WDP1)|(1<<WDP2)|(0<<WDCE)|(1<<WDE)

out WDTCR,temp

Теперь осталось по каждому прерыванию от часов (INTO) просто сбрасывать сторожевой таймер:

EXT_INT0:

wdr   ;сброс сторожевого таймера

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

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

Листинг 17.5

proc_F2:   ;F2 читать flash

cli  ;запрещаем прерывания выключить WD:

        wdr   ;Reset WDT

        in temp, WDTCR

        ori temp,(1<<WDCE)|(1<<WDE)

        out WDTCR, temp

        ldi temp,(0<<WDE)   ;выключить WDT

        out WDTCR, temp

rcall ReadFullFlash   ;читаем данные

     ;запускаем WDT обратно, 2 с

      wdr   ;команда на сброс

     ldi temp,(1<<WDCE)|(1«WDE) out WDTCR,temp

     ldi temp,(1«WDP0)|(1«WDP1)|(1<<WDP2)|(0<<WDCE)|(1«WDE)

     out WDTCR,temp

sei  ;разрешаем прерывания — необязательно, уже есть в ReadFullFlash

rjmp Gcykle

Заметьте, что после такой длительной процедуры можно прерывания и не разрешать (для этого придется убрать разрешение и из самой процедуры ReadFullFlash) — а вдруг мы чего-то нарушили в работе? Тогда контроллер просто перезапустится с нуля, и работа восстановится.

 

Глава 18

Персональный компьютер и системы на МК

Если вы умеете паять и знаете, с какого конца отсчитываются выводы у микросхемы — в современной электронике это даже не полдела. В лучшем случае вы сможете повторить часть тех конструкций, что описаны в этой книге, публикуются в журнале «Радио» или размещаются на сайте shema.ru. Но ничего серьезного вам создать не удастся, пока вы не научитесь самостоятельно писать «верхние» программы, т. е. такие, с помощью которых персональный компьютер (ПК) «общается» с вашим устройством.

Каналом связи с ПК имеет смысл снабжать практически все конструкции — я уже отмечал, что огромная доля современной электроники модифицируется простой заменой микропрограммы («перешивкой»), это стало даже стандартным маркетинговым приемом. Но дело даже не в этом: возьмите довольно простые часы, которые мы разбирали в главе 14. Не правда ли, они бы ничего не потеряли, если бы были снабжены дополнительной возможностью установки времени через ПК, а не только кнопочками? Нажал экранную кнопку «Установить», и время из компьютера переписалось в часы. Секундное дело — вместо того, чтобы вручную «ковыряться» с кнопками.

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

Правда, в этом деле есть два смягчающих обстоятельства. Во-первых, создание таких программ заметно проще, чем написание офисных или веб-приложений. Вам не потребуется знание SQL, Java или Ajax — достаточно общего понимания того, как работает Windows. С другой стороны, вам и не удастся обойтись стандартными компонентами Delphi или Visual Basic — придется привлекать нестандартные компоненты или использовать функции Windows API, обращение с которыми может вызвать приступы «истерического бешенства» похуже, чем любая Java. Но в целом создание типовых приложений для обслуживания электронных приборов — все же сравнительно несложная и к тому же достаточно консервативная область искусства программирования.

Начнем с того, что разберем технические аспекты взаимодействия ПК с микроконтроллерными устройствами.

Соединение ПК и МК

Я уже упоминал о том, что средой обмена данными между UART служит RS-232, где логика отрицательная (логическая единица есть низкий уровень напряжения, см. рис. 16.1), а сами уровни двуполярные, причем с большим допуском: низкий уровень представлен напряжением от -12 до -3 В, а высокий от +3 до +12 В. В этих пределах любая линия передачи по этому стандарту надежно работает.

Как мы говорили в главе 16, приемник RS-232 дополнительно снабжают схемой, которая фиксирует уровень не один раз за период действия бита, а трижды, при этом за окончательный результат принимается уровень двух одинаковых из трех полученных состояний линии (мажоритарная схема), таким образом удается избежать случайных помех. И хотя длина линии связи по стандарту не должна превышать 15 м, но на практике это могут быть много большие величины. Если скорость передачи не выбирать слишком высокой, то RS-232 может уверенно работать на расстояниях в десятки и даже сотни метров (автору этих строк удавалось без дополнительных ухищрений наладить обмен с компьютером на скорости 4800 но кабелю, правда, довольно толстому, длиной около полукилометра). В табл. 18.1 приведены ориентировочные эмпирические данные по длине неэкранированной линии связи для различных скоростей передачи.

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

Замечание

В правильно построенной экранированной линии экран не должен быть одним из токоведущих проводов, т. е. контакты GND соединяются отдельным проводом в кабеле, а экран либо соединяется с GND только на одной стороне — там, где имеется более качественное настоящее заземление, либо — в случае, если сигнальная «земля» GND является «плавающей» относительно «настоящей» земли — вообще только с заземлением.

При «несанкционированной» длине кабеля связи, особенно на больших скоростях передачи, следует применять меры по дополнительной проверке целостности данных: контроль четности, и/или программные способы (вычисление контрольных сумм и т. п.).

RS-232 также стандартизирует всем известные разъемы типа DB. Полный список всех контактов для двух стандартных разъемов типа DB (9- и 25-контактного) приведен в табл. 18.2. Нумерация контактов DB-разъема обычно приведена прямо на нем. Отметим, что 25-контактный разъем был создан первоначально в расчете на развитие стандарта, но позднее стало ясно, что развития не предвидится, и создали 9-контактный, которого достаточно для всех нужд, и в настоящее время практически только он и используется. Кабельная часть для RS-232 обычно представляет собой гнездовую часть разъема («маму»), а приборная — штыревую («папу»). Потому, к примеру, разъем LPT, который тоже есть 25-контактный DB, перепутать с СОМ-портом невозможно: для LPT на компьютере установлена гнездовая часть, а для СОМ-порта — штыревая.

Смысл дополнительных линий в том, что они могут применяться для организации различных синхронных протоколов обмена (протоколов с handshakes — «рукопожатием»). В «чистый» UART они не входят, в контроллере их организуют выводами обычных портов (но они входят в отдельные микросхемы UART для реализации полного протокола RS-232, а также в USART). Большинство устройств их не задействует. Однако любое устройство, использующее «рукопожатия», можно подключить к устройству, не имеющему этой функции (потеряв, конечно, возможности синхронизации), если соединить на каждой стороне между собой выводы RTS и CTS, а также выводы DSR, DCD и DTR.

Для нормальной совместной работы приемника и передатчика выводы RxD и TxD, естественно, нужно соединять накрест — TxD одного устройства с RxD второго, и наоборот (то же относится и к RTS, CTS и т. д.). Кабели RS-232, которые устроены именно таким образом, называются еще нуль-модемными (в отличие от простых удлинительных). Их стандартная конфигурация показана на рис. 18.1. В варианте рис. 18.1, в дополнительные выводы подключены именно так, как описано ранее, для возможности соединения устройств с «рукопожатием».

Рис. 18.1. Схемы нуль-модемных кабелей RS-232 :

а , б — различные полные варианты, в — минимальный вариант

Выходные линии RTS и DTR иногда могут служить и для «незаконных» целей — питания устройств, подсоединенных к COM-порту. Именно так устроены, например, компьютерные мыши, работающие через СОМ. Разумеется, для работы такого устройства требуется установить эти линии в нужное состояние. Тех, кто интересуется этим вопросом, я отсылаю к моей книге [11]Если у вас есть такая возможность, то для написания ассемблерных программ и программирования контроллеров лучше использовать ее, а не ХР и не Vista — меньше проблем на вашу голову.
, а здесь мы на этом не будем останавливаться, т. к. сейчас подобных устройств никто не проектирует. Мы займемся более актуальным вопросом: как обеспечить преобразование уровней UART в уровни RS-232 со стороны микроконтроллерного устройства?

Преобразователи уровней UART в уровни RS-232

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

Рис. 18.2. Простейший вариант самодельного преобразователя уровней RS-232 — UART при соединении контроллера с компьютером

«Официальный» путь состоит в том, чтобы применять специальные микросхемы приемопередатчиков RS-232 (правильнее их было бы называть преобразователями уровня), это, например, МАХ202, МАХ232, ADM202 и подобные, которые содержат внутри преобразователь — инвертор напряжения, подобный тому, что мы применяли для питания измерителя в автономном режиме (см. главу 17). Вариант построения такой схемы показан на рис. 18.3. Выходные уровни вывода TxD здесь при интенсивном обмене составят не менее ±7 В.

Рис. 18.3. Вариант одноканального преобразователя уровней RS-232 — UART на микросхеме МАХ202

Рис. 18.3. Вариант одноканального преобразователя уровней RS-232 — UART на микросхеме МАХ202

Одной из этих схем следует дополнить наш измеритель температуры и давления, чтобы получить возможность соединения его с ПК. Если выбран разъем DB-9M (штыревая часть, как на самом ПК), спроектированный для установки на плату (т. е. типа DRB), то вы сможете соединить ваше устройство с компьютером только одним способом: с помощью симметричного нуль-модемного кабеля, который имеет на обоих концах гнездовые части. Удлинительный кабель RS-232, в котором линии передачи не перекрещиваются, имеет на одном из концов гнездовую, на другом — штыревую часть, и с его помощью подсоединить компьютер не удастся. Можно, конечно, спроектировать устройство в расчете на удлинительный кабель (тогда надо поставить разъем DRB-9F и поменять местами выводы RxD и TxD).

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

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

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

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

Один из вариантов такой развязки, реализованный на относительно быстродействующем оптроне типа 6N139, показан на рис. 18.4. Верхняя часть схемы (оптрон D1) служит для передачи сигналов от контроллера к компьютеру. Сигнал TxD с контроллера должен иметь положительный уровень не ниже 4,5 В под нагрузкой, в противном случае следует увеличить номинал резистора R1.

Рис. 18.4. Вариант одноканального преобразователя уровней RS-232 — UART с гальванической развязкой

Приемная часть построена на оптроне D2. Ток через входной светодиод оптрона идет во время положительного уровня напряжения на линии TxD COM-порта, а диод VD3 защищает этот светодиод от обратного напряжения.

Здесь питание той части схемы, которая подает сигнал к компьютеру, обеспечивается преобразователем напряжения TMA0505D фирмы TRACO, имеющим гальваническую развязку между входом и выходом. Он обеспечивает преобразование положительного напряжения +5 В в два напряжения ±5 В.

Схема спроектирована в расчете на то, что оптоизолягор выполнен в форме удлинительного кабеля для COM-порта. Разъем Х2 типа DRB и остальные детали, кроме разъема XI, монтируют на макетной плате размерами 30x60 мм. С противоположной от разъема стороны распаивают трехжильный плоский кабель примерно 0,5 м длиной, закрепляют его на плате и соединяют с разъемом XI. Разъем XI может быть не только DB, а любого удобного типа, например, IDC. После проверки плату затягивают в отрезок термоусадочного кембрика подходящего диаметра. Разъем Х2 вместе с платой соединяется прямо с COM-портом, ответная часть разъема X1 располагается на плате устройства.

Для того чтобы вывести напряжение +5 В, пришлось тянуть отдельную линию (контакт 1 разъема X1). Поэтому, если вы собираетесь подключать ПК к компьютеру таким способом, вам придется использовать только этот кабель. В большинстве случаев вместо этого проще будет установить всю схему на плату устройства. Тогда, естественно, разъем XI следует исключить, но учтите, что у нас в схеме линии RxD и TxD уже перекрещиваются и соединять придется не нуль-модемным, а удлинительным кабелем (что и обеспечивает разъем DRB-9F). Если есть желание унифицировать соединение (нуль-модемные кабели более распространены, чем удлинительные), то следует взять разъем DRB-9M, а линии RxD и TxD в нем поменять местами.

Подключение через USB

Еще не так давно считалось, что протокол обмена данными по USB настолько сложен, что его реализовать под силу только далеко не рядовым специалистам. Но спрос рождает предложение. Вероятно, самое удобное на сегодняшний день решение для эмуляции COM-порта через USB предлагает английская (точнее шотландская) фирма Future Technology Devices Intl. Limited — FTDI. Читатель, несомненно, в курсе того, что подобные готовые «переходники» СОМ/USB имеются в продаже вместе с уже настроенным драйвером, но мы немного остановимся на том, как спроектировать такой интерфейс самому и как с ним обращаться.

Наилучший для практики способ построения последовательного порта через USB-интерфейс — на микросхеме FT232BM. С возможностями ее и других USB-микросхем этой фирмы можно ознакомиться из хорошей подборки статей на сайте компании «ЭФО» [12]$025F = 607, т. е. всего адресов 608, из которых 96 ($5F) занимают регистры, итого получается 512 ($0200) незанятых байтовых ячеек, составляющих ОЗУ.
. Самое главное преимущество этой микросхемы — наличие драйверов для Windows (притом бесплатно распространяемых), которые обеспечат, в том числе, полную эмуляцию последовательного COM-порта со скоростями до 1 Мбит в секунду. На рис. 18.5 без лишних объяснений я привожу схему устройства сопряжения USB/RS-232, которая без изменений честно заимствована из фирменной документации FTDI [13]В МК семейства Mega и Tuny вывод Reset можно использовать и под другие нужды, но в «классике» это было еще не так, да и неудобно это.
. В русифицированном варианте эта схема приведена в [14]Как раз сейчас, когда я редактирую эти строки, давление в Москве составило уникально низкую величину — 712 мм рт. ст. Но это, конечно, крайне редкая аномалия.
, где она также заимствована из размещенной на сайте екатеринбургской фирмы «Институт радиотехники» () статьи А. Лысенко, Р. Назмутдинова, И. Малыгина в журнале «Радио» (2002, № 6 и 7).

Согласно уверениям производителя, если вы просто припаяете микросхему FT232BM без дополнительного программирования внешней EEPROM (микросхема 93С46 на схеме), в которой должны храниться идентификаторы устройства и прочая служебная информация, и даже вообще без нее, то устройство все равно будет работать, хотя могут возникнуть сложности с подключением других подобных устройств. Если же есть желание EEPROM запрограммировать, то специально этим заниматься не требуется, при установке драйвера типа D2XX (как указано далee) это можно сделать прямо на готовой плате через специальную фирменную утилиту EditEEPROM. Есть, по слухам, некоторые особенности с обеспечением скоростного режима этих микросхем, но вдаваться в подробности в рамках этой книги не имеет смысла.

Имейте в виду, что максимальную скорость обмена здесь ограничивает не интерфейс, а применяемые компоненты. Так, в схеме по рис. 18.5 преобразователь МАХ213 или ADM213 могут обеспечить 115 кбод, микросхема SP213 — 500 кбод, а 1 Мбод вы получите только при выборе МАХ3245. Правда, при этом встанет необходимость еще и запрограммировать как UART, так и виртуальный COM-порт на такие скорости. На самом деле схема, приведенная на рис. 18.5, целесообразна только для устройств с уже готовым интерфейсом RS-232. Если вы устройство целиком проектируете самостоятельно, то нет никакого смысла преобразовывать уровни UART в уровни RS-232 и обратно, дважды устанавливая приемопередатчик — в этом случае его из схемы на рис. 18.5 нужно исключить, а вместо него линии RxD и TxD подсоединить прямо к контроллеру. Остальные линии можно не подключать, вывод CTS# микросхемы FT232BM при этом следует заземлить.

Рис. 18.5. Рекомендуемая производителем схема преобразователя USB-RS-232 с использованием микросхемы FT232BM

Как я уже говорил, к устройствам FTDI прилагаются бесплатные и свободно распространяемые драйверы под все основные ОС, в том числе и под Windows семейств NT и 9х. Разновидностей таких драйверов две — это VCP и D2ХХ-драйверы.

VCP означает Virtual Communication Port, этот драйвер просто-напросто транслирует все стандартные функции Win32 API, которые мы будем использовать далее, в необходимые команды для USB, и через микросхему FT232BM или аналогичные ей, находящиеся в устройстве, опять преобразует их в битовые последовательности UART. При подключении устройства в системе возникает новый виртуальный COM-порт, и все описанные далее программы без переделок будут работать через него. Это касается и случая, когда у вас покупной «шнурок» USB/RS-232, и когда сами «изобретаете» интерфейс по схеме рис. 18.5. Мало того, в Windows ХР такой драйвер уже встроен, и там ничего вообще устанавливать не надо. Единственная разница между таким подключением и обычным в том, что виртуальный СОМ, в отличие от реального, не будет виден в системе, пока вы свое устройство не подключите к USB.

Один вопрос, впрочем, может возникнуть в случае применения VCP-драйверов, и касается он упомянутых скорости передачи — если уж USB, то хочется реализовать хоть малую долю возможностей этой шины. Между тем стандартно COM-порт может иметь скорость максимум в 256 кбод. Есть приемы установки для COM-порта нестандартной скорости обмена, но вопрос о том, насколько это корректно для данного случая, лично для меня, ввиду новизны ситуации, пока открыт (официальная документация не слишком уверенно утверждает, что допустимы только стандартные скорости). Второй — менее серьезный — недостаток драйверов VCP заключается в том, что вы не можете через него работать со встроенной в FT-устройство EEPROM, в которой записан идентификатор устройства и прочие необходимые для USB «прибамбасы».

В этих случаях следует выбрать 02ХХ-драйвер, работа с которым отличается, и похожа на работу с разобранным далее компонентом AfComPort. Программы, конечно, придется переписывать. С установкой Э2ХХ-драйвера связана одна совершенно «идиотская» трудность (так и хочется написать — «характерная для продуктов Microsoft») — как указывалось ранее, в ХР VCP-драйвер уже есть, и для установки Э2ХХ-драйвера нужно устраивать некоторые «пляски с бубном», цитирую из русского переложения фирменных рекомендаций по этому поводу ():

«Немного сложнее обстоит дело в случае операционной системы Windows ХР, которая уже имеет в своем составе сертифицированные VCP-драйверы FTDI. При попытках присоединить к компьютеру новое USB-устройство со стандартными идентификаторами FTDI (например, любой DLP-модуль) система по умолчанию, не спрашивая пользователя, самостоятельно установит VCP-драйверы. Пользователю, желающему работать с D2XX-драйверами, необходимо в этот момент вспомнить, что очень полезно воспитывать в себе терпение и воспользоваться утилитой ftxprcvr.exe, входящей в состав дистрибутива В2ХХ-драйверов для Windows ХР. Утилита ftxprcvr.exe, используя установившиеся по умолчанию VCP-драйверы, перепрограммирует внешнюю EEPROM в присоединенном устройстве и задаст новые значения идентификаторов (VID=0403 и PID=6006). После этого необходимо повторить процедуру установки В2ХХ-драйверов с начала, т. е. отключить и снова присоединить устройство. Теперь система даст возможность пользователю указать директорию для установки D2XX-драйверов.»

Перепрограммировать EEPROM в устройстве USB на основе микросхем FT232BM через свою программу не обязательно, как уже упоминалось, для этого есть фирменная утилита EditEEPROM (). Приводить примеры программ для работы с D2ХХ-драйвером я не вижу никакого смысла, т. к. фирменная документация уже не однажды переписана и переведена на русский, см., например, цитированную ранее статью или книгу [14]Как раз сейчас, когда я редактирую эти строки, давление в Москве составило уникально низкую величину — 712 мм рт. ст. Но это, конечно, крайне редкая аномалия.
. На сайте екатеринбургской фирмы «Институт радиотехники» есть, в том числе, работающий пример проекта на Delphi (). А вот сами драйверы с этого сайта скачивать не следует, на момент написания этих строк там лежит устаревший вариант, лучше обратиться к первоисточнику () или на упоминавшийся сайт компании «ЭФО» ().

На этом мы с вопросом эмуляции СОМ через USB закончим и обратимся непосредственно к программам, которые взаимодействуют с СОМ-портом, неважно настоящим или эмулированным, но через стандартные функции API.

Программа СОМ2000

Обещанная универсальная программа для доступа к микроконтроллерным устройствам через COM-порт под названием СОМ2000 находится на моей домашней страничке по адресу: . Устанавливать ничего не требуется, просто распакуйте содержащий два файла архив в любую папку. Сама программа содержится в файле com2000.exe. Файл помощи help2000.htm можно открыть как изнутри программы (через меню со знаком вопроса или клавишей ), так и обычным способом в браузере, что удобнее. Собственно, в этом файле все рассказано, здесь я только немного подробнее опишу основные возможности программы.

На рис. 18.6 представлено основное и единственное окно программы СОМ2000. Когда-то подобные программы называли эмуляторами терминала (сейчас это название забыто вместе с самим понятием «терминалы»). Основная их функциональность заключается в постоянном ожидании приема данных по заданному порту с заданной скоростью (на рис. 18.6 установлен порт СОМ1 и скорость 9600, см. статусную строку внизу). Принятые данные побайтно выводятся на экран, причем отображение их может осуществляться тремя различными способами (в соответствии с выбором из показанного на рис. 18.6 меню в пункте Receive): в шестнадцатеричной форме, в десятичной и в виде текстового символа, соответствующего значению принятого байта. К последней возможности нужно относиться с осторожностью — Windows не «любит» встречать в текстовых компонентах несуществующие символы (вроде символа с номером 0) и программа может «рухнуть». Так что текстовый режим следует выбирать только, если вы ожидаете именно текст.

Рис. 18.6. Программа COM2000

На рис. 18.6 показан пример приема байтов в шестнадцатеричной форме в ответ на посланные команды (во втором случае, видном на экране полностью, это команда $Е2). Посылать команды можно выбором из меню Send Byte(s) также одной из трех возможностей: с клавиатуры (пункт Keyboard. +), непосредственным вводом значений (Value, +) или из файла (From file, +). Посылка с клавиатуры означает то же, что и прием в текстовой форме: при нажатии буквенной клавиши посылается ее код в виде байта с соответствующим значением. В Windows с ее путаницей в отношении виртуальных кодов клавиш эта возможность почти потеряла значение, но до сих пор встречаются устройства, в инструкции к которым команды записаны именно в виде символов (а не их номеров в таблице ASCII). Для совместимости с этими устройствами и сохранена такая возможность.

Если вы выбрали ввод с клавиатуры, то внизу в статусной строке надпись Keyboard Off сменится надписью Keyboard On. Не забудьте обратиться к пункту меню Keyboard или нажать + еще раз, чтобы выключить отсылку символов с клавиатуры после ввода, иначе они будут отсылаться и дальше при любом нажатии клавиш.

В обычном режиме используются две другие возможности, в основном вторая — посылка байтов с конкретным значением. При обращении к меню Send Byte(s)|Value (+) вы вызовете на экран однострочный редактор, в поле которого можно ввести нужное значение байтов, причем сразу много (до 32). Байты можно вводить в десятичном или шестнадцатеричном виде (с предваряющим знаком $) вперемешку, разделяя их пробелами. В выпадающем списке редактора запоминаются ранее отосланные вами строки (в том числе там есть несколько значений по умолчанию, для образца). После ввода значений нужно либо нажать на , либо дважды щелкнуть мышью в окне редактора с введенной строкой значений. Обратите внимание, что проверка значения не производится, и при превышении диапазона посылаемый байт усекается до 8 разрядов, например значение 257 будет послано, как 257–256 = 1. Проверяется только корректность записи, например, при попытке послать ОА без предваряющего $, вам будет выдано сообщение об ошибке.

Аналогично осуществляется посылка из файла, которая хороша тогда, когда нужно послать много байтов, и вводить их в однострочный редактор неудобно. Тогда следует создать текстовый файл, в котором содержится строка со значениями, составленная по точно таким же правилам, что действуют для непосредственной посылки, и выбрать этот файл через меню Send Byte(s)|From file (+).

Заметим, что непрерывный прием можно отключить, если выбрать пункт меню Disable (он изменится на Enable и для включения его следует нажать еще раз). Это полезно, когда устройство (вроде GPS-навигатора) выдает данные непрерывно, и не хочется «забивать» экран ненужными данными. Только будьте внимательны: если режим непрерывного приема отключен, вы можете забыть об этом и подумать, что прибор внезапно перестал работать. Пункт меню Clear предназначен для очистки экрана.

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

В программе можно, естественно, выбирать COM-порт (от СОМ1 до COM4) и скорость обмена (пункт меню СОМ). Кроме этого, можно менять оформление программы (цвет фона и надписей) через пункт меню Receive|Colors. Оформление и заданные режимы запоминаются к следующему сеансу.

Программа СОМ2000 очень удобна для отладки, о чем рассказывалось в разделе главы 16 «Отладка программ с помощью UART». Вы соединяете компьютер через один COM-порт с программатором, в свою очередь, соединенным со схемой, а через второй — с выходом UART схемы. Держа на экране открытыми одновременно три окна (редактор, программу для загрузки через программатор и СОМ2000), вы получаете возможность почти в реальном времени править программу и немедленно проверять ее работоспособность, временно расставляя контрольные вызовы функции out_com в нужных местах.

Работа с COM-портом в Delphi

В подавляющем большинстве случаев для создания даже таких относительно «навороченных» программ, как СОМ2000, можно обойтись стандартным Турбо Паскалем под DOS. Это было бы целесообразно, однако пользование DOS-программами на современном ПК крайне неудобно и порождает множество проблем совместимости (не говоря уж о многозадачности — конечно, работать в режиме немедленной проверки в DOS далеко не так удобно, как в Windows). Потому мы остановимся на Delphi, как одном из самых популярных в нашей стране средств быстрого создания приложений (RAD, Rapid Application Development). Можно использовать и Visual Basic (в некоторых отношениях это даже более удобно, т. к. в силу происхождения он лучше интегрирован с Windows), однако мой личный опыт общения с этим продуктом отрицательный — значительную часть времени приходится тратить на то, чтобы бороться с внутренними проблемами самого VB. Язык Delphi (Object Pascal) проще и понятнее новичку. Для тех, кто привык к С, можно рекомендовать Borland C++ Builder — это почти то же самое, что и Delphi, даже с общей библиотекой компонентов, только язык другой.

Мы будем ориентироваться на Delphi 7 — последнюю версию для Win32. Программы работают без оговорок во всех версиях Windows (начиная с 98-й). Насколько программы для этой платформы работоспособны в Vista — на момент написания этих строк мне еще не удалось проверить, но, по слухам, все более-менее в порядке (правда, установление этого порядка все требует некоторых «плясок с бубном»), а на крайний случай там имеется режим совместимости с более ранними версиями, который работает, говорят, лучше, чем в Windows ХР. Потому, надеюсь, с изучением платформы. NET можно повременить.

В дальнейшем я буду предполагать, что читатель имеет некоторые навыки работы в Delphi, поскольку рассмотрение данного вопроса выходит за рамки этой книги. Остальным я рекомендую обратиться к [15]Кстати, у многих начинающих программистов возникает вопрос — а можно ли перезапустить контроллер программно? Для этого нужно воспользоваться сторожевым таймером — см. главу 17 .
и [16]Имейте в виду, что подключение снятой с производства FT232AM (ее полное название FT8U232AM) несколько отличается.
, а также к моей книге [11]Если у вас есть такая возможность, то для написания ассемблерных программ и программирования контроллеров лучше использовать ее, а не ХР и не Vista — меньше проблем на вашу голову.
.

Работа через функции Win32 API

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

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

Объявим следующие переменные:

var

   Form1:TForm1;

   hCOM:hFile=0;

   pDCB:TDCB;

   comt ime:TCOMMTIMEOUTS;

   xb:byte;

   xn:dword;

   ab:array[1..32768] of byte;

   st,stcom:string;

   ttime,told:TDateTime;

Размер буфера ab может быть произвольным в зависимости от количества ожидаемых данных (в данном случае он подогнан для чтения данных из используемой нами внешней flash-памяти). К началу выполнения процедуры iniCOM у нас в строке stcom должно содержаться название порта, например, «СОМ1». Задаваемые параметры: прием по схеме 8n1, скорость 9600. Текст процедуры приведен в листинге 18.1.

Листинг 18.1

procedure IniCOM;

var i:integer;

begin

{инициализация СОМ — номер в строке stcom}

   hCOM:=CreateFile(Pchar(stcom),

   GENERIC_READ+GENERIC_WRITE,0,nil,OPEN_EXISTING,0,0);

   if (hCom = INVALID_HANDLE_VALUE) then

   begin

      st:=stcom+'не найден';

      Application.MessageBox(Pchar(st),'Error',MB_OK);

      exit;

    end ;

   if GetCommState(hCOM,pDCB)

   then st:=stcom+': baud=9600 parity=N data=8 stop=l';

   if BuildCommDCB(Pchar(st), pDCB) the n SetCommState(hCOM,pDCB)

   else

   begin

      st:=stcom+' занят или заданы неверные параметры';

      Application.MessageBox(Pchar(st),'Error',MB_OK);

      exit;

   end ;

   GetCommTimeouts(hCom,comtime) ; {устанавливаем задержки:}

   comtime.WriteTotalTimeoutMultiplier:=1;

   comtime.WriteTotalTimeoutConstant:=10;

   comtime.ReadlntervalTimeout:=10;

   comtime.ReadTotalTimeoutMultiplier:=1;

   comtime.ReadTotalTimeoutConstant:=2 000 ; {ждем чтения 2 с}

   SetCommTimeouts(hCom,comtime);

   ab[1]:=ord('A'); {будем посылать инициализацию модема}

   ab[2]:=ord('T');

   ab[3]:=13  ;{CR}

   ab[4]:=10  ;{LF}

   WriteFile(hCOM,ab,4,xn,nil);

   if ReadFile(hCOM,ab,10,xn,nil) then {ответ модема 10 знаков}

    begin

       st:=»;

       for i:=1 to 10 do st:=st+chr(ab[i]);

       if pos('OK',st)<>0 then

        begin

           st:=stcom+' занят модемом';

           Application.MessageBox(Pchar(st),'Error',MB_OK);

           CloseHandle(hCOM); hCOM:=0;

           Forml.Label7.Caption:='COM?';

           exit;

        end;

    end;

    Form1.Labe17.Caption:=s tcom+' 9600 1;

end;

Сначала текст тут мало отличается от того, что описано во всех стандартных рекомендациях по программированию порта. Единственный момент, который несколько выходит за рамки стандарта: мы не устанавливаем поля структуры DCВ напрямую, а используем функцию BuildCommDCB. Я это делаю отчасти потому, что структура DCB в Delphi транслируется из API не полностью (сравните ее описание в Windows.pas и в Win32.hip), и хотя для данного случая, разумеется, все нужные поля имеются, но функция BuildCommDCB все равно удобнее.

После стандартных установок мы сразу выполняем два действия, о которых упоминают далеко не всегда. Во-первых, мы устанавливаем все возможные задержки (timeout) для разных вариантов приема и передачи. В параметрах, которые заканчиваются на «Multiplier» можно для простоты всегда ставить «1» (если больше, то процедуры чтения/записи будут отслеживать еще и скорость поступления байтов, что нам не требуется). А остальные из этих параметров делают следующее: если задержка посылки через порт больше, чем writeTotaiTimeoutConstant (в миллисекундах), то будет прервана передача, а при задержке между поступающими байтами больше, чем ReadIntervaiTimeout, и при задержке всей процедуры чтения (в данном случае — самый главный параметр) больше, чем ReadTotalTimeoutConstant, будет прерван прием. Последний параметр мы установили равным двум секундам. При выборе этих параметров следует иметь в виду, что один байт при скорости 9600 передается/принимается примерно за 1 мс. Если эти параметры вообще не устанавливать (оставить их в значении «0», как по умолчанию), то при отсутствии принимаемых байтов процедура чтения через ReadFile просто зациклится и «повесит» всю программу.

Во-вторых, мы определяем, не является ли установленный нами порт модемом. Так как мы рассматриваем «чистый» RS-232, то для нас модемный порт все равно как бы занят. Определяем модем мы очень просто: посылаем в выбранный порт символьный код инициализации, который одинаков для всех модемов: «AT» (65 84 13 10). В ответ мы от модема должны получить строку «АТOK» (65 84 13 10 13 10 79 75 13 10), но все такие подробности нам не требуются, подозреваю, что строка для разных модемов может немного отличаться. Но в любом случае в ней должны содержаться символы «ОК», если модем, конечно, свободен (с занятым модемом я предоставляю читателю разобраться самостоятельно). В последнем операторе, если порт инициализировался нормально, выводим в Label номер порта и скорость.

Эту процедуру мы выполним сразу при запуске (для СОМ1). Заодно напишем процедуру (листинг 18.2) закрытия порта (ведь порт все время занят, пока программа запущена).

Листинг 18.2

procedure TForm1.FormCreate(Sender:TObject);

begin

{инициализация COM1 при запуске}

    stcom:='COM1';

    IniCOM;

end;

procedure TForm1.FormDestroy(Sender:TObject);

begin {уничтожаем COM}

        CloseHandle(hCOM);

end;

Кроме этого, конечно, следует создать меню для выбора порта по ходу работы с программой (аналогичное меню СОМ в моей программе СОМ2000, но мы не будем здесь на этом останавливаться, т. к. на основе приведенной процедуры его создать несложно.

Итак, порт открыт, инициализирован с нужными параметрами — что дальше? Далее все очень непросто, потому что мы здесь не можем, как в DOS, зациклить программу в ожидании поступившего байта или нажатия клавиши. Windows и сама представляет собой бесконечный цикл в ожидании сообщений (событий) — в точности такой же, как мы организовывали у себя в контроллере в ожидании прерываний. И вот этими сообщениями мы и должны ее снабдить.

Сначала разберем простейший способ обмена данными: когда мы точно знаем, что нам придет в ответ и в каком количестве. Например, посмотрим, как можно организовать процедуру приема значения часов в ответ на команду $А2 (описание того, как это работает в контроллере, см. главу 16). Расставим на форме следующие компоненты: кнопку Button1, а также Label и напротив него StaticText, и то и другое в количестве 6 штук. В компоненты Label запишем, соответственно, по порядку: «Часы», «Минуты», «Секунды», «Дата», «Месяц», «Год» (в таком же порядке их выдает наш контроллер). Листинг 18.3 иллюстрирует, как будет выглядеть сама процедура, когда по нажатию кнопки Button1 в МК выдается команда $А2, а потом принятые значения выводятся в Static Text (т. к. они в BCD-формате, то приходится переводить в НЕХ-представление).

Листинг 18.3

procedure TForm1.ButtonlClick(Sender: TObject);

begin {запрос}

      if (hCQM=0) or (hCOM=INVALID_HANDLE_VALUE) then exit;

              {если порт еще не инициализирован — выход}

     PurgeComm(hCOM,PURGE_RXCLEAR); {очищаем буфер}

     xb:=$А2;

     WriteFile(hCOM,xb,1,xn,nil);

     told:=Time;

     if ReadFile(hCOM,ab,6,xn,nil) then {читаем 6 байтов в массив ab}

      begin

         ttime:=Time;

         if SecondsBetween(told,ttime)>0 then

          begin

             Application.MessageBox('Устройство не обнаружено','Error',MB_OK);

             exit;

          end;

          if xn<>6 then

          begin

              Application.MessageBox('Неправильный формат данных', 1 Error',MB_OK);

             exit;

          end;

         StaticText1.Caption:=IntToHex(ab[1],2)

         StaticText2.Caption:=IntToHex(ab[2],2)

         StaticText3.Caption:=IntToHex(ab[3],2)

         StaticText4.Caption:=IntToHex(ab[4],2)

         StaticText5.Caption:=IntToHex(ab[5],2)

         StaticText6.Caption:=IntToHex(ab[6],2)

    end else {если не сработало}

    begin

         Application.MessageBox('COM сломался','Error',MB_0K);

    exit;

end;

end;

Результат будет выглядеть примерно так, как показано на рис. 18.7.

Рис. 18.7. Результат приема значений времени из часов-измерителя

Здесь процедура PurgeComm нужна для очистки приемного буфера, на случай, если там случайно задержались какие-то байты (те, кто работал с СОМ-портом в DOS, часто об этом забывают, т. к. там никаких буферов не было). Второй момент, который нужно прокомментировать, связан с возможным отсутствием нужного устройства на втором конце линии (или его неработоспособностью — включить забыли). Мы здесь, как видите, все делаем очень просто: читаем системное время до и после вызова функции ReadFile, и выясняем — если прошло более 1 с (a timeout у нас задан в 2 с), то «устройство не обнаружено». Опыт показывает, что это самый надежный метод. Если же связь каким-то образом прервется посередине посылки, то мы получим меньше байтов, чем заказывали, и программа выдаст сообщение «Неправильный формат данных». Не забудьте, что при работе с функциями времени в Delphi надо добавить ссылку на модуль DateUtils.

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

Это требует довольно тонкой организации процесса и вообще не универсально: а как быть, если мы не знаем точно момента прихода данных? Ну, например, пишем программу для взаимодействия с упоминавшимся GPS-навигатором, который выдает данные (и точно неизвестно, сколько именно) каждые несколько секунд?

Для этой цели можно организовать прием данных в отдельном потоке. Мы здесь не будем разбирать этот способ, потому что из-за «заумности» соответствующих функций API самое простое, что там приходится делать, — это создавать сам параллельный поток (по сути очень простое действие, которое почему-то вызывает дрожь у новичков, в значительной степени из-за неудобства этого процесса в Delphi). А вот собственно прием данных и их передача в основную программу там получается неоправданно сложной и запутанной процедурой. Потому я не буду на этом останавливаться (интересующихся подробностями отсылаю опять же к книге [11]Если у вас есть такая возможность, то для написания ассемблерных программ и программирования контроллеров лучше использовать ее, а не ХР и не Vista — меньше проблем на вашу голову.
), а сразу покажу, как можно создать подобную программу на основе дополнительного стороннего компонента, специально созданного для приема через COM-порт. Именно такой способ используется в программе СОМ2000.

Использование драйвера AsyncFree

Компонентов для работы с COM-портом довольно много, есть среди них платные и бесплатные. Мы будем использовать один из самых удачных и профессионально сделанных компонентов для СОМ-порта — свободно распространяемый AsyncFree некоего Петра Вониса (Petr Vones), судя по электронному адресу из Чехии. Компонент доступен бесплатно, с исходными кодами, и скачанный архив включает в себя, в том числе, и файлы dpk, что упрощает процедуру установки до предела — нужно просто щелкнуть мышью на том из dpk-файлов, который соответствует имеющейся у вас версии Delphi, и компонент установится самостоятельно, без утомительных процедур ручной инсталляции. Хотя к самому компоненту приложена ссылка на (отличный, кстати) сайт Delphree Open Source Initiative (), однако на нем я нашел только старую версию AsyncFree под Delphi 5, а скачивать последние версии лучше отсюда: showfiles.php?group_id=20226.

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

После установки компонент будет находиться в палитре компонентов на вкладке AsyncFree. На самом деле там образуется много компонентов, но нам требуется только самый первый из них под названием AfComPort. Установим его на форму. В перечень переменных добавим:

FlagCOM:boolean=False;

FlagSend:integer=0;

tall:integer;

Переменные told и ttime нам здесь не понадобятся. На форму добавим Label7, в который будем выводить установленный порт и скорость передачи, и Timer. Проверьте, чтобы у таймера интервал составлял 1000 мс (так по умолчанию). Кроме этого, установим компонент comboBox (выпадающий список), у которого в свойстве Items сразу запишем строки для выбора СОМ-порта («СОМ1», «COM2» и т. п.). Аналогичный список можно создать для выбора скорости передачи, но если речь идет о конкретном приборе, то это необязательно (а вот СОМ выбирать, скорее всего, придется).

Начнем с того, что перепишем процедуру IniCoM, что иллюстрирует листинг 18.4 (как и в предыдущем случае, к моменту ее вызова в stcom должна находиться строка с номером порта, например, «СОМ1»).

Листинг 18.4

procedure IniCOM;

var i, err: integer;

begin

    FlagCOM:=False;

    Form1.Label7.Caption:='COM?' ;

{инициализация COM — номер в строке stcom}

    Form1.AfComPort1.Close ; {закрываем старый COM, если был}

    val(stcom[length(stcom)],i,err) ; {извлекаем номер порта}

     if err=0 then Form1.AfComPort1.ComNumber:=i else exit ;

                               {здесь требуется число, а не строка}

      Form1.AfComPortl.BaudRate:=br9600 ; {скорость 9600}

     try

         Form1.AfComPortl.Open   ;{пробуем открыть}

     except

       if not Form1.AfComPort1.Active then {если не открылся}

       begin

           st:=stcom+' does not be present or occupied.';

           Application.MessageBox(Pchar(st),'Error',MB_OK);

           exit {выход из процедуры — неудача}

        end;

   end;

   ab[1]:=ord('A') ; {будем посылать инициализацию модема}

    ab[2]:=ord('Т');

   ab[3]:=13;  {CR}

   ab[4]:=10;   {LF}

   for i:=1 to 4 do Form1.AfComPort1.WriteData(ab[i],1);

   {ответ не сразу:}

   Form1.Timer1.Enabled:=True;

   tall:=0;

   while tall<1 do application.ProcessMessages ; {пауза в 1 с}

   Form1.Timer1.Enabled:=False;

   st:=Form1.AfComPort1.ReadString ; {ответ модема 10 знаков}

    if pos(1 OK',st)<>0 then {модем}

begin

         st:=stcom+' занят модемом';

         Application.MessageBox(Pchar(st),'Error',MB_OK);

         exit;

   end else {все нормально, COM открыт}

   begin

   Form1.Label7.Caption:=stcom+' 96001;

   FlagCOM:=True;

   end;

end;

Как видим, процедура создания порта много понятнее, чем в случае прямого обращения к API — все через привычную установку свойств компонента. FlagCOM играет у нас роль индикатора, доступен порт или нет. Если он остался при значении False, то процедуру следует повторить с другим значением в строке stcom (каковую мы задаем с помощью ComboBox, см. далее). При определении модема применен хитрый способ задания паузы — вместо обычного оператора Sleep, который тормозит программу, мы использовали таймер. Чтобы это сработало, надо в обработчике события OnTimer: все время увеличивать переменную tall. Полностью процедура по таймеру приводится далее, т. к. tall нам понадобится не только для этого.

Как только мы обратились к процедуре AfComPort1.open, у нас немедленно будет создан параллельный поток и весь прием пойдет через него. Поэтому, чтобы при определении модема принятые байты не обрабатывались, нужно не забыть добавить в процедуру приема выход по условию FiagCOM=Faise.

Для создания этой процедуры обычным способом — через инспектор объектов — создадим обработчик события AfComPort1DataRecived (листинг 18.5).

Листинг 18.5

procedure TForm1.AfComPort1DataRecived(Sender:TObject; Count:Integer);

{чтение очередного байта по сообщению wmCOMPORT}

var i: integer;

begin

    if FlagCOM=False then exit ; {если модем еще не опрошен}

     if count<>0 then {если что-то принято}

     begin

        AfComPort1.ReadData(ab,count); {читаем буфер в массив}

        хn:=xn+count; {число принятых байт}

         tall:=0; {обнуляем время}

      end;

end;

На самом деле условие count<>0 не требуется, оно введено просто ради порядка (иначе бы процедура просто не была бы вызвана). По выходу из процедуры в переменной хn будет накапливаться количество принятых байтов. Осталось только дописать остальные процедуры (листинг 18.6).

Листинг 18.6

procedure TForm2.Button2Click(Sender:TObject);

begin {запрос}

    if FlagCOM=False then exit;

      {если порт еще не инициализирован — выход}

     AfComPortl.PurgeRX; {очищаем буфер порта на всякий случай}

     xb:=$А2;

    AfComPort1.WriteData(xb,1); {посылаем команду}

    FlagSend:=$А2; {обозначаем посылку запроса времени}

    tall:=0; {обнуляем время}

     хn:=0; {счетчик принятых байтов}

    Timer1.Enabled:=True; {запускаем таймер}

end;

procedure TForm1.FormCreate(Sender:TObject);

begin

{инициализация COM1 при запуске}

    stcom:='COM1';

    IniCOM;

    end;

procedure TForm1.ComboBox1Select(Sender: TObject);

begin

     stcom:=ComboBox1.Text; {устанавливаем порт COM1,2,3,4}

     IniCOM;

end;

procedure TForm1.FormDestroy(Sender: TObject);

begin

     AfComPort1.Close; {закрытие порта}

end;

Теперь нам осталось разобраться с тем, что мы там напринимали. Это позволит сделать установленное нами значение FiagSend и таймер. В таймере переменную tall мы будем увеличивать на единицу, а в процедуре приема мы все время ее обнуляем, так что пока она равна нулю, можно полагать, что прием еще не закончился. Как только она станет больше единицы (прошло более секунды с момента последнего принятого байта или прием вообще не происходил), мы начинаем что-то делать, но только в том случае, если флаг FlagCOM установлен (True), иначе это вообще был не прием, а опрос модема. Сказанное иллюстрирует листинг 18.7.

Листинг 18.7

procedure TForm1.Timer1Timer(Sender:TObject);

var i: integer;

begin {таймер}

     inc tall

     if FlagCOM=False then exit;

     if tall>1 then

     begin

         Timer1.Enabled:=False; {выключаем таймер}

         if xn=0 then {если счетчик = 0, то ничего не принято}

          begin

             Application.MessageBox('Устройство не обнаружено 1,'Error',МВ_ОК);

             exit {выход из процедуры — неудача}

            end else

           begin {иначе обрабатываем данные}

               if FlagSend=$A2 then {если был запрос времени}

               begin

                   if xn<>6 then

                   begin

                       Application.MessageBox('Неправильный формат данных','Error',MB_OK);

                       exit;

                   end;

                   StaticText1.Caption:=IntToHex(ab[1],2);  // часы

                    StaticText2.Caption:=IntToHex(ab[2],2); // минуты

                    StaticText3.Caption:=IntToHex(ab[3],2); // секунды

                    StaticText4.Caption:=IntToHex(ab[4],2);  // дата

                    StaticText5.Caption:=IntToHex(ab[5],2); //месяц

                    StaticText6.Caption:=IntToHex(ab[6],2);  // год

                end;

           end;

       end;

end; {конец таймера)

По аналогии вы легко добавите процедуры, соответствующие всем остальным командам, предусмотренным в программе нашего измерителя. Пользуясь функцией DateTime, легко создать процедуру, которая будет загружать из компьютера точное время (только С форматом TDateTime придется немного попотеть, см. по этому поводу [15]Кстати, у многих начинающих программистов возникает вопрос — а можно ли перезапустить контроллер программно? Для этого нужно воспользоваться сторожевым таймером — см. главу 17 .
и [16]Имейте в виду, что подключение снятой с производства FT232AM (ее полное название FT8U232AM) несколько отличается.
). Не забывайте принимать и анализировать возвращаемые байты для процедур записи. При длинной процедуре приема данных из flash, когда число байтов заранее неизвестно, суммарное значение счетчика хn покажет, сколько именно байтов принято. Причем если это число не кратно четырем, то можно смело утверждать, что целостность данных была нарушена. И не забудьте увеличить размер массива ab, если у вас энергонезависимая память большей емкости!

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

 

Глава 19

Практические схемы на AVR

Возможности МК серии AVR весьма велики, вплоть до того, что на старших моделях можно выстраивать полноценные системы управления, например, жидкокристаллическими дисплеями. Среди продукции Atmel есть контроллер USB на AVR-ядре, и с его помощью можно делать настоящие USB-устройства, не прибегая к эмуляции СОМ-порта. Но и младшие модели AVR вполне пригодны для серьезных вещей, достаточно упомянуть, что такое устройство, как компьютерная клавиатура, изначально была спроектирована на куда менее мощном, чем почти любой современный AVR, контроллере 8048.

Здесь мы рассмотрим способы построения некоторых типовых узлов с помощью AVR, не задаваясь вопросом конструирования законченных приборов. Довести до ума и встроить в необходимые вам устройства эти узлы вы сможете сами, я лишь покажу, как это делается в принципе. И начнем мы с самого, пожалуй, интересного — попробуем заставить МК воспроизводить цифровой звук.

Заставить камни заговорить

На самом деле способность воспроизводить звук встроена во все МК AVR изначально. Для этого надо лишь иметь «исходник»— ранее записанный звуковой файл с определенными параметрами. Такое устройство можно использовать, как узел голосовой сигнализации — например, если укомплектовать им наш измеритель с часами, и он сможет вслух сообщать время и температуру. Для этого придется сделать немного— разделить в памяти звуковые сэмплы, произносящие различные слова, и комбинировать их по ходу дела в нужном порядке. Именно так работают системы автоматического оповещения, например, о задолженности по телефонным счетам. Здесь мы в подробности влезать не будем, а покажем, как вообще организовать на AVR режим воспроизведения цифрового звука.

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

Предположим, что мы имеем в качестве источника набор байтов, напрямую представляющий исходную оцифрованную последовательность (сжатые форматы мы не рассматриваем, поскольку это увело бы нас далеко за рамки темы книги). «Лобовой» метод понятен из той же главы 10: взять ЦАП, подать ему на вход оцифрованный звук с той же частотой, с которой его оцифровывали, а к его выходу подключить фильтр, усилитель и динамик. Но вот, например, производители сотовых телефонов просто замучили своей «полифонией»: сидя в маршрутке, невольно вздрагиваешь, когда у соседки в сумочке вдруг то ребенок заплачет, то котенок замяукает. Не многие, однако, задавались вопросом — а как это делается? Неужели в мобильник встраивают настоящий цифровой тракт, со всеми этими ЦАПами, фильтрами и усилителями? Вовсе нет. Главная идея, которая заложена в простой реализации воспроизведения цифрового звука, называется «усилитель в режиме D», от слова digital — цифровой.

В усилителях класса D цифровой звук вообще не переводится в аналоговую форму какими-то специальными устройствами. Наоборот, там обычный звук представляется в виде последовательности прямоугольных импульсов, пропорциональных по длительности интенсивности сигнала. Для усиления таких импульсов не нужно никаких ухищрений — чаще всего используют комплементарную пару транзисторов, подобно тому, как усиливается сигнал на выходе логических КМОП-микросхем. Выгода заключается в том, что теоретически КПД такого импульсного усилителя может быть равен 100 %, ведь какой-то из транзисторов пары всегда заперт, а второй транзистор в это время полностью открыт и мощности на них не выделяется. Это, конечно, в теории, потому что из главы 3 вы знаете, что падение напряжения на открытом транзисторе все же имеет место, да и переключение не происходит мгновенно. Но вопросы КПД нас тут не интересуют, т. к. мы не собираемся конструировать 100-ваттные усилители, и указанная схема нас привлекает не столько КПД, сколько простотой и компактностью.

Представление синусоидального сигнала в виде последовательности импульсов различной длительности называется ШИМ-модуляцией (по-английски, PWM — Pulse-Wide Modulation). Фокус заключается в том, что для извлечения исходного синусоидального сигнала из ШИМ не требуется никаких специальных сложных приборов — достаточно обычного резисторно-конденсаторного ФНЧ с подходящей частотой среза (о ФНЧ см. главу 2). В результате весь звуковой тракт упрощается до предела (рис. 19.1).

Рис. 19.1. Принцип работы выходной части усилителя в режиме D

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

А как сформировать входной сигнал для такого усилителя, если у нас в наличии имеется лишь аналоговая звуковая волна? Нужно ли ее оцифровывать? Совсем нет: исходный аналоговый сигнал поступает на один вход компаратора, а на второй его вход подается напряжение треугольной формы и подходящей амплитуды. Тогда на выходе компаратора мы получим ШИМ-сигнал. Работа счетчика-таймера, показанная на рис. 19.2 далее, делает в точности то же самое, но в цифровой форме.

Для того чтобы получить ШИМ-сигнал из уже оцифрованного звука, у нас есть такая «штука», как микроконтроллер, причем уже специально приспособленный для подобных целей. Если вы уже имеете книгу [1]Меандр — тип геометрического узора с повторяющимися ломаными линиями (по названию извилистой реки Меандр в Малой Азии).
или [2]Префикс «К» в названии отечественных микросхем, обозначающий их принадлежность к бытовому/коммерческому диапазону температур, мы будем в этой книге опускать, подробнее см. главу 8 .
, или скачивали фирменный PDF-документ с описанием какого-то из контроллеров AVR, и при этом ваше любопытство зашло столь далеко, что вы эти источники даже немного пролистали, то, несомненно, заметили, что в описаниях таймеров PWM-режиму уделяется довольно много места — больше, чем всем остальным режимам вместе взятым. Это потому, что PWM-режим сложнее простого счета. Но на самом деле идея, которая в него заложена, очень проста: мы загружаем в регистр сравнения очередное число, взятое из звуковой последовательности, и запускаем таймер на счет с нуля, а когда он дойдет до верхнего предела, то сразу реверсируется и начинает считать обратно до нуля. В контроллерах Tuny вместо реверсирования счетчик сбрасывают и начинают отсчет заново. В семействе Mega для формирования сигнала PWM есть и тот, и другой и еще некоторые режимы работы таймеров (например, с переменным битрейтом).

В момент, когда числа в счетчике таймера и в регистре сравнения равны между собой, в режиме PWM автоматически переключается знакомый нам выход, связанный с выбранным таймером (в главе 14 это был выход ОС1, который управлял миганием двоеточия). Только в данном случае он не переключается туда-сюда с каждым прерыванием от таймера, а находится в состоянии логического нуля, когда число в таймере больше, чем в регистре сравнения, и в состоянии логической единицы — когда меньше. В результате на один цикл счета «туда-обратно» мы получаем один период ШИМ-сигнала, в котором длительность состояния логической единицы строго пропорциональна числу в регистре сравнения. Меняя к следующему циклу это число на очередную выборку из звуковой последовательности, мы в результате получаем то, что требовалось: входной импульсный сигнал для усилителя в режиме D. Общая схема процесса показана на рис. 19.2 (на примере с использованием Timer 1). Кстати, отметим, что этот режим может применяться также, например, просто для формирования сигнала с определенной скважностью, не равной двум.

Рис. 19.2. Принцип работы счетчика-таймера в режиме PWM

Теперь надо понять, какие характеристики исходного оцифрованного сигнала нам нужны и какие параметры таймера необходимо устанавливать. Хотя мы будем использовать Timer 1, но задействовать все 16 разрядов в таком режиме он не может (счет в реверсивном режиме возможен максимум с 10 разрядами, а использовать режимы с переменной разрядностью мы не будем). Нам же будет достаточно и 8 — это означает, что глубина квантования исходного звука должна быть также 8 разрядов. Баха не очень сыграешь, но для передачи разборчивой речи достаточно.

Теперь разберемся с частотой оцифровки. Тактовую частоту МК для такой схемы лучше выбирать максимально возможной, для большинства AVR это 16 МГц (чтобы еще повысить качество звука, можно специально взять модель 2313, у которой максимальная частота 20 МГц, но мы будем ориентироваться на 16 МГц). Легко подсчитать, что реверсивный 8-разрядный счетчик будет считать туда и обратно с частотой fтакт/510, т. е. при такой тактовой частоте получится около 32 кГц. Это и будет несущая частота fоп на выходе ШИМ, что удовлетворительно, т. к. она выходит за пределы слышимого диапазона. Однако требуемая частота оцифровки исходного звука может быть все же заметно ниже (что удобно в целях экономии памяти). Пусть она составляет 4 кГц (это может возмутить аудиофилов, но для передачи речи это нормальный показатель).

Тогда можно сразу выбрать характеристики RC-фильтра: чтобы отфильтровать 32 кГц простой RC-цепочкой, нам желательно, чтобы частота среза не превышала частоту оцифровки, т. е. 4 кГц. Тогда 32 кГц затухнут в 8 раз по сравнению с верхней частотой диапазона оцифровки, и мы их влияние не почувствуем. Параметры фильтра рассчитываются по формуле fcp = 1/2πRС, и нашим требованиям удовлетворяют параметры R = 10 кОм и С = 3,9 нФ.

Для хранения звука используем память с I2С-интерфейсом АТ24С512. Одному отсчету тут будет соответствовать ровно один байт, одной секунде звучания — 4 кбайт. Итого 65 536 байт дадут нам около 16 с звучания. Этого достаточно, чтобы произнести стандартную предвыборную речь кандидата в президенты, если предварительно ее отредактировать и выбросить все фразы, не несущие смысловой нагрузки.

Все параметры схемы мы рассчитали, можно приступать к проектированию. В качестве звукового усилителя возьмем описанный в главе 6 микроусилитель МС34119. Выбор усилителя не имеет большого значения, но данная микросхема «умеет» работать с однополярным напряжением 5 В и это удобно. Общая схема соединений показана на рис. 19.3. Полную схему включения МС34119 см. на рис. 6.15.

Рис. 19.3. Принципиальная схема для использования AVR в режиме голосовой сигнализации

Программа для вывода звука

Здесь мы для простоты выберем АТ9 °C8515 семейства Classic (а точнее, ATmega8515 в режиме совместимости с АТ9 °C8515, потому что оригинал может работать максимум на 8 МГц тактовой частоты, а мы рассчитывали на 16 МГц). Это проще для нашего рассмотрения, поскольку в семействе Classic имеется лишь один режим PWM для таймеров, а в Mega их много и это лишь путает. В крайнем случае, разобраться в том, как дополнить программу выбором нужного нам в данном случае режима Phase Correct PWM вы сможете самостоятельно. Для работы с интерфейсом I2С в программе используется тот же самый, что и в главе 16, включаемый файл i2c.prg (см. Приложение 5, листинг П5.3).

В целях компактности из него можно для данного случая удалить процедуры, относящиеся к RTC (write_i2c и read_i2c), но будьте осторожны, чтобы не удалить что-то нужное. Кроме того, следует обратить внимание на величину задержки в процедуре delay — там у нас установлена величина 5 мкс в расчете на 4 МГц. При 16 МГц задержка укоротится вчетверо, и память будет работать на пределе (400 кГц), что не очень хорошо. Хотя обычно (если линия передачи не слишком длинная) память справляется, но помнить об этом параметре в случае возникновения каких-то сбоев следует. Но и снижать скорость шины до тех величин, что у нас были в измерителе не следует, т. к. чтение может не успеть за битрейтом (см. далее).

Программа для данного случая содержится в листинге 19.1.

Листинг 19.1

/==== программа вывода цифрового звука ====

;процессор mеgа8515 в режиме 8515, частота 16 МГц

.include "853 5def.inc"

.equ T_Sample = 193   ;предварительное значение для Timer 0 при 4 кГц

.equ bSample = 0  ;бит готовности к чтению

;регистры temp и DATA определены в "I2С.рrg" (Приложение 5)

.def FLAGS = r19

; прерывания

rjmp RESET   ;начальный загрузчик

reti

reti

reti

reti

reti

reti

rjmp TIMO   ;обработчик прерывания переполнения Timer 0

reti

reti

reti

reti

.include "I2C.prg"

RESET:

            ldi temp,0b00100000

            out DDRD,temp   ;ОС1А — на выход

             ldi temp,(1<<C0M1A1)|(1<<PWM10)   ;инициализация PWM

             out TCCR1A,temp

            ldi temp,1<<CS10   ;включаем Timer1, 1/1

             out TCCR1B,temp

            ldi temp,(1<<CS01)|(1<<CS11)   ;включаем Timer0, 1/64

            out TCCR0,temp

            out TCNT0,T_Sample   ;T_Sample=6, заряжаем таймер 0 на 4 кГц

             ldi temp,(1<<TOIE0)   ;разрешаем прерывание Timer0

             out TIMSK,temp

            clr temp   ;очищаем все регистры

            out OCR1AH,temp

            out OCR1AL,temp

            out OCR1BH,temp

            out OCR1BL,temp

            ldi XH,high(Nbytes)

            ldi XL,low(Nbytes)   ;зарядка счетчика выводимых байт

            ;вместо Nbytes подставить объем записи в байтах, не более 64К

             ldi YH,high(ADrWord)

            ldi YL,low(ADrWord)

            ;вместо ADrWord можно подставить начальный адрес

            ;во flash-памяти, он может быть отличен от 0:0

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

loop_voice:   ;читаем байт для последующего вывода

             rcall read_i2c   ;чтение памяти, YL,YH — адрес, в DATA — полученных байтов

             sleep   ;Idle-mode, проснется по прерыванию Timer0

             sbrs FLAGS,bSample   ;бит встает в 1 в обработчике прер. TIM0

             rjmp loop_voice   ;если еще не установлен, то на начало цикла

             adiw YL,1 ;иначе увеличиваем адрес на 1

            cbr FLAGS,1<<bSample   ;сбрасываем бит готовности к чтению

             sleep   ;Idle-mode, проснется по прерыванию Timer0

            in temp,TCCR0   ;проверяем, не остановлен ли таймер

             tst temp

            brne loop_voice   ;если не остановлен, то следующий цикл

            ;иначе вывод звука закончен — делаем что-то еще

;===== обработчик прерывания от Timer 0 =====

TIM0:

           out TCNT0,T_Sample   ;перезаряжаем Timer0

            clr temp

           out OCR1AH,temp

           out ОСR1AL,DATA   ;занесение байта в PWM

           sbr FLAGS,1<<bSample   ;бит готовности к чтению — в 1

            sbiw XL,1   ; уменьшаем счетчик прочитанных байтов

            brne rt_pwm_   ;если он равен 0

           out TCCR0,XL   ;то выключаем таймер 0

           out TCCR1B,XL   ;и Timer 1 также

rt_pwm_:

           reti   ;возврат из обработчика Timer0

В начале программы мы устанавливаем Timer 1 в PWM-режим и задаем ему переключающий режим по выходу ОС1А такой, чтобы там устанавливался низкий уровень, а также запускаем его с входной частотой, равной тактовой. Прерываний от Timer 1 никаких не требуется, он будет работать непрерывно, пока мы его не выключим. Управление битрейтом мы будем осуществлять по прерыванию переполнения Timer0. Если каждый раз в него записывать некоторое число, то можно регулировать частоту таких прерываний. Включим его с частотой на входе, равной 1/64 тактовой (последняя должна быть равна 16 МГц), тогда минимальная частота на выходе будет равна 976 Гц (976,5625 Гц = 16 МГц/64/256). Мы же здесь хотим частоту как можно ближе к 4 кГц (не следует окончательно портить звук еще и изменением битрейта), поэтому мы будем записывать каждый раз в таймер число 193, и он будет считать от 193 до 255, т. е. отсчитывать 62 такта, тогда прерывание будет происходить с частотой почти ровно 4 кГц. Меняя эти параметры (указанное число и частоту на входе Timer 0), можно устанавливать другой битрейт, ограниченный в данном случае скоростью чтения из памяти по интерфейсу I2С (при максимальной частоте шины 400 кГц эта скорость составит около 9 кбайт/с). При более скоростной памяти битрейт будет ограничен в принципе лишь скоростью работы таймера в PWM-режиме (32 кГц), но при воспроизведении такого звука могут возникнуть сложности из-за искажений. Более глубоко в этот вопрос влезать здесь нет смысла.

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

В этой программе число воспроизводимых байтов ограничено 65 536 (64 кбайт), т. к. для упрощения мы считаем их в 16-разрядном регистре х, но при необходимости несложно добавить еще один регистр счетчика адреса и задействовать большую емкость памяти (правда, для длинных клипов придется переходить на другие типы интерфейса, см. главу 16). В листинге 19.1 указаны теоретические начальный адрес (ADrWord) и объем записи (Nbytes), которые нужно для вашей задачи заменить на конкретные числа. Кроме того, по окончании звукового фрагмента программа просто остановится. Несложно сделать так, например, чтобы она «закольцевалась»: для этого вместо выключения таймера просто заново занесите значение Nbytes в регистры XH и XL. В общем, приспосабливайте программу для ваших нужд, как можете.

И еще несколько слов о том, откуда берутся исходные звуковые сэмплы. Для этого нужно записать в компьютере звук (моно!) в формате WAV, и обработать его в любом звуковом редакторе, который позволяет регулировать битрейт и глубину оцифровки (например, Sound Forge). Исходным материалом может быть как ваш собственный голос, записанный через микрофон, так и готовый звуковой клип. Формат WAV — чистый оцифрованный звук, и его можно напрямую перекачивать в нашу память. Проще всего для этого воспользоваться каким-нибудь универсальным программатором, но несложно модифицировать данную программу так, чтобы МК сам мог записывать клипы из компьютера через UART. Все необходимые сведения для создания такой программы в этой книге есть.

Аналоговая индикация

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

Существуют готовые микросхемы для управления шкальными индикаторами. Они представляют собой специализированные дешифраторы. Например, К155ИД11 при подключении 8 светодиодов, расположенных в ряд, формирует светящийся столбик, высота которого соответствует трехразрядному входному двоичному коду. Для каскадного включения таких микросхем предусмотрены дополнительные входы и выходы (разрешения и переноса), потому с их помощью можно создавать и более длинные шкалы. На практике для большинства применений достаточно 16 градаций.

Более современный аналог К1003ПП1 (UAA180) менее удобен, т. к. управляет 12 светодиодами — ни то ни се (24 при каскадном включении двух микросхем — много, 12 при одной микросхеме— мало). Впрочем, никто не мешает вам задействовать только часть разрядов, да и индикация с 24 разрядами, а то и более, тоже иногда требуется. Отметим, что К1003ПП2, которая вроде бы управляет 16 светодиодами, для практических целей не годится, т. к. высвечивает не столбик, а только один из светодиодов в линейке, что и некрасиво, и малоинформативно. Разумеется, есть и другие подобные микросхемы, но мы не будем на этом задерживаться.

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

Учтите, что приобрести готовую прилично выглядящую шкалу, представляющую собой линейку LED, крайне непросто. Основная проблема состоит в том, что близкорасположенные светодиоды засвечивают друг друга, и в стандартном технологическом процессе их приходится разделять промежутком, чтобы компаунд затекал в зазоры достаточно надежно. Выглядит такая шкала безобразно: лучше уж взять матричный индикатор и сформировать шкалу на его основе, хотя это усложняет управление и тоже не совсем то, что требуется. Наилучший способ — сформировать линейку из плоских светодиодов, вручную окрасить их боковые грани и затем установить на плату. Светодиоды обязательно должны быть с диффузным (матовым) рассеивателем, иначе равномерной засветки вы не добьетесь. В идеале их следует также отбирать по яркости свечения, в противном случае шкала будет неравномерной, хотя на практике это довольно сложно осуществить. Для окраски граней обойтись краской из баллончика не удастся, т. к. она будет просвечивать, особенно на ребрах, даже при покрытии в несколько слоев. Потому необходимо взять обычную темную и достаточно густую нитрокраску (можно даже дать постоять ей на воздухе, чтобы дополнительно загустела) и окрасить весь светодиод методом окунания. Когда краска полностью засохнет (высохнув, она значительно уменьшится в объеме), аккуратно сошлифуйте мелкой шкуркой ее с торца светодиода.

Большинство проблем, связанных со шкальной индикацией, с помощью К155ИД11 или ее многочисленных аналогов решить можно. Причем следует учесть, что на самом деле 8-разрядная линейка индикаторов имеет не 8, а 9 состояний (восемь зажженных LED плюс состояние, когда все погашены). На выходе К155ИД11, в частности, число зажженных LED на единицу больше входного кода: когда на входе 000, горит один, самый первый светодиод шкалы. Чтобы его тоже погасить, надо подать отдельно сигнал логического нуля на вход переноса (тогда микросхема перестает реагировать на входной код вообще). Поэтому иногда проще уменьшить число ступенек шкалы до 7 (а для 16-разрядного индикатора — до 15). Аналогичная проблема с числом состояний касается всех дешифраторов подобного рода.

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

Приведем пример того, как можно подключить шкальный индикатор к нашему измерителю с помощью микросхемы К155ИД11. Нашей целью будет индикация атмосферного давления в расчете на шкалу с 15 градациями (16 состояниями) в диапазоне от 710 до 785 мм рт. ст. (т. е. по 5 мм рт. ст. на одну градацию шкалы). При этом состоянию 710 и менее должны соответствовать все погашенные LED, от 711 до 715 — один горящий, от 716 до 720 — два горящих и т. д.

Схема на рис. 19.4 составлена в предположении, что от цифровой индикации мы отказались, и порт С, занятый ранее сегментами, у нас освободился. Младшие разряды этого порта мы и задействуем для управления линейкой светодиодов. Остальные соединения на схеме не показаны (см. рис. 15.2). Ради простоты индикацию температуры опустим. Вывод Е (разрешения) микросхем управляется старшим разрядом четырехбитового числа с вывода РСЗ. Логика К155ИД11 такова, что если на Е уровень логического нуля, то работа микросхемы DD2 запрещена, если «1»— запрещается работа микросхемы DD3. Так как во втором случае на выходе Р верхней микросхемы уровень логического нуля, то нижняя микросхема зажжет все светодиоды. Ограничительных резисторов для светодиодов не требуется, они встроены в микросхему, хотя яркость в этом случае может быть непредсказуемой (а вот К1003ПП1 «умеет», в том числе, управлять яркостью).

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

Программу придется переделать таким образом. Расчеты физических величин нам уже не требуются, но вот преобразование масштабов провести придется. Потому на подготовительном этапе нужно выяснить значение коэффициентов К и Z таких, чтобы по уравнению зависимости выходного кода от значений, прочитанных из АЦП, приведенному в главе 15, у нас значение выходного кода, равного нулю, соответствовало 710 мм рт. ст. Таким образом, коэффициент Z, который нужно вычесть из кода АЦП, будет равен значению кода при 710 мм рт. ст., или, как несложно рассчитать, примерно 840 (с учетом того, что датчик работает не с нуля давления, см. главу 15).

Значение же, соответствующее 785 мм рт. ст., должно соответствовать какому-нибудь «круглому» двоичному числу (не очень важно, какому, т. к. мы потом его все равно урежем до 4 бит). Из характеристик датчика мы знаем, что максимальная шкала АЦП в 10 бит соответствует давлению около 850 мм рт. ст. Нас же интересует шкала всего в 75 мм (от 710 до 785), что составит около 90 единиц кода. Потому мы смело можем выбрать, например, 128 для верхнего предела шкалы (что соответствует 7 битам). Тогда коэффициент К (который ранее составлял 0,895 мм рт. ст. на единицу кода), теперь будет примерно 128/90 = 1,422. Оба коэффициента, естественно, должны уточняться при калибровке.

Сама процедура расчета не нуждается в переделке (меняются только значения коэффициентов, остальное можно оставить, как есть, хотя если внимательно ее рассмотрите, то увидите, что есть резервы для сокращения необходимых ресурсов, например, задействованных регистров). Единственное, что следует учесть— вычисленные значения хранятся в регистрах AregH: AregL (см. Приложение 5), но 7-битовый результат, конечно окажется только в регистре AregL, а регистр АregH всегда будет равен нулю. После расчета, вместо преобразования В двоично-десятичный КОД (rcall bin2BCD16), мы должны записать:

lsr AregL

lsr AregL

lsr AregL   ;теперь результат усечен до 4 бит

in temp.PortC   ;значение разрядов PortC в temp

cbr temp,15   ;обнуляем младшие 4 бита

ori temp,AregL   ;устанавливаем младшие 4 бита

out PortC,temp   ;выводим

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

В заключение отмечу, что в сконструированном мной однажды приборе для некоей яхты была задействована более сложная и более красивая шкальная индикация: при значении параметра ниже некоторого порога индикаторы меняли цвет на красный, который сменялся зеленым при повышении параметра до нормы. Естественно, готовые микросхемы (типа К155ИД11 и подобные) такую задачу решить не позволяют, и пришлось городить всю процедуру внутри контроллера, управляя светодиодами напрямую. Задачу изменения цвета удалось решить, применив двухвыводные двухцветные LED (L117), которые обоими выводами коммутировались к выводам МК (для одного цвета действовало сочетание состояний 0 и 1, для другого — 1 и 0, а погашен LED оказывался при одинаковом состоянии выводов).

Подстройка внешних часов

Кварцевые резонаторы имеют весьма высокую точность: ±1 с в сутки для электронных часов равносильны, например, ошибке в 1 м на 86 км при измерении длины. Но, не говоря уж о специальных технических применениях, такой точности при измерении времени недостаточно даже в быту— часы с такой ошибкой будут уходить на полминуты в месяц и их придется подводить каждые два-три месяца, как минимум. Если для стрелочных часов такая процедура ничего сложного не представляет, то для электронных она выливается в довольно занудные манипуляции с кнопками или необходимость (как в нашем измерителе с часами) коррекции времени через подключение к компьютеру.

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

Для начала о самой поправке: можно, конечно, попросту ввести задержку вроде той, что служит для формирования сигналов I2С (собственно, так и отсчитывали время в контроллерах, когда в них еще не было таймеров). Но это не очень хорошо по той же причине, по которой такие паузы лучше не использовать в Windows, где много процессов протекают параллельно. Либо это задержит весь контроллер, либо (если прерывания разрешены) задержка может быть непредсказуемой длительности. Потому лучше задействовать таймер (скажем, Timer 2, который имеется во всех Mega, a Timer 1 оставим для других надобностей), только запускать его так, чтобы он формировал прерывания достаточно короткой длительности. Например, ежесуточная коррекция ступеньками по 10 мс нас будет вполне устраивать (при точном подборе задержки ошибка составит не более 1 с в три месяца, если не учитывать «гуляние» частоты кварца самой по себе).

Самое сложное здесь — понять, как автоматически ввести коррекцию в обе стороны (и когда часы спешат, и когда отстают). Я придумал следующий механизм: мы задаем некий коэффициент коррекции в виде байта Кккор. Если этот байт равен $FF (расчет на то, что именно такое значение содержится в чистой EEPROM), то коррекцию вообще не проводим, или загружаем значение по умолчанию (как и с коэффициентами).

Если часы спешат, то значение Кккор должно находиться в пределах 1—127 (старший бит равен нулю), и оно определяет задержку, которую мы проводим с помощью таймера, настроенного на 10 мс, раз в сутки. Для этого надо остановить часы, пропустить столько циклов таймера, сколько задано в Кккор, и запустить их заново с того же значения. Максимальная задержка составит 1,27 с в сутки, что намного перекрывает возможную ошибку кварца.

Сложнее, если часы отстают. Чтобы отличить эту ситуацию от предыдущей, мы задаем старший бит Кккор равным единице, но в пределах 155–254 (т. е. коррекция возможна в пределах 1 с). Почему именно в этих пределах, мы сейчас поймем. Величина задержки программой будет вычисляться, как разность между 255 и Кккор (задержка, равная нулю, не имеет смысла, отсюда максимальное значение 254). В МК мы останавливаем часы, включаем таймер, делаем столько сравнений, сколько задано этой разностью, затем устанавливаем секунды = 1 и запускаем часы опять (теперь они идут немного вперед). Отсюда понятно, почему мы не можем задать Кккор меньше 155 — при таком алгоритме значение, к примеру, 128 привело бы к еще большему отставанию. Следовательно, чем больше задержка (разность между 255 и Кккор), тем меньше коррекция в данном случае: если нам нужно коррекцию свести к нулю, то следует задать значение Кккор = 155. Отметим, что остановка и пуск часов сами по себе займут время порядка миллисекунд, но ошибка будет невелика, и ее можно не учитывать.

Для коррекции задействуем Timer 2 с коэффициентом 1:256, получаем на выходе 15 625 Гц (при тактовой частоте 4 МГц). Если использовать прерывание по сравнению с величиной 156, получаем примерно 100 Гц (или 10 мс). Кккор мы будем хранить в EEPROM (по адресу KоrrEE), и переписывать его в RAM (по адресу KorrRAM) — эти адреса выбираются из любых свободных. Процедура для инициализации регистра сравнения таймера и коэффициента коррекции приведена в листинге 19.2 (в секторе начальной загрузки, о самой программе см. главу 16).

Листинг 19.2

;команду ldi temp,(1<<TOIEO) заменяем на

           ldi temp, (1«TOIE0)|(1<<ОСIЕ2)

;добавляем:

           ldi temp,156

            out OCR2,temp   ;получаем 100 Гц Timer2

;коэффициент коррекции ============

           clr ZH;addr eepr

          ldi ZL,KorrEE

          rcall ReadEEP

          cpi temp,$FF

          brne corr_K

          ldi temp,100  ;если = FF, то по умолчанию 100

corr_K:

            ldi ZH,1

            ldi ZL,KorrRAM

            st Z,temp

Величина задержки по умолчанию определяется на основе предварительных изысканий по уходу конкретного кварца (здесь часы спешили примерно на 1 с в сутки). Не забудем заменить в четвертой сверху строке таблицы прерываний reti на rjmp TIM2_comp. Отведем для счета прерываний регистр count_msek (пусть будет r12). Бит 4 регистра Flag будет сигнализировать о том, отстают часы или спешат (если отстают, то бит установлен). Значение этого бита и регистра count_msek будем устанавливать в момент, когда будет вызываться процедура коррекции (см. далее), там же часы останавливаются. Сам обработчик приведен в листинге 19.3.

Листинг 19.3

ТIМ2_СОМР:

            dec count_msek

            brne end_t   ;если еще не 0, то на выход

             sbrs Flag,4

            rjmp k_minus   ;если спешат, то пропустить если отстают

            cbr Flag,8   ;очищаем бит к следующему разу; устанавливаем часы на 01 сек. и заводим их

            ldi temp,1

            rcall IniSek

            ldi count_sek,1   ;меняем значение в регистре секунд

            ldi ZH,1

            ldi ZL,Sek

            st Z,count_sek

            rjmp end_korr

k_minus:

;если спешат, просто заводим часы обратно

             clr temp

            rcall IniSek

end_t:

reti  ;конец прерывания коррекции

Теперь собственно процедура вызова коррекции: будем выполнять ее в полночь. Она довольно громоздкая, потому что приходится определять момент, когда полночь настала. Мы можем вклинить ее туда, где часы проверяются на кратность трем (0 минут и 0 секунд нам обеспечены). Однако сразу после этого производится запись во flash (а иногда и не производится), и она нам будет непредсказуемо тормозить коррекцию. Потому будем ее проводить в одну минуту первого (00:01:00). Сразу после метки sek_0 вписываем фрагмент кода, содержащийся в листинге 19.4.

Листинг 19.4

sek_0:

          ldi ZL,1   ;загружаем минуты

           ld temp,Z+;

          cpi temp,1   ;сравниваем минуты, если 1 — коррекция

          breq min_1

          cpi temp,0   ;сравниваем минуты, если 0 — запись

          breq mm0   ;на проверку трехчасового цикла

          reti   ;иначе выходим

min_1:

          ld temp,Z   ;загружаем часы

          cpi temp,0  ;сравниваем часы = 0

          breq hour_0   ;если равны 0, то на коррекцию

           reti   ;иначе выходим

hour_0:

         ldi ZL,KorrRAM   ;коэффициент коррекции Id

         count_msek,Z   ;в счетчик

          ldi temp,128

         ср count_msek,temp   ;определяем его величину

         brlo k_plus

         com count_msek   ;если больше 127, то вычитаем из 255

          sbr Flag,8   ;значит отстают

k_plus:   ;если меньше — часы отстают

          ldi temp,$80

         rcall IniSek   ;останавливаем часы

          ldi temp,0Ь00000110   ;заводим таймер

         out TCCR2,temp   ;Timer2 1:256

reti

mm:   ;далее по тексту программы

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

Измерение частоты

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

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

Предположим, измеряемая частота находится в диапазоне до 4 МГц, и нам желательно измерить ее с разрешением до 1 Гц. Прежде всего отметим, что тактовая частота контроллера должна превышать измеряемую не менее, чем в два раза — таково требование руководства. Обнаружение изменения внешнего сигнала производится по фронту тактового, и если период измеряемого сигнала слишком короткий, то в регистрации могут быть пропуски.

Замечание

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

Так что нам потребуется МК с тактовой частотой не менее 8 МГц. Выберем АТ9 °C2313 (Classic— при необходимости легко модифицировать алгоритм к любому AVR) с частотой 8 МГц. Для измерения мы используем два таймера— один 16-разрядный (Timer 1) для отсчета собственно внешней частоты, и второй (Timer 0) для отсчета измерительного интервала.

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

Результат измерения частоты 4 МГц с точностью до герца в принципе займет не менее трех 8-битовых регистров. Но реальное их число, которое требуется задействовать, будет зависеть от нижнего предела измеряемой частоты. В самом деле, предположим, что частота может меняться не более чем на 256 Гц. Тогда старшие два регистра всегда будут показывать одно и то же число (и точно известно, какое), а все изменения будут регистрироваться только в самом младшем регистре счетчика. Если же частота 4 МГц не меняется более чем на 65 кГц, то можно оставить только два регистра (собственно таймера). Здесь важно только, чтобы в процессе изменений частота не «переваливала» за границу, когда старший регистр тоже меняется (что в нашем случае произойдет, например, если средняя частота колеблется около значения 2 22 = 4 194 304), иначе возникнет неоднозначность (которую, впрочем, также в некоторых случаях можно учесть). Но мы не будем в этот вопрос углубляться, а тупо предположим, что частота в пределах емкости трех регистров (т. е. с большим запасом — до 16,7 МГц) может быть любой.

Для измерения нам потребуется ввести прерывание Timer 1 по переполнению, в котором третий регистр (назовем его count3) будет всякий раз увеличиваться на единицу. Входной сигнал подадим на вход Т1 (вывод 9 для 2313), с которого внешние импульсы поступают прямо на счетчик таймера, если ему задать соответствующий режим.

Теперь разберемся с формированием измерительного интервала. При 8 МГц тактовой частоты и коэффициенте предделителя для Timer 0, равном 1/256, прерывания будут происходить с частотой 122,07 Гц. Нам же требуется 1 с (1 Гц), потому мы введем счетчик (count_sek) и будем его с каждым прерыванием увеличивать, пока он не отсчитает ровно 122 таких прерывания. После этого можно фиксировать число импульсов, сосчитанное к тому времени в регистрах Timer 1. Но если кварцевый резонатор идеально точный, то секунда получится чуть меньше настоящей (неучтенные 0,07 Гц дадут ошибку 576 мкс со знаком «минус»), и перед чтением значений мы введем задержку для компенсации этой недостачи, с помощью которой наш частотомер можно еще дополнительно калибровать (т. е. учесть исходную неточность кварца). В начале и в конце интервала будем переключать разряд 6 порта D (вывод 11), чтобы контролировать измерительный интервал в процессе калибровки. На рис. 19.5 представлен МК AT90S2313 с обозначением выводов для нашей цели.

Рис. 19.5. Подключение AT90S2313 для измерения частоты

Программа частотомера приведена в листинге 19.5 (определение регистров я опускаю, в данном случае их потребуется всего три — temp, count3 и count_sek). В секции прерываний введем прерывания Timer 0 и Timer 1 по переполнению (по меткам TIM0 и TIM1). Инициализация таймеров в секции начальной загрузки сводится к разрешению прерываний, но обязательно здесь же нужно очистить счетные регистры таймеров и все дополнительные

Листинг 19.5

ldi temp,(1<<TOIE0)|(1<<ТO1Е1)   ;разр. прер. Timer0 и Timerl

out TIMSK,temp

clr temp

out TCNT1H,temp

out TCNT1L,temp   ;очищаем Timer1

out TCNT0,temp   ;очищаем Timer0

clr count3

clr count_sek   ;очищаем счетчик прерываний

ldi temp,0b00000100;

out TCCR0,temp   ;запускаем Timer0 div 1:256

Прерывание Timer 1 будет очень простое (листинг 19.6).

Листинг 19.6

TIM1:

    inc count3

reti

Теперь рассмотрим самое главное прерывание, Timer 0 (листинг 19.7).

Листинг 19.7

TIM0:  ;таймер 122,07 Гц

          inc count_sek

          cpi count_sek,122  ;получаем 0.999424 с

           breq corr_1   ;если секунда прошла, то на коррекцию счета

           cpi count_sek,1

          brne corr_1   ;в самом первом цикле запускаем Timer 1:

           ldi temp,0b00000111   ;внешний сигнал Т1 (выв. 9) по фронту

          out TCCR1B,temp  ;запускаем Timer1

corr_1:  ;1 сек + коррекция

          clr temp

          out TCCR0,temp   ;останавливаем Timer0

          ;задержка на -600 мкс для коррекции интервала

           ldi ZH,high(1200)   ;8 МГц, цикл 4 такта

          ldi ZL,low(1200)

loop:

          sbiw ZL,1

             brne loop

;переключение контр. выв 11 PinD6 период 1 с

             sbis PinD,6

             rjmp set_1

             cbi PortD,6

             rjmp Set_0

Set_1:

             sbi PortD,6

Set_0:

             clr temp

             out TCCR1B,temp   ;останавливаем Timer1

;читаем данные

              in temp,TCNT1L

             <пишем младший байт в память>

              in temp,TCNT1H

             <пишем второй байт в память>

             <пишем старший байт count3 в память>

;очищаем все регистры

             clr temp out TCNT1H,temp

             out TCNT1L,temp   ;очищаем Timer1

              out TCNT0,temp   ;очищаем Timer0

              clr count3

             clr count_sek   ;очищаем счетчик прерываний

;запускаем таймер 0 опять

              ldi temp,0Ь00000100;

             out TCCR0,temp   ;запускаем Timer0 div 1:256

reti

Обратите внимание, что читать данные из регистров таймера нужно в указанном порядке: сначала старший, потом младший, а записывать в обратном порядке. Это сделано специально: при чтении из старшего регистра TCNT1H данные из младшего TCNT1L одновременно переписываются в специальный регистр временного хранения, и ситуации, когда в промежутке между командами чтения младший разряд может измениться (при запущенном таймере), не возникает. То же самое, только в обратном порядке, происходит и при записи. Запись данных я не расшифровывал, потому что это может быть и запись в SRAM с последующим выводом на индикацию, и запись во внешнюю энергонезависимую память для последующего чтения из компьютера (или одновременно и то и другое). Простейший частотомер можно сделать, если организовать автоматическую передачу данных через UART в компьютер, который и занимается отображением и записью информации.

Из-за инструкции sbiw, которая занимает два такта (а не один, как инструкция dec, которую мы использовали в процедуре delay для интерфейса I2С, см. главу 16), здесь один цикл задержки равен четырем тактам, или 0,5 мкс при тактовой частоте 8 МГц. Меняя число циклов задержки, можно подстроить длительность секундного интервала в интервале от 576 мкс в сторону уменьшения (задержка равна нулю) до целых 131 мс в сторону увеличения. 576 мкс может показаться слишком маленьким значением, но этого достаточно для подстройки стандартного кварца, в крайнем случае, можно отобрать экземпляр из нескольких. Калибровка осуществляется измерением длительности импульса на контрольном вводе 11 МК с помощью точного частотомера (профессионального лабораторного прибора, а не любительского, который имеет недостаточную точность).

У AT90S2313 недостаточно выводов, чтобы напрямую обеспечить динамическую индикацию для нашей цели (7 десятичных разрядов), поэтому можно либо только использовать данные непосредственно, либо управлять разрядами через внешние дешифраторы (скажем, 561ИД5 позволяет управлять семисегментным индикатором четырехразрядным двоичным кодом с выводов PD0—PD3, а управление переключением семи и даже восьми разрядов можно тогда осуществить через полностью свободный порт В). Можно, конечно, выбрать другой контроллер. Динамическая индикация практически никак не помешает счету, так как таймер считает абсолютно независимо от остальных схем.

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

Без подробного обсуждения привожу одну из возможных схем такого формирователя импульсов (рис. 19.6), работающего на частотах до 4 МГц. 554САЗ заменяется на 521САЗ (импортный аналог — LM311). Конденсатор 100 пФ служит для фильтрации высокочастотных помех и его емкость подбирается при регулировке. Предпочтителен более современный (и несколько более быстродействующий) LM6511, совпадающий с указанными по выводам. Переключатель полярности сигнала в принципе не требуется (его можно реализовать программно простым переключением режимов Timer 1), но таким образом можно сэкономить вывод МК, который пришлось бы подсоединять к переключателю.

Рис. 19.6. Формирователь входных импульсов для частотомера 0–4 МГц

Объединение систем на МК

Как я уже упоминал в главе 16, последовательный порт USART может работать в режиме мультипроцессорного обмена. Однако реализован он довольно сложно и малопригоден для организации такого часто встречающегося варианта, когда есть главный компьютер и несколько равноправных систем на МК, с которым хочется организовать двустороннюю связь через единый COM-порт. Сейчас мы разберем одну из возможностей организации такого обмена.

Обмен такого рода не может обойтись без присвоения индивидуального адреса устройству, т. к. их надо как-то различать. Все подобные протоколы (I2С хотя бы) различаются лишь способом доставки и форматом этого адреса. В нашем же случае придется еще придумать, как обеспечить «прозрачное» переключение каналов обмена во избежание конфликтов (в USART это достигается односторонностью обмена— по линии от главного МК к ведомым передается только 9-битовый адрес, а обратно— только 8-битовые данные, нам же нужен двусторонний обмен).

Для реализации такого варианта мы сконструируем специальную плату-коммутатор на основе отдельного МК (возьмем тот же AT90S2313). Идея состоит в том, что мы выделяем специальные команды-адреса, которые воспринимает только этот МК, и в соответствии с ними переключает канал обмена на нужное устройство. Если переключать с помощью мультиплексоров/демультиплексоров 561КП2 (см. рис. 8.8), то можно адресовать до восьми устройств. В качестве команд адресации удобно выбрать числа от 0 до 7, тогда они прямо будут соответствовать коду, который требуется подать на мультиплексоры.

Естественно, среди команд управления устройствами и передаваемых к устройству данных байты с таким значением должны полностью отсутствовать (например, установку часов напрямую таким способом не передашь), и это накладывает ограничения, но не очень серьезные. В каждом конкретном случае можно что-нибудь придумать: например, дополнять данные со значением меньше 8 старшим битом, равным единице, или еще что-то в этом роде (с похожими проблемами сталкиваются при передаче произвольных данных — вложений — по электронной почте, и ничего, как видите, справляются). Разумеется, можно задействовать и дополнительные линии СОМ-порта для адресации коммутирующего МК отдельно от остальных, или использовать девятибитовые посылки адреса (так, как это делается в USART), чтобы отличить их от данных и т. п. Здесь я приведу только самый простой вариант.

Схема такого коммутатора показана на рис. 19.7.

Рис. 19.7. Коммутатор UART на 8 каналов

Выводы коммутатора, помеченные номером с буквой R, следует присоединить к выводам RxD устройств, а их выводы TxD (строго в том же порядке) следует соединить с выводами коммутатора, помеченными номером с буквой Т. Программа коммутатора (листинг 19.8) очень проста и даже не содержит таблицы векторов прерываний, поэтому я привожу ее целиком.

Листинг 19-8

;Тестовый коммутатор

;Кварц 4 МГц

.include <<2313def.inc»

;=======

.def    temp = r16

;======= программа

           ldi temp,low(RAMEND)   ;загрузка указателя стека

           out SPL,temp

          ldi temp,(1<<ACD)   ;выкл. аналог, компаратор

           out ACSR,temp

          ldi temp, (1<<RXEN|1<<TXEN|1<<RXB8|1<<TXB8)

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

          ldi temp,25

          out UBRR,temp   ;скорость передачи 9600

           ldi temp,0b00000111   ;устанавливаем PB0-PB2 выходы

           out DDRB,temp

          clr temp

          out PortB,temp   ;по умолчанию адресуется устройство 0

G_cykle:

          rcall in_com

          cpi temp,9   ;если принятый байт больше или равен 9

          brsh G_cykle   ;то ничего не делаем

          out PortB,temp   ;иначе выводим его в порт В

rjmp G_cykle

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

          sbis USR,RXC

          rjmp in_com

          in temp,UDR

ret

Больше ничего делать не требуется — «верхняя» программа всегда начинает с того, что посылает номер устройства n от 0 до 7, мультиплексор коммутирует выходы nR и nТ к выводам RxD и TxD устройства с номером и, и далее «общение» с ним происходит совершенно «прозрачно», как будто остальных устройств не существует.