Как мы перестали бояться Огра и начали делать на нем игру

Как совершать ошибки. 2011г

Как следует из названия, в далеком 2011 году мы совершили третью ошибку, выбрав в качестве основы для игрового движка Ogre3D. Третью, потому что первой ошибкой было решение делать игру, а второй — делать ее на своем движке. К счастью, это были те самые ошибки, с которых начинается увлекательная история. Это приключение, в котором мы прошли почти весь путь развития игровых движков, как зародыш проходит все этапы эволюции.
Конечно же, как и все начинающие разработчики, мы слабо представляли себе, что и зачем мы собираемся делать. Нами двигало желание рассказать свою историю, создать свой вымышленный мир, свою вселенную, и на волне популярности ММО, естественным позывом было сделать свою ММО с блекджеком и всем причитающимся. Позыв случился еще в 2010, а к 2011 была готова первая версия диздока. Земля же была безвидна и пуста, и тьма над бездною, и Дух Фоллаута витал над нами.
Мы шли путем проб и ошибок, собирая по пути все косяки и грабли. Как и большинство проектов, мы начали с самого простого. В плане графики (а я буду рассказывать только о графической части) первая версия движка позволяла использовать только диффузную карту и стенсильные тени.

image

2011г. Один из первых скриншотов

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

К осени 2012 года в плане графики мы доросли до использования карт нормалей и спекуляров. Влияние DOOM 3 было сильно на неокрепшие умы начинающих разработчиков.

2012г. До DOOM 3 как до Марса.

Как выбрать между дудочкой и кувшинчиком. 2013г.

Зимой 2013 года команда приросла красивым трехмерщиком и обаятельным программистом графики. Фантазии ведущего художника нашли точку опоры, и движок стал прирастать графическими нововведениями. Появились текстура глянцевитости (она же карта степенного коэффициента спекуляра, она же glossiness, она же shininess), каскадные текстурные тени, DoF(эффект глубины резкости), RIM-освещение и куча глюков. В этот период стали особенно явно всплывать проблемы коммуникаций разных специалистов. Одни и те же вещи для разработчиков с разным бэкграундом назывались совершенно по-разному, и требовали многократного проговаривания.
Все чаще стали затеваться жаркие баталии о траектории развитии движка. В условиях ограниченных бюджета и времени приходилось выбирать между программированием геймплейной части и визуальной. Так, RIM появился как компромисс между желанием художника видеть более явный метал, желанием трехмерщика иметь для этого отражения и текущими возможностями движка. Все более остро стал вставать вопрос о переходе на готовый движок: Unity3D становился все более функциональным и популярным, стали появляться слухи о человеческих схемах лицензирования UDK.

Начало 2013г. Картинка стала чуть веселей, но ненамного.

Конец 2013г. Картинка стала еще веселей.

Как нарваться на неприятности. 2013г.

Осенью 2013г. мы в первый раз вышли на кикстартер. Свидетельства этому печальному опыту мелькали даже на Хабре. Кампанию мы прикрыли в течение первой же недели, поскольку стало очевидно, что «не взлетит». ММО к этому моменту начали раздражать игроманов, очередной «клон ВоВ» (коим игра ни в коей мере не планировалась, но убедить в этом геймеров не удавалось) никого не интересовал. В качестве работы над ошибками было решено, что мы отныне делаем РПГ синглу с кооперативным прохождением.

Конец 2013г. Скриншот из презентационной сцены..

Как обрести свободу. 2014г.

Фантазия ведущего художника требовала больших и сложных пространств. Реализация этих фантазий и ведущий трехмерщик требовали возможности оперировать не пятью источниками света, а гораздо большим их количеством.
Ограничение в 5 (на самом деле 8, но фпс проседал уже на пятом) источников света было обусловлено применением forward render (прямого рендера)
Прямой рендер — стандартный способ рендера, который используют большинство движков. Каждый объект, полученный видеокартой проходит полный путь рендеринга. При этом вертексный и пиксельный шейдеры считаются для каждого источника света отдельно даже для тех пикселей, которые могут впоследствии перекрыться другими. Каждый дополнительный источник света создаёт дополнительную итерацию рассчётов по всей геометрии. И при восьми источниках света в сцене из 1 миллиона видимых треугольников рисуется уже около 9 миллионов треугольников. Это приводило к очень низкому FPS на сколько-нибудь сложных локациях…

Призрак Крузиса с его сотнями лампочек не давал спать по ночам. Было решено перейти на deferred render (отложенный рендер или отложенное освещение). При отложенном рендере формируется набор «конечных» изображений без расчета теней: изображение цвета, изображение глубины и изображение нормалей. Зная положение источников света, глубину пикселей и нормали можно рассчитать затенение.
По сравнению с forward-rendering мы получили несколько плюшек:
1) Повышение фпс за счёт того, что геометрия рендерится только один раз
2) Возможность работать со множеством источников света: добавление нового источника света слабо сказывается на производительности.
3) Ускорение некоторых видов пост-обработки, более эффективная реализации мягких частиц и возможность добавления обработки screen-space reflection. Мягкие частицы, а также эффекты пост-обработки (DOF, SSR, SSAO) требуют карты глубины и нормалей. Прямой рендер не даёт эти карты, и их приходится рендерить отдельно. При отложенном освещении эти карты нам подаются на блюдечке с голубой каёмочкой.

Недостатки:
1) Полупрозрачность. Полупрозрачные объекты нельзя рисовать в отложенном освещении, т.к. потребуется, чтобы один пиксел каждой текстуры (нормаль, диффуз и т.д.) содержал информацию о нескольких перекрывающихся объектах. Есть множество различных способов решения проблемы, но чаще всего используется один — все полупрозрачные объекты рендерятся отдельно с помощью прямого рендера.
2) Алиасинг. При отложенном освещении fsaa отключается и все треугольники рисуются с явно выраженным алиасингом. Для решения этой проблемы используют различные методы, например FXAA
3) Повышенное требование к пропускной способности памяти видеокарты.

Мы рассматривали три варианта реализации отложенного освещения:

Вариант А:


Расчет освещения делится на два этапа:
1. На первом этапе рисуется вся непрозрачная геометрия в 4 текстуры. (диффуз, спекуляр, глянцевитость, нормаль, карта глубины и карта свечения)
-диффуз, спекуляр, глянцевитость берутся напрямую из текстур геометрии,
-нормали добываются из карты нормалей натянутых на геометрию и преобразованных в координаты пространства камеры.
-карта свечения получается путем сложения карт самосвечения, RIM освещения, амбиентного диффузного освещения +слабой подсветки со стороны камеры.
2. Используя карты нормалей, глубины, диффуза, спекуляра и глянцевитости рассчитывается диффузное и спекулярное свечение каждой точки для каждого источника света и инкрементно суммируется с картой свечения. В этом же пассе проверяется попадание каждой точки экрана в теневые карты.

Это стандартный вариант отложенного освещения, реализуемый в большинстве игр.

Вариант Б:


Рассчет освещения делится на 3 этапа:
1. На первом вся непрозрачная геометрия рендерится в 1 текстуру (рендерятся карты нормалей, глубины и глянцевитости)
2. По картам нормалей, глубины и глянцевитости рендерятся две карты: диффузного и спекулярного освещения для каждого источника света.
3. На третьем этапе рендерится финальное изображение: рисуется вся непрозрачная геометрия ещё раз, но с диффузом и спекуляром и вычисляется освещенность каждой точки путем умножения диффузного освещения на диффузную карту+произведения спекулярного освещения на спекулярную карту+рим-освещение+карту самосвечения+свет от камеры.

Преимущества этого метода:
1) Меньше требований к пропускной способности видеопамяти видеокарты.
2) Меньше вычислительных операций на каждый источник света, т.к. часть операций переходит из второго этапа в третий.
3) Третий этап уже может редерится со включенным fsaa, что увеличивает качество картинки.

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

Вариант В:

Рассчет освещения делится на 3 этапа:
1. На первом этапе рисуется вся непрозрачная геометрия в 4 текстуры (диффуз, спекуляр, глянцевитость, нормаль, карта глубины и карта свечения (как сумма от сложения карт самосвечения, RIM освещения, амбиентного диффузного освещения +слабой подсветки со стороны камеры)).
2. По картам нормалей, глубины и глянцевитости рендерятся две карты: диффузного и спекулярного освещения для каждого источника света.
3. На третьем этапе рендерится финальное изображение: вычисляется освещенность каждой точки путем умножения диффузного освещения на диффузную карту+произведения спекулярного освещения на спекулярную карту карту самосвечения…

Этот вариант — смесь первых двух вариантов. По сравнению с вариантом A мы получаем выигрыш в скорости за счет особенности варианта Б: вместо четырёх выборок из текстуры идёт только одна.

Сейчас у нас реализован первый вариант отложенного совещения. Все каналы которые используются для отрисовки финальной картинки можно выводить в отладочном режиме.

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

SSR (Screen Space Reflection) — это алогоритм создания в сцене реалистичных отражений с использованием данных, которые уже отрендерены на экране. Кратко: от камеры пускается луч до пересечения со сценой. Используя нормаль в точке пересечения, считается отражение. По этому лучу отражения происходит трассировка карты глубины пока до попадания в какую-либо геометрию, в качестве результата берётся светимость найденой точки и умножается на спекуляр отражающей точки и записывается в светимость отражающей точки.
Сейчас реализовано два алгоритма Screen Space Reflections:
1) Трассировка происходит в координатах камеры — медленная, но дающая правильную картинку.
2) Трассировка происходит в координатах текстуры — быстрая но даёт погрешность при малых углах.

2014г. Презентационная диорама с включенными отражениями.

2014г. Презентационная диорама с включенными отражениями.

Мы используем свой игровой и сетевой движок. Графический движок — Ogre3D, Физический — Bullet. Скриптинг: Lua, C# (Mono). Во время разработки пришлось сильно допиливать Ogre3D и отлаживать его связку с Blender… В планах связаться с разработчиками Огра и предложить включить свои доработки в следующие сборки Огра.

Как ожидать лучшего. 2014г.

В данный момент, т.е. прямо вот сейчас, идет наша вторая кампания по сбору средств на кикстартере.


Используемые языки программирования: C++, PHP, Lua, C#, Python, Java, Groovy, Cg, GLSL, HLSL.

ссылка на оригинал статьи http://habrahabr.ru/post/228139/

Документация Mojolicious: Потерянные Главы

Это продолжение серии статей о веб-фреймворке для Perl — Mojolicious: первая часть.

Асинхронность: синхронизируем с помощью Mojo::IOLoop::Delay

Mojo::IOLoop::Delay предоставляет механизм, обеспечивающий для асинхронно выполняющихся callback-ов:

  • описание последовательно выполняющихся операций без «лапши» callback-ов
  • передачу результатов из callback-а(ов) текущего шага на следующий
  • общие данные для callback-ов, объединённых в одну задачу
  • синхронизацию групп callback-ов
  • перехват и обработку исключений в callback-ах

Используемые термины:

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

Альтернатива Promises

Это альтернативный подход к проблеме, обычно решаемой с помощью Promise/Deferred или Future. Вот приблизительное сравнение со спецификацией Promises/A+

  • Вместо цепочки ->then(\&cb1)->then(\&cb2)->… используется один вызов  ->steps(\&cb1, \&cb2, …).
  • Вместо передачи обработчика ошибки вторым параметром в ->then() он  устанавливается через ->catch(). Следствие: на все шаги этой задачи  может быть только один обработчик ошибок.
  • Результат возвращается через ->pass(), но в отличие от ->resolve()  в большинстве случаев он вызывается неявно — асинхронной операции в  качестве callback передаётся результат вызова генератора анонимных  функций ->begin, и возвращённая им функция автоматически делает  ->pass(), передавая срез своих параметров (т.е. результата работы  асинхронной операции) на следующий шаг. Следствие: не нужно писать  для каждой асинхронной функции callback, который будет возвращённый ею  результат преобразовывать в ->resolve() и ->reject().
  • Ошибки возвращаются только через исключения, аналога ->reject() нет.
  • Есть дополнительный шаг выполняемый в самом конце ->on(finish=>\&cb),  на который также можно перейти из обработчика ошибок.
  • Есть поддержка групп асинхронных операций: если на текущем шаге  запустить несколько операций, то следующий шаг будет вызван когда все  они завершатся.
  • Есть хранилище пользовательский данных, доступное всем шагам текущей  задачи.

По этим отличиям виден типичный для Mojo подход: всё что можно упрощено и предоставлены удобные «ленивчики» для типичных задач.

Что осталось за кадром

Я не буду описывать работу ->wait, с ним всё просто и понятно из официальной документации.

Кроме того, есть синонимы/альтернативы:

Mojo::IOLoop->delay(@params) # это полный аналог более длинного: Mojo::IOLoop::Delay->new->steps(@params)

$delay->catch(\&cb) # это более удобный (т.к. возвращает $delay, а не \&cb, # что позволяет продолжить цепочку вызовов) аналог: $delay->on(error=>\&cb)

$delay→begin

Это ключевая функция, без неё использовать Mojo::IOLoop::Delay не получится. Каждый вызов ->begin увеличивает счётчик запущенных (обычно асинхронных) операций и возвращает ссылку на новую анонимную функцию. Эту возвращённую функцию необходимо однократно вызвать по завершению операции — она уменьшит счётчик запущенных операций и позволит передать результаты операции на следующий шаг (который будет запущен когда счётчик дойдёт до нуля).

Есть два способа использования ->begin: вручную и автоматически.

В первом варианте функция возвращённая ->begin запоминается во временной переменной и по завершению операции вызывается вручную:

my $delay = Mojo::IOLoop->delay; for my $i (1 .. 10) {     my $end = $delay->begin;     Mojo::IOLoop->timer($i => sub {         say 10 - $i;         $end->();     }); }

Во втором варианте функция возвращённая ->begin используется в качестве callback для операции:

my $delay = Mojo::IOLoop->delay; for my $i (1 .. 10) {     Mojo::IOLoop->timer($i => $delay->begin); }

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

$delay->step(sub{ say "all timers done" });

В данном примере есть проблема: во втором варианте не выполняется say 10 - $i т.к. таймер не передаёт никаких параметров своему callback, и мы не можем узнать значение $i в callback если только не заклозурим его как в первом варианте. Но даже если бы таймер передавал $i параметром в callback вам бы это всё-равно не сильно помогло, т.к. шанс выполнить все десять say 10 - $i мы бы получили только на следующем шаге, а он запустится только после завершения всех таймеров — т.е. пропадёт эффект обратного отсчёта, когда say выполнялся раз в секунду.

В таких, редких, ситуациях необходимо использовать первый «ручной» вариант работы с ->begin. Но во всех остальных намного лучше использовать второй вариант: это избавит от временной переменной, «лапши» callback-ов, и даст возможность использовать (точнее, перехватывать) исключения в callback-ах (исключение в обычном callback-е — не «шаге» — попадёт не в $delay->catch а в обработчик исключений event loop и, по умолчанию, будет проигнорировано).

Функции ->begin можно передать параметры, и на первый взгляд (в официальную документацию) они могут выглядеть не очень понятно. Суть в том, что когда функция возвращаемая ->begin используется не в ручном варианте (когда вы сами её вызываете и контролируете с какими параметрами она будет вызвана), а в качестве непосредственного callback для операции, то она будет вызвана с теми параметрами, с которыми её вызовет эта операция. И все эти параметры вы получите как результат этой операции в параметрах следующего шага.

Например, $ua->get($url,\&cb) передаёт в callback два параметра: ($ua, $tx), и если на одном шаге запустить выкачку 3-х url, то следующий шаг получит 6 параметров (каждый шаг получает первым обязательным параметром объект $delay, а зачем в этом примере используется ->begin(0) я скоро объясню):

Mojo::IOLoop->delay(     sub {         my ($delay) = @_;         $ua->get($url1, $delay->begin(0));         $ua->get($url2, $delay->begin(0));         $ua->get($url3, $delay->begin(0));     },     sub {         my ($delay, $ua1,$tx1, $ua2,$tx2, $ua3,$tx3) = @_;     }, );

При этом все три $ua полученные вторым шагом будут одинаковыми. Поскольку это типичная ситуация, ->begin даёт вам возможность контролировать, какие именно из переданных операцией параметров он должен передать на следующий шаг. Для этого он принимает два параметра: индекс первого параметра и их количество — чтобы передать на следующий шаг срез. По умолчанию ->begin работает как ->begin(1) — т.е. передаёт на следующий шаг все параметры переданные операцией кроме первого:

Mojo::IOLoop->delay(     sub {         my ($delay) = @_;         $ua->get($url1, $delay->begin);         $ua->get($url2, $delay->begin);         $ua->get($url3, $delay->begin);     },     sub {         my ($delay, $tx1, $tx2, $tx3) = @_;     }, );

$delay→data

В принципе с ->data всё банально: хеш, доступный всем шагам — альтернатива передаче данных с одного шага на другой через параметры.

Mojo::IOLoop->delay(     sub {         my ($delay) = @_;         $delay->data->{key} = 'value';         ...     },     sub {         my ($delay) = @_;         say $delay->data->{key};     }, );

Альтернативой является использование клозур, что выглядит более лениво, привычно и читабельно:

sub do_task {     my $key;     Mojo::IOLoop->delay(         sub {             $key = 'value';             ...         },         sub {             say $key;         },     ); }

Но здесь вас поджидает неприятный сюрприз. Клозуры живут пока кто-то на них ссылается. А по мере выполнения шагов Mojo удаляет их из памяти. Таким образом, когда будет выполнен последний шаг, ссылавшийся на заклозуренную переменную — она тоже будет удалена. Что приводит к неприятному эффекту, если эта переменная была, например, объектом Mojo::UserAgent:

sub do_task {     my $ua = Mojo::UserAgent->new->max_redirects(5);     Mojo::IOLoop->delay(         sub {             my ($delay) = @_;             $ua->get($url1, $delay->begin);             $ua->get($url2, $delay->begin);             $ua->get($url3, $delay->begin);         },         sub {             my ($delay, $tx1, $tx2, $tx3) = @_;             # все $tx будут с ошибкой "соединение разорвано"         },     ); }

Как только первый шаг запустит неблокирующие операции выкачки url, завершится, и будет удалён из памяти — вместе с ним будет удалена и переменная $ua, т.к. больше нет шагов, которые на неё ссылаются. А как только будет удалена $ua все открытые соединения, относящиеся к ней, будут разорваны и их callback-и будут вызваны с ошибкой в параметре $tx.

Один из вариантов решения этой проблемы — использовать ->data для гарантирования времени жизни клозур не меньше, чем время выполнения всей задачи:

sub do_task {     my $ua = Mojo::UserAgent->new->max_redirects(5);     Mojo::IOLoop->delay->data(ua=>$ua)->steps(         sub {             my ($delay) = @_;             $ua->get($url1, $delay->begin);             $ua->get($url2, $delay->begin);             $ua->get($url3, $delay->begin);         },         sub {             my ($delay, $tx1, $tx2, $tx3) = @_;             # все $tx будут с результатами         },     ); }

finish

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

  • Если используется обработчик исключений ->catch, и бывают не фатальные  ошибки, после которых всё-таки имеет смысл штатно завершить текущую  задачу выполнив последний шаг — обработчик исключений сможет передать  управление обработчику «finish» через ->emit("finish",@results), но не  сможет обычному шагу.
  • Если финальный результат получен на промежуточном шаге, то чтобы  передать его на последний шаг нужно реализовать ручной механизм  «прокидывания» готового результата через все шаги между ними — но  если вместо последнего шага используется обработчик «finish», то можно  сразу вызвать его через ->remaining([])->pass(@result).
    • Так же нужно учитывать, что если этот шаг успел запустить какие-то  операции до передачи результатов в «finish», то обработчик «finish»  будет запущен только после того, как эти операции завершатся, причём  он получит параметрами не только вышеупомянутый @result, но и всё  что вернут операции.

ВНИМАНИЕ! Делать ->emit("finish") можно только внутри обработчика исключений, а в обычном шаге нельзя. При этом в обычном шаге это же делается через ->remaining([])->pass(@result), но в обработчике исключений это не сработает.

$delay→pass

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

$delay->pass;

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

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

Пример сложного парсера

Давайте рассмотрим пример, в котором используется почти всё вышеописанное. Предположим, что нам нужно скачать данные с сайта. Сначала нужно залогиниться ($url_login), потом перейти на страницу со списком нужных записей ($url_list), для некоторых записей может быть доступна ссылка на страницу с деталями, а на странице с деталями могут быть ссылки на несколько файлов «приаттаченных» к этой записи, которые необходимо скачать.

sub parse_site {     my ($user, $pass) = @_;     # сюда будем накапливать данные в процессе выкачки:     # @records = (     #   {     #       key1 => "value1",     #       …     #       attaches => [ "content of file1", … ],     #   },     #   …     # );     my @records;     # каждой запущенной задаче нужен свой $ua, т.к. можно запустить     # несколько одновременных выкачек с разными $user/$pass, и нужно     # чтобы в $ua разных задач были разные куки     my $ua = Mojo::UserAgent->new->max_redirects(5);     # запускаем задачу, удерживая $ua до конца задачи     Mojo::IOLoop->delay->data(ua=>$ua)->steps(         sub {             $ua->post($url_login, form=>{user=>$user,pass=>$pass}, shift->begin);         },         sub {             my ($delay, $tx) = @_;             die $tx->error->{message} if $tx->error;             # проверим ошибку аутентификации             if (!$tx->res->dom->at('#logout')) {                 die 'failed to login: bad user/pass';             }             # всё в порядке, качаем список записей             $ua->get($url_list, $delay->begin);         },         sub {             my ($delay, $tx) = @_;             die $tx->error->{message} if $tx->error;             # если записей на странице не будет и никаких операций             # на этом шаге не запустится - перейдём на следующий шаг             $delay->pass;             # считаем все записи             for ($tx->res->dom('.record')->each) {                 # парсим обычные поля текущей записи                 my $record = {                     key1 => $_->at('.key1')->text,                     # …                 };                 # добавляем эту запись к финальному результату                 push @records, $record;                 # если есть страница с деталями - качаем                 if (my $a = $_->at('.details a')) {                     # качаем страницу с деталями и приаттаченные к ней                     # файлы как отдельную задачу - это немного                     # усложнит, но зато ускорит процесс т.к. можно                     # будет одновременно качать и страницы с                     # деталями и файлы приаттаченные к уже скачанным                     # страницам (плюс при таком подходе мы лениво                     # клозурим $record и не нужно думать как привязать                     # конкретную страницу с деталями к конкретной                     # записи) - альтернативой было бы поставить на                     # выкачку только страницы с деталями, а на                     # следующем шаге основной задачи когда все                     # страницы с деталями скачаются ставить на выкачку                     # приаттаченные файлы                     Mojo::IOLoop->delay(                         sub {                             $ua->get($a->{href}, shift->begin);                         },                         sub {                             my ($delay, $tx) = @_;                             die $tx->error->{message} if $tx->error;                             # если файлов не будет - идём на след.шаг                             $delay->pass;                             # качаем 0 или более приаттаченных файлов                             $tx->res->dom('.file a')->each(sub{                                 $ua->get($_->{href}, $delay->begin);                             });                         },                         sub {                             my ($delay, @tx) = @_;                             die $_->error->{message} for grep {$_->error} @tx;                             # добавляем файлы к нужной записи                             for my $tx (@tx) {                                 push @{ $record->{attaches} }, $tx->body;                             }                             # нам необходимо чтобы finish вызвался без                             # параметров, а не с нашими @tx, поэтому:                             $delay->pass;                         },                     )->catch(                         sub {                             my ($delay, $err) = @_;                             warn $err; # ошибка выкачки или парсинга                             $delay->emit(finish => 'failed to get details');                         }                     )->on(finish => $delay->begin);                 } ### if .details             } ### for .record         },     )->catch(         sub {             my ($delay, $err) = @_;             warn $err; # ошибка логина, выкачки или парсинга             $delay->emit(finish => 'failed to get records');         }     )->on(finish =>         sub {             my ($delay, @err) = @_;             if (!@err) {                 process_records(@records);             }         }     ); }

Немного не очевидным моментом является способ обработки ошибок. Поскольку результаты работы передавать между шагами не требуется (они накапливаются в заклозуренном @records), то при успехе на следующий шаг передаётся пустой список (через $delay->pass;), а при ошибке передаётся текст ошибки. Таким образом, если последний шаг в обработчике finish получит какие-то параметры — значит где-то в процессе выкачки или парсинга была ошибка(и). Саму ошибку уже перехватили и обработали (через warn) в обработчиках ->catch — собственно это как раз они и обеспечили передачу ошибки параметром в обработчик finish.

Если кто-то знает, как можно проще и/или нагляднее решить такую задачу — пишите. Пример аналогичного решения на Promises тоже был бы кстати.

______________________

Текст конвертирован используя habrahabr backend для AsciiDoc.

ссылка на оригинал статьи http://habrahabr.ru/post/228141/

День рождения директора DARPA и крестного отца ARPANET

“People are as important as ideas.” (Herzfeld 2005)

Есть два Чарльза Херцфилда (Charles Herzfeld), один трудился над созданием фильмов «Черный лебедь», «Рестлер» и ‘Her", другой, будучи директором DARPA, принял персональное решение о создании ARPANET.

Цели ARPANET:
— проведение экспериментов в области компьютерных коммуникаций;
— объединение научного потенциала исследовательских учреждений;
— изучение способов поддержания устойчивой связи в условиях ядерного нападения;
— разработка концепции распределённого управления военными и гражданскими структурами в период ведения войны.

Сам Чарльзговорил следующее: «The ARPANET was not started to create a Command and Control System that would survive a nuclear attack, as many now claim. To build such a system was, clearly, a major military need, but it was not ARPA’s mission to do this; in fact, we would have been severely criticized had we tried. Rather, the ARPANET came out of our frustration that there were only a limited number of large, powerful research computers in the country, and that many research investigators, who should have access to them, were geographically separated from them.»

Короткая биография под катом

Родился в Австрии 29 июня 1925 года, но потом мигрировал в Америку и получил там гражданство.
Учился в Чикагском университете и попал на лекции фон Неймана и «загорелся» информационными технологиями.

Сначала Чарльз Херцфилд работал физиком с 1951 по 1953 в Ballistic Research Laboratory та самая, в которой изучали выстрел в Кеннеди) и с 1953 по 1955 в Naval Research Laboratory (та самая, где сейчас строят Railgun).
29 сентября 1961 пошел работать в DARAP`у (тогда еще была ARPA), координатором программы Project Defender, предвестник американской противоракетной обороны, (1961-1963)

image

В DARPA Чарльз Херцфилд наслушался познакомился с «духовным отцом всемирной сети» Джозефом Ликлайдером (физик, математик, психолог, психоакустик), который высказывал идеи
— о необходимости создания компьютеров, работающих в режиме реального времени
— о необходимости создания объединения компьютеров в сеть со свободным доступом любого человека из любого места мира к её ресурсам (Intergalactic Computer Network)
— о компьютерах, имеющих простые пользовательские интерфейсы (Man–Computer Symbiosis)
— о принципе указания и выбора (point-and-click),
— а так же предвосхитил такие феномены как цифровые библиотеки, электронную коммерцию (e-commerce), дистанционное банковское обслуживание (online banking), облачные вычисления

С 1963 по 1963 был зам директора, а в период с 1965 по 1967 был директором DARPA

После ухода из DARPA работал в бизнес-секторе (с 1967 по 1985 в ITT Corporation, с 1985 по 1990 в Aetna), потом в 1990 вернулся в оборонку Director of Defense Research and Engineering (1990-1991).

По сей день работает консультантом частных и правительственных компаний.

В 2012 году был введен Обществом Интернета в Зал славы Интернета

Любимая технология: Google Earth и подобные сайты. «Современный волшебный ковер, который унесет в любую точку. Когда я был ребенком, я мечтал о нем, сейчас у меня он есть»
Хобби: Дайвинг и подводная фотография
Лозунг: «Вы в долгу перед миром, никогда не сдавайтесь»
Эпитафия: «Моя жизнь — грандиозное путешествие. Я был великим путешественником»
Любимый фильм: Касабланка
Чего люди не знают о вас: «Я очень застенчивый, несмотря на внешнее впечатление.»

В статье рассказывается о встрече Чарльза Херцфилда и Джозефа Ликлайдера, о постановке задачи, что делать с потоком данных от ракет в 150 мбит/с, о встрече с Бобом Тэйлором
How Pacific Island Missile Tests Helped Launch the Internet

www.netaffair.org/people/Charles_Herzfeld

The Grill: ARPA Pioneer Charles M. Herzfeld on the Hot Seat

Интервью на английском о трудовых буднях крестного отца ARPANET

What was your introduction to computing?
When I was a graduate student at the University of Chicago, in 1948 or so, John von Neumann came and gave three seminars on electronic computing. He was instrumental in getting the ENIAC built, and he came to tell us about it. It was hugely important stuff, and it changed my life absolutely.

Then, before ARPA, J.C.R. Licklider gave two or three lectures at the Pentagon, and I remember those vividly. He said, The way we are doing computing is really pretty stupid. I think theres a better way. He was a brilliant man, and I became a disciple of his.

And a few years later, you and Licklider would end up at ARPA together, with Licklider the first director of its Information Processing Techniques Office.
Yes. IPTO was one of the things at ARPA that I became godfather of. I was the go-to guy if it got into trouble.

[The IPTO] directors changed the world, but I claim to be the godfather, not the father. And as godfather, I took their message to Congress.

What else did you do as godfather?
I signed the first two or three ARPA orders in 1966 and 1967 as director. I said, Do that build a network, however small and crappy it is. Lick was gone by then, but I had recruited Bob Taylor as the follow-on.

One day, Taylor dropped into my office, and he got $1 million in 20 minutes. [When Taylor recounts this story] he acts like I was sitting in my chair handing out million-dollar checks, but not so. I was sure that networking computers would change computing. I do not claim to have foreseen what happened, but I knew Licklider was on to something.

In those days, did you casually hand out big sums like that very often?
Whenever it was needed. My secret was that I always had money because there was a long list of things we were doing that we didnt have to do. I was ruthless about that.

What else did IPTO do in those early times?
We created the whole artificial intelligence community and funded it. And we created the computer science world. When we started [IPTO], there were no computer science departments or computer science professionals in the world. None.

Do you agree with the people who say that DARPA has pulled back from long-range, high-risk projects?
There certainly has been a change, and its not for the better. But it may be inevitable.

Im not sure one could start the old ARPA nowadays. It would be illegal, perhaps. We now live under tight controls by many people who dont understand much about substance.

What was unique about IPTO was that it was very broad technically and philosophically, and nobody told you how to structure it. We structured it. Its very hard to do that today.

But why? Why couldnt a Licklider come in today and do big things?
Because the people that you have to persuade are too busy, dont know enough about the subject and are highly risk-averse.

When President Eisenhower said, You, Department X, will do Y, theyd salute and say, Yes, sir. Now they say, Well get back to you. I blame Congress for a good part of it. And agency heads are all wishy-washy. Whats missing is leadership that understands what it is doing.

The Washington Post recently ran a Page One story saying that the FBI had given emergency responders $25 million in computer kits for exchanging information on suspected explosives, including weapons of mass destruction. But, the Post said, many of the kits didnt work and some were just abandoned. What do you make of that kind of report?
We are becoming incapable of handling a technology challenge of any major magnitude. We are losing the ability to do big, complicated things. In your example, nobody thought that someone had to organize a maintenance space for repairs, spare parts and so on. They only thought about buying the radios.

Is it partly a failure of technology?
Absolutely not. We have technology on the shelf we dont know what to do with, and we are buying more every day, to the tune of billions of dollars a year. Whats missing is leadership that understands what it is doing. The whole thing is just off the rails.

Whats going on at the National Science Foundation?
My friends complain that they have to submit 10 proposals to get one funded. Cuckoo. And its tremendously demoralizing and very inefficient. The process is too risk-averse. But doing really good research is a high-risk proposition. If the system does not fund thinking about big problems, you think about small problems.

Could there be another unhappy surprise like Sputnik?
Yes, I expect it. In the biological world, it may be an accident: Someone is doing virus research and comes up with something that spreads easily and kills a lot of people. There is terrorism. It is absolutely thinkable that these guys will steal a nuclear weapon, have some technical help and blow it off in New York Harbor.

ссылка на оригинал статьи http://habrahabr.ru/post/228137/

Ecole 42, учеба в сердце Парижа

Добрый день, сообщество! Я хочу рассказать вам про одну уникальную школу для программистов. Называется она “42” и находится в Париже. Что это за школа и в чем ее уникальность я расскажу под катом.

“42” была основана Ксавье Ниелом и открыла двери в ноябре 2013. Основная цель школы — выпуск квалифицированных, мотивированных разработчиков. Поскольку обучение бесплатно, шансы у всех равны. Для школы не важны навыки и знания, полученные до поступления. Учиться здесь могут как не умеющие ничего, так и уже состоявшиеся программисты, важны лишь мотивация и способности.
Школа располагается недалеко от центра Парижа, в собственном здании напичканном высокотехнологичными штуками (мне особенно понравиась серверная) 🙂

Почему “42” уникальна? В ней нет преподавателей, нет общепризнанной программы обучения (гос. диплома, впрочем, пока тоже нет). Основная идея обучения — собрать всех студентов (это от 800 до 1000 человек ) в одном большом помещении и давать им задания, каждое следующее сложнее предыдущего. Ученикам не рассказывают как решать задачу, дают лишь подсказки, они вольны использовать любые источники для получения необходимых знаний. Кооперация и обмен опытом приветствуется и, похоже, является обязательным для успешного обучения. Студенты работают в одном из таких помещений:
image
Всего таких помещений три, по одному на этаж.

Единственным ограничением для поступления является возраст — нужно быть старше 18 и моложе 30 лет. Процесс поступления разделен на два этапа. Первый этап — решение задачек он-лайн. Для этого нужно зарегистрироваться на сайте www.42.fr/ и следовать инструкциям. На этом этапе отбирается примерно 4000 человек, которые проходят во второй тур — “Piscine”. В общем, это можно назвать пулом, но организаторы настаивают, что Piscine это уже имя собственное. Идея все та же — потенциальных студентов собирают вместе на один месяц и дают задания. По результатам четырех пулов, будет отобрано до 1000 человек, показавших лучшие результаты и они получат право учиться в школе. Срок базового курса — три года, после можно остаться еще на два, где будут даваться основы бизнеса.

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

Мне, в первую очередь, интересно само место, возможность пожить в Париже и пообщаться с увлеченным народом. Думаю, что это может стать французской “Кремниевой долиной”.

29 июля начинается августовский пул, места есть и на август и на сентябрь, так что если кому-то будет интересно, пробуйте. Если возникнут вопросы, пишите, поделюсь всем, что знаю 🙂

ссылка на оригинал статьи http://habrahabr.ru/post/228127/

Контроллер светодиодов для авиамодели

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

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

Контроллер сделан на базе ATTiny13A, т.о. статья будет так же полезна тем, кто разбирается с микроконтроллерами AVR. Я старался все разжевать и разложить по полочкам, так что статья будет интересна в первую очередь новичкам.

Способности железяки можно оценить по этому видео:


Вступление

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

Задумывая очередную модель я сразу прикинул сколько светодиодов мне нужно и как они будут моргать. В результате «переписи» выяснилось, что мне нужно 4 канала (в каждом канале по 2-3 светодиода):

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

Модель большая, летает далеко. А значит, что бы светодиоды было видно, они должны быть мощными. На предыдущей модели я делал БАНО на одноваттных светодиодах – их прекрасно видно с расстояния в 50м даже в яркий солнечный день вечер. Значит это мой размерчик.

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

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

Электроника

С требованиями разобрались. Пора браться за паяльник.

Я в электронике, в общем то, новичок. А потому я творчески переработал схему от Acinonyx (который в свою очередь позаимствовал ее у САМОКАТ ВЕТЕРАНА). Мне потребовалось изменить следующее:

  • Выкинуть кнопки
  • Завести PWM вход от приемника
  • 3 ноги ATTiny определить как выходы и подключить к ним светодиодные драйверы
  • Добавить четвертый драйвер, который будет всегда включен (для БАНО)

В общем от оригинала мало что осталось.

В качестве драйвера удачно подошла микросхема ZXLD1350 которая как раз и рассчитана на питание одноваттных светодиодов (ток до 350мА). При чем в каждый канал можно ставить последовательно любое светодиодов, лишь бы все вместе суммарно вкладывались в напряжение питания. Т.е. если я буду запитывать схему от батареи 3S (11.1В) то смогу в каждый канал поставить до 3 светодиодов на кажом из которых падает 3.2В.

Микроконтроллер я запитал отдельно от приемника, тем же проводом что и PWM вход.

image

Схема. Каждый канал построен по схеме из даташита. Таких каналов на плате 4 штуки (я нарисовал только один). Светодиодов я нарисовал 3, но, как я уже сказал, можно ставить любое количество светодиодов в каждый канал. Можно даже ставить светодиоды разного цвета (на них падает разное напряжение), главное что бы они были рассчитаны на одинаковый ток. Драйвер сам подберет такое напряжение, что бы ток через диоды не превышал 350мА.

Вход ADJ трех из каналов подключен к выходу контроллера через транзистор. У драйвера ZXLD1350 есть специальный механизм с помощью которого можно включать и выключать светодиод с контроллера. Более того, можно плавно регулировать яркость меняя напряжение на входе или с помощью ШИМ. Вот только рабочее напряжение входа от 0.3В до 2.5В, а с контроллера выдает 5В. Благо даташит рекомендует решение в виде транзистора. Нужно только учесть, что этот транзистор инвертирует логическое состояние – ноль на ноге контроллера будет включать светодиод, а единица выключать. Впрочем, это не проблема пофиксить программно.

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

image

image

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

image

Готовое изделие. Лишний текстолит обрезал по рамочке. Получилась платка 27х22мм и весом 4г. Ну еще 2г на провода и раземы получилось. К приемнику устройство подключается через стандартный трехпиновый JR разъем. Светодиодные драйверы берут питание с балансировочного разъема батарейки.

image

image

Кому 1Вт мало может посмотреть на микросхему ZXLD1360. Она рассчитана на питание 3Вт светодиодов (ток 750мА). Схема включения и цоколевка такие же, так что разводка платы подойдет. Только номиналы некоторых деталей поменять нужно, курите даташит.

Для тех, кто не прокачался еще в травлении двухсторонних плат, я так же выкладываю несколько вариантов односторонних – для 2, 3 и 4 каналов.

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

image

А вот так на предыдущей модели.

image

Прошивка

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

Несмотря на то, что в микроконтроллере всего 1Кб флеша, я решил писать на С. Так удобнее и нагляднее. Скетчи ардуино, конечно, в чем то попроще будут, но все что я задумал не влезет в память контроллера. Поэтому пришлось спуститься на более низкий уровень и программировать регистры напрямую. К моему удивлению компилятор (gcc 3.4.2 из Atmel Studio 6) сгенерил довольно хороший код. Было, правда, пару мест, где компилятор действовал неоптимально, но эти места удалось скорректировать.

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

Приведу классический пример. Что если нам нужно моргать одним светодиодом? Ну тогда наша программа будет выглядеть как то так:

while(1) { 	led1(on); 	delay(500); 	led1(off); 	delay(500); } 

А что если нам нужно мигать двумя светодиодами, да еще и с разной частотой? Ну можно, конечно, извратиться и написать чтото типа такого:

while(1) { 	led1(on); 	delay(300); 	led2(on); 	delay(200); 	led1(off); 	delay(500); 	led2(off); 	delay(200); } 

Но скорее всего будет очнь сложно подобрать тайминги и последовтельность включений-выключений. Если получится, конечно (в чем я сомневаюсь). А если нужно тремя диодами моргать? А четырьмя?

Правильное решение — использовать таймеры. Но есть проблема: таймер в микроконтроллере всего 1, да и тот восьмибитный.

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

Давайте посмотрим, как это будет выглядеть в коде.

// Pointer to a timer handler typedef void (*eventHandler)();  // Software timers list typedef struct timer_t { 	uint16_t timeout; 	eventHandler handler; } timer_t;  #define TIMERS_LIST_SIZE 5 timer_t timersList[TIMERS_LIST_SIZE];  

Програмный таймер — это счетчик сколько раз должен прокрутиться основной (железный) таймер прежде чем вызвать обработчик. Указатель на обработчик прилагается. Для моих задач вполне достаточно трех таких записей, но на всякий случай я сделал список програмных таймеров размером в 5 элементов.

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

void setupEventQueue() { 	// Clear timers list 	memset(timersList, 0, sizeof(timersList));  }  

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

void addTimer(eventHandler handler, uint16_t timeout) { 	// Search through the timers list to find empty slot 	for(timer_t * timer = timersList; timer < timersList + TIMERS_LIST_SIZE; timer++) 	{ 		if(timer->handler != NULL) 			continue; 		 		// Add the timer to the list 		timer->handler = handler; 		timer->timeout = timeout; 		 		break; 	} } 

Основной цикл выглядит следующим образом.

void runEventLoop() { 	runTimer();  	// Set up sleep mode 	set_sleep_mode(SLEEP_MODE_IDLE);  	while(1) // Main event loop 	{ 		wdt_reset(); 		 		// Sleep until the timer event occurs 		sleep_enable(); 		sleep_cpu(); 		sleep_disable();  		//Iterate over timers 		for(timer_t * timer = timersList; timer < timersList + TIMERS_LIST_SIZE; timer++) 		{ 			// Skip inactive timers 			if(timer->handler == NULL) 				continue;  			if(timer->timeout) // Decrement timeout value 			{ 				timer->timeout--; 			} 			else // If it is already zero - execute handler 			{ 				timer->handler(); 				timer->handler = NULL; 			} 		} 	}	 } 

Вначале запускаем таймер (об этом чуть ниже, там не все так тривиально). Вместо активного ожидания я использую sleep. В начале каждого цикла процессор переходит в режим Idle. Это означает что сам CPU заснет, но все таймеры (ок, весь, он один!) продолжат работать. Когда таймер досчитает до конца и ресетнется в ноль возникнет прерывание, которое разбудит процессор и программа пойдет дальше. Как раз то, что нам нужно.

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

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

Поскольку все происходит в одном потоке, то никаких мьютексов и локов не требуется.

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

#define LED_A_PIN		PORTB0  void toggleLedATask() { 	PORTB ^= (1 << LED_A_PIN);  	addTimer(toggleLedATask, TIMEOUT_MS(300)); } 

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

int main(void) { 	// Set up ports 	PORTB = 1 << LED_A_PIN; // LEDs switched off  	DDRB = 1 << LED_A_PIN; // output mode for LED pins 	 	setupEventQueue(); 	addTimer(toggleLedATask, TIMEOUT_MS(0)); 	 	sei(); 	 	runEventLoop(); } 

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

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

#define LED_B_PIN		PORTB1  uint8_t delayIndex = 0; const uint16_t delays[] =  { 	TIMEOUT_MS(100), //on 	TIMEOUT_MS(700), //off 	TIMEOUT_MS(100), //on 	TIMEOUT_MS(200), //off 	TIMEOUT_MS(100), //on 	TIMEOUT_MS(700), //off 	TIMEOUT_MS(100), //on 	TIMEOUT_MS(200), //off 	TIMEOUT_MS(100), //on 	TIMEOUT_MS(200), //off 	TIMEOUT_MS(100), //on 	TIMEOUT_MS(1200), //off };  void complexLedTask() { 	 	PORTB ^= (1 << LED_B_PIN); 	 	uint16_t delay = delays[delayIndex]; 	delayIndex ++; 	if(delayIndex >= sizeof(delays)/sizeof(uint16_t)) //dim(delays) 		delayIndex = 0; 	 	addTimer(complexLedTask, delay); } 

Просто делаем таблицу с таймингами. Обработчик каждый раз меняет состояние светодиода и ждет уремя указаное в таблице.

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

int main(void) { 	// Set up ports 	PORTB = 1 << LED_A_PIN | 1 << LED_B_PIN | 1 << LED_C_PIN; // LEDs switched off 	DDRB = 1 << LED_A_PIN | 1 << LED_B_PIN | 1 << LED_C_PIN; // output mode for LED pins 	 	setupEventQueue(); 	addTimer(toggleLedATask, TIMEOUT_MS(0));  	addTimer(complexLedTask, TIMEOUT_MS(0));  	addTimer(blinkLedCTask, TIMEOUT_MS(0)); 	 	sei(); 	 	runEventLoop(); } 

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

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

Т.е. не часто и не редко. Но как именно? Ок, для предыдущей задачи диапазон возможных значений достаточно большой. Но нужно так же не забывать про задачу «слушать PWM вход». Конкретнее, там ходят импульсы длительностью 800-2200мкс и эту длину нам прийдется измерять. Для нашей задачи включать/выключать светодиод по команде с пульта будем считать так: если импульс короче чем 1500мкс – светодиод выключен, если длинее – включен.

В переводе на язык микроконтроллеров и таймеров мы будем считать сколько тиков таймера уместится в измеряемом отрезке времени. Проблема возникает когда длительность импульса примерно равна пороговой. Тогда возможны ложные срабатывания и светодиод будет промаргивать при изменении длины импульса. Что бы уменьшить вероятность промаргивания нам нужно точнее измерять длину импульса. Я думаю разрешение таймера должно быть в районе 1-2 мкс – такое разрешение обеспечит достаточную точность измерений.

Раз уж речь зашла про конкретные цифры нужно разобраться с частотой микроконтроллера. Контроллер может тактироваться от внутреннего и от внешнего генератора. Внешний генератор точнее, но это дополнительные детали и вес. Да и не нужна нам точность особо. Из внутренних генераторов доступны 128кГц, 4.8МГц и 9.6МГц. 128кГц маловато будет, будем выбирать между двумя другими вариантами.

Таймер в свою очередь может иметь ту же частоту, что и микроконтроллер, а может задействовать делитель частоты на 8, 64, 256 или 1024. Сам же таймер считает от 0 до 255 и потом сбрасывается в 0. В случае если делитель не используется один тик таймера соответствует одному тику процессора, что в большинстве случаев соответствует одной команде. Мы собирались делать полезную работу каждый полный цикл таймера. Но если нам эту работу нужно будет делать каждые 256 команд, то мы попросту не будем успевать делать эту работу (либо ее должно быть очень очень мало).

Итак, нужно выбирать между частотой 4.8МГц и 9.6Мгц, и делителями 8, 64 и 256. Как по мне, вариант 4.8МГц с делителем 8 довольно удачный. Таймер будет тикать с частотой 4.8МГц / 8 = 600кГц. А это означает, что один тик будет занимать 1.666мкс. Как раз укладывается в искомые 1-2мкс. Полный цикл таймера будет занимать 1,666 * 256 = 426.66 мкс. В качестве програмного таймера мы используем 16-битную переменную, а значит мы способны отмерять отрезки времени 65536 * 426,66мкс = 27,96с (с точностью тех же самых 426,66мкс)

Код запуска таймера:

void runTimer() { 	// Reset timer counter 	TCNT0 = 0; 	 	// Run timer at 4.8MHz/8 = 600 kHz 	// This gives 1.667 uSec timer tick, 426.667 uSec timer interval 	// Almost 28 seconds with additional 16bit SW timer value 	TCCR0A = 0; // Normal mode 	TCCR0B = 0 << CS02 | 1 << CS01 | 0 << CS00; // run timer with prescailer f/8 } 

В коде выше я использовал загадочный макрос TIMEOUT_MS. Пришло время его расшифровать.

#define TIMEOUT_MS(t)  ((uint32_t)t  * 600 / 256)    //4.8MHz / (8 prescailer * 256 full timer cycle * 1000 since we are counting in ms) 

Этот макрос определяет количество циклов по 426,6мкс необходимых для отмерения заданого количества милисекунд. К сожалению когда я бахнул туда полную формулу (ту, что в комментарии) то компилятор начал генерить страшные ворнинги с которыми я не справился. Пришлось формулу пересчитать до непонятных теперь 600/256.

Но вернемся к слушанию ШИМ входа. Что бы было чуть понятнее, расскажу еще раз как все работает, но другими словами. Основной 8-битный таймер тикает от 0 до 255. Каждый полный цикл таймера мы процессим список программных таймеров и запускаем обработчики, если нужно. Помимо этого значение самого 8-битного таймера используется в измерение длины импульса на входе. Делается это очень просто: если начался импульс – запоминаем значение таймера. Пока импульс идет таймер продолжает тикать. К моменту когда закончится импульс таймер дотикает до какого то нового значения. Соответственно по разнице значений мы можем вычислить длину импульса просто умножив на время одного тика (1,666мкс)

Стоп! Таймер то у нас 8битный а это означает, что таким способом можно измерить только импульсы длиной до 256 * 1,66 = 426,66мкс, в то время как входящие импульсы длиной до 2200мкс. Не беда! Можно искусственно расширить счетчик таймера добавив столько старших байт сколько нужно. Действует обычная бинарная математика – когда младший байт переполнился инкрементируем старшие байты.

// Additional high byte for 8bit timer value volatile uint8_t tcnth;   void runTimer() { 	// Reset timer counters 	tcnth = 0; 	TCNT0 = 0; 	 	// Run timer at 4.8MHz/8 = 600 kHz 	// This gives 1.667 uSec timer tick, 426.667 uSec timer interval 	// Almost 28 seconds with additional 16bit SW timer value 	TCCR0A = 0; // Normal mode 	TCCR0B = 0 << CS02 | 1 << CS01 | 0 << CS00; // run timer with prescailer f/8 	TIMSK0 = 1 << TOIE0; } 

Почти все тоже самое. Добавилась только переменная tcnth – «старший» байт в дополнение к младшему байту внутри таймера. Еще важна последняя строка – она включает прерывание по переполнению таймера. Это прерывание и будет инкрементировать старший байт:

ISR(TIM0_OVF_vect) { 	// Increment high byte of the HW counter 	tcnth++; } 

Обратите внимание, что переменная tcnth объявлена как volatile. Без этого ключевого слова компилятор в другой части программы может подумать, что переменная не меняется и оптимизировать лишнего. Он же не в курсе, что переменная изменяется в прерывании (по сути в другом потоке).

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

uint16_t pwmPulseStartTime; #define PWM_THRESHOLD	900	 // number of pulses in 1500 uS at 4.8MHz with /8 prescailer = 1500 * 4.8 / 8 = 900  // Pin Change interrupt ISR(PCINT0_vect) { 	/* 	// Get the current time stamp 	uint16_t curTime = (tcnth << 8) + TCNT0; 	 	Unfortunately gcc generates plenty of code when constructing 16 bit value from 2 bytes. Let's do it ourselves	 	*/  	union 	{ 		struct 		{ 			uint8_t l; 			uint8_t h; 		}; 		uint16_t val; 	} curTime; 	 	// Get the current time stamp 	curTime.h = tcnth; 	curTime.l = TCNT0;  	// It may happen that Pin Change Interrupt occurs at the same time as timer overflow 	// Since timer overflow interrupt has lower priority let's do its work here (increment tcnth) 	if(TIFR0 & (1 << TOV0)) 	{ 		curTime.h = tcnth+1; 		curTime.l = TCNT0; 	} 	 	 	if(PINB & (1 << PWM_INPUT_PIN)) // On raising edge just capture current timer value 	{ 		pwmPulseStartTime = curTime.val; 	} 	else // On failing edge calculate pulse length and turn on/off LED depending on time 	{ 		uint16_t pulseLen = curTime.val - pwmPulseStartTime; 		 		if(pulseLen >= PWM_THRESHOLD) 			PORTB |= (1 << LED_C_PIN); 		else	 			PORTB &= ~(1 << LED_C_PIN); 	} } 

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

Решение довольно простое – проверить, а не возникло ли переполнение таймера и если возникло, сделать ту же самую работу, что и обработчик этого переполнения – сделать +1 к старшему байту.

В этом месте я наткнулся на неоптимальность кода, сгенеренного гнусью. Код (tcnth << 8) + TCNT0 компилировал вот так, как написано — со сдвигами и сложениями. И это не смотря на включенную оптимизацию (-O1). Мне же в этом месте нужно всего лишь 2 байта трактовать как 16-битное число. Пришлось городить огород с юнионами.

Вторая часть обработчика делает собственно полезную работу. Если мы словили начало импульса – просто запомним временнУю метку в переменной pwmPulseStartTime. Если словили окончание импульса — считаем разницу временнЫх меток и включаем/выключаем светодиод в зависимости от значения. Порог срабатывания – 1500мс, или 900 тиков таймера по 1,66мкс каждый.

Чего тут не хватает, так это инициализации этого самого pin change interrupt:

#define PWM_INPUT_PIN	PCINT3  void setupPWMInput() { 	// Initialize the timestamp value 	pwmPulseStartTime = 0;  	// Set up pin configuration 	PORTB |= 1 << PWM_INPUT_PIN; // pull-up for PCINT3 	DDRB &= ~(1 << PWM_INPUT_PIN); // output mode for LED pins, input mode for PCINT3 pin  	// Use PCINT3 pin as input 	PCMSK = 1 << PWM_INPUT_PIN;  	// Enable Pin Change interrupt 	GIMSK |= 1 << PCIE; } 

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

Можно написать код, который будет включать и выключать светодиод. Благо таймеров теперь можно наделать сколько хочешь. Но зачем, если PWM генерация уже встроена в контроллер? При чем можно независимо управлять аж двумя каналами генерации – на ножках OC0A и OC0B (они же PB0 и PB1).

Работает это так. Единственный таймер крутится как обычно с заданой скоростью. В начале цикла на ножке выставляется единица, по достижению некоторого определенного значения (задается регистрами OCR0A и OCR0B) на ножке выставляется ноль. Далее цикл повторяется. Чем больше значение регистра, тем больше скважность и тем ярче светится диод. Это называется Non-Inverting режим. Поскольку светодиоды подключены через инвертор то нам больше подойдет Inverting mode – включаемся по значению в регистре, выключаемся когда таймер добегает до конца и ресетится.

// Current PWM value volatile uint8_t pwmAValue = 1; volatile uint8_t pwmBValue = 1;  void runTimer() { 	// Reset counter counters 	tcnth = 0; 	TCNT0 = 0; 	OCR0A = pwmAValue; 	OCR0B = pwmBValue; 	 	// Run timer at 4.8MHz/8 = 600 kHz 	// This gives 1.667 uSec timer tick, 426.667 uSec timer interval 	// Almost 28 seconds with additional 16bit SW timer value 	//TCCR0A = 1 << COM0A1 | 1 << COM0A0 | 1 << COM0B1 | 1 << COM0B0 | 1 << WGM01 | 1 << WGM00; // Fast PWM on OC0A and OC0B pins, inverting mode 	TCCR0A = 1 << COM0A1 | 1 << COM0A0 | 1 << WGM01 | 1 << WGM00; // Fast PWM on OC0A pin, inverting mode 	TCCR0B = 0 << CS02 | 1 << CS01 | 0 << CS00; // run timer with prescailer f/8 	TIMSK0 = 1 << TOIE0; } 

Пришлось слегка подкорректировать инициализацию таймера. Биты WGM00 и WGM01 включают режим генерации Fast-PWM. Биты COM0A0, COM0A1, COM0B0 и COM0B1 включают Inverting mode в каналах A и B. Точнее закоментареная строка включает в обоих, раскоментареная только для OC0A.

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

ISR(TIM0_OVF_vect) { 	// Update the PWM values 	OCR0A = pwmAValue; 	OCR0B = pwmBValue; 	 	// Increment high byte of the HW counter 	tcnth++; } 

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

Саму яркость можно менять в уже знакомых програмных таймерах. Например, так:

uint8_t directionA = 0;  void pwmLedATask() { 	if(directionA) // Incrementing 	{ 		pwmAValue += 2; 		 		if(pwmAValue == 255) 			directionA = 0; 	} 	else //decrementing 	{ 		pwmAValue -= 2; 		 		if(pwmAValue == 1) 			directionA = 1; 	} 	 	 	addTimer(pwmLedATask, TIMEOUT_MS(2)); } 

Просто понемногу увеличиваем или уменьшаем значение переменной pwmAValue, которое потом будет занесено в соответствующий регистр. Хотя для эмуляции настоящего проблескового маячка придется придумать что нибудь покрасивее. Например, так:

typedef struct complexPWM { 	uint8_t step; 	uint8_t maxValue; 	uint16_t delay; } complexPWM;  complexPWM pwmItems[] = { 	{0, 1, TIMEOUT_MS(1000)}, 	{2, 127, TIMEOUT_MS(2)}, 	{-2, 33, TIMEOUT_MS(2)}, 	{2, 255, TIMEOUT_MS(2)}, 	{-2, 1, TIMEOUT_MS(2)} }; uint8_t pwmTableIndex = 0;  void complexPWMTask() { 	complexPWM * curItem = pwmItems + pwmTableIndex; 	pwmAValue += curItem->step; 	 	if(curItem->maxValue == pwmAValue) 		pwmTableIndex++; 		 	if(pwmTableIndex == sizeof(pwmItems)/sizeof(complexPWM)) //dim(pwmItems) 		pwmTableIndex = 0; 	 	addTimer(complexPWMTask, curItem->delay); } 

Не уверен, что это похоже на проблесковый маяк, но в этом куске кода делается предвспышка до яркости 127, потом уменьшаем яркость до 33 и делаем полную вспышку (до 255).

На этом, наверное, по прошивке и все. Со всеми потрохами и моргульками все влазит в 500-600 байт – даже еще запас остается. Остается осветить один важный момент – Fuse биты. Они равны hfuse=0xff, lfuse=0x79. За расшифровкой попрошу в даташит. В двух словах, пару бит в этих байтах заставляют контроллер работать на частоте 4.8МГц. Остальные биты оставлены в дефолтном состоянии.

Как это выглядит в реальности можно оценить по видео в шапке статьи.

Заключение

В этой статье я описал свой вариант контроллера для разных авиамодельных лампочек – БАНО, посадочных фар и стробов. Останется только чуток подковырять прошивку и Ваши модели копии станут выглядеть как настоящие.

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

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

  • Как на одном таймере сэмулировать несколько таймеров
  • Как выбирать параметры таймера
  • Как настраивать таймер в режим генерации PWM
  • Как слушать PWM вход и делать полезную работу на основе считаных значений

Если абстрагироваться от задачи моргания светодиодами, то получился неплохой такой каркас для «многозадачных» («многопоточных»?) приложений на микроконроллере. Конечно, это еще не RTOS, но уже избавляет от целой кучи рутинных операций. В прошивке этот каркас я вынес в отдельный модуль EventQueue.c/.h. Используйте наздоровье.

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

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

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

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

Тем не менее, не все авиамоделисты дружат с компилятором. Так что я все же скомпилировал некий средний вариант: один канал моргает раз в 2 секунды (строб), ШИМ канал мигает чуть-чуть чаще двойными вспышками, третий канал включается по команде с пульта, четвертый, как и раньше, светит всегда. Эта прошивка будет некой отправной точкой для последующего допиливания. Примеры представленые в статье я так же оставил в коде, только вызовы закоментарил.

Успехов!

Исходники прошивки и разводка плат.

ссылка на оригинал статьи http://habrahabr.ru/post/228083/