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

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

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

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

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

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

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

  • У есть у нас вывод на который повешан выход с некого датчика и кнопка. Выход с датчика может быть 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.
В принципе, на эти выводы можно еще повесить и кнопки. В таком случае никто никому мешать не будет, главное во время прошивки не жать на эти кнопки. Также можно повесить и светодиоды (правда в этом случае простейший программатор Громова может дать сбой, а вот USBasp молодцом!) тогда при прошивке они будут очень жизнерадостно мерцать:)))

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

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

Ножки можно зажать, например, на светодиодах:

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

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

Тут все просто — превращая один из выводов то в 0 то в 1 гоняем ток то в одну сторону то в другую. В результате горит то один то другой диод. Для погашения обоих — переводим ноги в какое то единое положение 11 или 00. Два диода сразу зажечь не получится, но можно сделать динамическую индикацию — если их быстро быстро переключать, то глаз не заметит подставы, для него они будут оба горящими. А добавив третью линию можно по трем ногам прогнать до 6 светодиодов на том же принципе.

А применив сходную тактику к кнопкам можно либо упростить разводку, либо по трем ножкам развести 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) включать ни в коем случае нельзя, т.к. при нажатии на кнопку можно пожечь порт.
Минусы — при нажатии на кнопку зажигается светодиод как ни крути. Впрочем, это может быть не багой, а фичей:)

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

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

О какой нагрузке идет речь? Да о любой — релюшки, лампочки, соленоиды, двигатели, сразу несколько светодиодов или сверхмощный силовой светодиод-прожектор. Короче, все что потребляет больше 15мА и/или требует напряжения питания больше 5 вольт.

Вот взять, например, реле. Пусть это будет BS-115C. Ток обмотки порядка 80мА, напряжение обмотки 12 вольт. Максимальное напряжение контактов 250В и 10А.

Подключение реле к микроконтроллеру это задача которая возникала практически у каждого. Одна проблема — микроконтроллер не может обеспечить мощность необходимую для нормальной работы катушки. Максимальный ток который может пропустить через себя выход контроллера редко превышает 20мА и это еще считается круто - мощный выход. Обычно не более 10мА. Да напряжение у нас тут не выше 5 вольт, а релюшке требуется целых 12. Бывают, конечно, реле и на пять вольт, но тока жрут больше раза в два. В общем, куда реле не целуй — везде жопа. Что делать?

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

Поскольку у нас принято, что 1 это включено, а 0 выключено (это логично, хотя и противоречит моей давней привычке, пришедшей еще с архитектуры AT89C51), то 1 у нас будет подавать питание, а 0 снимать нагрузку. Возьмем биполярный транзистор. Реле требуется 80мА, поэтому ищем транзистор с коллекторным током более 80мА. В импортных даташитах этот параметр называется Ic, в наших Iк. Первое что пришло на ум - КТ315 - шедевральный совковый транзистор который применялся практически везде:) Оранжевенький такой. Стоит не более одного рубля. Также прокатит КТ3107 с любым буквенным индексом или импортный BC546 (а также BC547, BC548, BC549). У транзистора, в первую очередь, надо определить назначение выводов. Где у него коллектор, где база, а где эмиттер. Сделать это лучше всего по даташиту или справочнику. Вот, например, кусок из даташита:

Обратите внимание на коллекторный ток - Ic = 100мА (Нам подоходит!) и маркировку выводов.

Цоколевка нашего КТ315 определяется так

Если смотреть на его лицевую сторону, та что с надписями, и держать ножками вниз, то выводы, слева направо: Эмиттер, Колектор, База.

Берем транзистор и подключаем его по такой схеме:

Коллектор к нагрузке, эмиттер, тот что со стрелочкой, на землю. А базу на выход контроллера.

Транзистор это усилитель тока, то есть если мы пропустим через цепь База-Эмиттер ток, то через цепь Колектор-Эмиттер сможет пройти ток равный входному, помноженному на коэффициент усиления hfe.
hfe для этого транзистора составляет несколько сотен. Что то около 300, точно не помню.

Максимальное напряжение вывода микроконтроллера при подаче в порт единицы = 5 вольт (падением напряжения в 0.7 вольт на База-Эмиттерном переходе тут можно пренебречь). Сопротивление в базовой цепи равно 10000 Ом. Значит ток, по закону Ома, будет равен 5/10000=0.0005А или 0.5мА — совершенно незначительный ток от которого контроллер даже не вспотеет. А на выходе в этот момент времени будет Ic=Ibe*hfe=0.0005*300 = 0.150А. 150мА больше чем чем 100мА, но это всего лишь означает, что транзистор откроется нараспашку и выдаст максимум что может. А значит наша релюха получит питание сполна.

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

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

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

Красота! Но можно сделать еще лучше — снизить потребление. У реле довольно большой ток срывания с места, а вот ток удержания якоря меньше раза в три. Кому как, а меня давит жаба кормить катушку больше чем она того заслуживает. Это ведь и нагрев и энергозатраты и много еще чего. Берем и вставляем в цепь еще и полярный конденсатор на десяток другой микрофарад с резистором. Что теперь получается:

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

Добавим еще один апгрейд.
При размыкании реле энергия магнитного поля стравливается через диод, только вот при этом в катушке продолжает течь ток, а значит она продолжает держать якорь. Увеличивается время между снятием сигнала управления и отпаданием контактной группы. Западло. Надо сделать препятствие протеканию тока, но такое, чтобы не убило транзистор. Воткнем стабилитрон с напряжением открывания ниже предельного напряжения пробоя транзистора.
Из куска даташита видно, что предельное напряжение Коллектор-База (Collector-Base voltage) для BC549 составляет 30 вольт. Вкручиваем стабилитрон на 27 вольт — Profit!

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

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

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

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

МОП (по буржуйски MOSFET) расшифровывается как Метал-Оксид-Полупроводник из этого сокращения становится понятна структура этого транзистора.

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

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

Недостаток же вытекает из его емкостного свойства — наличие емкости на затворе требует большого зарядного тока при открытии. В теории, равного бесконечности на бесконечно малом промежутки времени. А если ток ограничить резистором, то конденсатор будет заряжаться медленно - от постоянной времени RC цепи никуда не денешься.

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

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

У меня самыми ходовыми МОП транзисторами являются IRF630 (n канальный) и IRF9630 (p канальный) в свое время я намутил их с полтора десятка каждого вида. Обладая не сильно габаритным корпусом TO-92 этот транзистор может лихо протащить через себя до 9А. Сопротивление в открытом состоянии у него всего 0.35 Ома.
Впрочем, это довольно старый транзистор, сейчас уже есть вещи и покруче, например IRF7314, способный протащить те же 9А, но при этом он умещается в корпус SO8 - размером с тетрадную клеточку.

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

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

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

  • Применить транзистор с малым отпирающим напряжением. Например из серии IRL630A или им подобные. У них открывающие напряжения привязаны к логическим уровням. У них правда есть один недостаток — их порой сложно достать. Если обычные мощные полевики уже не являются проблемой, то управляемые логическим уровнем бывают далеко не всегда.

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

Выбор транзистора тоже не очень сложен, особенно если не заморачиваться на предельные режимы. В первую очередь тебя должно волновать значение тока стока - I Drain или ID выбираешь транзистор по максимальному току для твоей нагрузки, лучше с запасом процентов так на 10. Следующий важный для тебя параметр это VGS - напряжение насыщения Исток-Затвор или, проще говоря, управляющее напряжение. Иногда его пишут, но чаще приходится выглядывать из графиков. Ищешь график выходной характеристики Зависимость ID от VDS при разных значениях VGS. И прикидыываешь какой у тебя будет режим.

Вот, например, надо тебе запитать двигатель на 12 вольт, с током 8А. На драйвер пожмотился и имеешь только 5 вольтовый управляющий сигнал. Первое что пришло на ум после этой статьи — IRF630. По току подходит с запасом 9А против требуемых 8. Но глянем на выходную характеристику:

Видишь, на 5 вольтах на затворе и токе в 8А падение напряжения на транзисторе составит около 4.5В По закону Ома тогда выходит, что сопротивление этого транзистора в данный момент 4.5/8=0.56Ом. А теперь посчитаем потери мощности — твой движок жрет 5А. P=I*U или, если применить тот же закон Ома, P=I2R. При 8 амперах и 0.56Оме потери составят 35Вт. Больно дофига, не кажется? Вот и мне тоже кажется что слишком. Посмотрим тогда на IRL630.

При 8 амперах и 5 вольтах на Gate напряжение на транзисторе составит около 3 вольт. Что даст нам 0.37Ом и 23Вт потерь, что заметно меньше.

Если собираешься загнать на этот ключ ШИМ, то надо поинтересоваться временем открытия и закрытия транзистора, выбрать наибольшее и относительно времени посчитать предельную частоту на которую он способен. Зовется эта величина Switch Delay или ton, toff, в общем, как то так. Ну, а частота это 1/t. Также не лишней будет посмотреть на емкость затвора Ciss исходя из нее, а также ограничительного резистора в затворной цепи, можно рассчитать постоянную времени заряда затворной RC цепи и прикинуть быстродействие. Если постоянная времени будет меньше чем период ШИМ, то транзистор будет не открыватся/закрываться, а повиснет в некотором промежуточном состоянии, так как напряжение на его затворе будет проинтегрировано этой RC цепью в постоянное напряжение.

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

А в процессе проектирования схемы запомни еще одно простое правило — ни в коем случае нельзя оставлять висеть затвор полевика просто так — иначе он нажрет помех из воздуха и сам откроется. Поэтому обязательно надо поставить резистор килоом на 10 от Gate до GND для N канального или на +V для P канального, чтобы паразитный заряд стекал.

Тиристор

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

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

Если речь идет о переменном токе, то лучше использовать симисторы или тиристоры. Что это такое? А сейчас расскажу.

Симистор BT139
Схема включения из даташита на MOC3041

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

Если соединить встречно параллельно два тиристора, то получится симистор — отличная штука для коммутации нагрузки на переменном токе.

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

Но тут есть одна тонкость — коммутируем мы силовую высоковольтную цепь, 220 вольт. А контроллер у нас низковольтный, работает на пять вольт. Поэтому во избежание эксцессов нужно произвести потенциальную развязку. То есть сделать так, чтобы между высоковольтной и низковольтной частью не было прямого электрического соединения. Например, сделать оптическое разделение. Для этого существует специальная сборка — симисторный оптодрайвер MOC3041. Замечательная вещь!
Смотри на схему подключения — всего несколько дополнительных деталек и у тебя силовая и управляющая часть разделены между собой. Главное, чтобы напряжение на которое расчитан конденсатор было раза в полтора два выше напряжения в розетке. Можно не боятся помех по питанию при включении и выключении симистора. В самом оптодрайвере сигнал подается светодиодом, а значит можно смело зажигать его от ножки микроконтроллера без всяких дополнительных ухищрений.

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

Ну, а в качестве симистора рекомендую BT139 - с хорошим радиатором данная фиговина легко протащит через себя ток в 16А

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

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

Рассмотрим режим работы, когда при каждом клике ногой CLK происходит сдвиг битов влево по направлению к старшему (S0 поднята, S1 опущена). Взглянем на сдвиговый регистр U1. При каждом дрыге ногой CLK, бит, который находится на выводе Qn, перемещается на вывод Qn+1, тоесть сдвигается в сторону старшего бита (влево). Но так как биту, который находится на ноге Q7 уже некуда сдвигаться, то он по идее должен бы был пропасть. Чтобы этого не произошло, мы посылаем его на следующий сдвиговй регистр U2, подключив Q7 регистра U1 к ноге SR SER регистра U2. Объясню, что же это за нога. В рассматриваемом нами режиме работы сдвигового регистра (S0 поднята, S1 опущена) биты смещаются в cторону старшего, а на место младшего становится бит, который в данный момент находится на ноге SR SER. Так как два наших сдвиговых регистра тактируются от одного источка (микроконтроллера), то бит на ноге Q7 сдвигового регистра U1, при сдвиге не теряется, а перескакивает на сдвиговый регистр U2, где продолжает свой путь в микроконтроллер.
Помимо SR SER, существует нога SL SER. Она обладает практически идентичными свойствами, за исключением того, что она используется при сдвигании регистров вправо, а не влево (режим, который мы не используем, S0 опущена, S1 поднята. В данном режиме биты будут двигаться по направлению к младшему байту, т.е вправо).

