DevBoy: faire un générateur de signaux

Bonjour mes amis!

Dans des articles précédents, j'ai parlé de mon projet et de sa partie logicielle . Dans cet article, je vais vous expliquer comment créer un générateur de signal simple pour 4 canaux - deux canaux analogiques et deux canaux PWM.



Canaux analogiques


Le microcontrôleur STM32F415RG intègre un convertisseur DAC (numérique-analogique) 12 bits en deux canaux indépendants, ce qui permet de générer des signaux différents. Vous pouvez charger directement des données dans les registres du convertisseur, mais ce n'est pas très approprié pour générer des signaux. La meilleure solution consiste à utiliser une matrice dans laquelle générer une onde du signal, puis à exécuter le DAC avec un déclencheur provenant du temporisateur et du DMA. En modifiant la fréquence de la minuterie, vous pouvez modifier la fréquence du signal généré.

Les formes d'onde « classiques » comprennent: sinusoïdale, sinueuse, triangulaire et en dents de scie.

image

La fonction de génération de ces ondes dans le tampon est la suivante
// ***************************************************************************** // *** 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; } 

Vous devez passer un pointeur sur le début du tableau, la taille du tableau, la valeur maximale et la forme d'onde souhaitée dans la fonction. Après l'appel, le tableau sera rempli d'échantillons pour une onde de la forme requise et vous pouvez démarrer le chronomètre pour charger périodiquement la nouvelle valeur dans le DAC.

Le DAC de ce microcontrôleur a une limitation: le temps de stabilisation typique (le temps entre le chargement d'une nouvelle valeur dans le DAC et quand elle apparaît sur la sortie ) est de 3 ms. Mais tout n'est pas si simple - ce temps est maximum, c'est-à-dire passer du minimum au maximum et vice versa. Lorsque vous essayez de retirer le méandre, ces fronts jonchés sont très clairement visibles:



Si une onde sinusoïdale est émise, l'obstruction des fronts n'est plus aussi perceptible en raison de la forme d'onde. Cependant, si la fréquence augmente, le signal sinusoïdal devient triangulaire et, avec une augmentation supplémentaire, l'amplitude du signal diminue.

Génération à 1 KHz ( 90% d'amplitude ):



Génération à 10 KHz ( 90% d'amplitude ):



Génération à 100 KHz ( 90% d'amplitude ):



Les étapes sont déjà visibles - car de nouvelles données sont chargées dans le DAC à une fréquence de 4 MHz.

De plus, le bord de fuite du signal en dents de scie est encombré et par en dessous le signal n'atteint pas la valeur à laquelle il devrait. En effet, le signal n'a pas le temps d'atteindre le niveau bas spécifié et le logiciel charge de nouvelles valeurs

Génération à 200 KHz ( 90% d'amplitude ):



Ici, vous pouvez déjà voir comment toutes les vagues se sont transformées en triangle.

Chaînes numériques


Avec les canaux numériques, tout est beaucoup plus simple - dans presque tous les microcontrôleurs, il y a des temporisateurs qui vous permettent d'émettre un signal PWM vers les sorties du microcontrôleur. Il est préférable d'utiliser un minuteur 32 bits - dans ce cas, vous n'avez pas besoin de compter le pré-minuteur du minuteur, chargez simplement la période dans un registre et chargez le rapport cyclique requis dans un autre registre.

Interface utilisateur


Il a été décidé d'organiser l'interface utilisateur en quatre rectangles, chacun avec une image du signal de sortie, de la fréquence et de l'amplitude / rapport cyclique. Pour le canal actuellement sélectionné, les données de texte sont affichées en blanc, le reste en gris.



Il a été décidé de contrôler les encodeurs: celui de gauche est responsable de la fréquence et du canal actuellement sélectionné ( change lorsque le bouton est enfoncé ), celui de droite est responsable de l'amplitude / rapport cyclique et de la forme d'onde ( change lorsque le bouton est enfoncé ).

De plus, la prise en charge de l'écran tactile est implémentée - lorsque vous cliquez sur un canal inactif, il devient actif, lorsque vous cliquez sur un canal actif, la forme d'onde change.

Bien sûr, DevCore est utilisé pour faire tout cela. Le code pour initialiser l'interface utilisateur et mettre à jour les données à l'écran ressemble à ceci:

Structure contenant tous les objets d'interface utilisateur
  // ************************************************************************* // *** 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]; 
Code d'initialisation de l'interface utilisateur
  // 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); } 
Code de mise à jour de l'écran
  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(); 

Une implémentation intéressante du clic de bouton est implémentée (c'est un rectangle sur lequel les éléments restants sont dessinés ). Si vous avez regardé le code, vous auriez dû remarquer une telle chose: ch_dsc [i] .box.SetCallback (& ​​Callback, this, nullptr, i); appelé en boucle. C'est le travail de la fonction de rappel qui sera appelée lorsque le bouton est enfoncé. Les éléments suivants sont transférés à la fonction: l'adresse de la fonction statique de la fonction statique de la classe, le pointeur this et deux paramètres utilisateur qui seront transmis à la fonction de rappel - un pointeur ( non utilisé dans ce cas - nullptr est transmis ) et un numéro (le numéro de canal est transmis ).

Du banc universitaire, je me souviens du postulat: " Les fonctions statiques n'ont pas accès aux élèves non statiques ." Ce n'est donc pas vrai . Puisqu'une fonction statique est membre d'une classe, elle a accès à tous les membres de la classe si elle a un lien / pointeur vers cette classe. Jetez maintenant un œil à la fonction de rappel:

 // ***************************************************************************** // *** 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; } 

Dans la première ligne de cette fonction, la « magie » se produit, après quoi vous pouvez accéder à tous les membres de la classe, y compris les membres privés.

Soit dit en passant, cette fonction est appelée dans une autre tâche ( rendre l'écran ), donc à l'intérieur de cette fonction, vous devez vous occuper de la synchronisation. Dans ce simple projet " couple de soirées ", je ne l'ai pas fait, car dans ce cas particulier ce n'est pas indispensable.

Code source du générateur téléchargé sur GitHub: https://github.com/nickshl/WaveformGenerator
DevCore est désormais alloué à un référentiel séparé et inclus en tant que sous-module.

Eh bien, pourquoi ai-je besoin d'un générateur de signaux, ce sera dans le prochain article ( ou l'un des suivants ).

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


All Articles