Подключение кнопки к AVR. Примеры на Си для микроконтроллеров Atmel AVR Код для кнопки микроконтроллера на языке c

Казалось бы простая тема, а однако в комментах меня завалили вопросами как подключить микроконтроллер. Как подключить к нему светодиод, кнопку, питание. Что делать с AGND или AREF . Зачем нужен AVCC и все в таком духе. Итак, раз есть вопросы, значит тема не понятна и надо дать по возможности исчерпывающий ответ. Все описываю для контроллеров AVR, но для каких нибудь PIC все очень и очень похоже. Т.к. принципы тут едины.

Питание
Для работы микроконтроллеру нужна энергия — электричество. Для этого на него естественно нужно завести питалово. Напряжение питание у МК Atmel AVR разнится от 1.8 до 5 вольт, в зависимости от серии и модели. Все AVR могут работать от 5 вольт (если есть чисто низковольтные серии, то просьба уточнить в комментах, т.к. я таких не встречал). Так что будем считать что напряжение питания контроллера у нас всегда 5 вольт или около того. Плюс напряжения питания обычно обозначается как Vcc . Нулевой вывод (а также Земля, Корпус, да как только его не называют) обозначают GND . Если взять за пример комповый блок питания. То черный провод это GND (кстати, земляной провод традиционно окрашивают в черный цвет), а красный это +5, будет нашим Vcc . Если ты собираешься запитать микроконтроллер от батареек, то минус батареек примем за GND , а плюс за Vcc (главное чтобы напряжение питания с батарей было в заданных пределах для данного МК, позырь в даташите. Параметр обычно написан на первой странице в общем описании фич:

Operating Voltages
–1.8 — 5.5V (ATtiny2313V)
–2.7 — 5.5V (ATtiny2313)
Speed Grades
–ATtiny2313V: 0 — 4 MHz @ 1.8 — 5.5V, 0 — 10 MHz @ 2.7 — 5.5V
–ATtiny2313: 0 — 10 MHz @ 2.7 — 5.5V, 0 — 20 MHz @ 4.5 — 5.5V

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

Для работы контроллерам серии AVR достаточно только питания. На все входы Vcc надо подать наши 5 (или сколько там у тебя) вольт, а все входы GND надо посадить на землю. У микроконтроллера может быть много входов Vcc и много входов GND (особенно если он в квадратном TQFP корпусе. У которого питалово со всех сторон торчит). Много выводов сделано не для удобства монтажа, а с целью равномерной запитки кристалла со всех сторон, чтобы внутренние цепи питания не перегружались. А то представь, что подключил ты питалово только с одной стороны, а с другой стороны чипа навесил на каждую линию порта по светодиоду, да разом их зажег. Внутренняя тонкопленочная шина питания, офигев от такой токовой нагрузки, испарилась и проц взял ВНЕЗАПНО и без видимых, казалось бы, причин отбросил копыта. Так что ПОДКЛЮЧАТЬ НАДО ВСЕ ВЫВОДЫ Vcc и GND . Соединить их соответственно и запитать.

Отдельные вопросы вызвают AGND и AVCC — это аналоговая земля и питание для Аналого-Цифрового Преобразователя. АЦП это очень точный измеритель напряжения, поэтому его желательно запитать через дополнительные фильтры, чтобы помехи, которые не редки в обычной питающей цепи, не влияли на качество измерения. С этой целью в точных схемах проводят разделение земли на цифровую и аналоговую (они соединены должны быть только в одной точке), а на AVCC подается напряжение через фильтрующий дроссель. Если ты не планируешь использовать АЦП или не собираешься делать точные измерения, то вполне допустимо на AVCC подать те же 5 вольт, что и на Vcc , а AGND посадить на ту же землю что и все. Но подключать их надо обязательно!!! ЕМНИП от AVCC питается также порт А.

Warning!!!

В чипе Mega8 похоже есть ошибка на уровне топологии чипа — Vcc и AVcc связаны между собой внутри кристалла. Между ними сопротивление около (!!!) 5Ом Для сравнения, в ATmega16 и ATmega168 между Vcc и AVcc сопротивление в десятки МЕГА ом! В даташите на этот счет никаких указаний нет до сих пор, но в одном из топиков за 2004 год на AVRFreaks сказано, что люди бодались с цифровым шумом АЦП, потом написали в поддержку Atmel мол WTF??? А те, дескать, да в чипе есть бага и Vcc и AVcc соединены внутри кристалла. В свете этой инфы, думаю что ставить дроссель на AVcc для Mega8 практически бесполезно. Но AVcc запитывать надо в любом случае — кто знает насколько мощная эта внутренняя связь?

Простейшая схема подключения Микроконтроллера AVR приведена ниже:

Как видишь, добавился дроссель в цепь питания AVCC , а также конденсаторы. Хорошим тоном является ставить керамический конденсатор на сотню нанофарад между Vcc и GND у каждой микросхемы (а если у микрухи много вход питания и земель, то между каждым питанием и каждой землей) как можно ближе к выводам питания — он сгладит краткие импульсные помехи в шине питания вызыванные работой цифровых схем. Конденсатор на 47мКФ в цепи питания сгладит более глубокие броски напряжения. Кондесатор между AVcc и GND дополнительно успокоит питание на АЦП .

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

Схема сброса
Резистор на RESET . Вообще в AVR есть своя внутренняя схема сброса, а сигнал RESET изнутри уже подтянут резистором в 100кОм к Vcc . НО! Подтяжка это настолько дохлая, что микроконтроллер ловит сброс от каждого чиха. Например, от касания пальцем ножки RST , а то и просто от задевания пальцем за плату. Поэтому крайне рекомендуется RST подтянуть до питания резистором в 10к. Меньше не стоит, т.к. тогда есть вероятность, что внутрисхемный программатор не сможет эту подтяжку пересилить и прошить МК внутри схемы не удасться. 10к в самый раз.

Есть еще вот такая схема сброса:

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

Задержка будет примерно T=R*C для данного примера — около секунды. Зачем эта задержка? Да хотя бы для того, чтобы МК не стартовал раньше чем все девайсы платы запитаются и выйдут на установившийся режим. В старых МК (АТ89С51 , например) без такой цепочки, обеспечивающей начальный сброс, МК мог вообще не стартануть.

В принципе, в AVR задержку старта, если нужно, можно сделать программно — потупить с пол секунды прежде чем приступать к активным действиям. Так что кондер можно выкинуть нафиг. А кнопку… как хочешь. Нужен тебе внешний RESET ? Тогда оставь. Я обычно оставляю.

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

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


Генератор может быть:

  • Внутренним с внутренней задающей RC цепочкой.
    В таком случае никакой обвязки не требуется вообще! А выводы XTAL1 и XTAL2 можно не подключать вовсе, либо использовать их как обычные порты ввода вывода (если МК это позволяет). Обычно можно выбрать одно из 4х значений внутренней частоты. Этот режим установлен по дефолту .
  • Внутренним с внешней задающей RC цепочкой.
    Тут потребуется подключить снаружи микроконтроллера конденсатор и резистор. Позволяет менять на ходу тактовую частоту, просто подстраивая значение резистора.
  • Внутренним с внешним задающим кварцем.
    Снаружи ставится кварцевый резонатор и пара конденсаторов. Если кварц взят низкочастотный (до 1МГц) то конденсаторы не ставят.
  • Внешним.
    С какого либо другого устройства идет прямоугольный сигнал на вход МК, который и задает такты. Полезен этот режим, например, если надо чтобы у нас несколько микроконтроллеров работали в жестком синхронизме от одного генератора.

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

У внешнего кварца отличные показатели точности, но он стоит лишних 15 рублей и требует дополнительных деталей и, что самое обидное, часто съедает пару ног I/O. Также на внешнем же кварце можно добиться максимальной производительности от МК. Частота МК определяется частотой на которую заточен выбранный кварц. Внешная RC цепь позволяет тикать генератору МК быстрей чем от внутренней, стоит дешевле кварца, но имеет те же проблемы со стабильностью частоты, что и внутренняя RC цепь.

Способы тактования МК описаны в даташите в разделе System Clock and Clock Options и всецело определяются конфигурацией Fuse Bit’s . Пока же я настоятельно рекомендую НЕ ТРОГАТЬ FUSE пока ты не будешь твердо знать что ты делаешь и зачем. Т.к. выставив что нибудь не то, можно очень быстро превратить МК в кусок бесполезного кремния, вернуть к жизни который будет уже очень непросто (но возможно!)

Подключение к микроконтроллеру светодиода и кнопки
Сам по себе, без взаимодействия с внешним миром, микроконтроллер не интересен — кому интересно что он там внутри себя тикает? А вот если можно как то это отобразить или на это повлиять…

Итак, кнопка и светодиод подключаются следующим образом:


Для кнопки надо выбраную ножку I/O подключить через кнопку на землю. Сам же вывод надо сконфигурировать как вход с подтяжкой (DDRxy=0 PORTxy=1). Тогда, когда кнопка не нажата, через подтягивающий резистор, на входе будет высокий уровень напряжения, а из бит PINху будет при чтении отдавать 1. Если кнопку нажать, то вход будет положен на землю, а напряжение на нем упадет до нуля, а значит из PINxy будет читаться 0. По нулям в битах регистра PINх мы узнаем что кнопки нажаты.

Пунктиром показан дополнительный подтягивающий резистор. Несмотря на то, что внутри AVR на порт можно подключить подтяжку, она слабоватая — 100кОм. А значит ее легко придавить к земле помехой или наводкой, что вызовет ложное срабатывание. А еще эти внутренние подтягивающие резисторы очень любят гореть от наводок. У меня уже с десяток микроконтроллеров с убитыми PullUp резисторами. Все работает, но только нет подтяжки — сгорела. Вешаешь снаружи резистор и работает как ни в чем ни бывало. Поэтому, для ответственных схем я настоятельно рекомендую добавить внешнюю подтяжку на 10кОм — даже если внутреннюю накроет, внешняя послужит. В процессе обучения на это можно забить.

Светодиод подключается на порт двумя способами. По схеме Порт-земля или Порт-Питание . В первом случае для зажигания диода надо выдать в порт лог1 — высокий уровень (примерно равен Vcc). Во втором случае для зажжения диода требуется выдать в порт лог0 — низкий уровень (около нуля). Для AVR разницы вроде бы нет, а вот многие старые серии микроконтроллеров вниз тянули куда лучше чем вверх, так что схема Порт-Питание распространена чаще. Я применяю и ту и другую схему исходя из удобства разводки печатной платы. Ну, а на программном уровне разницы особой нет.
Вывод порта для работы со светодиодом надо сконфигурировать на выход (DDRxy=1) и тогда в зависимости от значения в PORTxy на ножке будет либо высокий либо низкий уровень напряжения.

Светодиод надо подключать через резистор . Дело в том, что прямое сопротивление светодиода очень мало. И если не ограничивать ток через него, то он просто напросто может сгореть нафиг. Либо, что вероятней, пожечь вывод микроконтроллера, который, к слову, может тянуть что то около 20-30мА. А для нормального свечения обычному светодиоду (всякие мы не рассматриваем сейчас, эти монстры могут и ампер сожрать) надо около 3…15мА.

Так что, на вскидку, считаем:

  • Напряжение на выходе ноги МК около 5 вольт, падение напряжени на светодиоде обычно около 2.5 вольт (выше нельзя, иначе диод сожрет тока больше чем надо и подавится, испустив красивый дым)
  • Таким образом, напряжение которое должен взять на себя ограничительный резистор будет 5-2.5 = 2.5В.
  • Ток нам нужен 5мА — нефига светодиод зря кормить, нам индикация нужна, а не освещение:)
  • R=U/I= 2.5/5E-3 = 500Ом. Ближайший по ряду это 510 Ом. Вот его и возьмем. В принципе, можно ставить от 220 Ом до 680 Ом что под руку попадется — гореть будет нормально.

