Завантаження конфігурації в ПЛІС через USB або розбираємо FTDI MPSSE

У житті кожного плисовода настає момент, коли потрібно написати власний завантажувач файлу конфігурації в ПЛІС. Довелося мені брати участь у розробці навчального стенду для кафедри одного технічного вузу. Стенд призначений для вивчення цифрової обробки сигналів, хоча в рамках цієї статті це не має особливого значення. А значення має те, що в основі стенду варто ПЛІС (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.

Для початку конфігурації зовнішній провідний контролер повинен генерувати перехід з низького рівня до високого на лінії 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 МГц.

 

Мінімальної ж частоти передачі не існує, теоретично можна призупинити конфігурацію на невизначений час. Це дає відмінні можливості покрокової налагодження з участю осцилографа, ніж ми неодмінно скористаємося.

 

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

Хитрий звір 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 добре задокументована Application Software Development D2XX programmer’s Guide, широко і давно відома у вузьких колах. І так, FTD2XX також доступна для різних операційних систем.

Читайте також  Несподівана зустріч. Глава 18 [остання глава, вихід на краудфандінг]

 

Перед розробниками FTDI стояло завдання щодо укласти новий MPSSE в існуючу програмну модель взаємодії D2XX. І їм це вдалося, для роботи в режимі MPSSE використовується той же набір функцій, що і для інших “класичних” режимів, використовується та ж бібліотека FTD2XX API.

 

Якщо коротко, то алгоритм роботи в режимі MPSSE можна описати наступним чином:

 

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

 

Пишемо завантажувач

 

Приступимо до практичної частини. У своїх експериментах в якості IDE я буду використовувати Eclipse версії Oxygen.3a Release (4.7.3 a), як компілятора — 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 %dn",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%xn",devInfo[i].Flags);
 printf(" Type=0x%xn",devInfo[i].Type);
 printf(" ID=0x%xn",devInfo[i].ID);
 printf(" LocId=0x%xn",devInfo[i].LocId);
 printf(" SerialNumber=%sn",devInfo[i].SerialNumber);
 printf(" Description=%sn",devInfo[i].Description);
 }

Привітаємо мій зоопарк

D:workspaceftdi-mpsse-psDebug>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 = B LESO7
Dev 3:
Flags = 0x2
Type = 0x8
ID = 0x4036014
LocId = 0x213
SerialNumber = FTYZ92L6
Description = LESO4.1_ER

 

До мого ПК підключено три приладу з чіпами FTDI: FT232RL (type 0x5), FT2232H (type 0x6) і FT232H (tepe 0x8). Чіп 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 ("Ореп failurern");
 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 failurern");
 return FT_DEVICE_NOT_OPENED;
}
 printf ("open OK, %drn", 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 %dn", 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.

 

Читайте також  DevCore: програмна частина проекту DevBoy

Після кожної відправки команди корисно віднімати відповідь процесора, так як у випадку невірної команди відповідь може містити повідомлення про помилку — символ 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 МГц. Звідси формула для розрахунку швидкості передачі даних:

 

 

 

де 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 (0x8A) і шолом команду процесору:

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

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("Errorrn");
 return FT_OTHER_ERROR;
}
else if (dwNumBytesToRead > 0)
{
 printf("dwNumBytesToRead = %d: dwNumBytesToRead);

 for ( int i = 0; i < dwNumBytesToRead; i++)
 printf (" %02Xh", byInputBuffer[i]);

printf("rn");
 return FT_INVALID_PARAMETER;
}

return FT_OK;

 

У порядку експерименту, замість дійсної команди 0x8A, пошлемо значення 0xFE, яким не відповідає жоден op-code, висновок консолі:

 

dwNumBytesToRead = 2: FAh FEh

 

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

Лінії DCLK, DATA[0], nCONFIG повинні бути сконфігуровані як виходу, лінії nSTATUS, CONF_DONE — як входи. По діаграмі визначаємо які початкові стани повинні бути у ліній. Для наочності терморегулятори схеми зведемо в таблицю:

 

FPGA pin Pin Name Pin MPSSE Direction default

DCLK BDBUS0 38 TCK/SK Out
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 0x80. Це команда передбачає два аргументи: перший наступний за 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 interface MPSSE
#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 interface MPSSE
 // - 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 interface MPSSE
 // - 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 MPSSErn");
 return FT_OTHER_ERROR;
}
 else if (dwNumBytesToRead > 0)
{
 printf("Error in initializing the MPSSE, bad code:rn");

 for ( int i = 0; i < dwNumBytesToRead; i++)
 printf (" %02Xh", byInputBuffer[i]);

printf("rn");
 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 — позитивний, 1 — негативний
1 1 — робота з байтами, 0 — робота з бітами
2 Синхронізація по фронту на читання: 0 — позитивний, 1 — негативний
3 Режим передачі: 1 — LSB, 0 — MSB first
4 Передача даних по лінії TDI
5 Читання даних з лінії TDO
6 Передача даних по лінії TMS
7 Повинен бути 0, інакше це інша група команд
Читайте також  Курс молодого бійця PostgreSQL

 

При конфігуруванні ПЛІС за схемою PS передача даних відбувається по передньому фронту в режимі LSB. Для нас зручніше оперувати байтами, а не бітами, в цьому випадку op-code прийме значення 0001_1000b або 0x18 у шістнадцятковому представленні. Аргументами команди буде довжина переданої послідовності (два байти, починаючи з молодшого), і сама послідовність даних. Слід врахувати невелику особливість: довжина кодується за вирахуванням одиниці. Тобто, якщо ми хочемо відправити один байт, то довжина дорівнює 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 datarn");
 return ftStatus;
 } else if (dwNumBytesSent != dwNumBytesToSend)
{
 printf ("ERROR send data, %d %drn", 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));

 

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

 

Синій канал — сигнал з лінії 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("read Error Lbytern");
 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 Lbytern");
 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 filern");
 return -1;
}

 frbf = fopen(argv[1],"rb");
 if (frbf == NULL)
{
 printf ("Error open rbfrn");
 return -1;
}

 ftStatus = FT_CreateDeviceInfoList(&numDevs);
 if ((numDevs == 0) || (ftStatus != FT_OK))
{
 printf("Error. FTDI devices not found in the systemrn");
 return -1;
}

 ftStatus = MPSSE_open ("LESO7 B");
 if (ftStatus != FT_OK)
{
 printf("Error in MPSSE_open %dn", ftStatus);
EXIT(-1);
}

MPSSE_setup();
 if (ftStatus != FT_OK)
{
 printf("Error in MPSSE_setup %dn", ftStatus);
EXIT(-1);
}

 printf ("nConfig -> 0rn");
 MPSSE_set_lbyte(0, 1 << N_CONFIG);

 printf ("nConfig -> 1rn");
 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 respondingrn");
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("rn");
}
 while (readBytes == MPSSE_PCK_SEND_SIZE);

printf("rn");

 memset(buff, 0x00, sizeof(buff));
 MPSSE_send(buff, 1); // невже ні хто не помітить цю дивну рядок?
 printf("Load completern");

 // wait CONF_DONE set
 // A low-to-high on the transition 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 ("read Error CONF_DONErn");
EXIT(-1);
}
 if (i++ > TIMEOUT_CONF_DONE)
{
 printf ("Error CONF_DONErn");
EXIT(-1);
}
Sleep(2);
}
 while (((lowByte >> CONF_DONE) & 1) == 0);
 printf("Configuration completern");

FT_Close(ftHandle);
fclose(frbf);
}

 

Приклад запуску програми:

 

Ореп "LESO7 B" OK
nConfig -> 0
nConfig -> 1
**
Load complete
Configuration complete

 

Утиліта успішно завантажує rbf-файл в ПЛІС. ПЛІС радісно моргає світлодіодами. Виставляємо максимальну швидкість передачі даних до 30 Мбіт/сек і переконуємося у працездатності.
До мінусів можна віднести те, що відсутня можливість налагодження і одержаний завантажувач все-таки не JTAG.

 

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

 

  1. FTDI-MPSSE-Altera PS. Репозиторій з проектом.
  2. Навчальний стенд для ЦГЗ. Залізо для досвіду. Там же знайдете повну принципову схему приладу.
  3. Application Software 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.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *