
Diagnozer joysticków (np. do Pegasusa) na Atmega 8
Projekt
Przyszedł wreszcie czas naprawić posiadane przeze mnie joysticki do Pegasusa. Zwykle uszkodzeniu (urwaniu) uległ kabel lub taki niesprawdzony/niesprawny joystick został zakupiony bez znajomości jego przeszłości. Jeśli kable się urwały, to mamy mały problem, bo płytka PCB joysticka składa się z jednego glutowego układu scalonego (rejestr przesuwny + generator do przycisków TURBO) i ciężko stwierdzić, gdzie co podłączyć.

A podłączanie jak leci aż za którymś razem zadziała może się zakończyć tragicznie:
- zewrzemy dwa wyjścia,
- podłączymy odwrotnie VCC i GND.
Protokół komunikacyjny z joystickiem Pegasusa
Kontrolery mają czteropozycyjny krzyżak (d-pad), przycisk select, start, A, B, czasem Turbo A, Turbo B. Złącze joysticka składa się z 5 pinów:
- GND: masa,
- VCC: zasilanie +5V,
- STROBE: strob zapisu – konsola generując opadające zbocze rozpoczyna odczytywanie stanu klawisy
- CLK: sygnał zegarowy – kolejne zbocza narastające generowane przez konsolę powodują wystawienie przez joystick na złącze DATA stanu kolejnych przycisków w kolejności: A,B, SELECT, START, GÓRA, DÓŁ, LEWO, PRAWO
- DATA: wystawiane przez konsolę stany przycisków (0 – przycisk wciśnięty, 1 – przycisk zwolniony, otwarty kolektor)
Założenia
Postanowiłem więc stworzyć mini-diagnozer do joysticków, który w zamyśle ma działać tak:
1. podłączamy do niego joystick od Pegasusa,
2. w diagnozerze wybieramy, jaki klawisz joysticka testować,
2. na joysticku trzymamy wciśnięty wcześniej ustawiony klawisz (np. START),
3. wciskamy przycisk na diagnozerze,
4. diagnozer testuje wszystkie możliwe kombinacje połączenia przewodów i:
- jeśli znajdzie działającą kombinacje i jest ona poprawną – wyświetla komunikat OK,
- jeśli znajdzie działającą kombinacje, ale nie jest ona poprawna – wyświetla, które połączenia są zamienione, np. G9 – na 9 pinie jest MASA, S5 – na 5 pinie jest sygnał Strobe, c3 – na 3 pinie jest sygnał CLK;
- jeśli żadna kombinacja jest niepoprawna – wyświetla komunikat: ER (error).
Wystarczy więc – jeśli kabelki w joysticku się oderwały – przylutować je, jak leci, a następnie podłączyć do diagnozera. Wszystkie sygnały w diagnozerze mają w szeregu rezystor 180 ograniczający prąd, więc podczas testowania nie ma zwarć i joystick nie ulegnie uszkodzeniu. Procesor ze swoich wyjść zasila (VCC+gnd) badany joystick. Diagnozer ma możliwość wysterowania dowolnych z 9 pinów DB9, więc w przyszłości można dodać implementację testowania innych rzeczy (np. myszki COM, klawiatury – z przejściówką, itp).
Efekt
Dzięki urządzeniu udało się uratować 4 joysticki, a 2 zostały wykryte jako uszkodzone – pewnie słusznie, bo na teście diod w mierniku jeden z pinów joysticka w ogóle nie ma połączenia do żadnego z pozostałych.

Ciekawostki
Chciałem, aby PCB joysticka było jak najmniejsze – stąd na PCB tylko:
- jeden przycisk do rozpoczęcia testu,
- wyświetlacz LED 7 segmentowy w minimalistycznym połączeniu – jedynie 2 rezystory ograniczające prąd dla każdej z cyfr, wadą jest oczywiście jaśniejsze świecenie cyfry gdy mniej segmentów jest zapalonych (można to programowo zminimalizować przez PWM)
- AVR Atmega 8 taktowana wewnętrznym kwarcem 8 MHz
- całość zasilana z portu programatora ISP
- bardzo chciałem wszystko zmieścić na jednej warstwie








Kłopoty
Od dłuższego czasu napisałem sobie specjalne makra pod AVR definiujące wszystkie porty, aby np. zamiast pisać:
#define BTN1_DDR DDRC
#define BTN1_PORT PORTC
#define BTN1_PIN PINC
#define BTN1_P PC0
#define BTN2_DDR DDRB
#define BTN2_PORT PORTB
#define BTN2_PIN PINB
#define BTN2_P PB1
#define BTN3_DDR DDRD
#define BTN3_PORT PORTD
#define BTN3_PIN PIND
#define BTN3_P PD7
...
cbi(BTN1_DDR, BTN1_P) //przycisk BTN1 jako wejście
sbi(BTN1_PORT, BTN1_P); //włącz rezystor obciągający na BTN1
cbi(BTN2_DDR, BTN2_P) //przycisk BTN2 jako wejście
sbi(BTN2_PORT, BTN2_P); //włącz rezystor obciągający na BTN2
cbi(BTN3_DDR, BTN3_P) //przycisk BTN3 jako wejście
sbi(BTN3_PORT, BTN3_P); //włącz rezystor obciągający na BTN3
while (get_bit(BTN1_PIN, BTN1_P)); //czekaj aż przycisk BTN1 nie zostanie wciśnięty
móc napisać:
#define BTN1 C0
#define BTN2 B1
#define BTN3 D7
as_input(BTN1, PULLUP_ON);
as_input(BTN2, PULLUP_ON);
as_input(BTN3, PULLUP_ON);
while (!in(BTN1));
Makra te definiują w stałej np. A0 wszystkie informacje o tym porcie, tj:
- adres rejestru DDR dla A0 (DDR),
- adres rejestru PORT dla A0 (PORTA),
- adres rejestru PIN dla A0 (PINA),
- numer portu dla A0 (0).
Każda z tych zmiennych jest 8-bitowa, potrzebujemy więc 32 bity:
#define A0 (PA0 | CAST1(&PORTA) << 8 | CAST1(&PINA) << 16 | CAST1(&DDRA) << 24)
i teraz, jeśli chcemy się dowiedzieć np. jaki jest rejestr DDR dla końcówki A0, po prostu wyłuskujemy to, np. makrem:
#define DDR(name) \
(*(volatile uint8_t*)(int)(((name) >> 24) & 0xFF))
zatem makro as_output wygląda tak:
#define as_output(pinname) \
sbi(DDR(pinname), P(pinname))
Ponadto sprytny kompilator C widząc, że wszystko dzieje się na stałych, zoptymalizuje kod tak, że wywołanie tych funkcji ograniczy się do dwóch rozkazów assemblera, np:
00000024: 98A0 CBI 0x14,0 Clear bit in I/O register
+00000025: 9AA8 SBI 0x15,0 Set bit in I/O register
18: as_input(BTN2, PULLUP_ON);
czyli w zasadzie wyjdzie tak samo, jakbyśmy napisali.
cbi ddrc, 0
sbi portc, 0
Dużo czytelniej, a tak samo szybko. Często jednak kusi nas do tego stopnia, że zamiast przekazywać takie końcówki wprost do funkcji, chcielibyśmy stworzyć ich tablicę, np:
const uint32_t btns[] = {BTN1, BTN2, BTN3};
as_input(btns[0], PULLUP_ON);
as_input(btns[1], PULLUP_ON);
as_input(btns[2], PULLUP_ON);
Słowem klucz jest tutaj const – jeśli dodamy je przed deklaracją tablicy, kompilator uzna, że wartości tablicy nie będą się zmieniać, więc będzie mógł od razu stwierdzić, że powyższe wywołanie będzie równoznaczne temu z przykładu wyżej. Często jednak chcemy np. iterować po takiej tablicy lub wręcz zmieniać jej wartość w trakcie programu – wtedy nie możemy dać const. I tutaj pojawia się mój kłopot – kompilator wtedy nie może rozwinąć takiego makra do jednej instrukcji assemblera, lecz musi te wszystkie przesunięcia bitowe zamienić na rozkazy procesora. I tu pojawia się problem – kompilator zamienia to w taki sposób, że już instrukcja ustawienia bitu nie jest tożsama z instrukcją sbi/cbi z Assemblera. Kompilator najpierw odczytuje do rejestru wartość np. zmiennej DDRC, portem ustawia w niej odpowiedni bit, a potem tak zmodyfikowaną wartość z powrotem zapisuje do rejestru DDR. Problem jest jednak taki, że gdy działamy na przerwaniach, to takie ciągi instrukcji mogą być przerwane przerwaniami i w rzeczywistości dostajemy niezłą sieczkę na portach – sam tego doświadczyłem, bo gdy program testował joystick, to wartość na wyświetlaczu siedmiosegmentowym, sterowanym w przerwaniu zmieniała się jak szalona. W obecnej chwili rozwiązałem to tak, że przed każda z instrukcji np. as_input(...), as_output(...), out(.., 0), out(..., 1), in(...) wyłączam przerwania, a po – włączam. Jest to jednak nieekonomiczne, macie jakiś pomysł jak zmusić kompilator, aby kompilował te instrukcje z wykorzystaniem rozkazów sbi/cbi ?
W obecnej chwili – np. gdy przed deklaracją tablicy wręcz dodam „volatile”, kompilator kompiluje mi to do:
0000002F: 93DF PUSH R29 Push register on stack
+00000030: 93CF PUSH R28 Push register on stack
+00000031: B7CD IN R28,0x3D In from I/O location
+00000032: B7DE IN R29,0x3E In from I/O location
+00000033: 972C SBIW R28,0x0C Subtract immediate from word
+00000034: B60F IN R0,0x3F In from I/O location
+00000035: 94F8 CLI Global Interrupt Disable
+00000036: BFDE OUT 0x3E,R29 Out to I/O location
+00000037: BE0F OUT 0x3F,R0 Out to I/O location
+00000038: BFCD OUT 0x3D,R28 Out to I/O location
17: volatile uint32_t btns[] = {BTN1, BTN2, BTN3};
+00000039: 01DE MOVW R26,R28 Copy register pair
+0000003A: 9611 ADIW R26,0x01 Add immediate to word
+0000003B: E6E0 LDI R30,0x60 Load immediate
+0000003C: E0F0 LDI R31,0x00 Load immediate
+0000003D: E08C LDI R24,0x0C Load immediate
+0000003E: 9001 LD R0,Z+ Load indirect and postincrement
+0000003F: 920D ST X+,R0 Store indirect and postincrement
+00000040: 5081 SUBI R24,0x01 Subtract immediate
+00000041: F7E1 BRNE PC-0x03 Branch if not equal
19: as_input(btns[0], PULLUP_ON);
Cool? Ranking DIY