Architecture d'un Processeur Monocycle : Conception Verilog et Simulation sous ModelSim

Principes de l'Architecture Monocycle

Un processeur monocycle est conçu de manière à ce que l'exécution de chaque instruction soit intégralement réalisée au cours d'un unique cycle d'horloge. L'ensemble des opérations, incluant l'extraction de l'instruction, le décodage, l'exécution算术逻辑, l'accès à la mémoire et l'écriture du résultat, se déroule de manière séquentielle au sein de ce même intervalle de temps. Cette approche impose une contrainte majeure : la période de l'horloge doit être dimensionnée en fonction du chemin critique de l'instruction la plus complexe, ce qui limite la fréquence maximale de fonctionnement. Malgré cette limitation de performance, la structure matérielle reste extrêmement lisible, ce qui en fait un modèle d'étude fondamental pour la compréhension des chemins de données et des unités de contrôle.

Conception des Modules Matériels en Verilog

L'architecture matérielle se divise en deux entités principales : le chemin de données, qui assure le transit et la transformation des informations, et l'unité de contrôle, qui génère les signaux de commande synchronisant les opérations.

Comtpeur de Programme (PC)

Le compteur de programme maintient l'adresse de l'instruction en cours d'exécution. Dans cette implémentation, il supporte l'incrémentation séquentielle ainsi que les sauts inconditionnels et les branchements conditionnels.

module program_counter (
    input wire halt,
    input wire clock,
    input wire reset,
    input wire jump_uncond,
    input wire branch_en,
    input wire branch_flag,
    input wire [7:0] target_addr,
    output reg [7:0] pc_out
);
    always @(posedge clock) begin
        if (reset) begin
            pc_out <= 8'd0;
        end else if (!halt) begin
            if (jump_uncond) begin
                pc_out <= target_addr;
            end else if (branch_en && branch_flag) begin
                pc_out <= pc_out + target_addr;
            end else begin
                pc_out <= pc_out + 8'd1;
            end
        end
    end
endmodule

L'utilisation systématique d'assignations non bloquantes (<=) dans les blocs séquentiels est impérative pour éviter les conditions de compétition et garantir une mise à jour synchrone correcte des bascules.

Banc de Registres

Le banc de registres fournit un espace de stockage rapide pour les opérandes. Il dispose de deux ports de lecture asynchrones et d'un port d'écriture synchrone.

module register_file (
    input wire [2:0] r_addr1,
    input wire [2:0] r_addr2,
    input wire [2:0] w_addr,
    input wire [15:0] w_data,
    input wire clock,
    input wire w_en,
    output reg [15:0] r_data1,
    output reg [15:0] r_data2
);
    reg [15:0] registers [0:7];

    // Lecture combinatoire
    always @(*) begin
        r_data1 = registers[r_addr1];
        r_data2 = registers[r_addr2];
    end

    // Écriture séquentielle sur front montant
    always @(posedge clock) begin
        if (w_en) begin
            registers[w_addr] <= w_data;
        end
    end
endmodule

Unité Arithmétique et Logique (ALU)

L'ALU exécute les opérations de calcul. Il s'agit d'un circuit purement combinatoire dont la sortie dépend exclusivement des opérandes d'entrée et du code d'opération fourni par l'unité de contrôle.

module arithmetic_logic_unit (
    output reg [15:0] alu_out,
    input wire [15:0] in_a,
    input wire [15:0] in_b,
    input wire [2:0] op_code
);
    always @(*) begin
        case (op_code)
            3'b000: alu_out = in_a + in_b;
            3'b001: alu_out = in_a - in_b;
            3'b010: alu_out = in_a & in_b;
            3'b011: alu_out = in_a | in_b;
            3'b100: alu_out = in_a << in_b[3:0];
            3'b101: alu_out = in_a >> in_b[3:0];
            3'b110: alu_out = in_a ^ in_b;
            3'b111: alu_out = ~in_a;
            default: alu_out = 16'h0000;
        endcase
    end
endmodule

Unité de Contrôle (CU)

L'unité de contrôle décode le code opération de l'instruction et active les signaux de contrôle correspondants. L'assignation de valeurs par défaut avant l'instruction case est une pratique essentielle pour prévenir l'inférence de verrous (latches) indésirables lors de la synthèse logique.

module control_unit (
    input wire [7:0] acc_in,
    input wire [15:0] instr,
    output reg halt_out,
    output reg [1:0] acc_op,
    output reg acc_en,
    output reg [2:0] alu_op,
    output reg mem_en,
    output reg jump_out,
    output reg branch_en_out,
    output reg branch_cond_out
);
    always @(*) begin
        halt_out = 1'b0; acc_en = 1'b0; mem_en = 1'b0;
        jump_out = 1'b0; branch_en_out = 1'b0; branch_cond_out = 1'b0;
        alu_op = 3'b000; acc_op = 2'b00;

        case (instr[15:8])
            8'h00: begin /* NOP */ end
            8'hFF: halt_out = 1'b1;
            8'h01: begin acc_en = 1'b1; acc_op = 2'b00; end
            8'h02: begin acc_en = 1'b1; acc_op = 2'b01; end
            8'h03: begin acc_en = 1'b1; acc_op = 2'b10; end
            8'h04: begin acc_en = 1'b1; acc_op = 2'b11; end
            8'h05: begin acc_en = 1'b1; alu_op = 3'b000; end
            8'h06: mem_en = 1'b1;
            8'h07: acc_en = 1'b1;
            8'h08: jump_out = 1'b1;
            8'h09: begin
                branch_en_out = 1'b1;
                branch_cond_out = acc_in[7];
            end
            default: begin /* Instruction non définie */ end
        endcase
    end
endmodule

Intégration du Chemin de Données

Le module de niveau supérieur instancie les sous-modules et établit les interconnexions physiques via des fils (wire). La cohérence des largeurs de bus et la polarité des signaux de contrôle sont vérifiées à cette étape.

module cpu_top (
    input wire clock,
    input wire reset,
    output wire [7:0] pc_current
);
    wire [15:0] instr_word;
    wire [15:0] alu_res, acc_val, mem_out, reg_out1, reg_out2;
    wire w_en, mem_en, jump_sig, br_en, br_cond, acc_en, halt_sig;
    wire [2:0] alu_sel;
    wire [1:0] acc_sel;

    program_counter pc_inst (
        .halt(halt_sig), .clock(clock), .reset(reset),
        .jump_uncond(jump_sig), .branch_en(br_en), .branch_cond(br_cond),
        .target_addr(instr_word[7:0]), .pc_out(pc_current)
    );

    instruction_rom rom_inst (
        .addr(pc_current), .data_out(instr_word)
    );

    control_unit cu_inst (
        .acc_in(acc_val[7:0]), .instr(instr_word),
        .halt_out(halt_sig), .acc_op(acc_sel), .acc_en(acc_en),
        .alu_op(alu_sel), .mem_en(mem_en), .jump_out(jump_sig),
        .branch_en_out(br_en), .branch_cond_out(br_cond)
    );

    register_file rf_inst (
        .r_addr1(instr_word[10:8]), .r_addr2(instr_word[13:11]),
        .w_addr(instr_word[10:8]), .w_data(alu_res),
        .clock(clock), .w_en(w_en),
        .r_data1(reg_out1), .r_data2(reg_out2)
    );

    arithmetic_logic_unit alu_inst (
        .in_a(acc_val), .in_b(reg_out2),
        .op_code(alu_sel), .alu_out(alu_res)
    );

    data_ram ram_inst (
        .addr(instr_word[7:0]), .data_in(acc_val),
        .enable(mem_en), .clock(clock), .data_out(mem_out)
    );

    accumulator acc_inst (
        .enable(acc_en), .op_code(acc_sel),
        .data_in(alu_res), .clock(clock), .data_out(acc_val)
    );
endmodule

Environnement de Simulation et Analyse Temporelle

La validation fonctionnelle nécessite un banc de test (Testbench) générant les stimuli d'horloge et de réinitialisation, tout en surveillant les signaux internes du dispositif sous test (DUT).

Banc de Test (Testbench)

`timescale 1ns/1ps
module cpu_testbench;
    parameter CLK_PERIOD = 20;
    reg clock;
    reg reset_n;
    wire [7:0] pc_monitor;

    cpu_top dut (
        .clock(clock),
        .reset(reset_n),
        .pc_current(pc_monitor)
    );

    initial begin
        clock = 1'b0;
        forever #(CLK_PERIOD/2) clock = ~clock;
    end

    initial begin
        reset_n = 1'b0;
        #100;
        reset_n = 1'b1;
        #(CLK_PERIOD * 100);
        $stop;
    end

    initial begin
        $dumpfile("simulation_waveforms.vcd");
        $dumpvars(0, cpu_testbench);
    end
endmodule

Analyse des Formes d'Onde

L'observation des chronogrammes sous ModelSim permet de valider la propagation des signaux. Pour une instruction de saut inconditionnel, le signal jump_sig s'active durant le cycle d'extraction. Au front montant suivant, la valeur du compteur de programme est remplacée par l'adresse cible, rompant la séquence d'incrémentation linéaire. Pour les instructions arithmétiques, la sortie de l'ALU se stabilise après un délai de propagation combinatoire, et le résultat est capturé par le banc de registres ou l'accumulateur au front d'horloge actif.

Méthodologie de Débogage

L'isolation des défaillances s'effectue par vérification modulaire. L'injection forcée de valeurs sur les signaux de contrôle permet de découpler le test du chemin de données de celui de l'unité de contrôle. L'apparition d'états indéterminés (X) ou de haute impédance (Z) dans les chronogrammes indique généralement des conflits de pilotes multiples sur un même réseau ou des registres non initialisés. Les avertissements de synthèse concernant les signaux non connectés ou les verrous inférés doivent être traités comme des erreurs de conception potentielles.

Limitations du Chemin Critique

L'analyse statique de temporisation révèle que la fréquence d'horloge maximale est inversement proportionnelle au délai de propagation du chemin le plus long. Dans une architecture monocycle, ce chemin critique traverse typiquement la mémoire d'instructions, l'unité de contrôle, le banc de registres, l'ALU et la mémoire de données avant de revenir au registre de destination. Cette contrainte temporelle stricte, imposée par l'instruction la plus lente, rend l'architecture monocycle sous-optimale pour les applications nécessitant un haut débit. L'évolution vers des architectures multi-cycles ou pipelinées permet de segmenter ce chemin critique en étapes plus courtes, augmentant ainsi la fréquence d'horloge et le débit global d'exécution des instructions.

Étiquettes: Verilog ModelSim Architecture des processeurs Logique numérique FPGA

Publié le 17 septembre à 10h56