В предыдущих статьях мы сделали достаточно интересную железку, состоящую из контроллера FX3 и ПЛИС Cyclone IV. Мы научились гонять через шину USB 3.0 потоки данных с достаточно высокой скоростью (я доказал, что поток 120 МБ/с из ULPI будет проходить через эту систему без искажений и потерь). Всё хорошо, но система, которая просто гонит данные, не имеет смысла. Любую систему надо настраивать. То есть, хочешь — не хочешь, а кроме скоростных данных надо слать не очень спешные команды.
У шины USB для передачи команд предназначена конечная точка EP0. Сегодня мы потренируемся дорабатывать «прошивку» FX3 так, чтобы она обрабатывала команды от PC, а также транслировала их через GPIO в сторону ПЛИС. Кстати, именно здесь проявляется преимущество контроллера над готовым мостом. Что меня в текущей реализации Redd сильно удручает – я не могу посылать никаких команд. Их можно только упаковать в основной поток. В случае же с контроллером – что хочу, то и делаю. Начинаем творить, что хотим…
Предыдущие статьи цикла:
Осматривая исходники типовой «прошивки», я нашёл знакомое имя функции в файле cyfxgpiftousb.c. Функцию зовут:
Имея за плечами опыт работы с кучкой USB-контроллеров, начиная от прямого предка нашего (это был FX2LP), через STM32 и далее со всеми остановками, я уже нутром чую, что нужная нам функциональность начинается здесь. Собственно, код этой функции как раз разбирает команды группы STANDARD Request. Осталось добавить туда свою группу VENDOR COMMANDS. Жаль только, что все команды, которые уже имеются в готовой функции, не передают данных. Они ограничиваются работой с полями wData и wIndex, Мне этого недостаточно. Я хочу передавать в ПЛИС байт и два 32-битных слова (команда, адрес, данные), либо передавать байт и DWORD, после чего – принимать DWORD (передали команду и адрес, приняли данные). То есть, без фазы данных точно не обойтись. Начинаем разбираться, где черпать вдохновение и добавлять желаемую функциональность.
Итак. Добавить фазу данных. Гуглю по слову:
CyU3PUsbAckSetup
И первая же ссылка ответила на все мои вопросы. На всякий случай вот она.
В том коде данные гоняют и туда, и обратно. Хорошо. Начнём с малого. Сначала вставляем только прогон данных через USB, без их передачи в ПЛИС. Будем для самоконтроля отправлять данные в UART, а при приёме, чтобы не тратить время на сложный вспомогательный код, просто будем заполнять память константами 00, 01 02 03…
Добавляем в конец функции CyFxApplnUSBSetupCB() такой блок:
«Волшебная константа» 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 – получаем:
Прекрасно! Эта часть готова! Переходим к работе с аппаратурой.
В том же примере, который я нашёл на github, идёт и работа с GPIO. Вот как красиво выглядит это в пользовательской части:
Красиво? Ну, конечно же, красиво! Но впору вспомнить, что я писал в одной из статей про нашу ОСРВ МАКС.
Я там рассказывал, что операторы new и delete по факту раскрываются в огромный кусок кода с непредсказуемым временем исполнения. Примерно так и тут. Функция CyU3PGpioSetValue() раскрывается в такую громаду, что я спрячу её под кат.
Какое будет максимальное быстродействие у кода, вызывающего эту функцию в цикле, мне страшно подумать. У неё есть более компактный аналог, но и его я предпочту спрятать под кат.
Так что придётся написать что-то своё на скорую руку, выкинув лишние проверки. Эта функция обслуживает вызовы не от безвестных пользователей, которые в теории могут учудить всё, что угодно, а от меня. Про некоторых пользователей я наслышан от коллеги, разбирающего запросы поддержки одной библиотеки. Но я уж точно настроил порты при старте, зачем при каждом обращении к порту это проверять, тратя такты процессора?
Чтобы не хранить маску записанных в порт данных, а также обеспечить себе максимальную потокобезопасность, мы можем воспользоваться аппаратурой, дающей независимый доступ к каждому биту порта. Вдохновение мы будем искать в разделе 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. Вот с ними я и буду работать.
Все, кто хорошо знаком с архитектурой 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);
Поправим его так:
Биты 9, 10, 11 и 12 в коде – это биты старшего слова. Поэтому физически они соответствуют битам GPIO 9+32=41, 10+32=42, 11+32=43 и 12+32=44. Тем самым, с которыми я собираюсь работать.
Зададим ещё им направления. Скажем, я раскидаю их так:
Объявим для этого следующие макросы:
А в функцию CyFxApplnInit() добавим такой код:
Всё, блок GPIO инициализирован, направления заданы. А линия SS ещё и взведена в единицу. Можно начинать пользоваться GPIO для реализации функциональности.
Запись в SPI я сделаю в виде макросов «взвести в 1» и «Сбросить в 0» (увы, именно макросов, перед нами же код на чистых Сях, в плюсах я бы сделал на шаблонных функциях) и одной функции, которая обращается к ним. Получилось так:
Соответственно, вместо вывода в UART в ранее написанном обработчике USB-команд, я сделаю вывод в SPI, но по очень хитрому алгоритму. Сначала – байт USB-команды. Затем – слова wData и wIndex, и потом – DWORD, пришедший в фазе данных. При такой солянке сборной, удобнее всё передавать младшим битом вперёд (именно так работает функция SPI_Write()).
Чтение я пока делать не буду. Сейчас проверяется сама идея. Чтобы проверить чтение, надо делать «прошивку» и для ПЛИС, а запись я могу проконтролировать и при помощи осциллографа.
В результате, код обработчика Vendor-команды трансформируется следующим образом:
Итого, даём такой запрос:
И получаем такой результат:
Видно, что данные передаются младшим битом вперёд, хорошо видны байт 0x23 и начало байта 0x55. Всё верно. Правда, частота, конечно, не ахти (её можно разглядеть, если кликнуть по рисунку и посмотреть его в увеличенном виде). Примерно 1.2 мегагерца. В целом, меня сейчас это сильно не беспокоит, но здесь скорее важен сам принцип. Не люблю, когда всё совсем медленно, и всё тут! Смотрим, во что превратилась функция записи, в этом нам поможет файл GpifToUsb.lst:
16 строк. Вполне компактно… Я уже много раз писал, что не собираюсь становиться гуру FX3. Поэтому решил не вчитываться в километры документов, а поиграть с кодом на практике. Само собой, несколько часов опытов я опущу, и приведу только итоговый результат. Так что немножко младшим учеником старшего помощника второго заместителя гуру побыть пришлось… Но так или иначе. Я изучил вопрос настройки тактирования GPIO и пришёл к выводу, что оно вполне оптимальное.
Но напишем такой тестовый блок кода (первый макрос роняет значение в порту, второй – взводит, а дальше идёт чреда взлётов и падений):
Ему соответствует участок ассемблерного кода, оптимизировать который в целом, невозможно. Он идеален:
Результат прогона (получаем меандр с частотой 12.5 МГц):
А теперь заменим запись констант с прямой записи на чтение — модификацию — запись, как это реализовано в моих макросах для SPI:
В ассемблерном коде покажу только одну итерацию вверх-вниз
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
Более, чем в шесть раз частота упала! Получается, что чтение из порта – довольно медленная операция. Значит, чуть переписываем мои макросы записи в порт, убирая из них операции чтения:
Делаем прогон записи в SPI…
4 мегагерца. Ну вот. Не особо напрягаясь, разогнали систему почти вчетверо. Меня не покидает ощущение, что всё можно разогнать ещё сильнее, но оставим это на потом. Сейчас особо это не требуется.
Мы освоили механизм добавления VENDOR команд в USB-устройство на базе FX3. При этом мы испытали работу с командами, передающими данные через конечную точку EP0 в обоих направлениях. Также мы освоили работу с GPIO у этого контроллера. Теперь, кроме скоростной передачи через конечные точки типа BULK и GPIF, мы можем передавать команды в свою «прошивку» ПЛИС.
А для чего я хочу это применять, будет рассказано в следующей статье.
У шины 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<wLength;i++)
{
ep0_buffer [i] = (uint8_t) i;
}
CyU3PUsbSendEP0Data (wLength, ep0_buffer);
isHandled = CyTrue;
} else
{
CyU3PUsbGetEP0Data (wLength, ep0_buffer, NULL);
ep0_buffer [wLength] = 0; // Null terminated String
CyU3PDebugPrint (4, (char*)ep0_buffer);
CyU3PUsbAckSetup();
isHandled = CyTrue;
}
}
«Волшебная константа» 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() раскрывается в такую громаду, что я спрячу её под кат.
Смотреть текст функции 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 = %d\r\n", 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<wLength;i++)
{
ep0_buffer [i] = (uint8_t) i;
}
CyU3PUsbSendEP0Data (wLength, (uint8_t*)ep0_buffer);
isHandled = CyTrue;
} else
{
CyU3PUsbGetEP0Data (wLength, (uint8_t*)ep0_buffer, NULL);
ep0_buffer [wLength] = 0; // Null terminated String
CyU3PDebugPrint (4, (char*)ep0_buffer);
CLR_IO_BIT(MY_BIT_SS);
SPI_Write(bRequest,8);
SPI_Write(wValue,16);
SPI_Write(wIndex,16);
SPI_Write(ep0_buffer[0],32);
SET_IO_BIT(MY_BIT_SS);
CyU3PUsbAckSetup();
isHandled = CyTrue;
}
Итого
Итого, даём такой запрос:
И получаем такой результат:
Немного оптимизации
Видно, что данные передаются младшим битом вперёд, хорошо видны байт 0x23 и начало байта 0x55. Всё верно. Правда, частота, конечно, не ахти (её можно разглядеть, если кликнуть по рисунку и посмотреть его в увеличенном виде). Примерно 1.2 мегагерца. В целом, меня сейчас это сильно не беспокоит, но здесь скорее важен сам принцип. Не люблю, когда всё совсем медленно, и всё тут! Смотрим, во что превратилась функция записи, в этом нам поможет файл GpifToUsb.lst:
40003404 <SPI_Write>:
40003404: ea00000d b 40003440 <SPI_Write+0x3c>
40003408: e59f303c ldr r3, [pc, #60] ; 4000344c <SPI_Write+0x48>
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 <SPI_Write+0x4>
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, мы можем передавать команды в свою «прошивку» ПЛИС.
А для чего я хочу это применять, будет рассказано в следующей статье.
VBKesha
Спасибо за статью!
А аппаратный SPI не использовали просто потому, что эксперименты с GPIO шли или были ещё причины?
EasyLy Автор
Ой, там целая куча причин, и все — личные.
Первое — в следующей статье я покажу, что мне крайне невыгодна реально высокая скорость SPI. Я в ПЛИС автомат построил такой, который не терпит высокой скорости. А более крутой — не хотелось тратить время на разработку.
Дальше — я играл в SPI и даже в SDIO на разновидности FX3 по имени FX3S. Там работа была возможна только через хитрый API. Как я уже отметил, API у Кипарисов уж чересчур хитрый. Наверняка можно и напрямую через порты, но не хотелось тратить время на изучение.
Опять же, когда я просил товарища развести мне плату — не было времени детально разбираться и указывать настоящие SPI ноги (я до сих пор не знаю, какие именно нужны были бы, и есть ли они у FX3 без S). Тем более, что на тот момент у меня ещё не было видения, как я буду управлять ПЛИСкой со стороны FX3. Про SPI придумалось во время проекта на базе Litex, когда я играл в переходники «что угодно в WISHBONE». Именно тогда я подумал, что почему бы не сделать свой SPI в AVALON_MM?
В общем, управляющие ноги на плате выбраны по принципу «А почему бы и нет?». Сейчас я работаю на уже готовой плате. Так что на ней — только через GPIO и программную шину.
Итого, технических причин нет. Все — глубоко личные. Для моей задачи хватит этого, тратить время на ненужное — нет этого самого времени, а к статье это относится всё равно чисто, как вспомогательная вещь. Так что статью про Vendor команды вполне не стыдно было написать и с таким уровнем SPI.
А для моей задачи — я уже в этой статье писал, что «я не проверяю статуса, ведь я знаю, что всё работает медленно, так что точно успеет отработать». И перейдя с TCL на С++ и USB3, я продолжаю говорить то же самое. Теперь гарантированная задержка возникает в SPI. Удобно! Для данной конкретной задачи, разумеется.
Inanity
Есть причины. Если GPIF работает в 32-битном режиме, то SPI нельзя использовать — архитектурное ограничение FX3. Так что есть целесообразность реализации программного SPI для FX3. Единственная беда в том, что делитель клока для GPIO слишком большой, поэтому не получается переключать порт в ручном режиме на высокой скорости. Частота в 500КГц — уже удача. То, что автору удалось её поднять до 4МГц это реально успех, спасибо ему за это. Сам предложенную в статье идею с ускорением не проверял ещё, хотя есть один свой проект на FX3, где это очень даже будет полезно.