Hello there!
In this little tutorial, we will create a simple controller that configures LCD and when ready - streams pixels into it, let's get started.
Create Vivado projectFirst, we need to create some empty Vivado project to work on:
After creating the project, we need to add new design files. Use ALT+A or FILE→Add Sources to create new design files for the LCD control.
And create block design for the project by clicking create block design in Vivado:
The TFTLCD is using ILI9341 as its driver, configured in 4-spi mode.
For communication, it uses the following pins:
- CS - chip select (CSX)
- RS - register select (D/CX)
- CLK - spi clock (SCL)
- MOSI - master output slave input (SDA)
On the page 35 of the documentation, we can see the following the waveform:
From the waveform, we can deduce that:
- When CS is high, all commands and parameters are ignored
- We are starting the transition by holding CS in low state
- At the next positive edge of the SCL, the oldest bit - D7 is being sampled, then on the next edge D6 etc...
- On the last bit of the byte - D0, the driver also samples D/CX signal. Before that, D/CX is ignored.
If we know how the LCD is receiving the data, we need to think how we can create an IP block that generates the same waveform. For that, we need at least 3 modules. SPI TX (spi_4l_8b.v) and memory module (spi_4l_8b_fifo.v) for configuration data. Some commands require a delay before sending the next command, for example,"software reset" or "sleep off". For that, we will add another module (spi_4l_8b_cmd_delay.v) that detects a particular command and stalls SPI transactions for a fixed amount of time.
IP developmentFirst, let's start with TX module. This module only responsibility is sending data to the LCD. When input AXI4-stream interface is valid, the module copies data to "command" reg and sends the data bit by bit.
module spi_4l_8b(
input [8:0]command_tdata,
input command_tvalid,
output reg command_tready,
output reg spi_cs,
output reg spi_mosi,
output spi_rs,
output spi_clk,
input clk,
input reset
);
reg data_locked = 0;
reg [8:0] command = 0;
reg [3:0] command_bit_counter = 7;
assign spi_clk = clk;
asign spi_rs = command[8];
always @(posedge clk) begin
if (!reset) begin
data_locked <= 0;
command <= 0;
command_tready <= 0;
command_bit_counter <= 7;
spi_cs <= 1;
end else begin
if (!data_locked) begin: wait_for_transaction
spi_cs <= 1;
if (command_tready && command_tvalid) begin
command <= command_tdata;
command_tready <= 0;
data_locked <= 1;
end else begin
command_tready <= 1'b1;
end
end else begin
spi_cs <= 0;
spi_mosi <= command[command_bit_counter];
if (!command_bit_counter) begin: send_spi_data
command_bit_counter <= 7;
data_locked <= 0;
end else begin
command_bit_counter <= command_bit_counter - 1;
end
end
end
end
endmodule
Command delay module detects if, after an outgoing command, the design needs to stall for "delay_val" cycles. If it does, then it forces tvalid and tready signals to logic level low.
module spi_4l_8b_cmd_delay
#(parameter delay_val = 500000)(
output [8:0]out_command_tdata,
output out_command_tvalid,
input out_command_tready,
input [8:0]in_command_tdata,
input in_command_tvalid,
output in_command_tready,
input clk,
input reset
);
reg [31:0] delay_counter = 0;
reg lock = 0;
assign out_command_tdata = in_command_tdata;
assign out_command_tvalid = in_command_tvalid && (!lock);
assign in_command_tready = out_command_tready && (!lock);
always @(posedge clk) begin
if (!reset) begin
delay_counter <=0;
lock <= 0;
end else begin
if (
!in_command_tdata[8] && //If incoming data is command
(in_command_tdata[7:0] != 8'b0010_1100) && //If command is not screen write
out_command_tready && //If AXI4 transaction
in_command_tvalid && //passes
!lock // and IP is not already locked.
) begin
lock <= 1;
delay_counter <= delay_val;
end else if (!delay_counter) begin
lock <= 0;
end
if (delay_counter) begin: decrement_delay_counter
delay_counter <= delay_counter - 1;
end
end
end
endmodule
The last module is FIFO/Memory. It has three tasks:
- First, after reset, initialize LCD with default values.
- Second, send memory write command to LCD after 240x320 pixels.
- Third, parse the incoming 8-bit stream into 9-bit SPI commands.
With power of Vivado we have a possibility to define sequence of ili operations and use Verilog tasks in initial block like a software function that sends command to the LCD. Normally we should use $readmemh for sake of project portability as not all vendors support initial begin - task memory initialization.
`define ili_NOP 8'h00 // No Operation - NOP
`define ili_SWRESET 8'h01 // Software Reset - SWRESET
`define ili_SLPOUT 8'h11 // Sleep Out
`define ili_GAMSET 8'h26 // Gamma Set
`define ili_DISPOFF 8'h28 // Display OFF
`define ili_DISPON 8'h29 // Display ON
`define ili_CASET 8'h2A // Column Address Set
`define ili_PASET 8'h2B // Page (row) Address Set
`define ili_RAMWR 8'h2C // Memory Write
`define ili_MADCTL 8'h36 // Memory Access Control
`define ili_IDMOFF 8'h38 // Idle Mode OFF
`define ili_IDMON 8'h39 // Idle Mode ON
`define ili_PIXSET 8'h3A // COLMOD: Pixel Format Set
`define ili_RAMWRCont 8'h3C // Write Memory Continue
`define ili_FRMCTR1 8'hB1 // Frame Rate Control (In Normal Mode/Full Colors)
`define ili_DISCTRL 8'hB6 // Display Function Control
`define ili_PWCTRL1 8'hC0 // Power Control 1
`define ili_PWCTRL2 8'hC1 // Power Control 2
`define ili_VMCTRL1 8'hC5 // VCOM Control 1
`define ili_VMCTRL2 8'hC7 // VCOM Control 2
`define ili_PGAMCTRL 8'hE0 // Positive Gamma Correction
`define ili_NGAMCTRL 8'hE1 // Negative Gamma Correction
`define ili_PCA 8'hCB // Power Control A
`define ili_PCB 8'hCF // Power Control B
`define ili_DTCA_ic 8'hE8 // Driver Timming Control A
`define ili_DTCB 8'hEA // Driver Timming Control B
`define ili_POSC 8'hED // Power On Sequence Control
`define ili_E3G 8'hF2 // Enable 3G
`define ili_PRC 8'hF7 // Pump Ratio Control
module spi_4l_8b_fifo
#(parameter MEMORY_LIMIT = 96) // Nb of init commands
(
input [7:0]in_command_tdata,
input in_command_tvalid,
output reg in_command_tready,
output [8:0]command_tdata,
output reg command_tvalid,
input command_tready,
output reg counter_ce,
input clk,
input reset
);
reg [8:0] memory[MEMORY_LIMIT - 1:0];
reg [18:0] memory_counter = 0;
reg [8:0] output_reg = 0;
localparam INIT = 0;
localparam NEXT_FRAME = 1;
localparam SEND_FRAME = 2;
localparam LCD_SIZE = 153600; //240x320*2 -> 2 transactions per pixel.
reg [1:0]state = INIT;
integer i;
task tft_write (input [7:0] cmd, input type);
begin
memory[i] = {type, cmd};
i = i + 1;
end
endtask
task write_data8 (input [7:0] cmd);
begin
tft_write(cmd, 1'b1);
end
endtask
task write_cmd (input [7:0] cmd);
begin
tft_write(cmd, 1'b0);
end
endtask
task write_data16(input [7:0] cmd1, input [7:0] cmd2);
begin
write_data8(cmd1);
write_data8(cmd2);
end
endtask
task delay();
begin
tft_write(8'h00, 1'b0);
end
endtask
initial begin
i = 0;
write_cmd(`ili_SWRESET);
write_cmd(`ili_NOP);
// Power Control A
write_cmd(`ili_PCA);
write_data8(8'h39);
write_data8(8'h2C);
write_data8(8'h00);
write_data8(8'h34);
write_data8(8'h02);
// Power Control B
write_cmd(`ili_PCB);
write_data8(8'h00);
write_data8(8'hC1);
write_data8(8'h30);
// Driver Timming Control A
write_cmd(`ili_DTCA_ic);
write_data8(8'h85);
write_data8(8'h00);
write_data8(8'h78);
// Driver Timming Control B
write_cmd(`ili_DTCB);
write_data8(8'h00);
write_data8(8'h00);
// Power On Sequence Control A
write_cmd(`ili_POSC);
write_data8(8'h64);
write_data8(8'h03);
write_data8(8'h12);
write_data8(8'h81);
// Pump Ratio Control
write_cmd(`ili_PRC);
write_data8(8'h20);
// Power Control 1
write_cmd(`ili_PWCTRL1);
write_data8(8'h23);
// Power Control 2
write_cmd(`ili_PWCTRL2);
write_data8(8'h10);
// VCOM Control 1
write_cmd(`ili_VMCTRL1);
write_data8(8'h3E);
write_data8(8'h28);
// VCOM Control 2
write_cmd(`ili_VMCTRL2);
write_data8(8'h86);
// Memory Access Control
write_cmd(`ili_MADCTL);
write_data8(8'hA8);
// Pixel Format Set
write_cmd(`ili_PIXSET);
write_data8(8'h55);
// Frame Rate Control
write_cmd(`ili_FRMCTR1);
write_data8(8'h00);
write_data8(8'h18);
// Display Function Control
write_cmd(`ili_DISCTRL);
write_data8(8'h08);
write_data8(8'h82);
write_data8(8'h27);
// Enable 3G
write_cmd(`ili_E3G);
write_data8(8'h00);
// Gamma Set
write_cmd(`ili_GAMSET);
write_data8(8'h01);
// Positive Gamma Correction
write_cmd(`ili_PGAMCTRL);
write_data8(8'h0F);
write_data8(8'h31);
write_data8(8'h2B);
write_data8(8'h0C);
write_data8(8'h0E);
write_data8(8'h08);
write_data8(8'h4E);
write_data8(8'hF1);
write_data8(8'h37);
write_data8(8'h07);
write_data8(8'h10);
write_data8(8'h03);
write_data8(8'h0E);
write_data8(8'h09);
write_data8(8'h00);
// Negative Gamma Correction
write_cmd(`ili_NGAMCTRL);
write_data8(8'h00);
write_data8(8'h0E);
write_data8(8'h14);
write_data8(8'h03);
write_data8(8'h11);
write_data8(8'h07);
write_data8(8'h31);
write_data8(8'hC1);
write_data8(8'h48);
write_data8(8'h08);
write_data8(8'h0F);
write_data8(8'h0C);
write_data8(8'h31);
write_data8(8'h36);
write_data8(8'h0F);
// Sleep Out
write_cmd(`ili_SLPOUT);
write_cmd(`ili_NOP);
//Display ON
write_cmd(`ili_DISPON);
write_cmd(`ili_NOP);
write_cmd(`ili_CASET);
write_data8(8'h00);
write_data8(8'h00);
write_data8(8'h01);
write_data8(8'h40);
write_cmd(`ili_PASET);
write_data8(8'h00);
write_data8(8'h00);
write_data8(8'h00);
write_data8(8'hEF);
//Init Done
end
always @(posedge clk) begin
if(!reset) begin
memory_counter <= 0;
counter_ce <= 0;
state <= INIT;
end else begin
case (state)
INIT: begin
counter_ce <= 0;
command_tvalid <= 1;
if (command_tvalid && command_tready) begin
memory_counter <= memory_counter + 1;
end
if (memory_counter == MEMORY_LIMIT) begin
state <= NEXT_FRAME;
command_tvalid <= 0;
end
end
NEXT_FRAME: begin
memory_counter <= 0;
command_tvalid <= 1;
if (command_tvalid && command_tready) begin
state <= SEND_FRAME;
command_tvalid <= 0;
counter_ce <= 1;
end
end
SEND_FRAME: begin
counter_ce <= 0;
in_command_tready <= command_tready;
command_tvalid <= in_command_tvalid;
if (in_command_tready && in_command_tvalid) begin
memory_counter <= memory_counter + 1;
end
if (memory_counter >= LCD_SIZE - 1)
state <= NEXT_FRAME;
end
default: begin
state <= INIT;
memory_counter <= 0;
end
endcase
end
end
assign command_tdata = state == INIT ? memory[memory_counter] : state == NEXT_FRAME ? {1'b0, `ili_RAMWR} : {1'b1, in_command_tdata};
endmodule
TestbenchThe testbench is really simple, as the FIFO needs to program LCD, it generates a known list of vectors that need to be serialized and displayed at SPI output. If input data is a command, we should see no operation in the simulation waveform for a while.
`timescale 1ns / 1ns
module spi_4l_8b_tb;
wire [8:0]in_command_tdata;
wire in_command_tvalid;
wire in_command_tready;
wire [8:0]out_command_tdata;
wire out_command_tvalid;
wire out_command_tready;
wire spi_cs;
wire spi_mosi;
wire spi_rs;
wire spi_clk;
reg clk = 0;
reg reset = 0;
reg [7:0] acu = 8'hFF;
reg acu_vld = 1'b1;
wire sink;
wire counter_ce;
spi_4l_8b_fifo u0_spi_4l_8b_fifo(
.in_command_tdata(acu),
.in_command_tvalid(acu_vld),
.in_command_tready(sink),
.command_tdata(in_command_tdata),
.command_tvalid(in_command_tvalid),
.command_tready(in_command_tready),
.counter_ce(counter_ce),
.clk(clk),
.reset(reset)
);
spi_4l_8b_cmd_delay #(.delay_val(20))
u0_spi_4l_8b_cmd_delay(
.out_command_tdata(out_command_tdata),
.out_command_tvalid(out_command_tvalid),
.out_command_tready(out_command_tready),
.in_command_tdata(in_command_tdata),
.in_command_tvalid(in_command_tvalid),
.in_command_tready(in_command_tready),
.clk(clk),
.reset(reset)
);
spi_4l_8b u0_spi_4l_8b (
.command_tdata(out_command_tdata),
.command_tvalid(out_command_tvalid),
.command_tready(out_command_tready),
.spi_cs(spi_cs),
.spi_mosi(spi_mosi),
.spi_rs(spi_rs),
.spi_clk(spi_clk),
.clk(clk),
.reset(reset)
);
always begin #5 clk = !clk; end
initial begin
#20 reset = !reset;
$monitor("New data_in: %h %0t", u0_spi_4l_8b.command, $time);
end
endmodule
Set the testbench as a top file in simulation sources and click run simulation.
After a few seconds, we should get the waveform:
Design seems to work correctly, now it is time to fill up constraints file with correct pinout.
#######################################################################
# Pmod #1
#######################################################################
#set_property PACKAGE_PIN L15 [get_ports PMOD1_PIN1]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN1]
#set_property PACKAGE_PIN M15 [get_ports PMOD1_PIN2]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN2]
#set_property PACKAGE_PIN L14 [get_ports PMOD1_PIN3]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN3]
set_property PACKAGE_PIN L14 [get_ports spi_rs]
set_property IOSTANDARD LVCMOS33 [get_ports spi_rs]
#set_property PACKAGE_PIN M14 [get_ports PMOD1_PIN4]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN4]
#set_property PACKAGE_PIN K13 [get_ports PMOD1_PIN7]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN7]
set_property PACKAGE_PIN K13 [get_ports spi_cs]
set_property IOSTANDARD LVCMOS33 [get_ports spi_cs]
#set_property PACKAGE_PIN L13 [get_ports PMOD1_PIN8]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN8]
set_property PACKAGE_PIN L13 [get_ports spi_mosi]
set_property IOSTANDARD LVCMOS33 [get_ports spi_mosi]
#set_property PACKAGE_PIN N13 [get_ports PMOD1_PIN9]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN9]
#set_property PACKAGE_PIN N14 [get_ports PMOD1_PIN10]
#set_property IOSTANDARD LVCMOS33 [get_ports PMOD1_PIN10]
set_property PACKAGE_PIN N14 [get_ports spi_clk]
set_property IOSTANDARD LVCMOS33 [get_ports spi_clk]
Block designThe last step before implementing the design is to connect everything in block design. First, we need a clock source, for this add ZYNQ7 PS IP'core and run block automation.
Add SPI modules to block design, by dragging them from sources and other IPs for generating LCD signal and also a VIO for reset control. Minized PS IP FCLK_CLK0 is 50Mhz. LCD module maximum frequency is 10Mhz, so we need to use clocking wizard IP to lower clock frequency.
Save block design, Vivado should automatically detect and set this block design as a top module:
Generate bitstream and program Minized. If you have problems with VIO control, try to lower JTAG frequency. Use VIO to control the reset bit, as reset is active low, set this bit to high and look at the LCD.
For practice, I suggest adding AXI4 datawidth converter IP and widen binary counter to 16 bits for more LCD colors :)
If you have problems with not detecting VIO activity, that means that ZYNQ7 PS is not programmed. You can try to boot Minized from flash memory and then reprogram FPGA from Vivado, or export hardware project and program flash with Vitis using for example "Hello world" application.
The endThank you for your time, wish you luck with future projects. I hope that somebody will find this tutorial useful :)
Take care!
Comments