Table of Contents

Ioan-Vladimir OLTEAN - PIC la PM

Autorul poate fi contactat la adresa: Vladimir Oltean.

Proiectul a fost realizat in cadrul laboratorului EAP InGear, impreuna cu Iulian Calciu (333 AC), cu suportul financiar al firmelor Digilent si Microchip. Durata proiectului: martie-mai 2014.

Descriere de ansamblu

Asa cum spune si numele proiectului, acesta este un POV RGB implementat cu un microcontroller PIC de la Microchip. Din punct de vedere software, ceea ce s-a dorit a fost afisarea pe cele 48 de LED-uri ale display-ului a unei imagini in format bitmap, de dimensiune 48 de pixeli pe verticala, respectiv variabila pe orizontala. In acest sens, am creat doua aplicatii diferite, una ce ruleaza pe PC (x86, Linux), si alta embedded, pe placuta de dezvoltare Digilent chipKIT32 (Microchip PIC32, arhitectura MIPS32). Ambele programe au fost scrise in limbajul C, si compilate cu GCC, respectiv Microchip XC32. Scopul aplicatiei de PC este de a interfata functiile POV-ul, pentru a putea oferi un suport software celor ce ar putea dori sa scrie software pentru el. Din acest motiv, modalitatea in care am preferat sa integram codul a fost cea a bibliotecilor dinamice Linux (shared objects, .so). Scopul acestora este de a preprocesa o imagine .bmp si de a o trimite catre POV. Fiindca modulul de comunicatii wireless inca nu este functional, modalitatea curenta de comunicatie este un fisier cu sintaxa de C, ce contine pixelii imaginii grupati in mai multe matrice, si ce poate fi inclus direct in codul sursa al aplicatiei. Apoi, la reprogramarea microcontrollerului, imaginea va fi vazuta ca variabila globala, stocata pe canale de culoare si intr-un format hardware-specific, si va fi afisata pe LED-uri.

Detalii asupra software-ului embedded

Cum principiul de functionare al unui POV de 360 de grade este afisarea unei coloane diferite din imagine la un interval regulat de timp, si pastrarea constanta a pozitiei unei coloane (adica eliminarea efectului de “jitter”), principalele resurse software folosite au fost intreruperile de timer, pentru a asigura o sincronizare corecta. De asemenea, la fel ca si in implementarea precedenta a POV-ului, am folosit un senzor cu efect Hall pentru a depista trecerea pe deasupra unui punct fix (un magnet), iar impreuna cu un timer pe 32 de biti, am putut masura “elapsed time” pentru o rotatie completa, de 360 de grade. Impartind aceasta valoare la numarul de coloane al imaginii, obtinem intervalul necesar cat trebuie tinuta o coloana aprinsa. Pentru a mentine lucrurile simple, am folosit un registru pe 32 biti si pentru acest al doilea timer, ce genereaza intreruperi, renuntand astfel la cel de 16 biti cu prescaler, din prima implementare. Probleme deosebite a ridicat transmisia seriala a datelor catre driverele de LED-uri. Desi acestea au ramas aceleasi ca si in prima implementare (Texas Instruments TLC5947), acum transmisia a devenit mai complexa prin simpla cantitate a datelor: 48 de LED-uri RGB = 144 de valori de transmis, cu o adancime de culoare de 12 biti fiecare. Deci, in total, 1728 de biti de transmis la fiecare coloana (se poate calcula cat inseamna in cicli de instructiune). Toate aceste date se distribuie, evident, la toate cele 6 drivere disponibile (3 pe stanga, 3 pe dreapta), ceea ce a condus la primul model al gestiunii imaginii in memorie: 6 matrice, din care 3 reprezinta randurile pare, 3 randurile impare (ele vor fi afisate pe paleta stanga, respectiv dreapta, si vor forma imaginea interleaved). In cadrul unui grup de 3 matrice, (semi)imaginea este sparta in 3 fasii astfel: liniile 0-7, 8-15, 16-23. Desigur, nu am mentionat ca acest lucru trebuie efectuat pentru toate cele 3 canale de culoare, conducand la un numar de 18 matrice in memorie.

Detalii asupra software-ului de PC

Cum nu se poate descrie o aplicatie fara sa se mentioneze si cealalta, parte din gestiunea datelor imaginii a fost deja descrisa anterior. Inainte de prezentarea codului, cateva generalitati despre formatul de imagine bitmap:

  1. chiar la inceputul fisierului exista un “bitmap file header”, cu doi octeti “magici” pentru identificarea extensiei. Acestia au valorile caracterelor ASCII 'B' si 'M', de la “bitmap”, insa din cauza unui fail little-endian (x86), ajung sa fie citite si comparate cu “MB”.
  2. in continuarea file header-ului exista un “bitmap info header”, cu informatii auxiliare precum dimensiunea imaginii, adancime de culoare, etc. Trebuie facute cateva verificari inainte de a trece mai departe: are 48 de pixeli inaltime imaginea? (daca nu, Gimp rezolva) are 24 de biti adancime?
  3. la un offset specificat in file header, se afla memoria propriu-zisa de imagine. Ea contine toate cele 3 canale de culoare, interleaved: R[0][0], G[0][0], B[0][0], R[0][1], G[0][1] etc. Octetii de culoare de pe o linie au padding la multiplu de 4, asadar, pentru a evita problemele, este bine ca si IMAGE_WIDTH sa fie multiplu de 4.

Au existat doua versiuni ale acestei aplicatii, prima din care implementa exact transmisia descrisa anterior: aplicatia de PC genera 18 matrice, iar transmisia seriala, in functie de coloana ce trebuia afisata, selecta pixelii corespunzatori din matrice, ii descompunea pe biti in format MSB-first, si apoi ii trimitea serial folosind bit-banging. Snippet-uri pot fi gasite la [1] si la [2]. Problemele cu aceasta implementare sunt urmatoarele:

  1. se transmit 18 pointeri pe stiva, la fiecare apel de functie
  2. se dezactiveaza intreruperile pe parcursul transmisiei (desi, in general, asta e un lucru de dorit, vom reveni cu mai multe subtilitati in sectiunea “Bug-uri si alte probleme intampinate”
  3. se dezactiveaza afisajul pe parcursul transmisiei: BLANK = 1 (…) BLANK = 0;. Acest lucru s-a dovedit a fi nenecesar, fiindca driverele nu transfera datele din registrul de shiftare de la input in registrul de output, decat la frontul crescator al semnalului XLAT (latch).
  4. functia este foarte costisitoare computational: executa multe operatii de shiftare pe biti
  5. de asemenea, chipul PIC32 contine si memorie cache, cu care codul de mai jos nu este extrem de prietenos (nu are nici spatialitate locala, nici temporala)
  6. desi toate cele 6 linii seriale sunt plasate pe acelasi port (PORTE), scrierea se face secvential

Ca atare, in versiunea a doua am decis sa mutam toate procesarile in cadrul aplicatiei de PC, microcontrollerul primind doar un stream de octeti, care nu stie ce reprezinta, dar pe care trebuie sa ii puna pe port in ordinea corecta. Optimizarea a constat in faptul ca, profitand de faptul ca serialele sunt mapate intr-o ordine oarecum buna pe pinii portului E (PORTE0-5), si ca pe acest port nu mai sunt mapate alte semnale (in afara de BLANK, care am convenit ca vrem sa fie zero pe parcursul transmisiei), am putut precomputa pur si simplu valorile ce trebuie scrise pe intregul port, nu 1 si 0 la nivel de bit. Optimizarile de mai sus reprezinta o reducere de portabilitate, nu una de generalitate, in sensul ca sunt hardware-specific, insa nu reduc in niciun fel functionalitatea dispozitivului. Acum, codul de transmisie seriala este mult mai concis, si mai cache-friendly, asa cum se poate vedea in snippet-urile [3] si [4].

Bug-uri si alte probleme intampinate

In primul rand, un bug suparator a existat in snippet-ul [1], la sectiunea: for (resolutionBit = 7; resolutionBit >= 0; resolutionBit --). Aceasta bucla, din motive aparent bizare, cicla la infinit. Din rationamentul simplu ca bitii dintr-un numar sunt valori naturale, initial variabila resolutionBit a fost declarata ca uint8_t. O privire mai atenta asupra situatiei arata ca este nevalida conditia de iesire din bucla: resolutionBit >= 0, fiindca, de ce nu, orice unsigned este mai mare sau egal decat zero, iar 0 - 1 = 255. Transformand variabilele in int8_t am rezolvat problema. Bug-urile urmatoare, mentionate si in sectiunea precedenta, au trasatura ca se manifesta doar la frecvente mari, facandu-le greu de depistat si depanat. Primul din ele, prezent tot in snippet-ul [1], este cauzat de prezenta instructiunilor __builtin_disable_interrupts() si __builtin_enable_interrupts() in jurul functiei. Initial, aceasta era apelata direct din intreruperea de timer, dupa care am trecut la varianta actuala, cu semaforul activat din intrerupere si dezactivat din main, pentru a reduce la minim timpul de procesare in intrerupere (a se vedea snippet-ul [5]). Efectul este acela ca, din cauza dezactivarii temporare a intreruperilor, tratarea acestora era intarziata, lucru neglijabil la frecvente mici, insa care a devenit foarte vizibil la viteze mai mari ale motorului, cand imaginea se latea in mod inexplicabil. Alta problema de sincronizare, vizibila in snippet-ul [5], este ca, din cauza frecventei limitate a procesorului (oscilator de 8 MHz + PLL x8 = 64 MHz), uneori mai sunt pierdute intreruperi. In aceasta situatie, este de dorit afisarea coloanei care ar fi trebuit sa apara in locatia respectiva, nu a coloanei care a fost intarziata. Din acest motiv, incrementarea indicatorului de coloana se face in intrerupere, insa transmisia se face in main(). Un ultim bug se datoreaza documentatiei neclare a procesorului in ceea ce priveste tratarea intreruperilor de Change Notice. Asa cum se poate vedea, in mod normal, primul lucru care trebuie facut in intrerupere este dezactivarea manuala a Interrupt Flag-ului, pentru ca intreruperea sa nu mai fie tratata si a doua oara, la iesirea din rutina. Din nefericire, intreruperea de Change Notice se comporta putin diferit, in sensul ca, inainte de dezactivarea IF-ului, trebuie citit in mod explicit portul care a declansat intreruperea. In cazul in care citirea se face dupa dezactivarea flag-ului, rezultatele sunt “ciudate”, in sensul ca intreruperea se genereaza de doua ori, la frecvente mici, si o singura data, la frecvente mari. Problema poate fi vazuta in snippet-ul [6].

Probleme cunoscute si planuri de imbunatatiri

In primul rand, culorile nu sunt calibrate corespunzator, astfel incat albul are o nuanta puternica de violet (canalul de verde este insuficient de intens). In momentul actual, afisarea unei imagini bazate mai mult pe grafica decat pe text este destul de greu de recunoscut. De asemenea, in varianta urmatoare, vom implementa transmisia imaginii de pe PC pe POV prin intermediul modulului WiFi integrat pe placa, folosind double buffering.

Code snippets

[1]:

embedded_serial_original
void printout(uint8_t lR1[8], uint8_t lG1[8], uint8_t lB1[8],
              uint8_t lR2[8], uint8_t lG2[8], uint8_t lB2[8],
              uint8_t lR3[8], uint8_t lG3[8], uint8_t lB3[8],
              uint8_t rR1[8], uint8_t rG1[8], uint8_t rB1[8],
              uint8_t rR2[8], uint8_t rG2[8], uint8_t rB2[8],
              uint8_t rR3[8], uint8_t rG3[8], uint8_t rB3[8]) {
 
    uint8_t i, led;
    uint8_t resolutionBit;
 
    __builtin_disable_interrupts();
 
    SCLK = 0;
    XLAT = 0;
    BLANK = 1;
 
    for (led = 0; led < 8; led ++) {
        /* Clockez cei mai semnificativi 8 biti din culoare */
        for (resolutionBit = 7; resolutionBit >= 0; resolutionBit --) {
            SOUT1_L = getBit(lR1[led], resolutionBit);
            SOUT1_R = getBit(rR1[led], resolutionBit);
            SOUT2_L = getBit(lR2[led], resolutionBit);
            SOUT2_R = getBit(rR2[led], resolutionBit);
            SOUT3_L = getBit(lR3[led], resolutionBit);
            SOUT3_R = getBit(rR3[led], resolutionBit);
            SCLK = 1;
            SCLK = 0;
        }
        /* Clockez zero-uri pana la restul de 12 biti */
        SOUT1_L = 0;
        SOUT1_R = 0;
        SOUT2_L = 0;
        SOUT2_R = 0;
        SOUT3_L = 0;
        SOUT3_R = 0;
        for (resolutionBit = 0; resolutionBit < 4; resolutionBit ++) {
            SCLK = 1;
            SCLK = 0;
        }
        for (resolutionBit = 7; resolutionBit >= 0; resolutionBit --) {
            SOUT1_L = getBit(lG1[led], resolutionBit);
            SOUT1_R = getBit(rG1[led], resolutionBit);
            SOUT2_L = getBit(lG2[led], resolutionBit);
            SOUT2_R = getBit(rG2[led], resolutionBit);
            SOUT3_L = getBit(lG3[led], resolutionBit);
            SOUT3_R = getBit(rG3[led], resolutionBit);
            SCLK = 1;
            SCLK = 0;
        }
        /* Clockez zero-uri pana la restul de 12 biti */
        SOUT1_L = 0;
        SOUT1_R = 0;
        SOUT2_L = 0;
        SOUT2_R = 0;
        SOUT3_L = 0;
        SOUT3_R = 0;
        for (resolutionBit = 0; resolutionBit < 4; resolutionBit ++) {
            SCLK = 1;
            SCLK = 0;
        }
        for (resolutionBit = 7; resolutionBit >= 0; resolutionBit --) {
            SOUT1_L = getBit(lB1[led], resolutionBit);
            SOUT1_R = getBit(rB1[led], resolutionBit);
            SOUT2_L = getBit(lB2[led], resolutionBit);
            SOUT2_R = getBit(rB2[led], resolutionBit);
            SOUT3_L = getBit(lB3[led], resolutionBit);
            SOUT3_R = getBit(rB3[led], resolutionBit);
            SCLK = 1;
            SCLK = 0;
        }
        /* Clockez zero-uri pana la restul de 12 biti */
        SOUT1_L = 0;
        SOUT1_R = 0;
        SOUT2_L = 0;
        SOUT2_R = 0;
        SOUT3_L = 0;
        SOUT3_R = 0;
        for (resolutionBit = 0; resolutionBit < 4; resolutionBit ++) {
            SCLK = 1;
            SCLK = 0;
        }
    }
    /* Pe frontul crescator al lui XLAT, datele sunt transferate din
     * registrul de shiftare in latch */
    XLAT = 1;
    XLAT = 0;
 
    /* Mai astept un ciclu de instructiune, apoi aprind LED-urile! */
    BLANK = 0;
 
    __builtin_enable_interrupts();
 
    /* Mai astept putin timp sa treaca, pentru a permite
     * LED-urilor sa se aprinda macar putin */
    for (i = 0; i < 100; i ++)
        asm("nop");     //__builtin_nop();
}

[2]:

pc_preprocessing_original
/* Print matrix to file */
void process3() {
 
    int i, j, k;
    int width = BitmapInfo->bmiHeader.biWidth;
 
    printf("#define IMAGE_WIDTH %d\n", width);
 
    /* i = driver */
    for (i = 0; i < 3; i ++) {
 
        printf("uint8_t lR%d[IMAGE_WIDTH][8] = {\n", i + 1);
        /* k = column, j = row */
        for (k = 0; k < width; k ++) {
            for (j = 0; j < 8; j ++) {
                printf("%3d", lR[i][j][k]);
                printf("%s", (k == width - 1 && j == 7) ? "};\n" : ", ");
            }
            printf("\n");
        }
        printf("uint8_t lG%d[IMAGE_WIDTH][8] = {\n", i + 1);
        /* k = column, j = row */
        for (k = 0; k < width; k ++) {
            for (j = 0; j < 8; j ++) {
                printf("%3d", lG[i][j][k]);
                printf("%s", (k == width - 1 && j == 7) ? "};\n" : ", ");
            }
            printf("\n");
        }
        printf("uint8_t lB%d[IMAGE_WIDTH][8] = {\n", i + 1);
        /* k = column, j = row */
        for (k = 0; k < width; k ++) {
            for (j = 0; j < 8; j ++) {
                printf("%3d", lB[i][j][k]);
                printf("%s", (k == width - 1 && j == 7) ? "};\n" : ", ");
            }
            printf("\n");
        }
        printf("uint8_t rR%d[IMAGE_WIDTH][8] = {\n", i + 1);
        /* k = column, j = row */
        for (k = 0; k < width; k ++) {
            for (j = 0; j < 8; j ++) {
                printf("%3d", rR[i][j][k]);
                printf("%s", (k == width - 1 && j == 7) ? "};\n" : ", ");
            }
            printf("\n");
        }
        printf("uint8_t rG%d[IMAGE_WIDTH][8] = {\n", i + 1);
        /* k = column, j = row */
        for (k = 0; k < width; k ++) {
            for (j = 0; j < 8; j ++) {
                printf("%3d", rG[i][j][k]);
                printf("%s", (k == width - 1 && j == 7) ? "};\n" : ", ");
            }
            printf("\n");
        }
        printf("uint8_t rB%d[IMAGE_WIDTH][8] = {\n", i + 1);
        /* k = column, j = row */
        for (k = 0; k < width; k ++) {
            for (j = 0; j < 8; j ++) {
                printf("%3d", rB[i][j][k]);
                printf("%s", (k == width - 1 && j == 7) ? "};\n" : ", ");
            }
            printf("\n");
        }
    }
}

[3]:

pc_preprocessing_optimized
/* Multumiri speciale: Razvan Tataroiu
 * Bufferul este format din WIDTH blocuri de cate 288 de octeti fiecare.
 * Un bloc semnifica o unitate elementara ce trebuie transmisa serial
 * catre driverele de led-uri, la un moment dat de timp. Optimizarea consta
 * in faptul ca scrierile se fac simultan catre toate cele 6 seriale,
 * motiv pentru care octetii in sine din buffer reprezinta 0 sau 1 pe bitii
 * corespunzatori serialei din portul E:
 *      PORTE0=SOUT1_L
 *      ...
 * In cadrul unui bloc de 288 de octeti, avem date pentru 8 LED-uri (are sens,
 * fiindca fiecare driver controleaza 8 LED-uri, iar datele sunt gata mixed
 * in cadrul octetilor, pentru toate driverele.
 * Datele elementare pentru 1 LED (de fapt, 6 LED-uri, ca sunt 6 seriale)
 * ocupa 36 de octeti: 288 / 8, sau 12 octeti per canal de culoare * 3 (R,G,B).
 * Scrierea unui pixel presupune setarea tuturor acestor 36 de octeti cu
 * valorile MSB-first ale culorilor. Ordinea octetilor in bloc este:
 * R[11], G[11], B[11], ... R[0], G[0], B[0].
 * Din col se selecteaza bloc_par sau bloc_impar.
 * Din row se selecteaza driverul (shift) */
void setPixel(uint8_t *buf, unsigned row, unsigned col,
        uint8_t r, uint8_t g, uint8_t b, uint8_t width) {
 
    unsigned bloc_impar = wrapAround((col + width / 2), width) * 288;
    unsigned bloc_par   = col * 288;
    unsigned offset     = 36 * ((row >> 1) & 0x7);
    unsigned impar      = row & 0x1;
    unsigned blk        = (impar) ? bloc_impar : bloc_par;
    unsigned msb        = 1 << 7;
    unsigned and_mask;
    unsigned or_mask;
    unsigned shift;
    unsigned index;
    unsigned i, j;
    unsigned tmp;
 
    /* Shift = driverul destinatie: 0->2 = left, 3->5 = right */
    shift = 2 - ((row >> 4) & 0x3);
    if (impar)
        shift += 3;
    or_mask = 1 << shift;
    and_mask = ~or_mask;
    index = blk + offset;
 
    /* Scriem in buffer bitii, pe rand, pentru r, g si b.
     * For-ul fiind pana la 12, iar valorile fiind pe 8 biti,
     * cei mai nesemnificativi 4 biti vor fi automat 0 */
    for (i = 0; i < 12; i ++) {
        tmp = buf[index] & and_mask;
        buf[index] = (r & msb) ? (tmp | or_mask) : (tmp);
        r <<= 1;
        index ++;
    }
    for (i = 0; i < 12; i ++) {
        tmp = buf[index] & and_mask;
        buf[index] = (g & msb) ? (tmp | or_mask) : (tmp);
        g <<= 1;
        index ++;
    }
    for (i = 0; i < 12; i ++) {
        tmp = buf[index] & and_mask;
        buf[index] = (b & msb) ? (tmp | or_mask) : (tmp);
        b <<= 1;
        index ++;
    }
}
 
void process2() {
 
    int i, j;
    int width, height;
    uint8_t buf[288 * (STD_WIDTH + 1)];
 
    width = BitmapInfo->bmiHeader.biWidth;
    height = BitmapInfo->bmiHeader.biHeight;
 
    /* Imaginile sunt stocate "de jos in sus" intr-un
     * bitmap, motiv pentru care trebuie sa le rasturnam
     * ("height - i - 1" vs "i") aici */
    for (i = 0; i < height; i ++)
        for (j = 0; j < width; j ++)
            setPixel(buf, height - i - 1, j,
                    matR[i][j], matG[i][j], matB[i][j], width);
 
    printf("#define IMAGE_WIDTH %d\n", width);
    printf("uint8_t buf[] = {\n");
 
    for (i = 0; i < 288 * width; i ++) {
 
        printf("%3d", buf[i]);
        if (i % 16 == 15)
            printf(",\n");
        else if (i != 288 * width)
            printf(", ");
    }
    printf("};\n\n");
}

[4]:

embedded_serial_optimized
void printout(uint16_t col) {
 
    uint16_t offset = col * 288;
    uint16_t i;
 
    SCLK = 0;
    XLAT = 0;
 
    for (i = 0; i < 288; i ++) {
        SOUT_PORT = buf[offset++] & 0x3F;
        SCLK = 1;
        SCLK = 0;
    }
 
    /* On the rising edge of XLAT, grayscale data is transferred
     * from the shift register to the latch */
    BLANK = 1;
    XLAT = 1;
    XLAT = 0;
    /* Wait another clock cycle, then turn on the LEDs ! */
    BLANK = 0;
}

[5]:

race_condition
/**
 * This is the timer interrupt which generates the output.
 */
void __ISR(_TIMER_5_VECTOR, ipl1) Timer5Handler(void) {
 
    /* Clear TMR5 interrupt flag. */
    IFS0bits.T5IF = 0;
 
    column = wrapAround(increment(column), IMAGE_WIDTH);
    semaphore = 1;
}
 
int main() {
 
    /* (...) */
 
    while (1) {
        if (semaphore != 0) {
            semaphore = 0;
            printout(column);
        }
    }
    return 0;
}

[6]:

isr_bug
/**
 * Input change notification interrupt, triggered by the
 * Hall sensor. Used to determine what the motor speed
 * is, so we are able to display the image properly. */
void __ISR(_CHANGE_NOTICE_VECTOR, ipl7) ChangeNotice_Handler(void) {
 
    /* Time variables used to count clock ticks */
    uint32_t period = 0;
    uint32_t dummy;
    uint8_t hall;
 
    /* First thing, read the port, so no further
     * interrupts will be generated! */
    hall = HALL;
 
    /* Clear interrupt flag */
    IFS1bits.CNIF = 0;
 
    /* We only want rising-edge interrupts, so ignore
     * all others */
    if (hall != 0)
        return;
 
    /* Calculate how much has passed since last interrupt */
    period = readCounter();
 
    /* Count again for the next interrupt */
    startCounter();
 
    /* We must configure a timer to trigger at every column of pixels */
    dummy = period / IMAGE_WIDTH;
    initTimer(dummy);
 
    /* Reset the column index for the LEDs */
    column = START_POINT; // defined as 0
}

Legenda (define-uri folosite in cod)

/* Porturi in varianta originala */
#define SOUT1_L LATEbits.LATE0
#define SOUT2_L LATEbits.LATE1
#define SOUT3_L LATEbits.LATE2
#define SOUT1_R LATEbits.LATE3
#define SOUT2_R LATEbits.LATE4
#define SOUT3_R LATEbits.LATE5
#define BLANK   LATEbits.LATE9
#define SCLK    LATDbits.LATD1
#define XLAT    LATDbits.LATD2
#define HALL    PORTDbits.RD4
 
/* Porturi in varianta optimizata */
#define SOUT_PORT LATE
#define BLANK   LATEbits.LATE9
#define SCLK    LATDbits.LATD1
#define XLAT    LATDbits.LATD2
#define HALL    PORTDbits.RD4
 
#define getBit(x, n) (((x) & (1 << (n))) >> (n))
 
#ifdef BACKWARDS
    #define wrapAround(x, lim) (((x) >= 0) ? (x) : ((lim) + (x)))
    #define START_POINT (IMAGE_WIDTH - 1)
    #define increment(x) ((x) - 1)
    #define getOpposite(x) ((x) - (IMAGE_WIDTH >> 1))
#else
    #define wrapAround(x, lim) (((x) < (lim)) ? (x) : ((x) - (lim)))
    #define START_POINT 0
    #define increment(x) ((x) + 1)
    #define getOpposite(x) ((x) + (IMAGE_WIDTH >> 1))
#endif

Rezultate obtinute

watch AACR6X0prDTzv4pndasv6Sqsa