Язык verilog с нуля + видео обзор

Digitrode

цифровая электроника вычислительная техника встраиваемые системы

Verilog. Базовый курс. Часть I

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

Перед началом изучения Verilog читатель должен иметь базовые представления о булевой логике и уметь решать хотя бы простейшие логические выражения. Например, чему будет равна функция F в выражении F = (A • B), если A в лог. «1», а B в лог. «0»? Если читатель знаком с языком C/C++, то ему будет легче понимать некоторые вещи, поскольку Verilog имеет схожий синтаксис. В целом, Verilog достаточно прост в изучении, поэтому поехали…

Итак, что же такое Verilog? Если кратко, то это язык описания аппаратуры (hardware description language или HDL). Но без пояснения этого термина вряд ли новичку будет что-либо понятно. Языки описания аппаратуры используются для формального описания функционирования схемы. Существует несколько языков описания аппаратуры, но популярны лишь два – VHDL и Verilog.

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

Язык verilog с нуля

Перед эпохой ПЛИС FPGA под прототипированием цифровой схемы понималась покупка каждого компонента этой схемы (логические элементы, мультиплексоры и т.д.) и соединение этих элементов между собой зачастую перемычками на макетной плате. Но благодаря Verilog, плате с FPGA, светодиодами и кнопками мы можем быстро и легко проверить работоспособность нашей схемы.

FPGA представляет собой чистый лист для вашей цифровой схемы. Интегрированные среды разработки вроде Xilinx ISE Design Suite преобразуют ваш «высокоуровневый» код на Verilog в поток битов, который и определит поведение FPGA. Поэтому вместо покупки мультиплексоров и прочих элементов мы напишем код и определим, какие выводы FPGA будут входами, а какие выходами.

Verilog-код, который разделяется на модули, описывает, что должно произойти в схеме, а UCF-файл (User Constraint File) сообщает микросхеме, какую функцию несет каждый вывод. FPGA имеет немалое число выводов, которые могут служить входами или выходами, и, чтобы наша задача по проектированию считалась завершенной, мы должны определить те выводы, которые будем использовать. Например, вывод G12 подключен на плате к светодиоду, поэтому будет логично назначить его выходом. Если для работы с этим светодиодом вы создадите переменную LED, то в UCF-файле нужно будет сделать так, что упоминание LED в коде должно сразу отсылать к выводу G12. К придумыванию имен переменных нужно подходить сознательно.

Ваш первый проект на Verilog

Именно проект, а не код! Хотя этот процесс напоминает программирование, в результате мы получаем не программу, а определенную схему. Возможно, вначале будет не совсем понятно, что к чему, но Verilog учится и понимается довольно быстро.

В отношении Verilog нужно сделать несколько замечаний. Этот язык чувствителен к регистру. То есть большие буквы и маленькие буквы – разные вещи. Также следует обращать внимание на отступы, хотя нижеприведенный пример и не демонстрирует этого, но в объемном коде это заметно. Компилятор и среда разработки «проглотят» плохо форматированный код, но плохой стиль будет источником непонимания и возможных ошибок.

Итак, вот «Hello, World» на Verilog:

Далее мы рассмотрим все по порядку.

Это директива, которая определяет единицу модельного времени (первый параметр, 1 нс) и точность модельного времени (второй параметр, 1 пс). Для своего проекта, конечно, вы можете установить другие значения, впрочем для FPGA эта директива не так важна, поскольку в ней нельзя задавать задержки вентилей или таким способом манипулировать с временными параметрами, но при симуляции схемы эта директива важна.

//это комментарий на Verilog

Как и на C++ (потом вы увидите, что синтаксис очень схож с синтаксисом C/C++) комментарий в одну строку начинается с «//». Все после этих двух слешей на текущей строке игнорируется компилятором. Для того, чтобы закомментировать блок, состоящий из нескольких строк, используются «/*» и «*/». Все, что попадет между этими символами, будет закомментировано. Не скупитесь на комментарии, но и не злоупотребляйте ими. Грамотно составленные комментарии облегчат жизнь при отладке.

