Параллельный интерфейс на ПЛИС. Обработка данных. Часть I.

DSCN59545410

Естественным образом ПЛИС используется не только как периферийное или комбинированное устройство, а как цифровой обработчик данных. Все устройство в простом случае делится на две части: интерфейс и обработчик. 

 

 

Я предлагаю для примера реализовать связку FPGA и ARM контроллера. На моем тестовом макете: Cyclone I и ATSAM4C соответственно.

DSCN5865

 

Параллельный интерфейс

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

Ashampoo_Snap_2014.10.13_23h28m55s_008_-e1413459436843

 

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

Ashampoo_Snap_2015.01.23_20h22m44s_018_

 

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

Тактовый сигнал

От host-а к нашему обработчику должен подходить тактовый сигнал работы внешних интерфейсов, либо тактовый сигнал ЦП. При всем при этом, его частота должна быть как минимум в двое больше частоты передающих линий.

Модуль синхронизации

Первое, что необходимо сделать на стороне интерфейса  — подвести тактовый сигнал от управляющего устройства. Второе — это четко различить команду. Управляющих сигналов у нас всего три: WE (запись), OE (чтение) и CS (выборка).

Мы фиксируем управляющие по фронту тактового сигнала host-а. Заметьте, что на схему я уже поставил NOR логику, которая инвертирует управляющие и выделяет только обращения к нашему устройству.

Фиксация данных 

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

 

Как ни странно, адрес нам фиксировать не нужно, так как у него самый низкий приоритет. Кстати, если подключить вход к выходу, то мы получим 1 регистр I/O. :-)

Простейший регистр

Пожалуй та вещь, без которой не обойдется ни одна периферия — простейшая ячейка с данными. Мы рассмотрим входной и выходной регистры, которые, как правило, будут иметь свой уникальный адрес. По сути — это просто несколько триггеров.
Ashampoo_Snap_2015.02.03_20h55m42s_035_
Входящий регистр довольно прост: мы подключаем его к нашему интерфейсу линиями data_in, addr, wr;  данные для ПЛИС будут храниться в data_out. Не стоит забывать назначать каждому регистру свой уникальный адрес. Выходной регистр подключается к интерфейсу линиями data_out, addr, rd, а остальные исключительно для ПЛИС. Заметьте, что выход данных имеет состояние Z для возможности подключения нескольких регистров. Ну не mux-ы же лепить… Код модулей на Verilog-е ниже:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
module register_i (
   input wire clk, //Тактовая
 
	input wire [7:0] data_in, //Входящие данные
	input wire [11:0] addr, //Уникальный адрес
	input wire wr, //Сигнал записи
 
	output reg [7:0] data_out, //Сам регистр для ПЛИС
	input wire clr //Сброс состояния
);
 
parameter address = 0;
parameter clear = 0;
 
always @(posedge clk)
begin	
	if (wr && address == addr)
		data_out <= data_in;
 
	if (clr)
		data_out <= clear;
end
endmodule
 
module register_o (
   input wire clk, //Тактовая
 
	input wire [7:0] data_in, //Даннные для записи
	input wire [11:0] addr, //Уникальный адрес
	input wire rd, //Сигнал чтения от устройства
	input wire wr, //Запись
 
	inout reg [7:0] data_out, //Выход данные для устройства
	input wire clr //Очистка ячейки
);
 
parameter address = 0;
parameter clear = 0;
 
reg [7:0] data;
 
always @(posedge clk)
begin	
	if (rd && address == addr)
		data_out <= data;
	else
		data_out <= 8'bZZZZZZZZ;
 
	if (wr)
		data <= data_in;
 
	if (clr)
		data <= clear;		
end
endmodule

Синхронная память случайного доступа

Ради эксперимента соорудим простейшую синхронную SRAM. Многие FPGA имеют на борту M4K блоки памяти, которые можно использовать. Символ называется lpm_ram_dq. Я выделил под нее 4кб.

Кстати, триггеры на входе и выходе даже можно убрать, потому что у памяти они уже есть внутри. А вообще, не стоит увлекаться триггерами, иначе можно случайно получить конвейер, за чем последует задержка данных.

Использование FIFO

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

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

Может всплыть проблема при чтении: когда происходит запрос самого первого элемента, данные не успевают подготовиться, поэтому необходимо использовать Show-ahead mode.

Разобравшись с интерфейсами и системами хранения данных, мы переходим к устройству их обработки.

Потоковая обработка несвязанной информации

Последние два слова написаны не случайно, модель обработчика, которую я опишу, прекрасна во всем, но информация идет несвязанным потоком: мы не можем иметь доступ к определенному элементу, обработчик не видит весь объем.
Ashampoo_Snap_2015.02.08_22h20m15s_005_
Управляющее устройство загружает данные в первое FIFO, и уже в этот момент происходит обработка данных, которые поступают во второе FIFO.

