Хабрахабр

Загрузка конфигурации в ПЛИС через USB или разбираем FTDI MPSSE

Пришлось мне участвовать в разработке учебного стенда для кафедры одного технического вуза. В жизни каждого плисовода наступает момент, когда требуется написать собственный загрузчик файла конфигурации в ПЛИС. А значение имеет то, что в основе стенда стоит ПЛИС (Altera Cyclone IV), на которой по задумке автора стенда студенты собирают всякие схемы ЦОС. Стенд предназначен для изучения цифровой обработки сигналов, хотя в рамках этой статьи это не имеет особого значения. Требуется выполнить загрузку ПЛИС с компьютера через USB.
Стенд подключается к компьютеру через USB.

Один канал будет использован для конфигурации ПЛИС, другой может быть использован для высокоскоростного обмена в режиме FIFO. Принято решение для подключения к ПК использовать FTDI в ее двухканальной ипостаси — FT2232H.

Принципиальные схемы в свободном доступе. У FTDI есть отладочная плата MORPH-IC-II, где через USB прошивается ПЛИС Cyclone II. По правде сказать, изначально я планировал в своем проекте использовать этот загрузчик, ну или на крайний случай сделать свою оболочку на базе их dll. Исходные коды загрузчика частично открыты: сам загрузчик доступен, однако вся логика работы с FTDI вынесена в закрытую библиотеку и не может быть модифицирована. На макетной плате работоспособность решения MORPH-IC-II была полностью подтверждена, однако проблема, как оно часто бывает, пришла откуда не ждали. Загрузка прошивки в ПЛИС осуществляется в пассивном последовательном режиме (passive serial — PS), FTDI работает в режиме MPSSE. Одновременная работа с ними не представляется возможной. Выяснилось, что при работе dll MORPH-IC-II все подключенные устройства FTDI блокируются, а в составе учебного комплекса есть еще два устройства с подобными преобразователями: генератор и анализатор сигналов. Чертовски странно и досадно.

Там тоже используется FTDI в режиме MPSSE, только в отличии от MORPH-IC-II работа с ПЛИС происходит в режиме JTAG. Похожий кейс реализован у ребят из Марсохода: USB JTAG программатор MBFTDI. Поэтому использовать их в коммерческом проекте у меня рука не поднялась. Исходники в свободном доступе, однако внятного указания на их статус (лицензии) я не нашел.

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

Загрузка файла конфигурации в микросхему ПЛИС

Для тех, кто только начинает знакомится с темой, проведу маленький экскурс. В первую очередь стоит разобраться с режимом загрузки ПЛИС. Хотя на моей плате установлена ПЛИС Altera (Intel) семейства Cyclone IV E, методы загрузки аналогичны для всей группы ПЛИС Cyclone, и есть подозрение, что в том или ином виде подходят для многих других семейств.

Эти конфигурационные данные определяют функционал итогового устройства. В ПЛИС данного типа используется энергозависимая SRAM для хранения конфигурационных данных. Таким образом, прошивка хранится в специальном ОЗУ и каждый раз при включении устройства должна быть загружена в кристалл ПЛИС. На профессиональном жаргоне эти данные часто называют "прошивкой". Существует несколько способов (схем конфигурации), которыми прошивка может быть загружена в SRAM (список актуален для Cyclone IV E):

  1. Активный последовательный (Active serial (AS)).
  2. Активный параллельный (Active parallel (AP)).
  3. Пассивный последовательный (Passive serial (PS)).
  4. Быстрый пассивный параллельный (Fast passive parallel (FPP)).
  5. JTAG.

Режим JTAG доступен всегда. Выбор конкретного режима загрузки выполняется с помощью внешних выводов ПЛИС (группа MSEL). В пассивном режиме ПЛИС ждет, когда внешний носитель в инициативном порядке передаст ей данные конфигурации. Активный режим подразумевает, что при подаче питания ПЛИС самостоятельно вычитывает данные из внешней памяти (последовательной или параллельной). В активных режимах ПЛИС выступает в качестве ведущего, а в пассивных — в качестве ведомого. Данные схемы хорошо укладываются в концепцию ведущий (Master) — ведомый (Slave).

А для экономия ножек микросхемы выбираем последовательный интерфейс. В рассматриваемой задаче не ПЛИС, а пользователь должен решать, когда должна обновляться прошивка, поэтому режим загрузки должен быть пассивным. Логика работы JTAG несколько сложнее, поэтому остановимся на первом варианте.
Ниже на рисунке показана схема подключения ПЛИС к внешнему контроллеру для загрузки в режиме PS. Здесь подходит пассивный последовательный (PS) режим и JTAG.

Как только ПЛИС будет готова к приему данных, она сформирует высокий уровень на линии nSTATUS. Для начала конфигурации внешний ведущий контроллер должен генерировать переход из низкого уровня в высокий на линии nCONFIG. Данные должны передаваться в целевое устройство до тех пор, пока на линии CONF_DONE не установится высокий уровень (или данные не закончатся), при этом ПЛИС перейдет в состояние инициализации. После чего ведущий может начать передавать данные по линии DATA[0], а соответствующие тактовые импульсы — по линии DCLK. Следует учесть, что после того как CONF_DONE установилась в единицу, нужно подать еще два тактовых импульса, чтобы началась инициализация ПЛИС.

Данные передаются младшим значащим разрядом (LSB) вперед, то есть, если конфигурационный файл содержит последовательность 02 1B EE 01 FA (пример взять как есть из Handbook), на линии данных должна быть сформирована последовательность:

0100-0000 1101-1000 0111-0111 1000-0000 0101-1111

Таким образом, используется всего пять линий: линии DATA[0] и DCLK — для последовательной передачи, линии nCONFIG, nSTATUS, CONF_DONE — для управления.
По своей сути режим PS есть не что иное, как SPI с дополнительной манипуляцией флагами.
Скорость передачи данных должна быть ниже указанной в документации максимальной частоты, для используемой в проекте серии Cyclone IV E — это 66 МГц.

Это дает отличные возможности пошаговой отладки с участием осциллографа, чем мы непременно воспользуемся. Минимальной же частоты передачи не существует, теоретически можно приостановить конфигурацию на неопределенное время.

На рисунке ниже показана временная диаграмма интерфейса с наиболее значащими таймингами.

Хитрый зверь MPSSE

Режим MPSSE (Multi-Protocol Synchronous Serial Engine), на мой взгляд, является более-менее удачной попыткой создать некий конструктор последовательных интерфейсов, дать разработчику возможность реализовать широко распространенные протоколы передачи данных, такие как SPI, I2C, JTAG, 1-wire и многие другие на их основе. Рассмотрим работы FTDI в режиме MPSSE.

В своем проекте я использую FT2232H, поэтому в большей степени речь идет о ней. В настоящий момент режим доступен для микросхем: FT232H, FT2232D, FT2232H, FT4232H. Каждый байт может быть прочитан или установлен. Для режима MPSSE выделено 16 ножек, разделенных на два байта: младший L и старший H. Каждая нога может быть настроена как вход или выход, для вывода может быть задано значение по умолчанию. Четыре младшие ноги байта L имеют особые функции — через них может происходить последовательная передача данных. Для последовательной передачи настраивается порядок следование бит (MSB/LSB), длина передаваемого слова, частота тактовых импульсов, фронт синхронизации — передний (Rising) или задний (Falling), можно выбрать передачу только тактовых импульсов без данных, или выбрать 3-х фазовое тактирование (актуально для I2C) и многое другое.

Существуют два альтернативных способа программного взаимодействия с чипами FTDI: первый, назовем его классическим, в этом случае при подключении к порту USB микросхема в системе определяется как виртуальный последовательный порт (COM), операционная система использует драйвер VCP (Virtual COM Port). Плавно переходим к программированию. Причем это справедливо для различных операционных систем, включая Linux и Mac OS. Все дальнейшее программирование не отличается от программирования классического COM порта: открыл — передал/считал — закрыл. Второй способ обеспечивается проприетарной библиотекой FTD2XX, это интерфейс предоставляет специальные функции, которые не доступны в стандартном API COM порта, в частности, доступна настройка и использование специальных режимов работы, таких как MPSSE, 245 FIFO, Bit-bang. Однако при таком подходе не получится реализовать все возможности контроллера FTDI — чип будет работать как переходник USB-UART. И да, FTD2XX также доступна для различных операционных систем. Библиотека FTD2XX API хорошо задокументирована Software Application Development D2XX Programmer's Guide, широко и давно известна в узких кругах.

И им это удалось, для работы в режиме MPSSE используется тот же набор функций, что и для других "классических" режимов, используется та же библиотека FTD2XX API. Перед разработчиками FTDI стояла задача уложить относительно новый MPSSE в существующую программную модель взаимодействия D2XX.

Если коротко, то алгоритм работы в режиме MPSSE можно описать следующим образом:

  1. Найти девайс в системе и открыть его.
  2. Выполнить первичную инициализацию чипа и перевести его в режим MPSSE.
  3. Настроить режим работы MPSEE.
  4. Непосредственная работа с данными: передаем, принимаем, управляем GPIO — реализуем целевой протокол обмена.
  5. Закрыть девайс.

Пишем загрузчик

В своих экспериментах в качестве IDE я буду использовать Eclipse версии Oxygen. Приступим к практической части. 7. 3a Release (4. 3. 3a), в качестве компилятора — mingw32-gcc (6. Операционная система Win7. 0).

В архиве находим заголовочный файл ftd2xx.h с описанием всех функций API. С сайта FTDI скачиваем последнюю актуальную версию драйвера для своей операционной системы. Для моего случая ftd2xx.lib лежит в каталоге i386. Сам API реализован в виде ftd2xx.dll, но динамический импорт оставим на потом, и воспользуемся статической линковкой: нам понадобится файл библиотеки ftd2xx.lib.

Создание makefile можно доверить IDE. В Eclipse создаем новый Си проект. Я не буду заострять внимание об особенностях настройки проекта под Eclipse, так как подозреваю, что большинство для программирования под Win использует другие среды и компиляторы. В настройках линковшика указываем путь и название библиотеки ftd2xx (я требуемые файлы перенес в директорию проекта в папочку ftdi).

Пункт первый. Найти девайс и открыть его

Это может быть его порядковый номер в системе: первая подключенная микросхема FTDI примет номер 0, последующая 1 и так далее. FTD2XX API позволяет открыть чип используя ту или иную известную информацию о нем. Для открытия чипа по номеру используется функция FT_Open. Номер в системе определяется порядком подключения микросхем, мягко говоря, это не всегда удобно. Серийный номер и описание хранятся во внутренней памяти чипа и могут быть записаны туда при производстве прибора в составе которого установлен FTDI. Открыть чип можно по его серийному номеру (FT_OPEN_BY_SERIAL_NUMBER), описанию (FT_OPEN_BY_DESCRIPTION) или по расположению (FT_OPEN_BY_LOCATION), для этого используется функция FT_OpenEx. Поэтому, наиболее удобным вариантом идентификации поддерживаемых разрабатываемой программой приборов является его описание. Описание, как правило, характеризует тип прибора либо семейство, а серийный номер должен быть уникальным для каждого изделия. Фактически, если нам изначально известна строка дескриптора чипа, то и искать прибор в системе не нужно, однако в порядке эксперимента, выведем все подключенные к компьютеру приборы с FTDI. FTDI чип будем открывать по описанию (дескриптору). С помощью функции FT_CreateDeviceInfoList создадим подробный список подключенных чипов, а с помощью функции FT_GetDeviceInfoList считаем его.

Список подключенных устройств. Листинг:

ftStatus = FT_CreateDeviceInfoList(&numDevs);
if (ftStatus == FT_OK)
{ printf("Number of devices is %d\n",numDevs);
} if (numDevs == 0) return -1; // allocate storage for list based on numDevs
devInfo = (FT_DEVICE_LIST_INFO_NODE*)malloc(sizeof(FT_DEVICE_LIST_INFO_NODE)*numDevs); ftStatus = FT_GetDeviceInfoList(devInfo,&numDevs); if (ftStatus == FT_OK) for (int i = 0; i < numDevs; i++)

Поприветствуем мой зоопарк

D:\workspace\ftdi-mpsse-ps\Debug>ftdi-mpsse-ps.exe
Number of devices is 4
Dev 0:
Flags = 0x0
Type = 0x5
ID = 0x4036001
LocId = 0x214
SerialNumber = AI043NNV
Description = FT232R USB UART
Dev 1:
Flags = 0x2
Type = 0x6
ID = 0x4036010
LocId = 0x2121
SerialNumber = L731T70OA
Description = LESO7 A
Dev 2:
Flags = 0x2
Type = 0x6
ID = 0x4036010
LocId = 0x2122
SerialNumber = L731T70OB
Description = LESO7 B
Dev 3:
Flags = 0x2
Type = 0x8
ID = 0x4036014
LocId = 0x213
SerialNumber = FTYZ92L6
Description = LESO4.1_ER

Чип FT2232H в системе отобразился как два независимых прибора (Dev 1 и Dev 2). К моему ПК подключено три прибора с чипами FTDI: FT232RL (type 0x5), FT2232H (type 0x6) и FT232H (tepe 0x8). Открываем его: Интерфейс PS ПЛИС подключен к Dev 2, его дексриптор "LESO7 B".

//Open a device with device description "LESO7 B"
ftStatus = FT_OpenEx("LESO7 B", FT_OPEN_BY_DESCRIPTION, &ftHandle);
if (ftStatus != FT_OK)
{ printf ("Оpen failure\r\n"); return -1;
}

Их много, но достаточно знать, что значение FT_OK — отсутствие ошибки, все остальные значения — коды ошибок. Большинство функций API возвращают статус своего вызова типа FT_STATUS, все возможные значения описаны в виде enum'а в заголовочном файле. Хорошим стилем программирования будет проверять значение статуса после каждого вызова функции API.

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

Для этого используется функция FT_Close: После завершения работы с чипом, его нужно закрыть.

FT_Close(ftHandle);

Пункт 2. Инициализируем чип и включаем MPSSE

Настройка типичная для большинства режимов и хорошо описана в документации AN_135 FTDI MPSSE Basics.

  1. Выполняем сброс (резет) чипа. Функция FT_ResetDevice.
  2. На случай, если в буфере приема завалялся какой-то мусор, очищаем его. Функция FT_Purge.
  3. Настраиваем размер буферов для чтения и записи. Функция FT_SetUSBParameters.
  4. Отключаем контроль четности. FT_SetChars.
  5. Задаем таймауты на чтение и запись. По умолчанию таймауты отключены, включаем таймаут на передачу. FT_SetTimeouts.
  6. Настраиваем время ожидания отправки пакета с чипа на хост. По умолчанию 16 мс, ускоряем до 1 мс. FT_SetLatencyTimer.
  7. Включаем для синхронизации входящих запросов управление потоком. FT_SetFlowControl.
  8. Все готово для активации режима MPSSE. Сбрасываем контроллер MPSSE. Используем функцию FT_SetBitMode, устанавливаем режим 0 (mode = 0, mask = 0).
  9. Включаем режим MPSSE. Функция FT_SetBitMode — mode = 2, mask = 0.

Открытие и настройку чипа объединяем в функцию MPSSE_open, в качестве параметра передаем строку с дескриптором открываемого прибора:

Листинг MPSSE_open

static FT_STATUS
MPSSE_open (char *description)
{ FT_STATUS ftStatus; ftStatus = FT_OpenEx(description, FT_OPEN_BY_DESCRIPTION, &ftHandle); if (ftStatus != FT_OK) { printf ("open failure\r\n"); return FT_DEVICE_NOT_OPENED; } printf ("open OK, %d\r\n", ftHandle); printf("\nConfiguring port for MPSSE use...\n"); ftStatus |= FT_ResetDevice(ftHandle); //Purge USB receive buffer first by reading out all old data from FT2232H receive buff: ftStatus |= FT_Purge(ftHandle, FT_PURGE_RX); //Set USB request transfer sizes to 64K: ftStatus |= FT_SetUSBParameters(ftHandle, 65536, 65536); //Disable event and error characters: ftStatus |= FT_SetChars(ftHandle, 0, 0, 0, 0); //Sets the read and write timeouts in milliseconds: ftStatus |= FT_SetTimeouts(ftHandle, 0, 5000); //Set the latency timer to 1mS (default is 16mS): ftStatus |= FT_SetLatencyTimer(ftHandle, 1); //Turn on flow control to synchronize IN requests: ftStatus |= FT_SetFlowControl(ftHandle, FT_FLOW_RTS_CTS, 0x00, 0x00); //Reset controller: ftStatus |= FT_SetBitMode(ftHandle, 0x0, FT_BITMODE_RESET); //Enable MPSSE mode: ftStatus |= FT_SetBitMode(ftHandle, 0x0, FT_BITMODE_MPSSE); if (ftStatus != FT_OK) { printf("Error in initializing the MPSSE %d\n", ftStatus); return FT_OTHER_ERROR; } Sleep(50); // Wait for all the USB stuff to complete and work return FT_OK;
}

Пункт 3. Настроим режим работы MPSEE

Команды представляют собой байтовые последовательности, первый байт которых — "op-code", далее следуют параметры команды. Собственно, на этом этапе процессор MPSSE активирован и готов к приему команд. Команды передаются с помощью функции FT_Write, ответ от процессора MPSSE можно получить с помощью функции FT_Read. Команда может не иметь параметров и состоять из одного "op-code".

Механизм "плохая команда — ответ 0xFA" можно использовать для синхронизации прикладной программы с процессором MPSSE. После каждой отправки команды полезно вычитать ответ процессора, так как в случае неверной команды ответ может содержать сообщение об ошибке — символ 0xFA. Op-code описаны в Command Processor for MPSSE and MCU Host Bus Emulation Mode.
Настройка MPSSE сводится к заданию скорости передачи данных, направления и начальных состояний линий ввода-вывода.
Рассмотрим настройку скорости передачи данных процессора MPSSE. Если все ОК, тогда на заведомо ошибочную команду чип вернет символ 0xFA. В устаревшем FT2232D используется тактовый генератор 12МГц, а в современных — 60 МГц. Настройка для чипов с поддержкой только режима Full-speed (FT2232D) и чипов с High-speed (FT2232H, FT232H, FT4232H) происходит несколько по разному. Отсюда формула для расчета скорости передачи данных:

$Data Speed = \frac{f_{core}}{(1+Divisor)\cdot 2}$

Макрос возвращает делитель: где fcore — частота ядра FTDI, Divisor — двухбайтовый делитель, который, собственно, и задает частоту тактирования данных.
В результате, если делитель равен нулю, то максимальная скорость передачи данных составит 30 Мбит/с, а минимальная скорость передачи данных будет при делителе 65535 — 458 бит/с.
Расчет делителя поручим препроцессору.

#define FCORE 60000000ul
#define MPSSE_DATA_SPEED_DIV(data_speed) ((FCORE/(2*data_speed)) -1)

А эти два макроса возвращают соответственно старший и младший байты делителя:

#define MPSSE_DATA_SPEED_DIV_H(data_speed) ((MPSSE_DATA_SPEED_DIV(data_speed)) >> 8)
#define MPSSE_DATA_SPEED_DIV_L(data_speed) \ (MPSSE_DATA_SPEED_DIV(data_speed) - (MPSSE_DATA_SPEED_DIV_H(data_speed)<< 8))

Этот делитель по умолчанию активирован, в нашем случае его стоит отключить.
Находим соответствующий op-code (0x8A) и шлем команду процессору: Кроме того, следует учесть, что в современных чипах для совместимости со старичком FT2232D есть дополнительный делитель на 5, который превращает 60 МГц в 12 МГц.

Листинг отправки команды

BYTE byOutputBuffer[8], byInputBuffer[8];
DWORD dwNumBytesToRead, dwNumBytesSent = 0, dwNumBytesRead = 0;
byOutputBuffer[0] = 0x8A;
ftStatus = FT_Write(ftHandle, byOutputBuffer, 1, &dwNumBytesSent);
Sleep(2); // Wait for data to be transmitted and status ftStatus = FT_GetQueueStatus(ftHandle, &dwNumBytesToRead);
ftStatus |= FT_Read(ftHandle, byInputBuffer, dwNumBytesToRead, &dwNumBytesRead); if (ftStatus != FT_OK)
{ printf("Error\r\n"); return FT_OTHER_ERROR;
}
else if (dwNumBytesToRead > 0)
{ printf("dwNumBytesToRead = %d:", dwNumBytesToRead); for ( int i = 0; i < dwNumBytesToRead; i++) printf (" %02Xh", byInputBuffer[i]); printf("\r\n"); return FT_INVALID_PARAMETER;
} return FT_OK;

В порядке эксперимента, вместо действительной команды 0x8A, пошлем значение 0xFE, которому не соответствует ни один op-code, вывод консоли:

dwNumBytesToRead = 2: FAh FEh

Таким образом, отправив несколько команд сразу, мы сможем не только отследить сам факт ошибки, но и понять на какой команде эта ошибка произошла.
Для того, чтобы в дальнейшем не иметь дело с "магическими числами", все op-code оформим в виде констант и поместим в отдельный заголовочный файл.
Для полной настройки режима требуется задать направление линий ввода-вывода и их значение по умолчанию. Процессор вернул два байта, байт "плохая команда" — 0xFA и значение этой "плохой" команды. Для того, чтобы не загромождать и без того раздутую статью, я перечертил интересующий фрагмент схемы: Обратимся к принципиальной схеме подключения.

По диаграмме определяем какие начальные состояния должны быть у линий. Линии 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

Для установки значения используем op-code 0x80. Все используемые линии расположены на младшем байте порта MPSSE. Это команда предполагает два аргумента: первый следующий за op-code байт — это побитовое значение, а второй — направление (единичка — порт на вывод, ноль — порт на ввод).
В рамках борьбы с "magic number" все порядковые номера линий и их значения по умолчанию оформим в виде констант:

Define ports

#define PORT_DIRECTION (0x07)
#define DCLK (0)
#define DATA0 (1)
#define N_CONFIG (2)
#define N_STATUS (3)
#define CONF_DONE (4) // initial states of the MPSSE interface
#define DCLK_DEF (1)
#define DATA0_DEF (0)
#define N_CONFIG_DEF (1)
#define N_STATUS_DEF (1)
#define CONF_DONE_DEF (1)

Осталось только убедиться, что отключена петля TDI — TDO (может быть активирована для тестирования) и оформить в отдельную функцию:

Листинг функции MPSSE_setup

static FT_STATUS
MPSSE_setup ()
{ DWORD dwNumBytesToSend, dwNumBytesSent, dwNumBytesToRead, dwNumBytesRead; BYTE byOutputBuffer[8], byInputBuffer[8]; FT_STATUS ftStatus; // Multple commands can be sent to the MPSSE with one FT_Write dwNumBytesToSend = 0; // Start with a fresh index byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_DISABLE_DIVIDER_5; byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_DISABLE_ADAPTIVE_CLK; byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_DISABLE_3PHASE_CLOCKING; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); dwNumBytesToSend = 0; // Reset output buffer pointer // Set TCK frequency // Command to set clock divisor: byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_SET_TCK_DIVISION; // Set ValueL of clock divisor: byOutputBuffer[dwNumBytesToSend++] = MPSSE_DATA_SPEED_DIV_L(DATA_SPEED); // Set 0xValueH of clock divisor: byOutputBuffer[dwNumBytesToSend++] = MPSSE_DATA_SPEED_DIV_H(DATA_SPEED); ftStatus |= FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); dwNumBytesToSend = 0; // Reset output buffer pointer // Set initial states of the MPSSE interface // - low byte, both pin directions and output values /* | FPGA pin | Pin Name | Pin | MPSSE | Dir | def | | --------- | -------- | --- | ------ | --- | --- | | 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 | */ // Configure data bits low-byte of MPSSE port: byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_SET_DATA_BITS_LOWBYTE; // Initial state config above: byOutputBuffer[dwNumBytesToSend++] = (DCLK_DEF << DCLK) | (DATA0_DEF << DATA0) | (N_CONFIG_DEF << N_CONFIG) | (N_STATUS_DEF << N_STATUS) | (CONF_DONE_DEF << CONF_DONE); // Direction config above: byOutputBuffer[dwNumBytesToSend++] = PORT_DIRECTION; ftStatus |= FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); // Send off the low GPIO config commands dwNumBytesToSend = 0; // Reset output buffer pointer // Set initial states of the MPSSE interface // - high byte, all input, Initial State -- 0. // Send off the high GPIO config commands: byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_SET_DATA_BITS_HIGHBYTE; byOutputBuffer[dwNumBytesToSend++] = 0x00; byOutputBuffer[dwNumBytesToSend++] = 0x00; ftStatus |= FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); // Disable loopback: byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_DISABLE_LOOP_TDI_TDO; ftStatus |= FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); Sleep(2); // Wait for data to be transmitted and status ftStatus = FT_GetQueueStatus(ftHandle, &dwNumBytesToRead); ftStatus |= FT_Read(ftHandle, byInputBuffer, dwNumBytesToRead, &dwNumBytesRead); if (ftStatus != FT_OK) { printf("Unknown error in initializing the MPSSE\r\n"); return FT_OTHER_ERROR; } else if (dwNumBytesToRead > 0) { printf("Error in initializing the MPSSE, bad code:\r\n"); for ( int i = 0; i < dwNumBytesToRead; i++) printf (" %02Xh", byInputBuffer[i]); printf("\r\n"); return FT_INVALID_PARAMETER; } return FT_OK;
}

Пункт 4. Реализуем протокол загрузки

Во-первых, проверим, что инициализация выполняется корректно, в основном теле программы вызовем MPSSE_open() и MPSSE_setup(), а перед закрытием устройства (FT_Close) поместим пустой getchar(). Кажется все готово для практических экспериментов. Поменяв значение этих уровней в инициализации (ничего страшного с ПЛИС не случится), убеждаемся, что процессор MPSSE желаемое выдает за действительное — все адекватно работает и можно переходить к передаче данных.
Последовательная отправка и прием данных выполняется в командном режиме с помощью все тех же op-code. Запустим программу и с помощью осциллографа убедимся, что на всех линиях PS установились заданные по умолчанию уровни. Процессор MPSSE может передавать и принимать данные, также делать это одновременно. Первый байт команды — op-code, который определяет тип операции, за ним следует длина передаваемой или принимаемой последовательности и, если это передача, собственно данные. Передача данных может происходить либо по переднему, либо по заднему фронту тактовых импульсов. Передача может осуществляться либо младшим значащим битом вперед (LSB), либо старшим (MSB). Для каждой комбинации вариантов есть свой op-code, каждый бит op-code описывает режим работы:

Бит

Функция

0

Синхронизация по фронту на запись: 0 — положительный, 1 — отрицательный

1

1 — работа с байтами, 0 — работа с битами

2

Синхронизация по фронту на чтение: 0 — положительный, 1 — отрицательный

3

Режим передачи: 1 — LSB, 0 — MSB first

4

Передача данных по линии TDI

5

Чтение данных с линии TDO

6

Передача данных по линии TMS

7

Должен быть 0, иначе это другая группа команд

Для нас удобнее оперировать байтами, а не битами, в этом случае op-code примет значение 0001_1000b или 0x18 в шестнадцатеричном представлении. При конфигурировании ПЛИС по схеме PS передача данных происходит по переднему фронту в режиме LSB. Следует учесть небольшую особенность: длина кодируется за вычетом единицы. Аргументами команды будет длина передаваемой последовательности (два байта, начиная с младшего), и сама последовательность данных. Думаю, оно понятно зачем так сделано. То есть, если мы хотим отправить один байт, то длина будет равна 0, если хотим отправить 65536, то нужно указать длину 65535. Отправку блока данных оформим в виде функции MPSSE_send.

Листинг функции MPSSE_send

static BYTE byBuffer[65536 + 3];
static FT_STATUS
MPSSE_send(BYTE * buff, DWORD dwBytesToWrite)
{ DWORD dwNumBytesToSend = 0, dwNumBytesSent, bytes; FT_STATUS ftStatus; // Output on rising clock, no input // MSB first, clock a number of bytes out byBuffer[dwNumBytesToSend++] = MPSSE_CMD_LSB_DATA_OUT_BYTES_POS_EDGE; // 0x18 bytes = dwBytesToWrite -1; byBuffer[dwNumBytesToSend++] = (bytes) & 0xFF; // Length L byBuffer[dwNumBytesToSend++] = (bytes >> 8) & 0xFF; // Length H memcpy(&byBuffer[dwNumBytesToSend], buff, dwBytesToWrite); dwNumBytesToSend += dwBytesToWrite; ftStatus = FT_Write(ftHandle, byBuffer, dwNumBytesToSend, &dwNumBytesSent); if (ftStatus != FT_OK ) { printf ("ERROR send data\r\n"); return ftStatus; } else if (dwNumBytesSent != dwNumBytesToSend) { printf ("ERROR send data, %d %d\r\n", dwNumBytesSent, dwNumBytesToSend); } return FT_OK;
}

Можно выкинуть byBuffer, если при вызове функции действительные данные помещать начиная с четвертого элемента массива buff, таким образом, зарезервировав первые три байта под op-code с длиной. В этой функции есть один скользкий момент — необходимость держать внутренний буфер на 65 кбайт, а все из-за того, что во входящий блок данных нужно встроить op-code и длину последовательности. Отправляем тестовую последовательность: Для ограниченной памяти микроконтроллера, я так бы и сделал, но в программе для ПК позволим себе такую роскошь.
Как уже отмечалось выше, при отладке с помощью осциллографа целесообразно установить "комфортную" скорость обмена, у моего осциллографа полоса пропускания всего 25 МГц, поэтому, для экспериментов, я выставлю частоту в 1 МГц (благодаря макросам, для этого достаточно задать #define DATA_SPEED 1000000ul).

BYTE byOutputBuffer[] = {0x02, 0x1B, 0xEE, 0x01, 0xFA};
MPSSE_send(byOutputBuffer, sizeof(byOutputBuffer));

И смотрим результат (картинка кликабельная):

Для наглядности активирована функция последовательного декодирования и бинарный код показан непосредственно под сигналом. Синий канал — сигнал с линии DATA[0], красный канал — DCLK. Как видно, что отправили, то и получили.

Для того, чтобы превратить его в PS, нужно настроить работу с флагами. На данном этапе мы можем утверждать, что у нас реализован SPI интерфейс (ну почти). Первый флаг — это вывод, мы им должны управлять из приложения, два других — входа, мы их должны уметь считывать. Три флага nCONFIG, nSTATUS, CONF_DONE.

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

Листинг функции MPSSE_get_lbyte

static FT_STATUS
MPSSE_get_lbyte(BYTE *lbyte)
{ DWORD dwNumBytesToSend, dwNumBytesSent, dwNumBytesToRead, dwNumBytesRead; BYTE byOutputBuffer[8]; FT_STATUS ftStatus; dwNumBytesToSend = 0; byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_GET_DATA_BITS_LOWBYTE; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); Sleep(2); // Wait for data to be transmitted and status ftStatus = FT_GetQueueStatus(ftHandle, &dwNumBytesToRead); ftStatus |= FT_Read(ftHandle, lbyte, dwNumBytesToRead, &dwNumBytesRead); if ((ftStatus != FT_OK) & (dwNumBytesToRead != 1)) { printf("Error read Lbyte\r\n"); return FT_OTHER_ERROR; // Exit with error } return FT_OK;
}

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

Листинг функции MPSSE_set_lbyte

static FT_STATUS
MPSSE_set_lbyte(BYTE lb, BYTE mask)
{ DWORD dwNumBytesToSend, dwNumBytesSent; BYTE byOutputBuffer[8], lbyte; FT_STATUS ftStatus; ftStatus = MPSSE_get_lbyte(&lbyte); if ( ftStatus != FT_OK) return ftStatus; // Set to zero the bits selected by the mask: lbyte &= ~mask; // Setting zero is not selected by the mask bits: lb &= mask; lbyte |= lb; dwNumBytesToSend = 0; // Set data bits low-byte of MPSSE port: byOutputBuffer[dwNumBytesToSend++] = MPSSE_CMD_SET_DATA_BITS_LOWBYTE; byOutputBuffer[dwNumBytesToSend++] = lbyte; byOutputBuffer[dwNumBytesToSend++] = PORT_DIRECTION; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); if ((ftStatus != FT_OK) & (dwNumBytesSent != 1)) { printf("Error set Lbyte\r\n"); return FT_OTHER_ERROR; } return FT_OK;
}

Алгоритм программы следующий: открываем FTDI; активируем и настраиваем MPSSE; открываем rbf-файл на чтение, подаем на линию nCONFIG логический ноль, дожидаемся логического нуля на линии N_STATUS; последовательно считываем содержимое rbf-файла и передаем в ПЛИС; после того, как файл передан полностью, дожидаемся логической единицы на линии CONF_DONE. Все кирпичики собраны, обожжены и готовы к кладке. Однако при этом, флаг nCONFIG окажется в нуле и ПЛИС "забудет" все то, что мы в нее загрузили, поэтому после отработки алгоритма оставляем все как есть, просто закрываем файл и порт. Во всех примерах и мануалах, после работы с процессором MPSSE перед закрытием FTDI рекомендуется перевести ее в режим по умолчанию.

Листинг функции main

int main(int argc, char *argv[])
{ FT_STATUS ftStatus; BYTE lowByte; DWORD numDevs; // create the device information list if ( argv[1] == NULL) { printf ("NO file\r\n"); return -1; } frbf = fopen(argv[1],"rb"); if (frbf == NULL) { printf ("Error open rbf\r\n"); return -1; } ftStatus = FT_CreateDeviceInfoList(&numDevs); if ((numDevs == 0) || (ftStatus != FT_OK)) { printf("Error. FTDI devices not found in the system\r\n"); return -1; } ftStatus = MPSSE_open ("LESO7 B"); if (ftStatus != FT_OK) { printf("Error in MPSSE_open %d\n", ftStatus); EXIT(-1); } MPSSE_setup(); if (ftStatus != FT_OK) { printf("Error in MPSSE_setup %d\n", ftStatus); EXIT(-1); } printf ("nConfig -> 0\r\n"); MPSSE_set_lbyte(0, 1 << N_CONFIG); printf ("nConfig -> 1\r\n"); MPSSE_set_lbyte(1 << N_CONFIG, 1 << N_CONFIG); if (MPSSE_get_lbyte(&lowByte) != FT_OK) { EXIT(-1); } if (((lowByte >> N_STATUS) & 1) == 0) { printf("Error. FPGA is not responding\r\n"); EXIT(-1); } int i = 0; size_t readBytes = 0; // Send the configuration file: do { readBytes = fread(buff, 1, MPSSE_PCK_SEND_SIZE, frbf); if (MPSSE_send(buff, readBytes) != FT_OK) EXIT(-1); putchar('*'); if (!((++i)%16)) printf("\r\n"); } while (readBytes == MPSSE_PCK_SEND_SIZE); printf("\r\n"); memset(buff, 0x00, sizeof(buff)); MPSSE_send(buff, 1); // неужели ни кто не заметит эту странную строку? printf("Load complete\r\n"); // wait CONF_DONE set // A low-to-high transition on the CONF_DONE pin indicates that the configuration is // complete and initialization of the device can begin. i = 0; do { if (MPSSE_get_lbyte(&lowByte) != FT_OK) { printf ("Error read CONF_DONE\r\n"); EXIT(-1); } if (i++ > TIMEOUT_CONF_DONE) { printf ("Error CONF_DONE\r\n"); EXIT(-1); } Sleep(2); } while (((lowByte >> CONF_DONE) & 1) == 0); printf("Configuration complete\r\n"); FT_Close(ftHandle); fclose(frbf);
}

Пример запуска программы:

Оpen "LESO7 B" OK
nConfig -> 0
nConfig -> 1
**
Load complete
Configuration complete

ПЛИС радостно моргает светодиодами. Утилита успешно загружает rbf-файл в ПЛИС. Выставляем максимальную скорость передачи данных в 30 Мбит/сек и убеждаемся в работоспособности ПО.
К минусам решения можно отнести то, что отсутствует возможность отладки и получившийся загрузчик все-таки не JTAG.

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

  1. FTDI-MPSSE-Altera PS. Репозиторий с проектом.
  2. Учебный стенд для ЦОС. Железо для опыта. Там же найдете полную принципиальную схему прибора.
  3. Software Application Development D2XX Programmer's Guide. То с чего начинается разработка софта для FTDI. Руководство по API D2XX.
  4. FTDI MPSSE Basics. Application Note AN_135. По названию все понятно. Основы FTDI MPSSE. Описание сути режима с примерами кода.
  5. Command Processor for MPSSE and MCU Host Bus Emulation Modes. Application Note AN_108. Справочник по op-code. Без него никак.
  6. D2XX Drivers. Драйвер FTDI.
Показать больше

Похожие публикации

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

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

Кнопка «Наверх»