В языке Verilog module является ключевым словом. Это как в C++ объявление типа, void hello_world(параметры) – находите сходство?

Впрочем, модуль Verilog не возвращает никакого значения (также как и при void в C/C++) и он не является, по сути, функцией, но синтаксис очень похож, и мы можем думать о модуле как об объявлении типов, у него даже есть свое имя – hello_world. Хороший Verilog-код – модульный, то есть каждый функциональный блок должен представлять собой модуль.

В скобках у нас имеется что-то вроде списка параметров. Модуль содержит вход switch[0], еще один вход switch[1] и один выход под названием led. input и output по сути являются типами переменных.

После объявления всех входов и выходов список параметров закрывается скобкой, и ставится точка с запятой. В Verilog, как и в C/C++, можно писать длинные выражения в несколько строк, и только точка с запятой будет означать его окончание.

assign led = switch[0] &

В этой строчке и заключается вся магия. До этого момента в нашем коде были описаны входы и выход, и теперь вы можете манипулировать выходом на основе входов. Здесь мы встречаем еще одно ключевое слово – assign. И оно определяет выходное значение на основе состояния одного или нескольких входов. Это похоже на приравнивание значения определенного выражения определенной переменной, но здесь есть одно «но». Назначение выхода через assign происходит лишь единожды, и в дальнейшем с ним нельзя будет провернуть подобную операцию, но уже с другим выражением, поскольку выход уже аппаратно соответствует именно этому выражению.

Следует помнить, что Verilog – это язык описания аппаратуры, а значит, вы описываете только поведение схемы. На входах switch[0] и switch[1] в разное время могут присутствовать лог. «0» и «1», но состояние выхода led всегда будет определяться выражением «switch[0] &

А что вообще значит это выражение? Что такое switch[0] и switch[1] – понятно. А «&» и «

При описании обычной цифровой схемы мы используем знак «•» для логического умножения «И» (AND), «+» для логического сложения «ИЛИ» (OR) и «⊕» для исключающего ИЛИ (XOR). Чтобы показать инверсию, мы рисуем черточку над переменной или выражением. В Verilog всему этому есть эквивалентные обозначения:

Язык verilog с нуля

Мы можем группировать вместе выражения, используя скобки. Это особенно полезно для создания вентилей NAND,NOR и XNOR. Для этого можно составить выражение для неивертированной версии, а затем перед ним добавить символ «

». Например, чтобы составить выражение типа «F = A xnor B» следует написать «assign F =

Возвращаясь к «assign led = switch[0] &

switch[1];», можно сказать, что выход led будет равен единице только, когда switch[0] будет равно единице, и switch[1] будет равно нулю нулю.

Окончание модуля Verilog

Это ключевое слово обозначает конец модуля и позволяет компилятору понять, что все что выше этой строки – полезная информация. Хотя среды разработки не особо следят за оформлением, но если у вас будет пустая строка до и пустая строка после строки с endmodule, а сама она не будет иметь отступов, то вы, возможно, сможете избежать некоторых проблем, в первую очередь связанных с пониманием.

Источник

Verilog

Verilog за день часть 1.

Введение

До появления Verilog, разработка цифровых схем велась исключительно с помощью схематического проектирования. Каждый проект, независимо от сложности, был разработан через схемотехнику. Такие проекты было трудно проверить это приводило к долгим, утомительным циклам проектирования/проверки.

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

Как только мы описали спецификацию, мы можем создать блок-схему, которая является абстракцией потока данных через систему (что входит или выходит из черных ящиков?). Так как примером, который мы взяли, является простым, мы можем создать блок-схему как показано ниже. Мы не говорим о том, что содержится в «волшебных» черных ящиках.

Блок-схема арбитра

Язык verilog с нуля

Проектирование низкого уровня

Чтобы увидеть, как с помощью Verilog спроектировать нашего арбитра, давайте вернемся к нашей машине состояний- теперь мы займемся разработкой низкого уровня. Мы рассмотрим как на черный ящик с предыдущей диаграммы, влияют наши входы.

Язык verilog с нуля

Модули

Мы должны будем немного прерваться, и рассказать о модулях Verilog. Если Вы смотрите на блок схему арбитра на первом рисунке, то увидим, что модуль имеет имя («арбитр») и порты ввода/вывода (req_0, req_1, gnt_0, и gnt_1).

Код модуля «арбитр»

Если Вы посмотрите на блок арбитра, то увидите, что есть стрелки, (приходящие для входов и уходящие для выходов). В Verilog, после того, как мы объявили имя модуля и названия портов, мы можем определить направление каждого порта. (примечание: В Verilog 2001 мы можем определить порты и направления порта в объявлении модуля), пример показан ниже.

Здесь у нас представлено только два типа портов, входящие и выходящие. В реальной жизни у нас также могут быть двунаправленные порты. Verilog позволяет нам определять двунаправленные порты директивой «inout».

inout read_enable; //двунаправленный порт

Как Вы определяете векторные сигналы (сигналы, составленные из последовательностей больше одного бита)? Verilog обеспечивает простой способ определить такие сигналы.

inout [7:0] address; //двунаправленный 8 битный порт » address «

Мы изучили, как о бъявить блок/модуль в Verilog.

Мы изучили, как о бъявить порт и его направление.

Мы изучили, как объявлять векторные/скалярные порты.

Типы данных

Какое отношение имеют типы данных к аппаратным средствам? Фактически никакое. Люди просто хотели следовать традициям языков программирования. Но в нашем случае типы данных имеют другой смысл.

Verilog имеет два вида драйверов.

Драйвер это тип данных, который может управлять нагрузкой. В принципе, в физическом цепи, драйвером будет нечто, что может проводить электроны.

Драйвер, который может сохранять значение (пример: триггер).

Драйвер, который не может сохранять значение, но соединяет две точки на схеме (пример: провод).

Первый тип драйвера называют reg в Verilog (сокращение от «регистр»). Второй тип данных называют wire (просто «провод»). Вы можете обратиться к описанию языка, чтобы получить более подробную информацию.

Примеры:

Резюме

Операторы

Источник

Verilog

Введение в язык Verilog.

Введение в язык Verilog

Verilog – язык описания аппаратуры (HDL). Это язык, используемый для описания цифровых систем, например, сетевого коммутатора, микропроцессора, памяти или простого триггера. Это означает, что на нем можно описать любую (цифровую) аппаратуру, на любом уровне.

Язык verilog с нуля

Можно описать простой триггер, показанный на рисунке выше, и можно описать сложное устройство, содержащее миллион логических элементов. Verilog – это один из языков, используемых в промышленности для описания аппаратуры. Он позволяет нам проектировать цифровые системы на уровнях: поведения, передачи данных между регистрами (RTL), логики, и уровне переключателей (транзисторов). Verilog позволяет разработчикам описывать поведение устройства, откладывая детали его выполнения на будущее.

Многие инженеры, желающие изучить Verilog часто интересуются: – сколько времени займет его изучение? Ответ автора будет следующим: – не больше недели, если вы уже знакомы хотя бы с одним из языков программирования.

Методы проектирования

Verilog, подобно другим языкам описания аппаратуры, позволяет вести проектирование как восходящим, так и нисходящим методом.

Восходящий метод проектирования

Это традиционный метод проектирования в электронике. Каждый проект выполняется снизу, начиная с использования стандартной логики (См. подробности в Digital Section). С увеличением сложности проекта такой метод становится использовать все труднее, вплоть до невозможности. Современные разработки содержат специализированные интегральные схемы (ASIC) или микропроцессоры, содержащие тысячи транзисторов. Поэтому традиционный восходящий метод разработки уступает дорогу новому, структурному, иерархическому методу, дающему возможность справиться со сложностью новых устройств.

Нисходящий метод проектирования

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

Пример нисходящего проектирования

Язык verilog с нуля

Уровни абстракции Verilog

Verilog поддерживает проектирование на разных уровнях абстракции. Три из них очень важны:

Уровень поведения

Этот уровень описывает систему при помощи конкурирующих алгоритмов (поведения). Каждый алгоритм последователен, что означает его представление в виде набора инструкций, выполняющихся одна за другой. Функции, задачи и постоянные блоки являются основными элементами. Структурная реализация на этом уровне не учитывается.

Уровень межрегистровых передач (УМП)

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

Уровень логических элементов

На уровне логики характеристики системы описываются связями между логическими элементами и их временными свойствами (задержкой распространения сигналов). Все сигналы считаются дискретными. Они должны иметь только стандартные логические уровни: 0, 1, X, Z. Используемые операции являются стандартными логическими примитивами: И, ИЛИ, НЕ и т.д. Нужно учесть, что использование уровня логики не является хорошей идеей. Такой уровень автоматически синтезируется соответствующими программами, и полученный список узлов используется симулятором и для дальнейших целей.

Источник

Проектирование синхронных схем. Быстрый старт с Verilog HDL

Язык verilog с нуляНа просторах рунета можно найти достаточно много статей с введением в Verilog HDL. Все они описывают синтаксис и семантику языка, но, к сожалению, не раскрывают основных парадигм, используемых при проектировании цифровых схем. Представьте себе, что вам объясняют синтаксис языка Java, но не рассказывают ничего про объектно-ориентированное проектирование. Если вы знакомы с ООП, то такого введения будет достаточно, но если вы знаете только Си, то писать скорей всего будете “по-старому”, создавая огромные классы со сложными методами.

Примерно так происходит с программистами, изучающими цифровую схемотехнику и языки описания аппаратуры. Быстро разобравшись с несложным синтаксисом языка, они начинают описывать конструкции, безумные с точки зрения хардверного инженера. Среди моих студентов встречались люди, написавшие “сортировку пузырьком” за такт, сумасшедшие асинхронные схемы, которые работали по-разному при каждом запуске и разной погоде за окном, огромные комбинационные делители, уводившие place&route в глубокую многочасовую задумчивость.

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

Синхронные схемы

Синхронные цифровые схемы состоят из комбинационных вентилей(gates), цепей (nets) и триггеров (flip-flops). В синхронной схеме есть единственный сигнал синхронизации, который управляет всеми элементами памяти (триггерами).

А вот несколько примеров схем, не являющихся синхронными:
Язык verilog с нуля

Практически все существующие цифровые схемы являются синхронными, либо состоят из нескольких синхронных схем, взаимодействующих через асинхронные каналы. Причина популярности синхронных схем — в простоте анализа времени распространения сигналов. Временной анализ (англ. Timing analysis) — тема для отдельной статьи, но кратко коснуться этого вопроса все-таки надо.

Рассмотрим следующую схему, реализующую функцию R = A+B+1:
Язык verilog с нуля

Регистры A, B и R сохраняют значения на входах D по переднему фронту сигнала синхронизации (clk), т.е. в тот момент времени, когда значение clk изменяется из 0 в 1.

Сигналы распространяются через сумматоры (и другие комбинационные элементы) не мгновенно, а с задержкой, зависящей от длины самого большого пути из вентилей (критического пути), т.е. от сложности элемента. К примеру, критический путь в сумматоре будет проходить через сигналы переноса в старший разряд (представьте себе вычисление суммы “столбиком”).

Предположим, что сначала во всех регистрах был записан 0. А на входы in0, in1 сначала подаются значения 1 и 4, а потом 2 и 1. Тогда временная диаграмма для нашей схемы может выглядеть следующим образом:

Язык verilog с нуля

По первому фронту clk значения 1 и 4 будут записаны в регистры A и B. После того как сигнал распространится через сумматоры, значение результата 1 + 4 + 1 = 6 появится на проводе t1. Затем, по второму фронту clk результат будет записан в регистр R, а новые входные значения в регистры A и B.

Теперь представим, что период сигнала clk уменьшился в два раза. Тогда второй фронт сигнала clk появится на регистре R до того, как на t1 появятся правильные данные. Т.е. схема будет работать неверно!

Отсюда следует основное правило корректности работы синхронной схемы:

Задержка через критический путь в схеме должна быть меньше периода сигнала синхронизации.

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

Синхронная схема работает на частоте, определяемой критическим путем в схеме.

Представьте, что в схеме тысячи комбинационных путей с задержкой в 1 наносекунду. И один путь с задержкой в 2 наносекунды. Из-за этого единственного пути схема должна тактироватся на частоте в 500 МГц, хотя могла бы работать на гигагерце. Поэтому, при проектировании синхронных схем длинные комбинационные цепочки разбивают регистрами на конвейерные стадии. К примеру, в процессоре AMD Bulldozer средняя длина комбинационного пути – 12-14 FO4 эквивалентных вентилей (задержка, эквивалентная инвертору единичного размера, нагруженному 4-мя инверторами).

Несмотря на этот недостаток, синхронные схемы стали очень популярны. Синхронные схемы без труда поддаются автоматическому временному анализу, т.е. частота, на которой схема может корректно работать, определяется программой (временным анализатором) автоматически. Когда разработчик может отстраниться от этих деталей, синхронную схему можно специфицировать набором пересылок между регистрами. Такой подход к описанию схем – Register Transfer Logic (RTL) стал мэйнстримом в описании логики работы цифровых схем. К примеру, схему из нашего примера можно описать следующими пересылками:

A = in0
B = in1
R = A+B+1

На каждом такте в регистр А записывается in0, в регистр B записывается in1, а в регистр R значение A + B + 1. Идея описывать схемы на RTL в виде текста лежит в основе языков описания аппаратуры: Verilog HLD и VHDL. Пришло время познакомиться с одним из них поближе.

Описание синхронных схем на Verilog HDL

Модули

Программа на Verilog, она же описание схемы, состоит из модулей (module), точнее из экземпляров модулей (module instances). Модуль можно представить как “черный ящик” с торчащими из него проводами — портами (ports). Порты бывают трех типов: входные (input), выходные (output) и двунаправленные (inout). В большинстве случаев используются первые два типа портов. Двунаправленные порты нужны для моделирования двунаправленных шин, на базе выходов с тремя состояниями и открытым стоком. Их мы рассматривать не будем.

Список портов описывается в заголовке модуля. К примеру, рассмотрим этот пустой модуль:

В теле модуля описывается его функциональность. Этот модуль пустой, его порты никуда не подключены. Перед тем как перейти к описанию функциональности модулей, познакомимся с основными типами данных в Verilog.

Типы данных

В Verilog существуют два класса типов: типы для моделирования аппаратуры и стандартные арифметические типы данных, скопированные из языка Си. Мы будем рассматривать только первый из классов, т.к. именно он используется для моделирования сигналов в схеме.

Рассмотрим каждый из типов по отдельности.

Тип wire служит для моделирования сигналов, которые не могут “хранить” состояние. К примеру, значение на выходе комбинационной схемы полностью определяется значениями на входах. Если значения на входе меняются, меняется и значение на выходе, т.е. состояние не хранится. Тип wire используется вместе с операцией непрерывного присваивания — assign. При непрерывном присваивании, всякий раз когда меняется значение переменных в правой части присваивания, обновляется значение переменной в левой части. К примеру, простую комбинационную схему можно описать следующим образом:
module comb
(
input wire [7:0] a,b,
output [7:0] res // тип wire устанавливается по-умолчанию, но можно написать output wire [7:0] res
);

Язык verilog с нуля

Сигналы можно объявлять и внутри тела модуля:

wire [3:0] x,y;
assign x = a[3:0] + b[3:0]; // складываются 4 младших разряда на шинах а и b
assign y = a[7:4] + b[7:4]; // складываются 4 старших разряда на шинах а и b
assign res = x+y;
endmodule

Язык verilog с нуля

Процедурные блоки могут моделировать как синхронные схемы (схемы с памятью), так и комбинационные схемы. К примеру, рассмотрим описание схемы двоичного счетчика:

module counter
(
input clk, reset,
output [7:0] data
);
reg [7:0] counter;

assign data = counter;

always @(posedge clk) // процедурный блок срабатывает по положительному фронту clk
begin
if (reset)
counter = 0;
else
counter = counter + 1;
end

Язык verilog с нуля

Строка always @(posedge clk) называется списком чувствительности. Она определяет события, по которым выполняется процедурный блок. Данный блок выполняется по каждому положительному фронту (positive edge) сигнала синхронизации. Таким образом, блок моделирует логику работы счетчика, сигнал counter будет просинтезирован как регистр.

Процедурные блоки могут моделировать и комбинационные схемы. К примеру, следующий код просинтезируется в комбинационный сумматор:
wire [7:0] a,b;
reg [7:0] res;

always @*
begin
res = a+b;
end

Здесь список чувствительности always @* означает, что процедурный блок выполняется каждый раз, когда изменяются значения сигналов в правой части операций присваивания. В данном случае процедурный блок срабатывает каждый раз, когда изменяются сигналы a и b. Эквивалентном этой строчке, будет следующий список чувствительности:

always @(a or b) // блок срабатывает каждый раз, когда меняется a или b

При описании комбинационной схемы с помощью процедурного блока always необходимо помнить, что значения всех изменяемых в блоке сигналов, должны полностью определятся сигналами в списке чувствительности. В противном случае синтезатору придется вставить в схему элементы памяти — защелки (latches).

К примеру, рассмотрим следующий процедурный блок:

wire a,b;
reg res;

В процедурном блоке не описано, что будет в случае если b = 0. Если b = 0 значение res не должно меняться, не зависимо от значения a. Поэтому будет просинтезирована следующая схема:

Язык verilog с нуля

Как правило, появление защелок в схеме означает ошибку в коде.

Неблокирующие присваивания
Все присваивания, которые мы использовали в примерах с процедурными блоками, в Verilog называются «блокирующими» присваиваниями. Они работают привычным программисту образом.

Помимо блокирующих присваиваний, в Verilog есть еще один тип присваивания, используемый в процедурных блоках — неблокирующее присваивание, обозначаемое оператором »
reg a,b;
always @(posedge clk)
begin
a
В этом примере на каждом такте сигналы a и b будут обмениватся значениями. Синтезированная схема будет выглядеть следующим образом:

Язык verilog с нуля

Неблокирующие присваивания обычно используют при описании логики работы регистров. Блокирующие присваивания чаще используются для описания комбинационных схем.

Заключение

На этом небольшое введение в Verilog заканчивается. Надеюсь, кому-то оно было полезно.

Источник

Видео

Verilog - Язык Проектирования Схем §0

Verilog - Язык Проектирования Схем §0

Модули в языке Verilog

Модули в языке  Verilog

Verilog - Язык Проектирования Схем §1

Verilog - Язык Проектирования Схем §1

Verilog - Язык Проектирования Схем §3

Verilog - Язык Проектирования Схем §3

Структурное и поведенческое описание в языке Verilog

Структурное и поведенческое описание в языке Verilog

FPGA. Цифровая схемотехника на языке Verilog HDL

FPGA. Цифровая схемотехника на языке Verilog HDL

Разработка стека на языке Verilog

Разработка стека на языке Verilog

3. Программирование ПЛИС: графическое представление, AHDL, VHDL, Verilog

3. Программирование ПЛИС: графическое представление, AHDL, VHDL, Verilog

Verilog#1.KEYs&LEDRs. Первый проект.

Verilog#1.KEYs&LEDRs.  Первый проект.

Verilog - Язык Проектирования Схем §6

Verilog - Язык Проектирования Схем §6
Поделиться или сохранить к себе:
Добавить комментарий

Нажимая на кнопку "Отправить комментарий", я даю согласие на обработку персональных данных, принимаю Политику конфиденциальности и условия Пользовательского соглашения.