Если надо подключить много светодиодов, то на каждый мы вешаем по собственному резистору. Конечно, можно пожадничать и поставить на всех один резистор. Но тут будет западло — резистор то один, а диодов много! Соответственно чем больше диодов мы запалим тем меньше тока получит каждый — ток от одного резистора разделится между четырьмя. А поставить резистор поменьше нельзя — т.к. при зажигании одного диода он получит порцию тока на четверых и склеит ласты (либо пожгет порт).

Немного схемотехнических извратов или пара слов о экономии выводов

То что не удается запаять приходится программировать. (С) народная мудрость.

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

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

Главное, тут следовать двум правилам:

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

Приведу пример:

  • У есть у нас вывод на который повешан выход с некого датчика и кнопка. Выход с датчика может быть 0, 1 в активном режиме и Hi-Z когда на датчик не приходит сигнал Enable.
  • Кнопка же дает на линию жесткий 0, путем короткого замыкания.

Как это должно работать:
Скажем, основную часть времени у нас ввод микроконтроллера настроен на вход Hi-Z и мы снимаем показания с датчика на который подан еще и сигнал Enable. Когда нам надо опросить кнопку, то мы отбираем у датчика Enable и его выходы становятся в режим Hi-Z и нам не мешают. Вывод микроконтроллера мы переводим в режим Pull-Up и проверяем нет ли на входе нуля — сигнал нажатой кнопки. Проверили? Переводим вход МК в Hi-Z вход и подаем Enable на датчик снова. И так много раз в секунду.

Тут у нас возникает два противоречия:

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

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

  • Электрическое противоречие
    Если датчик выставит 1, а мы нажмем кнопку, то очевидно, что GND с Vcc в одном проводе не уживутся и кто нибудь умрет. В данном случае умрет выход датчика, как более слабый — куда там хилому транзистору тягаться с медной кнопкой.

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

    Если у нас, например, вывод датчика может дать не более 10мА, то резистор нужен такой, чтобы ток через него от Vcc до GND не превышал этой величины. При питании 5 вольт это будет 510Ом. Теперь, даже если на линии со стороны датчика будет лог1, высокий уровень, то нажатие на кнопку не вызовет даже искажения логического уровня т.к. резистор рассчитан с учетом максимальной нагрузки порта

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

Ну и несколько примеров нескольких функций на одной ноге:
Во-первых, ISP разьем . Я уже давным давно забыл что такое тыкать микроконтроллер вначале в колодку программатора, потом в плату, потом обратно и так по многу раз, пока прогу не отладишь. У меня на плате торчат 6 выводов ISP разьема и при отладке программатор вечно воткнут в плату, а программу я перешиваю порой по нескольку раз в 10 минут. Прошил — проверил. Не работает? Подправил, перепрошил еще раз… И так до тех пор пока не заработает. Ресурс у МК на перепрошивку исчисляется тысячами раз. Но ISP разьем сжирает выводы. Целых 3 штуки — MOSI, MISO, SCK.

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

На линии под ISP можно повесить и что нибудь другое, главное, чтобы при прошивке это ЧТОТО не начало ВНЕЗАПНО чудить . Например, управление стокилограммовым манипулятором висит на линии ISP и во время прошивки на него пошла куча бредовых данных — так он может свихнуться и кому нибудь бошку разнести. Думать надо, в общем. А вот с каким нибудь , который работает по шинному интерфейсу прокатит такая схема:

Переключаем выход с 0 на 1 и зажигаем то верхний то нижний диод. Если надо зажечь оба, то мы просто переводим вывод микроконтроллера в режим Hi-Z и словно нет его, а диоды будут гореть сквозным током. Либо быстро быстро переключать диоды между собой, в этом случае на глаз они будут оба гореть. Недостаток схемы очевиден — диоды нельзя погасить. Но если по задумке хотя бы один должен гореть, то почему бы и нет? UPD: Тут подумал, а ведь можно подобрать светодиоды и резисторы так, чтобы их суммарное падение напряжения было на уровне напряжения питания, а суммарные резисторы в таком случае загонят ток в такой мизер, что когда нога в Hi-Z то диоды вообще гореть не будут. По крайней мере на глаз это будет не заметно совсем. Разве что в кромешной тьме.

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

А применив сходную тактику к кнопкам можно либо упростить разводку, либо по трем ножкам развести 6 кнопок.
Тут тоже все просто — одна нога дает подтяг, вторая косит под землю. Нажатие кнопки дает просадку напряжения на подтягивающей ножке. Это чует программа, поочередно опрашивающая каждую кнопку. Потом роли ножек меняются и опрашивается следующая кнопка.

В шестикнопочном режиме ситуация схожая — одна ножка дает подтяг, другая землю, а третья прикидывается ветошью Hi-Z и не отсвечивает. Но тут есть один побочный эффект. Например, опрашиваем мы кнопку «В». Для этого у нас верхняя линия встает на вход с подтяжкой (PORTxy=1, DDRxy=0), средня дает низкий уровень на выходе (PORTxy=0, DDRxy=1), нижняя не участвует в процессе ибо стоит в Hi-Z (PORTxy=0, DDRxy=0). Если мы нажмем кнопку «В» то верхняя линия в этот момент просядет и программа поймет что нажата кнопка «В», но если мы не будем жать «В», а нажмем одновременно «Е» и «Б» то верхняя линия также просядет, а программа подумает что нажата «В», хотя она там и рядом не валялась. Минусы такой схемы — возможна неправильная обработка нажатий. Так что если девайсом будут пользоваться быдло-операторы, жмущие на все подряд без разбора, то от такой схемы лучше отказаться.

Ну и, напоследок, схема показывающая как можно обьединить кнопку и светодиод:


Работает тоже исключительно в динамике. То есть все время мы отображаем состояние светодиода — то есть выдаем в порт либо 0 (диод горит) либо Hi-Z (диод не горит). А когда надо опросить кнопку, то мы временно (на считанные микросекунды) переводим вывод в режим вход с подтягом (DDRxy=0 PORTxy=1) и слушаем кнопку. Режим когда на выводе сильный высокий уровень (DDRxy=1 PORTxy=1) включать ни в коем случае нельзя, т.к. при нажатии на кнопку можно пожечь порт.

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

Вот такие пироги. А теперь представьте себе прогу в которой реализованы все эти динамические фичи + куча своего алгоритма. Выходит либо бесконечная череда опросов, либо легион всяких флагов. В таких случаях простейшая диспетчеризация или кооперативная это то что доктор прописал — каждый опрос гонишь по циклу своей задачи и не паришься. Зато юзаешь везде какую-нибудь ATTiny2313 и ехидно глядишь на тех кто в ту же задачу пихает Mega8 или что пожирней:)

Я ничего не знаю и боюсь что либо сжечь, что мне делать???

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

А потом, на примере того как сделана демоплата, попробовать сделать что то свое. Сама же демоплата представляет собой микроконтроллер + немного стартовой периферии, которой хватит на ряд несложных опытов и которая может облегчить подключение и исследование других устройств. Демоплаты есть разные, например фирменные комплексы вроде STK500 или AVR Butterfly или моя которая была спроектированна исходя из моего опыта и на которой будет строится весь дальнейший учебный курс.

Итак мы добрались до неотъемлемой части большинства проектов на микроконтроллерах - до кнопок. Кнопка достаточно простое устройство, имеющее, как правило, всего два состояния, если говорить языком программирования это состояние логической 1 (контакты замкнуты) и логического 0 (контакты разомкнуты). Рассмотрим схему.

Имеем все туже схему с семисегментными индикаторами, но добавлены 4 кнопки. При помощи кнопок группы A будем увеличивать или уменьшать выводимое значение на первых трех индикаторах, а кнопками группы B – изменять значение на последних двух индикаторах.

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

Итак программа.

#include //подключаем библиотеки #include #define SPI_SS PB2 //выход SS #define SPI_MOSI PB3 //выход MOSI #define SPI_MISO PB4 //выход MISO #define SPI_SCK PB5 //выход SCK #define BUTTON_AP PD4 //выход кнопки A+ #define BUTTON_AM PD5 //выход кнопки A- #define BUTTON_BP PD6 //выход кнопки B+ #define BUTTON_BM PD7 //выход кнопки B- char di; void spi(char cmd,char data) //Функция передачи двух пакетов по 8 бит по протоколу SPI { PORTB &= ~(1<999)a=999; //проверка достижения максимального значения переменной a if(a<0)a=0; //проверка достижения минимального значения переменной a if(b>99)b=99; //проверка достижения максимального значения переменной b if(b<0)b=0; //проверка достижения минимального значения переменной b } return 0; }

Приступим. Программу на C обычно начинают с подключения внешних библиотек. За это в программе отвечают две строки:

#include #include

