Хабрахабр

Пишем загрузчик ПЛИС в LabVIEW. Часть 2

Часть 1 Загрузка конфигурации в ПЛИС через USB или разбираем FTDI MPSSE
Пишем загрузчик ПЛИС в LabVIEW.

В этот раз мы познакомимся с новыми приемами работы в LabVIEW, разберем особенности обработки ошибок и завершим проект: реализуем протокол загрузки файла конфигурации в ПЛИС. В первой статье мы обкатали алгоритм загрузки на старом добром Си, во второй статье разобрались, как в LabVIEW можно организовать программу и реализовать простой интерфейс пользователя.

Обработка ошибок

Несмотря на алгоритмическую простоту (функции вызываются друг за дружкой) требуется импортировать довольно много элементов API D2XX: FT_OpenEx, FT_ResetDevice, FT_Purge, FT_SetUSBParameters, FT_SetChars, FT_SetTimeouts, FT_SetLatencyTimer, FT_SetFlowControl, FT_SetBitMode. Открываем сишный исходник, анализируем функцию MPSSE_open. Этот узел имеет выделенные терминалы для контроля ошибок. Как было показано в предыдущей статье, импорт функций осуществляется с помощью узла Call library Function. Большинство встроенных ВП неукоснительно следует ему. В LabVIEW есть одно простое правило: все ВП должны отслеживать ошибки и сообщать об ошибках, возвращаемых терминалами ошибок. В LabVIEW нет строгой последовательности выполнения приборов на блок-диаграмме: прибор выполняется, когда на его входах будут готовы данные. Надеюсь всем понятно, насколько важно контролировать и обрабатывать ошибки особенно на этапе отладки, однако есть еще одна причина, почему это так важно, неочевидная "классическим" программистам. А как быть, если нет передачи данных, а ВП выполняют независимые действия? Если с выхода одного ВП данные передаются на вход другого ВП, то понятно, что в начале отработает первый ВП, только после него второй. Конечно можно воспользоваться громоздкой "Flat Sequence Structure", но гораздо удобнее соединить приборы между собой потоком ошибок.

Первый — это ошибка непосредственно импорта — возвращает сам блок Call library Function. При импорте функций D2XX мы сталкиваемся с двумя типами ошибок. Все возможные значения описаны в виде enum'а в заголовочном файле ftd2xx.h. Второй — ошибка самой библиотеки, возвращается почти каждой функцией через FT_STATUS. Хотя достаточно знать, что значение FT_OK — отсутствие ошибки, а все остальные значения — коды ошибок, хотелось бы отследить не только сам факт ошибки, но и какая ошибка произошла и где именно она произошла.

Это такой специальный выделенный тип данных, в LabVIEW есть множество ВП и функций для работы с ним. В LabVIEW данные об ошибке распространяются через кластеры error. Статус показывает, произошла ли ошибка, код ошибки определяет ее тип и используется специальными ВП для формирования отчета. Кластер ошибок состоит из трех элементов: логическая переменная — отображает статус, целое знаковое число — код ошибки, строка — источник ошибки. В LabVIEW принято, если статус равен TRUE, то это ошибка, если статус равен FALSE, но код не равен нулю и строка описания не пустая, то это предупреждение, если же статус FALSE, код равен нулю и строка пустая — нет ошибки. Строка дает более развернутое представление о том, где именно произошла ошибка.

Для каждого типа ошибок выделен специальный диапазон значений кодов. LabVIEW содержит внутреннюю базу данных, в которой код каждой ошибки связан с ее описанием. Для ошибок, определяемых пользователем зарезервировано два диапазона от –8999 до –8000 и от 5000 до 9999. Например, для ошибок связанных с работой сети выделено несколько диапазонов: от –2147467263 до –1967390460, от 61 до 65, от 116 до 118 и 122, 1101, 1114, 1115, 1132 до 1134, от 1139 до 1143 и от 1178 до 1185. Из этих диапазонов мы можем выбрать значения для кодов ошибок библиотеки D2XX.

Большинство функций и ВП в LabVIEW, получив на вход Error In статус TRUE, не выполняют свой код, а передают информацию об ошибке на терминал Error Out. Создадим ВП, принимающий на вход статус функции D2XX и конвертирующий этот статус в кластер ошибки LabVIEW. Желательно, чтобы и наши ВП вели себя аналогично. Это позволяет эффективно передать информацию о источнике через всю цепочку до обработчика ошибок, исключив выполнение кода в аварийном режиме.

Оформим список статусов D2XX в виде enum и вынесем его в отдельный тип (в предыдущей статье мы так поступили с типами FTDI).

enum FT_Status

На переднюю панель добавляем два кластера Error In и Error Out, найти их можно в палитре "Array, Matrix & Cluster". Новый ВП сохраняем под именем FT_error.vi. На блок-диаграмму добавляем структуру Case, на вход Case selector подаем кластер Error In, после чего структура Case меняет цвет и делиться две поддиаграммы: "No Error" — зеленый цвет, и "Error" — красный цвет. Подсоединяем их к терминалам на панели соединений в нижнем левом и нижнем правом углах соответственно, как уже говорилось в прошлой статье, это принятое в LabVIEW расположение терминалов потока ошибок. А в зеленом случае добавляем еще один Case, он в зависимости от статуса будет определять, следует ли создавать ошибку (статус не равен FT_OK), или оставить все как есть: пропустить входной кластер ошибки на выход без изменения. Внутри случая Error передаем кластер ошибок от терминала селектора напрямую к выходному туннелю на правой границе.

Этот SubVI в описание ошибки добавляет цепочку вызова, благодаря чему мы сможем определить не только что произошло, но еще и где это случилось. Для того, чтобы технично преобразовать код ошибки в кластер, можно использовать ВП Error Cluster From Error Code VI.

Text". Чтобы выделить текст, соответствующий входному статусу (FT_Status), используем блок свойств: выбираем "RingText. Текст ошибки передаем на вход error message ВП Error Cluster From Error Code VI.
Не забываем нарисовать "говорящую" иконку.

FT_error.vi


Передняя (фронт) панель подприбора

На входе ошибка
Блок-диаграмма.

На входе нет ошибки и статус равен FT_OK
Блок-диаграмма.

На входе нет ошибки, но статус отличен от FT_OK
Блок-диаграмма.

Для испытания FT_error можно создать пустой ВП, добавить туда созданный ВП и посмотреть, как будет меняться значение при запуске, если подавать различные статусы.

Тест FT_error.vi


Передняя (фронт) панель прибора


Блок-диаграмма

А кластер ошибок будет проходить через все ВП по всей иерархии вызова. Теперь, после любого вызова функции из API D2XX, мы будем использовать SubVI FT_error.vi.

Диалоговое окно — самый простой и наиболее популярный способ отчета об ошибках. В ВП верхнего уровня мы должны определиться, что делать с обнаруженной ошибкой: можно вывести сообщение в диалоговом окне, записать его в файл отчета, проигнорировать или просто "тихо" завершить приложение. В каждом ВП по умолчанию активирован режим автоматической обработки ошибок (Enable automatic error handling, находится в категории Execution меню ВП Properties). А еще он удобен для начинающего программиста, так как делать ничего не надо. Если терминал Error Out узла соединен, то поток ошибки распространяется, как запрограммировано, и никаких дополнительных действий не происходит. Работает он так: если в каком-то узле выходной терминал Error Out никуда не подключен, и в этом узле происходит ошибка, то LabVIEW приостанавливает выполнение приложения и выдает диалоговое окно. При этом информацию об ошибке мы можем использовать для завершения программы. Однако окно сообщения можно вызвать программно, для этого нужно воспользоваться ВП General Error Handler и Simple Error Handler (находятся в палитре "Dialog&User Interface"). На блок-диаграмме это выглядит примерно так:


Картинка кликабельная

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

Окно отчета

Открыть и закрыть FTDI

Создаем новый VI. Итак, возвращаемся к функции MPSSE_open. Добавляем структуру выбора и на селектор подаем вход Error In. Первым делом, добавляем терминалы для потока ошибок. Все узлы Call Library Function Node соединяем в цепочку потоком ошибок. В зеленом кейсе делаем импорт функций в порядке и с параметрами как в Сишном прототипе. В красном кейсе через тунель соединяем Error In с выходным терминалом ошибки.


Картинка кликабельная


ВП MPSSE_open.vi

На вход SubVI подается строка с описанием FTDI (Description), на выходе — Handle и инициализированный чип FTDI в режиме MPSSE.

Создадим ВП, завершающий работу с FTDI и можно уже проверить работоспособность на железе.

FT_Close.vi


Блок-диаграмма


Передняя панель

Добавляем на его блок-диаграмму MPSSE_open.vi и FT_Close.vi. В предыдущей статье для отладки интерфейса мы сделали ВП заглушку SP_FT_MPSSE_FPGA.vi, сейчас настало время наполнить его. На данном этапе достаточно сложно оценить, верно ли прошла инициализация, однако ненулевое значение Handle на выходе MPSSE_open.vi и отсутствие ошибки нам уже о многом скажет.


Блок-диаграмма SP_FT_MPSSE_FPGA.vi

Это удобный инструмент отладки, позволяющий вывести значение данных на любом (почти любом) проводе в процессе выполнения прибора. Для того, чтобы посмотреть значение Handle можно воспользоваться окном "Probe Watch Window". Откроется окно "Probe Watch Window", а на линии появится циферка с номером пробы. Для того чтобы установить пробу на линию, нужно в контекстном меню этой самой линии выбрать пункт "Probe". На рисунке выше это "3".

Probe Watch Window


На линии Handle значение 698389336

Запускаем ВП верхнего уровня, подключаем к компьютеру отладочную плату. Отлично! Только в окне "Probe Watch" появилось значение Handle. В списке "Выберите устройство" появляется описание подключенной микросхемы FTDI, нажимаем кнопку "Программировать" и… ничего не происходит. И это хорошо.

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

Окно отчета

После нажатия кнопки "Continue", ВП завершает свою работу.

Модифицируем кейс "Timeout" обработчика событий. Стоит запретить нажимать кнопку, если не найдено ни одного устройства. Создаем для "Программировать" свойство Disabled, и, если годных устройств не обнаружено, то отключаем и затемняем кнопку. Напомню, два раза в секунду сканируются подключенные к ПК чипы FTDI, если таковые обнаружены и могут быть использованы для программирования ПЛИС, то через свойство Strings[] их дескрипторы добавляются в Devices list.

Case Timeout


Картинка кликабельна

Осваиваем GPIO

По наторенной дорожке создаем соответствующие VI: FT_Write.vi, FT_Read.vi, FT_Queue.vi. После того, как MPSSE активирован, работа с ним осуществляется через так называемые "op-code", а из функций API D2XX используется только FT_Write, FT_Read и FT_Queue (чтобы узнать статус буфера приемника).

Немного рутины


FT_Write.vi

FT_Write.vi
Блок-диаграмма.


FT_Read.vi

FT_Read.vi
Блок-диаграмма.


FT_Queue.vi

FT_Queue.vi
Блок-диаграмма.

Значение удобно представить в виде массива булевых переменных. Теперь из этих трех кирпичиков выкладываем ВП для чтения параллельного порта и записи в него.

MPSSE_Set_LByte.vi и MPSSE_Get_LByte.vi


MPSSE_Set_LByte.vi

MPSSE_Set_LByte.vi
Блок-диаграмма.


MPSSE_Get_LByte.vi

MPSSE_Get_LByte.vi
Блок-диаграмма.

Каюсь, мне было лениво создавать именованный список для всех op-code, поэтому оставил их в виде Magic Numbers.

Всего используется пять ножек: линии DCLK, DATA[0], nCONFIG должны быть сконфигурированы как выхода, линии nSTATUS, CONF_DONE — как входы. Как говорилось в самой первой статье протокол загрузки ПЛИС "Passive Serial" есть ничто иное как SPI с дополнительной манипуляцией флагами.

Распиновка схемы в виде таблицы

FPGA pin

Pin Name

Pin

MPSSE

Direction

default

DCLK

BDBUS0

38

TCK/SK

Out

0

DATA[0]

BDBUS1

39

TDI/DO

Out

1

nCONFIG

BDBUS2

40

TDO/DI

Out

1

nSTATUS

BDBUS3

41

TMS/CS

In

1

CONF_DONE

BDBUS4

43

GPIOL0

In

1

Первым делом создаем Enum с порядковыми номерами ножек в порту, сохраняем в виде "Strict Type Def" в файл SP_LBYTE_BITS.ctl. Нам понадобится ВП, который сможет менять значение на выбранной ножке не затрагивая все остальные. Считываем текущее значение параллельного порта с помощью MPSSE_Get_LByte.vi, с помощью функции Replace Array Subset модифицируем нужный бит и записываем значение обратно в порт (MPSSE_Set_LByte.vi). Создаем новый ВП, добавляем привычные терминалы потока ошибок.

SP_Set_Flag.vi


SP_Set_Flag.vi

SP_Set_Flag.vi
Блок-диаграмма.


Enum SP_LBYTE_BITS.ctl

Как только ПЛИС будет готова к приему данных, она сформирует высокий уровень на линии nSTATUS. Для начала конфигурации контроллер MPSSE должен генерировать переход из низкого уровня в высокий на линии nCONFIG. На блок-диаграмму SP_FT_MPSSE_FPGA.v добавляем управление линией nCONFIG — после инициализации MPSSE подаем низкий уровень, а затем высокий. На данном этапе у нас все готово для эксперимента в железе. После каждой операции (для отладки) считываем состояние ножек порта.

SP_FT_MPSSE_FPGA.vi


Во время запуска


Блок-диаграмма

Но не будет лишним проконтролировать это с помощью осциллографа. В целом, во время запуска VI видно, что ПЛИС реагирует на переход на линии nCONFIG — на ножке nSTATUS устанавливается ноль, а затем единица. Канал А (синий трек) я ставлю в контрольную точку цепи nCONFIG, канал B (красный трек) — цепь nSTATUS. Годится почти любой двуканальный осциллограф с возможностью запуска по триггеру (ждущий режим). Триггер настроен на спадающий фронт канала A.

С подробностями!
Картинка кликабельна.

Работа с файлом

А готовы ли мы передать файл в ПЛИС? ПЛИС готова принять файл конфигурации.

Не скажу, что функционала хватает на абсолютно весь спектр задач, однако базовые операции типа чтение и запись выполняются легко и приятно. LabVIEW содержит обширный набор инструментов для работы с файлами. Для решаемой задачи требуется открыть файл конфигурации, оценить его размер (нам нужно знать, сколько байт отправлять ПЛИС), прочесть его и закрыть. Основной набор VI для работы с файлами можно найти в палитре "File I/O". Используем ВП Open/Create/Replace File, Get File Size, Read from Binary File, Close File, объединяем их цепочкой потока ошибок и refnum — число, типа файлового дескриптора, создается при открытии файла и должно быть передано на вход другим ВП, работающим с этим файлом. Все просто и друг за другом.

В контекстном меню активируем опцию "Hex Display", включаем вертикальный скроллбар (Visible Items -> Vertical Scrollbar) и после запуска наблюдаем содержимое бинарного файла конфигурации. Пока нам некуда утилизировать считанные данные, но если очень хочется проверить работоспособность цепочки, то можно создать индикатор типа String и немножко настроить его.

SP_FT_MPSSE_FPGA.vi

Смотрим на содержимое файла
Передняя панель.

Каринка кликабельная
Блок-диаграмма.

Для того, чтобы свести параллельные потоки в один терминал Error Out, используется функция Merge Errors. На блок-диаграмме ВП образовалось две независимые параллельные линии кода, поэтому для них используются раздельные цепочки ошибок. Если ошибок нет, то возвращает первое попавшееся предупреждение. Эта функция просматривает ошибки на входе сверху вниз (да, там может более двух входных терминалов, растягивается мышкой) и возвращает первую, которую найдет. Важно отметить, что порядок подключения входов Merge Errors определяет приоритет ошибок, и если ошибка возникнет сразу в двух цепочках, то нижняя ошибка будет проигнорирована. Если и предупреждений не обнаружено, то на выходе ошибка отсутствует. К этому нужно относиться внимательно.

LabVIEW: (Hex 0x596) The path is empty or relative. Если мы попытаемся в ВП верхнего уровня нажать кнопку "Программировать" не выбрав файл, то на вход SP_FT_MPSSE_FPGA.vi поступит пустой путь, что вызовет ошибку "Error 1430. И ошибка эта — вовсе не ошибка, а так, невнимательность пользователя. You must use an absolute path." Как говорит мой друг детства: "Пустяки, дело-то житейское!". Для фильтрации ошибки используем ВП "Clear Errors" из палитры "Dialog&User Interface". Останавливать программу и ругаться на него окном с красным крестиком мы не будем, просто удалим ошибку с этим кодом из потока и в диалоговом окне порекомендуем пользователю определиться с файлом. Для вывода сообщения — "One Button Dialog".

Блок-диаграмма


Картинка кликабельна

Загрузка конфигурации

Длина кодируется за вычетом единицы. Для последовательной передачи данных процессору MPSSE нужно послать op-code 0x18, аргументами команды будет длина передаваемой последовательности (два байта, начиная с младшего), и сама последовательность данных. Отправку блока данных оформим в виде ВП MPSSE_send.

MPSSE_Send.vi


MPSSE_Send.vi


Блок-диаграмма

Размер входного буфера (Array Size) преобразовываем к двухбайтовому типу U16, отнимаем единицу, меняем местами младший и старший байт (Swap Bytes) — отправлять длину нужно начиная с младшего, и преобразовываем двухбайтовое число в однобайтный массив (Type Cast).

Это такой универсальный преобразователь типов, сообразительность которого порою сильно удивляет. Функция Type Cast заслуживает отдельного внимания. Если коротко, то:


Наглядно для програмиста

Эта функция позволяет выполнять преобразование между несовместимыми типами данных, при этом функция не брезгует выравниванием входных данных и даже удалением "лишних" частей. Однако это не просто приведение данных к другому типу, это еще и эвристическая интерпретация. Для начинающего разработчика LabVIEW Type Cast может стать палочкой-выручалочкой, но с взрослением, лучше от такого преобразователя отказаться — сильно много скрыто от глаз и может стать источником непредвиденных ошибок. Если запрошенный тип данных требует памяти больше, чем у входных данных, то функция выделит недостающий объем. Лучше использовать более явные методы преобразования, например, Coerce To Type.

Воспользуемся функцией Array Subset, эта функция выделяет из массива подмассив начиная с элемента index и длинною length. При инициализации процессора MPSSE, мы задали максимально допустимый размер буфера для передачи данных в 65536 байт, следовательно файл конфигурации мы должны разделить на фрагменты, размер которых не превышает указанный размер. Как только не удастся от основного массива отщипнуть 65536 байта, берем все, что осталось, отправляем и останавливаем цикл. Разбивать будем в цикле While, каждую итерацию индекс будем увеличивать на 65536, между итерациями значение передадим через сдвиговый регистр.

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

SP_FT_MPSSE_FPGA.vi


Картинка кликабельна

Для того, чтобы понять успех прошивки, считаем флаги, и, если CONF_DONE установлен в единицу, рапортуем ВП верхнему уровня, что все ОК.

Осталось убедиться, что ПЛИС успешно прошивается, а плата счастливо мигает светодиодиками. Программа завершена.

Про именование ВП

Я привык имена файлам ВП давать на основе их места в иерархии разрабатываемого ПО. Ни для кого не секрет, что в классическом программировании всем пользовательским объектам и функциям нужно давать осмысленные имена, то же можно сказать и о LabVIEW, особенно, если в роли объектов выступает SubVI. В текущем приложении можно выделить четыре уровня абстракции:

  • Самый низкий уровень — это ВП, выполняющие непосредственное взаимодействие с FTDI, в большинстве своем они сводятся к вызову соответствующей функции из API D2XX. В своем проекте имена ВП этого уровня я начинал с префикса "FT", например FT_Close.vi или FT_Read.vi.
  • Второй уровень — это взаимодействие с процессором MPSSE. Имена ВП этого уровня начинаются с префикса "MPSSE". Пример: MPSSE_open.vi, MPSSE_Set_LByte.vi, MPSSE_Get_LByte.vi.
  • Третий уровень — это реализация протокола "Passive Serial" поверх MPSSE. Все файлы имеют префикс "SР". Например, SP_FT_MPSSE_FPGA.vi (жуткое имя, состоящее из аббревиатур) и SP_LBYTE_BITS.ctl.
  • Уровень приложения. ВП верхнего уровня. Имя может быть произвольным, человекоориентированным.

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

Заключение

Может показаться, что описание процесса излишне подробно, но уж очень не хотелось создать очередное пособие по рисованию совы.

Рисуем сову

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

Материалы по теме

  1. Блюм П. LabVIEW: стиль программирования. Пер. с англ. под ред. Михеева П.– М.:
    ДМК Пресс, 2008 – 400 с.: ил.
  2. labview_mpsse. Репозиторий с проектом.
  3. Учебный стенд для ЦОС. Железо для опыта
  4. Software Application Development D2XX Programmer's Guide. Руководство по API D2XX.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть