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

dxi7pxs820_gc9p_q_1m6zoaakm.jpeg

В жизни каждого плисовода наступает момент, когда требуется написать собственный загрузчик файла конфигурации в ПЛИС. Пришлось мне участвовать в разработке учебного стенда для кафедры одного технического вуза. Стенд предназначен для изучения цифровой обработки сигналов, хотя в рамках этой статьи это не имеет особого значения. А значение имеет то, что в основе стенда стоит ПЛИС (Altera Cyclone IV), на которой по задумке автора стенда студенты собирают всякие схемы ЦОС. Стенд подключается к компьютеру через USB. Требуется выполнить загрузку ПЛИС с компьютера через USB.
Принято решение для подключения к ПК использовать FTDI в ее двухканальной ипостаси — FT2232H. Один канал будет использован для конфигурации ПЛИС, другой может быть использован для высокоскоростного обмена в режиме FIFO.
У FTDI есть отладочная плата MORPH-IC-II, где через USB прошивается ПЛИС Cyclone II. Принципиальные схемы в свободном доступе. Исходные коды загрузчика частично открыты: сам загрузчик доступен, однако вся логика работы с FTDI вынесена в закрытую библиотеку и не может быть модифицирована. По правде сказать, изначально я планировал в своем проекте использовать этот загрузчик, ну или на крайний случай сделать свою оболочку на базе их dll. Загрузка прошивки в ПЛИС осуществляется в пассивном последовательном режиме (passive serial — PS), FTDI работает в режиме MPSSE. На макетной плате работоспособность решения MORPH-IC-II была полностью подтверждена, однако проблема, как оно часто бывает, пришла откуда не ждали. Выяснилось, что при работе dll MORPH-IC-II все подключенные устройства FTDI блокируются, а в составе учебного комплекса есть еще два устройства с подобными преобразователями: генератор и анализатор сигналов. Одновременная работа с ними не представляется возможной. Чертовски странно и досадно.
Похожий кейс реализован у ребят из Марсохода: USB JTAG программатор MBFTDI. Там тоже используется FTDI в режиме MPSSE, только в отличии от MORPH-IC-II работа с ПЛИС происходит в режиме JTAG. Исходники в свободном доступе, однако внятного указания на их статус (лицензии) я не нашел. Поэтому использовать их в коммерческом проекте у меня рука не поднялась.
Исправлю такую оплошность, все, что будет представлено в рамках данной статьи, выложено в открытый репозиторий под лицензией 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.

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


q-rjpeqe5y8ojlu8ywedoejklxq.png

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


54etuy0yva1est_qtdqp9yopqj8.png

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

Рассмотрим работы FTDI в режиме MPSSE. Режим MPSSE (Multi-Protocol Synchronous Serial Engine), на мой взгляд, является более-менее удачной попыткой создать некий конструктор последовательных интерфейсов, дать разработчику возможность реализовать широко распространенные протоколы передачи данных, такие как SPI, I2C, JTAG, 1-wire и многие другие на их основе.
В настоящий момент режим доступен для микросхем: FT232H, FT2232D, FT2232H, FT4232H. В своем проекте я использую FT2232H, поэтому в большей степени речь идет о ней. Для режима MPSSE выделено 16 ножек, разделенных на два байта: младший L и старший H. Каждый байт может быть прочитан или установлен. Четыре младшие ноги байта L имеют особые функции — через них может происходить последовательная передача данных. Каждая нога может быть настроена как вход или выход, для вывода может быть задано значение по умолчанию. Для последовательной передачи настраивается порядок следование бит (MSB/LSB), длина передаваемого слова, частота тактовых импульсов, фронт синхронизации — передний (Rising) или задний (Falling), можно выбрать передачу только тактовых импульсов без данных, или выбрать 3-х фазовое тактирование (актуально для I2C) и многое другое.
Плавно переходим к программированию. Существуют два альтернативных способа программного взаимодействия с чипами FTDI: первый, назовем его классическим, в этом случае при подключении к порту USB микросхема в системе определяется как виртуальный последовательный порт (COM), операционная система использует драйвер VCP (Virtual COM Port). Все дальнейшее программирование не отличается от программирования классического COM порта: открыл — передал/считал — закрыл. Причем это справедливо для различных операционных систем, включая Linux и Mac OS. Однако при таком подходе не получится реализовать все возможности контроллера FTDI — чип будет работать как переходник USB-UART. Второй способ обеспечивается проприетарной библиотекой FTD2XX, это интерфейс предоставляет специальные функции, которые не доступны в стандартном API COM порта, в частности, доступна настройка и использование специальных режимов работы, таких как MPSSE, 245 FIFO, Bit-bang. Библиотека FTD2XX API хорошо задокументирована Software Application Development D2XX Programmer’s Guide, широко и давно известна в узких кругах. И да, FTD2XX также доступна для различных операционных систем.
Перед разработчиками FTDI стояла задача уложить относительно новый MPSSE в существующую программную модель взаимодействия D2XX. И им это удалось, для работы в режиме MPSSE используется тот же набор функций, что и для других «классических» режимов, используется та же библиотека FTD2XX API.
Если коротко, то алгоритм работы в режиме MPSSE можно описать следующим образом:


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


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

Приступим к практической части. В своих экспериментах в качестве IDE я буду использовать Eclipse версии Oxygen.3a Release (4.7.3a), в качестве компилятора — mingw32-gcc (6.3.0). Операционная система Win7.
С сайта FTDI скачиваем последнюю актуальную версию драйвера для своей операционной системы. В архиве находим заголовочный файл ftd2xx.h с описанием всех функций API. Сам API реализован в виде ftd2xx.dll, но динамический импорт оставим на потом, и воспользуемся статической линковкой: нам понадобится файл библиотеки ftd2xx.lib. Для моего случая ftd2xx.lib лежит в каталоге i386.
В Eclipse создаем новый Си проект. Создание makefile можно доверить IDE. В настройках линковшика указываем путь и название библиотеки ftd2xx (я требуемые файлы перенес в директорию проекта в папочку ftdi). Я не буду заострять внимание об особенностях настройки проекта под Eclipse, так как подозреваю, что большинство для программирования под Win использует другие среды и компиляторы.


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

FTD2XX API позволяет открыть чип используя ту или иную известную информацию о нем. Это может быть его порядковый номер в системе: первая подключенная микросхема FTDI примет номер 0, последующая 1 и так далее. Номер в системе определяется порядком подключения микросхем, мягко говоря, это не всегда удобно. Для открытия чипа по номеру используется функция FT_Open. Открыть чип можно по его серийному номеру (FT_OPEN_BY_SERIAL_NUMBER), описанию (FT_OPEN_BY_DESCRIPTION) или по расположению (FT_OPEN_BY_LOCATION), для этого используется функция FT_OpenEx. Серийный номер и описание хранятся во внутренней памяти чипа и могут быть записаны туда при производстве прибора в составе которого установлен FTDI. Описание, как правило, характеризует тип прибора либо семейство, а серийный номер должен быть уникальным для каждого изделия. Поэтому, наиболее удобным вариантом идентификации поддерживаемых разрабатываемой программой приборов является его описание. 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++)
  {
    printf("Dev %d:\n",i);
    printf(" Flags=0x%x\n",devInfo[i].Flags);
    printf(" Type=0x%x\n",devInfo[i].Type);
    printf(" ID=0x%x\n",devInfo[i].ID);
    printf(" LocId=0x%x\n",devInfo[i].LocId);
    printf(" SerialNumber=%s\n",devInfo[i].SerialNumber);
    printf(" Description=%s\n",devInfo[i].Description);
  }


Поприветствуем мой зоопарк
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

К моему ПК подключено три прибора с чипами FTDI: FT232RL (type 0×5), FT2232H (type 0×6) и FT232H (tepe 0×8). Чип FT2232H в системе отобразился как два независимых прибора (Dev 1 и Dev 2). Интерфейс 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;
}

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

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

$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))

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


Листинг отправки команды
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;

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

dwNumBytesToRead = 2: FAh FEh

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


sc6gnoqy5_eyc5lsjpmlqbnl2-m.png

Линии 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

Все используемые линии расположены на младшем байте порта MPSSE. Для установки значения используем op-code 0×80. Это команда предполагает два аргумента: первый следующий за 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(). Запустим программу и с помощью осциллографа убедимся, что на всех линиях PS установились заданные по умолчанию уровни. Поменяв значение этих уровней в инициализации (ничего страшного с ПЛИС не случится), убеждаемся, что процессор MPSSE желаемое выдает за действительное — все адекватно работает и можно переходить к передаче данных.
Последовательная отправка и прием данных выполняется в командном режиме с помощью все тех же op-code. Первый байт команды — op-code, который определяет тип операции, за ним следует длина передаваемой или принимаемой последовательности и, если это передача, собственно данные. Процессор MPSSE может передавать и принимать данные, также делать это одновременно. Передача может осуществляться либо младшим значащим битом вперед (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, иначе это другая группа команд

При конфигурировании ПЛИС по схеме PS передача данных происходит по переднему фронту в режиме LSB. Для нас удобнее оперировать байтами, а не битами, в этом случае op-code примет значение 0001_1000b или 0×18 в шестнадцатеричном представлении. Аргументами команды будет длина передаваемой последовательности (два байта, начиная с младшего), и сама последовательность данных. Следует учесть небольшую особенность: длина кодируется за вычетом единицы. То есть, если мы хотим отправить один байт, то длина будет равна 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;
}

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

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

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

Синий канал — сигнал с линии DATA[0], красный канал — DCLK. Для наглядности активирована функция последовательного декодирования и бинарный код показан непосредственно под сигналом. Как видно, что отправили, то и получили.
На данном этапе мы можем утверждать, что у нас реализован SPI интерфейс (ну почти). Для того, чтобы превратить его в PS, нужно настроить работу с флагами. Три флага 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. Во всех примерах и мануалах, после работы с процессором MPSSE перед закрытием FTDI рекомендуется перевести ее в режим по умолчанию. Однако при этом, флаг nCONFIG окажется в нуле и ПЛИС «забудет» все то, что мы в нее загрузили, поэтому после отработки алгоритма оставляем все как есть, просто закрываем файл и порт.


Листинг функции 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.

© Habrahabr.ru