Загрузка конфигурации в ПЛИС через USB или разбираем FTDI MPSSE
Пишем загрузчик ПЛИС в LabVIEW. Часть 1
В первой статье мы обкатали алгоритм загрузки на старом добром Си, во второй статье разобрались, как в LabVIEW можно организовать программу и реализовать простой интерфейс пользователя. В этот раз мы познакомимся с новыми приемами работы в LabVIEW, разберем особенности обработки ошибок и завершим проект: реализуем протокол загрузки файла конфигурации в ПЛИС.
Обработка ошибок
Открываем сишный исходник, анализируем функцию MPSSE_open. Несмотря на алгоритмическую простоту (функции вызываются друг за дружкой) требуется импортировать довольно много элементов API D2XX: FT_OpenEx
, FT_ResetDevice
, FT_Purge
, FT_SetUSBParameters
, FT_SetChars
, FT_SetTimeouts
, FT_SetLatencyTimer
, FT_SetFlowControl
, FT_SetBitMode
. Как было показано в предыдущей статье, импорт функций осуществляется с помощью узла Call library Function
. Этот узел имеет выделенные терминалы для контроля ошибок. В LabVIEW есть одно простое правило: все ВП должны отслеживать ошибки и сообщать об ошибках, возвращаемых терминалами ошибок. Большинство встроенных ВП неукоснительно следует ему. Надеюсь всем понятно, насколько важно контролировать и обрабатывать ошибки особенно на этапе отладки, однако есть еще одна причина, почему это так важно, неочевидная "классическим" программистам. В LabVIEW нет строгой последовательности выполнения приборов на блок-диаграмме: прибор выполняется, когда на его входах будут готовы данные. Если с выхода одного ВП данные передаются на вход другого ВП, то понятно, что в начале отработает первый ВП, только после него второй. А как быть, если нет передачи данных, а ВП выполняют независимые действия? Конечно можно воспользоваться громоздкой "Flat Sequence Structure", но гораздо удобнее соединить приборы между собой потоком ошибок.
При импорте функций D2XX мы сталкиваемся с двумя типами ошибок. Первый — это ошибка непосредственно импорта — возвращает сам блок Call library Function
. Второй — ошибка самой библиотеки, возвращается почти каждой функцией через FT_STATUS
. Все возможные значения описаны в виде enum'а в заголовочном файле ftd2xx.h. Хотя достаточно знать, что значение FT_OK
— отсутствие ошибки, а все остальные значения — коды ошибок, хотелось бы отследить не только сам факт ошибки, но и какая ошибка произошла и где именно она произошла.
В LabVIEW данные об ошибке распространяются через кластеры error
. Это такой специальный выделенный тип данных, в LabVIEW есть множество ВП и функций для работы с ним. Кластер ошибок состоит из трех элементов: логическая переменная — отображает статус, целое знаковое число — код ошибки, строка — источник ошибки. Статус показывает, произошла ли ошибка, код ошибки определяет ее тип и используется специальными ВП для формирования отчета. Строка дает более развернутое представление о том, где именно произошла ошибка. В LabVIEW принято, если статус равен TRUE
, то это ошибка, если статус равен FALSE
, но код не равен нулю и строка описания не пустая, то это предупреждение, если же статус FALSE
, код равен нулю и строка пустая — нет ошибки.
LabVIEW содержит внутреннюю базу данных, в которой код каждой ошибки связан с ее описанием. Для каждого типа ошибок выделен специальный диапазон значений кодов. Например, для ошибок связанных с работой сети выделено несколько диапазонов: от –2147467263 до –1967390460, от 61 до 65, от 116 до 118 и 122, 1101, 1114, 1115, 1132 до 1134, от 1139 до 1143 и от 1178 до 1185. Для ошибок, определяемых пользователем зарезервировано два диапазона от –8999 до –8000 и от 5000 до 9999. Из этих диапазонов мы можем выбрать значения для кодов ошибок библиотеки D2XX.
Создадим ВП, принимающий на вход статус функции D2XX и конвертирующий этот статус в кластер ошибки LabVIEW. Большинство функций и ВП в LabVIEW, получив на вход Error In
статус TRUE
, не выполняют свой код, а передают информацию об ошибке на терминал Error Out
. Это позволяет эффективно передать информацию о источнике через всю цепочку до обработчика ошибок, исключив выполнение кода в аварийном режиме. Желательно, чтобы и наши ВП вели себя аналогично.
Оформим список статусов D2XX в виде enum
и вынесем его в отдельный тип (в предыдущей статье мы так поступили с типами FTDI).
Новый ВП сохраняем под именем FT_error.vi. На переднюю панель добавляем два кластера Error In
и Error Out
, найти их можно в палитре "Array, Matrix & Cluster". Подсоединяем их к терминалам на панели соединений в нижнем левом и нижнем правом углах соответственно, как уже говорилось в прошлой статье, это принятое в LabVIEW расположение терминалов потока ошибок. На блок-диаграмму добавляем структуру Case
, на вход Case selector
подаем кластер Error In
, после чего структура Case
меняет цвет и делиться две поддиаграммы: "No Error" — зеленый цвет, и "Error" — красный цвет. Внутри случая Error передаем кластер ошибок от терминала селектора напрямую к выходному туннелю на правой границе. А в зеленом случае добавляем еще один Case
, он в зависимости от статуса будет определять, следует ли создавать ошибку (статус не равен FT_OK), или оставить все как есть: пропустить входной кластер ошибки на выход без изменения.
Для того, чтобы технично преобразовать код ошибки в кластер, можно использовать ВП Error Cluster From Error Code VI
. Этот SubVI в описание ошибки добавляет цепочку вызова, благодаря чему мы сможем определить не только что произошло, но еще и где это случилось.
Чтобы выделить текст, соответствующий входному статусу (FT_Status), используем блок свойств: выбираем "RingText.Text". Текст ошибки передаем на вход error message
ВП Error Cluster From Error Code VI
.
Не забываем нарисовать "говорящую" иконку.
Передняя (фронт) панель подприбора
Блок-диаграмма. На входе ошибка
Блок-диаграмма. На входе нет ошибки и статус равен FT_OK
Блок-диаграмма. На входе нет ошибки, но статус отличен от FT_OK
Для испытания FT_error можно создать пустой ВП, добавить туда созданный ВП и посмотреть, как будет меняться значение при запуске, если подавать различные статусы.
Передняя (фронт) панель прибора
Блок-диаграмма
Теперь, после любого вызова функции из API D2XX, мы будем использовать SubVI FT_error.vi. А кластер ошибок будет проходить через все ВП по всей иерархии вызова.
В ВП верхнего уровня мы должны определиться, что делать с обнаруженной ошибкой: можно вывести сообщение в диалоговом окне, записать его в файл отчета, проигнорировать или просто "тихо" завершить приложение. Диалоговое окно — самый простой и наиболее популярный способ отчета об ошибках. А еще он удобен для начинающего программиста, так как делать ничего не надо. В каждом ВП по умолчанию активирован режим автоматической обработки ошибок (Enable automatic error handling, находится в категории Execution меню ВП Properties). Работает он так: если в каком-то узле выходной терминал Error Out
никуда не подключен, и в этом узле происходит ошибка, то LabVIEW приостанавливает выполнение приложения и выдает диалоговое окно. Если терминал Error Out
узла соединен, то поток ошибки распространяется, как запрограммировано, и никаких дополнительных действий не происходит. Однако окно сообщения можно вызвать программно, для этого нужно воспользоваться ВП General Error Handler
и Simple Error Handler
(находятся в палитре "Dialog&User Interface"). При этом информацию об ошибке мы можем использовать для завершения программы. На блок-диаграмме это выглядит примерно так:
Когда произойдет ошибка, программа будет приостановлена, появится окно с отчетом, после закрытия окна программа корректно завершиться.
Открыть и закрыть FTDI
Итак, возвращаемся к функции MPSSE_open
. Создаем новый VI. Первым делом, добавляем терминалы для потока ошибок. Добавляем структуру выбора и на селектор подаем вход Error In
. В зеленом кейсе делаем импорт функций в порядке и с параметрами как в Сишном прототипе. Все узлы Call Library Function Node
соединяем в цепочку потоком ошибок. В красном кейсе через тунель соединяем Error In
с выходным терминалом ошибки.
ВП MPSSE_open.vi
На вход SubVI подается строка с описанием FTDI (Description
), на выходе — Handle
и инициализированный чип FTDI в режиме MPSSE.
Создадим ВП, завершающий работу с FTDI и можно уже проверить работоспособность на железе.
Блок-диаграмма
Передняя панель
В предыдущей статье для отладки интерфейса мы сделали ВП заглушку SP_FT_MPSSE_FPGA.vi, сейчас настало время наполнить его. Добавляем на его блок-диаграмму MPSSE_open.vi и FT_Close.vi. На данном этапе достаточно сложно оценить, верно ли прошла инициализация, однако ненулевое значение Handle
на выходе MPSSE_open.vi и отсутствие ошибки нам уже о многом скажет.
Блок-диаграмма SP_FT_MPSSE_FPGA.vi
Для того, чтобы посмотреть значение Handle
можно воспользоваться окном "Probe Watch Window". Это удобный инструмент отладки, позволяющий вывести значение данных на любом (почти любом) проводе в процессе выполнения прибора. Для того чтобы установить пробу на линию, нужно в контекстном меню этой самой линии выбрать пункт "Probe". Откроется окно "Probe Watch Window", а на линии появится циферка с номером пробы. На рисунке выше это "3".
На линии Handle значение 698389336
Отлично! Запускаем ВП верхнего уровня, подключаем к компьютеру отладочную плату. В списке "Выберите устройство" появляется описание подключенной микросхемы FTDI, нажимаем кнопку "Программировать" и… ничего не происходит. Только в окне "Probe Watch" появилось значение Handle
. И это хорошо.
Отключаем плату, список устройств очищается. Нажимаем "Программировать". Вот тут-то выскакивает окно с отчетом об ошибке.
После нажатия кнопки "Continue", ВП завершает свою работу.
Стоит запретить нажимать кнопку, если не найдено ни одного устройства. Модифицируем кейс "Timeout" обработчика событий. Напомню, два раза в секунду сканируются подключенные к ПК чипы FTDI, если таковые обнаружены и могут быть использованы для программирования ПЛИС, то через свойство Strings[]
их дескрипторы добавляются в Devices list
. Создаем для "Программировать" свойство Disabled
, и, если годных устройств не обнаружено, то отключаем и затемняем кнопку.
Осваиваем GPIO
После того, как MPSSE активирован, работа с ним осуществляется через так называемые "op-code", а из функций API D2XX используется только FT_Write
, FT_Read
и FT_Queue
(чтобы узнать статус буфера приемника). По наторенной дорожке создаем соответствующие VI: FT_Write.vi, FT_Read.vi, FT_Queue.vi.
FT_Write.vi
Блок-диаграмма. FT_Write.vi
FT_Read.vi
Блок-диаграмма. FT_Read.vi
FT_Queue.vi
Блок-диаграмма. FT_Queue.vi
Теперь из этих трех кирпичиков выкладываем ВП для чтения параллельного порта и записи в него. Значение удобно представить в виде массива булевых переменных.
MPSSE_Set_LByte.vi
Блок-диаграмма. MPSSE_Set_LByte.vi
MPSSE_Get_LByte.vi
Блок-диаграмма. MPSSE_Get_LByte.vi
Каюсь, мне было лениво создавать именованный список для всех op-code, поэтому оставил их в виде Magic Numbers.
Как говорилось в самой первой статье протокол загрузки ПЛИС "Passive Serial" есть ничто иное как SPI с дополнительной манипуляцией флагами. Всего используется пять ножек: линии DCLK, DATA[0], nCONFIG должны быть сконфигурированы как выхода, линии nSTATUS, CONF_DONE — как входы.
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
Enum SP_LBYTE_BITS.ctl
Для начала конфигурации контроллер MPSSE должен генерировать переход из низкого уровня в высокий на линии nCONFIG. Как только ПЛИС будет готова к приему данных, она сформирует высокий уровень на линии nSTATUS. На данном этапе у нас все готово для эксперимента в железе. На блок-диаграмму SP_FT_MPSSE_FPGA.v добавляем управление линией nCONFIG — после инициализации MPSSE подаем низкий уровень, а затем высокий. После каждой операции (для отладки) считываем состояние ножек порта.
Во время запуска
Блок-диаграмма
В целом, во время запуска 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
— число, типа файлового дескриптора, создается при открытии файла и должно быть передано на вход другим ВП, работающим с этим файлом.
Пока нам некуда утилизировать считанные данные, но если очень хочется проверить работоспособность цепочки, то можно создать индикатор типа String
и немножко настроить его. В контекстном меню активируем опцию "Hex Display", включаем вертикальный скроллбар (Visible Items -> Vertical Scrollbar) и после запуска наблюдаем содержимое бинарного файла конфигурации.
На блок-диаграмме ВП образовалось две независимые параллельные линии кода, поэтому для них используются раздельные цепочки ошибок. Для того, чтобы свести параллельные потоки в один терминал Error Out
, используется функция Merge Errors
. Эта функция просматривает ошибки на входе сверху вниз (да, там может более двух входных терминалов, растягивается мышкой) и возвращает первую, которую найдет. Если ошибок нет, то возвращает первое попавшееся предупреждение. Если и предупреждений не обнаружено, то на выходе ошибка отсутствует. Важно отметить, что порядок подключения входов Merge Errors
определяет приоритет ошибок, и если ошибка возникнет сразу в двух цепочках, то нижняя ошибка будет проигнорирована. К этому нужно относиться внимательно.
Если мы попытаемся в ВП верхнего уровня нажать кнопку "Программировать" не выбрав файл, то на вход SP_FT_MPSSE_FPGA.vi поступит пустой путь, что вызовет ошибку "Error 1430. LabVIEW: (Hex 0x596) The path is empty or relative. You must use an absolute path." Как говорит мой друг детства: "Пустяки, дело-то житейское!". И ошибка эта — вовсе не ошибка, а так, невнимательность пользователя. Останавливать программу и ругаться на него окном с красным крестиком мы не будем, просто удалим ошибку с этим кодом из потока и в диалоговом окне порекомендуем пользователю определиться с файлом. Для фильтрации ошибки используем ВП "Clear Errors" из палитры "Dialog&User Interface". Для вывода сообщения — "One Button Dialog".
Загрузка конфигурации
Для последовательной передачи данных процессору MPSSE нужно послать op-code 0x18, аргументами команды будет длина передаваемой последовательности (два байта, начиная с младшего), и сама последовательность данных. Длина кодируется за вычетом единицы. Отправку блока данных оформим в виде ВП MPSSE_send.
MPSSE_Send.vi
Блок-диаграмма
Размер входного буфера (Array Size
) преобразовываем к двухбайтовому типу U16
, отнимаем единицу, меняем местами младший и старший байт (Swap Bytes
) — отправлять длину нужно начиная с младшего, и преобразовываем двухбайтовое число в однобайтный массив (Type Cast
).
Функция Type Cast
заслуживает отдельного внимания. Это такой универсальный преобразователь типов, сообразительность которого порою сильно удивляет. Если коротко, то:
Наглядно для програмиста
Однако это не просто приведение данных к другому типу, это еще и эвристическая интерпретация. Эта функция позволяет выполнять преобразование между несовместимыми типами данных, при этом функция не брезгует выравниванием входных данных и даже удалением "лишних" частей. Если запрошенный тип данных требует памяти больше, чем у входных данных, то функция выделит недостающий объем. Для начинающего разработчика LabVIEW Type Cast
может стать палочкой-выручалочкой, но с взрослением, лучше от такого преобразователя отказаться — сильно много скрыто от глаз и может стать источником непредвиденных ошибок. Лучше использовать более явные методы преобразования, например, Coerce To Type
.
При инициализации процессора MPSSE, мы задали максимально допустимый размер буфера для передачи данных в 65536 байт, следовательно файл конфигурации мы должны разделить на фрагменты, размер которых не превышает указанный размер. Воспользуемся функцией Array Subset
, эта функция выделяет из массива подмассив начиная с элемента index
и длинною length
. Разбивать будем в цикле While
, каждую итерацию индекс будем увеличивать на 65536, между итерациями значение передадим через сдвиговый регистр. Как только не удастся от основного массива отщипнуть 65536 байта, берем все, что осталось, отправляем и останавливаем цикл.
Согласно протоколу загрузки, после того, как все данные были переданы, нужно подать еще два тактовых импульса, чтобы началась инициализация ПЛИС. Для этого после цикла выполняем отправку еще одного "пустого" байта.
Для того, чтобы понять успех прошивки, считаем флаги, и, если 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, показать как строится логика разработки в этой среде. Человек со стороны поймет, нужно ли это ему вообще, начинающий разработчик несомненно почерпнет для себя много нового, профессионал снисходительно улыбнется и вспомнит себя в молодости (либо кинет в меня помидором). А мне будет куда направить моих студентов и дипломников.
Материалы по теме
- Блюм П. LabVIEW: стиль программирования. Пер. с англ. под ред. Михеева П.– М.:
ДМК Пресс, 2008 – 400 с.: ил. - labview_mpsse. Репозиторий с проектом.
- Учебный стенд для ЦОС. Железо для опыта
- Software Application Development D2XX Programmer's Guide. Руководство по API D2XX.
Автор: Шауэрман Александр