В этой статье я хочу сделать краткий обзор на плату расширения к китайской плате с ПЛИС. Данная плата хорошо дополняет функционал основной платы EBAZ. В конце статьи будет демонстрация ролика Bad Apple.

Имеется

Когда плата появилась на Алиэкспресс, она стоила 700-900 рублей, затем китайцы поняли, что плату в голом виде использовать не удобно и начали делать различные платы расширения. Я выбрал ту, что на фото выше, о ней и пойдет речь.

На данной плате имеется дисплей ST7789V, buzzer, HDMI, так же 5 кнопок и 3 светодиода. Китайцы не забыли и про USB‑UART.

Отличительной особенностью этой платы от многих других плат с ПЛИС является то, что на ней нанесены надписи к каким пинам ПЛИС подключается тот или иной модуль. Например, если присмотреться к кнопкам и светодиодам, возле каждого элемента имеется характерная надпись (Например LED1_E19 или K1_T19. E19 и T19 — есть пины ПЛИС, которые нужно указать при ее программировании).

Также они не забыли подписать 20-пиновую гребенку контактов на обратной стороне. Кроме подписанных пинов можно обратить внимание на место пайки шлейфа дисплея — первые два контакта спаяны вместе. Один из контактов является сигналом подсветки, а второй GND, таким образом подсветка всегда включена. Я на своей плате это соединение убрал и подсветкой управляю непосредственно с ПЛИС.

Недостатки/достоинства. Из недостатков хотел бы обратить внимание на два момента. Во‑первых, на плате нет кнопки питания, и в случае, если ее нужно перезагрузить либо сбросить прошивку, приходится дергать разъем USB. Во‑вторых, плата расширения перекрывает 6-пиновый разъем питания, но в целом без него все работает, поэтому у меня претензий нет, но может кому‑то пригодилось бы. Из достоинств так же есть два момента. Во‑первых — размер, есть аналоги которые имеют гораздо большую площадь, а функционал либо хуже либо такой же. Во‑вторых, питание и USB‑UART объединены в один интерфейс, а это уменьшает число проводов.

Кому интересно тут разные варианты плат расширения:

HDMI. Внимание так же привлекает HDMI порт. Никакой дополнительной микросхемы для этого порта не предусмотрено, а это значит, что все сигналы нужно полностью формировать в ПЛИС. Китайцы и тут позаботились о разработчиках и вместе документацией поставляют IP-ядро, которое конвертирует привычный нам VGA в HDMI. Как я понял, это IP-ядро было выдернуто с одной из отладочных плат Digilent.

IP-ядро достаточно простое само по себе.
IP-ядро достаточно простое само по себе.

Теперь самая интересная часть. Работа с дисплеем ST7789.

Наверняка некоторые читают статью ради того, чтобы посмотреть ролик BadApple, поэтому не будем тянуть дисплей за пиксель и приступим.)

Цветовые режимы. Дисплей представляет из себя экран на 240х240 пикселей, с различными цветовыми режимами работы RGB 4:4:4, RGB 5:6:5, RGB 6:6:6. Я реализовал два последних режима работы, они удобнее в реализации, а также режим 4:4:4 мне не интересен, так как имеет небольшой цветовой диапазон.

Частота тактирования. В документации сказано что минимальный период тактирующего сигнала (SCL) составляет 66 нс, что соответствует 15.151 МГц. На просторах интернета я натыкался на высказывания, что предел частоты данного дисплея 50 МГц, я ставил частоту порядка 100 МГц и дисплей справлялся. В итоге в своем проекте поставил частоту 50 МГц и, как будет видно ниже, все работает.

Область отрисовки. Дисплей предусматривает возможность рисовать в любой области дисплея. Так же можно переключаться на ходу. Это позволяет не только выводить картинку целиком на экран, но и выводить отдельно символы, так же можно добиться вывода только одного пикселя.

Примеры рисования на дисплее:
Демонстрация возможности вывода небольших областей. В данном случае сначала был выведен фон из 4х полос, затем поверху выведены небольшие квадратики с буквой А. Это было выполнено за счет изменения области отрисовки.
Демонстрация возможности вывода небольших областей. В данном случае сначала был выведен фон из 4х полос, затем поверху выведены небольшие квадратики с буквой А. Это было выполнено за счет изменения области отрисовки.

Вывод любой картинки на дисплей с ПК. Картинка передавалась по UART.
Вывод любой картинки на дисплей с ПК. Картинка передавалась по UART.

Простой градиент каналов RGB
Простой градиент каналов RGB

Модуль для работы с LCD дисплеем.

В модуль я заложил три этапа работы. Этап инициализации, этап установки области отрисовки, этап передачи данных (значения пикселей).

Обобщенная структурная схема модуля
Обобщенная структурная схема модуля

Этап инициализации запускается автоматически после сигнала сброса. В рамках инициализации происходит выставление сигнала сброса для дисплея (RES), затем выжидается время, за которое дисплей приходит в себя. В документации указаны различные тайминги после сигнала сброса, когда можно писать в регистры и когда можно включать дисплей, я поступил хитрым образом — просто после сигнала сброса выждал 500 мс и начал инициализацию внутренних регистров дисплея.

Вот список адресов, которые я инициализировал (список адресов указан в порядке их инициализации): MADCTL, COLMOD, PORCTRL, GCTRL, VCOMS, LCMCTRL, VDVVRHEN, VRHS, VDVS, FRCTRL, PWCTRL1, PVGAMCTRL, NVGAMCTRL, INVON, SLPOUT, DISPON.

Этот список адресов я брал из китайского примера. В ходе экспериментов я отключил PVGAMCTRL, NVGAMCTRL и PORCTRL. Наверняка можно отключить и еще большинство регистров, но раз работает, то трогать не стал.

Этап установки области отрисовки заключается в передаче двух координат, начала и конца прямоугольника. Они пишутся в регистры CASET и RASET. Обязательным условием является сброс указателя текущего пикселя, это регистр RAMWR. После этой команды дисплей ждет массив пикселей.

Этап передачи данных. Смысл этого этапа очевиден, после настройки дисплея и выставления области отрисовки, нужно передать значения пикселей в том формате, в котором выбрали. Для SPI это будут либо 2 байта, либо 3 байта, в зависимости от выбранного цветового диапазона. Для входного интерфейса модуля — это 3 байта(RGB) и сигнал валида, тут нужно учесть, что в зависимости от выбранного режима будет использованы разное количество бит из входных данных.

Вот код модуля на SystemVerilog:

Чтобы применить код, просто скопируйте его в отдельные файлы (1 модуль = 1 файл), импортируйте к себе в проект и смотрите результат. По сигналу ready_RGB можно начинать передавать данные на дисплей. Если нужно указать другую область вывода на дисплее, то нужно воспользоваться сигналами: set_size_img_en_i, set_new_XY_i, X1, X2, Y1, Y2. set_size_img_en_i – поднимается на 1 такт и говорит модулю чтобы тот запустил процесс изменения области отрисовки. set_new_XY_i — если нужно задать новые размеры области отрисовки, если оставить в нуле то размер не изменится, только сбросится указатель текущего пикселя к нулевым координатам. X1, X2, Y1, Y2 — используются для задания координат двух точек прямоугольника, начала и конца. Так же нужно учитывать что частота сигнала SCL всегда будет меньше в 2 раза чем частота работы модуля(если clk == 50МГц то SCL == 25МГц).

module top_LCD
`timescale 1ns / 1ps



module top_LCD
#(
    parameter FREQ_CLK = 50000000, 
    parameter PIXEL_MODE = 8'h06   // 'h03 - 4:4:4, 'h05 - 5:6:5, 'h06 - 6:6:6;
)(
    input clk,
    input rst,
    
    input set_size_img_en_i,
    input set_new_XY_i,
    input [15:0] X1,
    input [15:0] X2,
    input [15:0] Y1,
    input [15:0] Y2,
    
    input valid_RGB,
    output ready_RGB,
    input [7:0] R,
    input [7:0] G,
    input [7:0] B,
    
    output reg BL = 1,  // display backlight
    output DC,  // data strobe: command or data. "0" - cmd, "1" - data
    output SCL, // clk SPI
    output SDA, // data SPI
    output nRES  // reset LCD. "0" - reset, "1" - normal work
    );
    
    
logic done_init,done_set_img;    

logic DC_init,SCL_init,SDA_init;
logic [15:0] X1_reg = 0, X2_reg = 239, Y1_reg = 0, Y2_reg = 239;
logic set_new_XY = 0, set_size_img_en = 0;
logic DC_set_img,SCL_set_img,SDA_set_img;

logic ready_RGB_s,valid_RGB_s;
logic DC_rgb,SCL_rgb,SDA_rgb;

logic [3:0] state_lcd = 0;

always_ff@(posedge clk)
begin
    if(rst) begin
        state_lcd <= 'd0;
        {X1_reg, X2_reg, Y1_reg, Y2_reg} <= {16'd0,16'd239,16'd0,16'd239};
    end else begin
        case(state_lcd)
            0: begin //wait block init
                if(done_init) begin
                    state_lcd <= 'd1;
                    set_size_img_en <= 'd1;
                end
            end
            1: begin // wait block set size image
                if(set_size_img_en) begin
                    set_size_img_en <= 'd0;
                    set_new_XY <= 'd0;
                end else if (done_set_img) begin
                    state_lcd <= 'd2;
                end
            end
            2: begin // normal work
                if(set_size_img_en_i) begin//waiting set new image size
                     if(set_new_XY_i) begin
                        {X1_reg, X2_reg, Y1_reg, Y2_reg} <= {X1,X2,Y1,Y2};
                        set_new_XY <= 'd1;
                     end                     
                     state_lcd <= 'd3;
                end
            end
            3: begin
                if(ready_RGB_s) begin
                    set_size_img_en <= 'd1;
                    state_lcd <= 'd1;
                end
            end
        endcase
    end
end

       
init_LCD
#(
    .FREQ_CLK(FREQ_CLK),
    .PIXEL_MODE(PIXEL_MODE)
)
init_LCD_inst 
(
    .clk (clk),
    .rst (rst),
    .start_init('d0),
    .done(done_init),
    .DC  (DC_init),
    .SCL (SCL_init),
    .SDA (SDA_init),
    .nRES(nRES)
);  
 

set_img_size
set_img_size
(
    .clk(clk),
    .set_size_img_en (set_size_img_en),
    .set_new_XY      (set_new_XY),
    .X1     (X1_reg),
    .X2     (X2_reg),
    .Y1     (Y1_reg),
    .Y2     (Y2_reg),
    .done   (done_set_img),
    
    .DC  (DC_set_img ),
    .SCL (SCL_set_img),
    .SDA (SDA_set_img)
);


RGB_transmit
#(
    .PIXEL_MODE(PIXEL_MODE) // 'h03 - 4:4:4 (not supported here), 'h05 - 5:6:5, 'h06 - 6:6:6; 
)
RGB_transmit_inst
(
    .clk(clk),
    .valid_RGB(valid_RGB_s),
    .ready    (ready_RGB_s),
    .R(R),
    .G(G),
    .B(B),
    
    .DC (DC_rgb),
    .SCL(SCL_rgb),
    .SDA(SDA_rgb)
);

assign valid_RGB_s = (state_lcd == 'd2) ? valid_RGB : 'd0;
assign ready_RGB   = (state_lcd == 'd2) ? ready_RGB_s : 'd0;


assign  DC    = (state_lcd == 'd0) ? DC_init : 
                (state_lcd == 'd1) ? DC_set_img :
                (state_lcd == 'd2) ? DC_rgb :
                'd0;

assign  SCL   = (state_lcd == 'd0) ? SCL_init: 
                (state_lcd == 'd1) ? SCL_set_img:
                (state_lcd == 'd2) ? SCL_rgb:
                'd1;
                
assign  SDA   = (state_lcd == 'd0) ? SDA_init: 
                (state_lcd == 'd1) ? SDA_set_img:
                (state_lcd == 'd2) ? SDA_rgb:
                'd0;

endmodule

module init_LCD
`timescale 1ns / 1ps


module init_LCD
#(
    parameter FREQ_CLK = 50000000, 
    parameter PIXEL_MODE = 8'h06   // 'h03 - 4:4:4, 'h05 - 5:6:5, 'h06 - 6:6:6;
)
(
    input clk,
    input rst,
    
    input start_init,
    output logic done = 0,
    
    output DC,  // data strobe: command or data. "0" - cmd, "1" - data
    output SCL, // clk SPI
    output SDA, // data SPI
    output reg nRES = 0  // reset LCD. "0" - reset, "1" - normal work
    );

// display color correction. adress command 'hE0, 'hE1(PVGAMCTRL,NVGAMCTRL). "FALSE" - off setting state.
localparam COLOR_CORRECT = "FALSE"; 
localparam SETTING_PORCTRL = "FALSE";//adress command 'hB2 //"FALSE" - off setting state.
    
logic [7:0] send_data = 0;
logic send_valid = 0;    
logic send_ready;
logic send_cd = 0;

logic [7:0] state_init = 0;

logic [31:0] rst_LCD = 0;
logic [3:0] ch_reg = 0;
logic [3:0] ch_reg_1 = 0;
logic [3:0] ch_reg_2 = 0;

logic [7:0] MADCTL [0:1];
logic [7:0] COLMOD [0:1];
logic [7:0] PORCTRL [0:5];
logic [7:0] GCTRL [0:1];
logic [7:0] VCOMS [0:1];
logic [7:0] LCMCTRL [0:1];
logic [7:0] VDVVRHEN [0:1];
logic [7:0] VRHS [0:1];
logic [7:0] VDVS [0:1];
logic [7:0] FRCTRL [0:1];
logic [7:0] PWCTRL1 [0:2];
logic [7:0] PVGAMCTRL [0:14];
logic [7:0] NVGAMCTRL [0:14];
logic [7:0] START_WORK [0:2];

initial begin

// cmd and params MADCTL
MADCTL[0] = 'h36;
MADCTL[1] = 'h00;

//RGB format 
COLMOD[0] = 'h3A;
COLMOD[1] = PIXEL_MODE; //interface pixel format(RGB) 'h03 - 4:4:4, 'h05 - 5:6:5, 'h06 - 6:6:6;

//setting porch
PORCTRL[0] = 'hB2;
PORCTRL[1] = 'h0C;
PORCTRL[2] = 'h0C;
PORCTRL[3] = 'h00;
PORCTRL[4] = 'h33;
PORCTRL[5] = 'h33;

//GCTRL. Gate Control
GCTRL[0] = 'hB7;
GCTRL[1] = 'h35; 

//VCOMS. VCOM Setting
VCOMS[0] = 'hBB;
VCOMS[1] = 'h19;

//LCMCTRL. LCM Control
LCMCTRL[0] = 'hC0;
LCMCTRL[1] = 'h2C;

//VDVVRHEN. VDV and VRH Command Enable
VDVVRHEN[0] = 'hC2;
VDVVRHEN[1] = 'h01;

//VRHS. VRH Set
VRHS[0] = 'hC3;
VRHS[1] = 'h12;

//VDVS. VDV Set
VDVS[0] = 'hC4;
VDVS[1] = 'h20;

//FRCTRL. Frame Rate Control in Normal Mode
FRCTRL[0] = 'hC6;
FRCTRL[1] = 'h0F; // 60Hz

//PWCTRL1. Power Control 1
PWCTRL1[0] = 'hD0;
PWCTRL1[1] = 'hA4; // const
PWCTRL1[2] = 'hA1;

//PVGAMCTRL. Positive Voltage Gamma Control
PVGAMCTRL[0] = 'hE0 ;
PVGAMCTRL[1] = 'hD0 ;
PVGAMCTRL[2] = 'h04 ;
PVGAMCTRL[3] = 'h0D ;
PVGAMCTRL[4] = 'h11 ;
PVGAMCTRL[5] = 'h13 ;
PVGAMCTRL[6] = 'h2B ;
PVGAMCTRL[7] = 'h3F ;
PVGAMCTRL[8] = 'h54 ;
PVGAMCTRL[9] = 'h4C ;
PVGAMCTRL[10] = 'h18 ;
PVGAMCTRL[11] = 'h0D ;
PVGAMCTRL[12] = 'h0B ;
PVGAMCTRL[13] = 'h1F ;
PVGAMCTRL[14] = 'h23 ;

//NVGAMCTRL. Negative Voltage Gamma Control
NVGAMCTRL[0] = 'hE1 ;
NVGAMCTRL[1] = 'hD0 ;
NVGAMCTRL[2] = 'h04 ;
NVGAMCTRL[3] = 'h0C ;
NVGAMCTRL[4] = 'h11 ;
NVGAMCTRL[5] = 'h13 ;
NVGAMCTRL[6] = 'h2C ;
NVGAMCTRL[7] = 'h3F ;
NVGAMCTRL[8] = 'h44 ;
NVGAMCTRL[9] = 'h51 ;
NVGAMCTRL[10] = 'h2F ;
NVGAMCTRL[11] = 'h1F ;
NVGAMCTRL[12] = 'h1F ;
NVGAMCTRL[13] = 'h20 ;
NVGAMCTRL[14] = 'h23 ;

// last commands to work.
START_WORK[0] = 'h21; // INVON, turns on inversion
START_WORK[1] = 'h11; // SLPOUT, turn off sleep mode
START_WORK[2] = 'h29; // DISPON, display on


end


always_ff@(posedge clk)
begin
    if(rst) begin
        state_init <= 'd0;
        nRES <= 'd0;
        rst_LCD <= 'd0;
        done <= 'd0;
        ch_reg <= 'd0;
        ch_reg_1 <= 'd0;
        ch_reg_2 <= 'd0;
    end else begin
        case(state_init)
            0: begin // reset LCD. iter 0
                nRES <= 'd1;
                if(rst_LCD >= FREQ_CLK) begin
                    rst_LCD <= 'd0;
                    state_init <= 1;
                end else rst_LCD <= rst_LCD + 1;
            end
            1: begin // reset LCD. iter 1                
                
                if(rst_LCD == 'd0) begin// reset on
                    nRES <= 'd0;
                end else if(rst_LCD >= (2*FREQ_CLK)) begin //wait delay
                    state_init <= 2;
                end else if(rst_LCD >= FREQ_CLK) begin //reset off
                    nRES <= 'd1;
                end
                rst_LCD <= rst_LCD + 1;
            end
            2: begin //send cmd MADCTL ('h36), data 8'h00; see datasheet page 212.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= MADCTL[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 3;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end               
            end
            3: begin //send cmd COLMOD ('h3A), data 8'h03/8'h05/8'h06; see datasheet page 221.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= COLMOD[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        if(SETTING_PORCTRL == "FALSE") state_init <= 5;
                        else state_init <= 4;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end
            4: begin //send cmd PORCTRL ('hB2), 5 bytes data; see datasheet page 260.
                if(SETTING_PORCTRL != "FALSE") begin
                    send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                    if(send_ready & (!send_valid)) begin
                        send_data <= PORCTRL[ch_reg];
                        send_valid <= 'd1;
                        if(ch_reg == 5) begin
                            state_init <= 5;
                            ch_reg <= 'd0;
                        end else ch_reg <= ch_reg + 1;
                    end else begin
                        send_valid <= 'd0;
                    end 
                end else state_init <= 0;
            end   
            5: begin //send cmd GCTRL ('hB7), data 'h35; see datasheet page 263.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= GCTRL[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 6;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end  
            6: begin //send cmd VCOMS ('hBB), data 'h19; see datasheet page 266.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= VCOMS[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 7;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end   
            7: begin //send cmd LCMCTRL ('hC0), data 'h2C; see datasheet page 268.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= LCMCTRL[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 8;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end 
            8: begin //send cmd VDVVRHEN ('hC2), data 'h01; see datasheet page 270.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= VDVVRHEN[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 9;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end 
            9: begin //send cmd VRHS ('hC3), data 'h12; see datasheet page 271.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= VRHS[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 10;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end   
            10: begin //send cmd VDVS ('hC4), data 'h20; see datasheet page 273.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= VDVS[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 11;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end  
            11: begin //send cmd FRCTRL ('hC6), data 'h0F; see datasheet page 277.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= FRCTRL[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 1) begin
                        state_init <= 12;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end    
            12: begin //send cmd PWCTRL1 ('hD0), 2 bytes data; see datasheet page 283.
                send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
                if(send_ready & (!send_valid)) begin
                    send_data <= PWCTRL1[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 2) begin
                        if(COLOR_CORRECT == "FALSE") state_init <= 15;
                        else state_init <= 13;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end  
            13: begin //send cmd PVGAMCTRL ('hE0), 14 bytes data; see datasheet page 287.
                if(COLOR_CORRECT != "FALSE") begin
                    send_cd <= (ch_reg_1 == 'd0) ? 'd0 : 'd1;
                    if(send_ready & (!send_valid)) begin
                        send_data <= PVGAMCTRL[ch_reg_1];
                        send_valid <= 'd1;
                        if(ch_reg_1 == 14) begin
                            state_init <= 14;
                            ch_reg_1 <= 'd0;
                        end else ch_reg_1 <= ch_reg_1 + 1;
                    end else begin
                        send_valid <= 'd0;
                    end  
                end else state_init <= 0;
            end  
            14: begin //send cmd NVGAMCTRL ('hE1), 14 bytes data; see datasheet page 289.
                if(COLOR_CORRECT != "FALSE") begin
                    send_cd <= (ch_reg_2 == 'd0) ? 'd0 : 'd1;
                    if(send_ready & (!send_valid)) begin
                        send_data <= NVGAMCTRL[ch_reg_2];
                        send_valid <= 'd1;
                        if(ch_reg_2 == 14) begin
                            state_init <= 15;
                            ch_reg_2 <= 'd0;
                        end else ch_reg_2 <= ch_reg_2 + 1;
                    end else begin
                        send_valid <= 'd0;
                    end 
                end else state_init <= 0;
            end    
            15: begin //send cmd INVON ('h21), SLPOUT('h11), DISPON('h29). 0 bytes data; see datasheet page 187, 181, 193.
                send_cd <= 'd0; 
                if(send_ready & (!send_valid)) begin
                    send_data <= START_WORK[ch_reg];
                    send_valid <= 'd1;
                    if(ch_reg == 2) begin
                        state_init <= 16;
                        ch_reg <= 'd0;
                    end else ch_reg <= ch_reg + 1;
                end else begin
                    send_valid <= 'd0;
                end  
            end
            16: begin//idle state. and waiting for start_init signal
                send_valid <= 'd0;
                if(start_init) begin
                    state_init <= 0;
                    done <= 'd0;
                    rst_LCD <= 'd0;
                end else if(send_ready & (!send_valid)) begin
                    done <= 'd1;
                end                                
            end                                                                                                        
        endcase
    end
end


    
    
send_byte
send_byte_init
(
    .clk(clk),
    
    .cmd_data (send_cd),
    .data  (send_data ),
    .valid (send_valid),
    .ready (send_ready),
    
    .DC  (DC ),  
    .SCL (SCL), 
    .SDA (SDA) 
);    
endmodule

module set_img_size
`timescale 1ns / 1ps


module set_img_size(
    input clk,
    
    input set_size_img_en,
    input set_new_XY,
    input [15:0] X1,
    input [15:0] X2,
    input [15:0] Y1,
    input [15:0] Y2,
    
    output reg done = 1,
    
    
    output DC,  // data strobe: command or data. "0" - cmd, "1" - data
    output SCL, // clk SPI
    output SDA // data SPI

    );

logic [7:0] send_data = 0;
logic send_valid = 0;    
logic send_ready;
logic send_cd = 0;  

logic [3:0] ch_reg = 0;  
logic [3:0] state_set_img = 0;

logic [7:0] CASET[0:4];
logic [7:0] RASET[0:4];

initial
begin

//CASET
CASET[0] = 'h2A;
CASET[1] = 'h00; //H // X1 == 0
CASET[2] = 'h00; //L
CASET[3] = 'h00; //H // X2 == 239
CASET[4] = 'hEF; //L

//RASET
RASET[0] = 'h2B;
RASET[1] = 'h00;
RASET[2] = 'h00;
RASET[3] = 'h00;
RASET[4] = 'hEF;
end

always_ff@(posedge clk)
begin    
    case(state_set_img)
        0: begin //wait set_size_img_en
            if(set_size_img_en) begin
                done <= 'd0;
                state_set_img <= 'd1;
                if(set_new_XY) begin
                    CASET[1] <= X1[15:8]; //H 
                    CASET[2] <= X1[7:0]; //L
                    CASET[3] <= X2[15:8]; //H 
                    CASET[4] <= X2[7:0]; //L
                    
                    RASET[1] <= Y1[15:8]; //H 
                    RASET[2] <= Y1[7:0]; //L  
                    RASET[3] <= Y2[15:8]; //H 
                    RASET[4] <= Y2[7:0]; //L                      
                end
            end
        end
        1: begin //send cmd CASET ('h2A), 4 bytes data; see datasheet page 195.
            send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
            if(send_ready & (!send_valid)) begin
                send_data <= CASET[ch_reg];
                send_valid <= 'd1;
                if(ch_reg == 4) begin
                    state_set_img <= 2;
                    ch_reg <= 'd0;
                end else ch_reg <= ch_reg + 1;
            end else begin
                send_valid <= 'd0;
            end               
        end
        2: begin //send cmd RASET ('h2B), 4 bytes data; see datasheet page 197.
            send_cd <= (ch_reg == 'd0) ? 'd0 : 'd1;
            if(send_ready & (!send_valid)) begin
                send_data <= RASET[ch_reg];
                send_valid <= 'd1;
                if(ch_reg == 4) begin
                    state_set_img <= 3;
                    ch_reg <= 'd0;
                end else ch_reg <= ch_reg + 1;
            end else begin
                send_valid <= 'd0;
            end               
        end  
        3: begin //send cmd RAMWR ('h2C). see datasheet page 199. for set cursot position to (0,0)
            send_cd <= 'd0; //(ch_reg == 'd0) ? 'd0 : 'd1;
            if(send_ready & (!send_valid)) begin
                send_data <= 'h2C;
                send_valid <= 'd1;
                state_set_img <= 4;
            end else begin
                send_valid <= 'd0;
            end 
        end  
        4: begin//wait end transmit spi
            send_valid <= 'd0;
            if(send_ready & (!send_valid)) begin
                done <= 'd1;            
                state_set_img <= 0;
            end
        end
    endcase   
end 
    
    
send_byte
send_byte_set_img
(
    .clk(clk),
    
    .cmd_data (send_cd),
    .data  (send_data ),
    .valid (send_valid),
    .ready (send_ready),
    
    .DC  (DC ),  
    .SCL (SCL), 
    .SDA (SDA) 
);      
endmodule

module RGB_transmit
`timescale 1ns / 1ps


module RGB_transmit
#(
    parameter PIXEL_MODE = 'h06 // 'h03 - 4:4:4 (not supported here), 'h05 - 5:6:5, 'h06 - 6:6:6; 
)(
    input clk,
    
    input valid_RGB,
    output reg ready = 0,
    input [7:0] R,
    input [7:0] G,
    input [7:0] B,
    
    output DC,  // data strobe: command or data. "0" - cmd, "1" - data
    output SCL, // clk SPI
    output SDA // data SPI

    );
    
logic [7:0] send_data = 0;
logic send_valid = 0;    
logic send_ready;
logic send_cd = 1;  

logic [7:0] bytes_send [0:2];

logic [3:0] state_RGB = 0;

always_ff@(posedge clk)
begin
    case(state_RGB)
        0: begin //wait valid data
            if(valid_RGB & ready) begin
                state_RGB <= 'd1;
                if(PIXEL_MODE == 'h05) begin
                    bytes_send[0] <= {R[4:0],G[5:3]};
                    bytes_send[1] <= {G[2:0],B[4:0]};
                end else if(PIXEL_MODE == 'h06) begin
                    bytes_send[0] <= {R,2'd0};
                    bytes_send[1] <= {G,2'd0};
                    bytes_send[2] <= {B,2'd0};
                end
                ready <= 'd0;
            end else begin
                ready <= 'd1;
            end
            send_valid <= 'd0;
        end
        1: begin
            if(send_ready & (!send_valid)) begin
                send_data <= bytes_send[0];
                send_valid <= 'd1;
                state_RGB <= 2;
            end else begin
                send_valid <= 'd0;
            end 
        end
        2: begin 
            if(send_ready & (!send_valid)) begin
                send_data <= bytes_send[1];
                send_valid <= 'd1;
                if(PIXEL_MODE == 'h05) begin
                    state_RGB <= 0;
                    ready <= 'd1;
                end else if (PIXEL_MODE == 'h06) state_RGB <= 3;
            end else begin
                send_valid <= 'd0;
            end 
        end  
        3: begin
            if(send_ready & (!send_valid)) begin
                send_data <= bytes_send[2];
                send_valid <= 'd1;
                state_RGB <= 0;
                ready <= 'd1;
            end else begin
                send_valid <= 'd0;
            end 
        end       
    endcase
end
    
    
send_byte
send_byte_RGB
(
    .clk(clk),
    
    .cmd_data (send_cd),
    .data  (send_data ),
    .valid (send_valid),
    .ready (send_ready),
    
    .DC  (DC ),  
    .SCL (SCL), 
    .SDA (SDA) 
);        
endmodule

module send_byte
`timescale 1ns / 1ps

module send_byte(
    input clk,
    input cmd_data, // cmd - '0', data - '1'
    input [7:0] data,
    input valid,
    output reg ready = 1,
    
    output reg DC  = 0, // data strobe: command or data. "0" - cmd, "1" - data
    output reg SCL = 1, // clk SPI
    output reg SDA = 0  // data SPI
    );
        
logic [7:0] buf_data_send = 0;   
logic [2:0] state = 0;
logic [3:0] cnt_bits = 0;
     

always_ff@(posedge clk)
begin
    case(state)
    0: begin // wait byte to send
        if(valid & ready) begin
            buf_data_send <= data;
            DC <= cmd_data;
            ready <= 'd0;
            state <= 'd1;            
        end else begin
            ready <= 'd1;
        end        
    end
    1: begin // send byte
        if(SCL == 1) begin //set SCL=0, need set data to SDA
            SDA <= buf_data_send[7];
            buf_data_send <= {buf_data_send,1'b0};
            SCL <= 'd0;
        end else begin
            SCL <= 'd1;
            if(cnt_bits == 'd7) begin
                state <= 'd0;
                cnt_bits <= 'd0;
            end else cnt_bits <= cnt_bits + 1;
        end
    end
    endcase    
end    
    
endmodule

Ролик Bad Apple

Основной проблемой запуска ролика был его размер. Нужно было сохранить 189_273_600 пикселей, ситуацию упрощало (но не полностью), что ролик черно-белый, даже двухцветный. Этот массив пикселей я обработал и упаковал в одномерный массив, который подцепил затем в SDK при программировании Zynq. Файл получился тяжелым, порядка 43 Мбайт (не забывайте о форматировании массива, чтобы синтаксис языка пройти).

Также на помощь пришла ОЗУ процессора и DMA. Zynq занимался тем, что говорил DMA отправлять в модуль данные из ОЗУ, а так же Zynq предоставил возможность легко поднять интерфейс DDR. Грузил я эту всю прошивку через программатор, прямиком в DDR, это заняло 5–6 минут чистого времени.

Давайте посмотрим, что получилось:

Комментарии к ролику. Я посчитал что будет скучно смотреть на один только дисплей, поэтому добавил музыкальное сопровождение. Ну и парой строк кода сделал мини эквалайзер из светодиодов, они загораются в зависимости от частоты звучания мелодии. Также выполнена плохая синхронизация звука и мелодии, особенно это заметно в конце когда музыка закончилась, а ролик воспроизводится дальше.

Используемые источники
  1. https://github.com/MasterPlayer/lcd-st7789-sv/tree/main  — еще один вариант реализации

  2. https://pdf1.alldatasheet.com/datasheet-pdf/download/1132511/SITRONIX/ST7789V.html — даташит на дисплей

  3. https://github.com/swindlesmccoop/bad-apple-arduino/blob/master/badapple/melody.h — тут я взял мелодию BadAplle, товарищ использовал 5 каналов для воспроизведения, я выбрал самую подходящую и импортировал к себе. Кратное пояснение: в переменных d1,d2,d3… хранятся значения времени в миллисекундах от начала запуска, это время показывает когда нужно переключиться на следующую частоту звучания. В переменных m1,m2,m3… хранятся частоты в герцах, которые нужно сгенерировать на buzzer.

  4. https://wx.mail.qq.com/ftn/download?func=3&k=ce9c50661c249426fcef1c6635323839015f006737323839144c4a120104590d05551f0706530c1403570a521a505d5d064e0752550b590f550102550f5129397421733c0300080c6e075312561c4a5843775b8ed2ba8d0dcad41eda98efd476f20825d116c2&key=ce9c50661c249426fcef1c6635323839015f006737323839144c4a120104590d05551f0706530c1403570a521a505d5d064e0752550b590f550102550f5129397421733c0300080c6e075312561c4a5843775b8ed2ba8d0dcad41eda98efd476f20825d116c2&code=1c2f7289&from=  — ссылка на скачивание архива документов что поставляют китайцы.

  5. https://www.youtube.com/@mihas6705 — хочу порекомендовать этот канал, там товарищ взял аналогичную плату за 700 рублей с авито.

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


  1. KurtkaBeyn
    12.07.2024 17:31

    Спасибо за такой подробный материал, таких статей побольше бы на хабре


  1. mkevac
    12.07.2024 17:31

    Для чего можно использовать эту плату?


    1. yamifa_1234 Автор
      12.07.2024 17:31

      В первую очередь для обучения. Плата и сама ПЛИС не выделяются особыми характеристиками, но обладают основным функционалом как у более мощных плис этого семейства(zynq7000). Например существуют такие отладочные платы как ZedBoard, они позволяют подключить мезонинный модуль с АЦП/ЦАП и реализовать ЦОС.


  1. checkpoint
    12.07.2024 17:31
    +1

    За Bad Apple спасибо!

    Для ценителей настоящего пиксель арта ниже мой вариант Bad Apple на ПЛИС Lattice ECP5.

    Bad apple на ПЛИС Lattice ECP5


    1. yamifa_1234 Автор
      12.07.2024 17:31

      Здорово! Я вижу у вас больше двух цветов получилось, это какая-то специальная обработка ?


      1. checkpoint
        12.07.2024 17:31

        Это артефакты загразки в сдвиговые регистры. :)