logo_elektromys.eu

/ Komplementární PWM DAC |

Při pročítání literatury jsem narazil na elegantní postup jak snížit zvlnění v "PWM DA převodníku". Snížení zvlnění umožňuje použit RC filtr s menší časovou konstantou a otevírá vám možnost zkrátit dobu přeběhu a tedy generovat "rychlejší" průběhy. Celá finta spočívá v tom vytvořit si komplementární (negovaný) signál k stávajícímu PWM a skrze střídavou vazbu ho sčítat s původním signálem. Jinak řečeno vytvořit si zvlnění s opačnou polaritou a odečíst ho od původního signálu se zvlněním. A protože většina moderních MCU má časovače schopné generovat komplementární PWM signály, jeví se mi zapojení jako velmi užitečné. Dolní propust je tvořena odporem R1 a kondenzátorem C1. Horní propust složená z R2 a C2 je zapojená na komplementární PWM a slouží k redukci zvlnění.

R1 a C1 tvoří dolní propust pro PWM signál. Komplementární signál skrze R2 a C2 odečítá zvlnění. Typicky je R2=R1 a C2=C1.

Přibližný vztah pro stanovení zvlnění se střídou 50% jednoduchého RC filtru (bez redukce zvlnění) je:
Vpp=Vfs*T / (4RC)
kde:
Vpp je zvlnění "peak to peak"
Vfs je "Full scale", tedy amplituda PWM signálu
T je perioda
R a C je odpor a kapacita RC filtru

Pro vylepšenou variantu (s redukcí zvlnění) by měl platit následující přibližný vztah:
Vpp=0.5*Vfs*(T/(4RC))^2

A pro praktické použití uvádím ještě inverzní vztah pro stanovení RC konstanty v závislosti na požadovaném zvlnění:
RC=T*sqrt(Vfs/(32*Vpp))

Pro jistotu jsem si to vyzkoušel a udělal i drobné srovnání s variantou "bez redukce zvlnění". Chtěl jsem vtvořit 8bitové DAC takže jsem zvlnění Vpp zvolil na úrovni 1LSB. Což při 5V dává přibližně 20mV. Perioda pro 16MHz časovač s 256 kroky PWM vychází 16us (62.5kHz). RC konstanta pro "vylepšenou" variantu vychází přibližně 45us a proto jsem zvolil R1=R2=4k7 a C1=C2=10nF. Pro test jsem použil STM8S208, jehož TIM1 nabízí komplementární výstupy (stejně jako TIMery na STM32). Vnitřní odpor výstupů je u STM8 přibližně 100Ohm a je pro účely testů zanedbatelný. Pro srovnání jsem připravil i variantu "bez redukce zvlnění" se stejným zvlněním (R1=150k, C1=10nF). Díky tomu můžeme porovnat rychlost přeběhu obou variant. Výsledky si můžete prohlédnout na následujících oscilogramech.

Zvlnění "vylepšené" varianty s R1=R2=4k7, C1=C2=10nF (žlutá stopa) s "jednoduchou" variantou R=150k, C=10nF (zelená stopa).
Srovnání rychlosti přeběhu "vylepšené" (žlutá stopa) a "jednoduché" (oranžová stopa) varianty z 25% na 75% PWM. Obě mají stejné zvlnění, hodnoty součástek popsány výše. Rychlost přeběhu je přibližně o řád lepší.

| Závěr /

Jen namátkou, podobný přístup lze použít v podstatě na všech STM32 (všechny mají časovače s komplementárním výstupem) a asi i na všech STM8. Problém nebudou mít ani starší AVRka (viz poznámka níže) a najdou se mezi nimi i varianty které mají "hardwarovou" podporu pro komplementární PWM jako například Attiny25/45/85 (64MHz timer viz stručná ukázka). A podobně, nebo možná lépe na tom budou i moderní AVRka jako Attiny416, či Atmega4809 atp. Vlastně asi všechny mikrokontroléry s časovačem by to měly zvládat. Přirozeně pokud by bylo cílem dosáhnout nějaké slušnější přesnosti, bylo by asi vhodnější použít analogový switch a referenci, protože napětí výstupu mikrokontroléru nijak zvlášť přesné. Pro případné detailnější informace jako například vztahy pro stanovení settling time apod. mrkněte do literatury z odkazů.

| Doplněk /

Předpokládám, že se ještě několikrát setkám s někým kdo bude chtít vytvářet nějaké průběhy z Arduina. Připravil jsem tedy velice jednoduchou, začátečnickou, demonstraci jak tímto způsobem využít Timer0 na Atmega328P (Arduino UNO,Mini,Micro). Ve zkratce, taktuji čip 16MHz, nastavím Timer0 do režimu Fast PWM, výstup OC0A aktivuji jako pozitivní PWM, výstup OC0B jako invertované PWM (tím vznikne komplementární dvojice PWM signálu). Časovač spustím s předděličkou /1, tedy s periodou PWM 16us (shodně s předchozími příklady). Naivní ukázka je založená na lookup tabulce se 64 hodnotami sinusovky. Prodlevu mezi vzorky zajišťuje tupá delay funkce a v praxi by se jistě hodilo nasadit nějaké lepší řešení. Změnu PWM bychom mohli dělat naprosto tupě pouhým přepisem OCR0A a OCR0B na novou (shodnou) hodnotu. Tím bychom se ale vystavili riziku že vznikne glitch (pokud mezi zápisem do OCR0A a OCR0B přeteče timer). Tento problém jsem ošetřil tím že přepis obou registrů provádím ihned po přetečení časovače - to by mi mělo dát dost času zapsat nové hodnoty do obou. Přirozeně pořád hrozí riziko že celou časoě kritickou akci zhatí nějaké přerušení. Mohu tedy přerušení vypnout (ale je potřeba vzít v úvahu že to může trvat až 16us a to už může být pro někoho nepřijatelné řešení). Elegantnější by tedy bylo provádět změnu PWM v rutině přerušení od přetečení (TOV). Takže jen pro zajímavost...

// Příklad PWM DAC využívající komplementárních PWM výstupů pro redukci zvlnění (případně snížení rychlosti přeběhu)
#define F_CPU 16000000
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

void init_timer(void);
void set_pwm(uint8_t value);

// tabulka pro sinus (například 64 hodnot)
const uint8_t table[]={
 0x80,0x8c,0x98,0xa5,0xb0,0xbc,0xc6,0xd0,
 0xda,0xe2,0xea,0xf0,0xf5,0xfa,0xfd,0xfe,
 0xff,0xfe,0xfd,0xfa,0xf5,0xf0,0xea,0xe2,
 0xda,0xd0,0xc6,0xbc,0xb0,0xa5,0x98,0x8c,
 0x80,0x73,0x67,0x5a,0x4f,0x43,0x39,0x2f,
 0x25,0x1d,0x15,0xf,0xa,0x5,0x2,0x1,
 0x0,0x1,0x2,0x5,0xa,0xf,0x15,0x1d,
 0x25,0x2f,0x39,0x43,0x4f,0x5a,0x67,0x73
};
uint8_t i=0; // index na procházení tabulkou

int main(void){
 init_timer(); // rozběhnout timer

 while(1){
  // každých ~31us zapsat novou hodnotu stídy (64 úrovní * 31us => perioda sinu 2ms => f~500Hz)
  _delay_us(31); // tupé čekání na další hodnotu "DAC", v praxi jsou lepší způsoby, ale o ty teď nejde
  i++;
  if(i>=sizeof(table)){i=0;}
  set_pwm(table[i]); // nastavit novou hodnotu střídy
 }
}

// nastavení PWM s ošetřením glitchů
void set_pwm(uint8_t value){
 TIFR0 |= (1<<TOV0); // vymažeme vlajku přetečení timeru
 //cli(); // pokud chci mít jistotu že nevznikne glitch (což zde asi nebude vadit), musím vypnout přerušení (to může být nežádoucí)
 while(!TIFR0 & (1<<TOV0)); // počkáme až timer přeteče (pak budeme mít dosta času přepsat oba registry)
 OCR0A=value; // přepíšu oba Compare registry... 
 OCR0B=value; // ..díky bufferování se s příštím přetečením nastaví na obou kanálech ZÁROVEŇ nová hodnota střídy 
 //sei(); // zapnu přerušení pokud jsem ho vypnul
}

void init_timer(void){
 DDRD |= (1<<DDD5) | (1<<DDD6); // PD5 (OC0B) a PD6 (OC0A) jako výstupy
 // OC0A - PWM, OC0B inverzní PWM, Režim Fast-PWM (strop 255)
 TCCR0A = (1<<COM0A1) | (1<<COM0B1) | (1<<COM0B0) | (1<<WGM01) | (1<<WGM00);
 OCR0A = 0; // výchozí hodnoty
 OCR0B = 0;
 TCCR0B = (1<<CS00); // spustit timer s taktem F_CPU (16MHz) a tedy periodou 16us. 
}
No comment...

| Zdroje a odkazy /

Home
| V1.00 30.12.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /