Хабрахабр

[Из песочницы] На столбе висят три глаза, или сказ о том, что пяти ног ATtiny13 вполне достаточно


КДПВ «Ой, всё».

Но если вы зачем-то трогали руками arduino, в кладовке пылится паяльник, понимаете, почему у батарейки один плюс, а у С++ два, то вас не смогут оставить равнодушными поистине волшебные и удивительные чудеса. Мало шансов, что сей лонгрид станет живительным источником мудрости интеллектуалам, искушенным в тайнах гадания на картах Карно и познавшим потаенный смысл Третьей Нормальной Формы. Итак, имею удовольствие рекомендовать вам номера сегодняшнего представления бродячего цирка «Саман с Самшитом»:

  • Добавление RAM и ROM в ATtiny13!
  • Искусственный интеллект в микропроцессор — про и контра, или спящая красавица — ну она не дура ли?
  • Или все таки dura lex sed lex?
  • Как добавить ножек в ATtiny13?
  • Пару слов о пятом измерении: как впихнуть невпихуемое?
  • Распиливание напополам не-девствениц с перемешиванием содержимых половин (с гарантией восстановления).
  • Номер «Кормление страждущих» (см. более ранний случай насыщения пяти тысяч человек пятью ячменными хлебами и двумя рыбами).

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

Светофор первый или Каа принимает бой.

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

Тем, кто такого навыка пока не приобрел, объясню — с великой степенью вероятности сейчас родителей ошарашивают чем-то наподобие: «Папа! Имеющие родительский опыт, прекрасно знают, что именно в такой момент можно услышать. В детский садик! Нам завтра! Принести поделку на конкурс ёлочных игрушек! Надо! И я хочу первое место!».

Завтра. Надо. Поделку. Нам. Лучшую.

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

На страну накатывалась ночь, а у нас закипала работа.

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

Козырьки над лампами по сделанной выкройке обводились на обыкновенной 80-ти граммовой бумаге и вырезались вручную. Крышки верха и низа светофора раскроены из картона по месту.

Затем были приклеены на ПВА.

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

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

Каптерщицкая жаба наотрез отказалась выдавать папе из наличия ЩЗР еще и аж три таблеточных литиевых батарейки, а светиться от одной не соглашались светодиоды, заявляя, что они белые и яркие, и падение напряжения на них от 2. Подсвечивать изнутри было решено парой сверхъярких белых светодиодов из щедрых закромов родины (ЩЗР). 9 вольт. 8 до 3. Поразмыслив и погуглив, папа пришлось принять предложение. Максимум, на что удалось выторговаться с жабой — на одну батарейку АА, ферритовое колечко от дросселя сгоревшей материнки и транзистор КТ315. Да и учитывая количество шаловливых ручек в каждой группе детского сада, идея с литиевыми элементами питания смотрелась не особо привлекательной.

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

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

Совершенно простая схема получения переменного напряжения с пиками в 3-7В от 1,5В батарейки G1 давно и широко известна.

Физически принципиальная схема воплотилась вот так:

В красной изоленте — алкалиновая батарейка АА.

А сам светофор в сборе — вот так.

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

С первым местом на конкурсе, правда, не сложилось — крайний срок сдачи конкурсных работ, как оказалось, уже несколько дней как прошел.

И крутилась мысль "а можно ли полное решение уместить, скажем в 13ю тиньку, что у жабы в «ЩЗР» давно валяется? Дома оставались два корпуса на подоконнике и папина зудящая неудовлетворенность решением задачи «хочу как настоящий светофор». Хватит ли пяти ножек, килобайта на код и 64 байта оперативки и чтобы, как заказывалось, как настоящий"?

Так что всё это только присказка была, сказка об окончательном решении светофорного вопроса впереди.

Светофор второй, как настоящий

Используемый инструментарий, материалы и документация

6/1. Язык/фреймворк: C / Arduino 1. 8.
IDE: MS Visual Studio 2012 + Visual Micro plugin + git.
CAD: DipTrace.
HW: МК ATtiny13, клон Arduino Nano на ATmega328, USB-UART на FT232R, программатор china-noname USBISP.
Технология ПП: ЛУТ.
Инструменты: Паяльник, клеевой пистолет, кухонная духовка, нож, ножницы, кусачки.
Материалы: Из «ЩЗР» — по 4 красных, желтых, зеленых выводных LED марки noname, SMD танталовый конденсатор и пара резисторов 0805 по 10к, полдюжины выводных 0,25Вт токоограничивающих резисторов MF-25, китайский повышающий DC-DC 5В преобразователь, аэрозольные баллончики с серебряной и черной краской, бумага А4, полимерная глина с распродажи.

Исходники и документы доступны на Github под лицензией MIT, коммиты исходного кода совпадают с нижеописанными итерациями прошивки, все упоминаемые пути к файлам — относительно корня репозитория.

Документация:
./docs/ATtiny13A datasheet.pdf [Спецификация на МК Atmel ATtiny13A]
./docs/ATmega328 datasheet.pdf [Спецификация на МК Atmel ATmega328]
./docs/AVR4027 — Tips and Tricks to Optimize Your C Code.pdf [Atmel AVR4027: Tips and Tricks to Optimize Your C Code for 8-bit AVR Microcontrollers]
./docs/AVR4013 — PicoPower basics.pdf [Заметки по режимам энергосбережения]

Ихний езык Wiring — иезуитски тонкое издевательство над всем, что может быть свято у embed-программистов. Из Arduino IDE, как из облака одеяло. А тут еще дядюшка Ляо, готовый продать пригоршню клонов по цене одного оригинала. Но Arduino, как экосистема, жил, жив и жить будет, и в силу простоты установки окружения на компьютеры под различные ОС, и потому, что они первыми дали возможность без особых затрат на хардварный программатор-отладчик, используя копеечный UART и фирменный бутлоадер, заниматься разработкой в достаточно серьёзных по начинке МК. То, что в старших атмегах можно отладить логику и работу программы, а потом с минимальными изменениями перенести на ту же тиньку — это тоже Довод.

О плагине VisualMicro, добавляющем в MS VisualStudio поддержку Arduino уже было достаточно подробно и толково написано на хабре.
Фокус " Arduino на ATtiny13" на хабре так же был рассмотрен давно и неоднократно. Мне привычно и комфортно работать в MS VisualStudio, лишнего места для AtmelStudio или WinAVR нет и изыскивать не хочу, без аппаратного отладчика ультимативных удобств они не несут.

Вкратце — в установленную инсталляцию среды Arduino добавить модуль MicroCore, после чего появляется возможность выбора в целевых платах МК ATtiny13.

Привычка несложная, но правильная: начиная работу над любой программой просто в командной строке набрать "git init" в каталоге. Git — безальтернативен для любых, даже самых домашних проектов. Встроенная в MS Visual Studio поддержка комитов в локальном репозитории не раз сэкономит нервы и время.

Кроме этого, мне импонирует их политика лицензирования: ограничений бесплатной для России, Украины, Республики Беларусь лицензии Non profit standard (1000 pins, 4 signal layers), в 99% случаев хватает для домашних поделок. DipTrace, как САПР для схемотехники и печатной платы — удобный, отечественный, схематика и разводка дружелюбна к пользователю, легкое добавление пользовательских компонентов (УГО, контакты), при желании печатную плату с компонентами можно покрутить в 3D, качественная справка и обучающие уроки в комплекте.

По-быстрому набросал командных файлов для компиляции/прошивки — в директории ./gcc в проекте. Мне не удалось нормально заставить заработать Arduino Nano, как программатор для тиньки, но при наличии USB ISP программатора проблема решается несложно.

Файл .ino (а реально он С/C++) безошибочно компилирующийся в студии "под ардуино под ATtiny13", просто подается параметром этого батника в командной строке:

>"./gcc/0_MAKE & upload.cmd" MyArduinoFile.ino

Если в этот момент воткнут ISP программатор, а к нему присоединен МК, то пройдёт и компиляция и прошивка.

>"./gcc/0_MAKE & asm.cmd" MyArduinoFile.ino

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

Душа светофора

Upgrade ROM&RAM под Arduino для ATtiny13

Скелет Arduino программ (последовательно setup(); loop(){...};) незримо для ардуинщика при компилировании фактически "заворачивается" в классическую С-шную int main(){setup(); loop();}, немножк прибавляя в весе за счет накладных расходов by Arduino.

uint8_t cnt;
void setup() { cnt=0;
}
void loop() { cnt++;
}
//Program size: 164 bytes (used 16% of a 1 024 byte maximum) (0,57 secs)
//Minimum Memory Usage: 5 bytes (8% of a 64 byte maximum)

Собственно абсолютно то же самое, но без вызова функций на простом С:

uint8_t cnt;
int main(){ cnt=0; // < setup() while(1){ cnt++; // < loop() }
}
//Program size: 60 bytes (used 6% of a 1 024 byte maximum) (1,23 secs)
//Minimum Memory Usage: 1 bytes (2% of a 64 byte maximum)

А еще вызов любой функции до самого её завершения незримо расходует память как минимум на адрес возврата в стеке, а стек — это тоже часть физически наличествующих 64 байт оперативки. Кроме уменьшения с 16% до 6% требуемой программной памяти, обратите внимание, добавилось 6% бесплатной оперативной памяти на переменных.

Интеллект спящей красавицы

Неинтеллектуальный процессор будет раз за разом прокручивать бесконечный цикл, бесполезно переводя энергию в тепло, и лишь изредка, при изменении внешних условий, выполнять работу по изменению состояния. Обязательным признаком интеллекта, безусловно, является лень: разумное существо не станет выполнять бесполезную работу. В целях повышения интеграционного показателя разума на планете Грязь, переводим тиньку на следующую ступень интеллектуального развития: наш CPU будет большую часть времени спать, просыпаясь и работая только когда это необходимо. Более разумный процессор, напротив, работает только тогда, когда внешние условия уже поменялись. Во время сна без участия "мозга"-CPU все равно будет работать таймер, увеличивающийся во сне на единицу каждые (9. Работа у него — инкрементировать глобальную переменную, хранящую значение времени каждые N единиц времени. 0001067 секунды. 6 МГц тактов в секунду / значение делителя таймера 1024 = 9370 Гц, столько раз в секунду таймер инкрементируется) 1/9370 = 0. А проснувшись, первым делом метнётся, как ночью в туалет, в процедуру обработки события "переполнение таймера". Но проснется ЦПУ только когда 8-ми битный таймер переполнится. Таких пробуждений у него будет 37 раз за секунду (256 х 0. А после побежит по программе дальше, начиная с места, где засыпал, пока не дойдет до места в бесконечном цикле с командой "спать". 027315; 0. 0001067 = 0. 01065 ~= 1s). 027315 х 37 = 1.

