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

от автора

Здравствуйте. Меня зовут Дмитрий. Сегодня мы научимся работать с 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *