Kursy • Poradniki • Inspirujące DIY • Forum
Interfejs I2C
I2C jest jednym z najpopularniejszych standardów komunikacyjnych występującym w świecie układów scalonych. Dzięki niemu jesteśmy w stanie przesyłać informacje cyfrowo.
I²C – szeregowa, dwukierunkowa magistrala służąca do przesyłania danych w urządzeniach elektronicznych. Została opracowana przez przedsiębiorstwo Philips na początku lat 80. Znana również pod akronimem IIC, którego ang. rozwinięcie Inter-Integrated Circuit oznacza „pośredniczący pomiędzy układami scalonymi”.
Interfejsy cyfrowe - co zyskujemy?
Po pierwsze, dane przesyłane analogowo narażone są na zakłócenia. Urządzenie odbiorcze musi być wyposażone w przetwornik analogowo-cyfrowy, który będzie w stanie odczytać przesyłaną informację. W urządzeniach komunikujących się za pomocą interfejsów cyfrowych pomiary, filtracja i przekształcenia matematyczne odbywają się wewnątrz urządzenia, a my otrzymujemy gotowy wynik w postaci liczby.
Po drugie, każda informacja, którą chcemy przesyłać analogowo wymaga osobnej linii danych. Generuje to bardzo duże ograniczenie, związane z wielkością układu scalonego, ilością wyjść i miejscem na płytce. Wykorzystując interfejs cyfrowy ten problem znika prawie całkowicie (ogranicza nas jedynie prędkość transmisji danych).
Po trzecie, daje to możliwość przesyłania wielu różnych informacji nie tylko z czujnika do mikrokontrolera, ale również w drugą stronę.
Dzięki temu jesteśmy w stanie konfigurować parametry pracy
układu, z którym się komunikujemy.
Warstwa fizyczna I2C
I2C do komunikacji wykorzystuje tylko dwie linie transmisyjne.
- SDA (Serial Data Line) - linia danych.
- SCL (Serial Clock Line) - linia zegara.
Linie są dwukierunkowe, co umożliwia transmisję Half-Duplexową (obydwa urządzenia mogą nadawać i odbierać dane, ale nie jednocześnie). Linie są na stałe podciągnięte do źródła zasilania przez rezystory podciągające.
Oczywiście do poprawnej pracy niezbędne jest również połączenie mas obydwu układów, więc w praktyce do transmisji danych potrzebne są 3 linie.
Dodatkową zaletą I2C jest możliwość podłączenia wielu urządzeń do magistrali. W opracowanym w latach 80 standardzie było to dokładnie 112 układów (7 bitowa przestrzeń adresowa pomniejszona o adresy zarezerwowane). W nowej wersji wartość ta wzrosła do nieco powyżej tysiąca urządzeń (10 bitowa przestrzeń adresowa).
Restrykcją w tej kwestii jest również pojemność na liniach magistrali,
która nie może przekroczyć 400pF.
Sposób działania I2C
W komunikacji I2C wyróżnia się dwa typy urządzeń - master i slave. Urządzenie master generuje sygnał zegarowy i inicjuje komunikację z urządzeniami slave, które odpowiadają na zapytania otrzymywane od mastera.
Protokół komunikacyjny
Istnieją cztery potencjalne tryby działania urządzeń podłączonych do magistrali I2C:
- master transmit - urządzenie master wysyła dane do urządzenia slave,
- master receive - urządzenie master odbiera dane nadawane przez urządzenie slave,
- slave transmit - urządzenie slave wysyła dane do urządzenia master,
- slave receive - urządzenie slave odbiera dane od nadawane przez urządzenie master.
Domyślnie master jest w trybie master transmit i tylko ten układ może rozpocząć komunikację.
Do magistrali I2C może być podłączone więcej niż jedno urządzenie typu master.
Istnieją również 4 podstawowe typy transmisji:
- urządzenie master przesyłające jeden bajt do urządzenia slave,
- urządzenie master przesyłające wiele bajtów do urządzenia slave,
- urządzenie master odczytujące jeden bajt z urządzenia slave,
- urządzenie master odczytujące wiele bajtów z urządzenia slave.
Aby lepiej zrozumieć co będziemy potem implementować w programie, przyjrzyjmy się jak wygląda pierwszy z nich i jak wygląda podstawowa ramka danych w protokole I2C.
Master przesyłający jeden bajt do slave - schemat
- Transmisja rozpoczyna się od wysłania bitu startu przez urządzenie master (ST - Start).
- Następnie urządzenie master nadaje siedmiobitowy adres urządzenia, do którego ma trafić wiadomość (SAD - Slave Address). Na końcu adresu urządzenia (SAD) dodawany jest ósmy bit, który w zależności od swojego stanu komunikuje chęć dalszego nadawania bądź odbierania danych przez urządzenie master(+W - Write).
- Po odebraniu swojego adresu, urządzenie slave wysyła bit potwierdzenia (SAK - slave acknowledge).
- Następnie urządzenie master transmituje ośmiobitowy adres rejestru znajdującego się w urządzeniu slave, do którego mają trafić dane (SUB - sub-address).
- Po odebraniu adresu rejestru wewnętrznego, urządzenie slave ponownie potwierdza odbiór bitem SAK.
- W końcu urządzenie master transmituje bajt danych (DATA), który trafia do wcześniej sprecyzowanego rejestru (SUB).
- Ponowne potwierdzenie ze strony urządzenia slave.
- Urządzenia master zakańcza transmisję wysyłając bit stopu (SP - Stop).
Jak widzimy, schemat jest nieco bardziej skomplikowany niż w przypadku UARTa, lecz pozwala na wygodną komunikację pomiędzy pamięciami układów scalonych. Tyle teorii wystarczy nam, aby zrozumieć fundamentalne założenia interfejsu I2C. Pora na praktykę!
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw elementów do przeprowadzenia wszystkich ćwiczeń z kursu STM32 F4 można nabyć u naszego dystrybutora! Zestaw zawiera m.in. płytkę Discovery, wyświetlacz OLED, joystick oraz enkoder.
Zamów w Botland.com.pl »Akcelerometr
Akcelerometr jest urządzeniem mierzącym przyspieszenie liniowe wzdłuż konkretnej osi (lub wielu osi). W stanie spoczynku będzie on wskazywał przyspieszenie o wartości 1g wzdłuż osi Z, wynikające z przyspieszenia ziemskiego. Dzięki temu, za pomocą prostych przekształceń można określić orientację akcelerometru w przestrzeni względem wektora grawitacji.
Poza mierzeniem przyspieszenia ziemskiego, akcelerometr pozwala na wykrywanie wszelkich innych przyspieszeń, takich jak np. zmiany prędkości ruchu pojazdu, swobodny spadek, drgania.
Akcelerometrów używa się w zastosowaniach takich jak:
- stabilizacja lotu obiektów latających takich jak samoloty, helikoptery, rakiety,
- obracanie obrazu w telefonach i tabletach tak, aby zawsze był skierowany w górę,
- wykrywanie drgań w obiektach mechanicznych, budynkach,
- pomiar przyspieszenie pojazdów,
- aktywności sportowe - chodzenie, bieganie, skakanie etc.
Układ scalony LSM303DLHC
Na dużej części płytek serii Discovery umieszczono układ scalony LSM303DLHC, zawierający w sobie 3 osiowy akcelerometr i 3 osiowy magnetometr. Płytka STM32F411DISCOVERY, na której pracujemy w ramach tego kursu również jest w niego wyposażona.
Najważniejsze parametry układu to:
- pomiar przyspieszenia w zakresie ±2g/±4g/±8g/±16g,
- pomiar pola magnetycznego w zakresie od ±1.3 do ±8.1 gaussa,
- szesnastobitowy pomiar wartości,
- dopuszczalne zasilanie w zakresie 2.16 - 3.6 V
Więcej podstawowych informacji na temat LSM303DLHC
można znaleźć na pierwszej stronie dokumentacji.
Konfiguracja I2C w Cube
Krok 1. Tworzymy nowy projekt i konfigurujemy zegary jak było to omówione wcześniej.
Krok 2. Sprawdzamy w schemacie płytki Discovery, do których linii mikrokontrolera podłączony jest układ LSM303.
Ze schematu wynika, że linia zegara SCL podłączona jest do wyprowadzenia PB6, a linia danych SDA do wyprowadzenia PB9.
Na schemacie widać, że do mikrokontrolera podłączonych jest jeszcze kilka dodatkowych linii. Wyprowadzenia INT1 i INT2 układu LSM303 można skonfigurować tak, aby generowały przerwania po każdym zakończonym pomiarze. Dzięki temu możemy dokonywać odczytu tylko wtedy, gdy w układzie pojawiły się nowe dane.
Krok 3. Konfigurujemy linie PB6 i PB9 w odpowiednich trybach.
Krok 4. Widzimy, że linie podświetlone są na żółto. Aby w pełni włączyć peryferium I2C1 należy to sprecyzować w panelu z lewej strony.
Krok 5. Przechodzimy do panelu konfiguracyjnego I2C1.
Sekcja Master Features zawiera parametry interfejsu I2C, które dotyczą sytuacji, w których nasze urządzenie działa w trybie master.
- I2C Speed Mode - pozwala na wybranie jednego ze standardów określających częstotliwość linii zegarowej I2C. Dostępne prędkości to:
- Standard Mode - oryginalna częstotliwość z początków lat 80, która wynosi 100 kHz
- Fast Mode - 400kHz
- High-speed mode - 3.4 MHz
- Fast-mode plus (FM+) - 1MHz
- Ultra Fast-mode (UFm) - 5MHz
Wszystkie peryferia I2C znajdujące się na wyposażeniu naszego mikrokontrolera mogą prowadzić transmisję tylko w standardzie Standard Mode oraz Fast Mode. Ponieważ układ LSM303DLHC również obsługuje Fast Mode, możemy sobie pozwolić na konfigurację właśnie tego trybu. Po wybraniu Fast Mode pojawi się nowy parametr, który należy pozostawić na wartości domyślnej.
- I2C Clock Speed (Hz) - pokazuje obecnie ustawioną częstotliwość linii zegarowej.
Sekcja Slave features zupełnie nas nie interesuje, ponieważ mikrokontroler w przedstawionych tu zastosowaniach zawszę będzie urządzeniem typu master. Finalnie ustawione parametry powinny prezentować się następująco.
Krok 6. Generujemy projekt pod nazwą 06_I2C.
Uruchomienie i odczytywanie danych z akcelerometru
Jesteśmy już gotowi do rozpoczęcia komunikacji z naszym urządzeniem. Musimy jednak wiedzieć co i w jaki sposób wysyłać, aby układ odpowiedział nam pożądanymi danymi. W tym celu zagłębimy się nieco w dokumentację układu LSM303.
Adresy urządzeń I2C
Pierwszą rzeczą jakiej potrzebujemy w każdej komunikacji I2C jest adres główny urządzenia (SAD). Na stronie 21 dokumentacji znajdziemy następujący fragment:
Jak już wcześniej wspomniałem, adresy urządzeń są zazwyczaj 7 bitową liczbą, która w tym wypadku wynosi 0011001. W tabeli 14 widzimy, że aby zapisywać dane do modułu na końcu adresu należy dodać bit zerowy. W celu odczytywania danych ósmy bit musi być jedynką.
W bibliotekach HAL dodanie bitu zapisu/odczytu odbywa się automatycznie!
Oznacza to, że niezależnie od tego co sami tam wpiszemy, biblioteka sama ustawi odpowiednio ostatni bit zgodnie z wywołaną funkcją. My musimy tylko zadbać o to, żeby pierwsze 7 bitów zawierało poprawny adres. Jeżeli podamy do funkcji adres o wartości 00110011, zostanie on zamieniony na 0011001x i w miejsce x wstawiony zostanie odpowiednią wartość, zgodnie z wywołaną funkcją.
Problemy z adresowaniem w I2C
W tym miejscu pozwolę sobie poświęcić nieco miejsca na dywagację na temat adresów, ponieważ potrafią one sprawić niemało problemów. Większość z nich sprowadza się do przedstawionego scenariusza.
Z dokumentacji wyczytujemy, że adres naszego urządzenia to 0011001. Szybko wrzucamy tę liczbę do konwertera BIN->HEX (konwerter liczb binarnych na heksadecymalne) i otrzymujemy wartość 0x19. Umieszczamy ją w programie i próbujemy przesłać dane do urządzenia.
Niestety próba połączenia się z urządzeniem
pod tym adresem zakończy się niepowodzeniem.
Dlaczego?
Pamiętajmy, że każda liczba przedstawiona w ten sposób w systemie ma 8 bitów. My podaliśmy tu tylko 7, dlatego musiało nastąpić niejawne dodanie jednego bitu z którejś ze stron. Na nasze nieszczęście zawsze będzie to dodanie zera z lewej strony, więc nasz adres zostaje odczytany jako 00011001, a więc w praktyce 0001100x. A przecież powinno to być 0011001x, gdzie x oznaczać będzie bit zapisu/odczytu!
Rozwiązanie:
Aby uniknąć nieporozumień z systemem, zawsze należy podać adres zapisany przez nas jawnie na 8 bitach. Możemy to zrobić na kilka sposobów. Najpopularniejszym z nich jest dokonanie przesunięcia bitowego w lewo na wartości, którą otrzymaliśmy po konwersji 7 bitowego adresu.
// rejestry
#define LSM303_ACC_ADDRESS (0x19 << 1) // adres akcelerometru: 0011 001x
Taką właśnie linijkę wpisujemy do naszego programu (np. w sekcji private variables).
Uruchomienie akcelerometru
Następnie musimy sprawdzić czy akcelerometr jest domyślnie włączony, czy może trzeba go uruchomić programowo. Informacje na ten temat znajdziemy w opisie rejestru Control Register 1 na stronie 25 dokumentacji:
Widzimy, że rejestr składa się z 3 głównych części:
- Bity [7:4] - ODR [3:0] - odpowiadają za parametr ODR (Octal Data Rate). Jest to po prostu częstotliwość dokonywania pomiarów. W tabeli 19 możemy znaleźć informację, że domyślna wartość to 0000. Sprawdzając co oznacza ta wartość w tabeli 20 dowiadujemy, się że domyślnie akcelerometr jest w trybie Power-down Mode, a więc domyślnie nie dokonuje pomiarów.
- Bit [3] - LPEN - pozwala na uruchomienie trybu niskiego poboru mocy. Domyślnie ten tryb jest wyłączony.
- Bity [2:0] - ZEN (Z axis enable), YEN, XEN - włączenie lub wyłączenie pomiarów na poszczególnych osiach. Domyślnie wszystkie osie są włączone.
Po krótkiej analizie przedstawionej wyżej strony dokumentacji wiemy już, że w celu uruchomienia dokonywania pomiarów trzeba wpisać odpowiednią wartość do rejestru CTRL_REG1_A. Dodajemy więc do naszego programu jego adres, aby później wiedzieć w które miejsce wpisywać dane.
#define LSM303_ACC_CTRL_REG1_A 0x20 // rejestr ustawien 1
Następnie musimy stworzyć maski bitowe odpowiadające konkretnym ustawieniom. Na początek chcemy odczytywać dane tylko z osi Z, z częstotliwością 100Hz. W tym celu potrzebne nam będą następujące maski.
// CTRL_REG1_A = [ODR3][ODR2][ODR1][ODR0][LPEN][ZEN][YEN][XEN]
#define LSM303_ACC_Z_ENABLE 0x07 // 0000 0100
#define LSM303_ACC_100HZ 0x50 // 0101 0000
Teraz potrzebujemy zmiennej, do której wpiszemy utworzoną przez nas konfigurację.
// wypelnienie zmiennej konfiguracyjnej odpowiednimi opcjami
uint8_t Settings = LSM303_ACC_Z_ENABLE | LSM303_ACC_100HZ;
Znak "|" oznacza sumę bitową. Dzięki temu zmienna Settings powinna w wyniku zawierać wartość:
0000 0100 | 0101 0000 = 01010100.
Następnie musimy wpisać stworzone ustawienia do odpowiedniego rejestru. W tym celu posłuży nam funkcja HAL_I2C_Mem_Write.
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
Przyjmuje ona sześć parametrów. Przeanalizujmy po kolei każdy z nich.
- I2C_HandleTypeDef *hi2C - wskaźnik na strukturę konfiguracyjną I2C.
- uint16_t DevAddress - adres urządzenia, do którego mają zostać wysłane dane.
- uint16_t MemAddSize - rozmiar rejestru pamięci (w bajtach).
- uint8_t *pData - wskaźnik na pamięć, która zawiera dane do przesłania.
- uint16_t Size - liczba bajtów danych do przesłania.
- uint32_t Timeout - czas, który I2C może przeznaczyć na oczekiwanie na poprawne zakończenie komunikacji (w milisekundach).
W naszym wypadku poprawnie wypełniona funkcja będzie wyglądać następująco:
HAL_I2C_Mem_Write(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_CTRL_REG1_A, 1, &Settings, 1, 100);
Tym sposobem uruchomiliśmy pomiary akcelerometru w osi Z z częstotliwością 100Hz.
Odczytywanie danych z akcelerometru
Teraz potrzebne są nam informacje dotyczące tego skąd możemy odczytać dane. Zaglądając do zestawienia wszystkich rejestrów układu LSM303DLHC na stronie 23 dokumentacji bardzo szybko odnajdziemy interesujące nas rejestry.
Widzimy, że rejestrów danych jest aż 6, po dwa na każdą oś. Jest tak, ponieważ rejestry układu są ośmiobitowe, a dane z każdej osi zapisane są na 16 bitach. Litera L w nazwie rejestru oznacza Low, a więc młodszy (niższy) bajt całej wartości (mniej ważny). Litera H (High) oznacza bajt starszy (wyższy/ważniejszy).
Skąd biorą się określenia starszy czy ważniejszy? Przypomnijmy sobie jak zapisywana jest liczba szesnastobitowa, konkretnie typ uint16_t.
Widzimy, że bajt starszy składa się z bitów o wyższych wartościach, a więc [15:8]. Na nim to zapisywana jest główna składowa wartości całkowitej. Ma więc on dużo większą wagę w kontekście całej liczby, dlatego jest "ważniejszy". Jeżeli liczba jest typu signed (ze znakiem - np. int16_t), najstarszy bit informuje o znaku ± całej liczby.
Na początek wystarczy nam odczyt jednego bajtu, a więc ważniejszej części danych. W tym celu potrzebujemy dwóch zmiennych. Do jednej będziemy wpisywać bezpośrednio odczytany bajt danych z akcelerometru. Druga będzie służyła do reprezentacji faktycznej wartości (po przesunięciu bitowym oraz zamianie na liczbę ze znakiem).
uint8_t Data = 0; // Zmienna do bezposredniego odczytu z akcelerometru
int16_t Zaxis = 0; // Zawiera przeksztalcona forme odczytanych danych
Do odczytu danych z akcelerometru użyjemy teraz funkcji HAL_I2C_Mem_Read. Budowa jest taka sama jak w przypadku funkcji Write, z jedną drobną różnicą. Tym razem parametr 4 to wskaźnik na obszar pamięci, do którego zostaną wpisane pobrane z urządzenia dane (a nie jak poprzednio, obszar z którego dane zostaną pobrane).
Wywołanie z odpowiednimi parametrami przedstawiono na poniższym listingu.
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_Z_H_A, 1, &Data, 1, 100);
Ostatnią rzeczą którą musimy zrobić jest przepisanie pobranej wartości do zmiennej zawierającej wartość właściwą. Pamiętamy, że pobraliśmy starszy bajt danych, dlatego przy wpisywaniu do liczby typu int16_t musimy przesunąć go na odpowiednią pozycję, a więc 8 bitów w lewo.
Zaxis = Data << 8;
Świetnie! Pełny kod powinien wyglądać jak na poniższych dwóch listingach.
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
//Rejestry
#define LSM303_ACC_ADDRESS (0x19 << 1) // adres akcelerometru: 0011001x
#define LSM303_ACC_CTRL_REG1_A 0x20 // rejestr ustawien 1
#define LSM303_ACC_Z_H_A 0x2D // wyzszy bajt danych osi Z
// Maski bitowe
// CTRL_REG1_A = [ODR3][ODR2][ODR1][ODR0][LPEN][ZEN][YEN][XEN]
#define LSM303_ACC_Z_ENABLE 0x07 // 0000 0100
#define LSM303_ACC_100HZ 0x50 //0101 0000
// Zmienne
uint8_t Data = 0; // Zmienna do bezposredniego odczytu z akcelerometru
int16_t Zaxis = 0; // Zawiera przeksztalcona forme odczytanych danych
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
// wypelnienie zmiennej konfiguracyjnej odpowiednimi opcjami
uint8_t Settings = LSM303_ACC_Z_ENABLE | LSM303_ACC_100HZ;
// Wpisanie konfiguracji do rejestru akcelerometru
HAL_I2C_Mem_Write(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_CTRL_REG1_A, 1, &Settings, 1, 100);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
// Pobranie wyzszego bajtu danych osi Z
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, (LSM303_ACC_Z_H_A), 1, &Data, 1, 100);
Zaxis = Data << 8;
}
/* USER CODE END WHILE */
Budujemy i wgrywamy stworzony program. Następnie otwieramy STMStudio i konfigurujemy podgląd zmiennej Zaxis.
Jeśli wszystko zrobiliśmy tak jak w przedstawionej instrukcji, powinniśmy mieć na ekranie aktualne odczytu z akcelerometru. Jeżeli płytka leży poziomo "mikrokontrolerem do góry", wartość zmiennej Zaxis powinna oscylować w okolicach 16000.
Interpretacja odczytów z akcelerometru
Obracając płytkę w rękach można zauważyć zmieniającą się wartość przyspieszenia. Dlaczego tak jest i co oznaczają te liczby? Aby odpowiedzieć na powyższe pytanie, musimy po raz kolejny sięgnąć do dokumentacji i zobaczyć gdzie w naszym układzie umieszczona jest oś Z. Odpowiednie informacje znajdziemy na stronie 17.
Wiemy zatem, że oś Z skierowana jest prostopadle do płaszczyzny płytki i wartość dodatnią ma po stronie, na której znajduje się mikrokontroler. Co zatem oznacza odczytana z akcelerometru wartość? Żeby się tego dowiedzieć, musimy ponownie zajrzeć do dokumentacji i przywołać kilka wcześniej wspomnianych faktów.
Pierwszą informacją, której potrzebujemy, jest maksymalna wartość przyspieszenia, jaką jest w stanie zmierzyć akcelerometr. Zaglądając na stronę 27 dokumentacji możemy zobaczyć, że odpowiadają za to bity FS[1:0] rejestru CTRL_REG4_A.
Domyślną wartością tego rejestru jest 00, co oznacza, że maksymalna wartość przyspieszenia, jaką możemy teraz zmierzyć to ±2g. Drugą informacją, która jest nam potrzebna do obliczeń, to maksymalna wartość, którą jest w stanie przyjąć zmienna przechowująca wartości pomiarów. Wiemy, że pomiar jest 16 bitowy i że jest ze znakiem. Oznacza to, że mierząc wartość przyspieszenia 2g, zmienna osiągnie swoją maksymalną wartość, a więc 32767.
Z tą wiedzą jesteśmy w stanie ułożyć równanie, przekształcające pobrane wartości na bardziej intuicyjną jednostkę z układu SI - g (przyspieszenie ziemskie).
Żeby nie wpisywać ręcznie maksymalnej wartości zmiennej, można wykorzystać bibliotekę limits.h. Zawiera ona między innymi zdefiniowane maksymalne wartości różnych typów zmiennych.
Dodajemy zatem linijkę dołączającą wspomnianą bibliitekę.
/* USER CODE BEGIN Includes */
#include <limits.h>
/* USER CODE END Includes */
Następnie dodajemy zmienną, która będzie przechowywać wynik naszej operacji.
uint8_t Data = 0; // Zmienna do bezposredniego odczytu z akcelerometru
int16_t Zaxis = 0; // Zawiera przeksztalcona forme odczytanych danych
float Zaxis_g = 0; // Zawiera przyspieszenie w osi przekstalcone na jednostke fizyczna [g]
Na końcu wpisujemy równanie przekształcające zmierzoną wartość na jednostkę fizyczną.
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
// Pobranie wyzszego bajtu danych osi Z
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, (LSM303_ACC_Z_H_A), 1, &Data, 1, 100);
Zaxis = Data << 8;
Zaxis_g = ((float)Zaxis*LSM303_ACC_RESOLUTION)/(float)INT16_MAX;
}
/* USER CODE END WHILE */
Budujemy i wgrywamy program na mikrokontroler. W STMStudio importujemy do podglądu zmienną Zaxis_g. Zmieniamy typ wyświetlania danych na Bar Graph i zmieniamy wartości graniczne wykresu na -2.0 i 2.0 (zaznaczone na czerwono).
Żeby zmienić tytuł wykresu oraz tytuły poszczególnych osi (zaznaczone na niebiesko), wystarczy kliknąć na wykresie prawym przyciskiem myszy, wybrać Properties i zmodyfikować parametry.
Nasz wykres powinien teraz wizualizować mierzone przyspieszenie na wykresie słupkowym.
Zauważmy, że gdy płytka leży nieruchomo, wartość przyspieszenia wynosi około 1g (czyli tak jak jest w rzeczywistości).
Odczytywanie większej liczby danych
Jak dotąd odczytywaliśmy tylko jeden (ważniejszy) bajt danych z akcelerometru. Pomiar akcelerometru jest jednak szesnastobitowy i warto to wykorzystać. Aby odczytać dwa bajty, musimy zmodyfikować program. Po pierwsze musimy zadeklarować zmienną Data jako tablicę.
uint8_t Data[2]; // Zmienna do bezposredniego odczytu danych z akcelerometru
Następnie przypominamy sobie, że parametr piąty funkcji HAL_I2C_Mem_Read (parametr size) oznacza liczbę bajtów, którą chcemy odczytać z urządzenia. Zaglądając do spisu rejestrów widzimy, że rejestry zawierające dane wyjściowe osi Z są umiejscowione w pamięci obok siebie.
Odczytując więc dwa bajty z rejestru OUT_Z_L_A (adres w pamięci: 2C), zostanie odczytana jego wartość, a następnie nastąpi przejście do kolejnej komórki pamięci, a więc OUT_Z_H_A (adres w pamięci: 2D) i stamtąd odczytany zostanie drugi bajt.
Wystarczy więc wywołać funkcję z odpowiednimi parametrami.
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_Z_L_A, 1, Data, 2, 100);
Niestety! Po takim wywołaniu funkcji w obydwu polach zmiennej Data pojawią się te same dane, z podanego do funkcji rejestru OUT_Z_L_A.
Dlaczego?
Jest to związane z drobnym niuansem, który łatwo przeoczyć przeglądając dokumentację. Znajduje się on na stronie 21.
Mówi nam to tyle, że jeśli chcemy odczytać więcej niż jeden bajt danych, najstarszy bit adresu rejestru docelowego (SUB(7)) musi zostać ustawiony na 1. Zauważmy, że adresy rejestrów pamięci akcelerometru podane w dokumentacji również są zapisane na siedmiu bitach.
Jest to sytuacja zbliżona do tej opisanej przy okazji głównego adresowania, z tym że wtedy ustawienie odpowiedniego bitu odbywało się automatycznie i dotyczyło to bitu najmłodszego. W tym wypadku musimy ten bit ustawić sami i jest to bit najstarszy.
Aby uruchomić autoinkrementację adresów rejestru, do adresu odczytywanego rejestru trzeba w takim razie dodać liczbę 0b1000 0000 (0b na początku oznacza, że liczba zapisana jest binarnie.
Wyposażeni w tę wiedzę definiujemy nową stałą zawierającą zmodyfikowany adres.
// mlodszy bajt danych osi Z z najstarszym bitem ustawionym na 1 w celu
// wymuszenia autoinkrementacji adresow rejestru w urzadzeniu docelowym
#define LSM303_ACC_Z_L_A_MULTI_READ (LSM303_ACC_Z_L_A | 0x80)
Teraz możemy już spokojnie wywołać funkcję odczytującą z właściwymi parametrami.
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_Z_L_A_MULTI_READ, 1, Data, 2, 100);
Ostatnia rzecz jaką musimy zmodyfikować, to połączenie obydwu bajtów danych podczas przekształcenia na liczbę typu int16_t. Ponieważ młodszy bajt danych odczytujemy jako pierwszy, pole Data[0] musimy po prostu przepisać do zmiennej bez modyfikacji. Starszy bajt będący w Data[1] przepisujemy tak jak poprzednio, z przesunięciem bitowym.
Zaxis = ((Data[1] << 8) | Data[0]);
W całości nasz kod będzie się prezentował jak na poniższych listingach:
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
//Rejestry
#define LSM303_ACC_ADDRESS (0x19 << 1) // adres akcelerometru: 0011001x
#define LSM303_ACC_CTRL_REG1_A 0x20 // rejestr ustawien 1
#define LSM303_ACC_Z_H_A 0x2D // wyzszy bajt danych osi Z
#define LSM303_ACC_Z_L_A 0x2C // nizszy bajt danych osi Z
// mlodszy bajt danych osi Z z najstarszym bitem ustawionym na 1 w celu
// wymuszenia autoinkrementacji adresow rejestru w urzadzeniu docelowym
#define LSM303_ACC_Z_L_A_MULTI_READ (LSM303_ACC_Z_L_A | 0x80)
// Maski bitowe
// CTRL_REG1_A = [ODR3][ODR2][ODR1][ODR0][LPEN][ZEN][YEN][XEN]
#define LSM303_ACC_Z_ENABLE 0x07 // 0000 0100
#define LSM303_ACC_100HZ 0x50 //0101 0000
#define LSM303_ACC_RESOLUTION 2.0 // Maksymalna wartosc przyspieszenia [g]
// Zmienne
uint8_t Data[2]; // Zmienna do bezposredniego odczytu danych z akcelerometru
int16_t Zaxis = 0; // Zawiera przeksztalcona forme odczytanych danych
float Zaxis_g = 0; // Zawiera przyspieszenie w osi przekstalcone na jednostke fizyczna [g]
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
uint8_t Settings = LSM303_ACC_Z_ENABLE | LSM303_ACC_100HZ;
// Wpisanie konfiguracji do rejestru akcelerometru
HAL_I2C_Mem_Write(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_CTRL_REG1_A, 1, &Settings, 1, 100);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
// Pobranie wyzszego bajtu danych osi Z
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_Z_L_A_MULTI_READ, 1, Data, 2, 100);
Zaxis = ((Data[1] << 8) | Data[0]);
Zaxis_g = ((float)Zaxis*LSM303_ACC_RESOLUTION)/(float)INT16_MAX;
}
/* USER CODE END WHILE */
Tym sposobem otrzymaliśmy pełny pomiar przyspieszenia w osi Z.
Określenie orientacji płytki na podstawie odczytów z akcelerometru
Ostatnią rzeczą, którą dziś zrobimy będzie wykorzystanie odczytów z akcelerometru w praktyce. Zadaniem będzie sterowanie diodami LED w sposób odzwierciedlający pomiary akcelerometru.
Krok 1. Konfigurujemy odpowiednie piny tak, aby można było sterować świeceniem diod LED.
Krok 2. Modyfikujemy nasz kod tak, aby odczytywać dane z wszystkich trzech osi. W tym celu musimy włączyć pomiary z wszystkich trzech osi, a następnie odczytać 6 bajtów danych zaczynając od pierwszego w kolejności rejestru z danymi, a więc OUT_X_L_A.
W sekcji definicji dodajemy stałe przedstawione na poniższym listingu.
#define LSM303_ACC_X_L_A 0x28 // mlodszy bajt danych osi X
// mlodszy bajt danych osi X z najstarszym bitem ustawionym na 1 w celu
// wymuszenia autoinkrementacji adresow rejestru w urzadzeniu docelowym
// (zeby moc odczytac wiecej danych na raz)
#define LSM303_ACC_X_L_A_MULTI_READ (LSM303_ACC_X_L_A | 0x80)
#define LSM303_ACC_XYZ_ENABLE 0x07 // 0000 0111
Krok 3. Następnie zwiększamy rozmiar tablicy Data i dodajemy dodatkowe zmienne do kolejnych osi (oczywiście moglibyśmy wszystko zrobić na tablicach, ale rozdzieliłem to w przykładzie dla zwiększenia czytelności).
uint8_t Data[6]; // Zmienna do bezposredniego odczytu danych z akcelerometru
int16_t Xaxis = 0; // Zawiera przeksztalcona forme odczytanych danych z osi X
int16_t Yaxis = 0; // Zawiera przeksztalcona forme odczytanych danych z osi Y
int16_t Zaxis = 0; // Zawiera przeksztalcona forme odczytanych danych z osi Z
float Xaxis_g = 0; // Zawiera przyspieszenie w osi X przekstalcone na jednostke fizyczna [g]
float Yaxis_g = 0; // Zawiera przyspieszenie w osi Y przekstalcone na jednostke fizyczna [g]
float Zaxis_g = 0; // Zawiera przyspieszenie w osi Z przekstalcone na jednostke fizyczna [g]
Krok 4. Na końcu modyfikujemy ustawienia, które zostaną wpisane do akcelerometru, tak żeby uruchomić pomiary na wszystkich trzech osiach. Modyfikujemy odpowiednio parametry wywołania funkcji HAL_I2C_Mem_Read i dodajemy obliczenia do pozostałych osi.
/* USER CODE BEGIN 2 */
// wypelnieine zmiennej konfiguracyjnej odpowiednimi opcjami
uint8_t Settings = LSM303_ACC_XYZ_ENABLE | LSM303_ACC_100HZ;
// Wpisanie konfiguracji do rejestru akcelerometru
HAL_I2C_Mem_Write(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_CTRL_REG1_A, 1, &Settings, 1, 100);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
// Pobranie 6 bajtow danych zawierajacych przyspieszenia w 3 osiach
HAL_I2C_Mem_Read(&hi2c1, LSM303_ACC_ADDRESS, LSM303_ACC_X_L_A_MULTI_READ, 1, Data, 6, 100);
// Konwersja odebranych bajtow danych na typ int16_t
Xaxis = ((Data[1] << 8) | Data[0]);
Yaxis = ((Data[3] << 8) | Data[2]);
Zaxis = ((Data[5] << 8) | Data[4]);
// obliczenie przyspieszen w kazdej z osi w jednostce SI [g]
Xaxis_g = ((float) Xaxis * LSM303_ACC_RESOLUTION) / (float) INT16_MAX;
Yaxis_g = ((float) Yaxis * LSM303_ACC_RESOLUTION) / (float) INT16_MAX;
Zaxis_g = ((float) Zaxis * LSM303_ACC_RESOLUTION) / (float) INT16_MAX;
// prog, po przekroczeniu ktorego wykona sie okreslona nizej akcja [g]
const float Threshold = 0.5;
// Sterowanie diodami na podstawie przyspieszenia w osi X
if (Xaxis_g > Threshold) { // jezeli przyspieszenie w osi X jest wieksze niz ustalony prog
HAL_GPIO_WritePin(LED_X_POSITIVE_GPIO_Port, LED_X_POSITIVE_Pin, GPIO_PIN_SET); // zapal diode
} else if (Xaxis_g < -Threshold) { // jezeli przyspieszenie w osi X jest mniejsze niz ustalony prog
HAL_GPIO_WritePin(LED_X_NEGATIVE_GPIO_Port, LED_X_NEGATIVE_Pin, GPIO_PIN_SET); // zapal diode
} else { // w przeciwnym razie zgas wszystkie diody
HAL_GPIO_WritePin(LED_X_POSITIVE_GPIO_Port, LED_X_POSITIVE_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_X_NEGATIVE_GPIO_Port, LED_X_NEGATIVE_Pin, GPIO_PIN_RESET);
}
// Sterowanie diodami na podstawie przyspieszenia w osi Y
if (Yaxis_g > Threshold) { // jezeli przyspieszenie w osi Y jest wieksze niz ustalony prog
HAL_GPIO_WritePin(LED_Y_POSITIVE_GPIO_Port, LED_Y_POSITIVE_Pin, GPIO_PIN_SET);
} else if (Yaxis_g < -Threshold) { // jezeli przyspieszenie w osi Y jest mniejsze niz ustalony prog
HAL_GPIO_WritePin(LED_Y_NEGATIVE_GPIO_Port, LED_Y_NEGATIVE_Pin, GPIO_PIN_SET);
} else { // w przeciwnym razie zgas wszystkie diody
HAL_GPIO_WritePin(LED_Y_POSITIVE_GPIO_Port, LED_Y_POSITIVE_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_Y_NEGATIVE_GPIO_Port, LED_Y_NEGATIVE_Pin, GPIO_PIN_RESET);
}
}
/* USER CODE END WHILE */
Teraz w STMStudio możemy podglądać przyspieszenie w 3 osiach, a na diodach LED obserwować stworzone przez nas "sterowanie ruchem".
Oczywiście zamiast sterowania zero-jedynkowego można by tu było zastosować sygnał PWM o wypełnieniu proporcjonalnym do przyspieszenia w danej osi. Wtedy jasność świecenia diod płynnie sygnalizowałaby aktualne przyspieszenie.
Podsumowanie
W tym odcinku nauczyliśmy się jak korzystać z jednej z podstawowych form komunikacji występującej między układami - interfejs I2C. Wiemy również jak uruchomić akcelerometr, jak odczytać z niego dane i jak je przedstawić za pomocą fizycznych jednostek. W załączniku znaleźć można oczywiście kody programów wykonywanych w tym artykule!
W następnym odcinku wykorzystamy kolejny interfejs - SPI. Dzięki niemu możliwe będzie wygodne używanie wyświetlacza OLED. Jeżeli macie jakieś pytania, sugestie lub coś jest dla was nie zrozumiałe, śmiało piszcie w komentarzach!
Nawigacja kursu
Autor kursu: Bartek (Popeye) Kurosz
Redakcja: Damian (Treker) Szymański
Załączniki
Powiązane wpisy
akcelerometr, i2c, kurs, kursSTM32F4, programowanie, stm32
Trwa ładowanie komentarzy...