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.

Les commentaires sont fermés.