В конце сентября – в начале октября в Барнауле прошел кейс-чемпионат «Код успеха». О чемпионате можно прочитать здесь. Один из кейсов “Retro Game Console” и подразумевал  разработку на ПЛИС. Частью кейса было задание разработать векторную каркасную картинку-чертёж. Это в свою очередь потребовало реализовать модуль который будет отрисовывать линии. Вспомнив по алгоритм Брезенхэма я взялся за работу. Реализация заняла значительно больше времени чем я планировал, был момент когда я думал, что задача не решаема при данных условиях. Надо сказать, что в видео-контроллере не было фреймбуффера, изображение нужно было формировать «на лету» в процессе развертки кадра.

Для тех кто забыл в чем суть алгоритма Брезенхэма напомню в двух словах.

Скрытый текст

Рассмотрим построение линии в диапазоне углов 0-45⁰. У нас есть точки начала(Х1,Y1) и конца отрезка(X2,Y). Для начала вспомним уравнение прямой из школьного учебника

 y = kx+c \\  \text{где: }  \qquad  \qquad \qquad  \qquad\qquad  \qquad\qquad  \qquad \qquad \qquad \\ k=Δy/Δx=(y_2-y_1)/(x_2-x_1 )\\ \quad \\c = y_{1}

Не трудно заметить что значение y увеличивается на k при изменении x на 1. Перепишем эту формулу в итерационном виде.

x_{i+1}=x_{i}+1 \\y_{i+1} = y_{i} + k

Значение y на каждом шаге отклоняется от первоначального на значение k и в какой‑то момент отклонение превышает 1. 

Введем промежуточную переменную error и будем добавлять к ней на каждом шаге коэффициент k. В момент, когда error превысит 1, перенесем 1 из error в y, то есть добавим 1 к y, а из error 1 вычтем. Это позволит использовать целочисленную переменную y. Но для накопления отклонения пока все еще используется переменная error с плавающей запятой. Для перехода к целочисленной арифметике нам нужно избавится от дробного коэффициента k. Это возможно умножив k на Δx. В этом случае k превращается в Δy. А сравнение переменной error нужно делать не с 1 а с Δx. .

Тогда формула координат новой точки принимает вид

 error_{i+1} =   \begin{cases}     error_{i} + Δy       & \quad \text{если } error_{i} + Δy <  \text{ Δx }\\     error_{i} + Δy  - Δx & \quad \text{ иначе }     \end{cases}  y_{i+1} =   \begin{cases}     y_{i}       & \quad \text{если } error_{i} + Δy <  \text{ Δx }\\     y_{i} + 1  & \quad \text{ иначе }     \end{cases}

В этом случае все переменные целочисленные.

В общем, реализовать алгоритм Брезенхэма для прямых с наклоном 0⁰-45⁰ не сложно. Нам надо хранить информацию о координатах текущей точки и вычислять координаты следующей. Для реализации  я использовал блок always_comb  для вычисления координат новой точки и блок always_ff для сохранения полученных значений.

always_comb begin
      new_y = dot_y;
      if ((error + dy) > dx) begin
        new_error = error + dy -  dx;
        new_y     = dot_y + 1; 
      end  
      else 
        new_error = error + dy;
   end 
always_ff @(posedge clk)
   if (start_frame) begin
     dot_x <= in_x1;
     dot_y <= in_y1;
     error <= '0;
   end 
   else 
     if (en) begin
       dot_x <= dot_x + 1;
       dot_y <= new_y;
         error <= new_error; 
   end

Работа блока always_comb прозрачна не содержит ни каких хитростей. Про блок always_ff нужно сказать пару слов. Так как, точки мы формируем «на лету», то в начале каждого кадра нужно провести сброс переменных в начальное состояние. Это мы делаем по сигналу start_frame. В остальное время сохраняем значения координат и ошибки в регистрах. Кроме вычисления значений координат нужно синхронизировать вычисление новых точек с их отображением. В моем случае частота тактирования в два раза превышает  частоту вывода пикселей. То есть схема может посчитать координаты двух точек за время отоб��ажения одной. Для синхронизации процессов вычисления и отображения ввел сигнал en.

assign en =  (y == dot_y) & (x == dot_x) & (x <= in_x2); 

Сигнал en обеспечивает расчет одной следующей точки за время отображения текущей. Точки подсвечиваем, если координаты отображаемой точки совпадают с рассчитанными координатами.

assign out = (x == dot_x) & (y == dot_y);

После синтеза проекта на экране я увидел такую картинку.

Линия - первое явление миру.
Линия - первое явление миру.

Линия как будто есть и в тоже время как будто нет. Линия получилась очень бледной. Поиск причин показал, что дело в строке формирования выходного сигнала.

assign out = (x == dot_x) & (y == dot_y);

Сигнал формируется пока не вычисляться координаты следующей точки.  В моем случае это половина времени отображения точки, соответственно яркость линии получается низкой. Для повышения яркости сигнал выхода пришлось сохранять сигнал в триггере и обновлять значение один раз за время отображения пикселя. Для этого я добавил два блока always_ff.

always_ff @(posedge clk)
     pred_x <= x[0];
   assign strobe_x = x ^ pred_x;
      
always_ff @(posedge clk)
   if (strobe_x) line_out <= (x == dot_x) & (y == dot_y); 
   
   assign out = line_out;

В первом always блоке выделяем момент изменения координаты x во втором блоке собственно сохраняем значение выходного сигнала.

Две линии, разница в яркости заметна.
Две линии, разница в яркости заметна.

Как видно не вооруженным глазом яркость линии повысилась.

Следующим этапом разработки была задача отображения линий в секторе 45⁰-90⁰. Очевидно что описанный выше алгоритм не может корректно отображать линии секторе 45⁰-90⁰. Вопрос решается достаточно просто, нужно провести замену осей x и y. То есть на каждом шаге увеличивать y, а x изменять в зависимости от накопленной ошибки. И естественно нужно добавить сигнал выбора режима. Код стал значительно сложнее.

assign s1 = ((x2 - x1) >= (y2 - y1))? 1'b1: 1'b0;        // sertor 0-45
   assign s2 = ((x2 - x1) <  (y2 - y1))? 1'b1: 1'b0;
assign s1 = ((x2 - x1) >= (y2 - y1))? 1'b1: 1'b0;        // sector 45-90     
   assign s2 = ((x2 - x1) <  (y2 - y1))? 1'b1: 1'b0;
    

   always_comb begin
      new_y = dot_y;
      new_x = dot_x;
      new_error = error;
      case ({s2,s1})
      2'b01:               if ((error + dy) > dx) begin
                             new_error = error + dy -  dx;
                             new_y     = dot_y + 1; 
                           end  
                           else 
                             new_error = error + dy;
                           

      2'b10:               if ((error + dx) > dy) begin
                             new_error = error + dx -  dy;
                             new_x     = dot_x + 1; 
                           end  
                           else 
                             new_error = error + dx;
                           
     endcase                            
   end

   always_ff @(posedge clk)
   if (start_frame) begin
     dot_x <= in_x1;
     dot_y <= in_y1;
     error <= '0;
   end 
   else begin
    // sector 1 
    if (en1) begin
       dot_x <= dot_x + 1;
       dot_y <= new_y;
       error <= new_error; 
    end
   // sector 2 
    if (en2) begin
       dot_x <= new_x;
       dot_y <= dot_y + 1 ;
       error <= new_error; 
    end
   end 

Построение линии в секторе 45⁰-90⁰ выполняется аналогично построению в секторе 45⁰-90⁰, с той разницей, что x нужно не увеличивать, а уменьшать при накоплении ошибки. При реализации алгоритма для секторов 45⁰-90⁰ и 90⁰-135⁰ особых проблем не возникло.

А вот построение линии в секторе 135⁰-180⁰ меня сильно озадачило. Я даже думал, что при данных условиях построить линию в диапазоне 135⁰-180⁰. Я не понимал как отображать точки время отображения которых уже прошло.

Даже появилось желание создать машину времени. Было бы замечательно в момент ti вычислять координату следующей точки, переносится в момент ti-1 и ее отображать. Потом вычислять координаты следующей точки переносится в момент ti-2 и так далее. Потихоньку в своих мечтах я дошел до начала строки и тут я испытал чувство которое испытывал Архимед бегая голым по городу и крича "Эврика". Я понял, что мне не нужна машина времени, мне нужны координаты начала и конца горизонтального отрезка который подсветить в текущей строке. А рассчитать эти координаты я могу находясь в предшествующей строке.

Алгоритм такой - введем две переменные x_st и x_end для хранения координат начала и конца отрезка, в начале кадра переменным dot_x и x_st присваиваем значение x1. В строке предшествующей первой строке линии начинаем вычислять error пока не превысим dx при этом не забываем уменьшать dot_x при каждом вычислении. В конце строки значение x_st записываем в x_end, а dot_x в x_st, error уменьшаем на значение dx. Продолжаем пока не дойдем до y2. Реализация алгоритма рисования линий в диапазоне 0⁰-180⁰ на systemverilog выглядит так:

// calculate error and new dot 
   always_comb begin
      new_y = dot_y;
      new_x = dot_x;
      new_error = error;
      case ({s4,s3,s2,s1})
      4'b0001:             if ((error + dy) > dx) begin             // sector 1
                             new_error = error + dy -  dx;
                             new_y     = dot_y + 1; 
                           end  
                           else 
                             new_error = error + dy;
                       
      4'b0010,
      4'b0100:            if ((error + dx) > dy) begin            // sectors 2, 3
                             new_error = error + dx -  dy ;
                             if(s2)
                                new_x     = dot_x + 1;
                             else 
                                new_x     = dot_x - 1;
                           end  
                           else 
                             new_error = error + dx;

    4'b1000:              new_error = error + dy;                // sector 4
                          
     endcase                            
   end

   always_ff @(posedge clk)
     if (start_frame) begin
       dot_x <= in_x1;
       dot_y <= in_y1;
       error <= '0;
       x_end <= in_x1;                                         // for 4 sector 
     end 
     else begin
      // sector 1 
     if (en1) begin
       dot_x <= dot_x + 1;
       dot_y <= new_y;
       error <= new_error; 
      end
     // sectors 2,3 
      if (en2 | en3) begin
        dot_x <= new_x;
        dot_y <= dot_y + 1 ;
        error <= new_error; 
      end
    // sector 4
      if (en4) begin
        if (error <= dx ) begin 
          dot_x <= dot_x - 1;
          error <= new_error;
        end  
        else if (x == screen_width) begin   
         x_st  <= dot_x;
         if (y + 1 == y1  )
           x_end <= x1;
         else 
           x_end <= x_st;  
         error <= error - dx ;      
       end  
    end
   end 

   always @(posedge clk)
   if (strobe_x) line_out <= (x == dot_x) & (y == dot_y); 

Для отображения линий в диапазоне 180⁰-360⁰ достаточно поменять местами начало и конец отрезка и использовать алгоритм. приведенный выше. Код замены приводить не буду, дабы не засорять заметку.

Теперь мы можем рисовать линии в любом направлении, можем нарисовать любую фигуру. например пирамиду.

В место заключения:

  • Приведенная реализация алгоритма просто демонстрация возможности и безусловно может быть улучшена.

  • Простые алгоритмы, типа алгоритма Брезенхэма прекрасно подходят для обучения/ конкурсных заданий, так как могут дать неожиданные ограничения при реализации на FPGA.

  • Я хотел бы поблагодарить организаторов кейс-чемпионат «Код успеха» за проведение мероприятия, за интересные задачи. Надеюсь на повторение.

PS: За рамками заметки остались некоторые технические детали, если есть интерес то код модуля можно взять в моем репозитории .

Комментарии (0)