<< Zpět

 

  Česky: , English:

PicoQVGA - minimalistický displej QVGA na Raspberry Pico

verze 1.0, září 2021

(c) Miroslav Němeček

Download knihovny PicoQVGA (kopie na ulozto)

PicoQVGA na Github: https://github.com/Panda381/PicoQVGA

 

Obsah

PicoQVGA je minimalistická knihovna generující VGA signál pro VGA monitor na Raspberry Pico v rozlišení 320x240 / 8 bitů. Na kódu knihovny je formou tutoriálu podrobně vysvětlena funkce a použití řadičů PIO a DMA. I přes jednoduchost knihovny (jádro zabere asi 400 řádků v C) je knihovna postačující pro rozsáhlou oblast využití. PicoQVGA je zjednodušenou verzí plné verze knihovny PicoVGA.

Schéma zapojení

Výstup barevného signálu zajišťuje 8 rezistorů, R1 až R8. Barevné složky jsou: 2 bity pro modrou (B0 až B1), 3 bity pro zelenou (G0 až G2) a 3 bity pro červenou (R0 až R2). 8-bitový výstup zajistí malé nároky na RAM paměť, které není v Raspberry Pico nazbyt (obrazový buffer 320x240/8 zabere 76 KB z dostupných 256 KB).

Knihovna generuje oddělené synchronizační signály HSYNC a VSYNC. S malou úpravou lze generovat jen jeden společný synchronizační signál CSYNC (není součástí kódu), který ušetří 1 pin Pico, avšak některé starší VGA monitory tento mód nepodporují (není jich moc).

Ve schématu je doplňkově uvedeno i zapojení audio PWM výstupu, který je využíván v některých ukázkových programech, ale není nutné ho použít.

V ukázkových programech je k ovládání použita konzolová klávesnice hostitelského počítače, s využitím programovacího USB konektoru a konzolového programu (begPutty).

Budete-li chtít zapojení modifikovat, je možné signály barev B0 až R2 přesunout na jiné piny. Vždy ale musí platit, že signály musí zachovat stejnou spojitou řadu. To je z důvodu, že PIO umožňuje měnit počátek pole pinů, ale neumožňuje měnit pořadí pinů. Podobně signály HSYNC a VSYNC mohou být na jiných pinech, nezávisle na signálech barev, opět ale platí, že je nutné zachovat jejich posloupnost - tj. pin VSYNC musí bezprostředně následovat za pinem HSYNC.

 

Časování

Jako první krok si potřebujeme rozplánovat časování výstupního signálu. Vyjdeme z časování VGA signálu, ale použijeme poloviční rozlišení (QVGA). Obrazové videolinky budou zdvojené. Z normy VGA známe tyto údaje:

VGA rozlišení:

VGA vertikální časování:

VGA horizontální časování:

Implicitní systémové hodiny Raspberry Pico jsou 125 MHz. Vydělíme-li hodiny číslem 10, dostaneme 12.5 MHz jako nejbližší číslo k požadovaným hodinám pixelů (12.5875 MHz). Tuto frekvenci bychom sice mohli použít (a je i součástí zdrojového kódu jako alternativa), ale mohlo by to znamenat, že 2 pixely po stranách obrazu se mohou už dostat mimo viditelnou oblast monitoru. Zvolíme proto raději další nejbližší vyšší frekvenci systémových hodin, 126 MHz. Pixel clock pro QVGA v tom případě bude 12.6 MHz (0.079365 us na pixel) a obraz bude celý viditelný.

Jak můžeme zjistit pomocí přiloženého programu PicoClock, systémové hodiny 126 MHz dostaneme nastavením PLL na hodnoty: vco=1008 MHz, fbdiv=84, pd1=4, pd2=2.

Pro optimální práci s PIO budeme PIO taktovat systémovými hodinami dělenými číslem 2. Na 1 pixel budeme potřebovat 5 taktů PIO hodin. Z uvedených údajů vypočteme potřebné časování PIO:

K front a back porch jsme přičetli 1 takt (symetricky, aby obraz zůstal vystředěný), protože aktivní obraz je o 2 takty kratší než předepsané časování.

PIO program

O PIO modulu se dá říct, že je to jedna z hlavních vlastností, která vyzvedává Raspberry Pico o několik úrovní výše. PIO je jakoby další procesor v procesoru. PIO modul je zaměřený především na programování komunikačního rozhraní, proto od něj nečekejte např. provádění výpočtů. Využije se tam, kde je potřeba přesné časování.

Raspberry Pico má dva nezávislé PIO moduly. Každý PIO modul má v sobě paměť programu o velikosti 32 instrukcí a 4 statové automaty SM (state machine). Stavové automaty jednoho PIO modulu sdílejí spolu programovou paměť, ale jinak je jejich funkce nezávislá.

Začneme PIO programem. PIO program jsou instrukce pro PIO modul. Délka programu je omezena na 32 instrukcí. Do programové paměti PIO můžeme zavést samostatné programy pro každý z SM modulů, nebo programy můžeme sdílet, omezeni jsme pouze velikostí paměti 32 instrukcí. PIO program se překládá překladačem "pioasm", který je součástí Raspberry Pico SDK. V původním SDK naleznete verzi překladače pro Linux, zde v přiloženém balíku je verze pro Windows.

Soubor instrukcí PIO procesoru není rozsáhlý, je to jen 9 instrukcí (každá instrukce je dlouhá 16 bitů), ale jak zjistíte, pro komunikační programy postačují. Jedná se o tyto instrukce:

PIO procesor má k dispozici následující registry:

Vysvětlení k posuvným registrům. Při vysílání dat přes PIO zapíše hlavní procesor vysílané 32-bitové slovo do vysílací fronty TX FIFO. Vysílací fronta je dlouhá 4 pozice. Není-li potřebný příjem, může se sloučit s přijímací frontou na délku 8 pozic. Z vysílací fronty si PIO program přečte vysílané slovo instrukcí PULL a uloží ho do výstupního posuvného registru OSR. Z OSR registru si program postupně načítá data po bitech pomocí instrukce OUT. Případně je instrukcí OUT může zapsat přímo na výstupní piny.

Při příjmu dat přijímá PIO procesor postupně stav pinů po bitech instrukcí IN do registru ISR. Instrukcí PUSH přenese ISR registr do přijímací fronty RX FIFO, odkud si načtenou 32-bitovou hodnotu přečte hlavní procesor.

Pro všechny PIO instrukce platí, že každá instrukce trvá implicitně přesně 1 takt PIO hodin, nezávisle na ostatní činnosti hlavního procesoru. To umožňuje psát programy s přesným časováním. Instrukce mohou trvat delší čas v případě zádrhelu ve FIFO frontě, nestíhá-li hlavní procesor data včas zpracovávat.

Každá PIO instrukce obsahuje 5-bitové pole sloužící ke 2 účelům:

  1. Přídavná prodleva "delay". Kromě základní doby provedení instrukce 1 takt může každá instrukce čekat ještě dalších až 31 taktů hodin.
  2. Přímý výstup na piny "side-set". Kromě programového výstupu na výstupní piny lze řídit až 5 výstupních pinů přímo, jako součást kódu instrukce.

Kolik bitů z 5-bitového pole bude využito pro prodlevu a kolik pro side-set, lze určit konfigurací SM. V našem driveru budeme používat 2 side-set bity pro výstup synchronizačních signálů HSYNC a VSYNC. Zbývající 3 bity použijeme pro prodlevu, každá instrukce tak může trvat 1 až 8 taktů.

Jeden z bitů 5-bitového pole může být využit jako příznak, zda se side-set pole používá nebo ne. Neznamená to ovšem, že by všechny zbylé bity byly použitelné pro prodlevu. Znamená to jen, zda u instrukcí musíme zadávat side-set nebo ne. Zvolíme raději pevné rozdělení pole a side-set budeme zadávat u každé instrukce.

Přejděme k PIO programu. Na začátku programu uvedeme následující definice:


.program qvga
.side_set	2	; HSYNC and VSYNC output (2 bits)
.define NOSYNC	0	; no sync, output image or blanking
.define HSYNC	1	; HSYNC pulse (or CSYNC pulse)
.define VSYNC	2	; VSYNC pulse
.define VHSYNC	3	; HSYNC and VSYNC pulse
.define BPP	8	; number of bits per pixel

První řádek označuje jméno programu. Jednak pomocí tohoto jména se budeme odkazovat na vygenerovaná návěští a jednak to slouží k oddělení programů, obsahuje-li jeden soubor více programů.

Druhý řádek definuje nastavení side-set. Číslo 2 udává, že v každé instrukci budou využity 2 bity 5-bitového pole jako výstup hodnoty na výstupní piny. Zbylé 3 bity budou využity pro přídavnou prodlevu.

Hodnota BPP udává počet bitů na pixel. Kód programu můžeme používat pro barevnou hloubku 1, 2, 4, 8, 16 a 32 bitů. Je nutné počítat s tím, že délka zapsaných obrazových dat musí být zarovnaná na 32-bitové slovo. My použijeme 8-bitové pixely. Obrazová data tedy musí být zarovnaná na 4 pixely.

Ostatní řádky jsou definice hodnoty signálů pro side-set. Signály HSYNC a VSYNC musí být na pinech bezprostředně po sobě následujících. Signál VGA vyžaduje negativní tvar synchronizačních pulzů - tj. v klidu jsou signály v úrovni HIGH, pouze během pulzu úroveň klesne na LOW. Tento stav můžeme připravit i naší definicí, ale pro přehlednost použijeme v programu raději pozitivní polarizaci a v konfiguraci výstupních pinů zvolíme negací výstupního signálu.

Program se bude skládat z krátkých úseků, kde každý úsek bude generovat jeden typ signálu. Začneme úsekem, který bude generovat signál HSYNC.


; ===== [3 instructions] HSYNC pulse, N=delay in clock cycles - 3
; starts HSYNC at time 0

public hsync:
	jmp	x--,hsync	side HSYNC	; [N+1] loop
public entry:					; program entry point
	out	x,27		side HSYNC	; [1] get next loop counter N
	out	pc,5		side HSYNC	; [1] jump to next function

Řádek "public hsync" je veřejné návěští. Znamená to, že překladač vygeneruje C kód s hodnotami návěští, aby je mohl hlavní program používat.

První instrukce, "jmp x--,hsync", je programová smyčka s podmíněným skokem. Instrukce se podívá do registru X a je-li jeho hodnota nenulová, dekrementuje hodnotu X a provede skok na zadané návěští. V případě nulové hodnoty pokračuje dál a hodnotu X nezmění. Obsahuje-li registr X na začátku hodnotu N, provede se celá smyčka za N+1 taktů. Následující 2 řádky zaberou 2 hodinové takty. Chceme-li tedy dosáhnout určité doby provádění celého úseku, musíme na začátku nastavit hodnotu X o 3 nižší než je požadovaná doba.

Řádek "public entry:" je vstupní bod do programu. Sem nasměrujeme program po startu. Zajistí se tím načtení prvního řídicího slova.

Další 2 řádky načtou řídicí slovo z OSR registru a provedou skok na další programový segment. Nemusíme používat instrukci PULL, protože v konfiguraci bude nastavené automatické načítání registru OSR z vysílací fronty TX FIFO. První instrukce, "out x,27", načte 27 bitů z registru OSR do registru X. Tuto hodnotu použijeme jako čítač pro následující programový segment. Druhá instrukce, "out pc,5", načte 5 bitů do registru PC. PC je 5-bitový čítač programu, nastavením jeho hodnoty se provede skok na jinou adresu v programu.

Výstupní registr OSR se během instrukce OUT bude posouvat směrem doprava (k nižším bitům). To je dáno formátem obrazových dat - pixel na nižší adrese je potřeba vysílat dřív než na vyšší adrese. Ve skutečnosti se data z OSR do X nepřenášejí po bitech, ale jako celistvý bitový úsek, směr posunu OSR tedy pořadí přenesených bitů neovlivní.

Přídavný parametr "side HSYNC" zajistí, že během provádění tohoto programového úseku bude nastaven signál HSYNC jako aktivní (side-set výstupní piny se nastaví na hodnotu "1").

Podobným způsobem vytvoříme i programové segmenty aktivující signál VSYNC a signál HSYNC+VSYNC.


; ===== [3 instructions] VSYNC pulse, N=delay in clock cycles - 3
; starts VSYNC at time 0

public vsync:
	jmp	x--,vsync	side VSYNC	; [N+1] loop
	out	x,27		side VSYNC	; [1] get next loop counter N
	out	pc,5		side VSYNC	; [1] jump to next function

; ===== [3 instructions] VSYNC and HSYNC pulse, N=delay in clock cycles - 3
; starts HSYNC and VSYNC at time 0

public vhsync:
	jmp	x--,vhsync	side VHSYNC	; [N+1] loop
	out	x,27		side VHSYNC	; [1] get next loop counter N
	out	pc,5		side VHSYNC	; [1] jump to next function

Blíže se pozastavíme u úseku generujícího tmu.


; ===== [4 instructions] DARK pulse, N=delay in clock cycles - 4
; sets blanking at time 0, starts NOSYNC at time 0

public dark:
	mov	pins,null	side NOSYNC	; [1] dark output
dark_loop:
	jmp	x--,dark_loop	side NOSYNC	; [N+1] loop
.wrap_target					; wrap jump target
	out	x,27		side NOSYNC	; [1] get next loop counter N
	out	pc,5		side NOSYNC	; [1] jump to next function

Oproti předešlým úsekům přibyl řádek "mov pins,null". Řádek zajistí nastavení výstupních pinů barev na hodnotu 0, protože registr null představuje hodnotu nula. Kdybychom potřebovali opačnou hodnotu, použili bychom možnost provedení inverze hodnoty během provádění instrukce.

Nakonec zbývá úsek, kterým odešleme obrazová data na výstup barev.


; ===== [3 instructions] output pixels at 5 clocks per pixel, N=number of pixels-2
; number of pixels must be multiple of: 1 at BP=32, 2 at BPP=16, 4 at BPP=8, 8 at BPP=4, 16 at BPP=2, 32 at BPP=1
; Output first pixel at time 0

public output:
	out	pins,BPP	side NOSYNC [2]	; [3] output pixel
	jmp	x--,output	side NOSYNC [1]	; [2] loop (N+1 pixels)
	out	pins,BPP	side NOSYNC [2]	; [3] output pixel
.wrap						; wrap jump to .wrap_target

První instrukce, "out pins,BPP", načte z OSR registru 8 bitů (obsahujících data pro 1 pixel) a zapíše je na výstupní piny barev. Druhá instrukce je čítač průchodů. Obsahuje-li registr X na začátku počet N, odešle se touto smyčkou N+1 obrazových pixelů na výstup.

Kromě side-set pole obsahují instrukce i delay prodlevu, uvedenou v hranatých závorkách []. Je to čas v taktech, který se přidá k základní době provedení instrukce, která je 1. První instrukce smyčky (out) bude trvat 3 takty, druhá instrukce (jmp) bude trvat 2 takty. Celkem tedy jeden průchod smyčkou zabere 5 taktů.

Kdybychom nyní tento úsek ukončili instrukcemi pro načtení X a skok, které zaberou 2 takty, byl by poslední pixel dlouhý 7 taktů namísto 5. Proto výstup na poslední pixel provedeme pomocí další instrukce "out". Bude trvat 3 takty, následující instrukce skoku zaberou 2 takty a tím splníme požadavek délky posledního pixelu 5 taktů.

Na vstupu do segmentu musí registr X obsahovat počet pixelů snížený o 2.

Poslední segment není ukončený 2 instrukcemi 'out' jako v předešlých případech, ale je ukončen symbolem ".wrap". Wrap je vlastnost používaná u smyček. PIO procesor umožňuje jednu adresu programu označit návěštím .wrap a pokud na tuto adresu narazí, provede okamžitý skok na jinou adresu, označenou návěštím ".wrap_target". To slouží k vytváření smyček, kdy je na konci smyčky uveden ".wrap" a na začátku smyčky ".wrap_target". Wrap má efekt nepodmíněného skoku, ovšem s tím rozdílem, že skok nezabere 1 takt hodin, ale provede se okamžitě, bez prodlevy. Lze tak využít ke zrychlení smyčky a zkrácení programu. My ho zde nevyužijeme ve smyčce, ale ke zkrácení programu - wrap provede skok na instrukce skoku v předešlém segmentu a tím můžeme u posledního segmentu ušetřit 2 instrukce.

Program přeložíme příkazem "pioasm.exe -o c-sdk qvga.pio qvga.pio.h". Vygenerovaný soubor obsahuje definice v C kódu s přeloženým programem, symbolická návěští a pomocné podprogramy.

Inicializace PIO

Připravíme si funkci, která inicializuje PIO a SM. Pro přehlednost budu uvádět proměnné a definice bezprostředně před jejich použitím. Nejdříve zavedeme PIO program do paměti.


#define QVGA_PIO	pio0	// QVGA PIO
#define QVGA_SM		0	// QVGA state machine
uint QVGAOff;			// offset of QVGA PIO program

// load PIO program
QVGAOff = pio_add_program(QVGA_PIO, &qvga_program);

V definici jsme si určili, že pro VGA výstup použijeme PIO0 a SM0. Ale mohli bychom nechat na SDK knihovně, aby určila volný PIO a SM a namísto konstant používat proměnné.

Funkce pio_add_program převezme strukturu vygenerovanou překladačem a instrukce programu v ní uvedené zapíše do programové paměti PIO SM. Poznamenejme, že programová paměť PIO je dostupná ve formě 32 portů po 16 bitech. Funkce vyhledá od konce programové paměti volné místo, tam program uloží a navrátí offset začátku programu. Mohli bychom sice program zavést na pevně danou adresu (při překladu použít příkaz .origin), ale tímto volnějším způsobem můžeme program snáze kombinovat s jinými programy.

Nakonfigurujeme výstupní piny.


// QVGA port pins
#define QVGA_GPIO_FIRST	2	// first QVGA GPIO
#define QVGA_GPIO_NUM	8	// number of QVGA color GPIOs, without HSYNC and VSYNC
#define QVGA_GPIO_LAST	(QVGA_GPIO_FIRST+QVGA_GPIO_NUM-1) // last QVGA GPIO
#define QVGA_GPIO_HSYNC	10	// QVGA HSYNC/CSYNC GPIO
#define QVGA_GPIO_VSYNC	(QVGA_GPIO_HSYNC+1) // QVGA VSYNC GPIO

// configure GPIOs for use by PIO
for (i = QVGA_GPIO_FIRST; i <= QVGA_GPIO_LAST; i++) pio_gpio_init(QVGA_PIO, i);
pio_gpio_init(QVGA_PIO, QVGA_GPIO_HSYNC);
pio_gpio_init(QVGA_PIO, QVGA_GPIO_VSYNC);

// set pin direction to output
pio_sm_set_consecutive_pindirs(QVGA_PIO, QVGA_SM, QVGA_GPIO_FIRST, QVGA_GPIO_NUM, true);
pio_sm_set_consecutive_pindirs(QVGA_PIO, QVGA_SM, QVGA_GPIO_HSYNC, 2, true);

// negate HSYNC and VSYNC output
gpio_set_outover(QVGA_GPIO_HSYNC, GPIO_OVERRIDE_INVERT);
gpio_set_outover(QVGA_GPIO_VSYNC, GPIO_OVERRIDE_INVERT);

V definicích jsme zvolili, že barevný port je 8 GPIO pinů, počínaje GPIO2. HSYNC signá je na GPIO10 a VSYNC na GPIO11.

Funkce pio_gpio_init nastaví výstupní piny do režimu, kdy jsou ovládané přímo z PIO. Příkazem pio_sm_set_consecutive_pindirs nastavíme piny jako výstup. U pinů HSYNC a VSYNC nastavíme inverzi výstupu - to proto, abychom mohli v PIO programu požívat pozitivní polarizaci. Ale není to nutné a v programu můžeme pracovat s negativní polarizací signálů.

Připravíme strukturu pro konfiguraci SM.


// prepare default PIO program config
pio_sm_config cfg = qvga_program_get_default_config(QVGAOff);

// map state machine's OUT and MOV pins	
sm_config_set_out_pins(&cfg, QVGA_GPIO_FIRST, QVGA_GPIO_NUM);

// set sideset pins (HSYNC and VSYNC)
sm_config_set_sideset_pins(&cfg, QVGA_GPIO_HSYNC);

Funkci qvga_program_get_default_config připravil překladač PIO programu. Kromě přípravy implicitní konfigurační struktury obsahuje nastavení wrap návěští (funkce sm_config_set_wrap) a nastavení side-set módu (funkce sm_config_set_sideset). Funkci předáváme offset programu, který nám dříve navrátila funkce pro zavedení programu do paměti.

Funkcí sm_config_set_out_pins řekneme SM, na kterém pinu začínají výstupní piny a kolik pinů to je. Jedná se o mapování pro instrukce OUT a MOV. Znamená to, že pokud instrukcemi OUT nebo MOV zapíšeme nějaká data na výstupní port PINS, zapíšou se data přímo na výstupní piny. Např. začínají-li piny na GPIO2, zapíše se bit 0 odeslaných dat do portu GPIO2.

Podobně funkce sm_config_set_sideset_pins mapuje side-set piny nastavením čísla počátečního pinu. Počet pinů zde nenastavujeme, ten byl určen v dřívější funkci sm_config_set_sideset.


#define QVGA_CLKDIV	2	// SM divide clock ticks

// join FIFO to send only
sm_config_set_fifo_join(&cfg, PIO_FIFO_JOIN_TX);

// PIO clock divider
sm_config_set_clkdiv(&cfg, QVGA_CLKDIV);

// shift right, autopull, pull threshold
sm_config_set_out_shift(&cfg, true, true, 32);

V dalším kroku připojíme přijímací frontu RX FIFO k vysílací frontě TX FIFO - data nebudeme přijímat, budeme jen vysílat, a tak můžeme zvýšit délku fronty na 8 úrovní.

Hodiny pro PIO odvodíme ze systémových hodin vydělením číslem 2. Výstup jednoho pixelu v PIO programu zabere 5 PIO taktů a tím dosáhneme pixel clock 12.6 MHz.

Nastavíme výstupní posuvný registr OSR. Bude se posouvat doprava (jako první půjde ven nižší bajt obrazových dat), pull se bude dělat automaticky s hranicí 32 bitů.

Konfigurační strukturu odešleme na SM. V parametru uvedeme vstupní bod, ze kterého program bude startovat. Zatím ale SM startovat nebudeme.


// initialize state machine
pio_sm_init(QVGA_PIO, QVGA_SM, QVGAOff+qvga_offset_entry, &cfg);

Inicializace bufferů

Data nebudeme do PIO odesílat softwarově procesorem, ale pomocí DMA řadiče. To proto, abychom procesor zbytečně nezatěžovali, a také protože procesor by nezajistil spojitý přenos dat. K tomu si potřebujeme připravit buffery, jejichž obsah se bude vysílat na PIO.

Jak víme z našeho PIO programu, základem řízení jsou 32-bitová slova obsahující nastavení čítače X a skokovou adresu. Na to si připravíme makro.


// PIO command (jmp=program address, num=loop counter)
#define QVGACMD(jmp, num) ( ((u32)((jmp)+QVGAOff)<<27) | (u32)(num))

V makru je 'jmp' adresa skoku. Budeme sem zadávat hodnoty návěští, které nám vygeneroval překladač. Uvnitř makra se k návěštím přičte offset počátku programu a tím se zajistí správná hodnota skoku. Adresa skoku se posune do bitů 27..31, kde ji bude očekávat PIO program. Na nižších 27 bitech doplníme hodnotu pro čítač X. Nesmíme ovšem zapomenout odečítat od hodnoty potřebné korekce.

Budeme potřebovat 4 buffery.


#define WIDTH		320	// display width

#define QVGA_TOTAL	2002	// total clock ticks
#define QVGA_HSYNC	240	// horizontal sync clock ticks
#define QVGA_BP		121	// back porch clock ticks
#define QVGA_FP		41	// front porch clock ticks

// Scanline data buffers (commands sent to PIO)
u32 ScanLineImg[3];	// image: HSYNC ... back porch ... image command
u32 ScanLineFp;		// front porch
u32 ScanLineDark[2];	// dark: HSYNC ... back porch + dark + front porch
u32 ScanLineSync[2];	// vertical sync: VHSYNC ... VSYNC(back porch + dark + front porch)

// image scanline data buffer: HSYNC ... back porch ... image command
ScanLineImg[0] = QVGACMD(qvga_offset_hsync, QVGA_HSYNC-3); // HSYNC
ScanLineImg[1] = QVGACMD(qvga_offset_dark, QVGA_BP-4); // back porch
ScanLineImg[2] = QVGACMD(qvga_offset_output, WIDTH-2); // image

// front porch
ScanLineFp = QVGACMD(qvga_offset_dark, QVGA_FP-4); // front porch

// dark scanline: HSYNC ... back porch + dark + front porch
ScanLineDark[0] = QVGACMD(qvga_offset_hsync, QVGA_HSYNC-3); // HSYNC
ScanLineDark[1] = QVGACMD(qvga_offset_dark, QVGA_TOTAL-QVGA_HSYNC-4); // back porch + dark + front porch

// vertical sync: VHSYNC ... VSYNC(back porch + dark + front porch)
ScanLineSync[0] = QVGACMD(qvga_offset_vhsync, QVGA_HSYNC-3); // VHSYNC
ScanLineSync[1] = QVGACMD(qvga_offset_vsync, QVGA_TOTAL-QVGA_HSYNC-3); // VSYNC(back porch + dark + front porch)

Jednotlivé videolinky začínají HSYNC pulsem, za kterým následuje back porch, obrazová data a front porch. Obrazová videolinka používá buffery ScanLineImg a ScanLineFp. Při generování začínáme bufferem ScanLineImg. V prvním slově obsahuje vyvolání programového segmentu hsync, který aktivuje HSYNC signál. V údaji délky nastavíme délku HSYNC na 240 taktů, ovšem musíme ji snížit o 3, což je základní režie čekací smyčky.

Po HSYNC následuje back porch - což je tma, po dobu 121 taktů. Údaj musíme snížit o 4.

Následuje příkaz pro odeslání obrazových dat. Příkaz provede skok na obsluhu "output", s počtem WIDTH-2. Další data nebudeme odesílat z bufferů, ale přímo z obrazové paměti. Za obrazovými daty odešleme front porch, z bufferu ScanLineFp. Front porch je nutné vždy odeslat po obrazových datech, protože zajistí ztmavení obrazu na černou barvou. Případný následující HSYNC signál už barvu nenastavuje.

Možná vás teď napadne otázka - jak PIO program rozliší obrazová data od řídicích slov? Jednoduše - neřeší to. :-) PIO má dobrou vlastnost v tom, že při správné funkci zůstává neustále synchronní s příchozími daty a tak ani po delší době nedojde k nesynchronitě.

Buffer ScanLineDark používáme k zobrazení tmavé linky bez obrazu, ve vertikálním front a back porch. Buffer obsahuje jen signál HSYNC následovaný tmavým obrazem až do konce linky.

Buffer ScanLineSync generuje signál VSYNC a od signálu tmavé linky se liší pouze aktivací signálu pro VSYNC. Nenastavuje černou barvu na obrazové piny, protože počítáme s tím, že černá barva je nastavená z předešlých linek.

Kromě datových bufferů, pro datový DMA, potřebujeme i řídicí buffery, pro řídicí DMA. Podrobněji v dalších odstavcích.


// Scanline control buffers
#define CB_MAX 8	// size of one scanline control buffer (1 link to data buffer requires 2x u32)
u32 ScanLineCB[2*CB_MAX]; // 2 control buffers

// control buffer 1 - initialize to VSYNC
ScanLineCB[0] = 2; // send 2x u32 (send ScanLineSync)
ScanLineCB[1] = (u32)&ScanLineSync[0]; // VSYNC data buffer
ScanLineCB[2] = 0; // stop mark
ScanLineCB[3] = 0; // stop mark

// control buffer 1 - initialize to VSYNC
ScanLineCB[CB_MAX+0] = 2; // send 2x u32 (send ScanLineSync)
ScanLineCB[CB_MAX+1] = (u32)&ScanLineSync[0]; // VSYNC data buffer
ScanLineCB[CB_MAX+2] = 0; // stop mark
ScanLineCB[CB_MAX+3] = 0; // stop mark

Inicializace DMA

Z předchozích řádků už víme, jaká data chceme odesílat na PIO a jak je dále zpracovat. A jak jsem se už zmínil, nebudeme je odesílat programově, ale pomocí DMA řadiče. Pro každou videolinku chceme odeslat několik datových bufferů. Mohli bychom pro každý buffer aktivovat DMA přenos programově, což je poměrně dost úkonů. Raspberry Pico má pro tento účel lepší řešení - zřetězení DMA řadičů. Namísto abychom datový DMA konfigurovali pro každý buffer zvlášť, necháme to dělat další DMA.

Zřetězení bude fungovat následujícím způsobem. Data bude na PIO odesílat první DMA kanál, datový. Když dokončí požadovaný úkol, aktivuje nový přenos druhého DMA kanálu, řídicího. Řídicí DMA kanál přenese další řídicí informace do nastavení datového kanálu a spustí běh datového kanálu. Data pro řidicí DMA jsou uložena v řídicím bufferu. Poslední řídicí data obsahují hodnotu nula. Řídicí DMA tuto hodnotu také přenese do nastavení datového DMA kanálu, ale ten rozezná nulovou hodnotu, označující konec přenosu, a aktivuje IRQ signál. Tím se vyvolá přerušení hlavního procesoru, který znovu nakonfiguruje řídicí DMA pro nový přenos. IRQ se generuje na konci každé videolinky. Obsluha přerušení aktivuje přenos pro další videolinku a mezitím připraví buffery pro příští přenos.

Nejdříve připravíme konfiguraci řídicího DMA kanálu.


#define QVGA_DMA_CB	0	// DMA control block of base layer
#define QVGA_DMA_PIO	1	// DMA copy data to PIO (raises IRQ0 on quiet)

// prepare DMA default config
dma_channel_config cfg = dma_channel_get_default_config(QVGA_DMA_CB);

// increment address on read from memory
channel_config_set_read_increment(&cfg, true);

// increment address on write to DMA port
channel_config_set_write_increment(&cfg, true);

// each DMA transfered entry is 32-bits
channel_config_set_transfer_data_size(&cfg, DMA_SIZE_32);

// write ring - wrap to 8-byte boundary (TRANS_COUNT and READ_ADDR_TRIG of data DMA)
channel_config_set_ring(&cfg, true, 3);

// DMA configure
dma_channel_configure(
	QVGA_DMA_CB,		// channel
	&cfg,			// configuration
	&dma_hw->ch[QVGA_DMA_PIO].al3_transfer_count, // write address
	&ScanLineCB[0],		// read address - as first, control buffer 1 will be sent out
	2,			// number of transfers in u32 (number of transfers per one request from data DMA)
	false			// do not start yet
);

Funkcí dma_channel_get_default_config si připravíme implicitní konfigurační strukturu.

Funkcí channel_config_set_read_increment zvolíme automatickou inkrementaci čtecí adresy. Řídicí DMA čte data z řídicích bufferů a je tedy potřeba, aby se po každém čtení adresa posunula na další slovo.

Funkcí channel_config_set_write_increment zapneme automatickou inkrementaci zápisové adresy. Řídicí DMA kanál bude přenášet slova z řídicího bufferu přímo do řídicích registrů datového DMA kanálu. Budou se přenášet při každém přenosu 2 slova po 32 bitech, je tedy nutná automatická inkrementace adresy.

Velikost jedné jednotky přenášených dat je funkcí channel_config_set_transfer_data_size nastavena na 32 bitů. Každý z řídicích registrů DMA datového kanálu je 32-bitový.

Funkcí channel_config_set_ring je nastavená velikost kruhu zápisových adres na 8 bajtů. Budou se přenášet 2 slova po 32 bitech, což je dohromady 8 bajtů. Parametr "3" představuje mocninu 2 velikosti kruhových dat, tedy 8 bajtů. DMA kanál přenese 2 slova do řídicích registrů, poté se ukazatel resetuje o 8 bajtů zpět.

Funkcí dma_channel_configure se DMA kanál nakonfiguruje podle připravené konfigurační struktury. Jako zápisová adresa je zvolen třetí registr ze čtvrté sady aliasů registrů - tj. registry trans_count a read_addr. Každý DMA kanál lze řídit pomocí 4 registrů a tyto registry jsou uspořádány ve 4 variantách pořadí. U každé varianty platí, že zápisem do 4. registru se aktivuje přenos DMA kanálu. Jednotlivé aliasy DMA registrů jsou:


//			+0x0		+0x4		+0x8		+0xC (Trigger)
// 0x00 (alias 0):	READ_ADDR	WRITE_ADDR	TRANS_COUNT	CTRL_TRIG
// 0x10 (alias 1):	CTRL		READ_ADDR	WRITE_ADDR	TRANS_COUNT_TRIG
// 0x20 (alias 2):	CTRL		TRANS_COUNT	READ_ADDR	WRITE_ADDR_TRIG
// 0x30 (alias 3):	CTRL		WRITE_ADDR	TRANS_COUNT	READ_ADDR_TRIG ... we use this!

Každým přenosem se přenesou 2 slova po 32 bitech. DMA kanál zatím startovat nebudeme.

Dále si připravíme datový DMA kanál.


// prepare DMA default config
cfg = dma_channel_get_default_config(QVGA_DMA_PIO);

// increment address on read from memory
channel_config_set_read_increment(&cfg, true);

// do not increment address on write to PIO
channel_config_set_write_increment(&cfg, false);

// each DMA transfered entry is 32-bits
channel_config_set_transfer_data_size(&cfg, DMA_SIZE_32);

// DMA data request for sending data to PIO
channel_config_set_dreq(&cfg, pio_get_dreq(QVGA_PIO, QVGA_SM, true));

// chain channel to DMA control block
channel_config_set_chain_to(&cfg, QVGA_DMA_CB);

// raise the IRQ flag when 0 is written to a trigger register (end of chain)
channel_config_set_irq_quiet(&cfg, true);

// set high priority
cfg.ctrl |= DMA_CH0_CTRL_TRIG_HIGH_PRIORITY_BITS;

// DMA configure
dma_channel_configure(
	QVGA_DMA_PIO,		// channel
	&cfg,			// configuration
	&QVGA_PIO->txf[QVGA_SM], // write address
	NULL,			// read address
	0,			// number of transfers in u32
	false			// do not start immediately
);

Podobně, jako u řídicího DMA kanálu, zvolíme automatickou inkrementaci čtecí adresy a velikost jednotky dat 32 bitů. U zápisové adresy nebudeme volit automatickou inkrementaci - budeme zapisovat data vždy jen na jednu adresu, do TX FIFO řadiče PIO SM.

Funkcí channel_config_set_dreq nastavíme, že DMA přenos se bude spouštět na požadavek dat od PIO v případě nedostatku dat ve vysílacím FIFO.

Funkce channel_config_set_chain_to zajistí zřetězení datového DMA s řídicím DMA. Dokončí-li datový DMA kanál svůj přenos, probudí řídicí DMA kanál pomocí zřetězení, a ten mu odešle nové řídicí informace.

Funkce channel_config_set_irq_quiet aktivuje "tichý režim". Poslední řídicí informace, kterou řídicí DMA kanál předá datovému DMA kanálu (na konci videolinky), bude hodnota 0. Zápisem hodnoty 0 do spouštěcího (posledního) registru se nenastartuje další přenos, ale datový DMA kanál aktivuje IRQ signál, který vyvolá přerušení hlavního procesoru a zahájení nového přenosu.

Datovému kanálu je nastavena vyšší priorita. Jednotlivé DMA kanály jsou procesorem obsluhovány postupně (do kruhu), s upřednostněním kanálů s vyšší prioritou. QVGA generátor vyžaduje vysokou prioritu - zpoždění DMA přenosu by mohlo znamenat výpadek obrazového signálu (stává se např. při čtení dat z externí Flash paměti).

Jako zápisová adresa se zvolí vysílací FIFO port PIO SM řadiče. Ostatní parametry není nutné nastavovat, nastaví je řídicí DMA kanál při prvním přenosu.

Nakonec nakonfigurujeme IRQ0, který bude aktivován datovým DMA na konci každé videolinky.


// enable DMA channel IRQ0
dma_channel_set_irq0_enabled(QVGA_DMA_PIO, true);

// set DMA IRQ handler
irq_set_exclusive_handler(DMA_IRQ_0, QVgaLine);

// set highest IRQ priority
irq_set_priority(DMA_IRQ_0, 0);

Funkcí dma_channel_set_irq0_enabled se povolí, že datový DMA kanál bude aktivovat signál IRQ0.

Funkce irq_set_exclusive_handler nastaví adresu funkce pro obsluhu IRQ0 přerušení. Použijeme exkluzivní obsluhu, protože obsluha musí být rychlá, bez prodlev, jinak by mohl obraz vypadávat. Ze stejného důvodu je pro přerušení IRQ0 nastavena maximální priorita.

Obsluha přerušení

Na konci každé videolinky vyvolá datový DMA kanál přerušení IRQ0. Vyvolá se obsluha, ve které aktivujeme DMA přenos pro další videolinku a připravíme buffery pro příští videolinku. Přerušení se aktivuje ve chvíli, kdy se do vysílacího FIFO odešlou poslední obrazová data a příkaz pro front porch, tedy před příštím HSYNC. Prodleva pro front porch a velikost FIFO bufferu zajistí prodlevu k nastavení dalšího přenosu. U obsluhy si tak lze vystačit s C kódem bez assembleru, přesto ale času není nazbyt a je potřeba rychlá odezva.


u32* ScanLineCBNext;	// next control buffer

// saved integer divider state
hw_divider_state_t SaveDividerState;

// QVGA DMA handler - called on end of every scanline
void __not_in_flash_func(QVgaLine)()
{
	// Clear the interrupt request for DMA control channel
	dma_hw->ints0 = (1u << QVGA_DMA_PIO);

	// update DMA control channel and run it
	dma_channel_set_read_addr(QVGA_DMA_CB, ScanLineCBNext, true);

	// save integer divider state
	hw_divider_save_state(&SaveDividerState);

	...

	// restore integer divider state
	hw_divider_restore_state(&SaveDividerState);
}

Z důvodu rychlé odezvy je záhlaví funkce označeno typem __not_in_flash_func. V tom případě je celý kód funkce při startu programu zkopírován do RAM paměti, odkud je spouštěn, namísto spouštění z externí Flash paměti. U krátkého programu rozdíl nezpozorujete, ale bude-li váš program obsahovat více kódu, nevejdou se instrukce programu do cache paměti procesoru a budou načítány z externí Flash paměti. To je poměrně zdlouhavá operace. Pokud by byl kód přerušení načítán z externí Flash, znamenalo by to příliš dlouhou prodlevu a tím i výpadek videosignálu.

Na začátku funkce je nejdříve vynulován požadavek přerušení IRQ0. To kdyby se funkce zdržela v obsluze, tak ať se neztratí případný příští požadavek.

Funkcí dma_channel_set_read_addr se nastaví čtecí adresa řídicího DMA kanálu pro nový přenos a DMA přenos se spustí. Ostatní parametry, jako počet přenesených dat, zůstávají stejné z dřívějšího nastavení.

Funkce hw_divider_save_state uchová registry hardwarové děličky do pomocného bufferu. Hardwarová dělička je specialita Raspberry Pico. Umožňuje 32-bitové celočíselné dělení a výpočet trvá 8 systémových taktů. Její stav není automaticky uchován v obsluze přerušení, protože nepatří do standardních registrů procesoru. Pokud by překladač zde v obsluze použil celočíselné dělení, obsah děličky by se změnil a hlavní program by mohl dostat chybný výpočet. Obsah děličky se obnoví funkcí hw_divider_restore_state na konci obsluhy přerušení. Kódu uvnitř obsluhy se budeme věnovat dále. Před uložením stavu děličky je potřeba počkat minimální čas 8 taktů hodin, aby se dokončila prováděná operace, ale to je v tomto případě zajištěno.


int QVgaBufInx;	// current running control buffer

// switch current buffer index (bufinx = current preparing buffer, QVgaBufInx = current running buffer)
int bufinx = QVgaBufInx;
QVgaBufInx = bufinx ^ 1;

// prepare control buffer to be processed
u32* cb = &ScanLineCB[bufinx*CB_MAX];
ScanLineCBNext = cb;

Při obsluze řídicího DMA používáme 2 řídicí buffery ScanLineCB, které mezi sebou neustále střídáme. Který buffer se aktuálně používá, nám říká proměnná QVgaBufInx. Při každé obsluze přerušení překlopíme číslo aktuálního bufferu a připravíme si ukazatel na řídicí buffer, který bude použit v příští obsluze.


volatile int QVgaScanLine;	// current processed scan line 0... (next displayed scan line)
volatile u32 QVgaFrame;		// frame counter

// increment scanline (1..)
int line = QVgaScanLine; 	// current scanline
line++; 			// new current scanline
if (line >= QVGA_VTOT) 		// last scanline?
{
	QVgaFrame++;		// increment frame counter
	line = 0; 		// restart scanline
}
QVgaScanLine = line;		// store new scanline

Proměnná QVgaScanLine nás informuje o tom, která videolinka aktuálně probíhá. V každé obsluze přerušení ji inkrementujeme a pokud dosáhne konce snímku, vynulujeme ji a současně inkrementujeme čítač snímků QVgaFrame. Čítač videolinek můžeme používat v hlavním programu k detekci zatemňovacího pulsu (pro "čisté" zobrazení bez narušení obrazu) a čítač snímků k časování a synchronizaci programu.

V další obsluze budeme připravovat obsah řídicího bufferu pro další videolinku, v závislosti podle čísla videolinky.


// check scanline
line -= QVGA_VSYNC;
if (line < 0)
{
	// VSYNC
	*cb++ = 2;
	*cb++ = (u32)&ScanLineSync[0];
}

Při obsluze linky s vertikální synchronizací uložíme do řídicího bufferu adresu datového bufferu s řídicími kódy pro vertikální synchronizaci. Buffer obsahuje 2 slova po 32 bitech - HSYNC a zatemnění.


// front porch and back porch
line -= QVGA_VBACK;
if ((line < 0) || (line >= QVGA_VACT))
{
	// dark line
	*cb++ = 2;
	*cb++ = (u32)&ScanLineDark[0];
}

V obsluze front a back porch vysíláme pouze černý obraz, tedy 2 slova z bufferu zatemnění.


// prepare image line
line >>= 1;

// HSYNC ... back porch ... image command
*cb++ = 3;
*cb++ = (u32)&ScanLineImg[0];

// image data
*cb++ = WIDTH/4;
*cb++ = (u32)&pFrameBuf[line*WIDTH];

// front porch
*cb++ = 1;
*cb++ = (u32)&ScanLineFp;

V obsluze zobrazení obrazu vydělíme číslo videolinky číslem 2, protože linky budou zdvojené. Namísto 480 videolinek budeme generovat 240 linek obrazu. Nejdříve vložíme odkaz na buffer se začátkem videolinky - HSYNC, back porch a příkaz k vysílání dat. Dále uložíme odkaz na obrazová data. Vypočteme adresu v obrazovém bufferu, jako délku dat použijeme šířku obrazu/4 (vysílá se po 32 bitech). Z tohoto důvodu je nutné, aby obrazové buffery byly vždy zarovnané na 32 bitů. Po obrazových datech odešleme front porch, jako 1 řídicí slovo.


// end mark
*cb++ = 0;
*cb = 0;

Na závěr všech segmentů ukončíme řídicí buffer nulami. To je koncová značka, která příště zajistí vyvolání nového IRQ přerušení.

Nejdelší řídicí kód je pro obsluhu zobrazení. Do řídicího bufferu se uloží 6 řídicích slov, plus koncová značka 2 slova. To je celkem 8 slov. Z toho důvodu je velikost jednoho řídicího bufferu nastavena na 8 slov, pomocí definice CB_MAX. V případě modifikací kódu je na to potřeba pamatovat a případně velikost bufferu zvětšit.

Inicializace QVGA knihovny

Funkce pro celkovou inicializaci QVGA knihovny bude mít následující obsah.


#define QVGA_VCO	(1008*1000*1000) // PLL VCO frequency in Hz
#define QVGA_PD1	4	// PLL PD1
#define QVGA_PD2	2	// PLL PD2

// initialize system clock
set_sys_clock_pll(QVGA_VCO, QVGA_PD1, QVGA_PD2);

// initialize PIO
QVgaPioInit();

// initialize scanline buffers
QVgaBufInit();

// initialize DMA
QVgaDmaInit();

// initialize parameters
QVgaScanLine = 0; // currently processed scanline
QVgaBufInx = 0; // at first, control buffer 1 will be sent out
QVgaFrame = 0; // current frame
ScanLineCBNext = &ScanLineCB[CB_MAX]; // send control buffer 2 next

// enable DMA IRQ
irq_set_enabled(DMA_IRQ_0, true);

// start DMA
dma_channel_start(QVGA_DMA_CB);

// run state machine
pio_sm_set_enabled(QVGA_PIO, QVGA_SM, true);

Před inicializací knihovny jsou funkcí set_sys_clock_pll nastaveny systémové hodiny na 126 MHz. Funkci je možné uvést v hlavní funkci programu nebo ji úplně vynechat (ponechat implicitní nastavení 125 MHz) a použít trochu jiné nastavení časování QVGA knihovny - může se ovšem projevit chybějícími 2 pixely na začátku a konci obrazu.

Funkcí QVgaPioInit se inicializuje PIO. Funkcí QVgaBufInit se připraví datové a řídicí buffery. Funkcí QVgaDmaInit se inicializují DMA kanály.

Nastaví se globální proměnné na výchozí hodnoty - start videolinkou 0, start prvního řídicího bloku, příště bude řídicí blok 1.

Funkcí irq_set_enabled se povolí vyvolání přerušení IRQ0.

Funkcí dma_channel_start se odstartuje řídicí DMA kanál. Ten načte první řídicí blok a aktivuje datový DMA kanál.

Funkcí pio_sm_set_enabled se odstartuje PIO program. Program začne odčerpávat data z FIFO, vyvolávat požadavky na DMA přenos a vysílat data na výstupní porty.


#define FRAMESIZE (WIDTH*HEIGHT) // display frame size in bytes (=76800)

// display frame buffer
ALIGNED u8 FrameBuf[FRAMESIZE];

pFrameBuf = FrameBuf; // front buffer
pDrawBuf = FrameBuf; // back buffer

Před aktivací QVGA knihovny je nutné připravit v hlavním programu obrazový buffer. Buffer musí mít velikost FRAMESIZE a musí být zarovnaný na 32 bitů (to zajistí makro ALIGNED). Ukazatel na obrazový buffer je potřeba nastavit do proměnné pFrameBuf. Je-li používána knihovna pro kreslení, je potřeba nastavit i ukazatel do proměnné pDrawBuf. Knihovna umožňuje používat jiný buffer pro kreslení a jiný pro zobrazení. Je tak možné připravit obraz bez zobrazení a bez vzniku přechodných artefaktů a obrazy poté přepnout.

Multicore

Obsluha QVGA zobrazení je poměrně choulostivá záležitost. Pokud se přenosový řetězec někde přeruší nebo pozastaví, dojde k výpadku generovaného signálu, což může znamenat ztmavení obrazu QVGA monitoru i na několik sekund. Proto je potřeba dbát na co nejlepší provozní podmínky QVGA generátoru.

Jedním možným zádrhelem je přerušení. V době zpracování jiného přerušení nemůže procesor obsloužit požadavek přerušení od obsluhy QVGA signálu, obraz by mohl vypadnout. To by znamenalo, že by nebylo možné používat jiná přerušení než z generátoru QVGA signálu.

Naštěstí procesor Rasberry Pico má na to řešení, pomocí druhého jádra procesoru. Obsluhy přerušení jader procesoru jsou na sobě nezávislé. Je tedy možné vyhradit druhé jádro (s číslem 1) pro obsluhu QVGA obrazu a první jádro (s číslem 0) pro chod hlavního programu a obsluhy ostatních přerušení. Obsluha QVGA signálu je časově nenáročná a proto není problém používat druhé jádro i pro chod programů. Omezení spočívá pouze ve vyhrazení přerušení v druhém jádru pouze pro QVGA.


// initialize QVGA
// QVgaInit();
multicore_launch_core1(QVgaCore);

void (* volatile Core1Fnc)() = NULL; // core 1 remote function

// QVGA core
void QVgaCore()
{
	void (*fnc)();

	// initialize QVGA
	QVgaInit();

	// infinite loop
	while (true)
	{
		// data memory barrier
		__dmb();

		// execute remote function
		fnc = Core1Fnc;
		if (fnc != NULL)
		{
			fnc();
			__dmb();
			Core1Fnc = NULL;
		}
	}
}

// execute core 1 remote function
void Core1Exec(void (*fnc)())
{
	__dmb();
	Core1Fnc = fnc;
	__dmb();
}

// check if core 1 is busy (executing remote function)
Bool Core1Busy()
{
	__dmb();
	return Core1Fnc != NULL;
}

// wait if core 1 is busy (executing remote function)
void Core1Wait()
{
	while (Core1Busy()) {}
}

Funkce QVgaCore je obsluha QVGA displeje v druhém jádru procesoru. Obsluhu spustíme funkcí multicore_launch_core1. Funkce při svém startu spustí obsluhu QVGA na druhém jádru funkcí QVgaInit. Poté procesor běhá v nekonečné smyčce a čeká na případnou aktivaci dálkové funkce. Přijde-li od hlavního procesoru povel k provedení dálkové funkce (voláním funkce Core1Exec), funkce se provede a pokračuje se v nekonečné smyčce.

Povšimněte si funkcí __dmb. To je instrukce datové bariéry. Datová bariéra zajistí, že jádro procesoru zajistí uložení své zápisové cache paměti do sdílené RAM paměti a tím data uvidí i druhé jádro. Bez datové bariéry by se mohlo stát, že jedno jádro nastaví obsah proměnné, ale změněná data zůstanou jen v cache paměti jednoho jádra, a druhé jádro změněná data neuvidí.

Funkce Core1Exec slouží ke spuštění funkce v druhém jádru. Funkcí Core1Busy může první jádro sledovat, zda se funkce stále ještě zpracovává. Případně může funkcí Core1Wait počkat, až se funkce dokončí. Při předávání dat mezi jádry procesoru pamatujte na vkládání datové bariéry __dmb.

Abychom nemuseli QVGA obsluhu spouštět z hlavního programu, je do knihovny doplněno automatická inicializace knihovny pomocí následujícího kódu.


// auto initialization
Bool QVgaAutoInit()
{
	multicore_launch_core1(QVgaCore);
	return True;
}

Bool QVgaAutoInitOK = QVgaAutoInit();

Podpůrné funkce

Kromě hlavního jádra obsahuje PicoQVGA knihovna i další podpůrné funkce. Programy a utility jsou ve Windows EXE formátu.

draw - je knihovna pro kreslení do obrazového bufferu. Před použitím je nutné nastavit proměnnou pDrawBuf na adresu obrazového bufferu, s rozměry WIDTH x HEIGHT. Knihovna obsahuje funkce pro kreslení obdélníku, rámu, kruhu, kružnice, texty a obrázky. Ukazatel na obrazový buffer je možné nastavit jiný než pro zobrazení a tak si připravit obraz na pozadí, a až po vykreslení obrazové buffery přepnout.

rand - generátor náhodného čísla. Knihovna generuje náhodná čísla s 32-bitovým seed, s různou velikostí výstupních dat a ve volitelných rozsazích. Poznámka: nepoužívejte ke generování náhodného čísla v intervalu funkci modulo %, výsledná náhodnost není rovnoměrná. Knihovna zajistí rovnoměrnější rozložení pravděpodobnosti.

pwmsnd - zvukový výstup. Knihovna aktivuje PWM generátor na příslušném výstupním pinu. K pinu lze (přes RC filtr) připojit sluchátka nebo externí zesilovač. Zvukový soubor má formát PCM 22050 Hz, mono, 8 bitů unsigned. Je možné jak jednorázové přehrátí zvuku, tak i opakované přehrávání s proměnlivou rychlostí.

pal332 - program pro vygenerování barevných palet RGB332 pro knihovnu PicoQVGA. Vytvořený soubor pal332.act lze použít v programu Photoshop ke konverzi obrázků na palety knihovny PicoQVGA.

RaspPicoImg - utilita pro import obrázků do zdrojového kódu. Obrázek musí být BMP v 8-bitovém módu RGB332 bez komprese, se zapnutým přepínačem pro obrácení pořadí linek.

PicoClock - výpočet nastavení PLL generátoru systémových hodin.

RaspPicoSnd - konverze zvuků do zdrojového kódu. Zvuky musí být WAV ve formátu PCM, 8 bitů unsigned mono, 22050 Hz.

Ukázkové programy

Programy jsou připravené pro ovládání klávesnicí přes konzoli PC. Jako konzoli lze použít program begPutty a virtuální sériový USB port. Některé programy používají zvuk - generování PWM zvuku přes pin GP0 (označeno poznámkou "(zvuk)").

Balloons - demonstrace kreslení do back bufferu, poletující balonky (celkem 23 sprajtů).
Draw - demonstrace kreslení grafických prvků. Pro ukázku se střídá pomalé vykreslování a kreslení maximální rychlostí.
Earth - rotující zeměkoule. Softwarová sférická transformace obrázku.
Eggs - logická hra (zvuk). Vychází ze hry Reversi. Cílem je získat co nejvíce vlastních kamenů. Jeden hráč mění kameny ve směru slepice-kuře-vejce, druhý hráč opačným směrem. Ovládání: L vpravo, I nahoru, J vlevo, K dolů, H pomoc, Q konec, P 2 hráči, D demo, mezerník položení kamene, Enter ok. Lze hrát proti jinému hráči i proti počítači.
Fifteen - logická hra (zvuk). Cílem je seřadit kameny v pořadí 1 až 15. Ovládání: L vpravo, I nahoru, J vlevo, K dolů, Q nová hra.
Flag - vlající vlajka.
Hello World - nejjednodušší ukázkový příklad použití knihovny PicoQVGA.
Hypno - hypnotizující rotující obrazec. Ukázka maticové transformace obrázku.
Life - simulátor života buněk (celulární automat). Buňky se v každém kroku mění podle počtu sousedních buněk: pro 1 a méně buňka zaniká na osamocení, pro 4 a více buňka zaniká na přemnožení, pro 3 vznikne nová buňka, pro 2 není změna. Ve hře se lze přepínat mezi 10 obrazovkami (sloty) a přenášet obraz mezi nimi pomocí schránky. V každém slotu je předpřipravena definice populárních kombinací. Ovládání: L vpravo, I nahoru, J vlevo, K dolů, C kopie do schránky, V vložení ze schránky, D vymazání plochy, mezerník změna buňky, Enter start/stop automatu, 0-9 výběr slotu.
Lines - generátor čárových relaxačních obrazců.
Mandelbrot - generátor fraktálových obrazců Mandelbrotovy množiny. Ke generování je použita integer matematika a díky tomu je překreslení obrazce rychlé. Je však nutno počítat s tím, že při zvětšování měřítka zobrazení je potřebná rostoucí přesnost počtu číslic. Použitá integer a float matematika vystačí do měřítka zvětšení 10^5, double matematika do měřítka 10^10. Při dalším zvětšování se namísto obrazce zobrazí už jen barevné čáry. Ovládání: E nahoru, S vlevo, D vpravo, X dolů, Q zvětšení měřítka, A zmenšení měřítka, I přepnutí na celočíselnou matematiku (nejrychlejší, dosah do 10^-5), F přepnutí na float matematiku (pomalejší, dosah do 10^-5), B přepnutí na double matematiku (pomalá, dosah do 10^-10), O snížení počtu kroků iterací, P zvýšení počtu kroků iterací, mezera překreslení obrazovky.
Matrix Rain - simulace "matrixového kódového deště".
Maze - cílem je najít cestu z bludiště. Bludiště jsou generována náhodně programově. Ovládání: J vlevo, I nahoru, L vpravo, K dolů, H pomoc (ukázání dveří).
Monoscope - test displeje, zobrazí monoskop.
Pi - výpočet čísla Pi na 1180 číslic. Po výpočtu je výsledek zkontrolován s očekávaným vzorkem.
Pixels - náhodné generování barevných pixelů.
Raytrace - generování 3D obrazce metodou ray tracing. Vzhledem k omezené barevné hloubce PicoQVGA je při zobrazení použit rastrový dithering ("zrnitost" obrázku).
RGBtest - test barev
Spheres - náhodné generování koulí.
Spots - náhodné generování skvrn.
Twister - zkroucení texturovaného kvádru. Slouží jako ukázka programové deformace obrázku.
Water Surface - simulace vlnící se vodní hladiny (zvuk).

Download knihovny PicoQVGA

Miroslav Němeček

<< Zpět