Превращаем VGA-монитор в «большой OLED-экранчик» с помощью iCEstick

2 апреля 2018

Некоторое время назад я прочитал в блоге hackaday.com заметку о проекте VGA1306. Некто господин Dan O’Shea (@uXeBoy) решил сделать адаптер, преобразующий сигнал к OLED-экранчикам на базе SSD1306 в VGA-сигнал, и использовал для этого FPGA серии ICE40. Просто берем любой существующий код, работающий с крохотными дисплеем 0.96", и выводим его на монитор с диагональю, скажем, 24". В силу пропорций изображения 128x64, без внесения искажений мы сможем использовать только 17.3″ монитора, но все равно очень круто. В общем, идея мне так понравилась, что я решил попробовать повторить проект. К счастью, автор любезно согласился поделиться исходниками.

Работа с VGA-сигналом ранее была подробно рассмотрена в заметке Учим iCEstick передавать видео-сигнал по VGA. Кое-какие подробности об экранчиках на базе чипа SSD1306 и протоколе (точнее, протоколах) работы с ними вы найдете в посте Микроконтроллеры STM32: работа с OLED-экранчиками на базе SSD1306 по I2C и SPI. Если вдруг вы пропустили заметку Знакомимся с iCEstick и полностью открытым ПО для разработки под FPGA, ее я также рекомендовал бы к ознакомлению. Далее предполагается, что вы знакомы с материалом из этих статей.

Так вот, я почитал код проекта VGA1306, и он показался мне… не слишком изящным. Кроме того, он был заточен под плату BlackIce II на базе ICE40HX4K, которой у меня не было. Наконец, по всей видимости, в коде использовались некоторые особенности того, как работает с OLED-экранчиками использованная автором библиотека. С используемой мной библиотекой для STM32 оригинальная конфигурация так просто дружить не хотела. Таким образом, мне предстояло (1) портировать код на iCEstick, (2) научить его работать с используемой мной библиотекой, а также (3) сделать его более читаемым и поддерживаемым для будущих проектов.

В итоге вот что у меня получилось:

/* vim: set ai et ts=4 sw=4: */

`default_nettype none

module top(
    input logic clkin,
    output logic D5,

    output logic vga_r,
    output logic vga_g,
    output logic vga_b,
    output logic vga_hs,
    output logic vga_vs,

    input logic wclk,
    input logic dc,
    input logic din,
    input logic cs
);

logic clk;

// 640x480 @ 60Hz
// 25.125 Mhz, see `icepll -i 12 -o 25`
SB_PLL40_CORE #(
    .FEEDBACK_PATH("SIMPLE"),
    .PLLOUT_SELECT("GENCLK"),
    .DIVR(4'b0000),
    .DIVF(7'b1000010),
    .DIVQ(3'b101),
    .FILTER_RANGE(3'b001),
) uut (
    .REFERENCECLK(clkin),
    .PLLOUTCORE(clk),
    .LOCK(D5), // keep this!
    .RESETB(1'b1),
    .BYPASS(1'b0)
);

parameter addr_width = 13;
logic mem [(1<<addr_width)-1:0];

logic [addr_width-1:0] raddr = 0;
logic [addr_width-1:0] waddr = 0;

always_ff @(posedge wclk) // write memory
begin
    if(cs == 0) // chip select
    begin
        if(dc) // dc = high, accept data
        begin
          mem[waddr] <= din;
          waddr <= waddr + 1;
        end // dc = low, ignore command
    end
    else
        waddr <= 0;
end

parameter h_pulse  = 96;  // h-sunc pulse width
parameter h_bp     = 48;  // back porch pulse width
parameter h_pixels = 640; // number of pixels horizontally
parameter h_fp     = 16;  // front porch pulse width
parameter h_frame  = h_pulse + h_bp + h_pixels + h_fp;

parameter v_pulse  = 2;   // v-sync pulse width
parameter v_bp     = 31;  // back porch pulse width
parameter v_pixels = 480; // number of pixels vertically
parameter v_fp     = 11;  // front porch pulse width
parameter v_frame  = v_pulse + v_bp + v_pixels + v_fp;

parameter border   = 10;
parameter h_offset = (h_pixels - (128*4))/2;
parameter v_offset = (v_pixels - (64*4))/2;

logic [addr_width-1:0] h_pos = 0;
logic [addr_width-1:0] v_pos = 0;

assign vga_hs = (h_pos < h_pixels + h_fp) ? 0 : 1;
assign vga_vs = (v_pos < v_pixels + v_fp) ? 0 : 1;

logic color = 0;
assign vga_r = color;
assign vga_g = color;
assign vga_b = color;

always_ff @(posedge clk) begin
    // update current position
    if(h_pos < h_frame - 1)
        h_pos <= h_pos + 1;
    else
    begin
        h_pos <= 0;
        if(v_pos < v_frame - 1)
            v_pos <= v_pos + 1;
        else
            v_pos <= 0;
    end

    // are we inside centered 512x256 area plus border?
    if((h_pos >= h_offset - border) &&
       (h_pos < (h_pixels - h_offset + border)) &&
       (v_pos >= v_offset - border) &&
       (v_pos < (v_pixels - v_offset + border)))
    begin
        if((h_pos >= h_offset) && (h_pos < h_pixels - h_offset) &&
           (v_pos >= v_offset) && (v_pos < v_pixels - v_offset))
        begin // inside centered area
            color <= mem[raddr];

            // addr = (X + (Y / 8) * 128)*8 + (7 - Y % 8)
            // X = (h_pos - h_offset) >> 2
            // Y = (v_pos - v_offset) >> 2
            raddr <= (
                      (
                       // X
                       ((h_pos - h_offset) >> 2) |
                       (
                          //  Y  div 8
                          ((((v_pos - v_offset) >> 2) >> 3) & 3'b111)
                          // mul 128
                          << 7
                       )
                       // mul 8 (size of byte in bits)
                      ) << 3
                        // + (7 - (Y % 8))
                     ) | (7 - (((v_pos - v_offset) >> 2) & 3'b111));
        end
        else // outside centered area, draw the border
            color <= 1;
    end
    else // everything else is black
        color <= 0;
end

endmodule

Рассмотрим основные части приведенного кода.

Картинка 128x64 хранится в регистре mem:

parameter addr_width = 13;
logic mem [(1<<addr_width)-1:0];

Запись в него происходит забавно:

always_ff @(posedge wclk) // write memory
begin
    if(cs == 0) // chip select
    begin
        if(dc) // dc = high, accept data
        begin
          mem[waddr] <= din;
          waddr <= waddr + 1;
        end // dc = low, ignore command
    end
    else
        waddr <= 0;
end

Здесь используется 4-wire SPI. Но все команды к экранчику, включая команды, задающие адрес страницы, попросту игнорируются. Принимаются лишь данные, которые легко отличить от команд по высокому напряжению на пине DC. То есть, предполагается, что используемая библиотека должна передавать все страницы строго в порядке их очередности. Плюс к этому, если между передачами страниц на время пометить чип, как не выбранный, убрав низкое напряжение с пина CS, текущий адрес waddr обнулится, и фокус не удастся. Это, конечно же, грязный хак. Но он будет работать со многими библиотеками, так как страницы они передают в правильном порядке, и во время передачи не убирают высокое напряжение с CS.

Наконец, остальная часть кода просто выводит изображение по VGA. Как это делается, мы с вами уже знаем. Самым сложным было аккуратно записать формулу, вычисляющую адрес текущего пикселя в регистре mem по текущим координатам h_pos и v_pos на экране.

Для тестирования всего этого дела я использовал плату iCEstick с уже знакомым нам VGA-адаптером от Digilent. Для подачи входного сигнала по протоколу SSD1306 я использовал плату Blue Pill и собственную библиотеку, про которую писал ранее. Кстати, править библиотеку мне вообще не пришлось. В итоге, выглядело это как-то так:

Платы iCEstick и Blue Pill, а также VGA-адаптер от Digilent

А вот и пример выводимой картинки:

Использование VGA-монитора в качестве большого OLED-экранчика

Здесь выполняется процедура ssd1306_TestFPS() (см файл ssd1306_tests.c), которая определяет количество фреймов, передаваемых в единицу времени по используемой шине.

Итак, что же мы имеем в итоге? Повторить проект получилось, хотя завелся он и не с пол пинка. Что же до практической ценности, я думаю, что этот проект стоит держать на вооружении как минимум в обучающих целях. Кроме того, если вы когда-нибудь окажетесь недовольны устройством, использующим слишком маленький OLED-экранчик, теперь вы знаете, что его можно научить выводить изображение на монитор.

Исходники к этому посту вы найдете на GitHub.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.