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


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


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

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


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


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


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


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


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


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


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



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



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



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



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


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


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


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


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



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



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


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



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



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


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


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


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


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


Язык/фреймворк: C / Arduino 1.6/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 [Заметки по режимам энергосбережения]


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


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


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



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


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


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


Файл .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)

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


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


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


Изменение скелета программы: устанавливаем тактирование и делитель таймера-счетчика (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 ноги: вход, выход, земля и питание"(с) жалостливая студенческая песня


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


Уровень сигнала и тип ножки в 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 объяснить режимы работы и установки портов у меня вряд ли получится.



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


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


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

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


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

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


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

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


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

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


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

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


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


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

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


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


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


"*При этом длительность сигнала "красный с желтым" рекомендуется устраивать не более 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 восемь (или девять?) пинов ввода-вывода


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


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


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


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


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


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


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


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


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



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


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


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

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

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


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


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


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


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


Любое рабочее состояние светофора полностью определяется вышеописанной структурой типа lightSignalization. Массив вышеописанных структур lightSignalization содержит данные по всем возможным состояниям режимов работы. Элементы массива с 0го по 7й регулирующие сигналы, 8й — мигающий желтый, и 9й — режим, где все "лампочки" у светофора выключены. В дефайнах определены номера элементов, но обязательным должны быть только значения номеров с 0 по 7, их номероместо используется в логике программы. Да, я знаю, что тащить данные в код непристойно. Но альтернативой работы с 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. Сигнализация выключена, все огни погашены.
    Номер в массиве traffic_signals[] — LIGHT_NUM_LIGHTS_OFF
    {0, 0, 0, 0, 0, 0} // traffic lights off
    Самый простой режим — в lightSignalization все нули, все пины управления светодиодами — входы, продолжительность сигнала lightSignalization.signal_period==0 (т.е. бесконечность), продолжительность мигания lightSignalization.flash_period==0, т.е. та же бесконечность.


  2. Нерегулирующая сигнализация, мигающий желтый.
    Номер в массиве 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. Но есть время изменения состояния lightSignalization.flash_period=ONE_SECOND, когда переключаются между собой:


    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, после которого необходимо переключиться на следующий режим.


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


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

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


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 когда счетчик нажатия кнопки больше длинного нажатия

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


Окончательный исходный код прошивки
#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 мм больше, чем
диаметр батареи АА, на которой намотано несколько оборотов бумаги.



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



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



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



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



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



Оживление


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



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



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


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


Тип батареи Емкость, мАч Саморазряд, %/мес Ток саморазряда, мкА
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.


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


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



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



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


Во включенном состоянии время жизни светофора будет меньше от 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.

Комментарии (81)


  1. igrblkv
    10.03.2019 23:47
    +1

    Зелёный мигает явно не по ГОСТу


    1. Archy_Kld Автор
      11.03.2019 10:03
      +1

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


      1. igrblkv
        11.03.2019 10:26
        +2

        Вот так всегда у нас: Заказчик сказал, а на ГОСТы можно и покласть…

        PS: Шутка, если что.


        1. vvzvlad
          12.03.2019 18:18

          Там все равно «рекомендуется»


  1. MEG123
    11.03.2019 01:08

    принципиальная схема простого светофора — огонь! Земля нарисована на все деньги!


    1. sim2q
      11.03.2019 03:21

      Да и статья — космос!)
      Хорошо — дочка, а то такое бы могло получиться)))


    1. batman12345
      11.03.2019 03:26

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


      1. Archy_Kld Автор
        11.03.2019 09:46

        Косяк. Обидно, что давно исправленный, в "./schematic/traffic light ver0.dch" схема верная, вот такая.

        А в скриншотах к статье пропустил давнюю, неисправленную.


  1. batman12345
    11.03.2019 03:40
    -1

    Что такое нужно делать с многострадальным си, чтобы переменная заняла 50 байт? Поставьте уже AtmelStudio и не позорьтесь.
    Добавление копеечной микросхемы сдвигового регистра позволит расширить число выводов без программного садо-мазо.


    1. Archy_Kld Автор
      11.03.2019 09:41

      Предположу, Вы о ремарке Утащить внутрь функции main глобальные переменные — каждая глобальная переменная в С — примерно +50 байт требуемого ROM..
      Очень подозреваю, что результат AtmelStudio вряд ли сильно будет отличаться от GCC, рост расхода памяти с глобальными переменными в сравнении с локальными не обусловлен конкретным тулчейном.
      Самому любопытно, и буду благодарен, если кто-то проведет эксперимент по компиляции под AtmelStudio и в комментарии расскажет результаты.

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


      1. augorelov
        11.03.2019 10:11

        AtmelStudio содержит компилятор для AVR на основе GCC, как в прочем и WinAVR, и псевдосреда разработки для «дуино». И смысла ставить еще одну монструозную среду на основе VisualStudio нет.


      1. olartamonov
        11.03.2019 10:54

        Глобальная переменная в C занимает ровно столько байт, сколько положено размерностью её типа.

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


        1. Archy_Kld Автор
          11.03.2019 12:09

          Нет, не так.
          Глобальная переменная gcc инициализируется, локальная нет — накладные расходы в .bss.

          #include <avr/io.h>  
          uint8_t cnt;	//global
          int main(){
          	//uint8_t cnt;	//local
              cnt=0;      // < setup()
              while(1){
                  cnt++;  // < loop()
              }
          }

          Только что проверил: при компиляции с флагом -O0 (без оптимизации) размер прошивки с глобальной переменной 78 байт, с локальной 58.
          При оптимизации по размеру (-Os) 60 байт и 40 соответственно.
          Дизассемблирование показывает — куда уходит память, слева — вариант с глобальной переменной.
          image


          1. Moduvator
            11.03.2019 13:49

            Если вам настолько нужно экономить ROM, можно сказать AVR-GCC, чтобы тот поместил глобальную переменную в область .noinit, а не в .bss, как это он делает по-умолчанию:

            int foo __attribute__ ((section (".noinit")));


            1. Archy_Kld Автор
              11.03.2019 14:59

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

              uint8_t cnt __attribute__ ((section (".noinit")));

              Компиляция с -О0, локальная переменная прошивка 58 байт, глобальная в секции .noinit — 62 байта.
              Слева — вариант с глобальной переменной в .noinit, справа — локальное объявление, остальные части листинга практически одинаковы.
              Обратите внимание: по смещению 2e (начало while(1) слева) чтение значения из адреса — это 4 байта, инкремент (ага, вычесть 255), затем опять 4 байта на сохранение значения.
              image
              Чем больше раз мы будем обращаться к переменной, тем больше вырастет код.
              Для переменных с типами размером больше байта ситуация еще грустнее.

              Кстати, наверное, стоило поделиться хитростью в статье.
              Чтобы по-быстрому откомпилировать-залить, или посмотреть асм-листинг не запуская IDE, я открываю cpp/c/ino в notepad++, и прямо из него по F5 (или меню Запуск-Запуск) запускаю коммандный файл из директории ./gcc.
              "....path/gcc/0_MAKE & asm-O0.cmd" $(FULL_CURRENT_PATH)
              Добавил «0_MAKE & asm-O0.cmd» в репозиторий на github.


          1. olartamonov
            11.03.2019 14:26

            Ну да, do_clear_bss заливает нулями всю область bss в ОЗУ.

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

            И как уже сказали, есть .noinit.


            1. Archy_Kld Автор
              11.03.2019 15:22

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


              1. olartamonov
                11.03.2019 15:28

                Давайте я буду краток?

                Утверждение «каждая глобальная переменная в С — примерно +50 байт требуемого ROM» в корне неверно.

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

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

                Более того, я без труда могу показать вам ситуацию, когда локальные переменные сожрут у вас радикально больше ОЗУ, чем глобальные.


                1. Archy_Kld Автор
                  11.03.2019 17:04

                  Интригующе смотрится Ваша осведомленность в том, что и как мне представляется. И желание оставить вне поля зрения факт, что переменные не только читаются, но и пишутся в память при изменении — те же +2 байта.
                  Досадно, что Вы сейчас приводите свою экспертную оценку, споря с результатами работы компилятора на конкретном примере.
                  Но, может, на «большом» исходнике TrafficLight13.ino всё будет по отстаиваемой Вами точке зрения?
                  Напомню её "Глобальная переменная в C занимает ровно столько байт, сколько положено размерностью её типа."
                  Там есть подходящая локальная переменная, перенесем ее в глобальные для проверки.

                  int main() {
                  	uint8_t	current_signal;				// 1 байт на текущее состояние, номер в traffic_signals
                  

                  При компиляции с нулевой оптимизацией -О0 размер прошивки 2038 bytes (199.0% Full). С параметром -Os 938 bytes (91.6% Full).
                  Если переменную вынести в глобальные размер прошивки 2066 bytes (201.8% Full) с флагом О0, а с оптимизацией по размеру 974 bytes (95.1% Full).
                  И если описать её как глобальную в секции .noinit размер прошивки те же
                  2066 bytes (201.8% Full) (что логично — остальные глобальные в .noinit не попали, 20 байт на его обнуление все равно задействованы) и с оптимизацией по размеру -Оs те же 974 bytes (95.1% Full).
                  Компиляция без оптимизации интересна тем, что asm файл довольно точно следует исходному коду на С, никакой отсебятины и экономии от компилятора не добавляется.
                  Однако всё равно 2046 больше, чем 2038, а разница при оптимизации по размеру 974 и 938 еще более впечатляющая — на фоне суммарного размера ROM.
                  Увы.
                  Буду краток.
                  — Перенос глобальной переменной в локальную дает экспериментально точно выявленную экономию: 36 байт. Ранее, напомню, моя оценка такого действия была «примерно добавит 50 байт». Если все глобальные вынести в .noinit, вероятно, экономия будет +20. Суммарно на все, но среди остальных несколько двухбайтовых типов, где экономия обещает быть больше, то на то.
                  — Ваше мнение о том, что место в памяти для глобальной переменной расходуется только на размер типа не подвердилось, упущены расходы на инициализацию .bss и повышенные расходы на чтение и запись, извините.
                  — Вы не путаете виды памяти, обещая мне показать перерасход ОЗУ в контексте беседы о экономии ROM?
                  — Самое забавное, Вы, ошибаясь конкретно в этом примере, по общей теории практически во многом правы. Но правы «вообще», не по обсуждаемой конкретной задаче минимизации конкретного кода в конкретном килобайтном кристалле.


                  1. GarryC
                    11.03.2019 18:36
                    +2

                    Вот это как раз моя делянка, только я смотрю не размер кода вообще, а конкретный ассемблер.
                    Если байтовая переменная глобальная, то любой доступ к ней занимает 2 слова и 3 цикла, для статической переменной — то же самое, а вот для локальной на стеке — 1 слово и 2 цикл, ну а для регистровой — 0 слов и 0 тактов.
                    При этом размер переменной как таковой нисколько не меняется, меняется только совокупная стоимость владения, отраженная в объеме памяти программ, но никак не памяти данных, причем конкретное значение предсказать нельзя от слова совсем.
                    В то же время обращение к элементу массива длинной более 64 байт приведет к катастрофической просадке варианта с локальным массивом. Поэтому и Олег прав, но и Вы тоже правы, но по своему, вопрос в трактовке.


                    1. Archy_Kld Автор
                      11.03.2019 19:54
                      +1

                      Мало чего добавить можно к комментарию. Спасибо разве сказать, хорошо сказано.
                      Первоначальное заявление Олега было настолько воскитительно бескопромиссным и безапелляционным, что было сложно удержаться и не показать, что жесткие формулировки обязаны быть еще и всеохватывающими. Заявлять, что снег всегда только белый — дискредетировать свою же информацию, хотя в подавляющей массе своей действительно не желтый. Переменная С в памяти требует не только место для себя, любимой, но существуют и накладные расходы, в стесненных рамках надо понимать и помнить, что они есть и какие именно. Чтобы была возможность выбирать чем платить будем — лишним словом ROM при обращении или лишним байтом оперативки или регистром. Я не просто так у каждой глобальной переменной писал в комментарии потребный размер, это ценники. Тинька с её 64 байтами RAM и 1к ROM отлично иллюстрирует необходимостью ощущать узкие стенки коридора решений.
                      Мне казалось очевидным, что фраза по которой начался спор, наряду со своими соседками в перечислении, является одной из спекулятивных гипотез, требующих проверки и (возможно) обеспечивающую экономию программной памяти. В случае, если это потребуется, и какой случай так же описан.
                      Но если я в цирке смеюсь, это не значит, что все такие же, и так же воспримут очевидные мне вещи.
                      Смешно тут то, что в конкретном программном коде оценка навскидку оказалось верной. А верное в базе мнение оппонента из-за своей неполноты и жесткости конструкции определения наоборот было опровергнуто экспериментом.
                      Но что еще ожидать от циркового представления?


                      1. olartamonov
                        11.03.2019 20:19
                        -1

                        Тинька с её 64 байтами RAM и 1к ROM отлично иллюстрирует необходимостью ощущать узкие стенки коридора решений


                        Иллюстрирует. Только вы их не ощущаете, вы тыкаетесь наугад и делаете неверные выводы.

                        int foo(void) {
                           static int x = 0;
                           x++;
                           return x;
                        }


                        Всё ещё считаете, что локальная переменная не требует ваших «50 байт»?..


                        1. GarryC
                          12.03.2019 09:19
                          +2

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


                          1. olartamonov
                            12.03.2019 10:24

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


                          1. splav_asv
                            12.03.2019 10:32

                            Технически, локальная переменная — переменная с локальной областью видимости.
                            Область видимости и расположение это разные характеристики.
                            Что такое «настоящая» локальная переменная? Локальные переменные могут быть на стеке, в памяти(статические), в регистре. Можно еще thread local storage вспомнить.


                  1. olartamonov
                    11.03.2019 20:15

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

                    То есть, серьёзно, в насколько изменённом состоянии сознания надо пребывать, чтобы спокойно, мимоходом отметить, что на один байт каждой переменной у вас непонятно откуда берутся аж 50 байт кода?..

                    Ещё раз: нет, это не так.

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


                    Вы понимаете, что вот это конкретное ваше утверждение — оно безусловно ложное, несмотря на то, что вы даже можете показать пример, в котором оно кажется истинным?

                    Сделайте десять глобальных переменных и убедитесь, что нет, они не заняли у вас во флэше 360 байт.

                    упущены расходы на инициализацию .bss и повышенные расходы на чтение и запись


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

                    Потому что я вам могу, в конце концов, показать программу, в которой глобальных переменных нет вообще, а инициализация bss и чтение/запись переменных в ОЗУ — есть.

                    Потому что средний ардуинщик, увлекшись вашими рассказами, легко и непринуждённо выстрелит себе в ногу, в порыве «экономии» раздув локальными переменными стек.

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


                    1. Archy_Kld Автор
                      11.03.2019 22:09

                      Вы чудесны и удивительны. Нет, на самом деле я так вижу.

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

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

                      Нельзя?
                      Я знаю.
                      Нельзя делить на ноль, нельзя не защищать GPIO резисторами, нельзя тормозить после входа в поворот на мотоцикле, нельзя допускать срыва машины в занос, нельзя все переменные засовывать в стек, нельзя купаться при температуре -15, нельзя…
                      Да много чего нельзя.
                      Возможно, это может показаться немножечко чудом, или ересью, в зависимости от состояния желчного пузыря, но всё это можно, если понимать когда можно и как можно и почему нельзя.

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

                      Нельзя вот так просто взять и отказаться от чуда.
                      Погладьте кролика, он существует.
                      Просто гладьте.

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


                      1. olartamonov
                        11.03.2019 22:59
                        -1

                        Нет.

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

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


                      1. v-oz
                        12.03.2019 11:19

                        ланиты, это ж щеки, верно? вот прям руку запускаете? о_О

                        Жаль, конечно, что спор вышел на неровном месте. Но что-то полезное удастся вынести из статьи. Спасибо.


                      1. augorelov
                        12.03.2019 12:41

                        Вот объясните мне: «Почему?»
                        Код весь показать не могу по ряду причин, лень было создавать новый проект.

                        Есть два массива

                        unsigned char DataIn[512]; 
                        unsigned char DataOut[512];
                        int main (void)
                        {
                           ...
                        }

                        Собираю проект: Code=2572 RO-data=224 RW-data=12 ZI-data=2148

                        Код тот же самый, но только
                        
                        int main (void)
                        {
                           unsigned char DataIn[512]; 
                           unsigned char DataOut[512];
                           ...
                        }

                        Собираю проект:Code=2568 RO-data=224 RW-data=12 ZI-data=1124

                        Я вижу экономию в ZI ровно 1024?
                        Что я делаю не так?


                        1. Archy_Kld Автор
                          12.03.2019 13:28

                          Zero-Initialized Data уменьшился на размер массивов, всё верно.
                          А что, простите, Вы ожидали увидеть?
                          В первом варианте оперативку зарезервирует компилятор, и добавит еще забой нолями.
                          Во втором случае оперативка будет выделена после начала работы/исполнения main(). Будет ли далее память инициализироваться, нет ли жизни на Марсе — наука пока данных не получила.

                          Вот интереснее, что Code уменьшился на 4 байта — но тут я предположить ничего не готов.


                          1. augorelov
                            12.03.2019 14:03

                            Продолжаем эксперимент.
                            Массивы

                            unsigned char DataIn[512]; 
                            unsigned char DataOut[512];

                            остались в коде и расположены в функции main()
                            Добавляем в код следующий массив
                            short Sinewave[384] = {
                                0, 402, 804, 1206, 1608, 2009, 2410, 2811, 
                                3212, 3612, 4011, 4410, 4808, 5205, 5602, 5998, 
                                6393, 6786, 7179, 7571, 7962, 8351, 8739, 9126, 
                                9512, 9896, 10278, 10659, 11039, 11417, 11793,
                                ...
                            }

                            Размещаем до функции main().
                            Наблюдаем результат компиляции: Code=2572 RO-data=224 RW-data=780 ZI-data=2148
                            Total ROM Size (Code + RO Data + RW Data) 3576 ( 3.49kB)
                            Размещаем внутри функции main().
                            Наблюдаем результат компиляции: Code=2592 RO-data=992 RW-data=12 ZI-data=2148
                            Total ROM Size (Code + RO Data + RW Data) 3596 ( 3.51kB)

                            Меняем тип массива с short на int массив до main(), видим Code=2636 RO-data=224 RW-data=1548 ZI-data=2148

                            Вот никак у меня не получается найти лишние байты на глобальных переменных.


                            1. Archy_Kld Автор
                              12.03.2019 15:36

                              Постановка задачи — посмотреть, что будет, если выпить касторки при кашле, потому что при запоре она помогала?
                              Поправьте меня, если ошибаюсь, но Вы сейчас компилируете пример под ARM архитектуру (32бита?) для проверки (? нет?) описанного уменьшения размера в программе под 8бит AVR?

                              Выше, в беседе с Олегом я вставлял картинку сравнения двух дизассемблированных вариантов. Сравнивая ассемблерные файлы здорово видно, что уменьшение размеров кода прошивки (постоянной памяти) произошло из-за конкретных особенностей архитектуры AVR.
                              — уменьшение кода за счет исчезновения инициализации .bss на 20 байт.
                              — уменьшение кода за счет того, что длинна инструкции чтения/записи из/в конкретного адреса памяти по конкретному адресу занимает по 4 байта, а длинна команды для чтения из памяти меньше — загрузка Y регистра 2 байта, затем (ldd r24, Y+1) чтение по косвенной адресации размером в 2 байта, запись обратно — тоже только 2 байта.

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

                              Но.
                              Размеры, занимаемые переменными в ОПЕРАТИВНОЙ памяти зависят исключительно от типа переменной. Для глобальных и статических ячейки памяти будут заняты постоянно, а для локальных переменных возможны варианты, причем глобальные переменные будут «внизу» памяти, а локальные на стеке будут «свисать» с потолка, что может привести при нарастании к их пересечению, но это сейчас не важно, как и фрагментация. Адресация же в 32 и 8 битной архитектуре, понятно, фундаментально разная, Игорь использовал более корректный термин «слово» вместо «байта».
                              Возможен ли для 32битной архитектуры ARM описанный полушутливый трюк — тут смотреть надо, точно так же взять простенький тестовый файл и откомпилировать в разных вариантах, посмотреть размеры прошивки, посмотреть получающийся asm.


                              1. olartamonov
                                12.03.2019 16:24

                                Вот видите, вы уже начали двигаться в правильном направлении — думать, почему вы видите то, что вы видите.

                                Правда, пока не до конца:

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


                                Нет, не надо.

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

                                Можно этот код просто посмотреть.

                                И для ARM его точно так же можно посмотреть (и даже проще, с большой вероятностью он будет не на ассемблере).

                                И никакой магии, никаких волшебных шляп с кроликами.


                                1. Archy_Kld Автор
                                  12.03.2019 17:32

                                  Олег, вы полагаете найти какая-то иная причина, вероятно, отличающаяюся от моего описания фокуса с кроликом, прячется где то в этом 70 килобайтном файле?

                                  Я не силён в ассемблерах, могу попросить Вас самого описать — отчего, что было причиной, что в конкретных примерах программ, случилось уменьшение размеров кода при переносе переменной из глобальных в локальные?

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

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


                                  1. olartamonov
                                    12.03.2019 18:11

                                    Почему вы считаете, что ваша личность и «спор» с вами для меня вообще имеют какое-то принципиальное значение?

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


                                    1. Archy_Kld Автор
                                      12.03.2019 18:32

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

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

                                      Предлагаю эксперимент. В свободное/ближайшее время я в исходник светофора добавлю глобальную переменную. Буду в ней считать что-нибудь, неважно, бесполезное. Количество переключений, или сравнивать с чем то, не суть. Просто новая переменная.
                                      Затем сравню размеры прошивок — где переменная как глобальная, и переменная, та же самая, как локальная. Код выложу на github — чтобы любой мог повторить эксперимент.
                                      Моё мнение — что размеры прошивок будут разными, причем в случае с локальной переменной размер будет меньше.

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

                                      Всё просто, честно, наглядно.
                                      Да или нет?


                                      1. olartamonov
                                        12.03.2019 20:49
                                        -1

                                        Господи, вы ведь до сих пор даже не смогли понять, что до вас пытаются донести.

                                        Всё-таки ардуина — это не платформа, это диагноз. «В контроллере творится магия, сейчас я выложу код с трюком».


                                        1. Archy_Kld Автор
                                          12.03.2019 20:57

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


                                          1. olartamonov
                                            12.03.2019 21:02

                                            Это вопрос уровня «перестали ли вы пить коньяк по утрам». Вам уже три раза объяснили, почему увеличение футпринта при использовании в конкретной программе одной глобальной переменной не означает, что каждая глобальная переменная будет так же увеличивать код, что локальная переменная не будет увеличивать код, и т.д. и т.п.

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

                                            Засим откланиваюсь.


                                            1. Archy_Kld Автор
                                              13.03.2019 00:08

                                              Действительно, моя искренняя благодарность Moduvator и GarryC за участие, пояснения и объяснения.

                                              А теперь результаты эксперимента.
                                              Diff исходника с добавленной бесполезной переменной.

                                              Глобально объявляю uint16_t useless_var __attribute__ ((section (".noinit")));
                                              Размер прошивки 2192 и 1060 байт для компиляции без оптимизации и с оптимизацией по размеру.
                                              В main локально объявляю uint16_t useless_var.
                                              Программа 2146 для компиляции без оптимизации и 982 байт (Ура, влазит (95.9% Full)) с флагом -Os.
                                              Экономия на переносе 78 байт стала позволять поместиться прошивке в ROM тиньки.

                                              Выводы, постараюсь сформулировать аккуратно.
                                              У ATtiny13 (а вероятно у 8-бит AVR вообще) каждая глобальная переменная, которая используется только в основной функции, и может быть перенесена внутрь неё как локальная — вызывает перерасход программной памяти. Объявление хотя бы одной глобальной переменной без директивы расположения в секции ".noinit" так же вызывает перерасход программной памяти. Размер экономии памяти при перемещении переменной в локальную зависит от частоты использования переменной в программе, от количества других переменных, но даже в вырожденном случае, при единственной переменной во всей программе, состоящей из инкремента в цикле для однобайтного типа минимальная экономия будет 4 байта, для двухбайтного типа 10 байт.
                                              Формулировка верна как минимум до двух (Привет, второй кролик) таких переменных.
                                              GarryC намекал, что до 64, почему — мне пока не понятно.
                                              Причина — практически дизассемблированием .elf обнаружил, что avr-gcc\5.4.0-atmel3.6.1 генерит различный код для организации доступа к глобальным и локальным переменным для ATtyny13.


                              1. augorelov
                                12.03.2019 22:42

                                Вы не поверите, провел эксперименты на 8 разрядном микроконтроллере STM8. И представьте себе не увидел, что переменные занимают больше места в .bss, чем положено (char — 1Байт, short — 2Байта). Это к разговору о разрядности архитектуры.
                                Я не знаю, что я делаю не так?!

                                З.Ы.
                                Использовал Ваш код с гитхаба.


                                1. Archy_Kld Автор
                                  12.03.2019 23:54

                                  Можно объяснить существующее явление, а не отсутствующее.
                                  Я сейчас, не поверите, ровно так же сидел и проверял.
                                  Что происходит в AVR 8-битном, из-за чего уменьшается потребная программная память мне понятно: глобальная и локальная переменные. Достаточно посмотреть-сравнить размеры памяти на строки 33-34 и дальше.
                                  У глобальных переменных копируется значение из адреса, прямо указываемого в операторе, 4 байта на чтение, 4 байта на запись.
                                  А для чтения локальной — адрес грузится в регистровую пару Y, один раз. А операции чтения и потом записи — двубайтовые.


                                  1. augorelov
                                    13.03.2019 01:39

                                    И какой напрашивается вывод?


                                    1. Archy_Kld Автор
                                      13.03.2019 10:11

                                      Компилятор avr-gcc (GCC) версии 5.4.0 для МК AVR ATtiny13 и ATMega328 (локальная и глобальная) генерирует различный машинный код при оперировании глобальными и локальными переменными.
                                      На примере инкремента переменной

                                      cnt++;

                                      Для глобального её определения код занимает 10 байт:
                                      ...
                                      2e:  80 91 60 00   lds    r24, 0x0060  ; 0x800060 
                                      32:  8f 5f         subi   r24, 0xFF    ; 255
                                      34:  80 93 60 00   sts    0x0060, r24  ; 0x800060 
                                      

                                      При определении переменной как локальной 6 байт:
                                      ...
                                      2e:    89 81           ldd    r24, Y+1   ; 0x01
                                      30:    8f 5f           subi   r24, 0xFF  ; 255
                                      32:    89 83           std    Y+1, r24   ; 0x01
                                      

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

                                      С теоретической точки зрения нет никаких оснований ожидать, что для фактически одного действия — загрузки/выгрузки переменной из ОЗУ в регистры компилятор будет выдавать различный машинный код для локальных и глобальных вариантов использования переменной. Здесь я целиком разделяю мнение Олега.

                                      Реальное же поведение как минимум конкретной версии компилятора, как минимум для двух target МК, которые я проверил — вот тут кроличья нора. Как назвать этот факт — чудо, фокус или баг — зависит лишь от состояния желчного пузыря.
                                      При необходимости сэкономить программную память на AVR, можно вспомнить и попробовать использовать этот трюк. Или сразу при написании программы стараться НЕ использовать глобальные переменные.

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


                                      1. olartamonov
                                        13.03.2019 11:09
                                        -1

                                        С теоретической точки зрения нет никаких оснований ожидать, что для фактически одного действия — загрузки/выгрузки переменной из ОЗУ в регистры компилятор будет выдавать различный машинный код для локальных и глобальных вариантов использования переменной. Здесь я целиком разделяю мнение Олега.


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

                                        И считаете это всё трюками и прочей магией.

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

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

                                        Заодно отпадёт необходимость проверять «как минимум для двух МК» — где-то в момент осознания, что компилятор не то что «минимум на двух», а на всей линейке с одним и тем же ядром работает одинаково.

                                        Или сразу при написании программы стараться НЕ использовать глобальные переменные.


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

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


                                      1. augorelov
                                        13.03.2019 11:49

                                        Уберите из статью это фразу: "каждая глобальная переменная в С — примерно +50 байт требуемого ROM."
                                        Так как она в корне не верна.
                                        Поясните почему происходит увеличение прошивки при использовании переменных с различными областями видимости, в результате работы конкретного компилятора и конкретных его настроек, и конкретного скрипта компоновщика. Почему нельзя злоупотреблять локальными переменными, чем это вредит.
                                        А так это выглядит: «я тут ткнул пальцем в небо и решил».


  1. vsvasya
    11.03.2019 05:01

    У настоящих светофоров никогда не делают световозвращатели


    1. augorelov
      11.03.2019 10:06

      Не путайте понятия отражатель и световозвращатель.


  1. Gourry_aka_pm
    11.03.2019 07:45

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


    1. splav_asv
      11.03.2019 21:29

      Только после пешеходного. Те самые три секунды недостающего желтого сигнала.


  1. vin2809
    11.03.2019 08:27
    +1

    Большое спасибо! Отличная статья.

    P.S. На настоящую игрушку, может и не дотягивает, но великолепно подходит как заготовка для целой серии игрушек…


  1. mcroitor
    11.03.2019 09:32

    Еще не дочитал, но уже нравится. Оставляю как чтиво после работы, вместо Донцовой.


  1. qb60
    11.03.2019 10:25
    +1

    Хорошая длинная статья, написано интересно, спасибо.
    Один момент: если кто-то будет учиться программированию AVR по ней, прошу обратить внимание на использование глобальных volatile uint16_t переменных.
    У автора основной цикл спит между прерываниями таймера, но вообще есть опасность нарваться на неатомарный доступ к таким переменным, когда команды чтения в основном цикле прерываются записью в прерывании.


    1. Archy_Kld Автор
      11.03.2019 11:24

      Верно, не акцентировал внимания, исправляюсь.
      Квалификатор volatile не только компилятору запрещает кешировать переменную в регистре, но и накладывает обязательство на автора программы — рассмотреть последствия возможных изменений.
      Здесь volatile только переменная глобального таймера, основной цикл выполняется за время многократно меньшее промежутка её инкремента от переполнения, а выполнение цикла инициируется её изменением от старта прерывания переполнения счетчика.
      А вот при просыпании из POWER_DOWN_MODE, хотя она обнуляется, но таймер не реинициализируется, и, естественно, её значение может скакнуть в любой момент.
      Последствиями будет возможное изменение периода самого первого после просыпания сигнала на дельту времени до 1/37 секунды. (Или до 1/64, если выполняется на МК с частотой 16МГц).
      Критической проблема может стать, если случится переполнение целого беззнакового, uint16_t из 65535 превратится в 0: вся логика сравнений превратится в тыкву. Для недопущения этого при достижении безопасного порога в половину максимально возможного значения типа переменной в самом начале основного цикла идет корректировка глобального таймера и значений, которые с ним сравниваются.

      #define MAX_GLOBAL_TIMER_VALUE	(USHRT_MAX / 2)		// uint16_t globalTimer - защита от переполнения. 65535 /2 
      // любой период должен быть меньше, чем MAX_GLOBAL_TIMER_VALUE - 1
      ...
      // переполнение глобального таймера? 
      		if(globalTimer > MAX_GLOBAL_TIMER_VALUE){
      			globalTimer -= MAX_GLOBAL_TIMER_VALUE;			// откатить глобальный таймер
      ...
      


  1. Ranburu
    11.03.2019 10:34

    Статья неполная! Тема сисек трассировки печатной платы и ее вытравливания не раскрыта!


  1. we1
    11.03.2019 12:46

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


  1. GarryC
    11.03.2019 13:18
    +1

    Мне почему то кажется, что при аккуратном обращении в данном конкретном случае волшебное слово «Чарлиплексинг» нивелировало бы некоторые свои минусы, хотя особых плюсов и не дало, помимо учебных целей.

    Ну и в порядке занудства, почему

    #define PERIOD_0    ONE_SECOND * 10 //R G R G   0. красный --- зеленый  (10 сек)
    , а не
    #define PERIOD_RED_GREEN    ONE_SECOND * 10 //R G R G   (10 сек)
    Неужели мы экономим символы?


    1. Archy_Kld Автор
      11.03.2019 14:07

      Чарлиплексинг я упомянул, как один из возможных вариантов, вовсе не подразумевая, что он негоден. Не упомянул его преимущество (теоретически, проверять все же надо) возможности отказаться от токоограничивающих резисторов для LED, т.к. их питание пойдет в импульсном режиме. И подрастет код (сейчас 96% кристалла), задача уместить не выглядит невозможной, но это будет уже иная конструкция.

      Дефайны PERIOD_0 — PERIOD_7 используются только при инициализации массива состояний traffic_signals[], и я счел более важным их наименованием подчеркнуть небесспорный и неочевидный момент: номера в массиве используются в коде. Перемещение их на другие места не допускается. Переименовывание их в предложенный вариант — потеря информации. Конечно, можно сохранить отсылку к нумерации и добавить информацию о цветах режима.

      #define PERIOD_0_RED_GREEN    ONE_SECOND * 10 //R G R G   (10 сек)

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


      1. GarryC
        11.03.2019 18:45

        Ну я имел в виду, что при аккуратном размещении, наверное, можно сделать все режимы статическими, но это в Вашем случае не особо нужно.
        А вот о второй части Вашего предложения — не учите детей плохому, НЕЛЬЗЯ отказываться от токо-ограничивающих резисторов, от слова совсем нельзя, и неважно, какой у Вас режим — статика либо динамика.
        Производитель МК НИЧЕГО не гарантирует при превышении тока на ножку, и ссылка на конкретного Дядю Ляо, который себе это позволяет, не прокатывает.

        А по поводу имени дефайна — говорят, что хорошо написанный код само-документирован и это как раз тот случай.


  1. Polaris99
    11.03.2019 15:00

    В целях повышения интеграционного показателя разума на планете Грязь, переводим тиньку на следующую ступень интеллектуального развития: наш CPU будет большую часть времени спать, просыпаясь и работая только когда это необходимо.
    Ну отлично, чё, вместо 3 мА получим 1 мА — достижение! Может, не стоило так издеваться над здравым смыслом? Или Ардуино — это всегда издевательство над здравым смыслом? В то же время использование в таком примитивном приложении внешнего часового кварца позволило и без всяких трендовых режимов сна иметь потребление на уровне 15 мкА.


    1. Archy_Kld Автор
      11.03.2019 17:23

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

      Извините, что у меня не получилось всё это показать и сделать для Вас.
      Я, честное слово, старался.


      1. Polaris99
        11.03.2019 18:48

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


  1. IgorPie
    11.03.2019 15:49

    Можно было бы уполовинить статью, поставив stm32f030f4p6. Площадь на плате как у attiny13, стоимость примерно схожая. Остались бы ножки для объединения светофоров в сеть для сложных перекрестков. DIP Trace плата вполне трассируется, утюжится не хуже.


    1. Archy_Kld Автор
      11.03.2019 18:04

      Судите сами: стоимость ATtiny13 = 0 (ноль) рублей, я там выше писал, мне её жаба выдала со склада. Любая положительная цена, хоть одна копейка за десяток или сотню stm — это в миллиарды, в триллионы раз большая сумма.
      А если серьёзно, нужно смотреть по первоначальным требованиям. При необходимости разработки тиражируемого устройства, и DC-DC лучше свой на плате расположить, и МК я выбирал бы другой. И сравнивал бы наверное сначала по критерию энергопотребления различные варианты, по удобству заливки прошивки на потоке и обновления, по фаршу, а затем уже по цене. Возможно, что что-то из TI MSP430 оказалось бы интереснее STM32. Но в любом МК, какого бы размера не были ROM и RAM всегда может случится ситуация нехватки десятка байт. Или пары ножек. Ресурсы — они конечны по определению, хорошо, когда понимаешь какиеконкретно есть варианты более рационального использования.

      Проблемы с трассировкой решать меня научили еще на DOSовском PCADе когда-то. И изготавление ПП по ЛУТ для FT232RL с её шириной проводников и зазорами АФАИР 0.3мм тоже пройденный этап. Да и сейчас подготовка гербера и сдача на производство услуга очень распространенная.


      1. IgorPie
        11.03.2019 18:23
        -1

        Ну, если ваше время бесплатно, то можно Хоть на Z80 делать.
        FT232R шаг 0.65, там SSOP.


        1. Archy_Kld Автор
          11.03.2019 21:28
          +1

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

          Ширина проводников 0.3 + зазор 0.3 = 0.6 мм.
          Да, Вы правы, действительно не 0.65, получается приврал на лапку семнадцатого воробушка.


          1. VT100
            11.03.2019 22:09

            Да ладно! Дочи-ж помогали папе?


  1. pproger
    11.03.2019 17:50
    +1

    миша, все херня. давай по новой, на ФПГА!

    пс. а вообще-то мне (как специалисту по миганию светодиодами) проект понравился.


  1. VT100
    11.03.2019 22:09

    Светодиоды светят на отражатель (как в некоторых старых фонариках)… Ня!


  1. teleghost
    12.03.2019 00:25

    Автор, вот это я и называю культурой! Хотя никогда особо не считал себя "в теме", но из N прочитанных статей про AVR это первая, где, черт возьми, сделан акцент на энергопотреблении и отказ от раздражающего бесконечного цикла.


    … Когда-то давно было у меня учебное задание на супергарварде, и я успокоился только тогда, когда главный цикл выродился в HALT (без DI:), а всё буферное I/O ушло в прерывания и DMA. Хотя мог и не выпендриваться, а сделать как все… Но мой перфекционизм, конечно, тут нервно курит в сторонке:)


    1. teleghost
      12.03.2019 01:29

      HALT IDLE, блин 20 лет прошло:)


  1. siargy
    12.03.2019 07:05

    богатый внутренний мир простого светофора.
    спасибо, було интересно.


  1. Vitaliy_dzen
    12.03.2019 12:16

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


  1. shuvaevgl
    12.03.2019 14:36

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


  1. Vovan64
    13.03.2019 00:10

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


  1. lazy-fox
    13.03.2019 00:11
    +1

    Мигать лампочками как blink или щелкая клавишами можно и без микроконтроллера, а вот светофор — действительно уже представляет логику управления. Собирал такой на ATtiny2313 (ATtiny13 не оказалось, а тех была пригоршня, но остались свободные ноги) в штекере автомобильного прикуривателя.
    image
    Думалось помигает недолго как легкая елочная игрушка (вместо гирлянды) на еловых ветках в вазе на полке с машинками. Поэтому питал CR2032, держалка которой стала основанием. А вот режим работы предполагал при включении кнопкой сверху: помигать 5 сек желтыми и перейти в рабочий режим на 15 минут, потом ещё 7 минут помигать желтыми и уснуть.


  1. YRevich
    13.03.2019 06:47
    +1

    Нифига не понял момент с системными клоками. Откуда 9600000? Я нигде не встретил (может невнимательно смотрел?) указания на то, что вы меняли умолчательные фьюзы Tiny13. Но минутное курение даташита сообщает нам, что выпускается с фабрики он с внутренним RC, настроенным на 9,6 МГц при установленном CKDIV8, что означает клоки в 1,2 МГц. Поправьте меня, если я ошибаюсь (но я помню, что все AVR выпускаются с настройкой на внутреннее тактирование 1 МГц. Или в данном случае, близко к тому).

    При такой частоте смысл нагружать программу режимом IDLE пропадает полностью. Tiny13 при 1 МГц и 5 вольтах, согласно стр. 125 даташита, потребляет 0,7 мА, что само по себе намного меньше светодиодов. IDLE снизит эти 0,7 мА хорошо, если до 0,5 мА — а оно важно? Если уж sleep, то Power-down всегда, а тактировать можно вообще от прерываний Watchdog, что будет проще.

    И еще есть много замечаний по неоптимальному построению программы, но для этого стоит перейти для начала на чистый ассемблер. Эта ваша задачка — однозначно для ассемблерной реализации. Я почитал там вашу с Олегом полемику — вы боретесь с компилятором, а не проектируете устройство. Я прикинул — гарантирую, что можно запихать весь код в полкилобайта Flash (а скорее всего меньше), а SRAM, вероятно, вообще не потребуется, кроме стека для прерываний и подпрограмм.


    1. Archy_Kld Автор
      13.03.2019 11:07

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

      Абсолютно с Вами согласен.
      Позволю себе привести цитату себя же из этой же статьи
      — Перевести на ассемблер — по ROM ужаться раза в 2 можно, переменные сделать регистровыми — не надо будет постоянно читать-писать в память при работе с ними.
      Правда, я тешу себя надеждой, что средний ардуинщик сможет разобраться с приведенным С кодом, а вот с ASM иллюзий особых не испытываю. Статья стала бы сильно менее интересной для многих. Задачка же решенная использованием 95% кристалла, или 50%, или 10% на выходе имеет один и тот же результат. Как Вы наверняка прочитали, не ставилось целью оптимизировать программу по критерию «как можно меньший программный код».
      «Ничего, ничего, и без паровозика на пиках неплохо вышло»(с).
      Поправьте меня, если я ошибаюсь (но я помню, что все AVR выпускаются с настройкой на внутреннее тактирование 1 МГц.

      Необъяснимо. Эта тинька валялась в чулане, при заливке на неё прошивки (видно, что avrdude.exe из пакета MicroCore фьюзы не трогает), в которой я определил #define F_CPU 9600000UL, и по этой частоте считал соответствие переполнения счетчика для глобального таймера, оказалось, что временные интервалы отсчитываются верно. Выяснять отчего она работает так, как ожидал я, а не как должна по настройкам с завода, согласитесь, было бы бессмысленно.

      Безусловно, вариант цикла через watchdog по энергопортреблению именно МК (а еще и уход в минимальную частоту, да и Brown-Out Detector отключить полезно) бьёт что угодно. Но как видите, на фоне потребления LED и DC-DC потребление самой тиньки — копейки. Для кратного увеличения времени жизни батарейки лучшее решение, по-моему, через полевые транзисторы вообще отключать батарейку на время «выключения», но это другая схема и другая прошивка.

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


      1. YRevich
        13.03.2019 11:51

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

        А про такты — действительно необъяснимо. Смиримся.


  1. lazy-fox
    13.03.2019 12:53

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