Implementando la Unidad de Control en un Procesador RISC-V 32I en FPGA
Seguimos avanzando en nuestra serie sobre cómo construir un procesador RISC-V 32I utilizando la FPGA Tang Nano 20K. Hoy hablaremos de la Unidad de Control, un componente clave que le da vida a nuestro procesador. Vamos a explicarla de forma sencilla y amigable, y luego implementaremos juntos el código en Verilog, detallando cada parte para que sea fácil de entender.
¿Qué es exactamente la Unidad de Control?
La Unidad de Control es como el director de una orquesta: se encarga de que todos los componentes del procesador trabajen en armonía. Su principal tarea es leer (decodificar) cada instrucción que recibe desde la memoria, entender qué significa y luego enviar las señales necesarias a las demás partes del procesador para ejecutar correctamente esa instrucción.
Sus funciones principales son:
- Decodificar instrucciones: Determina qué tipo de instrucción llegó y cómo ejecutarla.
- Generar señales de control: Activa o desactiva componentes internos, como la ALU, memoria o registros.
- Sincronización: Coordina los tiempos para que todo funcione en orden.
Implementando la Unidad de Control en Verilog (paso a paso)
Ahora sí, manos a la obra con el código en Verilog, que vamos a explicar parte por parte.
Definición del módulo
Primero declaramos qué entradas y salidas tiene nuestra unidad de control:
module ControlUnit (
input wire [6:0] opcode, // Identifica el tipo de instrucción
output reg RegWrite, // Permite escribir en registros
output reg ALUSrc, // Selecciona el segundo dato para la ALU
output reg MemRead, // Lee datos desde memoria
output reg MemWrite, // Escribe datos en memoria
output reg MemtoReg, // Mueve datos desde memoria al registro
output reg Branch, // Indica si es una instrucción de salto condicional
output reg Jump, // Indica si es una instrucción de salto
output reg [1:0] ALUOp // Tipo de operación que realizará la ALU
);
¿Qué significan esos códigos binarios (opcodes)?
Es buena práctica definir claramente qué hace cada código para que el código sea más fácil de leer y mantener:
localparam R_TYPE = 7'b0110011; // Operaciones matemáticas y lógicas
localparam I_LOAD = 7'b0000011; // Cargar datos desde la memoria
localparam I_TYPE = 7'b0010011; // Operaciones con valores inmediatos
localparam S_TYPE = 7'b0100011; // Almacenar datos en memoria
localparam B_TYPE = 7'b1100011; // Saltos condicionales
localparam JAL = 7'b1101111; // Salto incondicional y guardar posición
localparam JALR = 7'b1100111; // Salto a posición en registro
localparam LUI = 7'b0110111; // Cargar valor inmediato en parte alta del registro
localparam AUIPC = 7'b0010111; // Añadir inmediato al contador del programa (PC)
El corazón del módulo: bloque combinacional
Aquí la unidad de control toma decisiones según la instrucción recibida:
always @(*) begin
// Primero asignamos valores por defecto para evitar errores
ALUSrc = 0;
MemRead = 0;
MemWrite = 0;
RegWrite = 0;
Branch = 0;
Jump = 0;
MemtoReg = 0;
ALUOp = 2'b00;
// Ahora, dependiendo del opcode, ajustamos las señales
case(opcode)
R_TYPE: begin // Instrucciones aritméticas y lógicas
RegWrite = 1;
ALUSrc = 0;
ALUOp = 2'b10;
end
I_LOAD: begin // Cargar datos desde memoria (ej. LW)
RegWrite = 1;
ALUSrc = 1;
MemRead = 1;
MemtoReg = 1;
ALUOp = 2'b00;
end
I_TYPE: begin // Operaciones inmediatas (ej. ADDI)
RegWrite = 1;
ALUSrc = 1;
ALUOp = 2'b11;
end
S_TYPE: begin // Guardar datos en memoria (ej. SW)
MemWrite = 1;
ALUSrc = 1;
ALUOp = 2'b00;
end
B_TYPE: begin // Saltos condicionales (ej. BEQ)
Branch = 1;
ALUOp = 2'b01;
end
JAL: begin // Saltos incondicionales
Jump = 1;
RegWrite = 1;
end
JALR: begin // Saltos a dirección en registro
Jump = 1;
ALUSrc = 1;
RegWrite = 1;
end
LUI: begin // Cargar valor inmediato superior
RegWrite = 1;
end
AUIPC: begin // Añadir inmediato superior al PC
RegWrite = 1;
ALUOp = 2'b00;
end
default: begin // Por defecto, ninguna operación
RegWrite = 0;
ALUSrc = 0;
MemRead = 0;
MemWrite = 0;
ALUOp = 2'b00;
end
endcase
end
Codigo completo
Finalmente este es el codigo completo.
module control_unit(
input [6:0] opcode,
output reg ALUSrc,
output reg MemtoReg,
output reg RegWrite,
output reg MemRead,
output reg MemWrite,
output reg Branch,
output reg Jump,
output reg [1:0] ALUOp
);
// Definición de los opcodes RV32I
localparam R_TYPE = 7'b0110011;
localparam I_LOAD = 7'b0000011;
localparam I_ALU = 7'b0010011;
localparam S_TYPE = 7'b0100011;
localparam B_TYPE = 7'b1100011;
localparam JAL = 7'b1101111;
localparam JALR = 7'b1100111;
localparam LUI = 7'b0110111;
localparam AUIPC = 7'b0010111;
always @(*) begin
// Valores predeterminados (evita latch)
ALUSrc = 1'b0;
MemtoReg = 0;
RegWrite = 0;
MemRead = 0;
MemWrite = 0;
Branch = 0;
Jump = 0;
ALUOp = 2'b00;
case(opcode)
R_TYPE: begin // R-Type
ALUSrc = 0;
RegWrite = 1;
ALUOp = 2'b10;
end
I_ALU: begin // I-Type inmediato (addi, andi, ori, etc.)
ALUSrc = 1;
RegWrite = 1;
ALUOp = 2'b11;
end
I_LOAD: begin // Load (lw, lb, lh)
ALUSrc = 1;
MemRead = 1;
MemtoReg = 1;
RegWrite = 1;
ALUOp = 2'b00;
end
S_TYPE: begin // Store (sw, sb, sh)
ALUSrc = 1;
MemWrite = 1;
ALUOp = 2'b00;
end
B_TYPE: begin // Branch (beq, bne, etc.)
Branch = 1;
ALUOp = 2'b01;
end
JAL: begin // Jump and Link (JAL)
Jump = 1;
RegWrite = 1;
ALUOp = 2'b00;
end
JALR: begin // Jump and Link Register (JALR)
ALUSrc = 1;
Jump = 1;
RegWrite = 1;
ALUOp = 2'b00;
end
LUI: begin // Load Upper Immediate
RegWrite = 1;
ALUOp = 2'b00;
end
AUIPC: begin // Add Upper Immediate to PC
RegWrite = 1;
ALUOp = 2'b00;
end
default: begin // Instrucción no reconocida
ALUSrc = 0;
MemtoReg = 0;
RegWrite = 0;
MemRead = 0;
MemWrite = 0;
Branch = 0;
Jump = 0;
ALUOp = 2'b00;
end
endcase
end
endmodule
Conclusión: la Unidad de Control haciendo magia
Con esta unidad de control, nuestro procesador RISC-V 32I ya puede interpretar instrucciones y ejecutar tareas coordinadas y precisas. Este módulo es clave para que nuestro sistema cobre vida y funcione correctamente.
En la próxima entrega trabajaremos en la Unidad Aritmético-Lógica (ALU), encargada de realizar los cálculos y operaciones lógicas. ¡Sigue atento y avancemos juntos! 🚀