Гидропоника на подоконнике или C++11 в микроконтроллерах AVR

Проект не содержит Ардуино


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

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

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

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

Гидропоника с подобным сифоном:


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

Перед описанием подробностей список ресурсов проекта:

Здесь можно посмотреть фотографии результата и процесса изготовления.

Небольшое видео:



Проект доступен на GitHub. Там же в релизах выложен файл с проектом электронной части в KiCAD и проектами конструктивных прибамбасов в SolidWorks (STL-файлы для печати прилагаются).

Особенности сборки прошивки
Для сборки используется другой мой проект — библиотека под кодовым названием «велосипедная фабрика». Там можно найти вещи, достойные отдельной статьи, например, душераздирающая история о собственной полностью программной реализации USB протокола для микроконтроллеров AVR (да, я в курсе, что я не первый, но для меня главное процесс, плюс не забываем о кодовом названии), но в данном проекте используется только система сборки и немного вынесенного туда типового кода прошивки. Если кто-то реально решит собрать прошивку, то эту библиотеку надо скачать, выставить переменную окружения 'ADK_ROOT' равную пути до её директории, и в директории проекта прошивки выполнить команду 'scons'.


Схема электронной части:



Далее подробности, описание подводных камней и немного кода. Описание программных моментов в самом конце. Возможно, кому-то будет интересно посмотреть новый пример реализации работы с I2C, валкодером, модулем RTC, графическим дисплеем. Весь код в проекте написан «с нуля» без использования сторонних решений (потому что могу).

Датчик уровня воды


Самый щекотливый вопрос решался первым. Был, конечно, вариант какого-нибудь поплавка, чтобы он, например, двигал рейку, на которой нанесён код Грея, и оптические датчики бы считывали. Но очень уж выглядело ненадёжно. Поиск по eBay результата не дал — там были либо поплавковые концевики (достигнут нужный уровень или нет), либо погружённые электроды и показания на основе проводимости среды, но это сразу отметалось, так как состав воды бы постоянно менялся вместе с проводимостью от добавляемых удобрений и растворения примесей из субстрата. В итоге, пришла идея использовать ультразвуковой дальномер, из тех, что обычно ставятся на разных роботов. По задумке, датчик ставится в крышке бака и сигнал отражается непосредственно от поверхности воды. Был куплен HC-SR04 (выбор по самому маленькому значению минимальной рабочей дистанции — она у него 2см), и на ведре с водой была проверена концепция. Оказалось, что это вполне себе работает (были опасения, что от поверхности воды не будет нормального отражения, или, что не хватит направленности луча и будут нежелательные отражения от стенок бака). Кстати, запасным вариантом был тоже дальномер, но инфракрасный. На поверхности воды предполагалось бросить поплавок с отражателем. Единственная проблема — минимальная рабочая дистанция у них 10см (из тех, что я нашёл), что уже многовато для заданных габаритов.




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

Интерфейс у датчика простой — на вход trigger подаётся импульс, который запускает эхо-сигнал. На выходе echo формируется импульс, длина которого равна времени от начала излучения до принятия отражённого эхо-сигнала. Измерив длину импульса, зная скорость звука и тот факт, что сигнал идёт до объекта и обратно, можно посчитать дистанцию. В проекте это реализовано в классе LevelGauge. Для измерения длины импульса используется аппаратная возможность МК AVR «input capture». При этом аппаратный таймер сбрасывается по восходящему фронту импульса, а по нисходящему значение таймера аппаратно сохраняется в регистре ICR1, и генерируется прерывание. Таким образом, можно измерить длительность импульса с достаточной точностью и минимальным расходом процессорного времени.

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

Подсветка


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

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





Важная особенность стабилизатора — наличие входа для ШИМ-регуляции, который я использую для регуляции яркости. Тут очередные китайские грабли. Во-первых, это оказался просто функционал включения/выключения тока. То есть я ожидал, что выходной ток модулироваться не будет, а его значение будет зависеть от скважности ШИМ-сигнала, но ток просто повторял импульсы на управляющем входе. Но это полбеды, другая засада была в том, что на ШИМ с достаточно высокой частотой регулятор реагирует неадекватно. Пришлось снизить до 300Гц, на которых он работал более-менее нормально. ШИМ-сигнал генерируется микроконтроллером аппаратно, используя один из таймеров.



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

Обратите внимание на подключение датчиков. Каждый из них имеет два фиксированных диапазона, которые выбираются подключением к нулю соответствующего резистора. Нога микроконтроллера, подключённая ко второму резистору, в это время переводится в состояние высокого сопротивления. Можно включить и оба резистора одновременно, тогда будет три фиксированных диапазона измерений. Сигнал с эмитерных резисторов пропускается через RC-цепочку для фильтрации модулирующих импульсов — свет от свтодиодов пульсирует вместе с ШИМ-сигналом на регуляторе тока.

Насос




Самый дешёвый китайский, шестерёночный, с двигателем постоянного тока. Засады, конечно же, имеются. Несмотря на то, что на нём написано 12В, при таком напряжении он долго не проработает. Один сгорел ещё до сборки конструкции. В схеме для него предусмотрен ШИМ, максимальная мощность настраивается в интерфейсе, на практике не ставил выше 70%. Уже на этом уровне он дико завывает при работе, но большую часть времени он работает на гораздо меньшей мощности — около 30% и достаточно тихо урчит. О его режимах работы ниже, в описании логики затопления. Конденсатор побольше (C8 на схеме) надо расположить поближе к контуру питания насоса, иначе будут большие помехи на всю схему (на практике оказалось, что к ним более всего чувствителен регулятор тока для светодиодов, начинается светомузыка).

Часы реального времени


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



Модуль на основе DS3231 имеет I2C интерфейс, собственный резервный источник питания — время не собьётся при обесточивании. Есть выход меандра на несколько фиксированных частот — 1кГц, 4 кГц и 8 кГц. Это очень пригодилось для звуковых сигналов — опять же не надо загружать MCU, да и таймеров свободных для этого не оставалось. Бонусом идёт EEPROM на 32Кбит, но в этом проекте оно не используется.

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

За работу с данным модулем в коде отвечает класс Rtc.

Дисплей


Давно хотелось сделать что-нибудь с графическим дисплеем. Поиск самого дешёвого с I2C интерфейсом выдал данный вариант.



Монохромный OLED-дисплей 128х64 пикселя на основе довольно популярного контроллера SSD1306. При выборе надо внимательно смотреть описание — этот же чип поддерживает другие интерфейсы, кроме I2C, и встречаются варианты без него. Либо пишут, что универсальный, поддерживает I2C в том числе, но на деле потребуется немного модифицировать плату, переставив нулёвки на другие площадки. Поэтому, если планируете использовать I2C, лучше выбирайте такой, где на плате выведен только I2C, будет меньше возни с платой, не имеющей практически никакой документации (документация только на чип). Данное исполнение работает от 5В, на плате стоит регулятор на 3.3В, требуемых для контроллера. Встречал отзывы, что в каких-то исполнениях его может не быть.

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

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

Часто практикуется подход, при котором память дисплея дублируется в RAM MCU — сначала все действия с изображением производятся в RAM, а потом все изменённые пиксели копируются в память дисплея. В данном проекте подобный подход не используется в целях экономии ресурсов. Все изменённые места перерисовываются сразу в памяти дисплея.

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

В коде работа с дисплеем реализована в классе Display.

Валкодер:



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

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

Вроде бы всё просто, но, видимо, не всегда разработчики способны сделать качественную поддержку подобного устройства. К примеру, на моём 3Д-принтере стоит плата RAMPS и подключена плата с дисплеем и точно таким же валкодером. Прошивка Marlin с ним работает, но впечатления от использования очень нехорошие — нет ощущения надёжности — когда при повороте ручке происходит щелчок, в интерфейсе зачастую остановка происходит не на том пункте меню или значении параметра, на котором ожидалось. При быстром вращении, создаётся ощущение, что щелчки пропускаются. В какой-то момент, переключения начинают происходить не во время щелчков, а где-то между ними, очень неприятно. Да что там Marlin, у меня на встроенной мультимедиа-системе в машине иногда такие же ощущения. В связи с этим несколько советов (ну и, конечно, смотрите в код в окрестностях класса RotEnc).

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

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

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

Конструкция в целом


Выполнена из подручных материалов и различных деталей, напечатанных на 3д-принтере из ABS.

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

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

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

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

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

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


А где же C++11?



Возможно, кто-то усомнится, что от C++11 может быть польза при программировании микроконтроллеров (из числа тех, кто вообще в курсе, что микроконтроллеры можно программировать на C++). Попробую привести конкретные примеры пользы от C++11 в этой сфере (помимо очевидных приятных мелочей типа constexpr, override, default и пр.).

Размещение строковых ресурсов

Многие знают, что RAM в микроконтроллерах — очень ограниченный ресурс. Это может стать проблемой, если ваше приложение, к примеру, имеет пользовательский интерфейс, и ваша программа использует достаточно большое количество строк. Если в коде написать что-нибудь типа
PromptUser("Are you sure you want to format SD-card?");

то строка, переданная в аргументах, будет размещена в секции инициализированных данных (здесь и далее речь о поведении компилятора GCC для платформы AVR) — то есть в области RAM, которая при старте (до вызова функции main) инициализируется из программной флеш-памяти. Функции PromptUser() будет передан указатель на нужное место в RAM. Если использовать подобный подход по всей программе, то RAM довольно быстро закончится (в использованном в данном проекте ATMEGA328P его всего 2 килобайта, а это ещё для BSS, кучи и стека). Чтобы обойти это ограничение, функции типа PromptUser() учат работать не с указателями на RAM, а с указателями на область в программной флеш-памяти. Читать оттуда можно только с помощью специальных инструкций, которые, к примеру, в avr-libc завёрнуты в функции семейства eeprom_read_[byte|word|dword|...].
При этом строку надо предварительно поместить в переменную, снабжённую атрибутом PROGMEM, который говорит компилятору, что её следует размещать в программной памяти.
char prompt[] PROGMEM = "Are you sure you want to format SD-card?";
PromptUser(prompt);

Здесь возникает неудобство, если вы хотите все строки объявить централизованно. Тогда вам придётся сначала в заголовочном файле объявить их декларацию:
extern char prompt[] PROGMEM;

А в отдельном .cpp файле дать определение:
char prompt[] PROGMEM = "Are you sure you want to format SD-card?";

Дублирование кода, что не есть хорошо, и очень неудобно, когда таких строк много. Да, это можно обойти, сделав хитрый макро, и включив заголовочный файл в отдельный .cpp файл, в котором макро раскроется в определение, тогда как в остальных контекстах он будет раскрываться в декларацию. Но с C++11 есть вариант почище, если использовать инициализацию членов класса при декларации. В заголовочном файле декларируем класс со строками:
#define DEF_STR(__name, __text) \
    const char __name[sizeof(__text)] = __text;

class Strings {
public:
    DEF_STR(Prompt, "Are you sure you want to format SD-card?")
    DEF_STR(OtherString, "...")
    …
} __attribute__((packed));

extern const Strings strings PROGMEM;

В .cpp файле:
const Strings strings PROGMEM;

Теперь все строки объявлены в одном месте, размещаются в программной памяти, и обращаться к ним можно так:
PromptUser(strings.prompt);

В данном проекте основанный на том же принципе подход используется и для определения битмапов — различных картинок, выводимых на графический дисплей.
/** Bitmap descriptor. */
struct Bitmap {
    /** Pointer to data array if in data memory. Offset of data array relatively
     * to Bitmaps class instance start address if in program memory.
     */
    const u8 *data;
    /** Number of pages in the bitmap. */
    u8 numPages,
    /** Number of columns in the bitmap. */
       numColumns;
} __PACKED;

template<u8... data>
constexpr static u8
Bitmap_NumDataBytes()
{
    return sizeof...(data);
}

/** Define bitmap.
 * @param __name Name for accessing.
 * @param __numPages Number of pages in the bitmap. Number of columns defined as
 *      total number of data bytes divided by number of pages.
 * @param __VA_ARGS__ Data bytes.
 */
#define DEF_BITMAP(__name, __numPages, ...) \
    const u8 __CONCAT(__name, __data__) \
        [Bitmap_NumDataBytes<__VA_ARGS__>()] = { __VA_ARGS__ }; \
    const Bitmap __name { \
        reinterpret_cast<const u8 *>(OFFSETOF(Bitmaps, __CONCAT(__name, __data__))), \
        __numPages, \
        sizeof(__CONCAT(__name, __data__)) / __numPages};

/** Global bitmaps repository. Stored in program memory. */
class Bitmaps {
public:

    /** Thermometer icon. */
    DEF_BITMAP(Thermometer, 1,
        0b01101010,
        0b10011110,
        0b10000001,
        0b10011110,
        0b01101010
    )

    /** Sun icon. */
    DEF_BITMAP(Sun, 1,
        0b00100100,
        0b00011000,
        0b10100101,
        0b01000010,
        0b01000010,
        0b10100101,
        0b00011000,
        0b00100100
    )
    ...
};
extern const Bitmaps bitmaps PROGMEM;

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

Двоичные литералы

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



Variadic templates

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

SendCommand(Command::DISPLAY_ON);
SendCommand(Command::SET_COM_PINS, COM_PINS | COM_PINS_ALTERNATIVE);
SendCommand(Command::SET_COLUMN_ADDRESS, curVp.minCol, curVp.maxCol);

Удобно, не правда ли?
    /** Queue command sending.
     * @param bytes Up to MAX_CMD_SIZE bytes of command data.
     */
    template <typename... TByte>
    void
    SendCommand(TByte... bytes)
    {
        cmdSize = sizeof...(bytes);
        controlSent = false;
        cmdInProgress = true;
        SetCmdByte(sizeof...(bytes) - 1, bytes...);
        i2cBus.RequestTransfer(DISPLAY_ADDRESS, true,
                               CommandTransferHandler);
    }

    template <typename... TByte>
    inline void
    SetCmdByte(int idx, u8 byte, TByte... bytes)
    {
        cmdBuf[idx] = byte;
        SetCmdByte(idx - 1, bytes...);
    }

    inline void
    SetCmdByte(int, u8 byte)
    {
        cmdBuf[0] = byte;
    }


В файле variant.h описан класс, отдалённо напоминающий boost::variant, используя variadic templates. Он используется для организации страниц пользовательского интерфейса. Дело опять же в экономии памяти — там, где динамическое управление памятью — непозволительная роскошь, приходиться изворачиваться (хотя 2КБ — это ещё много, можно было и не изворачиваться, но в той же линейке ATMEGA её размер доходит до 512 байт, и каждый байт на счету). В моём интерфейсе на экране в любой момент времени показывается одна страница. Соответственно для всех страниц можно использовать один и тот же кусок памяти, то, что в C называется union. Для классов в C++ это обычно называется variant. В отличие от union нам надо не забывать вызывать деструктор предыдущего содержимого, перед тем как вызвать конструктор нового.

    Variant<MainPage,
            Menu,
            LinearValueSelector,
            TimeSelector> curPage;
    ...
    /** Get type code for the specified page class. */
    template <class TPage>
    static constexpr u8
    GetPageTypeCode()
    {
        return decltype(curPage)::GetTypeCode<TPage>();
    }
...
curPage.Engage(nextPageTypeCode, page);


Для компиляции используется GCC и GNU binutils для платформы AVR (в Ubuntu есть готовый пакет gcc-avr). Выше были приведены подробности процесса сборки. Параметры компилятору выглядят примерно так (специфичные для проекта дефайны и инклуды опущены):
avr-g++ -o build/native-debug/src/firmware/cpu/lighting.cpp.o -c -fno-exceptions -fno-rtti -std=c++1y -Wall -Werror -Wextra -ggdb3 -Os -mcall-prologues -mmcu=atmega328p -fshort-wchar -fshort-enums src/firmware/cpu/lighting.cpp

Линковка:
avr-g++ -o build/native-debug/src/firmware/cpu/cpu -mmcu=atmega328p build/native-debug/src/firmware/cpu/adc.cpp.o build/native-debug/src/firmware/cpu/application.cpp.o …

Конвертация кодовой секции в hex формат:
avr-objcopy -j .text -j .data -O ihex build/native-debug/src/firmware/cpu/cpu build/native-debug/src/firmware/cpu/cpu_rom.hex

Создание образа EEPROM:
avr-objcopy -j .eeprom --change-section-lma .eeprom=0 -O ihex build/native-debug/src/firmware/cpu/cpu build/native-debug/src/firmware/cpu/cpu_eeprom.hex

Прошивка микроконтроллера:
avrdude -p atmega328p -c avrisp2 -P /dev/avrisp -U flash:w:build/native-debug/src/firmware/cpu/cpu_rom.hex:i


P.S. Уже созрели первые помидоры, и на вкус они оказались не очень. Видимо, в питании им что-то не понравилось. Наверное, придётся менять культуру.

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

Какой язык вы используете для программирования микроконтроллеров?

  • 29,4%Программирую только Ардуино124
  • 6,2%Только ассемблер26
  • 40,8%C (с возможными ассемблерными вставками)172
  • 12,8%C++ (с возможными ассемблерными вставками)54
  • 5,9%C++11 и выше (с возможными ассемблерными вставками)25
  • 5,0%Другое21

Средняя зарплата в IT

110 500 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 6 970 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 104

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

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

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

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

      Система как-то реагирует на аварии — протечки?

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

        Классически это так:
        image
        ВУ — верхний уровень.
        НУ — нижний уровень.
          0
          Ну вот пример того, что я видел на eBay. Насколько я понимаю, аналоговый выход пропорционален уровню погружения.

          А доли Ома мерить не проблема, если абсолютная величина порядка долей Ома (то есть 0.1-0.5, а не 100.1-100.5). Например, включив измеряемое сопротивление в плечо моста с фиксированными сопротивлениями того же порядка, и в диагональ какой-нибудь операционный усилитель.

            0
            Да, судя описанию, изменение сопротивления между проводниками усиливается транзистором.

            Может быть, если поделить измеряемые значения на несколько диапазонов, для компенсации проводимости, то можно получить аналог «железного» указателя уровня.
            image
              +2
              Сопротивление измерять бессмысленно — электроды могут вносить поправку из-за электрохимического потенциала… да и растворятся будут понемногу, если не позолоченные или из инертного материала.
              Лучший способ измерения уровня — емкостной. Сенсоров только много надо будет. Но их преимущество — нет непосредственного контакта с водой.
                0
                Я вот тоже не до конца понимаю, на каком принципе работает этот датчик. Возможно, там как раз-таки измеряется не сопротивление, а ЭДС, возникающая между электродами, погруженными в электролит.

                Вариант с ёмкостным датчиком я тоже рассматривал, но отбросил из-за возможной капризности, хотя уже начинаю жалеть, что не поэкспериметировал с этим. Ёмкость микроконтроллером померить несложно, например, включив ёмкость в задающий контур генератора, и замеряя его выходную частоту.
                  0
                  Это фигня. Так много не намеряешь. Лучше подавать фиксированную частоту на емкость сенсора через резистор. Он образует элементарный RC фильтр низких частот, когда емкость сенсора растёт меняется частота среза фильтра и резко падает амплитуда. Но и этот способ не идеален…
                  Можно просто заряжать емкость большим сопротивлением 100К-1М и измерять время необходимое на заряд емкости сенсора с нуля. Так работают практически ВСЕ реализации емкостных сенсоров и тач-панелей. Итого — нужен только один резистор на сенсор… иногда даже длительность заряда не нужно измерять — достаточно прочитать значение на порту сразу после переключения пина из состояния «выход, 0» в режим «вход» — этого достаточно чтобы зарегистрировать емкость сенсора в 10пФ, если больше — отложить операцию чтения парочкой NOP-ов.
                    0
                    Можно просто заряжать емкость большим сопротивлением 100К-1М и измерять время необходимое на заряд емкости сенсора с нуля.

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

                      image
                        0
                        Если быть точным, то дешевле всего разобрать использованные батарейки — угольный стержень бесплатно.
                          0
                          Оно то бесплатно, но и столь же бесполезно. Сколько работы надо будет проделать, чтобы из угольного стерженька сделать электрод для измерителя уровня. А главное как их срастить с медным проводником, чтобы в последствии контакт с жидкостью имел только графит иначе всё это зря.
                            0
                            У графитного стрежня припрессован металлический колпачок "+" — к нему припаять, сам колпачок с проводом и местом пайки залить, например, эпоксидкой.
                          0
                          Ты смотри, не поленились даже фиксирующие винты позолотить…
                        +1
                        Самая жепь это электролиз раствора. А идет он очень мощно даже при коротких импульсах. Очень. Там pH улетает и что только ни начинает плавать в растворе.
                        0
                        А почему бы не использовать пластиковую трубку с герконами и магнитом на поплавке? Просто, дешево и сердито.
                          0
                          Соли… всё обложат и в один прекрасный момент поплавок просто застрянет.
                            0
                            А электроды соли не не обложат?
                              0
                              Они не помешают работе электродов.
                            0
                            Ну не так уж просто — герконов надо много, потому что шкала трбуется с достаточно большим разрешением, чтобы как можно более точно обнаруживать, когда уровень воды начал понижаться или повышаться (да и вообще, хочется точно знать уровень). Каким-то образом всю эту кучу надо подключить к микроконтроллеру, желательно не тридцатью проводами. (Как вариант, можно сделать цепочку резисторов, и чтобы геркон или пара электродов, как предлагали выше, замыкали часть цепочки, и в итоге её общее сопротивление было пропорционально уровню жидкости. И дальше уже на АЦП мерить его.). Но всё-таки просто — это пара изолированных электродов, как в случае с ёмкостным датчиком (если, конечно, эта идея рабочая и такой датчик может выдавать надёжные показания).
                              0
                              чтобы отслеживать изменение уровня, а возможно и сам уровень, как вариант можно поставить датчик давления под сам бак, чтобы измерять изменение веса, либо положить его на дно бака :)
                                0
                                Разница уровней в 20см по жидкости… для измерения надо будет сильно бороться с помехами и влиянием температуры на датчик, да и дорогое вобщем-то выйдет решение и недостаточно точное.
                                  0
                                  У меня была такая идея, когда хотел делать с аквариумом. Но не под аквариум, а под такую мензурку рядом с ним, которая сообщается с ним через трубку по принципу сифона. Мне тогда казалось это простым и надёжным решением.
                      0
                      .
                        0
                        Там жёлтые светодиоды? Для растений они слабо подходят.
                        Смотрите тут, раздел светодиоды:
                        https://ru.wikipedia.org/wiki/Искусственное_освещение_растений
                          +2
                          Да, я об этом тоже узнал, когда уже заказал «тёплые» белые (ну как же, они ведь почти как солнце :) ). Только потом включилась логика — если листья зелёные, значит зелёный цвет они отражают, а поглощают красный и синий. Но на самом деле это вопрос эффективности. Эти жёлтые (точнее тёплые белые, фотографии не очень хорошо получились) тоже подходят, просто больше энергии тратится впустую, отражаясь от листьев.
                            0
                            Проблема искусственного освещения растений несколько глубже. Была статья примерно год назад по поводу системы искусственного выращивания, там прозвучали утверждения что от соотношения цветовых составляющих изменяются разные аспекты роста растения — насколько быстро оно наберёт массу, насколько интенсивный будет вкус плодов и т.д.
                            Хоть красно-синее излучение и приводит к интенсивному росту растения, но плоды становятся безвкусными и водянистыми. Это хорошо для не плодоносящих: цветы, марихуана, конопля…
                              0
                              Насколько я понимаю, эта проблема стоит oстро, если выращиваемые растения получают только искусственное освещение, например, такие фермы. У меня же основное освещение — естесственное от окна, подсветка только для продливания светового дня, задумывалась для зимы. Не знаю, насколько вообще эта концепция рабочая, и даст ли какой-то толк, но мне кажется, что в такой ситуации спектральный состав освещения гораздо менее критичен (главное, конечно, чтоб не оказалось, что оно совсем бесполезно).
                              0
                              «Теплые» белые светодиоды тоже относительно неплохо — у них максимум приходится на красную часть спектра, поменьше не сине-фиолетовую и меньше всего как раз на зеленую.

                              Тут (слева внизу график) спектр плохеньких китайских светодиодов «теплого» класса:
                              lamptest.ru/images/graph/new-energy-led-corn-bulb-12w.png
                              Тут получше качеством тоже теплые (3000К)
                              lamptest.ru/images/graph/camelion-led10-a60-830-e27.png

                              Как ни странно чисто с точки зрения эффективности конвертации энергии — солнечный свет самый плохой вариант (т.к. в нем максимум приходится как раз на не усваиваемый растениями зеленый участок спектра). Он рулит только за счет своей огромной интенсивности — даже в облачный день интенсивность естественного солнечного света как минимум на порядок выше яркого искусственного.
                            0
                            Для измерения уровня жидкости еще вот такой датчик есть, хотя он довольно дорогой — www.adafruit.com/products/463
                              0
                              del
                                0
                                Ну да, у меня в принципе все компоненты в сумме где-то на столько и наберутся, может даже немного поменьше.
                                0
                                OLED дисплей — это красиво, но недолговечно. За год яркость постоянно светящихся пикселей заметно снизится.
                                  0
                                  У меня тоже были такие подозрения, поэтому большую часть времени дисплей отключен. Он включается, если тронуть валкодер, и горит, пока с момента последних действий не пройдёт какой-то таймаут (минут 5 вроде поставлено). Добавлю в текст этот момент.
                                  0
                                  Дешевый и надежный датчик уровня жидкости — контактный с дискретными показаниями. Просто делается набор электродов разной длины на плате и выводится контактными площадками на разных уровнях. В зависимости от того, где коротит — там и вода. Не надо париться с электропроводностью.

                                  Еще вариант — емкостный, но он более капризный.
                                    0
                                    А если просто сделать поплавок и соединить его вторым герметичным концом с реостатом? Он будет вверх вниз двигаться и менять сопротивление. Такую штуку можно легко собрать и достаточно точно откалибровать.
                                      0
                                      Это были первые мысли. Но есть вопросы к надёжности такого решения — любая механика увеличивает сложность и уменьшает надёжность (при этом оно может быть ещё и достаточно громоздким). Ещё из тех же соображений хочется бесконтактный вариант — реостат будет изнашиваться.
                                        0
                                        В таком случае лучше на датчиках холла. Но поплавок в любом случае и дороже и менее надежен (зарастает солями жесткости, например).
                                      0
                                      Электроника для гидропоники это круто, но меня больше интересует какие удобрения надо использовать для питательного раствора и как его приготовить. В интернете куча разных советов по этому поводу начиная от готовых и заканчивая самостоятельным приготовлением из разных химреактивов.
                                        0
                                        Книг по этому поводу издано много, например, Вахмистров Д. Растения без почвы, 1960 года. Меня в этом вопросе интересует другой вопрос — как не получить овощи с передозировкой нитратов и т.п…
                                        Кроме того, содержание нитратов в овощах может резко увеличиться при неправильном применении азотистых удобрений (не только минеральных, но и органических). Например, при внесении их незадолго до уборки
                                          +1
                                          Я на самом деле к этому вопросу безответственно подошёл. Пока росток был маленький подлил в воду чуть-чуть обычного жидкого удобрения для помидоров. Потом, когда вода начала цвести, решил, что ничего больше добавлять не буду, только марганцовкой изредка травил живность. В итоге весь этот куст вырос практически на чистой воде и марганцовке. Ну собственно, и на вкус он получился достаточно противный. В общем агроном из меня не очень. Ну и всё-таки это первый экземляр продукта — больше фана было наблюдать, как работает электроника и изредка исправлять огрехи в прошивке, когда что-то шло не так. Когда буду сажать следующего клиента, уделю вопросу правильного питания больше внимания.
                                            0
                                            Кстати по поводу живности в воде geektimes.ru/post/255486
                                            После двух месяцев роста клубники возникли проблемы. Я заметил, что её листья становились скорее белыми, чем зелёными. Перелопатив массу информации, я выяснил, что возможная причина заключается в нехватке питательных элементов в воде, необходимых для формирования правильного роста. Какую-либо химию добавлять не хотел. Поэтому принял простое решение – увеличить количество рыб, так как больше рыб выработают больше продуктов жизнедеятельности.

                                            По истечении месяца положительного результата не наблюдалось, листья по-прежнему оставались бледно-зелёные. Но зато начала обильно плодоносить клубника.
                                              0
                                              Да, нечто подобное я и хотел изначально сделать. В этой статье проскочила хорошая мысль — делать не дома, а на работе :).
                                              0
                                              Марганец вызовет отравление. Вначале на молодых листьях.
                                              Избыток марганца, в отличие от его недостатка проявляется чаще на кислых почвах. В результате избытка марганца в клетках растений уменьшается содержание хлорофилла, поэтому при этом симптомы будут такие же, как и при недостатке магния, т.е. начинается мезжилковый хлороз, в первую очередь со старых листьев, появляются бурые некротичные пятна. Листья сморщиваются и облетают.
                                              0
                                              Нитратами перекормить почти нереально. Растение подохнет от передоза. Вообще выход за рамки это всегда стресс и замедление роста.
                                            0
                                            а есть ли веб ресурсы(рус/англ), специализирующиеся на аква-/гидропонике «домашнего» масштаба?
                                              0
                                              На форумах «дачников» такие темы есть, только это сплошное имхо на любые темы, иногда с переходом на личности. Например здесь много фото урожаев можно найти.
                                                0
                                                По гидропонике много, только они определённой направленности, вот например – olkpeace.ru
                                                  0
                                                  http://forum.ponics.ru/. Один из лучших. Крайне рекомендую ресурсы по выращиванию альтернативных «трав». Принципы те же, но там люди упоротые в плане выхода конечного продукта на стоимость электричества/удобрений и т.п. Народ цеые заросли в холодильнике и прочем выращивает.
                                                  0
                                                  Кстати, чтобы не мучится со шрифтами сделал программу для их редактирования и экспорта в код. от 5x7 до 16x16.
                                                    0
                                                    Я брал за основу сгенерированный шрифт. Но всё-таки потом пришлось вручную подправлять много погрешностей, осматривая с лупой каждый символ.
                                                      0
                                                      вот собственно для этого и сделал. Можно даже весь шрифт окинуть взглядом 1:1 и увеличенное изображение символа, и основное поле с «квадратами размером с кулак».
                                                    0
                                                    А собственно помидорки то как? Растут? Вкусные?
                                                      0
                                                      У меня вкусные росли. Томат Жёлтый малыш. Но у меня смесь кокоса и агроперлита. Капельная система.
                                                        0


                                                          0
                                                          А чем подсвечивали? Какие удобрения давали?
                                                            0
                                                            Без подсветки на северной стороне. Света им маловато, но прожектор был занят салатами. В Краснодаре с солнцем неплохо. В основном кальциевая селитра и Буйские удобрения «Овощные» и «Плодовые». В одних больше азота, в других калия. Плюс микроэлементы в хороших пропорциях.
                                                              +1
                                                              Самый ступор был, когда у меня грибы в кокосе выросли.
                                                              image
                                                              А ещё перцы отлично растут в кокосе.

                                                              image
                                                                0
                                                                Класс… с грибами кстати можно было и таймлапс снять… они относительно быстро растут.
                                                                  0
                                                                  Они там надолго поселились) постоянно лезут. Мицелий же жив
                                                          0
                                                          Ну я там написал в конце, что не очень получились. Как пишут в комментариях — подбор раствора целая наука, скорей всего из-за этого. Я же на первом поселенце вообще эту тему не изучал и на авось этот вопрос оставил, больше уделяя внимание на наблюдение за работой электроники и периодическое исправление недочётов в прошивке. Следующему поколению уделю больше внимания в вопросах питания.
                                                          0
                                                          Спасибо за статью, очень интересно.
                                                          Очень не хватает схемы установки гидропоники. На словах непонятно как работает всё в целом, и понятно только по отдельности.

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


                                                            Про ассемблер — это, чтобы не отпугнуть от ответа тех, кто делает ассемблерные вставки, чтобы не расценили, что ответ с их преимущественным языком из-за этого не подходит. Без них ведь иногда никак. К примеру, в этом проекте для них нашлось место в планировщике задач:

                                                            /* Atomic sleeping. */
                                                            __asm__ volatile ("sei; sleep");
                                                            

                                                            Согласно даташиту на AVR атомарное засыпание следует делать таким образом. Специально для этих целей инструкция SEI имеет особенность — следующая за ней инструкция выполнится до того, как обработается первое прерывание в очереди. Это для того, чтобы не получилось race condition — прерывание может придти после sei, но до sleep, в результате процессор заснёт, вместо того, чтобы выполнить действия для soft interrupt в главном цикле.
                                                              0
                                                              спасибо, вроде яснее стало, это видео куда удачнее первого.

                                                              а насчёт ассемблера:
                                                              Я пишу под МК на базе кортексов М0… М4. Там все такие особенности уже реализованы в библиотеке CMSIS от арм консорциума и прочих подобных.
                                                              За 5 лет ни разу не приходилось использовать асм ни в одном проекте что для кортексов писал все эти года. Потому что либо всё сделано уже до меня в либах и очень хорошо сделано. Обычно производительности не хватает не 5-10%, а не хватает в 5-10 раз, и тут надо либо менять МК либо ставить ПЛИС либо пересматривать хотелки и ТЗ в сторону упрощения или вообще менять идею целиком. А компилятору я доверяю больше чем моим умениям вылизывать инструкции: местами может и выйдет быстро если на асме написать, но в целом современные компиляторы существенно лучше.
                                                              Поэтому такое сочетание С++11 и упора на асм вызвало удивление: обычно любят что то одно.
                                                                0
                                                                Добавлю видео в текст.

                                                                Я поэтому и привёл пример, когда без этого никак. Речь не об оптимизациях, а именно о специфических инструкциях, для которых нет нужной обёртки в стандартной библиотеке. Да и подобные библиотеки тоже кто-то пишет.
                                                            0
                                                            Если уж C++11, то зачем макросы, можно
                                                            static constexpr auto &&
                                                            
                                                            .
                                                            И удостовериться, что во всех translation unit'ах используется один и тот же экземпляр.
                                                              0
                                                              Не совсем уловил мысль. Можете привести более полный пример?
                                                                0
                                                                Пардон, static вам, видимо, не подходит из-за необходимости добавить аттрибут PROGMEM.
                                                                Я предлагал завести тип со статическими полями, объявленными примерно так:
                                                                struct Strings {
                                                                    static constexpr auto&& Prompt /*PROGMEM*/ = "Are you sure you want to format SD-card?";
                                                                };
                                                                

                                                                но видимо
                                                                static
                                                                и
                                                                PROGMEM
                                                                не сочетаемы, так как по сути оба являются storage спецификаторами.
                                                                  0
                                                                  PROGMEM — это атрибут GCC, раскрывается в "__attribute__((__progmem__))". Со static он не конфликтует. Вот это компилируется.
                                                                  struct Strings {
                                                                      static constexpr auto&& Prompt PROGMEM = "Are you sure you want to format SD-card?";
                                                                  };
                                                                  

                                                                  Если при линковке для всех compilation unit каждая строка в итоге будет выделена только в одном месте, то это да, лучший вариант.
                                                              0
                                                              del
                                                                0
                                                                Вопрос по ЛУТ.
                                                                Со стороны меди у вас плохо перенесена краска, со стороны шелкографии — наоборот. Какой бумагой пользуетесь?
                                                                Я со стороны меди использую фотомумагу для струйников Lomond, результат отличный.
                                                                Со стороны шелкографии обычно кальку, потому что её не надо отмывать — результат так себе, размазано немного.
                                                                Грею просто утюгом через газету, без конфорки.
                                                                  0
                                                                  В этом проекте попробовал какую-то китайскую с eBay специально для ЛУТа. Думаю, что не очень хороший результат связан с неправильной температурой для моего тонера или бумаги, неравномерным проглаживанием или ещё какими-нибудь тонкостями процесса. Я платы довольно редко делаю, ещё не отработал эту технологию. В принципе уже привык ретушировать маркером после перевода.
                                                                    0
                                                                    Спасибо. Как она отмывается? Прилипает или легко отходит?
                                                                      0
                                                                      Отходит сама, но, как видите, частично с краской в моём случае.
                                                                  0
                                                                  А насчёт RTC — внутрениий таймер по моему опыту (небольшому) использовать получается очень криво. Из-за обилия других прерываний он очень сильно уходит. Поэтому думаю что внешние RTC очень даж верное решение.
                                                                    0
                                                                    Ну по моему опыту, когда таймеров хватает, вполне сносно работает (если не нужно большой точности, но до показателя пару секунд за несколько месяцев, ему, конечно, далеко). Другие прерывания влиять не должны — прерывание от таймера (я использовал 16-битный таймер с верхним значением, удобным для ровного деления) довольно редкие, пропустить их сложно (они ведь в очередь аппартно ставятся), если код написан правильно. Как-то так для 20МГц:

                                                                    /** TOP value for clock counter. */
                                                                    #define CLOCK_MAX_VALUE     62499
                                                                    
                                                                    /** Called on each second. */
                                                                    static inline void
                                                                    OnClockSecond()
                                                                    {
                                                                        g_clockSec++;
                                                                        if (g_clockSec < 60) {
                                                                            return;
                                                                        }
                                                                        g_clockSec = 0;
                                                                        g_clockMin++;
                                                                        if (g_clockMin >= 60) {
                                                                            g_clockMin = 0;
                                                                            g_clockHour++;
                                                                            if (g_clockHour >= 24) {
                                                                                g_clockHour = 0;
                                                                                g_clockDow++;
                                                                                if (g_clockDow >= 7) {
                                                                                    g_clockDow = 0;
                                                                                }
                                                                            }
                                                                        }
                                                                        OnClockMinute();
                                                                    }
                                                                    
                                                                    /** Clock ticks occur with TICK_FREQ frequency. */
                                                                    static inline void
                                                                    OnClockTick()
                                                                    {
                                                                        static u8 divisor = TICK_FREQ;
                                                                    
                                                                        g_clockTicks++;
                                                                        divisor--;
                                                                        if (divisor == 0) {
                                                                            divisor = TICK_FREQ;
                                                                            OnClockSecond();
                                                                        }
                                                                        ProcessTaskQueue();
                                                                    }
                                                                    
                                                                    ISR(TIM1_COMPA_vect)
                                                                    {
                                                                        OnClockTick();
                                                                    }
                                                                    
                                                                    static inline void
                                                                    ClockInit()
                                                                    {
                                                                        /* CLK / 8
                                                                         * WGM = 0100 (CTC)
                                                                         */
                                                                        TCCR1B = _BV(CS11) | _BV(WGM12);
                                                                        OCR1A = CLOCK_MAX_VALUE;
                                                                        TIMSK1 = _BV(OCIE1A);
                                                                    }
                                                                    

                                                                    Но в целом да, если требуется устройство для длительной автономной работы с точным временем, то только специализированные модули.
                                                                      0
                                                                      /** Clock ticks frequency. */
                                                                      #define TICK_FREQ       40
                                                                      

                                                                      20000000/8/62500
                                                                        0
                                                                        Если прерываний много и МК находится в другом прерывании (а там обычно ставят запрет прерываний) то прерывания от таймера не будет. Я вот это имел ввиду. То есть будут пропуски отсчёта времени.
                                                                        Если же прерывание одно, то и тут будут сдвиги — из-за многотактовых команд. Т.е. сначала довыполняется текущая команда, потом уже прерывание, а не сразу.
                                                                          0
                                                                          Не сразу заметил — про очередь прерываний. Они точно ставятся в очередь?
                                                                            0
                                                                            Я не прав, вот какая задержка будет:

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

                                                                            Точно так же уже запущенный обработчик INT1 будет прерван более приоритетным INT0 лишь в том случае, если сам установит Global Interrupt Enable. Приоритет важен лишь в момент выбора одного прерывания из нескольких одновременных запросов.

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

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

                                                                              Ещё стоит помнить, что обработчики прерываний обычно делают по возможности очень короткими, использую подход т.н. software interrupt — непосредственно обработчик только выставляет флаг, по которому уже делаются какие-то действия в главном цикле, вне контекста прерывания.
                                                                                0
                                                                                Всё верно. Там ещё есть тонкость с асинхронным таймером, у меня был он на отдельном кварце.
                                                                                  0
                                                                                  Кстати да, если есть возможность поставить часовой кварц для таймера, то с часами вообще проблем быть не должно.
                                                                                    0
                                                                                    Да вот нет, чтобы точно работало нужно засыпать постоянно камнем. Часовые прерывания там тактуются отдельно по частоте часового кварца и пока на них переключится камень тоже проходит время. Всеми тонкостями я ещё не владею.
                                                                                      0
                                                                                      На точность отсчёта времени это никак не влияет. Таймер как тепловоз — идёт себе независимо ни от чего, досчитает до переполнения и выставит признак прерывания — когда его контроллер обработает его не волнует абсолютно и отсчет при этом НЕ останавливается, следующий «флажок» будет выставлен в строго назначенное время, незвисимо от того насколько быстро и своевременно отработал обработчик прерывания, да хоть прямо перед следующим прерыванием…
                                                                                      Очень важны только две вещи: 1) не трогать ни коим образом работающий таймер, 2) обработать прерывание ДО прихода следующего.
                                                                                        0
                                                                                        Не согласен. Вы можете пропустить событие прерывания если у вас их несколько (от разных источников). Флажок-то будет выставлен, только он может быть несколько раз выставлен.
                                                                                          0
                                                                                          Ну так и я, и предыдущий комментатор писали:
                                                                                          2) обработать прерывание ДО прихода следующего.

                                                                                          В моём примере кода периодичность прерывания 40Гц, времени на обработку более, чем достаточно, если МК не перегружен, а про это я тоже говорил, перегруженный МК — неправильный дизайн — либо код, либо не хватает вычислительной мощности для выполняемой задачи и надо использовать более производительный МК.
                                                                                            0
                                                                                            Следующего того же прерывания или вообще любого? Я понял что такого же.
                                                                                            ОК.
                                                                                              0
                                                                                              Да, до прихода следующего такого же, у каждого прерывания есть свой специфичный аппаратный флаг. Другие прерывания не затрут ничего, связанного с данным.
                                                                                                0
                                                                                                До при хода любого прерывания. Иначе отсчёт времени будет нарушен.
                                                                                                  0
                                                                                                  И как он будет нарушен?
                                                                                                  Единственная ситуация когда прерывание не будет обработано до возникновения следующего это постоянно срабатывающее прерывание более высокого приоритета до того как оно будет обработано. Например, прерывание по уровню на порту, и если этот уровень держать постоянно, то флаг прерывания от порта будет висеть постоянно, контроллер не выйдет с обработки этого прерывания пока не уберёшь уровень по которому оно срабатывает. Но допущения таких ситуаций — это проблемы с дизайном…
                                                                                                    0
                                                                                                    Обработка часового прерывания будет не в момент отсчёта времени, а позже. Статистически скажем за час будет одинаковое число обработок, только сам момент срабатывания будет гулять.
                                                                                                    Это если предположить что нет обработчика какого-то любого прерывания, отрабатывающего медленнее, чем частота часового таймера. В этом случае будут пропуски.
                                                                                                      0
                                                                                                      Статистически скажем за час будет одинаковое число обработок, только сам момент срабатывания будет гулять.

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

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

                                                                                                      Это и есть ошибка дизайна. Опять же, как сказано выше, обработчики должны быть как можно более короткими, длинные операции надо выносить за пределы контекста прерываний, например, устанвливая в обработчике только флаг, и в главном цикле проверяя его и выполняя работу (подразумевая, что обработка может длиться несколько прерываний и быть к этому готовым).
                                                                                                        0
                                                                                                        Ясно. Спасибо за ответы!
                                                                    • НЛО прилетело и опубликовало эту надпись здесь
                                                                        +1
                                                                        Сейчас у меня раз в 6 часов стоит. Подобрал на глазок, через прозрачную стенку видно, насколько влажно внутри. По ощущениям сейчас можно и меньше. Летом, когда стояло на солнце весь день, было раз в 3 часа.
                                                                        • НЛО прилетело и опубликовало эту надпись здесь
                                                                            0
                                                                            Ага, понятно какой «цветочек» :D
                                                                              0
                                                                              Вот у меня коллеги тоже не верили, что будет помидор. А он внезапно оказался помидором.
                                                                                0
                                                                                =)
                                                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                                                • НЛО прилетело и опубликовало эту надпись здесь

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

                                                                          Самое читаемое