В этой статье я хочу сделать краткий обзор на плату расширения к китайской плате с ПЛИС. Данная плата хорошо дополняет функционал основной платы 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.
Теперь самая интересная часть. Работа с дисплеем 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 МГц и, как будет видно ниже, все работает.
Область отрисовки. Дисплей предусматривает возможность рисовать в любой области дисплея. Так же можно переключаться на ходу. Это позволяет не только выводить картинку целиком на экран, но и выводить отдельно символы, так же можно добиться вывода только одного пикселя.
Примеры рисования на дисплее:
Модуль для работы с 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 минут чистого времени.
Давайте посмотрим, что получилось:
Комментарии к ролику. Я посчитал что будет скучно смотреть на один только дисплей, поэтому добавил музыкальное сопровождение. Ну и парой строк кода сделал мини эквалайзер из светодиодов, они загораются в зависимости от частоты звучания мелодии. Также выполнена плохая синхронизация звука и мелодии, особенно это заметно в конце когда музыка закончилась, а ролик воспроизводится дальше.
Используемые источники
https://github.com/MasterPlayer/lcd-st7789-sv/tree/main — еще один вариант реализации
https://pdf1.alldatasheet.com/datasheet-pdf/download/1132511/SITRONIX/ST7789V.html — даташит на дисплей
https://github.com/swindlesmccoop/bad-apple-arduino/blob/master/badapple/melody.h — тут я взял мелодию BadAplle, товарищ использовал 5 каналов для воспроизведения, я выбрал самую подходящую и импортировал к себе. Кратное пояснение: в переменных d1,d2,d3… хранятся значения времени в миллисекундах от начала запуска, это время показывает когда нужно переключиться на следующую частоту звучания. В переменных m1,m2,m3… хранятся частоты в герцах, которые нужно сгенерировать на buzzer.
https://wx.mail.qq.com/ftn/download?func=3&k=ce9c50661c249426fcef1c6635323839015f006737323839144c4a120104590d05551f0706530c1403570a521a505d5d064e0752550b590f550102550f5129397421733c0300080c6e075312561c4a5843775b8ed2ba8d0dcad41eda98efd476f20825d116c2&key=ce9c50661c249426fcef1c6635323839015f006737323839144c4a120104590d05551f0706530c1403570a521a505d5d064e0752550b590f550102550f5129397421733c0300080c6e075312561c4a5843775b8ed2ba8d0dcad41eda98efd476f20825d116c2&code=1c2f7289&from= — ссылка на скачивание архива документов что поставляют китайцы.
https://www.youtube.com/@mihas6705 — хочу порекомендовать этот канал, там товарищ взял аналогичную плату за 700 рублей с авито.
Комментарии (6)
mkevac
12.07.2024 17:31Для чего можно использовать эту плату?
yamifa_1234 Автор
12.07.2024 17:31В первую очередь для обучения. Плата и сама ПЛИС не выделяются особыми характеристиками, но обладают основным функционалом как у более мощных плис этого семейства(zynq7000). Например существуют такие отладочные платы как ZedBoard, они позволяют подключить мезонинный модуль с АЦП/ЦАП и реализовать ЦОС.
checkpoint
12.07.2024 17:31+1За Bad Apple спасибо!
Для ценителей настоящего пиксель арта ниже мой вариант Bad Apple на ПЛИС Lattice ECP5.
Bad apple на ПЛИС Lattice ECP5
yamifa_1234 Автор
12.07.2024 17:31Здорово! Я вижу у вас больше двух цветов получилось, это какая-то специальная обработка ?
KurtkaBeyn
Спасибо за такой подробный материал, таких статей побольше бы на хабре