Notizen des Amateurs oder die Geschichte der Konfiguration des FPGA Scala-Entwicklers

  • Tutorial

Ich habe lange davon geträumt, zu lernen, wie man mit FPGA arbeitet. Ich habe zugesehen. Dann kaufte ich ein Debug-Board, schrieb ein paar Hallo-Welten und steckte das Board in eine Box, weil nicht klar war, was ich damit anfangen sollte. Dann kam die Idee: Lassen Sie uns einen Composite-Videosignalgenerator für einen alten CRT-Fernseher schreiben. Die Idee ist natürlich witzig, aber ich kenne Verilog wirklich nicht, und ich muss mich noch daran erinnern, und ich brauche diesen Generator nicht so sehr ... Und ich wollte vor kurzem nach RISC-V- Softwareprozessoren suchen . Sie müssen irgendwo anfangen, und der Rocket-Chip- Code (dies ist eine der Implementierungen) ist in Chisel geschrieben - dies ist das DSL für Scala. Dann fiel mir plötzlich ein, dass ich zwei Jahre lang professionell bei Scala gearbeitet hatte und wusste: Die Zeit ist gekommen ...


Wenn Sie also die Geschichte des Zangenlebens, des Digitalmultimeters und des sich selbst bewusst gewordenen Oszilloskops lesen möchten, dann sind Sie bei der Katze willkommen.


Was wird also in diesem Artikel sein? Darin beschreibe ich meine Versuche, ein zusammengesetztes PAL-Videosignal zu erzeugen (warum PAL? Ich habe gerade ein gutes Tutorial nur zum Erzeugen von PAL erhalten) auf dem Mars Rover 2 von nckma . Ich werde in diesem Artikel nichts über RISC-V sagen. :)


Zunächst ein wenig über Scala und Chisel: Scala ist eine Sprache, die auf der Java Virtual Machine ausgeführt wird und vorhandene Java-Bibliotheken transparent verwendet (obwohl auch Scala.js und Scala Native verfügbar sind). Als ich begann , es zu studieren, habe ich das Gefühl , dass dies eine sehr tragfähige ist hybrid von „Profis“ und Haskell (aber Kollegen dieser Meinung nicht teilen) - ein bisschen zu weit fortgeschritten Systemtypen und prägnante Sprache, sondern wegen der Notwendigkeit zu überqueren funktsionalschinu mit OOP Die Fülle von Sprachkonstrukten an einigen Stellen rief Erinnerungen an C ++ hervor. Haben Sie jedoch keine Angst vor Scala - es ist eine sehr lakonische und sichere Sprache mit einem mächtigen Typsystem, das Sie zunächst einfach in verbessertem Java schreiben können. Soweit ich weiß, wurde Scala ursprünglich als eine Sprache entwickelt, die sich leicht gestalten lässt.Bei domänenspezifischen Sprachen werden beispielsweise digitale Geräte oder Notizen in einer formalen Sprache beschrieben, und diese Sprache erscheint aus der Sicht ihres Themenbereichs recht logisch. Und dann stellt man plötzlich fest, dass es der richtige Code auf Scala (oder Haskell) war - freundliche Leute haben eine Bibliothek mit einem praktischen Interface geschrieben. Chisel ist eine solche Scala-Bibliothek, mit der Sie digitale Logik auf einem geeigneten DSL beschreiben und dann den resultierenden Scala-Code ausführen und den Code bei Verilog (oder etwas anderem) generieren können, der in das Quartus-Projekt kopiert werden kann. Nun, oder führen Sie sofort die Standard-Unit-Tests im Scala-Stil aus, die selbst Testbenches simulieren und einen Bericht über die Ergebnisse erstellen.


Zum Kennenlernen digitaler Schaltungen empfehle ich dieses Buch (es existiert bereits in der gedruckten russischen Version). In der Tat endet meine systematische Bekanntschaft mit der FPGA-Welt in diesem Buch fast, daher ist konstruktive Kritik in den Kommentaren willkommen (jedoch, ich wiederhole, das Buch ist wunderbar: Es erzählt von den Grundlagen bis zur Erstellung eines einfachen, konturierten Prozessors. Und es gibt Bilder;). Nun, laut Meißel gibt es ein gutes offizielles Tutorial .


Haftungsausschluss: Der Autor ist nicht für den Verlust der Ausrüstung verantwortlich. Wenn Sie das Experiment wiederholen möchten, überprüfen Sie die Signalpegel mit einem Oszilloskop, ändern Sie den analogen Teil usw. Befolgen Sie im Allgemeinen die Sicherheitsvorkehrungen. (Ich habe zum Beispiel beim Schreiben des Artikels festgestellt, dass auch meine Beine Gliedmaßen sind, und ich muss sie nicht an die Zentralheizungsbatterie kleben und meine Hand über den Ausgang der Platine halten ...) Diese Infektion störte übrigens auch den Fernseher im nächsten Raum beim Debuggen ...


Projekteinrichtung


Code über wir in sein werde IntelliJ Idea Community Edition , als Buildsystem SBT daher ein Verzeichnis erstellen, setzen wir auf .gitignore, project/build.properties, project/plugins.sbthier und


etwas vereinfachtes build.sbt
defscalacOptionsVersion(scalaVersion: String): Seq[String] = {
  Seq() ++ {
    // If we're building with Scala > 2.11, enable the compile option//  switch to support our anonymous Bundle definitions://  https://github.com/scala/bug/issues/10047CrossVersion.partialVersion(scalaVersion) match {
      caseSome((2, scalaMajor: Long)) if scalaMajor < 12 => Seq()
      case _ => Seq("-Xsource:2.11")
    }
  }
}
name := "chisel-example"
version := "1.0.0"
scalaVersion := "2.11.12"
resolvers ++= Seq(
  Resolver.sonatypeRepo("snapshots"),
  Resolver.sonatypeRepo("releases")
)
// Provide a managed dependency on X if -DXVersion="" is supplied on the command line.val defaultVersions = Map(
  "chisel3" -> "3.1.+",
  "chisel-iotesters" -> "1.2.+"
  )
libraryDependencies ++= (Seq("chisel3","chisel-iotesters").map {
  dep: String => "edu.berkeley.cs" %% dep % sys.props.getOrElse(dep + "Version", defaultVersions(dep)) })
scalacOptions ++= scalacOptionsVersion(scalaVersion.value)

Jetzt öffnen wir es in der Idee und bitten um Import des sbt-Projekts - während sbt die notwendigen Abhängigkeiten herunterlädt.


Erste Module


PWM


Versuchen wir zunächst, eine einfache PWM zu schreiben . Die Logik, die ich hatte, war ungefähr die folgende: Um ein Signal mit dem Füllfaktor n / m zu erzeugen, setzen wir zunächst das Register 0 und addieren es n in jedem Schritt. Wenn der Registerwert m überschreitet, subtrahieren Sie m und geben Sie für einen Taktzyklus einen hohen Pegel an. Tatsächlich schlägt dies fehl, wenn n> m ist. Wir betrachten dies jedoch als undefiniertes Verhalten, das zur Optimierung der tatsächlich verwendeten Fälle erforderlich ist.


Ich werde den gesamten Anfängerleitfaden nicht erneut erzählen - er wird in einer halben Stunde gelesen, ich sage nur, dass wir zur Beschreibung des Moduls chisel3._die abstrakte Klasse importieren und erben müssen Module. Es ist abstrakt, weil wir es Bundleunter dem Namen beschreiben müssen io- es wird die gesamte Schnittstelle des Moduls enthalten. Gleichzeitig werden wir implizite Eingänge haben clockund reset- es ist nicht notwendig, sie separat zu beschreiben. Folgendes ist passiert:


import chisel3._
classPWM(width: Int) extendsModule{
  val io = IO(newBundle {
    val numerator   = Input(UInt(width.W))
    val denominator = Input(UInt(width.W))
    val pulse = Output(Bool())
  })
  privateval counter = RegInit(0.asUInt(width.W))
  privateval nextValue = counter + io.numerator
  io.pulse := nextValue > io.denominator
  counter := Mux(io.pulse, nextValue - io.denominator, nextValue)
}

Beachten Sie, dass wir die Methode .Wmit dem normalen int aufrufen, um die Portbreite zu erhalten, und .asUInt(width.W)wir rufen die Methode im Allgemeinen mit dem Integer-Literal auf! Wie ist das möglich? - Nun, in Smalltalk würden wir einfach die Integer-Klasse definieren (oder wie auch immer sie genannt wird), aber in der JVM haben wir immer noch kein gesamtes Objekt - es gibt auch primitive Typen, und Scala versteht dies (und außerdem Es gibt Kurse von Drittanbietern, die wir nicht ändern können. Daher gibt es verschiedene implizite s: In diesem Fall findet Scala wahrscheinlich etwas Ähnliches


implicitclassBetterInt(n: Int) {
  defW: Width = ...
}

Im gegenwärtigen Gesichtsfeld erscheinen daher die üblichen Inta-Supermächte. Hier ist eine der Funktionen, die Scala für das Erstellen von DSL übersichtlicher und bequemer macht.


Dazu noch eine Prise Tests
import chisel3.iotesters._
import org.scalatest.{FlatSpec, Matchers}
objectPWMSpec{
  classPWMTesterConstant(pwm: PWM, denum: Int, const: Boolean)extendsPeekPokeTester(pwm) {
    poke(pwm.io.numerator, if (const) denum else0)
    poke(pwm.io.denominator, denum)
    for (i <- 1 to 2 * denum) {
      step(1)
      expect(pwm.io.pulse, const)
    }
  }
  classPWMTesterExact(pwm: PWM, num: Int, ratio: Int) extendsPeekPokeTester(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)
    }
  }
  classPWMTesterApproximate(pwm: PWM, num: Int, denom: Int) extendsPeekPokeTester(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")
  }
}
classPWMSpecextendsFlatSpecwithMatchers{
  importPWMSpec._
  behavior of "PWMSpec"deftestWith(testerConstructor: PWM => PeekPokeTester[PWM]): Unit = {
    chisel3.iotesters.Driver(() => newPWM(4))(testerConstructor) shouldBe true
  }
  it should "return True constant for 1/1" in {
    testWith(newPWMTesterConstant(_, 1, true))
  }
  it should "return True constant for 10/10" in {
    testWith(newPWMTesterConstant(_, 10, true))
  }
  it should "return False constant for 1/1" in {
    testWith(newPWMTesterConstant(_, 1, false))
  }
  it should "return False constant for 10/10" in {
    testWith(newPWMTesterConstant(_, 10, false))
  }
  it should "return True exactly once in 3 steps for 1/3" in {
    testWith(newPWMTesterExact(_, 1, 3))
  }
  it should "return good approximation for 3/10" in {
    testWith(newPWMTesterApproximate(_, 3, 10))
  }
}

PeekPokeTester- Dies ist einer der drei Standardtester in Meißel. Hier können Sie die Werte an den Eingängen des Prüflings (Prüfling) einstellen und die Werte an den Ausgängen überprüfen. Wie wir sehen, wird für Tests der übliche ScalaTest verwendet, und die Tests nehmen fünfmal mehr Platz ein als die Implementierung selbst, was im Prinzip für Software normal ist. Ich vermute jedoch, dass erfahrene Entwickler von Geräten, die "in Silizium gegossen" sind, nur mit einer solch mikroskopisch kleinen Anzahl von Tests lächeln werden. Laufen und oops ...


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 returnTrueconstantfor1/1
[info] - should returnTrueconstantfor10/10 *** FAILED ***
[info]   false was not equal totrue (PWMSpec.scala:56)
[info] - should returnFalseconstantfor1/1
[info] - should returnFalseconstantfor10/10
[info] - should returnTrue exactly once in3 steps for1/3
[info] - should return good approximation for3/10

Ja, lass uns das io.pulse := nextValue > io.denominatorZeichen auf der PWM-Linie reparieren >=, die Tests erneut starten - es funktioniert! Ich befürchte, dass erfahrene Entwickler digitaler Geräte mich wegen einer so frivolen Einstellung zum Design umbringen wollen (und einige Software-Entwickler werden sich ihnen gerne anschließen) ...


Impulsgenerator


Wir brauchen auch einen Generator, der Synchronisationsimpulse für die "Halbbilder" erzeugt. Warum "halb"? weil ungerade Saiten zuerst übertragen werden, dann gerade Saiten (naja oder umgekehrt, aber wir sind jetzt nicht fett).


import chisel3._
import chisel3.util._
classOneShotPulseGenerator(val lengths: Seq[Int], val initial: Boolean) extendsModule{
  // Add sentinel value here, so no output flip required after the last stateprivateval delayVecValues = lengths.map(_ - 1) :+ 0val io = IO(newBundle {
    val signal = Output(Bool())
  })
  privateval nextIndex = RegInit(1.asUInt( log2Ceil(delayVecValues.length + 1).W ))
  privateval countdown = RegInit(delayVecValues.head.asUInt( log2Ceil(lengths.max + 1).W ))
  privateval output    = RegInit(initial.asBool)
  privateval delaysVec = VecInit(delayVecValues.map(_.asUInt))
  privateval moveNext = countdown === 0.asUInt
  privateval finished = nextIndex === delayVecValues.length.asUInt
  when (!finished) {
    when (moveNext) {
      countdown := delaysVec(nextIndex)
      nextIndex := nextIndex + 1.asUInt
      output := !output
    }.otherwise {
      countdown := countdown - 1.asUInt
    }
  }
  io.signal := output
}

Wenn ein Signal entfernt wird, werden resetrechteckige Impulse mit Intervalllängen zwischen den durch den Parameter angegebenen Umschaltvorgängen ausgelöst lengths, wonach es für immer im letzten Zustand verbleibt. Dieses Beispiel zeigt die Verwendung von Tabellen mit den Werten VecInit, sowie ein Verfahren zur Bestimmung der erforderlichen Registerbreite Herstellung: chisel3.util.log2Ceil(maxVal + 1).W. Ich kann mich ehrlich gesagt nicht daran erinnern, wie es in Verilog gemacht wird, aber in Chisel genügt es, um ein derart parametrisiertes Modul mit einem Wertevektor zu erstellen, den Klassenkonstruktor mit dem erforderlichen Parameter aufzurufen.


Sie fragen sich wahrscheinlich: „Wenn die Eingaben clockauch resetimplizit generiert werden, wie können wir dann den Impulsgenerator für jeden Frame neu laden?“ Die Entwickler von Meißel haben alles vorausgesehen:


val module = Module( newMyModule() )
  val moduleWithCustomReset = withReset(customReset) {
    Module( newMyModule() )
  }
  val otherClockDomain = withClock(otherClock) {
    Module( newMyModule() )
  }

Naive Signalgenerator-Implementierung


Damit der Fernseher uns irgendwie verstehen kann, müssen Sie das "Protokoll" der durchschnittlichen Verschlagenheit unterstützen: Es gibt drei wichtige Signalpegel:


  • 1.0V - weiße Farbe
  • 0,3 V - schwarze Farbe
  • 0V - Spezialstufe

Warum habe ich 0V Special angerufen? Denn mit einem fließenden Übergang von 0,3 V auf 1,0 V gehen wir problemlos von Schwarz nach Weiß und zwischen 0 V und 0,3 V, soweit ich das verstehen kann, keine Zwischenstufen vor, und 0 V wird nur für die Synchronisation verwendet. (Im Bereich von 0 V - 1 V und -0,3 V - 0,7 V ändert sich das nicht, aber der Eingangskondensator befindet sich hoffentlich immer noch am Eingang.)


Wie dieser bemerkenswerte Artikel lehrt, besteht das zusammengesetzte PAL-Signal aus einem endlosen Strom von wiederholten 625 Zeilen: Die meisten davon sind Zeilen, in der Tat, Bilder (einzeln gerade und separat ungerade), einige werden für Synchronisationszwecke verwendet (für sie gemacht) Signale), einige sind nicht auf dem Bildschirm sichtbar. Sie sehen so aus (ich werde keine Piraterie und gebe Links zum Original):



Versuchen wir die Schnittstellen der Module zu beschreiben:


BWGenerator Wird die Zeitsteuerung usw. steuern, muss er wissen, in welcher Frequenz er arbeitet:


classBWGenerator(clocksPerUs: Int) extendsModule{
  val io = IO(newBundle {
    valL = 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 berechnet den Pegel des Luminanzsignals sowie ein zusätzliches Chrominanzsignal:


classPalColorCalculatorextendsModule{
  val io = IO(newBundle {
    val red   = Input(UInt(8.W))
    val green = Input(UInt(8.W))
    val blue  = Input(UInt(8.W))
    val scanLine = Input(Bool())
    valL = Output(UInt(8.W))
    val millivolts = Output(UInt(12.W))
  })
  // Заглушка -- пока Ч/Б
  io.L := (0.asUInt(10.W) + io.red + io.green + io.blue) / 4.asUInt
  io.millivolts := 0.asUInt
}

Im Modul verbinden wir PalGeneratoreinfach die zwei angegebenen Module erneut:


classPalGenerator(clocksPerUs: Int) extendsModule{
  val io = IO(newBundle {
    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(newBWGenerator(clocksPerUs))
  val color = Module(newPalColorCalculator)
  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
}

Und jetzt werden wir die erste Eule traurig beenden ...
package io.github.atrosinenko.fpga.tv
import chisel3._
import chisel3.core.withReset
import io.github.atrosinenko.fpga.common.OneShotPulseGeneratorobjectBWGenerator{
  valScanLineHSyncStartUs = 4.0valScanLineHSyncEndUs   = 12.0valTotalScanLineLengthUs = 64.0valVSyncStart = Seq(
    2, 30, 2, 30,  // 623 / 3112, 30, 2, 30// 624 / 312
  )
  valVSyncEnd = Seq(
    30, 2, 30, 2,  // 2 / 31430, 2, 30, 2,  // 3 / 3152, 30, 2, 30,  // 4 / 3162, 30, 2, 30// 5 / 317
  )
  valVSync1: Seq[Int] = VSyncStart ++ Seq(
    2, 30, 2, 30,  // 62530, 2, 30, 2// 1
  ) ++ VSyncEnd ++ (6 to 23).flatMap(_ => Seq(4, 60))
  valVSync2: Seq[Int] = VSyncStart ++ Seq(
    2, 30, 30, 2// 313
  ) ++ VSyncEnd ++ (318 to 335).flatMap(_ => Seq(4, 60))
  valBlackMv = 300.asUInt(12.W)
  valWhiteMv = 1000.asUInt(12.W)
  valFirstHalf = (24, 311)
  valSecondHalf = (336, 623)
  valTotalScanLineCount = 625
}
classBWGenerator(clocksPerUs: Int) extendsModule{
  importBWGenerator._
  val io = IO(newBundle {
    valL = 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))
  })
  privateval scanLineNr = RegInit(0.asUInt(10.W))
  privateval inScanLineCounter = RegInit(0.asUInt(16.W))
  when (inScanLineCounter === (TotalScanLineLengthUs * clocksPerUs - 1).toInt.asUInt) {
    inScanLineCounter := 0.asUInt
    when(scanLineNr === (TotalScanLineCount - 1).asUInt) {
      scanLineNr := 0.asUInt
    } otherwise {
      scanLineNr := scanLineNr + 1.asUInt
    }
  } otherwise {
    inScanLineCounter := inScanLineCounter + 1.asUInt
  }
  privateval fieldIActive = SecondHalf._2.asUInt <= scanLineNr ||
                             scanLineNr < FirstHalf._1.asUInt
  privateval fieldIGenerator = withReset(!fieldIActive) {
    Module(newOneShotPulseGenerator(VSync1.map(_ * clocksPerUs), initial = false))
  }
  privateval fieldIIActive = FirstHalf._2.asUInt <= scanLineNr &&
                              scanLineNr < SecondHalf._1.asUInt
  privateval fieldIIGenerator = withReset(!fieldIIActive) {
    Module(newOneShotPulseGenerator(VSync2.map(_ * clocksPerUs), initial = false))
  }
  privateval inFirstHalf  = FirstHalf ._1.asUInt <= scanLineNr &&
                             scanLineNr < FirstHalf ._2.asUInt
  privateval inSecondHalf = SecondHalf._1.asUInt <= scanLineNr &&
                             scanLineNr < SecondHalf._2.asUInt
  io.inScanLine :=
    (inFirstHalf || inSecondHalf) &&
      ((ScanLineHSyncEndUs * clocksPerUs).toInt.asUInt <= inScanLineCounter)
  io.x := Mux(
    io.inScanLine,
    inScanLineCounter - (ScanLineHSyncEndUs * clocksPerUs).toInt.asUInt,
    0.asUInt
  ) / 4.asUInt
  io.y := Mux(
    io.inScanLine,
    Mux(
      inFirstHalf,
      ((scanLineNr - FirstHalf ._1.asUInt) << 1).asUInt,
      ((scanLineNr - SecondHalf._1.asUInt) << 1).asUInt + 1.asUInt
    ),
    0.asUInt
  )
  when (fieldIActive) {
    io.millivolts := Mux(fieldIGenerator .io.signal, BlackMv, 0.asUInt)
  }.elsewhen (fieldIIActive) {
    io.millivolts := Mux(fieldIIGenerator.io.signal, BlackMv, 0.asUInt)
  }.otherwise {
    when (inScanLineCounter < (ScanLineHSyncStartUs * clocksPerUs).toInt.asUInt) {
      io.millivolts := 0.asUInt
    }.elsewhen (inScanLineCounter < (ScanLineHSyncEndUs * clocksPerUs).toInt.asUInt) {
      io.millivolts := BlackMv
    }.otherwise {
      io.millivolts := (BlackMv + (io.L << 1).asUInt).asUInt
    }
  }
}

Erzeugen Sie synthetisierten Code


Es ist alles gut, aber wir möchten das resultierende Design in die Hand nehmen. Dafür müssen Sie Verilog synthetisieren. Dies geschieht auf sehr einfache Weise:


import chisel3._
import io.github.atrosinenko.fpga.common.PWMobjectCodegen{
  classTestModule(mhz: Int) extendsModule{
    val io = IO(newBundle {
      val millivolts = Output(UInt(12.W))
    })
    val imageGenerator = Module(newTestColorImageGenerator(540, 400))
    val encoder = Module(newPalGenerator(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
    overridedefdesiredName: String = "CompositeSignalGenerator"
  }
  defmain(args: Array[String]): Unit = {
    Driver.execute(args, () => newPWM(12))
    Driver.execute(args, () => newTestModule(mhz = 32))
  }
}

In der zweizeiligen Methode machen main()wir es zweimal, der Rest des Codes ist ein weiteres Modul, das nebeneinander bleibt.


Absolut stumpfer Testbildgenerator
classTestColorImageGenerator(width: Int, height: Int) extendsModule{
  val io = IO(newBundle {
    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)
}

Nun müssen Sie es in das Projekt Quartus einfügen. Für Mars Rover 2 benötigen wir eine kostenlose Version von Quartus 13.1. Die Installation ist auf der Mars Rovers-Website beschrieben. Von dort habe ich das „Erste Projekt“ für das Mars Rover 2 Board heruntergeladen, in das Repository gestellt und ein wenig korrigiert. Da ich kein Elektroniker bin (und FPGA interessiert mich eigentlich eher als Beschleuniger als als Schnittstellenkarte), dann


wie in diesem Witz ...

Сидит программист глубоко в отладке.
Подходит сынишка:
— Папа, почему солнышко каждый день встает на востоке, а садится на западе?
— Ты это проверял?
— Проверял.
— Хорошо проверял?
— Хорошо.
— Работает?
— Работает.
— Каждый день работает?
— Да, каждый день.
— Тогда ради бога, сынок, ничего не трогай, ничего не меняй.


... Ich habe gerade den VGA-Signalgenerator gelöscht und mein eigenes Modul hinzugefügt.


Umstellung auf Quatus


Danach habe ich den analogen TV-Tuner an einen anderen Computer (Laptop) angeschlossen, so dass zwischen der Stromversorgung des Generators und dem Verbraucher der Signale zumindest eine gewisse galvanische Trennung bestand, und es wurde nur ein Signal von den IO7 (+) - und GND-Pins (-) des Motherboards auf den Composite-Eingang (minus) gegeben am äußeren Kontakt, plus - in der Mitte). Nun, wie "nur" ... Es wäre nur, wenn die Hände von wo her wachsen würden, oder wenn ich weibliche Verbindungsdrähte hätte. Aber ich habe nur ein paar männliche und männliche Kabel. Aber ich habe eine aufrechte Haltung und Zangen! In der Regel, eine Verstopfung mit einem Draht, machte ich mir immer noch fast zwei Arbeiter - mit Schwierigkeiten, aber klammerte sich an die Tafel. Und hier habe ich gesehen:


Erstes Schwarzweißbild


Ich habe Sie natürlich ein bisschen getäuscht. Der obige Code zeigte sich nach etwa drei Stunden Debugging "auf Hardware", aber verdammt, ich habe es geschrieben und es funktioniert !!! Und in Anbetracht der Tatsache, dass ich mit ernsthafter Elektronik kaum vertraut war, glaube ich, dass die Aufgabe kein Horror war, was für ein schwieriges Unterfangen.


Farbvideo-Erzeugung


Nun, das Geschäft bleibt zurück, um den Generator für das Farbvideosignal hinzuzufügen. Ich nahm das Tutorial und begann zu versuchen, einen Farbstoß zu bilden (addiert sich zum Schwarzwert einer Sinuskurve bei der Trägerfrequenz des Farbsignals, wird während HSync für kurze Zeit ausgegeben) und tatsächlich das Farbsignal durch die Formel. Aber es kommt nicht heraus, auch wenn Sie platzen ... Irgendwann dämmerte es mir, dass der Fernseher trotz der Tatsache, dass die Frequenz nicht mit einem schnellen Blick auf das Dokument stürzte, kaum auf ein beliebiges eingestellt werden konnte. Nach der Suche stellte ich fest, dass der PAL eine Trägerfrequenz von 4,43 MHz verwendet. "Fall mit Hut" - dachte ich. "Fick dich", antwortete der Tuner. Nach einem ganzen Tag des Debugging und nur einmal auf dem Bild (und wenn ich dem Tuner sagte, dass es NTSC im Allgemeinen ist)


... Mir wurde klar, wie die Hoffnungslosigkeit wirklich aussieht

Dann wurde mir klar, dass ich auf ein Oszilloskop nicht verzichten könnte. Und, wie ich schon sagte, die Elektronik ist mir nicht vertraut, und ich habe natürlich kein Wunder der Technik. Kaufen? Teuer für ein Experiment ... Und wovon kann es am Knie gebaut werden? Verbinden Sie das Signal mit der Line-In-Soundkarte. Ja, viereinhalb Megahertz - fangen Sie kaum an (zumindest ohne Nacharbeit). Der Mars Rover verfügt zwar über einen 20-MHz-ADC, aber die Übertragung eines rohen Streams serieller Schnittstellengeschwindigkeit auf den Computer reicht nicht aus. Nun, irgendwo muss das Signal noch verarbeitet werden, um auf dem Bildschirm angezeigt zu werden. Tatsächlich werden dort ziemlich viele Informationen angezeigt, aber es ist immer noch ein Problem mit der seriellen Schnittstelle, das Schreiben von Computerprogrammen ... Und dann dachte ich, dass der Ingenieur sich entwickeln sollte in einer gesunden Uprotik:


Lyrischer Exkurs (wie sie sagen: „Die Meinung des Schülers muss nicht mit der Meinung des Lehrers, dem gesunden Menschenverstand und Peanos Axiomatik übereinstimmen“): Als ich die Farbgenerierung mit allen möglichen Multiplikationen und anderen komplizierten Dingen hinzufügte, ließ Fmax für den Signalaufbereiter stark nach. Was ist Fmax? Wie ich es aus dem Harris & Harris-Lehrbuch verstehe, zieht es CAD für FPGA vor, wenn Verilog nicht irgendwie als Standard geschrieben wird, sondern "nach Konzepten": Zum Beispiel sollte ein synchrones Schema erhalten werden - eine Art gerichtetes azyklisches Netz von Kombinationslogik(Addition, Multiplikation, Division, logische Operationen, ...), hängt mit seinen Ein- und Ausgängen an den Ausgängen bzw. Eingängen der Trigger. Triggerflanke Taktsignal speichert den gesamten nächsten Zyklus den Wert seines Eingangs, welche Ebene sollte als eine Zeit nach vorne stabil sein und wie etwas - nach (dies zwei Mal se Konstanten). Die Signale von den Ausgängen der Flip-Flops wiederum starten nach dem Taktsignal zu den Ausgängen der Kombinationslogik (und damit zu den Eingängen anderer Auslöser. Nun und den Ausgängen des Chips), die ebenfalls durch zwei Intervalle gekennzeichnet sind: Die Zeit, in der noch kein Ausgang vorhanden ist Zeit, um mit dem Ändern zu beginnen, und die Zeit, nach der sich die Änderungen beruhigen (sofern sich die Eingabe einmal geändert hat). Hier ist die maximale Häufigkeit, mit der die Kombinationslogik die Erfüllung der Anforderungen der Trigger sicherstellt - und das ist Fmax. Wenn die Schaltung zwischen zwei Takten mehr zählen kann, nimmt Fmax ab. Natürlich möchte ich, dass es mehr ist, aber wenn es plötzlich zehnmal gesprungen ist (und sogar die Anzahl der Frequenzbereiche im CAD-Bericht gesunken ist) - überprüfen Sie, vielleicht haben Sie irgendwo einen Fehler gemacht.


Oszilloskop-Werbung


Nein, nicht der, nach dem eine Drehung des Oszilloskops und eine Handvoll unnötiger Teile erfolgt, und das Bootstrapping des Oszilloskops gleicht einem Compiler-Bootstrapping nur für ein Oszilloskop.


Wir machen ein Oszilloskop, nehmen die Anzahl der Abtastwerte des Eingangssignals auf und zeigen dann nur das aufgezeichnete an. Da er irgendwie einen Befehl zum Schreiben geben muss, um ihn dann zu navigieren, werden wir einige Tasten-Controller benötigen. Ich schrieb, es sei nicht sehr bequem, aber recht primitiv. Hier ist es:


classSimpleButtonController(
      clickThreshold: Int,
      pressThreshold: Int,
      period: Int,
      pressedIsHigh: Boolean) extendsModule{
  val io = IO(newBundle {
    val buttonInput = Input(Bool())
    val click     = Output(Bool())
    val longPress = Output(Bool())
  })

SCHOCK! SENSATION! Damit es funktioniert, brauchen Sie nur ...
privateval cycleCounter   = RegInit(0.asUInt(32.W))
  privateval 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
    }
  }
}

So sieht das Oszilloskop aus:


classOscilloscope(
      clocksPerUs: Int,
      inputWidth: Int,
      windowPixelWidth: Int,
      windowPixelHeight: Int) extendsModule{
  val io = IO(newBundle {
    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())
  })
  privateval mem = SyncReadMem(1 << 15, UInt(inputWidth.W))
  privateval 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
  }
  privateval shiftedX = io.x + io.visualOffset
  privateval 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) {
    // Нарисуем 1мс чёрно-белую шкалу
    io.output := !(
      io.y > (windowPixelHeight + 10).asUInt && io.y < (windowPixelHeight + 20).asUInt &&
        (io.x / clocksPerUs.asUInt)(0)
      )
  } otherwise {
    // Нарисуем, собственно, сигнал// signal / 2^inputWidth ~ y / windowPixelHeight// signal * windowPixelHeight ~ y * 2^inputWidth
    io.output :=
      (currentValue * windowPixelHeight.asUInt >= ((io.y - 5.asUInt) << inputWidth).asUInt) &&
      (currentValue * windowPixelHeight.asUInt <= ((io.y + 5.asUInt) << inputWidth).asUInt)
  }
}

Und so - der Controller, der Tastenanschläge verarbeitet:


classOscilloscopeController(
      visibleWidth: Int,
      createButtonController: () =>SimpleButtonController
    ) extendsModule {
  val io = IO(newBundle {
    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
  privateval offset = RegInit(0.asUInt(16.W))
  privateval 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
}

Im Oszilloskop-Code können Sie das Beispiel für die Arbeit mit der Registerdatei betrachten (vielleicht nicht ganz korrekt), aber es gibt etwas Interessantes in dem Controller: In seinem Konstruktor ist das zweite Argument einfach und natürlich, wir übertragen - nein, nicht der Button-Controller -, sondern das Lambda. Erstellen in der erforderlichen Anzahl von Klassen (in diesem Fall - zwei Stück). Es wäre notwendig - wir würden dieses Lambda und Argumente geben! Ich frage mich, ob Verilog das kann?


So sieht der Graph des ursprünglich digitalen Signals aus, ohne den FPGA zu verlassen:


Vom Signalaufbereiter --- sofort zur Grafik


Und so - (nicht nur von PWM nach IO7, sondern von VGA_GREEN mit dem R-2R DAC) und mit dem ADC-Chip des Mars Rover digitalisiert:


Analog, dann in "Abbildung" und dann in der Tabelle "---"


Im Allgemeinen, wie lange, kurz - und so habe ich es versucht, und das, aber die Farbe erschien nicht. Bei Wikipedia gibt es sogar eine Comic-Dekodierung der Abkürzung PAL - "Picture At Last (endlich ein Bild!)".


Der Code auf Github .


Schlussfolgerungen


Scala + Chisel bilden eine moderne Sprache zur Beschreibung digitaler Geräte - wenn Ausdruckskraft gefragt ist, dann mit Unterstützung des Funktionalismus und aller höherwertigen Typen. Und mit Hilfe des üblichen Scala-Plug-Ins Idea, das nichts über Chisel weiß, ist es auch sehr angenehm, darauf zu programmieren. Und das alles kostenlos und ohne Bezug zum CAD des Herstellers bestimmter FPGA-Chips. Im Allgemeinen - Schönheit!


Der Leser kann fragen: "Und wo ist das Happy End?" - KEIN IT! Aber es gibt ein Oszilloskop ...


Jetzt auch beliebt: