FPGA. Просто про складне - Філософія написання конфігурацій для ПЛІС
У попередній статті циклу "FPGA. Просто про складне" було розглянуто внутрішнє пристрій FPGA, в цій статті йтиметься про те, як пишеться конфігурація для цих БІС.
Справа в тому, що всі статті для новачків, які мені попадалися, були типу "помигати світлодіодиком" або на ALTERA, або на Xilinx і не розкривали філософії паралельності обчислень. Так, ви все правильно зрозуміли, в ПЛІС всі обчислення виконуються паралельно. У цій статті я намагатимусь розповісти про те, як пишеться конфігурація для ПЛІС без прив'язки до якого-небудь виробника. Усі приклади будуть представлені на VHDL. Бутує думка, що Verilog більш зрозумілий, але, якщо чесно, це лише "справа релігії" на якій мові писати, в будь-якому випадку, все сказане буде справедливим і для Verilog. Після прочитання цієї статті, ви будете мати уявлення, як писати під ПЛІС будь-якої фірми в будь-якому IDE на будь-якій мові.
Для початку давайте розберемося, навіщо потрібно робити конфігурацію на VHDL або Verilog, адже в Інтернеті повно прикладів з редакторами, в яких потрібно просто з'єднувати елементи "І", "І-НЕ" тощо, щоб отримати потрібну схему. Наприклад, як на малюнку нижче:
Може здатися, що це зручно, адже все наочно і звично, але тільки як виглядатиме схема, якщо вам раптом знадобиться додати в проект хоча б один FIFO? А якщо в проекті потрібно реалізувати БПФ (швидке перетворення Фур'є)? Така схема розростеться до епічних масштабів, так що вчіть мову, не знущайтеся над собою. На підприємствах, що спеціалізуються на розробці під ПЛІС, подібна реалізація конфігурацій вже 30 років як не робиться, все пишеться з допомогою VHDL або Verilog, тому що це швидше і зрозуміліше. Оскільки ми з'ясували, що єдиний шлях для створення великих проектів — це мови VHDL або Verilog, то приступимо до розгляду прийомів програмування, але перед цим врахуємо кілька нюансів:
1) Усі проекти складаються з модулів, написаних на мові опису, які мають порти для зв'язку з іншими модулями.
2) Будь-які операції можуть бути синхронними (виконуються тільки за сигналом тактування), або асинхронними (не залежать від сигналу тактування і виконуються в будь-який момент часу).
Нижче на картинці представлений приклад асинхронних обчислень, давайте його розберемо: на вхід clk подається тактовий сигнал. Тактовий сигнал потрібен для синхронізації інших сигналів за часом, і його джерелом зазвичай виступає кварцовий генератор, конкретно в цьому прикладі він потрібен тільки для наочності і ніде не використовується. На вхідну 8-бітну шину data подаються 8-бітні слова x"01", x"02", x"03", x"04", джерелом даних на цій шині може служити будь-який інший модуль у цій же ПЛІС або, якщо прив'язати цю шину до реальних портів, будь-яке інше джерело з зовнішнього світу. Сигнали sum1 і sum2 по суті є 8-бітними реєстрами, куди ми поміщаємо значення суми між приходящим по шині data словом і деяким додаваемим значенням. Сигнал sum3 є результатом сумування sum1 і sum2. Обчислення sum1, sum2 і sum3 виконуються асинхронно, це значить, що обчислення відбуваються миттєво з зміною сигналу data і ніяк не синхронізуються за тактовим сигналом.
(Примітка: b"1000_000" — це бінарна запис шестнадцятиричного числа x"80", а b"0000_0001" це x"01")
Тепер давайте подивимося на осцилограму нижче. Ви можете переконатися, що операції обчислення sum1, sum2 і sum3 виконувалися паралельно і асинхронно, тобто над одним і тим же значенням сигналу data одночасно проводилися дві різні операції sum1 і sum2, і в цей же момент часу проводилася операція сумування між sum1 і sum2. Також на осцилограмі нижче видно, що за один такт прийшло два слова (асинхронність обведена фіолетовим), але це ніяк не вплинуло ні на час обчислень, ні на їх результат.
Тепер трохи змінимо програму: додамо синхронізацію обчислення sum3 по фронту тактового сигналу clk і отримаємо приклад синхронних обчислень.
Якщо раніше значення sum3 обчислювалося одночасно з sum1 і sum2, то тепер, як видно з осцилограми нижче, значення sum3 обчислюється тільки з настанням фронту тактового сигналу, тобто на наступний такт. Такі затримки в такт потрібно враховувати при написанні синхронних процесів. Крім того, на осцилограмі видно, що в момент, коли за один такт на шину data прийшло два слова, обчислення відбулися лише над другим словом, так як воно було останнім встановленим значенням перед фронтом тактового сигналу.
На осцилограмі нижче проілюстровано, як відбуваються ті ж обчислення, якщо синхронізувати джерело, що посилає на шину data дані, з тактовим сигналом (з кожним тактом приходить тільки одне слово): ми провели операції сумування з усіма вхідними даними, нічого не втративши.
Логічно запитати, мовляв, навіщо взагалі потрібна синхронізація, якщо все можна робити асинхронно і вигравати в швидкості? Відповідь проста: щоб мати можливість проводити певні обчислення в певний момент часу. Наприклад, стоїть завдання прийняти пакет з 100 8-бітних слів і в 99 слово записати якесь своє значення, щоб відправити його далі. Знаючи, що за один такт нам може прийти тільки одне слово, не важко завести лічильник і порахувати до 99.
Підсумовуючи цю статтю, можу сказати, що ніхто не заважає вам писати як синхронні процеси, так і асинхронні, комбінувати їх, все залежить від ситуації: якщо потрібна прив'язка до часу (різні протоколи, інтерфейси, лічильники/таймери тощо), то робіть синхронізацію, якщо потрібно зловити зміну сигналу в будь-який невизначений момент часу, то робіть процес асинхронним.
Автор намагався викладати максимально доступно, дякую, що дочитали до кінця.