Notes of the amateur, or the Tale of how the FPGA Scala-developer has configured

For a long time I dreamed of learning how to work with FPGA, I watched. Then I bought a debug board, wrote a couple of hello worlds, and put the board in the box, because it was not clear what to do with it. Then the idea came: let's write a composite video signal generator for an ancient CRT TV. The idea, of course, is funny, but I really don’t know Verilog, and I’ll still have to remember it, and I don’t need this generator so much ... And recently I wanted to look towards RISC-V software processors. We need to start somewhere, and the Rocket Chip code (this is one of the implementations) is written in Chisel - this is the DSL for Scala. Then I suddenly remembered that I had been working professionally on Scala for two years and understood: the time has come ...


So, if you want to read the story of the life of the pliers, the digital multimeter and the oscilloscope, which is aware of itself, then welcome to the cat.


So what will be in this article? In it, I will describe my attempts to generate a composite PAL video signal (why PAL? I just got a good tutorial just for generating PAL) on the Mars Rover 2 board by nckma . About RISC-V in this article I will not say anything at all. :)


First, a little about Scala and Chisel: Scala is a language that runs on top of the Java Virtual Machine and uses existing Java libraries transparently (although Scala.js and Scala Native are also available). When I first began to study it, I had the feeling that it was such a very viable hybrid of “pluses” and Haskell (however, my colleagues do not share this opinion) - it’s a painfully advanced type system and laconic language, but because of the need to cross functionality with OOP The abundance of language constructs in some places brought back memories of C ++. However, do not be afraid of Scala - it is a very concise and safe language with a powerful type system, which at first you can simply write as in improved Java. And as far as I know, Scala was originally developed as a language for conveniently creating Domain Specific Languages - this is when you describe, say, digital equipment or notes in a formal language, and this language looks quite logical from the point of view of its subject area. And then you suddenly find out that it was the correct code on Scala (well, or Haskell) - just kind people wrote a library with a convenient interface. Chisel is just such a Scala library that allows you to describe digital logic on a convenient DSL, and then run the resulting Scala code and generate the code on Verilog (or something else), which can be copied into the Quartus project. Well, or immediately run the standard scala-style unit-tests, which themselves simulate testbenches and issue a report on the results.


For acquaintance with digital circuitry I highly recommend this book (it already exists in the printed Russian version). In fact, my systematic acquaintance with the FPGA world almost ends on this book, so constructive criticism is welcome in the comments (however, I repeat, the book is wonderful: it tells from the basics to creating a simple, contourized processor. And there are pictures there;)). Well, according to Chisel there is a good official tutorial .


Disclaimer: the author is not responsible for the equipment that was lost, and if you decide to repeat the experiment, it is better to check the signal levels with an oscilloscope, redo the analog part, etc. And in general - follow the safety precautions. (I, for example, in the process of writing the article realized that my legs are also limbs, and I don’t have to shove them into the central heating battery, holding my hand over the output board ...) By the way, this infection also interfered with the TV in the next room in the course of debugging ...


Project Setup


We will write the code in IntelliJ Idea Community Edition , as the build system will be sbt , so we will create a directory, put there .gitignore , project/build.properties , project/plugins.sbt from here and


