Enviar respuesta 
 
Calificación:
  • 0 votos - 0 Media
  • 1
  • 2
  • 3
  • 4
  • 5
Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
03-06-2013, 04:23 PM (Este mensaje fue modificado por última vez en: 05-06-2013 08:09 PM por admin.)
Mensaje: #1
Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
Antes de empezar es recomendable tener ligeras nociones de la sintaxis de VHDL. Si el inglés no es un inconveniente, este tutorial que enlazo os va a venir de perlas:
http://www.seas.upenn.edu/~ese171/vhdl/vhdl_primer.html


En este proyecto iré implementando poco a poco en VHDL un ZX Spectrum en la placa MOD-VGA (basada en la FPGA XC3S200A). Como mi experiencia en este lenguaje es escasa, no garantizo llegar hasta el final, así que si sigues este hilo espero que sólo sea para aprender.

Por otro lado ya existe una implementación completa realizada por McLeod y Superfo con el código fuente disponible (la mayoría en Verilog). Ver este hilo http://retrolandia.net/foro/showthread.php?tid=44

Empecemos pues. Lo primero de todo, este tutorial está centrado en la placa MOD-VGA configurada según el hilo anteriormente citado, pero es fácilmente extrapolable a cualquier placa entrenadora de FPGAs, siempre que hagáis los cambios pertinentes en el archivo .UCF. Entiendo que estáis familiarizados con el entorno de desarrollo de vuestra entrenadora (en mi caso es ISE Webpack 12.1) por lo que me centro únicamente en el código VHDL.

La primera prueba que voy a hacer es muy sencilla, sería mostrar una especie de carta de ajuste estática en pantalla. El contenido de la imagen serán 16 barras del mismo ancho cada una, las 8 primeras con colores sin brillo y las 8 últimas con colores con brillo. Serían los mismos colores que en un ZX Spectrum.

Empecemos con las primeras líneas de main.vhd:
Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity main is port(
    clk   : in  std_logic;
    sync  : out std_logic;
    r     : out std_logic_vector (2 downto 0);
    g     : out std_logic_vector (2 downto 0);
    b     : out std_logic_vector (2 downto 0));
end main;

Lo primero son las declaraciones de librerías. En este caso usamos std_logic_1164 y numeric_std de la librería ieee, necesarias para los tipos que vamos a implementar. En este ejemplo vamos a usar sólo 3 tipos: std_logic, std_logic_vector y unsigned.

Después viene la declaración de entidad. Al ser main.vhd el componente principal lo que vemos aquí son las entradas y salidas que vamos a usar de la MOD-VGA. clk es la entrada de reloj de 25MHz, sync es la salida de sincronismo, y r, g, b son cada una de las componentes que van a los DACs. La placa MOD-VGA tiene 3 DACs de 3 bits cada uno, uno para cada componente. Usaré la notación ternaria (R, G, B) que va desde el (0, 0, 0)=negro, al (7, 7, 7)=blanco de mayor intensidad. Aunque en teoría tendríamos hasta 512 posibles colores, en el ZX Spectrum sólo utilizamos 15 de ellos imponiendo 2 restricciones: La primera restricción es que para cada componente sólo existen tres posibles niveles: 0, 5 y 7, que se corresponden a: apagado, normal y con brillo. La segunda restricción es que no se pueden mezclar componentes normales y con brillo en un mismo color.

Los colores serían los siguientes:
(0, 0, 0) (0, 0, 5) (0, 5, 0) (0, 5, 5) (5, 0, 0) (5, 0, 5) (5, 5, 0) (5, 5, 5)
(0, 0, 7) (0, 7, 0) (0, 7, 7) (7, 0, 0) (7, 0, 7) (7, 7, 0) (7, 7, 7)

Para simplificar el diseño, hemos creado una variable "color" de 4 bits y un módulo que se encarga de convertir esta variable en su correspondiente color de 9 bits. Cada número de 4 bits se corresponde con un color, exceptuando el negro que se corresponde con los códigos "0000" y "1000".

Prosigamos con el código:
Código:
architecture behavioral of main is

  signal  clk7    : std_logic;
  signal  hcount  : unsigned  (8 downto 0);
  signal  vcount  : unsigned  (8 downto 0);
  signal  color   : std_logic_vector (3 downto 0);

  component clock7 is port(
      clkin_in  : in  std_logic;
      clkfx_out : out std_logic);
  end component;

  component colenc is port(
      col_in  : in  std_logic_vector (3 downto 0);
      r_out   : out std_logic_vector (2 downto 0);
      g_out   : out std_logic_vector (2 downto 0);
      b_out   : out std_logic_vector (2 downto 0));
  end component;

begin
  clock7_inst: clock7 port map (
    clkin_in  => clk,
    clkfx_out => clk7);

  colenc_inst: colenc port map (
    col_in => color,
    r_out  => r,
    g_out  => g,
    b_out  => b);

El resto de variables (que en VHDL se denominan signals) son: clk7, hcount y vcount. La primera es el reloj de 7Mhz que tenemos que generar, de lo cual se encarga el componente clock7. Luego tenemos dos contadores, el contador horizontal y el vertical. En estos casos empleamos el tipo "unsigned" en lugar de "std_logic_vector" porque nos interesa operar (en nuestro caso incrementar) con dicho valor. En hcount la cuenta máxima es 447, y en vcount la cuenta máxima es 311, por lo que con 9 bits tenemos suficiente en ambos casos.

Luego tenemos dos bloques "component" donde declaramos los componentes clock7 y colenc. El primero se encarga de generar el reloj de 7MHz partiendo de una entrada de 25MHz y el segundo hace la conversión de color de 4 a 9 bits antes explicada. Estos bloques lo usamos para hacer el diseño más modular. Después del begin instanciamos los componentes, es decir, le decimos a qué señales va a ir conectado nuestro módulo. Podemos instanciar un mismo componente tantas veces como queramos, aunque en esta caso sólo lo hacemos una.

Voy a emplear un ejemplo sencillo, una puerta AND. Con la declaración estaríamos diciendo: "En nuestro diseño vamos a usar un tipo de puerta que se llama AND y tienen 2 entradas y 1 salida". En la instanciación, o mejor dicho en cada una de las instanciaciones, le decimos: "Queremos conectar una puerta AND con una entrada en la señal X, otra entrada en la señal Y y la salida a la señal Z"


Prosigamos:
Código:
  process (clk7)
  begin
    if falling_edge( clk7 ) then
      if hcount=447 then
        hcount <= (others => '0');
      else
        hcount <= hcount + 1;
      end if;
    end if;
    if falling_edge( clk7 ) and hcount=447 then
      if vcount=311 then
        vcount <= (others => '0');
      else
        vcount <= vcount + 1;
      end if;
    end if;
  end process;

En este código tenemos un proceso con 2 IFs que se disparan con la señal clk7. A diferencia de un lenguaje de programación procedural, aquí todo se ejecuta concurrentemente, por lo que podríamos haber cambiado el orden de los IFs y el resultado sería el mismo. Básicamente lo que hacemos es incrementar 2 contadores y en función del valor de éstos generar las señales de color y sincronismo.

El primer IF hace que se incremente hcount de 0 a 447 en los flancos bajos de clk7, y que el siguiente valor de la cuenta, en lugar de ser 448 sería 0. Sería un reset síncrono, sin glitches.

El segundo IF incrementa la variable vcount de 0 a 311 cada vez que se acabe la cuenta de hcount, esto es en el flanco de bajada del último ciclo (cuando hcount vale 447). Al igual que hcount el reset es síncrono. La asignación <= (others => '0') es equivalente a "000000000" (poner a cero todos los bits), aunque es preferible usar la notación "others". De esta forma podemos cambiar el tamaño de la variable sin necesidad de cambiar las asignaciones.

El resto del código de main.vhd es el siguiente:
Código:
  process (hcount, vcount)
  begin
    color <= "0000";
    if  ( vcount>=248   and vcount<252  ) or
        ( hcount>=344-8 and hcount<376-8) then
      sync <= '0';
    else
      sync <= '1';
      if hcount<256 and vcount<192 then
        color <= std_logic_vector(hcount(7 downto 4));
      elsif hcount<320-8 or hcount>=416-8 then
        color <= "0111";
      end if;
    end if;
  end process;
end behavioral;

Este código es concurrente a los 2 IFs anteriores y se encarga de generar las señales sync y color partiendo de los contadores hcount y vcount. Los timings son los mismos del clon Harlequín y salvo pequeñas diferencias en el ancho de sync es exactamente igual a la señal que se genera en un ZX Spectrum 48K. En la parte no visible de la pantalla pintamos siempre de negro para que la TV pueda reconecer bien los sincronismos. Dentro de la parte visible de la pantalla hay dos zonas: un rectángulo central y un borde. En un ZX Spectrum el borde es de un color liso y el rectángulo central es una representación de la memoria de video. En nuestro ejemplo pintamos el borde de blanco normal, y en el rectángulo central mostramos 16 barras verticales, una de cada color (repitiendo el negro). Esto es tan sencillo como pasar a la variable color los bits del 7 al 4 de hcount. El std_logic_vector() es un casting de variables, ya que hcount es del tipo unsigned y color es del tipo std_logic_vector.

Los -8 los he añadido a última hora para mover el rectángulo central a la derecha y que quede centrado, ya que estaba un poco desplazado. Lo he puesto a parte porque no coincide con el timing del clon Harlequín, en el cual no existe dicho desplazamiento porque las señales van retardadas.

Ya hemos acabado con el main.vhd, aunque todavía me falta por explicar los archivos clock7.vhd, colenc.vhd y clown.ucf. Empecemos con el clock7.vhd:
Código:
library ieee;
use ieee.std_logic_1164.all;
library unisim;
use unisim.vcomponents.all;

entity clock7 is
  port( clkin_in  : in    std_logic;
        clkfx_out : out   std_logic);
end clock7;

architecture behavioral of clock7 is

  signal clkfb_in : std_logic;
  signal clkfx_buf: std_logic;
  signal clk0_buf : std_logic;
  signal gnd_bit  : std_logic;

begin
  gnd_bit <= '0';
  clkfx_bufg_inst : bufg
    port map( i => clkfx_buf,
              o => clkfx_out);
  clk0_bufg_inst : bufg
    port map( i => clk0_buf,
              o => clkfb_in);
  dcm_sp_inst : dcm_sp
    generic map ( clk_feedback          => "1X",
                  clkdv_divide          => 2.0,
                  clkfx_divide          => 25,
                  clkfx_multiply        => 7,
                  clkin_divide_by_2     => false,
                  clkin_period          => 20.0,
                  clkout_phase_shift    => "NONE",
                  deskew_adjust         => "SYSTEM_SYNCHRONOUS",
                  dfs_frequency_mode    => "LOW",
                  dll_frequency_mode    => "LOW",
                  duty_cycle_correction => true,
                  factory_jf            => x"C080",
                  phase_shift           => 0,
                  startup_wait          => false)
    port map( clkfb   => clkfb_in,
              clkin   => clkin_in,
              dssen   => gnd_bit,
              psclk   => gnd_bit,
              psen    => gnd_bit,
              psincdec=> gnd_bit,
              rst     => gnd_bit,
              clkdv   => open,
              clkfx   => clkfx_buf,
              clkfx180=> open,
              clk0    => clk0_buf,
              clk2x   => open,
              clk2x180=> open,
              clk90   => open,
              clk180  => open,
              clk270  => open,
              locked  => open,
              psdone  => open,
              status  => open);
end behavioral;

Para esto lo mejor es irse al datasheet del dispositivo y leer detenidamente cómo funciona el módulo DCM (Digital Clock Manager). A grandes rasgos dividimos la frecuencia entre 25 y la multiplicamos por 7, obteniendo así un reloj de 7MHz a la salida.

Vamos ahora al archivo colenc.vhd:
Código:
library ieee;
use ieee.std_logic_1164.all;

entity colenc is
  port (  col_in  : in  std_logic_vector (3 downto 0);
          r_out   : out std_logic_vector (2 downto 0);
          g_out   : out std_logic_vector (2 downto 0);
          b_out   : out std_logic_vector (2 downto 0));
end colenc;

architecture behavioral of colenc is
begin
  process (col_in)
  begin
    if( col_in(3)='0' ) then
      r_out<= col_in(1) & '0' & col_in(1);
      g_out<= col_in(2) & '0' & col_in(2);
      b_out<= col_in(0) & '0' & col_in(0);
    else
      r_out<= col_in(1) & col_in(1) & col_in(1);
      g_out<= col_in(2) & col_in(2) & col_in(2);
      b_out<= col_in(0) & col_in(0) & col_in(0);
    end if;
  end process;
end behavioral;

Como veis este módulo es muy sencillo, tiene un proceso con un IF simple y es totalmente combinacional. Transformamos los 4 bits de entrada (brillo, verde, rojo, azul) en sus 9 bits correspondientes, 3 bits a cada componente (r_out, g_out, b_out). Aquí hemos usado la concatenación, &, y la repetición de bits, por lo que en cada componente sólo son posibles los valores 000 (0, componente desactivada), 101 (5, brillo normal) y 111 (7, brillo intenso).

Por último tenemos el archivo clown.ucf:
Código:
net "clk"   loc = "p83" | iostandard= lvcmos33;
net "r<2>"  loc = "p15" | iostandard= lvcmos33;
net "r<1>"  loc = "p16" | iostandard= lvcmos33;
net "r<0>"  loc = "p19" | iostandard= lvcmos33;
net "g<2>"  loc = "p13" | iostandard= lvcmos33;
net "g<1>"  loc = "p12" | iostandard= lvcmos33;
net "g<0>"  loc = "p10" | iostandard= lvcmos33;
net "b<2>"  loc = "p9"  | iostandard= lvcmos33;
net "b<1>"  loc = "p6"  | iostandard= lvcmos33;
net "b<0>"  loc = "p5"  | iostandard= lvcmos33;
net "sync"  loc = "p4"  | iostandard= lvcmos33;

Esto no tiene mucho que explicar. Nos vamos al esquemático del MOD-VGA:

https://www.olimex.com/Products/Modules/..._B_sch.pdf

Y vemos a qué patilla corresponde cada una de las señales que utilizamos (que hemos declarado tras el entity main). Las señales tipo vector de bits acaban en <X>, siendo X el número de bit. Por último, con iostandard= lvcmos33 le indicamos en qué niveles deseamos trabajar con dicho pin. Por ahora nos apañamos con CMOS a 3.3V, pero más adelante podemos necesitar conectar con otra tecnología, por ejemplo TTL o CMOS a 5V.

Bueno aquí se acaba la primera lección. Como véis el ejemplo es muy sencillo. En la siguiente lección las cosas se van a complicar bastante, porque trataré de implementar gran parte de la ULA del ZX Spectrum, en concreto la memoria de video. Lo que haré será volcar una pantalla de Spectrum en la BRAM para hacerla funcionar a modo de ROM, tal cual se mostraría en un Spectrum real, incluído el flash.


Archivo(s) adjuntos Miniatura(s)
   

.zip  clown_leccion1.zip (Tamaño: 2.02 KB / Descargas: 122)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
04-06-2013, 01:12 PM (Este mensaje fue modificado por última vez en: 05-06-2013 01:08 PM por admin.)
Mensaje: #2
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
Visto el éxito que ha tenido la primera lección, os voy a hacer un adelanto de la segunda. Como imagen he escogido este fantástico remake de la pantalla de carga del Game Over realizado por MAC_BG:

http://www.zonadepruebas.com/viewtopic.php?f=4&t=3284

En principio cogí otra imagen más fea sólo por el hecho de que tenía flash, pero visto que funcionaba me he decantado por otra más espectacular. De momento os dejo el zip de la lección junto a la imagen obtenida mediante una capturadora de video.

Edito: Si se han bajado el .zip que he adelantado, bájense de nuevo el .zip que hay más abajo, puesto que he realizado algunos cambios y no se corresponde con la explicación.
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
05-06-2013, 01:04 PM (Este mensaje fue modificado por última vez en: 15-06-2013 11:56 PM por admin.)
Mensaje: #3
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
Para empezar he creado una pequeña herramienta llamada bramgen en C para transformar el fichero binario gameover.scr (vale para cualquier fichero binario) en código fuente de la ram de video. Generamos el archivo de texto gameover.txt y lo dejamos pendiente para más adelante.

Las primeras líneas de main.vhd son las mismas que en la lección anterior:
Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity main is port(
    clk   : in  std_logic;
    sync  : out std_logic;
    r     : out std_logic_vector (2 downto 0);
    g     : out std_logic_vector (2 downto 0);
    b     : out std_logic_vector (2 downto 0));
end main;

architecture behavioral of main is

  signal  clk7    : std_logic;
  signal  hcount  : unsigned  (8 downto 0);
  signal  vcount  : unsigned  (8 downto 0);
  signal  color   : std_logic_vector (3 downto 0);

Analicemos ahora el resto de variables:
Código:
  signal  vid     : std_logic;
  signal  viddel  : std_logic;
  signal  at2clk  : std_logic;
  signal  al1     : std_logic;
  signal  al2     : std_logic;
  signal  ccount  : unsigned  (4 downto 0);
  signal  flash   : unsigned  (4 downto 0);
  signal  at1     : std_logic_vector (7 downto 0);
  signal  at2     : std_logic_vector (7 downto 0);
  signal  da1     : std_logic_vector (7 downto 0);
  signal  da2     : std_logic_vector (7 downto 0);
  signal  addrv   : std_logic_vector(12 downto 0);
  signal  vd      : std_logic_vector (7 downto 0);
  signal  gencol  : std_logic_vector (2 downto 0);

La señal vid nos indica (cuando vale 0) que estamos pintando el rectángulo central, y esto ocurre cuando hcount es menor de 256 y vcount menor de 192. Luego tenemos viddel, que es una versión negada y retrasada 8 ciclos de la señal vid.

Lo siguiente es at2clk, que es la señal de reloj del registro at2. Tenemos 4 registros de 8 bits (at1, at2, da1 y da2) donde almacenamos temporalmente dos atributos y dos bytes de datos de la memoria de video. El secreto de la ULA está en el orden en que se cargan estos 4 registros y se muestran en pantalla. Las variables al1 y al2 se encargan de la carga de registros at1 y da1 y de la generación del bus de direcciones de la memoria contenida.

A continuación tenemos la señal ccount, que es una versión retrasada de 5 bits intermedios de hcount. Se utiliza en la generación de la dirección de video, ya que en un momento dado necesitamos "recordar" el valor que tenía hcount antes del último incremento. La siguiente variable, flash, es como su nombre indica para generar el parpadeo de los caracteres con el atributo flash activado. Es simplemente un contador de frames de 5 bits cuyo bit más significativo empleamos para invertir o no (función xor) la salida del registro desplazamiento que hay en da2.

Ahora vienen los 4 registros antes mencionados. Son todos registros normales, salvo da2 que a la vez es registro desplazamiento, que se encarga de ir presentando los bits de uno en uno para luego transformar cada bit en un pixel. La variable addrv genera la dirección que va a la RAM de video a partir de las señales vcount y ccount y dependiendo de si estamos generando un byte de dato o de atributo (en función de al1). La variable vd es el byte que extraemos de la RAM y va a parar a los registros at1 y da1, como sólo leemos de la RAM, vd es una salida.

Por último tenemos gencol, que multiplexa los 6 bits menos significativos de at2 entre el color de tinta y de fondo.

Ahora vienen las declaraciones e instanciaciones de los módulos, que son los mismos que en la lección anterior excepto vram:
Código:
  component clock7 is port(
      clkin_in  : in  std_logic;
      clkfx_out : out std_logic);
  end component;

  component colenc is port(
      col_in  : in  std_logic_vector (3 downto 0);
      r_out   : out std_logic_vector (2 downto 0);
      g_out   : out std_logic_vector (2 downto 0);
      b_out   : out std_logic_vector (2 downto 0));
  end component;

  component vram is port(
      clk     : in  std_logic;
      addr    : in  std_logic_vector(12 downto 0);
      dataout : out std_logic_vector( 7 downto 0));
  end component;

begin
  clock7_inst: clock7 port map (
    clkin_in  => clk,
    clkfx_out => clk7);

  colenc_inst: colenc port map (
    col_in => color,
    r_out  => r,
    g_out  => g,
    b_out  => b);

  video_ram: vram port map (
    clk     => clk,
    addr    => addrv,
    dataout => vd);

Para simplificar vram funciona a modo de ROM. A diferencia de una RAM normal, la block RAM que viene incluída en la FPGA se activa con una señal de reloj. Para no complicarme mucho la alimento con 25MHz y no tengo que preocuparme de activar nada, tan pronto cambie la dirección addr, se mostrará en la salida vd.

El siguiente código nos resultará familiar de la lección anterior:
Código:
  process (clk7)
  begin
    if falling_edge( clk7 ) then
      if hcount=447 then
        hcount <= (others => '0');
        if vcount=311 then
          vcount <= (others => '0');
          flash <= flash + 1;
        else
          vcount <= vcount + 1;
        end if;
      else
        hcount <= hcount + 1;
      end if;
      da2 <= da2(6 downto 0) & '0';
      if at2clk='0' then
        ccount <= hcount(7 downto 3);
        if viddel='0' then
          da2 <= da1;
        end if;
      end if;
    end if;
    if rising_edge( clk7 ) then
      if hcount(3)='1' then
        viddel <= vid;
      end if;
    end if;
  end process;

Con el primer IF generamos hcount, vcount y flash. Aprovechando el flanco de bajada de clk7, el siguiente IF carga o desplaza da2 según el at2clk y viddel. actlos siguienLos dos primeros IFs son casi idénticos, salvo que se incrementa la variable flash al acabar el frame. Luego tenemos una asignación más un IF, que conjuntamente generan da2 (que o bien es una carga de da1 o un desplazamiento de sí mismo) y ccount. El último IF es el encargado de retrasar 8 ciclos e invertir la señal vid, convirtiéndola en viddel.

Código:
  process (al1)
  begin
    if rising_edge( al1 ) then
      da1 <= vd;
    end if;
  end process;

  process (al2)
  begin
    if rising_edge( al2 ) then
      at1 <= vd;
    end if;
  end process;

Estos dos procesos cargan los registros da1 y at1 partiendo de la salida de la ram, en el momento que indican los flancos de subida de al1 y al2 respectivamente.

Código:
  process (at2clk)
  begin
    if rising_edge( at2clk ) then
      if( viddel='0' ) then
        at2 <= at1;
      else
        at2 <= "00000000";
      end if;
    end if;
  end process;

Aquí cargamos el registro at2 en cada flanco de subida de at2clk, dependiendo de viddel. Si estamos dentro de pantalla leemos del registro at1; si estamos fuera de ella, o sea en el borde, cargamos un color fijo (negro). En futuras lecciones meteremos aquí los 3 bits del borde (los que se escriben en el puerto $FE).

Prosigamos, a partir de ahora vienen los procesos totalmente combinacionales:
Código:
  process (hcount, vcount, gencol, at2(6))
  begin
    color <= "0000";
    vid   <= '1';
    if  (vcount>=248 and vcount<252) or
        (hcount>=344 and hcount<376) then
      sync <= '0';
    else
      sync <= '1';
      if hcount>=416 or hcount<320 then
        color <= at2(6) & gencol;
        if hcount<256 and vcount<192 then
          vid <= '0';
        end if;
      end if;
    end if;
  end process;

Este código es muy parecido al de la lección anterior, con la salvedad de que generamos una nueva señal, vid, y la variable color se calcula de una forma diferente. El bit 6 de at2 nos indica el brillo, y "color" la obtenemos de la salida del multiplexor en gencol.

Código:
  process (hcount)
  begin
    al1 <= '1';
    al2 <= '1';
    if hcount(3 downto 1)=3 or hcount(3 downto 1)=5 then
      al1 <= '0';
    end if;
    if hcount(3 downto 1)=4 or hcount(3 downto 1)=6 then
      al2 <= '0';
    end if;
  end process;

Estas señales (al1 y al2) tiene doble función. Gobiernan la carga de los registros at1 y da1, y también conmutan el multiplexor que genera la dirección de video (entre dirección de atributo y dirección de dato). Se crean en función de los bits 1, 2 y 3 del contador horizontal: los valores 3 y 5 activan al1, y los valores 4 y 6 activan al2.

Código:
  process (al1, vcount, ccount)
  begin
    if al1='0' then
      addrv <= std_logic_vector(vcount(7 downto 6) & vcount(2 downto 0)
                & vcount(5 downto 3) & ccount);
    else
      addrv <= "110" & std_logic_vector(vcount(7 downto 3) & ccount);
    end if;
  end process;

Aquí vemos cómo se genera la dirección de la vram, partiendo de la multiplexación de las señales vcount y ccount (contador vertical y contador horizontal retrasado). En un caso obtenemos el byte de atributo, y en el otro el byte de dato. Sé que resulta confuso llamarlo byte de dato, pero no se me ocurre otra forma de denominarlo. ¿Alguna sugerencia?

Código:
  process (flash(4), at2, da2(7))
  begin
    if (da2(7) xor (at2(7) and flash(4)))='0' then
      gencol <= at2(5 downto 3);
    else
      gencol <= at2(2 downto 0);
    end if;
  end process;

En el siguiente proceso tenemos el multiplexor que genera "gencol". Escogemos una rama u otra en función de 3 señales siguiendo los esquemas del clon Superfo.

El siguiente código:
Código:
  process (clk7, hcount)
  begin
    at2clk <= not clk7 or hcount(0) or not hcount(1) or hcount(2);
  end process;
end behavioral;
Por último tenemos la generación de at2lck de forma totalmente combinacional, en concreto es una OR de 4 entradas para alimentar el reloj del registro at2, señal a la que hemos denominado at2clk. Esta señal es la misma que aparece en los esquemas del clon Superfo.

Ya hemos acabado con main.vhd, el resto de archivos son iguales que en la lección anterior, salvo que ahora tenemos uno nuevo, vram.vhd. Se trata de un módulo que simula una ROM de 8K partiendo del elemento RAMB16_S9 de la librería unisim.vcomponents, en concreto es una RAM de 2Kx8.

Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

library unisim;
use unisim.vcomponents.all;

entity vram is port(
    clk     : in  std_logic;
    addr    : in  std_logic_vector(12 downto 0);
    dataout : out std_logic_vector( 7 downto 0));
end vram;

architecture Behavioral of vram is

  type arrena is array(3 downto 0) of std_logic;
  type arrdoa is array(3 downto 0) of std_logic_vector(7 downto 0);
  signal ena : arrena;
  signal doa : arrdoa;

En la declaración de librerías tenemos algo nuevo, la librería unisim.vcomponents. En la declaración de entidad nos ceñimos a lo más básico: reloj, dirección de entrada y dato de salida.

Luego tenmos las variables. Aquí vemos algo nuevo, que es cómo declarar un array de variables con la directiva type. Esto nos es muy útil para evitar tener código redundante, ya que automáticamente es como si generara las señales ena(0), ena(1), ena(2), ena(3) una a una (ídem con doa(x)). Esto lo veremos a continuación:

Código:
begin
  process(addr, doa)
  variable i : integer;
  begin
    dataout <= (others => '0');
    for i in 0 to 3 loop
      ena(i) <= '0';
      if (to_integer(unsigned(addr(12 downto 11))) = i) then
        ena(i) <= '1';
        dataout <= doa(i);
      end if;
    end loop;
  end process;

Aquí vemos dos cosas nuevas, no os asustéis. Por un lado una declaración con la directiva "variable" y por otro un bucle for. Lo primero genera una nueva variable que sólo entiende el preprocesador, y lo segundo va repitiendo el código que haya dentro del for cambiando el valor de la variable i en cada iteración. Esto equivaldría, tras pasar por el preprocesador, al siguiente código:

Código:
  process(addr, doa(0))
  begin
    dataout <= (others => '0');
    ena(0) <= '0';
    if (to_integer(unsigned(addr(12 downto 11))) = 0) then
      ena(0) <= '1';
      dataout <= doa(0);
    end if;
  end process;
  process(addr, doa(1))
  begin
    dataout <= (others => '0');
    ena(1) <= '0';
    if (to_integer(unsigned(addr(12 downto 11))) = 1) then
      ena(1) <= '1';
      dataout <= doa(1);
    end if;
  end process;
  process(addr, doa(2))
  begin
    dataout <= (others => '0');
    ena(2) <= '0';
    if (to_integer(unsigned(addr(12 downto 11))) = 2) then
      ena(2) <= '1';
      dataout <= doa(2);
    end if;
  end process;
  process(addr, doa(3))
  begin
    dataout <= (others => '0');
    ena(3) <= '0';
    if (to_integer(unsigned(addr(12 downto 11))) = 3) then
      ena(3) <= '1';
      dataout <= doa(3);
    end if;
  end process;

Por último tenemos las instanciaciones de los bloques BRAM:

Código:
  ram0 : RAMB16_S9
  generic map (
    init_00 => X"579500912A0140F5FFFFFFFF7CF003E00FE50BE080B8FFBFAAAA9057550A0000",
    ...
    init_3F => X"AA8AAA0800EAFFFFFE3FF5E39F48B2507716F4FF00E41EF00404202010080000",
    write_mode => "READ_FIRST")
  port map (
    do    => doa(0),
    addr  => addr(10 downto 0),
    clk   => clk,
    en    => ena(0),
    we    => '0',
    ssr   => '0');

  ram1 : RAMB16_S9
  generic map (
    init_00 => X"5555545540B5FFFFEE3FFAA33F885153EFA8000000CC21F0A150858A04010000",
    ...
    init_3F => X"0540FA15F7FFFF01A82A1800050080AB00D0A900D83E008F011000E0FF3F0E00",
    write_mode => "READ_FIRST")
  port map (
    do    => doa(1),
    addr  => addr(10 downto 0),
    clk   => clk,
    en    => ena(1),
    we    => '0',
    ssr   => '0');

  ram2 : RAMB16_S9
  generic map (
    init_00 => X"0BFFF555FFDB9B0154150C008000E05500A4E900607F808E0000000000000000",
    ...
    init_3F => X"00000042D08F01A83CFE7F010000000000000009545755555555250000000000",
    write_mode => "READ_FIRST")
  port map (
    do    => doa(2),
    addr  => addr(10 downto 0),
    clk   => clk,
    en    => ena(2),
    we    => '0',
    ssr   => '0');

  ram3 : RAMB16_S9
  generic map (
    init_00 => X"0808080B0B0B08080808084828284848282948480D2818080808080101010101",
    ...
    init_17 => X"38383D3D2938323232323278787D78787879797D7D4F06060606060606060606",
    write_mode => "READ_FIRST")
  port map (
    do    => doa(3),
    addr  => addr(10 downto 0),
    clk   => clk,
    en    => ena(3),
    we    => '0',
    ssr   => '0');
end Behavioral;

En resumen, lo que hemos hecho es demultiplexar a la señal ena(x) en función de los 2 bits más significativos de addr, y multiplexar doa(x) a una salida común al bloque vram. Los datos con los que se inicializa la BRAM los he obtenido con la herramienta bramgen, concretamente del archivo gameover.txt que hemos indicado al principio de la lección.

Y listo. Esta lección es bastante más complicada que la anterior, ya que requiere saber cómo funciona la ULA del ZX Spectrum. Si no se entiende bien, mirar los esquemas del clon Superfo 128, el libro de Chris Smith o hacer una simulación con iSim para ver los cronogramas de las señales y entender así el comportamiento.

Si el objetivo es sólo mostrar la imagen, podríamos haberlo hecho de otra manera más fácil. Pero como el objetivo final es clonar un ZX Spectrum, he respetado los timings de la ULA, así en lecciones futuras habría que hacer menos cambios.

En la siguiente lección meteremos un Z80, usaremos 16K de ROM y otros 16K de RAM mediante bloques BRAM. En resumen, implementaremos un ZX Spectrum 16K sin teclado, con lo que seguramente le meteré un juego IF2 a la ROM para ver alguna animación en pantalla.


Archivo(s) adjuntos Miniatura(s)
   

.zip  clown_leccion2.zip (Tamaño: 39.47 KB / Descargas: 94)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
12-06-2013, 07:56 PM (Este mensaje fue modificado por última vez en: 12-06-2013 11:30 PM por admin.)
Mensaje: #4
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
Siguiendo las recomendaciones de McLeod en este hilo:

http://zonadepruebas.com/viewtopic.php?f...=10#p23862

Voy a cambiar algunas cosas para mejorar la legibilidad y la portabilidad. Por otro lado, voy a hacer el código compatible para la placa OLS, que por lo que veo es más común. No voy a dar soporte a ninguna placa más, la idea es que captéis cómo hacer una máquina genérica, y cómo adaptarla al hardware en cuestión. El pinout que uso es el mismo del clon de jepalza, aquí lo tenéis (sólo estoy usando los 5 pines de VIDEO):

[Imagen: usercontent,img,1356005798]

Las diferencias entre ambas placas son tres (son más, pero para las lecciones que llevamos estas son las relevantes):
  1. El reloj de entrada. En el caso de la OLS es de 50MHz, y en el de la MOD-VGA es de 25MHz.
  2. Las salidas de color. La placa OLS no tiene DAC (serían las 4 señales R,G,B,I), y la MOD-VGA sí que tiene, una para cada componente (9 señales a la salida).
  3. La memoria RAM interna y las puertas equivalentes. Las FPGAs son ligeramente distintas. La de la OLS es una xc3s250e y la de la MOD-VGA es una xc3s200a. En la práctica, la FPGA de la OLS tiene más "puertas equivalentes" pero sólo tiene 24K de RAM; sin embargo la de la MOD-VGA es más pequeña en cuanto a puertas pero tiene más RAM, en concreto 32K.


Para estas 2 primeras lecciones no nos importa la RAM, aunque para las siguientes sí que es un problema, porque en una OLS no cabe un ZX Spectrum 16K, hay que caparlo, ya sea reduciendo a la mitad la ROM o ídem con la RAM. En la MOD-VGA sí que cabe un ZX Spectrum 16K completo. Estamos contando con la RAM interna de la FPGA, si añadimos RAM externa sí que caben modelos superiores.

Voy a "traducir" sólo la lección 1, os dejo como ejercicio hacer lo mismo con la lección 2. Este es el diagrama de cómo está implementada la lección 1 hasta ahora:

   

De todos los módulos (archivos .vhd) hay uno especial llamado TLD ó Top Level Design. Yo lo llamé main.vhd, pero a partir de ahora lo llamaré TLDxxx.vhd siendo xxx una referencia a la implementación.

Por otro lado, voy a llamar lec1.vhd a la máquina que quiero diseñar, en concreto es un generador de carta de ajuste. El objetivo para buscar la máxima portabilidad es que no haya que modificar ni lec1.vhd ni los módulos que vayan por debajo. Así que lo que haré será meter la parte dependiente de la arquitectura fuera.

Aquí tienen un diagrama de como quedaría el diseño con la placa OLS:

   

Y así sería el diseño de la placa MOD-VGA

   

Y estos serían los archivos de la lección 1 siguiendo el nuevo paradigma.


Archivo(s) adjuntos Miniatura(s)
   

.zip  leccion1_nueva.zip (Tamaño: 2.85 KB / Descargas: 88)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
13-06-2013, 12:37 PM
Mensaje: #5
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
Ya he traducido la lección 2. Ahora empleo otro método distinto (más sencillo) para generar la ROM, que además es independiente de la plataforma. Para ello hay que pasar el archivo .scr por la nueva herramienta vHexArr creada para la ocasión.

Como en la placa OLS hay 3 formas distintas de sacar RGB:

http://zonadepruebas.com/viewtopic.php?f...997#p23997

Dejo como ejercicio que cada cual adapte los fuentes a la suya, creando los archivos TLDols.vhd y ols.ucf. A partir de ahora (lección 3 en adelante), sólo publicaré los fuentes de la máquina que voy a implementar: archivos lecx.vhd, módulos inferiores y archivos auxiliares como binarios de roms. Los archivos TLDmodvga.vhd, modvga.ucf, TLDols.vhd, ols.ucf no irán incluidos.


Archivo(s) adjuntos
.zip  leccion2_nueva.zip (Tamaño: 42.26 KB / Descargas: 85)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
16-06-2013, 02:25 AM (Este mensaje fue modificado por última vez en: 16-06-2013 02:48 AM por admin.)
Mensaje: #6
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
En esta nueva lección voy a implementar una aproximación a un Spectrum 16K. Los motivos son varios: en lugar de 16K de ROM voy a implementar 1K, la contención es muy simple (sólo ejecutamos la CPU durante los bordes superior, inferior y retrazo vertical) y no tenemos teclado, sonido, ni cualquier otra cosa accesible por los puertos.

Para que el resultado sea vistoso y teniendo en cuenta que aún no tenemos teclado (una pantalla en blanco con el texto de copyright no me pareció adecuado) he escogido este juego de 16K:

http://www.worldofspectrum.org/infoseeki...id=0002560

Como no se necesita toda la ROM (de hecho lo único que se ejecuta es la interrupción de la ROM que escanea el teclado) he reducido la ROM al primer Kb, así cabe sin problemas en placas cuya FPGA tiene la RAM interna limitada, como la OLS o la Papillo One 250K. Lo único que he parcheado de la ROM son 6 bytes al comienzo, para inicializar SP y saltar al comienzo del juego. El juego funciona porque la RAM de la FPGA (al contrario de lo que podría esperarse en una RAM) se inicializa con el contenido de la memoria que tendría el juego al comenzar. En este juego tan sólo he puesto un punto de ruptura en $64F6, y tras esto un volcado de la memoria desde $4000 hasta $8000 (con Spectaculator se hace con File->Export). Para pasar de binarios (tanto ROM como RAM) a código fuente VHDL he empleado la herramienta vHexArr disponible en el archivo anterior (leccion2_nueva.zip).

Ya que estamos en materia, voy a empezar mostrando el código de los módulos rom.vhd y ram.vhd. El fuente VHDL de la ROM es el siguiente:

Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity rom is port(
    clk   : in  std_logic;
    addr  : in  std_logic_vector(9 downto 0);
    dout  : out std_logic_vector(7 downto 0));
end rom;

architecture behavioral of rom is

  type rom_t is array (0 to 1023) of std_logic_vector(7 downto 0);
  signal rom : rom_t := (
  X"F3", X"31", X"40", X"7C", X"C3", X"F6", X"64", X"11", --0000
...
  X"EF", X"31", X"27", X"C0", X"03", X"34", X"EC", X"6C");--03F8

begin

  process (clk)
  begin
    if rising_edge(clk) then
      dout <= rom(to_integer(unsigned(addr)));
    end if;
  end process;

end behavioral;

Como parámetros de entrada tenemos clk (las BRAM son síncronas), addr y dout. El funcionamiento es muy sencillo, en cada flanco de subida de clk (lo alimentamos con 7MHz) este bloque se encarga de poner en la salida dout (8 bits) el contenido del byte al que apunta addr (en este caso 10 bits, porque estamos implementando una memoria de 1024 bytes).

Luego viene la inicialización de la memoria, donde primero nos creamos un tipo array de 1K, y luego definimos la variable (con signal) de dicho tipo, inicializándola con el contenido de la ROM que queremos implementar.

El código tras el begin es un simple process que se dispara en cada ciclo de reloj y donde pasamos del array "rom" a la salida "dout" indexando con la entrada "addr". Supongo que es la primera vez que veis el to_integer(unsigned(x)). Es lo que en programación se conoce como "casting" o conversión de tipos, y es muy habitual en VHDL, donde continuamente pasamos de entero a vector de bits y viceversa. En este caso necesitamos un entero para indexar el array, pero tenemos un vector de bits en "addr", por eso hacemos la conversión.

Visto el código de rom.vhd, no os costará mucho entender el de ram.vhd:

Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ram is port(
    clk   : in  std_logic;
    wr    : in  std_logic;
    addr  : in  std_logic_vector(13 downto 0);
    din   : in  std_logic_vector( 7 downto 0);
    dout  : out std_logic_vector( 7 downto 0));
end ram;

architecture behavioral of ram is

  type ram_t is array (0 to 16383) of std_logic_vector(7 downto 0);
  signal ram : ram_t := (
  X"E7", X"DF", X"FF", X"FF", X"FF", X"FF", X"FF", X"FF", --0000
...
  X"00", X"00", X"00", X"00", X"00", X"00", X"00", X"00");--3FF8

begin

  process (clk)
  begin
    if(rising_edge(clk)) then
      if wr='1' then
        ram(to_integer(unsigned(addr))) <= din;
      end if;
      dout <= ram(to_integer(unsigned(addr)));
    end if; 
  end process;

end behavioral;

Aquí tenemos dos nuevos parámetros de entrada: wr y din. El primero, "wr", es para indicar que queremos escribir, es una señal activa a nivel alto (al contrario por ejemplo del pin análogo en el Z80). El segundo parámetro, "din", es para indicar a esta memoria el dato que queremos escribir en caso de que "wr" esté activa.

Ya hemos acabado con las memorias. Antes de entrar en el código principal voy a explicar por encima el módulo de la CPU. Se trata de la implementación llamada T80 que hay en opencores, aunque me la he badajo de fpgaarcade.com que está un poco más actualizada. Existen 4 opciones de CPU dentro de estas librerías: T80a, T80s, T80se y T8080se. Yo he escogido la primera, por ser la más parecida a una CPU real, ya que es asíncrona. Las demás versiones son síncronas. Por otro lado existen dos implementaciones para el banco de registros: T80_Reg y T80_RegX. He optado por la segunda, ya que aunque sea dependiente de la arquitectura (Xilinx) ofrece mejores resultados (menos recursos) a la hora de compilar. Ya sabéis, si tenéis una Altera u otra FPGA distinta de Xilinx, cambiáis el archivo T80_RegX por el T80_Reg.

He metido todos los fuentes de la CPU en un directorio distinto, T80, para no tenerlo todo mezclado en la raíz, aunque esto es irrelevante. El ISE Webpack se encarga automáticamente de añadir los fuentes que agreguemos en la jerarquía de módulos donde corresponda.

Ahora sí, entremos en materia de verdad, explicando el módulo principal lec3.vhd:

Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity lec3 is port(
    clk7  : in  std_logic;
    sync  : out std_logic;
    r     : out std_logic;
    g     : out std_logic;
    b     : out std_logic;
    i     : out std_logic);
end lec3;

architecture behavioral of lec3 is

  signal  hcount  : unsigned  (8 downto 0);
  signal  vcount  : unsigned  (8 downto 0);
  signal  vid     : std_logic;
  signal  viddel  : std_logic;
  signal  at2clk  : std_logic;
  signal  al1     : std_logic;
  signal  al2     : std_logic;
  signal  ccount  : unsigned  (4 downto 0);
  signal  flash   : unsigned  (4 downto 0);
  signal  at1     : std_logic_vector (7 downto 0);
  signal  at2     : std_logic_vector (7 downto 0);
  signal  da1     : std_logic_vector (7 downto 0);
  signal  da2     : std_logic_vector (7 downto 0);
  signal  addrv   : std_logic_vector (13 downto 0);
  signal  wrv     : std_logic;
  signal  clkcpu  : std_logic;
  signal  abus    : std_logic_vector (15 downto 0);
  signal  dbus    : std_logic_vector (7 downto 0);
  signal  din_rom : std_logic_vector (7 downto 0);
  signal  din_ram : std_logic_vector (7 downto 0);
  signal  mreq_n  : std_logic;
  signal  iorq_n  : std_logic;
  signal  wr_n    : std_logic;
  signal  rd_n    : std_logic;
  signal  int_n   : std_logic;

De las señales nuevas, empecemos por las que van directamente al Z80: abus, dbus, mreq_n, iorq_n, wr_n, rd_n e int_n. Excepto abus y dbus, que son el bus de direcciones y el bus de datos respectivamente, el resto se llaman igual que los pines de la CPU a los que van conectadas. La "_n" del final es para indicar que la señal es negada (activa a nivel bajo).

Luego tenemos las señales din_rom y din_ram. Como las salidas de las memorias son siempre salidas, y el bus de datos de la CPU (dbus) puede ir en ambas direcciones, necesitamos un mecanismo para enrutar de la salida de la ROM o la RAM hacia la CPU cuando ésta quiera leer un dato. Estas señales están para hacer dicha multiplexación.

La siguiente señal que voy a explicar es clkcpu, es la señal de reloj con la que alimentaremos la CPU, y será de 3.5MHz mientras esté activa y 0 si está inactiva. La contención que se aplica es muy sencilla (y distinta a la del spectrum original): durante el borde superior, borde inferior y retrazo vertical activamos el reloj; el resto del tiempo (192 líneas centrales) paramos el reloj para que no interfiera con las lecturas de RAM de la ULA.

Por último tenemos la señal wrv, con la cual alimentamos la entrada de escritura de la RAM. Estará desactivada durante las lecturas de la ULA, y sólo la activará la CPU cuando proceda: una escritura en memoria en el rango $4000-$7FFF. A diferencia de las señales de la CPU, es activa a nivel alto (lógica positiva).

Sigamos con el código:

Código:
  component ram is port(
      clk   : in  std_logic;
      wr    : in  std_logic;
      addr  : in  std_logic_vector(13 downto 0);
      din   : in  std_logic_vector( 7 downto 0);
      dout  : out std_logic_vector( 7 downto 0));
  end component;

  component rom is port(
      clk   : in  std_logic;
      addr  : in  std_logic_vector(9 downto 0);
      dout  : out std_logic_vector(7 downto 0));
  end component;

  component T80a is port(
      RESET_n : in std_logic;
      CLK_n   : in std_logic;
      WAIT_n  : in std_logic;
      INT_n   : in std_logic;
      NMI_n   : in std_logic;
      BUSRQ_n : in std_logic;
      M1_n    : out std_logic;
      MREQ_n  : out std_logic;
      IORQ_n  : out std_logic;
      RD_n    : out std_logic;
      WR_n    : out std_logic;
      RFSH_n  : out std_logic;
      HALT_n  : out std_logic;
      BUSAK_n : out std_logic;
      A       : out std_logic_vector(15 downto 0);
      D       : inout std_logic_vector(7 downto 0));
  end component;

Aquí simplemente hemos declarado los módulos que vamos a utilizar: ram, rom y T80a. Lo interesante viene ahora, en la instanciación, que es donde le decimos cómo conectar dichos módulos:

Código:
begin

  ram_inst: ram port map (
    clk   => clk7,
    wr    => wrv,
    addr  => addrv,
    din   => dbus,
    dout  => din_ram);

  rom_inst: rom port map (
    clk   => clk7,
    addr  => abus(9 downto 0),
    dout  => din_rom);

  T80a_inst: T80a port map (
    RESET_n => '1',
    CLK_n   => clkcpu,
    WAIT_n  => '1',
    INT_n   => int_n,
    NMI_n   => '1',
    BUSRQ_n => '1',
    MREQ_n  => mreq_n,
    IORQ_n  => iorq_n,
    RD_n    => rd_n,
    WR_n    => wr_n,
    A       => abus,
    D       => dbus);

La RAM va conectada al reloj de 7MHz, la salida va a din_ram, la entrada va directamente al bus de datos de la CPU, dbus, y se emplea cuando queremos escribir en RAM, y las señales de dirección y escritura (addrv y wrv) las generamos específicamente ya que tenemos que hacer de árbitro entre la CPU y la ULA, ya que ambas van a querer acceder a dicha RAM.

La ROM es mucho más sencilla, va conectada al mismo reloj de 7MHz, a los 10 bits bajos del bus de direcciones (no olvidemos que sólo implementamos 1K de ROM) y la salida va a la señal din_rom.

Por último tenemos la CPU. Las entradas que no utilizamos de momento las ponemos a '1'. Las entradas que utilizamos son clkcpu, que es el reloj que he explicado antes, e int_n, que es la entrada de interrupción que activamos una vez en cada frame. Las salidas que no utilizamos no hace falta ponerlas, por eso no están, y las que utilizamos van a señales que he explicado ya: mreq_n, iorq_n, rd_r, wr_n, abus y dbus.

Prosigamos:

Código:
  process (clk7)
  begin
    if falling_edge( clk7 ) then
      if hcount=447 then
        hcount <= (others => '0');
        if vcount=311 then
          vcount <= (others => '0');
          flash <= flash + 1;
        else
          vcount <= vcount + 1;
        end if;
      else
        hcount <= hcount + 1;
      end if;

Mismo código que en la lección 2. Generamos 3 contadores en cascada: hcount va desde 0 hasta 447 y se incrementa en cada flanco de bajada de clk7, vcount va desde 0 hasta 311 y se incrementa cada vez que hcount llega al final de su cuenta, y flash (contador de frames) va desde 0 hasta 31 y se incrementa en cada pasada de vcount.

Ahora generamos int_n:

Código:
      int_n <= '1';
      if vcount=248 and hcount<32 then
        int_n <= '0';
      end if;

Como es activa a nivel bajo, vale 1 todo el tiempo excepto en los primeros 32 píxeles de la línea 248, que vale 0.

En el siguiene código...
Código:
      da2 <= da2(6 downto 0) & '0';
      if at2clk='0' then
        ccount <= hcount(7 downto 3);
        if viddel='0' then
          da2 <= da1;
        end if;
      end if;
    end if;
... generamos ccount a partir de 5 bits de hcount, siendo una versión más retrasada de ésta. También alimentamos el segundo registro de datos, da2: consigo mismo pero desplazando para ir sacando los píxeles, o bien a partir de da1 si ya hemos "pintado" los 8 bits, para pintar los 8 siguientes. Es exactamente el mismo código de la lección anterior.

Código:
    if rising_edge( clk7 ) then
      if hcount(3)='1' then
        viddel <= vid;
      end if;
    end if;
  end process;

Aquí hemos generado viddel a partir de vid, retrasándola 8 ciclos. Al igual que antes, esto no ha cambiado con respecto a la lección 2.

Código:
  process (al1)
  begin
    if rising_edge( al1 ) then
      da1 <= din_ram;
    end if;
  end process;

  process (al2)
  begin
    if rising_edge( al2 ) then
      at1 <= din_ram;
    end if;
  end process;

  process (at2clk)
  begin
    if rising_edge( at2clk ) then
      if( viddel='0' ) then
        at2 <= at1;
      else
        at2 <= "00111000";
      end if;
    end if;
  end process;

Aquí se han cargado los registros da1 y at1 partiendo de la salida de la ram, en el momento que indican los flancos de subida de al1 y al2 respectivamente. Luego hemos cargado el registro at2 en cada flanco de subida de at2clk, dependiendo de viddel. Si estamos dentro de pantalla leemos del registro at1; si estamos fuera de ella, o sea en el borde, cargamos un color fijo. A diferencia de la lección anterior, aquí hemos puesto un borde fijo de color blanco.

Ahora viene el proceso que genera las componentes de color:

Código:
  process (hcount, vcount, at2, da2(7), flash(4))
  begin
    r <= '0';
    g <= '0';
    b <= '0';
    i <= '0';
    vid   <= '1';
    if  (vcount>=248 and vcount<252) or
        (hcount>=344 and hcount<376) then
      sync <= '0';
    else
      sync <= '1';
      if hcount>=416 or hcount<320 then
        if (da2(7) xor (at2(7) and flash(4)))='0' then
          r <= at2(4);
          g <= at2(5);
          b <= at2(3);
        else
          r <= at2(1);
          g <= at2(2);
          b <= at2(0);
        end if;
        i <= at2(6);
        if hcount<256 and vcount<192 then
          vid <= '0';
        end if;
      end if;
    end if;
  end process;

Es un código muy parecido al de la lección anterior, aunque un poco más largo. He prescindido de la señal intermedia gencol porque desde mi punto de vista queda más claro así (en lugar de tener dos procesos desperdigados tenemos uno sólo).

En el siguiente proceso...
Código:
  process (hcount, vid)
  begin
    al1 <= '1';
    al2 <= '1';
    if vid='0' then
      if hcount(3 downto 1)=3 or hcount(3 downto 1)=5 then
        al1 <= '0';
      end if;
      if hcount(3 downto 1)=4 or hcount(3 downto 1)=6 then
        al2 <= '0';
      end if;
    end if;
  end process;

...hemos metido todo en un bloque if, de tal forma que al1 y al2 se activan sólo durante la generación de la pantalla (y no durante el borde ni los retrazos). Antes (en la lección 2) nos daba igual porque no había otro elemento (CPU) que quisiera acceder a la memoria de video, de hecho dicha memoria era una ROM.

Veamos ahora otro proceso que ha cambiado:

Código:
  process (al1, al2, vcount, ccount, abus, mreq_n)
  begin
    if (al1 and al2)='0' then
      wrv <= '0';
      if al1='0' then
        addrv <= '0' & std_logic_vector(vcount(7 downto 6) & vcount(2 downto 0)
                  & vcount(5 downto 3) & ccount);
      else
        addrv <= "0110" & std_logic_vector(vcount(7 downto 3) & ccount);
      end if;
    else
      wrv <= not (wr_n or mreq_n or abus(15) or not abus(14));
      addrv <= abus(13 downto 0);
    end if;
  end process;

Antes addrv conmutaba entre leer un byte o leer un atributo. Ahora, además, tenemos que arbitrar con la CPU cuando estén desactivadas al1 y al2, por eso hemos añadido el if externo. Además, ahora se da la circunstancia de que la memoria de video es de 16K y no e 8K, por lo que concatenamos un bit más (un '0' a la izquierda tanto en byte como en atributo). Al ser una RAM tenemos que generar la señal de escritura, wrv, que es la versión negada de wr_n en accesos a memoria (mreq_n=0) de video (a15=0 y a14=1). Esta señal, wrv, se desactiva en los accesos de la ULA (wrv <= '0').

Lo siguiente es la generación de at2clk, igual que en la lección 2:

Código:
  process (clk7, hcount)
  begin
    at2clk <= not clk7 or hcount(0) or not hcount(1) or hcount(2);
  end process;

Veamos ahora los últimos dos procesos, que además son los más importantes de la lección. El primero de ellos es el multiplexador del bus de datos. Como sólo tenemos 2 elementos de memoria y la CPU (de momento no se usan los puertos), las posibilidades son estas:
  • Lectura de ROM
  • Lectura de RAM
  • Escritura de RAM

Teniendo en cuenta que el bus de datos de la CPU es de entrada/salida, la única forma de que la CPU pueda escribir en la RAM es si ponemos dbus en alta impedancia (others => 'Z'); de lo contrario habría una colisión de salidas entre la CPU y la señal que generamos. La premisa por tanto es dejar por defecto la señal dbus en alta impedancia, salvo que querramos leer de la ROM/RAM. Esta condición se testea en el if más externo (mreq_n=rd_n=0). Lo que hacemos con el if interno es conectar el bus a la salida de la ROM o a la salida de la RAM, dependiendo de los 2 bits más significativos el bus de direcciones. Como no tenemos RAM por encima de $8000, cualquier lectura por encima de $4000 activaría nuestra RAM de 16K, comportándose los rangos $8000-$BFFF y $C000-$FFFF como un espejo de $4000-$7FFF. No es lo deseable (en un Spectrum 16K no se producen escrituras por encima de $8000 y todas las lecturas devuelven $FF) pero de momento lo dejamos así, ya lo puliremos más adelante.

Tras la explicación, aquí tenemos el proceso en cuestión:

Código:
  process (rd_n, wr_n, mreq_n, abus)
  begin
    dbus <= (others => 'Z');
    if mreq_n='0' and rd_n='0' then
      if abus(15 downto 14)="00" then
        dbus <= din_rom;
      else
        dbus <= din_ram;
      end if;
    end if;
  end process;

El último proceso es digamos "la chapucilla" que me he inventado para hacer funcionar el clon sin tener todavía implementada la contención del ZX Spectrum, que es más complicada y requiere de más procesos/ecuaciones. Simplemente paramos el reloj cuando el contador de línea es menor o igual a 192, y lo ponemos en marcha cuando es mayor. De esta forma podemos estar seguros de que mientras el reloj de la CPU está andando la ULA está inactiva y viceversa, aunque habrá periodos en los que CPU y ULA estén inactivos y estemos desperdiciando ciclos innecesarios. De los 3.5MHz disponibles sólo estamos aprovechando el 40% del tiempo, lo que equivaldría a un reloj de 1.35MHz. En una futura lección implementaremos la contención "como dios manda". De momento nos apañamos con esto para tener un clon funcional.

La próxima lección la dedicaré íntegramente al teclado. Un teclado matricial sería lo más sencillo a nivel de hardware (menos código VHDL que picar), pero tiene los inconvenientes de necesitar muchos pines de la FPGA (en concreto, 13), tener físicamente dicho teclado (quitándoselo a un Spectrum) y conectarlo a la FPGA con su correspondiente conector de faja. Así que lo que haremos será implementar la interfaz necesaria para que funcione con un teclado de PC, en concreto PS/2 que tiene una norma más sencilla. Por suerte la placa MOD-VGA dispone de conector PS/2 y no hay que hacer nada hardware salvo puentearlo a la FPGA. Para otras placas como la OLS hay que colocar dicho conector, que sigue siendo más cómodo que adaptar un teclado matricial.

Para terminar os dejo un video donde podéis ver el resultado. He aprovechado ya que estaba para cargar los bitstreams de las lecciones 1 y 2 previamente. Recuerdo que no se incluyen en el zip ni el módulo TLD ni el archivo .ucf, tendréis que usar los de la última lección.





Archivo(s) adjuntos
.zip  clown_leccion3.zip (Tamaño: 42.86 KB / Descargas: 82)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
21-06-2013, 01:43 PM (Este mensaje fue modificado por última vez en: 25-07-2013 03:56 PM por admin.)
Mensaje: #7
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
En esta lección añadimos dos nuevas características al clon: una contención exacta a nivel de ciclo y un teclado. Como el código es muy parecido al de la lección 3, en lugar de repasarlo todo voy a mostrar sólo las partes nuevas.

La contención es el método que se emplea para que varios dispositivos (CPU y generador de video) puedan acceder a un mismo elemento de memoria. Normalmente lo que se hace es parar la CPU durante algunos ciclos, ya que el generador de video tiene un patrón constante de lecturas y perder un byte significa que se mostrará al menos un pixel incorrectamente. Hay alternativas para evitar perder ciclos de la CPU, como usar memorias de dos puertos o memorias más rápidas y multiplexar el bus en el tiempo, pero generalmente la solución más económica es hacer que la CPU "espere".

Para diseñar un sistema de contención hay que conocer muy bien cómo funciona la CPU, los mecanismos que hay para "congelarla en el tiempo" y los tiempos de acceso de las memorias. Para empezar separamos la memoria de video del resto en un banco aparte que direccionamos en $4000-$7fff (la memoria de video está en $4000-$5aff). Del resto de memoria (tanto RAM como ROM) no nos preocupamos, ya que la CPU hace uso exclusivo de ella. El método que Richard Altwasser empleó para detener la CPU fue tan sencillo como generar la señal de reloj que la alimenta, que serán ciclos de 3.5MHz en el caso de que esté activado. La clave de todo está en las condiciones que deben darse para activar o no activar dicho reloj, que en concreto dependen de 2 biestables y de algunas señales del Z80, éste es el circuito en cuestión.

   

Este circuito introduce una pausa en la CPU de entre 0 y 6 ciclos de reloj sólo si estamos escribiendo la ULA está pintando en pantalla y si la CPU accede a la memoria contenida o al puerto $FE. El patrón de pausas (o ciclos de espera) viene explicado en este enlace (si os calentáis mucho la cabeza podéis deducirlo del circuito de arriba).
http://www.worldofspectrum.org/faq/refer...Contention

Nuestra tarea consiste, por tanto, en describir el circuito mediante VHDL. Los elementos de memoria son dos biestables, a los que llamaremos cbis1 (el de arriba) y cbis2 (el de abajo). Primero los declaramos:

Código:
  signal  cbis1   : std_logic;
  signal  cbis2   : std_logic;

Ahora implementamos el biestable de arriba (cbis1). Las señales del circuito C6 y C7 vienen de un decodificador que depende de las 3 señales: hcount(3), hcount(2), hcount(1). Cuando estas tres señales concatenadas forman un 6 (110), la señal C6 se pone a cero y todas las demás (C0 a C7) se ponen a 1. Como en VHDL no hemos implementado dicho decodificador, lo que hacemos es generar la misma señal de forma equivalente. En el circuito no se ve, pero el decodificador se desactiva en el caso de que vid valga 1, por eso lo incluímos en la ecuación.


Código:
    if falling_edge( clk7 ) then
    ...
      cbis1 <= vid nor (hcount(3) and hcount(2));
    end if;

Vayamos al segundo biestable. En este caso el biestable se activa con el flanco positivo de clkcpu, señal que todavía no hemos generado (cosas que tiene la realimentación). La señal IORQULA del circuito, no la tenemos en nuestras ecuaciones, pero se obtiene fácilmente mediante un or entre /IORQ y A0.

Código:
  process (clkcpu)
  begin
    if rising_edge( clkcpu ) then
      cbis2 <= (iorq_n or abus(0)) and mreq_n;
    end if;
  end process;

Por último generamos el reloj que va a la CPU, clkcpu, a partir de hcount(0) (señal de reloj continua de 3.5MHz), cbis1, cbis2, y combinacionales de salidas del Z80.

Código:
  process (hcount(0), cbis1, cbis2, iorq_n, abus)
  begin
    clkcpu <= hcount(0) or (cbis1 and ((abus(15) or not abus(14)) nand (iorq_n or abus(0))) and cbis2);
  end process;

Como veis la contención es sencilla de implementar, a pesar de lo difícil que es comprenderla (y explicarla).

Hago la prueba con el jueguecillo (ROM de 8K) y todo va como la seda. El problema viene después, al meterle la ROM de 16K del Spectrum: en las columnas impares se corrompen los atributos. Y lo único diferente ha sido el tamaño de la ROM, no el contenido. Al final conseguí solucionarlo, más o menos supe intuir donde estaba el problema. Las ecuaciones las he creado en función de un esquemático, compuesto por diversos componentes discretos de la familia 74HCXX (HCMOS). Dicho diseño está optimizado minimizando el número de integrados, con la contrapartida de tener que relajar ciertas reglas de diseño digital. La regla en cuestión es que no se deben generar relojes (o cualquier señal que active por flanco elementos secuenciales) empleando circuitos combinacionales. Dicha regla la hemos roto al generar las señales al1 y al2, y para subsanarlo tenemos que rehacer algunas ecuaciones. En concreto quitamos esto (la propia generación de las señales):

Código:
  process (hcount, vid)
  begin
    al1 <= '1';
    al2 <= '1';
    if vid='0' then
      if hcount(3 downto 1)=3 or hcount(3 downto 1)=5 then
        al1 <= '0';
      end if;
      if hcount(3 downto 1)=4 or hcount(3 downto 1)=6 then
        al2 <= '0';
      end if;
    end if;
  end process;

Y cambiamos estos fragmentos de código:

Código:
  process (al1)
  begin
    if rising_edge( al1 ) then
      da1 <= din_ram;
    end if;
  end process;

  process (al2)
  begin
    if rising_edge( al2 ) then
      at1 <= din_ram;
    end if;
  end process;

    if (al1 and al2)='0' then
      wrv <= '0';
      if al1='0' then
        addrv <= '0' & std_logic_vector(vcount(7 downto 6) & vcount(2 downto 0)

Por estos otros:

Código:
    if falling_edge( clk7 ) then
      ...
      if vid='0' then
        if (hcount(1) and (hcount(2) xor hcount(3)))='1' then
          da1 <= din_ram;
        end if;

    if falling_edge( clk7 ) then
      ...
        if (not hcount(1) and hcount(3))='1' then
          at1 <= din_ram;
        end if;

    if (vid or (hcount(3) xnor (hcount(2) and hcount(1))))='0' then
      wrv <= '0';
      if (hcount(1) and (hcount(2) xor hcount(3)))='1' then
        addrv <= '0' & std_logic_vector(vcount(7 downto 6) & vcount(2 downto 0)

Finalizada la contención y resuelto el problema que nos ha aparecido, el siguiente paso es dotar al clon de entrada/salida a través de puertos: borde, teclado, sonido, entrada cassette, etc... Lo más básico de todo es el teclado, porque sin él poco podemos hacer con el clon. Un teclado matricial, como el que tiene el Spectrum, es fácil de implementar en VHDL, pero tiene el inconveniente de que es difícil de conseguir, aparatoso de conectar y requiere de muchos pines libres en la FPGA. Con un teclado de PC nos ahorramos dichos inconvenientes, aunque tenemos que currarnos la interfaz en VHDL. Y de eso es de lo que va a tratar la segunda parte de esta lección.

Lo primero es definir los nuevos pines que vamos a usar en el TLD:

Código:
entity TLDmodvga is port(
    clk     : in  std_logic;
    sync    : out std_logic;
    rout    : out std_logic_vector (2 downto 0);
    gout    : out std_logic_vector (2 downto 0);
    bout    : out std_logic_vector (2 downto 0);
    flashcs : out std_logic;
    clkps2  : in  std_logic;
    dataps2 : in  std_logic);
end TLDmodvga;

E indicar en el archivo .ucf con qué pines se corresponde en la FPGA, si os dais cuenta, a diferencia del resto de señales, éstas tienen niveles TTL en lugar de CMOS a 3.3V:

Código:
net "flashcs" loc = "p27" | iostandard= lvcmos33;
net "clkps2"  loc = "p53" | iostandard= lvttl;
net "dataps2" loc = "p51" | iostandard= lvttl;

La señal flashcs es para deshabilitar la Flash ROM, ya que los pines que vamos a emplear para el puerto PS/2 (clkps2 y dataps2) coinciden con los del SPI de dicha ROM y no queremos que las señales de la Flash interfieran con el puerto PS/2. En realidad podríamos haber obtenido los pines de otro lado (INT, VSYNC), he escogido estos dos por simplicidad al no tener que hacer soldaduras en la placa MOD-VGA. Para deshabilitar la Flash ROM lo único que hay que hacer es poner a 1 dicha señal.

Código:
  flashcs <= '1';

Creamos un nuevo módulo, llamado ps2k.vhd, y lo declaramos e instanciamos en el archivo lec4.vhd:

Código:
  component ps2k is port(
      clk     : in  std_logic;
      ps2clk  : in  std_logic;
      ps2data : in  std_logic;
      rows    : in  std_logic_vector(7 downto 0);
      keyb    : out std_logic_vector(4 downto 0));
  end component;

  ps2k_inst: ps2k port map (
    clk     => hcount(5),
    ps2clk  => clkps2,
    ps2data => dataps2,
    rows    => abus(15 downto 8),
    keyb    => kbcol);

Tanto el reloj como el dato lo genera el teclado. En realidad el host puede controlar el bus, desactivándolo o incluso enviando datos al teclado como los indicadores de bloqueo a los 3 leds, pero nosotros para simplificar nos remitimos a hacer lecturas constantemente. Para una información más detallada sobre el protocolo os aconsejo leer este enlace:

http://www.computer-engineering.org/ps2protocol/

La señal ps2clk es el reloj que genera el teclado, de entre 10 y 16.7 KHz. Como es una señal ruidosa no es aconsejable leerla directamente, lo que hacemos es ir muestreando con otra señal que nosotros mismos creamos, hcount(5), de 109KHz y así nos evitamos los posibles glitches.

Estas son las primeras líneas del archivo ps2k.vhd:

Código:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ps2k is port (
    clk     : in  std_logic;
    ps2clk  : in  std_logic;
    ps2data : in  std_logic;
    rows    : in  std_logic_vector(7 downto 0);
    keyb    : out std_logic_vector(4 downto 0));
end ps2k;

architecture behavioral of ps2k is

  type    key_matrix  is array (7 downto 0) of std_logic_vector(4 downto 0);
  signal  keys      : key_matrix;
  signal  pressed   : std_logic;
  signal  lastclk   : std_logic;
  signal  bit_count : unsigned (3 downto 0);
  signal  shiftreg  : std_logic_vector(8 downto 0);
  signal  parity    : std_logic;

Además de clk y ps2clk que ya he explicado, el módulo contiene 3 parámetros más: ps2data, rows y keyb. La primera de ellas, ps2data, es la línea de datos mediante la cual recibimos los códigos de las teclas pulsadas/soltadas, que debemos muestrear en los flancos de bajada de ps2clk. En rows, le indicamos qué filas queremos leer del teclado, que hemos conectado con el byte alto del bus de direcciones. El resultado nos lo devuelve el módulo en el parámetro keyb (dependiendo de los 40 bits de estado del teclado y de las filas que nos solicite "rows"). Este parámetro va conectado a la señal kbcol dentro del módulo lec4.vhd.

Vayamos ahora a las señales del propio módulo. Primero definimos el tipo key_matrix, un array de 8 filas por 5 columnas de un bit que representa el estado de las 40 teclas del Spectrum. Luego definimos la variable keys con el tipo recién creado. A continuación tenemos pressed, que usaremos a modo de flag para indicar si la tecla en cuestión ha sido pulsada o soltada (cuando una tecla es soltada va precedida del código $f0). Para detectar los flancos de bajada de ps2clk necesitamos almacenar el estado anterior de dicha señal, y esto lo haremos en lastclk. La siguiente señal es bit_count, que es para llevar la cuenta de los bits que hemos leído. Tras esta tenemos shiftreg, el registro desplazamiento donde almacenamos el código que vamos leyendo bit a bit. Por último tenemos parity, con el que calculamos la paridad y comprobaremos si es correcta al final de la lectura.

Prosigamos con el código, lo siguiente es un process gigante:

Código:
  process (clk)
  begin
    if rising_edge(clk) then
      lastclk <= ps2clk;
      if ps2clk='0' and lastclk='1' then
        if bit_count=0 then
          parity <= '0';
          if ps2data='0' then
            bit_count <= bit_count + 1;
          end if;
        else
          if bit_count<10 then
            bit_count <= bit_count + 1;
            shiftreg  <= ps2data & shiftreg(8 downto 1);
            parity    <= parity xor ps2data;
          elsif ps2data='1' then
            bit_count <= (others => '0');
            if parity = '1' then
              pressed  <= '1';
              case shiftreg(7 downto 0) is
                when X"f0" => pressed    <= '0';
                when X"12" => keys(0)(0) <= pressed; -- Left shift (CAPS SHIFT)
                when X"59" => keys(0)(0) <= pressed; -- Right shift (CAPS SHIFT)
                when X"1a" => keys(0)(1) <= pressed; -- Z
                when X"22" => keys(0)(2) <= pressed; -- X
                when X"21" => keys(0)(3) <= pressed; -- C
                when X"2a" => keys(0)(4) <= pressed; -- V
                when X"1c" => keys(1)(0) <= pressed; -- A
                when X"1b" => keys(1)(1) <= pressed; -- S
                when X"23" => keys(1)(2) <= pressed; -- D
                when X"2b" => keys(1)(3) <= pressed; -- F
                when X"34" => keys(1)(4) <= pressed; -- G
                when X"15" => keys(2)(0) <= pressed; -- Q
                when X"1d" => keys(2)(1) <= pressed; -- W
                when X"24" => keys(2)(2) <= pressed; -- E
                when X"2d" => keys(2)(3) <= pressed; -- R
                when X"2c" => keys(2)(4) <= pressed; -- T
                when X"16" => keys(3)(0) <= pressed; -- 1
                when X"1e" => keys(3)(1) <= pressed; -- 2
                when X"26" => keys(3)(2) <= pressed; -- 3
                when X"25" => keys(3)(3) <= pressed; -- 4
                when X"2e" => keys(3)(4) <= pressed; -- 5
                when X"45" => keys(4)(0) <= pressed; -- 0
                when X"46" => keys(4)(1) <= pressed; -- 9
                when X"3e" => keys(4)(2) <= pressed; -- 8
                when X"3d" => keys(4)(3) <= pressed; -- 7
                when X"36" => keys(4)(4) <= pressed; -- 6
                when X"4d" => keys(5)(0) <= pressed; -- P
                when X"44" => keys(5)(1) <= pressed; -- O
                when X"43" => keys(5)(2) <= pressed; -- I
                when X"3c" => keys(5)(3) <= pressed; -- U
                when X"35" => keys(5)(4) <= pressed; -- Y
                when X"5a" => keys(6)(0) <= pressed; -- ENTER
                when X"4b" => keys(6)(1) <= pressed; -- L
                when X"42" => keys(6)(2) <= pressed; -- K
                when X"3b" => keys(6)(3) <= pressed; -- J
                when X"33" => keys(6)(4) <= pressed; -- H
                when X"29" => keys(7)(0) <= pressed; -- SPACE
                when X"14" => keys(7)(1) <= pressed; -- CTRL (Symbol Shift)
                when X"3a" => keys(7)(2) <= pressed; -- M
                when X"31" => keys(7)(3) <= pressed; -- N
                when X"32" => keys(7)(4) <= pressed; -- B
                when X"6B" => keys(0)(0) <= pressed; -- Left (Caps 5)
                              keys(3)(4) <= pressed;
                when X"72" => keys(0)(0) <= pressed; -- Down (Caps 6)
                              keys(4)(4) <= pressed;
                when X"75" => keys(0)(0) <= pressed; -- Up (Caps 7)
                              keys(4)(3) <= pressed;
                when X"74" => keys(0)(0) <= pressed; -- Right (Caps 8)
                              keys(4)(2) <= pressed;
                when X"66" => keys(0)(0) <= pressed; -- Backspace (Caps 0)
                              keys(4)(0) <= pressed;
                when X"58" => keys(0)(0) <= pressed; -- Caps lock (Caps 2)
                              keys(3)(1) <= pressed;
                when X"76" => keys(0)(0) <= pressed; -- Break (Caps Space)
                              keys(7)(0) <= pressed;
                when others=> null;
              end case;
            end if;
          else
            bit_count <= (others => '0');
          end if;
        end if;
      end if;
    end if;
  end process;

Vamos a explicarlo desde fuera hacia dentro. Los primeros dos IFs son:

Código:
  process (clk)
  begin
    if rising_edge(clk) then
      lastclk <= ps2clk;
      if ps2clk='0' and lastclk='1' then
      ...
      end if;
    end if;
  end process;

El IF externo muestrea clk en cada flanco de subida. "clk" es una señal de 100KHz que se encarga de muestrear otra, ps2clk, de entre 10 y 16KHz. La detección del flanco de bajada la hacemos en el IF interno, ya que en lastclk almacenamos el ps2clk que habíamos leído en el último muestreo, y sólo en el caso de que "antes había un 1" y "ahora hay un 0" podemos decir que estamos en el flanco de bajada de ps2clk, que es cuando tenemos que leer de ps2data.

En resumen, lo que hay dentro del IF interno se ejecuta en cada flanco de bajada de ps2clk, y es lo siguiente:

Código:
        if bit_count=0 then
          parity <= '0';
          if ps2data='0' then
            bit_count <= bit_count + 1;
          end if;
        else
          if bit_count<10 then
            bit_count <= bit_count + 1;
            shiftreg  <= ps2data & shiftreg(8 downto 1);
            parity    <= parity xor ps2data;
          elsif ps2data='1' then
            bit_count <= (others => '0');
            if parity = '1' then
              pressed  <= '1';
              [bloque case]
            end if;
          else
            bit_count <= (others => '0');
          end if;
        end if;

Antes de explicar este código veamos el cronograma de una lectura genérica:

   

En la primera parte del IF, inicializamos parity a 0 y no incrementamos bit_count hasta que no veamos un 0 en ps2data (el bit de start). La segunda parte del IF (el else) se ejecuta tras haber leído el bit de comienzo (START), y se trata de otro IF con 3 cláusulas. La primera de ellas ocurre mientras estamos leyendo (contador es menor de 10), y lo que hacemos es incrementar el contador, introducir el bit que nos viene de ps2data en el registro desplazamiento shiftreg e ir calculando la paridad. La segunda claúsula se ejecuta en el caso de que hayamos leído los 10 bits, si todo ha ido bien lo siguiente que nos encontraremos el bit de parada (STOP), y en ese caso reseteamos el contador y procesamos el resultado en el caso de que la paridad sea correcta. Por último, la tercera claúsula sólo se ejecuta si el bit de parada es inválido tras leer los 10 bits, lo único que hacemos es resetear el contador.

Veamos ahora lo que ocurre dentro de la segunda claúsula, mostrando el contenido del bloque case:

Código:
            if parity = '1' then
              pressed  <= '1';
              case shiftreg(7 downto 0) is
                when X"f0" => pressed    <= '0';
                when X"12" => keys(0)(0) <= pressed; -- Left shift (CAPS SHIFT)
                when X"59" => keys(0)(0) <= pressed; -- Right shift (CAPS SHIFT)
                when X"1a" => keys(0)(1) <= pressed; -- Z
                when X"22" => keys(0)(2) <= pressed; -- X
                when X"21" => keys(0)(3) <= pressed; -- C
                when X"2a" => keys(0)(4) <= pressed; -- V
                when X"1c" => keys(1)(0) <= pressed; -- A
                when X"1b" => keys(1)(1) <= pressed; -- S
                when X"23" => keys(1)(2) <= pressed; -- D
                when X"2b" => keys(1)(3) <= pressed; -- F
                when X"34" => keys(1)(4) <= pressed; -- G
                when X"15" => keys(2)(0) <= pressed; -- Q
                when X"1d" => keys(2)(1) <= pressed; -- W
                when X"24" => keys(2)(2) <= pressed; -- E
                when X"2d" => keys(2)(3) <= pressed; -- R
                when X"2c" => keys(2)(4) <= pressed; -- T
                when X"16" => keys(3)(0) <= pressed; -- 1
                when X"1e" => keys(3)(1) <= pressed; -- 2
                when X"26" => keys(3)(2) <= pressed; -- 3
                when X"25" => keys(3)(3) <= pressed; -- 4
                when X"2e" => keys(3)(4) <= pressed; -- 5
                when X"45" => keys(4)(0) <= pressed; -- 0
                when X"46" => keys(4)(1) <= pressed; -- 9
                when X"3e" => keys(4)(2) <= pressed; -- 8
                when X"3d" => keys(4)(3) <= pressed; -- 7
                when X"36" => keys(4)(4) <= pressed; -- 6
                when X"4d" => keys(5)(0) <= pressed; -- P
                when X"44" => keys(5)(1) <= pressed; -- O
                when X"43" => keys(5)(2) <= pressed; -- I
                when X"3c" => keys(5)(3) <= pressed; -- U
                when X"35" => keys(5)(4) <= pressed; -- Y
                when X"5a" => keys(6)(0) <= pressed; -- ENTER
                when X"4b" => keys(6)(1) <= pressed; -- L
                when X"42" => keys(6)(2) <= pressed; -- K
                when X"3b" => keys(6)(3) <= pressed; -- J
                when X"33" => keys(6)(4) <= pressed; -- H
                when X"29" => keys(7)(0) <= pressed; -- SPACE
                when X"14" => keys(7)(1) <= pressed; -- CTRL (Symbol Shift)
                when X"3a" => keys(7)(2) <= pressed; -- M
                when X"31" => keys(7)(3) <= pressed; -- N
                when X"32" => keys(7)(4) <= pressed; -- B
                when X"6B" => keys(0)(0) <= pressed; -- Left (Caps 5)
                              keys(3)(4) <= pressed;
                when X"72" => keys(0)(0) <= pressed; -- Down (Caps 6)
                              keys(4)(4) <= pressed;
                when X"75" => keys(0)(0) <= pressed; -- Up (Caps 7)
                              keys(4)(3) <= pressed;
                when X"74" => keys(0)(0) <= pressed; -- Right (Caps 8)
                              keys(4)(2) <= pressed;
                when X"66" => keys(0)(0) <= pressed; -- Backspace (Caps 0)
                              keys(4)(0) <= pressed;
                when X"58" => keys(0)(0) <= pressed; -- Caps lock (Caps 2)
                              keys(3)(1) <= pressed;
                when X"76" => keys(0)(0) <= pressed; -- Break (Caps Space)
                              keys(7)(0) <= pressed;
                when others=> null;
              end case;
            end if;

Lo que hacemos es trasladar la señal pressed al array keys (array bidimensional de 5x8 donde guardamos el estado de las 40 teclas) dentro de la sentencia case. Fíjense en el detalle de que no leemos los 9 bits de shiftreg, sino los 8 de menor peso, ya que el noveno contiene la paridad y es redundante. El array emplea lógica positiva, o sea que un bit a 1 significa la tecla está pulsada y un 0 que la tecla está sin pulsar. En el protocolo PS/2, cuando pulsamos por ejemplo la tecla A recibimos del teclado el código $1c. Sin embargo al soltar la tecla recibimos el código $f0 seguido del $1c. Por eso creamos la señal pressed, que vale 0 si el último código fue $f0, y 1 en cualquier otro caso. Nos basta con asignar a la tecla pulsada dicha señal. En los casos de teclas extendidas del Spectrum, por ejemplo el cursor izquierdo, simulamos la pulsación de dos teclas distintas en el Spectrum, en este caso Caps Shift+5.

Ya tenemos la mitad del trabajo hecho, generar un array con el estado de las teclas. Ahora necesitamos comunicárselo al Spectrum cuando éste lo requiera. El Spectrum lo hace leyendo del puerto $FE, poniendo en el byte alto del bus de direcciones las columnas que quiere leer del teclado. Normalmente sólo solicita una columna, pero se puede dar el caso de solicitar varias a la vez e incluso todas, y el teclado debe responder haciendo un OR de todas las columnas solicitadas. Tanto columnas como filas están en lógica negativa, al revés de cómo hemos almacenado el estado de las teclas, por lo que tenemos que negar. Lo he hecho así porque al no inicializar, el array por defecto tiene todos sus valores a cero, y en caso de optar por la opción más directa (lógica negativa) me encontraría con la situación de que están todas las teclas pulsadas.

Mostremos el código:

Código:
  process (keys, rows)
  variable tmp: std_logic;
  begin
    for i in 0 to 4 loop
      tmp:= '0';
      for j in 0 to 7 loop
        tmp:= tmp or (keys(j)(i) and not rows(j));
      end loop;
      keyb(i) <=  not tmp;
    end loop;
  end process;

A simple vista es difícil de entender porque contiene mucho código de preprocesador: dos bucless FOR y una variable tmp. Así que pongo el tocho inicial y luego vamos simplificando:

Código:
  keyb(0) <= not ( (keys(0)(0) and not rows(0))
                or (keys(1)(0) and not rows(1))
                or (keys(2)(0) and not rows(2))
                or (keys(3)(0) and not rows(3))
                or (keys(4)(0) and not rows(4))
                or (keys(5)(0) and not rows(5))
                or (keys(6)(0) and not rows(6))
                or (keys(7)(0) and not rows(7)) );

  keyb(1) <= not ( (keys(0)(1) and not rows(0))
                or (keys(1)(1) and not rows(1))
                or (keys(2)(1) and not rows(2))
                or (keys(3)(1) and not rows(3))
                or (keys(4)(1) and not rows(4))
                or (keys(5)(1) and not rows(5))
                or (keys(6)(1) and not rows(6))
                or (keys(7)(1) and not rows(7)) );

  keyb(2) <= not ( (keys(0)(2) and not rows(0))
                or (keys(1)(2) and not rows(1))
                or (keys(2)(2) and not rows(2))
                or (keys(3)(2) and not rows(3))
                or (keys(4)(2) and not rows(4))
                or (keys(5)(2) and not rows(5))
                or (keys(6)(2) and not rows(6))
                or (keys(7)(2) and not rows(7)) );

  keyb(3) <= not ( (keys(0)(3) and not rows(0))
                or (keys(1)(3) and not rows(1))
                or (keys(2)(3) and not rows(2))
                or (keys(3)(3) and not rows(3))
                or (keys(4)(3) and not rows(4))
                or (keys(5)(3) and not rows(5))
                or (keys(6)(3) and not rows(6))
                or (keys(7)(3) and not rows(7)) );

  keyb(4) <= not ( (keys(0)(4) and not rows(0))
                or (keys(1)(4) and not rows(1))
                or (keys(2)(4) and not rows(2))
                or (keys(3)(4) and not rows(3))
                or (keys(4)(4) and not rows(4))
                or (keys(5)(4) and not rows(5))
                or (keys(6)(4) and not rows(6))
                or (keys(7)(4) and not rows(7)) );

Evidentemente aunque el comportamiento es exactamente el mismo, es un código muy repetitivo, así que en una primera aproximación empleamos un bucle FOR:

Código:
  for i in 0 to 4 loop
    keyb(i) <= not ( (keys(0)(i) and not rows(0))
                  or (keys(1)(i) and not rows(1))
                  or (keys(2)(i) and not rows(2))
                  or (keys(3)(i) and not rows(3))
                  or (keys(4)(i) and not rows(4))
                  or (keys(5)(i) and not rows(5))
                  or (keys(6)(i) and not rows(6))
                  or (keys(7)(i) and not rows(7)) );
  end loop;

Ya está mucho mejor, aunque todavía le falta algo. Ese algo lo solucionamos con una variable, tmp, y otro bucle FOR. Esta variable sirve para indicarle al compilador que no queremos repetir código, sólo es una forma simplificada de expresarlo, detrás de dicha variable no hay nada más. Es decir, no es como una señal, que físicamente se corresponde con un bit, un registro o una línea. El código final sería (ya lo he puesto antes pero lo repito) el siguiente:

Código:
  process (keys, rows)
  variable tmp: std_logic;
  begin
    for i in 0 to 4 loop
      tmp:= '0';
      for j in 0 to 7 loop
        tmp:= tmp or (keys(j)(i) and not rows(j));
      end loop;
      keyb(i) <=  not tmp;
    end loop;
  end process;

Ya hemos generado el vector de 5 bits, keyb, que se actualiza tras cualquier cambio en la matriz keys (se ha soltado o pulsado cualquier tecla) o en el byte alto del bus de direcciones (se solicitan otras filas). La mayoría del tiempo no nos importa el valor de dicho vector, sólo en el caso de una lectura del puerto $FE dicho valor es relevante, y lo indicamos en el proceso que escribe en el bus, que ahora quedaría así (kbcol es el mismo vector que keyb visto desde el módulo lec4.vhd):

Código:
  process (rd_n, wr_n, mreq_n, iorq_n, abus)
  begin
    dbus <= (others => 'Z');
    if rd_n='0' then
      if mreq_n='0' then
        if abus(15 downto 14)="00" then
          dbus <= din_rom;
        else
          dbus <= din_ram;
        end if;
      elsif iorq_n='0' and abus(0)='0' then
        dbus <= "101" & kbcol;
      end if;
    end if;
  end process;

Con esto se acaba la lección, espero que haya sido entretenida. Como ya quedan detalles mínimos para acabar el clon de 16K (borde, puerto ear, sonido), en la siguiente lección probablemente incluiré el manejo de RAM externa, actualizándonos al modelo 48K. Así que los que tengan la MOD-VGA o similar, que vayan soldando el chip de RAM si quieren seguir la lección. Desgraciadamente para los que tengan la OLS, lo siento, no hay posibilidad de añadir RAM externa y se van a tener que conformar con ver los videos.

Os dejo el video y el .zip de la lección. Hasta la próxima.





Archivo(s) adjuntos
.zip  clown_leccion4.zip (Tamaño: 53.22 KB / Descargas: 74)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
24-06-2013, 07:08 PM (Este mensaje fue modificado por última vez en: 26-06-2013 07:18 PM por admin.)
Mensaje: #8
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
Ya está lista la lección 5. Como se supone que ya habéis alcanzado un nivel suficiente, sólo pondré el zip de la lección. Es cuestión de que abráis el zip, lo comparéis con el de la lección anterior, y estudiéis vosotros mismos las diferencias. Lo que se ha añadido es:
  • Borde
  • Entrada de cassette
  • Salida de audio
  • 32K superiores

Con esto ya hemos creado un ZX Spectrum 48K completo. Tengo que probar a cargar la Shock Megademo por el puerto EAR, para comprobar que los timings son correctos. En cuanto lo haga subo el video.

Para la siguiente lección iremos a por el modelo 128K.

Edito: Ya he probado la demo en el clon, funciona perfectamente. El video no se ve fluído porque está grabado a 12 FPS. El circuito EAR que he tenido que construir es éste:

http://retrolandia.net/foro/showthread.p...409#pid409

Y a continuación tenéis el video:





Archivo(s) adjuntos
.zip  clown_leccion5.zip (Tamaño: 53.62 KB / Descargas: 82)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
29-06-2013, 12:53 PM
Mensaje: #9
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
En esta lección (ya vamos por la 6) no voy a implementar nada nuevo. Es la misma implementación pero de una forma diferente, en lugar de usar BRAM (ram interna de la FGPA) para la ROM, empleamos RAM externa. Esto nos deja libres la mitad de la RAM interna, que nos hace falta para meter la página 7 de RAM en la siguiente lección y así poder hacer el clon de 128K.

Lo que hay que hacer es inicializar la RAM de video con el contenido de la ROM y copiar esta RAM de video a la RAM externa. Para ello nos hemos inventado un nuevo biestable, xpage, que conmuta entre un modo de paginación especial que solo se ejecuta al inicio. Este modo de paginación intercambia el mapeado entre ROM y RAM de video sólo durante el copiado, que dura unos pocos milisegundos. Así, cuando se produce el reset de la CPU, saltamos a la dirección $0000, que es directamente la RAM de video. Si no hacemos esto se saltaría a la RAM externa, y al no estar inicializada podría pasar cualquier cosa.

También durante este tiempo (xpage está a '0') se puede escribir en la ROM, y es precisamente lo que hacemos. Mediante un parche en ROM, hacemos un LDIR (una copia) desde $0000-$3fff en sí mismo. Lo hacemos así porque durante las lecturas en $0000-$3fff (mientras xpage es '0') se hacen en RAM de video pero las escrituras se hacen en la ROM (ram externa), así se hace el copiado. En futuros RESETs manuales (no están implementados aún) este LDIR sería inocuo.

Tras el copiado volvemos al modo normal, y esto lo hacemos poniendo xpage a '1' tras la primera escritura en el puerto $FE. He escogido esta solución por simplicidad, ya que justo después hay una instrucción de este tipo. También podría haber puesto un contador que dé un margen de tiempo suficiente para el copiado.


Archivo(s) adjuntos
.zip  clown_leccion6.zip (Tamaño: 27.12 KB / Descargas: 71)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
09-07-2013, 12:09 PM
Mensaje: #10
RE: Tutorial de VHDL construyendo un clon (llamado Clown) desde cero
He dado un paso atrás y ahora en lugar de obtener la ROM mediante la BRAM inicializada, la obtengo directamente desde la SPI Flash. Es decir, antes de resetear el Z80 hacemos un copiado de SPI Flash a la parte de RAM estática que implementará la ROM, y una vez se ejecuta la máquina la escritura a esa parte de RAM queda inhabilitada. La incrustación de datos en la SPI Flash se hace de una forma muy sencilla con la herramienta srec_cat, disponible aquí.

http://srecord.sourceforge.net/

Tan sólo hay que ejecutar el archivo generamcs.bat que hay dentro del zip. Este paso atrás me permitirá avanzar mejor en la implementación de modelos 128K, ya que no estoy limitado a los 32K de ROM que me facilita la BRAM.

Si ejecutáis esta lección veréis lo mismo que en las 2 lecciones anteriores, un ZX Spectrum 48K. Espero que la siguiente lección os resulte más interesante.


Archivo(s) adjuntos
.zip  clown_leccion7.zip (Tamaño: 5.33 KB / Descargas: 81)
Encuentra todos sus mensajes
Cita este mensaje en tu respuesta
Enviar respuesta