Simulation de circuits binaires
Dans ce TD, vous allez construire un petit simulateur de circuits binaires (plus précisément, un compteur) en utilisant la bibliothèque du projet. Le TD peut se faire en utilisant l'implantation séquentielle fournie ici, et vous servira de tests supplémentaire pour l'implantation parallèle de votre projet. Si vous avez déjà implanté un moteur d'exécution parallèle, vous pouvez bien sûr l'utiliser pour ce TD.
Commencez par créer dans le projet un fichier src/bin/circuit.rs
contenant le
code suivant (si vous avez changé le nom du projet dans le Cargo.toml
,
remplacez extern crate rrs
en utilisant le nom que vous avez indiqué) :
extern crate rrs; use rrs::api::prelude::*; use rrs::common::prelude::*; fn main() { // ... }
Vous pourrez ainsi lancer votre code pour le TD à l'aide de la commande cargo run --bin circuit
. Une fois que le code fonctionne, il est recommandé de
l'ajouter aux tests dans src/lib.rs
!
Circuits combinatoires
Nous allons commencer par implanter un additioneur 8 bits à l'aide du moteur d'exécution. Vous pouvez vous référer à la page wikipédia pour les détails du circuit d'un additioneur (les schémas de la version anglaise sont plus détaillés).
Question 1. En vous inspirant des tests d'exemple fournis avec le projet
dans le fichier src/lib.rs
, écrivez un demi-additioneur sur 1 bit en
utilisant des ports booléens. Vous utiliserez pour cela le module
sequential::multiple_uses
, ainsi que des variables locales pour sauver le
résultat de l'addition. Vérifiez que la table de vérité de votre
demi-additioneur est correcte, et expliquez pourquoi il est plus simple de
créer le circuit en commençant par la fin.
Question 2. La création de circuits en commençant par la fin est peu
intuitif, et requiert une certaine gymnastique mentale. Nous allons donc
plutôt créer les circuits en modifiant les activateurs après coup. Le code
suivant définit un type utilitaire InputBuilder
permettant de combiner une
arête entrante avec une fonction pour y connecter un activateur, ainsi qu'une
macro pour faciliter son utilisation.
# #![allow(unused_variables)] #fn main() { pub struct InputBuilder<E, C> { pub edge: E, pub connect: C, } impl<'b, 'r: 'b, E: InputEdgeMut<Runtime<'r>> + 'r, C: FnOnce(RuntimeActivator<'r>) + 'b> InputBuilder<E, C> { pub fn new(edge: E, connect: C) -> Self { InputBuilder { edge, connect } } } macro_rules! Input { ($a:lifetime, $r:lifetime, $dtype:ty) => { InputBuilder< DataInput<RcReceiver<Cell<$dtype>>>, impl FnOnce(RuntimeActivator<$r>) + $a, > }; } #}
Il peut être utilisé par exemple en définissant des méthodes additionelles sur
le ScopedGraphBuilder
:
# #![allow(unused_variables)] #fn main() { impl<'a, 'r> ScopedGraphBuilder<'a, Runtime<'r>> { pub fn binop<T: 'r>( &mut self, lhs: Input!['a, 'r, Option<T>], rhs: Input!['a, 'r, Option<T>], f: impl Fn(T, T) -> T + 'r, ) -> Input!['a, 'r, Option<T>] { let (out_send, out_recv) = self.port(None).split(); let mut node = self.node(TaskNode { inputs: (lhs.edge, rhs.edge), outputs: (out_send.with_activator(Default::default()),), task: StrictTask::new( move |lhs, rhs| (Some(f(lhs.unwrap(), rhs.unwrap())),)), }); (lhs.connect)(node.add_activator()); (rhs.connect)(node.add_activator()); InputBuilder::new(out_recv.as_data_input(), move |activator| { node.borrow_mut().outputs.0.activator = activator; }) } } #}
Vous pouvez ensuite utiliser ces fonctions dans votre code:
# #![allow(unused_variables)] #fn main() { let (lhs_sender, lhs_receiver) = runtime.port(None).split(); let (rhs_sender, rhs_receiver) = runtime.port(None).split(); let mut lhs_in = lhs_sender.with_activator(Default::default()); let mut rhs_in = rhs_sender.with_activator(Default::default()); { let lhs_ref = &mut lhs_in; let rhs_ref = &mut rhs_in; runtime.build_scope(move |b| { let i32_add = b.binop( InputBuilder::new(lhs_receiver.as_data_input(), move |x| { *lhs_ref = x }), InputBuilder::new(rhs_receiver.as_data_input(), move |x| { *rhs_ref = x }), |x: i32, y: i32| -> i32 { x + y }, ); // Use i32_add.edge and i32_add.connect... }) } #}
En suivant l'exemple du binop
fourni ci-dessus, ajoutez les méthodes
suivantes (qui opèrent sur des booléens) au ScopedGraphBuilder
:
not
qui calcule un "non" binairexor
qui calcule un "ou exclusif" binaireand
qui calcule un "et" binaireor
qui calcule un "ou" binairenand
qui calcule un "non-et" binairedup
qui duplique son entrée
Question 3. Ajoutez au ScopedGraphBuilder
une méthode half_adder
construisant un demi-additioneur 1 bit, puis une méthode full_adder
construisant un additioneur 1 bit. Vérifiez leurs tables de vérité, puis
construisez un additioneur 4 bits en chaînant plusieurs additioneurs 1 bit
(voir la section "additioneur parallèle à propagation de retenue" ou
"Ripple-carry adder" sur Wikipédia). Effectuez quelques calculs simples comme
2 + 3
(0b10 + 0b11
) et vérifiez que le résultat est correct.
Question 4 (optionnel). Construisez des additioneur 8, 16 et 32 bits en utilisant le principe de la retenue anticipée.
Circuits séquentiels
On veut maintenant gérer des circuits à travers plusieurs instants. Plusieurs mécanismes sont possibles pour gérer les instants; nous utiliserons dans le TD une liste de noeuds à exécuter à chaque instant. Au début d'un instant, les noeuds de la liste sont activés, puis on effectue une exécution classique.
Question 5. Ajoutez au runtime un nouveau champ clock
de type
Vec<RuntimeActivator<'r>>
et modifiez la méthode execute
pour qu'elle
appelle tous les activateurs au début de l'instant.
Question 6. Écrivez une méthode const_fby
qui calcule un retard, de signature:
# #![allow(unused_variables)] #fn main() { pub fn const_fby( &mut self, init: bool, val: Input!['a, 'r, bool], ) -> Input!['a, 'r, bool] #}
La valeur du noeud de sortie est init
au premier instant, puis la valeur de
val
à l'instant précédent. Vous implémenterez const_fby
à l'aide de deux
noeuds set
et get
et d'une dépendance de contrôle assurant que get
est
exécuté avant set
au sein d'un instant. N'oubliez pas d'enregistrer get
dans l'horloge!
Note : De la même façon que as_data_input
et as_data_output
sont fournis
dans le projet, il pourra être judicieux d'ajouter une méthode
as_control_output
sur les activateurs pour les transformer en arêtes sans
composante de données (type de donnée ()
).
Question 7. En utilisant les fonctions des questions précédantes, écrivez un circuit séquentiel sur 4 (ou plus) bits qui compte en partant de 0 et s'arrête en cas d'overflow.
Question 8 (optionnel). Implémentez un circuit à une deux entrées a: bool
et b: i32
et qui calcule if a { (b + 2) * 2 } else { (b * 2) + 2 }
en
n'utilisant une seule addition et une seule multiplication.