Изменение скелета программы: устанавливаем тактирование и делитель таймера-счетчика (HW блока, инкрементирующего своё значение каждые 1024 тактов), при переполнении 8бит этого счетчика — запускается обработчик прерывания переполнения, затем управление передается на следующую строчку программы после той, в которой он уснул.

Важно: переменная globalTimer определена как volatile, это означает, что изменяется она в совершенно непредсказуемое и неизвестное время, как следствие, перед любым её использованием CPU обязан сначала прочитать её актуальное значение из памяти, даже если действия именно с ней были в предыдущей строчке программы.

#include <avr/io.h> // Для компиляции не из IDE - определения регистров
#include <avr/sleep.h> // Да, будем спать
#include <avr/interrupt.h> // будет использоваться прерывание
volatile uint16_t globalTimer; //трачу два байта оперативки из 64 на глобальный таймер
// часики - переполнение таймера инкрементирует globalTimer каждые 1/37 секунды
ISR(TIM0_OVF_vect){ globalTimer++; // а больше ничего тут делать не надо. Проснется, сплюсует, пробежит while(1) цикл и уснет.
}
int main() { // Планировщик работает по схеме "а потом спи-отдыхай". set_sleep_mode(SLEEP_MODE_IDLE); //установить режим сна - см.даташит sleep_enable(); // разрешаем уход в сон TCCR0B = _BV(CS02) | _BV(CS00); // Тактирование таймера0 - clock frequency / 1024 TIMSK0 |= _BV(TOIE0); // При переполнении будет вызвано прерывание overflow interrupt sei(); // Глобально разрешаем обработку прерываний while(1){ //......... действия основного цикла sleep_cpu(); //и в самом конце цикла - уходим в сон. }
}
//Program size: 128 bytes (used 13% of a 1 024 byte maximum) (0,86 secs)
//Minimum Memory Usage: 2 bytes (3% of a 64 byte maximum)

"У кошки 4 ноги: вход, выход, земля и питание"(с) жалостливая студенческая песня

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

Уровень сигнала и тип ножки в Arduino задаются очень просто: сначала для ножки платы Arduino номер pin_number включаем режим работы на вход (или выход):

pinMode(pin_number, OUTPUT); // Был бы INPUT - был бы вход

А потом или выставляем высокое/низкое значение на ножке для выхода, или считываем — какое напряжение приходит на ногу для входа.

Value = digitalRead(pin_input_number); //Value - присваивается HIGH или LOW (1 или 0)
digitalWrite(pin_outpit_number, Value); // На ножке pin_outpit_number - HIGH или LOW

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

номер пина Arduino 7 равнозначен номеру ножки 11 или PD7 — седьмому биту порта D.
Все конструкции digitalRead, digitalWrite, pinMode и т.п., при компиляции всё равно приходят к установке режимов работы и значений в нумерации, близкой для процессора, увы, добавляя при каждом обращении к Wiring-функциям просто устрашающую кучу вызовов дополнительных действий, утяжеляя код и на порядки замедляя работу. Т.о.

Лучше, чем DIHALT объяснить режимы работы и установки портов у меня вряд ли получится. Управление состояниями ножки — оно еще проще, чем учит нас ардуина.

У малоногой тиньки мало того, что только 6 ножек можно задействовать физически (рис. Изнутри процессора ножки ввода-вывода порта общего назначения (GPIO) видятся сгруппированными по логическим портам, у 8-ми битных процессоров — максимум 8 ножек на один порт. Управляется любой порт ввода-вывода общего назначения тремя регистрами: (на примере порта с именем "B") DDRB, PINB и PORTB. ATtiny13 pinout, белым по зеленому), с PB0 по PB6, так еще и если использовать PB6 — то исчезает возможность программирования MK без специального высоковольтного программатора. Обозначения PB0, PB1, PB2, PB3, PB4, PB5 — соответствие ножек битам байтов регистров порта B. Регистр управления периферией — ячейка памяти, которой можно присваивать значения, или читать их. При добавлении в начало программы #include <avr/io.h> (или даже сразу iotn13.h для тиньки), эти определения обретут номера битов (PB0 станет 0,..., PB5 — 5) в байтах значений регистров в IDE, и станет возможной компиляция без задействования фреймворка Arduino.

Как правило, значения по-умолчанию оставляют блок периферии в выключенном состоянии, но, например, нули в DDRx GPIO определяют, что ножки — входы. Важно: если программа не обращается к регистрам периферии, это то же самое, что запись в эти регистры нулевых значений. Им вполне хватит электромагнитного поля от электропроводки, чтобы 50 раз в секунду менять состояние между 0 и 1. А одновременно с этим ноль в PORTx — внутренняя подтяжка НЕ включена, потенциал "где-то меж нулём и единицей", ноги чутко ловят, в какую сторону им склонится — вверх или вниз, что записать в PINx. И на каждое переключение процессор будет затрачивать электричество, бессмысленно и не очевидно для программиста.

Пусть, например

в бинарной записи число хххх11х0, где х — что угодно, хоть 1 хоть 0 — в условиях не оговорено. на ножку PB2 хочу вывести HIGH, +5В, на ножку PB3 сигнал LOW, 0В, а с ножки PB0 наоборот, прочитать — какой логический уровень напряжения на ней.
DDRB присваиваю значение числа, у которого бит PB0, номер ноль (самый левый) = 0 (PB0 определится как вход), второй и третий биты = 1 (PB2, PB3 — выходы), т.е.

DDRB = 12; // двоичное 000001100 в десятичном виде - 12
//режимы работы сразу всех пинов порта устанавливаются одномоментно (!)
// не надо для каждой ножки отдельно вызывать pinMode().

Чтобы установить на выводах заданные значения — присвоить PORTB число, у которого второй бит=1 (PB2 в HIGH), а третий — нулю (PB3 в LOW), остальные — не важно, хххх01хх

PORTB = 8; //(dec)8 === (bin)000001000
//Опять же - все значения всех ножек разом одной командой установлены.

_BV(PB3) — устанавливает третий бит в 1.
Запись _BV(PB3) | _BV(PB2) при компиляции превратится в число 00001100 (| — логическое побитовое ИЛИ).
Важно: макрос разворачивается в число уже при компиляции, в прошивке не будет никаких битовых действий, а будет использоваться получившаяся константа. Для чтения уровня с ножки у порта есть третий и последний регистр управления PINB, в нем число, значения бит которого — это логические уровни напряжения соответствующих ножек.
Все бы хорошо, но перевод из бинарных чисел в десятичные и обратно — некоторая морока.
Как установить бит номер 3 (PB3) в единицу: просто поставить 1 на нулевое место, и сдвинуть ее 3 раза влево.
3<<00000001 === PB3<<1 // результат — 00001000
Для совсем лёгкой жизни существует макрос, _BV(x), который при компиляции заменяется на (x<<1), т.о.

PORTB |= _BV(PB3) | _BV(PB2); // 2 и 3 биты порта == 1, уровни напряжения на ножках PB2 и PB3 станут HIGH, остальные биты PORTB не изменяются

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

PORTB &= ~(_BV(PB3) | _BV(PB2)); // В регистре PORTB 2й и 3й биты - стали нули, LOW, остальные не изменились.

Он сложнее GPIO, управляется уже не тремя, а семью регистрами, подробно их значения описаны в datasheet ATtiny13, если кому-то хочется прочитать еще и по-русски, можно посмотреть назначения битов в блоге Ilya Ananev в очень похожем по функционалу таймере-счетчике 0 в старшей ATmega328. В листинге выше инициализируется иной HW блок, таймер-счетчик 0, Timer0.

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

TCCR0B = _BV(CS02) | _BV(CS00); // Биты с названиями CS02 и CS00 в 1 - установить тактирование таймера0 = clock frequency / 1024
TIMSK0 |= _BV(TOIE0); // Бит номер TOIE0 в 1 - при переполнении вызвать прерывание TIM0_OVF
// в этом контексте равнозначно и TIMSK0 = _BV(TOIE0);

ГОСТ или закон есть закон.

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

Актуальные порядок и длительность чередования сигналов светофора находятся в документе "Распоряжение ФДА от 27. Светофор от цветомузыки отличается те, что режимы его работы диктуются государственным стандартом. 13" согласно 7 раздела ГОСТа Р 52289-2004 в части режимов работы светофоров, и полностью соответствует международной Конвенции о дорожных знаках и сигналах.
Сигналы чередуются в такой последовательности (одна из разрешенных): красный, красный с желтым, зеленый, зеленый мигающий, желтый, красный. 02.

"*При этом длительность сигнала "красный с желтым" рекомендуется устраивать не более 2 с, длительность желтого сигнала — 3 с.

В режимах работы светофорной сигнализации с использованием светофоров рекомендуется предусматривать мигание зеленого сигнала в течение 3 с непосредственно перед его выключением с частотой 1 миг/с.*"

Опираясь на данный документ, добавлю определения длительности периодов последовательности (как и в большом светофоре, перпендикулярные направления имеют различные длительности разрешающего/запрещающего сигнала — PERIOD_0 и PERIOD_4, длительность остальных состояний — по ГОСТу).

#define ONE_SECOND 37 // количество переполнений счетчика в 1 секунду
#define QT_SECOND 9 // четверть секунды #define PERIOD_FLASH_GREEN QT_SECOND //период мигания зеленым цветом (четверть сек) - перед переключением в желтый
#define PERIOD_FLASH_YELLOW ONE_SECOND * 1 //период мигания желтым цветом - регулировка светофором отключена - секунды // север --- восток
#define PERIOD_0 ONE_SECOND * 10 //R G R G 0. красный --- зеленый (10 сек)
#define PERIOD_1 ONE_SECOND * 3 //R g R g 1. красный --- зеленый мигающий (3 сек) #define PERIOD_2 ONE_SECOND * 1 //R Y R Y 2. красный --- желтый (1 сек)
#define PERIOD_3 ONE_SECOND * 2 //RY Y RY Y 3. красн+желтый --- желтый (2 сек) #define PERIOD_4 ONE_SECOND * 7 //G R G R 4. зеленый --- красный (7 сек)
#define PERIOD_5 ONE_SECOND * 3 //g R g R 5. зеленый мигающий --- красный(3 сек)
#define PERIOD_6 ONE_SECOND * 1 //Y R Y R 6. желтый --- красный (1 сек)
#define PERIOD_7 ONE_SECOND * 2 //Y RY Y RY 7. желтый --- красный+желтый (2 сек)

И опишу структуру, которая будет хранить данные сигналов светофора, их длительности и мигании.

typedef struct{ const uint8_t ddr_val_0; // DDRB value - в горящем состоянии const uint8_t port_val_0; // PORTB value - в горящем состоянии const uint8_t ddr_val_1; // DDRB value - в состоянии "мигания", если такое есть const uint8_t port_val_1; // PORTB value - аналогично const uint16_t flash_period; // period of flashing - переключение между _val_1 и _val_0 const uint16_t signal_period; // period of this lighting state
}lightSignalization; // состояние огней светофора, _0 и _1 - состояния при мигании, flash_long - время переключения мигания
//Если flash_period == 0, значит мигания нет, использовать только _val_0.
//Если signal_period == 0, значит переключения на следующее значение не лимитировано по времени.

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

Как найти в ATtiny13 восемь (или девять?) пинов ввода-вывода

Когда-то хабровчанин denvo в статье "Мало выводов? По данной матрице периодов, и учитывая, что направления север-юг и запад-восток у светофора по управлению равнозначны, естественный вывод: для управления огнями необходимо и достаточно 6 сигналов управления. Используем RESET" при решении сходной задачи светофора ограничился 5-ю выводами для управления сигнализацией светофора, сделав управление желтым цветом общим, но в случае моей матрицы видно, что желтыми сигналами необходимо управлять раздельно, так что все-таки ножек нужно шесть.

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

Хорошо бы еще кнопку выключения — чтобы не тратилась, значится, зазря энергия, итого уже 8, нес па?

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

Человек, товарищи, есть хомо сапиенс, который может и хочет. Ведь что есть человек, философски говоря? Может, эта, всё, что хочет, а хочет всё, что может.

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

Надо решить задачу — как добавить недостающие ноги.

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

Схемотехнически можно управлять двумя зелеными ветками одним пином МК:

PORTB &= ~(_BV(GREEN_PIN)) — правую (восток и запад).
А что делать, когда необходимо выключить обе ветки, когда ни один зеленый не горит?
А просто ранее определенный выход (DDRB |= _BV(GREEN_PIN)) переопределить как вход, DDRB &= ~(_BV(GREEN_PIN)), GREEN_PIN в состоянии Hi-Z не привносит никакого потенциала в цепочку. Подача 1 на управляющий выход PORTB |= _BV(GREEN_PIN) — включает левую ветку (север и юг) светодиодов, а подача 0 т.е. 2В * 4) будет превышать имеющиеся 5В, и напряжения для свечения не хватит. Последовательное падение на четырех светодиодах (2.

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

Конечно, вру, не так уж обязательно и должен

Если чуть смухлевать, организовав импульсное управление — включая и выключая сигналы поочередно, для глаза 37/2=18 Гц не будут выглядеть постоянным свечением, но можно просыпаться в цикле не с такими длинными паузами, и переключать гораздо чаще. Можно, вполне можно реализовать управление и желтым ярусом также одним проводом по той же схеме подключения. Но:
а) идея не моя, самому в голову не пришло, не вспомнилось, подсказал человек, гораздо более опытный в схемотехнике;
б) усложнение кода — не просто из справочной таблицы назначать значения регистров, но еще и реализовать режим "быстрого переключения" или запуска аппаратного ШИМ для ситуации 4х желтых лампочек;
в) изменение яркости, когда после пары желтых начинают светить 4 (переходы PERIOD_2->PERIOD_3 и PERIOD_6->PERIOD_7);
г) использование 4 ног на управление всеми световыми сигналами и так освобождает достаточно ресурсов для реализации остальных хотелок человека желающего.

Окончательная принципиальная схема.

// PINB === 0 0 0 g r y0 btt y1
#define RED_PIN PB3 // OUT: 1 - "север-юг" красный, 0 - "запад-восток" , IN - ни один, выключен, подтяжку не(!) включать
#define YELLOW0_PIN PB2 // OUT: 1 - желтый "север-юг" #define YELLOW1_PIN PB0 // OUT: 1 - желтый "запад-восток" #define GREEN_PIN PB4 // OUT: 1 - "север-юг" зеленый, 0 - "запад-восток" , IN - ни один, выключен, подтяжку не(!) включать #define RED _BV(RED_PIN) // _BV - сдвиг влево единицы на количество(), 1<<VALUE
#define YELL0 _BV(YELLOW0_PIN) // включение желтого на север-юг
#define YELL1 _BV(YELLOW1_PIN)
#define GREEN _BV(GREEN_PIN) // включение зеленого на север-юг (а если 0 - то на восток-запад) при DDRB.GreenPin=1

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

#define BUTTON_PIN PB1
#define BUTTON_ON !(PINB & _BV(BUTTON_PIN)) //( (PINB & _BV(BUTTON_PIN)) == 0) // условие "кнопка нажата" - на кнопке LOW
#define BUTTON_OFF (PINB & _BV(BUTTON_PIN)) // условие "не нажата" - на пине кнопки HIGH, (пин на схеме притянут к питанию)

Для 4 пинов получим 12 управляемых светодиодов, то есть любым светодиодом светофора возможно будет управлять независимо. Читатели Гарри Поттера на этом месте могут возразить, что заклинание Чарлиплексинг, позволяет управляя n ножками зажигать (и тушить) n(n−1) = n²−n светодиодов. Индикация сигналов «да нет, наверное» или «ой всё, делай что хочешь» на произвольной стороне светофора превращается в тривиальную задачу.

Схемотехника варианта чарлиплексинга для 12 LED

постоянство свечения в чарлиплексинге это физиологическая иллюзия. ***Расплата — усложнение программного кода, исчезает красота переключения режимов горения просто присвоением значений DDRx|PORTx, работа в импульсном режиме на высокой частоте, т.к. Регулировка падения напряжения на разных цепочках принесет немало сюрпризов — три цвета диодов, это три варианта рабочих токов и падений напряжения, обеспечивать сравнимую яркость придется подбором скважности для различных цветов.

И все эти излишние сложности не дают никаких ключевых преимуществ именно для светофора по сравнению с вариантом управления ярусами на средней точке.***

Пару слов о пятом измерении: как впихнуть невпихуемое?

Массив вышеописанных структур lightSignalization содержит данные по всем возможным состояниям режимов работы. Любое рабочее состояние светофора полностью определяется вышеописанной структурой типа lightSignalization. В дефайнах определены номера элементов, но обязательным должны быть только значения номеров с 0 по 7, их номероместо используется в логике программы. Элементы массива с 0го по 7й регулирующие сигналы, 8й — мигающий желтый, и 9й — режим, где все "лампочки" у светофора выключены. Но альтернативой работы с 3х битным циклическим счетчиком (от 0 до 7) для 8ми состояний стандартного к-ж-з режима являются дополнительные проверки на достижения максимальных и минимальных значений режима при увеличении. Да, я знаю, что тащить данные в код непристойно. Поэтому назову это красивой непристойностью, практически светофорной эротикой. Любые ветки if-ов и снижают быстродействие, и раздувают объём кода.

lightSignalization traffic_signals[] = {// Порядок чередования сигналов // {DDRB0, PORTB0, DDRB_when_flashingif, PORTB_when_flasingif (if flashing), continous of half-period flashing, continous curr mode runing} {RED|GREEN, RED, 0, 0, 0, PERIOD_0}, // R G R G {RED, RED, RED|GREEN, RED, QT_SECOND, PERIOD_1}, // R g R g - flash east green {RED|YELL1, RED|YELL1, 0, 0, 0, PERIOD_2 }, // R Y1 R Y1 {RED|YELL0|YELL1, RED|YELL0|YELL1, 0, 0, 0, PERIOD_3 }, // RY0 Y1 RY0 Y1 {RED|GREEN, GREEN, 0, 0, 0, PERIOD_4}, // G R G R {RED|GREEN, GREEN, RED, 0, QT_SECOND, PERIOD_5 }, // g R g R - flash nord green {RED|YELL0, YELL0, 0, 0, 0, PERIOD_6}, // Y0 R Y0 R {RED|YELL0|YELL1, YELL0|YELL1, 0, 0, 0, PERIOD_7 }, // Y0 RY1 Y0 RY1 {YELL0|YELL1, YELL0|YELL1, YELL0|YELL1, 0, ONE_SECOND, 0}, // y0 y1 y0 y1 - flash yellows lights {0, 0, 0, 0, 0, 0} // traffic lights off, DDR in, Hi-Z
}; // номера режимов работы в массиве lightSignalization traffic_signals[]
#define LIGHT_NUM_YELLOW_FLASH 8 // номер состояния порта при мигании желтым - включено - flash yellows lights
#define LIGHT_NUM_STD_START 0 // С какого номера начинается работа стандартного режима
#define LIGHT_NUM_LIGHTS_OFF 9 // номер состояния порта всё выключено - в спячке - traffic lights off

Т.о. Каждый экземпляр lightSignalization требует 8 байт оперативной памяти. Нельзя забывать, что используется оперативка и для организации стека программы. для глобальной переменной массива traffic_signals[] из 10 таких значений памяти необходимо 80 байт (ага, из 64 физически присутствующих, причем 2 у нас уже под таймер).

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

Всего-то необходимо изменить определение массива как

const lightSignalization traffic_signals[] PROGMEM= {...

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

<avr/pgmspace.h> pgm_read_byte_near() и pgm_read_word_near()

для char и shortint соответственно.

Светофор работает в одном из 3 состояний режимов сигнализации.

  1. бесконечность), продолжительность мигания lightSignalization.flash_period==0, т.е. Сигнализация выключена, все огни погашены.
    Номер в массиве traffic_signals[] — LIGHT_NUM_LIGHTS_OFF
    {0, 0, 0, 0, 0, 0} // traffic lights off
    Самый простой режим — в lightSignalization все нули, все пины управления светодиодами — входы, продолжительность сигнала lightSignalization.signal_period==0 (т.е. та же бесконечность.

  2. Но есть время изменения состояния lightSignalization.flash_period=ONE_SECOND, когда переключаются между собой: Нерегулирующая сигнализация, мигающий желтый.
    Номер в массиве traffic_signals[] — LIGHT_NUM_YELLOW_FLASH
    {YELL0|YELL1, YELL0|YELL1, YELL0|YELL1, 0, ONE_SECOND, 0}, // y0 y1 y0 y1 — flash yellows lights
    Ограничения времени работы нет: lightSignalization.signal_period==0.

    DDRB = YELL0|YELL1; // YELL0, YELL1 - выходы
    PORTB = YELL0|YELL1; // YELL0, YELL1 = HIGH - высокий уровень

    и

    DDRB = YELL0|YELL1; // // YELL0, YELL1 - выходы по прежнему
    PORTB = 0; // YELL0, YELL1 = LOW - низкий уровень

  3. Регулирующий сигнал, красный-желтый-зеленый.

Самый красочный, классический работающий светофор.

В массиве traffic_signals[] самое первое состояние режима — LIGHT_NUM_STD_START
{RED|GREEN, RED, 0, 0, 0, PERIOD_0}, // R G R G
Режим не имеет мигания: lightSignalization.flash_period==0.
DDRB = RED|GREEN; //RED и GREEN — как выходы, остальные входы.
PORTB = RED; // на RED — HIGH (север-юг красный и схемотехнически красный восток выключен)
// а на GREEN — LOW (север зеленого выключен, а восток-запад — наоборот зеленые)
И имеет ограничение по времени работы lightSignalization.signal_period==PERIOD_0, после которого необходимо переключиться на следующий режим.

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

uint8_t current_signal; // 1 байт на текущее состояние, номер в traffic_signals
uint16_t tl_flash_end; // 2 байта на время окончания периода мигания (если !0), uint16_t tl_signal_end; // 2 байта на время окончания работы текущего сигнала и переключения на следующий (если !0)

И затем сбрасываются в 0 все разряды current_signal, большие 3, т.е
current_signal = current_signal & B00000111;
или иными словами Если tl_signal_end != 0, то условию globalTimer>tl_signal_end инкрементируется current_signal.

current_signal &= LIGHT_NUM_STD_MASK ; //current_signal & B00000111;

Что превращает current_signal в счетчик, который по кругу будет считать от 0 до 7.
В логике используется globalTimer — добавляю процедуру избежания возможности переполнения счетчиков времени.

Исходный код изрядно подрос

#include <limits.h> // USHRT_MAX
#include <avr/sleep.h> // Да, будем спать
#include <avr/interrupt.h> // будет использоваться прерывание
#include <avr/pgmspace.h> // Программная память для констант #define ONE_SECOND 37 // количество переполнений счетчика в 1 секунду
#define QT_SECOND 9 // четверть секунды
#define MAX_GLOBAL_TIMER_VALUE (USHRT_MAX / 2) // uint16_t globalTimer - защита от переполнения. 65535 /2 // любой период должен быть меньше, чем MAX_GLOBAL_TIMER_VALUE - 1
#define PERIOD_FLASH_GREEN QT_SECOND //период мигания зеленым цветом (четверть сек) - перед переключением в желтый
#define PERIOD_FLASH_YELLOW ONE_SECOND * 1 //период мигания желтым цветом - регулировка светофором отключена - секунды // север --- восток
#define PERIOD_0 ONE_SECOND * 10 //R G R G 0. красный --- зеленый (15 сек)
#define PERIOD_1 ONE_SECOND * 3 //R g R g 1. красный --- зеленый мигающий (3 сек) #define PERIOD_2 ONE_SECOND * 1 //R Y R Y 2. красный --- желтый (1 сек)
#define PERIOD_3 ONE_SECOND * 2 //RY Y RY Y 3. красн+желтый --- желтый (2 сек) #define PERIOD_4 ONE_SECOND * 7 //G R G R 4. зеленый --- красный (10 сек)
#define PERIOD_5 ONE_SECOND * 3 //g R g R 5. зеленый мигающий --- красный(3 сек)
#define PERIOD_6 ONE_SECOND * 1 //Y R Y R 6. желтый --- красный (1 сек)
#define PERIOD_7 ONE_SECOND * 2 //Y RY Y RY 7. желтый --- красный+желтый (2 сек) typedef struct{ const uint8_t ddr_val_0; // DDRB value при первом полутакте мигания const uint8_t port_val_0; // PORTB value const uint8_t ddr_val_1; // DDRB value при втором полутакте мигания const uint8_t port_val_1; // PORTB value const uint16_t flash_period; // period of flashing - переключение между _val_1 и _val_0 const uint16_t signal_period; // period of this lighting state
}lightSignalization; // состояние огней светофора, _0 и _1 - состояния при мигании, flash_long - время переключения мигания // По принципиальной схеме (PINS === 0 0 0 g r y0 btt y1): // далее по этим определениям "собираются" байты состояний порта при компиляции
#define BUTTON PB1
#define RED_PIN PB3 // OUT: 1 - "север-юг" красный, 0 - "запад-восток" , IN - ни один, выключен, подтяжку не(!) включать
#define YELLOW0_PIN PB2 // OUT: 1 - желтый "север-юг" #define YELLOW1_PIN PB0 // OUT: 1 - желтый "запад-восток" #define GREEN_PIN PB4 // OUT: 1 - "север-юг" зеленый, 0 - "запад-восток" , IN - ни один, выключен, подтяжку не(!) включать #define BUTTON_ON !(PINB & _BV(BUTTON)) //( (PINB & _BV(BUTTON)) == 0) // условие "кнопка нажата"
#define BUTTON_OFF (PINB & _BV(BUTTON)) // ~(PINB & _BV(BUTTON)) -\\- "не нажата" #define RED _BV(RED_PIN) // _BV - сдвиг влево единицы на количество(), 1<<VALUE
#define YELL0 _BV(YELLOW0_PIN) //
#define YELL1 _BV(YELLOW1_PIN)
#define GREEN _BV(GREEN_PIN) // номера режимов работы в массиве lightSignalization traffic_signals[]
#define LIGHT_NUM_YELLOW_FLASH 8 // номер состояния порта при мигании желтым - включено - flash yellows lights
#define LIGHT_NUM_STD_START 0 // С какого номера начинается работа стандартного режима
#define LIGHT_NUM_LIGHTS_OFF 9 // номер состояния порта всё выключено - в спячке - traffic lights off
#define LIGHT_NUM_STD_MASK 7 // текущий_номер_состояния_светофора++ &LIGHT_NUM_STD_MASK - обеспечивает счетчик от 0 до 7 по кругу //.................................... ГЛОБАЛЬНЫЕ переменные
// const lightSignalization traffic_signals[] PROGMEM= { // Порядок чередования сигналов, значения константные, хранятся во флеш-памяти, PINS === 0 0 0 g r y0 btt y1 // {DDRB0, PORTB0, DDRB_flashing, PORTB_flasinf (if flashing), continuous of half-period flashing, continuous id mode running} {RED|GREEN, RED, 0, 0, 0, PERIOD_0}, // R G R G {RED, RED, RED|GREEN, RED, QT_SECOND, PERIOD_1}, // R g R g - flash east green {RED|YELL1, RED|YELL1, 0, 0, 0, PERIOD_2 }, // R Y1 R Y1 {RED|YELL0|YELL1, RED|YELL0|YELL1, 0, 0, 0, PERIOD_3 }, // RY0 Y1 RY0 Y1 {RED|GREEN, GREEN, 0, 0, 0, PERIOD_4}, // G R G R {RED|GREEN, GREEN, RED, 0, QT_SECOND, PERIOD_5 }, // g R g R - flash nord green {RED|YELL0, YELL0, 0, 0, 0, PERIOD_6}, // Y0 R Y0 R {RED|YELL0|YELL1, YELL0|YELL1, 0, 0, 0, PERIOD_7 }, // Y0 RY1 Y0 RY1 {YELL0|YELL1, YELL0|YELL1, YELL0|YELL1, 0, ONE_SECOND, 0}, // y0 y1 y0 y1 - flash yellows lights {0, 0, 0, 0, 0, 0} // traffic lights off, }; volatile uint16_t globalTimer; //трачу два байта оперативки из 64 на глобальный таймер
uint8_t current_signal; // 1 байт на текущее состояние, номер в traffic_signals
uint16_t tl_flash_end; // 2 байта на время окончания периода мигания (если !0), uint16_t tl_signal_end; // 2 байта на время окончания работы текущего сигнала и переключения на следующий (если !0) //.................................... Прототипы функций
void setPeriods(uint8_t num, bool set_both_flash_and_signal); // установка tl_flash_end, tl_signal_end
void setPorts(uint8_t num, bool use_main_values); // установка режима работы портов //.................................... Вектора прерываний
//
// часики - переполнение таймера инкрементирует globalTimer каждые 1/37 секунды
ISR(TIM0_OVF_vect){ globalTimer++; // а больше ничего тут делать не надо. Проснется, потом пробежит while(1) цикл и уснет.
} int main() {
//
bool use_main_values = true; // lightSignalization.ХХХ_val_0 (1) или lightSignalization.ХХХ_val_1 (0)? - если мигание
current_signal = LIGHT_NUM_STD_START; // Установка режима работы. Кнопкой пока что не переключается. // Планировщик работает по схеме "а потом спи-отдыхай". set_sleep_mode(SLEEP_MODE_IDLE); //установить режим сна - см.даташит sleep_enable(); // разрешаем уход в сон TCCR0B = _BV(CS02) | _BV(CS00); // Тактирование таймера0 - clock frequency / 1024 TIMSK0 |= _BV(TOIE0); // При переполнении будет вызвано прерывание overflow interrupt sei(); // Глобально разрешаем обработку прерываний while(1){ // переполнение глобального таймера? if(globalTimer > MAX_GLOBAL_TIMER_VALUE){ globalTimer -= MAX_GLOBAL_TIMER_VALUE; // откатить глобальный таймер // if(tl_flash_end){ tl_flash_end -= MAX_GLOBAL_TIMER_VALUE; // откатить период мигания, если есть } if(tl_signal_end){ tl_signal_end -= MAX_GLOBAL_TIMER_VALUE; // // откатить период состояния, если есть } // setPeriods(currentMode, false); // код на 12 байт меньше, но tl_.._end сбросятся в исходное, будет единичным увеличенным интервалом переключения } //если в режиме работы есть мигание (tl_flash_end !=0 ) if(tl_flash_end){ // и время смены мигания пришло if(globalTimer > tl_flash_end){ use_main_values = !use_main_values; // !use_main_values - или или одно из двух )) setPorts(current_signal, use_main_values); // переключить режим текущего состояния на противоположный setPeriods(current_signal, false); // обновить только следующий период мигания, но не состояния } } // если в режиме работы есть ограничение времени состояния - собственно это только operating_std отображение последовательности по ГОСТУ - красный-желтый-зеленый if(tl_signal_end){ // и уже пора переключиться на следующий (use_main_values - чтобы переключение было с горящего зеленого - на желтый) if((globalTimer > tl_signal_end) && use_main_values){ current_signal ++; // следующий сигнал светофора current_signal &= LIGHT_NUM_STD_MASK; // обнулить биты выше 3-го, в основном режиме рабочие номера состояний с 0 по 7 use_main_values = true; // начинается - с нулевой пары состояний setPorts(current_signal, use_main_values); // переключить режим текущего состояния на следующий в массиве setPeriods(current_signal, true); // обновить следующий период мигания и период состояния } } sleep_cpu(); //и в самом конце бесконечного цикла - уходим в сон. }
} // установка значений портов void setPorts(uint8_t num, bool use_main_values){ uint8_t val; DDRB = 0; PORTB = 0; // Если основной режим (мигания) - ddr_val_0, else = ddr_val_1
// val = (use_main_values) ? pgm_read_byte_near(&(traffic_signals[num].ddr_val_0))
// : pgm_read_byte_near(&(traffic_signals[num].ddr_val_1)); // то же самое, но непонятно, некрасиво, арифметика указателей, данные в коде, зато на 14 (!!!) байт код короче. val = pgm_read_byte_near(&(traffic_signals[num].ddr_val_0)+( (use_main_values) ? 0 : 2) ); val &= ~_BV(BUTTON); // сброс бита пина кнопки - ВХОД DDRB = val; // установка режима пинов порта val = (use_main_values) ? pgm_read_byte_near(&(traffic_signals[num].port_val_0)) : pgm_read_byte_near(&(traffic_signals[num].port_val_1)); val|= _BV(BUTTON); // подтяжка на пине кнопки - активируется - срабатывание на низкий уровень PORTB = val; // установка значений выходов и входов порта
} //Установить время окончания режима мигания (или void setPeriods(uint8_t num, bool set_both_flash_and_signal){
// глобальные переменные tl_flash_end = pgm_read_word_near (&(traffic_signals[num].flash_period)); // период мигания tl_flash_end = (tl_flash_end)? tl_flash_end + globalTimer : 0; //время окончания режима мигания - если не нулевое значение периода //if(tl_flash_end){ tl_flash_end += globalTimer; } <- вот так код на 8 байт длиннее // если установить оба периода - и состояния и мигания if(set_both_flash_and_signal){ tl_signal_end = pgm_read_word_near(&(traffic_signals[num].signal_period)); tl_signal_end = (tl_signal_end)? tl_signal_end + globalTimer : 0; // время переключения на следующий режим, если не нулевое значение }
} //Program size: 610 bytes (used 60% of a 1 024 byte maximum) (0,58 secs)
//Minimum Memory Usage: 7 bytes (11% of a 64 byte maximum)

Сложная логика простой кнопки

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

#define PERIOD_PRESS_BUTTON_SHORT QT_SECOND // Длительность короткого нажатия на кнопку - переключение состояний
#define PERIOD_PRESS_BUTTON_LONG QT_SECOND*6 // Длительность долгого нажатия на кнопку - включение/выключение
uint8_t scan_button_cnt; // счетчик длительности нажатия кнопки //проверка нажатия кнопки if(BUTTON_ON){ if(scan_button_cnt<USHRT_MAX){ scan_button_cnt++; // еще одна 1/37 секунды кнопка продолжала быть нажатой } if(scan_button_cnt > PERIOD_PRESS_BUTTON_SHORT){ // кстати, нажатие уже длиннее короткого нажатия } if(scan_button_cnt > PERIOD_PRESS_BUTTON_LONG){ // и даже длиннее нажатия длинного } }

Листинг логики обработки кнопки — переложение рисунка машины состояний на С.

Вместо используемого ранее режима сна SLEEP_MODE_IDLE (отключается только программная память и CPU), в состоянии PwrDown переключаюсь в режим SLEEP_MODE_PWR_DOWN — а уйдя в него микропроцессор сможет проснуться только от поцелуя принца от прерываний watchdog таймера или внешнего прерывания INT0, если оно разрешено, а оно будет разрешено — и добавлю обработку внешнего прерывания INT0.

В разделе 7. Энергопотребление МК в этом режиме падает до минимума. Power management and sleep modes спецификации МК ATtiny13 подробно описано, как можно выжать еще процентов 5-10 экономии, но это уже блохи на фоне потребления остальной части изделия.

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

// uint8_t f_button_state_flags; // псевдорегистр машины состояний кнопки и вместилище булевых переменных // состояния машины состояний отслеживания кнопки MODES: wakeup 11 -> work 00 -> tosleep 01 -> pwrdown 11 -> wakeup 11
#define MODE_LBIT 0
#define MODE_HBIT 1
#define FORCE_SET_NEW_SIGNAL_BIT 2 // в конце бесконечного цикла установить новый режим работы портов по значению в current_signal
// запасной неиспользуемый бит 3
#define LIGHT_SIGNAL_ALT_MODE_BIT 4 // стандартный=0 (красный-желтый-зеленый) или альтернативный=1 (желтый мигающий) режим работы светофора
#define USE_FIRST_VALUES_LIGHT_BIT 5 // использовать первое значение пар ддр-порт структуры lightSignalization или второе
#define SHORT_PRESS_FLAG_BIT 6 // Булево, 1 когда счетчик нажатия кнопки больше короткого нажатия
#define LONG_PRESS_FLAG_BIT 7 // Булево, 1 когда счетчик нажатия кнопки больше длинного нажатия

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

Окончательный исходный код прошивки

#include <limits.h> // USHRT_MAX
#include <avr/io.h> // Для компиляции не из IDE
#include <avr/sleep.h> // Да, будем еще и немножечко спать
#include <avr/interrupt.h> // будет использоваться прерывание
#include <avr/pgmspace.h> // Программная память для констант #ifdef GIMSK // Если ATtiny13 - у неё есть регистр с таким названием
#define F_CPU 9600000UL // если компилируется не из ардуино среды, нужна скорость АЛУ
#define ONE_SECOND 37 // количество переполнений счетчика в 1 секунду
#define QT_SECOND 9 // четверть секунды
// GIMSK &= ~_BV(INT0); - запрет прерывания INT0 #define DISABLE_EXTERNAL_INT0 GIMSK &= ~(_BV(INT0)); GIFR &= ~(_BV(INTF0)) //EIMSK/EIFR у атмеги
//GIMSK |= _BV(INT0) - включить прерывание INT0
#define ENABLE_EXTERNAL_INT0 GIMSK |= _BV(INT0) ; GIFR &= ~(_BV(INTF0))
#else // мега328 - моя ардуинка нано
#define F_CPU 16000000UL
#define ONE_SECOND 64 // количество переполнений счетчика в 1 секунду - см. инициализацию таймера ниже
#define QT_SECOND 16 // четверть секунды
// АМ328 INT0 - ножка PD2... тут танцах вокруг единственного порта, малой кровью глубокий сон не забабахать (точнее, энергосбережение допиливать), Уход в сон от кнопки и просыпание на атмеге не реализованы полностью
#define DISABLE_EXTERNAL_INT0 EIMSK &= ~(_BV(INT0)); EIFR &= ~(_BV(INTF0)) // Увы, без эмуляции - EICRA - ISC00-ISC01 == 00, lo level, EIMSK - INT0, EIFR-INTF0 #define ENABLE_EXTERNAL_INT0 EIMSK |= _BV(INT0) ; EIFR &= ~(_BV(INTF0))
#endif #define MAX_GLOBAL_TIMER_VALUE (USHRT_MAX / 2) // uint16_t globalTimer - защита от переполнения. 65535 /2 // любой период должен быть меньше, чем MAX_GLOBAL_TIMER_VALUE - 1
#define PERIOD_PRESS_BUTTON_SHORT QT_SECOND/2 // Длительность короткого нажатия на кнопку (меньше - дребезг) - переключение состояний
#define PERIOD_PRESS_BUTTON_LONG QT_SECOND*4 // Длительность долгого нажатия на кнопку - включение/выключение
#define PERIOD_FLASH_GREEN QT_SECOND // период мигания зеленым цветом (четверть сек) - перед переключением в желтый
#define PERIOD_FLASH_YELLOW ONE_SECOND * 1 // период мигания желтым цветом - регулировка светофором отключена - секунды // север --- восток
#define PERIOD_0 ONE_SECOND * 5 // R G R G 0. красный --- зеленый (5 сек)
#define PERIOD_1 ONE_SECOND * 3 //R g R g 1. красный --- зеленый мигающий (3 сек) #define PERIOD_2 ONE_SECOND * 1 //R Y R Y 2. красный --- желтый (1 сек)
#define PERIOD_3 ONE_SECOND * 2 //RY Y RY Y 3. красн+желтый --- желтый (2 сек) #define PERIOD_4 ONE_SECOND * 7 // G R G R 4. зеленый --- красный (7 сек)
#define PERIOD_5 ONE_SECOND * 3 //g R g R 5. зеленый мигающий --- красный(3 сек)
#define PERIOD_6 ONE_SECOND * 1 //Y R Y R 6. желтый --- красный (1 сек)
#define PERIOD_7 ONE_SECOND * 2 //Y RY Y RY 7. желтый --- красный+желтый (2 сек) //структура одного режима/состояния световой сигнализации. Может иметь второе значение - как быдет выглядеть при мигании
typedef struct{ const uint8_t ddr_val_0; // DDRB value при первом полутакте мигания const uint8_t port_val_0; // PORTB value const uint8_t ddr_val_1; // DDRB value при втором полутакте мигания const uint8_t port_val_1; // PORTB value const uint16_t flash_period; // period of flashing - переключение между _val_1 и _val_0 (если =0, нет мигания) const uint16_t signal_period; // period of this lighting state (если =0, то режим не будет переключен со временем)
}lightSignalization; // состояние огней светофора, _0 и _1 - состояния при мигании, flash_long - время переключения мигания // По принципиальной схеме (PINS === 0 0 0 g r y0 btt y1): // далее по этим определениям "собираются" байты состояний порта при компиляции
#define BUTTON_PIN PB1 // вывод, у которого INT0. Кнопка, подтянута к питанию, нажатие = LOW
#define RED_PIN PB3 // OUT: 1 - "север-юг" красный, 0 - "запад-восток" , IN - ни один, выключен, подтяжку не(!) включать
#define YELLOW0_PIN PB2 // OUT: 1 - желтый "север-юг" #define YELLOW1_PIN PB0 // OUT: 1 - желтый "запад-восток" #define GREEN_PIN PB4 // OUT: 1 - "север-юг" зеленый, 0 - "запад-восток" , IN - ни один, выключен, подтяжку не(!) включать #define BUTTON_ON !(PINB & _BV(BUTTON_PIN)) //( (PINB & _BV(BUTTON)) == 0) // условие "кнопка нажата"
#define BUTTON_OFF (PINB & _BV(BUTTON_PIN)) // ~(PINB & _BV(BUTTON)) -\\- "не нажата" #define RED _BV(RED_PIN) // _BV - сдвиг влево единицы на количество(), 1<<VALUE
#define YELL0 _BV(YELLOW0_PIN) // включение желтого на север-юг
#define YELL1 _BV(YELLOW1_PIN)
#define GREEN _BV(GREEN_PIN) // включение зеленого на север-юг (а если 0 - то на восток-запад) при DDR=1 // номера режимов работы в массиве lightSignalization traffic_signals[]
#define LIGHT_NUM_YELLOW_FLASH 8 // номер состояния порта при мигании желтым (нерегулирующем сигнале) - включено - flash yellows lights
#define LIGHT_NUM_STD_START 0 // С какого номера начинается работа стандартного режима (крас-жел-зел)
#define LIGHT_NUM_LIGHTS_OFF 9 // номер состояния порта всё выключено - в спячке - traffic lights off
#define LIGHT_NUM_START_SHOW 10 // номер состояния ВКЛЮЧЕНИЯ СВЕТОФОРА ПРИ СБРОСЕ ИЛИ ПОДАЧЕ ПИТАНИЯ
#define LIGHT_NUM_ERR 11 // отображение ошибки - частое мигание желтого и зеленого #define MASK_LIGHT_NUM_STD 7 // текущий_номер_состояния_светофора++ &= LIGHT_NUM_STD_MASK - обеспечивает счетчик от 0 до 7 по кругу //.................................... ГЛОБАЛЬНЫЕ переменные
// const lightSignalization traffic_signals[] PROGMEM= { // Порядок чередования сигналов, значения константные, хранятся во флеш-памяти, PINS === 0 0 0 g r y0 btt y1 // {DDRB0, PORTB0, DDRB_flashing, PORTB_flasinf (if flashing), continuous of half-period flashing, continuous id mode running} {RED|GREEN, RED, 0, 0, 0, PERIOD_0}, // R G R G {RED, RED, RED|GREEN, RED, QT_SECOND, PERIOD_1}, // R g R g - flash east green {RED|YELL1, RED|YELL1, 0, 0, 0, PERIOD_2 }, // R Y1 R Y1 {RED|YELL0|YELL1, RED|YELL0|YELL1, 0, 0, 0, PERIOD_3 }, // RY0 Y1 RY0 Y1 {RED|GREEN, GREEN, 0, 0, 0, PERIOD_4}, // G R G R {RED|GREEN, GREEN, RED, 0, QT_SECOND, PERIOD_5 }, // g R g R - flash nord green {RED|YELL0, YELL0, 0, 0, 0, PERIOD_6}, // Y0 R Y0 R {RED|YELL0|YELL1, YELL0|YELL1, 0, 0, 0, PERIOD_7 }, // Y0 RY1 Y0 RY1 {YELL0|YELL1, YELL0|YELL1, YELL0|YELL1, 0, ONE_SECOND, 0}, // y0 y1 y0 y1 - flash yellows lights {0, 0, 0, 0, 0, 0}, // traffic lights off, {RED|GREEN|YELL0, RED|YELL0, RED|GREEN|YELL1, GREEN|YELL1, 1, PERIOD_2}, // PERIOD_2 секунд - горят все красные и зеленые огни, ПРИ СБРОСЕ ИЛИ ПОДАЧЕ ПИТАНИЯ {YELL0|GREEN, YELL0, YELL1|GREEN, YELL1|GREEN, 1, 0} // ОШИБКА - часто мигают зеленые и желтые
}; volatile uint16_t globalTimer; // трачу два байта оперативки из 64 на глобальный таймер
uint8_t scan_button_cnt; // один байт счетчик длительности нажатия кнопки
uint16_t tl_flash_end; // 2 байта на время окончания периода мигания (если !0), uint16_t tl_signal_end; // 2 байта на время окончания работы текущего сигнала и переключения на следующий (если !0) uint8_t f_button_state_flags; // 1б, псевдорегистр машины состояний кнопки и вместилище булевых переменных
// итого, 8 байт на глобальные переменные #pragma region bits_of_f_button_state_flags // состояния машины состояний отслеживания кнопки MODES: wakeup 11 -> work 00 -> tosleep 01 -> pwrdown 11 -> wakeup 11
#define MODE_LBIT 0
#define MODE_HBIT 1
#define MODE_VALUE ( f_button_state_flags & 3 ) // результат - численное значение MODE_
#define SET_MODE_WORK f_button_state_flags &= ~(_BV(MODE_HBIT) ); f_button_state_flags &= ~(_BV(MODE_LBIT) );// 00 - work
// f_button_state_flags &= ~( _BV(MODE_HBIT) | _BV(MODE_LBIT) ) - по размеру столько же, что странно
#define MODE_WORK_VALUE 0
#define SET_MODE_TOSLEEP f_button_state_flags &= ~(_BV(MODE_HBIT)); f_button_state_flags |= _BV(MODE_LBIT) // 01 - tosleep
#define MODE_TOSLEEP_VALUE 1
#define SET_MODE_PWRDOWN f_button_state_flags |= _BV(MODE_HBIT); f_button_state_flags &= ~(_BV(MODE_LBIT)) // 10 - pwrdown #define MODE_PWRDOWN_VALUE 2
#define SET_MODE_WAKEUP f_button_state_flags |= _BV(MODE_HBIT); f_button_state_flags |= _BV(MODE_LBIT) // 11 - wakeup #define MODE_WAKEUP_VALUE 3 #define FORCE_SET_NEW_SIGNAL_BIT 2 // в конце бесконечного цикла установить новый режим работы портов по значению в current_signal
#define IF_FORCE_SET_SIGNAL_FLAG ( f_button_state_flags & _BV(FORCE_SET_NEW_SIGNAL_BIT) ) // IF_ - в условие проверки флага
#define SET_FORCE_SET_SIGNAL_FLAG f_button_state_flags |= _BV(FORCE_SET_NEW_SIGNAL_BIT) // SET_ - бит флага в 1
#define RES_FORCE_SET_SIGNAL_FLAG f_button_state_flags &= ~( _BV(FORCE_SET_NEW_SIGNAL_BIT) ) // RES_ - бит флага в 0 // Еще 3й бит в запасе #define LIGHT_SIGNAL_ALT_MODE_BIT 4 // стандартный=0 (красный-желтый-зеленый) или альтернативный=1 (желтый мигающий) режим работы светофора
#define IF_LIGHT_SIGNAL_ALT_MODE_FLAG ( f_button_state_flags & _BV(LIGHT_SIGNAL_ALT_MODE_BIT) ) //1(желтый мигающий) или 0(красный-желтый-зеленый) режим работы светофора?
#define SET_LIGHT_SIGNAL_ALT_MODE_FLAG f_button_state_flags |= _BV(LIGHT_SIGNAL_ALT_MODE_BIT)
#define RES_LIGHT_SIGNAL_ALT_MODE_FLAG f_button_state_flags &= ~( _BV(LIGHT_SIGNAL_ALT_MODE_BIT) )
#define FLIP_LIGHT_SIGNAL_ALT_MODE_FLAG f_button_state_flags ^= _BV(LIGHT_SIGNAL_ALT_MODE_BIT) #define USE_FIRST_VALUES_LIGHT_BIT 5 // использовать первое значение пар ддр-порт структуры lightSignalization или второе
#define IF_USE_FIRST_VALUES_LIGHT_FLAG (f_button_state_flags & _BV(USE_FIRST_VALUES_LIGHT_BIT)) // lightSignalization.ХХХ_val_0 (1) или lightSignalization.ХХХ_val_1 (0)? - если мигание
#define SET_USE_FIRST_VALUES_LIGHT_FLAG f_button_state_flags |= _BV(USE_FIRST_VALUES_LIGHT_BIT)
#define RES_USE_FIRST_VALUES_LIGHT_FLAG f_button_state_flags &= ~( _BV(USE_FIRST_VALUES_LIGHT_BIT))
#define FLIP_USE_FIRST_VALUES_LIGHT_FLAG f_button_state_flags ^= _BV(USE_FIRST_VALUES_LIGHT_BIT) // Инвертировать флаг #define SHORT_PRESS_FLAG_BIT 6 // Булево, 1 когда счетчик нажатия кнопки больше короткого нажатия
#define IF_SHORT_PRESS_FLAG ( f_button_state_flags & _BV(SHORT_PRESS_FLAG_BIT) ) //условие - если значение == 1
#define SET_SHORT_PRESS_FLAG f_button_state_flags |= _BV(SHORT_PRESS_FLAG_BIT)
#define RES_SHORT_PRESS_FLAG f_button_state_flags &= ~(_BV(SHORT_PRESS_FLAG_BIT)) #define LONG_PRESS_FLAG_BIT 7 // Булево, 1 когда счетчик нажатия кнопки больше длинного нажатия
#define IF_LONG_PRESS_FLAG ( f_button_state_flags & _BV(LONG_PRESS_FLAG_BIT) ) //условие - если значение == 1
#define SET_LONG_PRESS_FLAG f_button_state_flags |= _BV(LONG_PRESS_FLAG_BIT)
#define RES_LONG_PRESS_FLAG f_button_state_flags &= ~(_BV(LONG_PRESS_FLAG_BIT))
#pragma endregion //.................................... Прототипы функций
void setPeriods(uint8_t num, bool set_both_flash_and_signal); // установка tl_flash_end, tl_signal_end
void setPorts(uint8_t num, bool use_main_values); // установка режима работы портов
void inline init_timer_clock(){ // тактирование таймера глобальной переменной времени
#ifdef GIMSK // Если ATtiny13 - TCCR0B = _BV(CS02) | _BV(CS00); // Тактирование таймера0 - clock frequency / 1024 TIMSK0 |= _BV(TOIE0); // При переполнении будет вызвано прерывание overflow interrupt
#else // Если ардуино нано, атмега328/16м // 100 - prescaler 64; Foverflow = 16M/64*256 ~=976.56Hz, TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20) ; // 111 - CLK/1024, 16M/1024*254 - 1/64 секунды TIMSK2 |=(1<<TOIE0); // interrupt ovfl enable //Serial.begin(115200);
#endif
} //.................................... Обработчики прерываний
//
// Прерывание - часики - переполнение таймера инкрементирует globalTimer каждые 1/37 секунды
#ifdef GIMSK // Если ATtiny13
ISR(TIM0_OVF_vect){ globalTimer++; // а больше ничего тут делать не надо. Проснется, потом пробежит while(1) цикл и уснет.
}
#else
// в ардуино таймер0 - непрозрачно задействован под собственные нужды, - или править ардуино-файлы - или просто уйти на Т2
ISR(TIMER2_OVF_vect){ globalTimer++;
}
#endif
// Внешнее прерывание - нажатие на кнопку (точнее, изменение состояния, отслеживать же надо 0)
ISR(INT0_vect){ DISABLE_EXTERNAL_INT0; SET_MODE_WAKEUP; // прерывание разрешено только во сне POWER DOWN globalTimer = 0; // проснувшись - сброс таймера scan_button_cnt = 0; // сброс нажатий кнопки RES_SHORT_PRESS_FLAG; // RES_LONG_PRESS_FLAG; // и флагов
} /*
// для отладки - поглядеть
void inline dbg(){ DDRB |= YELL0; PORTB ^= YELL0;
}
*/ //.................................... основная программа
//
int main() { uint8_t current_signal; // 1 байт на текущее состояние, номер в traffic_signals #pragma region Initialisation&setup // Планировщик работает по схеме "а потом спи-отдыхай". set_sleep_mode(SLEEP_MODE_IDLE); //установить режим сна - см.даташит sleep_enable(); // разрешаем уход в сон init_timer_clock(); // инициализация и запуск глобального таймера globalTimer = 0; // проснувшись - сброс таймера SET_MODE_WORK; // обработчик состояния нажатия кнопки SET_USE_FIRST_VALUES_LIGHT_FLAG; //первая пара значений scan_button_cnt = 0; // обнулить счетчик кнопки // Начало работы или сброс отобразить частым миганием current_signal = LIGHT_NUM_START_SHOW; // Установка режима работы сигнализации - индикация подачи питания/перезагрузки SET_FORCE_SET_SIGNAL_FLAG; // включить лампы согласно current_signal sei(); // Глобально разрешить обработку прерываний #pragma endregion while(1){ #pragma region TimerOVF // переполнение глобального таймера? if(globalTimer > MAX_GLOBAL_TIMER_VALUE){ globalTimer -= MAX_GLOBAL_TIMER_VALUE; // откатить глобальный таймер // if(tl_flash_end){ tl_flash_end -= MAX_GLOBAL_TIMER_VALUE; // откатить период мигания, если есть } if(tl_signal_end){ tl_signal_end -= MAX_GLOBAL_TIMER_VALUE; // // откатить период состояния, если есть } // setPeriods(currentMode, false); // код на 12 байт меньше, но tl_.._end сбросятся в исходное, будет единичным увеличенным интервалом переключения }
#pragma endregion #pragma region ButtonState //проверка нажатия кнопки if(BUTTON_ON){ if(scan_button_cnt < USHRT_MAX){ scan_button_cnt++; // еще одна 1/37 секунды кнопка продолжала быть нажатой } // СБРОС флагов - дело тех, кто их ниже обработает if(scan_button_cnt > PERIOD_PRESS_BUTTON_SHORT){ SET_SHORT_PRESS_FLAG; // кстати, нажатие уже длиннее короткого нажатия, запомним } if(scan_button_cnt > PERIOD_PRESS_BUTTON_LONG){ SET_LONG_PRESS_FLAG; // и даже длиннее нажатия длинного } }
#pragma endregion
#pragma region LightWorkLogic //если в режиме работы есть мигание (tl_flash_end !=0 ) if(tl_flash_end){ // и время смены мигания пришло if(globalTimer > tl_flash_end){ FLIP_USE_FIRST_VALUES_LIGHT_FLAG; // !use_main_values - или или одно из двух )) setPorts(current_signal, IF_USE_FIRST_VALUES_LIGHT_FLAG); // переключить режим текущего состояния на противоположный setPeriods(current_signal, false); // обновить только следующий период мигания, но не состояния } } // если в режиме работы есть ограничение времени состояния // - собственно это только operating_std отображение последовательности по ГОСТУ - красный-желтый-зеленый // ну и индикация старта, который после +1 станет меньше 7 if(tl_signal_end){ // и уже пора переключиться на следующий (use_main_values - чтобы переключение было с горящего зеленого - на желтый) if((globalTimer > tl_signal_end) && IF_USE_FIRST_VALUES_LIGHT_FLAG){ current_signal ++; // следующий сигнал светофора current_signal &= MASK_LIGHT_NUM_STD; // обнулить биты выше 3-го, в основном режиме рабочие номера состояний с 0 по 7 SET_FORCE_SET_SIGNAL_FLAG; // включить лампы согласно current_signal } }
#pragma endregion #pragma region MODE_VALUELogic // Машина состояний кнопки, 2 бита f_button_state_flags //? MODE_VALUE === pwrdown -> wakeup -> work -> tosleep -> pwrdown switch (MODE_VALUE){ case (MODE_WAKEUP_VALUE): set_sleep_mode(SLEEP_MODE_IDLE); // не спать! // лампы не включать, пока кнопка не нажата достаточно долго - IF_BUTTON_LONG_FLAG if(IF_LONG_PRESS_FLAG){ // а не включена ли уже сигнализация? тогда включать свет! if(current_signal == LIGHT_NUM_LIGHTS_OFF){ // последний раз перед засыпанием светофор был в режиме желтого мигающего? LIGHT_NUM_ERR current_signal = (IF_LIGHT_SIGNAL_ALT_MODE_FLAG) ? LIGHT_NUM_YELLOW_FLASH : LIGHT_NUM_STD_START; SET_FORCE_SET_SIGNAL_FLAG; // включить лампы согласно current_signal } } //о, кнопку отжали... if(BUTTON_OFF){ // а перед этим жали так долго, что светофор включился if(IF_LONG_PRESS_FLAG){ SET_MODE_WORK; }else{ // А не, фальстарт, для включения недожали, спать дальше SET_MODE_PWRDOWN; // на следующем цикле подготовит режим сна и заснет } scan_button_cnt = 0; // в любом случае сбросить счетчик длительности нажатий RES_SHORT_PRESS_FLAG; // и флаги нажатия, конечно RES_LONG_PRESS_FLAG; } break; case (MODE_WORK_VALUE): // кнопка нажималась? if(scan_button_cnt > 0){ // Нажатие оооочень длинное? if(IF_LONG_PRESS_FLAG){ current_signal = LIGHT_NUM_LIGHTS_OFF; // гаси свет SET_FORCE_SET_SIGNAL_FLAG; // бросай гранату - установить порты "свет выключен" SET_MODE_TOSLEEP; // команда всем спать! } //Кнопку отпустили? if(BUTTON_OFF){ // нажата была дольше короткого трешхолда? if(IF_SHORT_PRESS_FLAG){ FLIP_LIGHT_SIGNAL_ALT_MODE_FLAG; // инвертировать флаг режима работы сигнализации current_signal = (IF_LIGHT_SIGNAL_ALT_MODE_FLAG) ? LIGHT_NUM_YELLOW_FLASH : LIGHT_NUM_STD_START; // на начальный номер выбр. режима SET_FORCE_SET_SIGNAL_FLAG; // флаг установки режима лампы согласно current_signal } scan_button_cnt=0; RES_SHORT_PRESS_FLAG; //сброс флагов нажатия и счетчика RES_LONG_PRESS_FLAG; } } break; case (MODE_TOSLEEP_VALUE): // Выход из состояния - только по отжатой кнопке. if(BUTTON_OFF){ SET_MODE_PWRDOWN; // На следующем цикле уснет } break; case (MODE_PWRDOWN_VALUE): // О! доброе утро, проснулись! Нажата кнопка? if(BUTTON_ON){ set_sleep_mode(SLEEP_MODE_IDLE); SET_MODE_WAKEUP; }else{ // Не нажата? Спать дальше. scan_button_cnt = 0; RES_LONG_PRESS_FLAG; RES_SHORT_PRESS_FLAG; current_signal = LIGHT_NUM_LIGHTS_OFF; //гаси свет SET_FORCE_SET_SIGNAL_FLAG; set_sleep_mode(SLEEP_MODE_PWR_DOWN); // теперь крепко уснет - в конце while(1) ENABLE_EXTERNAL_INT0; // разрешить прерывание по нажатию кнопки } break; default: //! Что то пошло совсем не так - подать индикацию ошибки сюда. Вообще - невозможное состояние при правильно написанной программе current_signal = LIGHT_NUM_ERR; SET_FORCE_SET_SIGNAL_FLAG; //setPorts(current_signal,true); //setPeriods(current_signal,true); break; }
#pragma endregion // Высшие строки решили, что надо переключить состояние в какое-то другое? if(IF_FORCE_SET_SIGNAL_FLAG){ // Флаг принудительной установки состояния сигнализации по current_signal RES_FORCE_SET_SIGNAL_FLAG; // сброс флага SET_USE_FIRST_VALUES_LIGHT_FLAG; // Новый режим - начинать с 0-го значения пары ддр-порт setPorts(current_signal, IF_USE_FIRST_VALUES_LIGHT_FLAG); // переключить режим текущего состояния на #current_signal в массиве setPeriods(current_signal, true); } // спать еще на 1/37 секунды. Или, может, и дольше. sleep_cpu(); //и в самом конце бесконечного главного цикла - уходим в сон.
} //.................................... функции
//
// установка значений порта В void setPorts(uint8_t num, bool use_main_values){ uint8_t val; DDRB = 0; PORTB = 0; // Если основной режим (мигания) - ddr_val_0, else = ddr_val_1
// val = (use_main_values) ? pgm_read_byte_near(&(traffic_signals[num].ddr_val_0))
// : pgm_read_byte_near(&(traffic_signals[num].ddr_val_1)); // то же самое, но не так понятно, арифметика указателей, данные в коде, зато на 14 (!!!) байт код легче. val = pgm_read_byte_near(&(traffic_signals[num].ddr_val_0)+( (use_main_values) ? 0 : sizeof(uint8_t)*2 ) ); val &= ~_BV(BUTTON_PIN); // сброс бита пина кнопки, она всегда включена - ВХОД DDRB = val; // установка режима пинов порта val = (use_main_values) ? pgm_read_byte_near(&(traffic_signals[num].port_val_0)) : pgm_read_byte_near(&(traffic_signals[num].port_val_1)); val|= _BV(BUTTON_PIN); // подтяжка на пине кнопки - активируется - срабатывание на низкий уровень PORTB = val; // установка значений выходов и входов порта
} //Установить время окончания режима мигания (или длительность работы режима сигнализации)
void setPeriods(uint8_t num, bool set_both_flash_and_signal){
// глобальные переменные tl_flash_end = pgm_read_word_near (&(traffic_signals[num].flash_period)); // период мигания tl_flash_end = (tl_flash_end)? tl_flash_end + globalTimer : 0; //время окончания режима мигания - если не нулевое значение периода //if(tl_flash_end){ tl_flash_end += globalTimer; } <- вот так код на 8 байт длиннее // если установить оба периода - и состояния и мигания if(set_both_flash_and_signal){ tl_signal_end = pgm_read_word_near(&(traffic_signals[num].signal_period)); tl_signal_end = (tl_signal_end)? tl_signal_end + globalTimer : 0; // время переключения на следующий режим, если не нулевое значение }
}
//Program size: 976 bytes (used 95% of a 1 024 byte maximum) (0,83 secs)
//Minimum Memory Usage: 8 bytes (13% of a 64 byte maximum)

