Uděláme pomocí Raspberry Pi Pico jednoduchou "VGA kartu" s rozlišením 640x468 pixelů a 8 barvami.

Jak je to naprogramované

Tento kód používá tři stavové automaty PIO (synchronizované navzájem pomocí přerušení) k řízení obrazovky VGA. Data pixelů, která se mají vykreslit na obrazovku, jsou do stavových automatů PIO přenášena prostřednictvím kanálu DMA. Všechna tato data pixelů jsou uložena v globálním poli znaků. V aplikační části kódu tedy uživatel stačí upravit obsah tohoto globálního pole znaků a změny se automaticky zobrazí na obrazovce VGA.

Co se týče zdrojů, tento kód používá stavové automaty PIO 0, 1 a 2 na instanci PIO 0. Používá všech 32 dostupných instrukcí PIO a používá 2 kanály DMA (jeden pro přenos dat do systému PIO a druhý pro rekonfiguraci a restart prvního). Pro uložení barevných dat pro každý pixel se používají pouze 3 bity, což znamená, že na obrazovku lze vykreslit pouze 8 barev. Protože pro reprezentaci barvy v každém pixelu se používají pouze 3 bity, každý znak v poli znaků ukládá data pro 2 pixely (a 2 z 8 bitů se ztratí). To dává celkové využití paměti 1,536 kB.

Tento dokument vysvětluje kód ovladače VGA a poté jej používá k vykreslení Mandelbrotovy množiny na obrazovku.

flow

Protokol VGA

Řízení obrazovky VGA vyžaduje manipulaci se dvěma digitálními synchronizačními piny a třemi analogovými barevnými piny (ČERVENÁ, ZELENÁ a MODRÁ). Jeden ze synchronizačních pinů, HSYNC, říká obrazovce, kdy se má přesunout na nový řádek pixelů. Druhý synchronizační pin, VSYNC, říká obrazovce, kdy má začít nový snímek. Protokol je popsán níže, a to jak textově, tak vizuálně.

  1. VGA pixelový takt běží na 25,172 MHz (na mé VGA obrazovce si vystačím s 25 MHz).

  2. HSYNC i VSYNC začínají v aktivním režimu (logická úroveň vysoká).

  3. HSYNC zůstává v aktivním režimu po dobu 640 cyklů pixelových taktů (tj. jeden řádek displeje VGA).

  4. Pro každý z 640 cyklů se napětí na linkách ČERVENÁ, ZELENÁ a MODRÁ mění mezi 0 a 0,7 V, přičemž každé napětí představuje intenzitu dané barvy pro daný pixel.

  5. Po 640 taktovacích cyklech se linky ČERVENÁ, ZELENÁ a MODRÁ nastaví na 0 a linka HSYNC zůstává vysoká po celou dobu svého předního pulzu (16 taktovacích cyklů pixelů).

  6. HSYNC je nastaven na logickou úroveň nízkou po dobu 96 taktovacích cyklů pixelů (to je horizontální synchronizační impuls).

  7. HSYNC je nastaven na logickou úroveň vysokou po dobu svého zadního pulzu (48 taktovacích cyklů pixelů).

  8. HSYNC se poté vrátí na začátek aktivního režimu (krok 2 výše) a proces se opakuje pro další řádek pixelů. Každý řádek pixelů je řádek.

  9. VSYNC zůstává v aktivním režimu (logická úroveň vysoká) po dobu 480 řádků.

  10. Po 480 řádcích se napětí na linkách ČERVENÁ, ZELENÁ a MODRÁ nastaví na 0 a linka VSYNC zůstává vysoká po celou dobu svého předního pulzu (10 řádků).

  11. VSYNC je nastaven na logickou úroveň nízkou po dobu 2 řádků (to je vertikální synchronizační impuls).

  12. VSYNC je nastaven na logickou úroveň 1 až do konce (33 řádků).

  13. VSYNC se poté vrátí na začátek aktivního režimu (krok 2 výše) a proces se opakuje pro další rámec.

protocol

Vytváření Hsync pulsů

Kód v PIO assembleru vytváří signál HSYNC pro VGA displej. Tento kód používá hodiny běžící na kmitočtu 25 MHz.

pull block              ; Pull from FIFO to OSR (only happens once)
.wrap_target            ; Program wraps to here

; ACTIVE + FRONTPORCH
mov x, osr              ; Copy value from OSR to x scratch register
activeporch:
   jmp x-- activeporch  ; Remain high in active mode and front porch

; SYNC PULSE
pulse:
    set pins, 0 [31]    ; Low for hsync pulse (32 cycles)
    set pins, 0 [31]    ; Low for hsync pulse (64 cycles)
    set pins, 0 [31]    ; Low for hsync pulse (96 cycles)

; BACKPORCH
backporch:
    set pins, 1 [31]    ; High for back porch (32 cycles)
    set pins, 1 [12]    ; High for back porch (45 cycles)
    irq 0       [1]     ; Set IRQ to signal end of line (47 cycles)
.wrap

Řádek po řádku se děje toto:

  1. blok pull čeká, až kód C vloží informace do vyrovnávací paměti TX stavového automatu pio, a poté tyto informace stáhne do výstupního posuvného registru. Protože se tento řádek kódu nachází nad direktivou wrap_target, bude proveden pouze jednou. Toto se používá k uložení délky režimů active+front porch signálu HSYNC (640+16) do výstupního posuvného registru. Tuto hodnotu musíme inicializovat tímto způsobem, protože v assembleru PIO nemůžeme nastavit hodnotu žádného registru na číslo větší než 31.

  2. Direktiva .wrap_target označuje místo, kam se kód po dokončení zalomí. Pokud bychom neměli direktivy .wrap a .wrap_target, kód by se zalomil zpět na první řádek.

  3. Poté přejdeme do aktivních/předních režimů. Další řádek kódu přesune data z výstupního posuvného registru (který obsahuje délku aktivního a předního režimů) do pomocného registru x.

  4. activeporch je návěští, které můžeme použít s instrukcí jmp pro návrat na konkrétní místo v kódu.

  5. jmp x — activeporch bude snižovat hodnotu v pomocném registru x a vracet se na místo v activeporch v kódu, dokud je hodnota x větší než 0. Když se x rovná 0, podmínka jmp selže a kód se přesune na další řádek.

  6. Návěští pulse určuje část signálu HSYNC, která se týká synchronizačního pulsu.

  7. Nastavíme hodnotu skupiny pinů (mapovaných samostatně na konkrétní GPIO porty) na nízkou úroveň a čekáme 31 cyklů. Všimněte si, že zpoždění začíná po provedení řádku. Takže tento řádek nastaví hodnotu pinů na nízkou úroveň a poté bude čekat dalších 31 cyklů, celkem tedy 32.

  8. Znovu to uděláme to samé aby celkový čas s piny na nízké úrovni dosáhl 64 cyklů.

  9. A znovu, aby celkový čas s piny na nízké úrovni dosáhl 96 cyklů (délka synchronizačního impulsu).

  10. Poté vstoupíme do režimu backporch.

  11. Piny jsou nastaveny na vysokou úroveň a provedeme zpoždění o 31 cyklů.

  12. Piny zůstanou na vysoké úrovni a provedeme zpoždění o dalších 12 cyklů.

  13. Nastavíme přerušení 0 (toto se používá k synchronizaci s assemblerovým kódem VSYNC) a provedeme zpoždění o 1 cyklus. Toto zpoždění je zde proto, aby se tento kód vrátil do aktivního režimu ve stejném cyklu jako assemblerový kód VSYNC, se kterým je synchronizován.

Takto to vypadá na osciloskopu.

HSYNC signál. Naměřený celkový čas HIGH (backporch+frontporch+active) je \(28.2 \mu s\), jak je očekáváno po 704 cyklech hodin na 25 MHz.

hsync

Generování Vsync

Následující kód generuje VSYNC. Tento stavový automat má také hodiny běžící na 25 MHz, ale je z velké části řízen stavovým automatem HSYNC (protože VSYNC pracuje v jednotkách řádků a nikoli v pixelových hodinových cyklech).

pull block                        ; Načíteme z FIFO do OSR (pouze jednou)
.wrap_target                      ; Sem se bude program cyklit

; ACTIVE
mov x, osr                        ; Kopíruje se hodnota z OSR do prázdného registru x
active:
    wait 1 irq 0                  ; Čekáme, až hsync přejde na vysokou úroveň
    irq 1                         ; Signalizujeme, že jsme v aktivním režimu
    jmp x-- active                ; Zůstáváme v aktivním režimu a snižujeme čítač x

; FRONTPORCH
set y, 9                          ; Použijeme registr y jako čítač
frontporch:
    wait 1 irq 0                  ; Čekáme, až hsync přejde na vysokou úroveň
    jmp y-- frontporch            ; Zůstáváme ve frontporch a snižujeme čítač y

; SYNC PULSE
set pins, 0                       ; Nastavíme pin na nízkou úroveň
wait 1 irq 0                      ; Čekáme jeden VGA řádek
wait 1 irq 0                      ; Čekáme ještě jeden VGA řádek

; BACKPORCH
set y, 31                         ; First part of back porch into y scratch register (and delays a cycle)
backporch:
    wait 1 irq 0   side 1         ; Čekáme až se hsync dostane na vysokou úroveň - SIDESET REPLACEMENT HERE
    jmp y-- backporch             ; Zůstáváme v backporch a snižujeme čítač y

.wrap                             ; program odtud skočí na .wrap_target

Řádek po řádku se děje toto:

  1. pull block čeká, až kód C vloží informace do vyrovnávací paměti TX stavového automatu pio, a poté tyto informace stáhne do výstupního posuvného registru. Protože tento řádek kódu je nad direktivou .wrap_target, bude proveden pouze jednou. Toto se používá k uložení délky aktivního režimu signálu VSYNC (480 řádků) do výstupního posuvného registru. Tuto hodnotu musíme inicializovat tímto způsobem, protože v assembleru PIO nemůžeme nastavit hodnotu žádného registru na číslo větší než 31.

  2. Direktiva .wrap_target označuje místo, kam se kód po dokončení zalomí (zacyklí). Pokud bychom neměli direktivy .wrap a .wrap_target, kód by se zalomil zpět na první řádek.

  3. Poté přejdeme do aktivního režimu. Další řádek kódu přesune data z výstupního posuvného registru (který obsahuje délku aktivního režimu) do pomocného registru x.

  4. active je návěští, které můžeme použít s instrukcí jmp k návratu na konkrétní místo v kódu.

  5. Stavový automat čeká na přerušení nastavené stavovým automatem HSYNC, které signalizuje konec řádku. Tato přerušení určují tempo čtení a odpočítávání řádků.

  6. Pokaždé, když stavový automat HSYNC signalizuje nový horizontální řádek a stavový automat VSYNC je v aktivním režimu, nastaví samostatné přerušení. Toto přerušení používá stavový automat RGB PIO k určení, kdy začít ukládat data na piny RGB.

  7. Příkaz jmp x-- active snižuje hodnotu v pomocném registru x a vrací se do aktivní pozice v kódu, dokud je hodnota x větší než 0. Když se x rovná 0, podmínka jmp selže a kód pokračuje na další řádek.

  8. Další řádek nastaví hodnotu pomocného registru y na délku přední části signálu VSYNC (mínus 1, abychom získali celkem 10 čekání).

  9. Návěští frontporch určuje část signálu VSYNC, která se týká přední části signálu.

  10. Stejně jako dříve, kód používá příkazy wait a jmp k setrvání v tomto stavu, dokud stavový automat HSYNC nenastaví přerušení pro správný počet opakování.

  11. Nastavíme hodnotu skupiny pinů (mapovaných samostatně na specifické porty GPIO) na nízkou úroveň a poté čekáme na dokončení dvou linek HSYNC. Toto je délka synchronizačního impulsu VSYNC.

  12. Poté vstupujeme do zadní části signálu. Délka zadní části signálu je uložena v pomocném registru y.

  13. Stejně jako dříve používáme příkazy wait a jmp k setrvání v tomto stavu po odpovídající počet řádků.

K nastavení linky VSYNC na vysokou úroveň používáme instrukci side set místo instrukce set. Piny set a side set jsou konfigurovány tak, aby se překrývaly, a to nám ušetří jednu instrukci.

Generování barevných RGB signálů

Stavový automat pro generování 25MHz RGB signálů je poměrně jednoduchý. Po určité inicializaci čítacích registrů vynuluje barevné piny, dokud neobdrží signál ze stavového automatu VSYNC, že byl spuštěn nový řádek a VSYNC je v aktivním režimu. Když je tato podmínka splněna, RGB automat ví, že by měl naprogramovat 640 pixelů (tj. jeden řádek). Dělá to tak, že provede načítání z TX FIFO (které je přiváděno k pixelovým datům z DMA kanálu), odešle první 3 bity na RGB piny, čeká na příslušný počet cyklů, odešle další 3 bity, čeká na příslušný počet cyklů, poté se vrátí zpět a opakuje to, dokud nedokončí řádek. Jakmile je řádek dokončen, vynuluje výstupy a znovu čeká na signál ze stroje VSYNC. Všimněte si, že tento stavový automat běží na 125 MHz.

pull block          ; Načíteme z FIFO do OSR (pouze jednou)
mov y, osr          ; Zkopírujeme hodnotu z OSR do pomocného registru y
.wrap_target

set pins, 0         ; Vynulovat RGB piny
mov x, y            ; Inicializujeme proměnnou x čítače

wait 1 irq 1 [3]    ; Čekáme na aktivní režim vsync (spustí se 5 cyklů po provedení)

colorout:
pull block          ; Načteme hodnotu barvy
out pins, 3 [4]     ; hodnoty dáme na piny (první pixel)
out pins, 3 [2]     ; hodnoty dáme na piny (další pixel)
jmp x-- colorout    ; Opakujeme cyklus až do vyčerpání čítače x

.wrap

Použití DMA pro řízený pixelových dat

Existuje globální pole znaků s názvem vga_data_array o délce TXCOUNT (153600 bajtů). Každý znak v tomto poli obsahuje informace o barvě dvou sousedních pixelů. Tyto informace o barvě jsou sdělovány po 8 bitech do stavového automatu RGB PIO prostřednictvím kanálu DMA. Tento kanál DMA je řízen signálem požadavku na data DREQ_PIO0_TX2 (takže se přenáší pouze tehdy, když automat PIO vyprázdní FIFO) a je restartován a překonfigurován druhým kanálem DMA, který je zřetězen s prvním. Tento druhý kanál DMA zapisuje ukazatel na počáteční adresu prvního kanálu DMA do svého registru read_address.

// Kanály DMA - kanál 0 odesílá data o barvě, kanál 1 překonfiguruje a restartuje kanál 0
int rgb_chan_0 = 0;
int rgb_chan_1 = 1;

// Kanál nula (odesílá barevná data do počítače PIO VGA)
dma_channel_config c0 = dma_channel_get_default_config(rgb_chan_0); // výchozí konfigurace
channel_config_set_transfer_data_size(&c0, DMA_SIZE_8);             // 8bitové převodníky
channel_config_set_read_increment(&c0, true);                       // inkrementace adresu čtení
channel_config_set_write_increment(&c0, false);                     // neinkrementujeme adresu zápisu
channel_config_set_dreq(&c0, DREQ_PIO0_TX2);                        // DREQ_PIO0_TX2 (FIFO)
channel_config_set_chain_to(&c0, rgb_chan_1);                       // řetězení k kanálu 1

dma_channel_configure(
    rgb_chan_0,         // Kanál ke konfiguraci
    &c0,                // Konfigurace, kterou jsme právě vytvořili
    &pio->txf[rgb_sm],  // adresa zápisu (RGB PIO TX FIFO)
    &vga_data_array,    // Počáteční adresa pro čtení (pole barev pixelů)
    TXCOUNT,            // Počet přenosů; v tomto případě každý má 1 bajt.
    false               // Nezačínat ihned.
);

// Kanál jedna (překonfiguruje kanál 0)
dma_channel_config c1 = dma_channel_get_default_config(rgb_chan_1); // výchozí konfigurace
channel_config_set_transfer_data_size(&c1, DMA_SIZE_32);            // 32bitové převodníky
channel_config_set_read_increment(&c1, false);                      // žádné zvyšování čtení
channel_config_set_write_increment(&c1, false);                     // žádné zvyšování zápisu
channel_config_set_chain_to(&c1, rgb_chan_0);                       // řetězení na kanál 0

dma_channel_configure(
    rgb_chan_1,                         // Kanál ke konfiguraci
    &c1,                                // Konfigurace, kterou jsme právě vytvořili
    &dma_hw->ch[rgb_chan_0].read_addr,  // Adresa zápisu (adresa pro čtení kanálu 0)
    &address_pointer,                   // Adresa pro čtení (UKAZATEL NA ADRESU)
    1,                                  // Počet přenosů, v tomto případě každý má 4 bajty
    false                               // Nezačínat ihned.
);

Připojení hardwaru

Obě synchronizační linky (HSYNC a VSYNC) jsou připojeny přímo z GPIO portů RP2040 k příslušným pinům na VGA konektoru (označené níže). Piny s barvami R/G/B jsou analogové a displej očekává napětí v rozsahu 0-0,7 V. Výstup z RP2040 je 3,3 V. V displeji je rezistor 70 ohmů proti zemi. Umístěním rezistoru 330 ohmů mezi GPIO porty a VGA konektor se tedy vytvoří dělič napětí, který udržuje výstupní napětí v bezpečném rozsahu pro displej.

Schéma zapojení

Pico VGA schema

Pokud uzemníme jenom jeden pin VGA konektoru, bude to fungovat, ale obraz bude nestabilní a ošklivý. Je lepší přizemnit všechny nepoužité piny na VGA konektoru.
Pico piny

pico pinout

Celý program

/**
 * Hunter Adams (vha3@cornell.edu)
 *
 * VGA driver using PIO assembler
 *
 * HARDWARE CONNECTIONS
 *  - GPIO 16 ---> VGA Hsync
 *  - GPIO 17 ---> VGA Vsync
 *  - GPIO 18 ---> 330 ohm resistor ---> VGA Red
 *  - GPIO 19 ---> 330 ohm resistor ---> VGA Green
 *  - GPIO 20 ---> 330 ohm resistor ---> VGA Blue
 *  - RP2040 GND ---> VGA GND
 *
 * RESOURCES USED
 *  - PIO state machines 0, 1, and 2 on PIO instance 0
 *  - DMA channels 0 and 1
 *  - 153.6 kBytes of RAM (for pixel color data)
 *
 * HOW TO USE THIS CODE
 *  This code uses one DMA channel to send pixel data to a PIO state machine
 *  that is driving the VGA display, and a second DMA channel to reconfigure
 *  and restart the first. As such, changing any value in the pixel color
 *  array will be automatically reflected on the VGA display screen.
 *
 *  To help with this, I have included a function called drawPixel which takes,
 *  as arguments, a VGA x-coordinate (int), a VGA y-coordinate (int), and a
 *  pixel color (char). Only 3 bits are used for RGB, so there are only 8 possible
 *  colors. If you keep all of the code above line 200, this interface will work.
 *
 */
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/dma.h"
// Our assembled programs:
// Each gets the name <pio_filename.pio.h>
#include "hsync.pio.h"
#include "vsync.pio.h"
#include "rgb.pio.h"


// VGA timing constants
#define H_ACTIVE   655    // (active + frontporch - 1) - one cycle delay for mov
#define V_ACTIVE   479    // (active - 1)
#define RGB_ACTIVE 319    // (horizontal active)/2 - 1
// #define RGB_ACTIVE 639 // change to this if 1 pixel/byte

// Length of the pixel array, and number of DMA transfers
#define TXCOUNT 153600 // Total pixels/2 (since we have 2 pixels per byte)

// Pixel color array that is DMA's to the PIO machines and
// a pointer to the ADDRESS of this color array.
// Note that this array is automatically initialized to all 0's (black)
unsigned char vga_data_array[TXCOUNT];
char * address_pointer = &vga_data_array[0] ;

// Give the I/O pins that we're using some names that make sense
#define HSYNC     16
#define VSYNC     17
#define RED_PIN   18
#define GREEN_PIN 19
#define BLUE_PIN  20

// We can only produce 8 colors, so let's give them readable names
#define BLACK   0
#define RED     1
#define GREEN   2
#define YELLOW  3
#define BLUE    4
#define MAGENTA 5
#define CYAN    6
#define WHITE   7


// A function for drawing a pixel with a specified color.
// Note that because information is passed to the PIO state machines through
// a DMA channel, we only need to modify the contents of the array and the
// pixels will be automatically updated on the screen.
void drawPixel(int x, int y, char color) {
    // Range checks
    if (x > 639) x = 639 ;
    if (x < 0) x = 0 ;
    if (y < 0) y = 0 ;
    if (y > 479) y = 479 ;

    // Which pixel is it?
    int pixel = ((640 * y) + x) ;

    // Is this pixel stored in the first 3 bits
    // of the vga data array index, or the second
    // 3 bits? Check, then mask.
    if (pixel & 1) {
        vga_data_array[pixel>>1] |= (color << 3) ;
    }
    else {
        vga_data_array[pixel>>1] |= (color) ;
    }
}

int main() {

    // Initialize stdio
    stdio_init_all();

    // Choose which PIO instance to use (there are two instances, each with 4 state machines)
    PIO pio = pio0;

    // Our assembled program needs to be loaded into this PIO's instruction
    // memory. This SDK function will find a location (offset) in the
    // instruction memory where there is enough space for our program. We need
    // to remember these locations!
    //
    // We only have 32 instructions to spend! If the PIO programs contain more than
    // 32 instructions, then an error message will get thrown at these lines of code.
    //
    // The program name comes from the .program part of the pio file
    // and is of the form <program name_program>
    uint hsync_offset = pio_add_program(pio, &hsync_program);
    uint vsync_offset = pio_add_program(pio, &vsync_program);
    uint rgb_offset = pio_add_program(pio, &rgb_program);

    // Manually select a few state machines from pio instance pio0.
    uint hsync_sm = 0;
    uint vsync_sm = 1;
    uint rgb_sm = 2;

    // Call the initialization functions that are defined within each PIO file.
    // Why not create these programs here? By putting the initialization function in
    // the pio file, then all information about how to use/setup that state machine
    // is consolidated in one place. Here in the C, we then just import and use it.
    hsync_program_init(pio, hsync_sm, hsync_offset, HSYNC);
    vsync_program_init(pio, vsync_sm, vsync_offset, VSYNC);
    rgb_program_init(pio, rgb_sm, rgb_offset, RED_PIN);


    ////////////////////////////////////////////////////////////////////////////////////////////
    // ===========================-== DMA Data Channels ========================================
    ////////////////////////////////////////////////////////////////////////////////////////////

    // DMA channels - 0 sends color data, 1 reconfigures and restarts 0
    int rgb_chan_0 = 0;
    int rgb_chan_1 = 1;

    // Channel Zero (sends color data to PIO VGA machine)
    dma_channel_config c0 = dma_channel_get_default_config(rgb_chan_0);  // default configs
    channel_config_set_transfer_data_size(&c0, DMA_SIZE_8);              // 8-bit txfers
    channel_config_set_read_increment(&c0, true);                        // yes read incrementing
    channel_config_set_write_increment(&c0, false);                      // no write incrementing
    channel_config_set_dreq(&c0, DREQ_PIO0_TX2) ;                   // DREQ_PIO0_TX2 pacing (FIFO)
    channel_config_set_chain_to(&c0, rgb_chan_1);                        // chain to other channel

    dma_channel_configure(
        rgb_chan_0,                 // Channel to be configured
        &c0,                        // The configuration we just created
        &pio->txf[rgb_sm],          // write address (RGB PIO TX FIFO)
        &vga_data_array,            // The initial read address (pixel color array)
        TXCOUNT,                    // Number of transfers; in this case each is 1 byte.
        false                       // Don't start immediately.
    );

    // Channel One (reconfigures the first channel)
    dma_channel_config c1 = dma_channel_get_default_config(rgb_chan_1);  // default configs
    channel_config_set_transfer_data_size(&c1, DMA_SIZE_32);             // 32-bit txfers
    channel_config_set_read_increment(&c1, false);                       // no read incrementing
    channel_config_set_write_increment(&c1, false);                      // no write incrementing
    channel_config_set_chain_to(&c1, rgb_chan_0);                        // chain to other channel

    dma_channel_configure(
        rgb_chan_1,                         // Channel to be configured
        &c1,                                // The configuration we just created
        &dma_hw->ch[rgb_chan_0].read_addr,  // Write address (channel 0 read address)
        &address_pointer,                   // Read address (POINTER TO AN ADDRESS)
        1,                                  // Number of transfers, in this case each is 4 byte
        false                               // Don't start immediately.
    );

    ////////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////

    // Initialize PIO state machine counters. This passes the information to the state machines
    // that they retrieve in the first 'pull' instructions, before the .wrap_target directive
    // in the assembly. Each uses these values to initialize some counting registers.
    pio_sm_put_blocking(pio, hsync_sm, H_ACTIVE);
    pio_sm_put_blocking(pio, vsync_sm, V_ACTIVE);
    pio_sm_put_blocking(pio, rgb_sm, RGB_ACTIVE);


    // Start the two pio machine IN SYNC
    // Note that the RGB state machine is running at full speed,
    // so synchronization doesn't matter for that one. But, we'll
    // start them all simultaneously anyway.
    pio_enable_sm_mask_in_sync(pio, ((1u << hsync_sm) | (1u << vsync_sm) | (1u << rgb_sm)));

    // Start DMA channel 0. Once started, the contents of the pixel color array
    // will be continously DMA's to the PIO machines that are driving the screen.
    // To change the contents of the screen, we need only change the contents
    // of that array.
    dma_start_channel_mask((1u << rgb_chan_0)) ;


    ////////////////////////////////////////////////////////////////////////////////////////////
    // ===================================== An Example ========================================
    ////////////////////////////////////////////////////////////////////////////////////////////
    //
    // The remainder of this program is simply an example to show how to use the VGA system.
    // This particular example just produces a diagonal array of colors on the VGA screen.
    while (true) {

        int x = 0 ; // VGA x coordinate
        int y = 0 ; // VGA y coordinate

        // Array of colors, and a variable that we'll use to index into the array
        char colors [8] = {BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE} ;
        int index = 0;

        // A couple of counters
        int xcounter = 0;
        int ycounter = 0;

        for (y=0; y<480; y++) {                     // For each y-coordinate . . .
            if (ycounter==60) {                     //   If the y-counter is 60 . . .
                ycounter = 0 ;                      //     Zero the counter
                index = (index+1)%8 ;               //     Increment the color index
            }                                       //
            ycounter +=1 ;                          //   Increment the y-counter
            for (x=0; x<640; x++) {                 //   For each x-coordinate . . .
                if (xcounter == 80) {               //     If the x-counter is 80 . . .
                    xcounter = 0;                   //        Zero the x-counter
                    index = (index + 1)%8 ;         //        Increment the color index
                }                                   //
                xcounter += 1 ;                     //     Increment the x-counter
                drawPixel(x, y, colors[index]) ;    //     Draw a pixel to the screen
            }
        }
    }
}
Hotový prototyp

IMG 20250729 191025

IMG 20250729 191036

Závady

  • Stavové automaty Hsync a Vsync PIO by měly být spuštěny současně. Protože stavový automat RGB běží plnou rychlostí, synchronizace s ostatními dvěma není nutná.

  • Spouštěč pro čtení adresy DMA vyžaduje ukazatel na adresu začátku barevného pole, nikoli samotnou adresu (odhalení této chyby chvíli trvalo).

  • Při pokusu o čtení vysokorychlostních signálů na osciloskopu si všimněte, že v přívodu klipu je určitá kapacita a v zemnícím krokosvorce určitá indukčnost. Tato kapacita/indukčnost může způsobit určité zvonění. Chcete-li toto zvonění zmírnit, odstraňte přívod klipu a zemnící kabel, znovu nalaďte sondu v této konfiguraci a uzemněte vodič pomocí krátkého drátu. Tím se zvonění výrazně sníží.

  • Registr read_addr pro kanál DMA 0 očekává ukazatel na adresu začátku pixelového pole, nikoli samotnou adresu.

  • Při napájení z USB počítače je obraz velmi mizerný, při napájení z jiného zdroje je stabilní a pěkný. Bylo to způsobeno tím, že jsem měl uzemněn jenom jeden pin na VGA konktoru. Přizemněním dalších nepoužitých pinů VGA konektoru problém zmizel.

Zdroje a odkazy