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! 🚀