Jedním ze způsobů, jak získat rychlou odezvu mikrokontroléru, je přesunout problém pryč od procesoru. V případě Pica existují některá vestavěná zařízení, která mohou používat GPIO linky k implementaci protokolů bez zapojení CPU. V této kapitole se blíže podíváme na pulsně šířkovou modulaci (Pulse Width Modulation PWM), což se dá použít k generování zvuku, buzení LED diod, řízení motorů a serv.

Při provádění jejich nejzákladnější funkce, tedy výstupu, mohou být GPIO linky procesorem nastaveny na vysokou nebo nízkou úroveň. Jak rychle je lze nastavit na vysoké nebo nízké, závisí na rychlosti procesoru.

Pomocí GPIO linky v režimu Pulse Width Modulation (PWM) můžete generovat pulsní sledy až do 60 MHz. Důvodem zvýšení rychlosti je to, že GPIO je připojeno k pulznímu generátoru a jakmile je nastaveno generování pulzů specifického typu, pulzní generátor pracuje samostatně, aniž by potřeboval jakýkoliv zásah ze strany GPIO linky nebo procesoru. Ve skutečnosti bude pulzní výstup pokračovat i po skončení vašeho programu.

Samozřejmě, i když PWM linka může generovat velmi rychlé pulsy, obvykle chcete změnit povahu pulsů a to je pomalejší proces, který musíme udělat pomocí procesoru.

Základní pojmy

Některé pojmy stojí za to si ujasnit hned od začátku, i když význam některých bude jasný až s postupem času.

Za prvé, co je PWM? Jednoduchá odpověď: je to obdélníkový signál opakující se pevnou rychlostí – řekněme jeden cyklus každou milisekundu, ale šířku pulzu vzhledem k pevné rychlosti můžeme modulovat, tj. lze ji změnit od 0% do 100%.

Při generovaném sledu pulzů je třeba specifikovat dvě základní věci, opakovací frekvenci a šířku každého pulzu. Obvykle je rychlost opakování nastavena jako jednoduchá perioda opakování (repeat rate) a šířka každého impulsu je specifikována jako procento z opakovací periody, označované jako pracovní cyklus nebo koeficient naplnění (duty cycle).

Takže například frekvence opakování 1 ms a 60% pracovní cyklus (koeficient naplnění) udává periodu 1 ms, kde výstup je na vysoké úrovni po 60 % času, tj. šířka pulzu je 0,6 ms. Jsou dva extrémy: 100% koeficient naplnění, tj. linka je vždy na vysoké úrovni, a 0% koeficient naplnění, tj. linka je vždy na nízké úrovni.

Základní pojmy — repeat rate, duty cycle (60%)

pojmy rr dc

Všimněte si, že pracovní cyklus nebo koeficient naplnění (duty cycle) nese informaci o pulsně šířkové modulaci a ne o frekvenci (repeat rate). To znamená, že obecně zvolíte rychlost opakování (frekvenci) a budete se jí držet, a to, co během běhu programu budete měnit, je koeficient naplnění.

Stejná frekvence — koeficient zaplnění 30%

pojmy rr dc30

V mnoha případech je PWM implementováno pomocí speciálního hardwaru tzv. PWM generátoru, který je zabudován buď do procesorového čipu, nebo poskytnutý externím čipem. Procesor jednoduše nastaví rychlost opakování zápisem do registru a poté pracovní cyklus (koeficient naplnění) zápisem do jiného registru. To obecně poskytuje nejlepší druh PWM bez zatížení procesoru a funguje spolehlivě bez závad. Můžete si dokonce zakoupit přídavné desky, které poskytnou další kanály PWM bez zvýšení zátěže procesoru.

Alternativou k vyhrazenému hardwaru PWM je jeho implementace v softwaru. Můžete docela snadno přijít na to, jak to udělat. Vše, co potřebujete, je nastavit časovou smyčku s opakovací frekvencí a v ní nastavit linku na vysokou úroveň po dobu koeficientu naplnění a znovu nastavit na nízkou úroveň po zbytek času. Můžete to implementovat buď pomocí přerušení nebo smyčky dotazování a pokročilejšími způsoby, jako je použití kanálu DMA (Direct Memory Access).

Inicializace PWM

V případě Raspberry Pi Pico jsou PWM linky implementovány pomocí speciálního PWM hardwaru. Má osm generátorů PWM, z nichž každý má dva výstupy PWM. Kteroukoli z GPIO linek (kromě GPIO22) lze použít jako linku PWM, což znamená, že v daném okamžiku můžete mít v provozu až 16 linek PWM. Věci jsou trochu složitější v tom, že každý pár výstupů má stejnou frekvenci, což znamená, že máte osm, frekvenčně nezávisle nastavitelných párů výstupů. Navíc lze jeden z výstupů použít jako vstup, což ovšem snižuje počet dostupných výstupů.

Generátory PWM jsou přiřazeny k pinům GPIO v pevném pořadí:

Tabulka 1. Přiřazení GPIO k PWM
GPIO 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

PWM kanál

0A

0B

1A

1B

2A

2B

3A

3B

4A

4B

5A

5B

6A

6B

7A

7B

GPIO

16

17

18

19

20

21

22

23

24

25

26

27

28

29

PWM kanál

0A

0B

1A

1B

2A

2B

3A

3B

4A

4B

5A

5B

6A

6B

Nemusíte vědět o tom, jak funguje hardware PWM, ale pomůže vám to pochopit některá omezení.

Chcete-li používat hardware PWM, musíte změnit příkaz libraries v CMakeLists.txt na:

target_link_libraries(myprogram
            pico_stdlib
            hardware_gpio
            hardware_pwm)

to znamená, že musíte přidat hardware_pwm do knihoven.

Musíte také přidat na začátek programu do zdrojového kódu:

#include "hardware/pwm.h"

Mnohé z následujících programů jsou variacemi na hlavní program, a proto je prezentován pouze hlavní program. Pokud chcete seznam celého programu, navštivte webovou stránku této knihy na adrese www.iopress.info.

Když povolíte funkce PWM na lince GPIO:

gpio_set_function(gpio, GPIO_FUNC_PWM);

„Plátek“ generátoru PWM, který získáte, je uveden v tabulce. To, se kterým segmentem a kanálem pracujete, je důležité, protože to musíte určit, abyste mohli nakonfigurovat hardware. Můžete se jen podívat na tabulku a zjistit, že když používáte konkrétní GPIO řadu, musíte použít konkrétní řez. Například, pokud používáte GP22, pak pracujete s 3A, což je výstup A třetího řezu. To znamená, že číslo řezu (slice) je 3 a číslo kanálu (channel) je 0.

Případně můžete použít funkce:

static uint 	pwm_gpio_to_slice_num (uint gpio)
static uint 	pwm_gpio_to_channel (uint gpio)

pro vrácení čísla řezu a kanálu pro hardware PWM připojený k určenému GPIO pinu. Například:

gpio_set_function(22, GPIO_FUNC_PWM);
slice   = pwm_gpio_to_slice_num (22);
channel = pwm_gpio_to_channel (22);

vrátí řez 3 a kanál 0.

Jakmile nastavíte funkci pinu a nastavíte PWM, můžete spustit a zastavit generování signálu pomocí:

pwm_set_enabled (uint slice_num, bool enabled)

Jsou chvíle, kdy potřebujete zapnout nebo vypnout více signálů PWM, a k tomu můžete použít:

pwm_set_mask_enabled (​​uint32_t mask)

Maska představuje osm PWM řezů jako prvních osm bitů.

Musíte také upravit instrukci target_link_libraries v souboru CMakeLists.txt, abyste mohli číst:

target_link_libraries(myprogram pico_stdlib hardware_pwm)

Konfigurace PWM

Níže můžete vidět zjednodušený model PWM generátoru. Není celý příběh, ale je to způsob, jak nejlépe myslet, když právě začínáte nebo když generujete jednoduché signály PWM:

pwm2

Způsob, jakým funguje 16bitový čítač (counter), je klíčem k pochopení generování pulsně šířkové modulace (PWM). Výchozí takt procesoru u Raspberry Pi Pico je 125MHz a dělič (divider) je inicializován na 1, což znamená, že čítač je sešlápnutý každých 8ns. Chování čítače můžete upravit nastavením hodnoty obtékání:

pwm_set_wrap (uint slice_num, uint16_t wrap)

wrap je nejvyšší hodnota, do které bude čítač počítat, než přeskočí na nulu nebo začne směrem odpočítávat k nule. Tato dvě různá chování se řídí povolením nebo zakázáním režimu „fázově správné“ (phase correct), název bude vysvětlen později:

pwm_set_phase_correct (uint slice_num,  bool phase_correct)

Je-li phase_correct deaktivována (0) má za následek, že počítadlo počítá až do hodnoty wrap a poté se vynuluje. Je-li phase_correct povolena (1) způsobí, že čítač počítá až do hodnoty wrap a poté počítá dolů na nulu.

Signál PWM je generován z hodnoty čítače nastavením úrovně kanálu:

pwm_set_chan_level (uint slice_num, uint chan,  úroveň uint16_t)

nebo pro oba kanály současně:

pwm_set_both_levels (uint slice_num, uint16_t level_a,  uint16_t level_b)

K dispozici je také pomocná funkce:

pwm_set_gpio_level (uint gpio, uint16_t level)

který nastaví úroveň zadané linky GPIO, aniž by vám byl sdělen výsek nebo kanál.

Pokaždé, když čítač překročí úroveň, změní se stav výstupní linky. Linka je nastavena vysoko, když začíná počítání, a je nastavena nízko, když je úroveň překročena. Počítání pokračuje a po restartu je linka opět nastavena vysoko. Můžete to vidět v akci v nefázovém správném režimu na níže uvedeném schématu:

Fázově nekorektní režim

PWM generace fnc

Ve fázově správném režimu máme podobný diagram:

Fázově korektní režim

PWM generace fc

Můžete vidět, že hodnota wrap čítače nastavuje frekvenci a úroveň level nastavuje koeficient naplnění. Je také zřejmé, že ve fázově správném režimu je frekvence poloviční. Nyní můžete také vidět důvod, proč se nazývá fázově správný režim. V režimu fázové nekorektnosti, když změníte pracovní cyklus, začátek pulzu zůstane pevný, ale střed pulzu se posune. V režimu fázově korektní se začátek a konec pulsu pohybuje, ale střed pulsu zůstává neměnný – jeho fáze se nemění. Existují aplikace, kde na tomto rozdílu záleží, ale pro většinu situací PWM můžete rozdíl ignorovat.

Dalším jemným bodem je, že můžete kdykoli změnit úroveň, ale změna se projeví pouze tehdy, když se počítadlo vynuluje, takže nemůžete náhodně dostat dva pulzy během jednoho cyklu počítání.

Jak již bylo uvedeno, hodnota wrap určuje frekvenci. Nepotřebujete příliš mnoho matematiky, abyste zjistili, že v režimu, který není fázově správný je to:

\(f = \frac{f_c}{wrap+1}\)

kde \(f_c\) je frekvence hodin po zohlednění jakéhokoli dělení. (Pokud je dělitel roven 1, je to frekvence procesoru 125 MHz). Ještě užitečnější je, že můžete vypočítat wrap potřebný k poskytnutí jakékoli frekvence PWM:

\(wrap = \frac{f_c}{f-1}\)

Podobně úroveň nastavuje pracovní cyklus:

\(level = wrap * duty\)

kde duty je pracovní cyklus jako zlomek.

Například s taktovací frekvencí 125 MHz v režimu s nefázovou korektností můžete generovat PWM signál 10 kHz s použitím wrap = 12500 a 25% pracovní cyklus znamená úroveň 3125.

V režimu fázově korektní musí být vzorce upraveny tak, aby zohledňovaly počítání nahoru i dolů, což snižuje frekvenci na polovinu:

\(f = \frac{f_c}{2(wrap+1)}\)

\(wrap = \frac{f_c}{2f} - 1\)

Fázově nekorektní režim: f=50Hz duty=75%

Screenshot 20241017 032227 xyz.fhdm.scoppy

Fázově korektní režim: f=25Hz duty=75%

Screenshot 20241017 033125 xyz.fhdm.scoppy

To samé pro srovnání

Screenshot 20241017 035618 xyz.fhdm.scoppy

Dělení hodin

Čítač PWM je 16 bitový a to znamená, že jakmile dosáhneme čísla 65534, tak čítač začne čítat znovu od 0 a nemůžete už snížit frekvenci. Vzhledem k hodinovému vstupu 125 MHz to dává nejnižší frekvenci PWM na 1,9 kHz. To není moc dobré, pokud potřebujete 50 Hz signál pro řízení serva, viz dále. Řešením je pomocí hodinového děliče snížit takt 125MHz na něco nižšího.

Dělitel hodin je 16bitový zlomkový dělitel s osmi bity pro celočíselnou část a čtyřmi bity pro zlomek. Hodiny můžete nastavit pomocí:

pwm_set_clkdiv_int_frac (uint slice_num, uint8_t integer, uint8_t fract)

a pokud chcete zadat oddělovač jako hodnotu s plovoucí desetinnou čárkou, můžete použít:

pwm_set_clkdiv (uint slice_num, float div)

Dělitel hodin je složen z 8bitového celého čísla a 4bitové zlomkové části, která určuje zlomek jako frakt/16. To znamená, že největší dělitel je 255 15/16, což dává nejnižší hodinovou frekvenci 522 875,81 Hz

Výše uvedené vzorce můžete použít k výpočtu zalomení (wrap) a úrovně, pokud \(f_c\) je výsledný čas po dělení. Pokud například nastavíte dělitel hodin na 2 s taktovací frekvencí 125 MHz v režimu bez fázově správného nastavení, můžete generovat signál PWM 5 kHz s použitím wrap 12500 a 50% pracovní cyklus znamená úroveň 6250.

To je dost jednoduché, ale všimněte si, že nyní obvykle máte více než jeden způsob, jak generovat jakoukoli konkrétní frekvenci PWM – jak byste si měli vybrat hodinový dělitel? Odpověď je v tom, jaké rozlišení pracovního cyklu můžete nastavit. Předpokládejme například, že zvolíte dělitel, což znamená, že k dosažení požadované frekvence musíte použít wrap 2. Pak můžete nastavit pouze pracovní cykly 0, 1/3, 2/3 nebo 100 %, což odpovídá úrovně 0, 1, 2 a 3. Pokud chcete nastavit více pracovních cyklů, pak je jasné, že musíte mít obal co největší. Ve skutečnosti je snadné vidět, že rozlišení v bitech pracovního cyklu je dáno \(log_2\) wrapem, a protože maximální hodnota wrapu je 65 535, je tedy maximální rozlišení 16 bitů, což je velikost čítače (counteru).

Když to všechno dáte dohromady, uvidíte, že byste měli vždy zvolit dělitel (divider), který vám umožní použít největší hodnotu zalomení (wrap) pro generování frekvence, kterou hledáte.

Jinými slovy, dělitel (div) by měl být zvolen tak, aby byl větší než frekvence, kterou potřebujete, ale co nejblíže. Jinými slovy:

\(div = \frac{Ceil(\frac{16 \cdot f_c}{65536\cdot f_{pwm}})}{16}\)

\(div = \frac{Ceil(\frac{fc}{4096\cdot f_{pwm}})}{16}\)

Kde \(f_c\) je hodinová frekvence, \(f_pwm\) je požadovaná frekvence a Ceil je funkce, která vrací celé číslo jen větší, než je její argument (celočíselné zaokrouhlení nahoru).

Například, pokud chceme signál PWM při 50 Hz, výpočet je:

\(div = \frac{Ceil(\frac{125000000}{4096*50})}{16} = \frac{611}{16} = 38.1875\)

Pokud nastavujete dělič pomocí celého čísla a 4bitové zlomkové části, pak je to hodnota 611, která je užitečná, protože její spodní čtyři bity 0011 udává zlomkovou část. Takže dělič hodin je nastaven pomocí:

pwm_set_clkdiv_int_frac (slice_num, 38.3);

Použití tohoto dělitele poskytuje efektivní hodinovou frekvenci:

Pomocí toho nyní můžeme vypočítat potřebný obal:

Pokud to vyzkoušíte, zjistíte, že:

pwm_set_clkdiv_int_frac (slice_num, 38.3);
pwm_set_wrap(slice_num, 65465);
pwm_set_chan_level(slice_num, PWM_CHAN_A, 65465/2);

vytváří vlnovou formu PWM s frekvencí 50 Hz:

pwm5

Funkce pro nastavení frekvence a koeficientu zaplnění (duty cycle)

Ve většině případů stačí jednoduše nastavit frekvenci PWM a koeficient zaplnění (duty cycle) a nemusíte počítat co nejlepší frekvenci hodin, wrap a level pro danou úlohu, ale někdy je to nezbytné. V mnoha případech si vystačíte s funkcí, která používá vzorce ukázané výše v předchozím oddílu.

uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0 )
        divider16 = 16;
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xF);
    pwm_set_wrap( slice_num, wrap);
    pwm_set_chan_level( slice_num, chan, wrap * d / 100);
return wrap;
}

Tohle funguje tak, že nejprve spočítáme dělitele před dělením 16, t.j. divider16. Poznamenejme, že:

+ (clock % ( f * 4096) != 0 )

je standardní způsob jak zaokrouhlit nahoru kladné číslo, protože to přičte jedničku, pokud existuje zbytek po dělení. Příkaz if testuje, jesli je dělitel větší než 1 a zda nastavujeme divider16 na minimální hodnotu. Dále počítáme hodnotu zalomení wrap, kterou potřebujem k výpočtu požadované frekvence při daném děliteli. A nakonec nastavíme pomocí pwm funkcí hodnoty dělitele hodin (pwm_set_clkdiv_frac()), zalomení wrap (pwm_set_wrap()) a úrovně počítání level (pwm_set_chan_level()). Vypočtená hodnota wrap je vrácena funkcí pwm_set_freq_duty(), aby mohl program tuto hodnotu zkontrolovat, že je nastavena s dostatečnou citlivostí. Např. nastavíme PWM na 50 Hz a koeficient naplnění 75% takto:

pwm_set_freq_duty( slice_num, chan, 50, 75);

Celý program bude vypadat takto:

pwm1.c
/* pwm1.c
*   základní funkce
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"

uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint8_t pin = 16;

int main()
{
    gpio_set_function(pin, GPIO_FUNC_PWM);
    uint8_t slice   = pwm_gpio_to_slice_num (pin);
    uint8_t channel = pwm_gpio_to_channel (pin);
    pwm_set_freq_duty(slice, channel, 50, 75);
    pwm_set_enabled (slice, true);
    return 0;
}

Nezapomeňte do CMakeLists.txt přidat knihovnu hardware_pwm.

Používáme více PWM linek současně

Každý PWM slice má jednoho dělitele a wrap, ale každý z kanálů může mít nastavenu různou hodnotu level. To znamená, že oba kanály A a B jednoho a téhož slice beží se stejnou frekvencí, ale mohou se lišit koeficientem naplnění. To může vypadat poněkud omezující, ale je to přesně to potřebuje většina aplikací. Například pokud řídíte dva servomotory, jeden kanálem A a druhý kanálem B, potom frekvence pro oba motory je stejná, ale můžete měnit koeficient naplnění pro každý motor zvlášť.

To však vyvolává malý problém, protože nastavení koeficientu naplnění závisí tom, že známe hodnotu wrap, která byla kdysi nastavena. Jedním z řešení je přečíst si wrapm registr, ale SDK na to nemá zatím žádnou funkci. Naštěstí je poměrně snadné takovou funkci naprogramovat:

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

Pomocí této funkce můžete nastavit koeficient naplnění bez toho, že byste znali wrap anebo dělič hodin. Například:

int d=25;
pwm_set_chan_level( slice_num, chan, pwm_get_wrap( slice_num) * d/100);

a hned můžete snadno napsat funkci k nastavení koeficientu naplnění:

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}

Použítí funkce je opět snadné, například:

pwm_set_duty( slice_num, chan, 50 );

nastaví 50% koeficient naplnění bez toho, že by se měnila frekvence nebo znovu nastavovala hodnota wrap.

Demonstrace dvou nezávislých linek PWM je v nasledujícm programu:

pwm4.c
/* pwm4.c
*   základní funkce
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"

uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}


uint8_t pin1 = 20;
uint8_t pin2 = 21;

int main()
{
    gpio_set_function(pin1, GPIO_FUNC_PWM);
    gpio_set_function(pin2, GPIO_FUNC_PWM);
    uint slice_num   = pwm_gpio_to_slice_num (pin1);
    uint channel20 = pwm_gpio_to_channel(pin1);
    uint channel21 = pwm_gpio_to_channel(pin2);
    pwm_set_freq_duty(slice_num, channel20, 50, 75);
    pwm_set_duty(slice_num, channel21, 25);
    pwm_set_enabled (slice_num, true);
    return 0;
}
Výsledek na logickém analyzátoru (fázově nekorektní)

Screenshot 20241017 073726 xyz.fhdm.scoppy

Porovnejte to s tím, když dodáme do programu fázově korektní režim:
pwm_set_phase_correct( slice_num, true );.

Výsledek na logickém analyzátoru (fázově korektní)

Screenshot 20241017 074638 xyz.fhdm.scoppy

Nyní je krásně vidět, že pulsy nazačínají ve stejném okamžiku, ale jsou kolem jednoho okamžiku vycentrovány.

Podobně jako můžeme měnit úrověň level každého kanálu, můžeme měnit i polaritu pulsů pomocí funkce:
pwm_set_output_polarity( uint slice_num, bool a, bool b).

Můžeme tak dostat pusly kanálů A a B do protifáze, například takto:

pwm_set_output_polarity(slice_num, false, true );
Impulsy kanálů A a B v protifázi

Screenshot 20241017 080544 xyz.fhdm.scoppy

Zde vidíte, že pulsy v kanálu B jsou invertované.

Změna koeficientu naplnění (duty)

Z důvodů, které budou uvedeny později, ve většině případů řízení je potřeba měnit koeficient naplnění (duty) anebo periodu sledu pulsů. Z toho plyne otázka, jak rychle to můžeme dělat, jak rychle můžeme měnit charakteristiku PWM linky? Neboli jak rychle můžeme měnit koeficient naplnění. Není jednoduché na tuto otázku přesně odpovědět, ale ve většině případů nemá přesná odpověď příliš velký význam. Důvodem je, že PWM signál předává informaci v celých cyklech s daným koeficientem zaplnění za cyklus. Proto jsou obvykle často v aplikacích cykly zprůměrovány.

Máme ještě jeden problém a to synchronizaci. Je to jemnější než se na první pohled zdá. Hardware nemůže změnit koeficient naplnění dokud není současný cyklus dokončen, to znamená dokud počítadlo (counter) nedosáhne 0. Když změníte koeficient naplnění dříve, než bude cyklus dokončen (počítadlo dosáhne 0), bude to mít následující efekt.

Můžete se domnívat, že následující program bude fungovat a přepínat mezi dvěma různými koeficienty naplnění u jednotlivých cyklů. Tj. 1. cyklus bude mít duty 25%, druhý cyklus 50%, třetí cyklus 25% atd.

pwm7.c
/* pwm7.c
*   Změny koeficientu naplnění při 500 Hz
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/irq.h"
#include "math.h"

uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}

uint8_t pin = 20;


int main()
{
    gpio_set_function( pin, GPIO_FUNC_PWM );
    uint slice_num   = pwm_gpio_to_slice_num ( pin );
    uint chan        = pwm_gpio_to_channel( pin );

    uint wrap = pwm_set_freq_duty(slice_num, chan, 500, 50);
    pwm_set_enabled (slice_num, true);
    while( true )
    {
        pwm_set_duty( slice_num, chan, 25 );
        pwm_set_duty( slice_num, chan, 50 );
    }

return 0;
}

Pohledem na datový analyzátor budete překvapeni.

Frekvence 500Hz

Screenshot 20241017 131925 xyz.fhdm.scoppy

Nedostáváme jeden cyklus s 25% a druhý s 50%, ale množinu všelijak se míchajících puslů. Důvod je, že koeficient naplnění se mění asynchronně s wrapem. To znamená, že skutečné naplnění závisí na předchozí změně. Aby to fungovalo, musíme detekovat, kdy counter dosáhne nuly, a měnit duty, přesně, kdy je detekována 0 na counteru. Aby jsme to mohli provést, potřebujeme k tomu funkce pro práci s counterem.

Poznámka: Při mých pokusech na frekvenci 50 Hz se pulsy krásně střídaly (25%, 50%, 25%,...),
ale na frekvenci 500 Hz již ne. Můžete si to vyzkoušet sami.

Práce s počitadlem (counterem)

Je jasné, že pokud chceme měnit duty synchronně, potřebujeme přístup ke stavu počitadla (counteru). SDK na to má následující funkce:

  • static uint16_t pwm_get_counter ( uint slice_num )

  • pwm_set_counter( uint slice_num, uint16_t c )

  • pwm_advance_count( uint slice_num )

  • pwm_retard_count( uint slice_num )

Nejužitečnější je funkce pwm_get_counter, která vrací aktuální hodnotu počitadla v daný okamžik zavolání. Tu můžeme používat na testování, v jaké čísti periody se PWM nachází.

Méně používané jsou pwm_set_counter, pwm_advance_count a pwm_retard_count. Ty se používají na změnu počitadla (counteru). pwm_set_count uloží do counteru natvrdo. pwm_advance_count přičte k současné hodnotě counteru jedničku a pwm_retard_count odečte od součané hodnoty counteru jedničku. K čemu je to dobré? Nastavením counteru na novou hodnotu můžeme měnit okamžitou frekvenci PWM signálu, což je něco, co se používá při signalizaci pomocí PWM nebo při vytváření hudebních signálů. Funkce pwm_advance_count a pwm_retard_count fungují tak, že blokují hodinové tiky a při tom nemění hodnotu counter registru, je tedy potřeba, aby dělič (divider) byl větší než 1.

Vylepšíme příklad z předchozího oddílu, použijeme funkci pwm_get_counter, kterou budeme testovat, zda je counter 0 a v tomto okamžiku upravit koeficient naplnění (duty).

pwm8.c
/* pwm7.c
*   Změny koeficientu naplnění při 500 Hz
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/irq.h"
#include "math.h"

uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}

uint8_t pin = 20;


int main()
{
    gpio_set_function( pin, GPIO_FUNC_PWM );
    uint slice_num   = pwm_gpio_to_slice_num ( pin );
    uint chan        = pwm_gpio_to_channel( pin );

    uint wrap = pwm_set_freq_duty(slice_num, chan, 500, 50);
    pwm_set_enabled (slice_num, true);
    while( true )
    {
        while( pwm_get_counter( slice_num ) ) {};
        pwm_set_duty( slice_num, chan, 25 );
        while( pwm_get_counter( slice_num ) ) {};
        pwm_set_duty( slice_num, chan, 50 );
    }

return 0;
}

Koeficient naplnění se nastavuje hned po tom, co counter dosáhnul 0, pulsy se krásně střídají (ale jenom při nízké frekvenci).

50 Hz

Screenshot 20241017 142131 xyz.fhdm.scoppy

500 Hz (tady to trochu zlobilo)

Screenshot 20241017 142532 xyz.fhdm.scoppy

Jaký je nejvyšší kmitočet PWM signálu, kde se koeficient naplnění mění v každém pulsu? Je překvapivě nízký, okolo 300 Hz To můžeme vylepšit tak, že optimalizujeme program, ale nebude to slavné. Lepší způsob je použít přerušení.

PWM přerušení

Při okamžité změně koeficientu naplnění je použití přerušení (IRQ) rychlejší než čtecí smyčka (polling). Je to proto, že přečtení hodnoty počitadla pomocí funkce pwm_get_counter() je pomalé ve srovnání s tím, když vyvoláme přerušení v okamžiku kdy počitadlo (counter) dosáhne 0. Mohli bychom udělat čtecí smyčku rychlejší než používání přesušení tak, že bychom používali událost na hodnotě počitadla, ale SDK má naprogramované přerušení a nikoliv události.

Funkce pro práci s PWM přerušeními:

  • pwm_set_irq_enabled( uint slice_num, bool enabled )

  • pwm_set_irq_mask_enabled( uint32_t slice_mask, bool enabled )

  • pwm_clear_irq( uint slice_num )

  • static uint32_t pwm_get_irq_status_mask( void )

  • pwm_force_irq( uint slice_num )

Důležité dvě funkce jsou pwm_set_irq_enabled() a pwm_clear_irq(). První zapíná přerušení na zadaném slice a druhá nuluje přerušení (To znamená obvykle, že vyvolané přerušení bylo zpracováno). Obecně je potřeba vždycky vynulovat přerušení před tím, než ho nastavíme a také ho vynulovat na začátku funkce obsluhy přerušení (MyIRQHandler()). Poku máme více PWM strojků (slices), tak můžeme najednou nastavit přerušení kde chceme pomocí funkce pwm_set_irq_mask_enabled(). Maska má jeden bit pro každý strojek (slice), 0. bit je pro slice 0 atd. Funkce pwm_get_iqr_status_mask() vrací stav u kterých slice jsou nastavena přerušení tím samým způsobem, jako pwm_set_irq_mask_enabled(). Nakonec funkce pwm_force_irq() spouští přerušení do softwarového řízení.

V tomto okamžiku se můžete divit, kde se vlastně funkce definující obsluhu přerušení nachází. GPIO linky mají takovou funkci pro svoji obsluhu přerušení, ale PWM slice používají více všeobecnou obsluhu přerušení na Picu. Funkce:

irq_set_exclusive_handler( uint num, irq_handler_t handler )

nastavuje obsluhu přerušení pro jedno přerušení na Picu. PWM strojky (slices) všechny používají IRQ 4 neboli PWM_IRQ_WRAP. To znamená, že libovolný PWM strojek způsobí přerušení 4 a volá obsluhu přerušení 4. Existuje mechanismus, který umožňuje více obsluhám přerušení sdílet jedno přerušení určitého čísla, ale nejjednodušší je používat irq_set_exclusive_handler k nastavení jedné obsluhy přerušení pro dané číslo přerušení. V případě PWM to můžeme udělat takto:

irq_set_exclusive_handler( PWM_IRQ_WRAP, MyIRQHandler );

Máme nastavenu obsluhu přerušení, takže můžeme přerušení začít používat pomocí funkce:

irq_set_enabled( uint num, bool enabled)

což konkrétně pro PWM přerušení bude:

irq_set_enabled(PWM_IRQ_WRAP, true);

Všimněte si, že aby přerušení začali fungovat musíme udělat dvě věci. Nejprve zapnout řerušení všeobecně a potom nastavit slice, která budou přerušení generovat. Nestačí jenom zapnout přerušení na slice. Celé to vypadá takto:

pwm_clear_irq( slice_num );                 (1)
pwm_set_irq_enabled( slice_num, true);      (2)
irq_set_exclusive_handler( PWM_IRQ_WRAP, MyIRQHandler ); (3)
irq_set_enabled( PWM_IRQ_WRAP, true );  (4)
1 Vynulování IRQ na slice.
2 Nastavení slice, že bude generovat přerušení
3 Nastavení obsluhy přerušení
4 Spuštění mechanismu přerušení.

Dáme to celé dohromady a napíšeme jednoduchý program, který bude měnit duty z 50% na 25% pro každý lichý a sudý puls pomocí přerušení. Nezapomeňte do CMakeLists.txt přidat hardware_pwm knihovnu.

pwm9.c
/* pwm9.c
*   Používání přerušení při změně koeficientu naplnění (duty) u jednotlivých pulsů
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/irq.h"
#include "math.h"

uint8_t pin = 20; // pin, se kterým budeme pracovat
uint slice_num;
uint chan;
uint state = 0;


uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}

// funkce pro zpracování přerušení
void MyIRQHandler()
{
    pwm_clear_irq( slice_num );
    if( state ) {
        pwm_set_duty( slice_num, chan, 25 );
    } else {
        pwm_set_duty( slice_num, chan, 50 );
    }
    state = ~state;
}

int main()
{
    gpio_set_function( pin, GPIO_FUNC_PWM );
    slice_num   = pwm_gpio_to_slice_num ( pin );
    chan        = pwm_gpio_to_channel( pin );

    pwm_clear_irq( slice_num );
    pwm_set_irq_enabled( slice_num, true );
    irq_set_exclusive_handler( PWM_IRQ_WRAP, MyIRQHandler );
    irq_set_enabled( PWM_IRQ_WRAP, true );

    uint wrap = pwm_set_freq_duty(slice_num, chan, 100000, 25); // 100 KHz !
    pwm_set_enabled (slice_num, true);
    while( true )
    {

    }

return 0;
}

Můžeme vidět, že obsluha přerušení jednoduše mění koeficient naplnění u každého pulsu tak, že mění z liché na sudou proměnnou state. Hned na začátku obsluhy nulujeme přerušení, když to nebudeme dělat, program se zakousne.

A funguje to při kmitočtu 100 KHz, což je podstatné vylepšení proti 300 Hz a čtecí smyčce.

Změna duty pulsů pomocí přerušení (100 kHz)

Screenshot 20241018 023005 xyz.fhdm.scoppy

Použití PWM — převod digitálního na analogový signál

Na jaké věci je PWM určeno? Dá se s tím dělat mnoho velmi promakaných věcí. Zaměříme se na dvě aplikace PWM — na modulaci časového průběhu napětí nebo výkonu a signalizování serv.

Množství výkonu dodaného do zařízení pomocí vláčku pulsů je přímo úměrné koeficientu naplnění (duty cycle). Vláček pulsů, které mají 50% koeficient naplnění dodá 50% výkonu v čase a je to nezávislé na kmitočtu. Vláček pulsů, které mají 25% koeficient naplnění dodá 25% výkonu atd.

Dodaný výkon určuje pouze koeficient naplnění (duty). Kmitočet hraje roli v mnoha situacích, např. pokud chceme zabránit blikání LED nebo škubání motoru, při vyšších frekvencích je řízení výkonu plynulejší.

Pokud přidáme na výstup PWM signálu dolní propust, tak dostáváme napětí, které je přímo úměrné koeficientu naplnění. Dolní propust odfiltruje složky signálu s vyšší frekvencí a ponechá jenom pomalé složky, které tu jsou díky modulaci koeficientu naplnění.

Jak rychle to bude pracovat závisí na tom, jak rychle můžeme měnit koeficient naplnění, čili na rozlišení koeficientu naplnění. Pokud budem pracovat s 8 bitovým rozlišením, náš digitálně analogový převodník bude mít 256 různých úrovní, což nám při 3.3V na výstupu dává 3.3/256 = 13mV. To je docela dobré. Budeme-li mít wrap rovno 255 a děličku hodin nastavenou na 1, tak nám to dává vzrokovací frekvenci přibližně 488 KHz. PWM výstup v této konfiguraci bude imitovat 8 bitový diditálně annalogový převodník. Můžeme nastavovat koeficient naplnění od 0 do 256 jako úroveň u a pro výstupní úrověň napětí bude platit \(\frac{3.3\cdot u}{256} V\). Podle Nyquistova—​Shannonova vzorkovacího teorému nám to dává teoreticky použitelnou frekvenci 488/2 = 244 kHz, ale v praxi je tato hodnota nižší. Jako pravidlo můžeme brát, že nejvyšší frekvence, kterou můžeme generovat, je 7x menší než je vzorkovací frekvence, což je okolo 70 kHz.

Pokud bychom používali 16 bitové rozlišení, které například vyžaduje audio v CD kvalitě, tak nám vzorkovací frekvence klesne na 1.9 KHz, což je mnohem méně než 44 KHz, kterou CD kvalita audio signálu vyžaduje.

Jako demonstraci tohoto přístupu si uděláme převod digitálního signálu na analogový, následující program bude vytvářet sinusovku. Na to potřebujeme spocítat koeficient naplnění v 256 bodech, abychom měli kompletní cyklus změny koeficientu naplnění. Mohli bychom to udělat při každém změně duty, ale to by byl program velmi pomalý (operace v plovoucí řádové čárce nejsou rychlostně silnou stránkou Pica, počítají to knihovní funkce a ne hardware). Proto si spočítáme koeficienty naplnění dopředu a uložíme je do pole o 256 buňkách. Protože potřebujeme aktualizovat duty jednou za vzorek, tak nejjednoduší řešení je použít obsluhu přerušení, aby dělala tuto práci.

pwm10.c
/* pwm10.c
*   Sinusovka
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/irq.h"
#include "math.h"


void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}

uint slice_num;
uint chan;
uint state = 0;
uint8_t wave[256];

uint8_t pin = 20;

void MyIRQHandler()
{
    pwm_clear_irq(slice_num);
    pwm_set_duty(slice_num, chan, wave[state]);
    state = (state + 1) % 256;
}

int main()
{
    for( int i = 0; i < 256; i++ )
    {
        wave[i] = (uint8_t) ((128.0 + sinf((float)i * 2.0 * 3.14159 / 256.0) * 128.0) * 100.0/256.0); (1)
    }

    gpio_set_function( pin, GPIO_FUNC_PWM );
    slice_num   = pwm_gpio_to_slice_num ( pin );
    chan        = pwm_gpio_to_channel( pin );

    pwm_clear_irq( slice_num );
    pwm_set_irq_enabled( slice_num, true );
    irq_set_exclusive_handler( PWM_IRQ_WRAP, MyIRQHandler);
    irq_set_enabled( PWM_IRQ_WRAP, true );

    pwm_set_clkdiv_int_frac( slice_num, 1, 0 );
    pwm_set_wrap( slice_num, 255 );
    pwm_set_enabled (slice_num, true);
    while( true )
    {

    }

return 0;
}
1 Výpočet koeficientu naplnění a uložení do buňky pole

Na výslednou sinusovku se můžeme podívat pomocí osciloskopu.

Sinusovka (s filtrem vysokých frekvencí)

Screenshot 20241017 112945 xyz.fhdm.scoppy

Vysoké frekvence nad 30 kHz je potřeba odfiltrovat pomocí dolní propusti s hodnotami \(R = 220k\Omega\) a \(C = 22pF\). Mezní frekvence propusti je \(f_0 = \frac{1}{2 \pi \cdot R \cdot C} = \frac{1}{2 \cdot 3.14159 \cdot 220000 \cdot 0.000000000022} = 32.8 kHz\). Je to sice trochu vysoko pro frekvenci sinusovka 2 kHz, ale jinak je sinusovka docela hezká.

Bez filtru dostaneme na výstupu toto:

"Sinusovka?" bez filtru

Screenshot 20241017 124122 xyz.fhdm.scoppy což jako sinusovka vůbec nevypadá.

Touto technikou můžete vyrábět sinusovky, nebo zcela jiné průběhy jak chcete, ale pro kvalitu HiFi potřebujete vyšší vzorkovací frekvenci.

Frekvenční modulace (hudba)

Použití PWM k vytváření hudebních tónů a zvukových efektů je docela složitá věda, která se vymyká našemu kurzu. V mnoha případech budeme měnit koeficient naplnění při konstantní frekvenci, ale můžeme pracovat i s tím, že necháme koeficient naplnění třeba na 50% a budeme hýbat s frekvencí samplování. Touto technikou můžete vytvářet jednoduché tóny a stupnice.

Frekvence jednočárkovaného C (v hudbě) je 281.6 Hz, takže generovat jednočárkové C můžeme třeba takto:

int main()
{
    gpio_set_function(22, GPIO_FUNC_PWM);
    uint slice_num = pwm_gpio_to_slice_num(22);

    uint chan = pwm_gpio_to_channel(22);
    pwm_set_freq_duty( slice_num, chan, 281, 50 );

    pwm_set_enabled( slice_num, true );
}

Výsledek bude obdélníkový průběh s změřitelnou frekvencí 281 Hz, ale není to k poslouchání. Zlepšení lze dosáhnout zařazením nízkopásmového filtru (dolní propusti) podobně jako jsme to udělali když jsme vytvářeli sinusovku. Můžete si spočítat frekvence pro ostatní noty, uložit je do tabulky a potom podle tabulky generovat tóny.

Řízení LED

PWM můžeme používat k řízení fyzikálních veličin, jako například k řízení svítivosti LED nebo řízení rychlosti stejnosměrných motorů. Jediný rozdíl mezi těmito aplikacemi a předchozím řízením napětí je v tom, že musíme zjistit, jaká je závistost mezi koeficientem naplnění (duty) a fyzikální veličinou, kterou chceme řídit. Jinak řečeno, třeba chci zvětšit svítívost LED o 50%, o kolik musím zvětšit koeficient naplnění? (Mezi tím nemusí být lineární závislost.)

Jednoduchoučký příklad řízení LED pomocí PWM signálu.

pwm11.c
/* pwm11.c
*   řízení LED diody primitivním způsobem
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/irq.h"
#include "math.h"

uint8_t pin = 20; // pin, se kterým budeme pracovat
uint slice_num;
uint chan;
uint state = 0;


uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}


int main()
{
    gpio_set_function( pin, GPIO_FUNC_PWM );
    slice_num   = pwm_gpio_to_slice_num ( pin );
    chan        = pwm_gpio_to_channel( pin );

    uint wrap = pwm_set_freq_duty(slice_num, chan, 2000, 0); // 2 KHz
    pwm_set_enabled (slice_num, true);
    while( true )
    {
        for( int d=0; d<=100; d++ ) {
            pwm_set_duty(slice_num, chan, d);
            sleep_ms(50);
        }
    }

return 0;
}

Při pokusech s LED, nezapomeňte zapojit rezistor omezující proud (třeba \(220 \Omega\)) mezi pin 20 a LED. Pokud byste chtěli používat interní LED na pinu GPIO 25, tak na obyčejném Picu to jde, ale na Picu W to nepůjde, protože u Pica W není interní LED řiditelná pomocí PWM. Jak je vidět z videa, tak řízení je takové, že nejdříve LED svítí málo, ale rychle naběhne do plného svícení a potom už změny svítivosti nejsou téměř žádné (bohužel kamera si sama nastavuje clonu, takže je to na videu ještě horší než ve skutečnosti). Znamená to, že máme nelineární závislost mezi koeficientem naplnění a svítivostí.

Změnou koeficientu naplnění v PWM vláčku pulsů měníme výkon dodávaný do LED (nebo do jiného podobného zařízení) lineárně. Bohužel 50% duty neznamená 50% svítivost. (Obdobně to bude s otáčkami při řízení stejnosměrných motorů.) Nebyli bychom inženýři a programátoři, kdybychom se s tím nedokázali vypořádat. Ve fyzice existuje Weber-Fechnerův zákon, který říká že vztah mezi vnímanou intezitou světla naším okem a fyzikální intenzitou je logaritmická. V případě LED je spojení mezi koeficientem naplnění a svítivostí komplikovanou záležitostí, ale bylo vypozorováno, že vnímaná intenzita světla má inverzně kubickou závislost na koeficientu naplnění.

\(d = k\cdot b^3\), kde d je koeficient naplnění (duty), k je konstanta pro každou LED trochu jiná a b je svítivost.

Graf závislosti LED svítivosti b na koeficientu naplnění duty

graf svitivost led duty

Program implementující kubické stmívání:

pwm12.c
/* pwm12.c
*   řízení LED diody kubické stmívání
*/
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/irq.h"
#include "math.h"

uint8_t pin = 20; // pin, se kterým budeme pracovat
uint slice_num;
uint chan;
uint state = 0;


uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}


int main()
{
    gpio_set_function( pin, GPIO_FUNC_PWM );
    slice_num   = pwm_gpio_to_slice_num ( pin );
    chan        = pwm_gpio_to_channel( pin );

    uint wrap = pwm_set_freq_duty(slice_num, chan, 200, 0); // 200 Hz
    pwm_set_enabled (slice_num, true);

    int d = 0;
    while( true )
    {
        for( int b=0; b<=100; b++ ) {
            d = (b * b *b)/10000;
            pwm_set_duty(slice_num, chan, d);
            sleep_ms(100);
        }
    }

return 0;
}

Po vyzkoušení můžeme pozorovat, že subjektivně vnímaná svítivost LED je rovnoměrnější po celém rozsahu. Problém je jenom v tom, že rozlišení 100 stupňů není dostatečné k nastavení svítivosti na začátku, kdy LED svítí málo. Řešením je pracovat přesněji s rozlišením koeficientu naplnění.

V mnoha případe je jedno, jak přesně lineárně odpovídá svítivost LED koeficientu naplnění, prosté přiblížení k lineárnosti vypadá mnohem lépe pro lidské oko. Lze také plně odstoupit od kubického průběhu při stmívání LED. Jediná vyjimka je, když se snažíte řídit LED abyste udělali stupnici šedé nebo barevný displej s barevnou kalibrací, potom je vyžadována jiná úroveň přesnosti.

Otázkou bývá také, jakou frekvenci použít. Je jasné, že je potřeba použít frekvenci takovou, abychom neviděli blikání a to znamená, že frekvence by měla být vyšší než 80 Hz, což je horní hranice citlivosti na blikání pro lidské oko. Čím rychleji je LED přepínána, tím méně blikání je vidět, hlavně na pohybujících se objektech. To je důležité třeba při snímání kamerou. Proti tomu ale vystupuje spínací rychlost samotné LED, která při frekvenci větší než 1 kHz může celý proces zabít. Ukazuje se, že frekvence pod 1 kHz je optimální.

Pokud potřebujete stmívat cokoliv většího, než malou LEDku, potřebujete nějaký budič. Můžeme použít i jednoduchý budič s bipolárním tranzistorem, jako je na obrázku. Obvod je potřeba správně dimenzovat podle řízeného výkonu.

Příklad řízení LED bipolárním tranzistorem

tranzistor npn led

Praktické řešení řízení podsvitu LCD displeje pomocí PWM, které jsem použil v projektu Monochromatický LCD displej 128x64 ST7920 (sběrnice SPI).

PWM vstup

Kanál B každého PWM strojku (slice) lze použít jako vstup. V tomto režimu vypadá blokový diagram strojku následovně:

Blokový diagram strojku (slice) ve vstupním režimu

blokovy diagram BA

Signál přicházející na vstup B se používá ke změnám počítání:

  • začni počítat když je vstup B ve stavu 1 — v tomto režimu vstup řídí čítač

  • zvětši o jedničku čítač, když na vstup B přijde náběžná hrana

  • zvětši o jedničku čítač, když na vstup B přijde sestupná hrana

K nastavení vstupního režimu se používá funkce pwm_set_clkdiv_mode:

pwm_set_clkdiv_mode( uint slice, enum pwm_clkdiv_mode mode )

kde výčet pwm_clkdiv_mode může být:

  • PWM_DIV_FREE_RUNNING

  • PWM_DIV_B_HIGH

  • PWM_DIV_B_RISING

  • PWM_DIV_B_FALLING

První volba je obvyklá konfiguracem kdy hodinový signál na vstupu B jednoduše implementuje čítač.

K čemu je to dobré? Je to nejjednodušší cesta, jak používat alternativní hodiny pro PWM generátor. Pokud je nastaven režim PWM_DIV_B_RISING nebo PWM_DIV_B_FALLING, tak je čítač zvýšen o jedničku při každém příchodu náběžné resp. sestupné hrany na vstup. Důležité je, že dělička hodin je stále v činnosti signál na vstupu je dělen nastavenou hodnotou děličky.

Předpokládejme například konfiguraci, kdy vstupní signál o kmitočtu 1 kHz je směrován na vstup B. Je-li dělička nastavena na hodnotu 1 a čítač má nastavenu hodnotu wrap na 127 a úroveň level je 63 a režim počítání je nastaven na PWM_DIV_B_RISING, potom na výstupu je 1000/128 = 7.8 Hz a koeficient naplnění (duty) je 50%. Nastavíme-li děličku na hodnotu 2, potom výstupní frekvence bude 1000/(2*128) = 3.9 Hz a tak dále:

#include "pico/stdlib.h"
#include "hardware/pwm"

int main()
{
    gpio_set_function(20, GPIO_FUNC_PWM);
    gpio_set_function(21, GPIO_FUNC_PWM);
    uint slice_num = pwm_gpio_to_slice_num(20);

    uint chanA = pwm_gpio_to_channel(20);
    uint chanB = pwm_gpio_to_channel(21);
    pwm_set_clkdiv_int_frac( slice_num, 2, 0);
    pwm_set_wrap( slice_num, 127 );
    pwm_set_chan_level( slice_num, chanA, 63);
    pwm_set_clkdiv_mode( slice_num, PWM_DIV_B_RISING );
    pwm_set_enabled( slice_num, true);
}

Výstupní frekvence na pinu 20 nyní závisí na vstupním signálu do pinu 21 (B input) a můžete to použít k vytváření proměnlivého kmitočtu PWM signálu. Přesněji řečeno, výstupní signál (na pinu 20) závisí na signálu přiváděnému na vstup B (pin 21) a nastavení děličky a hodnoty wrap. Koeficient naplnění (duty) závisí na úrovni (level) jako obvykle.

PWM vstup PWM_DIV_B_HIGH

Pokud umožníme, aby byl čítač řízen po námi definovaný čas pomocí PWM_DIV_B_HIGH, můžeme teoreticky použít PWM strojek (slice) k měření koeficientu naplnění nebo frekvence vstupního signálu. V Pico SDK je na to příklad pico-examples/pwm/measure_duty_cycle, ovšem nejsou zde zdůrazněny podmínky, při kterých může správně fungovat. Režimu PWM_DIV_B_HIGH se chová jinak než PWM_DIV_B_RISING či PWM_DIV_B_FALLING. Nefunguje jako vstupní hodiny, ale jako brána k systémovým hodinám. To znamená, že PWM hodiny jsou poskytovány obvyklou cestou pomocí systémových hodin, děličky a čítače. Avšak čítač počítá jenom tehdy, je-li vstup B PWM kanálu na vysoké úrovni (1). To znamená, že PWM strojek (slice) je nastaven k počítání času jenom tehdy, kdy je vstup B na vysoké úrovni. Například jestliže spustíme stroje s nastavenými hodinami přibližně na 488 KHz, počkáme 10 ms a potom ho vypneme a přečteme si čítač, co nám to řekne? Předpokládjme, že count je 100, to znamená , že vstup B byl na vysoké úrovni po dobu 100 puslů hodin, neboli \(100\cdot 0.2 \mu s = 20 \mu s\). Z tohoto měření nemůžeme odvodit frekvenci, jednoduše nám to říká, že signál na vstupu B byl ve stavu vysoké úrovně po dobu 10 ms. Avšak, jestliže budeme vědět, že vstupní signál má 50% koeficient naplnění při libovolné frekvenci, potom nám to umožní určit polovinu měřené periody. Koeficient naplnění je:

\( d = \frac{t_h}{t_m} %\), kde \(t_h\) je čas, kdy byl vstup na vysoké úrovni a \(t_m\) je čas, který jsme naměřili.

Je to jenom průměrná hodnota koeficientu nalpnění za periodu měření. Abychom obdrželi odhad, který je blízký ke skutečnému koeficientu naplnění, je potřeba mít čas měření krátký, ale to snižuje přesnost, protože pravděpodobnost, že dostaneme část pulsu na začátku a na konci měření. Frekvence hodin musí být zvolena tak, že pro 100 % koeficient naplnění obdržíme maximální počet, t.j.

\( count_{max} = f_c \cdot t_m\), kde \(f_c\) je kmitočet systémových hodin, \(count_{max}\) je maximální počet, \(t_m\) je čas měření.

nebo můžeme přdpokládat, že \(count_{max} = 65535\):

\( f_c = \frac{65535}{t_m} \)

což nám dává možnost měřit frekvenci v řádu kilohertzů pokud je čas měření v milisekundách. Například, při času měření 10 ms, potřebujeme nastavi hodiny na:

\(\frac{65535}{10} = 6553.5 kHz\)

Užitečnější veličinou je dělitel potřebný k získání taktu:

\(div = \frac{125000 \cdot 10}{65535} = 19.074\)

Vezmeme-li nejbližší větší celé číslo k div nám dá rychlost hodin, která nedosahuje k maximálnímu counteru. Zvolíme-li nejbližší menší celé číslo dostaneme kompletní wrap a začátek dalšího cyklu vrátí malý počet na konci periody. Avšak nechceme, aby se počítadlo zalamovalo během měřené periody. Použijeme-li dělič 20, tak maximální počet bude:

\(count_{max} = \frac{125000000*1000}{20*10} = 62500\)

Dáme to všechno dohromady a napíšeme program na měření koeficientu naplnění:

pwm13.c
int main()
{
    stdio_init_all();
    gpio_set_function( 20, GPIO_FUNC_PWM );
    gpio_set_function( 21, GPIO_FUNC_PWM );
    uint slice_num = pwm_gpio_to_slice_num(20);

    uint chanA = pwm_gpio_to_channel( 20 );
    uint chanB = pwm_gpio_to_channel( 21 );

    pwm_set_clkdiv_int_frac( slice_num, 20, 0 );
    int maxcount = 125000000*10/20/1000;

    pwm_set_wrap( slice_num, 65535 );
    pwm_set_chan_level( slice_num, chanA, 100 );
    pwm_set_clkdiv_mode( slice_num, PWM_DIV_B_HIGH );

    while( true )
    {
        pwm_set_enabled( slice_num, true );
        sleep_ms( 10 );
        pwm_set_enabled( slice_num, false );
        uint16_t count = pwm_get_counter( slice_num );
        pwm_set_counter( slice_num, 0 );
        printf("count= %u  duty cycle=%d %%\n", count, (int) count * 100/maxcount );
        sleep_ms( 1000 );
    }

return 0;
}

Tento program dává docela přesné výsledky pokud je frekvence měřeného signálu představuje dostatečné množství pulsů v rámci periody 10 ms, což je v praxi 200 Hz. Celý program je zde.

Výsledky měření duty

Screenshot 20241019 203356 xyz.fhdm.scoppy

Konfigurační struktura

V SDK se nachází ještě alternativní způsob konfigurace PWM strojku. Místo toho, abychom postupně volali jednotlivé konfigurační funkce, můžeme nastavit všechny hodnot v konfigurační struktuře:

typedef struct {
    uint32_t csr;
    uint32_t div;
    uint32_t top;
}

a potom použít funkce k nastavení některých hodnot:

pwm_config_set_phase_correct( pwm:config *c, bool phase_correct )
pwm_config_set_clkdiv( pwm_config *c, float div )
pwm_config_set_clkdiv_int( pwm_config *c, uint div )
pwm_config_set_clkdiv_mode( pwm_config *c, enum pwm_clkdiv_mode mode )
pwm_config_set_output_polarity( pwm_config *c, bool a, bool b )
pwm_config_set_wrap( pwm_config *c, uint16_t wrap )

Celou strukturu lze inicializovat na implicitní hodnoty funkcí:

pwm_get_default_config( void );

a potom vložit svoji konfiguraci pomocí funkce:

pwm_init( uint slice_num, pwm_config *c, bool start )

Výhoda tohoto přístupu spočívá v tom, že jakmile máme nastavenu konfigurační strukturu, můžeme ji použít znova. Například:

pwm_config cfg = pwm_config_cfg = pwm_get_default_config()
pwm_config_set_wrap( &cfg, wrap );
pwm_init( slice_num, &cfg, true );

Můžete používat ke konfiguraci PWM metodu, která vám přijde přirozenější.

K čemu ješte je vhodné PWM?

PWM linky jsou neuvěřitelně universální a vždy vyzývají otázku "mohu to udělat pomocí PWM?" ať už se zabýváte jakýmkoliv problémem. Příklad s LED nám ukazuje PWM jako řízení výkonu. Tuto myšlenku můžeme rozšířit na počítačem řízený spínací zdroj. Potřebujeme nějaký kondezátor na vyhlazení napětí a možná transformátor na změnu úrovně napětí. PWM můžeme použít i na řízení stejnosměrných motorů a když přidáme můstkový obvod, můžeme řídit rychlost i směr otáčení. Nakonec můžeme použít PWM signál jako modulovanou nosnou v datových komunikacích. Například většina infračervených ovladačů pro dálkové ovládání používá 38 kHz nosnou, což znamená \(26 \mu s\) pulsy. To je přepínáno na zapnuto a vypnuto v rytmu 1 ms, což spadá do rozsahu PWM, který umíme udělat. Takže stačí zaměnit obyčejnou LED infračervenou diodou a můžeme se začít zabývat dálkovým ovládáním nebo datovými přenosy.

Velká oblast použití PWM je v oblasti řízení motorů a serv, čímž se budeme zabývat v další kapitole.

Zdroje a odkazy

Zdrojové kódy

Pokud používáte Pico SDK 2.0 a větší je potřeba opravit ve funkci valid_params_if první parametr z PWM na HARDWARE_PWM:

uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}