Gerador de sinais DIY – Mini Wave_Gen Parte 02 3

Nesta segunda parte do artigo sobre o Gerador de Sinais Mini Wave_Gen, explicaremos mais a fundo o projeto em si e o porquê das escolhas realizadas. Caso tenha chegado até aqui sem ler a primeira parte do artigo, encorajo-o a acessá-la neste link, onde você encontrará uma introdução sobre o projeto, seu circuito e também o código desenvolvido.

Para relembrar, a seguir estão os principais materiais utilizados neste projeto do gerador de sinais.

Principais materiais utilizados

O Projeto do “mini Wave_Gen

Nosso gerador de sinais “mini Wave_Gen” (nome carinhoso) tem as seguintes características

  • DAC de 12bit com interface i2c (4096 nível possíveis)
  • Sample rate: 16, 32, 64 ou 128 amostras por ciclo
  • Nível de tensão na saída analógica: 0 a 5V
  • Frequência do sinal: de DC até 568 Hz (Dependente da amostragem)
  • Formas de onda: Senoidal, Rampa, Quadrada e DC
  • Parâmetros configuráveis: Frequência, amplitude (Vpp), Tensão offset e quantidade de amostras

O cérebro do instrumento é um simples Arduino Nano. Achei interessante essa abordagem por se tratar de uma placa bastante tradicional e barata, permitindo que todos possam desenvolver este projeto.

Os parâmetros são apresentados através de um display OLED e a configuração dos parâmetros é realizada através de uma chave push-button e um encoder rotativo. Além disso a serial envia para o console parâmetros relacionados às formas de onda calculadas.

Devido às limitações do hardware (Arduino nano), sempre que o modo de configuração é ativado a forma de onda deixa de ser reproduzida, mas na prática não é algo que interfira muito no uso.

Para converter os valores computados pelo algoritmo em sinais analógicos utilizamos um conversor chamado DAC, ou Digital-to-Analog Converter. Este componente faz o caminho inverso do que o tradicional ADC faz na entradas analógicas de um microcontrolador, como o ATmega 328p presente no Arduino Nano/Uno. Nosso DAC tem 12 bits, o que significa 4096 níveis diferentes de saída.

Alimentando nosso DAC com 5 V teremos aproximadamente 1,22 mV/bit de resolução (menor incremento possível).

1,22 mV/bit de resolução (menor incremento possível)
1,22 mV/bit de resolução (menor incremento possível)

Para que a saída analógica do nosso DAC tenha a forma esperada é necessário que a alimentação do circuito seja bastante estável, então nada de utilizar a alimentação onboard da placa, principalmente se ela estiver sendo alimentada pela USB. Além da tensão ser menor que os 5V esperados, o sinal tende a flutuar bastante de acordo com a carga. Para resolver este problema utilizaremos um regulador de tensão externo, e pela praticidade indico este módulo ajustável para protoboard.

A alimentação do circuito do gerador de sinais afetará diretamente o comportamento do DAC4725, que utiliza a própria alimentação como referência de tensão. Note também que utilizamos apenas alimentação positiva (fonte simples, não simétrica), desta maneira devemos ter em mente que o sinal gerado pelo DAC será apenas positivo, ou seja, nossa forma de onda terá sempre um offset dc.

A figura abaixo apresenta o comportamento de uma onda quadrada gerada com o nosso mini Wave_Gen, note as variações no nível alto da onda quando alimentada pela USB (onda superior) e a ausência das variações quando alimentado pelo módulo ajustável (onda inferior). Note também a diferença da tensão máxima, que ficou na casa dos 4,1 V para a USB e aproximadamente 5 V para a alimentação regulada.

Onda quadrada

O nosso DAC será atualizado n vezes por segundo de acordo com o que for configurado pelo usuário. O intervalo entre cada atualização depende da frequência do sinal escolhido e também da quantidade de amostras por ciclo, ou seja, quanto maior a frequência e maior a quantidade de amostras, menor será o intervalo de atualização.

Time

Uma das formas mais tradicionais (e precisas) de se realizar essas chamadas periódicas é utilizando-se de algum timer interno presente no microcontrolador. Para o Arduino Nano/Uno existem inclusive bibliotecas que facilitam a configuração deste periférico. Infelizmente devido ao ao prescaler do TIMER1 interferir na i2c e também incompatibilidades da biblioteca utilizada para controle do DAC, não foi possível a adoção dessa abordagem de uma maneira mais simples.

Mas então, como contornar o problema? Talvez utilizando a tradicional função “delay( )”? Não, mas utilizando outro recurso bastante interessante e versátil, a função “micros( )”. Essa função retorna uma variável uint32_t com a contagem de tempo em microsegundos. Chamando esta função no momento A e depois chamando-a novamente no momento B, serão apresentados dois valores diferentes. A diferença “B-A” será o tempo decorrido em microsegundos, bastante útil não!?

E como o algoritmo sabe o momento de enviar uma nova amostra? Dentro do “loop( )”, através de uma verificação condicional, com um “if”, é possível saber se uma nova amostra deve ser enviada. O intervalo entre o envio das amostras é calculado através da fórmula apresentada anteriormente. É uma abordagem bastante simples e efetiva que permitiu contornar nossa limitação devido à biblioteca. O único inconveniente é que em frequências próximas limite superior será possível verificar um certo jitter e erro na frequência configurada pelo usuário, mas com percentual bastante pequeno.

E como são calculadas as amostras? Nosso gerador de sinais mini Wave_Gen trabalha com um buffer armazenando as amostras calculadas, ou seja, após você configurar as características da forma de onda, uma função será chamada para calcular o valor de cada amostra. Após o cálculo a forma de onda passa a ser reproduzida pelo DAC. Essa tática foi utilizada visto que calcular o valor de cada amostra no momento do envio iria impactar negativamente no limite máximo de frequência, mas é uma técnica possível se for utilizado um hardware mais “parrudo”.

Veja a seguir o resultado de uma onda senoidal com 16, 32, 64 e 128 pontos sendo sintetizada pelo mini Wave_Gen.

Onda senoidal

E quais as formas de onda disponíveis? conforme apresentado nas características do projeto mais acima, estão disponíveis ondas no formato Senoidal, Rampa, Quadrada e também DC, com o usuário podendo configurar seus valores de amplitude, frequência, tensão de offset e também quantidade de pontos por ciclo. A figura a seguir apresenta as formas de ondas disponíveis, geradas com 128 pontos (exceto o sinal DC).

Formas de Onda

Código do Gerador de Sinais

O código completo do nosso gerador de sinais foi apresentado na primeira parte do artigo, nesta segunda parte explicarei os principais pontos. Não se assuste com o tamanho do código, apesar de levemente extenso, está bem documentado, tornando fácil o entendimento das soluções abordadas.

Antes de comentar sobre o código devo relembrar que estamos utilizando algumas bibliotecas: “Adafruit_MCP4725”, responsável pelas funções para controle do DAC, “Adafruit GFX” e “Adafruit_SSD1306”, responsável pelas bibliotecas gráficas (formas geométricas, fontes) e pelo driver para displays OLED com o controlador SSD1306, neste caso, utilizando a comunicação i2c. Lembrando que as bibliotecas podem ser instaladas através do menu “gerenciar bibliotecas” (eu fiz assim), ou então baixando os “.zip” no github e instalando através do menu “Adicionar biblioteca .ZIP”.

Um comentário adicional deve ser feito sobre a biblioteca “Adafruit_MCP4725”. Ela configura a i2c em 400 kHz e isso limitava ainda mais a frequência máxima do sinal gerado. Para resolver este problema, siga o seguinte caminho “Documentos > Arduino > libraries > Adafruit_MCP4725”. Abra o arquivo “Adafruit_MCP4725.cpp”. Então modifique a seguinte linha:

TWBR = ((F_CPU / 400000L) - 16) / 2; // Set I2C frequency to 400kHz

Altere o divisor para 800000L e salve o arquivo.

TWBR = ((F_CPU / 800000L) - 16) / 2; // Set I2C frequency to 800kHz

essa alteração basicamente irá dobrar a frequência de clock da i2c, aumentando a taxa de tranferência de dados. Valores superiores não são indicados, levando ao travamento da aplicação, apesar de nosso DAC suportar frequências maiores, ou seja, chegamos em uma limitação do microcontrolador utilizado.

O fluxo básico do código é o seguinte.

  • No “setup( )” são configurados os periféricos necessários, como inicialização da serial, inicialização do DAC, inicialização do display OLED, configuração  interrupção para as chaves e atualização de parâmetros iniciais
  • No “loop( )” é realizado um pooling sequencial e de acordo com as variáveis de estado executa-se o estado RUNNING, CONFIGURING ou CALCULATING.

O sub loop “RUNNING” é executado de acordo com o intervalo entre as amostras a serem enviadas. Sempre que o tempo limite é atingido, uma nova amostra é enviada.

  if (SW1_state == RUNNING)
  {
    /*
     * runs the waveform stored in the buffer
     * - Use "micros ()" as reference because the DAC drive interferes with the i2c
     * - is not accurate as use a hw timer, but works
     * ------------------------------------------------------------------------------
     * executa a forma de onda calculada e armazenada no buffer
     * - utilizar como referência a função "micros( )" pois
     *   modificações no TIMER1 afetam a i2c (display e DAC)
     * - Não é tão preciso quanto um timer, mas funcionou bem
     */
    if ( (micros() - last_sample_time) >= sample_interval )
    {
      last_sample_time = micros();
      dac.setVoltage(wave[i & (samples_uint - 1)], false);
      i++;
    }
    else;
  }

Quando a chave SW1 é pressionada é gerada uma interrupção que incrementa uma variável de estado, que muda o estado para “CONFIGURING”. Neste loop é feita uma verificação periódica do encoder. Seu valor é utilizado para incrementar ou decrementar o parâmetro escolhido. A chave SW2 (botão do próprio encoder, ou outro externo caso preferir) será responsável por alternar entre os parâmetros, permitindo alterar seus valores.

O loop “CONFIGURING” é limitado a executar a atualização do display OLED a cada 500ms evitando que a leitura do encoder seja penalizada pela atualização do display.

Ao pressionar novamente SW1 a variável de estado é incrementada, levando a execução do sub loop “CALCULATING”. Neste ponto as variáveis configuradas anteriormente são passadas para a função que calcula as formas de onda. Essa função irá calcular o valor de cada amostra e armazenar no buffer mencionado anteriormente.

  else if (SW1_state == CALCULATING)
  {
    samples_uint = (uint16_t)wave_parameters[SAMPLES];
 
    if (waveform == SINE)
    {
      wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else if (waveform == RAMP)
    {
      wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else if (waveform == SQUARE)
    {
      wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else if (waveform == DC)
    {
      wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else;
 
    update_freq();
 
    // atualiza dos estados      
    SW1_state = RUNNING;
    SW2_state = 0;
  }
  else
  {
    SW1_state = 0;
  }

Ao término do cálculo a variável de estado será automaticamente zerada, levando ao estado “RUNNING” onde a forma de onda é reproduzida.

E como posso fazer a calibração do gerador de sinais mini Wave_Gen? A tensão de saída do módulo regulador tende a ser bastante próxima a 5V, mas com certa margem de tolerância. Caso você tenha disponível um multímetro, aconselho a medir a tensão fornecida pelo módulo e substituir este valor na variável “Vref” (logo no começo do código). Essa constante interfere nos cálculos internos, então quanto mais próxima do valor real, mais exata será a tensão na saída do DAC. Caso não tiver um multímetro em mãos, não tem problema, mantenha o valor 5 V por padrão.

Note que com o dispositivo devidamente calibrado pode-se alcançar uma exatidão bastante interessante. Neste caso, é gerado um sinal DC de 1 V na saída do DAC, note que a medição por dois multímetros é bastante coerente e com ótima exatidão.

Gerador de Sinais

Desta maneira se torna uma ferramenta bastante interessante para calibrar, por exemplo, seus projetos envolvendo conversores ADC.

A seguir apresento um pequeno vídeo demonstrando o funcionamento do gerador de sinais mini Wave_Gen com as formas de onda geradas.

Este foi um projeto bastante interessante de desenvolver, permitindo abusar um pouco das capacidades de um dos Arduinos mais tradicionais. Fica a ideia de você tentar migrar este projeto para plataformas mais potentes e com isso conseguir melhor desempenho, alcançando maiores frequências mesmo com um elevado número de pontos por ciclo.

Encorajo-o também a tentar contribuir com este projeto open-source, seja com comentários, correção de bugs ou adição de novas funcionalidades. Quem ganha com isso é a comunidade maker.

Os principais arquivos deste projeto estão disponíveis no meu Github, onde você também poderá enviar seus pull-requests.

Gostou do projeto do Gerador de Sinais DIY Mini Wave_Gen? Deixe seu comentário logo abaixo. 

Faça seu comentário

Acesse sua conta e participe

3 Comentários

  1. Olá!
    Achei interessante o artigo. Mas gostaria de informações de como você conseguiu visualizar essas formas de onda resultante do projeto. Você utilizou algum software?
    []’s
    Erik

  2. Parabéns Haroldo, muito legal esse projeto, principalmente para mim que não sei nada de eletrônica ou mesmo de arduino. Sou de TI e estou começando a dar uma olhada. no código que você postou, acho que podia dar uma simplificada, como no caso especifico você não faz nada mais além de chamar a procedure, poderia simplificar assim:

    colocar isso:

    wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);

    no lugar disso:

    if (waveform == SINE)
    {
    wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else if (waveform == RAMP)
    {
    wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else if (waveform == SQUARE)
    {
    wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }
    else if (waveform == DC)
    {
    wave_calc(wave, samples_uint, wave_parameters[AMPLITUDE], wave_parameters[OFFSET], waveform);
    }

    claro que do jeito que está, fica bastante didático, caso se queira fazer algo mais em cada tipo, essa seria a melhor forma, mas para simplificar, trocar só por uma linha ficaria com mais performance.

    1. Obrigado pelo comentário Josemar, fico feliz que curtiu o projeto.

      Você está correto, todo aquele código pode ser substituído apenas pela chamada da função. Isso é um resquício da versão anterior a final onde eu utilizava uma função separada para cada forma de onda (senoide, rampa, quadra…). Para diminuir o tamanho compilado e também quantidade de linhas transformei em uma versão unificada, mas esqueci de limpar essa parte do código.

      Felizmente o impacto no desempenho é mínimo visto que essa função é chamada apenas quando alguma forma de onda é modificada, no processo de cálculo da forma de onda.