У большинства "нормальных" программистов, мягко говоря, неоднозначное отношение к технологии LabVIEW. Тут спорить можно долго и безрезультатно. Ситуацию усугубляет то, что в сети масса примеров программ на LabVIEW, но все они ориентированы на новичка и сводятся к "ой, смотрите как все просто, соединил крутилку с индикатором, кручу ручку, меняется циферка", или в лучшем случае на график в цикле выводится случайное число или синус, все это сопровождается зубодробительным интерфейсом в виде гигантских тумблеров, крутилок и стрелочных индикаторов. Лично меня такой подход сознательного упрощения раздражает. В небольшом цикле статей я постараюсь познакомить читателя с процессом разработки прикладного ПО на LabVIEW. Для того, чтобы не уделять много времени предметной области, воспользуемся подробно описанным алгоритмом загрузки файла конфигурации в ПЛИС через FTDI в режиме MPSSE (Загрузка конфигурации в ПЛИС через USB или разбираем FTDI MPSSE). В этой статье я покажу как реализовать такой же загрузчик ПЛИС, но на языке LabVIEW.
Как уже говорилось выше, алгоритм загрузки ПЛИС в режиме Serial Passive (SP) и принцип работы FTDI хорошо описаны в предыдущей статье. Повторяться не буду. Считаем, что алгоритм протестирован и признан годным. И да, предположим, что читатель хотя бы поверхностно уже знаком с концепцией LabVIEW и неплохо разбирается в классическом программировании.
Хотя процесс создания загрузчика не займет много времени, описание процесса никак не удалось уместить в объем одной статьи, поэтому будет небольшая серия. Так как этап экспериментов уже завершен, а в работоспособности алгоритма я уверен, то позволю себе начать разработку с интерфейса пользователя. В первой статье создадим пользовательский интерфейс загрузчика, реализуем структуру программы. Во второй — через dll драйвера FTDI реализуем загрузку *.rbf файла в ПЛИС.
Для продуктивности беседы не лишним будет напомнить терминологию принятую в LabVIEW. В LabVIEW прикладная программа называется Виртуальный Прибор, он же ВП, он же Virtual instrument или сокращенно VI. ВП имеет две "стороны": передняя панель (Front Panel), где располагаются органы управления и индикаторы, и блок--диаграмма (Block Diagram), где эти элементы соединяются между собой и реализуются функции и потоки обработки данных. Как правило ВП имеет иерархическую структуру, все ВП в составе ВП верхнего уровня принято называть подприбором или SubVI.
Интерфейс пользователя
Наше приложение должно загружать файл конфигурации в ПЛИС. При этом подразумеваем, что к компьютеру одновременно может быть подключено несколько FTDI, и некоторые из них потенциально могут быть использованы для конфигурирования ПЛИС. Предлагаю выбор устройства сделать из раскрывающегося списка, выбор файла через кнопку с выводом пути. Для старта загрузки добавим кнопку "программировать". Виртуальный светодиодный индикатор отобразит статус операции. Запускаем LabVIEW.
Добавляем требуемые элементы на переднюю панель (Front Panel). Для оформления ВП я буду использовать стиль "Silver". Ниже на рисунке показан результат, все элементы в их первозданным состоянии. По названию, при необходимости, их достаточно легко найти в палитре.
Редактируем элементы: переносим, растягиваем, добавляем надписи — приводим в потребный вид. Здесь, на самом деле, требуется рука дизайнера, но как смог:
И блок-диаграмма:
Обращаю внимание. У каждого элемента есть два свойства, которые определяют название — это Label и Caption.
Первое свойство задает имя элемента, под этим именем он будет отображаться на блок-диаграмме. Label в отличии от Caption не может быть изменен в процессе выполнения ВП. В связи с этой особенностью я рекомендую в своих ВП на передней панели скрывать Label, а отображать Caption. Для Label придумывать осмысленное имя, как переменной в классическом языке программирования, желательно на латинской раскладке. Для Caption вводим уже человеко ориентированное имя, оно может быть достаточно длинным, включать пробелы и, при необходимости, на русском языке. Средствами LabVIEW можно настроить шрифт отображения Caption. Должен сказать, что никто не заставляет нас возиться с Caption: любая надпись может быть сделана непосредственно на FP в любом свободном месте.
Функционирование ВП реализуем по классической схеме: цикл While и обработчик событий. На блок-диаграмму добавляем цикл While
(Programming -> Structures -> While Loop), и структуру обработчика событий (Programming -> Structures ->Event Structure).
Структура ожидает наступления события, затем выполняет соответствующий обработчик. Структура событий имеет одну или несколько поддиаграмм — обработчиков событий, так называемых кейсов (case), один из которых выполняется при наступлении события. С помощью терминала в левом верхнем углу можно задать количество миллисекунд, в течение которых структура ожидает события. Если в течении этого времени ни одно событие не произошло, то выполниться поддиаграмма "Timeout". По умолчанию установлено значение минус 1, это означает, что время ожидания никогда не истекает.
- Метка селектора событий указывает, какие события вызывают выполнение текущего кейса. Для просмотра других обработчиков можно щелкнуть по стрелке вниз рядом с именем события.
- Терминал
timeout
задает количество миллисекунд для ожидания события. Если значение отлично от -1, то обязательно должна быть реализована поддиаграмма Timeout. - Терминал для ввода динамических событий. Данный терминал по умолчанию не отображается. Чтобы отобразить его нужно в контекстном меню выбрать пункт "Show Dynamic Event Terminals".
- Узел данных события. Когда происходит событие LabVIEW генерирует данные, связанные с этим событием. Этот узел предоставляет эти данные обработчику. Мышкой можно изменить размер узла по вертикали и выбрать необходимые элементы. Некоторые данные, например
Type
иTime
, являются общими для всех событий, другие, как напримерChar
иVKey
, зависят от типа настроенного события. - Узел фильтра событий определяет данные события, которые можно изменить, прежде чем пользовательский интерфейс обработает эти данные. Этот узел отображается только в тех обработчиках, в которых доступна фильтрация. Можно подключить и изменить элементы из узла данных события в узел фильтр событий. Можно также изменить данные события, подключив новые значения к клеммам узла. Можно полностью отменить реакцию интерфейса на событие, если подать
true
на терминалDiscard?
. Если не подключить значение к элементу фильтра, то этот элемент данных остается без изменения. - Как и структура
Case
, структура событий поддерживает туннели. Если добавить туннель в одном кейсе, он автоматически будет создан для каждого обработчика. Однако по умолчанию нет необходимости подключать выходные туннели структуры событий в каждом обработчике. Все туннели без подключения используют значение по умолчанию для типа данных туннеля. Потенциально это может привести к трудно обнаруживаемым ошибками. Можно вернуть режим, при котором туннель должен быть связан проводом во всех обработчиках, для этого в контекстном меню выбираем "Use Default If Unwired".
Первым делом позаботимся о том, как будет происходить завершение цикла и выход из программы. Самый простой способ — это добавить кнопку "стоп", которая остановит цикл, но, на мой взгляд, программу принято завершать красным крестиком в верхнем углу окна.
В структуру Event Structure
добавляем соответствующий обработчик. В контекстном меню выбираем "Add Event Case". В качестве источника события выбираем "This VI", событие указываем "Panel Close?"
Обращаю внимание, если выбрать "Panel Close" (без вопросика), то это событие без фильтра, его невозможно отменить. А если с вопросом, то при нажатии на крестик, мы можем перехватить управление и самостоятельно корректно завершить программу. В обработчике "Panel Close?" через туннель соединяем булеву константу true
с терминалом остановки цикла while
. На вход "Discard?" также подаем true
.
Теперь не очевидный нюанс: если текущий ВП будет запускаться не в среде LabVIEW, а в виде откомпилированного приложения, то нажатие крестика не закроет окно, а остановит приложение, чего нам не нужно. Для решения этой проблемы после завершения основного цикла, выполним проверку, находимся ли мы в среде разработки или в режиме программы, если все же в режиме программы, то завершим приложение. Для того, чтобы понять, где выполняется текущий ВП, воспользуемся узлом свойств (Programming -> Application Control -> Property Node). Данный узел предоставляет доступ к свойствам объекта по ссылке на этот объект. В данном случае мы должны получить ссылку на все приложение в целом. Выбираем константу VI Server Reference
из той же палитры. После того, как константа установлена на блок-диаграмму, нужно сменить ее тип на This Application
(левая кнопка мыши). Соединяем получившуюся ссылку с узлом свойств. Выбираем свойство Application:Kind Property
— возвращает тип системы LabVIEW в которой запущен текущий ВП. Выход свойства соединяем со структурой выбора (Case Structure), добавляем кейс "Run Time System", где завершаем приложение (Quit LabVIEW
). Для того, чтобы этот блок выполнился после цикла, а не до, входной терминал error in
соединяем с циклом через туннель.
Теперь при запуске программы ее можно остановить нажатием на крестик окна. Запущенная программа выглядит так:
На мой взгляд, много лишнего. Заходим в свойства VI (меню File -> VI Properties ), выбираем категорию "Window Appearance", устанавливаем Custom.
Отключаем отображение меню (в режиме редактирования меню остается), отключаем scroll bar, скрываем панель инструментов, когда приложение запущено (Show toolbar when running). Запрещаем менять размер окна, разрешаем сворачивать и минимизировать окно. Вот так-то лучше:
Конечно, стоило бы убрать надпись "National Instruments. LabVIEW Evaluation Software", но для домашнего компьютера покупать лицензию я пока не желаю, смиримся с надписью и обойдемся 45 дневным пробным периодом.
Естественно, можно настроить цвет фона и каждого элемента, подобрать шрифты, но я не дизайнер, а что-то подсказывает мне, что сделаю только хуже.
Список приборов
ВП должен предложить пользователю список подключенных к компьютеру устройств, пригодных для прошивки ПЛИС, для того, чтобы пользователь смог выбрать нужное. В библиотеке FTD2XX для этого предназначены функции FT_CreateDeviceInfoList
и FT_GetDeviceInfoDetail
. Как уже обсуждалось в предыдущей статье, для использования API FTD2XX можно использоваться библиотеками драйвера. В LabVIEW есть удобный механизм взаимодействия с динамическими библиотеками — узел вызова функции из библиотеки (Call Library Function Node), найти его можно в палитре "Connectivity -> Libraries & Executables". Узел вызова функции следует настроить: во-первых, указываем путь к dll (вкладка "Function"), после чего система сканирует библиотеку и в списке "Function Name" предложит выбрать имя функции — выбираем FT_CreateDeviceInfoList
, соглашение о вызове (Calling convention) — выбираем stdcall(WINAPI). Во-вторых, на вкладке "Parameters" нужно ввести список параметров функции, где первый элемент списка — возвращаемое значение. Тут перед глазами неплохо бы держать документацию на API, или заголовочный файл. При настройке параметров в области "Function prototype" отображается прототип импортируемой функции. Когда сигнатура из документации совпадает с настроенным прототипом, жмем ОК.
Положим, что сканирование должно выполняться раз в секунду. Узел вызова располагаем в структуре обработчика событий на вкладке "Timeout", время ожидания устанавливаем 1000 мс. Добавляем индикаторы на выводы узла, и если все сделано правильно, то при запуске ВП, должно отображаться число подключенных устройств с FTDI:
Аналогичным образом создаем узел для функции FT_GetDeviceInfoDetail
. Прототип функции имеет вид:
FTD2XX_API
FT_STATUS WINAPI FT_GetDeviceInfoDetail(
DWORD dwIndex,
LPDWORD lpdwFlags,
LPDWORD lpdwType,
LPDWORD lpdwID,
LPDWORD lpdwLocId,
LPVOID lpSerialNumber,
LPVOID lpDescription,
FT_HAN
При описании параметров следует обратить внимание, что lpdwFlags
, lpdwType
, lpdwID
, lpdwLocId
передаются как указатели на uint32
. Параметры lpSerialNumber
и lpDescription
— есть суть байтовые строки (массивы типа char
с нуль-терминатором). Параметры такого вида в узле вызова могут быть оформлены различными способами, можно ввести их в массив 8-битных слов, но я думаю, наиболее удобно сразу указать, что это строка, и задать ожидаемый размер. В этом случае на выходе сразу будет годная "лабвьюшная" строка и не потребуется каких-то дополнительных преобразований.
Call Library Function
Данная функция возвращает информацию по порядковому номеру dwIndex
. В случае если к компьютеру подключено несколько FTDI, для того чтобы прочитать информацию для каждого преобразователя, функцию нужно вызвать в цикле. Число итераций цикла нам даст предыдущая функция FT_CreateDeviceInfoList
.
Есть неприятная особенность: все порты узла вызова должны быть подключены хотя бы с одной стороны. Поэтому в цикле сделан туннель и для тех терминалов вывода, которые мы не собираемся использовать.
В массиве Types
содержаться типы чипов FTDI, они нам нужны для того, чтобы ограничить выбор только теми, которые поддерживают MPSSE и потенциально могут быть использованы для программирования ПЛИС. Однако оперировать "магическими цифрами" неудобно — предлагаю оформить типы FTDI в виде enum
. Тем более, что такой enum
уже есть в заголовочном файле ftd2xx.h. В LabVIEW для создания перечисления можно использовать два элемента управления: "Text Ring" и собственно сам "Enum". Оба содержат списки строк с числовыми значениями, между которыми можно переключатся. Основное различие в том, что "Enum" требует, чтобы числовые значения были целыми последовательными числами, в то время, как в "Text Ring" больше свободы — можно задать любые значения.
Вводим вручную значения, жаль что нет функции импорта enum
из Си
На фронт-панели так выглядит индикатор этого типа
Выбор значения осуществляется левой кнопкой мыши
Для того, чтобы связать и синхронизировать все будущие экземпляры созданного списка, удобно воспользоваться функцией "Make Type Def." (выбор через контекстное меню элемента). В результате будет создан пользовательский тип данных. Новый тип помещается в отдельный файл с расширением *.ctl. Редактирование этого файла приведет к изменению всех экземпляров этого элемента. Думаю, не стоит объяснять, насколько это может быть удобно. Доступ к файлу определения можно получить из контекстно меню экземпляра, выбрав пункт "Open Type Def", в этом же меню следует обратить внимание на пункты "Auto-Update from Type Def." и "Disconnect from Type Def".
Меняем индикатор Types
на массив индикаторов типа FTDI Type
и в результате при запуске ВП отображается тип подключенного конвертера:
Найдено три устройства
Легко заметить, что функционал получившегося кода в кейсе "Timeout" носит законченный характер, следовательно его можно вынести в отдельный SubVI. Выделяем элементы, которые хотим перенести в подприбор и в главном меню Edit выбираем пункт "Create SubVI".
Двойной клик по новому subVI запустит окно его редактирования. В первую очередь сохраняем его и даем осмысленное название, например "FT_GetDeviceInfo". Настроим терминалы ввода-вывода. Для этого служит панель соединений (Connector Pane):
Панель представляет собой набор терминалов, соответствующих элементам управления и индикаторам VI.
Если выделить терминал на панели соединений, то подсветится соответствующий элемент на передней панели. Если выделить пустой терминал, а затем щелкнуть по элементу на передней панели, то элемент привяжется к терминалу, однако до этого он не должен быть закреплен за каким-то другим терминалом. В контекстном меню можно по отдельности или для всех разом отсоединить терминалы от элементов, можно сменить паттерн панели в целом.
Мне не нравится как были назначены терминалы при создании текущего subVI, поэтому я выбираю пункт "Disconnect All Terminals" и размечаю вручную. Рекомендую входные терминалы размещать слева, а выходные справа, опциональные входные можно разместить сверху, а опциональные выходные снизу. Это позволит обеспечить хорошую читаемость кода и визуальный порядок на блок-диаграмме.
Для контроля ошибок создадим два дополнительных элемента Error in
и Error out
. Тема контроля ошибок в LabVIEW весьма обширная и выходит за рамки этой статьи, потому ограничимся минимумом пояснений и будем придерживаться принципа "делай как я". Итак, создаем два терминала для ошибок — входной и выходной.
Правая кнопка мыши по терминалу ошибки любого узла:
В LabVIEW принято входной терминал ошибки на панели соединений размещать слева снизу, а выходной — справа снизу.
Будет удобнее выходные данные объединить в структуру. Для вывода сделаем два массива: первый массив будет содержать все найденные устройства FTDI, второй массив — только те, которые умеют MPSSE и теоретически могут быть использованы для конфигурирования ПЛИС.
Последним штрихом при создании подприбора настроим иконку. Двойной клик по иконке в верхнем правом углу окна запускает редактор. Стараемся создавать какое-то осмысленное изображение, позволяющее однозначно интерпретировать назначение прибора на блок-диаграмме.
До этого момента раскрывающийся список с названием "Выберите устройство" был пуст, теперь у нас есть данные, чтобы наполнить его. Создаем для списка узел свойств со свойством "Strings[]" (контекстное меню -> Create -> Property Node -> Strings[]). Любое свойство доступно для записи и для чтения, выбор текущего режима выполняется в контекстном меню Property Node. При создании узла по умолчанию свойства сконфигурированы на чтение. Меняем на запись: "Change To Write".
Из массива структур выделяем массив с описанием и подаем его на Strings[]
. Выделить массив можно с помощью цикла For Loop
.
После запуска ВП нажав левую кнопку на элементе "Выберите устройство" можно указать прибор для конфигурирования. При этом список устройств обновляется два раза в секунду. Конечно можно было бы обновлять свойство, только если список обновился, но пока это выльется в лишнее загромождение блок-диаграммы.
Передняя панель
Ранее я забыл упомянуть такую интересную особенность, циклу For не обязательно указывать число итераций в явном виде, достаточно создать входной туннель массива и цикл выполниться для каждого элемента, это поведение напоминает цикл foreach
в C++11. Однако нужно быть осторожным, когда на вход цикла поступает более одного массива.
В структуру событий добавляем обработчик нажатия кнопки "программировать". Пока у нас нет VI, отвечающего за загрузку файла в ПЛИС, сделаем подприбор "заглушку". Положим, что он принимает на вход путь к файлу конфигурации и дескриптор FTDI, а по результатам работы возвращает статус прошивки: успешна или нет. А чтобы было интереснее испытать интерфейс программы, сделаем этот статус случайным.
Передняя панель
Блок-диаграмма
Дескриптор FTDI передадим на вход заглушки через свойство списка (контекстное меню -> Create -> Property Node -> Ring Text -> Text), для передачи пути к файлу элементу "File Path" создадим локальную переменную (контекстное меню -> Create -> Local Variable) и настроим ее на чтение (Change To Read). А выход статуса соединим непосредственно с индикатором Status
. Кнопку Programm
перетащим в обработчик события. Это хорошая практика, помещать элементы на которые настроено событие в обработчики — теперь двойной клик по этому элементу на передней панели покажет не только соответствующий элемент на блок-диаграмме, но и обработчик события, связанный с этим элементом.
Теперь по нажатию кнопки "Программировать" индикатор становится либо зеленым (успех), либо темно зеленым (не успех). Не слишком наглядно. В свойствах индикатора меняем цвет "Off" на красный. Так лучше. Если индикатор зеленый, то мы можем утверждать, что ПЛИС на выбранном устройстве сконфигурирован файлом, путь к которому указан в окне:
Передняя панель
Но это утверждение становится ложным, если мы сменили файл или выбрали другое устройство. При этом мы не можем окрасить индикатор в красный цвет, так как ошибки программирования не произошло. Удобным решением будет в случае смены файла или устройства, подчеркнуть, что значение индикатора не актуальны — затемнить его. Для это можно воспользоваться свойством индикатора Disabled
. Это свойство может принимать три значения Enabled
— нормальное отображение, пользователь может управлять объектом; Disabled
— объект отображается на передней панели как обычно, но пользователь не может управлять объектом; Disabled and Grayed Out
— объект отображается на передней панели затемненным, и пользователь не может управлять объектом.
Создаем обработчики события для Devices list
и File Path
, и в них затемняем индикатор статуса, а в обработчике кнопки "Программировать" присваиваем свойству Enabled
.
Сделаем удобным для пользователя поиск файла конфигурации — настроим окно просмотра файлов. Заходим в свойства элемента File Path
, на вкладке "Browse Options" заполняем Prompt, Pattern Label, указываем фильтр типа файла (Pattern) и название для кнопки (Button Text).
Отображаются только rbf файлы
Создание пользовательского интерфейса можно считать законченным.
Запущенный загрузчик
С чем познакомились сегодня?
В данной статье, на примере создания функционально полного приложения с минималистическим интерфейсом, я попытался показать различные подходы к работе в LabVIEW.
В итоге мы коснулись тем:
- Настройка свойства виртуального прибора. Узнали как отключить ненужные элементы типа меню.
- Структура виртуального прибора: цикл с событиями.
- Работа со структурой событий.
- Оформление подприбора.
- Импорт функций из dll.
- Работа со свойствами элемента.
В следующей статье мы углубимся в работу с API FTD2XX на примере MPSSE. Загрузим в ПЛИС бинарник конфигурации.
Материалы по теме
- Загрузка конфигурации в ПЛИС через USB или разбираем FTDI MPSSE
- labview_mpsse. Репозиторий с проектом.
- Учебный стенд для ЦОС. Железо для опыта
- Software Application Development D2XX Programmer's Guide. Руководство по API D2XX.
Комментарии (7)
Iryaz
25.10.2018 18:21+1Интересно, в программе на LabView можно выделять отдельные потоки для каждой операции?
red_perez
25.10.2018 19:05+1Интересно, в программе на LabView можно выделять отдельные потоки для каждой операции?
Да, моя картинка сверху это такой случай 2 независимые петли While = 2 потока.
Если не ошибаюсь начиная с версии 2015 в цикл FOR встроена нативная многопоточность, по правой кнопке включаете configure iteration parallelism и там появляются дополнительные терминалы P,C которые позволяют разделить вычисления на разные потоки.
Shamrel Автор
25.10.2018 19:26Круто! Действительно есть такое. Не знал. Я недавно только поставил 18-версию. Когда-то давно работал много на 12-ой, там такого не помню.
red_perez
Я на Лабьвью уже >20 лет работаю, ну да, соглашусь, отношение не всегда однозначное, однако бывают довольно хлебные места где этот редкий скилл очень даже востребован.
По сабжу добавлю 5 копеек, я за правило взял в случае применения Event структур делать программу по схеме — Producer-Consumer.
То есть Ивент генерит строго говоря одни ивенты, а обработка происходит в отдельной петле. Но это так — «бэст практис», чисто для поддержания разговора.
Shamrel Автор
Интересно было бы взглянуть на пример.
red_perez
2 параллельные петли,
— верхняя занимается исключительно регистрацией ивентов и генерит очередь комманд на обработку.
— нижняя петля спит и активируется когда в очереди появляются комманды
Это бест практис для интерфейсов, все что стоит внутри Event Structure затормаживает регистрацию следующего ивента пока старый не обработан полностью. Потому выносим из Event Structure все что возможно.
Иначе интерфейс тормозит и лагает.
Shamrel Автор
Согласен, красивое решение. Актуально, только если обработчик занимает много времени. В рассматриваемой задаче, даже непосредственно загрузка бинарника происходит за ~10 мс, потому усложнять не буду.