У шины USB для передачи команд предназначена конечная точка EP0. Сегодня мы потренируемся дорабатывать «прошивку» FX3 так, чтобы она обрабатывала команды от PC, а также транслировала их через GPIO в сторону ПЛИС. Кстати, именно здесь проявляется преимущество контроллера над готовым мостом. Что меня в текущей реализации Redd сильно удручает – я не могу посылать никаких команд. Их можно только упаковать в основной поток. В случае же с контроллером – что хочу, то и делаю. Начинаем творить, что хотим…
- Начинаем опыты с интерфейсом USB 3.0 через контроллер семейства FX3 фирмы Cypress
- Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus
- Учимся работать с USB-устройством и испытываем систему, сделанную на базе контроллера FX3
- Боремся с таймаутами при использовании USB 3.0 через контроллер FX3, возникающими при определенных условиях
Введение
Осматривая исходники типовой «прошивки», я нашёл знакомое имя функции в файле cyfxgpiftousb.c. Функцию зовут:
/* Callback to handle the USB setup requests. */
CyBool_t
CyFxApplnUSBSetupCB (
uint32_t setupdat0, /* SETUP Data 0 */
uint32_t setupdat1 /* SETUP Data 1 */
)
Имея за плечами опыт работы с кучкой USB-контроллеров, начиная от прямого предка нашего (это был FX2LP), через STM32 и далее со всеми остановками, я уже нутром чую, что нужная нам функциональность начинается здесь. Собственно, код этой функции как раз разбирает команды группы STANDARD Request. Осталось добавить туда свою группу VENDOR COMMANDS. Жаль только, что все команды, которые уже имеются в готовой функции, не передают данных. Они ограничиваются работой с полями wData и wIndex, Мне этого недостаточно. Я хочу передавать в ПЛИС байт и два 32-битных слова (команда, адрес, данные), либо передавать байт и DWORD, после чего – принимать DWORD (передали команду и адрес, приняли данные). То есть, без фазы данных точно не обойтись. Начинаем разбираться, где черпать вдохновение и добавлять желаемую функциональность.
Участок в зоне ответственности шины USB
Итак. Добавить фазу данных. Гуглю по слову:
CyU3PUsbAckSetup
И первая же ссылка ответила на все мои вопросы. На всякий случай вот она.
В том коде данные гоняют и туда, и обратно. Хорошо. Начнём с малого. Сначала вставляем только прогон данных через USB, без их передачи в ПЛИС. Будем для самоконтроля отправлять данные в UART, а при приёме, чтобы не тратить время на сложный вспомогательный код, просто будем заполнять память константами 00, 01 02 03…
Добавляем в конец функции CyFxApplnUSBSetupCB() такой блок:
if (bType == CY_U3P_USB_VENDOR_RQT)
{
// Cut size if need
if (wLength > sizeof(ep0_buffer))
{
wLength = sizeof (ep0_buffer);
}
// Need send data to PC
if (bReqType & 0x80)
{
int i;
for (i=0;i
«Волшебная константа» 0x80 – согласен, что некрасивая, но не нашлось ничего подходящего в заголовках в районе изучаемого участка, а дальше искать не хотелось. Но, наверное, все помнят, что именно старший бит задаёт направление. Мало того, я в терминологии USB вечно путаюсь, что значит IN, что значит OUT. Я просто запомнил, что, когда есть 0x80 – данные бегут в PC. Остальное, вроде, всё красиво и понятно получилось, даже не требует комментариев.
Чтобы не писать своей тестовой программы, проверять я сегодня буду в сниффере BusHound. Если в нём дважды щёлкнуть по устройству, то появляется очень полезный диалог. Вот тут щёлкаем:
И вот такую красоту получаем:
Я заполнил тип команды 0xC0 (Vendor Specific, данные из устройства в PC). Код команды я сделал равным 23 просто так, чисто во время экспериментов. Сейчас туда можно вписать всё, что угодно, в функции это поле не проверяется. Не проверяются и поля Value и Index. А вот когда я вбил поле Length, у меня внизу появился дамп. Всё готово к посылке команды. Нажимаем Run, получаем:
Всё верно. Функция CyFxApplnUSBSetupCB() посылает из FX3 в USB инкрементирующиеся байты, мы их видим. Теперь пробуем передавать. Подключаем UART (как это сделать – я рассказывал в одной из предыдущих статей), запускаем терминал. Меняем тип запроса на 0x40 (Vendos Specific Command, данные из PC в устройство). Заполняем поля данных ASCII символами:
Жмём Run – получаем:
Прекрасно! Эта часть готова! Переходим к работе с аппаратурой.
Работа с GPIO
Грустная теория
В том же примере, который я нашёл на github, идёт и работа с GPIO. Вот как красиво выглядит это в пользовательской части:
CyU3PGpioSetValue (FPGA_SOFT_RESET, !((ep0_buffer[0] & GPIO_FPGA_SOFT_RESET) > 0));
CyU3PGpioSetValue (FMC_POWER_GOOD_OUT, ((ep0_buffer[0] & GPIO_FMC_POWER_GOOD_OUT) > 0));
Красиво? Ну, конечно же, красиво! Но впору вспомнить, что я писал в одной из статей про нашу ОСРВ МАКС.
Я там рассказывал, что операторы new и delete по факту раскрываются в огромный кусок кода с непредсказуемым временем исполнения. Примерно так и тут. Функция CyU3PGpioSetValue() раскрывается в такую громаду, что я спрячу её под кат.
CyU3PReturnStatus_t
CyU3PGpioSetValue (
uint8_t gpioId,
CyBool_t value)
{
uint32_t regVal;
uvint32_t *regPtr;
if (!glIsGpioActive)
{
return CY_U3P_ERROR_NOT_STARTED;
}
/* Check for parameter validity. */
if (!CyU3PIsGpioValid(gpioId))
{
return CY_U3P_ERROR_BAD_ARGUMENT;
}
if (CyU3PIsGpioSimpleIOConfigured(gpioId))
{
regPtr = &GPIO->lpp_gpio_simple[gpioId];
}
else if (CyU3PIsGpioComplexIOConfigured(gpioId))
{
regPtr = &GPIO->lpp_gpio_pin[gpioId % 8].status;
}
else
{
return CY_U3P_ERROR_NOT_CONFIGURED;
}
regVal = (*regPtr & ~CY_U3P_LPP_GPIO_INTR);
if (!(regVal & CY_U3P_LPP_GPIO_ENABLE))
{
return CY_U3P_ERROR_NOT_CONFIGURED;
}
if (value)
{
regVal |= CY_U3P_LPP_GPIO_OUT_VALUE;
}
else
{
regVal &= ~CY_U3P_LPP_GPIO_OUT_VALUE;
}
*regPtr = regVal;
regVal = *regPtr;
return CY_U3P_SUCCESS;
}
Какое будет максимальное быстродействие у кода, вызывающего эту функцию в цикле, мне страшно подумать. У неё есть более компактный аналог, но и его я предпочту спрятать под кат.
CyU3PReturnStatus_t
CyU3PGpioSimpleSetValue (
uint8_t gpioId,
CyBool_t value)
{
uint32_t regVal;
if (!glIsGpioActive)
{
return CY_U3P_ERROR_NOT_STARTED;
}
/* Check for parameter validity. */
if (!CyU3PIsGpioValid(gpioId))
{
return CY_U3P_ERROR_BAD_ARGUMENT;
}
regVal = (GPIO->lpp_gpio_simple[gpioId] &
~(CY_U3P_LPP_GPIO_INTR | CY_U3P_LPP_GPIO_OUT_VALUE));
if (value)
{
regVal |= CY_U3P_LPP_GPIO_OUT_VALUE;
}
GPIO->lpp_gpio_simple[gpioId] = regVal;
return CY_U3P_SUCCESS;
}
Так что придётся написать что-то своё на скорую руку, выкинув лишние проверки. Эта функция обслуживает вызовы не от безвестных пользователей, которые в теории могут учудить всё, что угодно, а от меня. Про некоторых пользователей я наслышан от коллеги, разбирающего запросы поддержки одной библиотеки. Но я уж точно настроил порты при старте, зачем при каждом обращении к порту это проверять, тратя такты процессора?
Чуть более оптимистичная теория
Чтобы не хранить маску записанных в порт данных, а также обеспечить себе максимальную потокобезопасность, мы можем воспользоваться аппаратурой, дающей независимый доступ к каждому биту порта. Вдохновение мы будем искать в разделе 9.2 GPIO Register Interface документа FX3_Programmers_Manual.pdf.
Вот так выглядит блок GPIO:
Мы видим, что кроме классического двоичного представления, есть такое, где каждой линии (а их в контроллере 61 штука) соответствует собственное 32-разрядное слово. Формат его такой:
Собственно, всё ясно. Так как я собираюсь работать с конкретными линиями GPIO, я вполне могу обращаться к битам IN_VALUE и OUT_VALUE в этих регистрах. Больше мне ничего и не надо. Ну, и настройку направления можно произвести здесь же.
С какими линиями мы работаем
Хорошо. Как нам достукиваться до линий, понятно. А как они адресуются? Что за 61 линия GPIO, о которых говорится в документации? С чем предстоит работать мне? Плату для меня разводил знакомый, которому я поставил очень простую задачу: несколько свободных линий от FX3 завести на ПЛИС. Так как конкретные номера не были мною обозначены, он взял те, которые захотел. Вот участок ПЛИС, к которому подходят линии GPIO, именованные в той нотации, какая задана на шелкографии около разъёма макетки:
Я собираюсь программно реализовать шину SPI, значит, мне надо 4 линии (выбор кристалла, тактовый сигнал и данные туда-обратно). Возьмём линии от DQ24 до DQ27 по принципу «А почему бы и нет?». В одной из прошлых статей, я уже показывал таблицу, при помощи которой мы можем быстро сопоставить эти имена с реальными линиями GPIO. Смотрим в неё:
Значит, нас интересуют линии GPIO 41, 42, 43 и 44. Вот с ними я и буду работать.
Инициализация GPIO
Все, кто хорошо знаком с архитектурой ARM, знают, что любые порты надо инициализировать. Как это сделать в нашем случае? Мы работаем с демонстрационным приложением, так что часть работы уже сделана за нас. Доработаем кое-что из готового кода. В функции main(), есть такой участок:
io_cfg.isDQ32Bit = CyTrue;
io_cfg.useUart = CyTrue;
io_cfg.useI2C = CyFalse;
io_cfg.useI2S = CyFalse;
io_cfg.useSpi = CyFalse;
io_cfg.lppMode = CY_U3P_IO_MATRIX_LPP_DEFAULT;
/* No GPIOs are enabled. */
io_cfg.gpioSimpleEn[0] = 0;
io_cfg.gpioSimpleEn[1] = 0;
io_cfg.gpioComplexEn[0] = 0;
io_cfg.gpioComplexEn[1] = 0;
status = CyU3PDeviceConfigureIOMatrix (&io_cfg);
Поправим его так:
io_cfg.isDQ32Bit = CyFalse;
io_cfg.useUart = CyTrue;
io_cfg.useI2C = CyFalse;
io_cfg.useI2S = CyFalse;
io_cfg.useSpi = CyFalse;
io_cfg.lppMode = CY_U3P_IO_MATRIX_LPP_UART_ONLY;
/* No GPIOs are enabled. */
io_cfg.gpioSimpleEn[0] = 0;
io_cfg.gpioSimpleEn[1] = (1<<9)|(1<<10)|(1<<11)|(1<<12);
io_cfg.gpioComplexEn[0] = 0;
io_cfg.gpioComplexEn[1] = 0;
status = CyU3PDeviceConfigureIOMatrix (&io_cfg);
Биты 9, 10, 11 и 12 в коде – это биты старшего слова. Поэтому физически они соответствуют битам GPIO 9+32=41, 10+32=42, 11+32=43 и 12+32=44. Тем самым, с которыми я собираюсь работать.
Зададим ещё им направления. Скажем, я раскидаю их так:
Бит | Цепь | Направление |
---|---|---|
41 | SS | OUT |
42 | CLK | OUT |
43 | MOSI | OUT |
44 | MOSI | IN |
Объявим для этого следующие макросы:
#define MY_BIT_SS 41
#define MY_BIT_CLK 42
#define MY_BIT_MOSI 43
#define MY_BIT_MISO 44
А в функцию CyFxApplnInit() добавим такой код:
CyU3PGpioClock_t gpioClock;
gpioClock.fastClkDiv = 2;
gpioClock.slowClkDiv = 16;
gpioClock.simpleDiv = CY_U3P_GPIO_SIMPLE_DIV_BY_2;
gpioClock.clkSrc = CY_U3P_SYS_CLK;
gpioClock.halfDiv = 0;
apiRetStatus = CyU3PGpioInit (&gpioClock, NULL);
if (apiRetStatus != CY_U3P_SUCCESS)
{
CyU3PDebugPrint (4, "GPIO Init failed, error code = %drn", apiRetStatus);
CyFxAppErrorHandler (apiRetStatus);
}
GPIO->lpp_gpio_simple[MY_BIT_SS] = CY_U3P_LPP_GPIO_OUT_VALUE | CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE;
GPIO->lpp_gpio_simple[MY_BIT_CLK] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE;
GPIO->lpp_gpio_simple[MY_BIT_MOSI] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE;
GPIO->lpp_gpio_simple[MY_BIT_MISO] = CY_U3P_LPP_GPIO_INPUT_EN | CY_U3P_LPP_GPIO_ENABLE;
Всё, блок GPIO инициализирован, направления заданы. А линия SS ещё и взведена в единицу. Можно начинать пользоваться GPIO для реализации функциональности.
Участок в зоне ответственности аппаратуры
Запись в SPI я сделаю в виде макросов «взвести в 1» и «Сбросить в 0» (увы, именно макросов, перед нами же код на чистых Сях, в плюсах я бы сделал на шаблонных функциях) и одной функции, которая обращается к ним. Получилось так:
#define SET_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] |= CY_U3P_LPP_GPIO_OUT_VALUE
#define CLR_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] &= ~CY_U3P_LPP_GPIO_OUT_VALUE
void SPI_Write (unsigned int data, int nBits)
{
while (nBits)
{
if (data&1)
{
SET_IO_BIT (MY_BIT_MOSI);
} else
{
CLR_IO_BIT (MY_BIT_MOSI);
}
SET_IO_BIT (MY_BIT_CLK);
data >>= 1;
nBits -= 1;
CLR_IO_BIT (MY_BIT_CLK);
}
}
Соответственно, вместо вывода в UART в ранее написанном обработчике USB-команд, я сделаю вывод в SPI, но по очень хитрому алгоритму. Сначала – байт USB-команды. Затем – слова wData и wIndex, и потом – DWORD, пришедший в фазе данных. При такой солянке сборной, удобнее всё передавать младшим битом вперёд (именно так работает функция SPI_Write()).
Чтение я пока делать не буду. Сейчас проверяется сама идея. Чтобы проверить чтение, надо делать «прошивку» и для ПЛИС, а запись я могу проконтролировать и при помощи осциллографа.
В результате, код обработчика Vendor-команды трансформируется следующим образом:
// Need send data to PC
if (bReqType & 0x80)
{
int i;
for (i=0;i
Итого
Итого, даём такой запрос:
И получаем такой результат:
Немного оптимизации
Видно, что данные передаются младшим битом вперёд, хорошо видны байт 0x23 и начало байта 0x55. Всё верно. Правда, частота, конечно, не ахти (её можно разглядеть, если кликнуть по рисунку и посмотреть его в увеличенном виде). Примерно 1.2 мегагерца. В целом, меня сейчас это сильно не беспокоит, но здесь скорее важен сам принцип. Не люблю, когда всё совсем медленно, и всё тут! Смотрим, во что превратилась функция записи, в этом нам поможет файл GpifToUsb.lst:
40003404 :
40003404: ea00000d b 40003440
40003408: e59f303c ldr r3, [pc, #60] ; 4000344c
4000340c: e3100001 tst r0, #1
40003410: e59321ac ldr r2, [r3, #428] ; 0x1ac
40003414: e1a000a0 lsr r0, r0, #1
40003418: 13822001 orrne r2, r2, #1
4000341c: 03c22001 biceq r2, r2, #1
40003420: e58321ac str r2, [r3, #428] ; 0x1ac
40003424: e59321a8 ldr r2, [r3, #424] ; 0x1a8
40003428: e2411001 sub r1, r1, #1
4000342c: e3822001 orr r2, r2, #1
40003430: e58321a8 str r2, [r3, #424] ; 0x1a8
40003434: e59321a8 ldr r2, [r3, #424] ; 0x1a8
40003438: e3c22001 bic r2, r2, #1
4000343c: e58321a8 str r2, [r3, #424] ; 0x1a8
40003440: e3510000 cmp r1, #0
40003444: 1affffef bne 40003408
40003448: e12fff1e bx lr
4000344c: e0001000 .word 0xe0001000
16 строк. Вполне компактно… Я уже много раз писал, что не собираюсь становиться гуру FX3. Поэтому решил не вчитываться в километры документов, а поиграть с кодом на практике. Само собой, несколько часов опытов я опущу, и приведу только итоговый результат. Так что немножко младшим учеником старшего помощника второго заместителя гуру побыть пришлось… Но так или иначе. Я изучил вопрос настройки тактирования GPIO и пришёл к выводу, что оно вполне оптимальное.
Но напишем такой тестовый блок кода (первый макрос роняет значение в порту, второй – взводит, а дальше идёт чреда взлётов и падений):
#define DOWN GPIO->lpp_gpio_simple[MY_BIT_CLK] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE
#define UP GPIO->lpp_gpio_simple[MY_BIT_CLK] = CY_U3P_LPP_GPIO_OUT_VALUE | CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
Ему соответствует участок ассемблерного кода, оптимизировать который в целом, невозможно. Он идеален:
400036c4: e58421a8 str r2, [r4, #424] ; 0x1a8
400036c8: e58431a8 str r3, [r4, #424] ; 0x1a8
400036cc: e58421a8 str r2, [r4, #424] ; 0x1a8
400036d0: e58431a8 str r3, [r4, #424] ; 0x1a8
400036d4: e58421a8 str r2, [r4, #424] ; 0x1a8
400036d8: e58431a8 str r3, [r4, #424] ; 0x1a8
Результат прогона (получаем меандр с частотой 12.5 МГц):
А теперь заменим запись констант с прямой записи на чтение — модификацию — запись, как это реализовано в моих макросах для SPI:
#define UP GPIO->lpp_gpio_simple[MY_BIT_CLK] |= CY_U3P_LPP_GPIO_OUT_VALUE
#define DOWN GPIO->lpp_gpio_simple[MY_BIT_CLK] &= ~CY_U3P_LPP_GPIO_OUT_VALUE
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
UP;
DOWN;
В ассемблерном коде покажу только одну итерацию вверх-вниз
400036e4: e59431a8 ldr r3, [r4, #424]; 0x1a8
400036e8: e3c33001 bic r3, r3, #1
400036ec: e58431a8 str r3, [r4, #424]; 0x1a8
400036f0: e59431a8 ldr r3, [r4, #424]; 0x1a8
400036f4: e3833001 orr r3, r3, #1
400036f8: e58431a8 str r3, [r4, #424]; 0x1a8
Вместо пары строк получаем шесть. Частота упадёт втрое? Делаем прогон…
12.5/1.9=6.6
Более, чем в шесть раз частота упала! Получается, что чтение из порта – довольно медленная операция. Значит, чуть переписываем мои макросы записи в порт, убирая из них операции чтения:
#define SET_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] = CY_U3P_LPP_GPIO_OUT_VALUE | CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE
#define CLR_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE
Делаем прогон записи в SPI…
4 мегагерца. Ну вот. Не особо напрягаясь, разогнали систему почти вчетверо. Меня не покидает ощущение, что всё можно разогнать ещё сильнее, но оставим это на потом. Сейчас особо это не требуется.
Заключение
Мы освоили механизм добавления VENDOR команд в USB-устройство на базе FX3. При этом мы испытали работу с командами, передающими данные через конечную точку EP0 в обоих направлениях. Также мы освоили работу с GPIO у этого контроллера. Теперь, кроме скоростной передачи через конечные точки типа BULK и GPIF, мы можем передавать команды в свою «прошивку» ПЛИС.
А для чего я хочу это применять, будет рассказано в следующей статье.