Criando Efeito LED Respiratório Usando Onda Senoidal da Block RAM
Percebi que muitos dos gadgets que comprei nos últimos anos mudaram de LED piscando para LED respirando. A maioria dos gizmos 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 recarregar, 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 nos velhos tempos. É mais como um efeito de pulsação analógico com intensidade variando continuamente.
A animação Gif abaixo mostra meu mouse Logitech usando este efeito para indicar que está carregando a bateria. Chamo este efeito de LED respiratório porque o padrão de intensidade da luz é semelhante em velocidade e aceleração ao ciclo de respiração humano. Parece tão natural porque o ciclo de iluminação segue um padrão de onda senoidal.

Este artigo é uma continuação do post da semana passada sobre modulação por largura de pulso (PWM). Hoje, vamos usar o módulo PWM, o contador 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 block RAM
Embora seja possível gerar uma onda senoidal usando primitivos de processamento de sinal digital (DSP) no FPGA, uma maneira mais direta é armazenar os pontos de amostra na block RAM. Em vez de calcular os valores durante a 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 3x3 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 é 0 a 7.

A função trigonométrica seno 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 até, mas não incluindo 2π. O valor do seno para 2π é o mesmo que o seno para zero. A ilustração acima mostra este conceito. Estamos armazenando valores de seno de zero a \frac{7\pi}{4}, que é o último passo uniformemente espaçado antes do círculo completo de 2π.
A lógica digital não pode representar valores reais, como o ângulo ou valores de seno, com precisão infinita. Esse é o caso em qualquer sistema computacional. Mesmo ao usar números de ponto flutuante de precisão dupla, é apenas uma aproximação. É assim que números binários funcionam, e nossa ROM de seno não é diferente.
Para obter o máximo dos bits de dados disponíveis, adicionamos um offset e uma escala aos valores de seno quando calculamos o conteúdo da ROM. O menor valor possível de seno de -1 mapeia para o valor de dados 0, enquanto o maior valor possível de seno de 1 se traduz em 2^{data\_bits-1}, como 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}}
Claro, este método não lhe dá um conversor universal de ângulo para valor de seno. Se isso é o que você quer, terá que usar lógica adicional ou primitivos DSP para escalar a entrada de endereço e a saída de dados. Mas para muitas aplicações, uma onda senoidal representada como inteiro sem sinal é boa o suficiente. E como veremos na próxima seção, é exatamente o que precisamos para nosso projeto de pulsação de LED.
Módulo ROM de Seno
O módulo ROM de seno apresentado neste artigo inferirá block RAM na maioria das arquiteturas FPGA. Considere combinar os genéricos de largura e profundidade com os primitivos de memória do seu FPGA alvo. Isso dará a melhor utilização de recursos. Você sempre pode consultar o projeto de exemplo Lattice se não tiver certeza de como usar a ROM de seno para um projeto real de FPGA.
Deixe seu e-mail no formulário abaixo para baixar os arquivos VHDL e projetos ModelSim / iCEcube2!
A Entidade
O código abaixo mostra a entidade do módulo ROM de seno. Ele contém duas entradas genéricas que permitem especificar a largura e profundidade da block RAM inferida. Estou usando especificadores de range nas constantes para prevenir overflow 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 clock, mas não reset porque primitivos RAM não podem ter reset. A entrada addr é onde atribuímos o valor do ângulo escalado (x), e a saída data é onde o valor de seno escalado aparecerá.
Declarações de Tipo
No topo da região declarativa do arquivo VHDL, declaramos um tipo e um subtipo para nosso objeto de armazenamento ROM. O subtipo addr_range é um range inteiro igual ao número de slots em nossa RAM, e o rom_type descreve um array 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, ainda não vamos declarar o sinal de armazenamento. Primeiro, precisamos definir a função que produzirá os valores de seno, que podemos usar para transformar a RAM em uma ROM. Temos que declará-la acima da declaração do sinal para que possamos usar a função para atribuir um valor inicial ao sinal de armazenamento ROM.
Note que estamos usando o genérico addr_bits como base para definir addr_range. É por isso que tive que especificar um valor máximo de 30 para addr_bits. Porque para valores maiores, o cálculo 2**addr_bits - 1 fará overflow. Inteiros VHDL são de 32 bits, embora isso esteja prestes a mudar com VHDL-2019, que usa inteiros de 64 bits. Mas por enquanto, temos que conviver com esta 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 a função init_rom que gera os valores de seno. Ela retorna um objeto rom_type, por isso temos que declarar o tipo primeiro, 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 for-loop para iterar sobre o range de endereços, e para cada slot da ROM, calculamos o valor de seno usando as fórmulas que descrevi anteriormente. E no final, retornamos a variável rom_v que agora contém todas as amostras de seno.
A conversão inteira na última linha do for-loop é a razão pela qual tive que restringir o genérico data_bits a 31 bits. Fará overflow para quaisquer comprimentos de bit maiores.
constant rom : rom_type := init_rom;
Abaixo da definição da função init_rom, declaramos o objeto rom como uma constante. Uma ROM é simplesmente uma RAM que você nunca escreve, então isso está perfeitamente bem. E agora é hora de usar nossa função. Chamamos init_rom para gerar os valores iniciais, como mostrado no código acima.
O Processo ROM
A única lógica no arquivo da ROM de seno é o processo bastante simples listado abaixo. Ele descreve uma block 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 Topo
Este design é uma continuação do projeto PWM que apresentei no post anterior do blog. Ele tem um módulo de reset, um módulo gerador PWM e um módulo contador de ciclos de clock free-running (contador 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 topo.

O código abaixo mostra a região declarativa do arquivo VHDL topo. No projeto PWM da semana passada, o objeto duty_cycle era um alias para os MSBs do contador cnt. Mas isso não funcionaria agora porque a saída da ROM de seno controlará o ciclo de trabalho, então o 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 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 de seno no módulo topo na listagem abaixo. Definimos a largura e profundidade da RAM para seguir o comprimento do contador interno do módulo PWM. A saída data da ROM controla a entrada duty_cycle do módulo PWM. O valor no sinal duty_cycle retratará um padrão de onda senoidal quando lermos os slots da 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 topo no ModelSim. Mudei a apresentação do sinal unsigned duty_cycle para formato analógico para que possamos observar a onda senoidal.

É a saída top-level led_5 que carrega o sinal PWM, que controla o LED externo. Podemos ver que a saída muda rapidamente quando o ciclo de trabalho está subindo ou descendo. Mas quando o ciclo de trabalho está no topo da onda senoidal, led_5 é um '1' estável. Quando a onda está na parte inferior da inclinação, a saída permanece brevemente em '0'.
Quer testar no seu computador? Insira seu endereço de e-mail no formulário abaixo para receber os arquivos VHDL e os projetos ModelSim e iCEcube2!
Implementando LED respiratório no FPGA
Usei o software Lattice iCEcube2 para implementar o projeto na placa FPGA iCEstick. Use o formulário acima para baixar o projeto e testar na sua placa se você tiver um iCEstick!
A listagem abaixo mostra o uso de recursos, conforme relatado pelo software Synplify Pro que vem com iCEcube2. Mostra que o projeto usa um primitivo de block RAM. Essa é nossa ROM de seno.
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 arquivo .bin na pasta led_breathing_Implmnt\sbt\outputs\bitmap dentro do diretório do projeto Lattice_iCEcube2_proj.
Você pode usar o software Lattice Diamond Programmer Standalone para programar o FPGA, como 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 nisso.

Considerações finais
Usar block RAM para armazenar valores de seno pré-calculados é bastante direto. Mas há algumas limitações que tornam este 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 que você pode superar este problema calculando o valor de seno de forma mais inteligente, mas não é só isso.
Para cada bit que você estende o endereço ROM, você dobra o uso de RAM. Se você precisa 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 no eixo Y exceder a profundidade nativa da RAM, a ferramenta de síntese usará RAMs adicionais ou LUTs para implementar a ROM. Isso consumirá mais do seu orçamento de recursos à medida que você aumenta 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 estados finitos (FSM) para controlar a leitura da ROM. Ela teria que inverter o quadrante do seno para todas as quatro permutações dos eixos X e Y. Então, você poderia construir uma onda senoidal completa a partir do único quadrante armazenado na block RAM.
Infelizmente, é difícil fazer todos os quatro segmentos se juntarem suavemente. Duas amostras iguais nas junções no topo e base da onda senoidal distorcem os dados criando pontos planos na onda senoidal. Introduzir ruído derrota o propósito de armazenar apenas o quadrante para melhorar a precisão da onda senoidal.
VHDL
- Crie Seu Primeiro Template AWS CloudFormation — Um Guia Prático
- Criando Experiências de Usuário sem Atrito: Um Guia de Design UX
- Criando Listas de Strings em VHDL: Boas Práticas e Exemplo
- Criando um Testbench Dirigido por Tcl para um Módulo Code-Lock VHDL
- Inicializando Block RAM de FPGA a partir de Arquivos de Texto Usando a Biblioteca TEXTIO do VHDL
- Como Construir um Testbench de Auto-Verificação em VHDL – Guia Prático
- Implementando Lista Encadeada Dinâmica em VHDL com Tipos Protegidos e Ponteiros de Acesso
- Criando Array de Objetos em Java: Guia Passo a Passo
- Manufatura Digital Revoluciona Modelagem Anatômica na Medicina Moderna
- Invocando Function Blocks PLC via Clientes OPC UA: Guia Prático