Таким образом, соеденив два сдвиговых регистра, мы по сути получаем один 16-ти битный, поведение которого абсолютно идентично 8-ми битному, за исключением того, что каждый раз нам необходимо считывать не 8, а 16 бит. Как я уже говорил в первой статье, время на сканирования такой клавиатуры возрастает примерно в 2 раза. Безболезненно к данной схеме можно добавлять все новые и новые сдвиговые регистры, подключая Q7 пин предыдущего к SR SER последующего. В данной статье мы ограничимся лишь двумя.

Ниже представлена схема данного устройства

Тыц крупным планом

Схема упрощенная, показано только подключение клавиатуры и LCD. Питание и прочая обвязка контроллера как обычно.

Повторюсь, немаловажная деталь - подтягивающие резисторы R1 - R16. Если вы не знаете их назначения, прочитайте еще раз пункт “Описание кнопок” в первой части.

Далее переходим к написанию кода, который будет сканировать все 16 кнопок и что-то делать.

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

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

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

  • библиотека для работы с LCD (lcd4,asm, lcd4_macro.inc, от DI HALT’a)
  • Функция для прекодирования кирилических символов в ANSI кодировке, в символы, которые будут понятны дисплею. (ansi2lcd.asm) Данное решение встретил на форуме. Переписал его с Си на Асм и пользуюсь).
  • Функция для сканирования нашей клавиатуры. Собственно ее я опишу в этой статье. Я ее вынес отдельным файлом, для удобства ее последующего использования (keyb_scan_init.asm, keyb_scan.asm)

Сканирование клавиатуры
Принцип сканирования клавиатуры я описал в предыдущей статье. В данном случае у нас будет небольшое отличие, т.к. нам нужно считать не 8, а 16 бит, т.е. 2 байта, со сдвиговых регистров.

Общий план действий

  1. Устанавливаем бит T в регистре SREG. (Это пользовательский бит, который можно использовать для любых нужд. В нашем случае установленный бит будет означать, что мы считываем первый байт с нашей клавиатуры, если при проверке этот бит будет сброшен, то будем считать, что действие происходит со считыванием второго байта).
  2. В цикле считываем 8 бит из сдвигового регистра.
  3. Проверяем бит T:
    • Если он установлен, то мы только что считали первый байт, прячем его в закрома, сбрасываем бит T и возвращаемся на пункт 2.
    • Если он сброшен, то мы только что считали второй байт. Задача выполнена, выходим.

Код

1234567891011121314151617181920212223242526272829303132333435363738394041 .equ BTN_PORT = PORTA.equ BTN_DDR = DDRA.equ BTN_PIN = PINA.equ BTN_DATA_IN = 0.equ BTN_HOLD = 1.equ BTN_CLK = 2 btn_start: SBI BTN_PORT,BTN_HOLD; поднимаем S1 SBI BTN_PORT,BTN_CLK; Кликаем CBI BTN_PORT,BTN_CLK CBI BTN_PORT,BTN_HOLD; опускаем S1 SET; устанавливаем бит T в регистре SREG.; данный бит мы устанавливаем как флаг того, что мы считываем первый байт. btn_again: LDI R17,0; в этом регистре будет накапливаться результат. обнуляем его LDI R18,8; счетчик. цикл будем проделывать 8 раз btn_loop: LSL R17; если мы проходим тут, первый раз, то данная команда с нулем ничего не; сделает, если же нет, то двигаем все биты влево SBIC BTN_PIN,BTN_DATA_IN; если к нам на вход пришла 1, INC R17; записываем 1 в самый младший разряд регистра R17 SBI BTN_PORT,BTN_CLK; кликаем CBI BTN_PORT,BTN_CLK DEC R18; уменьшаем счетчик BREQ btn_loop_end; если счетчик досчитал до нуля, то переходим в btn_loop_end Rjmp btn_loop; иначе повторяем цикл, где первой же командой сдвигаем все биты влево.;Таким образом старые старшие байты постепенно сдвигаются на свое место. btn_loop_end: BRTC btn_exit; если бит T сброшен (а это значит, что мы уже приняли второй байт), то выходим из функции CLT; иначе сбрасываем бит T (это значит что мы закончили прием первого байта, и будем; принимать второй MOV R16,R17; сохраняем первый принятый байт в регистре R16 RJMP btn_again; и возвращаемся к считыванию байта btn_exit: RET

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

123 SBI BTN_DDR,BTN_HOLD ;выход HOLD SBI BTN_DDR,BTN_CLK ;Выход CLK SBI BTN_PORT,BTN_DATA_IN ;вход DATA_IN

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

12 .include "init.asm"; в данном файле хранится общая инициализация. include "keyb_scan_init.asm"; инициализация ног для сканирования клавиатуры

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

1234567 SysLedOn: SetTimerTask TS_SysLedOff,500 SBI PORTD,5 RET ;----------------------------------------------------------------------------- SysLedOff: SetTimerTask TS_SysLedOn,500 CBI PORTD,5 RET

И запустим их во время старта в области Background

1 RCALL SysLedOn

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

Далее перейдем в сканированию клавиатуры. Создадим задачу KeyScan и запустим ее в области Background

12 Background: RCALL SysLedOn RCALL KeyScan

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

12 Code_Table:.dw Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8Code_Table2:.dw Key9, Key10, Key11, Key12, Key13, Key14, Key15, Key16

Это две таблицы, одна для клавиш с 1 по 8, другая - с 9 по 16. В ней последовательно расположены адреса на функции, которые мы будем выполнять в зависимости от того, какая кнопка нажата.
Для этого мы заранее загрузим адрес начала таблицы в регистровую пару Z, и затем, вычислив, какая же по счету кнопка была нажата, прибавим это смещение к адресу начала таблицы. Получим адрес с ячейкой, в которой содержится адрес функции, которую нужно выполнить. Звучит немного сложно, но на самом деле, все достаточно просто и понятно. Главно вчитаться в предыдущее предложение.

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

Другая особенность:
При отсутствии нажатий с клавиатуры приходит 0b11111111. Тоесть ненажатая кнопка - высокий уровень. К примеру нажмем кнопку 3, и к нам придет число 0b11110111 (соответствующий бит сброшен). Поэтому выведем алгоритм: пришедший байт мы сначала будем сравнивать с маской 0b11111110, потом с 0b11111101, затем 0b11111011 и т.д. Мы просто будем в цикле сдвигать биты в маске влево, каждый раз сверяя ее с пришедшим байтом и увеличивая счетчик. В тот момент, когда будет совпадение - в счетчике будет номер нажатой кнопки. Что нам собственно и требуется.
В функции будет использоваться один байт из оперативной памяти.

123 ; RAM ===========================================. DSEGKeyFlag:.byte 1

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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 KeyScan: SetTimerTask TS_KeyScan,50 RCALL btn_start; сканируем клавиатуру. результат приходит в регистрах R16 и R17 SET; ставим флаг T в регистре SREG. Он будет означать, что мы обрабатываем первый принятный с клавиатуры байт LDI ZL,low(Code_Table*2); берем адрес первой таблицы с переходами; (для кнопок 1-8) LDI ZH,high(Code_Table*2) KS_loop: CPI R16,0xFF; если байт равен 0xFF, то нажатия не было, BREQ KS_loop_exit; переходим на обработку следующего байта с клавиатуры LDS R18,KeyFlag; берем последнее зарегестрированное нажатие CP R16, R18; сравниваем с текущим BREQ KS_loop_exit; если одинаковы, переходим на обработку следующего; байта с клавиатуры STS KeyFlag,R16; иначе сохраняем в RAM текущее нажатие; как последнее зарегестрированное PUSH R16 PUSH R17 SetTimerTask TS_ClearFlag, 200; ставим на запуск через 200 мс функцию очистки; последнего зарегестрированного нажатия POP R17; данная функция использует R16 и POP R16; R17, поэтому сохраняем их в стеке RJMP KS_got_smth; если мы дошли до этого места, то у нас; есть нажатие, которое нужно обработать. идем на обработку KS_loop_exit: BRTC KS_exit; проверяем флаг T в регистре SREG. Если он; не сброшен, а значит мы считали только; один байт с клавиатуры, то идем; дальше, иначе выходим CLT; сбрасываем флаг T.Это означает что мы считали; первый байт с клавиатуры, и готовы; ко второму. LDI ZL,low(Code_Table2*2); берем адрес второй таблицы с LDI ZH,high(Code_Table2*2); переходами (для кнопок 9-16) MOV R16,R17; второй принятый байт перекидываем в R16 RJMP KS_loop; и возвращаемся в цикл; тут мы оказываемся, когда нам нужно обработать нажатие.; KS_got_smth: CLR R18; R18 будет счетчиком. Нужно сравнить 8 возможных состояний пришедшего байта,; поэтому будем считать до 8 LDI R19,0b11111110; первоначальная маска для сравнения ее с пришедшим битом, и дальнейшего сдвигания влево KS_loop2: CP R16,R19; сравниваем маску с пришедшим байтом BREQ KS_equal; если равны, то переходим на действие INC R18; иначе увеличиваем счетчик CPI R18,8; сравниваем его с восьмеркой BREQ KS_exit; если досчитали до 8, то выходим SEC; тут двигаем нашу маску влево. так как младшие байты нам нужно заполнять; единицами, а функция ROL устанавливаем эту единицу только при наличии флага C,;то устанавливаем этот флаг ROL R19; двигаем биты в маске RJMP KS_loop2; и переходим опять на цикл KS_equal: LSL R18; R18 хранится число, до которого мы успели досчитать,; пока ждали совпадения байта; с клавиатуры с маской.В нем по сути находится номер нажатой кнопки.; умножаем его на 2, так как в талице переходов адреса; хранятся по 2 байта ADD ZL,R18; складываем смещение с заранее сохраненным адресом таблицы переходов ADC ZH,R0; в R0 я всегда храню ноль LPM R16,Z+ ;загружаю необходимый адрес из таблицы LPM R17,Z MOV ZL,R16; перекидываем его в адресный регистр Z MOV ZH,R17 ICALL; и вызываем функцию по этому адресу KS_exit: RET

Вместо ICALL можно в данном случае применить IJMP будет примерно тот же эффект, но выход из KeyScan будет через RET в вызваной функции. Не так очевидно, зато сэкономим два байта стека:) Формально это можно представить как то, что наша функция KeyScan это этакий многозадый кащей. Вошли в одну голову, а вывались через одну из задниц определенных нажатием клавиши.

Наверняка вы заметили следующуюю строчку:

1 SetTimerTask TS_ClearFlag,200

Данный макрос устанавливает на выполнение функцию ClearFlag через 200 мс. Данная функция должна удалить из ячейки KeyFlag в оперативной памяти информацию о прошлом нажатии. Так как при отсутствии нажатий с клавиатуры приходит байт 0b11111111, то в функции ClearFlag и будем записывать в ячейку KeyFlag это число:

123 ClearFlag: SER R16; R16 = 0xFF STS KeyFlag,R16; сохраняем это в RAM RET

Теперь рассмотрим таблицу с адресами переходов повнимательнее.

1 Code_Table:.dw Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8

Code_Table — адрес начала таблицы. Прибавляя к этому адресу необходимое нам смещение, мы будем получать адрес ячейки, в которой хранится адрес перехода (Key1, Key2, Key3 и т.д) на нужную нам функцию. Директива.dw означает что для каждого элемента, описанного далее в строке выделяется по 2 байта. Выделяем столько, ибо адреса у нас двухбайтовые.

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

12345 Key1: LDI R16,0x02; просто какой-то случайный код. не несет в себе смысла.; Тут вы подставите то, что нужно будет выполнить вам при нажатии на кнопку 1 LDI R17,0x03 SUB R16,R17 RET; обязательно выход из этой функции по RET, иначе будет переполнение стека

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

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

123456789 Letter_K_Table1:.db 0x2E,0x2C,0x3F,0x21,0, 0 ;""., ",", "?", "!" Letter_K_Table2:.db 0xE0,0xE1,0xE2,0xE3,0, 0 ;а, б, в, г Letter_K_Table3:.db 0xE4,0xE5,0xE6,0xE7,0, 0 ;д, е, ж, з Letter_K_Table4:.db 0xE8,0xE9,0xEA,0xEB,0, 0 ;и, й, к, л Letter_K_Table5:.db 0xEC,0xED,0xEE,0xEF,0, 0 ;м, н, о, п Letter_K_Table6:.db 0xf0,0xf1,0xf2,0xf3,0, 0 ;р, с, т, у Letter_K_Table7:.db 0xf4,0xf5,0xf6,0xf7,0, 0 ;ф, х, ц, ч Letter_K_Table8:.db 0xf8,0xf9,0xfa,0xfb,0, 0 ;ш, щ, ъ, ы Letter_K_Table9:.db 0xfc,0xfd,0xfe,0xff,0, 0 ;ь, э, ю, я

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

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

В обработчике нажатия первой кнопки Key1 запишем следующий код:

123456 ; символы "." "," "?" "!" key1: LDI ZL,low(Letter_K_Table1*2); загружаем в Z адрес начала таблицы LDI ZH,high(Letter_K_Table1*2); с символами, принадлежащей первой кнопке LDI R16,1; загружаем в R16 номер нажатой кнопки RCALL lcd_write_l; вызов функции вывода символа в видеопамять RET

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

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

Символы в LCD мы будем записывать не напрямую, а через промежуточную видеопамять, которая будет находиться в оперативной памяти (Хехехе дается мне на это повлиял алгоритм демопроги, что шел в документации к Pinboard прим. DI HALT;)). Это сделано для удобства последующего наращивания функционала программы. Набранный текст будет проще сохранять, обрабатывать, посылать на ПК и т.д. Позднее мы создадим задачу обновления дисплея, которая, периодически запускаясь, будет записывать символы из видеопамяти в дисплей. Получается такого рода отвязка основной логики программы от железа.
При необходимости, с легкостью можно будет применить любой другой дисплей, переписал лишь только функцию его обновления. Данную абстракцию логики программы от железа я произвожу в учебных целях. Пусть даже данное решение излишне для нашего задания, но правильно написанная программа впоследствии ползволяет сэкономить кучу времени себе и другим программистам. Поэтому лучше сразу привыкать писать правильно. (Как писать правильно, а как нет, это лишь мое сугубо личное мнение. У кого-то оно может отличаться. Я не навязываю свою точку зрения, я рассказываю то, что знаю сам).

Создаем ячейки для видеопамяти в RAM и кое-какие переменные:

123456 .equ LCD_MEM_WIDTH = 32; размер памяти LCD. у меня дисплей 2 строки по 16 символов. LCDMemory:.byte LCD_MEM_WIDTH PushCount:.byte 1; счетчик нажатий на кнопку KeyFlagLong:.byte 1; тут хранится номер последней нажатой кнопки CurrentPos:.byte 1; текущее положение курсора

Далее, привожу код всей функции.

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 .equ delay = 1000; задержка перед сдвигом курсора lcd_write_l: LDS R17,KeyFlagLong; загружаем номер последней нажатой кнопки CP R16,R17; сравниваем его с текущей нажатой кнопкой BREQ lwl_match ;---нажата новая кнопка--- lwl_not_match: STS KeyFlagLong,R16; была нажата другая кнопка. сохраняем ее номер в RAM CLR R17 STS PushCount,R17; и обнуляем счетчик нажатий кнопки, ибо эту кнопку мы нажали первый раз RJMP lwl_action ;---повторно нажата кнопка--- lwl_match: LDS R17,PushCount; если же была нажата кнопка повторно INC R17; увеличиваем счетчик нажатий LDS R18,CurrentPos DEC R18; сдвигаем текущее положение курсора; влево. так как нам необходимо будет; заново переписать букву на прежнем месте STS CurrentPos,R18 PUSH R17; макрос SetTimerTask использует регистр R17, поэтому заранее сохраняем его в стеке SetTimerTask TS_Reset_KeyFlagLong,delay; ставим задачу отчистки номера; о текущей кнопки.по истечению; этого времени мы сможет повторно; одной кнопкой вывести вторую букву POP R17 lwl_action: ADD ZL,R17 ;прибавляем смещение к адресу таблицы с ANSI; символами, принадлежащими данной кнопке ADC ZH,R0 LPM R16,Z; загружаем нужный нам символ из таблицы CPI R16,0; проверка на ноль. Если ноль - то конец таблицы BRNE lwl_next_act; если не конец таблицы, то продолжаем действие переходом на next_act SUB ZL,R17; иначе нам нужно вернуться на начало таблицы, SBCI ZH,0; поэтому обратно вычитаем смещение из адреса нашей таблицы CLR R17; в R17 у нас лежит счетчик нажатий. Обнуляем его. RJMP lwl_action; и повторяем все действие заново. но как будто это наше первое нажатие; на данную кнопку lwl_next_act: STS PushCount,R17; прямчем в RAM счетчик нажатий RCALL ansi2lcd; преобразование ANSI в кодировку, пригодную для LCD.; Вход и выход - R16. изменяет регистр R17 lwl_wr_mem: LDS R17,CurrentPos; загуржаем текущее положение курсора LDI ZL,low(LCDMemory*2); загружаем адрес таблицы видеопамяти LDI ZH,high(LCDMemory*2) ADD ZL,R17; складываем смещение (положение курсора) с началом таблицы ADC ZH,R0; R0 я держу всегда нулем ST Z,R16; сохраняем символ в видеопамяти INC R17; увеличиваем на 1 текущее положение CPI R17,LCD_MEM_WIDTH; сравниваем, достигло ли текущее положение конца памяти LCD BRNE lwl_not_end CLR R17; если да, обнуляем текущее положение lwl_not_end: STS CurrentPos,R17; и сохраняем текущее положение в RAM RET

RCALL ansi2lcd - данная строчка вызывает функцию преобразования ANSI символа в кодировку, понятную LCD на базе HD44780. Так как по умолчанию эти дисплеи плохо дружат с кирилицей, приходится немного извращаться, чтоб корректно выводить кирилические символы. Принцип действия данной функции я описывать не буду, можете самостоятельно подсмотреть код в файле ansi2lcd.asm. Скажу лишь, что символ посылаем в регистре R16, и получаем оттуда же. Данная функция также изменяет регистр R17, будьте аккуратны, не оставляйте в нем ничего нужного.

Вообщем, запись необходимого символа в видеопамять у нас реализована. Перейдем к функции отрисовки дисплея из видеопамяти. Она будет в цикле поочередно брать символы из видеопамяти и посылать их в LCD. По сути ничего сложного. Единственно надо будет отследить, когда курсор достигнет конца первой строки, затем перевести его на вторую. Иначе символы запишутся не в видимую часть дисплея. Подробнее об видимых и невидимых областях памяти дисплея можно прочитать в это статье http://easyelectronics.ru/avr-uchebnyj-kurs-podklyuchenie-k-avr-lcd-displeya-hd44780.html

Создадим новую задачу ОС и назовем ее LCD_Reflesh. Поставим ее на первоначальный запуск в области Background

123 Background: RCALL SysLedOn RCALL KeyScan RCALL LCD_Reflesh

и напишем саму функцию:

12345678910111213141516171819202122 LCD_Reflesh: SetTimerTask TS_LCD_Reflesh,100; запускаем обновление дисплея каждый 100 мс LDI ZL,low(LCDMemory*2); грузим в Z адрес видеопамяти LDI ZH,high(LCDMemory*2) LCD_COORD 0,0; устанавливаем текуюю координату курсора в LCD в самое начало LDI R18,LCD_MEM_WIDTH; грузим в R18 длину видеопамяти. это будет нас счетчик lcd_loop: LD R17,Z+; цикл. тут мы берем из видеопамяти один символ в регистр R17 RCALL DATA_WR; и записываем его в LCD. DEC R18; уменьшаем счетчик BREQ lcd_exit; если достигли конца видеопамяти - выходим CPI R18,LCD_MEM_WIDTH/2; достигли ли конца первой строки? brne lcd_next LCD_COORD 0,1; если да, устанавливаем текущую координату; курсора в LCD на вторую строчку lcd_next: RJMP lcd_loop; и продолжаем цикл lcd_exit: RET

Можно считать что минимальный рабочий код написал. Компилируем и прошиваем. Работать будут кнопки с буквами и символами. Подключаем к микроконтроллеру наш блок клавиатуры и LCD дисплей.

Клавиатура:

  • 1 пин блока кнопок - PORTA.0
  • 2 пин блока кнопок - PORTA.1
  • 3 пин блока кнопок - PORTA.2
  • 4 пин блока кнопок - +5 V
  • 5 пин блока кнопок - Общий провод (Ground)

LCD дисплей:

  • Пин E - PORTB.0
  • Пин RW - PORTB.1
  • Пин RS - PORTB.2
  • Пин Data.4 - PORTB.4
  • Пин Data.5 - PORTB.5
  • Пин Data.6 - PORTB.6
  • Пин Data.7 - PORTB.7

Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: