Laboratorul 3: Întreruperi externe. PWM

Acest laborator are ca scop familiarizarea voastră cu lucrul cu întreruperile externe și cu modul de generare a semnalelor PWM folosind timer-ele prezente în microcontroller-ul Atmega328p. Vom folosi întreruperi externe pentru a detecta apăsarea unui buton, independent de programul principal. Folosind semnale PWM vom controla un LED RGB și poziția unui servomotor.

1. Întreruperi externe. INT vs. PCINT

În Laboratorul 1 ați studiat mecanismul de întreruperi de pe Atmega328p și ați configurat întreruperi în contextul timer-elor.

Pe lângă întreruperile componentelor interne uC-ului (timer-e, interfețe seriale, convertor analog-digital), există și câteva linii pentru întreruperi de la periferice externe: INT0-INT1 și PCINT0-PCINT2. Diferența dintre aceste două tipuri de întreruperi externe este dată de capabilitățile suportate și de granularitatea lor. Semnalele pentru întreruperile INTn pot genera o întrerupere la tranziție (crescătoare, descrescătoare sau ambele) sau pe nivel 0, în funcție de configurarea întreruperii. Întreruperile PCINTn se declanșează la ambele tranziții (de unde și numele Pin Change INTerrupt) și câte 8 pini sunt multiplexați pe o singură întrerupere. Semnalele de întrerupere PCINT se pot activa individual, însă pentru a afla exact ce semnal a declanșat o anumită întrerupere trebuie verificat registrul PINn corespunzător. Dacă mai multe semnale se declanșează în același timp, ele nu vor putea fi deosebite.

În cazul întreruperilor de tip pin change interrupt, a nu se confunda vectorul de tratare a întreruperilor ex. PCINT2 cu întreruperea efectivă ex. PCINT20 (PD4). Maparea dintre vector și întreruperi se poate găsi în secțiunea 30 pag. 278 din datasheet

Configurare

În general, pașii parcurși pentru a activa o întrerupere externă INT sau PCINT sunt următorii:

Configurăm perifericul care va trimite întreruperile

În exemplu, pentru INT0 și INT1, configurarea se face din registrul EICRA (External Interrupt Control Register A, secțiunea 12.2.1, pagina 54 din datasheet, în timp ce pentru întreruperi de tip pin change se folosește registrul PCICR.

// întreruperi externe
EICRA |= (1 << ISC00);    // set INT0 to trigger on ANY logic change
// întreruperi de tip pin change (activare vector de întreruperi)
PCICR |= (1 << PCIE2); // enable the pin change interrupt, set PCIE2 to enable PCMSK2 scan
// alte întreruperi

O întrerupere externă poate fi configurată astfel încât să detecteze tranziția semnalului logic pe front crescător, descrescător sau oricare (vezi tabelul 13-1 din datasheet). În comparație, o întrerupere de tip pin change este generată la modificarea nivelului logic al semnalului, iar valoarea semnalului trebuie verificată explicit în codul întreruperii.

Scriem rutina de tratare a întreruperii

De exemplu, pentru întreruperile de tip INT0 și respectiv PCINT2, codul va arăta în felul următor:

ISR(INT0_vect)
{
  // cod întrerupere externă PD2 /INT0
  // verificare tranziție pe front crescător, descrescător sau oricare
  // (după cum este configurat INT0)
}
 
ISR(PCINT2_vect){
  // cod întrerupere de tip pin change
  if ((PIND & (1 << PD4)) == 0){
     // întrerupere generată de pinul PD4 / PCINT20
     // verificare nivel logic
  }
}

Dacă avem mai multe întreruperi pe același vector și de ex. mai multe butoane, nu se poate determina cu exactitate care dintre butoane a generat întreruperea de tip PCINT. În plus, este posibil să apară mai multe întreruperi în timp ce starea unui pin verificat să rămână neschimbată, caz în care va fi detectată o nouă apăsare în mod eronat.

Activăm întreruperea și testăm programul

În cazul de față, pentru activarea întreruperilor externe, se configurează registrul External Interrupt Mask Register (EIMSK) astfel:

  • biții INT1:0 controlează dacă întreruperile externe (INT0-INT1) sunt activate

Pentru celelalte întreruperi, se configurează registrul corespunzător (e.g. PCMSK2 pentru activarea întreruperilor de tip pin change pe fiecare pin de pe portul D)

// întrerupere externă
EIMSK |= (1 << INT0);     // Turns on INT0
// întrerupere de tip pin change
PCMSK2 |= (1 << PCINT20); // Turns on PCINT20 (PD2)
// activare întreruperi globale
sei();

2. PWM (Pulse-width Modulation)

PWM (Pulse Width Modulation) este o tehnică folosită pentru a varia în mod controlat tensiunea dată unui dispozitiv electronic. Această metodă schimbă foarte rapid tensiunea oferită dispozitivului respectiv din ON în OFF și invers (treceri rapide din HIGH în LOW, de exemplu 5V - 0V). Raportul dintre perioada de timp corespunzătoare valorii ON și perioada totală dintr-un ciclu ON-OFF se numește factor de umplere (duty cycle) și reprezintă, în medie, tensiunea pe care o va primi dispozitivul electronic. Astfel, se pot controla circuite analogice din domeniul digital. Practic, asta înseamnă că un LED acționat astfel se va putea aprinde / stinge gradual, iar în cazul unui motor acesta se va învârti mai repede sau mai încet.

Principiul de funcționare

Factorul de umplere se exprimă în procente și reprezintă cât la sută din perioada unui semnal acesta va fi pe nivelul ON. În figura de mai jos, se pot observa semnale PWM cu factori de umplere diferiți. Astfel, se poate deduce foarte ușor formula pentru a obține valoarea factorului de umplere (D):

$D[\%] = \frac{t\_on}{t\_on + t\_off} \cdot 100 = \frac{pulse\_width}{period} \cdot 100$

Astfel, tensiunea medie care ajunge la dispozitiv este dată de relația: D * Vcc.

Figura 8. Semnal PWM cu diferiți factori de umplere

Modularea folosește variația factorului de umplere a unui semnal dreptunghiular pentru a genera la ieșire o tensiune analogică. Considerând o formă de undă dreptunghiulară f(t) cu o valoare minimă ymin=0 și o valoare maximă ymax și factorul de umplere D (ca în figură) valoarea medie a formei de undă e dată de relația:

$\bar{y} = D \cdot ymax$

Multe circuite digitale pot genera semnale PWM. Majoritatea microcontroller-elor oferă această facilitate, pe care o implementează folosind un numărător care este incrementat periodic (conectat direct sau indirect la o unitate de ceas) și care este resetat la sfârșitul fiecărei perioade a PWM-ului. Când valoarea numărătorului este mai mare decât valoarea de referință, ieșirea PWM (output-ul) trece din starea HIGH în starea LOW (sau invers).

Semnalul PWM poate fi generat și software prin mai multe metode: bit-banging, timer cu ISR. Practic orice metodă prin care se poate genera software secvența:

  • Set Pin High
  • Wait for T_on
  • Set Pin Low
  • Wait For T_off

Totuși, în multe situații se dorește o frecvență de ordinul kHz sau zeci de kHz a semnalului PWM (ex. controlul motoarelor DC sau BLDC), iar o astfel de metodă nu este cea mai eficientă, fiind necesară intervenția procesorului pentru tratarea întreruperilor frecvente.

Aplicații ale PWM

Pentru a controla un LED folosind un semnal PWM, se poate conecta la fel ca în cazul on/off, folosind o rezistență de limitare a curentului. Dacă avem un LED de putere (ex. LED-uri auto, LED-uri pentru iluminare) este necesar un driver specializat.

În cazul unui motor, căruia i se aplică un semnal PWM cu factor de umplere de 0%, viteza de rotație a acestuia va fi egală cu 0 RPM. Un factor de umplere de 100% va duce la o turație maximă a acestuia. Pentru astfel de cazuri în care avem sarcină inductivă și curenți mari (releuri, motoare, inductoare, electrovalve, etc.) este necesară utilizarea unui driver (element de comutație/tranzistor, driver motor, etc.) și a unei diode flyback (Wikipedia Flyback diode). Semnalul PWM generat de microcontroller va comanda driver-ul (ex. baza tranzistorului printr-o rezistență) iar dioda flyback va prelua spike-urile de tensiune generate de motor/sarcina inductivă la închiderea tranzistorului (care altfel pot duce la distrugerea tranzistorului).

O altă utilizare a semnalelor PWM este de a genera semnal analogic cu aplicații în amplificare audio (de clasă D), încărcătoare de baterii, etc. Prin modificarea periodică a factorului de umplere, se pot obține semnale ce aproximează un semnal analogic (ex. o sinusoidă).

Generarea unui semnal sinusoidal folosind PWM:

Semnalele PWM pot avea și alte utilizări, precum trimiterea de comenzi către un dispozitiv de acționare. Un astfel de exemplu este servomotorul, care primește comenzi determinate de factorul de umplere al semnalului PWM pentru a controla poziția. Servo control

PWM pe Atmega328p

În cadrul laboratorului trecut am văzut că microcontroller-ul Atmega328 are 3 timere: Timer0 pe 8 biți, Timer1 pe 16 biți și Timer2 pe 8 biți.

Fiecare dintre aceste timere putea fi configurat în diferite moduri. În cazul timer-ului 1 configurarea se realiza prin intermediul registrelor TCCR1A și TCCR1B cu ajutorul biților WGM13 - WGM10. Printre aceste moduri se numărau:

  • Normal mode
  • CTC mode (Clear Timer on Compare match) cu TOP la OCRnA
  • CTC mode cu TOP la ICRn

Atmega328p dispune de 6 canale de PWM distribuite astfel:

  • Timer0 - OCR0A, OCR0B respectiv Timer2 - OCR2A, OCR2B - în total 4 canale pe 8 biți
  • Timer1 - OCR1A, OCR1B în total 2 canale pe 16 biți

Controller-ul dispune de 6 canale de PWM în cazul în care acestea sunt configurate în moduri ce au ca TOP valorile 0xFF respectiv 0xFFFF. Alternativ, putem configura fiecare timer (atât cele de 8 biți cât și cele de 16 biți) în moduri ce au ca TOP o valoare specificată într-unul din registrele OCRnA sau OCRnB. Un timer configurat astfel are doar un canal ce genereaza output așa cum ne-am aștepta. Și canalul corespunzător registrului ce conține valoare TOP va genera semnal, însa nu în modul dorit.

Conform tabelului 15-5. din datasheet, Timer-ul 1 mai poate fi configurat să funcționeze și în modul PWM. Din punct de vedere al microcontroller-ului Atmega328p există 3 tipuri de PWM:

  • Fast PWM
  • Phase Correct PWM
  • Phase and Frequency Correct PWM

Fast PWM

Numărarea se face doar pe frontul crescător al semnalului de ceas. În modul Fast PWM, modificarea factorului de umplere se realizează instant, în schimb semnalul nu este centrat (este defazat, practic apare un “glitch” la modificarea semnificativă a factorului de umplere). Se utilizează pentru majoritatea aplicațiilor, mai puțin cele în care este nevoie de un control precis (de exemplu motoare BLDC, audio). Există mai multe moduri de Fast PWM oferite de către microcontroller. De exemplu, pentru Timer-ul 1 avem:

  • Fast PWM pe 8 biți, cu valoarea de TOP = 0x00FF.
  • Fast PWM pe 9 biți, cu valoarea de TOP = 0x01FF.
  • Fast PWM pe 10 biți, cu valoarea de TOP = 0x03FF.
  • Fast PWM cu valoarea de TOP în ICR
  • Fast PWM cu valoarea de TOP în OCRnA

Frecvența semnalului PWM depinde de prescaler și de frecvența oscilatorului. Din secțiunea 15.9.3 (pag 102), formula de calcul a frecvenței în modul Fast PWM 8-bit este:

\begin{equation} f_{OCnX}=\frac{f_{clk}}{N \cdot (TOP + 1)}=\frac{f_{clk}}{N \cdot 256} \end{equation}

În acest laborator vom folosi doar modul Fast PWM. Pentru variantele de Fast PWM oferite de către celelalte timere (0 și 2) și pentru celelalte moduri PWM, consultați datasheet-ul.

Lucrul cu PWM-ul

Lucrul cu PWM-ul presupune inițializarea unui timer și configurarea output-ului pe pini. Fiecare timer are doi pini pe care genera ca output un astfel de semnal (cele două canale): Timer0 are OC0A și 0C0B, Timer1 are OC1A și OC1B etc.

De exemplu, presupunem că avem Timer1 configurat pe modul de funcționare Fast PWM (modul de funcționare nu trebuie neapărat să conțină cuvântul 'PWM' ca să poată fi folosit pentru generarea unui semnal). Fast PWM este caracterizat de o frecvență fixă și un prag stabilit de programator, ce poate fi modificat în timpul execuției.

  • Modul 1 0 pentru biții COM1A1 COM1A0 va lăsa semnalul de pe pinul OC1A pe 1 în timpul numărătorii (până la atingerea pragului) și va pune semnalul pe 0 de la atingerea pragului până la capătul unui ciclu (numărare completă până la TOP).
  • Pentru a obține un factor de umplere x%, aici vom seta OCR1A = x * TOP / 100 (pentru Fast PWM, TOP poate fi 0xFF, 0x1FF sau 0x3FF, în funcție de configurația aleasă)

Exemplu de inițializare a Timer1 in modul Fast PWM 8-bit non-inverting, cu Prescaler la valoarea 1024:

CCR1A = 0;
TCCR1B = 0;
TCNT1  = 0;
//PB1 output - OC1A este PB1
DDRB |= (1 << PB1);
//pentru modul Fast PWM 8-bit, biții WGMn0 si WGMn2 au valoarea 1
TCCR1A |= (1 << WGM10);
//TCCR1A conține doar biții WGM10 si WGM11, WGM12 și WGM13 se găsesc in TCCR1B
TCCR1B |= (1 << WGM12);
//pentru modul non-inverting, COM1A1 = 1 și COM1A0 = 0
TCCR1A |= (1 << COM1A1);
//pentru Prescaler de 1024 scriem 1 pe CS12 si CS10
TCCR1B |= (1 << CS12) | (1 << CS10);
//Pragul la care se schimbă semnalul pentru a obține un factor de umplere de 0.5
//Deoarece în acest mod TOP este 0xFF, OCR1A va fi 50 * 255 / 100 = 127 
OCR1A = 127;
sei();

3. Aplicații cu PWM în Arduino

În continuare, ne vom folosi de biblioteca Arduino pentru a realiza niște aplicații ale semnalelor PWM: controlul unui LED RGB și controlul poziției unui servomotor hobby. Alternativ, pentru cei care doresc să aprofundeze modul de programare la nivel de registre, se pot realiza aplicațiile folosind timer-e în modul Fast PWM și/sau CTC în loc de bibliotecile din Arduino (analogWrite, Servo).

AnalogWrite

Funcția analogWrite din Arduino, configurează de fapt un timer în modul Fast PWM pe 8 biți și poate genera semnal PWM (doar) pe pinii asociați unuia dintre timer-e. analogWrite(pin_arduino, value_0_255) De exemplu, pe Arduino/Atmega328p avem următorii pini care pot genera semnal PWM folosind funcția analogWrite:

Pin Arduino Pin Atmega328p Timer output Frecvența PWM (default)
5 PD5 OC0B (Timer0) 980 Hz
6 PD6 OC0A (Timer0) 980 Hz
9 PB1 OC1A (Timer1) 490 Hz
10 PB2 OC1B (Timer1) 490 Hz
11 PB3 OC2A (Timer2) 490 Hz
3 PD3 OC2B (Timer2) 490 Hz

Este posibil să avem nevoie să modificăm frecvența semnalului PWM, caz în care trebuie să configurăm explicit Timer-ul (ex. prescaler, TOP) folosind registre. SecretsOfArduinoPWM

LED RGB

Un LED RGB este compus din 3 diode care emit culori diferite: una roșie, una verde și una culoare albastră. Cu acest LED se poate obține orice culoare printr-o combinație de intensități pe fiecare diodă în parte. Cele trei LED-uri sunt conectate astfel:

  • OC1A este asociat timer-ului 1 (PB1 sau pinul 9 de pe Arduino) și controlează LED-ul roșu.
  • OC1B este asociat timer-ului 1 (PB2 sau pinul 10 de pe Arduino) și controlează LED-ul verde.
  • OC2A este asociat timer-ului 2 (PB3 sau pinul 11 de pe Arduino) și controlează LED-ul albastru.

În funcție de LED-ul RGB folosit, LED-urile individuale se pot conecta în modul catod comun/“active-high” (LED-ul este aprins atunci cand pinul aferent este HIGH, si stins atunci cand pinul este LOW) sau anod comun/“active-low” (LED-ul este aprins atunci cand pinul aferent este LOW, si stins atunci cand pinul este HIGH). În ambele cazuri, se folosește câte o rezistență de limitare a curentului pentru fiecare culoare.

Servomotoare

Servomotoarele sunt construite pe baza unui motor DC sau BLDC, au angrenaje integrate și un arbore (ax) care poate fi controlat cu precizie. Servo-urile standard permit poziționarea arborelui la diferite unghiuri, de obicei între 0 și 180 de grade. Există și variante cu rotație continuă ce permit modificarea precisă a vitezei de rotație a arborelui.

Servomotoarele hobby (Servo) sunt în mod uzual comandate printr-un semnal PWM cu frecvența de 50 Hz și perioada semnalului T_on în intervalul 1-2 ms, unde 1.5 ms reprezintă poziția de centru. Practic, semnalul PWM este folosit pentru a codifica și transmite poziția dorită către microcontroller-ul integrat care se ocupă de poziționarea efectivă servomotorului.

În Arduino, biblioteca Servo permite controlul servomotoarelor.

#include<Servo.h>

Servo myservo; // creates the servo object

void setup() {
  myservo.attach(9);  // attaches the servo on pin 9 to the servo object
}

void loop() {
  for (pos = 0; pos <= 180; pos += 1) { // goes from 0 degrees to 180 degrees
    // in steps of 1 degree
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(15);                       // waits 15ms for the servo to reach the position
  }
  for (pos = 180; pos >= 0; pos -= 1) { // goes from 180 degrees to 0 degrees
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(15);                       // waits 15ms for the servo to reach the position
  }
}

4. Exerciții

Task 0 (întreruperi / butoane)

Folosiți întreruperi externe (INT și/sau PCINT) pentru a detecta apăsarea unui buton conectat la PD2 (pin 2) și a unuia conectat la PD4 (pin 4). Modificați starea unui LED conectat la PD7 (pin 7) în ISR.

Tinkercad - Testați programul folosind următorul montaj:

ISR(INT0_vect)
{
  // cod întrerupere externă
  PORTD ^= (1 << PD7);  
}
  • Configurați întreruperea externă (INT) pe front descrescător (falling edge) sau PCINT-ul corespunzător pinului folosit
  • Modificați starea LED-ului la apăsarea oricărui buton (PD2, PD4)

Arduino - Încărcați codul pe placă și observați comportamentul. La apăsarea butonului, LED-ul își schimbă starea o singură dată? Adăugați o metodă de debouncing pentru a detecta o singură apăsare pe buton:

  • NU este recomandat să folosiți delay în ISR
  • Se poate folosi funcția millis() pentru a verifica intervalul de timp de la ultima apăsare (100 ms ar trebui să fie suficient pentru un pushbutton/microswitch, dar ajustați după caz)
  • [expert] Se poate folosi un timer pentru a înlocui apelul funcției millis()

Task 1a (LED RGB)

Conectați un LED RGB catod comun la pinii 9, 10, 11 de pe Arduino folosind câte o rezistență de 330ohm și rulați programul de mai jos:

  • Observați modificarea culorii LED-ului
  • Funcția analogWrite setează factorul de umplere PWM pentru fiecare culoare

Tinkercad - Testați programul folosind următorul montaj:

void setup() {
  // Start off with the LED off.
  setColorRGB(0,0,0);
}

void loop() {
  unsigned int rgb[3];

  // Start off with red.
  rgb[0] = 255;
  rgb[1] = 0;
  rgb[2] = 0;

  // Choose the colors to increment and decrement.
  for (int dec = 0; dec < 3; dec += 1) {
    int inc = dec == 2 ? 0 : dec + 1;

    // cross-fade the two colors.
    for (int i = 0; i < 255; i += 1) {
      rgb[dec] -= 1;
      rgb[inc] += 1;
      setColorRGB(rgb[0], rgb[1], rgb[2]);
      delay(5);
    }
  }
}

void setColorRGB(unsigned int red, unsigned int green, unsigned int blue) {
  analogWrite(9, red);
  analogWrite(10, green);
  analogWrite(11, blue);
}

Task 1b (LED RGB / HSV)

Modificați programul folosind functia setLedColorHSV:

  • Funcția setLedColorHSV permite modificarea culorii folosind reprezentarea alternativă HSV (Hue Saturation Value), fiind mai ușor apoi de modificat culoarea, saturația și intensitatea luminoasă. setLedColorHSV
  • Setați valorile pentru saturație (s) și intensitate (v) pe 1 și modificați culoarea (h) în intervalul 0-360

Pentru a urmări corespondența dintre cele 2 reprezentări (RGB și HSV) există selectoare de culori precum: Online Color Picker

Task 2 (LED RGB / Serial)

Scrieți un program care primește comenzi pe serială (USART) și setează o anumită culoare a LED-ului RGB, în funcție de valorile primite pentru fiecare canal, separate prin virgulă (R,G,B). Formatul de tip CSV reprezintă o variantă simplă și flexibilă (deși mai puțin eficientă decât transmiterea în format binar, sub formă de octeți) de codificare a datelor trimise pe serială.

  • Formatul comenzii este următorul: R(0-255),G(0-255),B(0-255). ex. 0,0,255 va comanda o culoare albastră
  • Pentru a identifica sfârșitul comenzii se poate folosi newline (\n) (în Tinkercad nu avem această posibilitate, dar se poate folosi un alt caracter de control, de ex. o literă)
  • La primirea comenzii, se vor schimba valorile factorului de umplere pentru semnalul PWM ce comandă fiecare culoare (R,G,B)
  • Folosiți funcția parseCSV pentru a extrage într-un vector valorile separate prin virgulă din mesajul comenzii
int parseCSV(char* inputString, int *outputArray, int outputArraySize) {
  char *pch;
  int val = 0;
  int index_serial_data = 0;
  pch = strtok(inputString, ",");

  while (pch != NULL && pch != "\n") {
    sscanf (pch, "%d", &val);
    outputArray[index_serial_data] = val;
    index_serial_data++;
    if (index_serial_data == outputArraySize) {
      break;
    }
    pch = strtok(NULL, ",");
  }
  return index_serial_data;
}

Task 3 (Servo / sweep)

Conectați un servo la pinul 9 și rulați exemplul standard din Arduino, prin care se modifică în mod “continuu” poziția (0-180)

  • Urmăriți pe osciloscop semnalul PWM generat
  • Se pot controla până la 12 servomotoare folosind un singur timer
  • Calculați factorul de umplere pentru intervalul de comandă al servomotorului (1ms-2ms). Se poate genera un astfel de semnal cu precizie bună folosind un timer în modul Fast PWM, ca mai devreme?
#include <Servo.h>

Servo myservo;  // create servo object to control a servo
// twelve servo objects can be created on most boards

int pos = 0;    // variable to store the servo position

void setup() {
  myservo.attach(9);  // attaches the servo on pin 9 to the servo object
  // test led
  DDRD |= (1 << PD7);
  PORTD &= ~(1 << PD7);
}

void loop() {
  for (pos = 0; pos <= 180; pos += 1) { // goes from 0 degrees to 180 degrees
    // in steps of 1 degree
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(15);                       // waits 15ms for the servo to reach the position
  }
  for (pos = 180; pos >= 0; pos -= 1) { // goes from 180 degrees to 0 degrees
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(15);                       // waits 15ms for the servo to reach the position
  }
}

Task 4 (Servo / buton)

Folosind întreruperile de la Task 0, modificați incremental poziția servomotorului (0-180)

  • Atenție la limite (min, max). Depășirea acestora poate avaria servomotorul.
  • Dacă lucrați pe placă, folosiți debouncing pentru a obține o funcționare corectă (o apăsare va incrementa o singură dată poziția)
  • Există și servomotoare care folosesc un alt interval de comenzi (ex. 0.7ms-2.3ms), se poate folosi funcția writeMicroseconds pentru a controla direct durata pulsului. Servo.writeMicroseconds

4. Resurse

pinout Arduino UNO

5. Linkuri utile

pm/lab/lab3-2021.txt · Last modified: 2021/04/02 10:55 by alexandru.predescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0