Нам даже не нужна никакая адресация! Второе FIFO должно быть самым большим, потому как первое освобождается быстро, а в случае, если данные не готовы или произошло переполнение — необходимо подать сигнал на IRQ линию host-а.

Для защиты от ошибок было бы замечательно добавить сигнал очистки для всех FIFO. Отдельно я скажу про сигнал IRQ — он инверсный и срабатывает:

  • При попытке записи в переполненное входное FIFO. 
    • Можно посылать его чуть раньше. (параметр almost full)
  • При попытке чтения пустого выходного FIFO.
    • Можно посылать его раньше. (параметр almost empty)

Ядром нашей системы будет сам обработчикcomputer. Его минимальные требования приведены ниже:
Ashampoo_Snap_2015.02.05_22h15m05s_042_
Его функцию возможно представить в виде простейшей машины состояний.

Ashampoo_Snap_2015.02.05_22h18m38s_043_

 

Машины уже оптимизирована для обработки за такт. Каждое состояние можно описать так:

  1. reset — сброс всех регистров.
  2. idle — проверка наличия входящих данных.
  3. get — получаем 1 ед. данных.
  4. action — обрабатываем информацию.
  5. put — отправляем обработанную ед. данных

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
module Computer (
    input wire clk,
 
	input wire [7:0] in,
	input wire empty,
	input wire full,
 
	output reg [7:0] out,
	output reg wr,
	output reg rd,
 
	output reg busy
);
 
parameter idle = 4;
parameter get = 1;
parameter action = 2;
parameter put = 3;
parameter reset = 0;
 
parameter false = 0, true = 1;
 
reg [2:0] state = reset;
reg [15:0] count;
 
always @(negedge clk) //Механизм переключения состояний
begin
	case (state)
		idle:
			if (!empty) //Если входное FIFO не пустое
				state <= get;
			else
				state <= reset;
 
		get:
			state <= action;
 
		action:
			if (!full) //Если выходное FIFO не переполнено
				state <= put;	
		put:
			if (!empty) //Оптимизация для пропуска ненужного состояния, если данные в FIFO еще есть
				state <= get;
			else
				state <= idle;
 
		reset:
			state <= idle;
 
		default:
			state <= reset;
	endcase	
end
 
always @(posedge clk) //Действия в определенном состоянии
begin	
	case(state)	
		idle:
			begin
				//Ничего
			end
 
		get:
			begin
				rd <= true; //Включаем режим-чтения
				busy <= true;
			end
 
		action:
			begin
				out <= in; //Получаем и записываем данные
				wr <= true; //Режим записи
				rd <= false; //Завершаем чтение
			end
 
		put:
			begin
				wr <= false; //Завершаем запись
			end
 
		reset:
			begin
				wr <= false;
				rd <= false;
				busy <= false;
			end
 
	endcase;
end
 
endmodule

Работу этого комплекса мы проверим на практике. Я подключу к нему ARM процессор, который будет отправлять обработчику картинку 64x64, а затем выводить ее на экран. Код тестирования довольно прост:uz4W29kLcSU

1
2
3
4
5
6
7
8
const uint8_t texture[64*64] ={...}
uint8_t buffer[64*64];
 
void test() {
  memcpy(FPGA, texture, sizeof(texture));
  memcpy(buffer, FPGA, sizeof(texture));
  image.print(buffer);
}

Результат… Вы сами видите, что ничего интересного не произошло.

DSCN5882

Обратите внимание на осциллограмму сигнала IRQ. Здесь четко видно, что ARM процессор вообще не простаивает. (бесполезная картинка!)

pic_397_1

Желтый сигналIRQ, но скорость чтения/записи примерно равна скорости обработки, поэтому прерываний нет. Если уменьшить размеры буферов, то прерывания начнут появляться.

Давайте усложним нашего обработчика: заставим его провести операцию над данными. А конкретнее: K[n] = (K[n] + K[n-1])/2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
module Computer (
    input wire clk,
 
	input wire [7:0] in,
	input wire empty,
	input wire full,
 
	output reg [7:0] out,
	output reg wr,
	output reg rd,
 
	output reg busy
);
 
parameter idle = 4;
parameter get = 1;
parameter action = 2;
parameter put = 3;
parameter reset = 0;
 
parameter false = 0, true = 1;
 
reg [2:0] state = reset;
reg [15:0] count;
 
always @(negedge clk) //Механизм переключения состояний
begin
	case (state)
		idle:
			if (!empty) //Если входное FIFO не пустое
				state <= get;
			else
				state <= reset;
 
		get:
			state <= action;
 
		action:
			if (!full) //Если выходное FIFO не переполнено
				state <= put;	
		put:
			if (!empty) //Оптимизация для пропуска ненужного состояния, если данные в FIFO еще есть
				state <= get;
			else
				state <= idle;
 
		reset:
			state <= idle;
 
		default:
			state <= reset;
	endcase	
end
 
always @(posedge clk) //Действия в определенном состоянии
begin	
	case(state)	
		idle:
			begin
				//Ничего
			end
 
		get:
			begin
				rd <= true; //Включаем режим-чтения
				busy <= true;
			end
 
		action:
			begin
				out <= (in + out)/2; //Выполняем арифметическую операцию и записываем.
				wr <= true; //Режим записи
				rd <= false; //Завершаем чтение
			end
 
		put:
			begin
				wr <= false; //Завершаем запись
			end
 
		reset:
			begin
				wr <= false;
				rd <= false;
				busy <= false;
			end
 
	endcase;
end
 
endmodule

Новых состояний мы не добавляем, поэтому на производительность это не повлияет. Результат:

DSCN5883

Текстура размылась линейно по оси X — это то, чего мы и ожидали от этого «фильтра».

Частотная изоляция

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

Обработка связанной/смешанной информации

Данная модель DSP имеет больше возможностей для обработки, мы можем проводить обработку как блоками, так и адресованным потоком.
Ashampoo_Snap_2015.02.08_22h21m08s_006_
Важным отличием является адресация памяти обработчика. Обработчик специализируется уже не на 1 ед.данных, а на целой задаче. Информация, находящаяся в FIFO, не обязательно должна быть исходным сырьем для конечных данных, мы можем загружать в него пакеты команд и т.п.

 

Сигнал IRQ будет появляться в следующих случаях:

  • Когда FIFO переполнено, и происходит запись.
  • Когда computer выполняет задачу, и происходит чтение.

Машина состояний обработчика будет отличаться и зависеть от конкретной задачи.

Ashampoo_Snap_2015.02.10_19h00m45s_007_

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

1
2
3
4
5
for (int y=0; y<64; ++y) {
  for (int x=0; x<64; ++x) {
    RAM[x + y*64] = FIFO.Out;
  }
}

Но давайте сделаем что-то по-сложнее: проведем аффинное преобразование. Сжатие объекта по оси-X будет выглядеть в псевдокоде так:

1
2
3
4
5
for (int y=0; y<64; ++y) {
  for (int x=0; x<64; ++x) {
    RAM[16 + (x/2) + y*64] = FIFO.Out;
  }
}

Перенести все это в язык Verilog не составит труда. Количество состояний не будет отличаться от предыдущей машины.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
module Computer (
    input wire clk,
 
	input wire [7:0] in,
	input wire empty,
	input wire full,
 
	output reg [7:0] out,
	output reg [11:0] addr,
	output reg wr,
	output reg rd,
 
	output reg busy
);
 
parameter idle = 4;
parameter get = 1;
parameter action = 2;
parameter put = 3;
parameter reset = 0;
 
parameter false = 0, true = 1;
 
reg [7:0] x; //Координаты для пересчета
reg [7:0] y;
 
reg [2:0] state = reset;
reg [15:0] count;
 
always @(negedge clk) //Механизм переключения состояний
begin
	case (state)
		idle:
			if (!empty) //Если входное FIFO не пустое
				state <= get;
 
		get:
			if (!empty) //Еще одна проверка, так как состояние idle не входит в цикл обработки
				state <= action;
 
		action:
			state <= put;
 
		put:
			if (y < 63) //Цикл, пока y != 64
				state <= get;
			else
				state <= reset;
 
		reset:
			state <= idle;
 
		default:
			state <= reset;
	endcase	
end
 
always @(posedge clk) //Действия в определенном состоянии
begin	
	case(state)	
		idle:
			begin
				//Ничего
			end
 
		get:
			begin
				rd <= true; //Включаем режим-чтения
				busy <= true;
			end
 
		action:
			begin
				out <= in; //Это просто операция копирования
				wr <= true; //Режим записи
				rd <= false; //Завершаем чтение
			end
 
		put:
			begin
				wr <= false; //Завершаем запись
				addr <= 16 + (x/2) + y*64; //Вычисляем адрес конечного пикселя + сжимаем по X
 
				x <= x + 1; //Приращение X 				
				if (x > 62) //Когда X == 64
					begin
						y <= y + 1; //Приращение Y
						x <= 0;
					end	
			end
 
		reset:
			begin
				wr <= false;
				rd <= false;
				addr <= 16; //Смещаю первый пиксель, тк. обработка адреса идет последней
				x <= 0;
				y <= 0;
				busy <= false;
			end
 
	endcase;
end
 
endmodule

Подобные аффинные преобразования каждого пикселя — не лучший вариант обработки текстур вообще, но в качестве примера подойдет. Если мы загрузим в устройство нашу прежнюю текстуру 64x64, то увидим следующее:

DSCN5885

Ох, как не легко нашему подопытному… Однако все верно.

Во входное FIFO могут попасть «мусорные» данные из-за незавершенных операций (если вдруг питание на FPGA подается уже во-время работы ARM), тогда картинка будет искажаться. Сигнал сброса здесь будет впору.

Универсальный обработчик

Мы подходим к конченому DSP. Данная структура устройства обработки является наиболее универсальной, а также самой сложной из представленных.

Ashampoo_Snap_2015.02.13_21h08m08s_010_

В схеме появились следующие компоненты:

  • Адресное пространство. Использование регистров, для управления системой, и FIFO одновременно.
  • Дуплексный канал с внутренней ОЗУ. Возможны любые операции с данными.
  • ПЗУ. Крайне необходима вещь, возможность хранить какие-либо наборы значений. (тригонометрические функции, например)

Если реализовать ее на ПЛИС, то уйдет не так уж и много ресурсов. Altera Cyclone I с легкостью вмещает этого DSP.

Tes34t

 

Выглядит ужасно, но что же поделаешь? Не все на Verilog-е пишут. :-) Заметьте, как реализовано включение FIFO в адресную шину: я взял 2-ой бит адреса как управляющий записью FIFO, адресация следующая:

  • 0x000 — Регистр 1 (запись)
  • 0x001 — Регистр 2 (запись)
  • 0x002 — 0xFFF — Адреса FIFO (запись в любой)
  • 0x000 — 0xFFF — Адреса QRAM (только чтение)

Как видите, адреса чтения/записи дифференцированные, но вы можете сделать по-другому. Для ПЗУ вам необходимо сформировать .mif файл с данными. Условия сигнала IRQ будут теми же:

  • Когда FIFO переполнено, и происходит запись.
  • Когда computer выполняет задачу, и происходит чтение.

Блок computer заметно пополнился сигналами:
Ashampoo_Snap_2015.02.14_15h30m54s_012_
Машина большая часть машины состояний обработчика будет зависеть от задачи, но структура все же есть:

Ashampoo_Snap_2015.02.10_19h00m45s_55007_

Любым действиям DSP должно положить начало какое-то определенное событие.  Это может быть значение в регистре или наполнение FIFO данными.

Структура работы может быть подобной:

  1. Ожидание события
  2. Переход в рабочий режим (установить BUSY)
  3. Сохранение значений всех регистров
  4. Операции…
  5. Сброс

Для примера я реализую XOR генератор текстуры заданного размера, на псевдокоде это выглядить так:

1
2
3
4
5
6
7
8
9
10
11
12
13
uint8_t REG1 = Ширина;
uint8_t REG2 = Высота;
 
if (REG2 != 0) { //Триггер по записи в регистр
  for (int y=0; y<REG2; ++y) {
    for (int x=0; x<REG1; ++x) {
      RAM[x + y*REG2] = x^y; //Генерация текстуры с цветом x^y
    }
  }
 
  REG2 = 0;
  REG1 = 0;
}

В данном случае я сделал запуск обработчика по изменению значения (отличное от нуля) в регистре №2. Не забываем сохранять значения регистров в регистрах обработчика во избежание ошибок. Код обработчика на Verilog-е следующий:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
module Computer (
    input wire clk,
 
	input wire [7:0] ram_in,
	input wire [7:0] fifo_in,
	input wire empty,
	input wire [7:0] r_1,
	input wire [7:0] r_2,
	input wire [7:0] rom_in,
	input wire r_we,
 
	output reg [7:0] ram_out,
	output reg [11:0] ram_addr,
	output reg [7:0] rom_addr,
	output reg ram_wr,
	output reg firo_rd,
	output reg r_clr,
 
	output reg busy
);
 
parameter idle = 4;
parameter pre = 1;
parameter action_1 = 2;
parameter action_2 = 3;
parameter reset = 0;
 
parameter false = 0, true = 1;
 
reg [7:0] x; //Координаты для пересчета
reg [7:0] y;
 
reg [7:0] xsize, ysize; //Размеры 
 
reg [7:0] trigger; //Буфер для регистра2
 
reg [2:0] state = reset;
reg [15:0] count;
 
