Kursy • Poradniki • Inspirujące DIY • Forum
Podstawowe informacje o LSM303D
Na początek musimy poznać nasz czujnik. Jest on nieco bardziej skomplikowany, niż omawiane wcześniej. Na szczęście dostępna jest do niego dobra dokumentacja. W naszym przykładzie nie omówimy wszystkich możliwości układu LSM303D. Skupimy się jednak na realizacji możliwie prostego i uniwersalnego mechanizmu odczytu danych z tego czujnika.
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!
Zamów w Botland.com.pl »Najważniejsze materiały to:
- datasheet - w nim znajdziemy pełny opis możliwości układu, protokół komunikacji itd.
- schemat modułu - jak zwykle obraz jest wart więcej niż tysiąc słów. Warto zwrócić uwagę na sposób podłączenia układu oraz obecne rezystory podciągające
Przy okazji ważna uwaga. Omawiamy czujnik LSM303D, jednak można się zetknąć z układami o podobnej nazwie, ale innym działaniu. Są to starsze modele LSM303DLM i LSM303DLHC - pomimo bardzo podobnej nazwy, ich sposób działania jest inny. Układy te nie są już produkowane. Istnieje nikła szansa, że się na nie natkniemy, ale sporo dostępnych przykładów i bibliotek przeznaczonych jest dla starszych modeli, które mogę nie działać z nowym LSM303D.
Starsze modele LSM303DLM i LSM303DLHC działają inaczej niż omawiany LSM303D. Należy dokładnie sprawdzać oznaczenie posiadanego układu.
LSM303D - komunikacja z modułem
W poprzedniej części kursu omówiliśmy sposób komunikacji poprzez I2C. Teraz możemy wykorzystać zdobytą wiedzę oraz napisany już program do rozpoczęcia pracy z nowym układem. Czytając datasheet zobaczymy, że komunikacja jest bardzo podobna do poznanej poprzednio.
Różnica, to inny adres układu oraz wysyłanie numeru rejestru zamiast adresu w pamięci EEPROM.
Poprzednio używaliśmy układu 24AA01, który posiadał adres 0xa0. Nowy układ może mieć jeden z dwóch adresów 0x3c lub 0x3a, w zależności od stanu wyprowadzenia SA0. Oglądając schemat modułu zobaczymy, że na płytce znajduje się rezystor podciągający, co oznacza, że linia SA0 ma domyślnie stan wysoki, a adres układu to 0x3a. Na schemacie zobaczymy również, że linie SDA i SCL posiadają już rezystory podciągające. Możemy więc zrezygnować z podłączania własnych rezystorów podciągających.
Moduł akcelerometru posiada wbudowane rezystory podciągające, więc nie ma konieczności podłączania rezystorów do linii SDA i SCL. Dodatkowo domyślnie wybierany jest adres 0x3a oraz komunikacja po I2C.
Teraz możemy podłączyć moduł zgodnie z rysunkiem:
Gdy mamy podłączony układ, czas zająć się programem. Wykorzystamy kod napisany poprzednio, dostosowując go do nowego układu.
Po pierwsze musimy zmienić adres układu. Poprzednio było to 0xa0, teraz zdefiniujemy stałą, w której zapiszemy adres układu. Ułatwi to ewentualną modyfikację adresu w przyszłości:
1 |
#define LSM303D_ADDR 0x3a |
Poprzednio wywoływaliśmy HAL_I2C_Mem_Read oraz HAL_I2C_Mem_Write, ale znaczna liczba parametrów oraz konieczność rzutowania typu na uint8_t nie jest zbyt wygodna. Dlatego w nowym programie napiszemy własne funkcje, które nieco ułatwią nam pracę. Na początek napiszemy funkcję odczytującą wartość z rejestru układu LSM303D. Schemat komunikacji z układem jest taki sam jak w przypadku pamięci eeprom, więc chociaż to nie pamięć również wywołamy HAL_I2C_Mem_Read.
Kod wygląda następująco:
1 2 3 4 5 6 7 8 |
uint8_t lsm_read_reg(uint8_t reg) { uint8_t value = 0; HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); return value; } |
Pełna lista oraz opis dostępnych rejestrów znajduje się w dokumentacji LSM303D. Poniżej jej fragment:
Zaczniemy od sprawdzenia, czy nasz układ w ogóle działa. Jak widzimy, dostępny jest rejestr WHO_AM_I (adres 0x0f). Jest to rejestr tylko do odczytu, który zawsze powinien zwracać wartość 01001001b, czyli 0x49.
Odczytamy teraz jego zawartość, sprawdzimy czy jest poprawna i wyślemy przez UART odpowiedni komunikat. Program, który to realizuje wygląda następująco:
1 2 3 4 5 6 7 8 |
printf("Wyszukiwanie akcelerometru...\n"); uint8_t who_am_i = lsm_read_reg(0xf); if (who_am_i == 0x49) { printf("Znaleziono akcelerometr LSM303D\n"); } else { printf("Niepoprawna odpowiedź układu (0x%02X)\n", who_am_i); } |
Komunikację przez UART oraz przekierowanie printf już znamy, więc program nie powinien być trudny do przeanalizowania:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
#include "stm32f1xx.h" #define LSM303D_ADDR 0x3a I2C_HandleTypeDef i2c; UART_HandleTypeDef uart; void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } int __io_putchar(int ch) { if (ch == '\n') send_char('\r'); send_char(ch); return ch; } uint8_t lsm_read_reg(uint8_t reg) { uint8_t value = 0; HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); return value; } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_OD; gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; // SCL, SDA gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio); gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); i2c.Instance = I2C1; i2c.Init.ClockSpeed = 100000; i2c.Init.DutyCycle = I2C_DUTYCYCLE_2; i2c.Init.OwnAddress1 = 0xff; i2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; i2c.Init.OwnAddress2 = 0xff; i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; i2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&i2c); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); printf("Wyszukiwanie akcelerometru...\n"); uint8_t who_am_i = lsm_read_reg(0xf); if (who_am_i == 0x49) { printf("Znaleziono akcelerometr LSM303D\n"); } else { printf("Niepoprawna odpowiedź układu (0x%02X)\n", who_am_i); } while (1) { } } |
W efekcie powinniśmy zobaczyć następujący komunikat w okienku terminala portu szeregowego:
Natomiast układ na płytce stykowej wyglądał tak:
Odczyt temperatury
Zanim przejdziemy dalej, napiszemy kilka funkcji pomocniczych oraz uporządkujemy nieco program. Napisaliśmy już funkcję do odczytu rejestru, więc teraz napiszemy jej odpowiednik do zapisu:
1 2 3 4 |
void lsm_write_reg(uint8_t reg, uint8_t value) { HAL_I2C_Mem_Write(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); } |
Dodatkowo czytając dokumentację, zauważymy, że odczytywane wartości np. temperatury, przyspieszenia, czy strumienia magnetycznego są wartościami 16-bitowymi ze znakiem (zapisanymi w kodzie U2).
Możemy również napisać prostą funkcję, która będzie odczytywała taką wartość:
1 2 3 4 5 6 7 8 |
int16_t lsm_read_value(uint8_t reg) { int16_t value = 0; HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg | 0x80, 1, (uint8_t*)&value, sizeof(value), HAL_MAX_DELAY); return value; } |
Ta sztuczka z reg | 0x80 oznacza włączenie auto-inkrementacji. Dzięki temu gdy odczytujemy dwa bajty, zamiast dwa razy to samo otrzymujemy wartości z kolejnych rejestrów. Wszystko jest opisane w dokumentacji LSM303D (ale łatwo o tym zapomnieć).
Teraz czas trochę posprzątać - podobnie jak w części dotyczącej wyświetlacza, przeniesiemy funkcje sterujące układem LSM303D do osobnych plików. Dodamy również stałe, definiujące rejestry układu. Będziemy mogli wykorzystywać je w programie zamiast "magicznych liczb".
Plik nagłówkowy lsm303d.h wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#ifndef LSM303D_H_ #define LSM303D_H_ #include "stm32f1xx.h" #define LSM303D_TEMP_OUT 0x05 #define LSM303D_STATUS_M 0x07 #define LSM303D_OUT_X_M 0x08 #define LSM303D_OUT_Y_M 0x0a #define LSM303D_OUT_Z_M 0x0c #define LSM303D_WHO_AM_I 0x0f #define LSM303D_CTRL0 0x1f #define LSM303D_CTRL1 0x20 #define LSM303D_CTRL2 0x21 #define LSM303D_CTRL3 0x22 #define LSM303D_CTRL4 0x23 #define LSM303D_CTRL5 0x24 #define LSM303D_CTRL6 0x25 #define LSM303D_CTRL7 0x26 #define LSM303D_STATUS 0x27 #define LSM303D_OUT_X_A 0x28 #define LSM303D_OUT_Y_A 0x2a #define LSM303D_OUT_Z_A 0x2c extern I2C_HandleTypeDef i2c; void lsm_write_reg(uint8_t reg, uint8_t value); uint8_t lsm_read_reg(uint8_t reg); int16_t lsm_read_value(uint8_t reg); #endif /* LSM303D_H_ */ |
Natomiast plik z kodem lsm303d.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include "lsm303d.h" #define LSM303D_ADDR 0x3a I2C_HandleTypeDef i2c; void lsm_write_reg(uint8_t reg, uint8_t value) { HAL_I2C_Mem_Write(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); } uint8_t lsm_read_reg(uint8_t reg) { uint8_t value = 0; HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); return value; } int16_t lsm_read_value(uint8_t reg) { int16_t value = 0; HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg | 0x80, 1, (uint8_t*)&value, sizeof(value), HAL_MAX_DELAY); return value; } |
Teraz możemy wrócić do programu głównego. Musimy odpowiednio skonfigurować LSM303D, czyli włączyć pomiar temperatury. W dokumentacji znajdujemy odpowiedni rejestr, czyli CTRL5.
Jak widzimy, domyślnie pomiar temperatury jest wyłączony (pole TEMP_EN ma wartość 0). Zapiszemy więc do CTRL5 odpowiednią wartość
1 |
lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz) |
Przy okazji ustawiliśmy częstotliwość odczytów z magnetometru na 50Hz - nie wykorzystamy tego w tym momencie, ale skoro jest w tym samym rejestrze, to dlaczego nie ustawić jej "na zapas".
Po włączeniu pomiaru, musimy dać układowi trochę czasu, dodajemy więc małe opóźnienie. Odczyt temperatury jest już prosty - wystarczy wczytywać wartość z rejestrów TEMP_OUT_L i TEMP_OUT_H.
Napisaliśmy funkcję lsm_read_value, więc oba rejestry odczytamy jednocześnie. Funkcja wykona za nas również konwersję na 16-bitową wartość ze znakiem. Kod wygląda następująco:
1 2 3 4 5 6 7 8 9 |
lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz) HAL_Delay(100); while (1) { int16_t temp = lsm_read_value(LSM303D_TEMP_OUT); printf("Temp = %d\n", temp); HAL_Delay(200); } |
Pełny kod programu widzimy poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#include "stm32f1xx.h" #include "lsm303d.h" UART_HandleTypeDef uart; void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } int __io_putchar(int ch) { if (ch == '\n') send_char('\r'); send_char(ch); return ch; } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_OD; gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; // SCL, SDA gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio); gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); i2c.Instance = I2C1; i2c.Init.ClockSpeed = 100000; i2c.Init.DutyCycle = I2C_DUTYCYCLE_2; i2c.Init.OwnAddress1 = 0xff; i2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; i2c.Init.OwnAddress2 = 0xff; i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; i2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&i2c); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); printf("Wyszukiwanie akcelerometru...\n"); uint8_t who_am_i = lsm_read_reg(LSM303D_WHO_AM_I); if (who_am_i == 0x49) { printf("Znaleziono akcelerometr LSM303D\n"); } else { printf("Niepoprawna odpowiedź układu (0x%02X)\n", who_am_i); } lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz) HAL_Delay(100); while (1) { int16_t temp = lsm_read_value(LSM303D_TEMP_OUT); printf("Temp = %d\n", temp); HAL_Delay(200); } } |
Rezultat działania widoczny w oknie terminala - powinna być to aktualna temperatura:
Akcelerometr w praktyce na STM32
Poznaliśmy już trochę układ LSM303D, czas przejść do głównego tematu tej części kursu, czyli odczytu danych z akcelerometru. W tym celu musimy nieco zmienić konfigurację układu (włączyć akcelerometr) oraz odczytać dane.
Opis konfiguracji jak zwykle znajdziemy w nocie katalogowej. Tym razem interesuje nas rejestr CTRL1. Trzy najwyższe bity określają prędkość odczytywania danych (ustawimy 25Hz jako przykład). Natomiast trzy najniższe bity odpowiadają za uruchomienie odczytów (z osi Z, Y i X):
Konfigurację wykonamy instrukcją:
1 |
lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN |
Teraz możemy, podobnie jak w przypadku temperatury pobierać i wyświetlać dane:
1 2 3 4 5 6 |
while (1) { int16_t a_x = lsm_read_value(LSM303D_OUT_X_A); int16_t a_y = lsm_read_value(LSM303D_OUT_Y_A); int16_t a_z = lsm_read_value(LSM303D_OUT_Z_A); printf("X = %d Y = %d Z = %d\n", a_x, a_y, a_z); } |
W programie znajdziemy nieco więcej kodu, w tym zakomentowane instrukcje printf - omówimy je za chwilę. Cały program wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
#include <math.h> #include "stm32f1xx.h" #include "lsm303d.h" I2C_HandleTypeDef i2c; UART_HandleTypeDef uart; void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } int __io_putchar(int ch) { if (ch == '\n') send_char('\r'); send_char(ch); return ch; } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_OD; gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; // SCL, SDA gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio); gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); i2c.Instance = I2C1; i2c.Init.ClockSpeed = 100000; i2c.Init.DutyCycle = I2C_DUTYCYCLE_2; i2c.Init.OwnAddress1 = 0xff; i2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; i2c.Init.OwnAddress2 = 0xff; i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; i2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&i2c); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); printf("Wyszukiwanie akcelerometru...\n"); uint8_t who_am_i = lsm_read_reg(LSM303D_WHO_AM_I); if (who_am_i == 0x49) { printf("Znaleziono akcelerometr LSM303D\n"); } else { printf("Niepoprawna odpowiedź układu (0x%02X)\n", who_am_i); } lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz) HAL_Delay(100); while (1) { int16_t a_x = lsm_read_value(LSM303D_OUT_X_A); int16_t a_y = lsm_read_value(LSM303D_OUT_Y_A); int16_t a_z = lsm_read_value(LSM303D_OUT_Z_A); printf("X = %d Y = %d Z = %d\n", a_x, a_y, a_z); float x = a_x * 2.0f / 32678.0f; float y = a_y * 2.0f / 32678.0f; float z = a_z * 2.0f / 32678.0f; //printf("X = %.2f Y = %.2f Z = %.2f\n", x, y, z); float alpha = atan2f(z, x); //printf("alpha = %.2f\n", alpha * 180.0f / M_PI + 90.0f); HAL_Delay(200); } } |
Efekt działania powinien wyglądać mniej więcej jak poniżej:
Wyniki mogą być nieco zaskakujące, jeśli nie zajmowaliśmy się wcześniej akcelerometrami. Spróbujmy więc zrozumieć co oznaczają. Odczyty są kodowane 16-bitowo ze znakiem. Więc ich zakres wynosi od -32768 do 32767. Czytając dokumentację, odkryjemy, że akcelerometr LSM303D może działać w kilku zakresach pomiarowych: 2 g, 4 g, 8 g i 12 g. Domyślna jest konfiguracja 2 g, do niej odnoszą się wyniki.
Ponieważ przyspieszenie 2 g, to pełny zakres, odpowiada mu odczyt 32767. Minimalna wartość wynosi -2 g, i daje wynik -32768. Możemy więc przeliczyć otrzymane wyniki na znane z fizyki wartości przyspieszenia:
1 2 3 4 |
float x = a_x * 2.0f / 32678.0f; float y = a_y * 2.0f / 32678.0f; float z = a_z * 2.0f / 32678.0f; printf("X = %.2f Y = %.2f Z = %.2f\n", x, y, z); |
W kodzie programu wystarczy odkomentować drugi printf (najlepiej zakomentowując pierwszy - inaczej wyniki będą nieco nieczytelne). Otrzymamy teraz następujący rezultat:
Mając wyniki w takiej postaci, łatwiej jest zrozumieć ich znaczenie. Jak widzimy, wzdłuż osi X i Y mamy niewielkie odczyty, a na oś Z działa przyspieszenie prawie 1 g. Wynik może być pewnym zaskoczeniem, przecież nasza płytka leży spokojnie na biurku, nie przyspiesza w żadnym kierunku.
Akcelerometr odczytuje przyspieszenie ziemskie, które wynosi właśnie 1 g.
Mamy wiec poprawny wynik - na oś Z działa przyspieszenie ziemskie, a na X i Y - też trochę. Gdyby czujnik idealnie wypoziomować, powinniśmy otrzymać przyspieszenie zerowe na X i Y oraz 1 g dla osi Z. Niestety, nawet wtedy otrzymany wynik mógłby się zmienić np. po zmianie temperatury. Dlatego czujnik powinien być kalibrowany.
Co więcej chcąc obliczać np. przesunięcie robota, musimy pamiętać, że 1 g pochodzące od przyspieszenia ziemskiego i tak pojawi się w otrzymywanych wynikach.
Odpowiednia kalibracja i filtracja, to całkiem skomplikowane zadanie.
STM32 i LSM303D - Projekt prostej poziomicy
Warto sprawdzić jak będą zmieniały się odczyty przechylając płytkę z czujnikiem. Na module z LSM303D znajdziemy nadruk z oznaczeniem kierunków osi X, Y oraz Z:
Spróbujemy wykorzystać nasz czujnik do określania nachylenia czujnika - zbudujemy więc prostą, cyfrową poziomicę. Na początek uprościmy sobie trochę zadanie i zamiast w 3D, będziemy rozwiązywać problem w dwóch wymiarach.
Po prostu wybierzemy 2 osie (X i Z) i tylko nimi się będziemy zajmować.
Spróbujmy napisać program, który wyświetli kąt pod jakim nasz czujnik jest ułożony względem płaszczyzny ziemi. Z pomocą przychodzi nam trygonometria. Funkcja arcus tangens pozwala na przeliczenie uzyskanych wyników:
1 2 |
float alpha = atan2f(z, x); printf("alpha = %.2f\n", alpha * 180.0f / M_PI + 90.0f); |
Ponieważ wyniki są w radianach, przeliczamy je na stopnie. Dodajemy też 90, aby uzyskać kąt między płytką, a płaszczyzną ziemi. Efekt wygląda jak na poniższym screenie:
Do prezentowania wyników wykorzystamy wyświetlacz LCD, który omówiliśmy wcześniej. Mamy więc przygotowane odpowiednie procedury - wystarczy skopiować pliki omówione wcześniej.
Układ podłączamy zgodnie z rysunkiem:
Jednocześnie będziemy wykorzystywać komunikację przez SPI, I2C oraz UART. W tym celu kopiujemy następujące moduły z poprzednich przykładów (pliki .h oraz .c):
- lsm303d,
- lcd,
- bitmap,
- font.
Mając odpowiednie biblioteki, możemy przystąpić do pisania programu. W programie wyświetlimy poziomą linię - która będzie zachowywała kierunek nawet podczas obracania płytki z czujnikiem i wyświetlaczem.
Do poprzedniego programu dodajemy kod inicjalizujący wyświetlacz - był omówiony wcześniej. Teraz omówimy tylko pętlę główną programu:
1 2 3 4 5 6 7 8 9 10 |
while (1) { int x = LCD_WIDTH * lsm_read_value(LSM303D_OUT_Y_A) / 32768; int y = LCD_HEIGHT * lsm_read_value(LSM303D_OUT_X_A) / 32768; lcd_clear(); lcd_draw_line(LCD_WIDTH / 2 + x, LCD_HEIGHT / 2 - y, LCD_WIDTH / 2 - x, LCD_HEIGHT / 2 + y); lcd_copy(); HAL_Delay(100); } |
Odczytujemy przyspieszenie działające wzdłuż osi X oraz Y (układ dużo lepiej działa w pionie, dlatego zamiast osi Z tym razem używana jest oś Y).
Mając wektor przyspieszenie ziemskiego, łatwo możemy obliczyć współrzędne odcinka do niego prostopadłego. W tym celu zamieniamy miejscami współrzędne x i y oraz przeliczamy zakres odczytów akcelerometru. Pełne wyprowadzenie matematyczne nie jest trudne - wystarczą podstawy geometrii, które pominiemy już w kursie.
Kod programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
#include "stm32f1xx.h" #include "lsm303d.h" #include "lcd.h" #include "bitmap.h" UART_HandleTypeDef uart; void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } int __io_putchar(int ch) { if (ch == '\n') send_char('\r'); send_char(ch); return ch; } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_OD; gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; // SCL, SDA gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio); gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_5 | GPIO_PIN_7; // SCK, MOSI gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; // MISO gpio.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pin = LCD_DC|LCD_CE|LCD_RST; gpio.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOC, &gpio); HAL_GPIO_WritePin(GPIOC, LCD_CE|LCD_RST, GPIO_PIN_SET); spi.Instance = SPI1; spi.Init.Mode = SPI_MODE_MASTER; spi.Init.NSS = SPI_NSS_SOFT; spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 1MHz spi.Init.Direction = SPI_DIRECTION_2LINES; spi.Init.CLKPhase = SPI_PHASE_1EDGE; spi.Init.CLKPolarity = SPI_POLARITY_LOW; spi.Init.DataSize = SPI_DATASIZE_8BIT; spi.Init.FirstBit = SPI_FIRSTBIT_MSB; spi.Init.TIMode = SPI_TIMODE_DISABLE; spi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spi.Init.CRCPolynomial = 7; HAL_SPI_Init(&spi); __HAL_SPI_ENABLE(&spi); i2c.Instance = I2C1; i2c.Init.ClockSpeed = 100000; i2c.Init.DutyCycle = I2C_DUTYCYCLE_2; i2c.Init.OwnAddress1 = 0xff; i2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; i2c.Init.OwnAddress2 = 0xff; i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; i2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&i2c); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); lcd_setup(); lcd_draw_bitmap(forbot_logo); lcd_copy(); printf("Wyszukiwanie akcelerometru...\n"); uint8_t who_am_i = lsm_read_reg(LSM303D_WHO_AM_I); if (who_am_i == 0x49) { printf("Znaleziono akcelerometr LSM303D\n"); } else { printf("Niepoprawna odpowiedź układu (0x%02X)\n", who_am_i); } lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz) HAL_Delay(1000); while (1) { int x = LCD_WIDTH * lsm_read_value(LSM303D_OUT_Y_A) / 32768; int y = LCD_HEIGHT * lsm_read_value(LSM303D_OUT_X_A) / 32768; lcd_clear(); lcd_draw_line(LCD_WIDTH / 2 + x, LCD_HEIGHT / 2 - y, LCD_WIDTH / 2 - x, LCD_HEIGHT / 2 + y); lcd_copy(); HAL_Delay(100); } } |
Działanie programu w praktyce widoczne jest na poniższym filmie:
Podsumowanie
W tej części kursu poznaliśmy podstawy działania czujnika LSM303D. Sprawdziliśmy sposób komunikacji z układem. Wykorzystaliśmy wbudowany termometr oraz akcelerometr. Nie zajęliśmy się natomiast magnetometrem. Zadanie to pozostaje dla chętnych, jako zadanie domowe.
Nawigacja kursu
W kolejnym odcinku zajmiemy się małym podsumowaniem całego kursu. Poruszymy również temat kontynuacji serii związanej z programowaniem STM32.
Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański
Powiązane wpisy
akcelerometr, kurs, kursSTM32F1HAL, programowanie, stm32, żyroskop
Trwa ładowanie komentarzy...