Pendant longtemps j'ai rêvé d'apprendre à travailler avec FPGA, j'ai regardé de près. Puis il a acheté un tableau de débogage, a écrit quelques mondes bonjour et a mis le tableau dans une boîte, car on ne savait pas quoi en faire. Puis l'idée est venue: écrivons un générateur vidéo composite pour un ancien téléviseur CRT. L'idée, bien sûr, est drôle, mais je ne connais pas vraiment Verilog, et je dois encore m'en souvenir, et je n'ai pas vraiment besoin de ce générateur ... Et récemment, je voulais regarder vers les processeurs logiciels RISC-V . Vous devez commencer quelque part, et le code Rocket Chip (c'est l'une des implémentations) est écrit en Chisel - c'est un tel DSL pour Scala. Puis je me suis soudain souvenu que pendant deux ans j'avais évolué professionnellement sur Scala et j'ai réalisé: le moment était venu ...
Donc, si vous voulez lire une histoire de la vie des coupe-fils, d'un multimètre numérique et d'un oscilloscope qui s'est réalisé, alors bienvenue chez cat.
Que sera donc cet article? Dans ce document, je décrirai mes tentatives pour générer un signal vidéo PAL composite (pourquoi PAL? - Je viens de découvrir un bon tutoriel spécifiquement sur la génération PAL) sur la carte Mars rover 2 de nckma . À propos de RISC-V dans cet article, je ne dirai rien du tout. :)
Tout d'abord, un peu sur Scala et Chisel: Scala est un langage qui s'exécute au-dessus de la machine virtuelle Java et utilise de manière transparente les bibliothèques Java existantes (bien qu'il existe également Scala.js et Scala Native). Lorsque j'ai commencé à l'étudier pour la première fois, j'avais le sentiment qu'il s'agissait d'un hybride très viable de «plus» et de Haskell (cependant, les collègues ne partagent pas cette opinion) - c'est un système de type douloureusement avancé et un langage concis, mais en raison de la nécessité de croiser le fonctionnalisme avec OOP une abondance de constructions de langage à certains endroits a évoqué des souvenirs de C ++. Cependant, n'ayez pas peur de Scala - c'est un langage très concis et sûr avec un système de type puissant, dans lequel vous pouvez tout d'abord écrire simplement en Java amélioré. Et aussi, pour autant que je sache, Scala a été initialement développé comme un langage pour la création pratique de langages spécifiques au domaine - c'est lorsque vous décrivez, disons, des équipements numériques ou des notes dans un langage formel, et ce langage semble assez logique du point de vue de son domaine. Et puis vous découvrez soudainement que c'était le bon code dans Scala (enfin, ou Haskell) - juste des gens gentils ont écrit une bibliothèque avec une interface pratique. Chisel est juste une telle bibliothèque pour Scala, qui nous permet de décrire la logique numérique sur un DSL pratique, puis d'exécuter le code Scala résultant et de générer du code sur Verilog (ou autre chose) qui peut être copié dans le projet Quartus. Eh bien, ou exécutez immédiatement les tests unitaires standard de style scala, qui simuleront eux-mêmes les bancs d'essai et publieront un rapport sur les résultats.
Pour me familiariser avec les circuits numériques, je recommande fortement ce livre (il est déjà dans la version imprimée russe). En fait, ma connaissance systématique du monde FPGA se termine presque sur ce livre, donc les critiques constructives dans les commentaires sont les bienvenues (cependant, je le répète, le livre est merveilleux: il raconte des bases à la création d'un simple processeur transporté. Et il y a des images là -bas;)). Eh bien, selon Chisel, il existe un bon tutoriel officiel .
Avertissement: l' auteur n'est pas responsable de l'équipement brûlé, et si vous décidez de répéter l'expérience, il est préférable de vérifier les niveaux du signal avec un oscilloscope, de refaire la partie analogique, etc. Et en général - respectez les consignes de sécurité. (Par exemple, lors de la rédaction de l'article, j'ai réalisé que les jambes sont aussi des membres, et il n'y a rien pour les coller dans la batterie de chauffage central, tenant à la sortie de la carte ...) Au fait, cette infection a également causé des interférences avec le téléviseur dans la pièce voisine en cours de débogage ...
Configuration du projet
Nous allons écrire le code dans IntelliJ Idea Community Edition , car le système de construction sera sbt , alors créez un répertoire, mettez .gitignore
, project/build.properties
, project/plugins.sbt
d'ici et
build.sbt quelque peu simplifié def scalacOptionsVersion(scalaVersion: String): Seq[String] = { Seq() ++ {
Ouvrez-le maintenant dans l'Idée et demandez d'importer le projet sbt - tandis que sbt téléchargera les dépendances nécessaires.
Premiers modules
PWM
Tout d'abord, essayons d'écrire un PWM simple. Ma logique était approximativement la suivante: pour générer un signal de rapport cyclique n / m, nous avons initialement mis 0 dans le registre et nous y ajouterons n à chaque étape. Lorsque la valeur du registre dépasse m, soustrayez m et donnez un niveau élevé pour un cycle d'horloge. En fait, ce sera buggé si n> m, mais nous considérerons cela comme un comportement indéfini, qui est nécessaire pour optimiser les cas réels utilisés.
Je ne raconterai pas tout le guide du débutant - il se lit dans une demi-heure, je dirai seulement que pour décrire le module, nous devons importer chisel3._
et hériter de la classe abstraite Module
. Il est abstrait car nous devons décrire le Bundle
sous le nom io
- il aura l'intégralité de l'interface du module. En même temps, les entrées d' clock
et de reset
apparaîtront implicitement - vous n'avez pas besoin de les décrire séparément. Voici ce qui s'est passé:
import chisel3._ class PWM(width: Int) extends Module { val io = IO(new Bundle { val numerator = Input(UInt(width.W)) val denominator = Input(UInt(width.W)) val pulse = Output(Bool()) }) private val counter = RegInit(0.asUInt(width.W)) private val nextValue = counter + io.numerator io.pulse := nextValue > io.denominator counter := Mux(io.pulse, nextValue - io.denominator, nextValue) }
Remarquez, nous appelons la méthode .W
sur un entier régulier pour obtenir la largeur du port, et la .asUInt(width.W)
nous appelons généralement un littéral entier! Comment est-ce possible? - Eh bien, dans Smalltalk, nous définirions simplement une nouvelle méthode pour la classe Integer (ou quoi que ce soit là -bas), mais dans la JVM, nous n'avons toujours pas l'objet entier - il y a aussi des types primitifs, et Scala le comprend (et, en plus, il existe des classes tierces que nous ne pouvons pas modifier). Par conséquent, il existe une variété de s implicites: dans ce cas, Scala trouve probablement quelque chose comme
implicit class BetterInt(n: Int) { def W: Width = ... }
dans le cadre actuel, les ints ordinaires ont donc des super-pouvoirs. Voici l'une des fonctionnalités qui rendent Scala plus concis et facile à créer DSL.
Ajoutez une pincée de tests à cela. import chisel3.iotesters._ import org.scalatest.{FlatSpec, Matchers} object PWMSpec { class PWMTesterConstant(pwm: PWM, denum: Int, const: Boolean) extends PeekPokeTester(pwm) { poke(pwm.io.numerator, if (const) denum else 0) poke(pwm.io.denominator, denum) for (i <- 1 to 2 * denum) { step(1) expect(pwm.io.pulse, const) } } class PWMTesterExact(pwm: PWM, num: Int, ratio: Int) extends PeekPokeTester(pwm) { poke(pwm.io.numerator, num) poke(pwm.io.denominator, num * ratio) val delay = (1 to ratio + 2).takeWhile { _ => step(1) peek(pwm.io.pulse) == BigInt(0) } println(s"delay = $delay") for (i <- 1 to 10) { expect(pwm.io.pulse, true) for (j <- 1 to ratio - 1) { step(1) expect(pwm.io.pulse, false) } step(1) } } class PWMTesterApproximate(pwm: PWM, num: Int, denom: Int) extends PeekPokeTester(pwm){ poke(pwm.io.numerator, num) poke(pwm.io.denominator, denom) val count = (1 to 100 * denom).map { _ => step(1) peek(pwm.io.pulse).toInt }.sum val diff = count - 100 * num println(s"Difference = $diff") expect(Math.abs(diff) < 3, "Difference should be almost 0") } } class PWMSpec extends FlatSpec with Matchers { import PWMSpec._ behavior of "PWMSpec" def testWith(testerConstructor: PWM => PeekPokeTester[PWM]): Unit = { chisel3.iotesters.Driver(() => new PWM(4))(testerConstructor) shouldBe true } it should "return True constant for 1/1" in { testWith(new PWMTesterConstant(_, 1, true)) } it should "return True constant for 10/10" in { testWith(new PWMTesterConstant(_, 10, true)) } it should "return False constant for 1/1" in { testWith(new PWMTesterConstant(_, 1, false)) } it should "return False constant for 10/10" in { testWith(new PWMTesterConstant(_, 10, false)) } it should "return True exactly once in 3 steps for 1/3" in { testWith(new PWMTesterExact(_, 1, 3)) } it should "return good approximation for 3/10" in { testWith(new PWMTesterApproximate(_, 3, 10)) } }
PeekPokeTester
est l'un des trois testeurs standard de Chisel. Il vous permet de régler les valeurs aux entrées du DUT (appareil sous test) et de vérifier les valeurs aux sorties. Comme nous pouvons le voir, le ScalaTest habituel est utilisé pour les tests et les tests prennent 5 fois plus de place que l'implémentation elle-même, ce qui est en principe normal pour les logiciels. Cependant, je soupçonne que les développeurs expérimentés d'équipements "coulés dans du silicium" ne souriront qu'avec un tel nombre microscopique de tests. Lancement et oups ...
Circuit state created [info] [0,000] SEED 1529827417539 [info] [0,000] EXPECT AT 1 io_pulse got 0 expected 1 FAIL ... [info] PWMSpec: [info] PWMSpec [info] - should return True constant for 1/1 [info] - should return True constant for 10/10 *** FAILED *** [info] false was not equal to true (PWMSpec.scala:56) [info] - should return False constant for 1/1 [info] - should return False constant for 10/10 [info] - should return True exactly once in 3 steps for 1/3 [info] - should return good approximation for 3/10
Oui, io.pulse := nextValue > io.denominator
dans PWM dans la ligne io.pulse := nextValue > io.denominator
connectez-vous >=
, redémarrez les tests - tout fonctionne! J'ai peur que les développeurs expérimentés d'équipements numériques veuillent me tuer pour une attitude aussi frivole envers la conception (et certains développeurs de logiciels se joindront volontiers à eux) ...
Générateur d'impulsions
Nous aurons également besoin d'un générateur qui émettra des impulsions de synchronisation pour les "demi-trames". Pourquoi semi? parce que d'abord les lignes impaires sont transmises, puis les paires (enfin, ou vice versa, mais maintenant on ne se soucie plus des graisses).
import chisel3._ import chisel3.util._ class OneShotPulseGenerator(val lengths: Seq[Int], val initial: Boolean) extends Module {
Lorsque le signal de reset
est supprimé, il tire avec des impulsions rectangulaires avec les longueurs des intervalles entre commutations spécifiées par le paramètre lengths
, après quoi il reste pour toujours dans le dernier état. Cet exemple illustre l'utilisation de tables de valeurs à l'aide de VecInit
, ainsi qu'un moyen d'obtenir la largeur de registre requise: chisel3.util.log2Ceil(maxVal + 1).W
. Honnêtement, je ne me souviens pas comment cela a été fait dans Verilog, mais dans Chisel, pour créer un tel module paramétré par un vecteur de valeurs, il suffit d'appeler le constructeur de la classe avec le paramètre requis.
Vous vous demandez probablement: "Si les entrées d' clock
et de reset
sont générées implicitement, comment allons-nous" recharger "le générateur d'impulsions pour chaque image? Les développeurs de ciseaux ont tout prévu:
val module = Module( new MyModule() ) val moduleWithCustomReset = withReset(customReset) { Module( new MyModule() ) } val otherClockDomain = withClock(otherClock) { Module( new MyModule() ) }
Implémentation naïve d'un générateur de signaux
Pour que le téléviseur nous comprenne au moins d'une manière ou d'une autre, vous devez prendre en charge le «protocole» du niveau de truc moyen: il existe trois niveaux de signal importants:
- 1.0V - couleur blanche
- 0,3 V - couleur noire
- 0V - niveau spécial
Pourquoi ai-je appelé 0V spécial? Parce qu'avec une transition en douceur de 0,3 V à 1,0 V, nous passons en douceur du noir au blanc, et entre 0 V et 0,3 V, pour autant que je puisse comprendre, il n'y a pas de niveaux intermédiaires et 0 V est utilisé uniquement pour la synchronisation. (En fait, il ne change même pas dans la plage 0V - 1V, mais -0,3V - 0,7V, mais, espérons-le, il y a toujours un condensateur à l'entrée)
Comme nous l'apprend ce merveilleux article , un signal PAL composite se compose d'un flux sans fin de 625 lignes répétitives: la plupart sont des lignes, en fait, des images (séparément paires et impaires séparément), certaines sont utilisées à des fins de synchronisation (nous avons fait le générateur pour elles signaux), certains ne sont pas visibles à l'écran. Ils ressemblent à ceci (je ne vais pas pirater et donner des liens vers l'original):
Essayons de décrire les interfaces des modules:
BWGenerator
gérera les horaires, etc., il doit savoir à quelle fréquence il fonctionne:
class BWGenerator(clocksPerUs: Int) extends Module { val io = IO(new Bundle { val L = Input(UInt(8.W)) val x = Output(UInt(10.W)) val y = Output(UInt(10.W)) val inScanLine = Output(Bool()) val millivolts = Output(UInt(12.W)) })
PalColorCalculator
calculera le niveau du signal de luminance, ainsi qu'un signal de couleur supplémentaire:
class PalColorCalculator extends Module { val io = IO(new Bundle { val red = Input(UInt(8.W)) val green = Input(UInt(8.W)) val blue = Input(UInt(8.W)) val scanLine = Input(Bool()) val L = Output(UInt(8.W)) val millivolts = Output(UInt(12.W)) })
Dans le module PalGenerator
, PalGenerator
simplement PalGenerator
deux modules spécifiés:
class PalGenerator(clocksPerUs: Int) extends Module { val io = IO(new Bundle { val red = Input(UInt(8.W)) val green = Input(UInt(8.W)) val blue = Input(UInt(8.W)) val x = Output(UInt(10.W)) val y = Output(UInt(10.W)) val millivolts = Output(UInt(12.W)) }) val bw = Module(new BWGenerator(clocksPerUs)) val color = Module(new PalColorCalculator) io.red <> color.io.red io.green <> color.io.green io.blue <> color.io.blue bw.io.L <> color.io.L bw.io.inScanLine <> color.io.scanLine bw.io.x <> io.x bw.io.y <> io.y io.millivolts := bw.io.millivolts + color.io.millivolts }
Et maintenant, nous dessinons malheureusement le premier hibou ... package io.github.atrosinenko.fpga.tv import chisel3._ import chisel3.core.withReset import io.github.atrosinenko.fpga.common.OneShotPulseGenerator object BWGenerator { val ScanLineHSyncStartUs = 4.0 val ScanLineHSyncEndUs = 12.0 val TotalScanLineLengthUs = 64.0 val VSyncStart = Seq( 2, 30, 2, 30,
Génération de code synthétisé
Tout cela est bien, mais nous voulons coudre le motif résultant dans une planche. Pour ce faire, vous devez synthétiser Verilog. Cela se fait de manière très simple:
import chisel3._ import io.github.atrosinenko.fpga.common.PWM object Codegen { class TestModule(mhz: Int) extends Module { val io = IO(new Bundle { val millivolts = Output(UInt(12.W)) }) val imageGenerator = Module(new TestColorImageGenerator(540, 400)) val encoder = Module(new PalGenerator(clocksPerUs = mhz)) imageGenerator.io.x <> encoder.io.x imageGenerator.io.y <> encoder.io.y imageGenerator.io.red <> encoder.io.red imageGenerator.io.green <> encoder.io.green imageGenerator.io.blue <> encoder.io.blue io.millivolts := encoder.io.millivolts override def desiredName: String = "CompositeSignalGenerator" } def main(args: Array[String]): Unit = { Driver.execute(args, () => new PWM(12)) Driver.execute(args, () => new TestModule(mhz = 32)) } }
En fait, dans la méthode à deux lignes main()
nous le faisons deux fois, le reste du code est un autre module qui colle ensuite
Générateur d'images de test absolument terne class TestColorImageGenerator(width: Int, height: Int) extends Module { val io = IO(new Bundle { val red = Output(UInt(8.W)) val green = Output(UInt(8.W)) val blue = Output(UInt(8.W)) val x = Input(UInt(10.W)) val y = Input(UInt(10.W)) }) io.red := Mux((io.x / 32.asUInt + io.y / 32.asUInt)(0), 200.asUInt, 0.asUInt) io.green := Mux((io.x / 32.asUInt + io.y / 32.asUInt)(0), 200.asUInt, 0.asUInt) io.blue := Mux((io.x / 32.asUInt + io.y / 32.asUInt)(0), 0.asUInt, 0.asUInt) }
Vous devez maintenant l'intégrer au projet Quartus. Pour Mars rover 2, nous aurons besoin de la version gratuite de Quartus 13.1. Comment l'installer, il est écrit sur le site des Mars Rovers. De là , j'ai téléchargé le "Premier projet" pour la carte Mars Rover 2, je l'ai mis dans le référentiel et je l'ai un peu corrigé. Comme je ne suis pas ingénieur en électronique (et FPGA je m'intéresse plus aux accélérateurs qu'aux cartes d'interface), alors
comme dans cette blague ...Le programmeur est profondément impliqué dans le débogage.
Fils en forme:
"Papa, pourquoi le soleil se lève-t-il à l'est tous les jours et s'assoit-il à l'ouest?"
"Avez-vous vérifié cela?"
- Vérifié.
- Bien vérifié?
- Bien.
- Ça marche?
- Ça marche.
- Ça marche tous les jours?
- Oui, tous les jours.
- Alors pour l'amour de Dieu, fils, ne touche Ă rien, ne change rien.
... Je viens de supprimer le générateur de signaux VGA et d'ajouter mon module.

Après cela, j'ai connecté le tuner TV analogique à un autre ordinateur (ordinateur portable), afin qu'il y ait au moins une certaine isolation galvanique entre l'alimentation du générateur de signaux et du consommateur et j'ai simplement envoyé un signal des broches IO7 (+) et GND (-) de la carte à l'entrée composite (moins contact externe, plus au centre). Eh bien, c'est, comme "simple" ... Ce serait simplement si les mains se sont développées de l'endroit où elles devraient, eh bien, ou si j'avais des fils de connexion femelle-mâle. Mais je n'ai qu'un tas de fils mâles-mâles. Mais j'ai de la ténacité et des pinces! En général, il y a un fil de constipation, je me suis fait presque deux ouvriers - avec difficulté, mais accroché à la planche. Et voici ce que j'ai vu:

En fait, je vous ai bien sûr un peu trompé. Le code montré ci-dessus que j'ai obtenu après environ trois heures de débogage "sur le matériel", mais, bon sang, je l'ai écrit, et ça marche !!! Et, étant donné que j'étais peu familier avec l'électronique sérieuse, je pense que la tâche n'était pas terrible, quelle tâche difficile.
Génération de vidéo couleur
Eh bien, la chose reste petite - pour ajouter un générateur de signal vidéo couleur. J'ai suivi le didacticiel et j'ai commencé à essayer de former un éclat de couleur (ajouté au niveau de noir de l'onde sinusoïdale à la fréquence porteuse du signal de couleur, qui est produit pendant une courte période pendant HSync) et, en fait, le signal de couleur selon la formule. Mais cela ne sort pas, même si vous craquez ... À un moment donné, je me suis rendu compte que, malgré le fait que la fréquence ne m'ait pas attiré un coup d'œil rapide sur le document, le téléviseur était à peine réglé sur un arbitraire. Après recherche, j'ai trouvé que le PAL utilise une fréquence porteuse de 4,43 MHz. Le truc, c'est le chapeau, pensai-je. "Va te faire foutre," répondit le tuner. Après une journée entière de débogage et une seule fois des aperçus de couleurs sur l'image (d'ailleurs, quand j'ai dit au tuner que c'était NTSC en général)
... j'ai réalisé à quoi ressemble vraiment le désespoir J'ai alors réalisé que je ne pouvais pas me passer d'un oscilloscope. Et, comme je l’ai déjà dit, je ne suis pas familier avec l’électronique et, bien sûr, je n’ai pas un tel miracle technologique à la maison. Pour acheter? Un peu cher pour une expérience ... Et à partir de quoi peut-il être construit sur le genou? Connecter un signal à l'entrée ligne de la carte son? Oui, 4 mégahertz et demi - est peu susceptible de démarrer (au moins sans modification). Hmm, le rover Mars a un ADC à 20 MHz, mais le transfert d'un flux brut de vitesse d'interface série vers l'ordinateur n'est pas suffisant. Eh bien, quelque part, vous devez toujours traiter le signal pour l'affichage, et en fait il y aura une quantité acceptable de bits d'information, mais c'est aussi pour jouer avec le port série, écrire des programmes pour l'ordinateur ... Ensuite, j'ai pensé que l'ingénieur devrait développer il y a une bonne ténacité en soi: il y a un imageur couleur inactif, il y a un ADC ... Mais l'image en noir et blanc est restituée de manière stable ... Eh bien, laissez le générateur de signaux se déboguer lui-même!
Digression lyrique (comme on dit: «L'opinion de l'élève ne doit pas nécessairement coïncider avec l'opinion de l'enseignant, le bon sens et les axiomatiques de Peano»): Lorsque j'ai ajouté la génération de couleurs avec toutes sortes de multiplications et d'autres choses compliquées, Fmax s'est affaissé fortement pour le conditionneur de signal. Qu'est-ce que Fmax? D'après ce que j'ai compris dans le manuel Harris & Harris, CAD pour FPGA préfère quand ils écrivent sur Verilog non pas comme dans la norme, mais «par concepts»: par exemple, le résultat devrait être un circuit synchrone - une sorte de web acyclique directionnel issu de la logique combinatoire (addition, multiplication , division, opérations logiques, ...), collé avec ses entrées et sorties aux sorties et entrées des déclencheurs, respectivement. Un déclencheur sur le bord du signal d'horloge se souvient de la valeur de son entrée pour tout le cycle d'horloge suivant, dont le niveau doit être stable quelque temps avant le front et quelque temps après (ce sont deux constantes de temps). Les signaux des sorties des déclencheurs, à leur tour, après le signal d'horloge commencent leur course vers les sorties de la logique combinatoire (et, par conséquent, les entrées d'autres déclencheurs. Eh bien, et les sorties du microcircuit), qui est également caractérisé par deux intervalles: le temps pendant lequel aucune sortie n'a encore aura le temps de commencer à changer, et le temps après lequel les changements se calmeront (à condition que l'entrée ait changé une fois). Voici la fréquence maximale à laquelle la logique combinatoire garantit que les exigences des déclencheurs sont remplies - c'est Fmax. Lorsque le circuit entre deux horloges devrait avoir le temps de compter davantage, Fmax diminue. Bien sûr, je veux que la fréquence soit plus grande, mais si elle a soudainement sauté 10 fois (ou même le nombre de domaines de fréquence dans le rapport CAD a diminué) - vérifiez, peut-être que vous avez gâché quelque chose quelque part, et en conséquence, CAD a trouvé une expression constante et je l'ai utilisé avec bonheur pour l'optimisation.
Promotion des oscilloscopes
Non, pas celui après lequel il y a une torsion de l'oscilloscope et une poignée de pièces supplémentaires, mais l'amorçage de l'oscilloscope est comme l'amorçage du compilateur, uniquement pour l'oscilloscope.
Nous ferons un oscilloscope, sur commande, enregistrant quelques échantillons du signal d'entrée, après quoi afficher uniquement l'enregistrement. Puisqu'il devra en quelque sorte donner une commande à enregistrer, et après - naviguer à travers, nous aurons besoin de contrôleurs de boutons - j'ai écrit pas très pratique, mais assez primitif, le voici:
class SimpleButtonController( clickThreshold: Int, pressThreshold: Int, period: Int, pressedIsHigh: Boolean ) extends Module { val io = IO(new Bundle { val buttonInput = Input(Bool()) val click = Output(Bool()) val longPress = Output(Bool()) })
CHOC! SENSATION! Pour le faire fonctionner, il vous suffit de ... private val cycleCounter = RegInit(0.asUInt(32.W)) private val pressedCounter = RegInit(0.asUInt(32.W)) io.click := false.B io.longPress := false.B when (cycleCounter === 0.asUInt) { when (pressedCounter >= pressThreshold.asUInt) { io.longPress := true.B }.elsewhen (pressedCounter >= clickThreshold.asUInt) { io.click := true.B } cycleCounter := period.asUInt pressedCounter := 0.asUInt } otherwise { cycleCounter := cycleCounter - 1.asUInt when (io.buttonInput === pressedIsHigh.B) { pressedCounter := pressedCounter + 1.asUInt } } }
:
class Oscilloscope( clocksPerUs: Int, inputWidth: Int, windowPixelWidth: Int, windowPixelHeight: Int ) extends Module { val io = IO(new Bundle { val signal = Input(UInt(inputWidth.W)) val visualOffset = Input(UInt(16.W)) val start = Input(Bool()) val x = Input(UInt(10.W)) val y = Input(UInt(10.W)) val output = Output(Bool()) }) private val mem = SyncReadMem(1 << 15, UInt(inputWidth.W)) private val physicalPixel = RegInit(0.asUInt(32.W)) when (io.start) { physicalPixel := 0.asUInt } when (physicalPixel < mem.length.asUInt) { mem.write(physicalPixel, io.signal) physicalPixel := physicalPixel + 1.asUInt } private val shiftedX = io.x + io.visualOffset private val currentValue = RegInit(0.asUInt(inputWidth.W)) currentValue := ((1 << inputWidth) - 1).asUInt - mem.read( Mux(shiftedX < mem.length.asUInt, shiftedX, (mem.length - 1).asUInt) ) when (io.x > windowPixelWidth.asUInt || io.y > windowPixelHeight.asUInt) {
— , :
class OscilloscopeController( visibleWidth: Int, createButtonController: () => SimpleButtonController ) extends Module { val io = IO(new Bundle { val button1 = Input(Bool()) val button2 = Input(Bool()) val visibleOffset = Output(UInt(16.W)) val start = Output(Bool()) val leds = Output(UInt(4.W)) }) val controller1 = Module(createButtonController()) val controller2 = Module(createButtonController()) controller1.io.buttonInput <> io.button1 controller2.io.buttonInput <> io.button2 private val offset = RegInit(0.asUInt(16.W)) private val leds = RegInit(0.asUInt(4.W)) io.start := false.B when (controller1.io.longPress && controller2.io.longPress) { offset := 0.asUInt io.start := true.B leds := leds + 1.asUInt }.elsewhen (controller1.io.click) { offset := offset + (visibleWidth / 10).asUInt }.elsewhen (controller2.io.click) { offset := offset - (visibleWidth / 10).asUInt }.elsewhen (controller1.io.longPress) { offset := offset + visibleWidth.asUInt }.elsewhen (controller2.io.longPress) { offset := offset - visibleWidth.asUInt } io.visibleOffset := offset io.leds := leds }
(, ), - : — , — , ( — ). — ! , Verilog ?..
- , FPGA:

— ( IO7, VGA_GREEN R-2R ) :

, — , , . PAL — "Picture At Last (-, !)"
GitHub .
Conclusions
Scala + Chisel — , Higher-kinded types. Scala- , Chisel , . . — !
: " -?" — ! ...