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.