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

Здравствуйте. Меня зовут Дмитрий. Сегодня мы научимся работать с SDRAM памятью и нарисуем множество Мандельброта на экране.
Данная статья является продолжением статьи Создание видеокарты Бена Итера на FPGA чипе [1]. Если вы не читали то очень рекомендую. Ну а мы начинаем.
Память 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;
}
После выполнения программы в каталоге с программой появится вот такая картинка.

Но это 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
Нажмите здесь для печати.