Боремся с таймаутами при использовании USB 3.0 через контроллер FX3, возникающими при определенных условиях

    Итак, в блоке из предыдущих трёх статей, мы рассмотрели, как можно изменить идеологию, принятую в первой версии комплекса Redd, заменив двухпроходную прокачку потоковых данных (сначала в буферное динамическое ОЗУ, а уже затем – в PC через интерфейс USB 2.0) на однопроходную (сразу в PC через интерфейс USB 3.0). Всё было замечательно, все тесты проходили на ура… И тут я решил проверить систему при поведении источника, отличном от того, в котором работали инженеры Cypress. И сразу нарвался на проблему, которая чуть было не похоронила все мои задумки. Как я к этому пришёл, и как прорвался – будет описано в данной внеплановой небольшой статье. Девизом её я бы сделал фразу, что не всегда хорошие показатели являются признаком полностью работающей системы.



    Предыдущие статьи цикла:

    1. Начинаем опыты с интерфейсом USB 3.0 через контроллер семейства FX3 фирмы Cypress
    2. Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus
    3. Учимся работать с USB-устройством и испытываем систему, сделанную на базе контроллера FX3

    Введение


    На самом деле, мне просто стало интересно, насколько стабильно система ведёт себя в моменты, когда FIFO опустошается. Напомню, о каком FIFO идёт речь (термин «Шина FX3» заменяет термин Slave FIFO, чтобы не создавать путаницы, пусть на рисунке слово FIFO присутствует только один раз):



    Входная шина FIFO тактируется частотой 60 МГц (это частота работы ULPI), выходная – 100 МГц (эту частоту я подглядел в примерах от Cypress). Если паузы, создаваемые работой шины FX3, малы (а они, как мы выяснили, достаточно малы), то FIFO рано или поздно опустеет и начнёт работать в очень неприятном режиме. Слово получено, тут же ушло, блок FIFO снова пуст примерно на такт (примерно – потому что отношение частот не целое). И так далее. То есть, что передавать, то нет.

    Мы уже выяснили, что при старте системы, контроллер FX3 ничего не выдаёт в шину USB, а генератор тестовых данных выдаёт поток всё время. Соответственно, FIFO заполняется под завязку. Как бы создать такую ситуацию, когда генератор начинает слать данные только тогда, когда у него попросят, но при этом не сильно перепахивать систему? Очень просто. Смотрим исходный код генератора:

        always @ (posedge clk, posedge reset)
        if (reset == 1)
        begin
            counter <= 0;
        end else
        begin
            if (source_valid)
                counter <= counter + 1;
        end
    

    Надо просто удерживать кнопку Reset: когда она нажата – передача не идёт… Правда, эта кнопка дребезжит, поэтому система в раздаточном материале несколько отличается от той, которая была выдана в прошлый раз. Но борьба с дребезгом выходит за рамки цикла статей. Кому интересно, как я её реализовал, может посмотреть, скачав материалы. Также слегка изменён и код таймера, но честно говоря, я даже точно не помню, как именно.

    Итак, удерживаем кнопку Reset, загружаем FX3, загружаем ПЛИС… Запускаем код для PC, отпускаем кнопку Reset… Барабанная дробь… Получаем картину Репина «Приплыли»:



    Это не ошибка эксперимента. Надпись появляется стабильно при этом сценарии, а при штатном сценарии, всё работает.

    Начинаем разбираться.

    Кто виноват


    Раз у нас уже настроен SignalTap, грех не проверить, как там всё проходит. Вот такая картинка появляется в момент отпускания кнопки Reset (кто возьмёт старые исходники, тот увидит сильнейший дребезг и начало счёта от нуля, снова от нуля, опять от нуля, вновь от нуля и так далее, так что брать надо материалы из сегодняшнего раздаточного материала).



    Причём до отпускания кнопки, анализатор не будет стартовать. Всё верно.

    Далее, флаги flaga и flagb будут в нулевом состоянии, а значит – система заполнила все буфера и не может от нас ничего принимать.



    Но хоть она и заполнила буфера, а не отдаёт ничего в USB! PC ждёт, а FX3 – не даёт. Хоть ему и есть, что отдать!

    Я провёл опрос среди знакомых, когда-либо работавших с FX3. Оказалось, что все они работали с потоками (на проекте с SDR-модемом), поэтому не могут вспомнить каких-либо проблем. Самое разумное, что они смогли предположить – что почему-то не запускается DMA.

    Но это проверить не просто, а очень просто. DMA у нас работает в ручном режиме. Я не знаю почему, пример так настроен был. Я уже отмечал, что в рамках решения этой задачи, не хочу становиться гуру FX3. Я хочу просто взять готовый пример от производителя и работать с ним. Так что в ручном и в ручном. А в ручном режиме, когда данные пришли из GPIF, мы должны отстробировать этот факт. За него отвечает вот такой интересный кусок, доставшийся нам также от производителя.

    Смотреть код.
    void
    CyFxSlFifoPtoUDmaCallback (
            CyU3PDmaChannel   *chHandle,
            CyU3PDmaCbType_t  type,
            CyU3PDmaCBInput_t *input
            )
    {
        CyU3PReturnStatus_t status = CY_U3P_SUCCESS;
    
        if (type == CY_U3P_DMA_CB_PROD_EVENT)
        {
            /* This is a produce event notification to the CPU. This notification is 
             * received upon reception of every buffer. The buffer will not be sent
             * out unless it is explicitly committed. The call shall fail if there
             * is a bus reset / usb disconnect or if there is any application error. */
            status = CyU3PDmaChannelCommitBuffer (chHandle, input->buffer_p.count, 0);
            if (status != CY_U3P_SUCCESS)
            {
                CyU3PDebugPrint (4, "CyU3PDmaChannelCommitBuffer failed, Error code = %d\n", status);
            }
    
            /* Increment the counter. */
            glDMATxCount++;
        }
    }
    


    В нём интересна последняя строка. Она увеличивает некий счётчик. Что это за счётчик? Оказывается, он раз в секунду выкидывается в UART, за что отвечает такой код:
        for (;;)
        {
            CyU3PThreadSleep (1000);
            if (glIsApplnActive)
    {
      /* Print the number of buffers received so far from the USB host. */
      CyU3PDebugPrint (6, "Data tracker: buffers received: %d, buffers sent: %d. \r\n",
            glDMARxCount, glDMATxCount);
      }
    }
    

    Что за UART? Тоже всё красиво. Разработчики макетки своих пользователей очень любят. Я уже отмечал их удобную шелкографию, сейчас — за UART похвалю. Подключаемся к вот этому разъёму Micro USB:



    И у нас в системе появляется COM-порт. Подключаемся к нему терминалом на скорости 115200 и начинаем мониторинг. Вот что он говорит, когда данных нет:



    Всё по нулям. Теперь запускаем программу для PC (я сделал так, чтобы она принимала 0x200000 байт, больше нам не надо). Показания не изменяются. Всё те же нули!!! Логично, источник же не выдал еще ничего, нам нечего отправлять.

    Быстренько, пока EXEшник не отвалился по таймауту, отпускаем кнопку Reset. Тут же получаем новые показания:



    Что за константа 4? Да это же число буферов, которое мы задавали в позапрошлой статье!

    Получается, что данные по шине не просто побежали (это мы и на анализаторе видели), но ещё и были обработаны! 4 буфера прибежало – 4 раза вызвалась функция CyFxSlFifoPtoUDmaCallback(). Прекрасно DMA функционирует! Но EXEшник сказал про таймаут. То есть, он данные не получил.

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

    Ещё раз напомню, что я не планирую становиться гуру FX3, поэтому не собираюсь блуждать в недрах его API. А в нашей части всё отрабатывает, проблема таится где-то в системных дебрях, для скачивания исходников которых надо регистрироваться. Нет, я нашёл относительно свежую версию, которую добрый человек прикрепил к своему проекту на GitHub, но побродив по её закоулкам, понял, что процесс предстоит долгий, а времени у меня нет. Проще затею бросить. Поэтому я пошёл другим путём.

    Что делать


    Напомню, что кроме знаменитого примера AN65974, на который ссылаются все форумы, есть чуть менее знаменитый пример AN86947 (цифры те же, но чуть в иной последовательности, чтобы всех запутать, наверное). Этот пример не показывает, как работать с шиной, а просто измеряет максимально достижимую производительность. В нём есть четыре ветки:

    • GpifToUsb
    • USBBulkSourceSink
    • USBIntrSourceSink
    • USBIsoSourceSink

    Изохронная передача и прерывания нас сейчас не интересуют. Bulk-пример ничем не отличается от уже изученного нами. А вот в примере GpifToUsb DMA используется не в ручном, а в автоматическом режиме! Правда, сам автомат GPIF нас не устроит. Он просто гонит константу.





    Весьма спартанский дизайн… Но мы уже накопили достаточно опыта, чтобы быстро всё переделать под себя. Поэтому делаем так:

    • Берём пример GpifToUsb за основу
    • Из уже имеющегося нашего проекта, из файла cyfxgpif2config.h перетаскиваем все смысловые таблицы в файл нового проекта. Почему таблицы, а не просто копируем файл? Имена у таблиц – вдрызг разные. Это мы выстрадали две статьи назад. Я не знаю, почему программисты Cypress так делают. Но надо творчески копировать. Ну, или взять этот файл из раздаточных материалов, приложенных к статье.
    • Не забываем про строчки, где вызываются функции CyU3PGpifSocketConfigure(). Мы это выстрадали в позапрошлой статье.
    • Ну, и правку VID/PID, выстраданные в прошлой статье, не забываем:



    То же самое текстом.
    const uint8_t CyFxUSB30DeviceDscr[] __attribute__ ((aligned (32))) =
    {
        0x12,                           /* Descriptor size */
        CY_U3P_USB_DEVICE_DESCR,        /* Device descriptor type */
        0x00,0x03,                      /* USB 3.0 */
        0x00,                           /* Device class */
        0x00,                           /* Device sub-class */
        0x00,                           /* Device protocol */
        0x09,                           /* Maxpacket size for EP0 : 2^9 */
    #ifdef CY_VID_PID
        0xB4,0x04,                      /* Vendor ID */
        0xF1,0x00,                      /* Product ID */
    #else
        0x34,0x12,                      /* Vendor ID */
        0x05,0x00,                      /* Product ID */
    #endif
    …
    


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

    Небольшой бонус


    Статья получилась совсем смешная по размеру. Поэтому размещу тут ответ на вопрос, с которого она началась. Как часто возникает проблема постоянно опустошающегося FIFO? С поправкой на текущий вариант таймера (там FIFO опорожняется не досуха, а чуть-чуть) – в каждой паузе. Вот так это выглядит в мелком разрешении:



    Перед пропаданием готовности колбаска вполне себе рыхлая. Линии FIFO и зависящую от них линию WR постоянно колбасит, потому что в FIFO постоянно кончаются данные. Пока FX3 не готова принимать данные, они копятся в FIFO. И когда FX3 снова встаёт в строй, линия WE падает, а FIFO постоянно находится в состоянии «У меня имеются данные». Но FIFO заполняется с частотой 60 МГц, а мы отдаём данные в шину на частоте 100 МГц. Рано или поздно, а накопившийся материал полностью уходит, и статус FIFO с линией WE снова начинает колбасить. А раз EXEшник не выявляет проблем – у нас всё работает хорошо.

    Вот переходный момент покрупнее.



    Заключение


    Как видно, все примеры от Cypress направлены на демонстрацию высоких показателей. Пропуски данных эти показатели снижают, поэтому разработчиками наверняка не рассматривались. Автор же, создав простейший пропуск, получил проблемы. К счастью, эти проблемы решаемы. Путь решения показан в статье.

    Блок из четырёх статей показал, что выбранное решение по доработки комплекса Redd, вполне жизнеспособно.

    Материалы, созданные во время работы над статьёй, можно скачать тут.

    Огромный постскриптум


    Статьи выкладываются сильно медленнее, чем пишутся. Написать документ в редакторе – не так-то и долго, а вот выгрузить его на сайт – тот ещё процесс. При подготовке следующих трёх статей, мне удалось создать тестовую среду, при помощи которой я смог спровоцировать проявление ещё одной давно теоретически предсказанной проблемы, которая невозможна при потоковой передаче, но возникнет при передачах разовых. Давайте очень коротко рассмотрим как её, так и ход решения.

    Напомню, что флаги у FX3 обладают латентностью. То есть, реально при переполнении буфера, базовый флаг взводится тогда, когда он нас уже не интересует, так как в шину ушли три лишних слова. Рассмотрим суть.



    Кому и зачем нужно это – неизвестно. Но инженеры Cypress дают возможность взводить флаг за N шагов до переполнения буфера. За счёт латентности, флаг (в нашей системе он выведен на FLAGB) будет на ножке как раз тогда, когда он нам может пригодиться.



    Всё замечательно, пока данные идут потоком или хотя бы блоками. Но в своей схеме я хочу сделать так, чтобы данные могли бы идти и поодиночке. Да, в конце концов, они будут копиться в группы в буфере FX3 и затем уже организованно уходить в канал. Но из ПЛИС они по моей задумке должны выходить именно, как хотят. Хоть гуртом, хоть в одиночку. В подавляющем большинстве мест разрывы между посылками никак не влияют на процесс, но имеются три такта, которые случайно поймать не так просто, но умозрительно они видны. И я целенаправленно сделал тестовую систему, которая провоцирует именно такие ситуации.

    Нельзя обрывать поток данных, когда флаг внутри контроллера уже упал, а наружу ещё не вышел. Задержка выхода зависит не от данных, а от латетнтности системы. Мы принимаем за факт, что ещё три такта данные будут идти. Будут реальные данные или нет – флагу на ножке микросхемы уже суждено упасть. А когда он упал – ПЛИС уже не выдаст новых данных. Без них же FX3 не пошлёт блок в канал, то есть, возникнет DeadLock.


    Я залью серым цветом тот участок данных, где они не выдаются источником. Ну просто нет их ещё у источника. А красным – где они в источнике вроде, уже появились, но переданы быть не могут, так как текущая логика запрещает передавать данные, если сброшен FLAGB. Всё. Зависли!



    Придумав такой сценарий, я также придумал и методику лечения. Мы знаем, что данные из FX3 уходят только блоками. Конкретный размер блока зависит от установок, заданных в «прошивке», но он точно не меньше килобайта. То есть, в момент, когда буфер переполнен, число ушедших слов будет круглым. Возникло желание завести счётчик, который сбрасывается в начале передачи, а затем отсчитывает число переданных слов. Причём нас интересуют только его младшие биты. Почему? Давайте я поясню это на следующем рисунке, причём жёлтым цветом обозначу участок, где FLAGB ещё не упал, а зелёным – где уже упал.



    Да, мы видим, что в трёхбитном варианте значения всё время повторяются (в таблице выше – 6 и 7 на входе, больше строк мне просто не хотелось вписывать, чтобы не создавать нагромождений). Но если анализировать их только на участке, где FLAGB упал, значения будут всегда однозначными.

    Жаль только, что когда я сделал тестовую систему, которая позволяет спровоцировать подобную проблему, всё оказалось несколько сложнее, чем виделось на первый взгляд.

    Первое, что бросилось в глаза – не стоит разделять поведение на «не случилось задержек» и «случились». Всегда переходим от работы по флагу на работу по счётчику. Никаких прямых перескоков, если не было задержек! Были, не были, а выходим из нормальной работы в процесс остановки, затем – уже всё останавливается. Это выстрадано. Кто не верит – может повторить мои мучения. А кто верит, примите, как должное, что флаг должен падать заранее. Иначе замучаетесь делать разруливающую логику! Поэтому строка в коде FX3 теперь выглядит вот так:

    CyU3PGpifSocketConfigure (0,CY_U3P_PIB_SOCKET_0,4,CyFalse,1);

    В таком варианте флаг упадёт, когда данные на выходе ещё гарантированно появятся.

    Дальше – больше. Я долго пытался отладить логику (напомню, что при разработке я черпал вдохновение, глядя в код с Гитхаба, который рассчитан на потоковую работу). Потом понял, что всё-таки поведение системы в устоявшемся режиме и в режиме работы по счётчику, сильно различается, поэтому добавил-таки автомат. Исходно было три состояния: «работаем», «останавливаемся» и «остановились, ждём появления флага».

    Оттачивая работу буквально по тактику, я понял, что в состоянии «останавливаемся» надо усложнить работу. Это связано с тем, что, как я отмечал в одной из предыдущих статей, нам надо защёлкивать данные, приходящие из FIFO. То есть, у нас приём из AVALON_ST и передача в FX3 сдвинуты на такт. Для этого сдвига в состоянии «останавливаемся» появился анализ значений счётчика E и F.

    Потом я почувствовал себя анестезиологом, когда понял, что кроме того, что систему надо аккуратно усыпить, её надо ещё и аккуратно пробудить. К автомату было добавлено два пробуждающих состояния, всё начало по времянкам работать, а по факту – зависать. Полдня я изобретал, как мне поймать на SignalTap момент подвисания. Это было нелегко, но внеся в систему ряд изменений (которые потом пришлось удалить, так как в реальной работе они мешают, так что состояние failure вы увидите, но вход в него — закомментирован), я это добился. Выяснилось, что мало быть анестезиологом, надо быть ещё и акушером. Так как при начале жизни системы в счётчик следует загружать не 0, а F.

    Если я опишу всё это в деталях – всё равно почти никто не станет это читать. А опираясь на приведённый выше крик души, все желающие поймут, что именно реализовано в коде, который можно скачать тут.

    Если бы я делал срочный проект, разумеется, я бы просто организовал накопление данных в килобайтном буфере внутри ПЛИС и их оптовую выгрузку в FX3. Есть флаг в момент, когда буфер накоплен, – поехали выгружать всё, он точно до конца передачи не упадёт. Но так как всё это создаётся для души, хотелось потренироваться в создании красивых решений. Поэтому реализовано всё было именно так. К FX3 пристыкована обыкновенная шина AVALON_ST с непредсказуемым потоком данных. И оно работает! Самый главный плюс в таком подходе – сэкономлен килобайт дефицитного ОЗУ внутри ПЛИС. А трудоёмкость обоих вариантов примерно одинакова, мучения при разработке были бы примерно такие же. И от основной проблемы, поднятой в статье, это никак не защитило бы.

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

    Комментарии 2

      0
      Спасибо за статью!
      Мдя ох уж эти грабли.
      Мы года 3 назад собирались использовать FT2232H интересно сколько граблей бы там пришлось преодолевать.
        0
        В синхронном режиме у нас получилось использовать её только с восьмибитными данными (по документации, старший байт в этом режиме не используется). И конкретно я не понял, можно ли там посылать что-то, что будет информировать ПЛИС, что перед нею команда. Только поток слать у меня получилось (я специально отмечаю, что «у меня» — может я просто слепой).

        Подробнее — тут и чуть-чуть тут

        В общем, если мы будем делать новую ревизию платы Redd, то выкинем её. Одна из задач игр в FX3 — чтобы понять, можно рассматривать её на замену, или лучше поставить очень старую, но очень добрую FX2LP.

        В следующей статье я буду в FX3 команды слать как раз через EP0. А через одну — этими командами до AVALON_MM достукиваться. Один из законов Мерфи гласит: «Всякую вещь можно наладить, если достаточно долго вертеть её в руках». Вот FX3 у меня налаживается быстрее, чем FT2232H (ту я бросил быстрее, чем получил что-то красивое).

        Но знакомый, который работает через FT600 — говорит, что он у себя тоже уже всё наладил. В отличие от FT2232H, там можно через разные точки разные потоки слать, как он говорит. Получается поток команд и поток данных, например. То есть, FTDI тоже идёт навстречу пользователям… Хотя, про латентность у FT600 он тоже говорил много нехороших слов. Так что грабли есть везде.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое