Русский
Русский
English
Статистика
Реклама

Добавляем поддержку Vendor-команд к USB3.0 устройству на базе FX3

В предыдущих статьях мы сделали достаточно интересную железку, состоящую из контроллера FX3 и ПЛИС Cyclone IV. Мы научились гонять через шину USB 3.0 потоки данных с достаточно высокой скоростью (я доказал, что поток 120 МБ/с из ULPI будет проходить через эту систему без искажений и потерь). Всё хорошо, но система, которая просто гонит данные, не имеет смысла. Любую систему надо настраивать. То есть, хочешь не хочешь, а кроме скоростных данных надо слать не очень спешные команды.

У шины USB для передачи команд предназначена конечная точка EP0. Сегодня мы потренируемся дорабатывать прошивку FX3 так, чтобы она обрабатывала команды от PC, а также транслировала их через GPIO в сторону ПЛИС. Кстати, именно здесь проявляется преимущество контроллера над готовым мостом. Что меня в текущей реализации Redd сильно удручает я не могу посылать никаких команд. Их можно только упаковать в основной поток. В случае же с контроллером что хочу, то и делаю. Начинаем творить, что хотим



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

Введение


Осматривая исходники типовой прошивки, я нашёл знакомое имя функции в файле cyfxgpiftousb.c. Функцию зовут:
/* Callback to handle the USB setup requests. */CyBool_tCyFxApplnUSBSetupCB (        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 needif (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_tCyU3PGpioSetValue (                   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_tCyU3PGpioSimpleSetValue (                         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_VALUEvoid 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 b40003440 <SPI_Write+0x3c>40003408:e59f303c ldrr3, [pc, #60]; 4000344c <SPI_Write+0x48>4000340c:e3100001 tstr0, #140003410:e59321ac ldrr2, [r3, #428]; 0x1ac40003414:e1a000a0 lsrr0, r0, #140003418:13822001 orrner2, r2, #14000341c:03c22001 biceqr2, r2, #140003420:e58321ac strr2, [r3, #428]; 0x1ac40003424:e59321a8 ldrr2, [r3, #424]; 0x1a840003428:e2411001 subr1, r1, #14000342c:e3822001 orrr2, r2, #140003430:e58321a8 strr2, [r3, #424]; 0x1a840003434:e59321a8 ldrr2, [r3, #424]; 0x1a840003438:e3c22001 bicr2, r2, #14000343c:e58321a8 strr2, [r3, #424]; 0x1a840003440:e3510000 cmpr1, #040003444:1affffef bne40003408 <SPI_Write+0x4>40003448:e12fff1e bxlr4000344c:e0001000 .word0xe0001000

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 strr2, [r4, #424]; 0x1a8400036c8:e58431a8 strr3, [r4, #424]; 0x1a8400036cc:e58421a8 strr2, [r4, #424]; 0x1a8400036d0:e58431a8 strr3, [r4, #424]; 0x1a8400036d4:e58421a8 strr2, [r4, #424]; 0x1a8400036d8:e58431a8 strr3, [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, мы можем передавать команды в свою прошивку ПЛИС.

А для чего я хочу это применять, будет рассказано в следующей статье.
Источник: habr.com
К списку статей
Опубликовано: 02.02.2021 12:18:59
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Системное программирование

Fpga

Программирование микроконтроллеров

Компьютерное железо

Контроллер fx3

Плис

Плис cyclone iv

Redd

Usb-анализатор

Gpio

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru