Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus

В прошлой статье мы сделали черновые прошивки для контроллера FX3 и ПЛИС, чтобы начать работу с шиной USB 3.0. Статья так разрослась, что проверку работоспособности системы мы отложили на потом. Сегодня мы проведём эту проверку (а как выяснится по ходу работ – ещё и оптимизацию «прошивки» для вывода работы на максимальную скорость).

Чтобы повысить полезность статьи, проверку мы будем производить при помощи логического анализатора SignalTap, встроенного в среду разработку Quartus Prime. Он позволит нам снимать временные диаграммы, не подключая никаких щупов к системе и не покупая никакого дополнительного оборудования.

Первая доработка кода FX3

Итак. В прошлой статье мы поняли, что надо импортировать в среду разработки EZ USB Suite проект SlaveFifoSync и подменить в нём файл cyfxgpif_syncsf.h на выданный мною (либо сгенерить основу при помощи GPIF II Designer, а затем – долго и нудно синхронизировать её содержимое с этим файлом, так как там все имена констант различаются).

Всё? Нет! В Application Note, который мы рассматривали в прошлой статье, сказано, что уровень срабатывания флага Watermark надо настроить при помощи вызова функции CyU3PGpifSocketConfigure(). Даже место для вставки указано (после вызова функции CyU3PGpifLoad()). А вот какое значение следует задать, это целая загадка. Сказано «зависит от настроек». И приведена кучка примеров, причём все не наши. Хорошо, я возьму блок из примера, который прилагается к Application Note. Точное же значение подберём по месту. В файле cyfxslfifosync.c появился такой фрагмент (я возьму несколько строк выше и ниже него, как реперные точки):

    /* Load the GPIF configuration for Slave FIFO sync mode. */
    apiRetStatus = CyU3PGpifLoad (&Sync_Slave_Fifo_2Bit_CyFxGpifConfig);
    if (apiRetStatus != CY_U3P_SUCCESS)
    {
        CyU3PDebugPrint (4, "CyU3PGpifLoad failed, Error Code = %dn",apiRetStatus);
        CyFxAppErrorHandler(apiRetStatus);
    }

#if(CY_FX_SLFIFO_GPIF_16_32BIT_CONF_SELECT == 1)
    CyU3PGpifSocketConfigure (0,CY_U3P_PIB_SOCKET_0,6,CyFalse,1);
    CyU3PGpifSocketConfigure (3,CY_U3P_PIB_SOCKET_3,6,CyFalse,1);
#else
    CyU3PGpifSocketConfigure (0,CY_U3P_PIB_SOCKET_0,3,CyFalse,1);
    CyU3PGpifSocketConfigure (3,CY_U3P_PIB_SOCKET_3,3,CyFalse,1);
#endif

    /* Start the state machine. */
    apiRetStatus = CyU3PGpifSMStart (SYNC_SLAVE_FIFO_2BIT_RESET, SYNC_SLAVE_FIFO_2BIT_ALPHA_RESET);
    if (apiRetStatus != CY_U3P_SUCCESS)
    {
        CyU3PDebugPrint (4, "CyU3PGpifSMStart failed, Error Code = %dn",apiRetStatus);
        CyFxAppErrorHandler(apiRetStatus);
    }

Пока с доработкой GPIF – всё.

Небольшая порча исходника для ПЛИС

Давайте условимся, что мы ещё не знаем, что надо задержать сигнал flagb на один такт. Так рассказ будет интереснее. Поэтому временно в файле USB3testMyCoresFX3_to_ST.sv заменим строку:
assign flagsab=flaga&flagb_d;
на:
assign flagsab=flaga&flagb;

Всё. Собираем оба проекта, заливаем их в целевые устройства… Как залить в контроллер, мы рассматривали в прошлой статье, а в ПЛИС мы уже на протяжении пары десятков статей всё заливаем. Если наше устройство нашлось со знаком вопроса, попросим систему обновить драйвер, указав путь для поиска примерно такой: C:Program Files (x86)CypressEZ-USB FX3 SDK1.3driver.

И вот мы видим его, готового к работе:

Как мы будем работать

Возникает вопрос: как мы будем работать с этим устройством? Элементарно! Через то же самое приложение Control Center, через которое заливаем «прошивку» в FX3. Раскрываем дерево и встаём на конечную точку 0x81 (маска 0x80 – признак того, что данные идут из устройства в PC):

А теперь – в правой части окна выбираем вкладку Data Transfers… И вот перед нами инструмент, при помощи которого мы сможем эти данные читать!

Мелочиться не станем. Попросим, скажем, 262144 байт (почему так много – расскажу в следующей статье). Задали, нажали кнопку Transfer Data-IN… И получили таймаут…

Приехали! Что не так? И как мы будем выявлять врага?

Знакомимся с логическим анализатором SignalTap

Итак. У нас что-то пошло не так. Надо бы понять, что именно. Но вот беда, модели для проведения моделирования у нас нет. Честно говоря, даже идеального описания-то я не нашёл. Хорошо бы поиграть с сигналами вживую, но настоящий логический анализатор подключать неудобно. Да и на частоте 100 МГц длинные провода дадут дикие наводки. Что делать? Сдаваться? Ну уж нет! Да и стал бы я писать статью, где в выводах стояло бы: «Ну всё, сдаёмся»? Поэтому действуем так: в Квартусе выбираем пункт меню Tools->Signal Tap Logic Analyzer.

Как этот анализатор работает? Он берёт свободную часть внутреннего ОЗУ ПЛИС и сохраняет в нём выбранные нами сигналы. Затем передаёт их в PC и отображает в виде временных диаграмм. Именно в этом его слабое место: обычно в моих проектах всё ОЗУ занято под нужды основной «прошивки». Опять же, много данных в это ОЗУ не поместишь. Но сегодня совершенно случайно у нас есть свободное пространство в памяти, поэтому можно рискнуть. Итак, мы выбрали пункт меню. Что мы видим? Сейчас я сожму окно, чтобы показать общий план. Потом буду дёргать участки, о которых будет идти рассказ.

С чего начать? Конечно, с добавления сигналов. Дважды щёлкаем по области с надписью «Double Click to add nodes». Попадаем в меню выбора сигналов:

Что будет, если просто нажать List? Будет тихий ужас. Перед нами появится вот такой список:

Имена сигналов достаточно сильно искажены при компиляции. Начинаем изучать правила искажения? Вовсе нет! Обратите внимание на строку, выделенную зелёной рамкой. По умолчанию выбран фильтр по типу Post Fitter. Искажения произошли сначала на этапе синтеза, потом – на этапе размещения. А можно взять имена, которые были до синтеза? Оказывается, можно! Мне нравятся вот эти два варианта (красный – больше всего, но и зелёный я иногда выбираю). Как видно, до них список надо прокрутить, скроллер расположен в самом низу.

Выбрали красный вариант, снова нажали List. Получили уже знакомые нам вещи:

Теперь выбрали из них те, которые полезны при отладке, и перекинули их в правый список, после чего – нажали кнопку Insert, а уже затем – Close. Не забывайте нажать Insert, я сначала забывал, думая, что достаточно перекинуть всё в правый список. Нет! Надо ещё и вставить… В общем, я себе сделал такой набор:

Теперь перейдём в правую часть окна. Любой анализатор должен тактироваться от сигнала с какой-то частотой дискретизации. Говорим, что мы будем тактироваться от Clk (нажав многоточие, далее – точно так же построив список сигналов и выбрав нужный):

Глубина подбирается методом проб и ошибок – на сколько хватит свободной памяти кристалла. Мне её хватило на 8 киловыборок. Это я выяснил опытным путём, постепенно увеличивая значение и проверяя, выругается о нехватке памяти при очередной сборке проекта или успешно соберёт.

Анализатор позволяет задавать массу вещей для того, чтобы набрать в прокрустово ложе имеющегося ОЗУ как можно больше полезной информации. Но цель сегодняшней статьи – не рассказать о нём, а просто показать, что он есть, и что с его помощью мы вполне можем отладить проект, не трогая систему руками, а чисто при помощи «мыши». Поэтому познакомиться с другими настройками и возможностями вы сможете, прочитав соответствующие статьи и посмотрев учебные видео, благо их в сети много. Из этой статьи только запомните, что, выбрав тип сигналов Pre Synthesis, вы получите вполне читаемые имена, а так – сегодня нам чрезвычайно важны временные характеристики, поэтому никакое сжатие мы выбирать не будем, а воспользуемся настройками по умолчанию, когда данные копятся от срабатывания триггера и до заполнения буфера, такт за тактом.

Следующий шаг – условие запуска (триггер). Я опущу десятки опытов по подбору лучшего значения для нашего случая, а просто скажу, что мы будем запускаться на падении сигнала flagb (у этого сигнала активное состояние – нулевое, так что это как раз факт, что буфер почти полон). Для этого встаём на строку flagb, столбец Trigger Condition и нажимаем правую кнопку «Мыши». В выпавшем контекстном меню выбираем отрицательный фронт.

Итого, список становится таким:

Запускаем анализатор? Нет! Это же всё должно быть размещено в ПЛИС. Поэтому сначала надо собрать ПЛИСовый проект с учётом внесённых изменений. Удобная кнопка для этого имеется в самом SignalTap, хотя можно воспользоваться и той кнопкой в Квартусе, к которой вы привыкли. Удобная кнопка – вот эта:

Компиляция завершена. Теперь-то запускаем? Нет! Об этом нам напоминает вот такое сообщение:

Сначала надо залить проект. Это можно сделать привычным вам способом либо через удобную кнопку в самом SignalTap:

Если эта кнопка не активна, выбираем при помощи соседней скрепки SOF-файл (файл, который следует заливать в ПЛИС), и она активируется.
Уффф. Файл собран и загружен, начинаем прогон. Делаем это при помощи следующей кнопки:

Первый прогон

Итак, я для чистоты эксперимента снова загрузил все прошивки, затем запустил логический анализатор на прогон и попытался считать данные при помощи Control Center. Вот итоговая временная диаграмма:

Flagb упал, flaga – нет. И никогда не упадёт. А flagb – никогда не поднимется. Поэтому новая передача не начнётся – мы никогда не взведём sink_ready (почему – исходники были в прошлой статье).
Почему flaga не упадёт? Потому что мы недозаполнили буфер. Надо положить туда ещё данных. То есть, мы слишком рано роняем flagb. Значение 3, переданное в аргументе функции CyU3PGpifSocketConfigure() – неправильное!

Я попробовал значение 2, получил тот же эффект. А вот со значением 1, получилась совсем другая проблема.

Второй прогон

Итак, теперь задание порога срабатывания flagb выглядит так:

То же самое текстом.

    CyU3PGpifSocketConfigure (0,CY_U3P_PIB_SOCKET_0,1,CyFalse,1);
    CyU3PGpifSocketConfigure (3,CY_U3P_PIB_SOCKET_3,1,CyFalse,1);

Оба флага падают вовремя:

И даже данные пришли:

Но эти данные несколько искажены. Они искажены в случайных местах и случайным образом. Так как я сейчас в теме, то быстро понял, что прост FIFO постоянно переполняется, поэтому таймер всё время показывает сбой. Почему это происходит? Давайте посмотрим на временную диаграмму в полном масштабе (картинку можно увеличить, чтобы разглядеть в деталях):

Слева две колбаски с данными, справа начинается колбаска с данными… И всё. Остальное время – пустота. Разумеется, скорость передачи просто смешная. Большую часть времени система простаивает. Разумеется, таймер, тикающий на частоте 60 МГц, переполнит FIFO, пусть даже мы забираем данные на частоте 100 МГц, но сильно реже, чем надо.

Первое, что приходит в голову – внедрить вот этот рисунок из постоянно рассматриваемого нами AppNote:

Да-да! Он тоже не внедрён в типовой пример! Увеличим число буферов! Для этого в файле cyfxslfifosync.h заменим строку:
#define CY_FX_SLFIFO_DMA_BUF_COUNT (2) /* Slave FIFO channel buffer count */

На… Я попробовал менять константу на 4, 8 и даже 16. В примере используется 8 буферов. Так получилось, что сейчас я остановился на четырёх (почему – опишу чуть ниже). Итого:

#define CY_FX_SLFIFO_DMA_BUF_COUNT (4) /* Slave FIFO channel buffer count */

Третий прогон

Собираем новую версию, заливаем, проверяем… Колбасы стало чуть больше, но не очень существенно

Что-то здесь не так! Тогда я стал искать документацию и нашёл другой Application Note — AN86947. Там достигаются просто бешеные скорости, правда, для нашего режима гонится константа. Но зато я в коде, прилагаемом к тому документу, подглядел, как правильно произвести настройку буферов. В файле cyfxslfifosync.h добавляем пару полезных констант:

#define CY_FX_DMA_BUF_SIZE              (16384)
#define CY_FX_EP_BURST_LENGTH           (8)

А в файле cyfxslfifousbdscr.c пользуемся ими, для чего строку:

заменяем на:

А в блоке

То же самое текстом.

    CyU3PMemSet ((uint8_t *)&epCfg, 0, sizeof (epCfg));
    epCfg.enable = CyTrue;
    epCfg.epType = CY_U3P_USB_EP_BULK;
    epCfg.burstLen = 1;
    epCfg.streams = 0;
    epCfg.pcktSize = size;

делаем замену

То же самое текстом

    CyU3PMemSet ((uint8_t *)&epCfg, 0, sizeof (epCfg));
    epCfg.enable = CyTrue;
    epCfg.epType = CY_U3P_USB_EP_BULK;
    epCfg.burstLen = (usbSpeed == CY_U3P_SUPER_SPEED) ? (CY_FX_EP_BURST_LENGTH) : 1;
    epCfg.streams = 0;
    epCfg.pcktSize = size;

Собираем, проверяем… А я говорил, что не надо прошивать в ПЗУ, а надо пока что заливать в ОЗУ… Сколько раз мы уже заливаем? И не факт, что сейчас мы это делаем в последний раз…

Четвёртый прогон

Ну вот! Теперь дефицита колбасы не наблюдается! Теперь мы можем доверять пришедшим данным. Если искажение и произошло, то не из-за переполнения FIFO, а из-за ошибок в реализации протокола:

Смотрим…

Идёт инкремент на 1, и вдруг БАЦ! Сразу на 2. И что интересно, точка сбоя – круглая. Это не случайность. Таких сбоев много. Все на единицу, все по круглым адресам. Анализируя временные диаграммы в увеличенном виде, я доказал, что это место, где падает FLAGB. Теперь он падает слишком поздно, и одно слово, передаваемое по шине, уходит в переполнение.

Прямо как у Ильича: «Вчера было рано, завтра будет поздно». Тут же всё хуже. Если константа срабатывания 2 – это ещё рано, то 1 – уже поздно. Удачных промежуточных значений просто нет. Почему? А потому, что константа задаётся в DWORDах. Два DWORDа – это четыре WORDа. А один DWORD – это два WORDа. А у нас латентность равна трём WORDам. Вот и разгадка.

Именно поэтому я и пошёл на ухищрение: задал в коде значение 2 (не забудем сейчас поправить, у нас сейчас там единица), а в Verilog добавлена задержка этого флага на такт (не забудем вернуть её).

2 * 2 = 4
4 – 1 = 3.

Уфффф. Всё! Пришла пора научиться использовать разработанную систему в собственных программах, но это мы сделаем уже в следующей статье.

Итого

Итого, сегодня мы поняли следующее:

  1. Чтобы система работала вообще, в функции CyU3PGpifSocketConfigure должен использоваться аргумент 2. Это два 32-битных слова, что соответствует четырём 16-битным словам;
  2. В коде для ПЛИС flagb должен быть задержан на 1 такт, чтобы из четырёх 16-битных слов сделать три 16-битных слова;
  3. Чтобы канал использовался эффективно, надо воспользоваться примером из AN86947, а именно — увеличить число буферов (например, до четырёх, как сделано в том примере), увеличить размер буфера DMA, увеличить размер пакета. Почему-то в типовом примере эти значения заданы не оптимальным способом.

Заключение

Мы освоили работу с логическим анализатором SignalTap, встроенным в среду разработки Quartus Prime. С его помощью мы не только выявили и устранили две ошибки, но и выявили жутчайшую неоптимальность системы. Он, как тот котик с первого рисунка, помог нам выявить пропуски в заполнении колбасок. Зная о них, мы нашли очередной материал, позволивший нам всё оптимизировать (сделать заполнение безразрывным). И всё это мы сделали, не прикасаясь к плате руками. Никаких щупов! В теории, мы вообще это могли бы отлаживать удалённо, что полностью соотносится с концепцией комплекса Redd.

Аппаратура начерно готова, и можно приступать к программной работе с нею. Вот там будет 100% системное программирование…

Let’s block ads! (Why?)

Read More

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

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