Virgule : un cœur RISC-V minimal

Virgule est un processeur RISC (Reduced Instruction Set Computer) développé par Guillaume Savaton dans un but pédagogique. Il implémente un sous-ensemble minimal du jeu d’instruction RISC-V 32 bits. Ici, « minimal » signifie que ce processeur accepte toutes les instructions de traitement de données en nombres entiers, de branchement et d’accès mémoire susceptibles d’être produites par un compilateur C comme GCC pour un programme autonome typique.

Motivations

Les enseignements en architecture de l’ordinateur et en conception de circuits numériques visent l’acquisition des compétences suivantes :

D’un point de vue pratique, le choix d’une architecture de processeur comme support pédagogique est souvent un compromis entre, d’une part le besoin de faciliter les apprentissages, et d’autre part l’adaptation de l’enseignement aux réalités industrielles. Nous avons ainsi identifié trois contraintes :

Parmi les candidates, l’architecture RISC-V remplit tous ces critères :

Références

Différences entre Virgule et l’architecture RISC-V officielle

Virgule est capable d’exécuter toutes les instructions de traitement de données, de branchement et d’accès mémoire du jeu d’instructions de base (RV32I) de la spécification RISC-V. Nous avons également conservé une instruction de retour d’exception (mret), ce qui permet de prendre en charge les interruptions matérielles.

Les instructions qui ont été écartées sont :

L’architecture RISC-V supporte trois niveaux de privilèges (utilisateur, superviseur, machine). Le mode machine est obligatoire et les deux autres sont facultatifs. Le cœur Virgule fonctionne uniquement dans le mode machine.

Par ailleurs, la gestion des interruptions a été simplifiée à l’extrême.

Registres

Virgule possède les registres suivants, tous de largeur 32 bits :

Pour un compilateur respectant l’ABI (Application Binary Interface) RISC-V, les registres généraux sont utilisés de la manière suivante :

Registre Nom ABI Rôle
x0 zero Zéro câblé
x1 ra Adresse de retour
x2 sp Pointeur de pile
x3 gp Pointeur gobal
x4 tp Pointeur de thread
x5 à x7 t0 à t2 Données temporaires
x8 s0 ou fp Donnée sauvegardée
x9 s1 Donnée sauvegardée
x10 à x11 a0 à a1 Arguments et résultats de sous-programmes
x12 à x17 a2 à a7 Arguments de sous-programmes
x18 à x27 s2 à s11 Donnée sauvegardée
x28 à x31 t3 à t6 Données temporaires

Le pointeur global peut être utilisé pour simplifier l’accès aux variables globales en utilisant un adressage relatif.

Par convention, les valeurs des registres s0 à s11 sont sauvegardés dans la pile en début de sous-programme et restaurées à la fin. Les valeurs des registres t0 à t6 doivent être sauvegardées par l’appelant s’il souhaite les conserver.

Organisation de la mémoire

Les formats de données utilisent la terminologie suivante :

Taille (bits) Nom
8 Byte (B)
16 Half word (H)
32 Word (W)

Les accès sur 16 ou 32 bits devront être alignés respectivement sur des adresses multiples de 2 ou 4. L’ordre des octets en mémoire suit la convention little-endian, c’est-à-dire que les valeurs sont rangées, de l’octet de poids faible à l’octet de poids fort, à des adresses croissantes.

Jeu d’instructions

Dans la syntaxe des instructions :

Instruction Signification Opération
LUI rd, imm Load Upper Immediate rd ← imm
AUIPC rd, imm Add Upper Immediate to PC rd ← pc + imm
JAL rd, imm Jump And Link rd ← pc + 4; pc ← pc + imm
JALR rd, rs1, imm Jump And Link Register rd ← pc + 4; pc ← rs1 + imm
BEQ rs1, rs2, imm Branch if Equal pc ← si rs1 = rs2: pc + imm, sinon: pc + 4
BNE rs1, rs2, imm Branch if Not Equal pc ← si rs1 ≠ rs2: pc + imm, sinon: pc + 4
BLT rs1, rs2, imm Branch if Less Than pc ← si signed(rs1) < signed(rs2): pc + imm, sinon: pc + 4
BGE rs1, rs2, imm Branch if Greater or Equal pc ← si signed(rs1) ≥ signed(rs2): pc + imm, sinon: pc + 4
BLTU rs1, rs2, imm Branch if Less Than Unsigned pc ← si unsigned(rs1) < unsigned(rs2): pc + imm, sinon: pc + 4
BGEU rs1, rs2, imm Branch if Greater or Equal Unsigned pc ← si unsigned(rs1) ≥ unsigned(rs2): pc + imm, sinon: pc + 4
LB rd, imm(rs1) Load Byte rd ← signed(mem[rs1+imm])
LH rd, imm(rs1) Load Half word rd ← signed(mem[rs1+imm:rs1+imm+1])
LW rd, imm(rs1) Load Word rd ← signed(mem[rs1+imm:rs1+imm+3])
LBU rd, imm(rs1) Load Byte Unsigned rd ← unsigned(mem[rs1+imm])
LHU rd, imm(rs1) Load Half word Unsigned rd ← unsigned(mem[rs1+imm:rs1+imm+1])
SB rs2, imm(rs1) Store Byte mem[rs1+imm] ← rs2[7:0]
SH rs2, imm(rs1) Store Half word mem[rs1+imm:rs1+imm+1] ← rs2[15:0]
SW rs2, imm(rs1) Store Word mem[rs1+imm:rs1+imm+3] ← rs2
ADDI rd, rs1, imm Add Immediate rd ← rs1 + imm
SLLI rd, rs1, imm Shift Left Logical Immediate rd ← rs1 sll imm
SLTI rd, rs1, imm Set on Less Than Immediate rd ← si signed(rs1) < signed(imm): 1, sinon: 0
SLTIU rd, rs1, imm Set on Less Than Immediate Unsigned rd ← si unsigned(rs1) < unsigned(imm): 1, sinon: 0
XORI rd, rs1, imm Exclusive Or Immediate rd ← rs1 xor imm
SRLI rd, rs1, imm Shift Right Logical Immediate rd ← rs1 srl imm
SRAI rd, rs1, imm Shift Right Arithmetic Immediate rd ← rs1 sra imm
ORI rd, rs1, imm Or Immediate rd ← rs1 or imm
ANDI rd, rs1, imm And Immediate rd ← rs1 and imm
ADD rd, rs1, rs2 Add rd ← rs1 + rs2
SUB rd, rs1, rs2 Subtract rd ← rs1 - rs2
SLL rd, rs1, rs2 Shift Left Logical rd ← rs1 sll rs2
SLT rd, rs1, rs2 Set on Less Than rd ← si signed(rs1) < signed(rs2): 1, sinon: 0
SLTU rd, rs1, rs2 Set on Less Than Unsigned rd ← si unsigned(rs1) < unsigned(rs2): 1, sinon: 0
XOR rd, rs1, rs2 Exclusive Or rd ← rs1 xor rs2
SRL rd, rs1, rs2 Shift Right Logical rd ← rs1 srl rs2[4:0]
SRA rd, rs1, rs2 Shift Right Arithmetic rd ← rs1 sra rs2[4:0]
OR rd, rs1, rs2 Or rd ← rs1 or rs2
AND rd, rs1, rs2 And rd ← rs1 and rs2
MRET Machine Return pc ← mepc

Les opérations logiques sont notées de la manière suivante :

Notation Opération
and Et logique bit à bit
or Ou logique bit à bit
xor Ou exclusif bit à bit
sll Décalage logique à gauche (avec insertion de zéros par la droite)
srl Décalage logique à droite (avec insertion de zéros par la gauche)
sra Décalage arithmétique à droite (avec extension du bit de signe)

Gestion des interruptions

Le cœur Virgule réalise un mécanisme simple d’interruption matérielle dans le mode machine du RISC-V. Aucun registre ou instruction de contrôle des interruptions n’est implémenté dans le processeur (cf les CSR de la spécification RISC-V). L’activation, la désactivation et l’interrogation de l’état des interruptions seront gérés, soit individuellement sur chaque périphérique, soit à l’aide d’un contrôleur d’interruption séparé.

Dans le plan mémoire, le vecteur d’interruption se trouve à l’adresse 4. À cette adresse, on placera l’une des choses suivantes :

Lors d’une demande d’interruption :

  1. Le processeur passe dans un mode non interruptible.
  2. La prochaine valeur de pc est mémorisée dans mepc. Si l’instruction en cours d’exécution est un branchement, alors mepc reçoit l’adresse de destination du branchement. Sinon, mepc reçoit pc + 4.
  3. pc reçoit l’adresse du vecteur d’interruption.

Lors d’un retour d’interruption (instruction mret) :

  1. pc reçoit la valeur de mepc.
  2. Le processeur quitte le mode non interruptible.

Codage des instructions

Formats

Le code d’une instruction se compose des champs suivants :

On distingue six formats d’instructions qui utilisent chacun une partie des champs ci-dessus :

Format 31:25 24:20 19:15 14:12 11:7 6:0
R funct7 rs2 rs1 funct3 rd opcode
I imm[11:5]/funct7 imm[4:0] rs1 funct3 rd opcode
S imm[11:5] rs2 rs1 funct3 imm[4:0] opcode
B imm[12,10:5] rs2 rs1 funct3 imm[4:1,11] opcode
U imm[31:25] imm[24:20] imm[19:15] imm[14:12] rd opcode
J imm[20,10:5] imm[4:1,11] imm[19:15] imm[14:12] rd opcode

Les valeurs immédiates subissent une extension de bit de signe pour atteindre 32 bits. Les bits de poids faible qui ne sont pas codés dans l’instruction sont mis à 0.

Les formats B et J sont présentés comme des variantes des formats S et U. Dans les formats B et J, les valeurs immédiates sont toujours paires. Elles représentent un déplacement dans une instruction de branchement. Elles sont codées de manière à posséder un grand nombre de bits communs avec d’autres formats tout en plaçant leur bit de signe sur le bit 31 de l’instruction.

La composition des valeurs immédiates est résumée dans le tableau ci-dessous :

Format imm[31:25] imm[24:21] imm[20] imm[19:15] imm[14:12] imm[11] imm[10:5] imm[4:1] imm[0]
I inst[31] inst[31] inst[31] inst[31] inst[31] inst[31] inst[30:25] inst[24:21] inst[20]
S inst[31] inst[31] inst[31] inst[31] inst[31] inst[31] inst[30:25] inst[11:8] inst[7]
B inst[31] inst[31] inst[31] inst[31] inst[31] inst[7] inst[30:25] inst[11:8] 0
U inst[31:25] inst[24:21] inst[20] inst[19:15] inst[14:12] 0 0 0 0
J inst[31] inst[31] inst[31] inst[19:15] inst[14:12] inst[20] inst[30:25] inst[24:21] 0

Opcodes de base

Le tableau ci-dessous liste les opcodes du jeu d’instructions RISC-V qui ont été conservés dans le cœur Virgule. À chaque opcode, correspond un format d’instruction. Dans la liste des instructions plus bas, on désignera les opcodes par leurs noms plutôt que par leurs valeurs binaires.

inst[6:0] Nom Format
0000011 LOAD I
0010011 OP-IMM I
0010111 AUIPC U
0100011 STORE S
0110011 OP R
0110111 LUI U
1100011 BRANCH B
1100111 JALR I
1101111 JAL J
1110011 SYSTEM I

Liste des instructions

Dans le tableau ci-dessous, la colonne opcode fait référence aux noms figurant dans le tableau des opcodes de base.

Instruction opcode funct3 funct7 rs2
LUI LUI
AUIPC AUIPC
JAL JAL
JALR JALR 000
BEQ BRANCH 000
BNE BRANCH 001
BLT BRANCH 100
BGE BRANCH 101
BLTU BRANCH 110
BGEU BRANCH 111
LB LOAD 000
LH LOAD 001
LW LOAD 010
LBU LOAD 100
LHU LOAD 101
SB STORE 000
SH STORE 001
SW STORE 010
ADDI OP-IMM 000
SLLI OP-IMM 001 0000000
SLTI OP-IMM 010
SLTIU OP-IMM 011
XORI OP-IMM 100
SRLI OP-IMM 101 0000000
SRAI OP-IMM 101 0100000
ORI OP-IMM 110
ANDI OP-IMM 111
ADD OP 000 0000000
SUB OP 000 0100000
SLL OP 001 0000000
SLT OP 010 0000000
SLTU OP 011 0000000
XOR OP 100 0000000
SRL OP 101 0000000
SRA OP 101 0100000
OR OP 110 0000000
AND OP 111 0000000
MRET SYSTEM 000 0011000 00010