ملاحظات الهواة ، أو حكاية كيفية تكوين مطور Scala FPGA

لفترة طويلة حلمت بتعلم كيفية العمل مع FPGA ، نظرت عن كثب. ثم اشترى لوحة تصحيح ، وكتب بعالمين مرحبًا ووضع اللوحة في صندوق ، لأنه لم يكن واضحًا ما يجب فعله بها. ثم جاءت الفكرة: دعنا نكتب مولد فيديو مركب لتلفزيون CRT قديم. الفكرة ، بالطبع ، مضحكة ، لكنني لا أعرف فيريلوج حقًا ، ولا يزال يتعين علي تذكرها ، ولا أحتاج حقًا إلى هذا المولد ... ومؤخرًا أردت أن أتطلع إلى معالجات برامج RISC-V . تحتاج إلى البدء في مكان ما ، ويتم كتابة رمز رقاقة الصواريخ (هذا هو أحد التطبيقات) باللغة Chisel - وهذا هو DSL لـ Scala. ثم تذكرت فجأة أنه لمدة عامين كنت أتطور بشكل احترافي على Scala وأدركت: حان الوقت ...


لذا ، إذا كنت ترغب في قراءة قصة من حياة قواطع الأسلاك ، وجهاز رقمي متعدد ومقياس تذبذب أدرك نفسه ، فمرحباً بك في القط.


فماذا سيكون في هذه المقالة؟ في ذلك ، سأصف محاولاتي لإنشاء إشارة فيديو PAL مركبة (لماذا PAL؟ - لقد صادفت للتو برنامجًا تعليميًا جيدًا على وجه التحديد حول جيل PAL) على لوحة Mars rover 2 بواسطة nckma . حول RISC-V في هذه المقالة لن أقول أي شيء على الإطلاق. :)


أولاً ، القليل عن Scala و Chisel: Scala هي لغة يتم تشغيلها فوق Java Virtual Machine وتستخدم مكتبات Java الموجودة بشفافية (على الرغم من وجود Scala.js و Scala Native). عندما بدأت بدراستها لأول مرة ، شعرت أنها كانت هجينًا قابلاً للتطبيق من "الإيجابيات" وهاسكيل (ومع ذلك ، لا يشارك الزملاء هذا الرأي) - إنه نظام نوع متقدم مؤلم ولغة موجزة ، ولكن بسبب الحاجة إلى تقاطع الوظائف مع OOP وفرة من بنيات اللغة في بعض الأماكن أثار ذكريات C ++. ومع ذلك ، لا تخف من Scala - إنها لغة موجزة وآمنة للغاية مع نظام نوع قوي ، حيث يمكنك في البداية ببساطة كتابة جافا محسنة. وأيضًا ، على حد علمي ، تم تطوير Scala في الأصل كلغة للإنشاء المريح للغات محددة للنطاق - هذا عندما تصف ، على سبيل المثال ، المعدات الرقمية أو الملاحظات بلغة رسمية ، وتبدو هذه اللغة منطقية تمامًا من وجهة نظر موضوعها. ثم اكتشفت فجأة أنه كان الرمز الصحيح في Scala (حسنًا ، أو Haskell) - فقط كتب أشخاص طيبون مكتبة مع واجهة ملائمة. Chisel هي مجرد مكتبة لـ Scala ، والتي تسمح لك بوصف المنطق الرقمي على DSL مناسب ، ثم تشغيل كود Scala الناتج وإنشاء رمز على Verilog (أو أي شيء آخر) يمكن نسخه إلى مشروع Quartus. حسنًا ، أو قم على الفور بإجراء اختبارات الوحدة القياسية على غرار سكالا ، والتي ستقوم هي نفسها بمحاكاة مقاعد الاختبار وإصدار تقرير عن النتائج.


للتعرف على الدوائر الرقمية ، أوصي بشدة بهذا الكتاب (إنه موجود بالفعل في النسخة الروسية المطبوعة). في الواقع ، فإن معرفتي المنتظمة بعالم FPGA تكاد تنتهي عند هذا الكتاب ، لذا فإن النقد البناء في التعليقات مرحب به (ومع ذلك ، أكرر ، الكتاب رائع: فهو يخبر من الأساسيات إلى إنشاء معالج ناقل بسيط. وهناك صور هناك ؛)). حسنًا ، وفقًا لـ Chisel ، هناك برنامج تعليمي رسمي جيد .


