Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Industrial programming >> VHDL

Como criar um efeito de LED de respiração usando uma onda senoidal armazenada no bloco RAM


Percebi que muitos dos gadgets que comprei nos últimos dois anos mudaram de LED piscando para respiração conduzida. A maioria dos aparelhos eletrônicos contém um LED de status cujo comportamento dá indicações do que está acontecendo dentro do dispositivo.

Minha escova de dentes elétrica pisca um LED quando é hora de recarregá-la, e meu celular usa o LED para chamar minha atenção por uma ampla variedade de razões. Mas os LEDs não estão piscando como antigamente. É mais como um efeito pulsante analógico com intensidade variável contínua.

A animação Gif abaixo mostra meu mouse Logitech usando esse efeito para indicar que está carregando a bateria. Eu chamo esse efeito de LED de respiração porque o padrão de intensidade da luz é semelhante em velocidade e aceleração ao ciclo respiratório humano. Parece tão natural porque o ciclo de iluminação segue um padrão de onda senoidal.



Este artigo é uma continuação da postagem do blog da semana passada sobre modulação por largura de pulso (PWM). Hoje, vamos usar o módulo PWM, o contador de dente de serra e o módulo de reset que criamos no tutorial anterior. Clique no link abaixo para ler o artigo sobre PWM!

Como criar um controlador PWM em VHDL

Armazenando valores de onda senoidal na RAM do bloco


Embora seja possível gerar uma onda senoidal usando primitivas de processamento de sinal digital (DSP) no FPGA, uma maneira mais direta é armazenar os pontos de amostra na RAM do bloco. Em vez de calcular os valores durante o tempo de execução, calculamos uma série de valores de seno durante a síntese e criamos uma memória somente leitura (ROM) para armazená-los.

Considere o exemplo mínimo abaixo, que mostra como armazenar uma onda senoidal completa em uma ROM de 3×3 bits. Tanto a entrada de endereço quanto a saída de dados têm três bits de largura, o que significa que podem representar um valor inteiro no intervalo de 0 a 7. Podemos armazenar oito valores de seno na ROM, e a resolução dos dados também é de 0 a 7 .



A função seno trigonométrica produz um número no intervalo [-1, 1] para qualquer entrada de ângulo, x . Além disso, o ciclo se repete quando x ≥ 2π. Portanto, é suficiente armazenar apenas os valores de seno de zero e até, mas não incluindo 2π. O valor do seno para 2π é o mesmo que o seno para zero. A ilustração acima mostra esse conceito. Estamos armazenando valores de seno de zero a \frac{7\pi}{4}, que é o último passo de espaços iguais antes do círculo 2π completo.

A lógica digital não pode representar valores reais, como os valores de ângulo ou seno, com precisão infinita. Esse é o caso em qualquer sistema de computador. Mesmo ao usar números de ponto flutuante de precisão dupla, é apenas uma aproximação. É assim que os números binários funcionam, e nossa ROM senoidal não é diferente.

Para obter o máximo dos bits de dados disponíveis, adicionamos um deslocamento e uma escala aos valores do seno quando calculamos o conteúdo da ROM. O valor de seno mais baixo possível de -1 mapeia para o valor de dados 0, enquanto o valor de seno mais alto possível de 1 se traduz em 2^{\mathit{data\_bits}-1}, conforme mostrado pela fórmula abaixo.
\mathit{data} =\mathit{Round}\left(\frac{(1 + \sin \mathit{addr}) * (2^\mathit{data\_bits} - 1)}{2}\right)
Para traduzir um endereço ROM em um ângulo, x, podemos usar a seguinte fórmula:
x =\frac{\mathit{addr} * 2\pi}{2^\mathit{addr\_bits}}
Obviamente, esse método não fornece um conversor universal de valor de ângulo para seno. Se é isso que você deseja, você terá que usar lógica adicional ou primitivas de DSP para dimensionar a entrada de endereço e a saída de dados. Mas para muitas aplicações, uma onda senoidal representada como um inteiro sem sinal é suficiente. E como veremos na próxima seção, é exatamente o que precisamos para o nosso exemplo de projeto de pulsação de LED.

Módulo ROM senoidal


O módulo ROM sine apresentado neste artigo inferirá RAM de bloco na maioria das arquiteturas FPGA. Considere combinar os genéricos de largura e profundidade com os primitivos de memória de seu FPGA de destino. Isso lhe dará a melhor utilização de recursos. Você sempre pode consultar o exemplo do projeto Lattice se não tiver certeza de como usar a ROM senoidal para um projeto FPGA real.

Deixe seu email no formulário abaixo para baixar os arquivos VHDL e os projetos ModelSim/iCEcube2!




A entidade


O código abaixo mostra a entidade do módulo ROM senoidal. Ele contém duas entradas genéricas que permitem especificar a largura e a profundidade da RAM do bloco inferido. Estou usando especificadores de intervalo nas constantes para evitar estouro de inteiro não intencional. Mais sobre isso mais adiante neste artigo.
entity sine_rom is
  generic (
    addr_bits : integer range 1 to 30;
    data_bits : integer range 1 to 31
  );
  port (
    clk : in std_logic;
    addr : in unsigned(addr_bits - 1 downto 0);
    data : out unsigned(data_bits - 1 downto 0)
  );
end sine_rom; 

A declaração de porta tem uma entrada de relógio, mas não é redefinida porque as primitivas de RAM não podem ser redefinidas. O addr entrada é onde atribuímos o ângulo escalonado (x ) e os dados saída é onde o valor do seno escalonado aparecerá.

Declarações de tipo


Na parte superior da região declarativa do arquivo VHDL, declaramos um tipo e um subtipo para nosso objeto de armazenamento ROM. O addr_range subtype é um intervalo inteiro igual ao número de slots em nossa RAM, e o rom_type descreve uma matriz 2D que armazenará todos os valores de seno.
subtype addr_range is integer range 0 to 2**addr_bits - 1;
type rom_type is array (addr_range) of unsigned(data_bits - 1 downto 0);

No entanto, não vamos declarar o sinal de armazenamento ainda. Primeiro, precisamos definir a função que produzirá os valores do seno, que podemos usar para transformar a RAM em uma ROM. Temos que declará-lo acima da declaração do sinal para que possamos usar a função para atribuir um valor inicial ao sinal de armazenamento ROM.

Observe que estamos usando o addr_bits genérico como base para definir addr_range . Essa é a razão pela qual eu tive que especificar um valor máximo de 30 para addr_bits . Porque para valores maiores, o 2**addr_bits - 1 cálculo vai transbordar. Os inteiros VHDL têm 32 bits, embora isso esteja prestes a mudar com o VHDL-2019, que usava inteiros de 64 bits. Mas, por enquanto, temos que conviver com essa limitação ao usar inteiros em VHDL até que as ferramentas comecem a suportar VHDL-2019.

Função para gerar valores de seno


O código abaixo mostra o init_rom função que gera os valores do seno. Ele retorna um rom_type objeto, e é por isso que temos que declarar primeiro o tipo, depois a função e, finalmente, a constante ROM. Eles dependem um do outro nessa ordem exata.
function init_rom return rom_type is
  variable rom_v : rom_type;
  variable angle : real;
  variable sin_scaled : real;
begin

  for i in addr_range loop

    angle := real(i) * ((2.0 * MATH_PI) / 2.0**addr_bits);
    sin_scaled := (1.0 + sin(angle)) * (2.0**data_bits - 1.0) / 2.0;
    rom_v(i) := to_unsigned(integer(round(sin_scaled)), data_bits);
    
  end loop;
  
  return rom_v;
end init_rom;

A função usa algumas variáveis ​​de conveniência, incluindo rom_v , uma cópia local do array que preenchemos com valores de seno. Dentro do subprograma, usamos um loop for para iterar no intervalo de endereços e, para cada slot da ROM, calculamos o valor do seno usando as fórmulas que descrevi anteriormente. E no final, retornamos o rom_v variável que agora contém todas as amostras de seno.

A conversão de inteiros na última linha do loop for é a razão pela qual eu tive que restringir os data_bits genérico para 31 bits. Ele irá transbordar para quaisquer comprimentos de bits maiores.
constant rom : rom_type := init_rom;

Abaixo do init_rom definição da função, passamos a declarar o rom objeto como uma constante. Uma ROM é simplesmente uma RAM na qual você nunca escreve, então tudo bem. E agora é hora de usar nossa função. Chamamos init_rom para gerar os valores iniciais, conforme mostrado no código acima.

O processo de ROM


A única lógica no arquivo ROM sine é o processo bastante simples listado abaixo. Ele descreve um bloco de RAM com uma única porta de leitura.
ROM_PROC : process(clk)
begin
  if rising_edge(clk) then
    data <= rom(to_integer(addr));
  end if;
end process;

Módulo principal


Este design é uma continuação do projeto PWM que apresentei no meu post anterior. Possui um módulo de reset, um módulo gerador de PWM e um módulo contador de ciclos de relógio de funcionamento livre (contador de dente de serra). Consulte o artigo da semana passada para ver como esses módulos funcionam.

O diagrama abaixo mostra as conexões entre os submódulos no módulo superior.



O código abaixo mostra a região declarativa do arquivo VHDL superior. No projeto PWM da semana passada, o duty_cycle objeto era um alias para os MSBs do cnt contador. Mas isso não funcionaria agora porque a saída da ROM senoidal controlará o ciclo de trabalho, então a substituí por um sinal real. Além disso, criei um novo alias com o nome addr que são os MSBs do contador. Vamos usá-lo como a entrada de endereço para a ROM.
signal rst : std_logic;
signal cnt : unsigned(cnt_bits - 1 downto 0);
signal pwm_out : std_logic;
signal duty_cycle : unsigned(pwm_bits - 1 downto 0);

-- Use MSBs of counter for sine ROM address input
alias addr is cnt(cnt'high downto cnt'length - pwm_bits);

Você pode ver como instanciar nossa nova ROM senoidal no módulo superior na lista abaixo. Definimos a largura e a profundidade da RAM para seguir o comprimento do contador interno do módulo PWM. Os dados saída da ROM controla o duty_cycle entrada para o módulo PWM. O valor no duty_cycle sinal irá retratar um padrão de onda senoidal quando lermos os slots de RAM um após o outro.
SINE_ROM : entity work.sine_rom(rtl)
  generic map (
    data_bits => pwm_bits,
    addr_bits => pwm_bits
  )
  port map (
    clk => clk,
    addr => addr,
    data => duty_cycle
  );

Simulando a ROM de onda senoidal


A imagem abaixo mostra a forma de onda da simulação do módulo superior no ModelSim. Mudei a apresentação do duty_cycle não assinado sinal para um formato analógico para que possamos observar a onda senoidal.



É o led_5 saída de nível superior que transporta o sinal PWM, que controla o LED externo. Podemos ver que a saída está mudando rapidamente quando o ciclo de trabalho está aumentando ou diminuindo. Mas quando o ciclo de trabalho está no topo da onda senoidal, led_5 é um '1' constante. Quando a onda está na parte inferior da inclinação, a saída permanece brevemente em '0'.

Quer experimentá-lo em seu computador doméstico? Digite seu endereço de e-mail no formulário abaixo para receber os arquivos VHDL e os projetos ModelSim e iCEcube2!




Implementando a respiração de LED no FPGA


Usei o software Lattice iCEcube2 para implementar o projeto na placa iCEstick FPGA. Use o formulário acima para baixar o projeto e experimentá-lo em seu quadro se você possui um iCEstick!

A lista abaixo mostra o uso de recursos, conforme relatado pelo software Synplify Pro que acompanha o iCEcube2. Isso mostra que o design usa uma primitiva de RAM de bloco. Essa é a nossa ROM sine.
Resource Usage Report for led_breathing 

Mapping to part: ice40hx1ktq144
Cell usage:
GND             4 uses
SB_CARRY        31 uses
SB_DFF          5 uses
SB_DFFSR        39 uses
SB_GB           1 use
SB_RAM256x16    1 use
VCC             4 uses
SB_LUT4         65 uses

I/O ports: 7
I/O primitives: 7
SB_GB_IO       1 use
SB_IO          6 uses

I/O Register bits:                  0
Register bits not including I/Os:   44 (3%)

RAM/ROM usage summary
Block Rams : 1 of 16 (6%)

Total load per clock:
   led_breathing|clk: 1

@S |Mapping Summary:
Total  LUTs: 65 (5%)

Após rotear o projeto no iCEcube2, você encontrará o .bin arquivo no led_breathing_Implmnt\sbt\outputs\bitmap pasta dentro do Lattice_iCEcube2_proj diretório do projeto.

Você pode usar o software Lattice Diamond Programmer Standalone para programar o FPGA, conforme mostrado no manual do usuário do iCEstick. Foi o que fiz, e a animação Gif abaixo mostra o resultado. A intensidade da luz do LED oscila com um padrão de onda senoidal. Parece muito natural, e o LED parece estar “respirando” se você colocar um pouco de imaginação nele.


Considerações finais


Usar a RAM de bloco para armazenar valores de seno pré-calculados é bastante simples. Mas existem algumas limitações que tornam esse método adequado apenas para ondas senoidais com resoluções X e Y limitadas.

A primeira razão que vem à mente é o limite de 32 bits para valores inteiros que discuti anteriormente. Tenho certeza de que você pode superar esse problema calculando o valor do seno de forma mais inteligente, mas isso não é tudo.

Para cada bit com o qual você estende o endereço da ROM, você está dobrando o uso da RAM. Se você precisar de alta precisão no eixo X, pode não haver RAM suficiente, mesmo em um FPGA maior.

Se o número de bits usados ​​para os valores de seno do eixo Y exceder a profundidade de RAM nativa, a ferramenta de síntese usará RAMs ou LUTs adicionais para implementar a ROM. Isso consumirá mais do seu orçamento de recursos à medida que você aumentar a precisão Y.

Em teoria, só precisamos armazenar um quadrante da onda senoidal. Portanto, você poderia se safar com um quarto do uso de RAM se usasse uma máquina de estado finito (FSM) para controlar a leitura da ROM. Teria que inverter o quadrante seno para todas as quatro permutações dos eixos X e Y. Então, você pode construir uma onda senoidal completa a partir de um único quadrante armazenado na RAM do bloco.

Infelizmente, é difícil fazer com que todos os quatro segmentos se unam sem problemas. Duas amostras iguais nas juntas na parte superior e inferior da onda senoidal distorcem os dados criando pontos planos na onda senoidal. A introdução de ruído anula o propósito de armazenar apenas o quadrante para melhorar a precisão da onda senoidal.

VHDL

  1. Como criar um modelo CloudFormation usando AWS
  2. Como criar UX sem Fricção
  3. Como criar uma lista de strings em VHDL
  4. Como criar um testbench orientado por Tcl para um módulo de bloqueio de código VHDL
  5. Como inicializar a RAM do arquivo usando TEXTIO
  6. Como criar um testbench de autoverificação
  7. Como criar uma lista vinculada em VHDL
  8. Como criar uma matriz de objetos em Java
  9. Como os profissionais médicos estão usando a fabricação digital para criar modelos anatômicos de última geração
  10. Como chamar um Bloco Funcional de um Cliente OPC UA usando um Modelo de Informação