somewhat simplified build.sbt
 def scalacOptionsVersion(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/10047 CrossVersion.partialVersion(scalaVersion) match { case Some((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) 

Now open it in the Idea and ask to import the sbt-project - while sbt will download the necessary dependencies.


First modules


PWM


To begin, let's try to write a simple PWM . The logic I had was approximately the following: in order to generate a signal of the fill factor n / m, initially we put in register 0 and we will add to it n each step. When the register value exceeds m - subtract m and give a high level for one clock cycle. Actually, it will fail if n> m, but we will consider this to be an undefined behavior that is needed to optimize the actually used cases.


I will not retell the whole beginner's guide - it is read in half an hour, I will only say that in order to describe the module, we need to import chisel3._ and inherit from the abstract class Module . It is abstract because we need to describe a Bundle called io - it will contain the entire interface of the module. In this case, the clock and reset inputs will appear implicitly in us - we don’t need to describe them separately. Here is what happened:


 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) } 

Notice, we call the .W method .W the normal int to get the port width, and we call the .asUInt(width.W) method on the integer literal! How is this possible? - well, in Smalltalk we would simply define the Integer class (or whatever it is called), but in the JVM, we still don’t have an entire object - there are also primitive types, and Scala understands it (and, moreover, there are third-party classes that we can not change). Therefore, there are various implicit s: in this case Scala probably finds something like


 implicit class BetterInt(n: Int) { def W: Width = ... } 

in the current field of view, therefore, the normal inta appear superpowers. Here is one of the features that makes Scala more concise and convenient for creating DSL.


Add to this a pinch of tests
 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 is one of three standard testers in Chisel. It allows you to set values ​​at the DUT (device under test) inputs and check the values ​​at the outputs. As we can see, for tests, the usual ScalaTest is used and tests take places 5 times more than the implementation itself, which, in principle, is normal for software. However, I suspect that experienced developers of the equipment "cast in silicon" will only smile with such a microscopic number of tests. Run and 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 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 

Yeah, io.pulse := nextValue > io.denominator in PWM in line io.pulse := nextValue > io.denominator sign on >= , restart tests - everything works! I am afraid that experienced digital equipment developers will want to kill me for such a frivolous attitude to design (and some software developers will happily join them) ...


Pulse generator


We also need a generator that will generate synchronization pulses for the "half-frames". Why "semi-"? because odd strings are transmitted first, then even strings (well, or vice versa, but we are not into fat now).


 import chisel3._ import chisel3.util._ class OneShotPulseGenerator(val lengths: Seq[Int], val initial: Boolean) extends Module { // Add sentinel value here, so no output flip required after the last state private val delayVecValues = lengths.map(_ - 1) :+ 0 val io = IO(new Bundle { val signal = Output(Bool()) }) private val nextIndex = RegInit(1.asUInt( log2Ceil(delayVecValues.length + 1).W )) private val countdown = RegInit(delayVecValues.head.asUInt( log2Ceil(lengths.max + 1).W )) private val output = RegInit(initial.asBool) private val delaysVec = VecInit(delayVecValues.map(_.asUInt)) private val moveNext = countdown === 0.asUInt private val 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 } 

When a reset signal is removed, it fires rectangular pulses with lengths of gaps between the switchings specified by the lengths parameter, after which it remains forever in the last state. This example demonstrates the use of value tables using VecInit , as well as the method of obtaining the necessary register width: chisel3.util.log2Ceil(maxVal + 1).W . I don’t remember, frankly, how it is done in Verilog, but in Chisel, to create such a parameterized module with a vector of values, it is enough to call the class constructor with the necessary parameter.


You probably ask: “If the clock and reset inputs are generated implicitly, then how are we going to“ recharge ”the pulse generator for each frame?” The developers of Chisel provided for everything:


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

Naive signal generator implementation


In order for the TV to somehow understand us, you need to support the "protocol" of the average level of cunning: there are three important signal levels:



Why did I call 0V special? Because with a smooth transition from 0.3V to 1.0V, we smoothly go from black to white, and between 0V and 0.3V, as far as I can understand, there are no intermediate levels and 0V is used only for synchronization. (In fact, it does not even change in the range of 0V - 1V, and -0.3V - 0.7V, but hopefully the capacitor is still at the input)


As this remarkable article teaches us, the composite PAL signal consists of an endless stream of repeated 625 lines: most of them are lines, in fact, pictures (separately even and separately odd), some are used for synchronization purposes (for them we made signals), some are not visible on the screen. They look like this (I will not piracy and give links to the original):



Let's try to describe the interfaces of the modules:


BWGenerator will manage the timings, etc., he needs to know at what frequency it works:


 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 will calculate the luminance signal level, as well as an additional chrominance signal:


 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)) }) //  --  / io.L := (0.asUInt(10.W) + io.red + io.green + io.blue) / 4.asUInt io.millivolts := 0.asUInt } 

In the PalGenerator module PalGenerator simply re- PalGenerator two specified modules:


 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 } 

And now we will sadly finish the first owl ...
 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, // 623 / 311 2, 30, 2, 30 // 624 / 312 ) val VSyncEnd = Seq( 30, 2, 30, 2, // 2 / 314 30, 2, 30, 2, // 3 / 315 2, 30, 2, 30, // 4 / 316 2, 30, 2, 30 // 5 / 317 ) val VSync1: Seq[Int] = VSyncStart ++ Seq( 2, 30, 2, 30, // 625 30, 2, 30, 2 // 1 ) ++ VSyncEnd ++ (6 to 23).flatMap(_ => Seq(4, 60)) val VSync2: Seq[Int] = VSyncStart ++ Seq( 2, 30, 30, 2 // 313 ) ++ VSyncEnd ++ (318 to 335).flatMap(_ => Seq(4, 60)) val BlackMv = 300.asUInt(12.W) val WhiteMv = 1000.asUInt(12.W) val FirstHalf = (24, 311) val SecondHalf = (336, 623) val TotalScanLineCount = 625 } class BWGenerator(clocksPerUs: Int) extends Module { import BWGenerator._ 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)) }) private val scanLineNr = RegInit(0.asUInt(10.W)) private val 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 } private val fieldIActive = SecondHalf._2.asUInt <= scanLineNr || scanLineNr < FirstHalf._1.asUInt private val fieldIGenerator = withReset(!fieldIActive) { Module(new OneShotPulseGenerator(VSync1.map(_ * clocksPerUs), initial = false)) } private val fieldIIActive = FirstHalf._2.asUInt <= scanLineNr && scanLineNr < SecondHalf._1.asUInt private val fieldIIGenerator = withReset(!fieldIIActive) { Module(new OneShotPulseGenerator(VSync2.map(_ * clocksPerUs), initial = false)) } private val inFirstHalf = FirstHalf ._1.asUInt <= scanLineNr && scanLineNr < FirstHalf ._2.asUInt private val 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 } } } 

Generate synthesized code


It's all good, but we want to sew the resulting design in charge. For this you need to synthesize Verilog. This is done in a very simple way:


 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)) } } 

Actually, in the two-line main() method, we do it twice, the rest of the code is another module that sticks alongside.


Absolutely dull test image generator
 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) } 

Now you need to stuff it into the Quartus project. For Rover 2, we need a free version of Quartus 13.1. How to install it is written on the Mars Rovers website. From there, I downloaded the “First Project” for the Mars Rover 2 board, put it in the repository and corrected it a little. Since I am not an electronics engineer (and FPGA actually interests me more as accelerators than as interface boards), then


as in that joke ...

The programmer sits deep in debugging.
Suitable son:
- Dad, why does the sun rise every day in the east, and set in the west?
- Did you check it?
- Checked.
“Did you test well?”
- Good.
- Works?
- Works.
- Does every day work?
- Yes, every day.
- Then for God's sake, son, do not touch anything, do not change anything.


... I just deleted the VGA signal generator and added my own module.


Switching to Quatus


After that, I connected the analog TV tuner to another computer (laptop), so that there was at least some electrical isolation between the power supply of the generator and the consumer of signals and just gave a signal from the IO7 (+) and GND pins (-) of the motherboard to the composite input (minus on the outer contact, plus - in the center). Well, that is, how "just" ... It would just be if the hands grew from where I needed, well, or if I had female-male connecting wires. But I have only a bunch of male-male wires. But I have an uprightness and nippers! In general, one wire constipation, I still made myself almost two workers - with difficulty, but clinging to the board. And here is what I saw:


First B / W Image


In fact, I, of course, deceived you a little. The code above showed up after about three hours of debugging "on hardware", but, damn, I wrote it, and it works !!! And, considering that I used to be barely familiar with serious electronics, I think that the task turned out to be no horror, what a difficult one.


