
Здравствуйте. Меня зовут Дмитрий. Сегодня мы научимся работать с 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 память нам и не нужна. Картинка она вот уже тут бери да отображай. А вот если-бы была возможность сгенерировать картинку чтобы потом положить её в 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 репозиторий с проектом.
ссылка на оригинал статьи https://habr.com/ru/articles/901116/
Добавить комментарий