Affichage d’un terminal série sur une sortie DVI

Affichage d’un terminal série sur une sortie DVI

Terminal sur liaison série

Dans l’article précédent, je vous présentais les bases pour réaliser une sortie vidéo DVI avec un FPGA. Nous allons ici reprendre ces différents modules et y ajouter ceux nécessaires à l’affichage sur l’écran d’un terminal série.

 

 

Il nous faut avant tout un module de réception UART. Celui-ci sera en charge de convertir les bits reçues en série par le FPGA en un caractère sur 8 bits.

Il existe déjà sur internet beaucoup d’exemples d’UART en Verilog. J’ai choisis d’utiliser celui du site www.nandland.com.

Afin de simplifier l’implémentation, l’écran du terminal est composé de 32 lignes de 64 caractères. Le stockage de ces caractères est réalisé dans une RAM interne au FPGA. Le processus d’affichage sur l’écran (lecture RAM) étant complètement indépendant du positionnement des caractères reçus (écriture RAM), mon choix s’est porté sur une RAM double port (IP = RAM : 2-PORT) avec des horloges indépendantes pour la lecture et l’écriture.

Paramètres de l’IP :

  • IP = RAM : 2-PORT
  • Nom = RAM2P_CONSOLE
  • Type = Verilog
  • How many 8-bits word of memory = 2048
  • Dual clock : use separate ‘read’ and ‘write’ clocks.
  • Désélectionner « Read output port(s) » dans « Which port should be registered ?»

 

Gestion de la console

Pour l’écriture dans la RAM Console (gestion des sauts de ligne, effacement de l’écran,…), j’ai utilisé une machine d’état dont voici le diagramme :

 

Notes :

  • Colonne (rPTRCOL) est codé sur 6 bits. Son incrément en fin de ligne (=63), le remettra à 0.
  • Ligne (rPTRLIG) est codé sur 5 bits. Son incrément en fin d’écran (=31), le remettra à 0.
  • En fin d’écran, plutôt que de devoir décaler en mémoire toutes les lignes vers le haut, nous incrémentons DispLig (rDISPLIG) qui indique quelle est la ligne à afficher en haut de l’écran.

La machine d’état est implémentée en deux blocs. Le premier en logique combinatoire, calculera le prochain état et les prochaines valeurs (ligne,colonne,etc..) à mettre à jour. Le deuxième bloc fera cette mise à jour sur front montant d’une horloge :

 

 

Voici le code correspondant :

 

	// --------- Gestion de la console --------------------

	// Code contrôle ASCII
	localparam CONST_CHAR_LF   = 8'd10;	// Nouvelle ligne + retour chariot
	localparam CONST_CHAR_FF   = 8'd12;	// Effacement écran
	localparam CONST_CHAR_CR   = 8'd13;	// Retour chariot
	
	localparam [2:0] // Etat de la machine
		ST_IDLE 		= 3'b000,	// Attente
		ST_WR_CHAR		= 3'b001,	// Ecriture caractère en RAM
		ST_INC_COL		= 3'b010,	// Incrémente colonne
		ST_INC_LIG		= 3'b011,	// Incrémente ligne
		ST_WR_NUL		= 3'b100,	// Ecriture en RAM caractère NULL
		ST_INC_COL2		= 3'b101,	// Incrémente colonne (eff. ligne ou écran)
		ST_GEST_CRLF	= 3'b110,	// Gestion CR ou LF
		ST_GEST_CLEAR	= 3'b111;	// Gestion FF 

	reg [2:0] 	rSTATE;			// Etat actuel de la machine
	reg	[5:0] 	rPTRCOL;			// Colonne
	reg	[4:0] 	rPTRLIG;			// Ligne
	reg [4:0] 	rDISPLIG;		// Numéro de la 1er ligne a afficher
	reg			rFULLSCR;		// Fin d'écran a déjà été atteint
	
	// Prochaines valeurs pour la machine
	wire [5:0]	wNextCol;
	wire [4:0]	wNextLig;
	wire [4:0]	wNextDispLig;
	wire [2:0]	wNextState;
	wire		wNextFULLSCR;

	// Valeur a écrire en mémoire
	wire [7:0] 	wWRITECHAR;
	assign wWRITECHAR = ((rSTATE[2:0] == ST_IDLE) || (rSTATE[2:0] == ST_WR_CHAR)) ? iUART_CHAR : 8'd0;
	
	
	// Signal d'écriture en RAM
	wire wRAMCONS_WR_CLK;
	assign wRAMCONS_WR_CLK = wPARAM_UART ? (((rSTATE[2:0] == ST_WR_CHAR) || (rSTATE[2:0] == ST_WR_NUL)) ? 1'b1 : 1'b0) : (wSPI_WR_RAM_CONS);

	// Adresse RAM du caractère à écrire
	wire [10:0] wRAMCONS_WR_ADR;
	assign wRAMCONS_WR_ADR = wPARAM_UART ? {rPTRLIG,rPTRCOL} : AddrWrite;
	
	always @(*) begin

		// Par défaut on reprend les anciennes valeurs
		wNextCol = rPTRCOL;
		wNextLig = rPTRLIG;
		wNextState = rSTATE;
		wNextDispLig = rDISPLIG;
		wNextFULLSCR = rFULLSCR;

		case (rSTATE)
		
			ST_IDLE :
				begin
					if (iUART_CHARSIG == 1'b1)
						case (iUART_CHAR)
							CONST_CHAR_LF 	: wNextState = ST_GEST_CRLF;
							CONST_CHAR_FF 	: wNextState = ST_GEST_CLEAR;
							CONST_CHAR_CR 	: wNextState = ST_GEST_CRLF;
							default			: wNextState = ST_WR_CHAR;
						endcase
				end

			ST_WR_CHAR :
				begin
					wNextState = ST_INC_COL;
				end
		
			ST_INC_COL :
				begin
					wNextCol = rPTRCOL + 1;
					if (wNextCol == 0)	// Fin de ligne ?
						wNextState = ST_INC_LIG;
					else
						if (iUART_CHARSIG == 1'b0) // Attente fin du signal caractère
							wNextState = ST_IDLE;
				end
		
			ST_INC_LIG : 
				begin
					wNextState = ST_WR_NUL;
					wNextLig = rPTRLIG +1;
					if ((wNextLig == 0) || (rFULLSCR == 1)) // Fin d'écran ?
					begin
						wNextDispLig = rDISPLIG + 1;
						wNextFULLSCR = 1;
					end
				end

			ST_WR_NUL :
				begin
					wNextState = ST_INC_COL2;
				end
				
			ST_INC_COL2 : 
				begin
					wNextCol = rPTRCOL +1;
					if (wNextCol != 0)
						wNextState = ST_WR_NUL;
					else
						if (iUART_CHAR != CONST_CHAR_FF) 
							begin
								if (iUART_CHARSIG == 1'b0) // Attente fin du signal caractère
									wNextState = ST_IDLE;
							end
						else
						begin
							// Effacement écran
							if (rPTRLIG != 5'd31)	// Reste des lignes ?
								wNextState = ST_INC_LIG;
							else
								if (iUART_CHARSIG == 1'b0) // Attente fin du signal caractère
								begin
									wNextState = ST_IDLE;
									wNextDispLig = 0;
									wNextLig = 0;
									wNextFULLSCR = 0;
								end
						end
				end	
		
			ST_GEST_CRLF : 
				begin
					wNextCol = 0;
					if (iUART_CHAR == CONST_CHAR_LF) 
						wNextState = ST_INC_LIG;
					else
						if (iUART_CHARSIG == 1'b0) // Attente fin du signal caractère
							wNextState = ST_IDLE;
				end
	
			ST_GEST_CLEAR :
				begin
					wNextCol = 0;
					wNextLig = 0;
					wNextDispLig = 0;
					wNextState = ST_WR_NUL;
				end
		endcase

	end
	
	always @(posedge iCLKSYS) begin

		// Si l'état n'a pas changé on ne met pas à jour les variables
		if (wNextState != rSTATE)
			begin
				rPTRCOL <= wNextCol;
				rPTRLIG <= wNextLig;
				rSTATE <= wNextState;
				rDISPLIG <= wNextDispLig;
				rFULLSCR <= wNextFULLSCR;
			end
	end
 

 

Note : L’effacement de l’écran n’est pas optimisé et prend trop de temps avec une liaison UART à 115200 bauds. Il convient donc d’insérer un délai après l’envoi du caractère FormFeed (12).

Paramétrage de l’affichage

Les paramètres d’affichage sont stockés dans des registres accessibles via SPI :

  • ENA : Affichage console (0 = OFF, 1 = ON)
  • UART : Valide l’accès UART (0 = OFF, 1 = ON)
  • POSH_X : 4 bits poids fort position X de la console.
  • POSH_Y : 4 bits poids fort position Y de la console.
  • POSL_X : 8 bits poids faible position X de la console.
  • POSL_Y : 8 bits poids faible position Y de la console.
  • CHAR_COLOR : Couleur caractères (Bit[7:5] = Rouge, Bit[4:2] = Vert, Bit[1:0] = Bleu)
  • BACK_COLOR : Couleur fond (Bit[7:5] = Rouge, Bit[4:2] = Vert, Bit[1:0] = Bleu)

Afin de ne pas créer d’artefact sur l’écran ces registres sont recopiés en local en fin de trame écran lorsque la liaison SPI n’est pas active.

Voici le code :

 

	// --------- Gestion SPI --------------------
	reg [7:0] 	SPIRegisters[7:0];	// Registres SPI directs
	reg [7:0] 	LOCRegisters[7:0];	// Registres locaux (stables)
	reg [10:0] 	AddrWrite;				// Adresse d'écriture SPI

	wire [3:0] 	wSPI_DEVICE;	// Périphérique SPI en cours
	wire 		wPARAM_UART;	// Autorise ou non l'accès UART
	wire 		wPARAM_ENABLE;	// Affiche ou non la console
	
	wire [11:0] wPARAM_POSX, wPARAM_POSY;	// Positions X et Y de la console
	
	wire [7:0] 	wPARAM_CHAR_COLR, wPARAM_CHAR_COLG, wPARAM_CHAR_COLB;	// Couleur caractères
	wire [7:0] 	wPARAM_BACK_COLR, wPARAM_BACK_COLG, wPARAM_BACK_COLB;	// Couleur fond
	
	wire 		wSPI_WR_RAM_CONS, wSPI_WR_RAM_CHAR;	// Signaux d'écriture en RAM console et RAM caractère
	
	assign wSPI_DEVICE 		= iSPI_CMD[6:3];
	assign wSPI_WR_RAM_CONS = (wSPI_DEVICE == pSPI_RAM_CONS_ID) && iSPI_WRITE_SIG && (wPARAM_UART == 1'b0 );
	assign wSPI_WR_RAM_CHAR = (wSPI_DEVICE == pSPI_RAM_CHAR_ID) && iSPI_WRITE_SIG;
	assign wPARAM_UART 		= LOCRegisters[0][1];
	assign wPARAM_ENABLE 	= LOCRegisters[0][0];
	assign wPARAM_POSX 		= {LOCRegisters[1][7:4],LOCRegisters[2][7:0]};
	assign wPARAM_POSY 		= {LOCRegisters[1][3:0],LOCRegisters[3][7:0]};
										
	assign wPARAM_CHAR_COLR = (LOCRegisters[4][7:5] == 3'b111) ? 8'hFF : LOCRegisters[4][7:5]*36;
	assign wPARAM_CHAR_COLG = (LOCRegisters[4][4:2] == 3'b111) ? 8'hFF : LOCRegisters[4][4:2]*36;
	assign wPARAM_CHAR_COLB = LOCRegisters[4][1:0] * 85;
	
	assign wPARAM_BACK_COLR = (LOCRegisters[5][7:5] == 3'b111) ? 8'hFF : LOCRegisters[5][7:5]*36;
	assign wPARAM_BACK_COLG = (LOCRegisters[5][4:2] == 3'b111) ? 8'hFF : LOCRegisters[5][4:2]*36;
	assign wPARAM_BACK_COLB = LOCRegisters[5][1:0] * 85;
	

	 
	// Mise à jour registre d'adresse AddrWrite depuis le module SPI
	always @(posedge iSPI_WRITE_SIG )
	begin
		if (wSPI_DEVICE == pSPI_CONS_ID)
			SPIRegisters[iSPI_CMD[2:0]+AddrWrite[2:0]] <= iSPI_DATA;
	end

	// Incrémente l'adresse d'écriture
	always @(posedge iSPI_INC_WRADDR or negedge iSPI_CMD[7])
	begin
			if (iSPI_CMD[7] == 1'b0) 
				AddrWrite <= 1'b0;
		else 
			AddrWrite <= AddrWrite + 1'b1;
	end
	
	// Mise à jour des registres locaux à la fin de la trame DVI
	always @(posedge iVID_CLK)
	begin
		if ((wSPI_DEVICE != pSPI_CONS_ID)&&((iRFSH == 1'b1)||(iDISP_ENAB == 1'b0)))
			LOCRegisters <= SPIRegisters;
	end

 

Affichage sur la sortie DVI

Le module DISPLAY_CONSOLE reçoit en entrée les coordonnées du pixel en cours d’affichage. Il peut ainsi déterminer d’après la position de la console si le pixel le concerne ou non.

Si c’est le cas il récupère en fonction de la position relative du pixel dans la fenêtre (Coord X, Coord Y)  :

  • la ligne où se trouve le caractère à afficher : Coord Y / 8,
  • sa colonne : Coord X / 8,
  • la ligne du bitmap du caractère : Coord Y % 8,
  • la colonne de ce même bitmap : Coord X % 8.

 

Le principe d’affichage est le suivant :

 

La porte XOR en fin de traitement permet d’inverser le bitmap du caractère ASCII (0 à 127), si le bit 7 de poids fort est positionné à 1.

La RAM contenant les bitmaps des caractères sera également une RAM double port. Cette fois-ci nous demanderons à Quartus de l’initialiser avec un fichier contenant ces bitmaps : font8x8.hex (voir dans le projet Quartus)

Paramètres de la RAM :

  • IP = RAM : 2-PORT
  • Nom = RAM2P_CHAR
  • Type = Verilog
  • How many 8-bits word of memory = 1024
  • Dual clock : use separate ‘read’ and ‘write’ clocks.
  • Désélectionner « Read output port(s) » dans « Which port should be registered ?»
  • Use this file for the memory content data = ./font8x8.hex (link)

Le code permettant l’affichage sera donc très simple :

 

	// --------- Affichage Vidéo DVI --------------------

	// Sortie définissant si l'écran doit être affiché en fonction de sa position et de sa taille
	assign oDISP = ((wPARAM_ENABLE==1'b1)
						&&(iVID_X >= wPARAM_POSX)&&(iVID_X < wPARAM_POSX+64*8)
						&&(iVID_Y >= wPARAM_POSY)&&(iVID_Y < wPARAM_POSY+32*8)) ? 1'b1 : 1'b0;


	// Signal de lecture du caractère en RAM console
	wire wRAMCONS_RD_CLK;
	assign wRAMCONS_RD_CLK = ~(iVID_CLK & iVID_CLKx2);

	// Signal de lecture du bitmap (monte après wRAMCONS_RD_CLK)
	wire wRAM_RD_CLK;
	assign wRAM_RD_CLK = (~iVID_CLK);

	// Pixel du bitmap (Coord X % 8)
	wire wPIXEL;
	assign wPIXEL = wCHAR_BIN[3'(iVID_X - wPARAM_POSX)];

	wire [7:0] wCHAR_ASC;	// Code du caractère
	wire [7:0] wCHAR_BIN;	// 8 bits bitmap du caractère


	// Couleur en sortie pour le pixel en cours.
	assign oRED = (wPIXEL^wCHAR_ASC[7] == 1'b0) ? wPARAM_BACK_COLR : wPARAM_CHAR_COLR;
	assign oGRN = (wPIXEL^wCHAR_ASC[7] == 1'b0) ? wPARAM_BACK_COLG : wPARAM_CHAR_COLG;
	assign oBLU = (wPIXEL^wCHAR_ASC[7] == 1'b0) ? wPARAM_BACK_COLB : wPARAM_CHAR_COLB;

	
	// RAM CONSOLE
	RAM2P_CONSOLE RAM2P_CONSOLE_inst (
		.data(wWRITECHAR),
		.rdaddress({5'((iVID_Y-wPARAM_POSY)/8)+rDISPLIG,6'((iVID_X-wPARAM_POSX)/8)}),
		.rdclock(wRAMCONS_RD_CLK),
		.wraddress(wRAMCONS_WR_ADR),
		.wrclock(wRAMCONS_WR_CLK),
		.wren(1'b1),
		.q(wCHAR_ASC));

	// RAM BITMAP
	RAM2P_CHAR RAM2P_CHAR_inst (
		.data(iSPI_DATA),
		.rdaddress({wCHAR_ASC[6:0],3'(iVID_Y-wPARAM_POSY)}),
		.rdclock(wRAM_RD_CLK),
		.wraddress(AddrWrite[9:0]),
		.wrclock(wSPI_WR_RAM_CHAR),
		.wren(1'b0),
		.q(wCHAR_BIN));

 

Multiplexage vidéo.

Il ne reste plus qu’à écrire un petit module MUX_VIDEO qui transmettra soit la couleur du fond d’écran (VIDEO_BACKGROUND), soit la couleur de la console (DISPLAY_CONSOLE) si le pixel en cours appartient à cette dernière.

 

/*
* Copyright 2018 Philippe BOUDOT
* www.systemes-embarques.fr
*
* Not for commercial use
*
*/

module MUX_VIDEO 
 (

	input	[7:0] iVID_MUX_RED[1:0],
	input	[7:0] iVID_MUX_GRN[1:0],
	input	[7:0] iVID_MUX_BLU[1:0],
	
	input iSELCT_INPUT[0:0],
	
	output [7:0] oDVI_RED,
	output [7:0] oDVI_GRN,
	output [7:0] oDVI_BLU
	
  
 );

assign oDVI_RED = (iSELCT_INPUT[0] == 1'b1 ) ? iVID_MUX_RED[1]: iVID_MUX_RED[0];
assign oDVI_GRN = (iSELCT_INPUT[0] == 1'b1 ) ? iVID_MUX_GRN[1]: iVID_MUX_GRN[0];
assign oDVI_BLU = (iSELCT_INPUT[0] == 1'b1 ) ? iVID_MUX_BLU[1]: iVID_MUX_BLU[0];

 
endmodule
 

 

Le projet Quartus et le sketch associé sont disponibles sur la page de téléchargement .

 

 

 

Une réponse

  1. DjTGv dit :

    Bonjour Philippe,

    Weekend passionnant en perspective, sur l’Arduino FPGA MKR Vidor 4000, avec la mise en œuvre décrite dans ce nouvel article.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *