Реализация кольцевого буфера на языке Verilog HDL. Пара слов о поиске ошибок.

У нас уже есть примеры реализации кольцевого буфера на языках Assembler (для STM32) [1], C [2] и C++ [3]. Пожалуй, продолжим. В этой статье я приведу пример реализации простенького кольцевого буфера на языке Verilog HDL. Так же будет приведен разбор типовой ошибки, которую я допустил при написании этого кода.
В целях упрощения демонстрации работы алгоритма в модуле помимо собственно кода относящегося к буферу будет так же находиться код, представляющий интерфейс отладочной платы.
Думаю, вопросов на тему зачем в ПЛИС кольцевой буфер не встаёт — КБ это весьма простая и эффективная реализация FIFO, а FIFO в свою очередь достаточно часто востребованный базовый блок, используемый для построения более сложных модулей. К сожалению, в интернете гуглится не очень много примеров реализации FIFO на верилоге и то, что мне попадалось обычно сложно для понимания.
Пару слов об алгоритме. В статье про очередь задач на C было рассмотрено два подхода к организации доступа к буферу — индекс конца указывает на последний элемент, положенный в буфер и индекс конца указывает на первый свободный элемент. Там было решено, что второй вариант эффективнее, здесь мы пойдём по тому же пути, но немного по другой причине — в этом случае несколько проще организовать контроль за переполнением буфера. Контроль этот будет организован с помощью сравнения индексов начала и конца при добавлении элемента в очередь и при извлечении.
Для проверки алгоритма используется следующий интерфейс: на dip-переключателях выставляем данные, нажимаем на кнопку, байт добавляется в буфер; нажимаем другую кнопку — байт из буфера отображается на светодиодах; нажимаем третью кнопку — буфер очищается.


module fifo (
	input wire clk_50M,		//тактовая частота
	input wire [7:0]key,		//кнопки
	input wire [7:0]dip,		//dip-переключатели
	output wire [7:0]led		//светодиоды
);
parameter FIFO_SIZE_EXP = 3;				//размер буфера
parameter FIFO_SIZE = 1<<FIFO_SIZE_EXP;		//FIFO_SIZE = 2^FIFO_SIZE_EXP

//интерфейс модуля кольцевого буфера
wire RESET;			//сброс буфера
wire [7:0]fifo_data_in;	//входной регистр
reg [7:0]fifo_data_out;	//выходной регистр
wire fifo_put;			//сигнал положить байт из входного регистра в буфер
wire fifo_get;			//сигнал загрузить байт из буфера в выходной регистр
reg fifo_full = 0;		//буфер заполнен
reg fifo_empty = 1;		//буфер пуст


reg [7:0]fifo_buf [0:(FIFO_SIZE-1)];		//буфер
reg [(FIFO_SIZE_EXP-1):0]fifo_buf_start = 0;	//индекс начала буфера
reg [(FIFO_SIZE_EXP-1):0]fifo_buf_end = 0;	//индекс конца буфера

assign fifo_data_in = dip;				//подключаем дип-переключатели к входному буферу
assign led = fifo_data_out;				//подключаем светодиоды к выходному буферу

//формирование фронта и простейшая фильтрация дребезга контактов
reg key0;
reg key0_;
reg key2;
reg key2_;
reg key7;
reg key7_;
always @(negedge clk_50M) begin
	key0_ <= key[0];
	key2_ <= key[2];
	key7_ <= key[7];
end
always @(posedge clk_50M) begin
	key0 <= key0_;
	key2 <= key2_;
	key7 <= key7_;
end

//подключение кнопок к управляющим линиям
assign RESET = ~key0;
assign fifo_get = ~key2;
assign fifo_put = ~key7;

//реализация кольцевого буфера
always @(posedge clk_50M or posedge RESET) begin : fifo_load_byte
	reg fifo_get_prev = 0;				//состояние линии  fifo_get на предыдущем такте тактовой частоты
	reg fifo_put_prev = 0;				//состояние линии  fifo_put на предыдущем такте тактовой частоты

	if(RESET == 1) begin				//асинхронный сброс
		reg [(FIFO_SIZE_EXP):0]index = 0;		//индекс, необходимо считать до FIFO_SIZE, по-этому индекс должен иметь разрядность FIFO_SIZE_EXP + 1
		for(index = 0; index < FIFO_SIZE; index = index + 1) //цикл for разворачивается компилятором в параллельный сброс всех ячеек буфера
			fifo_buf[index] = 0;
		fifo_data_out <= 0;		//перевод внутренних регистров в начальное состояние
		fifo_buf_end <= 0;
		fifo_buf_start <= 0;
		fifo_full <= 0;
		fifo_empty <= 1;
	end else begin
		if((fifo_put_prev == 0) && (fifo_put == 1) && (fifo_full == 0)) begin //если пришёл сигнал fifo_put и есть место в буфере
			fifo_buf[fifo_buf_end] <= fifo_data_in; //записываем данные в очередную ячейку
			fifo_buf_end <= fifo_buf_end + 1;	//инкремент индекса конца; разрядность индекса соответствует величине буфера, так что следить за переполнением не надо
			fifo_empty <= 0;	//если добавили элемент, то буфер не пуст
			if(fifo_buf_end == fifo_buf_start) //если индекс конца стал равен индексу начала
				fifo_full <= 1;		//то буфер полон
		end
		if((fifo_get_prev == 0) && (fifo_get == 1) && (fifo_empty == 0)) begin //если пришёл сигнал fifo_get и буфер не пуст
			fifo_data_out <= fifo_buf[fifo_buf_start];	//считываем данные из очередной ячейки
			fifo_buf_start <= fifo_buf_start + 1;	//инкремент индекса начала; разрядность индекса соответствует величине буфера, так что следить за переполнением не надо
			fifo_full <= 0;			//если считали элемент, то буфер не заполнен
			if(fifo_buf_end == fifo_buf_start)	//если индекс конца стал равен индексу начала
				fifo_empty <= 1;	//то буфер пуст
		end
		fifo_put_prev = fifo_put;			//сохраняем состояние сигнала fifo_put
		fifo_get_prev = fifo_get;			//сохраняем состояние сигнала fifo_get
	end
end

endmodule


Компилируем, заливаем в кристалл и… Опытные разработчики наверняка сразу найдут в этом листинге ошибку, но я к таковым не отношусь, по-этому долго тупил в код и тыкал в кнопочки. Пока я сохранял один байт и тут же его вызывал всё работало хорошо, но стоило мне попытаться запомнить несколько байтов, как камню сносило голову. Убедившись, что методом тыка я ничего не добьюсь, а привычные мне по контроллерам методы отладки в данном случае малоэффективны (тут отлаживать в железе без логического анализатора дело тухлое), я полез в симулятор. Так как чип у меня от Альтеры, то симулятор я взял тоже ихний — ModelSim, хотя при более детальном рассмотрении выяснилось, что интеграции с квартусом там никакой нет, так что в принципе можно спокойно юзать любой симулятор, поддерживающий Verilog. Вообще, по началу ModelSim выглядит не особо дружелюбным, но почитав статьи по теме (например [4]) разобраться можно. Для начала нам нужен тестбенч — модуль верхнего уровня, который будет содержать в себе инстенс нашего подопытного и дёргать ему ножками в нужном порядке. Полный бенчмарк, реализующий все юз-кейсы я писать не стал, ограничившись необходимым минимумом.


module fifo_testbench;

reg clk_50M;
reg [7:0]key;
reg [7:0]dip;
wire [7:0]led;

//объявляем экземпляр нашего подопытного модуля
fifo fifo_inst (clk_50M, key, dip, led);

//формирование тактовой частоты
always
	#20 clk_50M = ~clk_50M;


initial begin
//начальные значения входных сигналов
	clk_50M = 0;	//обнуляем линию, иначе ей присвоится значение x и тактового сигнала не будет (!x == x)
	key = 8'hFF;	//все кнопки не нажаты
	dip = 8'h55;	//на dip-переключателях произвольный, но определённый тестовый код

	#100 key[7] = 0;	//спустя 100 шагов после начала симуляции нажимаем кнопку записи значения
	#50 key[7] = 1;	//спустя 50 шагов отпускаем кнопку
	#100 key[7] = 0;	//и так три раза =)
	#50 key[7] = 1;
	#100 key[7] = 0;
	#50 key[7] = 1;
	#100 key[2] = 0;	//спустя ещё 100 шагов нажимаем кнопку считывания
	#50 key[2] = 1;	//и отпускаем её спустя 50 шагов
end

endmodule


Таким образом мы подадим три импульса записи значения и один импульс считывания и посмотрим, что творится в регистрах буфера. Сохраняем файлики рядышком, компилируем в моделсиме и… Облом — Альтера ещё не успела договориться сама с собой о синтаксисе языка Verilog HDL.

vlog -reportprogress 300 -work work D:/FPGA/fifo.v
# Model Technology ModelSim ALTERA vlog 6.6c Compiler 2010.08 Aug 24 2010
# -- Compiling module fifo
# ** Error: D:/FPGA/fifo.v(52): Variable declaration assignments are only allowed at the module level or if the variables are automatic.
# ** Error: D:/FPGA/fifo.v(53): Variable declaration assignments are only allowed at the module level or if the variables are automatic.
# ** Error: D:/FPGA/fifo.v(56): Variable declaration assignments are only allowed at the module level or if the variables are automatic.
# ** Error: D:/FPGA/fifo.v(56): Declarations not allowed in unnamed block.
# C:/altera/10.1/modelsim_ase/win32aloem/vlog failed.

То, что молча проглатывает синтезатор квартуса, вводит симулятор ModelSim в состояние паники. Ну да ладно — не так страшно, поправим. Инициализацию закомментируем, блок обзовём. Компилируем заново, теперь всё нормально. Жмём Start Simulation, открываем панель Wave, кидаем туда интересующие линии и пишем в консоле «run 1000000». И вот, что мы видим:
Симуляция работы кольцевого буфера
(кликните, что бы посмотреть скриншот целиком)
На скриншоте видно, что при записи первого же слова ставится флаг fifo_full. Почему? Понятно дело, почему — в момент записи первого элемента индекс начала равен индексу конца — инкремент происходит в тоже время, параллельно. Что бы избавиться от этого эффекта нужно просто использовать для инкремента блокирующее присвоение. Блокирующее присвоение в данном случае гарантирует, что сравнение индексов произойдёт после инкремента, хотя и в том же такте. Кроме этого, в коде присутствует ещё одна проблема того же характера — поведение кода при одновременной подаче импульсов чтения и записи может быть некорректным, но это фиксить уже несколько сложнее.
Отмечу ещё пару моментов, с которыми столкнулся. Стоит обратить внимание на следующий фрагмент кода:

	if(RESET == 1) begin : fifo_reset				//асинхронный сброс
	<...>
	end else begin
	<...>
		fifo_put_prev = fifo_put;			//сохраняем состояние сигнала fifo_put
		fifo_get_prev = fifo_get;			//сохраняем состояние сигнала fifo_get
	end

Если строки сохранения результата переместить за пределы блока else (поставить сразу после end), то программа умирает. Точнее, может что-то там и работает, но светодиоды не загораются. Выяснить, в чём проблема не удалось, так как в симуляторе всё работает правильно. Это, кстати, показывает, что стопроцентно доверять симулятору нельзя — в железе тот же код может заработать совсем по-другому.
Пара D-триггеров на входах от кнопок нужны для формирования фронта. Без них нажатие на кнопки иногда прохлёбываются, так как фронт сигнала медленный по сравнению с тактовой частотой. Я бы долго не понимал, в чём проблема, если бы не замечание ув. Anatol в комментариях к теме про UART [5].
Цикл for, использующийся у меня для очистки памяти, на самом деле зло. Во-первых, из-за него нельзя объявить буфер большого размера, так как число интераций в нём не может быть больше 5000 (хотя может это и настраивается — надо смотреть), а во-вторых он исключает использование RAM для хранения буфера. С другой стороны, не обнулять буфер по резету тоже как-то не совсем правильно.

Ниже приведён окончательный код модуля fifo.

module fifo (
	input wire clk_50M,			//тактовая частота
	input wire RESET,			//сброс буфера
	input wire [7:0]fifo_data_in,		//входной регистр
	output reg [7:0]fifo_data_out,		//выходной регистр
	input wire fifo_put,			//сигнал положить байт из входного регистра в буфер
	input wire fifo_get,			//сигнал загрузить байт из буфера в выходной регистр
	output reg fifo_full = 0,		//буфер заполнен
	output reg fifo_empty = 1		//буфер пуст
);
parameter FIFO_SIZE_EXP = 3;			//размер буфера
parameter FIFO_SIZE = 1<<FIFO_SIZE_EXP;		//FIFO_SIZE = 2^FIFO_SIZE_EXP


reg [7:0]fifo_buf [0:(FIFO_SIZE-1)];		//буфер
reg [(FIFO_SIZE_EXP-1):0]fifo_buf_start = 0;	//индекс начала буфера
reg [(FIFO_SIZE_EXP-1):0]fifo_buf_end = 0;	//индекс конца буфера

//реализация кольцевого буфера
always @(posedge clk_50M or posedge RESET) begin : fifo_load_byte
	reg fifo_get_prev/* = 0*/;				//состояние линии  fifo_get на предыдущем такте тактовой частоты
	reg fifo_put_prev/* = 0*/;				//состояние линии  fifo_put на предыдущем такте тактовой частоты

	if(RESET == 1) begin : fifo_reset				//асинхронный сброс
		reg [(FIFO_SIZE_EXP):0]index/* = 0*/;		//индекс, необходимо считать до FIFO_SIZE, по-этому индекс должен иметь разрадность FIFO_SIZE_EXP + 1
		for(index = 0; index < FIFO_SIZE; index = index + 1) //цикл for разворачивается компилятором в параллельный сброс всех ячеек буфера
			fifo_buf[index] = 0;
		fifo_data_out <= 0;		//перевод внутренних регистров в начальное состояние
		fifo_buf_end <= 0;
		fifo_buf_start <= 0;
		fifo_full <= 0;
		fifo_empty <= 1;
	end else begin
		if((fifo_put_prev == 0) && (fifo_put == 1) && (fifo_full == 0)) begin //если пришёл сигнал fifo_put и есть место в буфере
			fifo_buf[fifo_buf_end] <= fifo_data_in; //записываем данные в очередную ячейку
			fifo_buf_end = fifo_buf_end + 1;	//инкремент индекса конца. разрядность индекса соответствует величине буфера, так что следить за переполнением не надо
			fifo_empty <= 0;	//если добавили элемент, то буфер не пуст
			if(fifo_buf_end == fifo_buf_start) //если индекс конца стал равен индексу начала
				fifo_full <= 1;		//буфер полон
		end
		if((fifo_get_prev == 0) && (fifo_get == 1) && (fifo_empty == 0)) begin //если пришёл сигнал fifo_get и буфер не пуст
			fifo_data_out <= fifo_buf[fifo_buf_start];	//считываем данные из очередной ячейки
			fifo_buf_start = fifo_buf_start + 1;	//инкремент индекса начала. разрядность индекса соответствует величине буфера, так что следить за переполнением не надо
			fifo_full <= 0;			//если считали элемент, то буфер не заполнен
			if(fifo_buf_end == fifo_buf_start)	//если индекс конца стал равен индексу начала
				fifo_empty <= 1;	//буфер пуст
		end
		fifo_put_prev = fifo_put;			//сохраняем состояние сигнала fifo_put
		fifo_get_prev = fifo_get;			//сохраняем состояние сигнала fifo_get
	end
end

endmodule

Простой пример использования модуля FIFO в составе передатчика UART.

module uart_tx (
	input wire clk_50M,				//тактовая частота
	output reg uart_tx = 1,			//линия UART TxD
	input wire [7:0]uart_tx_data,			//байт для отправки
	input wire uart_tx_start,			//сигнал загрузки байта
	output reg uart_tx_busy = 0			//идёт передача
);
parameter MAIN_CLK = (50_000_000);		//опорная частота 50MHz
parameter UART_CLK = (9600);			//чатота UART

reg tx_clk_9600 = 0;				//линия частоты UART
reg [9:0]tx_reg = 10'b1xxxxxxxx0;		//регистр UART (StartBit = 0, StopBit = 1, биты данных не определены)
reg [3:0]tx_cur_bit =4'd9;			//индекс передаваемого бита

reg fifo_rst;					//сброс FIFO (не используется)
wire [7:0]fifo_out;				//выход FIFO
wire fifo_get;					//сигнал выгрузки байта из FIFO
assign fifo_get = (tx_cur_bit == 4'b0) ? 1'b1 : 1'b0;	//выгружаем байт при отправке старт бита
wire fifo_full;					//индикатор заполненности FIFO (не используется)
wire fifo_empty;				//индикатор пустоты FIFO

fifo uart_tx_fifo (clk_50M, fifo_rst, uart_tx_data, fifo_out, uart_tx_start, fifo_get, fifo_full, fifo_empty);	//объявляем экземпляр буфера

initial begin
	uart_tx = 1;				//устанавливаем tx в 1 при старте
end


always @(posedge clk_50M) begin : baud_rate_generator
	reg [13:0]clkr_9600 = 0;			//счётчик для формирования несущей частоты UART
	clkr_9600 = clkr_9600 + 14'b1;		//инкремент счётчика
	if(clkr_9600 >= (MAIN_CLK/UART_CLK/2)) begin
		clkr_9600 = 14'b0;
		tx_clk_9600 = ~tx_clk_9600;		//формируем фронт несущей частоты
	end
end

always @(posedge tx_clk_9600) begin : uart_transmitter
	if((uart_tx_busy == 0) && (fifo_empty == 0)) begin //если ничего не передаём, но FIFO не пуст
		tx_cur_bit = 0;		//обнуляем индекс передаваемого бита
		uart_tx_busy = 1;		//ставим признак передачи данных
	end else if(tx_cur_bit <= 9) begin	//если ещё не вся посылка передана
		if(tx_cur_bit == 0)		//если передаётся стартбит
			tx_reg[8:1] = fifo_out;	//выгружаем новый байт из FIFO в регистр передатчика
		uart_tx = tx_reg[tx_cur_bit];			//выставляем бит на линию передатчика
		tx_cur_bit = tx_cur_bit + 4'b0001;		//инкремент счётчика передаваемого бита
		if((tx_cur_bit == 10) && (fifo_empty == 0))	//если полностью передали байт, но в FIFO есть ещё
			tx_cur_bit = 4'b0;	//на следующем шаге выставить стартбит
	end else				//всё что было передали
		uart_tx_busy = 0;		//обнуляем индикатор передачи данных
end

endmodule

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

Ссылки
  1. Вместо первой программы — реализация очереди задач на кольцевом буфере на Assembler stm32
  2. Минималистичная очередь задач на C — реализация очереди задач на кольцевом буфере на C
  3. AVR, С++ и умные указатели — реализация кольцевого буфера на C++
  4. Симулятор ModelSim — вводная статья о ModelSim на сайте проекта Марсоход
  5. UART приемник на VHDL — описание приёмника UART на языке VHDL
  6. Описание мегафункций FIFO от Альтеры
  • +2
  • 06 апреля 2011, 22:25
  • Alatar

Комментарии (8)

RSS свернуть / развернуть
хм…
и что этот код точно работает?
Я взял код Вашего фифо, откомпилировал Quartus II и смотрю его с помощью Quartus/Netlist Viewer/RTL Viewer.
Вход fifo_data_in похоже в проекте вообще не участвует?
Да и судя по RTL Viewer fifo_buf_start никогда не увеличивается…
0
кажись понял в чем дело…
в окончательном варианте вашего fifo стоит «output reg fifo_get» а видимо должно стоять «input wire fifo_get»
0
Да, это я опечатался, сейчас исправлю
0
Добавил пример использования для полноты картины
0
рискну высказать свое мнение по стилю программирования.
не сочтите за грубиянство мои советы.
имхо, не очень хорошо иметь в коде много вложенных ветвлений if-else особенно когда в разных ветках устанавливаются разные регистры.
Старайтесь поведение каждого регистра описывать исключительно отдельно. Писанины больше, но так лучше.
Представьте себе триггер (регистр). У него есть 4 базовых входа: data, clk, enable, async_reset. Других входов считайте и нет никаких.
Тогда конструкция, которая реализуется совсем без проблем будет следующая:
always @(posedge clk or posedge async_reset)
if(async_reset)
my_reg <= 0; //асинхронный сброс
else
if(enable)
my_reg <= data; //синхронная загрузка

Всякие дополнительные if-else добавят мультиплексоры перед входом данных. Чем больше этих if тем сложнее эти мультиплексоры на входе данных. это грозит снижением максимальной тактовой частоты
+1
Замечание полезное, спасибо. Сижу сейчас вкуриваю рекомендации Альтеры на тему стиля, пока правда не очень успешно — в теории всё понятно, а вот на практике… Не могли бы Вы показать, как должен выглядеть, например, блок uart_transmitter, или fifo_load_byte, переписанные в рекомендуемом Вами стиле?
Кстати, обратил внимание, в рекомендациях Альтеры FSM обычно описывается двумя always блоками, в каждом из которых находится case. Это тоже как-то связано с оптимизацией синтеза?
0
даже не уговаривайте :)
0
Думаю, что проблема с увеличением объема буфера из-за выбранной микросхемы(типо Max II), у Cyclone, например, побольше памяти и буфер благополучно увеличивается и до ста элементов
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.