Program size: 976 bytes (used 95% of a 1 024 byte maximum) (0,83 secs)
Minimum Memory Usage: 8 bytes (13% of a 64 byte maximum)

Всё.

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

Анатомия самшитово-саманного светофора

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

Нет, действительно много

Мозги

Светодиоды и токоограничивающие резисторы будут в других местах, с ПП их стираю. В DipTrace приведенная выше принципиальная схема из модуля «Схемотехника» естественным образом экспортируется в модуль «PCB Layout», предназначенный для редактирования печатных плат.

Размеры печатной платы — по размерам уже готовой внешней оболочки светофора.

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

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

Всегда можно подправить или дорисовать недостающее перманентным CD маркером.

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

Отлуженные дорожки — уже вполне себе ничего.

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

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

Внутренности

В хобби-маркете Леонардо как-то "Craft&Clay" 50-ти граммовые распродавались рублей по 70. Очень полезный для макетирования и поделок материал — полимерная глина. Отражатель будет из неё, вот он формуется на колпачке капсулы от бахил.

Эти капсулы так же незаменимы для хранения мелких рассыпчатых хрупких вещей.

Снятые с колпачков и прогретые после этого 15 минут в духовке при 130 градусах обработаны наждачкой.

Разность цветов полимера не имеет значения — отражатели пойдут под покраску.

Окрашенные и просохшие — соединяются по принципиальной схеме.

Провод МГТФ — пойдёт на управляющий вывод контроллера.

И провода правильно убирать в термоусадку.

Конструкция из 4 х отражателей располагается вокруг трубки, диаметром на 2-3 мм больше, чем
диаметр батареи АА, на которой намотано несколько оборотов бумаги.

Белый клей, вероятно, смотрелся бы симпатичнее, но "он ел что давали". И фиксируется термоклеем.

Естественно, все блоки поясов цветов должны подходить по месту к светофору.

На фото попал диод для зеленого пояса — без него все 4 зеленых LED последовательно немного светились от 5В без сигнала управления на центре. Окончательно склеенные отражатели напоминают спутник связи из KSP.

9-5В стоимостью меньше, чем разовый проезд на автобусе. АА батарея внутри бумажной трубки поджата с обеих сторон самодельными пробками с самодельными пружинами из стальной проволоки, к которой припаян МГТФ, идущий на DC-DC 0.

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

Оживление

Отладка, естественно, без батарейки, питание от программатора.

Пара добавочных проводов питания и разъём ISP на плате подключал к 10-ти пиновому разъему программатора.

Изучение жизнедеятельности светофора

Для автономного устройства всегда актуален вопрос времени работы, прописано ли требование в ТЗ, или просто висит в воздухе "как можно дольше". Светофор регулирует направления точно по ГОСТу, выглядит, как настоящий, но кое-что осталось за кадром. 5 В, в таблице сравнительной емкости выделил жирным используемый вариант — алкалиновую АА батарею. Светофор спроектирован для работы от 1. Нельзя не посоветовать хороший структуированный справочник по энергосбережению и всяческим энерготрюкам на МК, оттуда и взял данные для таблицы.

Тип батареи

Емкость, мАч

Саморазряд, %/мес

Ток саморазряда, мкА

CR1212

18

1

0.250

CR1620

68

1

0.950

CR2032

210

1

3

NiCD AAA

350

20

98

NiMH AAA

900

30

375

NiCd AA

1000

20

270

Alkaline AAA

1250

2

35

NiMH AA

2400

30

1000

Alkaline AA

2890

2

80

Li-Ion

4400

10

600

Например, выбранная алкалиновая батарея АА, к потреблению устройством добавляет (2/100)2890/(2430) = 80 мкА. Два последних столбца показывают значения саморазряда, который можно представить эквивалентом паразитного энергопотребления. Невеликий ток, но он в 2 с лишним раза больше, чем 32 мкА теоретического минимального потребления МК ATtiny13.

Основной потребитель энергии — светодиоды, подбором резисторов ток ограничивался до минимальных значений (от 4 мА до 14 мА на "полуветку"), при которых свечение было достаточной яркости, хотя вывод МК позволяет постоянно держать до 40 мА. При написании прошивки во многих местах задача автономности неявно решалась: это и увод в сон при бездельи, и подтягивание неиспользуемых GPIO, и отключение прерываний при отсутствии необходимости.

Подключая амперметр в цепь питания устройства, замерю ток в различных режимах отображения. Естественно, в различных режимах потребная мощность будет отличаться — самое время посмотреть насколько. 5 В -> 5 В. Кроме этого, замеры буду делать в двух вариантах: при питании от внешнего источника 5В получу данные энергопотребления светофора без DC-DC преобразователя, и в варианте, когда амперметр включается в цепь между батареей и повышающим DC-DC данные будут с учетом преобразования 1. Я не делал отдельной прошивки для каждого измерения, из-за чего достоверность измерений оцениваю не выше +-20%, оценивал порядок энергопотребления.

5 В), что согласуется со Данные однозначно показывают, что даже в спящем "выключенном" режиме энергопотребление 4-5 мА(5 В) или, что то же самое, 25 мА(1.

2890 мАч батареи АА хватит всего на 116 часов, что меньше пяти суток. Т.о. Полагаю, основное слагаемое таких больших токов утечки — токи через выбранную мной схему подключения светодиодов. Потребление самого ATtiny13 в режиме сна POWER DOWN с отключенной периферией не более нескольких сотен мкА. Кроме этого, потребление энергии DC-DC преобразователем во время сна не прекращается. По спецификации входное сопротивление пина с включенной подтяжкой 20-50 кОм, в альтернативном режиме включения, Hi-Z, ток утечки 1 мкА.

Мораль: выключатель питания в цепи батареи является деталью совершенно необходимой. Во включенном состоянии время жизни светофора будет меньше от 2 до 5 раз.

Некоторые процессы можно только прекратить, но не закончить

Совершенству пределов нет, можно продолжать гранить бриллиант и дальше:

  • При определении пина вводом, включать подтяжку — обязательно, если сэкономили на внешней подтяжке R2, уменьшит количество переключений триггеров состояния от наводок, сэкономит ещё чуть энергии. Можно безболезненно выкинуть из программы.
  • Освободить еще один пин. Подключить желтый пояс света так же как красный и зеленый — но придется отказаться спать на каждом цикле по 1/37 секунды, реализовав импульсное управление и подобрав светодиоды для компенсации снижения яркости.
  • Добавить пару полевиков в схему и реализовать честное отключение питания от повышающего преобразователя по сигналу МК, а не уход в глубокий сон, как сейчас. В режиме сна POWER_DOWN энергопотребление самого МК до 30 мкА, но кроме МК жрёт преобразователь напряжения, он работает на холостом ходу, и токи утечки через цепочки светодиодов все равно заметны.
  • Экземпляр структуры traffic_signals ужать с 8 байт до 4, не изменяя логики. Используются всего 4 pin — в 1 байт можно упаковать и DDR и PORTB
  • Двухбайтовый тип uint16_t для signal_period и flash_period — не обязателен, можно в четвертях секунды задавать, тогда по 1 байту хватит на хранение.
  • Перевести на ассемблер — по ROM ужаться раза в 2 можно, переменные сделать регистровыми — не надо будет постоянно читать-писать в память при работе с ними.
  • В освободившееся место и пины впихнуть обмен данными для управления несколькими светофорами по ИК/UART.
  • Утащить внутрь функции main глобальные переменные — каждая глобальная переменная в С — примерно +50 байт требуемого ROM.
  • МК позволяет снизить частоту внутреннего генератора с 9,6 МГц хоть до 128 кГц, уменьшая собственное потребление МК в десятки раз.

Но.

Но есть очень весомый контрдовод к продолжению работ: любимая жена говорит "выкинь ёлку", и, учитывая 8 марта, ёлку придётся разобрать, а ёлочную игрушку «Светофор» счесть прошедшей приемные испытания.

Осталось предоставить отчет по разработке на развлечение и суд хабрапублике.

Видео работы

Исходники доступны на Github, под лицензией MIT.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть