Demo aplikace, která zaznamenává průběhu napětí se vzrokováním až 1.55Msps v reakci na vnější spouštěcí impulz na STM32F0. Domonstruje jak lze využít timer ke spouštění kontinuálního AD převodu externím signálem.

Motivace

Rovnou přiznávám, že zatím jen matně tuším kde by následující aplikace šla použít, takže klasická motivace se dnes nekoná. Nechtěl jsem v domácí karanténě úplně zakrnět a tak jsem se rozhodl, že se pokusím vymáčknout z AD převodníku na STM32F0 maximální vzorkovací rychlost. A protože vzorkovat nějaký náhodný signál by byla dosti nuda, rozhodl jsem se, že budu měřit průběh pulzu přicházejícího za trigrovacím signálem.

Myšlenka

Teoretická maximální rychlost převodu na STM32F0 se pohybuje mezi 1 - 1.5Msps v závislosti na tom s jak moc mizerným rozlišením se smíříme. Protože netuším jestli by bylo jádro schopné takový datový tok vyhodnocovat včas, zvolil jsem raději záznam do paměti (s tím, že na pozadí nechť si s daty jádro dělá co chce). Shrňme si nejprve fakta, které dávají celou aplikaci dohromady:

  • AD převodník umí pracovat v kontinuálním režimu. V něm probíhá vzorkování a převod bez prodlev. Minimální vzorkovací čas je 1.5 periody převodníku, převod pak trvá 12.5, 11.5, 9.5 nebo 7.5 periody podle zvoleného rozlišení (12bit, 10bit, 8bit, 6bit). Maximální taktovací frekvence ADC je 14MHz a při ní je tedy vzorkovací frekvence od 1MSPS (pro 12bit) po 1.55Msps (pro 6bit rozlišení).
  • AD převod lze spouštět softwarově nebo interním signálem (z timerů) a to i v kontinuálním režimu (zde se spouští první převod).
  • Výsledky převodu z AD převodníku lze přenášet do paměti pomocí DMA.
  • V AD převodníku lze zvolit režim, v němž DMA po skončení přenosu zastaví kontinuální AD převod.
  • AD převod není možné spouštět přímo vnějším signálem (není k dispozici nic jako externí trigger)
  • Timer lze spouštět externím signálem (buď z ETR vstupu nebo i signálem z prvního a druhého kanálu)
  • Timer lze nastavit do one-pulse režimu. V něm se automaticky vypne po přetečení (uplynutí jedné periody)
  • Událost spuštění timeru lze použít jako vnitřní výstup z timeru (vedoucí nejen do AD převodníku)

Timer

O některých funkcích "slave controlleru", který umožňuje vyvolat klíčové události timeru (jako třeba spuštění, čítání atd.) jste se mohli dočíst v předchozích tutoriálech (Timer-link,PWM input). Volba padla na TIM3. Mohl jsem sáhnout i po TIM2, ale to bych si vyplýtval jediný 32bit timer na čipu (tuto demonstraci provádím na Nucleo STM32F042). Z podobného důvodu jsem nevolil ani TIM1. Ostatní timery buď neobsahují "slave controller" a nebo jejich výstup nevede do ADC. Konfiguraci timeru jsem pro názornost zakreslil do blokového schematu z datasheetu


Zelené šipky znázorňují naši konfiguraci.
Oranžová šipka alternativní možnost, pokud bychom chtěli reagovat na libovolnou hranu spouštěcího signálu.
červená šipka vyznačuje třetí možnost, kterou ale nelze použít na našem konkrétním MCU protože příslušné GPIO (TIM3_ETR) není přítomné


Pojďme si tedy okomentovat konfiguraci popořadě. Spouštěcí signál (vzestupná hrana log. signálu) přivedeme na vstup TIM3_CH1. Signál vstupuje do "filter & edge detector". Zde dochází k detekci hran a digitální filtraci. V našem programu nastavíme detekci vzestupné hrany a filtr vypneme. Tím vznikne signál TI1FP1. Ten vede přímo do "slave controlleru", který nastavíme tak aby příchod signálu z TI1FP1 vyvolal událost "enable" (v knihovnách pojmenovanou Trigger). Příchodem vzestupné hrany se tedy timer spustí. Vnitřní výstup timeru (TRGO) může posílat signál od několika různých událostí. Nám se hodí do krámu událost "enable". Tedy okamžik kdy byl timer (externím signálem) spuštěn. Signál z TRGO pak použijeme v ADC ke spuštění převodu. Protože náš timer nemá nic časovat, má hrát roli pouze detektoru spouštěcího signálu, nastavíme periodu (strop timeru) na nějakou miniální hodnotu a aktivujeme One-pulse mód. Tato volba zaručí že se timer dva tiky po startu zase sám vypne a je připraven detekovat další spouštěcí signál. Abych viděl s jakým zpožděním timer reaguje, dovolil jsem si využít jeho třetí kanál ke generování pulzu (PWM) o minimální šířce (1 tik). K funkci to není nutné, takže pokud plánujete příklad někde využít, můžete příslušný kus kódu vyhodit.

Na větších MCU je vyveden i signál TIM3_ETR, se kterým je možné provádět úplně stejnou akci. Pokud bychom chtěli timer spouštět na obě hrany (vzestupnou i sestupnou), můžeme použít signál TI1F_ED, což je výstup přímo z "edge detektoru". Kdyby to bylo potřeba, můžeme použít namísto kanálu 1 i kanál 2 (a jeho signál TI2FP2).

// TIM3 snímá vzestupnou hranu na trigger signálu a spouští sérii AD převodů - funguje jako externí trigger pro ADC
void init_TIM3(void){
LL_GPIO_InitTypeDef GPIO_InitStruct;

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOB);
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3);

        // PA6 (TIM3_CH1) - budoucí vstup pro trigger
 GPIO_InitStruct.Pin = LL_GPIO_PIN_6;
 GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
 GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
 GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
 GPIO_InitStruct.Pull = LL_GPIO_PULL_DOWN;
 GPIO_InitStruct.Alternate = LL_GPIO_AF_1;
 LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PB0 (TIM3_CH3) - informační výstup
 GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
 GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
 LL_GPIO_Init(GPIOB, &GPIO_InitStruct);

 // konfigurace Timeru
 LL_TIM_SetPrescaler(TIM3,0); // frekvence 48MHz
 LL_TIM_SetAutoReload(TIM3,2); // strop minimální (chceme aby se timer včas vypnul a byl připraven snímat další trigger)
 LL_TIM_SetOnePulseMode(TIM3,LL_TIM_ONEPULSEMODE_SINGLE); // one-shot, timer se po přetečení sám vypne (a bude čekat na další trigger)

 // konfigurace IC kanálu (vstup pro trigger timeru)
 LL_TIM_IC_SetPolarity(TIM3,LL_TIM_CHANNEL_CH1,LL_TIM_IC_POLARITY_RISING); // detekovat vzestupnou hranu
 LL_TIM_IC_SetFilter(TIM3,LL_TIM_CHANNEL_CH1,LL_TIM_IC_FILTER_FDIV1); // digitální filtr vypnutý
 // od teď jde na TI1FP1 pulz okamžitě po detekci vzestupné hrany na CH1 (PA6)

 // Informační výstup timeru generuje pulz
 LL_TIM_OC_SetMode(TIM3,LL_TIM_CHANNEL_CH3,LL_TIM_OCMODE_PWM2); // mód PWM (zarovnaný na "pravou stranu")
 LL_TIM_OC_SetCompareCH3(TIM3,1); // vzestupná hrana PWM 1 tik po startu timeru
 LL_TIM_CC_EnableChannel(TIM3,LL_TIM_CHANNEL_CH3); // od teď má TIM3 kontrolu nad CH3 (PB0)

 // nastavení master chování
 LL_TIM_SetTriggerOutput(TIM3,LL_TIM_TRGO_ENABLE); // vypustit TRGO signál jakmile je timer spuštěn

 // Nastavení Slave chování
 LL_TIM_SetTriggerInput(TIM3,LL_TIM_TS_TI1FP1); // Reagovat na signál TI1FP1
 LL_TIM_SetSlaveMode(TIM3,LL_TIM_SLAVEMODE_TRIGGER); // událostí timer spustit
}


ADC

Konfigurace AD převodníku je vcelku přímočará (a trochu zdlouhavá). Vstupní signál pustíme třeba na vstup ADC_IN0 (PA0), ale klidně můžete volit i jiný kanál. Zdroj clocku volím (asynchronních) 14MHz z interního RC oscilátoru, protože to je jeho maximální taktovací frekvence. Další možností by bylo taktovat ho 12MHz odvozenými od taktu celého MCU (48MHz), ale ztratil bych tím 15% rychlosti. Rozlišení AD převodu si můžeme volit podle potřeby a ještě se k němu budeme několikrát vracet. Jako TriggerSource volíme TRGO výstup TIM3. Aktivujeme už zmíněný kontinuální režim. DMA přenos volíme typu DMA_TRANSFER_LIMITED, což znamená že DMA po skončení přenosu zastaví AD převodník. Před spuštěním ADC ještě pro jistotu provedeme kalibraci. Nakonec provedu konfiguraci měřeného kanálu. Funkcí LL_ADC_REG_SetSequencerChannels() zvolím kanál, které chci měřit. Za běžných okolností je argumentem této funkce vícero kanálů které pak automaticky přepíná sekvencér, ale nám se tato funkce nehodí a volíme pouze jeden kanál. Funkcí LL_ADC_SetSamplingTimeCommonChannels() volíme vzorkovací čas. Pro nejrychlejší převod volím ten nejkratší - tedy 1.5 taktu ADC, což je přibližně 110ns. Tak krátký vzorkovací čas vyžaduje nízký výstupní odpor vstupního signálu (což by měl generátor s výstupní impedancí 50Ohm splňovat).

static void MX_ADC_Init(void){
  LL_ADC_InitTypeDef ADC_InitStruct = {0};
  LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};
  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  LL_DMA_InitTypeDef dma;

  uint16_t del=10; // poslouží k realizaci tupého delay

  LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_ADC1);
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
 
  // PA0 je vstup pro ADC
  GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  // 1.konfigurace ADC
  ADC_InitStruct.Clock = LL_ADC_CLOCK_ASYNC; // taktujeme interním 14MHz oscilátorem (umožňuje nejrychlejší převod)
  ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B; // rozlišení lze volit různě, menší rozlišení zkracuje dobu převodu
  ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT; // data zarovnaná vpravo
  ADC_InitStruct.LowPowerMode = LL_ADC_LP_MODE_NONE; // low power módy nás teď nezajímají
  LL_ADC_Init(ADC1, &ADC_InitStruct); // provede nastavení ADC

  // 2.konfigurace ADC
  ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_EXT_TIM3_TRGO; // převod startovat TRGO sginálem z TIM3
  ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE; // seqvencer nepoužíváme, Discontinuous režim nechceme
  ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_CONTINUOUS; // volíme kontinuální režim (!), ADC převádí od trigru, do signálu od DMA (konce přenosu)
  ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_LIMITED; // DMA má oprávnění zastavit převod, jakmile dojde ke konci přenosu (DMA řídí počet převodů)
  ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_PRESERVED; // overrun neošetřujeme, takže to je jedno
  LL_ADC_REG_Init(ADC1, &ADC_REG_InitStruct); // provede nastavení ADC

  // kalibrace ADC
  LL_ADC_StartCalibration(ADC1); // zahájí kalibraci
  while(LL_ADC_IsCalibrationOnGoing(ADC1)); // počká na dokončení
  while(del){del--;} //počká alespoň 2us od kalibrace

  // spuštění DAC
  LL_ADC_Enable(ADC1); // spustíme ADC
  while(!LL_ADC_IsActiveFlag_ADRDY(ADC1)); // počkáme až se ADC rozběhne
  LL_ADC_ClearFlag_ADRDY(ADC1); // uklidíme po sobě vlajku
 
  // konfigurace ADC kanálu
  LL_ADC_REG_SetSequencerChannels(ADC1, LL_ADC_CHANNEL_0); // vstupem je ADC_IN0 (pouze)
  LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_1CYCLE_5); // vzorkovací čas je co nejkratší

  // konfigurace DMA
  dma.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY; // transfer z ADC (periferie) do paměti
  dma.MemoryOrM2MDstAddress = (uint32_t)pulses; // nezáleží, budeme ji nastavovat znovu
  dma.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; // na straně paměti inkrementovat adresu
  dma.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_HALFWORD; // přenášíme 16bit (tady změnit pokud přenastavíme ADC na 8 nebo 6bit)
  dma.Mode = LL_DMA_MODE_NORMAL; // jednorázový přenos
  dma.NbData = SAMPLES; // počet přenášených vzorků (nezáleží, budeme jej nastavovat znovu)
  dma.PeriphOrM2MSrcAddress = (uint32_t)(&(ADC1->DR)); // zdrojová adresa v periferii (ADC)
  dma.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_HALFWORD; // z periferie (ADC) přenášíme 16bit (tady změnit pokud přenastavíme ADC na 8 nebo 6bit)
  dma.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; // v periferii (ADC) nechat adresu konstantní (neinkrementovat)
  dma.Priority = LL_DMA_PRIORITY_HIGH; // priorita DMA je celkem vysoká (nemá vliv, protože jiné DMA kanály stejně neběží)
  LL_DMA_Init(DMA1,LL_DMA_CHANNEL_1,&dma); // nastavit DMA (channel 1 odpovídá requestům od ADC)

  // nastavit přerušení od DMA
  __NVIC_SetPriority(DMA1_Channel1_IRQn,0); // vysoká priorita
  __NVIC_EnableIRQ(DMA1_Channel1_IRQn); // povolit v NVIC
  LL_DMA_EnableIT_TC(DMA1,LL_DMA_CHANNEL_1); // povolit přerušení od Transfer Complete našeho channel 1
}


DMA

Konfigurace DMA je jednoduchá. Nastavíme přenos z ADC do paměti, podle zvoleného rozlišení si vybereme zda přenášíme BYTE nebo HALFWORD. Adresu v paměti bude naše aplikace postupně měnit, takže při inicializaci ji není nutné znát. Stejně tak počet přenášených dat, budeme muset před každým znovu-spuštněním ADC opětovně zapsat. Voba vysoké priority zde nehraje roli, neboť poběží jen jeden DMA kanál. Smysl by dostala ve složitější aplikaci, kde by DMA obsluhovalo více přenosů. Jako poslední si od DMA povolím přerušení od "Transfer Complete". To bude okamžik kdy skončí sběr volitelného počtu vzorků (jednoduše řečeno kdy skončí jedno měření pulzu). V rutině přerušení pak provedeme znovu-připravení našeho systému k dalšímu měření. Díky tomu bude sběr vzorků z AD převodníku probíhat "na pozadí" a nebude příliš zaměstnávat jádro.

Pro záznam pulzů máme v paměti připravené dvourozměrné pole pulses[][]. Konkrétně máme vyhrazeno 16 buněk o 64 prvcích. Těch 64 prvků budeme plnit výsledky jednotlivých AD převodů a bude v nich uložen záznam jednoho pulzu. Těchto záznamů si uložíme celkem 16. Toto pole může fungovat třeba jako kruhový buffer a jádro tak může mít dost času provádět na pulzech nějakou analýzu třeba měřit amplitudu, nebo šířku a pod. Oba rozměry pole si můžete snadno změnit makry PULSE_COUNT a SAMPLES. Proměnná hotovo má pomocnou roli a slouží k identifikaci okamžiku kdy aplikace dokončí sběr všech 16ti pulzů. Proměnná pulse je počítadlo pulzů a dává nám informaci o tom do které ze 16ti buněk se zrovna provádí záznam. Proměnné cnt a uzitecna_cinnost nejsou pro práci klíčové. Abychom mohli měřit jak dlouho se aplikace připravuje na další záznam, indikujeme si tuto její činnost na výstupu PB1 (makra INFO_H a INFO_L).

(Re)start záznamu vypadá následovně. Nejprve vypneme DMA kanál abychom následně mohli zapsat počet vzorků záznamu (64) a nastavit adresu kam se mají data ukládat (ta se postupně posouvá). Pak DMA kanál zapneme, pro jistotu počkáme než se k jeho zapnutí dojde. Poté "spustíme" AD převod. Což neznamená že se rovnou zahájí první převod, ale jen to, že AD převodník začne čekat na spouštěcí signál z timeru. Toť vše. Nutné mazání vlajky a počítání změřených sad (pulse) nestojí za komentář.

// PA0 (A0) (ADC_IN0) - měřené pulzy
// PA6 (A5) (TIM3_CH1) - spuštění AD převodů
// PB0 (D3) (TIM3_CH3) - informační výstup (spuštění ADC)
// PB1 (D6) - informační výstup (reset ADC+DMA)

#include "main.h"
#include "string.h"

#define INFO_H LL_GPIO_SetOutputPin(GPIOB,LL_GPIO_PIN_1)
#define INFO_L LL_GPIO_ResetOutputPin(GPIOB,LL_GPIO_PIN_1)

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_ADC_Init(void);
void init_TIM3(void);
void get_pulses(void);

#define PULSE_COUNT 16
#define SAMPLES 64
volatile uint16_t pulses[PULSE_COUNT][SAMPLES]; // tady sbíráme celkem 16 pulzů, každý po 64 vzorcích (změnit na uint16_t pokud máte převod 10 nebo 12bit)
volatile uint8_t hotovo; // informuje hlavní smyčku o tom že sběr všech pulzů skončil
volatile uint16_t pulse=0; // počítá pulzy v rámci jedené sady (je přístupné zbytku programu)
uint32_t cnt=0; // počítá počet sad pulzů (jen tak)
uint32_t uzitecna_cinnost=0; // nějaká užitečná činnost v mezi čase


int main(void){
  // základní inicializace
  LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SYSCFG);
  LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);
  SystemClock_Config(); // 48MHZ (z HSI48)
  MX_GPIO_Init(); // konfigurace informačního výstupu (PB1)
  MX_ADC_Init(); // konfigurace ADC, vstupů i DMA
  init_TIM3(); // konfigurace TIM3 (trigrovacího systému pro ADC) a přidružených GPIO  
 
  hotovo=1; // spustí první měření

  while (1){
   if(hotovo){ // pokud jsme proveděli PULSE_COUNT měření, máme data ke zpracování
    INFO_H; // informujeme že probíhá "zpracování"
    pulse=0; // vynulujeme počítadlo pulzů
    // připravíme další sběr (stejně jako v DMA IRQ rutině)
    LL_DMA_DisableChannel(DMA1,LL_DMA_CHANNEL_1);
    LL_DMA_SetMemoryAddress(DMA1,LL_DMA_CHANNEL_1,(uint32_t)&pulses[pulse][0]);
    LL_DMA_SetDataLength(DMA1,LL_DMA_CHANNEL_1,SAMPLES);
    LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1);
    while(!LL_DMA_IsEnabledChannel(DMA1,LL_DMA_CHANNEL_1));
    LL_ADC_REG_StartConversion(ADC1); // od teď ADC čeká na trigger a pak zahájí sekvenci převodů
    hotovo=0;
    cnt++; // počítáme počet sad pulzů... jen tak.
    INFO_L;
    }
   uzitecna_cinnost++ ; // děláme něco smysluplného
  }
}

// dokončen sběr vzorků jednoho pulzu
void DMA1_Channel1_IRQHandler(void){
 INFO_H; // informujeme že probíhá reset DMA a ADC k dalšímu měření
 LL_DMA_ClearFlag_TC1(DMA1); // vyčistit vlajku abychom mohli detekovat příští událost (konec sběru)
 if(pulse < PULSE_COUNT){ // dokud nenasbíráme PULSE_COUNT záznamů
  LL_DMA_DisableChannel(DMA1,LL_DMA_CHANNEL_1); // vypneme DMA abychom ho mohli rekonfigurovat
  LL_DMA_SetMemoryAddress(DMA1,LL_DMA_CHANNEL_1,(uint32_t)&pulses[pulse][0]); // nastavíme novou adresu kam ukládat další pulz
  LL_DMA_SetDataLength(DMA1,LL_DMA_CHANNEL_1,SAMPLES); // znovunastavíme počet vzorků jednho měření
  LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1); // povolíme DMA kanál
  LL_ADC_REG_StartConversion(ADC1); // povlíme ADC čekat na trigger a zahájít sběr dat
  pulse++; // započítáme další pulz
  }else{  // posbírali jsme PULSE_COUNT záznamů a máme hotovo
   hotovo=1; // dáme vědět hlavní smyčce že pole je plné dat
  }
 INFO_L;
}


Výsledky

Nejprve se pojďme podívat na chování naší aplikace na oscilogramech. Na prvních z nich můžeme vidět celkovou práci programu. Přichází spouštěcí signál (červená), na to reaguje TIM3 (světle modrý pulz) a spouští kontinuální AD převod. Na žlutém kanál (měřený pulz) se objevují malé peaky značící že v těchto okamžicích probíhá vzorkování. Po skončení převodu se aplikace připravuje na další sběr a tuto činnost můžete vidět na tmavě modrém průběhu. Můžete si všimnout že doba "přípravy" k dalšímu měření je dvojí, krátká a dlouhé. Krátké doby odpovídají přípravě na další ze 16ti pulzů v rutině přerušení. Dlouhá doba přípravy pak odpovídá situaci kdy je skončen sběr všech 16ti pulzů (na tento okamžik si pak dáme do zdrojového kód breakpoint a stáhneme si surová data).


IN průběh je měřený pulz
TRIG signál je spouštěcí signál (trigger)
TIM signál je výstup timeru (reakce TIM3)
RST signál je příprava SW pro další měření


Další oscilogram ukazuje jeden z limitů naší aplikace. Reakce našeho timeru je drobně opožděná a zatížená jitterem, který vzniká synchronizací vnější asynchronní události (příchod spouštěcího pulzu) s vnitřním clockem čipu. Tento jitter je ale malý v rámci jednoho tiku 48MHz clocku, tedy cca 21ns. I tak je ale reakce timeru opožděná, neboť signál musí projít detektorem hran a digitálním filtrem. Zpoždění za time je tedy mezi 100-140ns. Reakce ADC je o poznání pomalejší (což se dá očekávat neboť je taktováno nižším kmitočtem než timer). První převod se zahájí přibližně za 1.2us a protože se TRGO signál z timeru musí synchronizovat s asynchronním clockem AD převodníku, vzniká další jitter o velikosti jednoho taktu, tedy 1/14M = ~71ns.


Zde je patrné zpoždění reakce timeru i ADC na spouštěcí signál. Dále je zde vidět "jitter" (tedy nejistota zpoždění s jakým se spustí první AD převod). dosahuje přibližně 80ns.


Na posledním oscilogramu si jen ověříme vzorkovací frekvenci 1Msps pro 12 bitový převod.


Zde je patrná vzorkovací frekvence. Snímek pochází z měření s 12bit rozlišením a rychlost odpovídá předpokládané hodnotě 1Msps.


Poslední co nám zbývá je podívat se jak moc se naší aplikaci povedlo tvar pulzu zaznamenat. Musíme tedy data z aplikace nějak vyčíst. Přirozeně bychom si je mohli poslat třeba UARTem do terminálu, ale to je pro vývoj zbytečně zdlouhavé. Využijeme tedy příkaz (a teď mě opravte jestli se mýlím) pro GDB server. Příkazem print /u pulses vypíšeme obsah pole pulses v podobě dekadického čísla (to se hodí až budete vypisovat 8bit proměnné). Data z řádku můžeme pomocí copy-paste přenést do textového souboru a zpracovat dle potřeby. Já ke zpracování použil octave/matlab a skript přikládám na konci článku. GDB se snaží výpis zkracovat, takže pokud je v datech za sebou řada stejných hodnot využije formulace např.
0
Tuto vlastnost lze vypnout, ale bohužel jsem zapomněl jak. Pokud však data zpracováváte pomocí Octave nebo Matlab, tak vám to nemusí vadit, neboť výraz lze snadno nahradit pomocí "najdi-nahraď" na
0 *ones(2,30)
což Octave spolehlivě spolkne


příkaz pro výpis obsahu pole


Rekonstruované pulzy pak vypadají následovně. Při 12bit rozlišení se nám kvůli menší vzorkovací frekvenci nedaří zachytit pulz dostatkem bodů, ale o to kvalitněji vidíme pomalejší děj přicházející po pulzu. Naopak puožitím 6bit rozlišení získáme o něco detailněji průběh pulzu, ale pozvolné změny za ním už hrubě trpí kvantovací chybou.



Rekonstrukce pulzu při 12bit záznamu s 1Msps (vykresleny první tři pulzy z 16ti zaznamenaných)
Rekonstrukce pulzu při 8bit záznamu s 1.27Msps (vykresleny první tři pulzy z 16ti zaznamenaných)
Rekonstrukce pulzu při 6bit záznamu s 1.55Msps (vykresleny první tři pulzy z 16ti zaznamenaných)
Srovnání všech tří měření (vykresleny pouze první pulzy)

Poznámky závěrem

Doufám že jste se u čtení dozvěděli něco nového a že snad někdy někdo něco z toho použije v reálné praxi

Odkazy

www.elektromys.eu
| V1.00 6.4.2020 /
| By Michal Dudka (m.dudka@seznam.cz) /