Частичная реконфигурация ПЛИС

Довелось по учебе разбираться с темой частичной реконфигурации(partial reconfiguration, PR) в ПЛИС фирмы Altera. Для тех, кто не в теме — частичная реконфигурация, это такая штука, которая позволяет динамически перепрошить часть нашей ПЛИС, пока остальная ПЛИСина продолжает работать. Это может быть удобно в случае ограниченности ресурсов, построения самовосстанавливающихся систем (Self-healing systems) и т.д.
В сети довольно немного информации на русском языке по этой теме, а еще меньше информации о том, как же заставить работать частичную реконфигурацию на плате с Cyclone V (пример, представленный в статье, был успешно запущен на плате DE1-SoC даташит на плату).
Приступим к практике.

Работать будем в среде Quartus Prime 16.0. Сразу стоит сказать, что для полноценной разработки PR-проектов нужно приобрести лицензию на сайте Altera. Итак, наш проект будет состоять из нескольких частей:
  • Модуль, который циклически выводит цифры от 0 до F на семиразрядный индикатор
  • Модуль, который поочередно зажигает светодиодики с первого до четвертого
Кроме этого будем использовать IP ядро для PR, и парочку альтеровских модулей.
Сделаем так, чтобы можно было динамически реконфигурировать модуль со светодиодами так, чтобы он начал зажигать светодиоды в обратном порядке (с четвертого до первого). При этом цифры на семиразряднике будут продолжать отображаться.
Создадим новый проект в Quartus. Первым делом добавим IP ядро в проект.



Даем ему какое то имя и нажимаем OK.



Перейдем к настройке ядра



Подробное описание ядра и его настроек по этой ссылке

Для нашего ядра снимем галочку напротив Use as PR Internal Host, так как для Cyclone V внутренний хост не поддерживается, о чем заботливо сообщает нам Quartus внизу окна. Использование внешнего хоста позволяет запустить процесс реконфигурации с другой ПЛИС или любого другого устройства. Так же обязательно поставим галочку напротив Enable JTAG debug mode, это позволит нам управлять реконфигурацией через встроенный Programmer.

Нажимаем Finish, ждем пока наше ядро сгенерируется и добавится в проект. Теперь попишем немного кода на Verilog.

Создадим модуль, который выводит числа на семисегментник.

module hex_view(
	input clk,
	output reg [6:0] HEX

);
reg [4:0] num=0;
reg [6:0] ss_arr [15:0];
reg [31:0] count = 0;
initial begin
	ss_arr[0]  = 7'h40; // 0
	ss_arr[1]  = 7'h79; // 1
	ss_arr[2]  = 7'h24; // 2
	ss_arr[3]  = 7'h30; // 3
	ss_arr[4]  = 7'h19; // 4
	ss_arr[5]  = 7'h12; // 5
	ss_arr[6]  = 7'h02; // 6
	ss_arr[7]  = 7'h78; // 7
	ss_arr[8]  = 7'h00; // 8
	ss_arr[9]  = 7'h10; // 9
	ss_arr[10] = 7'h08; // A
	ss_arr[11] = 7'h03; // B
	ss_arr[12] = 7'h46; // C
	ss_arr[13] = 7'h21; // D
	ss_arr[14] = 7'h06; // E
	ss_arr[15] = 7'h0e; // F
	
end

always @(posedge clk)
begin
	if (count >= 50000000)
	begin
		if (num == 15) begin
			num=0;
			end
		else begin
			num=num+1;
			end
			HEX = ss_arr[num];
		count <= 0;		
	end
	else
		count <= count + 1; 
end
endmodule


Ничего сложного, if (count >= 50000000) нужно, чтобы счет происходил раз в секунду, т.к. тактовая частота нашего устройства 50 МГц.

Теперь напишем первую версию модуля, который будет мигать нашими светодиодами


module led_flash
(
	input clk,
	output reg [3:0] leds
);

initial
begin
	leds = 4'b0001;
end

reg [31:0] count = 0;

always @(posedge clk)
begin
	if (count >= 25000000)
	begin 
		if (leds == 4'b1000)
			leds <= 4'b0001;
		else
			leds <= leds << 1;
		count <= 0;
	end
	else
		count <= count + 1;
end
endmodule


Мигать светодиодами будем в два раза быстрее. Ну потому что можем.

Частичная реконфигурация не может изменять I/O порты нашего модуля. Но что делать, если хочется один модуль заменить другим модулем, у которого отличаются I/O порты? Создадим модуль-обертку, который будет содержать все порты первого и второго модуля. Как это выглядит:
Допустим есть модуль A и модуль B.

module A(
 input a,
 input b,
 output c
);
....
endmodule


module B(
 input a,
 input b,
 input x,
 output c
);
....
endmodule

И мы бы хотели динамически подменить модуль А модулем B. Для это создадим модуль-обертку wrapper.

module wrapper(
    input a,
    input b,
    input x,
    output c
);
//А в теле модуля будем подключать в одном случае А, а в другом B.
....
endmodule

Мы просто добавили все порты из модуля A и B, теперь можем безболезненно в одной версии прошивки подключить внутри warapper модуль A, а в другой модуль B.

В нашем случае это необязательно, но все же сделаем такой wrapper.
module led_wrapper
(
	input clk,
	output [3:0] leds
);
	led_flash led_flash_inst(
		.clk(clk),
		.leds(leds)
	);
endmodule

С реконфигурируемым модулем могут работать какие-то другие модули нашей системы, поэтому хотелось бы узнавать, когда модуль реконфигурируется, а когда работает в штатном режиме. Для этих целей наше IP ядро предоставляет сигнал freeze. Когда freeze равен 1, то в нашей системе что-то реконфигурируется. Наш led_wrapper можно обернуть другим модулем freeze_region.
module freeze_region(
	input clk,
	input freeze,
	output [3:0] leds
);
	led_wrapper led_wrapper_int(
		.clk(clk),
		.leds(leds)
	);

    always @(posedge freeze)
    begin
        //тут можем что то вывести на output порты или сделать что нибудь еще
        ....
    end
endmodule


Теперь напишем модуль верхнего уровня, он будет содержать freeze_region, наше ip ядро, а также 2 дополнительный модуля — cyclonev_prblock и cyclonev_crcblock. Эти сущности предоставляют интерфейс для частичной реконфигурации нашей ПЛИС. Так как мы используем IP ядро в качестве внешнего хоста, мы соединим его с этими двумя модулями.

module top(
	(* altera_attribute = "-name IO_STANDARD \"3.3-V LVCMOS\"", chip_pin = "AF14" *) input clk,
	(* altera_attribute = "-name IO_STANDARD \"3.3-V LVCMOS\"", chip_pin = "V18, V17, W16, V16" *)	output [3:0] leds,
	(* altera_attribute = "-name IO_STANDARD \"3.3-V LVCMOS\"", chip_pin = "AH28, AG28, AF28, AG27, AE28, AE27, AE26" *) output [6:0] HEX,
	(* altera_attribute = "-name IO_STANDARD \"3.3-V LVCMOS\"", chip_pin = "Y16" *) input nreset

);

wire freeze_wire;

wire error_bridge;
wire ready_bridge;
wire error_pr_bridge;
wire done_bridge;
wire request_bridge;
wire [15:0] data_bridge;
wire corectl_bridge = 1;
wire clk_w;


hex_view hex_view_inst(
	.clk(clk),
	.HEX(HEX)
);


cyclonev_prblock cyclonev_prblock_inst(
	.clk(clk_w),
	.ready(ready_bridge),
	.error(error_pr_bridge),
	.done(done_bridge),
	.prrequest(request_bridge),
	.data(data_bridge),
	.corectl(corectl_bridge) 
);


cyclonev_crcblock cyclonev_crcblock_inst(
	.clk(clk_w),
	.shiftnld(1),
	.crcerror(error_bridge)
);
				


reconfig reconfig_inst(
	.clk(clk),
	.nreset(nreset),
	.freeze(freeze_wire),
	.crc_error_pin(error_bridge),
	.pr_ready_pin(ready_bridge),
	.pr_error_pin(error_pr_bridge),
	.pr_done_pin(done_bridge),
	.pr_request_pin(request_bridge),
	.pr_data_pin(data_bridge),
	.pr_clk_pin(clk_w)
	
);


freeze_region freeze_region_inst(
	.clk(clk),
	.leds(leds),
	.freeze(freeze_wire)
);

endmodule


Для наглядности приведу изображение с RTL Viewer.



Сигнал nreset должен быть подтянут резистором к питанию.
Скомпилируем проект. Теперь нужно указать, какие сущности будут реконфигурируемыми. Для этого перейдем в окно Hierarchy и пометим led_wrapper как Design Partition.


Так же в окне Design Partitions установим Allow Multiply Personas (чтобы открыть окно выберем в меню Assigments -> Design Partitions Window)


Так же сделаем Logic Lock Region для этого модуля.


В окне LogicLocks Regions (Assigments -> LogicLock Regions Windows). Зададим настройки для нашего региона



Сделаем ему фиксированный размер, зададим ширину и высоту региона, что фактически означает количество ресурсов, используемых нашим модулем. Пометим регион как Locked, чтобы в наш модуль не залез другой какой-нибудь модуль. Пометим Reserved on и включим частичную реконфигурацию. Если каких-то полей нет, их можно отобразить, кликнув правой кнопкой мыши в окне LogicLock Regions.
Скомпилируем проект.
Теперь создадим вторую версию нашего модуля светодиодов. Перейдем в окно ревизий и создадим новую реконфигурируемую ревизию. Назовем ее person


двойным щелчком сделаем эту ревизию текущей. удалим текущий модуль led_flash и создадим новый в папке нашей ревизии.
module led_flash
(
	input clk,
	output reg [3:0] leds
);

initial
begin
	leds = 4'b1000;
end

reg [31:0] count = 0;

always @(posedge clk)
begin
	if (count >= 25000000)
	begin 
		if (leds == 4'b0001)
			leds <= 4'b1000;
		else
			leds <= leds >> 1;
		count <= 0;
	end
	else
		count <= count + 1;
end
endmodule


Откроем окно Design Partitions и установим для нашего реконфигурируемого модуля Netlist Type в Source File.


Скомпилируем ревизию.

Важно! Базовая ревизия должна быть скомпилирована прежде, чем мы будем компилировать реконфигурируемые ревизии.

Все что осталось, это генерация файла частичной реконфигурации. Для этого в меню выберем File — Convert Programming Files. Для начала нужно сгенерировать файл маски нашего региона. Выбираем .pmsf file. Для его генерации понадобится .sof файл и .msf файл нашего региона. Они находятся в папке output_files.

Нажимаем Generate. Теперь необходимо сгенерировать .rbf файл аналогичным способом. Для его генерации необходим созданный нами на прошлом шаге .pmsf файл.

Все что осталось, это прошить плату.Заходим в Programmer и прошиваем top.sof. Чтобы прошить .rbf файл нажмем правой кнопкой в поле с файлом прошивки и жмем Add PR programming file…


Убираем галочку в поле Program/Configure для top.sof и устанавливаем для нашего .rbf файла. Жмем Start. Готово! Пока прошивался файл .rbf, счетчик продолжал работать.
Аналогичным способом можно создать .rbf файл для первой версии led_flash.

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

Проект-пример прикреплен к статье.
Файлы в топике: partial_reconfig.zip

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

RSS свернуть / развернуть
А самое интересное где? Я имею ввиду загрузку регионов из linux.
Баловство всё это. Я так и не смог для себя придумать, где это может пригодиться в реальных проектах. Так, поиграться немного и забыть.
-1
А что там интересного? Настроить биты на загрузку (в случе de1-soc выставляються на dip switch).
А дальше простая команда типа load to fpga my_partitial_project.rbf (команду не помню, если нужно можно в доках посмотреть).
Ну и касательно самого приминения, просто его не видно на мелком примере типа моргания светодиодов. А вот на чем-то большем может быть выигрыш. Например какая нибудь система управления движками требующая безостановочной работы может быть в одному углу ПЛИС. А в другом какая-нибудь числодробилка загружающая нужный алгоритм в зависимости от нужд процессора или текущей задачи.
Взять тот же сферический в вакууме осцил, в котором можно сделать хардвардный анализ фурье, генератор NCO или хитрую фильтрацию и при этом уложиться в дешовую плисину, а не брать что-то жирное где это все будет 90% времени лежать мертвым грузом.
Ну и из того что слышал, кто-то пытался с помощью частичной реконфигурации пилить модифицируемые нейронные сети. Но это так уже чисто академический подход.

ЗЫ Автору риспект давно не было годных статей в сообществе.
+2
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.