- PVSM.RU - https://www.pvsm.ru -

Доступ к SDRAM памяти на FPGA и «множество Мандельброта»

Доступ к SDRAM памяти на FPGA и «множество Мандельброта» - 1

Здравствуйте. Меня зовут Дмитрий. Сегодня мы научимся работать с SDRAM памятью и нарисуем множество Мандельброта на экране.

Данная статья является продолжением статьи Создание видеокарты Бена Итера на FPGA чипе [1]. Если вы не читали то очень рекомендую. Ну а мы начинаем.

Контроллер SDRAM памяти.

Память SDRAM была создана для обеспечения больших объемов. Поэтому каждая ячейка этой памяти имеет максимально простое устройств. Каждая ячейка состоит только из конденсатора и транзистора. Поэтому при одном и том-же техпроцессе удается создать больше ячеек. Например SRAM память требует 6 транзисторов на одну ячейку.

Но одновременно с большим объемом приходит и проблема. Для работы с SDRAM памятью требуется специальный контроллер. Обращение к SDRAM памяти напрямую невозможно. Из-за того что обращение происходит в несколько шагов. Именно поэтому в современных микропроцессорах присутствует кэш состоящий из ячеек SRAM памяти.

Сердцем нашего контроллера будет конечный автомат:

Скрытый текст
case(state_main)
		
			S_WAIT: 
			
			begin 
			
				if(cnt_wait != INIT_PER) cnt_wait <= cnt_wait + 1'b1; 
				
				else 
				begin
					state_main<= S_NOP;
					cnt_wait <= 0;
				end 
				
			end
			
			S_NOP: 
			
			begin 
			
				if(cnt_wait != 2000) cnt_wait <= cnt_wait + 1'b1;
				
				else
				begin 
					state_main<= S_PRECHARGE_ALL;
					cnt_wait <= 0;
				end 
				
			end
			
			S_PRECHARGE_ALL: 
			
			begin 
				if(cnt_wait != 1) cnt_wait <= cnt_wait + 1'b1;
				
				else 
				begin 
					cnt_wait <= 0;
					state_main <= S_AUTO_REFRESH;
				end 
				
			end
			
			S_AUTO_REFRESH: 
			
			begin 
				if(cnt_wait[14:0] != 6) cnt_wait <= cnt_wait + 1'b1;
				
				else 
				begin 
					cnt_wait[14:0] <= 0;
					
					if(cnt_wait[15]) 
					begin
					state_main <= S_LOAD_MODE;
					cnt_wait[15] <= 0;
					end 
					
					else cnt_wait[15] <= 1;
				end 
				
				
			end
			
			S_LOAD_MODE: 
			
			begin 
				if(cnt_wait != 1) cnt_wait <= cnt_wait + 1'b1;
				
				else
				begin 
					cnt_wait <= 0;
					state_main <= S_IDLE;
				end 
				 
			end
			
			S_IDLE:  
			
			begin 
				if(!m_valid) 
				begin
					if(&cnt_refresh_sdram) 
					begin
						state_main <= S_PRECHARGE_AFTER_WRITE;
						cnt_refresh_sdram <= 0;
					end 
					else cnt_refresh_sdram <= cnt_refresh_sdram + 1'b1;
				end
				
				else
				
				begin
					cnt_refresh_sdram <= 0;
					m_addr_set <= m_addr;
					state_main <= S_ACTIVATE_ROW;
					
				end 
				
			end
			
			S_ACTIVATE_ROW:
			
			begin 
				if(cnt_wait != CL) cnt_wait <= cnt_wait + 1'b1;
				
				else
				
				begin 
					cnt_wait <= 0;
					
					if(m_we) state_main <= S_WRITE;
					else state_main <= S_READ;
					
					flg_first_cmd <= 1;
				end 
			end
			S_WRITE: 
			
			begin
			
				m_ready<= 1'b1;
				
				if(flg_first_cmd) flg_first_cmd <= 0;
				
				else 
				begin 
				
					if(m_valid == 0) 
					begin
						m_ready<= 1'b0;
						
						state_main <= S_PRECHARGE_AFTER_WRITE;
						
					end
					
				end
			end
			
			S_PRECHARGE_AFTER_WRITE:
			
			begin 
				
				if(cnt_wait != 3) cnt_wait <= cnt_wait + 1'b1;
				
				else 
				begin 
					cnt_wait <= 0;
					state_main <= S_IDLE;
				end 
				
			end
			
			S_READ:
			
			begin 
			
				if(flg_first_cmd) flg_first_cmd <= 0;
				
				else 
				begin
					if (cnt_wait > CL) m_ready<= 1'b1;
					
					
					if(m_valid == 1'b0) 
					begin
					
						m_ready<= 1'b0;
					
						state_main <= S_REFRESH_AFTER_READ;
						
						cnt_wait <= 0;
						

					end
					
					else cnt_wait <= cnt_wait + 1'b1;
				end
				
				
			end
			
			S_REFRESH_AFTER_READ: 
			begin 
			
				if(cnt_wait != 3) cnt_wait <= cnt_wait + 1'b1;
				
				else
				begin 
					cnt_wait <= 0;
					state_main <= S_IDLE;
				end 
				 			
			end
		endcase

Сначала мы ожидаем определенный промежуток времени чтобы дать микросхеме памяти инициализироваться. Потом мы подаем команду "precharge". И производим выбор режима работы в состоянии "load". И только после этого мы сможем перейти в режим ожидания из которого при подачи сигнала m_valid можно перейти в состояние чтения либо записи (в зависимости от сигнала m_we). Когда произойдет чтение или запись контроллер активирует сигнал m_ready и данные можно будет считать.

У вас наверно возник вопрос а что это за команда precharge? Precharge это освобождение строки в банке. Дело в том что память SDRAM представляет из себя матрицу из строк и столбцов. И чтобы получить доступ к конкретной ячейки нужно сначала активировать строку а потом столбец. Поэтому данную команду нужно выполнять после доступа к каждой ячейке.

В зависимости от конкретного состояния нашего конечного автомата мы будем подавать управляющие сигналы:

Скрытый текст
case(state_main)

		
		S_PRECHARGE_ALL, S_REFRESH_AFTER_READ, S_PRECHARGE_AFTER_WRITE:  //precharge then NOP
		begin
			sd_cas_n <=	1;
			sd_ras_n <= (cnt_wait==0) ? 0:1;
			sd_we_n <= (cnt_wait==0) ? 0:1;
			sd_addr[12:0] <= (cnt_wait==0) ? {4'b0,1'b1,10'b0} : 0;
			
		end
		
		S_AUTO_REFRESH: //autorefresh  then NOP
		begin
			sd_cas_n <= (cnt_wait[14:0]==0) ? 0:1;
			sd_ras_n <= (cnt_wait[14:0]==0) ? 0:1;
			sd_we_n	<= 1;
			sd_addr[12:0] <= 0;
		end
		
		S_LOAD_MODE: //load mode then NOP
		begin
			sd_cas_n <= (cnt_wait==0) ? 0:1;
			sd_ras_n <= (cnt_wait==0) ? 0:1;
			sd_we_n <= (cnt_wait==0) ? 0:1;
			sd_addr[12:0] <= (cnt_wait==0)  ? {2'b00,3'b000,1'b1,2'b00,CL[2:0],1'b0,3'b000} : 0; 
			//BA[1:0]==0,A[12:10]==0,WRITE_BURST_MODE = 0,OP_MODE = 'd0, CL = 2, TYPE_BURST = 0, BURST_LENGTH = 1
		end
		
		S_ACTIVATE_ROW: //activate then NOP
		begin
			sd_cas_n <= 1;
			sd_ras_n <= (cnt_wait==0) ? 0:1;
			sd_we_n <= 1;
			sd_addr[12:0] <= (cnt_wait==0)  ? m_addr_set[21:9] : 0;
		end
		
		S_WRITE: //WRITE or NOP
		begin
			sd_cas_n <= (m_valid == 1 && m_ready == 1) ? 0:1;
			sd_ras_n <= 1;
			sd_we_n <= (m_valid == 1 && m_ready == 1) ? 0:1;
			sd_addr[12:0] <= {7'd0,m_addr_set[8:0]};
		end
		
		S_READ: //Read then NOP
		begin
			sd_cas_n <= (cnt_wait==0) ? 0:1;
			sd_ras_n <=  1;
			sd_we_n <=  1;
			sd_addr[12:0] <= {7'd0,m_addr_set[8:0]};
		end
		
		
		default: //NOP
		begin
			sd_cas_n <=	1; 
			sd_ras_n<= 1;
			sd_we_n	<= 1;
			sd_addr[12:0] <= 0;
		
		end
	endcase

Как видите сперва мы подаем команду микросхеме, а потом переключаемся в состояние NOP, и ждем выполнение данной команды.

Ну и наверно самая главная часть кода это строка:

assign  sd_data = (state_main == S_WRITE) ? in_data : 16'hzzzz;

Она переводит линию данных в состояние высокого импеданса, когда не происходит запись. Если этого не сделать может произойти повреждение микросхемы памяти или FPGA чипа.

Вот тут [2] можно почитать документацию на SDRAM на русском языке.

Множество Мандельброта.

Ну хорошо у нас есть контроллер памяти, а как проверить его работу? Если мы просто загрузим картинку в массив, как мы это сделали прошлой статье, то SDRAM память нам и не нужна. Картинка она вот уже тут бери да отображай. А вот если-бы была возможность сгенерировать картинку чтобы потом положить её в SDRAM память. И я скажу вам да. Такая возможность есть. И и называется она "Множество Мандельброта" [3].

Для начало чтобы потренироваться я написал небольшую программу на C++ которая генерирует это множество.

Скрытый текст
//Множество Мандельброта
#include <stdlib.h>
#include <iostream>
#include <stdio.h>
#include <conio.h>
#include <math.h>
#include <windows.h>
#include <filesystem>
#include <fstream>

#define WIDTH 800
#define HEIGHT 600
#define BITCOUNT 8
#define DEPTH 100    //  чем выше этот показатель, тем "глубже" получается картинка
#define WIDTH_COEF 0.375 //Коэффициент маштабирования по ширене 4:3(0.375 : 0.5) 16:9(0.29 : 0.5)
#define HEIGHT_COEF 0.5 //Коэффициент маштабирования по высоте
#define X_OFFSET 0.78 // Смещение фракталла по горизонтали
#define Y_OFFSET 0.5 // Смещение фракталла по вертикали

BITMAPFILEHEADER FileHeader;

BITMAPINFOHEADER InfoHeader;

RGBQUAD Palette[256];

BYTE Image[HEIGHT][WIDTH];
 




int main()
{
	std::ofstream fout("Mandelbrot.bmp",  std::ios::binary);
	
	FileHeader.bfType = 0x4D42,          // Обозначим, что это bmp 'BM'
	FileHeader.bfOffBits = sizeof(FileHeader) + sizeof(InfoHeader) + 1024; // Палитра занимает 1Kb
	FileHeader.bfSize = FileHeader.bfOffBits +  BITCOUNT * WIDTH * HEIGHT;	// Посчитаем размер конечного файла

	fout.write((char*)&FileHeader, sizeof(BITMAPFILEHEADER));

	InfoHeader.biSize = sizeof(InfoHeader);
	InfoHeader.biBitCount = BITCOUNT;								// 8 ,бит на пиксель
	InfoHeader.biCompression = BI_RGB;								// Без сжатия
	InfoHeader.biHeight = HEIGHT;
	InfoHeader.biWidth = WIDTH;
	InfoHeader.biPlanes = 1;										// Должно быть 1

	fout.write((char*)&InfoHeader, sizeof(BITMAPINFOHEADER));

	for (int i = 0; i < 16; i++)
	{
		Palette[i].rgbRed = i * 16;
		
	}

	for (int i = 16; i < 32; i++)
	{
		Palette[i].rgbRed = 255;
		Palette[i].rgbGreen = i * 16;
		
	}

	for (int i = 32; i < 48; i++)
	{
		Palette[i].rgbRed = 255;
		Palette[i].rgbGreen = 255;
		Palette[i].rgbBlue = i * 16;
	}
	

	fout.write((char*)&Palette, sizeof(RGBQUAD) * 256);

  	
	int progress = 0;
	int prog_coff = HEIGHT/100;
	int prog_buf = 0;
	std::cout << "progress = " << progress << std::endl;

  	for(int i = 0; i < HEIGHT; i++) //  проходим по всем пикселям оси y
   	{  
		float ci = (((float)i  - Y_OFFSET * HEIGHT )) / (HEIGHT_COEF * HEIGHT);                    //  присваеваем мнимой части 

		prog_buf = i / prog_coff;

		if ((progress + 4) < prog_buf) 
		{ 
			progress = prog_buf;
			std::cout << "progress = " << progress << std::endl;
		}
		

	 	for(int j = 0; j <  WIDTH; j++) //  проходим по всем пикселям оси x
	 	{                 
 
			float cr = (((float)j )  - X_OFFSET * WIDTH) / (WIDTH_COEF * WIDTH);             //  присваеваем вещественной части 
			float zi  = 0.0;                       //  присваеваем вещественной и мнимой части z - 0
			float zr = 0.0; 
			float tmp = 0.0;

			for(int k = 0; k < DEPTH; k++)   //  вычисляем множество Мандельброта
			{         
				tmp = zr*zr - zi*zi;
		  		zi = 2*zr*zi + ci;
		  		zr = tmp + cr;

		  		if (zr*zr + zi*zi > 1.0E16)     //  если |z| слишком велико, то выход из цикла  - это внешняя точка   
				{
					int m = k % 48;                       // 48 колличество цветов в палитре            
					Image[i][j] = m;   
					break;   
				}
		    	                         
			}
			
		}
	
 	}
 

	fout.write((char*)&Image, sizeof(BYTE) * WIDTH * HEIGHT);

	return 0;
} 

После выполнения программы в каталоге с программой появится вот такая картинка.

Доступ к SDRAM памяти на FPGA и «множество Мандельброта» - 2

Но это C++, а в Verilog у меня возникла сложность в том, что генерация данного множества требует вычислений с плавающей запятой. А стандарт Verilog этих вычислений не поддерживает. И мне пришлось использовать встроенные в Quartus модули для работы с числами с плавающей запятой. Поэтому если у вас плата от AMD то вам придется искать соответствующие аналоги.

Модуль я разделил на две части. Одна это MONDELBROTE_BUILDER модуль который собственно генерирует картинку. И MONDELBROTE этот модуль берет сгенерированную картинку и копирует её в SDRAM память попиксельно.

Вывод картинки на экран.

Вывод будем производить в порт VGA как это сделать, читайте статью Создание видеокарты Бена Итера на FPGA чипе [1]. С тои лишь разницей что мне пришлось добавить модуль String_Buffer который вычитывает из SDRAM памяти строку и ждет появления сигнала Hblank, который сигнализирует о том что пора читать следующую строку.

Вывод пикселей на экран происходит с частотой 40 МГц, а чтение и запись в память на частоте 120 МГц поэтому мне пришлось добавить PLL (Phase-Locked Loop) [4].

Вывод

В заключение хочу сказать что хоть я и недавно познакомился с FPGA чипами. Но я в полном восторге, потому что Verilog дает вам ощущение связи с "железом". Ну вот например в модуль генерации множества Мандельброта я добавил параметр INICIALIZATION_EN который позволяет пропустить эту самую генерацию и увидеть содержимое памяти. И если плату вы выключали не на большое время, то при включении вы увидите картинку которая была там до этого, хотя нам всегда говорили что конденсаторы в SDRAM памяти разряжаются за долю секунды, а оказалось что это не так. Вот на компьютере вы не сможете увидеть картинку которая там была до выключения а тут пожалуйста.

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

GitHub репозиторий [5] с проектом.

Автор: deema35

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/verilog/416776

Ссылки в тексте:

[1] Создание видеокарты Бена Итера на FPGA чипе: https://habr.com/ru/articles/893876/

[2] Вот тут: https://microsin.net/adminstuff/hardware/sdr-sdram-mt48lc16m16a2.html

[3] "Множество Мандельброта": https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%BE_%D0%9C%D0%B0%D0%BD%D0%B4%D0%B5%D0%BB%D1%8C%D0%B1%D1%80%D0%BE%D1%82%D0%B0#:~:text=%D0%9C%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%BE%20%D0%9C%D0%B0%D0%BD%D0%B4%D0%B5%D0%BB%D1%8C%D0%B1%D1%80%D0%BE%D1%82%D0%B0%20%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%BE%D0%B4%D0%BD%D0%B8%D0%BC%20%D0%B8%D0%B7,%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%20%D0%BF%D0%BE%D1%85%D0%BE%D0%B6%D0%B8%20%D0%B4%D1%80%D1%83%D0%B3%20%D0%BD%D0%B0%20%D0%B4%D1%80%D1%83%D0%B3%D0%B0.

[4] PLL (Phase-Locked Loop): https://marsohod.org/11-blog/212-pll

[5] GitHub репозиторий: https://github.com/Deema35/MandelbrotSet

[6] Источник: https://habr.com/ru/articles/901116/?utm_campaign=901116&utm_source=habrahabr&utm_medium=rss