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
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.