Color video generation


Well, well, it remains for the small - to add a color video signal generator. I took the tutorial and began to try to form a color burst (added to the black level of a sinusoid at the carrier frequency of the color signal, output for a short time during HSync) and, in fact, the color signal by the formula. But it does not come out, even if you are bursting ... At some point it dawned on me that, despite the fact that the frequency didn’t throw a glance at the document, the TV could hardly be adjusted to an arbitrary one. After searching, I found that the PAL uses a carrier frequency of 4.43 MHz. "Case in a hat" - I thought. “Fuck you,” answered the tuner. After a whole day of debugging and only once seeing glimpses of color in the picture (and when I told the tuner that it was NTSC in general)


... I understood how hopelessness actually looks

Then I realized that I could not do without an oscilloscope. And, as I have already said, I am not familiar with electronics, and I don’t have such a miracle of technology at home. Buy? Expensive for one experiment ... And from what it can be built on the knee? Connect the signal to the line-in sound card? Yeah, 4 and a half megahertz - hardly get started (at least without rework). Hmm, and in fact the Mars Rover has a 20 MHz ADC, but transferring a raw stream of serial interface speed to the computer is not enough. Well, somewhere you still have to process the signal for display on the screen, and in fact there will be quite an acceptable amount of information there, but it’s still messing with the serial port, writing computer programs ... And then I thought that the engineer should develop There is a healthy speed in itself: there is a broken color image shaper, there is an ADC ... But the black-and-white image is displayed stably ... Well, let the signal generator itself and debug!


Lyrical digression (as they say, “The student’s opinion does not have to coincide with the teacher’s opinion, common sense and Peano’s axiomatics”): When I added color generation with all sorts of multiplications and other complex things, Fmax for the signal conditioner strongly subsided. What is Fmax? As far as I understood it from the Harris & Harris textbook, CAD for FPGA prefers when Verilog is not written anyhow as within the standard, but “by concepts”: for example, a synchronous scheme should be obtained - a kind of directed acyclic web of combinational logic (addition, multiplication , division, logical operations, ...), stuck their inputs and outputs to the outputs and inputs of the triggers, respectively. The trigger on the clock front remembers for the entire next clock the value of its input, the level of which must be stable some time before the front and some time after (these are two time constants). The signals from the outputs of the triggers, in turn, after the clock signal begin their race to the outputs of combinational logic (and therefore, the inputs of other triggers. Well, and the outputs of the chip), which is also characterized by two intervals: the time during which no output has yet will have time to start changing, and the time after which the changes will calm down (provided that the input has changed once). Here is the maximum frequency at which the combinational logic ensures that the requirements of the triggers are met - and that is Fmax. When the circuit between two clocks should be able to calculate more, Fmax decreases. Of course, I want more frequency, but if it suddenly jumped 10 times (and even the number of frequency domains in the CAD report decreased) - check, maybe you got it wrong somewhere, and as a result, CAD found a constant expression and happily used it for optimization.


Oscilloscope promotion


No, not the one after which there is a twist of the oscilloscope and a handful of unnecessary parts, and the oscilloscope bootstrapping is like a compiler bootstrapping, only for an oscilloscope.


We will do an oscilloscope, recording the number of samples of the input signal, after which only the recorded one is recorded. Since he will need to somehow give a command to write, and then to navigate through it, we will need some button controllers - I wrote it is not very convenient, but quite primitive, here it is:


 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()) }) 

SHOCK! SENSATION! To make it work, you need only ...
  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 } } } 

This is what the oscilloscope will look like:


 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) { //  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) } } 

— , :


 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 ) :


,   &quot;&quot;,   ---


, — , , . PAL — "Picture At Last (-, !)"


GitHub .


findings


Scala + Chisel — , Higher-kinded types. Scala- , Chisel , . . — !


: " -?" — ! ...

Source: https://habr.com/ru/post/414995/


All Articles