DevBoy: criando um gerador de sinal

Olá amigos!

Nos artigos anteriores, falei sobre o meu projeto e sua parte do software . Neste artigo, mostrarei como criar um gerador de sinal simples para 4 canais - dois canais analógicos e dois canais PWM.



Canais analógicos


O microcontrolador STM32F415RG incorpora um conversor DAC de 12 bits (digital para analógico) em dois canais independentes, o que permite gerar sinais diferentes. Você pode carregar dados diretamente nos registros do conversor, mas isso não é muito adequado para gerar sinais. A melhor solução é usar uma matriz na qual gerar uma onda do sinal e executar o DAC com um gatilho do timer e do DMA. Alterando a frequência do temporizador, você pode alterar a frequência do sinal gerado.

As formas de onda " clássicas " incluem: sinusoidal, sinuoso, triangular e dente de serra.

imagem

A função de gerar essas ondas no buffer é a seguinte
// ***************************************************************************** // *** GenerateWave ******************************************************** // ***************************************************************************** Result Application::GenerateWave(uint16_t* dac_data, uint32_t dac_data_cnt, uint8_t duty, WaveformType waveform) { Result result; uint32_t max_val = (DAC_MAX_VAL * duty) / 100U; uint32_t shift = (DAC_MAX_VAL - max_val) / 2U; switch(waveform) { case WAVEFORM_SINE: for(uint32_t i = 0U; i < dac_data_cnt; i++) { dac_data[i] = (uint16_t)((sin((2.0F * i * PI) / (dac_data_cnt + 1)) + 1.0F) * max_val) >> 1U; dac_data[i] += shift; } break; case WAVEFORM_TRIANGLE: for(uint32_t i = 0U; i < dac_data_cnt; i++) { if(i <= dac_data_cnt / 2U) { dac_data[i] = (max_val * i) / (dac_data_cnt / 2U); } else { dac_data[i] = (max_val * (dac_data_cnt - i)) / (dac_data_cnt / 2U); } dac_data[i] += shift; } break; case WAVEFORM_SAWTOOTH: for(uint32_t i = 0U; i < dac_data_cnt; i++) { dac_data[i] = (max_val * i) / (dac_data_cnt - 1U); dac_data[i] += shift; } break; case WAVEFORM_SQUARE: for(uint32_t i = 0U; i < dac_data_cnt; i++) { dac_data[i] = (i < dac_data_cnt / 2U) ? max_val : 0x000; dac_data[i] += shift; } break; default: result = Result::ERR_BAD_PARAMETER; break; } return result; } 

Na função, você precisa passar um ponteiro para o início da matriz, o tamanho da matriz, o valor máximo e a forma de onda desejada. Após a chamada, o array será preenchido com amostras de uma onda da forma desejada e você poderá iniciar o timer para carregar periodicamente o novo valor no DAC.

O DAC neste microcontrolador tem uma limitação: o tempo típico de acomodação (o tempo de carregamento de um novo valor no DAC e quando ele aparece na saída ) é de 3 ms. Mas nem tudo é tão simples - esse tempo é máximo, ou seja, mude de mínimo para máximo e vice-versa. Ao tentar retirar o meandro, essas frentes desarrumadas são muito claramente visíveis:



Se uma onda sinusoidal for emitida, a obstrução das frentes não será mais tão perceptível devido à forma de onda. No entanto, se a frequência é aumentada, o sinal sinusoidal se torna triangular e, com um aumento adicional, a amplitude do sinal diminui.

Geração a 1 KHz ( 90% de amplitude ):



Geração a 10 KHz ( 90% de amplitude ):



Geração a 100 KHz ( 90% de amplitude ):



As etapas já estão visíveis - porque novos dados são carregados no DAC na frequência de 4 MHz.

Além disso, a borda posterior do sinal dente de serra é confusa e, por baixo do sinal, não atinge o valor ao qual deveria. Isso ocorre porque o sinal não tem tempo para atingir o nível baixo especificado e o software está carregando novos valores

Geração a 200 KHz ( 90% de amplitude ):



Aqui você já pode ver como todas as ondas se transformaram em um triângulo.

Canais digitais


Com os canais digitais, tudo é muito mais simples - em quase todos os microcontroladores, existem temporizadores que permitem emitir um sinal PWM para as saídas do microcontrolador. É melhor usar um cronômetro de 32 bits - nesse caso, não é necessário contar o pré-cronômetro, basta carregar o período em um registro e carregar o ciclo de trabalho necessário em outro registro.

Interface do usuário


Foi decidido organizar a interface do usuário em quatro retângulos, cada um com uma imagem do sinal de saída, frequência e amplitude / ciclo de serviço. Para o canal atualmente selecionado, os dados de texto são exibidos em branco e os demais em cinza.



Decidiu-se controlar os codificadores: o esquerdo é responsável pela frequência e o canal selecionado atual ( muda quando o botão é pressionado ), o direito é responsável pelo ciclo de amplitude / serviço e forma de onda ( muda quando o botão é pressionado ).

Além disso, o suporte à tela de toque é implementado - quando você clica em um canal inativo, ele se torna ativo; quando você clica em um canal ativo, a forma de onda muda.

Obviamente, o DevCore é usado para fazer tudo isso. O código para inicializar a interface do usuário e atualizar os dados na tela é semelhante a este:

Estrutura contendo todos os objetos da interface do usuário
  // ************************************************************************* // *** Structure for describes all visual elements for the channel ***** // ************************************************************************* struct ChannelDescriptionType { // UI data UiButton box; Image img; String freq_str; String duty_str; char freq_str_data[64] = {0}; char duty_str_data[64] = {0}; // Generator data ... }; // Visual channel descriptions ChannelDescriptionType ch_dsc[CHANNEL_CNT]; 
Código de inicialização da interface do usuário
  // Create and show UI int32_t half_scr_w = display_drv.GetScreenW() / 2; int32_t half_scr_h = display_drv.GetScreenH() / 2; for(uint32_t i = 0U; i < CHANNEL_CNT; i++) { // Generator data ... // UI data int32_t start_pos_x = half_scr_w * (i%2); int32_t start_pos_y = half_scr_h * (i/2); ch_dsc[i].box.SetParams(nullptr, start_pos_x, start_pos_y, half_scr_w, half_scr_h, true); ch_dsc[i].box.SetCallback(&Callback, this, nullptr, i); ch_dsc[i].freq_str.SetParams(ch_dsc[i].freq_str_data, start_pos_x + 4, start_pos_y + 64, COLOR_LIGHTGREY, String::FONT_8x12); ch_dsc[i].duty_str.SetParams(ch_dsc[i].duty_str_data, start_pos_x + 4, start_pos_y + 64 + 12, COLOR_LIGHTGREY, String::FONT_8x12); ch_dsc[i].img.SetImage(waveforms[ch_dsc[i].waveform]); ch_dsc[i].img.Move(start_pos_x + 4, start_pos_y + 4); ch_dsc[i].box.Show(1); ch_dsc[i].img.Show(2); ch_dsc[i].freq_str.Show(3); ch_dsc[i].duty_str.Show(3); } 
Código de atualização da tela
  for(uint32_t i = 0U; i < CHANNEL_CNT; i++) { ch_dsc[i].img.SetImage(waveforms[ch_dsc[i].waveform]); snprintf(ch_dsc[i].freq_str_data, NumberOf(ch_dsc[i].freq_str_data), "Freq: %7lu Hz", ch_dsc[i].frequency); if(IsAnalogChannel(i)) snprintf(ch_dsc[i].duty_str_data, NumberOf(ch_dsc[i].duty_str_data), "Ampl: %7d %%", ch_dsc[i].duty); else snprintf(ch_dsc[i].duty_str_data, NumberOf(ch_dsc[i].duty_str_data), "Duty: %7d %%", ch_dsc[i].duty); // Set gray color to all channels ch_dsc[i].freq_str.SetColor(COLOR_LIGHTGREY); ch_dsc[i].duty_str.SetColor(COLOR_LIGHTGREY); } // Set white color to selected channel ch_dsc[channel].freq_str.SetColor(COLOR_WHITE); ch_dsc[channel].duty_str.SetColor(COLOR_WHITE); // Update display display_drv.UpdateDisplay(); 

Uma implementação interessante do clique do botão é implementada ( é um retângulo sobre o qual os elementos restantes são desenhados ). Se você analisou o código, deveria ter notado isso: ch_dsc [i] .box.SetCallback (& ​​Callback, this, nullptr, i); chamado em um loop. Este é o trabalho da função de retorno de chamada que será chamada quando o botão for pressionado. Os seguintes são transferidos para a função: o endereço da função estática da função estática da classe, o ponteiro this e dois parâmetros do usuário que serão passados ​​para a função de retorno de chamada - um ponteiro ( não usado neste caso - nullptr é passado ) e um número ( número do canal é transmitido ).

Da bancada da universidade, lembro-me do postulado: " Funções estáticas não têm acesso a alunos não estáticos ". Então isso não é verdade . Como uma função estática é membro de uma classe, ela tem acesso a todos os membros da classe se tiver um link / ponteiro para essa classe. Agora, dê uma olhada na função de retorno de chamada:

 // ***************************************************************************** // *** Callback for the buttons ********************************************* // ***************************************************************************** void Application::Callback(void* ptr, void* param_ptr, uint32_t param) { Application& app = *((Application*)ptr); ChannelType channel = app.channel; if(channel == param) { // Second click - change wave type ... } else { app.channel = (ChannelType)param; } app.update = true; } 

Na primeira linha desta função, ocorre " mágica ", após o qual você pode acessar qualquer membro da classe, incluindo os privados.

A propósito, essa função é chamada em outra tarefa ( renderizando a tela ); portanto, dentro dessa função, você precisa cuidar da sincronização. Nesse projeto simples de " duas noites ", eu não fiz isso, porque nesse caso em particular não é essencial.

Código-fonte do gerador carregado no GitHub: https://github.com/nickshl/WaveformGenerator
O DevCore agora está alocado para um repositório separado e incluído como um submódulo.

Bem, por que preciso de um gerador de sinal, ele será publicado no próximo ( ou em um dos seguintes ) artigos.

Source: https://habr.com/ru/post/pt425409/


All Articles