Меня всегда интересовала цифровая схемотехника, а в частности языки описания аппаратуры — HDL. У меня давно лежала в списке будущего чтения книга Дэвида М. Хэррис и Сары Л. Хэррис «Цифровая схемотехника и архитектура компьютера», воспользовавшись свободным временем на самоизоляции, я добрался до этой замечательной книги. В процессе чтения я столкнулся с некоторыми трудностями, а в частности, как именно писать и отлаживать код в Quartus Prime. В процессе поисков мне очень помог сайт marsohod.org, но вот процесс симуляции схемы на этом сайте описан с использованием встроенных средств Quartus и в современных версиях программы, этих встроенных средств нет и необходимо использовать ModelSim. Чтобы как-то систематизировать те знания, которые я получил, используя Quartus и ModelSim, я решил написать эту статью. В процессе этой статьи я в качестве примера разберу задачу из книги Дэвида М. Хэррис и Сары Л. Хэррис «Цифровая схемотехника и архитектура компьютера», а конкретно задачу 3.26 про автомат газированной воды. На протяжении статьи я покажу, как установить Quartus, создать проект, написать код и произвести его симуляцию. Всем кому это будет интересно, добро пожаловать под кат.

image

Постановка задачи


Вас уговорили спроектировать автомат с прохладительными напитками для офиса. Расходы на напитки частично покрывает профсоюз, поэтому они стоят всего по 5 рублей. Автомат принимает монеты в 1, 2 и 5 рублей. Как только покупатель внесет необходимую сумму, автомат выдаст напиток и сдаст сдачу. Спроектируйте конечный автомат для автомата с прохладительными напитками. Входами автомата являются 1, 2 и 5 рублей, а именно, какая из этих монет вставлена.

Предположим, что по каждому тактовому сигналу вставляется только одна монета. Автомат имеет выходы: налить газировку, вернуть 1 рубль, вернуть 2 рубля, вернуть 2 по 2 рубля. Как только в автомате набирается 5 рублей (или больше), он выставляет сигнал «НАЛИТЬ ГАЗИРОВКУ», а также сигналы, возврата соответствующей сдачи. Затем автомат должен быть готов опять принимать монеты.

Теория


Конечные автоматы или finite state machine (FSM) относятся к классу синхронных последовательных схем, которые представляют подавляющее большинство схем цифровой схемотехники. Именно таким образом следует реализовывать свои проекты (по крайней мере на первых порах). Такой способ обеспечивает повторяемость и верифицированость схемы и не зависит от отношений задержек различных элементов схемы. Правила построения синхронных последовательных схем гласят, что схема является синхронной последовательной схемой, если ее элементы удовлетворяют следующим условиям:

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

Конечный автомат имеет несколько состояний, которое он хранит в регистрах. При поступлении тактового сигнала, конечный автомат может изменять свое состояние, причем как именно изменится состояние зависит от входных сигналов и текущего состояния. В простейшем случае входных сигналов может не быть вовсе, таким образом работает делитель частоты. Существуют два основных класса конечных автоматов: автомат Мура, в котором выходные сигналы зависят только от текущего состояния автомата и автомат Мили, в котором выходные сигналы зависят от текущего состояния и входных сигналов. В принципе любой конечный автомат можно реализовать как по схеме Мура, так и по схеме Мили, отличие между ними будет в том, что у автомата Мура будет больше состояний и он будет отставать на один такт от автомата Мили. Для схемы автомата газированной воды я буду использовать схему Мили. Распишем состояния конечного автомата:
Условное обозначение Описание
S0 Начальное состояние, накопленная сумма 0 руб.
S1 Накопленная сумма 1 руб.
S2 Накоплено 2 руб.
S3 Накоплено 3 руб.
S4 Накоплено 4 руб.

В качестве входного сигнала, будет выступать двухбитная шина, со следующим кодированием номинала монеты:
Условное обозначение Значение Описание
I1 01 1 руб.
I2 10 2 руб.
I5 11 5 руб.

Нарисуем диаграмму состояний нашего автомата (на диаграммах состояния автомата Мили, необходимо указывать выходные сигналы на стрелках перехода состояния, я этого делать не буду, чтобы не загромождать диаграмму, все выходные сигнала будут расписаны в таблице ниже):

image

Распишем таблицу изменения состояний и выходных сигналов:
Состояния Входные сигналы Выходные сигналы
Текущее S Следующее S' insert Налить воды pour_water Cдача 1 руб. change1 Сдача 2 руб. change2 Сдача 2 по 2 руб. change22
S0 S1 I1 0 0 0 0
S0 S2 I2 0 0 0 0
S0 S0 I5 1 0 0 0
S1 S2 I1 0 0 0 0
S1 S3 I2 0 0 0 0
S1 S0 I5 1 1 0 0
S2 S3 I1 0 0 0 0
S2 S4 I2 0 0 0 0
S2 S0 I5 1 0 1 0
S3 S4 I1 0 0 0 0
S3 S0 I2 1 0 0 0
S3 S0 I5 1 1 1 0
S4 S0 I1 1 0 0 0
S4 S0 I2 1 1 0 0
S4 S0 I5 1 0 0 1

Практика


Установка Quartus Prime


Quartus имеет бесплатную Lite Edition, которая имеет некоторые ограничения по сравнению с профессиональной редакцией, основное ограничение это не более 10000 строк исходного кода для симуляции проекта. Скачать её, после регистрации, можно по ссылке, на момент написания статьи наиболее свежая версия была 19.1, на основе работы с этой версией я писал статью. Выбираем Lite Edition, версия 19.1, операционная система Windows (надо отметить, что существует версия Quartus для Linux и она отлично работает, проблемы возникают с ModelSim, которая 32 битная и использует старую версию библиотеки отображения шрифтов, поэтому на первых порах я рекомендую использовать Windows версию), выбираем вкладку Combined Files. Размер архива для скачки весьма большой — 5.6 Gb, учитывайте это. Разворачиваем скаченный архив и запускаем setup.bat. Установка проходит стандартным образом, используем выбор компонентов по умолчанию.

Создание проекта


Для создания нового проекта выберем File -> New Project Wizard.... Первое окно Wizard'а информационное, жмем Next, на втором окне выбираем где будет располагаться проект, его название «soda_machine» и элемент дизайна верхнего уровня «soda_machine», как на рисунке:

image

В следующем окне выбираем «Empty project». Окно добавление файлов «Add files», ничего не добавляем. Окно выбора устройства «Family, Devices & Board Settings», для реального проекта очень важное, но так как, наш проект учебный и до реального ему далеко, здесь оставляем настройки по умолчанию, как на рисунке:

image

Окно выбора настроек других инструментов «EDA Tool Settings», выбираем для симуляции проекта использовать «ModelSim-Altera» и формат «System Verilog HDL» как на рисунке:

image

Последнее окно информационное «Summary», жмем Finish.

Написание исходного кода


У нас будет два основных файла с исходным кодом, это собственно модуль soda_machine и его test bench, оба эти файла будут использовать тип данных insert_type, который описывает как мы кодируем номиналы монет и его логично выделить в отдельный файл. Но есть некоторые трудности, связанные с особенностями компиляции Quartus и ModelSim. Quartus компилирует все файлы с исходным кодом за один проход, а ModelSim каждый файл компилирует отдельно, для того, что бы при компиляции Quartus'ом не возникало переопределения типа insert_type, я использовал технику из C/C++ include guard, основанную на директивах макропроцессора. Кроме того, что бы ModelSim был уверен что тип insert_type, используемый в модуле soda_machine и в test bench'е, один и тот же, поместил его описание внутри пакета soda_machine_types. С учетом этих требований файл soda_machine_types.sv выглядит следующим образом:

soda_machine_types.sv
`ifndef soda_machine_types_sv_quard

package soda_machine_types;

	typedef enum logic [1:0] {I1=2'b01, I2=2'b10, I5=2'b11} insert_type;
	
endpackage

`define soda_machine_types_sv_quard
`endif


Теперь собственно модуль soda_machine, находится в файле soda_machine.sv:

soda_machine.sv
`include "soda_machine_types.sv"
import soda_machine_types::*;


module soda_machine(
	input logic clk,          // Clock 
	input logic reset,        // Active high level
	input insert_type insert,
	output logic pour_water,
	output logic change1,
	output logic change2,
	output logic change22);

	typedef enum logic [2:0] {S0, S1, S2, S3, S4} state_type;
	(* syn_encoding = "default" *) state_type state, nextstate;
	// Логика сброса и хранения состояния конечного автомата
	always_ff @(posedge clk, posedge reset)
	if (reset)
		state <= S0;
	else
		state <= nextstate;
	// Логика вычисления следующего состояния в зависимости от текущего состояния и входных сигналов
	always_comb
		case (state)
			S0:
				case (insert)
					I1:
						nextstate = S1;
					I2:
						nextstate = S2;
					I5:
						nextstate = S0;
				endcase
			S1: 
				case (insert)
					I1:
						nextstate = S2;
					I2:
						nextstate = S3;
					I5:
						nextstate = S0;
				endcase
			S2:
				case (insert)
					I1:
						nextstate = S3;
					I2: 
						nextstate = S4;
					I5:
						nextstate = S0;
				endcase
			S3: 
				if (insert == I1)
					nextstate = S4;
				else
					nextstate = S0;
			S4:
				nextstate = S0;
		endcase
	// Логика получения выходных сигналов
	assign pour_water = (state == S4) | (insert == I5) | (state == S3) & (insert == I2);
	
	assign change1 = (state == S1) & (insert == I5) | (state == S3) & (insert == I5) | (state == S4) & (insert == I2);
							
	assign change2 = (state == S2) & (insert == I5) | (state == S3) & (insert == I5);
	
	assign change22 = (state == S4) & (insert == I5);
	
endmodule


Каким образом будут производится кодирование состояний конечного автомата, я оставил на усмотрение Quartus. Для того что бы указать, как именно следует производить кодирование, используется атрибут (* syn_encoding = «default» *), другие варианты кодирования можно увидеть здесь.

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

Для добавления файлов в проект используется File -> New «SystemVerilog HDL File» и при сохранении дать соответствующее имя. После добавления этих двух файлов, проект можно скомпилировать Processing -> Start Compilation. После успешной компиляции можно посмотреть полученную схему Tools -> Netlist Viewers -> RTL Viewer:

RTL Viewer
image

Для просмотра диаграммы состояний конечного автомата Tools -> Netlist Viewers -> State Machine Viewer

State Machine Viewer
image

На вкладке Encoding видно, что Quartus применил схему кодирования «one-hot», это когда для каждого состояния используется отдельный D-триггер, причем состояние S0 кодируется 0, а не 1 как для других состояний, делается это для упрощения схемы сброса в начальное состояние. Можно заметить, что RTL Viewer показывает не совсем принципиальную схему, это скорее концепт. Для просмотра принципиальной схемы использовать Tools -> Netlist Viewrs -> Technology Map Viewer (Post-Fitting)

Симуляция


В принципе на текущий момент у нас есть схема автомата по продаже газированной воды, но необходимо убедится, что она работает правильно, для этого напишем test bench и разместим его в файле soda_machine_tb.sv:

soda_machine_tb.sv
`include "soda_machine_types.sv"
import soda_machine_types::*;

module soda_machine_tb;

	insert_type insert;
	
	logic [5:0] testvectors[10000:0];
	
	int vectornum, errors;
	
	logic clk, reset, pour_water, change1, change2, change22;
	logic pour_water_expected, change1_expected, change2_expected, change22_expected;
	// Тестируемый модуль
	soda_machine dut(
		.clk(clk),
		.reset(reset),
		.insert(insert),
		.pour_water(pour_water),
		.change1(change1),
		.change2(change2),
		.change22(change22)
	);
	// Эмуляция тактовой частоты
	always
		#5 clk = ~clk;
	// Начальная установка сигналов
	initial begin
		// Чтение файла с тестовыми векторами
		$readmemb("../../soda_machine.tv", testvectors);
		vectornum = 0;
		errors = 0;
		clk = 1;
		// Выполним сброс схемы
		reset = 1; #13; reset = 0;
	end
	// Установка тестового воздействия
	always @(posedge clk) begin
		#1; {insert, pour_water_expected, change1_expected, change2_expected, change22_expected} = testvectors[vectornum];
	end
	// Проверка, соответствуют ли выходные сигналы нашим ожиданиям
	always @(negedge clk)
		if (~reset) begin
			if ((pour_water !== pour_water_expected) || (change1 !== change1_expected) || (change2 !== change2_expected) ||
				(change22 !== change22_expected)) begin
				$error("%3d test insert=%b\noutputs pour_water=%b (%b expected), change1=%b (%b expected), change2=%b (%b expected), change22=%b (%b expected)", 
					vectornum + 1, insert, pour_water, pour_water_expected, change1, change1_expected, change2, change2_expected, change22, change22_expected);
				errors = errors + 1;
			end
			vectornum = vectornum + 1;
			if (testvectors[vectornum] === 6'bx) begin
				$display("Result: %3d tests completed with %3d errors", vectornum, errors);
				$stop;
			end
		end

endmodule


Для проверки нашего модуля используется файл тестовых векторов soda_machine.tv:

soda_machine.tv
01_0_0_0_0
01_0_0_0_0
01_0_0_0_0
01_0_0_0_0
01_1_0_0_0
10_0_0_0_0
10_0_0_0_0
10_1_1_0_0
11_1_0_0_0
10_0_0_0_0
10_0_0_0_0
11_1_0_0_1
10_0_0_0_0
11_1_0_1_0
01_0_0_0_0
01_0_0_0_0
01_0_0_0_0
11_1_1_1_0


Первые два бита это входной сигнал insert, следующие 4 бита, это наши ожидания выходных сигналов: pour_water, change1, change2, change22. Например в начале файла 5 раз подряд вставляется рублевая монета, на пятой монете, мы ожидаем появления сигнала pour_water, при этом сигналы выдачи сдачи неактивны. Файл soda_machine.tv добавляется в проект File -> New «Text File»

Для удобства работы с ModelSim добавим файл soda_machine_run_simulation.do следующего содержания:

soda_machine_run_simulation.do
add wave /soda_machine_tb/dut/clk
add wave /soda_machine_tb/dut/reset
add wave /soda_machine_tb/dut/insert
add wave /soda_machine_tb/dut/state
add wave /soda_machine_tb/dut/nextstate
add wave /soda_machine_tb/dut/pour_water
add wave /soda_machine_tb/dut/change1
add wave /soda_machine_tb/dut/change2
add wave /soda_machine_tb/dut/change22
view structure
view signals
run -all
wave zoom full


Он запустит нашу симуляцию и выведет графики сигналов в ModelSim. Файл soda_machine_run_simulation.do добавляется в проект File -> New «Tcl script File»

Теперь настроим проект, что бы автоматически запускалась симуляция. Выбираем пункт меню Assignments -> Settings, выбираем категорию EDA Tool Settings -> Simulation. В настройках NativeLink settings выбираем Compile test bench: и нажимаем кнопку Test Benches... в открывшемся окне Test Benches нажимаем кнопку New... В открывшемся окне New Test Bench Settings заполняем поле Test bench name: soda_machine_tb и нажимаем кнопку выбора файла ... в нижней части окна, выбираем наш файл soda_machine_tb.sv и нажимаем кнопку Add. Должно получиться как на рисунке:

image

В окне New Test Bench Settings нажимаем OK. Окно Test Benches должно получить следующий вид:
image

В окне Test Benches нажимаем OK. В NativeLink settings устанавливаем галочку Use script to set up simulation и выбираем файл soda_machine_run_simulation.do. Окно Settings
должно иметь вид:

image

В окне Settings нажимаем OK, производим компиляцию проекта Processing -> Start Compilation, производим запуск симуляции Tools -> Run Simulation Tool -> RTL Simulation. Должен запустится ModelSim и произойти симуляция проекта. Внешний вид вкладки Transcript:

ModelSim вкладка Transcript
image

Красной рамкой выделен вывод нашего test bench'а о количестве выполненных тестов и обнаруженных ошибках. Внешний вид вкладки Wave:

ModelSim вкладка Wave
image

Исходный код проекта


Исходный код проекта находится в github.com/igoral5/soda_machine Клонировать проект, затем открыть проект при помощи Quartus File -> Open Project...
выбрать файл soda_machine.qpf. Затем скомпилировать проект Processing -> Start Compilation и запустить симуляцию Tools -> Run Simulation Tool -> RTL Simulation.