Kvadratúra jel számláló ATTiny-vel megvalósítva

Hiányzott egy mikrovezérlős projekt, de forrasztani nem nagyon szeretek. Ezért bevállaltam, hogy másvalaki hobbiprojektjéhez megírom a programot. A cél az volt, hogy egy esztergához készített műszert új funkciókkal lássunk el. A műszer alapfunkciója az, hogy két lineáris kvadratúra jeladó alapján kijelzi, hogy pontosan hol áll az eszterga kocsija. A munka során jócskán beleástam magam a kvadratúra számláló megvalósításba, erről fog szólni a blogpószt.

Az eredeti program PIC assembly-ben íródott, de "2020-ban ki nyúl már assembly-hez"? Jobb lenne C-ben megcsinálni, mondtam. Az új funkciók miatt új NYÁK is kell, akkor már álljunk át AVR-re, mert én inkább AVR fan vagyok. Tehát írjuk teljesen újra a programot!

A kvadratúra jeladó lényege az, hogy egy periodikus négyszög jelet két szenzorral érzékelünk úgy, hogy kicsit eltolva helyezzük el őket. Így a két négyszögjel egymáshoz képest épp negyed periódusnyit eltolt fázisban jelenik meg. Amennyiben a mozgás megfordul, akkor a jelek sorrendje a két érzékelőn változik, így fel lehet ismerni, hogy merre haladunk, végsősoron a pozíciót pontosan lehet követni akkora felbontással, ahogy a jelek jönnek. Ez az oldal szépen demonstrálja a kvadratúra jeladók működését: http://www.creative-robotics.com/quadrature-intro

Vannak mikrovezérlők, amikben hardveres kvadratúra számláló van, de olyat használni túl egyszerű lenne, és nincs elég sportértéke. Valósítsuk meg a feladatot AVR-en! Ha másra nem is jó, a program remekül alkalmazható állatorvosi lóként általános programozós és AVR-es trükkök bemutatására! (Az új szériás AVR-ekben van további hardveres támogatás is, de én "régi" AVR-en akartam megoldani a problémát. A programozható logikával való megvalósítás application note-ja itt található: http://ww1.microchip.com/downloads/en/AppNotes/00002434A.pdf )

A példakódok

A bemutatott progamot Ubuntu Linuxon fejlesztettem AVRA (AVR macro assembler Version 1.3.0 Build 1 (8 May 2010)) segítségével és avrdude-dal égettem be a csipbe. A programokat a disztribúció részét képező csomagból telepítettem, semmilyen egyéb szoftverforrásra nem volt szükség a munkához. A C példát pedig avr-gcc-vel fordítottam.

Első C nyelvű megvalósítás

Az első megközelítésem az volt, hogy egyetlen mikrovezérlőre teszem a rendszer összes feladatát. És persze megírom simán C-ben. Hiszen "a mai compilerek már jobban tudnak optimalizálni, mint te kézzel". A C-ben megírt program az eredeti megvalósítás C átirata volt: a jel változására élvezérelten reagáló interruptban az előző és az új értékből képezünk egy 4 bites értéket, azaz 16 kombinációt. A 16 kombinációt egy ugrótáblában (C-ben switch/case szerkezet) lekezeljük, és a számlálót növeljük, csökkentjük, hibát jelzünk, vagy maradunk. Az ugrótáblát logikusan könnyen ki lehet tölteni.

A meglévő ASM lista alapján kiszámoltam, hogy az eredeti program nagyjából mennyi idő alatt tud feldolgozni egy jelet. Mivel képzeletben a projektet egy gigantikus PIC vs AVR versennyé nagyítottam, ezért semmiképpen nem akartam kevésbé teljesítő programot írni, mint az eredeti.

Interrupt futásidő optimalizálás

A C nyelvű programot a gcc -E kapcsolójával ASM programra lehet fordítani. Illetve a kész programból a következő parancs segítségével is ki lehet gyűjteni az ASM listát:

````
avr-objdump -S --disassemble $(BINARYNAME).elf >$(BINARYNAME).disassemble.txt
````

Az ASM listát a parancsokat magyarázó dokumentáció alapján lehet visszafejteni, ami megtalálható a Microchip weboldalán: https://www.microchip.com/webdoc/avrassembler/avrassembler.wb_instructi…

Amire koncentrálnunk kell, az az, hogy melyik utasítás hány órajel alatt fut le. Ez az érték megtalálható az utasítás leírásában. (Figyelni kell, hogy az elágazó utasításik futásideje a két ágon nem egyforma, ugráskor eggyel több órajel a futásuk ideje.)

A C forráskódból generált ASM listát elemezve azt találtam, hogy körülbelül 50 órajel szükséges egy változás feldolgozásához. A fordító egy rakás regisztert használ - pedig kevesebb is elegendő volna, új regisztert allokál akkor is, amikor a másik változót már nem használjuk többet -, és ezeket mindet elmenti és visszaállítja a stackről az interrupton belül. Borzalmasan pazarlónak látszott a kód!

Ráadásul két kvadratúra jelet is egyszerre fel akartam tudni dolgozni egyazon processzoron. Ráadásul esetleg a többi funkció is kívánhat interruptot, illetve interrupt kizárt blokkot. A többi funkció interrupt feldolgozási idei összeadódnak, és a már eleve lassú megvalósítás összességében borzasztó lassú lesz. A számolásaim során végsősoron arra jutottam, hogy a számlálókat külön processzorokra kell tenni, és a C megvalósítás helyett optimalizált ASM-et kell írni. Különben legyőz a PIC, és azt semmiképpen sem hagyhatom annyiban!!4!

Újratervezés tervezési feltételek

 * ATMega processzorról könnyen kiolvasható legyen a pillanatnyi számláló érték.
 ** Legyen 32 bites a számláló, így a főprocesszornak nem kell iparkodni a kiolvasással, hogy a körbefordulást mindenképpen elkerülje.
 ** A kommunikáció legyen viszonylag gyors
 ** Ne kelljen az interruptokat letiltani a kiolvasáshoz (ezt végül mégsem tartottam be, mert bár megvalósítható lenne, de nagyon le kellett volna lassítani hozzá a kommunikációt)
 * Ne kelljen a számlálóhoz kvarcot használni: két lábat elhasznál, és bonyolítja a NYÁK-ot is. Kvarc nélkül a 8 bites serial általában működik ugyan, de sajnos adatlap szerint az órajel szórása miatt nem feltétlenül megbízható két AVR között.
 * Ha egymásra futnak a kvadratúra jelek, azaz gyorsabb a jel, mint amit fel tudunk dolgozni, akkor adjunk hibajelzést!
 * Kezeljünk egy nullázó jelet is (ez az igény csak később került elő, ezért az első változatokban még nem számoltam vele).

Választott csip

Az ATTiny25 csipre esett a választásom, mert ennek van egy beépített PLL órajel szorzója, aminek segítségével 16MHz belső órajele van. (A PLL nélküli 8 bites AVR-ek belső órajele 8-10MHz körül van csak.)

A csipnek 8 lába van, ezekből 5 tetszőlegesen használható (a RESET lábat nem használjuk GPIO-ként, így 12V-os vezérjel nélkül egyszerű programozóval programozható marad a csip).

Az első megvalósításban SPI buszt használtam és mind az 5 lábnak lett funkciója:

 * NCS - negált chip select jel
 * SPI SCK
 * SPI MOSI
 * Signal A
 * Signal B

Az SPI-ben azt szeretem, hogy a kommunikáció tempóját teljes mértékben a master adja, így ott nem kell kizárni az interruptokat kommunikáció közben, és akár szoftveresen is egyszerűen megvalósítható egy tökéletesen működő változat. (Nullázó jelnek viszont nem maradt láb - ezért sem lehetett ilyen a végső megvalósítás).

Látható az is, hogy SPI inputot nem használunk - nincs szükségünk rá. A csip folyamatosan mér, és kérésre mindig az aktuális értékeket adja ki. Ezért nincs szükség semmiféle vezérlésére.

Mérés

A jelfeldolgozás méréséhez szükségünk van egy jelforrásra. Mivel a célunk az, hogy a lehető leggyorsabb jelfeldolgozót valósítsuk meg, ezért olyan jelforrás kell, aminek a sebessége pontosan beállítható.

A legésszerűbb megoldás egy jelgenerátor programot írni erre a célra. Természetesen mire írnánk ezt a programot, ha nem AVR-re, azon belül is egy Arduino UNO-ra? A tesztprogram USB-uart segítségével csatlakozik egy vezérlő PC-re, ahol a számláló értékeit kiírja, illetve számláló parancsokat adhatunk ki. A programot úgy írtam meg, hogy sokmilliós számlálásokat indíthatok a legnagyobb sebességen, és a végén ellenőrizhetem, hogy tévesztett-e a számláló.

A kvadratúra jelalakot kétféle módszerrel lehet előállítani:

 * PWM generátorral. Be lehet úgy állítani két PWM generátort, hogy kvadratúra eltolásban adjon ki két négyszögjelet. Ez működik, de sajnos ezt a változatot nem sikerült úgy megcsinálnom, hogy az elején és a végén is kiszámítható legyen a jelforma. Márpedig az off-by-1 jellegű hibákat is fel kell tudni deríteni, nem elegendő, ha csak nagyjából jó a program! Tárolós szkóp nélkül nagyon nehéz nyomon követni, hogy mi történik a jelforma elején és végén is. Ráadásul a következő megoldás sokkal egyszerűbb. Ezért ezt az utat inkább feladtam. Ennek a megvalósításnak előnye lett volna, hogy "háttérben", interrupt által vezérelve működhetett volna.
 * Bit-banging módszerrel. Az utasítások futásidejét számlálva pontos jelalakot lehet megvalósítani "kézzel" vezérelve is. Például:

````
    ldi r1, 100    ; 100 ismétlés
loop:
    sbis PORTA, 0    ; A0 magasba
    nop
    nop
    nop
    sbis PORTA, 1    ; A1 magasba
    nop
    nop
    nop
    sbic PORTA, 0    ; A0 alacsonyba
    nop
    nop
    nop
    sbic PORTA, 1    ; A1 alacsonyba
    dec r1        ; számlálót csökkentjük
    brne loop    ; Amíg nem 0, addig ismétlünk
````

A nop utasítások szerepe, hogy minden fázis hossza egyforma legyen. A számlálás és visszaugrás ugyanis 3 órajel alatt fut le.

Ezzel a módszerrel pontos jeleket tudunk kiadni. Ha esetleg interruptok beesnek, akkor azok csak lassítják kicsit a jeleket, gyorsítani nem tudják. Ha teljesen pontos jelalak kell, akkor az interruptokat le kell tiltani.

Második megvalósítás: ASM pin change interrupttal

Mind az ASM listázásból, mind a mérésből látszott, hogy a jelfeldolgozás idején még lehet javítani, azonban ehhez már assemblert kell használni. Milyen trükköket lehet megvalósítani ASM-ben, amit C-ben nem? A trükkök egy része akkor is működik, ha csak 1-1 funkciót írunk meg ASM-ben, de van ami csak akkor, ha a teljes program ASM-ként van megvalósítva:

Csak az egyik bemenetre teszünk interruptot

Lehetséges csak az egyik bemenetre tenni interruptot, a másikra nem. Az átmenetkor meg kell vizsgálni, hogy a másik jel értéke éppen mi, és az átmenet irányából, illetve a másik jel értékéből tudhajtuk, hogy merre kell számolni. Ilyenkor fele annyi interruptunk lesz, mintha mindkettőre tettünk volna, tehát gyorsabb jelet is ki tudunk szolgálni. A számlálás lehetséges így is, de a pontosság felét elveszítjük.

Azonban ha a számláló kiolvasásakor kiolvassuk a pillanatnyi mintát újra, akkor abból az eredeti pontosság visszaállítható: az eredeti mintához képest 1 lépést jobbra vagy balra elléphettünk, ebből adódik plusz egy bitnyi pontosság és +1, 0, -1 vagy -2 vagy +2 érték lehetséges attól függően, hogy melyik állapotot olvastuk ki. Amennyiben több lépést tett meg a jel az utolsó interrupt óta, akkor már az interrupt is lefutott volna, így biztosak lehetünk benne, hogy a megfelelő értékre korrigáltunk. Ez a megvalósítás eléggé bonyolult logikát igényel a kiolvasó kódba, ezért végül nem valósítottam meg csak a felezett pontosságú változatot, és azzal mértem csak. A legnagyobb feldolgozható jelsebességet elvileg nem változtatná ez a megvalósítás, csak a kiolvasás idejét növeli minimálisan.

Csak 16 biten számlálunk az interruptban

A felső 16 bitet a főprogramban periodikusan hozzá számoljuk. Ha a főprogramban ez az igazítás lefut mielőtt az alsó 16 bit előjelet váltana, akkor ebből nem lesz hiba. 2 órajelet tudunk az interruptból spórolni ezzel, viszont a főprogram jóval komplexebbé válik.

Csak a felfelé menő élekre teszünk interruptot

Ha a hardver megengedi ez is egy lehetőség. Ha csak a felfelé menő élekkor mintavételezünk egyetlen bemeneten, akkor is követni lehet a számlálót: ha felfelé menő élkor a másik érték magas, akkor előre számlálunk, ha alacsony, akkor hátra.

Ez megoldás elveszít 2 bitet a pontosságból, ráadásul igencsak zavarérzékeny is: ha prellezik az input, akkor többet is számlálhat egy helyett.

=== Változók statikus allokációja regiszterekbe.

Másképpen mondva egy regiszterben mindig ugyanazt a változót tároljuk. Ezt csak tistán ASM-ben írt programmal lehet megcsinálni. (Pontosabban szólva lehetséges C-ben is, de körülményes - ilyet még sosem csináltam.) Ezzel a trükkel azok a változók, amiknek így foglalunk helyet gyorsabban elérhetőek. A memória olvasása általában 2 órajel, az írása szintén. Ha regiszterben van az érték, akkor csak elvégezzük rajta a műveletet amit akarunk, és kész is vagyunk. Általában 4 órajelet tudunk spórolni felhasznált bájtonként. Látni fogjuk, hogy a számláló program összes változója teljes egészében elfér az ATTiny25 32 regiszterében.

=== Állapotmentés ISR kiszolgálóban.

Mivel a regiszterek változókhoz vannak rendelve, ezért azokat nem kell stackre menteni és visszatölteni. Egyedül az SREG státusz regisztert kell menteni, ezt nem tudjuk megspórolni sajnos. Viszont ezt sem kell stackre menteni, elegendő egy erre dedikált regiszterbe tenni az értékét és onnan visszatölteni. (Esetleg úgy lehetne ezt is megspórolni, ha a főprogramban nem használnánk SREG-et, illetve ha használjuk, akkor cli/sei blokkba tesszük azt a programrészt. De ilyen piszkos trükköt még sosem csináltam.)

=== ISR kód közvetlenül az ISR táblába kerül.

Alap megvalósításban az ISR táblában rjmp ugrások vannak az interrupt kezelő függvényre. Interrupt végrehajtásakor a CPU a program memória első utasításaira ugrik (az N-edik interrupt típushoz az N-edik program szóra), és minden interruptnak pontosan egy utasítás jut, ezért ide csakis ugró utasítás fér el. Tisztán ASM-ben programozva lehetőség van arra, hogy ugró utasítás helyett az ISR kezelőt közvetlenül oda tegyük, ahová az interrupt ugrik, azon az áron, hogy az interrupt tábla többi bejegyzéseire rálóg a kódunk. Ez persze csak akkor lehetséges, ha ezeket a nagyobb indexű interruptokat nem használjuk. És összesen két órajelet spórolunk vele. Ki a kicsit nem becsüli...

=== Elágazás

Két egymás utáni bemenet értéke alapján ágazunk el, hogy felfelé, vagy lefelé kell-e számolni. Erre több lehetőség van:

 * Ugró utasításokkal (egymásba ágyazott if-ek) bitenként elágazva. Ez könnyen érthető azonban összesen sok órajelet elvisz.
 * ijmp/icall utasítást használva. Ha egy switch utasítást "tömören" töltünk ki, azaz 0-tól kezdve minden számhoz rendelünk egy értéket, akkor a fordító ijmp ugrásos elágazást generál. Ennek a lényege, hogy a változó értékét ugrás címként használjuk fel arra, hogy a megfelelő kódra ugorjunk. Ilyet ASM-ben is könnyen csinálhatunk. Ennek további trükkjei lehetségesek:
 ** .org utasítás segítségével fix helyre tesszük a táblázatot a program memóriában. Így a cím előkészítése egyszerűbb lesz. Például a 0x01XX címeket használhatjuk. Így a ZH-ba 1-et töltünk (1 órajel, vagy ha ügyesek vagyunk fixen ott is lehet ez az érték a program futása alatt), a ZL-be pedig az elágazást vezérlő értéket. A program memória második 256 szavát használjuk ugrótáblának.
 ** (a C fordító abszolút címek helyett csak relatív ijmp-t tud megvalósítani, azaz az értéket hozzáadja a pillanatnyi címhez. Az értékbetöltés és összeadás műveletek miatt ez plusz néhány órajelet jelent. További probléma, hogy hiány maszkoltam le a felső biteket, a fordító nem tudja/hiszi el, hogy azok nullák, és egy plusz maszkolást is betesz a biztonság kedvéért. Ez is további értékes CPU ciklusokat pazarol.)
 * lpm utasítást használva. A vezérlést nem ágaztatjuk el, hanem switch helyett a minta alapján egy program memóriabéli táblázatból olvassuk ki, hogy mennyit kell növelni, vagy csökkenteni az értéken, és azt adatként használjuk fel. (A program memória mellett RAM-ból is lehetne ilyet csinálni, de a választott processzorunk RAM-ja túl kicsi ekkora táblázathoz.)

=== Bemenetek trükkös PIN-hez rendelése

Úgy, hogy két egymás utáni bemenet könnyen kombinálható legyen. Az előző és a jelenlegi bemenet értékéből együtt kell egy 4 bites mintát készíteni. Ha a kiolvasott minta egymás melletti biteken van, akkor két shiftelésre van szükségünk, hogy messzebb kerüljenek egymástól, és or művelettel kombinálhatóak legyenek. Ha azonban a kiolvasott minta hasznos bitjei között van egy üres hely, akkor a minta beolvasható és összehozható az előzővel egybe 6 művelettel 6 órajel alatt:

````
in ZL, PINB
and ZL, 0b101    ; Értékes jegyek a 0b101 maszk alatt vannak, mert így osztottuk ki a lábakat
mov TMP, PREV_MINTA
mov PREV_MINTA, ZL    ; mostani minta tárolása a következő ciklus számára
lsl TMP        ; Előző minta értékes jegyei a 0b1010 maszk alatt
or ZL, TMP   ; Értékes jegyek a 0b1111 maszk alatt, azaz a 0-15 értékek lehetségesek
    ; Mivel ZL-ben van, ezért ugrótáblához, vagy lpm-hez is lehet használni
````

=== Állapotgép

Az állapotgép megvalósításban a legkellemetlenebb az állapotra ugrás megvalósítása. C-ben tipikusan switch-csel valósítjuk meg, ezért adódik, hogy ijmp, vagy icall megvalósítást csináljunk neki.

Az állapotokat tömören indexelve az állapotra ugrás után még egy rjmp szükséges, mivel a tömörség miatt oda helybe nem fér kód.

````
    ldi CURRENT_ALLAPOT, 0
    ...
    mov ZL, CURRENT_ALLAPOT
    ldi ZH, 2
    icall ;    allapotgep hivas
    ... folytatas

.org 0x200    ; ZH=2, azaz a harmadik 256 szóba tesszük az állapotgépünket
    rjmp handle_allapot0
    rjmp handle_allapot1
    ...

handle_allapot0:
    ...
    ret
handle_allapot1:
    ...
    ret
````

Tömör indexek helyett használhatjuk az állapot handler ZL címét közvetlenül állapotváltozónak így egy rjmp-t spórolunk. Illetve ha egy helyről hívjuk az állapotgépet, akkor icall helyett ijmp-t és ret helyett rjmp-t használhatunk a visszatérésre (mivel a visszatérési címet nem kell tárolni, ezzel is spórolunk pár CPU ciklust):

````
    ldi CURRENT_ALLAPOT, lo(handle_allapot0)
    ...
    mov ZL, CURRENT_ALLAPOT
    ldi ZH, 2
    ijmp ;    allapotgep hivas
ret_allapotgep
    ... folytatas

.org 0x200    ; ZH=2, azaz a harmadik 256 szóba tesszük az állapotgépünket
handle_allapot0:
    ...
    rjmp ret_allapotgep
handle_allapot1:
    ...
    rjmp ret_allapotgep
````

=== return helyett rjmp

Minden esetben, amikor egy "függvényt" csak egyetlen helyről hívunk meg, ott lehetséges ret helyett rjmp {cím} utasítással közvetlenül visszatérni. Természetesen rcall helyett is rjmp-t kell használni ekkor. Ezzel 3 órajelet spórolunk. Az ijmp-s állapotgép megvalósítással lehet kombinálni.

=== Elágazás után kódduplikálás

Ha elágaztunk, akkor a két ágat nem kell közösíteni a soron következő rjmp-ig. Ha ugyanazt kell csinálni, akkor azt leírjuk mindkét ágba. Így az egyik ágon (péládul a kitérő ágon) megspórolunk egy visszaugrást az eredeti ágba. 2 órajel.

=== Interruptos megvalósítás hátrányai

Az interruptos megvalósítás zavarérzékeny. Ha az interrupt aktiválása és a kiolvasás között prellezik a jel, akkor abból számlálási probléma lehet. A jelet tehát prellezésre szűrni kell még analóg módon.

Hátrány továbbá, hogy az interrupt lefutásnak komoly overheadje van, körülbelül 10 órajel a hivása és a visszatérés belőle a hasznos kódon túl.

Az is probléma, hogy ha a jel pont olyan gyors, vagy gyorsabb, mint amit éppen fel tudunk dolgozni, akkor a program többi részét "kiéheztetjük", azaz semmi más nem tud futni, mint a megszakításkezelő. Például a kommunikációs vonal beragadhat. Ha buszként használjuk ezt a vonalat, akkor ezzel minden más kommunikációt is elrontunk a rendszerünkben.

A kiéheztetés problémája megoldható lenne úgy, hogy minden jelfeldolgozás után egy időre tiltjuk a további jelek vételét - az interruptot. Ez azonban plusz overhead lenne - a lényegi programmal összemérhető költséggel -, ami tovább lassítani a megoldásunkat.

Nagyon nem tetszett az a gondolat, hogy túl gyors kvadratúra jel esetén a rendszer kommunikációja esetleg összeomlik. Pláne, ha buszon kommunikálunk, akkor minden le is fagyhat tőle, vagy ki nem javuló hibaállapotba kerülthet. Egy rendszer amit én írok, az működjön úgy, hogy meg vannak mondva a működőképes határok, és azokon kívül definit specifikált hibaállapotokba jussunk. Punktum!

== Periodikus mintavételezés ASM-ben

Ezen a ponton már annyira rápörögtem a kvadratúra számlálásra, hogy elhatároztam, hogy kihozom a legtöbbet a csipből, amire csak képes vagyok! Illetve nem is határoztam el, egy magasabbrendő erő rámkényszerítette. Egyszerűen csak meg kellett csinálnom és kész.

A következő változatban az interrupt kezelés hátrányaitól akartam megszabadulni. A teljes program legyen egyetlen periodikusan futó loop interruptok nélkül! Így a futás ideje teljesen kiszámítható lesz, azaz pontosan meg lehet mondani, hogy mi lesz a leggyorsabb feldolgozható jel. Még mérni sem kell, kiolvasató lesz a program listából! A program kiéheztetése sem lesz lehetséges. Ha túl sok jel jön, akkor hibajelzést okoz, amint két egymás utáni jel illegális lesz a kvadratúra specifikáció szerint.

Lássuk meddig lehet lemenni a mintavételi periódussal! A jel beolvasás és két jel kombinálása 6 órajelet vesz igénybe, a számlálóhoz adandó érték kiolvasása program memóriából 2, a négy bájtos számláló növelése legalább 4 órajel. Tehát 12 órajelnél járunk, és még kommunikálni is kellene valahogy. A kommunikációval együtt lesz ez 20 órajel is legalább, és akkor már nem is gyorsabb, mint az interruptos megvalósítás. Hogy leszünk mégis gyorsabbak?

=== Több minta egyszerre feldolgozása

A működő kód itt található: https://github.com/rizsi/Arduino-IR-decoder/tree/quadrature_counter_2.0…

Úgy leszünk gyorsabbak, hogy lehetséges több mintát egy lépésben feldolgozni! Azaz egy futási periódus alatt több mintát venni. Egy mintavétel 4 bites szelektor mintát ad (az előző mintával együtt). Két új minta 6 bitest (itt is az előző periódus utolsó mintáját be kell venni a buliba). Három új minta pedig 8 bitest. A 8 bit jól hangzik, mert az épp egy bájt.

Vegyünk tehát 3 mintát egy periódusban pontosan X processzor ciklusonként egyet! (X értéke végül 12 lesz) Ezeket kombináljuk össze egyetlen bájtba, ami egy ijmp paramétere lesz, aminek segítségével a megfelelő számlálásra ugrunk!

A kombinálás:

````
loop:
in ZL, PINB
and PRSAMPLE1, CONST_MASK ; Maszkolások, hogy csak a hasznos bitek legyenek meg
and PRSAMPLE2, CONST_MASK
and ZL, CONST_MASK
   ; PRSAMPLE0-t nem kell maszkolni, mert előző körben már maszkoltuk!
lsl PRSAMPLE1
or PRSAMPLE1, PRSAMPLE0
mov PRSAMPLE0, ZL ; A már maszkolt mostani jelet tegyül el a következő ciklusra!
swap PRSAMPLE1        ; a swap utasítás az alsó és felső 4 biteket felcseréli egy lépésben. Nagyon hasznos itt, mert különben négyet kellene shiftelni, ami 4 órajel
lsl PRSAMPLE2
or ZL, PRSAMPLE2
or ZL, PRSAMPLE1
ldi ZH, 1    ; Az ugrótábla a 0x01XX címeken van A ZH-t nem tudtam fixen ezen az értéken tartani ebben a példában, ezért fel kell tölteni
in PRSAMPLE1, PINB ; Pontosan 12 ciklus telt el: a második mintát be kell olvasni!
ijmp    ; Számláló szubrutin: 10 órajel: odaugrás 2, visszaugrás 2, belső rjmp 2, számolás 4
count_ret:
    ... kommunikációra van 1 ciklusunk itt
in PRSAMPLE2, PINB ; Pontosan 12 ciklus telt el: harmadik mintát kell olvasni
    ... kommunikáció maradék 11 ciklusa, a végén a loop-ra ugrik vissza
````

A számláló szubrutin ugrótáblának 256 bejegyzése van! Elég melós lenne kézzel kitölteni, ezért egy kódgenerátort készítettem hozzá, ami 4 egymásutáni minta minden kombinációjára megmondja, hogy mit kell tenni:

 * Számlálás -1, -2, -3, +1, +2, +3
 * Nincs számlálás
 * Hiba van

Ezeknek a végrehajtása 4 órajel alatt meg tud történni, így éppen beleférünk ugrásokkal együtt a 10 órajelbe. Magukat a számlálásokat kézzel írtam meg 1-1 szubrutinban.

Látható, hogy nagyon szűk az órajel költségvetés: egyetlen felesleges utasításunk sincsen. Ha nem lenne minden változó fix regiszterben tartva, akkor legalább 2-3-szor lassabb lenne a program.

A kommunikáció megvalósítását nem részletezem. A lényeg, hogy az állapotgép ugrótáblán belül minden állapotra 6 utasítás jut. Ebbe kényelmesen belefér a bitek kishiftelése. Ha valami nem fér bele 6 utasításba, akkor két állapotra lehet bontani. Ez kicsit lassítja a kommunikációt, de nem baj. Figyelni kell arra is, hogy az értékek kishiftelése alatt a számlálót latch-cselni kell: küldés előtt a számláló pillanatnyi értékét lemásoljuk, hogy egy fix értéket küldjünk akkor is, ha a kommunikáció alatt jelváltás történne. Ami simán történhet, mert a kommunikáció sebessége sokkal kisebb, mint a jelfeldolgozásé.

== Nullázó input kezelése periodikus programban ASM-ben

https://github.com/rizsi/Arduino-IR-decoder/blob/quadrature_counter_2.0…

Eddig ez az utolsó - egyben legfejlettebb változat. Szükségessé vált egy nullázó jel kezelése is. A nullázó egy külön bemenet, és az onnan jövő jel jelzi, hogy a jeladó a nulla pontjánál, avagy a kalibrációs pontjánál van. Igen ám, de nincs hozzá szabad lábunk! Át kell írni tehát a kommunikációt úgy, hogy ne kelljen hozzá 3 láb! Az is látható, hogy nincs szabad CPU időnk sem plusz funkciókhoz, tehát vagy lassulni fogunk, vagy további piszkos trükköket kell bevetni. Naná, hogy nem lassulunk csakazértse! Nem engedünk a 12-ből!

=== Dupla periódusidő alatt kétszer annyi mintavétel

Ahhoz, hogy a nullázáshoz időt nyerjünk a program periódusidejét megnöveltem: kétszer három mintát veszünk egy nagyperiódus alatt. A kétszer három új mintát két 8 bites kóddá alakítjuk. A 8 bites összesített minták alapján nem ijmp-vel ugrunk, hanem lpm-mel értéket olvasunk a memóriából. Ez azért jó, mert kétszer két helyről használjuk a mintafeldolgozást egy periódusban, nem tudnánk azt a trükköt kihasználni, hogy látszólag függvényhívást ijmp/rjmp párossal hajtsunk végre, muszáj lenne valódi icall/ret párost használni, ami már túl költséges volna. Plusz három órajelet ki engedhet meg magának manapság?

A program memóriából kiolvasott érték használata kicsit kellemetlen, mert a hibának elágazás kell, illetve a negatív számokhoz le kell gyártani egy 0xff értéket is, hogy a kivonás működjön. Ez jópár plusz órajelet megemészt. Cserébe a két félperiódus inkrementumának előjeles összeadása még 8 biten megtörténhet, így a 4 bájtos összeadást elég egyszer végrehajtani nagyperiódusonként, tehát 6 mintánként. Ez is 3 órajel mínusz.

A táblázat 512 helyett csak 256 bájtos lett így. Szép eredmény, de a spórolt helyre úgysem volt szükségünk. Van kérem bőven hely, ez egy ATTiny25! Nem kell fukarkodni! Gondolkodtam rajta, hogy 10 bitre növelhetnénk a címzést, tehát 4 mintát dolgozhatnánk fel egyszerre. Az ATTiny25-ön ez csak úgy férne el, ha mintánként egy bájt felét - 4 bitet használnánk csak. Elvben működhetne, próbáltam is leprogramozni. De túl sokat kellene az értékkel zsonglőrködni, ami emészti a ciklusokat. Sehogy nem jött ki a matek. Ezért a probléma diszkutálását az olvasóra bízzuk! ATTiny45-re vagy ATTiny85-re optimalizált megoldást is vár a szerkesztőség!

További spórolás, hogy ugrálásra sem megy el 8 órajel a számláláshoz mint az előző változatban. A számlálást a loop-ba építve el lehet végezni, mivel mindig ugyanaz a művelet, csak az adat tér el. Persze helyette némi aritmetikára szükség van, de összességében megéri.

=== A nullázás kezelése

A nullázás kezelése úgy történik, hogy ha a nullázó jel aktív, akkor a számlálót átmásoljuk a nulla helyet tároló változóba. Tehát valójában sosem nullázunk, csak felírjuk, hogy melyik értéknél kellene nullázni. Egy nullázás státusz bitet is karbantartunk, hogy a lekérdező master CPU tudja, hogy ez új érték, vagy már eddig is ez volt-e a nulla regiszterekben? Szerencsére van 1 órajel alatt lefutó két regisztert másoló utasítás, a nullázás tehát összesen 5 órajelből megvan. Így már simán befértünk a költségkeretbe:

````
    sbrc PRSAMPLE1, PIN_ZERO_IN    ; Ha alacsony, akkor frissítjük a nullázó regisztereket
    rjmp no_zero
    movw ZERO_0, COUNTER32_0    ; Két regiszter egy csapásra!
    movw ZERO_2, COUNTER32_2    ; Még két regiszter egy csapásra! Az négy regiszter két csapásra
    sbr COUNTER_STATUS, COUNTER_STATUS_MASK_ZEROED    ; A státuszba is beállítunk egy bitet
no_zero_ret:
    ...itt folytatódik a főprogram

no_zero:
    rjmp no_zero_ret    ; Ha nincs nullázás, akkor kiugrunk és beugrunk. Összesen így is 5 órajel alatt végzünk

````

=== Kommunikáció egyetlen vonalon

Ahogy a bevezetésben írtam, a nullázáshoz lábat a kommunikációtól lehetett elvenni. Hogy legylább kicsit egyszerűbb legyen, a kommunikáció kérdésében elvetettem egy követelményt. Olyan protokollt csináltam, ami a mester oldalon interruptot tiltva szigorú időzítéssel használható csak. Ez a protokoll viszont egyetlen lábon működik, két láb felszabadult. Az egyikre megy a nullázó, a másik pedig megmarad tartaléknak. Ki tudja mire lehet jó még egy szabad láb!

A kommunikációs protokoll lényegében 1 bites serial lett. A trükk az benne, hogy a serial protokollon ahogy a kései bitek felé haladunk, annál több bajt okoz az órák aszinkronitása. Legyen tehát 1 bites a soros protokollunk, így nagyon toleránsak leszünk az óránkkal szemben. 1 bites serialon kétféle jelforma van:

 * A busz magasra van húzva két jel között
 * startbit alacsony
 * adat alacsony vagy magas
 * stopbit magas

Mivel egyetlen vonalon megy a kommunikáció, ezért busz szerűen kell használni: hol az egyik oldal, hol a másik oldal küld információt. Hogy programhiba esetén se égessük le a csippek lábait, ezért az összeköttetésbe nem árt egy 470Ohm-os ellenállás. Ez az áramot a legrosszabb esetben is 10mA körülire korlátozza. Ami nem árt annak ellenére sem, hogy elvben sosem hajtja a két oldal egyszerre ezt a vonalat. Plusz mivel buszként használjuk egy felhúzó ellenállás is szükséges, hogy a reset utáni átmeneti időben, amikor még senki nem hajtja semerre a vonalat, akkor is jól meghatározott jelszinten legyen. Amikor már fut a mester programja, akkor egy felhúzó ellenállást aktívan tud tartani végig.

A mester nem beszél sokat: csak egy impulzust küld, amivel jelzi, hogy ki akarja olvasni a pillanatnyi értékeket. Mint az úr, aki csak egy kézjellel kommunikál a szolgája felé. Az impulzus legalább egy nagyperiódusnyi ideig kell hogy tartson, mivel a slave egyszer ellnőrzi ezt a vonalat a 72 órajel alatt. Az impulzus után a mester elengedi a vonalat. Erre válaszként küldi a slave az 1 bites serial csomagokat néhány előkészítő nagyperiódus után. Itt jön el a szolga ideje, amikor szinte mesternek érezheti magát: hogy pontosan mikor kezd el beszélni, és milyen órajellel, azt ő döntheti el! A programja persze nem sok mozgásteret biztosít neki, de mégsem kell alkalmazkodnia máshoz. Ez is valami. Az adatküldő utasításaira várva biztosan az eleve elrendeltségen és a szabad akaraton elmélkedik a kis ATTiny.

A mester pedig alkalmazkodik a szolga által küldött jelformákhoz. Felköti a gatyáját, mert ez azért 16.000.000/24=666.666-es baud rate. Ördögien gyors! A vevő megvalósításából egy külön cikket tervezek, mert ebben is van szépség.

== További lehetőségek

Mire elkészült a program utolsó változata a végére egy rakás nop is bekerült. Túl sok optimalizálás ötletem volt. Főleg miközben a cikket írtam egy csomó minden eszembe jutott :-). Összesen 8 nop van egy nagyperiódusban, amit ésszerűen szétosztva a mintavétel periódusa 11-re csökkenthető volna. És még mindig maradna 2 nop-unk.

Túl sok az ugrálás is. A kommunikáció állapotgépe a periódis közepére ugrik vissza. Ha átrendezném, hogy mindig a periódus legvégére essen a kommunikáció és onnan a program ismétlődő részének legelejére ugorjunk, akkor a visszaugrás lespórolható volna. -2 órajel.

Így a 11 órajeles programban is lenne 4 nop. Talán még kitalálok valami további trükköt és lemegyünk 10 órajelre is! Elméletileg a mintavételi periódusidő tovább csökkenthető lehet. Van egy elméleti minimuma, de azt még messze nem értük el.

5 bites serialt alkalmazva kommunikációs protokollként a belső órák pontossága még elegendő lehetne, és a mester oldalon hardveres vevőt lehetne alkalmazni. Nem kellene az interruptokat a vétel alatt teljesen tiltani. Esetleg a kliensen is használhatnánk hardveres serialt a küldéshez az ATTiny USI rendszerét felprogramozva.

SPI kliensként is meg lehetne valósítani esetleg az adattovábbítást, csak akkor pont a jól elhelyezkedő lábak nem használhatóak kvadratúra jel vételre. A RESET jelet is funkcionálisan használva azonban megoldható lenne ez a működési mód. Az SPI kliens megvalósítás előnye volna, hogy a mester oldal a saját tempójában kérheti le az adatokat interrupt tiltás, sőt akár bármilyen hardver használata nélkül szoftveres SPI-vel.

Nagyobb csipen ha van 256 bájt RAM, akkor a táblázat oda tölthető lenne. Ezzel a kiolvasásban 1 órajelet spórolnánk. Kétszer olvasunk ki egy nagyperiódus alatt, tehát az már 2 órajel! Tehát 6 órajel tartalékunk lenne megint, amivel 10-re szoríthatnánk a mintavétel periódusát... Aki először megcsinálja a 10 órajeles változatot, azt meghívom egy sörre!

== A folytatás előkészület alatt:

 * Mikrovezérlő program automatikus tesztelése PC-n szimulátorral. Ez a cikk azt mutatja be, hogy hogyan lehet egy ilyen közepesen komplex rendszer esetén biztosra menni, hogy minden esetben megfelelően fog-e működni? Honnan tudom, hogy a 256 elemű táblázatnak - amit ráadásul egy program készített - minden eleme helyes? Ha átírom egy kicsit a programot honnan tudhatom, hogy továbbra is jól fog működni?
 * Mikrovezérlő program prototípus futtatása weben. Hogyan lehet a programunkból prototípusokat weben megjeleníteni?
 * Soros kommunikáció megvalósítása és debuggolása. Avagy miért nem működik és mi kell ahhoz, hogy biztonságosan működjön?

Hozzászólások

Szerkesztve: 2020. 05. 31., v – 10:10

Szép projekt. Tetszik.
Egyébként otthoni egyéb barkácsoláshoz, ha keveset akarsz forrasztani, ajánlom figyelmedbe az ALI-ról az ATtiny, ATmega és STM32 alapú 2 dollár alatti árban futó NYÁK-okat. Az AVR-ekre SPI-n keresztül, az STM32-re UART-on (PA9, PA10) keresztül tudod letölteni a progit a BOOT0 lábról kihozott jumper átdugása+RESET után. Linux alól avrdude és stm32flash mindkettője benne van az Armbian és Raspbian disztribúciókban.

Szerkesztve: 2020. 05. 31., v – 12:41

"2020-ban ki nyúl már assembly-hez"?

Ezért a mondatodért letöltendőt adnék. :) Különös bája, hogy utána megírtad assemblyben. Sejtem, hogy ezek a mondatok azoktól a fiataloktól jönnek eredendően, akik képtelenek megérteni egy MCU működését, nem tudnak és nem is akarnak megtanulni egy assembly nyelvet sem. Legutóbb hőmérséklet mérést és szabályozást írtam full assembly-ben egy belső áramszabályozó hurokkal együtt úgy, hogy kommunikálni kell közben, s az IT rutint 16 futási szeletkére szegmentáltam, mert különben az alapszinten futó kommunikáció megdöglött volna. Időkritikus helyen bátran lehet assemblyben programozni, semmi baj nincs vele. Ha jól szervezi az ember, áttekinthető marad a kód. Commentelni persze célszerű.

Azért az egy kicsit disznóság, hogy két MCU-val akartál egy PIC-et „legyőzni”. Tekintsük ezeket eszközöknek, amelyben nyilván van érzelem, mert amivel korábban dolgozott már az ember, ahhoz szívesebben nyúl. Ki a fenének van kedve újabb 300 - 1500 oldalt elolvasni egy MCU-ról, amikor van olyan, amelynek a szokásait, nyűgjét-baját fejből tudom már?

Jegyzem meg, PIC-ekben van CLC modul, szerintem azzal színtisztán hardware-esen összelegózhattad volna ennek az időkritikus részét úgy, hogy talán még a számlálás is hardware-es, de ha nem, akkor is előemésztett jeleid lennének irány, hiba, számlálás jelzésével. Ráadásul, mivel hardware-es, csinos kis sebességgel, ami legalább 10 MHz lehet az input oldalon. Ehhez mindenképp hardware számlálás kell. Nem ördögtől való azonban elővenni akár egy 74HC191-et, vagy bármi mást, ha kell a sebesség. Jó, tény, hogy szinte biztos talál az ember olyan MCU-t, amelyben tokon belül minden megvan, vagy FPGA-ba bele lehet lapátolni, ami kell.

A sebességre volt követelményed, vagy sportból optimalizáltad?

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Sejtem, hogy ezek a mondatok azoktól a fiataloktól jönnek eredendően, akik képtelenek megérteni egy MCU működését, nem tudnak és nem is akarnak megtanulni egy assembly nyelvet sem.

Legtöbben nem nagyon fognak olyasmit megtanulni, aminek nincs köze a munkájukhoz. Ha valami feladatot meg lehet oldani, úgy, hogy nem kell assemblyben megírni a kódot, akkor miért írná? Elismerem az egy plusz igényesség, ha valaki ebben elmélyül vagy ez a hobbija de ha csak egy melóról van szó aminek a határideje nem teszi lehetővé, hogy egy működő megoldással még el lehessen szórakozni hetekig hogy szebb vagy optimalizáltab legyen akkor szerintem teljesen rendben van ha ezt valaki nem akarja megtanulni. Az nyilván más kérdés, ha ez szükséges ahhoz hogy a meló el legyen végezve de akkor meg ki fogják rúgni az illetőt vagy kénytelen lesz megtanulni (az MCU-t pl.)

Nyilván PC-re valami színes-szagos GUI-t nem assembly-ben kell írni, viszont egy olyan MCU-ra, amelyben van néhány tíz, vagy néhány száz byte RAM, meg néhány száz, vagy néhány ezer gépi utasításnak flash, valamint az órajel frekvencia legfeljebb 32 MHz, de ha fontos az alacsony fogyasztás, inkább kifejezetten lassan hajtod az eszközt, akkor nem a C, vagy valami más magas szintű nyelv a jó választás. És tudom, terjednek arról anekdoták a szakmában, hogy van, aki már a C++-ra is gépközeli, alacsony szintű programozásként tekint.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

> Azért az egy kicsit disznóság, hogy két MCU-val akartál egy PIC-et „legyőzni”.

Az eredeti rendszerben a két sínt két külön PIC számlálta, viszont ugyanazon csippen ment a számláló kijelzés is. Szóval ha disznóság, akkor az az, hogy a kijelzést külön csipre tettem a számlálótól. Azt, hogy a két kijelzés 1 csipre kerül viszont az indokolja, hogy az új követelmények miatt a két csipnek kommunikálni kellene, annál meg már egyszerűbb 1 csipre tenni a teljes logikát, és a számlálást delegálni külön csipre.

Ilyen fix periodikus megoldás mellé odatenni a kijelzést igencsak kellemetlen feladat volna, viszont ha már megvan agyonoptimalizálva, akkor már nem térnék vissza az interruptos megoldásra, ami mellé simán oda lehet tenni bármi más logikát is. Úgyhogy a teljesen összehasonlítható változatot nem szeretném megvalósítani.

A PIC vs AVR "verseny" nyilván csak vicc, igazi verseny akkor lenne, ha egy PIC szakértő ugyanezt a fix periódusú logikát megvalósítaná. Ha gondolod benevezhetsz a versenybe egy PIC ASM listával :-)

> Ki a fenének van kedve újabb 300 - 1500 oldalt elolvasni egy MCU-ról, amikor van olyan, amelynek a szokásait, nyűgjét-baját fejből tudom már?

Pontosan ezért mondom, hogy AVR-es vagyok. Ettől függetlenül még nekem is úgy tűnik, hogy ilyen időkritikus kérdésekben talán jobb is a PIC. De mint a példa is mutatja, kellő kitartással nagyon sokat ki lehet hozni ezekből a kis kütyükből is, ritka, hogy az ember a fizikai korlátokat eléri.

Érdekes egyébként, hogy először PIC-jeim voltak, csak valamiért nem tudtam összerakni, hogy Linux alól programozzam, AVR-rel viszont ez azonnal OOB ment, így lettem végül AVR-es.

> A sebességre volt követelményed, vagy sportból optimalizáltad?

Pontos követelményt nem kaptam, de azt gondoltam magamban, hogy legalább legyek olyan gyors, mint az eredeti. Aztán elkapott a gépszíj, és sportból mentem el "majdnem" a falig. Az utolsó bekezdésben leírtam, hogy talán 11-ig, vagy esetleg 10-ig még elképzelhetőnek tartom, hogy el lehetne menni, de azért itt inkább leálltam. Nem ezzel a problémával akarom eltölteni az egész életemet :-)

Sok a szöveg, miközben a kvadratúra dekóder egy hardver, és Lenin elvtárs is megmondta: Olvasni hatalom! :D

A forgásirány detektálása (fáziskomparátor ?) és a jelek számlálása (számláló) is hardver, miközben az "egy PIC szakértő" == Microchip.

Tehát google: quadrature decoder clc = Configurable Logic Cell Tips 'n Tricks - Microchip Technology

Ebben meg van egy ilyen: decoding a quadrature-encoded input signal.

A feladat megoldása:

- kialálod mit szeretnél

- kiválasztod a megfelelő PIC-et (nagyságrendileg 300Ft)

- GUI segítségével összekötögeted a hardvert - ez generálni fogja az ehhez szükséges néhány utasítást

- a "szoftver" ezzel kész, mert hardver ;) (==0 utasítás)

- az eredményt kiolvasod, tárolod, tetszés szerinti interfészen elküldöd...

A PIC(16) családban az a szép, hogy folyamatosan fejlesztik, miközben már-már kihasználhatatlan mennyiségű (standard) perifériát zsúfolnak minden tokba.

Ugyanezt beletették az "új szériás" AVR-ekbe is. Linkeltem is a poszt első negyedében. Csak az a baj vele, hogy újra kellene tanulni hozzá szinte mindent mivel a perifériákat újratervezték. Egyszer már elhatároztam, hogy kitanulom, de aztán az a projekt kútba esett egyéb okok miatt, úgyhogy egyelőre nem ismerkedtem meg ezekkel a CLC-s dolgokkal.

Van egy gyanúm, hogy a teljes hobbista szcéna hasonló cipőben jár, mert az új nagyobb tudású csipek olcsóbbak, mint a régi butábbak.

Jó kérdés egyébként, hogy ha már valami újat tanulok, akkor nem lenne-e ésszerűbb valami ARM32+FPGA core vonalon elindulni? Ott persze még magasabb a belépési küszöb, és várhatóan még gyorsabban fog változni is.

Igazából mint ez a példa is mutatja, némi kompromisszummal, de ezek a "régi" egyszerű MCU-k is mindenre elegendőek. Az ár pedig hobbi szinten pár darabnál teljesen lényegtelen.

Ugyanezt beletették az "új szériás" AVR-ekbe is.

A két AN között eltelt 6 évből is látszik, hogy kicsit későn. Tán ezért vette meg őket a Microchip, mert...

a teljes hobbista szcéna hasonló cipőben jár, mert az új nagyobb tudású csipek olcsóbbak, mint a régi butábbak

Ezt úgy szoktam magyarázni, hogy az ebay választékában leginkább a 10-15 éves csipek találhatók. Ezekhez rengeteg amatőr írás létezik, amelyek végeredménye jól vagy rosszul működik, de mindenképpen elavult.

ha már valami újat tanulok, akkor nem lenne-e ésszerűbb valami ARM32+FPGA core vonalon elindulni?

Neeem, a Core i7 sokkal jobb! ;)

Komolyabban fogalmazva el kell döntened, hogy szoftvert vagy hardvert szeretnél inkább. De ennél is fontosabb, hogy ki tudd választani a feladathoz szükséges (esetleg minimális) megoldást. A 32, 64 sőt több bites megoldás biztosan modernebb. Ez különösen akkor lehet szempont, ha egy feladathoz elegendő/sok egy 6 lábú PIC  0,5k flash és 32 bájt rammal. Vagy egy rpi. :-)

némi kompromisszummal, de ezek a "régi" egyszerű MCU-k is mindenre elegendőek

Hát nem. Két alesetet lehet megkülönböztetni:

- Nincs szükség külső aszinkron események pontos feldolgozására, vagy az órajel/felbontás elegendő a feladat elvégzéséhez.

- A külső események feldolgozására alkalmas hardver is rendelkezésre áll. (pl. CLC)

A régi egyszerű MCU csak az első esetben alkalmas. Ha ragaszkodsz a régihez, akkor külső hardvert kell építeni hozzá.

Ja, még valami. Tetszett, hogy olyan bitkiosztást választottál a lábakon, hogy 5 volt a maszkod, s így egyetlen shifteléssel - 0x0a - közé tudtad fésülni a régi értékeket, megkapva így a 4 bites állapotteret. Ha az a két input egymás mellett lenne, mondjuk 3-as maszkkal, akkor a 0x0c maszkhoz két shift kellett volna, hogy ugyanoda juss. Ez egy apró részlet, ami megmutatja, hogyan lehet már a hardware tervezésekor optimalizálni, gyorsabb futást kapni pusztán attól, hogy statikusan hova tesszük az input lábakat.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Nekem a társam tervezte annó az áramkört és a NYÁK-ot, én csak a szoftvert csináltam. Ő egyáltalán nem programozott, így jól kiegészítettük egymást.
Mindig jó érzés volt, amikor elkezdtem írni a szoftvert és rájöttem, hogy a srác bár egy sort sem programozott, mindig úgy kötötte be a mikrovezérlő lábait, hogy kézre állt szoftveroldalról.

Egyébként egy másik, számomra kedves bit-bang projekt még abból az időből, amikor a mikrovezérlőkben nem volt hardver USB támogatás:
   https://www.obdev.at/products/vusb/download.html
waitForJ cimkétől indul a buli. A 1,5 Mbps tempójú low speed usb-t legkevesebb 8 órajel/bit (12 MHz clk) tempóval szoftverből lekezelték. A mikroidőzítés értelemszerűen assembly utasításokkal volt végigtervezve.

Hiszen "a mai compilerek már jobban tudnak optimalizálni, mint te kézzel".

Hát ja. :) Szokták a C-t „platformfüggetlen assembly”-nek csúfolni; ha ezt veszem alapul, akkor „logikus”, hogy nem fogja a fordító a platform finomságait teljes mértékben kihasználni. Ha meg az ember megpróbálja rábeszélni a fordítót, hogy mégis tegye ezt meg, az lehet már akkora munka, amekkora a sima assembly változat se lenne. :) Ráadásul az AVR-t assembly-ben még egy ember is tudja programozni.

egy gigantikus PIC vs AVR versennyé nagyítottam

Az AVR típusát ismerjük, de az eredeti PIC mi volt? :) Ez a verseny már ott nem fair, hogy a „közismert” PIC-ek az utasításokat 4 órajelciklus alatt hajtják végre, míg az AVR-nél a nagy részük 1 ciklus alatt lefut.

ATTiny25-öt eddig még nem programoztam, de a 8 láb azért erős limitáció ebben a témában... Ez volt a fiókban? :) Szép projekt, gratulálok!

Az eredetit megnéztem, PIC16F886. Tényeg 4 órajel 1 utasítás, ezt eddig nem is néztem.

 

A 8 láb bőven elegendőnek tűnt :-). Volt a fiókban és kiskerben ez a legolcsóbb (pedig ugye nem számít) azok közül amit ismerek. Ja és ez az egyik típus, amit 20MHz-ig ki lehet húzatni kvarc nélkül, ez is szempont volt.

Aki először megcsinálja a 10 órajeles változatot, azt meghívom egy sörre!

9 órajel is er egy sört? :) 

.set    DDRB,   0x17
.set    PINB,   0x16
.set    PORTB,  0x18

reset:
        sbi     DDRB,   0       ; PB0: UART output at the standard baud rate of 921600.
        cbi     DDRB,   1       ; PB1: phase A input
        cbi     DDRB,   2       ; PB2: phase B input

                                ; PB3 + PB4 connect a 16.5888 MHz crystal

        eor     r1, r1          ; the constant "0", zero register
        ldi     r20,    2       ; the constant "2" (1<<1)
        ldi     r21,    1       ; the constant "1"
        ldi     r22,    0       ; counter

        in      r17, PINB       ; inital state

.macro  BL0
        in      r16, PINB
        sbrc    r16, 2
        eor     r16, r20
        sub     r17, r16
        sbrc    r17, 1
        subi    r22, 0xff       ; do this if bit 1 is set
        sbrc    r17, 2
        subi    r22, 0x02       ; do this if bit 2 is set
.endm

.macro  BL1
        in      r17, PINB
        sbrc    r17, 2
        eor     r17, r20
        sub     r16, r17
        sbrc    r16, 1
        subi    r22, 0xff
        sbrc    r16, 2
        subi    r22, 0x02
.endm

.macro  USTART
        BL0
        nop
        BL1
        out     PORTB, r1       ; UART start bit
.endm

.macro  UBIT0
        BL0
        mov     r23, r22
        BL1
        out     PORTB, r23      ; UART bit 0
.endm

.macro  UBITx
        BL0
        ror     r23
        BL1
        out     PORTB, r23      ; UART bits 1, 2, ... 7.
.endm

.macro  USTOP
        BL0
        nop
        BL1
        out     PORTB, r21      ; UART stop bit
.endm
        
.macro  UFINAL
        BL0
        ;nop                    ; clock cycle is reserved for RJMP
        BL1
        ;out    PORTB, r21      ; clock cycle is reserved for RJMP
.endm
        

start:
        USTART          ; UART start bit
        UBIT0           ; UART bit 0
        UBITx           ; UART bit 1
        UBITx           ; UART bit 2
        UBITx           ; UART bit 3
        UBITx           ; UART bit 4
        UBITx           ; UART bit 5
        UBITx           ; UART bit 6
        UBITx           ; UART bit 7
        USTOP           ; UART stop bit
        USTOP           ; UART stop bit
        USTOP           ; UART stop bit
        USTOP           ; UART stop bit
        USTOP           ; UART stop bit
        USTOP           ; UART stop bit
        UFINAL
        rjmp            start

;szerk: az elobb belejavitottam egy kicsit, jobban makrozva, hogy valamivel olvashatobb legyen ;)

Mondjuk ez csak 8 bites szamlalo... bar elvileg kicsit hasonlo trukkokkel ugyanigy (atlag) 9 orajelben maradva meg lehet csinalni 16-ra is vagy akar 32-re is. 

Szamlalo: van benne egy 2 bites gray -> bin atalakitas, majd kivonja az elozo 2 bites allapotbol. Ha az also bit nem nulla, akkor noveli a szamlalot eggyel, ha a felso bit nem nulla akkor csokkenti kettovel. Ha nincs megszaladas (fazis kimaradas) akkor a felso bit csak ugy lehet 1, ha az also is 1. Ezert a csokkentes az vegsosoron +1-2 formaban van.

A gray->bináris átalakítás után akár lehetne simán hozzáadni és kivonni az előzőt is, nem? És akkor az még eggyel kevesebb utasítás lenne talán. Sajnos maszkolni is kell, de ha a 0-1 lábakon jön az AB jel, akkor működik. Úgy meg az UART nem esik kézre.

Szerk.: rájöttem, hogy nem jó az összeadás levonás, mert ahhoz a két bit előjeles extendelése is kellene, az meg plusz idő.

Szerk.2: esetleg így:

        in      r17, PINB
    andi    r17, 0b00000011 //hasznos bitek
        sbrc    r17, 1
        eor     r17, r20(0b11111101) // grey-> bin + negatív extend
        add     r22, r17    // ezt hozzáadjuk
        sub     r22, r16    // előzőt levonjuk

Aha, jonak tunik ez is, igy akar meg 7 ciklusbol is megoldhato :) Azert "akar" mert meg akkor az uart-lekuldesnel trukkozni kell egy kicsit: nem a 0-s biten hanem a 2-esen megy/menne ki az adat. Es oda nem annyira konnyu igy ilyen interleaved modon belepakolni kimenetet. A ror ugyis a carry-t teszi be az lsb helyere, a carry meg egy ilyen add-sub par utan mindig megsemmisul... 

A mester csipen serial hardwerrel folyamatosan véve, és az RX interruptra téve egy logikát, ami a felsőbb bájtokat ott számolja és megfelelően átfordítja teljesen jó a 8 bites megoldás. A 8 bit átküldése alatt maximum 32-t számolunk, tehát mindig tudni fogjuk, hogy merre kell átfordulni.

A megoldásban annyi "hiba" van még, hogy a FINAL körben az in utasítás egy órajellel elcsúszik, egyszer kevesebb, egyszer több órajel telik el. Egyelőre elég bonyolultnak tűnik ennek az orvoslása, de talán lehetséges. De a sörök mindenképpen állnak, mert nagyon szép megoldás és az eredetileg kiírt 10 órajeles változatot pláne biztosan hozza.

Ha megfeleznénk az UART sebességét, akkor lenne plusz két utasítás körönként, azzal együtt valahogy biztosan meg lehetne oldani a serial küldést nem a legalsó biten is.

A vicc az, hogy az eredeti programba két hasonló számlálót egy időalapú interruptra téve elegendően jó megoldást lehetett volna adni, a program többi részének pedig nem is kell interrupt. Úgyhogy 1 csipen is működne a dolog.

A megoldásban annyi "hiba" van még, hogy a FINAL körben az in utasítás egy órajellel elcsúszik, egyszer kevesebb, egyszer több órajel telik el. Egyelőre elég bonyolultnak tűnik ennek az orvoslása, de talán lehetséges. 

Igen, az ottan nem egyszeru. Marmint leginkabb az rjmp + stall ket ciklus-igenye miatt. 

Ha megfeleznénk az UART sebességét, akkor lenne plusz két utasítás körönként, azzal együtt valahogy biztosan meg lehetne oldani a serial küldést nem a legalsó biten is.

Ez meg a carry miatt nem annyira magatol ertetodo. Nezegettem egy kicsit azota de nem sok otlet jutott eszembe... 

Az, hogy egy szakmainak mondott oldalon is szinte hacking blognak szamit mar egy ilyen cikk azert mond valamit.

Csak szerintem ?

Ja es igen cikk, mert szep es igenyes kivitelezes,

Koszi.

Every single person is a fool, insane, a failure, or a bad person to at least ten people.

Szerkesztve: 2020. 06. 02., k – 06:41

"A fordító egy rakás regisztert használ - pedig kevesebb is elegendő volna, új regisztert allokál akkor is, amikor a másik változót már nem használjuk többet"

Gyakran hívott IRQ-nál fontos, hogy a felesleget kerüljük. Kis ügyességgel maszírozható a C is. Persze az alábbi PORTC^=(1<<5) nem véletlen, hogy szét lett szedve regiszterműveletekre.

volatile unsigned char irq_reg1_save;
volatile unsigned char irq_reg2_save;

ISR (INT0_vect, ISR_NAKED) {
    register unsigned char reg1 asm("r24");
    register unsigned char reg2 asm("r25");
    irq_reg1_save = reg1;
    irq_reg2_save = reg2;

    reg1 = PORTC;
    reg2 = 1<<5;
    reg1 ^= reg2;
    PORTC = reg1;

    reg1 = irq_reg1_save;
    reg2 = irq_reg2_save;
    reti();
}

Viszont ahogy elnézem, bugos az avr-gcc 5.4.0 fordító. Nálam a fenti kód végén a reg2-t is r24-re fordítja.
Sajnos nem kerülhető el az IRQ-hoz némi közvetlen assembly. De lehet, hogy akkor már a push és pop egyszerűbb. Időben ugyanannyi.

ISR (INT0_vect, ISR_NAKED) {
    register unsigned char reg1 asm("r24");
    register unsigned char reg2 asm("r25");
    asm volatile (
        "push r24\n\t"
        "push r25\n\t"
    );

    reg1 = PORTC;
    reg2 = 1<<5;
    reg1 ^= reg2;
    PORTC = reg1;

    asm volatile (
        "pop r25\n\t"
        "pop r24\n\t"
    );
    reti();
}