Por um longo tempo, sonhei em aprender a trabalhar com o FPGA, observei atentamente. Então ele comprou um quadro de depuração, escreveu um par de alô mundos e colocou o quadro em uma caixa, porque não estava claro o que fazer com ele. Então surgiu a idéia: vamos escrever um gerador de vídeo composto para uma TV CRT antiga. A ideia, é claro, é engraçada, mas eu realmente não conheço a Verilog, e ainda preciso me lembrar dela, e não preciso realmente desse gerador ... E recentemente eu queria olhar para os processadores de software RISC-V . Você precisa começar em algum lugar, e o código do Rocket Chip (esta é uma das implementações) está escrito no Chisel - esse é um DSL do Scala. De repente, lembrei-me de que havia dois anos desenvolvia profissionalmente o Scala e percebi: chegara a hora ...
Então, se você quiser ler uma história sobre a vida de alicate, um multímetro digital e um osciloscópio que se realizou, então seja bem-vindo ao gato.
Então, o que haverá neste artigo? Nele, descreverei minhas tentativas de gerar um sinal de vídeo PAL composto (por que PAL? - acabei de encontrar um bom tutorial especificamente sobre geração PAL) na placa Mars rover 2 da nckma . Sobre o RISC-V neste artigo, não direi nada. :)
Primeiro, um pouco sobre o Scala e o Chisel: Scala é uma linguagem que roda em cima da Java Virtual Machine e usa de forma transparente as bibliotecas Java existentes (embora também haja Scala.js e Scala Native). Quando comecei a estudá-lo, tive a sensação de que era um híbrido muito viável de “vantagens” e Haskell (no entanto, colegas não compartilham essa opinião) - é um sistema do tipo dolorosamente avançado e linguagem concisa, mas devido à necessidade de cruzar o funcionalismo com OOP uma abundância de construções de linguagem em alguns lugares evocou memórias de C ++. No entanto, não tenha medo do Scala - é uma linguagem muito concisa e segura, com um poderoso sistema de tipos, no qual você pode simplesmente escrever como Java aprimorado. E também, tanto quanto eu sei, o Scala foi originalmente desenvolvido como uma linguagem para a criação conveniente de idiomas específicos do domínio - é quando você descreve, digamos, equipamentos ou anotações digitais em uma linguagem formal, e essa linguagem parece bastante lógica do ponto de vista de sua área de assunto. E, de repente, você descobre que era o código correto em Scala (bem, ou Haskell) - apenas pessoas gentis criaram uma biblioteca com uma interface conveniente. O Chisel é exatamente uma biblioteca do Scala, que permite descrever a lógica digital em uma DSL conveniente e, em seguida, executar o código Scala resultante e gerar o código no Verilog (ou qualquer outra coisa) que possa ser copiado para o projeto Quartus. Bem, ou execute imediatamente os testes de unidade padrão no estilo scala, que simularão as bancas de teste e emitirão um relatório sobre os resultados.
Para me familiarizar com o circuito digital, recomendo vivamente este livro (ele já está na versão impressa em russo). De fato, meu conhecimento sistemático do mundo do FPGA quase termina neste livro; portanto, críticas construtivas nos comentários são bem-vindas (no entanto, repito, o livro é maravilhoso: ele conta desde o básico até a criação de um simples processador transportador. E há fotos;)). Bem, de acordo com Chisel, há um bom tutorial oficial .
Isenção de responsabilidade: o autor não é responsável pelo equipamento queimado e, se você decidir repetir o experimento, é melhor verificar os níveis de sinal com um osciloscópio, refazer a parte analógica etc. E, em geral - observe as precauções de segurança. (Por exemplo, no processo de redação do artigo, percebi que as pernas também são membros, e não há nada para enfiá-las na bateria de aquecimento central, segurando a saída da placa ...) A propósito, essa infecção também causou interferência na TV na sala ao lado. no decorrer da depuração ...
Configuração do projeto
Escreveremos o código no IntelliJ Idea Community Edition , sbt será o sistema de compilação; portanto, crie um diretório, coloque .gitignore
, project/build.properties
, project/plugins.sbt
daqui e
build.sbt um tanto simplificado def scalacOptionsVersion(scalaVersion: String): Seq[String] = { Seq() ++ {
Agora abra-o na Idea e peça para importar o projeto sbt - enquanto o sbt fará o download das dependências necessárias.
Primeiros módulos
PWM
Primeiro, vamos tentar escrever um PWM simples. Minha lógica era aproximadamente a seguinte: para gerar um sinal de ciclo de serviço n / m, inicialmente colocamos 0 no registro e adicionaremos n a cada passo. Quando o valor do registro exceder m, subtraia me dê um nível alto para um ciclo de relógio. Na verdade, haverá erros se n> m, mas consideraremos um comportamento indefinido, necessário para otimizar os casos reais usados.
Não vou recontar todo o guia para iniciantes - ele lê em meia hora, apenas direi que, para descrever o módulo, precisamos importar o chisel3._
e herdar da classe abstrata Module
. É abstrato porque precisamos descrever o Bundle
sob o nome io
- ele terá toda a interface do módulo. Ao mesmo tempo, as entradas de clock
e reset
aparecerão implicitamente - você não precisa descrevê-las separadamente. Aqui está o que aconteceu:
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 chamamos o método .W
um int regular para obter a largura da porta, e o .asUInt(width.W)
geralmente chamamos um literal inteiro! Como isso é possível? - bem, no Smalltalk, definiríamos apenas um novo método para a classe Integer (ou como ele é chamado lá), mas na JVM ainda não temos o objeto inteiro - também existem tipos primitivos e o Scala entende isso (e, além disso, existem classes de terceiros que não podemos modificar). Portanto, há uma variedade de s implícitos: nesse caso, Scala provavelmente encontra algo como
implicit class BetterInt(n: Int) { def W: Width = ... }
no escopo atual, as entradas comuns têm superpotências. Aqui está um dos recursos que torna o Scala mais conciso e fácil de criar DSL.
Adicione uma pitada de testes a isso. 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
é um dos três testadores padrão no Chisel. Permite definir os valores nas entradas do DUT (dispositivo em teste) e verificar os valores nas saídas. Como podemos ver, o ScalaTest usual é usado para testes e os testes ocupam espaço 5 vezes a própria implementação, o que, em princípio, é normal para o software. No entanto, suspeito que desenvolvedores experientes de equipamentos "fundidos em silício" apenas sorriem com um número microscópico de testes. Lançamento e opa ...
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
Sim, corrija-o no PWM na linha io.pulse := nextValue > io.denominator
sign >=
, reinicie os testes - tudo funciona! Receio que desenvolvedores experientes de equipamentos digitais desejem me matar por uma atitude tão frívola em relação ao design (e alguns desenvolvedores de software terão prazer em se juntar a eles) ...
Gerador de impulsos
Também precisaremos de um gerador que forneça pulsos de sincronização para "semi-quadros". Por que semi? porque primeiro as linhas ímpares são transmitidas, depois as pares (bem ou vice-versa, mas agora não nos importamos com gordura).
import chisel3._ import chisel3.util._ class OneShotPulseGenerator(val lengths: Seq[Int], val initial: Boolean) extends Module {
Quando o sinal de reset
é removido, ele dispara com pulsos retangulares com os comprimentos dos intervalos entre a comutação especificada pelo parâmetro lengths
, após o que permanece para sempre no último estado. Este exemplo demonstra o uso de tabelas de valores usando VecInit
, bem como uma maneira de obter a largura de registro necessária: chisel3.util.log2Ceil(maxVal + 1).W
. Sinceramente, não me lembro como isso foi feito no Verilog, mas no Chisel, para criar um módulo parametrizado por um vetor de valores, basta chamar o construtor da classe com o parâmetro necessário.
Você provavelmente pergunta: "Se o clock
e as entradas de reset
forem geradas implicitamente, como" recarregaremos "o gerador de pulsos para cada quadro? Os desenvolvedores de cinzéis forneceram tudo:
val module = Module( new MyModule() ) val moduleWithCustomReset = withReset(customReset) { Module( new MyModule() ) } val otherClockDomain = withClock(otherClock) { Module( new MyModule() ) }
Implementação ingênua de um gerador de sinal
Para que a TV nos entenda pelo menos de alguma maneira, você precisa apoiar o "protocolo" do nível médio de truques: há três níveis de sinal importantes:
- 1.0V - cor branca
- 0.3V - cor preta
- 0V - nível especial
Por que liguei para 0V especial? Como com uma transição suave de 0,3V para 1,0V, alternamos suavemente de preto para branco e entre 0V e 0,3V, tanto quanto eu entendo, não há níveis intermediários e 0V é usado apenas para sincronização. (De fato, ele nem muda no intervalo 0V - 1V, mas -0,3V - 0,7V, mas, esperançosamente, ainda há um capacitor na entrada)
Como este maravilhoso artigo nos ensina, um sinal PAL composto consiste em um fluxo interminável de 625 linhas repetidas: a maioria delas são linhas, na verdade, imagens (separadamente pares e ímpares separadamente), algumas são usadas para fins de sincronização (fizemos o gerador para elas sinais), alguns não são visíveis na tela. Eles se parecem com isso (não vou piratear e dar links para o original):
Vamos tentar descrever as interfaces dos módulos:
BWGenerator
gerenciará os horários etc., precisa saber com que frequência ele 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á o nível do sinal de luminância, bem como um sinal de cor 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)) })
No módulo PalGenerator
, simplesmente PalGenerator
dois 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 }
E agora, infelizmente, desenhamos a primeira coruja ... 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,
Geração de código sintetizado
Tudo isso é bom, mas queremos costurar o design resultante em uma placa. Para fazer isso, você precisa sintetizar o Verilog. Isso é feito de uma maneira muito simples:
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)) } }
Na verdade, no método de duas linhas main()
fazemos isso duas vezes, o restante do código é outro módulo que fica próximo
Gerador de imagem de teste absolutamente monótono 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) }
Agora você precisa inseri-lo no projeto Quartus. Para Mars rover 2, precisaremos da versão gratuita do Quartus 13.1. Como instalá-lo, está escrito no site dos rovers de Marte. A partir daí, baixei o "Primeiro Projeto" para a placa Mars Rover 2, coloquei no repositório e o corrigi um pouco. Como não sou engenheiro eletrônico (e FPGA, na verdade, estou mais interessado em aceleradores do que em placas de interface),
como nessa piada ...O programador está profundamente envolvido na depuração.
Filho apto:
"Papa, por que o sol nasce no leste todos os dias e fica no oeste?"
"Você checou isso?"
- verificado.
- Bem verificado?
- bom
Isso funciona?
Isso funciona.
- Funciona todos os dias?
Sim todos os dias.
- Então, pelo amor de Deus, filho, não toque em nada, não mude nada.
... Acabei de excluir o gerador de sinal VGA e adicionei meu módulo.

Depois disso, conectei o sintonizador de TV analógico a outro computador (laptop), para que houvesse pelo menos algum isolamento galvânico entre a fonte de alimentação do gerador de sinais e do consumidor e simplesmente enviei um sinal dos pinos IO7 (+) e GND (-) da placa para a entrada composta (menos contato externo, além do centro). Bem, isto é, como "simples" ... Simplesmente seria se as mãos crescessem de onde deveriam, bem, ou se eu tivesse fios de conexão feminino-masculino. Mas eu só tenho um monte de fios macho-macho. Mas tenho tenacidade e garras! Em geral, há um fio de prisão de ventre; eu me tornei dois quase trabalhadores - com dificuldade, mas me agarrando ao quadro. E aqui está o que eu vi:

Na verdade, eu, é claro, enganei você um pouco. O código mostrado acima eu obtive após cerca de três horas de depuração "no hardware", mas, droga, eu escrevi e funciona! E, como eu não estava familiarizado com eletrônicos sérios, acho que a tarefa não foi terrível, que tarefa difícil.
Geração de vídeo em cores
Bem, então, a coisa permanece pequena - para adicionar um gerador de sinal de vídeo em cores. Peguei o tutorial e comecei a tentar formar uma explosão de cores (adicionada ao nível de preto da onda senoidal na frequência portadora do sinal de cor, durante um curto período de tempo produzido durante o HSync) e, de fato, o sinal de cor de acordo com a fórmula. Mas não sai, mesmo que você decifre ... Em algum momento, ocorreu-me que, apesar do fato de a frequência não ter chamado minha atenção ao dar uma olhada rápida no documento, a TV mal estava sintonizada em uma televisão arbitrária. Após a pesquisa, descobri que o PAL usa uma frequência portadora de 4,43 MHz. O negócio é o chapéu, pensei. "Foda-se", respondeu o afinador. Após um dia inteiro de depuração e apenas uma vez vendo vislumbres de cores na imagem (além disso, quando eu disse ao sintonizador que era NTSC em geral)
... eu percebi como a desesperança realmente parece Então eu percebi que não posso ficar sem um osciloscópio. E, como já disse, não estou familiarizado com a eletrônica e, é claro, não tenho um milagre da tecnologia em casa. Para comprar? Um pouco caro para um experimento ... E pelo que ele pode ser construído sobre o joelho? Conectar um sinal à entrada de linha da placa de som? Sim, 4 e meio megahertz - é improvável que comece (pelo menos sem alterações). Hmm, o rover Mars tem um ADC a 20 MHz, mas transferir um fluxo bruto da velocidade da interface serial para o computador não é suficiente. Bem, em algum lugar, você ainda precisa processar o sinal para exibição e, de fato, haverá uma quantidade aceitável de bits de informação, mas também é para mexer na porta serial, escrever programas para o computador ... Então pensei que o engenheiro deveria desenvolver existe uma tenacidade saudável em si: existe um gerador de imagens coloridas ociosas, existe um ADC ... Mas a imagem em preto e branco é impressa de maneira estável ... Bem, deixe o gerador de sinais se depurar!
Digressão lírica (como eles dizem: “A opinião do aluno não precisa coincidir com a opinião do professor, do senso comum e da axiomatics de Peano”): Quando adicionei geração de cores com todos os tipos de multiplicações e outras coisas complicadas, Fmax se inclinou fortemente para o condicionador de sinal. O que é o Fmax? Até onde eu entendo no livro de Harris & Harris, o CAD para FPGA prefere quando o Verilog é escrito não em nenhum caso como dentro do padrão, mas "por conceitos": por exemplo, o resultado deve ser um circuito síncrono - um tipo de rede acíclica direcional da lógica combinacional (adição, multiplicação , divisão, operações lógicas, ...), presos às suas entradas e saídas às saídas e entradas dos acionadores, respectivamente. Um gatilho na extremidade do sinal do relógio lembra o valor de sua entrada para todo o próximo ciclo do relógio, cujo nível deve ser estável algum tempo antes da frente e algum tempo depois (essas são duas constantes de tempo). Os sinais das saídas dos gatilhos, por sua vez, após o sinal do relógio começarem a correr para as saídas da lógica combinacional (e, portanto, as entradas de outros gatilhos. Bem, e as saídas do microcircuito), que também são caracterizadas por dois intervalos: o tempo durante o qual nenhuma saída ainda terá tempo para começar a mudar e o tempo após o qual as alterações se acalmarão (desde que a entrada tenha sido alterada uma vez). Aqui está a frequência máxima na qual a lógica combinacional garante que os requisitos dos gatilhos sejam atendidos - e isso é Fmax. Quando o circuito entre dois relógios tiver tempo para contar mais, o Fmax diminui. É claro que quero que a frequência seja maior, mas se subitamente subisse 10 vezes (ou até o número de domínios de frequência no relatório CAD diminuísse) - verifique, talvez você tenha estragado algo em algum lugar e, como resultado, o CAD encontrou uma expressão constante e felizmente o usei para otimização.
Osciloscópio Promoção
Não, não aquele após o qual há uma torção do osciloscópio e um punhado de peças extras, mas o bootstrapping do osciloscópio é como o bootstrapping do compilador, apenas para o osciloscópio.
Faremos um osciloscópio, sob comando, gravando algumas amostras do sinal de entrada, após o que apenas exibindo o gravado. Como ele precisará dar um comando para gravar e, depois - navegar nele, precisaremos de alguns controladores de botão - escrevi não muito conveniente, mas bastante primitivo, aqui 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()) })
CHOQUE! SENSAÇÃO! Para fazê-lo funcionar, você só precisa ... private val cycleCounter = RegInit(0.asUInt(32.W)) private val pressedCounter = RegInit(0.asUInt(32.W)) io.click := false.B io.longPress := false.B when (cycleCounter === 0.asUInt) { when (pressedCounter >= pressThreshold.asUInt) { io.longPress := true.B }.elsewhen (pressedCounter >= clickThreshold.asUInt) { io.click := true.B } cycleCounter := period.asUInt pressedCounter := 0.asUInt } otherwise { cycleCounter := cycleCounter - 1.asUInt when (io.buttonInput === pressedIsHigh.B) { pressedCounter := pressedCounter + 1.asUInt } } }
:
class Oscilloscope( clocksPerUs: Int, inputWidth: Int, windowPixelWidth: Int, windowPixelHeight: Int ) extends Module { val io = IO(new Bundle { val signal = Input(UInt(inputWidth.W)) val visualOffset = Input(UInt(16.W)) val start = Input(Bool()) val x = Input(UInt(10.W)) val y = Input(UInt(10.W)) val output = Output(Bool()) }) private val mem = SyncReadMem(1 << 15, UInt(inputWidth.W)) private val physicalPixel = RegInit(0.asUInt(32.W)) when (io.start) { physicalPixel := 0.asUInt } when (physicalPixel < mem.length.asUInt) { mem.write(physicalPixel, io.signal) physicalPixel := physicalPixel + 1.asUInt } private val shiftedX = io.x + io.visualOffset private val currentValue = RegInit(0.asUInt(inputWidth.W)) currentValue := ((1 << inputWidth) - 1).asUInt - mem.read( Mux(shiftedX < mem.length.asUInt, shiftedX, (mem.length - 1).asUInt) ) when (io.x > windowPixelWidth.asUInt || io.y > windowPixelHeight.asUInt) {
— , :
class OscilloscopeController( visibleWidth: Int, createButtonController: () => SimpleButtonController ) extends Module { val io = IO(new Bundle { val button1 = Input(Bool()) val button2 = Input(Bool()) val visibleOffset = Output(UInt(16.W)) val start = Output(Bool()) val leds = Output(UInt(4.W)) }) val controller1 = Module(createButtonController()) val controller2 = Module(createButtonController()) controller1.io.buttonInput <> io.button1 controller2.io.buttonInput <> io.button2 private val offset = RegInit(0.asUInt(16.W)) private val leds = RegInit(0.asUInt(4.W)) io.start := false.B when (controller1.io.longPress && controller2.io.longPress) { offset := 0.asUInt io.start := true.B leds := leds + 1.asUInt }.elsewhen (controller1.io.click) { offset := offset + (visibleWidth / 10).asUInt }.elsewhen (controller2.io.click) { offset := offset - (visibleWidth / 10).asUInt }.elsewhen (controller1.io.longPress) { offset := offset + visibleWidth.asUInt }.elsewhen (controller2.io.longPress) { offset := offset - visibleWidth.asUInt } io.visibleOffset := offset io.leds := leds }
(, ), - : — , — , ( — ). — ! , Verilog ?..
- , FPGA:

— ( IO7, VGA_GREEN R-2R ) :

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