“Klubowe” beacony mikrofalowe (23, 13 oraz 3 cm) wybierają się do nowego, trochę wyższego niż dach bloku QTH. Żeby wszystko było legalne, poprawne i zgodne ze sztuką, należało wymienionym urządzeniom zaktualizować oprogramowanie keyerów, aby przedstawiały się odpowiednim znakiem (SR3YOR) oraz nowym lokatorem.
Beacony na 3 i 23 cm to konstrukcje “klasyczne”, tzn. schemat blokowy urządzenia żywcem przypomina “nadajnik telegraficzny na jedno pasmo” z egzaminu na uprawnienia amatorskie.
Źródłem sygnału jest generator kwarcowy, którego sygnał jest wielokrotnie powielany, filtrowany i wzmacniany, aż do uzyskania odpowiedniej częstotliwości i mocy wyjściowej. Kluczowanie odbywa się poprzez włączanie i wyłączanie stopnia mocy (dla 23 cm) lub delikatną zmianę częstotliwości generatora za pomocą napięcia (3 cm – nadaje sygnał CW-FSK).
W paśmie 3 cm zastosowano kwarc o częstotliwości 108,009 MHz co pomnożone 96 razy produkuje sygnał w okolicy 10 368 MHz – dostępny jest trymer do precyzyjnego dostrajania, a o stabilność temperatury (i częstotliwości) dba OCXO wg. G8ACE, utrzymujące stałe 60°C.
Układy kluczujące w obydwu urządzeniach są względnie proste, bo ich zadaniem jest tylko wytworzenie przebiegu on/off odpowiadającego telegraficznej reprezentacji znaku i lokatora.
3 cm
Na pierwszy ogień weźmy beacon na 3 cm, z keyerem wykonanym przez SP3FYK i zaprogramowanym przez SP5MX.
Serce urządzenia to leciwy (a jeszcze niedawno całkiem nowy, w porównaniu do 2051…) mikrokontroler attiny2313. Wygodnie wyprowadzone złącze ISP kusi żeby podłączyć doń programator i zobaczyć co w beaconie piszczy.
Za pomocą programu avrdude odczytałem pamięc flash urządzenia:avrdude -c usbasp -p t2313 -U flash:w:3cm.hex:i
i zabrałem się za jej analizę. Mógłbym oczywiście bardzo małym nakładem pracy napisać swój program, w końcu to tylko ustawianie stanu HIGH oraz LOW na danym pinie, ale w ramach zabawy stwierdziłem, że przerobię to co już siedzi w mikrokontrolerze.
Tak prezentuje się surowy program. Zanim sięgniemy po deasembler, warto wizualnie ocenić zawartość pamięci – moją uwagę natychmiast przykuły ciągi “2D” i “2E” w pierwszej połowie zrzutu – odpowiadają one znakom ‘-‘ i ‘.’:
>>> chr(0x2d), chr(0x2e)
('-', '.')
To nie może być nic innego jak telegrafia. Zamieńmy więc wszystkie wystąpienia 2D
na myślniki, 2E
na kropki a 00
na spacje (global search-and-replace):
Cały alfabet telegraficzny w formie tablicy. Super, czyli gdzieś w kodzie musi znajdować się funkcja które iteruje po ciągu znaków i dla każdego wywołuje funkcję nadającą. To pozwala domniemywać, że gdzieś w pamięci musi znajdować się ciąg tekstowy, LUB, ewentualnie, szereg liczb określający pozycję w tablicy które tworzą znak i lokator. Tekst w pliku binarnym najłatwiej znaleźć za pomocą polecenia strings
, ale żeby móc go użyć, trzeba przekonertować nasz plik .hex na format binarny:
➜ objcopy -I ihex -O binary 3cm.hex 3cm.bin
➜ strings 3cm.bin
..--.-
.-.-.
--..---....-.-.-.--..-.
(....)
-.--
--..
sr3xhy sr3xhy sr3xhy jo82lj jo82lj jo82lj
Voila – mamy co chcieliśmy. Pozostało tylko znaleźć interesujący nas tekst w pliku .hex
i podmienić go na nowy.
>>> ''.join([hex(ord(x))[2:] for x in 'sr3xhy'])
'737233786879'
Zaznaczone na niebiesko bajty w pliku .hex (6A
i 23
) to sumy kontrolne – należy przeliczyć je, aby program avrdude
nie zgłosił błędu przy programowaniu. Użyłem pluginu do edycji plików intel hex do VS Code i zrobiłem to jednym kliknięciem. Pozostało wgrać zmieniony program:
avrdude -c usbasp -p t2313 -U flash:w:3cm.hex:i
I zgodnie z oczekiwaniami – dioda led na keyerze zaczęła mrugać w takt nowego znaku i lokatora.
23 cm
Tu jest gorzej. Narzędzie strings
nie zwraca żadnych “oczywistych” rezultatów, tak samo rzut oka na plik hex. Czas spojrzeć na program w postaci zdeasemblowanej:
avr-objdump -D -m avr 23cm.hex | less
Podstawowym punktem zaczepienia może być oczywiście fakt, że kluczowanie CW odbywa się za pomocą jakiegoś pinu procesora. Sterowanie pinami odbywa się za pomocą instrukcji sbi
i cbi
, które muszą występować listingu asemblera:
3b6: 08 95 ret
3b8: 95 9a sbi 0x12, 5 ; 18
3ba: a0 e6 ldi r26, 0x60 ; 96
3bc: 8d 91 ld r24, X+
3be: 9c 91 ld r25, X
3c0: 5b d0 rcall .+182 ; 0x478
3c2: 95 98 cbi 0x12, 5 ; 18
3c4: a0 e6 ldi r26, 0x60 ; 96
3c6: 8d 91 ld r24, X+
3c8: 9c 91 ld r25, X
3ca: 56 d0 rcall .+172 ; 0x478
3cc: 08 95 ret
3ce: 95 9a sbi 0x12, 5 ; 18
3d0: a2 e6 ldi r26, 0x62 ; 98
3d2: 8d 91 ld r24, X+
3d4: 9c 91 ld r25, X
3d6: 50 d0 rcall .+160 ; 0x478
3d8: 95 98 cbi 0x12, 5 ; 18
3da: a0 e6 ldi r26, 0x60 ; 96
3dc: 8d 91 ld r24, X+
3de: 9c 91 ld r25, X
3e0: 4b d0 rcall .+150 ; 0x478
3e2: 08 95 ret
W powyższym fragmencie widać dwie funkcje (pomiędzy ret
i ret
), które ustawiają stan wysoki a następnie niski na pinie 5
portu “D” – czyli 0x12
, zgodnie z dokumentacją attiny2313.
Poszukajmy w kodzie odwołań do tych funkcji, czyli skoków pod adresy 0x3b8
oraz 0x3ce
:
29e: 08 95 ret
2a0: 8b d0 rcall .+278 ; 0x3b8
2a2: 95 d0 rcall .+298 ; 0x3ce
2a4: 94 d0 rcall .+296 ; 0x3ce
2a6: 93 d0 rcall .+294 ; 0x3ce
2a8: 9d d0 rcall .+314 ; 0x3e4
2aa: 08 95 ret
2ac: 90 d0 rcall .+288 ; 0x3ce
2ae: 84 d0 rcall .+264 ; 0x3b8
2b0: 8e d0 rcall .+284 ; 0x3ce
2b2: 98 d0 rcall .+304 ; 0x3e4
2b4: 08 95 ret
2b6: 80 d0 rcall .+256 ; 0x3b8
2b8: 8a d0 rcall .+276 ; 0x3ce
2ba: 7e d0 rcall .+252 ; 0x3b8
2bc: 7d d0 rcall .+250 ; 0x3b8
2be: 92 d0 rcall .+292 ; 0x3e4
2c0: 08 95 ret
2c2: 85 d0 rcall .+266 ; 0x3ce
2c4: 84 d0 rcall .+264 ; 0x3ce
2c6: 8e d0 rcall .+284 ; 0x3e4
2c8: 08 95 ret
Mamy tutaj kilka funkcji. Jeśli przyjęlibyśmy, że funkcja spod 3b8
nadaje kropkę, a 3ce
kreskę, to składa się to w telegraficzne literki “J”, “K”, “L”, “M”, a z dużym prawdopodobieństwem 3e4
oznacza po prostu czekanie. Czas spojrzeć więc skąd wołane są w/w funkcje…
15a: cc d0 rcall .+408 ; 0x2f4
15c: c1 d0 rcall .+386 ; 0x2e0
15e: ec d0 rcall .+472 ; 0x338
160: aa d0 rcall .+340 ; 0x2b6
162: 98 d0 rcall .+304 ; 0x294
164: c2 d0 rcall .+388 ; 0x2ea
166: 3e d1 rcall .+636 ; 0x3e4
168: 9b d0 rcall .+310 ; 0x2a0
16a: af d0 rcall .+350 ; 0x2ca
16c: 08 d1 rcall .+528 ; 0x37e
16e: dd d0 rcall .+442 ; 0x32a
170: a2 d0 rcall .+324 ; 0x2b6
172: 96 d0 rcall .+300 ; 0x2a0
174: 37 d1 rcall .+622 ; 0x3e4
Pomijając funkcję pauzy (3e4
), mamy więc odwołania do adresów 2f4 2e0 338 2b6 294 2ea 2a0 2ca 37e 32a 2b6 2a0
. Użyjmy krótkiego skryptu w pythonie do zobaczenia, które funkcje są wywoływanie – nie będziemy tego sprawdzać ręcznie, w końcu to aż 12 znaków…
#!/usr/bin/python3
f = open('23cm.asm').read().splitlines()
for addr in ['2f4', '2e0', '338', '2b6', '294', '2ea', '2a0', '2ca', '37e', '32a', '2b6', '2a0']:
prin = False
out = ""
for l in f:
if l.startswith(f" {addr}") or prin:
prin = True
out += "-" if "0x3ce" in l else '.' if "0x3b8" in l else ""
if 'ret' in l:
prin = False
print(out)
Odpalmy:
➜ python3 parse.py
...
.-.
...--
.-..
....
-.--
.---
---
---..
..---
.-..
.---
I wszystko jasne – znaki układają się w ciąg SR3LHY JO82LJ. Analogicznie do poprzedniego przykładu z pasma 3 cm, należy teraz “tylko” podmienić adresy wywoływanych funkcji.
15a: cc d0 rcall .+408 ; 0x2f4
Skok pod adres 0x2f4
kodowany jest przez op-code “cc d0
“. Tak wygląda op-kod dla instrukcji rcall: (strona 137)
Cały trick polega na tym, że adres pod który skaczemy jest w instrukcji rcall
podawany relatywnie do naszej aktualnej pozycji w pamięci, czyli jeśli z 15a
chcemy skoczyć na 2f4
, to musimy skoczyć o 0x0cc
, czyli 204, razy dwa – 408 komórek pamięci.
Sprawia to, że podmiana wywołań nie jest trywialna, ale z wiedzą jak działa instrukcja rcall, nie jest też absolutnie trudna – wystarczy kalkulator i chwila cierpliwości. Po naniesieniu poprawek w pliku hex (i korekcie checksum na końcu każdej modyfikowanej linii) można taki plik wrzucić jeszcze raz do deasemblera (avr-objdump
) i upewnić się, że wołamy do odpowiednich miejsc. Finalnym potwierdzeniem było użycie avrdude
i uważna obserwacja diody na pudełku keyera 🙂
Dwa beacony ćwierkają już nowymi znakami, pozostał jeszcze model na 13 cm – już wkrótce!
73 / SQ3SWF