Teach Remote lab lessons

Teach lesson

Lógica digital na DE1-SoC (3/6): memória — flip-flops e registradores

Construa flip-flops D e registradores em Verilog, teste o comportamento de clock e reset e diferencie lógica combinacional de estado armazenado.

  • Altera DE1-SoC
  • 55 min
  • Graduação, introdutório
  • Português (Brasil)
  • Digital systems & FPGA
Altera DE1-SoC
Altera DE1-SoC

Learning Outcomes

  • Explicar por que a lógica sequencial precisa de memória e a lógica combinacional não.

  • Descrever um flip-flop D como amostragem e retenção na borda do clock, com reset síncrono.

  • Construir um registrador de carga paralela de 8 bits com flip-flops D que compartilham clock e reset.

  • Usar atribuição não bloqueante em blocos acionados por clock e reconhecer entradas KEY ativas em nível baixo.

Student activity preview

Activity Content

Preview only. In a class session, students can fill in responses and submit their work to the teacher.

1

Por que os circuitos precisam de memória

10 min

Todos os projetos até agora foram combinacionais: os LEDs eram uma função pura das posições das chaves *naquele instante*. Ao mudar uma chave, a saída muda assim que o sinal se propaga pelas portas. Um circuito combinacional não tem estado: ele não sabe quais eram as entradas um instante antes e não consegue se lembrar de nada.

Mas a maioria dos sistemas úteis precisa de memória. Um contador deve conhecer seu valor atual para produzir o próximo. Uma CPU precisa manter operandos e resultados entre instruções. Um semáforo deve saber qual cor está acesa para decidir o que vem depois. Nada disso é possível apenas com portas, porque elas sempre refletem somente suas entradas atuais.

Lógica sequencial é a lógica que se lembra. Sua saída depende não só das entradas atuais, mas também de um estado armazenado: o que aconteceu antes. O bloco básico que armazena um bit de estado é o flip-flop, e o marcador de tempo que informa a todos os flip-flops *quando* atualizar é o clock.

A DE1-SoC fornece um clock livre de 50 MHz chamado CLOCK_50: uma onda quadrada que sobe e desce 50.000.000 de vezes por segundo. A partir desta lição, seus projetos sequenciais devem listá-lo como entrada. O modelo padrão leds_mirror.v lista apenas chaves, botões e LEDs e não inclui clock; portanto, você mesmo adicionará input CLOCK_50; ao módulo. Esquecer essa entrada é o erro mais comum em projetos sequenciais.

Qual destes circuitos é sequencial — sua saída pode depender do passado — e não combinacional?

Os projetos combinacionais das Lições 1–2 listavam apenas chaves e LEDs, mas um projeto acionado por clock, como um flip-flop, também deve listar input CLOCK_50;. Por que um flip-flop ou registrador precisa do clock como entrada, e o que costuma acontecer se você omiti-lo? Responda em uma ou duas frases.

2

O flip-flop D: amostragem e retenção na borda do clock

11 min

Um flip-flop D é o elemento de memória mais simples. Ele tem uma entrada de dados D, uma entrada de clock e uma saída Q. Sua regra completa é:

> A cada borda de subida do clock, Q captura o valor de D e o mantém até a próxima borda de subida.

Entre as bordas, nada que aconteça na entrada importa: Q fica congelado. Isso é amostragem e retenção: o flip-flop amostra D no instante em que o clock passa de 0 para 1 e mantém essa amostra estável. É assim que um circuito ganha memória: Q pode continuar em 1 muito depois de D voltar a 0, pois se lembra da amostra.

Diagrama de temporização de um flip-flop D: uma onda quadrada de clock, um sinal de dados D que muda em instantes arbitrários e uma saída Q que só passa a acompanhar D em cada borda de subida e mantém esse valor até a borda seguinte, ilustrando amostragem e retenção.

Amostragem e retenção na borda do clock: Q só é atualizado em cada borda de subida de CLOCK_50, copiando o valor de D naquele instante e mantendo-o até a próxima borda. Mudanças em D entre as bordas são ignoradas.

Como escrever isso em Verilog — duas ideias novas.

Primeiro, o bloco acionado por clock. Descrevemos um comportamento acionado por borda com always @(posedge CLOCK_50): as instruções internas são executadas uma vez a cada borda de subida, e não continuamente. Um sinal atribuído dentro desse bloco deve ser declarado como reg, pois mantém um valor entre as bordas.

Segundo, a atribuição não bloqueante <=. Dentro de um bloco acionado por clock, use sempre <=, não =. O operador <= significa “amostre agora todos os lados direitos e depois atualize os lados esquerdos juntos na borda”, exatamente como se comportam flip-flops reais: todos amostram ao mesmo tempo. Usar = — uma atribuição bloqueante — dentro de um bloco acionado por clock é um erro clássico em Verilog: as atribuições passam a valer em sequência dentro da mesma borda, e um flip-flop pode enxergar o valor *novo* de outro em vez do valor anterior, produzindo um hardware diferente do pretendido. Regra prática: <= em blocos acionados por clock; = apenas em lógica combinacional com assign ou always @(*).

Reset síncrono. Também queremos uma forma de forçar Q a voltar a um valor conhecido. Um reset síncrono é verificado *dentro* do bloco acionado por clock, portanto só entra em vigor em uma borda do clock:

always @(posedge CLOCK_50) begin
    if (!KEY[0]) Q <= TODO_RESET_VALUE;   // reset sincrono, avaliado na borda
    else         Q <= TODO_CAPTURED_VALUE;
end

Como KEY é ativa em nível baixo, o botão apresenta 0 quando pressionado; portanto, !KEY[0] é verdadeiro exatamente enquanto o botão de reset está pressionado.

Suponha que você tenha escrito Q = SW[0]; com um = bloqueante, em vez de <=, dentro de always @(posedge CLOCK_50). Qual operador deveria ter usado, e o que pode dar errado quando vários flip-flops em um mesmo bloco acionado por clock são atualizados juntos com =?

Suponha que D passe brevemente para 1 apenas *entre* duas bordas de subida e volte a 0 antes da borda seguinte. Q chega a valer 1? Explique usando a regra de amostragem e retenção.

3

Exercício 2-A — flip-flop D (reset em ação)

12 min

Construa um único flip-flop D na placa real. As conexões são SW[0] = D, KEY[0] = reset (ativo em nível baixo), LEDR[0] = Q e LEDR[1] = ~Q.

Leia isto antes de executar — é importante. A 50 MHz, Q acompanha D 50.000.000 de vezes por segundo, muito mais rápido do que seus olhos ou a câmera conseguem perceber. Portanto, embora este circuito realmente *armazene* D, você não consegue *observar* o armazenamento: ao mudar SW[0], parece que LEDR[0] acompanha a chave imediatamente. Por isso, este exercício demonstra principalmente o reset, e não o armazenamento. O comportamento que você observará é:

- Com SW[0] para cima (D=1), LEDR[0] fica aceso, enquanto LEDR[1], que mostra ~Q, fica apagado.
- Mantenha KEY[0] pressionado — o botão apresenta 0, portanto o reset está ativo. LEDR[0] apaga mesmo que SW[0] continue para cima. O reset tem prioridade sobre os dados.
- Solte KEY[0]: LEDR[0] volta a acompanhar SW[0].

Você *verá* a memória no próximo exercício, 2-B. Por enquanto, concentre-se no reset que substitui a entrada de dados.

// Licao 3 - Exercicio 2-A: modelo inicial de flip-flop D
// SW[0]=D, KEY[0]=reset (ativo em nivel baixo), LEDR[0]=Q, LEDR[1]=~Q.
module leds_mirror(CLOCK_50, SW, KEY, LEDR);
    input        CLOCK_50;
    input  [9:0] SW;
    input  [3:0] KEY;
    output [9:0] LEDR;

    reg Q;
    always @(posedge CLOCK_50) begin
        if (!KEY[0]) Q <= TODO_RESET_VALUE;
        else         Q <= TODO_D_INPUT;
    end

    assign LEDR[0]   = TODO_Q_OUTPUT;
    assign LEDR[1]   = TODO_NOT_Q_OUTPUT;
    assign LEDR[9:2] = 8'b0;
endmodule
  1. Abra o laboratório Verilog da DE1-SoC. O editor mostra o arquivo leds_mirror.v.

  2. Selecione todo o conteúdo de leds_mirror.v e substitua-o pelo módulo do flip-flop D acima. Mantenha o nome do módulo como leds_mirror. Observe que ele agora declara input CLOCK_50;: essa linha é obrigatória em qualquer projeto acionado por clock.

  3. Clique em Sintetizar (Synthesize). Se um tour guiado cobrir o botão, feche ou pule o tour primeiro. Espere o processo terminar, o que leva cerca de 1 a 3 minutos. Se houver um erro, as causas mais comuns são a ausência de CLOCK_50 na lista de portas ou de um ponto e vírgula. Corrija e sintetize novamente. Prossiga somente quando o estado final indicar uma compilação bem-sucedida com 0 erros.

  4. Clique em Enviar para a FPGA (Upload to FPGA) e espere a sessão da placa e a câmera ao vivo.

  5. Coloque SW[0] para cima (D=1). Confirme que LEDR[0] (Q) está aceso e LEDR[1] (~Q) está apagado.

  6. Demonstre o reset: com SW[0] ainda para cima, mantenha KEY[0] pressionado. Observe LEDR[0] apagar enquanto a chave permanece igual: o reset substitui os dados. Solte KEY[0] e confirme que LEDR[0] volta a acender.

Ajuste SW[0] e KEY[0] como indicado em cada linha e registre o que LEDR[0] (Q) e LEDR[1] (~Q) mostram na câmera (1 = aceso, 0 = apagado). Lembre-se de que KEY é ativa em nível baixo: pressionado = 0, solto = 1.

SW[0] (D) KEY[0] (0=pressionado, 1=solto) LEDR[0] = Q LEDR[1] = ~Q

Qual é a diferença entre este flip-flop e um fio simples de SW[0] para LEDR[0]? Responda em termos de memória.

Este projeto usa um reset síncrono. Aponte a linha de código que realiza o reset e explique o que “síncrono” significa quanto ao momento em que o reset entra em vigor. Você conhecerá o reset assíncrono na Lição 5.

Por que LEDR[1] sempre mostra o oposto de LEDR[0]?

O Exercício 2-A realmente armazena D, mas na câmera você só conseguiu ver o reset funcionar, nunca o armazenamento. Usando a frequência de 50 MHz, explique por que o armazenamento é invisível e o reset é claramente visível.

4

Exercício 2-B — registrador de carga paralela de 8 bits (memória visível)

12 min

Um registrador é simplesmente um conjunto de N flip-flops D lado a lado, todos compartilhando o mesmo clock e o mesmo reset. Um registrador de 8 bits armazena 8 bits. Uma carga paralela copia todos os 8 bits de entrada para o registrador ao mesmo tempo em uma borda do clock: cada flip-flop captura seu próprio bit de dados simultaneamente.

O recurso que torna a memória visível aqui é o controle de carga. Em vez de capturar os dados a *cada* borda, como fazia o flip-flop isolado, este registrador só captura quando você manda. Assim, você pode mudar as chaves à vontade e o valor armazenado permanece igual até que seja feita uma carga. As conexões são SW[7:0] = dados, KEY[0] = reset (ativo em nível baixo) e KEY[1] = carga (ativa em nível baixo).

Como KEY é ativa em nível baixo, você deve pressionar e soltar KEY[1] para carregar: ao pressionar, KEY[1] apresenta 0, e o código verifica !KEY[1] para detectar essa condição. Observe que KEY[1] é *amostrada pelo clock*; ela não é um sinal de clock com eliminação de rebotes em hardware. O projeto simplesmente verifica o nível do botão a cada borda de subida, o que é suficiente para este exercício.

// Licao 3 - Exercicio 2-B: modelo inicial de registrador de carga paralela de 8 bits
// SW[7:0]=dados, KEY[0]=reset (ativo em nivel baixo), KEY[1]=carga (ativa em nivel baixo).
module leds_mirror(CLOCK_50, SW, KEY, LEDR);
    input        CLOCK_50;
    input  [9:0] SW;
    input  [3:0] KEY;
    output [9:0] LEDR;

    reg [7:0] Q;
    always @(posedge CLOCK_50) begin
        if (!KEY[0])      Q <= TODO_RESET_VECTOR;
        else if (!KEY[1]) Q <= TODO_LOAD_VALUE;
    end

    assign LEDR[7:0] = TODO_REGISTER_OUTPUT;
    assign LEDR[9:8] = 2'b00;
