Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров2.8K

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

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

Контроллер 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 чипа.

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

Последовательный доступ

Немного доработал SDRAM контроллер. Теперь у него появился вход Serial_access. Если его активировать то, можно производить чтение или запись группы байт не производя при этом активацию и деактивацию строки.

В конечном автомате появился новое состояние:

            S_NOT_PRECHARGE_IDLE:
			begin
				if (!Serial_access) state_main <= S_PRECHARGE_AFTER;
				else
				begin
				
					if (m_valid)
					begin
						if (m_addr_set[21:9] == m_addr[21:9])
						begin
							m_addr_set <= m_addr;
							flg_first_cmd <= 1;
							
							if(m_we) state_main <= S_WRITE;
							else state_main <= S_READ;
						end
						else  state_main <= S_PRECHARGE_AFTER;
						
					end
				end
			
			end

В этом состоянии производится проверка номера строки и если он совпадает, то сразу происходит переход на чтение или запись, минуя активацию/деактивацию строки.

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

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

Для начало чтобы потренироваться я написал небольшую программу на 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;
} 

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

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

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

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

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

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

Вывод

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

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

GitHub репозиторий с проектом

Другие мои статьи на эту тему

Создание видеокарты Бена Итера на FPGA чипе

Написание i2c контроллера для FPGA и подключение камер ov7670 и ov2640

Теги:
Хабы:
Всего голосов 16: ↑16 и ↓0+25
Комментарии18

Публикации

Ближайшие события