Notas de aficionados, o The Tale of How the Scala FPGA Developer Configured

Durante mucho tiempo soñé con aprender a trabajar con FPGA, miré de cerca. Luego compró un tablero de depuración, escribió un par de hola mundos y colocó el tablero en una caja, porque no estaba claro qué hacer con él. Entonces surgió la idea: escribamos un generador de video compuesto para un antiguo televisor CRT. La idea, por supuesto, es divertida, pero realmente no conozco a Verilog, y todavía tengo que recordarlo, y realmente no necesito este generador ... Y recientemente quería mirar hacia los procesadores de software RISC-V . Debe comenzar en alguna parte, y el código de Rocket Chip (esta es una de las implementaciones) está escrito en Chisel , este es un DSL para Scala. Entonces, de repente recordé que durante dos años había estado desarrollando profesionalmente en Scala y me di cuenta: había llegado el momento ...


Entonces, si desea leer una historia de la vida de los cortadores de alambre, un multímetro digital y un osciloscopio que se haya realizado, entonces bienvenido a cat.


Entonces, ¿qué habrá en este artículo? En él, describiré mis intentos de generar una señal de video PAL compuesta (¿por qué PAL? - Acabo de encontrar un buen tutorial específicamente sobre la generación PAL) en la placa Mars rover 2 de nckma . Sobre RISC-V en este artículo no diré nada en absoluto. :)


Primero, un poco sobre Scala y Chisel: Scala es un lenguaje que se ejecuta en la parte superior de la máquina virtual Java y utiliza de manera transparente las bibliotecas Java existentes (aunque también hay Scala.js y Scala Native). Cuando comencé a estudiarlo, tuve la sensación de que era un híbrido muy viable de "ventajas" y Haskell (sin embargo, los colegas no comparten esta opinión) : es un sistema de tipos dolorosamente avanzado y un lenguaje conciso, pero debido a la necesidad de cruzar el funcionalismo con OOP una abundancia de construcciones de lenguaje en algunos lugares evocó recuerdos de C ++. Sin embargo, no tenga miedo de Scala: es un lenguaje muy conciso y seguro con un sistema de tipo potente, en el que al principio puede simplemente escribir como Java mejorado. Y también, hasta donde yo sé, Scala se desarrolló originalmente como un lenguaje para la creación conveniente de lenguajes específicos de dominio : esto es cuando usted describe, digamos, equipos digitales o notas en un lenguaje formal, y este lenguaje parece bastante lógico desde el punto de vista de su área temática. Y de repente descubres que era el código correcto en Scala (bueno, o Haskell), solo personas amables escribieron una biblioteca con una interfaz conveniente. Chisel es una biblioteca para Scala, que nos permite describir la lógica digital en un DSL conveniente, y luego ejecutar el código Scala resultante y generar código en Verilog (o alguna otra cosa) que se pueda copiar al proyecto Quartus. Bueno, o ejecute inmediatamente las pruebas unitarias estándar de estilo scala, que simularán bancos de pruebas y emitirán un informe sobre los resultados.


Para familiarizarse con los circuitos digitales, recomiendo este libro (ya está en la versión impresa en ruso). De hecho, mi conocimiento sistemático del mundo de FPGA casi termina en este libro, por lo que las críticas constructivas en los comentarios son bienvenidas (sin embargo, repito, el libro es maravilloso: cuenta desde lo básico hasta la creación de un procesador transportador simple. Y hay imágenes allí;)). Bueno, según Chisel, hay un buen tutorial oficial .


Descargo de responsabilidad: el autor no es responsable del equipo quemado, y si decide repetir el experimento, es mejor verificar los niveles de señal con un osciloscopio, rehacer la parte analógica, etc. Y en general, observe las precauciones de seguridad. (Por ejemplo, en el proceso de escribir el artículo, me di cuenta de que las patas también son extremidades, y no hay nada que las pegue en la batería de calefacción central, aferrándose a la salida del tablero ...) Por cierto, esta infección también causó interferencia en el televisor de la habitación contigua. en el curso de la depuración ...


Configuración del proyecto


Escribiremos el código en IntelliJ Idea Community Edition , sbt será el sistema de compilación, así que cree un directorio, coloque .gitignore , project/build.properties , project/plugins.sbt desde aquí y


build.sbt algo simplificado
 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) 

Ahora ábralo en la Idea y solicite importar el proyecto sbt, mientras que sbt descargará las dependencias necesarias.


Primeros módulos


PWM


Primero, intentemos escribir un PWM simple. Mi lógica era aproximadamente la siguiente: para generar una señal de ciclo de trabajo n / m, inicialmente colocamos 0 en el registro y agregaremos n en cada paso. Cuando el valor del registro excede m, reste my dé un nivel alto para un ciclo de reloj. En realidad, será defectuoso si n> m, pero consideraremos que es un comportamiento indefinido, que es necesario para optimizar los casos reales utilizados.


No volveré a contar toda la guía para principiantes: se lee en media hora, solo diré que para describir el módulo, necesitamos importar el chisel3._ y heredarlo de la clase abstracta Module . Es abstracto porque necesitamos describir el Bundle bajo el nombre io ; tendrá toda la interfaz del módulo. Al mismo tiempo, las entradas de clock y reset aparecerán implícitamente; no es necesario que las describa por separado. Esto es lo que sucedió:


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

¡Observe que llamamos al método .W un int regular para obtener el ancho del puerto, y al .asUInt(width.W) generalmente llamamos a un literal entero! ¿Cómo es esto posible? - bueno, en Smalltalk simplemente definiríamos un nuevo método para la clase Integer (o como se llame allí), pero en la JVM todavía no tenemos todo el objeto - también hay tipos primitivos, y Scala entiende esto (y, además, hay clases de terceros que no podemos modificar). Por lo tanto, hay una variedad de s implícitos: en este caso, Scala probablemente encuentre algo como


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

en el ámbito actual, por lo que las entradas ordinarias tienen superpoderes Esta es una de las características que hace que Scala sea más conciso y fácil de crear DSL.


Agregue una pizca de pruebas a esto.
 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 es uno de los tres probadores estándar en Chisel. Le permite establecer los valores en las entradas del DUT (dispositivo bajo prueba) y verificar los valores en las salidas. Como podemos ver, el ScalaTest habitual se usa para las pruebas y las pruebas ocupan espacio 5 veces la implementación en sí, lo que, en principio, es normal para el software. Sin embargo, sospecho que los desarrolladores experimentados de equipos "fundidos en silicio" solo sonreirán con un número tan microscópico de pruebas. Lanzamiento y Ups ...


 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 

Sí, corríjalo en PWM en la línea io.pulse := nextValue > io.denominator regístrate >= , reinicia las pruebas, ¡todo funciona! Me temo que los desarrolladores experimentados de equipos digitales querrán matarme por una actitud tan frívola al diseño (y algunos desarrolladores de software con gusto se unirán a ellos) ...


Generador de pulso


También necesitaremos un generador que genere pulsos de sincronización para "medias tramas". ¿Por qué semi? porque primero se transmiten las líneas impares, luego las pares (bueno, o viceversa, pero ahora no nos interesa la grasa).


 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 } 

Cuando se elimina la señal de reset , dispara con pulsos rectangulares con las longitudes de los intervalos entre la conmutación especificada por el parámetro de lengths , después de lo cual permanece para siempre en el último estado. Este ejemplo demuestra el uso de tablas de valores usando VecInit , así como una forma de obtener el ancho de registro requerido: chisel3.util.log2Ceil(maxVal + 1).W . Sinceramente, no recuerdo cómo se hizo en Verilog, pero en Chisel, para crear dicho módulo parametrizado por un vector de valores, es suficiente llamar al constructor de la clase con el parámetro requerido.


Probablemente pregunte: "Si las entradas de clock y reset se generan implícitamente, ¿cómo" recargaremos "el generador de impulsos para cada cuadro?" Los desarrolladores de Cincel han proporcionado todo:


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

Implementación ingenua de un generador de señal


Para que el televisor nos entienda al menos de alguna manera, debe admitir el "protocolo" del nivel de truco promedio: hay tres niveles de señal importantes:


  • 1.0V - color blanco
  • 0.3V - color negro
  • 0V - nivel especial

¿Por qué llamé 0V especial? Debido a que con una transición suave de 0.3V a 1.0V, cambiamos suavemente de negro a blanco, y entre 0V y 0.3V, hasta donde puedo entender, no hay niveles intermedios y 0V se usa solo para sincronización. (De hecho, ni siquiera cambia en el rango 0V - 1V, pero -0.3V - 0.7V, pero, con suerte, todavía hay un condensador en la entrada)


Como nos enseña este maravilloso artículo , una señal PAL compuesta consiste en un flujo interminable de 625 líneas repetidas: la mayoría de ellas son líneas, de hecho, imágenes (por separado, pares e impares por separado), algunas se usan para fines de sincronización (hicimos el generador para ellas señales), algunos no son visibles en la pantalla. Se ven así (no piratearé y daré enlaces al original):



Intentemos describir las interfaces de los módulos:


BWGenerator gestionará los tiempos, etc., necesita saber con qué frecuencia funciona:


 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 calculará el nivel de la señal de luminancia, así como una señal de color adicional:


 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 } 

En el módulo PalGenerator , simplemente PalGenerator dos módulos especificados:


 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 } 

Y ahora lamentablemente dibujamos el primer búho ...
 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 } } } 

Generación de código sintetizado


Todo esto es bueno, pero queremos coser el diseño resultante en una tabla. Para hacer esto, debe sintetizar Verilog. Esto se hace de una manera muy 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 realidad, en el método main() dos líneas main() lo hacemos dos veces, el resto del código es otro módulo que sigue.


Generador de imágenes de prueba absolutamente aburrido
 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) } 

Ahora debe incluirlo en el proyecto Quartus. Para Mars rover 2, necesitaremos la versión gratuita de Quartus 13.1. Cómo instalarlo, está escrito en el sitio de los rovers de Marte. A partir de ahí, descargué el "Primer proyecto" para la placa Mars Rover 2, lo puse en el repositorio y lo corregí un poco. Como no soy ingeniero electrónico (y FPGA me interesan más como aceleradores que como tarjetas de interfaz), entonces


como en esa broma ...

El programador se encuentra profundamente en la depuración.
Hijo apto:
"Papá, ¿por qué sale el sol en el este todos los días y se sienta en el oeste?"
"¿Lo revisaste?"
- Comprobado.
- Bien comprobado?
- bien.
- Funciona?
- Funciona
- ¿Funciona todos los días?
Sí, todos los días.
- Entonces, por el amor de Dios, hijo, no toques nada, no cambies nada.


... Acabo de eliminar el generador de señal VGA y agregué mi módulo.


Cambio en quatus


Después de eso, conecté el sintonizador de TV analógico a otra computadora (computadora portátil), para que hubiera al menos un aislamiento galvánico entre la fuente de alimentación del generador de señal y el consumidor y simplemente envié una señal desde los pines IO7 (+) y GND (-) de la placa a la entrada compuesta (menos al contacto externo, más al centro). Bueno, es decir, como "simple" ... Simplemente sería si las manos crecieran de donde debería, bueno, o si tuviera cables de conexión hembra-macho. Pero solo tengo un montón de cables macho-macho. Pero tengo tenacidad y pinzas! En general, hay un hilo de estreñimiento, me hice casi dos trabajadores, con dificultad, pero aferrándome al tablero. Y aquí está lo que vi:


Primera imagen en blanco y negro


De hecho, yo, por supuesto, te engañé un poco. El código que se muestra arriba lo obtuve después de aproximadamente tres horas de depuración "en hardware", pero, ¡maldición, lo escribí y funciona! Y, dado que no estaba familiarizado con la electrónica seria, creo que la tarea no fue terrible, qué tarea difícil.


Generación de video en color


Bueno, entonces, la cosa sigue siendo pequeña: agregar un generador de señal de video en color. Tomé el tutorial y comencé a tratar de formar una explosión de color (agregada al nivel de negro de la onda sinusoidal a la frecuencia portadora de la señal de color, producida durante un corto tiempo durante HSync) y, de hecho, la señal de color de acuerdo con la fórmula. Pero no sale, a pesar de que se quiebra ... En algún momento me di cuenta de que, a pesar del hecho de que la frecuencia no me llamó la atención al echar un vistazo rápido al documento, el televisor apenas se sintonizó en uno arbitrario. Después de buscar, descubrí que el PAL usa una frecuencia portadora de 4.43 MHz. La cosa es el sombrero, pensé. "Jódete", respondió el sintonizador. Después de un día entero de depuración y solo una vez viendo destellos de color en la imagen (además, cuando le dije al sintonizador que era NTSC en general)


... me di cuenta de cómo se ve realmente la desesperanza

Entonces me di cuenta de que no puedo prescindir de un osciloscopio. Y, como ya he dicho, no estoy familiarizado con la electrónica, y, por supuesto, no tengo un milagro de la tecnología en casa. Para comprar? Un poco caro para un experimento ... ¿Y de qué se puede construir en la rodilla? ¿Conectar una señal a la entrada de línea de la tarjeta de sonido? Sí, 4 megahercios y medio, es poco probable que comience (al menos sin alteración). Hmm, el Mars Rover tiene un ADC a 20 MHz, pero no es suficiente transferir un flujo sin procesar de la velocidad de la interfaz serial a la computadora. Bueno, en algún lugar, todavía tienes que procesar la señal para mostrar, y de hecho habrá una cantidad bastante aceptable de bits de información, pero también es para meterse con el puerto serie, escribir programas para la computadora ... Entonces pensé que el ingeniero debería desarrollar hay una tenacidad saludable en sí misma: hay un generador de imágenes en color inactivo, hay un ADC ... Pero la imagen en blanco y negro se emite de forma estable ... Bueno, ¡deje que el generador de señal se depure!


Digresión lírica (como dicen, "La opinión del alumno no tiene que coincidir con la opinión del maestro, el sentido común y la axiomática de Peano"): cuando agregué generación de color con todo tipo de multiplicaciones y otras cosas complicadas, Fmax se hundió fuertemente por el acondicionador de señal. ¿Qué es la Fmax? Según tengo entendido por el libro de texto de Harris & Harris, CAD para FPGA prefiere cuando Verilog se escribe no en ningún caso como dentro del estándar, sino "por conceptos": por ejemplo, el resultado debería ser un circuito sincrónico, una especie de red acíclica direccional de lógica combinatoria (suma, multiplicación , división, operaciones lógicas, ...), pegado con sus entradas y salidas a las salidas y entradas de disparadores, respectivamente. Un disparador en el borde de la señal de reloj recuerda el valor de su entrada para todo el siguiente ciclo de reloj, cuyo nivel debe ser estable en algún momento antes del frente y algún tiempo después (estas son dos constantes de tiempo). Las señales de las salidas de los disparadores, a su vez, después de que la señal del reloj comience a correr hacia las salidas de la lógica combinacional (y, por lo tanto, las entradas de otros disparadores. Bueno, y las salidas del microcircuito), que también se caracteriza por dos intervalos: el tiempo durante el cual aún no se ha emitido ninguna salida. tendrá tiempo para comenzar a cambiar, y el tiempo después del cual los cambios se calmarán (siempre que la entrada haya cambiado una vez). Aquí está la frecuencia máxima a la que la lógica combinacional asegura que se cumplan los requisitos de los disparadores, y esto es Fmax. Cuando el circuito entre dos relojes debería tener tiempo para contar más, la Fmax disminuye. Por supuesto, quiero que la frecuencia sea mayor, pero si de repente saltó 10 veces (o incluso la cantidad de dominios de frecuencia en el informe CAD disminuyó), compruebe, tal vez haya estropeado algo en alguna parte, y como resultado, CAD encontró una expresión constante y felizmente lo usé para la optimización.


Promoción del osciloscopio


No, no después de la cual hay un giro del osciloscopio y un puñado de piezas adicionales, pero el arranque del osciloscopio es como el arranque del compilador, solo para el osciloscopio.


Haremos un osciloscopio, a pedido, grabando algunas muestras de la señal de entrada, después de lo cual solo se mostrará la grabación. Dado que necesitará de alguna manera dar un comando para grabar, y después de navegar por él, necesitaremos algunos controladores de botones, escribí no muy conveniente, pero bastante primitivo, aquí está:


 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! SENSACIÓN! Para que funcione, solo necesitas ...
  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) { //  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:


Desde el controlador de señal --- inmediatamente a la tabla


— ( IO7, VGA_GREEN R-2R ) :


En análogo, luego en "la figura", y luego --- en el gráfico


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


GitHub .


Conclusiones


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


: " -?" — ! ...

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


All Articles