endmodule
  1. Em leds_mirror.v, substitua todo o conteúdo pelo módulo do registrador de 8 bits acima. Mantenha o nome do módulo como leds_mirror e preserve input CLOCK_50;.

  2. Clique em Sintetizar (Synthesize), espere o sucesso e depois clique em Enviar para a FPGA (Upload to FPGA).

  3. Ajuste as chaves para SW[7:0] = 10101010, sendo SW[7] o bit mais significativo. Os LEDs ainda não mostrarão esse valor: nada foi carregado.

  4. Carregue: pressione e solte KEY[1]. LEDR[7:0] agora mostra 10101010. O registrador capturou as chaves.

  5. Veja a memória: agora mude as chaves para SW[7:0] = 01010101 sem tocar em KEY[1]. Observe os LEDs: eles não mudam. O registrador mantém o valor armazenado, embora as entradas sejam diferentes. Isso é memória.

  6. Atualize: pressione e solte KEY[1] novamente. Agora LEDR[7:0] muda para 01010101.

  7. Reinicie: pressione e solte KEY[0]. Todos os oito LEDs apagam — Q = 00000000 — independentemente das chaves.

Execute a sequência de carga e registre os 8 LEDs (LEDR[7:0]) em cada etapa. A observação principal é a linha 'chaves alteradas, sem carga': o valor armazenado deve permanecer igual ao da linha anterior.

Etapa Valor de SW[7:0] Ação em KEY LEDR[7:0] observado

Envie sua evidência. Faça uma captura da câmera que prove que o registrador está *mantendo* o valor: LEDR[7:0] mostra 10101010, o valor carregado anteriormente, enquanto as chaves na tela SW[7:0] estão em um padrão diferente, como 01010101, e você não pressionou KEY[1]. Mostre na mesma captura os LEDs mantidos e as posições diferentes das chaves e anexe-a abaixo.

Na etapa 5, você mudou as chaves, mas os LEDs não se alteraram. Explique por quê usando as palavras “armazenar” e “carregar”.

Se você mantivesse reset (KEY[0]) e carga (KEY[1]) pressionados ao mesmo tempo em uma borda do clock, qual valor o registrador assumiria e por quê? Aponte a linha de código que decide isso.

Qual é a finalidade prática de um registrador de carga paralela dentro de uma CPU? Dê um exemplo concreto do que esse registrador pode armazenar.

5

O que você construiu e uma observação sobre estilos de reset

8 min

Você construiu os dois fundamentos de toda memória em sistemas digitais: o flip-flop D, que armazena um bit por amostragem e retenção na borda do clock, e o registrador, formado por vários flip-flops carregados em paralelo que mantêm o valor até receberem outra instrução. Você usou always @(posedge CLOCK_50) com a atribuição não bloqueante <= e viu uma entrada KEY ativa em nível baixo funcionar como controle.

Uma observação para as próximas lições — dois estilos de reset. Os dois projetos desta lição usaram um reset síncrono: ele foi verificado *dentro* do bloco acionado por clock, portanto só entrou em vigor em uma borda de subida. Isso é simples e previsível, e continuará sendo usado nos elementos de armazenamento desta lição e da próxima. As lições de máquinas de estados finitos, 5 e 6, porém, passarão a usar um reset assíncrono ativo em nível baixo, escrito como always @(posedge CLOCK_50 or negedge KEY[0]), que força o circuito a um estado seguro imediatamente, sem esperar a borda do clock. Isso é útil para recuperar uma máquina de estados travada na placa. A diferença detalhada e a razão para as FSMs preferirem o estilo assíncrono serão explicadas lá. Por enquanto, basta saber que os dois estilos existem e que esta lição usou deliberadamente o síncrono.

Qual sequência corresponde ao que você fez em cada exercício desta lição?

Em três ou quatro frases, explique como o Exercício 2-B *comprovou* que o registrador tem memória — cite seus dados —, por que o Exercício 2-A só conseguiu demonstrar o reset e não o armazenamento e indique um momento em que a polaridade ativa em nível baixo de KEY foi importante.