always @(negedge clk) //Механизм переключения состояний
begin
	case (state)
		idle:
			if (trigger && r_we) //Если идет запись и регистр2 != 0
				state <= pre;
 
		pre:
			state <= action_1; //Вход в цикл XY
 
		action_1:
			state <= action_2;
 
		action_2:
			if (y < ysize-1) //Пока Y < Регистр2
				state <= action_1;
			else
				state <= reset;
 
		reset:
			state <= idle;
 
		default:
			state <= reset;
	endcase	
end
 
always @(posedge clk) //Действия в определенном состоянии
begin	
	case(state)	
		idle:
			begin
				trigger <= r_2; //Фиксируем значение регистра2
				r_clr <= false; //Запрещаем очистку регистров
			end
 
		pre:
			begin
				busy <= true; //Статус: занят
                                xsize <= r_1; //Сохраняем значения регистров
                                ysize <= r_2;
			end
 
		action_1:
			begin
				ram_out <= x ^ y; //Генерируем цвет из XY
				ram_wr <= true; //Запись
			end
 
		action_2:
			begin
				ram_wr <= false;
				ram_addr <= x + y * ysize; //Вычисляем следующий адрес
 
				x <= x + 1; //Приращение X 				
                                if (x > xsize-2) //Когда X >= Регистр1
					begin
						y <= y + 1; //Приращение Y
						x <= 0;
					end
			end
 
		reset:
			begin
				ram_wr <= false;
				firo_rd <= false;
				trigger <= 0;
				ram_addr <= 0;
				x <= 0;
				y <= 0;
				r_clr <= true; //Очистка регистров
				busy <= false;
			end
 
	endcase;
end
 
endmodule

Не стоит забывать очищать все регистры после выполнения (или во время), чтобы триггер не срабатывал постоянно. Работа с такой системой со стороны ARM будет выглядеть так:

1
2
3
4
5
6
7
8
9
uint8_t buffer[64*64];
 
void test() {
  FPGA[0] = 64; //Указываем ширину
  FPGA[1] = 64; //Указываем высоту
 
  memcpy(buffer, FPGA, sizeof(64*64));
  image.print(buffer);
}

Генерация такой текстуры 64x64 займет точно 8194 такта частоты, однако никто не запрещает вам оптимизировать процесс, хотя бы путем расширения шины данных и уменьшения количества состояний. Результат работы:

DSCN5894

Не обращайте внимания на артефакты изображения — это недостаток устройства вывода.

Я предлагаю усложнить задачу. Давайте будем не только генерировать текстуру, но и ее вращать на заданный угол. :-) Можно использовать довольно простую формулу матрицы вращения.

351c090833644816b1bc6137ae562f5c

То есть, для каждой точки текстуры мы будем вычислять новую координату на экране.  Это довольно затратный метод, из-за вычислений на каждый пиксель.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void rotate(int xsize, int ysize, float angle) { 
    int y0 = ysize / 2;
    int x0 = xsize / 2;
 
	float sinc = sin(angle);
	float cosc = cos(angle);
 
	for (int y=0; y<xsize; ++y) {
        for (int x=0; x<ysize; ++x) {
 
            unsigned int nx = (cosc*(x-x0)) - (sinc*(y-y0)) + x0;
            unsigned int ny = (sinc*(x-x0)) + (cosc*(y-y0)) + y0;
 
            uint8_t color = x^y;
 
            DrawPixel(nx, ny, color);
        }
    }     
}

Тут вылезают две очевидные проблемы при переносе на ПЛИС:

  1. Где взять тригонометрию?
  2. Где взять тип float?

Тригонометрию выгоднее сделать табличной, мы берем 256 значений синуса от 0 до 2Пи, используя формулу SIN(n/40.7436654315252)*255. Соответственно, диапазон значений: [-255; 255] Файл с готовой таблицей можно взять здесь /.

1
2
3
4
5
reg signed [8:0] sin, cos;
//.........
 
sin <= table[angle];
cos <= table[angle + 64];

Вместо float достаточно использовать числа с фиксированный точкой, в данный задаче хватит 8-ми битной точности (var<<8).

1
2
3
4
5
6
7
8
9
reg signed [16:0] var1; //Наше не целое число в формате [255.255]
reg signed [16:0] var2;
 
reg signed [8:0] result; //Целое число в формате [255] 
//.........
var1 <= var1 + var2; //Сложение 
var1 <= var1 - var2; //Вычитание
//.........
result <= (var1 + var2)>>8; //Сложение и преобразование в обычное число.

Машина состояний немного увеличивается, в связи с большими вычислениями: + 4 умножения + 8 сложений.
Ashampoo_Snap_2015.02.20_14h31m15s_001_
Реализация этого на Verilog-е. Кстати, необходимо расширить регистр данных для ПЗУ до 9 бит.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
module Computer (
    input wire clk,
 
	input wire [7:0] ram_in,
	input wire [7:0] fifo_in,
	input wire empty,
	input wire [7:0] r_1, //Ширина текстуры
	input wire [7:0] r_2, //Высота текстуры
	input wire signed [8:0] rom_in,
	input wire r_we,
 
	output reg [7:0] ram_out,
	output reg [11:0] ram_addr,
	output reg [7:0] rom_addr,
	output reg ram_wr,
	output reg firo_rd,
	output reg r_clr,
 
	output reg busy
);
 
parameter idle = 9;
parameter pre_1 = 1;
parameter pre_2 = 2;
parameter pre_3 = 3;
parameter action_1 = 4;
parameter action_2 = 5;
parameter action_3 = 6;
parameter action_4 = 7;
parameter action_5 = 8;
parameter reset = 0;
 
parameter false = 0, true = 1;
 
reg [7:0] x, y; //Координаты текстуры
 
reg [7:0] xsize, ysize; //Размеры
 
reg [7:0] nx, ny; //Координаты экрана
 
reg [7:0] x0, y0; //Координаты центра
 
reg signed [16:0] rx0, rx1; //Буферы для вычисления
reg signed [16:0] ry0, ry1; //Буферы для вычисления
 
reg signed [8:0] sin, cos; //Тригонометрия
 
reg [7:0] trigger;
reg [5:0] angle = 0; //Угол поворота
 
reg [3:0] state = reset;
reg [15:0] count;
 
always @(negedge clk)
begin
	case (state)
		idle:
			if (trigger)
				state <= pre_1;
 
		pre_1:
			state <= pre_2;
 
		pre_2:
			state <= pre_3;	
 
		pre_3:
			state <= action_1;					
 
		action_1:
			state <= action_2;
 
		action_2:
			state <= action_3;	
 
		action_3:
			state <= action_4;		
 
		action_4:
			if (y < ysize-1)
				state <= action_1;
			else
				state <= reset;
 
		reset:
			state <= idle;
 
		default:
			state <= reset;
	endcase	
end
 
always @(posedge clk)
begin	
	case(state)	
		idle:
			begin
				trigger <= r_2;
				r_clr <= false;
			end
 
		pre_1:
			begin
				busy <= true;
				rom_addr <= angle; //Задаем угол в адрес ПЗУ
			end
 
		pre_2:
			begin
				sin <= rom_in; //Получаем sin
				rom_addr <= angle + 64; //Задаем адрес для cos. cos(a) = sin(a + pi/2)
 
                                xsize <= r_1;
                                ysize <= r_2;
			end	
 
		pre_3:
			begin
				cos <= rom_in; //Получаем cos
 
				x0 <= xsize/2; //Находим центр текстуры
				y0 <= ysize/2;
			end		
 
		action_1:
			begin
				rx0 <= (cos * (x - x0)); //Вы полняем все необходимые умножения
				rx1 <= (sin * (y - y0));
 
				ry0 <= (sin * (x - x0));
				ry1 <= (cos * (y - y0));
			end
 
		action_2:
			begin
				nx <= ((rx0 - rx1)>>8) + x0; //Выполяем все сложения
				ny <= ((ry0 + ry1)>>8) + y0; //Вычисляем экранные координаты
			end		
 
		action_3:
			begin
				ram_out <= (x ^ y); //Генерим текстуру XOR методом
				ram_wr <= true;
			end
 
		action_4:
			begin
				ram_wr <= false;
				ram_addr <= nx + ny * ysize; //Вычисляем адрес точки на экране
 
				x <= x + 1; 				
                                if (x > xsize-2)
					begin
						y <= y + 1;
						x <= 0;
					end
			end
 
		reset:
			begin
				ram_wr <= false;
				firo_rd <= false;
				trigger <= 0;
				ram_addr <= 0;
				angle <= angle + 1; //Увеличиваем угол
				x <= 0;
				y <= 0;
				r_clr <= true;
				busy <= false;
			end
 
	endcase;
end
 
endmodule

Угол поворота не задается через регистр, для наглядности эффекта он увеличивается во время сброса.

rotate

Это лишь пример, здесь еще многое не учтено и нуждается в оптимизации. Также вместо XOR генерации можно загружать данные из FIFO, так как размеры текстуры фиксированы, а пиксели обрабатываются последовательно. 

Максимальная частота

Рано или поздно ваш обработчик начнет не справляться с задачей за выделенное время, вам необходимо контролировать максимальную частоту работу с помощью Timing Analysis.
Ashampoo_Snap_2015.02.20_15h28m00s_002_
Ну что ж. Зачем интерфейсу навязывать тактовый сигнал всей системе? Используем свою PLL или генератор.

Ashampoo_Snap_2015.02.20_15h33m53s_003_

 

Одну проблему составляет задержка данных FIFO, которые будут приходить только через 3 такта. Также следует быть осторожным с триггером запуска обработчика.

Насколько вы знаете, путь до той или иной логики занимает определенное время и может быть разным для каждой линии.
17dabe0468ba4a268e33b8ccde528faf
Fmax всегда определяется по критическому пути, даже если он только один критический во всей схеме.

Конвейерная обработка данных

Один и способов организации обработки информации — преимущественно большого объема — конвейер. Идея заключается в разбиении задачи на независимые стадии/уровни, которые передают данные последовательно друг другу и одновременно по тактовому импульсу.

Ashampoo_Snap_2015.02.21_21h45m40s_006_

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

Иногда эта мера бывает вынужденной, объясню на примере:

j1

 

Две задачи: два суммирования, которые занимают вместе, предположим, 10ns. Тогда максимальная частота работы цепи < 100MHz.

j2 

Триггером мы разделили задержки. Теперь максимальная частота работы увеличивается вдвое: < 200MHz. Однако наши данные будут запаздывать на 1 такт (такая характеристика называется латентностью).

Для оповещения дальнейших систем об этой задержке имеет смысл провести сигнал целостности данных таким образом:

j4

 

Однако не стоит превращать все свои схемы в конвейеры, он нужен не всегда и не везде.

Все же, наша задача с поворотом изображения может работать быстрее в формате конвейера. Дробления производятся в блоке Computer-а.
Ashampoo_Snap_2015.02.22_11h53m05s_002_
Минимальный конвейер в 4 уровня. А также два новых модуля, отвечающих за пересчет XY координат и записи в память, соответственно.

  • Верхний ряд вычисляет по матрице экранные координаты.
    1. = - x0; = — y0;
    2. = * sin; = * cos; (матрица)
    3. = +/-; = +/-; (матрица)
    4. =+ x0; =+ y0;
  • Средний ряд вычисляет цвет текстуры в точке XY (не экранные)
    • = x ^ y; (XOR)
  • Нижний ряд — сигнал целостности данных.

Стоит учесть, что кроме этих блоков у нас все же остается машина состояний, управляющая конвейером (control unit). Иначе, кто будет отвечать за запуск и сброс?

Ashampoo_Snap_2015.02.21_23h08m46s_011_

Я не стану разбивать все, как описал выше, на блоки. Все будет реализовано в одном модуле computer-а на Verilog.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
module Computer (
    input wire clk,
 
	input wire [7:0] ram_in,
	input wire [7:0] fifo_in,
	input wire empty,
	input wire [7:0] r_1, 
	input wire [7:0] r_2, 
	input wire signed [8:0] rom_in,
	input wire r_we,
 
	output reg [7:0] ram_out,
	output reg [11:0] ram_addr,
	output reg [7:0] rom_addr,
	output reg ram_wr,
	output reg firo_rd,
	output reg r_clr,
 
	output reg busy
);
 
parameter idle = 9;  //Ожидание
parameter pre_1 = 1; //Подготовка
parameter pre_2 = 2; //Подготовка
parameter pre_3 = 3; //Подготовка
parameter count = 4; //Счет
parameter post = 5;  //Завершение
parameter reset = 0; //Сброс
 
parameter false = 0, true = 1;
 
reg valid = false;
 
//Регистры для 1-ого уровня конвейера
reg signed [8:0]  stage_1_reg_1, stage_1_reg_2; //Рабочие переменные
reg [7:0] stage_1_reg_3, stage_1_reg_4;
reg stage_1_valid; //Сигнал целостности
 
//Регистры для 2-ого уровня конвейера
reg signed [16:0] stage_2_reg_1, stage_2_reg_2, stage_2_buf_1, stage_2_buf_2; //Рабочие переменные и дополнительные переменные
reg [7:0] stage_2_reg_3, stage_2_reg_4;
reg stage_2_valid; //Сигнал целостности
 
//Регистры для 3-ого уровня конвейера
reg signed [16:0] stage_3_reg_1, stage_3_reg_2; //Рабочие переменные
reg [7:0] stage_3_reg_3, stage_3_reg_4;
reg stage_3_valid; //Сигнал целостности
 
//Регистры для 4-ого уровня конвейера
reg signed [16:0] stage_4_reg_1, stage_4_reg_2; //Рабочие переменные
reg [7:0] stage_4_reg;
reg stage_4_valid; //Сигнал целостности
 
reg [7:0] x, y; 
 
reg [7:0] xsize, ysize;
 
reg [7:0] x0, y0; 
 
reg signed [8:0] sin, cos; 
 
reg [7:0] trigger;
reg [5:0] angle = 0; 
 
reg [3:0] state = reset;
//------------------------------------------------------------------------------------
//-----------Управляющая машина + счетчик---------------------------------------------
//------------------------------------------------------------------------------------
always @(negedge clk)
begin
	case (state)
		idle:
			if (trigger)
				state <= pre_1;
 
		pre_1:
			state <= pre_2;
 
		pre_2:
			state <= pre_3;	
 
		pre_3:
			state <= count;					
 
		count:
			if (y < ysize-1)
				state <= count;
			else
				state <= post;
 
		post:
			if (!ram_wr)
				state <= reset;
 
		reset:
			state <= idle;
 
		default:
			state <= reset;
	endcase	
end
 
always @(posedge clk)
begin	
	case(state)	
		idle:
			begin
				trigger <= r_2;
				r_clr <= false;
			end
 
		pre_1:
			begin
				busy <= true;
				rom_addr <= angle; 
			end
 
		pre_2:
			begin
				sin <= rom_in; 
				rom_addr <= angle + 64; 
 
				xsize <= r_1;
				ysize <= r_2;
			end	
 
		pre_3:
			begin
				cos <= rom_in; 
 
				x0 <= xsize/2;
				y0 <= ysize/2; 
 
				valid <= true; //Посылаем сигнал для начала работы (valid)
			end			
 
		count:
			begin			
				x <= x + 1; 				if (x > xsize-2)
					begin
						y <= y + 1;
						x <= 0;
					end
			end
 
		post:
			begin
				valid <= false; //Посылаем сигнал для завершения работы (invalid)
			end
 
		reset:
			begin
				firo_rd <= false;
				trigger <= 0;
				angle <= angle + 1;
				x <= 0;
				y <= 0;
				r_clr <= true;
				busy <= false;
			end
 
	endcase;
end
//------------------------------------------------------------------------------------
//-----------Конвейер-----------------------------------------------------------------
//------------------------------------------------------------------------------------
always @(posedge clk)
begin
	stage_1_reg_1 <= x - x0;
	stage_1_reg_2 <= y - y0;
	stage_1_reg_3 <= x;
	stage_1_reg_4 <= y;
	stage_1_valid <= valid;	
 
	stage_2_reg_1 <= stage_1_reg_1 * cos;
	stage_2_buf_1 <= stage_1_reg_2 * sin;
	stage_2_reg_2 <= stage_1_reg_1 * sin;
	stage_2_buf_2 <= stage_1_reg_2 * cos;
	stage_2_reg_3 <= stage_1_reg_3;
	stage_2_reg_4 <= stage_1_reg_4;
	stage_2_valid <= stage_1_valid;	
 
	stage_3_reg_1 <= stage_2_reg_1 - stage_2_buf_1;
	stage_3_reg_2 <= stage_2_reg_2 + stage_2_buf_2;
	stage_3_reg_3 <= stage_2_reg_3;
	stage_3_reg_4 <= stage_2_reg_4;
	stage_3_valid <= stage_2_valid;	
 
	stage_4_reg_1 <= (stage_3_reg_1>>8) + x0;
	stage_4_reg_2 <= (stage_3_reg_2>>8) + y0;
	stage_4_reg   <= stage_3_reg_3 ^ stage_3_reg_4;
	stage_4_valid <= stage_3_valid;	
 
end
//------------------------------------------------------------------------------------
//-----------Записывающее устройство--------------------------------------------------
//------------------------------------------------------------------------------------
always @(posedge clk)
begin
	ram_addr <= stage_4_reg_1 + stage_4_reg_2 * ysize;
	ram_out <= stage_4_reg;
	ram_wr <= stage_4_valid;
end
 
endmodule

Результат вас не должен удивить, он тот же самый:

rotate

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

  1. Без конвейера. 64px * 64px *4[операции]  = 16384 тактов
  2. Конвейер. 64px * 64px + 4[операции] + 1[запись] = 4101 тактов

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

Кстати, даже не обязательно, чтобы каждый конвейерный блок выполнял инструкции за 1 такт — конвейеризация реализуется и на высоком уровне.

Файлы проектов

Полезные ссылки

Вы можите оставить комментарий, или поставить трэкбек со своего сайта.

Есть 1 комментарий. к “Параллельный интерфейс на ПЛИС. Обработка данных. Часть I.”

  1. Необходимые правки и неточности в статье: http://marsohod.org/forum/proekty-polzovatelej/3008-obrabotka-dannykh-na-plis

    Thumb up 0 Thumb down 0

Написать комментарий

XHTML: Вы можете использовать эти теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Bug Report
Локализовано: шаблоны Wordpress