Na parte anterior, montamos um microcontrolador sem RAM baseado no FPGA da Altera / Intel. No entanto, a placa possui um conector com SO-DIMM DDR2 1Gb instalado que, obviamente, eu quero usar. Para fazer isso, precisamos envolver o controlador DDR2 com a interface ALTMEMPHY
em um módulo compreensível para o protocolo de memória TileLink usado no RocketChip. Sob a depuração tátil, programação de força bruta e RAKE.
Como você sabe, a Ciência da Computação tem dois problemas principais: invalidação de cache e nomeação de variáveis. No KDPV, você vê um momento raro - os dois principais problemas que o CS se conheceram e estão tramando algo .
AVISO LEGAL: Além do aviso do artigo anterior, recomendo fortemente que você leia o artigo até o final antes de repetir os experimentos, a fim de evitar danos ao FPGA, módulo de memória ou circuitos de energia.
Desta vez, eu queria, se não inicializar o Linux, pelo menos conectar a RAM, que na minha placa já tem um gigabyte inteiro (ou você pode colocar até quatro). Um critério de sucesso é proposto para considerar a capacidade de ler e gravar um monte de GDB + OpenOCD, incluindo endereços que não estão alinhados por 16 bytes (a largura de uma solicitação para a memória). À primeira vista, você só precisa corrigir um pouco a configuração, o gerador SoC não pode suportar RAM imediatamente. Ele suporta, mas através da interface MIG (bem, e, possivelmente, alguma outra interface do Microsemi). Por meio da interface padrão, o AXI4 também suporta, mas, pelo que entendi, não é tão fácil obtê-lo (pelo menos, não dominar o Platform Designer).
Digressão lírica: Até onde eu entendo, há uma série bastante popular de interfaces AXI "intra-chip" desenvolvidas pela ARM. Aqui alguém poderia pensar que tudo está patenteado e fechado. Mas depois que me inscrevi (sem nenhum “programa universitário” e qualquer outra coisa - apenas por e-mail e preenchi o questionário) e obtive acesso às especificações, fiquei agradavelmente surpreso. Obviamente, eu não sou advogado, mas parece que o padrão é bastante aberto: você precisa usar kernels licenciados da ARM ou não afirmam ser compatíveis com o ARM, e tudo parece estar bem . Mas, em geral, é claro, leia a licença, leia com advogados, etc.
Macaco e TileLink (fábula)
A tarefa parecia bastante simples e eu abri a descrição da placa do módulo ddr2_64bit
já disponível no projeto pelo fornecedor:
Propriedade Intel e geralmente module ddr2_64bit ( local_address, local_write_req, local_read_req, local_burstbegin, local_wdata, local_be, local_size, global_reset_n, pll_ref_clk, soft_reset_n, local_ready, local_rdata, local_rdata_valid, local_refresh_ack, local_init_done, reset_phy_clk_n, mem_odt, mem_cs_n, mem_cke, mem_addr, mem_ba, mem_ras_n, mem_cas_n, mem_we_n, mem_dm, phy_clk, aux_full_rate_clk, aux_half_rate_clk, reset_request_n, mem_clk, mem_clk_n, mem_dq, mem_dqs); input [25:0] local_address; input local_write_req; input local_read_req; input local_burstbegin; input [127:0] local_wdata; input [15:0] local_be; input [2:0] local_size; input global_reset_n; input pll_ref_clk; input soft_reset_n; output local_ready; output [127:0] local_rdata; output local_rdata_valid; output local_refresh_ack; output local_init_done; output reset_phy_clk_n; output [1:0] mem_odt; output [1:0] mem_cs_n; output [1:0] mem_cke; output [13:0] mem_addr; output [1:0] mem_ba; output mem_ras_n; output mem_cas_n; output mem_we_n; output [7:0] mem_dm; output phy_clk; output aux_full_rate_clk; output aux_half_rate_clk; output reset_request_n; inout [1:0] mem_clk; inout [1:0] mem_clk_n; inout [63:0] mem_dq; inout [7:0] mem_dqs; ...
A sabedoria popular diz: "Qualquer documentação em russo deve começar com as palavras:" Portanto, não funciona "." Mas a interface aqui não é totalmente intuitiva , por isso ainda a lemos . Na descrição, somos informados imediatamente que trabalhar com DDR2 não é uma tarefa fácil. Você precisa configurar o PLL, realizar alguma calibração, crack-fex-pex , o sinal local_init_done
está local_init_done
, você pode trabalhar. Em geral, a lógica de nomenclatura aqui é aproximadamente a seguinte: nomes com prefixos local_
são a interface de "usuário", portas mem_
ser mem_
diretamente para as pernas conectadas ao módulo de memória, pll_ref_clk
precisa receber um sinal de relógio com a frequência especificada na configuração do módulo - o restante será recebido dele frequências, bem, todos os tipos de entradas e saídas são redefinidas e saídas de frequência, com as quais a interface do usuário deve trabalhar de forma síncrona.
Vamos criar uma descrição dos sinais externos para a memória e a interface do módulo ddr2_64bit
:
memorando de características trait MemIf { val local_init_done = Output(Bool()) val global_reset_n = Input(Bool()) val pll_ref_clk = Input(Clock()) val soft_reset_n = Input(Bool()) val reset_phy_clk_n = Output(Clock()) val mem_odt = Output(UInt(2.W)) val mem_cs_n = Output(UInt(2.W)) val mem_cke = Output(UInt(2.W)) val mem_addr = Output(UInt(14.W)) val mem_ba = Output(UInt(2.W)) val mem_ras_n = Output(UInt(1.W)) val mem_cas_n = Output(UInt(1.W)) val mem_we_n = Output(UInt(1.W)) val mem_dm = Output(UInt(8.W)) val phy_clk = Output(Clock()) val aux_full_rate_clk = Output(Clock()) val aux_half_rate_clk = Output(Clock()) val reset_request_n = Output(Bool()) val mem_clk = Analog(2.W) val mem_clk_n = Analog(2.W) val mem_dq = Analog(64.W) val mem_dqs = Analog(8.W) def connectFrom(mem_if: MemIf): Unit = { local_init_done := mem_if.local_init_done mem_if.global_reset_n := global_reset_n mem_if.pll_ref_clk := pll_ref_clk mem_if.soft_reset_n := soft_reset_n reset_phy_clk_n := mem_if.reset_phy_clk_n mem_odt <> mem_if.mem_odt mem_cs_n <> mem_if.mem_cs_n mem_cke <> mem_if.mem_cke mem_addr <> mem_if.mem_addr mem_ba <> mem_if.mem_ba mem_ras_n <> mem_if.mem_ras_n mem_cas_n <> mem_if.mem_cas_n mem_we_n <> mem_if.mem_we_n mem_dm <> mem_if.mem_dm mem_clk <> mem_if.mem_clk mem_clk_n <> mem_if.mem_clk_n mem_dq <> mem_if.mem_dq mem_dqs <> mem_if.mem_dqs phy_clk := mem_if.phy_clk aux_full_rate_clk := mem_if.aux_full_rate_clk aux_half_rate_clk := mem_if.aux_half_rate_clk reset_request_n := mem_if.reset_request_n } } class MemIfBundle extends Bundle with MemIf
classe dd2_64bit class ddr2_64bit extends BlackBox { override val io = IO(new MemIfBundle { val local_address = Input(UInt(26.W)) val local_write_req = Input(Bool()) val local_read_req = Input(Bool()) val local_burstbegin = Input(Bool()) val local_wdata = Input(UInt(128.W)) val local_be = Input(UInt(16.W)) val local_size = Input(UInt(3.W)) val local_ready = Output(Bool()) val local_rdata = Output(UInt(128.W)) val local_rdata_valid = Output(Bool()) val local_refresh_ack = Output(Bool()) }) }
Aqui o primeiro monte de rakes estava me esperando: primeiro, ROMGenerator
a classe ROMGenerator
, pensei que o controlador de memória pudesse ser retirado das profundezas do design por meio de uma variável global, e o Chisel de alguma forma encaminharia os próprios fios. Não deu certo. Portanto, eu tive que fazer um MemIfBundle
fios MemIfBundle
que MemIfBundle
toda a hierarquia. Por que não sai do BlackBox
e não se conecta de uma só vez? O fato é que, com o BlackBox
todas as portas externas são inseridas em val io = IO(new Bundle { ... })
. Se todo o MemIfBundle
transformado em uma variável no pacote MemIfBundle
, o nome dessa variável será um prefixo para os nomes de todas as portas e os nomes não serão muito coincidentes com a interface do bloco. Talvez isso possa ser feito de maneira mais adequada , mas por enquanto vamos deixar assim.
Além disso, por analogia com outros dispositivos TileLink (principalmente vivendo em rocket-chip/src/main/scala/tilelink
), e especialmente no BootROM
, descreveremos nossa interface para o controlador de memória:
class AltmemphyDDR2RAM(implicit p: Parameters) extends LazyModule { val MemoryPortParams(MasterPortParams(base, size, beatBytes, _, _, executable), 1) = p(ExtMem).get val node = TLManagerNode(Seq(TLManagerPortParameters( Seq(TLManagerParameters( address = AddressSet.misaligned(base, size), resources = new SimpleDevice("ram", Seq("sifive,altmemphy0")).reg("mem"), regionType = RegionType.UNCACHED, executable = executable, supportsGet = TransferSizes(1, 16), supportsPutFull = TransferSizes(1, 16), fifoId = Some(0) )), beatBytes = 16 ))) override lazy val module = new AltmemphyDDR2RAMImp(this) } class AltmemphyDDR2RAMImp(_outer: AltmemphyDDR2RAM)(implicit p: Parameters) extends LazyModuleImp(_outer) { val (in, edge) = _outer.node.in(0) val ddr2 = Module(new ddr2_64bit) val mem_if = IO(new MemIfBundle)
ExtMem
chave ExtMem
padrão ExtMem
extraímos os parâmetros de memória externa da ExtMem
do SoC ( essa sintaxe estranha permite-me dizer “Eu sei que eles retornarão uma instância da classe de caso MemoryPortParameters
(isso é garantido pelo tipo de chave no estágio de compilação do código Scala, pela condição que não cairemos no tempo de execução, obtendo o conteúdo da Option[MemoryPortParams]
igual a None
, mas não havia nada para criar um controlador de memória no System.scala
...); portanto, não preciso da classe case e alguns de seus campos são necessários "). Em seguida, criamos a porta do gerenciador do dispositivo TileLink (o protocolo TileLink garante a interação de quase tudo relacionado à memória: o controlador DDR e outros dispositivos mapeados na memória, caches de processador, talvez algo mais, cada dispositivo pode ter várias portas, cada uma o dispositivo pode ser gerente e cliente). beatBytes
, como eu o entendo, define o tamanho de uma transação e temos 16 bytes trocados com o controlador. HasAltmemphyDDR2
e HasAltmemphyDDR2Imp
nos lugares certos no System.scala
, escreva a configuração
class BigZeowaaConfig extends Config ( new WithNBreakpoints(2) ++ new WithNExtTopInterrupts(0) ++ new WithExtMemSize(1l << 30) ++ new WithNMemoryChannels(1) ++ new WithCacheBlockBytes(16) ++ new WithNBigCores(1) ++ new WithJtagDTM ++ new BaseConfig )
Depois de fazer um "esboço de uma coruja" no AltmemphyDDR2RAMImp
, sintetizei o design (algo apenas a ~ 30MHz, é bom que eu calcule o clock a partir de 25MHz) e, colocando meus dedos nos módulos de memória e no chip FPGA, carreguei-o na placa. Então vi o que é uma interface intuitiva real : é quando você dá um comando no gdb para gravar na memória, e por um processador congelado e queimado Se sentir um calor forte, pressione o botão de reinicialização na placa e conserte o controlador com urgência.
Leia a documentação para o controlador DDR2
Aparentemente, é hora de ler a documentação no controlador além da lista de portas. Então, o que temos aqui? Opa, acontece que as E / local_
com o prefixo local_
não devem ser definidas de forma síncrona, não com pll_ref_clk
, que é de 25MHz, mas com phy_clk
que phy_clk
metade da frequência de memória para o controlador de meia-taxa ou, no nosso caso, aux_half_rate_clk
(talvez afinal aux_full_rate_clk
?), que aux_full_rate_clk
a freqüência de memória total e, por um minuto, é 166MHz.
Portanto, é necessário cruzar as fronteiras dos domínios de frequência. De acordo com a memória antiga, decidi usar travas, ou melhor, uma cadeia delas:
+-+ +-+ +-+ +-+ --| |--| |--| |--| |---> +-+ +-+ +-+ +-+ | | | | ---+ | | | inclk | | | | | | --------+----+ | outclk | | ------------------+ output enable
Mas, tendo mexido com a hora, cheguei à conclusão de que não podia lidar com duas filas nas travas "escalares" (no domínio de alta frequência e vice-versa), cada uma das quais com sinais antidirecionais ( ready
e valid
) e, mesmo assim, para garantir que alguns beatik não ficarão atrás de uma batida ou duas ao longo da estrada. Depois de algum tempo, percebi que descrever a sincronização como ready
- valid
sem um sinal de relógio comum - também é uma tarefa semelhante à criação de estruturas de dados sem bloqueio, no sentido de que você precisa pensar e provar muito formalmente , é fácil cometer um erro, é difícil perceber, e mais importante, tudo já está implementado antes de nós: a Intel possui um primitivo dcfifo
, que é uma fila de comprimento e largura configuráveis, lida e gravada em diferentes domínios de frequência. Como resultado, aproveitei a oportunidade experimental do cinzel fresco, a saber, as caixas pretas parametrizadas:
class FIFO (val width: Int, lglength: Int) extends BlackBox(Map( "intended_device_family" -> StringParam("Cyclone IV E"), "lpm_showahead" -> StringParam("OFF"), "lpm_type" -> StringParam("dcfifo"), "lpm_widthu" -> IntParam(lglength), "overflow_checking" -> StringParam("ON"), "rdsync_delaypipe" -> IntParam(5), "underflow_checking" -> StringParam("ON"), "use_eab" -> StringParam("ON"), "wrsync_delaypipe" -> IntParam(5), "lpm_width" -> IntParam(width), "lpm_numwords" -> IntParam(1 << lglength) )) { override val io = IO(new Bundle { val data = Input(UInt(width.W)) val rdclk = Input(Clock()) val rdreq = Input(Bool()) val wrclk = Input(Clock()) val wrreq = Input(Bool()) val q = Output(UInt(width.W)) val rdempty = Output(Bool()) val wrfull = Output(Bool()) }) override def desiredName: String = "dcfifo" }
E ele escreveu um binóculo simples de tipos de dados arbitrários:
object FIFO { def apply[T <: Data]( lglength: Int, output: T, outclk: Clock, input: T, inclk: Clock ): FIFO = { val res = Module(new FIFO(width = output.widthOption.get, lglength = lglength)) require(input.getWidth == res.width) output := res.io.q.asTypeOf(output) res.io.rdclk := outclk res.io.data := input.asUInt() res.io.wrclk := inclk res } }
Depuração
Depois disso, o código se transformou na transferência de mensagens entre domínios por meio de duas filas já unidirecionais: tl_req
/ ddr_req
e ddr_resp
/ tl_resp
(aquele com o prefixo tl_
é sincronizado com o TileLink, o fato de que ddr_
está com o controlador de memória). O problema é que tudo estava em um impasse de qualquer maneira, e às vezes era bastante quente. E se a causa do superaquecimento foi a configuração simultânea de local_read_req
e local_write_req
, não foi tão fácil lidar com conflitos. O código ao mesmo tempo era algo como
class AltmemphyDDR2RAMImp(_outer: AltmemphyDDR2RAM)(implicit p: Parameters) extends LazyModuleImp(_outer) { val addrSize = log2Ceil(_outer.size / 16) val (in, edge) = _outer.node.in(0) val ddr2 = Module(new ddr2_64bit) require(ddr2.io.local_address.getWidth == addrSize) val tl_clock = clock val ddr_clock = ddr2.io.aux_full_rate_clk val mem_if = IO(new MemIfBundle) class DdrRequest extends Bundle { val size = UInt(in.a.bits.size.widthOption.get.W) val source = UInt(in.a.bits.source.widthOption.get.W) val address = UInt(addrSize.W) val be = UInt(16.W) val wdata = UInt(128.W) val is_reading = Bool() } val tl_req = Wire(new DdrRequest) val ddr_req = Wire(new DdrRequest) val fifo_req = FIFO(2, ddr_req, ddr_clock, tl_req, clock) class DdrResponce extends Bundle { val is_reading = Bool() val size = UInt(in.d.bits.size.widthOption.get.W) val source = UInt(in.d.bits.source.widthOption.get.W) val rdata = UInt(128.W) } val tl_resp = Wire(new DdrResponce) val ddr_resp = Wire(new DdrResponce) val fifo_resp = FIFO(2, tl_resp, clock, ddr_resp, ddr_clock)
Para localizar o problema, decidi comentar banalmente todo o código interno withClock(ddr_clock)
(não é, visualmente, parece criar um fluxo) e substituí-lo por um esboço que funcione com certeza:
withClock (ddr_clock) { ddr_resp.rdata := 0.U ddr_resp.is_reading := ddr_req.is_reading ddr_resp.size := ddr_req.size ddr_resp.source := ddr_req.source val will_read = Wire(!fifo_req.io.rdempty && !fifo_resp.io.wrfull) fifo_req.io.rdreq := will_read fifo_resp.io.wrreq := RegNext(will_read) }
Como percebi depois, esse stub também não funcionou porque o construto Wire(...)
, que eu adicionei “por confiabilidade”, para mostrar que era um wire nomeado, na verdade usou o argumento apenas como um protótipo para criar tipo de significado, mas não o vinculou ao argumento-expressão . Além disso, quando tentei ler o que ainda era gerado, percebi que no modo de simulação há uma ampla seleção de asserções relacionadas à não conformidade com o protocolo TileLink. Eles provavelmente serão úteis para mim mais tarde, mas até agora não houve nenhuma tentativa de executar a simulação - por que iniciá-la? O Verilator provavelmente não conhece os núcleos IP da Alter, o ModelSim Starter Edition provavelmente se recusará a simular um projeto tão grande, mas eu também jurei a falta de um modelo de controlador para simulação. E para gerá-lo, você provavelmente precisará primeiro mudar para a nova versão do controlador (porque a antiga foi configurada no antigo Quartus).
De fato, os blocos de código foram retirados de uma versão quase funcional, e não a que foi ativamente depurada algumas horas antes. A propósito, é possível remontar constantemente o design mais rapidamente se a WithNBigCores(1)
for substituída por WithNSmallCores(1)
- do ponto de vista da funcionalidade básica do controlador de memória, parece não haver diferença. E um pequeno truque: para não direcionar os mesmos comandos para o gdb de cada vez (pelo menos não tenho histórico de comandos entre as sessões), basta digitar algo assim na linha de comando
../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "x/x 0x80000000" ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set variable *0x80000000=0x1234"
e execute conforme necessário usando meios regulares do shell.
Sumário
Como resultado, o seguinte código foi obtido para trabalhar com o controlador:
withClock(ddr_clock) { val rreq = RegInit(false.B)
Aqui ainda vamos mudar um pouco o critério de conclusão: já vi como, sem nenhum trabalho com memória, os dados gravados são como se fossem lidos, porque é um cache. Portanto, compilamos um simples pedaço de código:
#include <stdint.h> static volatile uint8_t *x = (uint8_t *)0x80000000u; void entry() { for (int i = 0; i < 1<<24; ++i) { x[i] = i; } }
../../rocket-tools/bin/riscv64-unknown-elf-gcc test.c -S -O1
Como resultado, obtemos o seguinte fragmento da listagem do assembler, inicializando os primeiros 16 MB de memória:
li a5,1 slli a5,a5,31 li a3,129 slli a3,a3,24 .L2: andi a4,a5,0xff sb a4,0(a5) addi a5,a5,1 bne a5,a3,.L2
bootrom/xip/leds.S
no início de bootrom/xip/leds.S
Agora, é improvável que tudo consiga manter apenas um cache. Resta executar o Makefile, reconstruir o projeto no Quartus, preenchê-lo no quadro, conectar o OpenOCD + GDB e ... Presumivelmente, aplausos, vitória:
$ ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" Remote debugging using :3333 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000000000010014 in ?? () (gdb) x/x 0x80000000 0x80000000: 0x03020100 (gdb) x/x 0x80000100 0x80000100: 0x03020100 (gdb) x/x 0x80000111 0x80000111: 0x14131211 (gdb) x/x 0x80010110 0x80010110: 0x13121110 (gdb) x/x 0x80010120 0x80010120: 0x23222120
É assim, descobriremos na próxima série (também não posso dizer sobre desempenho, estabilidade, etc.).
Código: AltmemphyDDR2RAM.scala .