Serwer WWW z elementami grafiki 3D – praktyczne wykorzystanie pakietów Node.js oraz Three.js w systemach wbudowanych (część 2)
W drugiej części artykułu kontynuujemy tematykę praktycznego wykorzystania pakietów Node.js oraz Three.js w urządzeniach wbudowanych pracujących pod kontrolą system operacyjnego Linux. Dotychczas zostały omówione zagadnienia instalacji frameworku Node.js, przygotowania prostego serwera WWW z podziałem na funkcje front-end/back-end, komunikacji z wykorzystaniem socket.io, a także tworzenia i odczytu danych z procesów potomnych. W niniejszym artykule, na przykładzie modułu KAmodL3GD20, zaimplementujemy prostą obsługę żyroskopu w przestrzeni użytkownika z wykorzystaniem interfejsu i2c-dev, a następnie rozbudujemy projekt graficzny strony WWW o elementy grafiki 3D, która w postaci sześciennej kostki będzie odwzorowywała ruch podłączonego modułu żyroskopowego.
Moduł KAmodL3GD20 – konfiguracja jądra Linux i obsługa programowa
W poprzedniej części artykułu, na potrzeby testów odczytu danych z procesu potomnego, przygotowano prosty kod gryo-i2c.c, którego zadaniem było generowanie losowych wartości dla mierzonego kąta obrotu w osiach X, Y oraz Z. W tym podrozdziale zaimplementujemy właściwą obsługę układu żyroskopowego L3GD20 firmy STMicroelectronics, zamontowanego na wygodnej do użycia płytce prototypowej KAmodL3GD20 – rysunek 1.
Rys. 1. Moduł żyroskopowy KAmodL3GD20 (źródło: kamami.pl)
Wykorzystany do praktycznej realizacji projektu procesor i.MX6 ULL z rdzeniem Cortex-A7 został wyposażony w cztery sprzętowe kontrolery magistrali I2C. Konfigurację jądra systemu Linux rozpoczynamy więc od wywołania narzędzia menuconfig, a następnie włączenia sterowników dla sprzętowego kontrolera magistrali:
Device Drivers ---> [*] I2C support ---> [*] I2C Hardware Bus Support ---> <*> IMX I2C interface < > GPIO-based bitbanging I2C
Do komunikacji z układem L3GD20 wykorzystany zostanie interfejs i2c-dev, który umożliwia uzyskanie dostępu do magistrali z poziomu przestrzeni użytkownika poprzez pliki specjalne urządzeń /dev/i2c-x (gdzie wartość x oznacza kolejny numer porządkowy interfejsu I2C). Interfejs ten, poprzez wygodne API, umożliwia przygotowanie obsługi urządzenia peryferyjnego bezpośrednio w przestrzeni użytkownika – z pominięciem dedykowanych sterowników w jądrze systemu. Rozwiązanie to jest praktyczne na etapie wczesnego projektowania obsługi sprzętu (brak potrzeby rekompilacji jądra lub modułu) lub w sytuacji gdy sterownik wybranego urządzenia peryferyjnego nie został zaimplementowany w systemie. Włączenie interfejsu i2c-dev w jądrze systemu:
Device Drivers ---> [*] I2C support ---> <*> I2C device interface
Po zakończonym procesie konfiguracji jądra, niezbędna jest również edycja opisu Device Tree, w którym to aktywujemy wybrany kontroler I2C – poprzez edycję pola status – oraz dokonamy konfiguracji funkcji alternatywnych dla wybranych wyprowadzeń procesora. W prezentowanym projekcie wybrano wyprowadzenia numer 3 i 5 gniazda J504 (umiejscowionego na płycie bazowej VisionCB-STD dla modułów VisionSOM), które mogą pełnić alternatywną funkcję linii SDA i SCL dla kontrolera I2C2 (domyślnie linie te są sygnałami TX oraz RX kontrolera UART5). Pełny schemat połączeń przedstawiono na rysunku 2.
Rys. 2. Schemat połączeń modułu KAmodL3GD20 z płytą bazową VisionCB-STD
Bazując na schemacie połączeń z rysunku 2, dokonajmy edycji opisu Device Tree w którym to aktywujemy kontroler magistrali I2C2:
&i2c2 { clock_frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; status = "okay"; };
oraz wyprowadzeniom MX6UL_PAD_UART5_TX_DATA i MX6UL_PAD_UART5_RX_DATA przypiszemy alternatywne funkcje linii SCL oraz SDA:
&iomuxc { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1>; imx6ul-evk { pinctrl_i2c2: i2c2grp { fsl,pins = < MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0 MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0 >; }; }; };
Po zakończonej konfiguracji jądra oraz edycji plików Device Tree, można przystąpić do kompilacji i zaktualizowania obrazu systemu (patrz ramka poniżej).
Po ponownym uruchomieniu komputera, poprawność konfiguracji i kompilacji jądra systemu możemy sprawdzić poprzez wyświetlenie listy dostępnych kontrolerów I2C w katalogu /dev:
root@somlabs:~# ls -l /dev/ | grep i2c crw-rw---- 1 root i2c 89, 1 May 25 18:36 i2c-1
Różnica w numeracji kontrolerów (w pliku Device Tree aktywowano kontroler I2C2, natomiast w katalogu /dev odnajdujemy kontroler i2c-1) wynika z różnic indeksowania – jądro systemu stosuje numerację zaczynającą się od wartości 0. Poprawnie skonfigurowany system pozwala na przejście do kolejnego etapu – weryfikacji połączeń sprzętowych. Jedną z najszybszych metod na nawiązanie komunikacji z podłączonym do magistrali sprzętem jest wykorzystanie pakietu i2c-tools w skład którego wchodzą między innymi takie narzędzia jak i2cdetect, i2cset oraz i2cget. Instalacja pakietu i2c-tools w dystrybucji Debian, odbywa się w sposób standardowy dla narzędzia apt-get:
root@somlabs:~# apt-get install i2c-tools Reading package lists... Done Building dependency tree Reading state information... Done Suggested packages: libi2c-dev python-smbus The following NEW packages will be installed: i2c-tools 0 upgraded, 1 newly installed, 0 to remove and 39 not upgraded. Need to get 0 B/57.6 kB of archives. After this operation, 196 kB of additional disk space will be used.
Program i2cdetect umożliwia przeskanowanie wybranej magistrali I2C, wskazanej poprzez parametr -y. Wynikiem działania polecenia jest tablica adresów wraz z listą dostępnych na magistrali urządzeń:
root@somlabs:~# i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- UU -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- 6b -- -- -- -- 70: -- -- -- -- -- -- -- --
Polecenie i2cdetect poprawnie wykryło podłączony do magistrali układ L3GD20 o przypisanym przez producenta adresie 0x6B (wykryte jednocześnie urządzenie o adresie 0x38 z oznaczeniem UU, jest obsługiwane przez sterownik w jądrze i nie jest dostępne w przestrzeni użytkownika). W ramach dodatkowego testu, korzystając z narzędzia i2cget, można wykonać odczyt zawartości rejestru WHO_AM_I układu L3GD20 – rejestr ten przechowuje ustalony przez producenta numer identyfikacyjny układu – zestawienie wybranych rejestrów żyroskopu L3GD20 przedstawiono na rysunku 3.
Rys. 3. Zestaw wybranych rejestrów układu L3GD20
Polecenie i2cget pozwala na odczyt danych z wybranego układu podrzędnego i określonego rejestru. Pierwszym parametrem polecenia jest numer porządkowy magistrali, następnie adres układu podrzędnego, numer rejestru spod którego będziemy wykonywać odczyt oraz rozmiar odczytywanej danej (domyślnie jeden bajt). Odczyt zawartości rejestru 0x0F (WHO_AM_I) z układu o adresie 0x6B podłączonego do magistrali i2c_1, może zostać realizowany następująco:
root@somlabs:~# i2cget -y 1 0x6b 0x0F 0xd4
Odczytana wartość 0xD4 jest zgodna z wartością ustaloną przez producenta, tak więc komunikacja z układem L3GD20 przebiega prawidłowo – możemy przystąpić do implementacji obsługi żyroskopu na potrzeby programu i2c-gyro.
Funkcję main() z pliku gyro-i2c.c rozpoczynamy od otworzenia pliku urządzenia /dev/i2c-1, a następnie za pomocą wywołania ioctl (fd, I2C_SLAVE, adres), ustawienia adresu urządzenia peryferyjnego z którym będzie realizowana dalsza wymiana komunikatów – listing 1.
#define GYRO_ADDR 0x6b int main (void) { int i2c_fd, ret; /* open i2c device */ i2c_fd = open ("/dev/i2c-1", O_RDWR); if (i2c_fd < 0) { printf ("Failed to open the i2c bus\n"); return EXIT_FAILURE; } /* set slave address */ ret = ioctl (i2c_fd, I2C_SLAVE, GYRO_ADDR); if (ret < 0) { printf ("Failed to acquire bus access and/or talk to slave\n"); goto exit; } /* … */ }
Listing 1. Otworzenie pliku urządzenia /dev/i2c-1 oraz ustawienie adresu I2C_SLAV
W następnej kolejności, poprzez zapis rejestrów sterujących CTRL_REG[x], wykonamy inicjalizację układu L3GD20. Do tego celu przygotowano funkcję gyro_init(), której zadaniem jest wysłanie na magistralę 6. bajtów danych – adresu rejestru CTRL_REG1 oraz kolejnych wartości wpisywanych odpowiednio do rejestrów CTRL_REG1…CTRL_REG5. W celu optymalizacji funkcji gyro_init() do postaci pojedynczego wywołania write(), wykorzystano wbudowany w układ mechanizm automatycznego zwiększania wartości adresu po każdym zapisie bajtu danych – włączenie tej funkcji wymaga ustawienia najstarszego bitu w adresie rejestru. W ramach funkcji inicjalizacji włączono pomiary w osiach X, Y oraz Z, ustawiono układ do pracy w trybie odpytywania (wymagający sprawdzenia bitu gotowości w rejestrze STATUS_REG), ustawiono zakres pomiarowy na wartość 250 dps oraz blokową aktualizację danych (wartości pomiarowe dla osi X, Y i Z przechowywane są w postaci 16-bitowej – włączenie funkcji Block Data Update zapewnia, że dane przechowywane w starszej i młodszej części rejestru pochodzą z jednej próbki pomiaru). Kod funkcji gyro_init() został przedstawiony na listingu 2.
#define AUTO_INCREMENT 0x80 static int gyro_init (int i2c_fd) { unsigned char init_seq[6]; init_seq[0] = (CTRL_REG1 | AUTO_INCREMENT); init_seq[1] = 0xCF; /* CTRL_REG1: normal mode, xyz enable */ init_seq[2] = 0x01; /* CTRL_REG2: <default value> */ init_seq[3] = 0x00; /* CTRL_REG3: <default value> */ init_seq[4] = 0x80; /* CTRL_REG4: 250dps, Block Data Update */ init_seq[5] = 0x02; /* CTRL_REG5: <default value> */ if (write (i2c_fd, init_seq, 6) != 6) return -1; return 0; }
Listing 2. Inicjalizacja układu L3GD20 poprzez zapis rejestrów CTRL_REG[X]