Poznámky o AD převodníku na STM32F0 I.

Tento článek vznikl zkřížením recenze a tutoriálu. Potřeboval jsem si otestovat parametry AD převodníku (od teď ADC) a když už jsem to udělal, napadlo mě, že bych si výsledky nemusel nechat jen pro sebe. Pro ryzí programátory bude v článku nepříjemná dávka analogové elektroniky, ale věřím že ji skousnete. Článek obsahuje jen jeden příklad jednoduchého měření pomocí ADC a většina obsahu se dotýká spíš elektronických problémů. Povíme si něco o tom jak správně volit vzorkovací dobu, předvedeme si jak inicializovat ADC, v jedné ukázce si naznačíme jak "přizpůsobit" vstupy pro rychlejší měření a podíváme se jak je na tom ADC s přesností.

Možnosti ADC

Na STM32F031 máte k dispozici 9-10 externích kanálů (označených ADC_INx) a tři interní kanály (teplotní senzor, 1.23V referenci a odporový dělič z pinu Vbat). Na STM32F031 slouží ADC piny PA0-PA7,PB0 a PB1 a nejsou na rozdíl od většiny ostatních 5V tolerantní. Převodník je typu SAR (AD převod s postupnou aproximací), umí pracovat s 12,10,8 a 6bit rozlišením a je schopen převádět rychleji jak 1MSPS. Jeho součástí je sekvencer, který automaticky přepíná kanály a umožňuje tak v rychlém sledu "skenovat" více vstupů. Převody je možné spouštět širokou paletou signálů a výsledky převodu je možné zpracovávat pomocí DMA. Zajímavou funkcí je režim "Analog watchdog", pomocí nějž umí ADC autonomně sledovat zda napětí na vybraných kanálech neopustilo vámi specifikovaný rozsah. Výčet funkcí přirozeně není ani zdaleka kompletní, ale o nich až jindy.

Reference ADC

Napájení i referenci ADC obstarává na čipech F031 VDDA doména. Ta obstarává napájení všech analogových funkcí (včetně PLL) a napětí se k ní přivádí pinem VDDA. Mezi napájecím napětím čipu (VDD) a napájením analogové části nesmí napětí přesáhnout 0.4V, což značně omezuje volbu reference a typicky vám nezbude nic jiného než volit referenční napětí rovné napájecímu. Napadají mě dvě možnosti jak referenci realizovat.

Jiné rodiny (například F4) pak mají samostatný referenční vstup a mají širší možnosti práce s referencí. Vnitřní napěťovou referenci o níž padla řeč v úvodu, nelze použít jako referenci ADC, ale lze díky ní měřit "přesnou" hodnotu VDDA (ale o tom až později).

Vzorkování (Sampling)

V hrubých rysech se na převod lze dívat následujícím způsobem. Nejprve sekvencer přepne analogový multiplexer a připojí k ADC vybraný kanál. Hned na to se zahájí samplování. Měřený signál se připojí k vnitřnímu vzorkovacímu kondenzátoru a ten se začne nabíjet (na hodnotu měřeného napětí). Tuto fázi nazvěme samplování. Po skončení samplování se zahájí samotný převod, který trvá 6-12 taktů ADC (podle zvolené přesnosti). Dobu samplování je možné volit a jak brzy uvidíte má klíčový dopad na výsledek převodu. Vzorkovací kondenzátor má kapacitu menší jak 8pF i tak ale nějakou dobu trvá než se nabije. Vy obecně chcete aby se nabil co nejrychleji a to zvlášť v případě, že máte v plánu převádět s velkou vzorkovací frekvencí (například uváděný 1MSPS). Protože se vzorkovací kondenzátor nabíjí přímo z měřeného signálu, musí mít zdroj měřeného signálu patřičně malý výstupní odpor. Možná si řeknete, že těch pár pikofaradů není žádná závratná kapacita. V takovém případě vás ale musím upozornit na fakt, že pokud měříte s 12bitovou přesností musí se kondenzátor nabít na 99.98% vstupního signálu). Když si pak vzpomenete na to, že proces nabíjení RC článku má exponenciální charakter jistě vás opustí poslední zbytky optimismu. Pro převody s vysokou vzorkovací frekvencí budete muset udržet výstupní odpor signálu pod 400Ohm. Typicky tedy bude vstup ADC buzen nějakým rychlým operačním zesilovačem.

Pro obyčejné použití, jako je například měření polohy potenciometrů, fotorezistorů, teplotních čidel atd. se vás ale tento problém netýká neboť zde můžete skoro vždy volit dlouhou vzorkovací dobu. Doba samplování se odvíjí od clocku AD převodníku, který lze přivést buď z APB sběrnice (APB/2 nebo APB/4) a nebo z vnitřního 14MHz RC oscilátoru (to proto aby mohl ADC převodník běžet "na plný výkon" i když čip spí nebo je kvůli úspoře energie "podtaktovaný"). Nejkratší samplování může trvat 1.5 taktu (pro 700kSPS až 1.5MSPS) a nejdelší až 239.5 taktů (pro měření signálů s vysokou impedancí). Přejděme ale rovnou k příkladu.

Inicializace ADC

Odpočineme si teď na chvíli od analogové elektroniky a připravíme si ukázku jak jednoduše měřit napětí na jednom vstupu (konkrétně na PA2) a jak měřit hodnotu VDDA (tedy napájecí napětí čipu a referenci AD převodníku). Zdrojový kód inicializace najdete níže (celý příklad pak v závěru článku). V první řadě je potřeba pin PA2 (ADC_IN2) nakonfigurovat jako analogový vstup. Dále se musíme rozhodnout jaký clock pro AD převodník použijeme. Já zvolil clock z APB a protože maximální frekvence převodníku je 14MHz musel jsem volit APB/4 = 12MHz. Jeden takt tedy trvá přibližně 83ns (bude se hodit až budeme nastavovat dobu vzorkování). Základní inicializace ADC probíhá klasicky pomocí struktur a funkce ADC_Init(). Rozlišení si volím nejvyšší (12bit) a zarovnání dat vpravo (smysl zarovnání vlevo mi uniká). Položkou ADC_ScanDirection se konfiguruje směr s jakým sekvencer skenuje kanály. V naší ukázce ale sekvencer nepoužiji (budu skenovat vždy jen jeden kanál), takže tato volba nehraje roli. Další dvě položky struktury se týkají spouštění (trigrování) převodu vnitřním nebo vnějším signálem. tuto funkci taktéž nepoužijeme. Stejně tak vypneme i "continuous conversion" neboť nechceme aby ADC převádělo nepřetržitě. Poté provedeme kalibraci. Tu je možné spustit jen pokud je ADC vypnutý, takže není od věci ho pro jistotu vypnout. Kalibrace spočívá v tom, že si ADC sám změří svůj offset a uloží si ho. Při měření ho pak automaticky od každého výsledku odečítá. Volá se funkcí ADC_GetCalibrationFactor(), která vrací hodnotu offsetu. Ta vám ale typicky k ničemu nebude. Tím je inicializace hotová a ADC už stačí pouze zapnout. V rámci ukázky budeme používat interní referenci (1.23V), takže ji musíme zapnout také (referenci trvá až 10us než se stabilizuje).

void ADC_init(void){
ADC_InitTypeDef adc_init_struct;
GPIO_InitTypeDef gp;

// konfigurace PA2 (ADC_IN2) jako Analog
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
gp.GPIO_Pin = GPIO_Pin_2;
gp.GPIO_Mode = GPIO_Mode_AN;
gp.GPIO_OType = GPIO_OType_PP;
gp.GPIO_PuPd = GPIO_PuPd_NOPULL;
gp.GPIO_Speed = GPIO_Speed_Level_1;
GPIO_Init(GPIOA, &gp);

// konfigurace ADC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); // spustit APB clock pro ADC
ADC_ClockModeConfig(ADC1,ADC_ClockMode_SynClkDiv4); // vybrat zdroj clocku - APB/4 - (48/4 = 12MHz)

adc_init_struct.ADC_Resolution = ADC_Resolution_12b; // plná 12bit přesnost
adc_init_struct.ADC_DataAlign = ADC_DataAlign_Right; // zarovnání dat vpravo
adc_init_struct.ADC_ScanDirection = ADC_ScanDirection_Upward; // nezáleží na tom
adc_init_struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_TRGO; // nezáleží na tom
adc_init_struct.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; // trigger nepoužíváme
adc_init_struct.ADC_ContinuousConvMode = DISABLE; // převod vždy jen jednou
ADC_Init(ADC1, &adc_init_struct);

// kalibrace (odstranění offsetu)
if(ADC_GetFlagStatus(ADC1,ADC_FLAG_ADEN) != RESET){ADC_Cmd(ADC1,DISABLE);} // kdyby bylo ADC zapnuté
ADC_GetCalibrationFactor(ADC1); // provedeme kalibraci, může vracet naměřený offset (ale na nic mi není)

ADC_VrefintCmd(ENABLE); // zapneme interní referenci (protože ji budu používat)
_delay_us(10); // dle datasheetu by se měla stabilizovat do 10us (ale negarantují to :D )

ADC_Cmd(ADC1,ENABLE); // spustím ADC
while (ADC_GetFlagStatus(ADC1,ADC_ISR_ADRDY) == RESET){} // počkám až je opravdu spuštěné
}

Jednoduché měření

Před samotným měřením je potřeba zvolit kanál a vzorkovací čas (sampling time). Váš program ale nemá přímou kontrolu nad tím který kanál se bude převádět. O to se stará sekvencer. Vy můžete pouze specifikovat které kanály chcete měřit (v registru ADC1->CHSELR) a zda má jejich měření proběhnout ve vzestupném nebo sestupném pořadí. Chcete-li obecně měřit jen jeden vybraný kanál, musíte v tomto registru povolit pouze jeho. Na to ale v SPL knihovnách (V1.5.0) není žádná funkce. Najdete jen funkci ADC_ChannelConfig() která je vyloženě blbá protože obsah registru ADC1->CHSELR pouze obohacuje. Jinak řečeno někdo v ní buď zapomněl jednu instrukci nebo napsal jedno ořítko navíc. Mate svým názvem a doporučuji ji nepoužívat a raději si vystačit se zápisem do registrů. Vzorkovací čas je pak možné zvolit v registru ADC1->SMPR. Po volbě kanálu už stačí jen zahájit převod (fce ADC_StartOfConversion()) a počkat na nastavení vlajky EOC nebo EOSEQ. Vlajka EOC se nastavuje po každém dokončeném převodu, EOSEQ pak po převedení posledního kanálu v sekvenci. V našem případě je první kanál také posledním takže teoreticky by měly být obě vlajky nastaveny zároveň (což jsem nezkoušel). Výsledek převodu si můžete přečíst funkcí ADC_GetConversionValue().

void adc_get( uint16_t* adc_val, uint16_t* volt){
uint16_t val;
// kanál a samplovací dobu specifikuji ručně v registrech - knihovní funkce je špatně
ADC1->CHSELR = ADC_Channel_2; // kanál ADC_IN2 (PA2)
ADC1->SMPR = ADC_SampleTime_71_5Cycles; // o tomhle bude ještě hodně řečí
ADC_StartOfConversion(ADC1); // zahájím převod
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET){} // počkám na jeho dokončení
val=ADC_GetConversionValue(ADC1); // přečtu výsledek
*adc_val = val; // vrátím ho
*volt = (uint16_t)((AREF*(uint32_t)val)/4095); // vrátím přepočtenou hodnotu napětí
}

Přepočet kódu na napětí je přirozeně pouhá trojčlenka, ale v podání ST má jeden háček. U valné většiny AD převodníků (např 12bit) můžete měřit vstupní napětí od 0V do 4095/4096*Vref (viz wiki a kapitola "Resolution"). Jinak řečeno mezi 0V a Vref je celkem 4096 kroků a pokud začnete počítat od nuly dojdou vám kombinace při zlomku 4095/4096, tedy přesně 1LSB pod referenčním napětím. To je naprosto běžné a v takovém případě se výstupní napětí vypočítá jako ADC_hodnota*Vref/4096. Z nějakého záhadného důvodu vás ale datasheet od STMka nabádá aby jste využili vztahu ADC_hodnota*Vref/4095. Je převodník od ST jiný nebo je to chyba v datasheetu ? Je vcelku těžké se o tom přesvědčit, protože když otázku přeformulujete zní takto: "Je 1LSB roven 1/4096 Vref nebo 1/4095 Vref ?" O to těžší když k tomu zahrnete i nepřesnosti ADC zmíněné v sekci "ADC accuracy". Dobrá zpráva je, že když se o tom nedá snadno přesvědčit tak to prostě "nejde poznat" a když to nejde poznat, tak vás to nemusí pálit ! Já se rozhodl věřit datasheetu. Ale pokud máte chuť odpověď zjistit, měla by vést nejjednodušší cesta skrze 6bit rozlišení (pokud to někdo zkusíte určitě mi napište). Jako Vref používáte napájecí napětí, které typicky není přesně 3.3V a je tedy vhodné si jeho přesnou hodnotu změřit voltmetrem a použít ji při výpočtu. V mém případě bylo VDDA (a tedy i VDD) rovno 3.312V (což si můžete prohlédnout v makru AREF).

Řekněme, že si ale nemůžete dovolit takový luxus a měřit si napětí VDDA voltmetrem. Například proto, že napětí v aplikaci vlivem teplot a odběru kolísá. Pak je tu pro vás vnitřní reference. Když totiž pomocí neznámé reference (to je vaše napájecí napětí) změříte známé napětí (vnitřní reference) můžete podle výsledku převodu zpětně spočítat referenční napětí (tedy VDDA). Prostě potřebujete trojčlenku a jeden pevný bod a tím by měla být vnitřní reference. Nikdo vám ale v principu nebrání připojit si jakoukoli jinou referenci (třeba TL431 nebo TLV431) na některý ze vstupů a používat ji stejně. A teď přijde malý vtípek. Vnitřní reference má dle datasheetu napětí někde mezi 1.2 a 1.25V. Tedy je přibližně stejně nepřesně určena jako napětí vašeho stabilizátoru :D Nebudu vás děsit, vy její přesnou hodnotu sice neznáte (zatím), ale máte ve flash paměti uložen klíč pomocí nějž ji můžete zjistit. A jakmile ji jednou zjistíte, máte vyhráno protože by se její hodnota neměla měnit. Asi vás napadne proč je to k čertu tak strašně zamotané. Z technologického hlediska není jednoduché vyrobit referenci s předem danou přesnou hodnotou. Takže kus od kusu vyjde reference jednou 1.223V a jindy třeba 1.241V. ST čip před expedicí připojí na přesné napájecí napětí 3.300V, změří hodnotu vnitřní reference a v podobě kódu vám výsledek uloží do flash paměti na známé místo. Kód v podobě 16bit čísla leží na adrese 0x1FFFF7BA a pokud budete chtít tak si pomocí trojčlenky dopočítáte její napětí. Ale můžu vás ubezpečit, že vám prakticky k ničemu nebude. Kdyby jste se totiž rozhodli, že si nejprve spočítáte napětí vnitřní reference, pak z jejího měření spočtete hodnotu VDDA a nakonec z hodnoty VDDA dopočtete napětí nějakého vnějšího signálu, tak děláte chybu. S každým mezivýsledkem se totiž dopustíte zaokrouhlovací chyby (jednoho z problémů při počítání s celými čísly). Je mnohem výhodnější použít trochu matematiky a upravit si vztah do tvaru který je v datasheetu:
U = (3.300*kód_z_flash*ADC_val)/(vysledek_mereni_reference*4095)
kde:

Podobný problém jak s pomocí známé reference dopočítat napájecí napětí si můžete přečíst v tutoriálu o ADC na AVR (v příkladu E). Do ukázky přidal funkci, která změří hodnotu VDDA a jen pro zajímavost ji můžete srovnat s tím co vám ukáže voltmetr. Všimněte si, že pro převod vnitřní reference nastavuji vzorkovací dobu na 71.5 taktu, tedy na 71.5*83.3ns =~ 6us. Datasheet totiž specifikuje, že doba měření by měla být minimálně 4us (kvůli relativně velkému výstupnímu odporu vnitřní reference).

void avdd_get(uint16_t* ref_val, uint16_t* avdd){
uint16_t val;
// kanál a samplovací dobu specifikuji ručně v registrech - knihovní funkce je špatně
ADC1->CHSELR = ADC_Channel_17; // volba kanálu (interní reference)
ADC1->SMPR = ADC_SampleTime_71_5Cycles; // samplování interní reference musí trvat min 4us (tedy 1/12MHz*71.5 = ~6us)
ADC_StartOfConversion(ADC1); // zahájím převod
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET){} // počkám na dokončení
val=ADC_GetConversionValue(ADC1); // přečtu hodnotu
*ref_val = val; // vrátím ji
*avdd = (uint16_t)((CAL_AREF*(uint32_t)(*VREFINT_CAL_ADDR))/val); // spočítám teoretické napětí AVDD a vrátím ho
}

Test č.1

Teď přijde experimentální část tutoriálu. Výše uvedený příklad jsem testoval na dvojici zapojení. V prvním případě jsem připojil 100k trimr mezi VDDA a GND a jezdec trimru jsem připojil na PA2. Nastavil jsem jezdec trimru tak aby napětí na PA2 dosáhlo hodnoty přibližně 400mV a proměřil jsem osciloskopem jak vypadá jeho průběh během převodu. Výsledek si prohlédněte na následujícím oscilogramu. Modrá křivka odpovídá měření se vzorkovacím časem 125ns (1.5 taktu) a zelená křivka času 20us (239.5 taktu). Rozdíl mezi křivkami je jen malý detail, podstatný je fakt že ihned po připojení vzorkovacího kondenzátoru vznikne ve výstupním napětí skok. Ten vzniká proto že skrze trimr protéká proud potřebný k nabíjení kondenzátoru a vytváří tak úbytek napětí. Skok je v řádu stovek mV a vzorkování nesmí skončit do té doby než se kondenzátor kompletně nabije. Což je v tomto případě doba v řádu jednotek mikrosekund. Měření s kratším vzorkovacím časem bude zkreslené. Skok lze snadno zahladit tím že na vstup připojíme kondenzátor o řádově větší kapacitě než má vzorkovací kondenzátor. Jenže takový luxus si nemůžete vždy dovolit (třeba pokud neměříte natočení trimru, ale rychle se měnící signál).


Obr.1 - napětí na PA2 během ADC převodu při měření napětí ze 100k trimru. (Modrá Sample time 1.5, Zelená Sample time 239.5)

Potěšující informací pro vás jistě bude následující tabulka. V té jsem srovnal výsledky AD převodu s měřením pomocí relativně přesného multimetru a řekl bych, že STMko obstálo. Upozorňuji vás na to že se ale nejedná o seriozní měření, ale spíš o informativní test. Neobtěžoval jsem se totiž korigovat vliv zatížení trimru voltmetrem. Připojení voltmetru se vstupním odporem 10MOhm může způsobit odchylku až 4mV (v situaci kdy je trimr v nevýhodné pozici přesně uprostřed). Podle očekávání jsou výsledky převodu částečně "zašuměné". Typický rozptyl změřených hodnot popisuje dvojice grafů pod tabulkou. Průměrování by to mělo vcelku hravě vyřešit.

Napětí Vypočítané napětí (výsledek převodu)
19.47 mV 15 mV (19)
143.2 mV 140 mV (173)
779.5 mV 777 mV (961)
1787.1 mV 1789 mV (2212)
3104 mV 3106 mV (3840)
3303 mV 3302 mV (4083)

Obr.2 - rozptyl hodnot ve výsledcích měření. Dobře posloužil program (STM Studio)

Test č.2

V druhém experimentu jsem si chtěl ověřit jak přizpůsobit vstup ADC pro kratší vzorkovací časy. Zapojen zůstal 100k trimr, protože představuje zdroj signálu s velkým výstupním odporem. A protože jsem chtěl návod udržet použitelný pro bastlíře, rozhodl jsem vystačit s nějakým obyčejným operačním zesilovačem. Výběr netrval dlouho, nejrychlejší operační zesilovač co doma mám je klasický TL072 (respektive TL062,TL082,TL084 atd.). Bohužel to není zesilovač typu rail-to-rail, takže bylo potřeba vyřešit otázku napájení. Pro kladnou větev napájení je k dispozici 5V z USB, ale zápornou větev bylo potřeba vytvořit. Poslouží k tomu jednoduchá nábojová pumpa, kterou si můžete prohlédnout na schématu (vytváří napětí přibližně -2.2V). K jejímu buzení slouží výstup časovače (PB3). Protože jsem měl strach aby mi zesilovač nepustil záporné napětí na analogový vstup předřadil jsem do PA2 ještě rezistor 220R. Ten malou měrou degraduje rychlost nabíjení vzorkovacího kondenzátoru, ale ve srovnání s tím jak celou věc limituje TL072, byl jeho vliv zanedbatelný.


Obr.3 - Zapojení s operačním zesilovačem. PB3 obdélníkový signál 5kHz.

Díky tomu, že má operační zesilovač malý výstupní odpor může nabíjení vzorkovacího kondenzátoru probíhat rychleji. Přirozeně i operačnímu zesilovači nějakou dobu trvá než na skok, vzniklý začátkem vzorkování, zareaguje. Jeho výstup v souladu s datasheetem drobně překmitne (Figure 19 v datasheetu). V našem případě trvá překmit o něco déle. A to z toho důvodu, že nutíme OZ pracovat blízko napájecích napětí, tedy v oblastech kde jsou jeho parametry horší. Vzorkování musíme ukončit až po ustálení výstupu. Na následujících dvou oscilogramech si můžete prohlédnout jak průběh vypadá. Pro zajímavost jsem pomocí kurzorů vyznačil okamžiky ukončení samplování pro doby 1.5, 7.5 a 13.5 taktu.


Obr.5 - Odezva výstupu OZ na vzorkování (poloha kurzorů ukazuje okamžik ukončení vzorkování pro 1.5 a 7.5 taktu)


Obr.6 - Odezva výstupu OZ na vzorkování (poloha kurzoru ukazuje okamžik ukončení vzorkování pro 13.5 taktu)

Napětí při kterém jsem test prováděl bylo 119mV. Se vzorkovací dobou 125ns (1.5 taktu) byl výsledek převodu 235mV a pro dobu 625ns (7.5 taktu) 82mV. Což krásně odpovídá průběhu na oscilogramu. Nejkratší doba ukončí samplování ještě před tím než stihne OZ zareagovat a výsledek převodu je vyšší. Vzorkovací doba 625ns se pak svým koncem trefí do překmitu zesilovače a výsledek je tím pádem nižší. Teprve doba 13.5taktu (1.125us) vypadá použitelně a ukončuje vzorkování až v okamžiku kdy je signál stabilní. I tak je to ale hodně "na těsno" a bylo by vhodnější zvolit o něco málo delší dobu. S tímto vzorkovacím časem (13.5 taktu) jsem provedl několik měření abych ověřil jak na tom bude ADC s přesností za takových podmínek. A opět jsem získal vcelku pozitivní výsledek. Rozptyl měření měl stejný charakter jako v předchozím testu.

Napětí Vypočítané napětí (výsledek převodu)
7 mV 3 mV (4)
49.8 mV 46 mV (57)
119 mV 116.5 mV (144)
533 mV 532 mV (658)
1448.3 mV 1446 mV (1789)
2433 mV 2434 mV (3010)
3066 mV 3066 mV (3792)
3267 mV 3269 mV (4042)
3310 mV 3311 mV (4094)

Závěr

Zdrojové kódy pro oba projekty jsou k dispozici (bez TL072 a s TL072). Verze "s TL072" obsahuje navíc časovač, který generuje ne PB3 obdélníkový signál o frekvenci 5kHz. Mám obavy, že pro mnohé z vás mohl být tento tutoriál těžko stravitelným soustem. I přes to ale doufám, že jste získali alespoň hrubou představu o tom jaká úskalí na vás při použití ADC čekají.


Obr.7 - Foto testovacího přípravku


Obr.8 - Foto testovacího přípravku

Home
V1.00 23.12.2017
By Michal Dudka (m.dudka@seznam.cz)