Inkrementális enkóderek alkalmazása

Kicsit lassan haladok az önképzéssel, nehezen gyűjtöm össze a megfelelő mennyiségű szorgalmat, hogy a hétköznapi hajtás után egy-egy hétvégén pihenés helyett elővegyem a hardvereket a fiókból. Ezen a hétvégén kivételesen sikerült. Hogy pontos legyek, kezembe került egy inkrementális enkóder, ami (akinek nem ismerős a fogalom) arra jó, hogy forgó mozgás irányát, mennyiségét és esetleg sebességét meghatározzuk.

Az abszolút enkóderekkel ellentétben az inkrementális enkóderek nem képesek megadni a forgás pillanatnyi helyét, segítségükkel csak azt lehet meghatározni, hogy történt-e forgás valamelyik irányba. Viszont jelentősen olcsóbbak, és kevesebb lábat foglalnak el a mikrokontrolleren. Ez sem utolsó szempont, tekintve hogy pár óra kínlódás után rájöttem, hogy a kontrolleremen két láb halott..

Általában véve a forgási enkóderek működését a Wikipedia vonatkozó cikke jobban elmagyarázza, mint én tudnám, így erre nem térek ki részletesen. A lényeg csupán annyi, hogy egy egységnyi forgás alatt két kapcsoló zár az enkóderben egymás után, a forgás irányától függően. Ennek tipikus bekötése active-low jelleggel rendkívűl egyszerű, az enkóder C csatlakozója ráköthető a földre, az A és B lábak számára pedig kiválasztunk két portot a kontrolleren, az én esetemben ez RB6 és RB7 lett. Ezen kívül semmilyen más alkatrészre nincs szükség, hiszen a Pic16F690-es processzor integráltan tartalmaz felhúzó áramkört, ami képes magas szinten tartani az adott portot, és leföldelés esetén biztonságos szinten tartja az áramot.

A feladat tehát a következő: detektálni kell az enkóder kapcsolóinak a zárását, figyelembe véve a sorrendjüket, és ennek megfelelően kell egy változó értékét növelni, vagy csökkenteni. Ha az A kapcsoló zár előbb, és később a B, akkor növelni, ha fordítva akkor csökkenteni kell eggyel a változót. Az általam használt EC12E típusú enkóder 24 diszkrét egységre bont egy kört, azaz 15 fokonként képes érzékelni a forgást, tehát a változó értékét 15-el megszorozva megkapjuk a pontos forgást. Az enkóder mechanikus jellege miatt a használat közben számítani kell pergés előfordulására, aminek a kezeléséről gondoskodni kell.

A mikroprogram gyors időközönként leolvassa a kapcsolók állását, és ez alapján kell eldöntenie, hogy merre forgatjuk az enkóder karját. Mivel figyelembe kell venni a kapcsolók korábbi állapotát is, triviálisan adja magát egy állapotgép-alapú megoldás, ami a pergést úgy küszöböli ki, hogy megengedi az állapotok közötti oda-vissza ugrálást a pergést követve, a pergés stabilizálódásával pedig az állapot is a megfelelő értéken rögzül.

Az állapotgép bemenete a két kapcsoló állása és minden leolvasáskor végrehajt egy lépést:

Az állapotgép megfelelő élei biztosítják, hogy amíg valamelyik kapcsoló pereg, addig a megfelelő két állapot között lépkedjen. Az ábrán pirossal és kékkel jelöltem azokat a lépéseket, mikor is növelem vagy csökkentem a forgást mérő változó értékét.

A programot a gyakorlatban is kipróbáltam, assembly-ben írtam a Pic16F690-hez a kódot, és a pickit2 próbapanelen lévő 4 led segítségével jelzem a változó aktuális értékét:

#include <p16F690.inc>
     __config (_INTRC_OSC_NOCLKOUT & _WDT_OFF & _PWRTE_OFF & _MCLRE_OFF & _CP_OFF & _BOR_OFF & _IESO_OFF & _FCMEN_OFF)
 
     cblock 0x20
State		;state machine variable
Display         ;data to be displayed
     endc
 
     org 0
Start:
	clrf PORTB
     	bsf STATUS,RP0		; select Register Page 1
	bcf OPTION_REG,7
     	movlw 0xFF
     	movwf TRISA		; Make PortA all input
     	movwf TRISB
     	clrf TRISC		; Make PortC all output
     	clrf ANSEL 		; all pins digital
    
     	bcf STATUS,RP0		; back to Register Page 0 
     	bsf STATUS,RP1		; Reg page 2
	
	movlw 0xFF		
	movwf WPUB		; enable weak pull-up for Port B

	bcf STATUS, RP1		; Reg page 0

     	clrf Display
	clrf State

MainLoop:

;state table:
; input	|  000 	|  010	|  100	|  011	|  101	|
;  00	| 		->000			| == always ->000
;  01	|      ->010 	| ->000 | ->010 |->000- | == if state,0 ->000 (dec if 101), else ->010
;  10	| ->100	| ->000	| ->100 |->000+	| ->100 | == if state,1 ->000 (inc if 011), else ->100
;  11	| ->000 | ->011 | ->101 | ->011 | ->101 | == if state,0 ->101, else if state,1 ->011, else ->000

;decide current input: 00, 01, 10, or 11
;note that all input are active low

	btfss PORTB,6
	goto Input0
	goto Input1

Input0:
	btfss PORTB,7
	goto Input00
	goto Input01	

Input1:
	btfss PORTB,7
	goto Input10
	goto Input11

Input11: ;00
	clrf State		;state ->000
	goto Output
Input10: ;01
	btfss State,0		
	goto I01B		;if 0xx
	goto I01A		;if 1xx
I01A:			
		btfsc State,2	;if xx1
		decf Display,1	; decrement data
		clrf State	;->000
		goto Output
I01B:
		clrf State	;->010
		bsf State,1
		goto Output
Input01: ;10
	btfss State,1
	goto I10B		;if x0x
	goto I10A		;if x1x
I10A:
		btfsc State,2	;if xx1
		incf Display,1	; increment data
		clrf State	;->000
		goto Output
I10B:
		clrf State	;->100
		bsf State,0
		goto Output
Input00: ;11
	btfsc State,0		;if 1xx
	goto IA
	btfsc State,1		;or x1x
	goto IA
	goto Output
IA:
		bsf State,2 	;->uu1
	goto Output

Output:

     movfw     Display       ; Copy the display to the LEDs
     movwf     PORTC

     goto      MainLoop
     end

A dolgot összeállítva minden várakozásomat felülmúlva működött, a forgást pontosan és gyorsan méri, bár csak kézzel forgattam, így nem tudtam letesztelni, hogy mekkora szögsebességet képes még mérni. Mindenestre akár felhasználónak nyújtott bemeneti eszközként is remekül szerepelhet, mindenképpen kifinomultabb megoldás mint pl. egy potméter A/D átalakítóval. Digitális hifiken rendszeresen találkozhatunk ezzel a megoldással hangerő-szabályozás formájában.

New service release: 0.7.2

I’m proud to announce that after a long time, we finally managed to throw out a new release. I’ve managed to improve the core functionality therefore this release contains a significant advance in overall performance.

Also, Stampie managed to fix a long-awaited feature, the two-way synchronization with the variables view.

In the future, we plan to redesign the use-cases of the tool. Currently, we’re in an experimental phase, which means we do not know how we want it to work.

Eclipse GEF wtf

Az előző kitérő után most térjünk vissza egy kis kocka témához. A minap érdekes felfedezést tettem, miközben véletlenül a GEF belső kódjába tévedtem debug közben. Alapvetően egyébként meg vagyok elégedve a GEF és általában az eclipse platform minőségével, ritka az az eset, hogy a fejemet fogom egy-egy megoldás láttán.

A következő kódrészlet ugyan működik és mivel a publikus api elrejti, az átlag fejlesztő nem találkozik vele, mégis érdemes rávetni egy pillantást. További szócséplés helyett következzék a kód, szerintem magáért beszél:


//AbstractEditPart.class

private Object[] policies;

//...

/**
* @see EditPart#installEditPolicy(Object, EditPolicy)
*/
public void installEditPolicy(Object key, EditPolicy editPolicy) {
Assert.isNotNull(key, "Edit Policies must be installed with keys");//$NON-NLS-1$
if (policies == null) {
policies = new Object[2];
policies[0] = key;
policies[1] = editPolicy;
} else {
int index = 0;
while (index < policies.length && !key.equals(policies[index])) index += 2; if (index < policies.length) { index++; EditPolicy old = (EditPolicy)policies[index]; if (old != null && isActive()) old.deactivate(); policies[index] = editPolicy; } else { Object newPolicies[] = new Object[policies.length + 2]; System.arraycopy(policies, 0, newPolicies, 0, policies.length); policies = newPolicies; policies[index] = key; policies[index + 1] = editPolicy; } } if (editPolicy != null) { editPolicy.setHost(this); if (isActive()) editPolicy.activate(); } }

Akinek nem világos elsőre, kifejteném a problémát: láthatóan egy tömböt használ a java-ban alapértelmezésként elérhető "Map" funkcionalitásának a kiváltására. A tömb páros (és nulladik) helyén szereplő elem tárolja a kulcsot, az utána lévő páratlan helyen lévő elem az érték.

Minden elem hozzáadásakor dinamikusan növeli a tömb méretét, törléskor meg egyszerűen null-ra állítja a tömb megfelelő elemét. Ehhez még társul egy custom iterátor is, ami a tömb nem null elemeit listázza.

Őszintén nem értem a tervezési döntést, ami a tömb-alapú Map-hez vezethetett. A memóriaigénye a HashMap-nek nem sokkal több, és mivel jellemzően kis elemszámú esetek fordulnak elő, ez nem számottevő. A sebessége a HashMap-nek jobb, a "get" és "put" metódusok általános esetben konstans, de mindenképpen kevesebb a tömb végigjárásánál. Mindennek a tetejébe a fenti kód Map alkalmazásával kb. 3 sorra cserélhető, nem beszélve az osztály egyéb kódjáról, ami a tömböt piszkálja.

Egyetlen érthető mentségként csak arra tudok gondolni, hogy esetleg a kód korábban íródott, minthogy a java collections API-ba belekerült volna a Map. Ez viszont az 1.2-es verzióban történt meg, tehát elég régen. Nem tudom mennyi idős a GEF, így ezt nem tudom eldönetni.. Mindenesetre ez a kód nálam megütötte a WTF szintet.

Java konstruktor hívási sorrend

Rég írtam, valóban. Hogy újra rávezessem magam a kedvenc munkán kívüli elfoglaltságomhoz, most egy egyszerűbb témát vetek fel, ami mégis okozott pár kobak-koppanást az asztalon. Nevezetesen arról szándékozom írni, hogy a Java hogyan sorrendezi egy objektum inicializálásában résztvevő kódokat. Mert bizony több lehet, és nem egyértelmű a lefutási sorrend, ahogy azt a követkető kódrészlet szemlélteti:


public class AB{
public static abstract class A{
public A(){
doSomething();
}
protected abstract void doSomething();
}

public static class B extends A{
final String s = "1234";
final Object o = "1234";
public B(){
super();
}
@Override
protected void doSomething(){
System.out.println(s.getClass());
System.out.println(o.getClass());
}
}

public static void main(String[] args){
new B();
}
}

A példát igyekeztem egyszerűnek tartani, de talán megérdemel egy rövid magyarázatot: Két osztályt definiáltam, az egyik leszármazottja a másiknak. Érdekesség még, hogy az ős konstruktora kódot hív meg a leszármazottból absztrakt metóduson keresztűl. Egy “B” típusú objektum létrehozásához láthatóan három helyről kell kódot hívni: Az “A” és a “B” konstruktora, továbbá a “B” osztályban lévő, konstruktoron kívüli inicializálás. A probléma ezen három kódrészlet lefutásának sorrendje.

Első ránézésre a kóddal semmi probléma nincs azon kívül, hogy teljesen haszontalan. Mindkét mező final, így gondolnánk a konstruktor elött inicializálódnak, tehát nem okozhat problémát. Azonban a kód futtatásakor egy csinos NullPointerException kacsint vissza ránk. A futás kimenete:


class java.lang.String
Exception in thread "main" java.lang.NullPointerException
at AB$B.doSomething(AB.java:22)
at AB$A.(AB.java:8)
at AB$B.
(AB.java:17)
at AB.main(AB.java:27)

Láthatóan a B.doSomething() első sora lefut hiba nélkül, a probléma utána keletkezik. Tehát az “s” változó már létezik, az “o” viszont nem. A konstruktorok lefutásának sorrendje tehát: s=..,A(),o=..,B(). A két mező között csupán az a különbség, hogy az “s” mező típusa megegyezik a futásidejű értékének típusával. Ez meghatározza, hogy a mező a szülő osztály konstruktora előtt vagy után kap értéket. Érdemes kipróbálni, ha az “s” mező elöl kivesszük a final kulcsszót, az azt okozza, hogy az is a szülő konstruktor hívása után kap értéket. Vajon hogy határozódik meg a pontos sorrend, mitől függ, hogy hova ütemezi be a java a mezők értékadását?

Nyílván valahogy igyekszik meghatározni, hogy az adott értékadás kiértékelhető-e az osztály örökölt részének inicializálása nélkül vagy sem. Ennek tesztelésére tettem még egy kísérletet, kicsit módosítva a példát:


public class AB{
public static abstract class A{
protected String a;
public A(){
a = "abc";
doSomething();
}
protected abstract void doSomething();
}

public static class B extends A{
final String s = "1234"+a;
final Object o = "1234";
public B(){
super();
}
@Override
protected void doSomething(){
System.out.println(s.getClass());
System.out.println(o.getClass());
}
}

public static void main(String[] args){
new B();
}
}

Ez a kód is szépen elszáll, méghozzá a B.doSomething() első sorában! Azaz, az explicit hivatkozás egy szülő objektum-beli elemre azt eredményezi, hogy a mező értékadását a szülő konstruktor utánra ütemezi. Elég intelligensnek tűnik a dolog, de mégis beleütközik az ember, mert másra számít.

Persze elkerülhető a dolog ha a konstruktorból hívott absztrakt metódusokat anti-pattern-nek kiáltjuk ki, bár gyakran jól jön. Mégis pontosan mi határozza meg a sorrendet, és lehet-e befolyásolni valahogyan? Van valaki aki nálam sikeresebben guglizott?

A burst of commits

I’ve commited the Debug Visualization project a long ago, but the previous few days I’ve had some spare time to kill on the project. The work mostly focused on the input package, which I didn’t liked at all when I wrote it in the first time. I just didn’t have better idea. But i have now and rewrote the whole part.

I’m satisfied with the currently used approach (for now), as I’ve managed to separate different functionalities, and connect them in a simple way. Also, it is reasonably faster than before, but it still makes the eclipse lagging when the user steps the debug context, which I don’t know why (yet). Anyway, navigating in the graph, hiding/showing nodes and other display-related functions are now fast enough.

For a more detailed explanation of the new operation, see the wiki page InputPackage.

ps.: for a change as big as this, I’m thinking of a maintenance step in the version numbering (0.7.0 instead of 0.6.2), what do you think? I’m hesitating because of the lack of new features.