Introdução
Neste artigo vamos implementar um PWM em VHDL. PWM é a abreviação de Pulse Width Modulation ou Modulação por Largura de Pulso. Este circuito é de grande importância e muito utilizado no nosso domínio de estudo, aplicando-se por exemplo para: controle de potência de motores e outras cargas, fontes chaveadas, modulação de sinais como IrDA (Infrared Data Association) e outras aplicações.
A implementação digital deste circuito é vantajosa em relação à implementação analógica devido à diminuição de ruído, tendo em vista que o circuito digital é menos afetado pelas variações de tensão, corrente, temperatura, entre outros fatores. Além disso, tem-se uma grande flexibilidade de especificar a frequência de operação, embarcá-lo e reprogramá-lo.
Em contrapartida, existem algumas limitações como a escolha do Duty Cycle, devido a não se ter uma variação continua deste e sim discreta devido a sua dependência em relação ao relógio (clock).
Princípio de funcionamento do PWM
Em uma frequência fixa pode-se variar o ciclo ativo (Duty Cycle) da onda e com isso tem-se um valor médio de tensão e uma potência entregue à carga controlados pela escolha do Duty Cycle. O valor do ciclo ativo e a tensão média são determinados como segue:
Para a implementação deste projeto, deve-se pensar no relógio e com isso fazer uma análise discreta do PWM. Na figura abaixo pode-se ver um possível comportamento de um sinal PWM digital que nos ajuda a compreender o que deve ser feito no código.
Observando a figura 2 e considerando a entrada TIMER carregada com o valor 15 (isto é, “1111”) e a variável CONTADOR contando as bordas de subida do relógio, então pode-se escolher um valor do Duty Cycle. Desta forma o CONTADOR vai contar até chegar ao valor CYCLE OFF e alterará o estado do PWM para alto, e quando chegar ao valor do TIMER alterará o estado do PWM para baixo e recomeçará a contar de zero. Pode-se notar então que, se o CONTADOR é alterado a cada período do clock não é possível obter uma faixa contínua de 0% à 100% do Duty Cycle. Para ilustrar, a tabela abaixo mostra os possíveis valores do ciclo ativo para esse caso:
Tabela 1: Duty Cycles possíveis para um TIMER de 4 bits
Poderá surgir a seguinte pergunta: “Não existe Duty Cycle em 0%?” A resposta é sim! O ciclo ativo é nulo quando a entrada ENABLE for zero e não quando DUTY=0. Foi escolhido projetar desta maneira para obter um total aproveitando das escolhas do ciclos ativo.
Exemplo: Ao carregar o TIMER com 1 e o Duty Cycle com zero, teremos a saída do PWM igual a uma onda com frequência metade da frequência do relógio e Duty Cycle 50%; se o Duty vale 1 a saída é 100%; se o ENABLE é ativo saída do PWM é zero. (Imagine agora esse mesmo exemplo com a condição em VHDL “se DUTY=0, saída igual a zero”).
Foi escolhido para o projeto um TIMER e um DUTY de 8 bits flexibilizando a escolha da frequência de trabalho e de Duty Cycles.
Diante do exposto, não é difícil ver que a frequência do PWM pode ser determinada pela fórmula abaixo, onde o valor do TIMER é somado mais 1, pois o TIMER começa em zero, logo conta-se mais 1 pulso do clock.
Circuito Prescaler
Para melhorar a flexibilidade da frequência do PWM foi implementado o circuito para dividir a frequência do clock, circuito denominado PRESCALER. Foi escolhida a possibilidade de escolher três PRESCALER’s 1, 4 e 16, que são, respectivamente, a frequência do clock original, a frequência dividida por 4 e a frequência dividida por 16. E assim a frequência do PWM pode ser reescrita como segue:
O esquema abaixo mostra como foi pensado e descrito o código VHDL. Pode-se ver que se o reset não estiver ativo, todos os sinais de clock estão disponíveis no MUX 4:1, onde a entrada SEL_PR de 2 bits seleciona o clock desejado na saída do MUX. Esta entrada determina o PRESCALER da seguinte maneira: SEL_PR = “00” implica PRESCALER = 1; SEL_PR = “01” implica PRESCALER = 4; SEL_PR = “10” implica PRESCALER = 16 e SEL_PR = “00” implica saída de clock zero (opção inválida).
VHDL PRESCALER
O código da implementação deste circuito foi feito como mostrado em seguida:
------------------------------ -- MODULO PRESCALER -- ------------------------------ -- -- EMBARCADOS.COM -- -- Ao usar o projeto dê os créditos! -- by Gabriel Villanova library IEEE; use IEEE.std_logic_1164.all; use IEEE.numeric_std.all; entity PRESCALER is port ( s_CLK : in std_logic; s_RST : in std_logic; s_SEL_PR : in std_logic_vector (1 downto 0); OUT_CLK : out std_logic); end PRESCALER; architecture RTL of PRESCALER is signal CLK4 : std_logic; signal CLK16 : std_logic; begin process(s_CLK,s_RST) variable CONT4 : integer range 0 to 2; variable CONT16 : integer range 0 to 8; begin if( s_RST = '1' ) then CLK4 <= '0'; CLK16 <= '0'; CONT4 := 0; CONT16 := 0; elsif( s_CLK = '1' and s_CLK'EVENT ) then -- GERAR AS DIVISOES DE FREQUENCIA /4 e /16 -- OBS: VARIAVEL DO TIPO INTEIRA É SEQUENCIAL, OU SEJA, A ORDEM DESSE CÓDIGO IMPORTA CONT4 := CONT4 + 1; CONT16 := CONT16 + 1; if( CONT4 = 2 ) then CLK4 <= not CLK4; CONT4 := 0; end if; if( CONT16 = 8 ) then CLK16 <= not CLK16; CONT16 := 0; end if; end if; end process; -- MUX 4:1 -- SELECIONA O CLK ESCOLHIDO with s_SEL_PR select OUT_CLK <= s_CLK and (not s_RST) when "00", CLK4 when "01", CLK16 when "10", '0' when "11", '0' when others; end RTL;
TESTBENCH PRESCALER
A seguir tem-se o código do testbench feito usando um clock de entrada em 1MHz.
----------------------------------
-- TEST BENCH PRESCALER --
----------------------------------
--
-- EMBARCADOS.COM
--
-- Ao usar o projeto dê os créditos!
-- by Gabriel Villanova
library IEEE;
use IEEE.std_logic_1164.all;
entity TB_PRESCALER is end TB_PRESCALER;
architecture RTL of TB_PRESCALER is
component PRESCALER
port ( s_CLK : in std_logic;
s_RST : in std_logic;
s_SEL_PR : in std_logic_vector (1 downto 0);
OUT_CLK : out std_logic);
end component;
signal sig_CLK : std_logic := '0';
signal sig_RST : std_logic;
signal sig_PR : std_logic_vector (1 downto 0);
signal sig_OCLK : std_logic;
begin
U0: PRESCALER port map (sig_CLK,sig_RST,sig_PR,sig_OCLK );
sig_CLK <= not sig_CLK after 500 ns; -- CLK = 1M Hz
sig_RST <= '1', '0' after 25 ns;
process
begin
wait for 25 ns;
-- CLK/1
sig_PR <= "00";
wait for 16000 ns;
-- CLK/4
sig_PR <= "01";
wait for 16000 ns;
-- CLK/16
sig_PR <= "10";
wait for 32000 ns;
-- CLK=0
sig_PR <= "11";
wait for 16000 ns;
end process;
end RTL;
Abaixo tem-se as formas de ondas encontradas, onde pode-se validar o bom funcionamento do nosso circuito:
CIRCUITO PWM
O esquema do projeto final é mostrado abaixo, onde tem-se as entradas TIMER (8 bits), DUTY (8 bits), SEL_PR (2 bits), ENABLE CLK e RST (1 bit), e logicamente a saída do PWM (1 bit). Com o embasamento teórico feito até aqui, pode-se entender/descrever o código facilmente.
VHDL
O código abaixo mostra o que foi feito:
------------------------------ -- MODULO PRESCALER -- ------------------------------ -- -- EMBARCADOS.COM -- -- Ao usar o projeto dê os créditos! -- by Gabriel Villanova library IEEE; use IEEE.std_logic_1164.all; use IEEE.std_logic_unsigned.all; entity PWM is port ( CLK : in std_logic; RST : in std_logic; ENABLE : in std_logic; SEL_PR : in std_logic_vector (1 downto 0); TIMER : in std_logic_vector (7 downto 0); DUTY : in std_logic_vector (7 downto 0); PWM_OUT : out std_logic); end entity; architecture RTL of PWM is -- Fclk -- Fpwm = ------------------------ -- (TIMER + 1)*PRESCALER -- -- TIMER 8bits -> 0 à 255 -- PRESCALER -> 1, 4 ou 16 -- -- DUTY X% = Duty/TIMER*100% component PRESCALER port ( s_CLK : in std_logic; s_RST : in std_logic; s_SEL_PR : in std_logic_vector (1 downto 0); OUT_CLK : out std_logic); end component; signal s_OUT_CLK : std_logic; signal CYCLE_OFF : std_logic_vector (7 downto 0); signal s_PWM_OUT : std_logic; signal CONT : std_logic_vector (7 downto 0); begin U0 : PRESCALER port map (CLK,RST,SEL_PR,s_OUT_CLK); CYCLE_OFF <= TIMER - DUTY - '1'; -- CASO ESPECIAL: with TIMER select PWM_OUT <= s_OUT_CLK and ENABLE and (not RST) when "00000000", s_PWM_OUT when others; process(s_OUT_CLK,RST) begin if( RST = '1' ) then s_PWM_OUT <= '0'; CONT <= "00000000"; elsif( s_OUT_CLK = '1' and s_OUT_CLK'EVENT ) then if( ENABLE = '1' ) then -- CONTA PULSOS DO CLOCK CONT <= CONT + '1'; if ( DUTY >= TIMER ) then s_PWM_OUT <= '1'; CONT <= "00000000"; elsif ( CONT = CYCLE_OFF ) then s_PWM_OUT <= '1'; elsif ( CONT = TIMER ) then s_PWM_OUT <= '0'; CONT <= "00000000"; end if; else s_PWM_OUT <= '0'; CONT <= "00000000"; end if; end if; end process; end RTL;
TESTBENCH
Foram feitos os testes, porém devido à gama muito grande de resultados, vamos fazer um exemplo prático para demonstração.
Iremos usar o circuito para gerar um PWM em 38kHz, frequência geralmente usada para modulação de controles remotos em infravermelho, TV, DVD, etc. Portanto, precisamos escolher o PRESCALER e o TIMER.
Considerando um clock de 1MHz e usando o PRESCALER = 1, achamos o valor do TIMER =25.31, seguindo a formula:
Da mesma maneira se usarmos PRESCALER = 4, achamos o valor do TIMER = 5.57 e para PRESCALER = 16 temos um TIMER = 0.64. Dessa forma, a melhor escolha é o PRESCALER = 1 e carregar o TIMER com 25 (maior resolução).
Como o TIMER deve ser arredondado para um inteiro mais próximo temos um erro na frequência. Ou seja:
De qualquer maneira, mesmo não tendo a frequência perfeitamente em 38kHz, esse resultado é bem aceitável para aplicações práticas, como a do nosso exemplo de modular sinal de controle remoto nessa frequência.
O test bench feito para esse exemplo é mostrado abaixo, onde foi variado o Duty Cycle a cada período do PWM, exemplificando todos os casos de Duty.
-- MODULO PRESCALER -- -- Ao usar o projeto dê os créditos! -- by Gabriel Villanova library IEEE; use IEEE.std_logic_1164.all; use IEEE.std_logic_unsigned.all; entity TB_PWM is end TB_PWM; architecture RTL of TB_PWM is component PWM port ( CLK : in std_logic; RST : in std_logic; ENABLE : in std_logic; SEL_PR : in std_logic_vector (1 downto 0); TIMER : in std_logic_vector (7 downto 0); DUTY : in std_logic_vector (7 downto 0); PWM_OUT : out std_logic); end component; signal sig_CLK : std_logic := '0'; signal sig_RST : std_logic; signal sig_ENABLE : std_logic; signal sig_SEL_PR : std_logic_vector (1 downto 0); signal sig_TIMER : std_logic_vector (7 downto 0); signal sig_DUTY : std_logic_vector (7 downto 0) := "00000000"; signal sig_PWM_OUT : std_logic; begin M1: PWM port map (sig_CLK,sig_RST,sig_ENABLE,sig_SEL_PR,sig_TIMER,sig_DUTY,sig_PWM_OUT); sig_CLK <= not sig_CLK after 500 ns; -- Fclk = 1M Hz. sig_RST <= '1','0' after 750 ns; sig_ENABLE <= '0','1' after 1250 ns; process variable i : integer range 0 to 255; begin wait for 1250 ns; sig_SEL_PR <= "00"; sig_TIMER <= "00011001"; for i in 0 to 25 loop sig_DUTY <= "00000000" + i; wait for 25 us; end loop; end process; end RTL;
Pode-se ver na figura gerada o círculo vermelho mostrando o período de PWM esperado. Além disso, a modulação do sinal.
INTERESSANTE
Abaixo é mostrado um esquema de PWM retirado do datasheet do microcontrolador PIC16F877A. Pode-se ver registradores para Duty Cycle, o TIMER (TMR2) e comparadores, que de modo geral é similar ao que foi feito nesse projeto.
Dessa forma podemos ter uma ideia do que acontece “por trás” do código C, quando necessitamos de usar uma saída PWM de um microcontrolador por exemplo, ganhando confiança ao programá-lo e ficando sensíveis aos possíveis erros de programação. Vamos estudar HDL!
Espero que tenham gostado do artigo! Até a próxima!










@Grabiel
tentei rodar o seu código, e apareceu o seguinte erro:
Error (10533): VHDL Wait Statement error at TB_PWM.vhd(40): Wait Statement must contain condition clause with UNTIL keyword
a linha 40 no código é: wait for 1250 ns;
O que pode ser isso?
Sou novo no assunto, porém estou me aprofundando o máximo que posso. Minha dificuldade foi em gerar um pwm de 50 KHz, ao compilar da um erro do TimeQuest e de design. Alguém poderia me dar algumas dicas?
Pode ser um exercício interessante transformar esse testbench em um testbench automatizado.
Ótima matéria! Trabalho com HDL desde o começo da minha graduação e continuo utilizando no meu mestrado e acho sempre interessante quando esse tipo de linguagem ganha espaço em um site grande como este! Parabéns aos envolvidos e que venham mais matérias como está!
Eu utilizava muito o MyHDL, nao utilizo mais pois nao estou trabalhando com FPGA no momento.
Recomendo de mais.
Obrigado por compartilhar os artigos André.
É interessante a abordagem da utilização de variáveis na solução do algoritmo do código gray. Eu, particularmente, tenho receio do que o compilador interpreta e transforma em circuito. Concordo que os compiladores estão muito mais eficientes hoje em dia e que talvez não fosse necessário se preocupar com isso, mas a pulga sempre fica atrás da orelha. Não conheço o MyHDL. Vocês utilizam essa linguagem?
Usar variaveis de forma correta eh excelente, nao tenham medo da ferramenta. Otimos artigos: https://www.jandecaluwe.com/hdldesign/thinking-software-rtl.html https://www.jandecaluwe.com/hdldesign/the-case-for-a-better-hdl.html Variables are one of the best understood programming concepts, and they are supported by both Verilog and VHDL. Nevertheless, even though mainstream HDL design is more than 20 years old, HDL designers still haven’t figured out how to use variables properly. Instead of teaching how to use variables, mainstream Verilog guidelines ban them altogether (in clocked processes). Thus, the confusion is “solved” by brute force amputation. In reality, local variables work just fine in clocked processes and synthesis supports them without a problem. They… Leia mais »
Caio,
Muito obrigado pelo feedback.
Eu estou de acordo, quando
escrevemos sinais ao invés de variáveis podemos ter mais controle do que
será sintetizado, além disso, meio que repetindo o que você já disse,
não temos necessidade de utilizar variáveis, afinal de contas tudo é
bit.
Bom, como o artigo está a nível de “tutorial” escolhi fazer dessa forma pra explorar a linguagem.
Estou aberto a discussões sobre o assunto!
Parabéns pelo artigo Gabriel.
Só queria deixar uma observação quanto a utilização de variáveis em processos deve ser utilizada com cautela. Algumas ferramentas de síntese podem inferir um circuito não desejável.
Eu, particularmente, evito o máximo a utilização de variáveis para deixar o código RTL mais independente do tipo de software de síntese. Até o momento não tive que desenvolver algum código em que fui obrigado a utilizar variáveis. Aproveito para abrir uma discussão sobre a utilização de sinais e variáveis em processos de VHDL.
Quais são as opniões de vocês?
Como colocado pelo @andrecas:disqus, não vejo qualquer problema em usar variáveis desde que se saiba descrever o circuito. Utilização de variáveis no código não vai torná-lo mais dependente IMHO. Uma coisa que me irrita bem mais é o impacto do circuito de reset na hora de portar entre Xilinx e Altera por exemplo. Mas mesmo isso no fim das contas não deve ser uma barreira pra portabilidade entre os dois principais players.
Obrigado por compartilhar sua opinião Euripedes! Um artigo sobre metodologias de reset em projetos de circuitos digitais (síncrono e assíncrono) seria bem interessante!
https://embarcados.com.br/arquitetura-reset-fpga/
😛
com certeza o Euripedes poderia escrever mais ( e melhor )
Desculpe-me o desconhecimento do artigo. Acho que estou precisando aprender a fazer buscas melhores no site. 😎