إخلاء المسؤولية: المؤلف غير مسؤول عن المعدات المحترقة ، وإذا قررت تكرار التجربة ، فمن الأفضل التحقق من مستويات الإشارة باستخدام مرسمة الذبذبات ، وإعادة تشكيل الجزء التناظري ، إلخ. وبشكل عام - مراعاة احتياطات السلامة. (على سبيل المثال ، في عملية كتابة المقالة ، أدركت أن الأرجل هي أيضًا أطراف ، ولا يوجد شيء لإلصاقها في بطارية التدفئة المركزية ، مع التمسك بإخراج اللوحة ...) بالمناسبة ، تسببت هذه العدوى أيضًا في حدوث تداخل في التلفزيون في الغرفة المجاورة في سياق التصحيح ...


إعداد المشروع


سنكتب الكود في IntelliJ Idea Community Edition ، حيث سيكون نظام البناء هو sbt ، لذا قم بإنشاء دليل ، ضع project/build.properties ، project/plugins.sbt project/build.properties ، project/plugins.sbt من هنا و


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) 

الآن افتحها في الفكرة واطلب استيراد مشروع sbt - بينما سيقوم sbt بتنزيل التبعيات اللازمة.


الوحدات الأولى


PWM


أولاً ، دعنا نحاول كتابة PWM بسيط. منطقتي كانت تقريبًا ما يلي: لتوليد إشارة دورة عمل n / m ، نضع 0 في البداية في السجل وسنضيف n إليها في كل خطوة. عندما تتجاوز قيمة التسجيل m ، اطرح m واعطي مستوى عالٍ لدورة ساعة واحدة. في الواقع ، سيكون عربات التي تجرها الدواب إذا كان n> m ، ولكننا سنعتبر هذا سلوكًا غير محدد ، وهو مطلوب لتحسين الحالات الفعلية المستخدمة.


لن أقوم بإعادة بيع دليل المبتدئين بالكامل - فهو يقرأ في غضون نصف ساعة ، سأقول فقط أنه لوصف الوحدة ، نحتاج إلى استيراد chisel3._ من Module الفصل المجردة. إنها مجردة لأننا بحاجة إلى وصف Bundle تحت اسم io - سيكون لها الواجهة الكاملة للوحدة النمطية. في نفس الوقت ، ستظهر clock ومدخلات reset بشكل ضمني - لا تحتاج إلى وصفها بشكل منفصل. إليك ما حدث:


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

لاحظ أننا نسمي طريقة .W عدد صحيح منتظم للحصول على عرض المنفذ ، وأسلوب .asUInt(width.W) الذي نطلق عليه بشكل عام حرف حرفي صحيح! كيف هذا ممكن؟ - حسنًا ، في Smalltalk سنقوم فقط بتحديد طريقة جديدة لفئة Integer (أو أيا كان اسمها هناك) ، ولكن في JVM ما زلنا لا نملك الكائن بالكامل - هناك أيضًا أنواع بدائية ، ويفهم Scala هذا (بالإضافة إلى ذلك ، هناك فئات خارجية لا يمكننا تعديلها). لذلك ، هناك مجموعة متنوعة من s الضمنية: في هذه الحالة ، ربما يجد Scala شيئًا مثل


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

في النطاق الحالي ، لذلك تمتلك Ints العادية قوى خارقة. فيما يلي إحدى الميزات التي تجعل Scala أكثر إيجازًا وسهولة في إنشاء DSL.


أضف القليل من الاختبارات لهذا.
 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 هو واحد من ثلاثة أجهزة اختبار قياسية في إزميل. يسمح لك بتعيين القيم عند مدخلات DUT (الجهاز قيد الاختبار) والتحقق من القيم عند المخرجات. كما نرى ، يتم استخدام ScalaTest المعتاد للاختبارات وتستهلك الاختبارات مساحة 5 أضعاف التنفيذ نفسه ، وهو ، من حيث المبدأ ، أمر طبيعي للبرمجيات. ومع ذلك ، أعتقد أن مطوري المعدات ذوي الخبرة "المصبوب في السيليكون" سوف يبتسمون فقط مع هذا العدد المجهري من الاختبارات. التشغيل والعفو ...


 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 

نعم ، قم بإصلاحه في PWM في السطر io.pulse := nextValue > io.denominator تسجيل io.pulse := nextValue > io.denominator >= ، io.pulse := nextValue > io.denominator تشغيل الاختبارات - كل شيء يعمل! أخشى أن المطورين ذوي الخبرة للمعدات الرقمية سيرغبون في قتلي بسبب هذا الموقف التافه للتصميم (وسوف ينضم إليهم بعض مطوري البرامج بكل سرور) ...


مولد النبض


سنحتاج أيضًا إلى مولد يعطي نبضات التزامن لـ "إطارات نصف". لماذا شبه؟ لأنه يتم أولاً نقل الخطوط الفردية ، ثم الخطوط الزوجية (حسنًا ، أو العكس بالعكس ، لكننا الآن لا نهتم بالدهون).


 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 } 

عندما تتم إزالة إشارة reset ، يتم تصويرها بنبضات مستطيلة بأطوال الفترات الفاصلة بين التبديل المحدد بواسطة معلمة lengths ، وبعد ذلك تظل إلى الأبد في الحالة الأخيرة. يوضح هذا المثال استخدام جداول القيم باستخدام VecInit ، بالإضافة إلى طريقة للحصول على عرض التسجيل المطلوب: chisel3.util.log2Ceil(maxVal + 1).W . بصراحة لا أتذكر كيف تم ذلك في Verilog ، ولكن في Chisel ، لإنشاء وحدة نمطية تم تحديدها بواسطة متجه القيم ، يكفي استدعاء مُنشئ الصف بالمعلمة المطلوبة.


ربما تسأل: "إذا تم إنشاء clock ومدخلات reset بشكل ضمني ، فكيف" نعيد شحن "مولد النبض لكل إطار؟" قدم مطورو إزميل كل شيء:


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

تنفيذ ساذج لمولد إشارة


لكي يفهمنا التلفزيون بطريقة أو بأخرى على الأقل ، تحتاج إلى دعم "بروتوكول" متوسط ​​مستوى الحيلة: هناك ثلاثة مستويات إشارة مهمة:


  • 1.0V - اللون الأبيض
  • 0.3V - اللون الأسود
  • 0V - مستوى خاص

لماذا اتصلت على 0V خاص؟ لأنه مع الانتقال السلس من 0.3V إلى 1.0V ، ننتقل بسلاسة من الأسود إلى الأبيض ، وبين 0V و 0.3V ، على حد علمي ، لا توجد مستويات وسيطة ويتم استخدام 0V فقط للمزامنة. (في الواقع ، لا يتغير حتى في النطاق 0V - 1V ، ولكن -0.3V - 0.7V ، ولكن نأمل أن لا يزال هناك مكثف عند الإدخال)


كما تعلمنا هذه المقالة الرائعة ، تتكون إشارة PAL المركبة من دفق لا نهاية له من تكرار 625 خطًا: معظمها خطوط ، في الواقع ، صور (بشكل فردي ومتساوي بشكل فردي) ، يتم استخدام بعضها لأغراض المزامنة (قمنا بعمل المولد لهم الإشارات) ، بعضها غير مرئي على الشاشة. إنهم يبدون مثل هذا (لن أقوم بالقرصنة وأعطي روابط للأصل):



دعونا نحاول وصف واجهات الوحدات:


BWGenerator التوقيت ، وما إلى ذلك ، وهي بحاجة إلى معرفة التردد الذي تعمل فيه:


 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 مستوى إشارة النصوع ، بالإضافة إلى إشارة لون إضافية:


 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 } 

في وحدة PalGenerator ، PalGenerator ببساطة PalGenerator وحدتين PalGenerator :


 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 } 

والآن نرسم للأسف البومة الأولى ...
 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 } } } 

توليد كود توليف


كل هذا جيد ، لكننا نريد خياطة التصميم الناتج في لوحة. للقيام بذلك ، تحتاج إلى تجميع Verilog. يتم ذلك بطريقة بسيطة للغاية:


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

في الواقع ، في طريقة الخطين main() نقوم بذلك مرتين ، أما بقية الكود فهو وحدة أخرى تلتصق بعد ذلك


اختبار صورة مملة للغاية
 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) } 

الآن تحتاج إلى دفعه في مشروع Quartus. بالنسبة إلى Mars rover 2 ، سنحتاج إلى الإصدار المجاني من Quartus 13.1. كيفية تثبيته ، هو مكتوب على موقع روفرز المريخ. من هناك ، قمت بتنزيل "المشروع الأول" للوحة Mars Rover 2 ، ووضعه في المستودع ، وقمت بتصحيحه قليلاً. نظرًا لأنني لست مهندسًا للإلكترونيات (و FPGA فأنا مهتم حقًا بالمسرعات أكثر من كونها بطاقات واجهة) ،


كما في تلك النكتة ...

المبرمج يجلس في عمق التصحيح.
ابن صالح:
"بابا ، لماذا تشرق الشمس في الشرق كل يوم وتجلس في الغرب؟"
"هل تحققت من ذلك؟"
- فحص.
- فحص جيد؟
- جيد.
- هل تعمل؟
- إنها تعمل.
- هل يعمل كل يوم؟
- نعم كل يوم.
- ثم في سبيل الله ، يا بني ، لا تلمس أي شيء ، لا تغير أي شيء.


... لقد قمت للتو بحذف مولد إشارة VGA وإضافة الوحدة النمطية الخاصة بي.


التبديل في Quatus


بعد ذلك ، قمت بتوصيل موالف التلفزيون التمثيلي بكمبيوتر آخر (كمبيوتر محمول) ، بحيث كان هناك على الأقل بعض العزلة الجلفانية بين مصدر الطاقة لمولد الإشارة والمستهلك وأرسلت للتو إشارة من دبابيس IO7 (+) و GND (-) من اللوحة إلى الإدخال المركب (ناقص للاتصال الخارجي ، بالإضافة إلى المركز). حسنًا ، هذا هو "بسيط" ... سيكون ببساطة إذا نمت الأيدي من حيث يجب ، حسنًا ، أو إذا كان لدي أسلاك توصيل ذكر-أنثى. ولكن لدي فقط مجموعة من الأسلاك من الذكور-الذكور. ولكن لدي المثابرة والقصوص! بشكل عام ، هناك سلك واحد من الإمساك ، جعلت نفسي عاملين تقريبًا - بصعوبة ، لكن التشبث باللوحة. وهذا ما رأيته:


صورة B / W الأولى


في الحقيقة ، بالطبع ، خدعتك قليلاً. لقد حصلت الشفرة الموضحة أعلاه بعد حوالي ثلاث ساعات من تصحيح الأخطاء "على الأجهزة" ، ولكن ، اللعنة ، قمت بكتابتها ، وهي تعمل !!! وبالنظر إلى أنني كنت غير مألوفة تقريبًا بالإلكترونيات الجادة ، أعتقد أن المهمة لم تكن رهيبة ، يا لها من مهمة صعبة.


توليد الفيديو الملون


حسنًا ، يبقى الأمر صغيرًا - لإضافة مولد إشارة فيديو ملونة. لقد أخذت البرنامج التعليمي وبدأت في محاولة تشكيل اندفاع لوني (يضاف إلى المستوى الأسود لموجة الجيب عند تردد الناقل لإشارة اللون ، والتي يتم إنتاجها لفترة قصيرة أثناء HSync) ، وفي الواقع ، إشارة اللون وفقًا للصيغة. لكن هذا لا يخرج ، حتى لو انكسر ... في وقت ما بزغ لي أنه ، على الرغم من حقيقة أن التردد لم يلفت نظري بنظرة سريعة على الوثيقة ، بالكاد تم ضبط التلفزيون على جهاز تعسفي. بعد البحث ، وجدت أن PAL يستخدم تردد الموجة الحاملة 4.43 ميجاهرتز. أعتقد أن الشيء هو القبعة. أجاب الموالف "اللعنة عليك". بعد يوم كامل من التصحيح ومرة ​​واحدة فقط لرؤية لمحات من الألوان في الصورة (علاوة على ذلك ، عندما أخبرت الموالف أنه كان NTSC بشكل عام)


... أدركت كيف يبدو اليأس حقا

ثم أدركت أنه لا يمكنني الاستغناء عن مرسمة الذبذبات. وكما قلت بالفعل ، لست على دراية بالإلكترونيات ، وبالطبع ليس لدي مثل هذه المعجزة التكنولوجية في المنزل. للشراء؟ مكلفة بعض الشيء لتجربة واحدة ... ومن ماذا يمكن بناؤها على الركبة؟ توصيل إشارة لإدخال خط بطاقة الصوت؟ نعم ، 4 ميغاهرتز ونصف - من غير المحتمل أن تبدأ (على الأقل بدون تغيير). حسنًا ، يحتوي المسبار المريخ على ADC بسرعة 20 ميجاهرتز ، ولكن نقل دفق أولي من سرعة الواجهة التسلسلية إلى الكمبيوتر ليس كافيًا. حسنًا ، في مكان ما ، لا يزال عليك معالجة الإشارة للعرض ، وفي الواقع سيكون هناك كمية مقبولة من بتات المعلومات ، ولكن أيضًا للتلاعب بالمنفذ التسلسلي ، وكتابة برامج للكمبيوتر ... ثم اعتقدت أنه يجب على المهندس تطوير هناك مثابرة صحية في حد ذاتها: هناك صورة ملونة خاملة ، وهناك ADC ... لكن الصورة بالأبيض والأسود يتم إخراجها بثبات ... حسنًا ، دع مولد الإشارة يعمل على تصحيح نفسه!


الانحدار الغنائي (كما يقولون ، "لا يجب أن يتطابق رأي الطالب مع رأي المعلم والحس السليم والبديهيات البينية"): عندما أضفت جيل الألوان مع جميع أنواع المضاعفات والأشياء المعقدة الأخرى ، تراجعت Fmax بقوة لمكيف الإشارة. ما هو Fmax؟ بقدر ما فهمت من كتاب Harris & Harris المدرسي ، يفضل CAD for FPGA عندما لا تتم كتابة Verilog على أي حال كما هو الحال في المعيار ، ولكن "حسب المفاهيم": على سبيل المثال ، يجب أن تكون النتيجة دائرة متزامنة - نوع من شبكة لا حلقية اتجاهية من المنطق التوافقي (إضافة ، الضرب ، التقسيم ، العمليات المنطقية ، ...) ، عالقة مع المدخلات والمخرجات إلى مخرجات ومدخلات المشغلات ، على التوالي. يتذكر الزناد على حافة إشارة الساعة قيمة مدخلاتها لدورة الساعة التالية بأكملها ، والتي يجب أن يكون مستواها مستقرًا في بعض الوقت قبل المقدمة وبعض الوقت بعد ذلك (هذان ثوابتان زمنيان). الإشارات من مخرجات المشغلات ، بدورها ، بعد أن تبدأ إشارة الساعة في تشغيلها إلى مخرجات المنطق التوافقي (وبالتالي ، مدخلات المشغلات الأخرى. حسنًا ، ومخرجات الدائرة المصغرة) ، والتي تتميز أيضًا بفاصلين: الوقت الذي لم يتم خلاله إخراج أي سيكون لديك الوقت لبدء التغيير ، والوقت الذي تهدأ بعده التغييرات (بشرط أن يكون الإدخال قد تغير مرة واحدة). فيما يلي أقصى تردد يضمن المنطق التوافقي تلبية متطلبات المحفزات - وهذا هو Fmax. عندما يجب أن يكون لدى الدائرة بين ساعتين وقت لحساب أكثر ، يقل Fmax. بالطبع ، أريد أن يكون التردد أكبر ، ولكن إذا قفز فجأة 10 مرات (أو حتى انخفض عدد نطاقات التردد في تقرير CAD) - تحقق ، ربما أفسدت شيئًا في مكان ما ، ونتيجة لذلك ، وجد CAD تعبيرًا ثابتًا وسعدنا باستخدامها للتحسين.


راسم الذبذبات


لا ، ليس الشيء الذي يوجد بعده تطور للذبذبات وحفنة من الأجزاء الإضافية ، ولكن ذراع تمهيد الذبذبات يشبه تقليب البومر ، فقط من أجل راسم الذبذبات.


سنقوم بعمل مذبذب ، عند الطلب ، لتسجيل بعض عينات إشارة الإدخال ، وبعد ذلك فقط عرض التسجيل. نظرًا لأنه سيحتاج إلى إعطاء أمر ما للتسجيل بطريقة ما ، وبعد - التنقل عبره ، سنحتاج إلى بعض وحدات تحكم الأزرار - لقد كتبت ليس مناسبًا جدًا ، ولكن بدائيًا تمامًا ، هنا هو:


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

صدمة! تحسس! لجعلها تعمل ، تحتاج فقط إلى ...
  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:


من محرك الإشارة --- مباشرة إلى الرسم البياني


— ( IO7, VGA_GREEN R-2R ) :


في النظير ، ثم في & quot ؛ الشكل & quot ؛ ثم --- على الرسم البياني


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


GitHub .


الاستنتاجات


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


: " -?" — ! ...

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


All Articles