-
Notifications
You must be signed in to change notification settings - Fork 195
Capítulo 27: Memoria ROM genérica
Ejemplos de este capítulo en github
Las memorias son elementos muy comunes, que usaremos mucho en nuestros diseños. En vez de estar haciendo memorias con un tamaño determinado, es más versátil crear una memoria genérica cuyos parámetros de longitud de datos y de direcciones se establezcan al instanciarlas.
Crearemos una memoria rom genérica y la utilizaremos en dos ejemplo: uno para reproducir una secuencia de luces en los leds y otro para tocar una melodía: la marcha imperial
La memoria rom genérica la denominaremos genrom
Tiene 3 parámetros que se asignan al instanciarse la rom:
- DW (Data width): Anchura de los datos (en bits)
- AW (Address width): Anchura de las direcciones (en bits)
- ROMFILE: Fichero con el contenido de la rom
Los puertos de la rom son los clásicos, pero ahora su tamaño no está especificado:
- Addr: Bus de direcciones
- data: Bus de datos
- clk: Reloj del sistema
Puesto que los puertos (addr y data) son genéricos, y por tanto se tienen que definir al declarar el módulo, es necesario primero definir los parámetros AW y DW. Luego, en función de ellos se definen los puertos. Esto se codifica en verilog con esta estructura:
module nombre #(definición de parametros) (definicion de puertos);
El código verilog de la memoria rom genérica es:
//-- Fichero: genrom.v
module genrom #( //-- Parametros
parameter AW = 5, //-- Bits de las direcciones (Adress width)
parameter DW = 4) //-- Bits de los datos (Data witdh)
( //-- Puertos
input clk, //-- Señal de reloj global
input wire [AW-1: 0] addr, //-- Direcciones
output reg [DW-1: 0] data); //-- Dato de salida
//-- Parametro: Nombre del fichero con el contenido de la ROM
parameter ROMFILE = "rom1.list";
//-- Calcular el numero de posiciones totales de memoria
localparam NPOS = 2 ** AW;
//-- Memoria
reg [DW-1: 0] rom [0: NPOS-1];
//-- Lectura de la memoria
always @(posedge clk) begin
data <= rom[addr];
end
//-- Cargar en la memoria el fichero ROMFILE
//-- Los valores deben estan dados en hexadecimal
initial begin
$readmemh(ROMFILE, rom);
end
endmodule
El tercer parámetro, ROMFILE, se define de la manera habitual, usando la palabra clave parameter dentro del módulo. Dentro de #() sólo se definen los parámetros que afectan a los puertos
La mejor manera de utilizar la rom genérica en nuestros diseños es volviendo a definir los parámetros AW y DW (aunque se pueden usar otros nombres) usando como valores los que necesitemos en nuestro diseño:
parameter AW = 5;
parameter DW = 5;
Ahora declaramos los cables que van conectados a la rom genérica en función de estos parámetros:
reg [AW-1: 0] addr; //-- Bus de direcciones
reg [DW-1: 0] data; //-- Bus de datos
y finalmente instanciamos la rom:
genrom
#( .ROMFILE(ROMFILE), //-- Asignacion de parametros
.AW(AW),
.DW(DW))
ROM ( //-- coneion de cables
.clk(clk),
.addr(addr),
.data(data)
);
En este ejemplo reproduciremos una secuencia en los leds, igual que en los ejemplos del capítulo anterior, pero usando una rom genérica
Utilizaremos 5 leds para la secuencia, por lo que la anchura de los datos será de 5 bits (DW = 5), y una anchura de direcciones también de 5 bits (AW = 5, para tener 32 posiciones). La secuencia es un contador. En cada posición de la memoria se almacena su número de dirección
La descripción en verilog del ejemplo es:
//-- Fichero: genromleds.v
`default_nettype none
`include "divider.vh"
module genromleds (input wire clk,
output wire [4:0] leds);
//- Tiempo de envio
parameter DELAY = `T_500ms;
//-- Fichero con la rom
parameter ROMFILE = "rom1.list";
//-- Numero de bits de la direccione
parameter AW = 5;
parameter DW = 5;
//-- Cable para direccionar la memoria
reg [AW-1: 0] addr;
reg rstn = 0;
wire clk_delay;
//-- Instanciar la memoria rom
genrom
#( .ROMFILE(ROMFILE),
.AW(AW),
.DW(DW))
ROM (
.clk(clk),
.addr(addr),
.data(leds)
);
//-- Contador
always @(negedge clk)
if (rstn == 0)
addr <= 0;
else if (clk_delay)
addr <= addr + 1;
//---------------------------
//-- Temporizador
//---------------------------
dividerp1 #(.M(DELAY))
DIV0 ( .clk(clk),
.clk_out(clk_delay)
);
//-- Inicializador
always @(negedge clk)
rstn <= 1;
endmodule
El fichero que se graba en la rom, con la secuencia de ejemplo (contador) es el siguiente:
//-- Fichero rom1.list
//-- Cada linea se corresponde con una posicion de memoria
//-- Se pueden poner comentarios
//-- ROM1: contiene los numeros del 0 al 31 (en hexadecimal)
0 //-- Posicion 0
1 //-- Posicion 1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
10
11
12
13
14
15
16
17
18
19
1A
1B
1C
1D
1E
1F
El banco de pruebas es el mismo que en el capítulo anterior. Para simular ejecutamos:
$ make sim
y en el gtkwave vemos lo siguiente:
Por los leds aparece la secuencia de cuenta, desde 0 hasta 31 (en hexadecimal)
Para sintetizar hay que ejecutar el comando:
$ make sint
Los recursos empleados son:
Recurso | ocupación |
---|---|
PIOs | 7 / 96 |
PLBs | 15 / 160 |
BRAMs | 1 / 16 |
El diseño se carga con:
$ sudo iceprog genromleds.bin
Se podrá ver por los leds la secuencia de cuenta
En el segundo ejemplo tocaremos un fragmento de la marcha imperial de star wars. Esto es un clásico en el grupo de Clone wars, tocándose con los motores paso a paso para comprobar su funcionamiento. Pues bien, una FPGA no la tendrás dominada hasta que no toques con ella la marcha imperial :-)
El diseño consta de una memoria ROM de 64 posiciones de 16 bits. En cada una de ellas se almacena el valor del divisor para generar una nota musical. Los 5 bits menos significativos de cada valor de la nota se envían a los leds, para ver la actividad
El generador de notas (notegen) es similar al que se hizo en el capítulo 17 pero adaptándolo a las reglas del diseño síncrono y haciendo que el módulo del contador se pase como una entrada más, en vez de ser un valor fijo. Además, se ha modificado para que la señal cuadrada generada tenga siempre un ciclo de trabajo del 50% (y que todas las notas suenen con la misma intensidad)
La duración de cada nota se ha establecido en 200ms. Colocando la misma nota en dos posiciones consecutivas de la memoria rom, su duración será el doble (400ms). De esta forma se controla de forma sencilla la duración de todas las notas y los silencios
Los dos ficheros principales son el notegen.v, que contiene el componente de generación de las notas musicales y romnotes.v que tiene el reproductor completo
//-- Fichero: notegen.v
module notegen(input wire clk, //-- Senal de reloj global
input wire rstn, //-- Reset
input wire [15:0] note, //-- Divisor
output reg clk_out); //-- Señal de salida
wire clk_tmp;
//-- Registro para implementar el contador modulo note
reg [15:0] divcounter = 0;
//-- Contador módulo note
always @(posedge clk)
//-- Reset
if (rstn == 0)
divcounter <= 0;
//-- Si la nota es 0 no se incrementa contador
else if (note == 0)
divcounter <= 0;
//-- Si se alcanza el tope, poner a 0
else if (divcounter == note - 1)
divcounter <= 0;
//-- Incrementar contador
else
divcounter <= divcounter + 1;
//-- Sacar un pulso de anchura 1 ciclo de reloj si el generador
assign clk_tmp = (divcounter == 0) ? 1 : 0;
//-- Divisor de frecuencia entre 2, para obtener como salida una señal
//-- con un ciclo de trabajo del 50%
always @(posedge clk)
if (rstn == 0)
clk_out <= 0;
else if (note == 0)
clk_out <= 0;
else if (clk_tmp == 1)
clk_out <= ~clk_out;
endmodule
La descripción en verilog del circuito reproductor se muestra a continuación:
//-- Fichero romnotes.v
//-- Incluir las constantes del modulo del divisor
`include "divider.vh"
//-- Parametros:
//-- clk: Reloj de entrada de la placa iCEstick
//-- ch_out: Canal de salida
module romnotes(input wire clk,
output wire [4:0] leds,
output wire ch_out);
//-- Parametros
//-- Duracion de las notas
parameter DUR = `T_200ms;
//-- Fichero con las notas para cargar en la rom
parameter ROMFILE = "imperial.list";
//-- Tamaño del bus de direcciones de la rom
parameter AW = 6;
//-- Tamaño de las notas
parameter DW = 16;
//-- Cables de salida de los canales
wire ch0, ch1, ch2;
//-- Selección del canal del multiplexor
reg [AW-1: 0] addr = 0;
//-- Reloj con la duracion de la nota
wire clk_dur;
reg rstn = 0;
wire [DW-1: 0] note;
//-- Instanciar la memoria rom
genrom
#( .ROMFILE(ROMFILE),
.AW(AW),
.DW(DW))
ROM (
.clk(clk),
.addr(addr),
.data(note)
);
//-- Generador de notas
notegen
CH0 (
.clk(clk),
.rstn(rstn),
.note(note),
.clk_out(ch_out)
);
//-- Sacar los 5 bits menos significativos de la nota por los leds
assign leds = note[4:0];
//-- Inicializador
always @(posedge clk)
rstn <= 1;
//-- Contador para seleccion de nota
always @(posedge clk)
if (clk_dur)
addr <= addr + 1;
//-- Divisor para marcar la duración de cada nota
dividerp1 #(DUR)
TIMER0 (
.clk(clk),
.clk_out(clk_dur)
);
endmodule
El parámetro DUR determina la duración mínima de una nota (o un silencio), que se ha establecido en 200ms. Para reproducir una nota del doble de duración, simplemente se toca dos veces. Tocándola N veces durará N * 200ms.
La melodía de la marcha imperial está almacenada en este fichero de texto:
//-- Marcha imperial
0 //-- Un 0 es un SILENCIO
0
0
0
0
0
0
0
0
471A //-- MI_4
471A
0
471A //-- MI_4
471A
0
471A //-- MI_4
471A
0
5996 //-- DO_4
5996
3BCA //-- SOL_4
471A //-- MI_4
471A
0
5996 //-- DO_4
5996
3BCA //-- SOL_4
471A //-- MI_4
471A
//----------- Segundo trozo
0
0
0
2F75 //-- SI_4
2F75
0
2F75 //-- SI_4
2F75
0
2F75 //-- SI_4
2F75
0
2CCB //-- DO_5
2CCB
3BCA //-- SOL_4
471A //-- MI_4
471A
0
5996 //-- DO_4
5996
3BCA //-- SOL_4
471A //-- MI_4
471A
Lo que se almacenan son los valores de los divisores (en hexadecimal) para generar las notas. Los valores de las diferentes notas se encuentran en el archivo notegen.vh.
Para que una nota dure más tiempo, se reproduce 2 ó más veces, colocándose en memoria las copias de la misma nota.
La nota 0 equivale a un silencio
El banco de pruebas es el clásico: se instancia el componente romnotes y se genera el reloj para que funcione:
//-- Fichero: romnotes_tb.v
module romnotes_tb();
//-- Registro para generar la señal de reloj
reg clk = 0;
//-- Salidas de los canales
wire ch_out;
//-- Instanciar el componente y establecer el valor del divisor
//-- Se pone un valor bajo para simular (de lo contrario tardaria mucho)
romnotes #(.DUR(2))
dut(
.clk(clk),
.ch_out(ch_out)
);
//-- Generador de reloj. Periodo 2 unidades
always
# 1 clk <= ~clk;
//-- Proceso al inicio
initial begin
//-- Fichero donde almacenar los resultados
$dumpfile("romnotes_tb.vcd");
$dumpvars(0, romnotes_tb);
# 200 $display("FIN de la simulacion");
$finish;
end
endmodule
Para simulare ejecutamos:
$ make sim2
Y en la simulación podremos ver cómo se van enviando los 5 bits menos significativos de la nota a los leds. En la simulación la duración de la nota está puesta a 2 unidades
La síntesis se realiza con el comando:
$ make sint2
Los recursos empleados son:
Recurso | ocupación |
---|---|
PIOs | 7 / 96 |
PLBs | 31 / 160 |
BRAMs | 1 / 16 |
El diseño se carga con:
$ sudo iceprog romnotes.bin
El resultado se puede ver en este vídeo de youtube:
¡Que las FPGAs te acompañen!
- Completar la marcha imperial, aumentando la memoria y añadiendo el resto de notas
TODO
0 You are leaving the privative sector (EN)
1 ¡Hola mundo! (EN) (RU)
2 De un bit a datos (EN)
3 Puerta NOT (EN)
4 Contador de 26 bits (EN)
5 Prescaler de N bits (EN)
6 Múltiples prescalers (EN)
7 Contador de 4 bits con prescaler (EN)
8 Registro de 4 bits (EN)
9 Inicializador (EN)
10 Registro de desplazamiento (EN)
11 Multiplexor de 2 a 1 (EN)
12 Multiplexor de M a 1 (EN)
13 Inicializando registros (EN)
14 Registro de N bits con reset síncrono
15 Divisor de frecuencias
16 Contador de segundos
17 Generando tonos audibles
18 Tocando notas
19 Secuenciando notas
20 Comunicaciones serie asíncronas
21 Baudios y transmisión
22 Reglas de diseño síncrono
23 Controladores y autómatas finitos
24 Unidad de transmisión serie asíncrona
25 Unidad de recepción serie asíncrona
26 Memoria ROM
27 Memoria ROM genérica
28 Memoria RAM
29 Puertas triestado
30 Hacia el microprocesador y más allá