Как стать автором
Обновить

Стартуем на ПЛИС, но сначала припаяем его с конструктором беспилотного автомобиля Zoox

Время на прочтение13 мин
Количество просмотров15K

Однажды мне не спалось ночью и я залип на сайтах про паяльники. Возникло желание купить и сразу появились вопросы: на сколько ватт? Не больше 30? А почему большинство на 60-80 ватт? 60/40 олово свинец? А почему куча паяльных станций идут в комплекте с lead-free проводами припоя? Канифоль сейчас внутри проволоки? А почему есть и проволока припоя без канифоли? Бронзовую мочалку для очистки? А почему столько комплектов с и белой и бронзовой?

Вспомнил и повод, чтобы научиться паять. Когда-то Руслан Тихонов, руководитель кружка из Москвы, говорил мне что хочет сделать простые упражнения на платах ПЛИС для школьников. Как часть триады "микросхемы малой степени интеграции - ПЛИС - Ардуино". По этому поводу я купил самую дешевую плату с CPLD Altera MAX II (ныне это Intel FPGA), но обнаружил что у нее не припаян переходник.

Я выставил вопросы про паяльники на Фейсбук и после оживленной дискуссии мой приятель Денис Никитин вызвался научить меня паять как полагается. Денис работает проектировщиком печатных плат(*) в компании Zoox, ныне часть компании Amazon. Zoox делает беспилотные автомобили, то есть Денис на передовом рубеже паятельного прогресса. Я заснял мастер-класс от Дениса на видео:

(*) Точное описание позиции Дениса: тех лид, Staff Electrical Engineer. Денис отвечает за процесс разработки электронных модулей, включая архитектуру, принципиальные схемы, печатные платы, выбор компонентов и программы испытаний. По словам Дениса: "Паяем мы на этапе проектирования/прототипирования. Потом конечно машины на заводе паяют."

Часть 1. Пайка

Часть 2. Плата ПЛИС, которую мы паяли

Что же из себя представляет плата, которую паяли мы вместе Денисом? Это самая базовая плата ПЛИС, которая бывает, и при этом на ней можно делать полезные упражнения, например познакомить школьников с проектированием цифровых схем на уровне регистровых передач с использованием языка описания аппаратуры SystemVerilog.

Вот сравните как работает макетка с микросхемами малой степени интеграции и плата с ПЛИС. Реализация сдвигового регистра на макетной плате с помощью микросхемы малой степени интеграции CMOS 4015:

Реализация двоичного счетчика на плате ПЛИС:

Внешне выглядит похоже, но первое - технология 1968 года, а второе - технология 2022 года: схема синтезирована из кода на языке описания аппаратуры SystemVerilog с помощью программы Intel Quartus Prime Lite Edition Design Software Version 22.1 (скачать для Linux и для Windows ):

Код для синтеза двоичного счетчика:

Вот как выглядит синтезируемая схема сдвигового регистра в Intel Quartus:

Часть 3. Связь рассыпухи, ПЛИС и Ардуино

Насчет микросхем малой степени интеграции у многих руководителей кружков есть сомнения: "ой, а зачем давать детям технологию 1968 года, это же остатки мамонта?" - спросят некоторые из них и прибавят: "Детям нужно Ардуино и Расберри Пай!"

А затем, что на микросхемах малой степени интеграции максимально наглядно видна работа базовых элементов интегральных схем: И / ИЛИ / НЕ, D-триггеров, мультиплексоров, декодеров и т. д. - всего, из чего состоят в том числе микроконтроллеры на платах Ардуино. Например, функцию D-триггера:

Никакое количество программирования Ардуино не заменит это знание, примерно так же как самые хорошие навыки по вождению автомашины не приблизят к навыкам по проектированию ее двигателя.

Это автомобиль Zoox, платы для которого проектирует на работе Денис
Это автомобиль Zoox, платы для которого проектирует на работе Денис

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

Когда вы синтезируете цифровые схемы и программируете их в ПЛИС, вы используете тот же язык проектирования Verilog, который использует и проектировщик микроконтроллера AVR на Ардуино, сидя в своем офисе в Колорадо Спрингс, и проектировщик микропроцессорного ядра ARM в Расберри Пай, сидя в своем офисе в Кембридже.

UPD: то, что Atmel куплен Microchip я знаю, но AVR могут продолжать поддерживать по старому адресу.

Часть 4. Главная технология проектирования микросхем в последние 30 лет

Немного про отличие процесса программирования от процесса проектирования микросхемы с помощью синтеза из языка описания аппаратуры (это стандартная методология проектирования микросхем последние 30 с лишним лет). Программирование - это превращение кода в цепочку инструкций, которые хранятся в памяти и которые оттуда вытаскивает и переваривает процессор:

А синтез схем на языке описания аппаратуры - это другой процесс, при котором получается сама схема из транзисторов и дорожек, в том числе и схема современного микропроцессора, в том числе и в Ардуино. Эта схема в формате геометрических фигур GDSII отправляется на фабрику, где по ней печатают микросхемы:

Все вместе и становится аппаратно-программным комплексом, который и представляет из себя Ардуино:

Часть 5. Природа ПЛИС

"Стоп!" - скажут некоторые читатели. "Но ведь микросхема выпекается на фабрике, а вы говорите что схема в ней потом меняется. Как это может быть?"

Пояснение: схема меняется в ПЛИС, а не обычном микроконтроллере Ардуино, который является вариантом микросхем ASIC (Application-Specific Integrated Circuits). ASIC строится из рядов фиксированных ячеек (так называемых standard cells). Их функция задается "при рождении". Одна ячейка - всегда И, другая - всегда D-триггер.

А вот функцию ячейки в ПЛИС можно менять - в ПЛИС находится "универсальная ячейка", гораздо крупнее, чем ячейка в ASIC. Она соединяется со специальной памятью, и биты в этой памяти определяют, через специальные переключатели / мультиплексоры, является ли ячейка И, ИЛИ, D-триггером или еще чем-то - уже на вашем столе, а не на фабрике.

Это не симулятор процессора в софтвере (в России симуляторы на уровне инструкций называются "эмуляторами", но это мы обсудим в другом посте). Симулятор процессора в софтвере - это программа, цепочка инструкций, а здесь конфигурация матрицы ячеек.

Из ячеек ПЛИС можно построить и аналог микропроцессорного ядра в Ардуино, но он будет дороже, с более низкой тактовой частотой и будет потреблять больше электроэнергии, чем фиксированная микросхема. Такова плата за гибкость.

Часть 6. Отступление о временах и нравах

Да, из ячеек ПЛИС можно строить даже процессор! Это тот самый "процессор в памяти", в существование которого 10 лет назад не верили топ-блоггеры Андрей Нифедов и Алекс Экслер. Они вместе высмеивали кружок электроники в Иркутске, хотя через лабы на платах ПЛИС прошли в свое время все современные проектировщики айфонов и микросхем в Теслах, это стандартная часть курса MIT 6.111. (Кружок в Иркутске мог использовать не ПЛИС, а GAL, но это детали, это все равно реконфигурируемая логика)

Учитывая, что Нифедов - бывший оператор станка с ЧПУ, а Экслер - обозреватель гаджетов, это было удивительно и поучительно! Особенно тысячи безумных комментов под их постами от людей, которые наверное думают, что раз они научилились тыкать пальцем в телефон, то что-то понимают как обучать будущих проектировщиков системы на кристалле (System on Chip - SoC) телефонов.

Часть 7. Пакет для обучения школьников с платами ПЛИС

Если какой-нибудь руководитель кружка хочет такое у себя внедрить, я создал проект на сайте GitFlic.ru под названием Пайка, ПЛИС и Линукс или FPGA Soldering Camp. Его описание "Исходники, презентации, задания и примеры для летнего лагеря по пайке, микросхемам ПЛИС и синтезу цифровых схем из кода на верилоге под Линуксом".

То есть идея в том, что школьники совершают своего рода многоборье, которое состоит из:

  1. Работы с паяльником - 1 день.

  2. Работы с макетными платами и микросхемами малой степени интеграции - 2 дня, потом им надоедает.

  3. Синтеза цифровых схем на ПЛИС с простой платой - от 2-3 дня.

  4. Работы с более сложной платой ПЛИС и например разработки простого однотактового процессора. Или интерфейса простой платы ПЛИС с Ардуино, если руководитель кружка хочет идти в эту сторону.

  5. Все это делается под Линуксом, чтобы привыкали. Все разработчики микросхем для Тесл и Айфонов делают синтез и моделирование под Линуксом просто потому что они используют Synopsys Design Compiler и Cadence Genus и Innovus, у которых нет версий ни под Windows, ни под Макинтошем, только под Linux.

Впрочем, если вам очень не нравится Linux, пакет работает и под Windows (так как у Intel FPGA Quartus есть версия и под Linux, и под Windows).

Работа с Intel FPGA Quartus под ALT Linux Образование, который загружается с внешнего SSD диска
Работа с Intel FPGA Quartus под ALT Linux Образование, который загружается с внешнего SSD диска

Часть 8. Предыдущий опыт семинаров для школьников с микросхемами малой степени интеграции и ПЛИС

Вообще, я опробовал лагерь такого рода несколько лет назад на Новосибирской Летней Школе Юных Программистов и в других местах (Киев, Москва, Зеленоград). Смотрите мои другие статьи на Хабре или на silicon-russia.com.

Вот одно из видео оттуда, с восьмикласником Арсением Чегодаевым:

Часть 9. Как скачать пакет и текущие примеры в нем

Пакет можно или клонировать с помощью Git, или скачать релиз одного из двух видов пакета:

  1. Пакет для использования только с GUI квартуса.

  2. Пакет для использования со скриптами, написаными на баше. Со скриптами работать быстрее, но для начинающих GUI может быть удобнее в освоении.

В пакет входят следующие примеры:

01_and_or_not_de_morgan - обычные И, ИЛИ, НЕТ, правила де Моргана на SystemVerilog:

    assign out_c1 = ~ (~ in_a & ~ in_b);
    assign out_c2 = in_a | in_b;  // Here is where De Mongan's Laws come in

02_xor_bitwise_concat_and_parity - операции с исключающим ИЛИ на SystemVerilog.

Аналог такого примера на микросхемах малой степени интеграции выглядит так:

03_add_signed_and_unsigned - сумматор со знаком и без знака

С микросхемами малой степени интеграции (CMOS 4008) аналог выглядит так:

04_mux - реализации мультиплексора на SystemVerilog пятью разными способами:

    // Five different implementations

    assign mux   = sel ? a : b;

    assign mux2 = (sel & a) | (~ sel & b);

    wire [1:0] ab = { a, b };
    assign mux3 = ab [sel];

    always_comb
        if (sel)
            mux4 = a;
        else
            mux4 = b;

    always_comb
        case (sel)
        1'b1: mux5 = a;
        1'b0: mux5 = b;
        endcase


05_binary_counter - уже показал в начале статьи. Вот его аналог с подсоединенной микросхемой-драйвером для семисегментного индикатора. Семисегментный индикатор можно подсоединить и к ПЛИС тоже:

06_shift_register - сдвиговый регистр, уже показал в начале статьи

07_decoder

Четыре способа реализации декодера на SystemVerilog
module top
(
    input  [2:0] in_n,
    output [7:0] out_n
);
    wire  [2:0] in = ~ in_n;

    logic [7:0] out;
    assign out_n = ~ out;

    // Implementation 1: tedious

    assign out [0] = ~ in [2] & ~ in [1] & ~ in [0];
    assign out [1] = ~ in [2] & ~ in [1] &   in [0];
    assign out [2] = ~ in [2] &   in [1] & ~ in [0];
    assign out [3] = ~ in [2] &   in [1] &   in [0];
    assign out [4] =   in [2] & ~ in [1] & ~ in [0];
    assign out [5] =   in [2] & ~ in [1] &   in [0];
    assign out [6] =   in [2] &   in [1] & ~ in [0];
    assign out [7] =   in [2] &   in [1] &   in [0];

    // Implementation 2: case

    always_comb
        case (in)
        3'b000: out = 8'b00000001;
        3'b001: out = 8'b00000010;
        3'b010: out = 8'b00000100;
        3'b011: out = 8'b00001000;
        3'b100: out = 8'b00010000;
        3'b101: out = 8'b00100000;
        3'b110: out = 8'b01000000;
        3'b111: out = 8'b10000000;
        endcase

    // Implementation 3: shift
    assign out = 8'b00000001 << in;

    // Implementation 4: index

    always_comb
    begin
        out = '0;
        out [in] = 1'b1;
    end

