17 January 2012

Разделяй и властвуй!

Хочу вернутся к теме вот этого проекта, в разработке которого я по прежнему участвую.
Продукт -- некий VoIP терминал. Под капотом -- ARM от Samsung и Linux. 
На примере софта для этой железки я хочу порассуждать на две темы.


Тема первая -- высокая эффективность embedded разработки через обеспечение работоспособности большей части кода на ПК

Собственно я по этой теме писал много раз, например такое:

"В случае embedded разработки одним из самых главных аспектов эффективной организации процесса есть возможность запускать и отлаживать бОльшую часть кода не на целевой платформе, а прямо на ПК. Потому что обычно процесс получения прошивки для target'а, плюс ее заливка в железо, плюс последующие манипуляции, для того, чтобы получить нужные тебе трассировки это бесконечно длинный и муторный процесс. На стадии запуска одной из версий проекта, которым я сейчас занимаюсь, работа шла в режиме, когда менялась буквально пара строчек кода, потом шла бесконечно длинная процедура билда и запуска новой версии на железе. Запуск свежего билда почти сразу вскрывал следующую проблему и все начиналось по новой. Ужасно неэффективный процесс, и мы даже шутили между собой по этому поводу "три раза сбилдился, глядишь, уже и домой надо собираться"... Естественно, проблема была хорошо прочувствована на собственном горбу, и через какое-то время мы занялись ее решением -- был написан "эмулятор", который позволял до 90% всех проблем кода, который должен работать на железе, выявить работая за персоналкой. Даже страшно представить сколько это сэкономило нам времени, сил и нервов..."


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

Терминал "в железе"

О каких конкретно подсистемах идет речь в данном случае?

Подсистема вывода звука, которую, наверное, более корректно называть миксером. Эта подсистема отвечает за ввод/вывод цифровых голосовых потоков и их микширование (в том числе и голосовых потоков, передающихся через Ethernet по RTP протоколу). 

Исходная реализация этой подсистемы в железе подразумевала наличие отдельно стоящего процессора (использовался Blackfin), который управлялся по TCP/IP и самостоятельно обслуживал всю звуковую часть, включая передачу голоса по сети (роль, аналогичная классическому baseband процессору в мобильных телефонах). 
Решение использовать второй процессор было продиктовано не каким-то соображениями безопасности, надежности или производительности, а исключительно горящими сроками, когда мы решили перестраховаться и использовать уже готовую точку обработки звука на основе Blackfin, которую мы уже несколько лет использовали в других продуктах. 

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

К.О. утверждает: связывая судьбу с той или иной embedded платформой, смотрите не только на мегагерцы и ценник, но и на инфраструктуру вокруг нее -- качество инструментария для разработки, полноту документации, наличие и работоспособность драйверов и так далее. Вот мы встряли в историю -- вроде как самый обычный ARM, вроде как самый обычный Linux. Никаких хитрых драйверов -- Ethernet, звук на AC97 чипе, камера. Но самостоятельно собрать до кучи нормально работающее ядро с полной поддержкой всей периферии -- та еще история, особенно если богатого опыта в этой области нет. Поэтому остается полагаться на то, что тебе дает поставщик, и вот тут как раз и начинается самое интересное, вроде глючного и кривого до безобразия драйвера звуковухи, на борьбу с которым мы положили литры крови и нервных клеток, и все равно получили весьма и весьма компромиссный вариант. 

Если же говорить об "эмуляции" подсистемы вывода звука на ПК, то тут все довольно просто. Код миксера и весь связанный с ним код, который работал на Blackfin, был использован повторно, плюс была добавлена поддержка звуковой карты через классы Qt. 
После отказа от второго процессора на железе, этот же вариант кода (т.н. "локальный миксер") начал использоваться на ARM, только вывод звука был переписан под использование ALSA. 

Следующая подсистема -- клавиатура. Сюда же относится светодиодная индикация рядом с клавишами, джойстик, датчик поднятия трубки и т.д. 

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

Эмулятор терминала. Так идет разработка

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

Ну и последнее, о чем надо написать в этой теме -- экран
В устройстве стоит экран с разрешением 800x480. Вся графика выводится с помощью Qt, при этом какие-то стандартные контролы не используются. Компоновка элементов на "экранах" написана таким образом, что она абсолютно корректно будет отображаться практически при любом разрешении экрана, причем пониженное разрешение 400x240 используется именно при работе на ПК, т.к. экран в исходном разрешении + клавиатура вокруг него далеко не на всяком мониторе комфортно умещается. 
В принципе, реализация непосредственно этой подсистемы очень слабо зависит от платформы, на которой она запущена (настоящее железо vs персоналка), но некоторые нюансы (вроде "встраивания" экрана в форму с клавиатурой), разумеется есть и тут. 
Сам по себе API этой подсистемы организован так, что подставить вместо него что-то другое довольно просто, но об этом я буду говорить уже в следующей теме. 

Резюмируя -- практика в очередной раз продемонстрировала сверхвысокую эффективность подобного подхода в embedded разработке, и все затраты, связанные с его реализацией, без всяких сомнений, окупились, причем многократно. 
Еще раз отмечу, что помимо удобства и высокой эффективности разработки и отладки, подобные решения решают еще одну серьезную проблему, особенно актуальную на ранних статьях разработки -- возможность писать и запускать код, пока нет еще живых макетов устройства, или когда этих макетов просто не хватает на всех разработчиков. 
Кстати, как побочный эффект всей этой истории, получается, что мы создали очередную VoIP звонилку для Linux и Windows. 



Тема вторая -- еще раз про MVC паттерн.

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

Наверное, один из самых сложных, интересных, модных, противоречивых и широко распространенных паттернов является паттерн Model-View-Controller или просто MVC.

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

О классической схеме MVC, с разделением кода на три компонента, я много читал и в книгах, и во всевозможных статьях в Сети, и у меня все чесались руки применить ее на практике. В конец-концов, мне однажды подвернулся проект, который вполне годился на роль подопытного кролика, и я попробовал сделать в нем всё в лучших английских традициях, дык по итогам могу определенно сказать, что ни к чему хорошему такой дизайн не привел. Главная проблема -- разбиение кода на представление и контроллер зачастую сильно избыточно, т.к. за этим всем обычно стоит форма с виджетами, которые почти всегда выступают и в той, и в другой роли. Собственно, так думаю не только я, например, в Qt используется усеченная форма паттерна -- model/view, без выделения контроллера.
Кстати, отмечу еще вот какой момент, связанный с MVC паттерном. Если смотреть на него с точки зрения воинствующей объекто-ориентированной парадигмы (в рамках которой, походу, невозможно создать ни одно мало-мальски серьезное приложение), то можно отметить, что оно крайне плохо придерживается принципа сокрытия данных. Путь истинного ОО джедая состоит в том, чтобы сделать для какого-то класса типа Data метод ShowAsDiagram(IDiagram&), и спрятать таким нехитрым образом все его внутренние детали от внешнего мира. В противном случае, View должен иметь доступ (пусть и в режиме read-only) все данные класс для того, чтобы отобразить их в том или ином виджете.

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

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

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

Третье -- как "экран" должен себя отображать на виджеты, описывающие его UI? И если "экран" это вроде как контроллер, нужно ли отделять от него view?
Тут как раз вылез один нюанс, который однозначно показал, какое решение нужно выбрать.
Дело в том, что в Qt существует понятие "GUI thread" (это поток, который совпадает с потоком, в котором начала работу функция main), и программист обязан производить все манипуляции с UI исключительно в контексте этого потока (как я понимаю, это дурацкое ограничение X Window System).
У нас подавляющее большинство кода написано в стиле объектов, которые живут и обмениваются между собой сообщениями в рамках единственного треда. Мы не используем для этих целей QThread и сообщения Qt, у нас свой велосипед, с блекджеком и шлюхами (наше решение легко портируемое, значительно более производительное, и у нас сделан упор на статическую типизацию еще на этапе компиляции, а не на связывание уже во время выполнения). Очевидно, что в рамках нашего основного потока, в котором живут и взаимодействуют посредством сообщений практически все объекты, мы получить доступ к GUI не можем. Таким образом, код, обслуживающий конкретные "экраны", превратился в контроллер, который посылает асинхронные сообщения с данными для отображения во view, работающем в контексте GUI треда. Данные все уходят, разумеется, по значению, дабы не решать проблему с временем их жизни, ну а implicit sharing в Qt способствует тому, чтобы это не было серьезной проблемой с точки зрения производительности.

Такая вот история. Мораль из нее каждый может сделать на свое усмотрение, например, такую: не надо зацикливаться на паттернах, поступай по ситуации...

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

На этом все, ругайте, хвалите!

No comments:

Post a Comment