Si të kryeni detyra paralele (Threads) në programin për Arduino. Arduino ndërpret me attachInterrupt Pse na duhen ndërprerjet e harduerit

Udhëzim

Në përgjithësi, Arduino nuk mbështet paralelizimin e vërtetë të detyrave, ose multithreading.
Por është e mundur të specifikoni në çdo përsëritje të ciklit "loop ()" për të kontrolluar nëse ka ardhur koha për të kryer ndonjë detyrë shtesë, në sfond. Në këtë rast, përdoruesit do t'i duket se disa detyra po kryhen njëkohësisht.
Për shembull, le të pulsojmë në një frekuencë të caktuar dhe në të njëjtën kohë të bëjmë tinguj të ngritjes dhe rënies si një sirenë nga një emetues piezo.
Dhe LED, dhe ne jemi lidhur tashmë me Arduino më shumë se një herë. Le të mbledhim qarkun, siç tregohet në figurë. Nëse po lidhni një LED me një kunj dixhitale të ndryshme nga "13", mos harroni për një rezistencë kufizuese të rrymës 220 ohm.

Le ta shkruajmë këtë skicë dhe ta ngarkojmë në Arduino.
Pas tabelës, mund të shihet se skica nuk po ekzekutohet saktësisht siç na nevojitet: derisa sirena të funksionojë plotësisht, LED nuk do të pulsojë dhe ne do të dëshironim që LED të ishte GJATË tingullit të sirenës. Cili është problemi këtu?
Fakti është se ky problem nuk mund të zgjidhet në mënyrën e zakonshme. Detyrat kryhen nga mikrokontrolluesi në mënyrë strikte në mënyrë sekuenciale. Deklarata "delay()" vonon ekzekutimin e programit për një periudhë të caktuar kohore dhe derisa kjo kohë të skadojë, komandat e mëposhtme të programit nuk do të ekzekutohen. Për shkak të kësaj, ne nuk mund të vendosim një kohëzgjatje të ndryshme për secilën detyrë në ciklin "loop()" të programit.
Prandaj, duhet të simuloni disi multitasking.

Varianti në të cilin Arduino do të kryejë detyra në pseudo-paralele është propozuar nga zhvilluesit e Arduino në artikullin https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay.
Thelbi i metodës është që në çdo përsëritje të lakut "loop ()", ne kontrollojmë nëse është koha për të ndezur LED (të kryejë një detyrë në sfond) apo jo. Dhe nëse është, atëherë ne përmbysim gjendjen e LED. Ky është një lloj anashkalimi i operatorit "delay()".
Një disavantazh i rëndësishëm i kësaj metode është se seksioni i kodit përpara bllokut të kontrollit LED duhet të ekzekutohet më shpejt se intervali kohor i ndezjes LED "ledInterval". Përndryshe, pulsimi do të ndodhë më rrallë sesa duhet dhe nuk do të marrim efektin e ekzekutimit paralel të detyrave. Në veçanti, në skicën tonë, kohëzgjatja e ndryshimit të zërit të sirenës është 200+200+200+200 = 800 ms, dhe intervalin e ndezjes së LED-it e vendosim në 200 ms. Por LED do të pulsojë me një periudhë prej 800 ms, e cila është 4 herë e ndryshme nga ajo që kemi vendosur. Në përgjithësi, nëse operatori "delay()" përdoret në kod, atëherë është e vështirë të simulohet pseudo-paralelizmi, prandaj është e dëshirueshme që të shmanget.
Në këtë rast, do të ishte e nevojshme që blloku i kontrollit të zërit të sirenës të kontrollojë gjithashtu nëse ka ardhur koha apo jo dhe të mos përdorë "vonesën (). Por kjo do të rriste sasinë e kodit dhe do të përkeqësonte lexueshmërinë e programit.

Për të zgjidhur këtë problem, ne do të përdorim bibliotekën e mrekullueshme ArduinoThread, e cila ju lejon të krijoni lehtësisht procese pseudo-paralele. Funksionon në një mënyrë të ngjashme, por ju lejon të mos shkruani kod për të kontrolluar kohën - duhet ta përfundoni detyrën në këtë cikël apo jo. Kjo zvogëlon sasinë e kodit dhe përmirëson lexueshmërinë e skicës. Le të kontrollojmë bibliotekën në veprim.
Para së gjithash, shkarkoni arkivin e bibliotekës nga faqja zyrtare https://github.com/ivanseidel/ArduinoThread/archive/master.zip dhe zbërthejeni atë në drejtorinë "libraries" të mjedisit të zhvillimit Arduino IDE. Pastaj riemërtoni dosjen "ArduinoThread-master" në "ArduinoThread".

Diagrami i instalimeve elektrike do të mbetet i njëjtë. Vetëm kodi i programit do të ndryshojë. Tani do të jetë njësoj si në shiritin anësor.
Në program, ne krijojmë dy fije, secila kryen funksionin e vet: njëri pulson LED, i dyti kontrollon tingullin e sirenës. Në çdo përsëritje të lakut për çdo thread, kontrollojmë nëse ka ardhur koha për ekzekutimin e tij apo jo. Nëse arrin, ai niset për ekzekutim duke përdorur metodën "run()". Gjëja kryesore është të mos përdorni operatorin "delay()".
Shpjegime më të hollësishme janë dhënë në kod.
Ngarkoni kodin në kujtesën Arduino, ekzekutoni atë. Tani gjithçka funksionon saktësisht siç duhet!

Ky artikull do të mbulojë çështjet e lidhjes paralele dhe serike të disa pajisjeve skllav me autobusin SPI, lidhjen serike të regjistrave të ndërrimit, punën me një ekran të dyfishtë me 7 segmente, zbatimin e proceseve të pavarura në Arduino. Si rezultat, ne do të bëjmë një qark në të cilin një gjarpër do të vrapojë përgjatë një segmenti të dyfishtë 7, dhe sekondat do të shënojnë në anën tjetër, të vetme, në këtë kohë.

u njohëm me autobusin SPI dhe mësuam se duhen 4 tela për të lidhur një skllav me një zotëri. Sidoqoftë, nëse ka më shumë se një pajisje skllav, ne tashmë kemi opsione interesante.

Lidhja e pajisjeve paralelisht me autobusin SPI

Kur lidhen paralelisht, disa pajisje skllav përdorin tela të përbashkët SCLK, MOSI Dhe MISO, ndërsa çdo skllav ka linjën e vet SS . Lehtësuesi përcakton pajisje që do të ndërrohet, nga duke formuar një sinjal të ulët në të SS .
Mund të shihet se për t'u lidhur n pajisjet e nevojshme n linjat SS , pra për funksionimin e mjedisit SPI me n për këtë duhet të ndahen skllevër n+3 këmbët e mikrokontrolluesit.

Lidhja serike e pajisjeve me autobusin SPI

Kur pajisjet lidhen në seri, ato përdorin tela të zakonshëm. SCLK Dhe SS , dhe dalja e njërës lidhet me hyrjen e tjetrës. MOSI master lidhet me pajisjen e parë, dhe MISO- deri tek i fundit. Kjo do të thotë, për masterin në autobusin SPI, kjo është, si të thuash, një pajisje.
Një lidhje e tillë ju lejon të ndërtoni, për shembull, një regjistër zhvendosjeje 16-bitësh nga dy regjistra të ndërrimit 8-bit, të cilin do ta bëjmë tani.
Mbetet të theksohet hijeshia e një lidhjeje të tillë: lidhni të paktën 3, të paktën 8 pajisje, do të duhen vetëm 4 këmbë në kontrollues.

Lidhja serike e dy regjistrave me ndërrime
Le të hedhim një vështrim tjetër në regjistrin e ndërrimit 74HC595:

Ne e kujtojmë atë D.S.- ka një kunj të hyrjes serike, dhe Q0-Q7 kunjat e daljes serike. Q7S, që nuk e përdorëm kur kishim vetëm një regjistër në qark, është dalja serike e një regjistri. Ai e gjen përdorimin e tij kur transferojmë më shumë se 1 bajt në regjistra. Nëpërmjet këtij pin, të gjithë bajtet e destinuara për regjistrat e mëvonshëm do të shtyhen në mënyrë sekuenciale, dhe bajti i fundit i transmetuar do të mbetet në regjistrin e parë.


Duke lidhur pinin Q7S të një regjistri të parë me pinin DS të të dytit (dhe kështu me radhë, nëse është e nevojshme), marrim një regjistër të dyfishtë (trefishtë, etj.).

Lidhja e një ekrani të dyfishtë me 7 segmente

Një ekran i dyfishtë me 7 segmente është zakonisht një pajisje me 18 këmbë, 9 për çdo karakter. Le të hedhim një vështrim në diagramin (ekrani im është emërtuar LIN-5622SR dhe ka një probabilitet të lartë që diagrami i tij i instalimeve elektrike të jetë unik):

Ky është një ekran i zakonshëm i anodës, që do të thotë se com1 dhe com2 duhet të furnizohen me një nivel të lartë TTL dhe një nivel të ulët në këmbën përkatëse për të ndezur diodën. Nëse keni një ekran të përbashkët katodë, bëni të kundërtën!

Lidhni ekranin siç tregohet në diagram:

Ekrani i majtë është i lidhur me regjistrin e parë: 1A në këmbën Q0, 1B në këmbën Q1, 1C në këmbën Q2, etj. Ne lidhim kontaktin e përbashkët com1 me tokën. Ne bëjmë të njëjtën gjë me ekranin e duhur: 2A në këmbën Q0, 2B në këmbën Q1, etj., kontakti i përbashkët com2 është në tokë.

Qarku nuk do të duket si fotografia, nëse pika e ekranit është e ndryshme nga e imja, këtu thjesht duhet të keni kujdes kur lidheni. Nëse ekrani është me një katodë të përbashkët, atëherë com1 dhe com2 lidhen me energjinë!

Një gjarpër i thjeshtë në një ekran të dyfishtë me 7 segmente

Pra, ne mësuam se si të ndezim numrat në një ekran me një karakter herën e kaluar, dhe sot do të vizatojmë një gjarpër në një ekran me dy karaktere. Për të filluar, ne do të bëjmë një gjarpër të thjeshtë, i cili përbëhet nga tre segmente dhe shkon në një rreth.

Cikli ynë do të përbëhet nga tetë korniza, secila prej të cilave do të ndezë tre LED të caktuara. Në kornizën e parë, 1E, 1F, 1A do të ndizet (shih diagramin), në të dytën - 1F, 1A, 2A, në të tretën - 1A, 2A, 2B dhe kështu me radhë, në të tetën - 1D, 1E, 1F .

Përsëri, për lehtësi, le të bëjmë një tabelë me bajt, duke kujtuar se, si parazgjedhje, bitet transmetohen duke filluar nga më e larta, d.m.th. 2 orë.

Kornizë

1 abcd efgh

2 abcd efgh

heks

0111 0011

1111 1111

ECFF

0111 1011

0111 1111

ED EF

0111 1111

0011 1111

EF CF

1111 1111

0001 1111

FF 8F

1111 1111

1000 1111

FF 1F

1110 1111

1100 1111

7F 3F

1110 0111

1110 1111

7E 7F

1110 0011

1111 1111

7CFF


Pritësi duhet të vendosë nivelin e ulët (aktiv) në tel SS , transferoni dy bajt dhe lironi telin. Në këtë moment, do të ndodhë një shul, një bajt do të shkruhet në secilin regjistër, dy karaktere do të ndizen.

#përfshi<SPI .h> // lidhni bibliotekën SPI
enum (reg=9); //zgjidh rreshtinSS regjistrohuni në pinin e 9-të të Arduino

i pavlefshëm konfigurimi () {
SPI.begin(); //initalizo SPI
//përktheni pinin e zgjedhur për transmetim në modalitetin e daljes
pinMode (reg, OUTPUT);
}


i pavlefshëm lak () {
//Plotësoni grupin me bajtet që do të dërgojmë
shifra statike uint8_t =
(0xFF,0xCE,0xFF,0xDE,0xFC,0xFE,0xF8,0xFF,
0xF1,0xFF,0xF3,0xF7,0xF7,0xE7,0xFF,0xC7};
//transferoni dy bajt nga grupi dhe mbyllni regjistrat
për (int i=0;i<16;i+=2){
digitalWrite (reg, LOW);
SPI .transfer (shifra[i]);
SPI.transfer (shifror);
digitalWrite (reg, LARTË);
vonesë (80); //pauzë ndërmjet kornizave
}
}


Video e programit:

Proceset paralele në Arduino

Pse zhvilluesit e Arduino i kushtojnë vëmendje të veçantë shembullit Blink pa vonesë?

Zakonisht programi Arduino është linear - së pari bën një gjë, pastaj një tjetër. Në shembullin e mësipërm, ne kemi përdorur funksionin vonesë (80) në mënyrë që çdo kornizë të vizatohet 80 milisekonda pas asaj të mëparshme. Megjithatë, në fund të fundit, këto 80 milisekonda procesori nuk bën asgjë dhe nuk lejon askënd të bëjë asgjë! Për të ekzekutuar dy ose më shumë procese paralele, duhet të ndryshojmë konceptin e ndërtimit të një programi, duke e braktisur vonesë () .

Thelbi i dizajnit tonë të ri do të jetë kohëmatësi. Kohëmatësi do të numërojë kohën dhe ne do të detyrojmë këtë apo atë ngjarje të ndodhë në intervale të caktuara. Për shembull, ekrani i orës do të shënojë çdo sekondë dhe LED do të pulsojë çdo 0,86 sekonda.

Ekziston një gjë në Arduino që numëron kohën që nga fillimi i programit, quhet millis (). Me ndihmën e tij organizohet "paralelizim" i detyrave.

Projekti përfundimtar: një orë dhe një gjarpër dinake


Le të bashkojmë këtë diagram:

Regjistrat e majtë dhe të mesëm funksionojnë për ne nga këndvështrimi i drejtuesit si një pajisje, dhe regjistri i djathtë - si një tjetër. Mund të shihet se këto dy pajisje përdorin të njëjtin tel. SCLK(Spina Arduino 13, tela e treguar në portokalli) dhe MOSI(Gunja e 11-të, e verdhë), SS përdoren të ndryshme (kunjat 8 dhe 9, ngjyrë jeshile). Lidhja e ekraneve me 7 segmente me regjistrat shfaqet për modelet e mia specifike dhe ndoshta nuk do të përputhet me tuajat.


Këtë herë ne do ta bëjmë gjarprin tonë më dinakë: ai do të kalojë nëpër të gjitha segmentet në të njëjtën mënyrë si ujku hipi në një motoçikletë përgjatë kryqëzimit rrugor në serinë "Vetëm ti prit!", e cila fillon me faktin se ai e nxjerr këtë. i njëjti motoçikletë nga garazhi dhe vendos një helmetë.

Sekuenca e bajtit për këtë gjarpër do të jetë:

Gjarpër statik uint8_t =


Tani thelbi: funksionin millis () ulet dhe numëron milisekonda nga fillimi i fillimit. Në fillim të çdo cikli ne kujtojmë vlerën millis () në një ndryshore kohëmatës. Vendosja e variablave snakeTimerPrev Dhe digitTimerPrev, i cili do të ruajë momentin e ngjarjes së mëparshme: për snakeTimerPrevështë përfshirja e kornizës së mëparshme të animacionit të gjarpërinjve, për digitTimerPrev- përfshirja e shifrës së mëparshme. Sapo diferenca aktuale kohore ( kohëmatës) dhe e mëparshme ( snakeTimerPrev ose digitTimerPrev) bëhet e barabartë me periudhën e specifikuar (në rastin tonë, përkatësisht 80 dhe 1000 ms), ne transmetojmë kornizën / bajtin tjetër.

Kështu,

  • çdo 80ms kontrolluesi do të ulë sinjalin në linjë SS ekran i dyfishtë, transmetoni dy bajt dhe lëshoni linjën.
  • çdo sekondë kontrolluesi do të ulë sinjalin në linjë SS ekran i vetëm, transmetoni një bajt dhe lëshoni linjën.
Le ta zbatojmë atë në Arduino. Unë kam përshkruar tashmë gjithçka në detaje më parë, mendoj se nuk ka kuptim të komentoj.

#përfshi<SPI .h>

enum ( snakePin = 9, digitPin = 8 );
kohëmatës i gjatë i panënshkruar=0, snakeTimerPrev=0, digitTimerPrev=0;
int i=0, j=0;



i pavlefshëm konfigurimi () {
SPI.begin();
pinMode (shifrorPin, OUTPUT );
pinMode (SnakePin, OUTPUT);
}


i pavlefshëm lak () {
shifra statike uint8_t =
(0xC0.0xF9.0xA4.0xB0.0x99.0x92.0x82.0xF8,
0x80.0x90.0x88.0x83.0xC6.0xA1.0x86.0x8E);
statike uint8_t gjarpër =
(0xFF,0x9E,0xFF,0xDC,0xFF,0xF8,0xFF,0xF1,
0xFF,0xE3,0xFF,0xA7,0xBF,0xAF,0xBD,0xBF,
0xBC,0xFF,0xDC,0xFF,0xCE,0xFF,0xC7,0xFF,
0xE3,0xFF,0xB3,0xFF,0xBB,0xBF,0xBF,0x9F);


timer=millis();


nëse (timer-snakeTimerPrev>80)(
digitalWrite (SnakePin, LOW);
SPI.transferim(gjarpër[j]);
SPI.transferim(gjarpër);
digitalWrite (SnakePin, LARTË);
j<30 ? j+=2: j=0;
snakeTimerPrev=timer;
}
nëse (timer-digitTimerPrev>1000)(
digitalWrite (shifrorPin, LOW);
SPI.transfer (shifra[i]);


Ndërpret hardueri

Nuk munda të gjeja një foto qesharake për këtë mësim, gjeta vetëm disa leksione mbi programimin, dhe vetë fillimi i këtij leksioni na shpjegon në mënyrë të përsosur se çfarë ndërpres. Një ndërprerje në Arduino mund të përshkruhet saktësisht në të njëjtën mënyrë: mikrokontrolluesi "heq gjithçka", kalon në ekzekutimin e një blloku funksionesh në mbajtësin e ndërprerjeve, i ekzekuton ato dhe më pas kthehet saktësisht në vendin e kodit kryesor ku ndaloi.

Ndërprerjet janë të ndryshme, domethënë jo vetë ndërprerjet, por shkaqet e tyre: një ndërprerje mund të shkaktojë një konvertues analog në dixhital, një numërues kohëmatës ose fjalë për fjalë një kunj mikrokontrollues. Ndërprerje të tilla quhen të jashtme. hardware dhe për këtë po flasim sot.

Ndërprerja e harduerit të jashtëm- Ky është një ndërprerje e shkaktuar nga një ndryshim i tensionit në pinin e mikrokontrolluesit. Çështja kryesore është se mikrokontrolluesi (bërthama kompjuterike) nuk sondazhi pin Dhe mos humbisni kohë për të, një tjetër "copë hekuri" është e angazhuar në fiksim. Sapo voltazhi në pin ndryshon (që do të thotë një sinjal dixhital, +5 aplikohet / +5 hiqet) - mikrokontrolluesi merr një sinjal, lë gjithçka, përpunon ndërprerjen dhe kthehet në punë. Pse është e nevojshme kjo? Më shpesh, ndërprerjet përdoren për të zbuluar ngjarje të shkurtra - pulse, apo edhe për të numëruar numrin e tyre, pa ngarkuar kodin kryesor. Një ndërprerje harduerike mund të kapë një shtypje të shkurtër të një butoni ose një ndezës sensori gjatë llogaritjeve komplekse të gjata ose vonesave në kod, d.m.th. përafërsisht - kunja është e anketuar paralel me kodin kryesor. Gjithashtu, ndërprerjet mund ta zgjojnë mikrokontrolluesin nga mënyrat e kursimit të energjisë, kur pothuajse të gjitha pajisjet periferike janë të fikur. Le të shohim se si të punojmë me ndërprerjet e harduerit në Arduino IDE.

Ndërpret në Arduino

Le të fillojmë me faktin se jo të gjitha kunjat "mund" të ndërpresin. Po, ekziston një gjë e tillë si pinChangeInterrupts, por ne do të flasim për të në mësime të avancuara. Tani duhet të kuptojmë se ndërprerjet e harduerit mund të gjenerojnë vetëm kunja të caktuara:

MK / numri i ndërprerjes INT 0 INT 1 INT 2 INT 3 INT 4 INT 5
ATmega 328/168 (Nano, UNO, Mini) D2 D3
ATmega 32U4 (Leonardo, Micro) D3 D2 D0 D1 D7
ATmega 2560 (Mega) D2 D3 D21 D20 D19 D18

Siç e keni kuptuar nga tabela, ndërprerjet kanë numrin e tyre, i cili është i ndryshëm nga numri i pinit. Ekziston një veçori e dobishme digitalPinToInterrupt(pin), i cili merr një numër pin dhe kthen numrin e ndërprerjes. Duke e ushqyer këtë funksion me numrin 3 në Arduino nano, marrim 1. Gjithçka është sipas tabelës së mësipërme, një funksion për dembelët.

Një ndërprerje lidhet duke përdorur funksionin bashkangjitInterrupt (pin, mbajtës, modalitet):

  • gjilpere- numri i ndërprerjes
  • mbajtës- emri i funksionit të mbajtësit të ndërprerjeve (duhet ta krijoni vetë)
  • modaliteti– ndërprerja e “modalitetit” të funksionimit:
    • I ULËT(i ulët) - i shkaktuar nga një sinjal I ULËT në kunj
    • RRITJE(rritje) - aktivizohet kur sinjali në pin ndryshon nga I ULËTI LARTË
    • RËNË(rënie) - aktivizohet kur sinjali në pin ndryshon nga I LARTËI ULËT
    • NDRYSHIM(ndryshim) - aktivizohet kur sinjali ndryshon (me I ULËTI LARTË dhe anasjelltas)

Ndërprerja gjithashtu mund të çaktivizohet duke përdorur funksionin shkëput ndërprerjen (pin), ku është përsëri kunja numri i ndërprerjes.

Ju gjithashtu mund të çaktivizoni globalisht ndërprerjet me funksionin pandërprerje () dhe zgjidhini ato përsëri me ndërpret (). Kujdes me ta! pandërprerje () do të ndalojë gjithashtu ndërprerjet e kohëmatësit, dhe të gjitha funksionet e kohës dhe gjenerimi i PWM do të "shkëputen" për ju.

Le të shohim një shembull ku shtypjet e butonave numërohen në ndërprerje, dhe në ciklin kryesor ato dalin me një vonesë prej 1 sekonde. Duke punuar me butonin në modalitetin normal, është e pamundur të kombinosh një dalje kaq të përafërt me vonesë:

Numëruesi int i paqëndrueshëm = 0; // variabël numërues void setup() ( Serial.begin(9600); // porta e hapur për komunikim // butoni i lidhur në D2 dhe GND pinMode(2, INPUT_PULLUP); \ // D2 është ndërprerje 0 // mbajtës - butoni i funksionit Shënoni // FALLING - kur të klikohet butoni, sinjali do të jetë 0, dhe ne e kapim bashkëngjitniInterrupt(0, buttonTick, FALLING); ) void buttonTick() ( counter++; // + shtypja ) void loop() ( Serial. println (counter); // vonesë në dalje (1000); // prisni)

Pra, kodi ynë numëron klikimet edhe gjatë vonesës! E madhe. Por çfarë është i paqëndrueshëm? Ne kemi deklaruar një ndryshore globale kundër, i cili do të ruajë numrin e klikimeve në buton. Nëse vlera e ndryshores do të ndryshojë në ndërprerje, duhet të informoni mikrokontrolluesin për këtë duke përdorur specifikuesin i paqëndrueshëm, e cila shkruhet para se të specifikohet lloji i të dhënave të ndryshores, përndryshe puna do të jetë e pasaktë. Ju vetëm duhet të mbani mend këtë: nëse një ndryshore ndryshon në një ndërprerje, bëjeni atë i paqëndrueshëm.

Disa pika më të rëndësishme:

  • Variablat e modifikuar në një ndërprerje duhet të deklarohen si i paqëndrueshëm
  • Ndërprerjet nuk kanë vonesa si vonesë ()
  • Nuk e ndryshon vlerën e tij në një ndërprerje millis () Dhe mikros ()
  • Në ndërprerje, dalja në port nuk funksionon siç duhet ( Serial.print()), gjithashtu mos e përdorni atje - ngarkon kernelin
  • Në ndërprerje, duhet të përpiqeni të bëni sa më pak llogaritje të jetë e mundur dhe në përgjithësi veprime "të gjata" - kjo do të ngadalësojë punën e MC me ndërprerje të shpeshta! Çfarë duhet bërë? Lexo me poshte.

Nëse ndërprerja kap ndonjë ngjarje që nuk ka nevojë të përpunohet menjëherë, atëherë është më mirë të përdorni algoritmin e mëposhtëm të trajtimit të ndërprerjeve:

  • Në mbajtësin e ndërprerjeve, thjesht ngrini flamurin
  • Në ciklin kryesor të programit, kontrollojmë flamurin, nëse është ngritur, e rivendosim atë dhe kryejmë veprimet e nevojshme
volatile boolean intFlag = false; // flamuri void setup() ( Serial.begin(9600); // porta e hapur për komunikim // butoni i lidhur në D2 dhe GND pinMode(2, INPUT_PULLUP); // D2 është ndërprerje 0 // mbajtës - butonTick funksion // FALLING - kur shtypet butoni, sinjali do të jetë 0, dhe e kapim bashkëngjitniInterrupt(0, buttonTick, FALLING); ) void buttonTick() ( intFlag = true; // ngriti flamurin e ndërprerjes ) void loop() (nëse (intFlag) ( intFlag = false; // rivendos // bëj diçka Serial.println ("Ndërprerje!"); ) )

Kjo është në thelb gjithçka që duhet të dini për ndërprerjet, ne do të analizojmë raste më specifike në mësime të avancuara.

Video

Pasi ta instaloni këtë program, do të habiteni se sa i ngjashëm është me Arduino IDE. Mos u habitni, të dy programet janë bërë në të njëjtin motor.

Aplikacioni ka shumë veçori, duke përfshirë një bibliotekë Seriali, kështu që ne mund të lidhim transferimin e të dhënave midis tabelës dhe .

Le të fillojmë Arduino IDE dhe të zgjedhim shembullin më të thjeshtë të daljes së të dhënave Porta serike:

void setup() ( Serial.begin(9600); ) void loop() ( Serial.println("Hello Kitty!"); // prisni 500 milisekonda përpara se të dërgoni përsëri vonesë (500); )

Le të ekzekutojmë shembullin dhe të sigurohemi që kodi të funksionojë.

Marrja e të dhënave

Tani duam të marrim të njëjtin tekst në . Ne fillojmë një projekt të ri dhe shkruajmë kodin.

Hapi i parë është importimi i bibliotekës. Shkojmë skicë | Biblioteka e importit | Seriali. Rreshti do të shfaqet në skicë:

Përpunimi i importit.serial.*; Serial serial; // krijoni një objekt porti serik String marrë; // të dhënat e marra nga konfigurimi i pavlefshëm i portës serike () ( Porta e vargut = Serial.list(); serial = serial i ri (ky, port, 9600); ) void draw() ( if (serial.available() > 0) ( // nëse ka të dhëna, të marra = seriale. readStringUntil("\n"); // lexoni të dhënat ) println(received); // shfaqni të dhënat në tastierë )

Për të siguruar që të dhënat të merren nga porta serike, na duhet një objekt i klasës Seriali. Meqenëse po dërgojmë të dhëna String nga Arduino, duhet ta marrim vargun edhe në Përpunim.

Në metodë konfigurimi () ju duhet të merrni një port serik të disponueshëm. Në mënyrë tipike, ky është porti i parë i disponueshëm në listë. Pas kësaj ne mund të konfigurojmë objektin Seriali, duke treguar portën dhe shpejtësinë e transferimit të të dhënave (është e dëshirueshme që tarifat të përputhen).

Mbetet të lidhni përsëri tabelën, të ekzekutoni skicën nga Përpunimi dhe të vëzhgoni të dhënat hyrëse në tastierën e aplikacionit.

Përpunimi ju lejon të punoni jo vetëm me tastierën, por edhe të krijoni dritare standarde. Le të rishkruajmë kodin.

Përpunimi i importit.serial.*; Serial serial; // krijoni një objekt porti serik String marrë; // të dhënat e marra nga konfigurimi i pavlefshëm i portës serike () ( madhësia (320, 120); Porta e vargut = Serial.list (); serial = serial i ri (ky, port, 9600); ) void draw() ( if (serial .available() > 0) ( // nëse ka të dhëna, // lexoni dhe shkruajini në variablin e marrë marrë = serial. readStringUntil("\n"); ) // Cilësimet për tekstin e madhësisë së tekstit (24); pastroni (); nëse (marrë != null) (tekst (marrë, 10, 30); ) )

Le të ekzekutojmë përsëri shembullin dhe të shohim një dritare me një mbishkrim, e cila është rivizatuar në një vend.

Kështu, ne mësuam se si të marrim të dhëna nga Arduino. Kjo do të na lejojë të vizatojmë grafikë të bukur ose të krijojmë programe për monitorimin e leximeve të sensorëve.

Dërgimi i të dhënave

Ne jo vetëm që mund të marrim të dhëna nga bordi, por edhe të dërgojmë të dhëna në tabelë, duke na detyruar të ekzekutojmë komanda nga kompjuteri.

Le të themi se dërgojmë karakterin "1" nga Përpunimi. Kur bordi zbulon karakterin e dërguar, ndizni LED-në në portën 13 (të integruar).

Skica do të jetë e ngjashme me atë të mëparshme. Për shembull, le të krijojmë një dritare të vogël. Kur klikojmë në zonën e dritares, ne do të dërgojmë "1" dhe do ta dublikojmë në tastierë për verifikim. Nëse nuk ka klikime, dërgohet komanda "0".

Përpunimi i importit.serial.*; Serial serial; // krijoni një objekt porti serik String marrë; // të dhënat e marra nga porti serik void setup() ( madhësia (320, 120); Porta e vargut = Serial.list (); serial = serial i ri (ky, port, 9600); ) void draw() ( if (miu i shtypur == e vërtetë) (//nëse kemi klikuar brenda serialit të dritares.write("1"); //send 1 println("1"); ) tjetër ( //nëse nuk ka pasur asnjë klikim serial.write(" 0"); // dërgo 0))

Tani le të shkruajmë një skicë për Arduino.

Vlera e komandës Char; // të dhënat që vijnë nga porta serike int ledPin = 13; // i integruar LED void setup() ( pinMode(ledPin, OUTPUT); // modaliteti i daljes së të dhënave Serial.begin(9600); ) void loop() ( if (Serial.available()) ( commandValue = Serial.read ( ); ) nëse (commandValue == "1") (DixhitalWrite(ledPin, LARTË); // ndizni LED-in) tjetër (dixhitalWrite(ledPin, LOW); // përndryshe fik) vonesë(10); // vonesë përpara leximit të ardhshëm të të dhënave)

Le të bëjmë të dy skicat. Ne klikojmë brenda dritares dhe vërejmë se LED ndizet. Ju as nuk mund të klikoni, por mbani butonin e miut të shtypur - LED do të ndizet vazhdimisht.

Shkëmbimi i të dhënave

Tani le të përpiqemi të kombinojmë të dy qasjet dhe të shkëmbejmë mesazhe midis bordit dhe aplikacionit në dy drejtime.

Për efikasitet maksimal, le të shtojmë një variabël boolean. Si rezultat, nuk kemi më nevojë të dërgojmë vazhdimisht 1 ose 0 nga Përpunimi dhe porta serike shkarkohet dhe nuk transmeton informacione të panevojshme.

Kur bordi zbulon njësinë e dërguar, ne ndryshojmë vlerën boolean në të kundërtën në lidhje me gjendjen aktuale ( I ULËTI LARTË dhe anasjelltas). NË tjetër ne përdorim vargun "Hello Kity", të cilin do ta dërgojmë vetëm nëse nuk gjejmë "1".

Funksioni krijimin e Kontaktit () dërgon vargun që presim të marrim në Përpunim. Nëse përgjigja vjen, atëherë Përpunimi mund të marrë të dhënat.

Vlera e komandës Char; // të dhënat që vijnë nga porta serike int ledPin = 13; ledState boolean = LOW; //kontrolloni gjendjen e konfigurimit të zbrazëtisë LED() ( pinMode(ledPin, OUTPUT); Serial.begin(9600); establishContact(); // dërgoni një bajt tek kontakti ndërsa marrësi po përgjigjet ) void loop() ( // nëse të dhënat mund të lexohen nëse (Serial.available() > 0) ( // read data commandValue = Serial.read(); if (commandValue == "1") ( ledState = !ledState; digitalWrite(ledPin, ledState );<= 0) { Serial.println("A"); // отсылает заглавную A delay(300); } }

Le të kalojmë te skica e Përpunimit. Ne do të përdorim metodën Ngjarje serike (), i cili do të thirret sa herë që një karakter i caktuar gjendet në buffer.

Shto një ndryshore të re boolean Kontakti i parë, i cili ju lejon të përcaktoni nëse ka një lidhje me Arduino.

Në metodë konfigurimi () shtoni një rresht serial.bufferUntil("\n");. Kjo na lejon të ruajmë të dhënat hyrëse në një buffer derisa të gjejmë një karakter specifik. Në këtë rast, ne kthehemi (\n) pasi jemi duke dërguar Serial.println() nga Arduino. "\n" në fund do të thotë që ne do të aktivizojmë një rresht të ri, domethënë, kjo do të jetë e dhëna e fundit që do të shohim.

Meqenëse ne po dërgojmë vazhdimisht të dhëna, metoda Ngjarje serike () kryen detyra të ciklit barazim (), mund ta lini bosh.

Tani merrni parasysh metodën kryesore Ngjarje serike (). Sa herë që futim një rresht të ri (\n), thirret kjo metodë. Dhe çdo herë që kryhet sekuenca e mëposhtme e veprimeve:

  • Lexohen të dhënat hyrëse;
  • Kontrollohet nëse ato përmbajnë ndonjë vlerë (d.m.th., nëse një grup bosh të dhënash ose "null" na është kaluar);
  • Heqja e hapësirave;
  • Nëse kemi marrë të dhënat e nevojshme për herë të parë, ndryshojmë vlerën e ndryshores boolean Kontakti i parë dhe i thoni Arduino-s se jemi gati të marrim të dhëna të reja;
  • Nëse kjo nuk është marrja e parë e llojit të kërkuar të të dhënave, ne i shfaqim ato në tastierë dhe dërgojmë të dhëna për klikimin që është bërë te mikrokontrolluesi;
  • Ne i themi Arduino se jemi gati të marrim një paketë të re të dhënash.
përpunimi i importit.serial.*; Serial serial; // krijoni një objekt porti serik String marrë; // të dhënat e marra nga porta serike // Kontrollo për të dhëna nga arduino boolean firstContact = false; void setup() ( madhësia (320, 120); Porta e vargut = Serial.list (); serial = serial i ri (ky, port, 9600); serial.bufferUntil ("\n"); ) void draw() ( ) void serialEvent(Serial myPort) ( // formoni një varg nga të dhënat që merren // "\n" - ndarësi - fundi i paketës së të dhënave të marrë = myPort. readStringUntil ("\n"); // sigurohuni se të dhënat tona nuk janë bosh përpara se si të vazhdojmë nëse (marrë != null) ( // hiq hapësirat e marra = shkurto (marrë); println (marrë); //kërko vargun tonë "A" për të filluar shtrëngimin e duarve //nëse gjetur, pastroni buferin dhe dërgoni kërkesën për të dhëna nëse (firstContact == false) ( if (received.equals ("A")) ( serial.clear(); firstContact = true; myPort.write("A"); println ("kontakt"); ) ) tjetër ( //nëse kontakti është vendosur, merrni dhe analizoni të dhënat println(marrë); nëse (Pressed mouse == e vërtetë) (//nëse kemi klikuar në serialin e dritares.write( "1"); //dërgoni 1 println(" 1"); ) // kur të keni të gjitha të dhënat, bëni një kërkesë për një serial të ri pakete. shkruani ("A"); ) ) )

Kur lidhet dhe lansohet, fraza "Hello Kitty" duhet të shfaqet në tastierë. Kur klikoni në dritaren e Përpunimit, LED në pinin 13 do të ndizet dhe fiket.

Përveç Përpunimit, mund të përdorni programet PuTTy ose të shkruani programin tuaj C# duke përdorur klasa të gatshme për të punuar me portet.

04.Komunikimi: Dimmer

Shembulli tregon se si mund të dërgoni të dhëna nga një kompjuter në një tabelë për të kontrolluar ndriçimin e një LED. Të dhënat vijnë në formën e bajteve të vetme nga 0 në 255. Të dhënat mund të vijnë nga çdo program në kompjuter që ka akses në portën serike, duke përfshirë Përpunimin.

Për shembull, keni nevojë për një qark standard me një rezistencë dhe një LED në pinin 9.

Skicë për Arduino.

Const int ledPin = 9; // LED në pinin 9 void setup() ( Serial.begin(9600); // vendos modalitetin në pinMode(ledPin, OUTPUT); ) void loop() (shkëlqimi i bajtit; // kontrolloni nëse ka të dhëna nga kompjuteri nëse (Serial.available()) ( // lexoni bajtet e fundit të marra nga 0 në 255 shkëlqim = Serial. read(); // vendosni ndriçimin e analogut LEDWrite(ledPin, ndriçimi); ) )

Kodi për përpunim

Përpunimi i importit.serial.*; porta serike; void setup() ( size(256, 150); println("Portet serike të disponueshme:"); println(Serial.list()); // Përdor portën e parë në këtë listë (numri 0). Ndrysho këtë për të zgjedhur porta // që korrespondon me bordin tuaj Arduino. Parametri i fundit (p.sh. 9600) është // shpejtësia e komunikimit. Duhet të korrespondojë me vlerën e kaluar në // Serial.begin() në skicën tuaj Arduino. port = serial i ri (kjo, Serial.list(), 9600); // Nëse e dini emrin e portit të përdorur nga bordi Arduino, specifikoni në mënyrë eksplicite //port = new Serial(this, "COM1", 9600); ) void draw( ) ( // vizatoni një gradient nga e zeza në të bardhë për (int i = 0; i

Nisni dhe lëvizni miun mbi dritaren e krijuar në çdo drejtim. Kur lëvizni në të majtë, ndriçimi i LED do të ulet, kur lëvizni në të djathtë, do të rritet.

04. Komunikimi: PhysicalPixel (Ndizni LED me miun)

Le ta ndryshojmë pak problemin. Ne do të lëvizim miun mbi katror dhe do të dërgojmë karakterin "H" (High) për të ndezur LED në tabelë. Kur miu largohet nga zona e sheshit, ne do të dërgojmë karakterin "L" (Low) për të fikur LED.

Kodi për Arduino.

Const int ledPin = 13; // pin 13 për LED int incomingByte; // variabël për marrjen e të dhënave void setup() ( Serial.begin(9600); pinMode(ledPin, OUTPUT); ) void loop() ( // nëse ka të dhëna nëse (Serial.available() > 0) ( // lexoni bajtet në buferin incomingByte = Serial.read(); // nëse ky është një karakter H (ASCII 72), atëherë ndizni LED nëse (incomingByte == "H") ( digitalWrite(ledPin, HIGH); ) // nëse ky është një karakter L (ASCII 76), atëherë fikni LED nëse (incomingByte == "L") (DixhitalWrite(ledPin, LOW); ) ) )

Kodi për përpunim.

Përpunimi i importit.serial.*; floatboxX; floatboxY; intboxSize=20; booleanmouseOverBox = false; porta serike; void setup() ( madhësia (200, 200); boxX = gjerësi / 2.0; boxY = lartësi / 2.0; rectMode(RADIUS); println(Serial.list()); // Hap portin me të cilin është lidhur bordi Arduino (në këtë rast #0) // Sigurohuni që të hapni portin me të njëjtën shpejtësi që Arduino përdor (9600bps) port = serial i ri(ky, Serial.list(), 9600); ) void draw() ( sfond(0 ); // Nëse kursori është mbi katror nëse (mouseX > boxX - boxSize && mouseX boxY - boxSize && mouseY

04. Komunikimi: Grafiku (Vizatoni një grafik)

Nëse në shembullin e mëparshëm dërguam të dhëna nga kompjuteri në tabelë, tani do të kryejmë detyrën e kundërt - do të marrim të dhëna nga potenciometri dhe do t'i shfaqim ato në formën e një grafiku.

Në përgjithësi, Arduino nuk mbështet paralelizimin e vërtetë të detyrave, ose multithreading. Por është e mundur me çdo përsëritje të ciklit lak () udhëzoni mikrokontrolluesin të kontrollojë nëse është koha për të ekzekutuar ndonjë detyrë shtesë në sfond. Në këtë rast, përdoruesit do t'i duket se disa detyra po kryhen njëkohësisht.

Për shembull, le të ndezim një LED në një frekuencë të caktuar dhe njëkohësisht të lëshojmë tinguj në rritje dhe në rënie si një sirenë nga një emetues piezo. Tashmë ne kemi lidhur LED dhe emetuesin piezo me Arduino më shumë se një herë. Le të mbledhim qarkun, siç tregohet në figurë.

Nëse po lidhni një LED me një kunj dixhitale të ndryshme nga "13", mos harroni për një rezistencë kufizuese të rrymës 220 ohm.

2 LED dhe Piezo Buzzer Control duke përdorur operatorin delay().

Le ta shkruajmë këtë skicë dhe ta ngarkojmë në Arduino.

Const int soundPin = 3; /* deklaroni një variabël me numrin e pinit me të cilin është lidhur elementi piezoelektrik */ const int ledPin = 13; // deklaroni një variabël me numrin e pinit LED konfigurimi i zbrazët ()( pinMode (SoundPin, OUTPUT); // deklaroni pin 3 si dalje. pinMode (ledPin, OUTPUT); // shpall pin 13 si dalje. } void loop() (// Kontrolli i zërit: toni (Pin e zërit, 700); // nxjerr një tingull në një frekuencë prej 700 Hz vonesë (200); ton (Pin e zërit, 500); // me vonesë 500 Hz (200); ton (SoundPin, 300); // me vonesë 300 Hz (200); ton (SoundPin, 200); // me vonesë 200 Hz (200); // Kontrolli LED: dixhitalWrite (ledPin, LARTË); // vonesë zjarri (200); digitalWrite (ledPin, LOW); // vonesa e shuarjes (200); }

Pasi ta ndizni, mund të shihet se skica nuk është ekzekutuar saktësisht siç na nevojitet: derisa sirena të funksionojë plotësisht, LED nuk do të pulsojë dhe ne do të dëshironim që LED të pulsojë gjatë zhurma e një sirene. Cili është problemi këtu?

Fakti është se ky problem nuk mund të zgjidhet në mënyrën e zakonshme. Detyrat kryhen nga mikrokontrolluesi në mënyrë strikte në mënyrë sekuenciale. Operatori vonesë () vonon ekzekutimin e programit për një periudhë të caktuar kohore dhe derisa të skadojë kjo kohë, komandat e mëposhtme të programit nuk do të ekzekutohen. Për shkak të kësaj, ne nuk mund të caktojmë një kohëzgjatje të ndryshme ekzekutimi për secilën detyrë në lak. lak () programet. Prandaj, duhet të simuloni disi multitasking.

3 Proceset paralele pa operator "delay()".

Opsioni në të cilin Arduino do të kryejë detyra në pseudo-paralele është propozuar nga zhvilluesit e Arduino. Thelbi i metodës është që me çdo përsëritje të ciklit lak () ne kontrollojmë nëse është koha për të ndezur LED (të kryejmë një detyrë në sfond) apo jo. Dhe nëse është, atëherë ne përmbysim gjendjen e LED. Ky është një lloj operatori anashkalues vonesë ().

Const int soundPin = 3; // variabël me numrin e pinit të elementit piezoelektrik const int ledPin = 13; // variabël me numër pin LED const long ledInterval = 200; // Intervali i ndezjes LED, msec. int ledState = LOW; // gjendja fillestare e LED-së e panënshkruar e gjatë e mëparshmeMillis = 0; // ruani kohën e ndezjes së mëparshme LED konfigurimi i zbrazët ()( pinMode (SoundPin, OUTPUT); // vendos pin 3 si dalje. pinMode (ledPin, OUTPUT); // vendos pinin 13 si dalje. } void loop() (// Kontrolli i zërit: toni (Pin e zërit, 700); vonesë (200); ton (Pin e zërit, 500); vonesë (200); ton (SoundPin, 300); vonesë (200); ton (SoundPin, 200); vonesë (200); // Blink LED: // koha që kur Arduino është ndezur, ms: rrymë e gjatë e panënshkruarMillis = millis(); // Nëse është koha për të pulsuar, nëse (currentMillis - previousMillis >= ledInterval) ( previousMillis = aktualMillis; // atëherë ruajeni kohën aktuale nëse (ledState == LOW) ( // dhe përmbysni gjendjen LED ledState = LARTË; ) tjetër ( ledState = LOW; ) digitalWrite (ledPin, ledState); // ndërroni gjendjen LED ) }

Një disavantazh i rëndësishëm i kësaj metode është se seksioni i kodit përpara bllokut të kontrollit LED duhet të ekzekutohet më shpejt se intervali kohor i ndezjes LED "ledInterval". Përndryshe, pulsimi do të ndodhë më rrallë sesa duhet dhe nuk do të marrim efektin e ekzekutimit paralel të detyrave. Në veçanti, në skicën tonë, kohëzgjatja e ndryshimit të zërit të sirenës është 200+200+200+200 = 800 ms, dhe intervalin e ndezjes së LED-it e vendosim në 200 ms. Por LED do të pulsojë në një periudhë prej 800 ms, që është 4 herë më shumë se sa kemi vendosur.

Në përgjithësi, nëse kodi përdor operatorin vonesë (), në këtë rast është e vështirë të simulohet pseudo-paralelizmi, prandaj është e dëshirueshme që të shmanget.

Në këtë rast, do të ishte e nevojshme që njësia e kontrollit të zërit të sirenës të kontrollojë gjithashtu nëse ka ardhur koha apo jo dhe të mos përdorë vonesë (). Por kjo do të rriste sasinë e kodit dhe do të përkeqësonte lexueshmërinë e programit.

4 Duke përdorur bibliotekën ArduinoThread për të krijuar fije paralele

Për të zgjidhur problemin, ne do të përdorim një bibliotekë të mrekullueshme Fije Arduino, i cili ju lejon të krijoni lehtësisht procese pseudo-paralele. Funksionon në një mënyrë të ngjashme, por ju lejon të mos shkruani kod për të kontrolluar kohën - duhet ta përfundoni detyrën në këtë cikël apo jo. Kjo zvogëlon sasinë e kodit dhe përmirëson lexueshmërinë e skicës. Le të kontrollojmë bibliotekën në veprim.


Para së gjithash, shkarkoni arkivin e bibliotekës nga faqja zyrtare e internetit dhe zbërthejeni atë në drejtori biblioteka/ Mjedisi i zhvillimit të Arduino IDE. Pastaj riemëroni dosjen Arduino Thread-master V Fije Arduino.

Diagrami i instalimeve elektrike do të mbetet i njëjtë. Vetëm kodi i programit do të ndryshojë.

#përfshi // lidhja e bibliotekës ArduinoThread const int soundPin = 3; // variabël me numrin e pinit të elementit piezoelektrik const int ledPin = 13; // variabël me numër pin LED Thread ledThread = Thread(); // krijoni një thread për të kontrolluar tingullin LED ThreadThread = Thread(); // krijoni një rrjedhë kontrolli për sirenën konfigurimi i zbrazët ()( pinMode (SoundPin, OUTPUT); // deklaroni pin 3 si dalje. pinMode (ledPin, OUTPUT); // shpall pin 13 si dalje. ledThread.onRun(ledBlink); // cakto një detyrë në thread ledThread.setInterval(1000); // vendos intervalin e përgjigjes, ms soundThread.onRun(tingulli); // cakto një detyrë në thread soundThread.setInterval(20); // vendosni intervalin e përgjigjes, ms } void loop() (// Kontrolloni nëse është koha për të ndërruar LED: nëse (ledThread.shouldRun()) ledThread.run(); // filloni thread-in // Kontrolloni nëse është koha për të ndryshuar tonin e sirenës: nëse (soundThread.shouldRun()) soundThread.run(); // fillimi i fillit } // Fluksi LED: void ledBlink() ( static bool ledStatus = false; // Gjendja LED Ndez/Fikur ledStatus = !ledStatus; // inverto gjendjen digitalWrite(ledPin, ledStatus); // ndizni/fikni LED-in } // Rrjedha e sirenës: tingull i zbrazët () ( ton statik int = 100; // toni i zërit, toni Hz (Pin e zërit, ton); // ndizni sirenën në "ton" Hz nëse (ton)

Në program, ne krijojmë dy fije - ledFije Dhe fije zanore, secili kryen funksionin e vet: njëri ndez LED, i dyti kontrollon tingullin e sirenës. Në çdo përsëritje të lakut për çdo thread, kontrollojmë nëse ka ardhur koha për ekzekutimin e tij apo jo. Nëse arrin, ai niset për ekzekutim duke përdorur metodën vraponi (). Gjëja kryesore është të mos përdorni operatorin vonesë (). Shpjegime më të hollësishme janë dhënë në kod.


Ngarkoni kodin në kujtesën Arduino, ekzekutoni atë. Tani gjithçka funksionon saktësisht siç duhet!



Copyright © 2023 Pak për kompjuterin.