avr/io.h это библиотека ввода/вывода которая объяснит компилятору какие порты ввода/вывода есть у микроконтроллера, как они обозначены и на что они способны. И самое интересное что эта библиотека сама выбирает из настроек проекта для какого микроконтроллера нужно применить описания, то позволяет использовать эту библиотеку для разных микроконтроллеров. Эту библиотеку нужно подключать в первую очередь.

Вторая часто используемая библиотека util/delay.h помогает создавать задержки в выполнении программы, что достаточно удобно.

#define BUTTON_AP PD4

указывает что на выводе PD4 ( регистр 4 порта B) мы будем подключать кнопку A+ ( смотрите схему ). Это нужно для того чтобы если мы вдруг надумаем кнопку переключить на другой вывод то нам не понадобится искать по всей программе, просто изменить в define название вывода PD4 на нужный, при этом в программе так и останется BUTTON_AP. Но в случае с выводами для SPI ничего изменить не получится потому что поддержка SPI аппаратная и жестко привязана к выводам микроконтроллера производителем.

Следующая интересная часть программы это описание портов.

DDRB |= (1<

Так были переключены на вывод перечисленные разряды порта B (по умолчанию все разряды всех портов настроены на ввод) . Э ту строку можно записать следующим образом без использования макроопределения в define

DDRB |= (1<

Эти записи равноценны и предназначены для подстановки 1 в регистр DDRB в соответствующий разряд BR3 ( разряд 3 ), BR5 ( разряд 5 ) и BR2 ( разряд 2 ). Остальные разряды регистра DDRB остаются без изменения. Также можно записать эту строку вот так

DDRB |= (1<<3)|(1<<5)|(1<<2);

что конечно более запутанно.

Запись 1 <<3 означает что двоичную единицу нужно сдвинуть влево на 3 позиции, позиции справа заполняются нулями.

1 <<3 будет означать 1000 (один ноль ноль ноль) в двоичной системе. И при выполнении операции (1<<3)|(1<<5) мы получим 101000 в двоичной системе.

В следующей строке программы подключаем подтягивающие резисторы для кнопок. Запишем единицы в соответствующие разряды регистра PORTD

PORTD |= (1<

Эти резисторы встроены в микроконтроллер. Их можно подключить на разряды порта с условием что эти разряды определены как ввод. Резисторы подтягивают нужный вывод микроконтроллера к логической 1.

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

Теперь рассмотрим основную часть программы которую микроконтроллер после основных настроек выполняет бесконечно. Эта часть программы заключена в бесконечный цыкл.

While(1) { };

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

Delay_ms(100);

для предотвращения ложного срабатывания от «дребезга» контактов кнопки. И второй раз опрашиваем кнопки о их нажатии и сверяем выставленным ранее флагам о нажатии. Если условия соблюдены изменяем значения выводимые на индикаторы. Затем сбрасываем флаги нажатия кнопок в 0. После чего цикл повторяется.

В следующей - заключительной части будет рассмотрен АЦП (аналого-цифровой преобразователь) и применение всего ранее изученного на практике.




В МК ATMega16 есть три таймера/счетчика – два 8-битных (Timer/Counter0, Timer/Counter2) и один 16-битный (Timer/Counter1). Каждый из них содержит специальные регистры, одним из которых является счетный регистр TCNTn (n – это число 0, 1 или 2). Каждый раз, когда процессор выполняет одну команду, содержимое этого регистра увеличивается на единицу (либо каждые 8, 64, 256 или 1024 тактов). Потому он и называется счетным. Помимо него, есть еще и регистр сравнения OCRn (Output Compare Register), в который мы можем сами записать какое-либо число. У 8-битного счетчика эти регистры 8-битные. По мере выполнения программы содержимое TCNTn растет и в какой-то момент оно совпадет с содержимым OCRn. Тогда (если заданы специальные параметры) в регистре флагов прерываний TIFR (Timer/Counter Interrupt Flag Register) один из битов становится равен единице и процессор, видя запрос на прерывание, сразу же отрывается от выполнения бесконечного цикла и идет обслуживать прерывание таймера. После этого процесс повторяется.

Ниже представлена временная диаграмма режима CTC (Clear Timer on Compare). В этом режиме счетный регистр очищается в момент совпадения содержимого TCNTn и OCRn, соответственно меняется и период вызова прерывания.

Это далеко не единственных режим работы таймера/счетчика. Можно не очищать счетный регистр в момент совпадения, тогда это будет режим генерации широтно-импульсной модуляции, который мы рассмотрим в следующей статье. Можно менять направление счета, т. е. содержимое счетного регистра будет уменьшаться по мере выполнения программы. Также возможно производить счет не по количеству выполненных процессором команд, а по количеству изменений уровня напряжения на «ножке» T0 или T1 (режим счетчика), можно автоматически, без участия процессора, менять состояние ножек OCn в зависимости от состояния таймера. Таймер/Счетчик1 умеет производить сравнение сразу по двум каналам – А или В.

Для запуска таймера нужно выставить соответствующие биты в регистре управления таймером TCCRn (Timer/Counter Control Register), после чего он сразу же начинает свою работу.

Мы рассмотрим лишь некоторые режимы работы таймера. Если вам потребуется работа в другом режиме, то читайте Datasheet к ATMega16 – там все подробнейше по-английски написано, даны даже примеры программ на С и ассемблере (недаром же он занимает 357 страниц печатного текста!).

Теперь займемся кнопками.

Если мы собираемся использовать небольшое количество кнопок (до 9 штук), то подключать их следует между «землей» и выводами какого-либо порта микроконтроллера. При этом следует сделать эти выводы входами, для чего установить соответствующие биты в регистре DDRx и включить внутренний подтягивающий резистор установкой битов в регистре PORTx. При этом на данных «ножках» окажется напряжение 5 В. При нажатии кнопки вход МК замыкается на GND и напряжение на нем падает до нуля (а может быть и наоборот – вывод МК замкнут на землю в отжатом состоянии). При этом меняется регистр PINx, в котором хранится текущее состояние порта (в отличие от PORTx, в котором установлено состояние порта при отсутствии нагрузки, т. е. до нажатия каких-либо кнопок). Считывая периодически состояние PINx, можно определить, что нажата кнопка.

ВНИМАНИЕ! Если соответствующий бит в регистре DDRx будет установлен в 1 для вашей кнопки, то хорошее нажатие на кнопку может привести к небольшому пиротехническому эффекту – возникновению дыма вокруг МК. Естественно, МК после этого придется отправить в мусорное ведро…

Перейдем к практической части. Создайте в IAR новое рабочее пространство и новый проект с именем, например, TimerButton. Установите опции проекта так, как это описано в предыдущей статье. А теперь наберем следующий небольшой код.

#include "iom16.h" void init_timer0(void ) //Инициализация таймера/счетчика0 { OCR0 = 255; //Содержимое регистра сравнения //Задаем режим работы таймера TCCR0 = (1 void init_timer2(void ) //Инициализация таймера/счетчика2 { OCR2 = 255; TCCR2 = (1 //Устанавливаем для него прерывание совпадения } void main (void ) { DDRB = 255; init_timer0(); init_timer2(); while (1) { } } #pragma vector = TIMER2_COMP_vect //Прерывание по таймеру2 __interrupt void flashing() { if ((PORTB & 3) == 1) { PORTB &= (0xFF // Отключение выводов PB0, PB1 PORTB |= 2; // Включение PB1 } else { PORTB &= (0xFF // Отключение выводов PB0, PB1 PORTB |= 1; // Включение PB0 } }

Давайте посмотрим, как это работает. В функциях init_timern задаются биты в регистрах TCCRn, OCRn и TIMSK, причем такой способ может кому-нибудь показаться странным или незнакомым. Придется объяснить сначала, что означает запись «(1

где a – это то число, двоичное представление которого нужно сдвинуть, а b показывает, на сколько битов нужно его сдвинуть. При этом возможна потеря значения, хранящегося в a (т.е. не всегда возможно восстановить из С то, что было в а). Рассмотрим пример:

Что окажется в С после выполнения строки C = (22

2 в двоичном коде будет выглядеть как 00010110, а после сдвига влево на 3 бита получим С = 10110000.

Аналогично существует и сдвиг вправо. Еще пример:

char C; … C = ((0xFF > 2);

Сначала выполнится действие во внутренних скобках (0xFF – это 255 в шестнадцатеричном коде), из 11111111 получится 11111100, потом произойдет сдвиг вправо и получим С = 00111111. Как видим, здесь две взаимно обратные операции привели к другому числу, т. к. мы потеряли два бита. Этого не произошло бы, если бы переменная С была типа int, т. к. int занимает 16 бит.

Теперь рассмотрим еще два битовых оператора, широко применяющиеся при программировании МК. Это оператор «побитовое и» (&) и «побитовое или» (|). Как они действуют, думаю, будет понятно из примеров:

Действие: Результат (в двоичном коде): С = 0; // C = 00000000 C = (1 // C = 00100101 C |= (1 // C = 00101101 C &= (0xF0 >> 2); // C = 00101100 C = (C & 4) | 3; // C = 00000111

Чуть не забыл! Есть еще «побитовое исключающее или» (^). Оно сравнивает соответствующие биты в числе, и, если они одинаковые, возвращает 0, иначе единицу.

Вернемся к нашей программе. Там написано «(1

/* Timer/Counter 0 Control Register */ #define FOC0 7 #define WGM00 6 #define COM01 5 #define COM00 4 #define WGM01 3 #define CS02 2 #define CS01 1 #define CS00 0

При компиляции программы запись WGM01 просто заменяется на число 3, и в результате получается уже корректная запись. WGM01 называется макросом и он, в отличие от переменной, не занимает места в памяти (разве что в памяти программиста:-).

Если заглянуть теперь в Datasheet, но нетрудно будет увидеть, что WGM01 – это имя третьего бита в регистре TCCR0. То же самое касается и остальных битов этого регистра. Это совпадение не случайно и относится ко всем регистрам МК (или почти ко всем). Т. е., написав «(1

Итого, строчка

означает, что включен режим СТС, при срабатывании таймера0 меняется состояние «ножки» ОС0 (Она же PB3), содержимое счетчика увеличивается каждые 1024 такта.

Аналогично для таймера2: TCCR2 = (1

В регистре TIMSK (Timer/counter Interrupt MaSK register) задается режим прерываний. Мы написали

что означает прерывание таймера2 по совпадении TCNT2 и OCR2. Самая последняя функция – это собственно функция прерывания совпадения таймера2. Прерывания объявляются следующим образом:

#pragma vector = ВЕКТОР __interrupt ТИП ИМЯ()

где ВЕКТОР – это макрос вектора прерывания (по смыслу просто число, характеризующее это прерывание); эти макросы в порядке снижения приоритета перечислены в файле iom16.h. ТИП – тип возвращаемого функцией значения, в нашем случае void (ничего). ИМЯ – произвольное имя для этой функции. С прерываниями мы еще успеем наработаться в будущем.

При выполнении нашей функции должны по очереди моргать светодиоды, подключенные к PB0 и PB1. Судя по всему, частота равна 11059200/(256*1024) = 42 Гц. Это быстро, но будет заметно невооруженным глазом. Кстати, применение таймеров дает возможность отсчитывать точные временные интервалы, не зависящие от сложности вашей программы и порядка ее выполнения (но если у Вас не более одного прерывания).

Итак, сохраняем файл как «TimerDebug.c», добавляем его в проект, компилируем, прошиваем МК. Что же мы видим? Светодиод, подключенный к выводу PB3, будет активно моргать, а на PB0 и PB1 нет ни каких изменений. В чем же дело? Неужели что-то неверно?

Чтобы это выяснить, придется отладить нашу программу. Поскольку в IAR нет Debuggerа, придется использовать AVR Studio. Эту среду разработки можно скачать с сайта производителя http://atmel.com . Проблем с ее установкой, думаю, не должно быть. Перед запуском AVR Studio выберите в IAR режим Debug и создайте отладочный cof-файл (все опции проекта должны быть выставлены, как описано в предыдущей статье).

Открыв AVR Studio, мы увидим окно приветствия, в котором выберем «Open». Теперь лезем в папку с проектом, там в Debug\Exe, выбираем там «TimerDebug.cof», создаем проект там, где предложат, выбираем дивайс ATMega16 и режим отладки Simulator. После этого, если все сделали правильно, сразу же идет процесс отладки

Среда отладки здесь очень удобная, т.к. позволяет просматривать содержимое всех регистров МК, а также вручную устанавливать значения для них щелчками мыши. Например, если установить флаг прерывания в регистре TIFR в бите 7 (под черным квадратом в TIMSK), то следующим шагом программы (нажатие F10 или F11) должна быть обработка прерывания (флаг будет установлен автоматически и при совпадении регистров TCNT2 и OCR2). Но, к нашему удивлению, прерывания не будет!

Возникает вопрос: почему?

Откроем регистр CPU, SREG. Этот регистр определяет работу процессора, а конкретно седьмой его бит (I-бит, Interrupt bit) ответственен за обработку всех прерываний в МК. У нас он не установлен. Стоит его выставить, как сразу же пойдет выполняться прерывание (если одновременно установлен седьмой бит в TIFR).

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

Значит, нужно добавить строчку кода для установки этого бита в единичное состояние. Добавим мы его в функцию init_timer2. Получится следующее:

void init_timer2(void ) { SREG |= (1 //Добавили эту строчку OCR2 = 255; TCCR2 = (1

Теперь, выбрав конфигурацию Release и прошив МК нажатием F7 и запуском AVReal32.exe, с радостью увидим, что все работает как надо.

Замечание: при отладке программы следует уменьшать интервалы таймеров, если они слишком длинные, т. к. в процессе отладки в AVR Studio программа выполняется в тысячи раз медленнее, чем внутри МК и вы не дождетесь срабатывания таймера. В целом отладка полностью аналогична таковой в других системах программирования, таких, как Visual C++.

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

#include "iom16.h" long unsigned int counter = 0; //Счетчик для формирования временных интервалов unsigned char B0Pressed = 0; //Здесь хранится состояние кнопки0 (0 - не нажата, 1 - нажата) unsigned char B1Pressed = 0; //Здесь хранится состояние кнопки1 (0 - не нажата, 1 - нажата) //Инициализация таймера2 //Нужно каждые 11059 такта (1 мс) увеличивать counter. У нас получается каждые 1,001175 мс void init_timer2() { OCR2 = 173; TCCR2 = (1 //Инициализация портов ввода/вывода init_io_ports() { DDRA =(1//формирование задержки в Pause_ms миллисекунд void delay(long unsigned int Pause_ms) { counter = 0; while (counter void main() { SREG |= (1 //Разрешаем прерывания init_timer2(); //Включаем таймер2 на каждые 64 такта, считать до 173 init_io_ports(); //Включаем порты ввода/вывода while (1) { //Обработка кнопки 0 if (B0Pressed == 1) { // уведичивает PORTB, ждет отпускания PORTB++; B0Pressed = 0; while ((PINC & (1 else { if ((PINC & (1 //Фиксирует нажатие { delay(50); if ((PINC & (1 //Проверяет нажатие { B0Pressed = 1; } } } //Обработка кнопки 1 if (B1Pressed == 1) //Если произошло нажатие на кнопку, { // уменьшает PORTB, ждет отпускания PORTB--; B1Pressed = 0; while ((PINC & (1 else { if ((PINC & (1 //Фиксирует нажатие { delay(200); //Устранение "дребезга клавиш" if ((PINC & (1 //Проверяет нажатие { B1Pressed = 1; //Устанавливает флаг "кнопка нажата" } } } } } //Прерывание по таймеру 2, прн этом увеличение счетчика counter #pragma vector = TIMER2_COMP_vect __interrupt void inc_delay_counter() { counter++; }

Сначала предлагаю взять уже готовый файл прошивки (файлы к статье, папка Release, файл TimerButton.hex или откомпилировать этот текст) и записать его в МК. После чего вынуть кабель прошивки, подключить к PC0 и PC1 кнопки и попробовать их понажимать. Увидим, что при нажатии на одну из кнопок увеличивается регистр PORTB (загораются светодиоды), а при нажатии на другую – уменьшается. Если не работает – попробуйте понажимать одну кнопку, удерживая другую – будет действовать. Дело в том, что я подключал кнопки следующим образом: при нажатии на кнопку вывод МК «болтается» в воздухе, а при отпускании замыкается на землю. Если вы подключили кнопки по-другому, то придется лишь чуть модернизировать программу.

Давайте разберемся с кодом. Здесь работа с таймером организована несколько иначе. Он срабатывает каждые 11072 такта (то есть каждые 1,001175 мс) и увеличивает содержимое переменной counter. Есть еще функция delay(long unsigned int Pause_ms), которая берет в качестве параметра количество миллисекунд Pause_ms, сбрасывает counter и ждет, когда counter достигнет значения Pause_ms, после чего продолжает работу МК. Таким образом, написав delay(1500), мы сформируем задержку в программе в 1,5 секунды. Это очень удобно для формирования временных интервалов.

С таймером вроде все понятно. Но для чего он используется? Рассмотрим бесконечный цикл while(1) в main(). В этом цикле проверяется состояние кнопок путем анализа содержимого регистра PINB. А зачем там стоит задержка на 50 мс? Это устранение т. н. «дребезга клавиш». Дело в том, что при нажатии на кнопку происходит удар одного контакта о другой, и, поскольку контакты металлические, удар этот упругий. Контакты, пружиня, замыкаются и размыкаются несколько раз, несмотря на то, что палец сделал лишь одно нажатие. Это приводит к тому, что МК фиксирует несколько нажатий. Давайте рассмотрим график зависимости напряжения на выходе PC0 от времени. Он может выглядеть так:

Точка А – момент нажатия кнопки. Он может быть зафиксирован МК. Затем идут несколько замыканий и размыканий (их может и не быть, а может быть и 12 штук – это явление можно считать случайным). В точке B контакт уже надежно зафиксирован. Между A и B в среднем около 10 мс. Наконец, в точке D происходит размыкание. Как же избавиться от этого неприятного явления? Оказывается, очень просто. Нужно зафиксировать момент нажатия кнопки (точка А), через какое-то время, например, 50 мс (точка С) проверить, что кнопка действительно нажата, сделать действие, соответствующее этой кнопке и ждать момент ее отпускания (точка D). То есть нужно сделать паузу от А до С, такую, чтобы весь «дребезг» оказался внутри этой паузы. А попробуйте теперь убрать строчку, формирующую задержку, откомпилировать программу и зашить ее в МК. Путем простых нажиманий на кнопки сможете легко убедиться, что все эти «мучения» не были напрасными.

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

При подаче на вывод PB0 лог. 1 (+5В), а на выводы PB1 и PB2 лог. 0 разрешается обработка кнопок 1, 4 и 7. После этого состояние каждой из них можно узнать, проверив напряжение на одном из выводов PB3..PB5. Таким образом, подавая последовательно на выводы PB0..PB2 лог. 1, можно определить состояние всех кнопок. Понятное дело, что выводы PB0..PB2 должны быть выходами, а PB0..PB2 входами. Чтобы определить, какое количество выводов потребуется для массива из Х кнопок, нужно найти пару сомножителей Х, сумма которых наименьшая (для нашего случая с 40 кнопками это будут числа 5 и 8). Это означает, что к одному МК можно подключить до 256 кнопок (а с применение дешифраторов и того больше, но о дешифраторах потом). Лучше сделать меньшее число выводов выходами, а большее – входами. В этом случае опрос всех строк матрицы займет меньше времени. Подобный способ подключения (стробирование) свойственен не только для кнопок. Там можно подключать самые разнообразные устройства, начиная от матриц светодиодов и заканчивая микросхемами flash-памяти.

© Киселев Роман
Июнь 2007

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

Аппаратная реализация.

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


Но устройство AVR контроллеров позволяет упростить схему за счет внутренних ресурсов чипа. В качестве резистора R* можно задействовать встроенный резистор в МК. Правда, это надо будет программно прописать (не забудьте об этом!!!). В конечном варианте схема будет выглядеть вот так:

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

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

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

Программная реализация.

Простейший случай.

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

Алгоритм получился примерно вот такой:

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

Дребезг контактов.

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

Для человека такое время колебания незначительно – мы его даже не заметим. Другое дело, микроконтроллер: предположим, проверка кнопки осуществляется в цикле, а контроллер работает на частоте 8 МГц, в таком случае, проверка кнопки будет осуществляться несколько тысяч, если не десятков тысяч раз в секунду. Может получиться так, (а скорее всего так и получится) что за то время, пока происходит дребезг, программа еще раз проверит кнопку – в таком случае предсказать результат сложно (раз на раз не приходится)

Программный антидребезг

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


Важно помнить, что при отпускании кнопки дребезг так же будет происходить!


Ожидание отпускания кнопки.

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

В плане теории, в общем-то все) Поздравляю, если вы дочитали до этого места!

Пример применения всего вышеописанного.

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

Реализация данного алгоритма зависит от того, на каком языке вы пишите, и как вы это делаете. В качестве примера привожу реализацию в компиляторе CodeVision AVR

200?"200px":""+(this.scrollHeight+5)+"px");">
/*****************************************************
This program was produced by the
CodeWizardAVR V2.04.0a Evaluation
Automatic Program Generator
© Copyright 1998-2009 Pavel Haiduc, HP InfoTech s.r.l.
http://www.hpinfotech.com

Project: Test
Version: 1.0.0.0.1
Date: 20.09.2009

Chip type: ATmega8
Program type: Application
AVR Core Clock frequency: 16,000000 MHz
Memory model: Small
External RAM size: 0
Data Stack size: 256
*****************************************************/

#include
#include

// Declare your global variables here
bit flag = 0;

Void set(){ // функция смены значения на выходе
flag = !flag;
PORTD.4 = flag;
}
void main(void)
{
// Declare your local variables here

// Input/Output Ports initialization
// Port B initialization
// Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In
// State7=T State6=T State5=T State4=T State3=T State2=T State1=P State0=T
PORTB=0x02;
DDRB=0x00;

// Port C initialization
// Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In
// State6=T State5=T State4=T State3=T State2=P State1=T State0=T
PORTC=0x04;
DDRC=0x00;

// Port D initialization
// Func7=In Func6=In Func5=In Func4=Out Func3=In Func2=In Func1=In Func0=In
// State7=T State6=T State5=T State4=0 State3=T State2=T State1=T State0=T
PORTD=0x00;
DDRD=0x10;

// Timer/Counter 0 initialization
// Clock value: Timer 0 Stopped
TCCR0=0x00;
TCNT0=0x00;

// Timer/Counter 1 initialization
// Clock source: System Clock
// Clock value: Timer 1 Stopped
// Mode: Normal top=FFFFh
// OC1A output: Discon.
// OC1B output: Discon.
// Noise Canceler: Off
// Input Capture on Falling Edge
// Timer 1 Overflow Interrupt: Off
// Input Capture Interrupt: Off
// Compare A Match Interrupt: Off
// Compare B Match Interrupt: Off
TCCR1A=0x00;
TCCR1B=0x00;
TCNT1H=0x00;
TCNT1L=0x00;
ICR1H=0x00;
ICR1L=0x00;
OCR1AH=0x00;
OCR1AL=0x00;
OCR1BH=0x00;
OCR1BL=0x00;

// Timer/Counter 2 initialization
// Clock source: System Clock
// Clock value: Timer 2 Stopped
// Mode: Normal top=FFh
// OC2 output: Disconnected
ASSR=0x00;
TCCR2=0x00;
TCNT2=0x00;
OCR2=0x00;

// External Interrupt(s) initialization
// INT0: Off
// INT1: Off
MCUCR=0x00;

// Timer(s)/Counter(s) Interrupt(s) initialization
TIMSK=0x00;

// Analog Comparator initialization
// Analog Comparator: Off
// Analog Comparator Input Capture by Timer/Counter 1: Off
ACSR=0x80;
SFIOR=0x00;

While (1)
{
if (PINC.2==0) { // ждем нажатия
if (PINC.2==0) set(); // если все норм, переключаем
else continue;
}
else continue;

While(PINC.2==0) {}; // ждем отпускания кнопки
delay_ms(50); // антидребезг
};
}

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

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

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

Итак, что у умеет наша кнопка?

  • Ее можно нажимать кратко
  • Можно жать длинно
  • Можно делать разные комбинации нажатий
  • Ее можно отпускать в нужный момент


Не густо, но вполне ничего. Для одной кнопки то. Главный затык при написании этого не просрать массу системных ресурсов (время процессора, таймеры и тыды) на обработку этой несчастной кнопки. Ведь нам придется отслеживать факт нажатия, факт отжатия, время нажатия, число нажатий с разным временем. Я видел такие адовые реализации этого интерфейса, что просто диву давался как можно нагородить столько тупняков и тормозов в этой одной сосне, да еще потратить все таймеры:)

Так что ТЗ выставим следующее:

  • Никаких аппаратных таймеров, кроме таймера диспетчера.
  • Никаких временных задержек, только вызов себя же по таймеру.
  • Никаких ожиданий нажатия-отжатия в цикле. Зашли, проверили — отдали управление.
  • Введем временной интервал mode_time, в течении которого будем отслеживать комбинацию нажатий. Скажем 2с
  • На выходе будем иметь число коротких и длинных нажатий за данный интервал

Алгоритм
Сделаем все на конечном автомате. У него будут три состояния:

  • Up — кнопка не нажата
  • Dn — кнопка нажата
  • Al — кнопка отпущена после длительного нажатия

А также будет одна служебная процедура, которая спустя mode_time (2c) после первого экшна с кнопкой сгребет все результаты и что-нибудь с ними сделает. Что — это уже не важно. От программы зависит.
И вся эта дребедень будет крутиться в цикле, вызывая сама себя через диспетчер (или каким еще образом) раз в 20мс.

Up
Входим.
Смотрим не нажата ли кнопка? Если нет — выходим. Если нажата, то переводим автомат в положение Dn
Проверяем первый ли раз за интервал мы тут? Если первый, то поставим нашу служебную процедуру на отложенный запуск (через 2с), взведем флаг, что процесс пошел.
Выходим.

Dn
Входим.
Еще нажата? Если нет, значит кнопка уже отпущена, скидываемся в состояние в Up и засчитываем одно короткое нажатие, увеличивая счетчик коротких нажатий cnt_s . Если еще нажата, то щелкаем счетчиком времени замера длительности нажатия Timе. Замер длительности у нас идет в итерациях автомата. Одна итерация 20мс. В лимит длинного нажатия я заложил 20 итераций, что дает около 400мс. Все что больше 0.4с считаем длинным нажатием. Как натикает больше 20 итераций, то засчитываем одно длинное нажатие и перекидываем автомат в состояние Al. Выходим.

Al
Входим.
Еще не отпустили? Если нет, то выходим. Если кнопка отпущена, то перебрасываемся в Up, скинув переменную Time.


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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; // Переменные - флажки нажатых кнопок. // Эффективней их сделать битовыми полями // Но мне было лень. Оптимизируйте сами:) u16 bt_mode_time = 2000 ; // Длительность последовательности анализа // Сделано переменной, а не константой // чтобы можно было менять на лету. Снижая // там, где не надо ждать длительных комбинаций // что сильно повышает отзывчивость интерфейса u08 bt_cnt; // Флаг сигнализирующий, что идет анализ последовательности u08 bt_cnt_s; // Счетчик коротких нажатий u08 bt_cnt_l; // Счетчик длинных нажатий void bt_scan(void ) // Эта функция сканер. Она должна вызываться раз в 20мс { #define up 0 // Дефайны состояний автомата. 0 - по дефолту. #define dn 1 #define al 3 static u08 bt_state= 0 ; // Переменная состояния автомата static u08 bt_time = 0 ; // Переменная времени работы автомата switch (bt_state) // Собственно автомат { case up: { if (Close) // Если нажата кнопка { bt_state = dn; // Стадию в Down bt_time = 0 ; // Обнуляем счетчик времени if (bt_cnt== 0 ) // Если первый раз в комбинации { SetTimerTask(bt_ok, bt_mode_time) ; // Запускаем процедуру анализа bt_cnt = 1 ; // Подсчет пошел! } } break ; // Выход } case dn: { if (Close) // Все еще нажато? { if (20 > bt_time) // Нажато меньше чем 20*20мс? { // Да bt_time++; // Увеличиваем счетчик итераций } else { bt_state = al; // Нет, уже больше! Да у нас длинное нажатие! Переходим в АЛ bt_time = 0 ; // Сбрасываем время замера нажатия bt_cnt_l++; // Засчитываем одно длинное нажатие } } else { bt_state = up; // Кнопка отпущена? bt_time = 0 ; // Время замера в ноль bt_cnt_s++; // Счетчик коротких нажатий } break ; // Выход } case al: // А тут мы если было длинное нажатие { if (Open) // Отпустили? { bt_state = up; // Да! Стадию в Up bt_time = 0 ; // Время в 0 bt_al = 1 ; // Зафиксировали событие "Отпускание после длинного" } break ; } } SetTimerTask(bt_scan, 20 ) ; // Зациклились через диспетчер. } // А это функция которая через 2 секунды сработает и подберет все результаты подсчета void bt_ok(void ) // Ловим дешифровку событий тут { switch (bt_cnt_s) // Смотрим сколько нажатий коротких { case 1 : bt1 = 1 ; break ; // Такой флажок и ставим case 2 : bt2 = 1 ; break ; case 3 : bt3 = 1 ; break ; case 4 : bt4 = 1 ; break ; case 5 : bt5 = 1 ; break ; default : break ; } switch (bt_cnt_l) // Смотрим сколько нажатий длинных { case 1 : bt_l = 1 ; break ; // Такой флажок и ставим case 2 : bt_l2 = 1 ; break ; default : break ; } bt_cnt = 0 ; // Сбрасываем счетчики bt_cnt_s = 0 ; bt_cnt_l = 0 ; }

#include u08 bt1,bt2,bt3,bt4,bt5,bt_l,bt_l2,bt_al; // Переменные - флажки нажатых кнопок. // Эффективней их сделать битовыми полями // Но мне было лень. Оптимизируйте сами:) u16 bt_mode_time = 2000; // Длительность последовательности анализа // Сделано переменной, а не константой // чтобы можно было менять на лету. Снижая // там, где не надо ждать длительных комбинаций // что сильно повышает отзывчивость интерфейса u08 bt_cnt; // Флаг сигнализирующий, что идет анализ последовательности u08 bt_cnt_s; // Счетчик коротких нажатий u08 bt_cnt_l; // Счетчик длинных нажатий void bt_scan(void) // Эта функция сканер. Она должна вызываться раз в 20мс { #define up 0 // Дефайны состояний автомата. 0 - по дефолту. #define dn 1 #define al 3 static u08 bt_state=0; // Переменная состояния автомата static u08 bt_time =0; // Переменная времени работы автомата switch(bt_state) // Собственно автомат { case up: { if(Close) // Если нажата кнопка { bt_state = dn; // Стадию в Down bt_time = 0; // Обнуляем счетчик времени if(bt_cnt==0) // Если первый раз в комбинации { SetTimerTask(bt_ok,bt_mode_time); // Запускаем процедуру анализа bt_cnt =1; // Подсчет пошел! } } break; // Выход } case dn: { if(Close) // Все еще нажато? { if (20 > bt_time) // Нажато меньше чем 20*20мс? { // Да bt_time++; // Увеличиваем счетчик итераций } else { bt_state = al; // Нет, уже больше! Да у нас длинное нажатие! Переходим в АЛ bt_time = 0; // Сбрасываем время замера нажатия bt_cnt_l++; // Засчитываем одно длинное нажатие } } else { bt_state = up; // Кнопка отпущена? bt_time = 0; // Время замера в ноль bt_cnt_s++; // Счетчик коротких нажатий } break; // Выход } case al: // А тут мы если было длинное нажатие { if(Open) // Отпустили? { bt_state = up; // Да! Стадию в Up bt_time = 0; // Время в 0 bt_al = 1; // Зафиксировали событие "Отпускание после длинного" } break; } } SetTimerTask(bt_scan,20); // Зациклились через диспетчер. } // А это функция которая через 2 секунды сработает и подберет все результаты подсчета void bt_ok(void) // Ловим дешифровку событий тут { switch(bt_cnt_s) // Смотрим сколько нажатий коротких { case 1: bt1 = 1; break; // Такой флажок и ставим case 2: bt2 = 1; break; case 3: bt3 = 1; break; case 4: bt4 = 1; break; case 5: bt5 = 1; break; default: break; } switch(bt_cnt_l) // Смотрим сколько нажатий длинных { case 1: bt_l = 1; break; // Такой флажок и ставим case 2: bt_l2 = 1; break; default: break; } bt_cnt = 0; // Сбрасываем счетчики bt_cnt_s = 0; bt_cnt_l = 0; }

Код написан так, что на AVR там завязана буквально пара строчек. По крайней мере в коде обработчика нажатий кнопки. Все привязки на железо идут в хидере, да и их там всего ничего:

1 2 3 4 5 6 7 8 9 10 11 #include #include #define Open (BTN_PIN & 1< #define Close (!Open) extern void bt_scan(void ) ; void bt_ok(void ) ; extern u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; extern u16 bt_mode_time;

#include #include #define Open (BTN_PIN & 1<

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

Все описанное в статье мясо лежит в двух файлах button.c и button.h

Видео работы

Дребезг
Боротся с дребезгом тут уже не обязательно. Т.к. частота сканирования небольшая, так что даже голимая и наглухо окисленная кнопка модели ТМ2 не давала дребезга — он кончался раньше, чем наступал следующий скан. А вот что тут можно докурить, так это защиту от ложных сработок в результате наводок. Ведь стоит помехе продавить линию в момент считывания и засчитается сработка однократного нажатия. Это можно избежать сделав проверочные состояния автомата. Скажем добавив в Up счетчик итераций, чтобы в течении, скажем, двух-трех итераций подтвердить, что кнопка таки нажата и только тогда переходить в Dn.

Варианты
Правда в своем проекте я несколько изменил обработку. Т.к. мне не нужны были множественные длинные нажатия, то я сделал выставление флага «Длинное нажатие» сразу же в обработчике AL и, заодно, убрал подсчет числа длинных нажатий. Что позволило повысить отзывчивость работы интерфейса прибора, где длительным нажатием осуществлялся вход в пункт меню, а комбинаций с двумя длинными нажатиями не использовались вообще.

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