Pull to refresh

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

Reading time18 min
Views54K
Проект не содержит Ардуино


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

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

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

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

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


Мозг устройства — микроконтроллер 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. Уже созрели первые помидоры, и на вкус они оказались не очень. Видимо, в питании им что-то не понравилось. Наверное, придётся менять культуру.
Only registered users can participate in poll. Log in, please.
Какой язык вы используете для программирования микроконтроллеров?
28.77% Программирую только Ардуино126
6.16% Только ассемблер27
40.64% C (с возможными ассемблерными вставками)178
13.01% C++ (с возможными ассемблерными вставками)57
6.62% C++11 и выше (с возможными ассемблерными вставками)29
4.79% Другое21
438 users voted. 187 users abstained.
Tags:
Hubs:
+48
Comments104

Articles

Change theme settings