Аналогичный декодер на микросхемах малой степени интеграции выглядит так:

08_priority_encoder

Приоритетный энкодер на SystemVerilog четырьмя способами
module top
(
    input  [3:0] in_n,
    output [7:0] out_n
);
    wire  [3:0] in = ~ in_n;

    logic [1:0] out0, out1, out2, out3;
    assign out_n = ~ { out0, out1, out2, out3 };

    // Implementation 1. Priority encoder using a chain of "ifs"

    always_comb
             if (in [0]) out0 = 2'd0;
        else if (in [1]) out0 = 2'd1;
        else if (in [2]) out0 = 2'd2;
        else if (in [3]) out0 = 2'd3;
        else             out0 = 2'd0;

    // Implementation 2. Priority encoder using casez

    always_comb
        casez (in)
        4'b???1: out1 = 2'd0;
        4'b??10: out1 = 2'd1;
        4'b?100: out1 = 2'd2;
        4'b1000: out1 = 2'd3;
        default: out1 = 2'd0;
        endcase

    // Implementation 3: Combination of priority arbiter
    // and encoder without priority

    localparam w = 4;

    wire [w - 1:0] c = { ~ in [w - 2:0] & c [w - 2:0], 1'b1 };
    wire [w - 1:0] g = in & c;

    always_comb
        unique case (g)
        4'b0001: out2 = 2'd0;
        4'b0010: out2 = 2'd1;
        4'b0100: out2 = 2'd2;
        4'b1000: out2 = 2'd3;
        default: out2 = 2'd0;
        endcase

    /*
    // A variation of Implementation 3: Using unusual case of "case"

    always_comb
        unique case (1'b1)
        g [0]:   out2 = 2'd0;
        g [1]:   out2 = 2'd1;
        g [2]:   out2 = 2'd2;
        g [3]:   out2 = 2'd3;
        default: out2 = 2'd0;
        endcase
    */

    // A note on obsolete practice:
    //
    // Before the SystemVerilog construct "unique case"
    // got supported by the synthesis tools,
    // the designers were using pseudo-comment "synopsys parallel_case":
    //
    // SystemVerilog : unique case (1'b1)
    // Verilog 2001  : case (1'b1)  // synopsys parallel_case

    // Implementation 4: Using "for" loop

    always_comb
    begin
        out3 = '0;

        for (int i = 0; i < w; i ++)
        begin
            if (in [i])
            begin
                out3 = 2' (i);
                break;
            end
        end
    end

endmodule

Приоритетный энкодер на микросхемах малой степени интеграции, вместе с микросхемой-драйвером семисегментного индикатора выглядит так:


09_fibonacci

Числа Фибоначчи, которые выводятся медленно, не на частоте 50 MHz
module top
(
    input        clk,
    input        rst_n,
    output [7:0] led
);

    wire rst = ~ rst_n;

    //------------------------------------------------------------------------

    logic [24:0] cnt;

    always_ff @ (posedge clk)
        if (rst)
            cnt <= '0;
        else
            cnt <= cnt + 1'd1;

    wire enable = (cnt == '0);

    //------------------------------------------------------------------------

    logic [7:0] num, num2;
    assign led = ~ num;

    // Note you have to press reset button to initialize the design

    always_ff @ (posedge clk)
        if (rst)
            { num, num2 } <= { 8'd1, 8'd1 };
        else if (enable)
            { num, num2 } <= { num2, num + num2 };

endmodule

10_round_robin_arbiter - а вот это уже пример не школьный, а студенческий. Арбитр-мельница, популярный вопрос на интервью в электронные компании для джуниора. Пять реализаций по мотивам статьи Matt Weber. Arbiters: Design Ideas and Coding Styles. SNUG Boston 2001.

Часть 10. Можно ли выучить все это на программном симуляторе?

Две проблемы:

Во-первых, на SystemVerilog можно написать код, который будет работать в симуляторе, но не будет синтезироваться в схему, так называемый несинтезируемый код, который пишется для тестов и моделей.

Во-вторых, даже если код синтезируется, он может глючить на плате, например из-за так называемых гонок (race condition). Чтобы они не происходили, нужно соблюдать правила методологии, в частности не использовать блокирующие присваивания для переменных, которые превращаются в D-триггеры, например "a" внутри:

always @ (posedge clk) a = b;
always @ (posedge clk) c = a;

В-третьих, код может симулироваться, но глючить на плате из-за метастабильного состояния, дребезга и нарушения тайминга внутри такта. Конечно, можно обязать ученика пропускать код через статический анализ тайминга одновременно с симуляцией, но без глючащей платы это выглядит как придирчивость преподавателя, а не реальное требование. Реальное железо лучше научит!

Часть 11. Что означают дополнительные файлы (причем на языке Tcl ?) в пакете?

Кстати, раз уж мы заговорили о нарушении тайминга и других таких параметрах. Преподавателю во время показа этих примеров ученикам, помимо кода на верилоге также нужно показать код на языке Tcl (знать его не обязательно) в двух файлах - top.qsf и top.sdc. Первый привязывает логические порты блока к физическим ножкам микросхемы.

Выглядит файл top.qsf так:
set_global_assignment -name DEVICE                    EPM240T100I5

set_global_assignment -name NUM_PARALLEL_PROCESSORS   4
set_global_assignment -name PROJECT_OUTPUT_DIRECTORY  .

set_global_assignment -name TOP_LEVEL_ENTITY          top
set_global_assignment -name SEARCH_PATH               ..
set_global_assignment -name SYSTEMVERILOG_FILE        top.sv

set_location_assignment PIN_72 -to in_n[3]
set_location_assignment PIN_73 -to in_n[2]
set_location_assignment PIN_70 -to in_n[1]
set_location_assignment PIN_71 -to in_n[0]

set_location_assignment PIN_56 -to out_n[7]
set_location_assignment PIN_57 -to out_n[6]
set_location_assignment PIN_54 -to out_n[5]
set_location_assignment PIN_55 -to out_n[4]
set_location_assignment PIN_52 -to out_n[3]
set_location_assignment PIN_53 -to out_n[2]
set_location_assignment PIN_50 -to out_n[1]
set_location_assignment PIN_51 -to out_n[0]

Второй файл, top.sdc, задает тактовые сигналы и говорит тулу для синтеза, что задержки на сигналах между внешним миром и D-триггерами анализировать не нужно. В какой-то момент обучения, например когда школьник захочет умножать или делить большие числа и обнаружит, что плата глючит, можно будет объяснить значение строчки в этом файле, которая говорит, что комбинационная логика внутри схемы обязана устаканиваться максимум за 20 наносекунд (1 / 50 MHz) иначе логику нужно разносить в несколько тактов:

Файл top.sdc выглядит так:
create_clock -period "50.0 MHz" [get_ports clk]

derive_clock_uncertainty

set_false_path -from [get_ports rst_n] -to [all_clocks]
set_false_path -from [get_ports {key[*]}] -to [all_clocks]
set_false_path -from * -to [get_ports {led[*]}]

Главный файл проекта, top.qpf, может оставаться пустым - Quartus хранит в нем всякий мусор типа даты и своей версии. Все реальные установки Quartus берет из .qsf и .sdc.

Разнообразные тьюториалы от Altera и Intel показывают, как связывать порты и ножки в GUI мышкой, но я так никогда не делаю - сначала вы можете просто использовать файлы выше, а когда вам понадобиться их менять, вы можете просто поменять текст, например добавить еще одну ножку. Все ножки перечислены в fpga-soldering-camp/boards/epm240_red/00_template/top.qsf

Часть 12. Установка Quartus под Линукс

Quartus ставится под Linux без особых сюрпризов. Скачать его можно отсюда. После инсталляции, когда вы его запустите и дойдете вот до этого места, нужно нажать "Run":

Я пробовал Quartus под Lubuntu, ALT Linux Образование, Simply Linux, Astra Linux, Ред ОС, ROSA Linux и Green Linux. Он везде работает, но инсталлятор путается в создании иконки на рабочем столе в русских дистрибутивах.

Вы можете создать иконку руками, заведите вот такой файл в "/home/$USER/Рабочий стол" (вместо "panchul" поставьте своего юзера):

[Desktop Entry]
Type=Application
Version=0.9.4
Name=Quartus (Quartus Prime 21.1) Lite Edition
Comment=Quartus (Quartus Prime 21.1)
Icon=/home/panchul/intelFPGA_lite/21.1/quartus/adm/quartusii.png
Exec=/home/panchul/intelFPGA_lite/21.1/quartus/bin/quartus --64bit
Terminal=false
Path=/home/panchul/gitflic/fpga-soldering-camp/boards/epm240_red

Перед тем, как вы попытаетесь сконфигурировать плату, вам нужно записать в директорию /etc/udev/rules.d файл с правилами для программатора USB Blaster. Самый простой способ это сделать - это просто кликнуть в скрипт под названием ВЫПОЛНИ_МЕНЯ_ПРИ_РАБОТЕ_ПОД_ЛИНУКСОМ_ПЕРЕД_ПЕРВЫМ_ЗАПУСКОМ_КВАРТУСА.bash и потом ввести пароль. Этот файл находится сразу внутри директории разархивированного пакета fpga-soldering-camp_20230108_gui_oriented.zip. Правда вы должны быть в списке пользователей, которые имеют право сделать sudo. Проконсультируйтесь со знакомым линуксоидом, если вы не в курсе этого вопроса. Потом вы будете работать внутри GUI Quartus и вопросы администрирования Линукса теоретически вас больше не затронут:

Когда войдете в GUI, кликайте не на "File | Open", а на "File | Open Project" - эту ошибку делают все, даже я, после многих лет работы с квартусом. Потом действуйте по какой-нибудь инструкции в интернете, на худой конец попросите помощи в телеграм-канале DigitalDesignSchool или FPGA-Systems.ru или на вебсайте Марсоход. Общая идея - Compile Design, Program Design, нажмите на checkbox, Start. Если не получается Start, нажмите Hardware Setup и кликните на USB Blaster.

Если программатор зависает, повтыкайте-повытыкайте, повходите-повыходите из квартуса, поперезагружайте. В Интеле эту часть продукта пишут не самые аккуратные программисты (самых аккуратных переманили в Гугл неподалеку), но после некоторого массажирования оно работает.

Если вы скачали версию пакета для использования со скриптами, fpga-soldering-camp_20230108_script_oriented.zip, то вы наверное можете разобраться как она работает без меня. Если кратко - вы просто заходите в директорию каждого примера и запускаете скрипт ./03_synthesize_for_fpga.bash . Скрипт создает временную директорию run для всех файлов, которые генерит Quartus, выполняет синтез, размещение, трассировку и конфигурацию ПЛИС. Вы только читаете текстовые отчеты о тайминге и ресурсах и смотрите на работу платы.

Часть 13. Установка Quartus под Windows

Если вы очень не любите Linux и хотите использовать пакет под Windows, вы можете это сделать. К сожалению, у вас может возникнуть проблема с Windows 11, который не любит старые неподписанные драйверы для USB Blaster. Но на Windows 10 все скорее всего будет работать после того как вы обновите драйвер. Драйвер находится прямо в дистрибутиве квартуса:

Вы также можете использовать версию пакета fpga-soldering-camp со скриптами, но тогда вам нужно установить Git для Windows, который включает в себя bash и все необходимые программы. При установке Git под Windows стоит поставить вот такую опцию.

Заключение

В заключение я хочу сказать, что в России есть платы Марсоход, которые полностью подходят для тех же целей, что и красная платка, которую я использовал для этого пакета. Просто красная платка для меня дешевле и мне ее проще заказать.

Информация о том, где купить платы, переходники и т.д. есть в README.md файле в пакете и в репозитории https://gitflic.ru/project/yuri-panchul/fpga-soldering-camp . Если вы хотите помочь в разработке примеров, вы можете завести аккаунт на gitflic.ru , клонировать репозиторию и сообщать мне о вашем прогрессе.

Теги:
Хабы:
Всего голосов 30: ↑27 и ↓3+32
Комментарии50

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань