W poprzednim temacie przedstawiałem wnętrze routera Cyfrowego Polsatu LT-6408n. Określiłem, że jest zbudowany on na układzie RTL8196C, którego bootloader oferuje prostą linię komend wraz z możliwością wgranie dowolnego kodu maszynowego do pamięci RAM. Tutaj postaram się to wykorzystać - zacznę, od wgrywania prostych instrukcji napisanych ręcznie w notacji szesnastkowej, a potem uruchomię kompilator i rozwinę własny "flasher", zamieniający stary router w namiastkę Arduino.
Poprzedni temat z serii: Analiza wnętrza routera, zgrywanie pamięci, bootloader, własny program do UART
Kilka słów o architekturze RTL8196C
W poprzednim temacie łatwo było zauważyć, że omawiany router opiera się na RTL8196C. RTL8196C to bardzo popularny, wysoce zintegrowany układ typu SoC (System on a Chip) ze stajni Realteka, będący sercem wielu budżetowych urządzeń sieciowych na rynku. Pod maską kryje się tu sprawdzony, 32-bitowy rdzeń procesora w architekturze MIPS (taktowany zazwyczaj zegarem około 400 MHz). Jak przystało na dedykowany układ sieciowy, procesor ten jest obudowany szeregiem sprzętowych dodatków - znajdziemy tu m.in. wbudowany 5-portowy switch Fast Ethernet (10/100 Mbps), interfejs PCIe (często używany do modułów Wi-Fi), kontrolery pamięci SDRAM i SPI Flash, a także pakiet podstawowych peryferiów, takich jak piny GPIO czy kontroler UART. To właśnie dzięki temu ostatniemu interfejsowi i specyfice tutejszego bootloadera, będziemy mogli rozpocząć naszą zabawę w programowanie bez wylutowywania pamięci i korzystania z zewnętrznego programatora.
Sprawa jest o tyle ciekawa, że mamy też dokumentację rejestrów, choć przyznam, że... na początku pisania tego tematu o tym nie wiedziałem, eksperymentowałem i bez pełnej wiedzy.
Użyte komendy
Nie musimy na szczęście programować bezpośrednio pamięci Flash, tym razem mój sprawdzony CH341 może zostać w szufladzie. Dostępne komendy z bootloadera pozwalają wgrać program w pamięć RAM i stamtąd go wykonać. Należą do nich:
| Komenda | Składnia | Opis działania |
| EW | EW <adres> <słowo1> [słowo2] ... | Zapis 32-bitowych słów (word) bezpośrednio do pamięci pod wskazany adres. |
| DW | DW <adres> <ilość> | Zrzut 32-bitowych słów z pamięci – przydatne do weryfikacji wgrywania. |
| J | J <adres> | Skok (Jump) do podanego adresu – wyzwala wykonanie wgranego kodu. |
Plan wgrywania jest prosty - wysyłamy słowa do wybranego miejsca w pamięci przez EW, potwierdzamy zapis przez DW, i wykonujemy skok J.
Pierwszy program pisany bez kompilatora
Zaczynamy od kompletnego zera. Program trzeba napisać w kodzie maszynowym. Wiemy, że RTL opiera się o rdzeń MIPS RLX4181. Możemy więc użyć podstawowych instrukcji tego asemblera do napisania, dla przykładu, pętli wysyłającej znak wykrzyknika '!' do portu nadawczego UART. Jego adres udało mi się określić przeszukując kody źródłowego podobnych Realteków:
Znajduje się on pod adresem 0xB8002000. Wysłanie każdego znaku przerywane będzie pustą pętlą (delay), aby nie przepełnić bufora nadawania. Można pomóc sobie dokumentacją oraz narzędziami online, przykładowo konwerterem MIPS.
Sam kod sprowadza się do kilku prostych instrukcji asemblera: załadowania docelowego adresu UART, zapisania do niego pojedynczego bajtu (wykrzyknika w kodzie ASCII), odliczenia czasu w pustej pętli (aby sprzęt zdążył "wypchnąć" znak na zewnątrz) i ostatecznie - wykonania bezwarunkowego skoku z powrotem na początek.
W efekcie uzyskujemy poniższy program:
| Address | Hex | Instrukcja | Opis | 80500000 | 3C08B800 | lui $t0, 0xB800 | Załaduj górne 16 bitów adresu bazowego UART | 80500004 | 35082000 | ori $t0, $t0, 0x2000 | $t0 = 0xB8002000 (rejestr nadawczy UART THR) | 80500008 | 34090021 | ori $t1, $zero, 0x21 | $t1 = '!' (ASCII 0x21) | 8050000C | A1090000 | sb $t1, 0($t0) | Zapisz znak '!' do rejestru transmisji UART | 80500010 | 340A8000 | ori $t2, $zero, 0x8000 | $t2 = 32768 (licznik opóźnienia) | 80500014 | 1540FFFF | bne $t2, $zero, delay | Jeśli licznik ≠ 0, skocz z powrotem do pętli | 80500018 | 214AFFFF | addi $t2, $t2, -1 | Zmniejsz licznik o 1 (slot opóźnienia) | 8050001C | 08140002 | j 0x80500008 | Skok z powrotem do początku pętli | 80500020 | 00000000 | nop | Brak operacji (slot opóźnienia) |
Po przerwaniu bootloadera wgrywamy ten kod (szesnastkowo, słowo po słowie) używając polecenia EW:
EW 80500000 3C08B800 35082000 34090021 A1090000 340A8000 1540FFFF 214AFFFF 08140002
EW 80500020 00000000
Rezultaty można zweryfikować poleceniem DW 80500000 9.
Uruchamiamy kod skokiem:
J 80500000
Rezultatem jest natychmiastowe zalanie okna terminala nieskończonym ciągiem wykrzykników, "!!!!".
Proof of concept działa, ale kto by chciał pisać kod maszynowy ręcznie?
Kompilator i język C
Ręczne pisanie instrukcji MIPS i konwertowanie ich na kody szesnastkowe w hex konwerterze jest uciążliwe. Do tworzenia bardziej skomplikowanych programów zdecydowałem się użyć odpowiednio dobranego kompilatora, zgodnego z użytym procesorem - mips-linux-gnu-gcc. Pozwala to pisać programy w C. Nie udało mi się go uruchomić na Windowsie, więc posłużyłem się środowiskiem Linuxowym na "okienka" - WSL. Nie miałem żadnych nagłówków pod tego Realteka, ale są one mi zbędne. Zacząłem od najprostszego kodu, który chcę skompilować:
Kod: C / C++
Taki kod trzeba dalej skompilować. Uruchamiam mips-linux-gnu-gcc z następującymi parametrami:
- -march=mips32 - generuj kod dla architektury MIPS32 (zgodnej z rdzeniem RTL8196C)
- -mno-abicalls -fno-pic - wyłącz kod pozycyjnie niezależny (PIC) - program działa pod stałym adresem
- -nostdlib -nostartfiles - nie linkuj biblioteki standardowej C ani domyślnego kodu startowego
- -ffreestanding - nie zakładaj obecności standardowego środowiska wykonawczego
- -O2 - optymalizacja - mniejszy i wydajniejszy kod maszynowy
- -T rtl8196c.ld - użyj własnego skryptu linkera definiującego układ pamięci (opisany niżej)
Kompilator generuje plik ELF, z którego za pomocą 'mips-linux-gnu-objcopy -O binary' wyciągany jest surowy plik binarny - sam kod maszynowy, bez nagłówków, gotowy do załadowania wprost do pamięci RAM.
Kluczowym elementem jest skrypt linkera rtl8196c.ld, który informuje kompilator, pod jakim adresem w pamięci RAM docelowo znajdzie się nasz program:
Kod: C / C++
Skrypt kompilacji (przepraszam, za formę obrazkową, ale silnik forum nie pozwala mi wkleić tych komend...):
Drugim etapem jest eksport komend z pliku binarnego do ładnych napisów, które można przekleić do Realteka. Normalnie się tego nie robi, więc posłużyłem się własnym skryptem Python:
Kod: Python
Wygenerowane komendy:
EW A0500000 3C05A050 24A50050 3C04B800 3C06001E 24020048 00A01825 304200FF 24630001
EW A0500020 A0822000 80620000 1440FFFC 304200FF 34C28480 00000000 2442FFFF 1440FFFD
EW A0500040 00000000 1000FFF3 24020048 00000000 4865792E 2E0D0A00 00000000 00000000
DW A0500000 24
J A0500000
Rezultat:
Program działa, choć tym razem wyszło nieco gorzej - znaki się gubią, chyba moja pętla oczekująca na zwolnienie UART nie zadziałała.
Własne narzędzie do wgrywania wsadu
Instrukcje już generują się same, trzeba je jeszcze automatycznie wgrywać. Tryb linii komend włącza wciśnięcie ESCAPE w trakcie bootowania, przecież to może robić program. Komendy też może wysyłać program. Musimy tylko określić, kiedy startuje router - można przechwycić jego komunikat startowy. Potem można kolejno przesyłać poszczególne komendy 'EW', weryfikując je poleceniem 'DW'. Taki właśnie proces realizuje udoskonalony przeze mnie skrypt w języku Python ('uart_auto_flash.py'). Działa on w prostej pętli: nasłuchuje komunikatu "Booting", automatycznie wciska klawisz ESCAPE, aby wejść do linii komend a następnie wysyła do konsoli paczki bajtów pobrane prosto ze skompilowanego pliku binarnego ('.bin'), weryfikując poprawność operacji po każdym bloku komendą 'DW'. Pozwala to całkowicie zautomatyzować proces bez uciążliwego kopiowania i wklejania instrukcji w oknie terminala.
Brakowało mi jeszcze automatycznego rebootowania. Docelowo reboot może robić pin DTR/RTS od odpowiedniego konwertera USB na UART, ale na razie takiego nie miałem takiego pod ręką, więc użyłem... gniazdka z oprogramowaniem Tasmota. Steruję nim przez klasyczne REST API dla komend, OpenBeken też trzyma ten standard.
W ten sposób wgranie testowanego programu na router sprowadza się już tylko do wywołania jednej komendy:
python ..\uart_auto_flash.py hello_world.bin --tasmota 192.168.0.176
Obsługa GPIO i niezbędne poprawki
Kolejna rzeczą którą próbowałem uruchomić jest klasyczne miganie diodą LED. Diody od statusu portów LAN są podłączone bezpośrednio do pinów Realteka, więc jest potencjał, by je wysterować. Opisane są jako, przykładowo, LED_PORT1/GPIOB3, co sugeruje dwojaką ich rolę - albo przejmowaną przez sterownik Ethernetowy, albo tryb generycznego, programowalnego IO.
Dodatkowo po drodze natrafiłem na kilka problemów - program zawieszał się po chwili, więc próbowałem ręcznie zainicjować wskaźnik stosu. Dodatkowo wyłączyłem przerwania. GPIO również na początku nie reagowało na sterowanie, ale to dlatego, że domyślnie był włączony tam kontroler sprzętowego switcha. Musiałem go ręcznie wyłączyć w kodzie.
Oto ostateczny, działający kod sterujący diodami:
Kod: C / C++
Rezultat:
Odliczanie na UART i finalizacja
Następne co chciałem uruchomić to wyświetlanie liczb przez UART. Na początek coś prostego, jakieś odliczanie. Zdecydowałem się na prosty sposób zamiany typu integer na napis:
Kod: C / C++
Niespodziewanie jednak potem program przestał działać. Zorientowałem się, że zapomniałem w linkerze wymusić pozycji funkcji startowej, na skutek czego bootloader Realteka skakał nie tam, gdzie trzeba. Musiałem do linkera, na pierwszy miejscu, dodać wpis: z entry a potem uwzględnić ten identyfikator w kodzie C:
Kod: C / C++
Reszta kodu zgodnie z oczekiwaniami - teraz główna pętla ma licznik i wyświetla go z każdą wiadomością. Nie ma funkcji sprintf, osobno wyświetlam statyczny napis, a osobno przekonwertowaną na napis liczbę.
Kod: C / C++
Rezultat:
Podsumowanie
W ten sposób udało mi się zorganizować podstawowe środowisko pracy dla RTL8196C. Eksperymenty zacząłem od prostych instrukcji, które można pisać ręcznie lub wspomagać się odpowiednim narzędziem online, potem znalazłem pasujący kompilator i od 0 przygotowałem dla niego własny skrypt linkera i kod startowy C. Rozwiązałem problemy z niepożądanymi przerwaniami, stosem oraz multipleksowaniem pinów, a cały proces flashowania zautomatyzowałem w Pythonie. Tym samym przekształciłem stary router w wygodną platformę deweloperską, na której mogę odpalać własny kod maszynowy. Zapraszam do dyskusji - a może ktoś z Was próbował podobnych zabaw z innymi SoC?
PS: Nie jest to ostatni temat z tej serii, a w całości mojego workflow wciąż są problemy do rozwiązania - próbowałem zrobić prostą konsolę na ten router i nieznanych przyczyn program stawał po przetworzeniu kilku znaków. Jak to rozwiążę, to pokażę bardziej złożony projekt oparty na tym Realteku.
Fajne? Ranking DIY Pomogłem? Kup mi kawę.