Kursy • Poradniki • Inspirujące DIY • Forum
Czego dowiesz się z tej części kursu STM32L4?
Tym razem zajmiemy się obsługą modułu, na którego pokładzie znajduje się nowoczesny i precyzyjny czujnik ciśnienia atmosferycznego, czyli cyfrowy barometr. Zaczniemy od omówienia najważniejszych informacji na temat LPS25HB i nawiązania komunikacji przez I2C, a skończymy na pomiarach ciśnienia względnego i bezwzględnego, pomiarze wysokości oraz kalibracji. Przy okazji będzie to dobry powód do szybkiej powtórki z fizyki.
Dlaczego czujniki ciśnienia nie są nudne?
Czujniki ciśnienia kojarzone są przez większość osób ze stacjami pogodowymi, które wskazują aktualne ciśnienie – funkcja ta może być przydatna, ale nie jest widowiskowa. Warto jednak pamiętać, że takie sensory używane są również np. do pomiaru wysokości.
Pomiar wysokości to funkcja używana w wielu urządzeniach – od pojazdów latających po popularne zegarki sportowe, dzięki którym można się dowiedzieć, na jakie wzniesienie udało nam się właśnie wjechać. Moduły te są tak dokładne, że bez problemu można za ich pomocą wykryć podniesienie urządzenia nawet o kilkadziesiąt centymetrów.
Dlatego nawet jeśli tematyka pomiaru ciśnienia wydaje się komuś nudna, to zdecydowanie warto przebrnąć przez podstawy, aby później wykorzystywać te informacje w znacznie ciekawszych projektach niż stacja pogodowa. Na wstępie trzeba jednak zdać sobie sprawę, że pomiar ciśnienia to trudny temat, a efekty uzyskiwane podczas wykonywania opisanych tutaj ćwiczeń będą różniły się zależnie od miejsca, w którym się znajdujemy, aktualnej pogody oraz wielu innych czynników.
LPS25HB – czujnik ciśnienia i temperatury
Podczas wykonywania ćwiczeń z tej części kursu STM32L4 będziemy korzystać z modułu firmy POLOLU. Został on wyposażony w czujnik ciśnienia LPS25HB oraz szereg dodatkowych elementów, które są niezbędne do poprawnej pracy tego układu – zaliczają się do nich m.in. rezystory pociągające na liniach I2C (nie musimy więc dodawać ich samodzielnie).
Co ciekawe, producentem samego czujnika LPS25HB jest STMicroelectronics, czyli ta sama firma, która odpowiada za opisywane w tym kursie mikrokontrolery STM32L4. Wszystkie informacje na temat czujnika znajdziemy w nocie katalogowej, mającej aż 50 stron.
Długość dokumentacji podpowiada już, że czujnik ten jest nieco bardziej skomplikowany, niż mogłoby się wydawać. Dlatego w tym kursie nie omówimy wszystkich możliwości LPS25HB – skupimy się głównie na realizacji możliwie prostego i uniwersalnego mechanizmu odczytu danych z tego czujnika za pomocą I2C, a następnie zajmiemy się obróbką tych danych. W ramach ciekawostki warto jednak wiedzieć, że z układem tym można również komunikować się za pomocą SPI.
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zamów zestaw elementów i wykonaj ćwiczenia z tego kursu! W komplecie płytka NUCLEO-L476RG oraz m.in. wyświetlacz graficzny, joystick, enkoder, czujniki (światła, temperatury, wysokości, odległości), pilot IR i wiele innych.
Zamów w Botland.com.pl »Komunikacja z czujnikiem LPS25HB
W poprzedniej części kursu omówiliśmy sposób komunikacji poprzez I2C na bazie pamięci EEPROM. Teraz bez problemu możemy wykorzystać zdobytą wcześniej wiedzę do pracy z nowym czujnikiem. Na początek podłączenie – w związku z tym, że korzystamy z gotowego modułu, nie musimy dodawać żadnych innych elementów (wystarczy zasilanie z 3,3 V i linie sygnałowe).
Jeśli zajrzymy do noty katalogowej, to zobaczymy, że komunikacja z LPS25HB przebiega podobnie do tego, co znamy z poprzedniej części kursu. Główna różnica to inny adres układu oraz fakt, że wysyłamy adres rejestru zamiast adresu pamięci EEPROM. Poprzednio używaliśmy układu 24AA01, który posiadał adres 0xA0. Tym razem natomiast posługujemy się układem, który może korzystać z adresu 0xB8 lub 0xBA – wszystko zależy od stanu wyprowadzenia SDO/SA0. Na module POLOLU wyprowadzenie to zostało podłączone przez rezystor z dodatnią szyną zasilania, a to oznacza, że adres układu to 0xBA.
Co więcej, układ LPS25HB na module POLOLU został skonfigurowany od razu do komunikacji przez I2C (odpowiada za to rezystor pull-up podłączony do wyprowadzenia CS).
Po podłączeniu czujnika możemy przejść do programowania. Zaczynamy od projektu z STM32L476RG, który pracuje z częstotliwością 80 MHz. Uruchamiamy debugger i USART2 w trybie asynchronicznym. Do tego uruchamiamy I2C1 z domyślnymi ustawieniami. Zaznaczamy również w opcjach projektu, że CubeMX ma wygenerować osobne pliki dla wszystkich modułów. Następnie (już standardowo) dodajemy przekierowanie printf na UART – wystarczy dodanie pliku nagłówkowego:
1 |
#include <stdio.h> |
oraz kodu zbliżonego do poniższego:
1 2 3 4 5 6 7 8 9 10 |
int __io_putchar(int ch) { if (ch == '\n') { __io_putchar('\r'); } HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, HAL_MAX_DELAY); return 1; } |
Na początek tworzymy stałą przechowującą adres naszego modułu – używanie tzw. magicznych liczb to niezbyt dobra praktyka, wykorzystanie zdefiniowanej wartości będzie znacznie lepszym rozwiązaniem.
1 |
#define LPS25HB_ADDR 0xBA |
W poprzedniej części kursu korzystaliśmy z funkcji HAL_I2C_Mem_Read oraz HAL_I2C_Mem_Write, dzięki którym odczytywaliśmy dane z pamięci EEPROM i zapisywaliśmy je tam. Te same funkcje będą również przydatne podczas komunikacji z czujnikiem ciśnienia.
Różnica jest tylko taka, że poprzednio czytaliśmy dane z komórek pamięci, do których wcześniej sami zapisywaliśmy dane, a teraz będziemy odczytywać informacje z rejestrów, w których przetrzymywane są informacje o konfiguracji czujnika i o aktualnych odczytach.
Opis rejestrów znajdziemy oczywiście w dokumentacji czujnika (oto fragment tabeli):
Standardowo najlepiej takie dane zapisać w kodzie w formie definicji. Na początek wystarczy kilka poniższych rejestrów, dzięki którym sprawdzimy komunikację z układem, dokonamy odpowiednich ustawień i odczytamy ciśnienie oraz temperaturę – oczywiście za chwilę omówimy te rejestry.
1 2 3 4 5 6 7 8 9 10 |
#define LPS25HB_WHO_AM_I 0x0F #define LPS25HB_CTRL_REG1 0x20 #define LPS25HB_CTRL_REG2 0x21 #define LPS25HB_CTRL_REG3 0x22 #define LPS25HB_CTRL_REG4 0x23 #define LPS25HB_PRESS_OUT_XL 0x28 #define LPS25HB_PRESS_OUT_L 0x29 #define LPS25HB_PRESS_OUT_H 0x2A #define LPS25HB_TEMP_OUT_L 0x2B #define LPS25HB_TEMP_OUT_H 0x2C |
Oprócz tego przyda nam się funkcja pomocnicza, dzięki której łatwo odczytamy informacje z danego rejestru. Jest to zabieg wyłącznie kosmetyczny, abyśmy nie musieli za każdym razem ręcznie wpisywać pełnego wywołania funkcji HAL_I2C_Mem_Read.
1 2 3 4 5 6 7 |
uint8_t lps_read_reg(uint8_t reg) { uint8_t value = 0; HAL_I2C_Mem_Read(&hi2c1, LPS25HB_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); return value; } |
Test komunikacji z czujnikiem LPS25HB
Zaczniemy od sprawdzenia, czy nasz układ w ogóle działa i czy komunikacja przebiega prawidłowo. Do tego celu wykorzystamy rejestr WHO_AM_I, który znajduje się pod adresem 0x0F. Rejestr ten powinien zwrócić 0xBD. Jeśli tak się stanie, to mamy pewność, że komunikacja z LPS25HB działa poprawnie.
Możemy teraz napisać program testujący komunikację z czujnikiem. Wyświetlamy komunikat w konsoli, a następnie wywołujemy funkcję, która ma odczytać zawartość rejestru WHO_AM_I. Jeśli odpowiedź będzie zgodna z dokumentacją, to wyświetlamy komunikat o znalezieniu czujnika, inna odpowiedź to sygnał, że coś jest źle – wyświetlamy komunikat błędu wraz z podaniem odczytanej wartości.
1 2 3 4 5 6 7 8 |
printf("Searching...\n"); uint8_t who_am_i = lps_read_reg(LPS25HB_WHO_AM_I); if (who_am_i == 0xBD) { printf("Found: LPS25HB\n"); } else { printf("Error: (0x%02X)\n", who_am_i); } |
Wystarczy, że kod ten wywołamy raz (przed pętlą while). Jeśli wszystko przebiegnie poprawnie, na ekranie powinien wyświetlić się poniższy komunikat. W przypadku błędu warto sprawdzić połączenia oraz konfigurację projektu.
Tego typu test jest bardzo ważny – pozwala nam się upewnić, że komunikacja z czujnikiem działa poprawnie, dzięki czemu możemy też zweryfikować model podłączonego czujnika. Jednak jeśli przy takim programie czujnik zostanie odłączony, to układ się „zawiesi” – wszystko dlatego, że funkcja odczytująca dane jako tzw. timeout ma podane HAL_MAX_DELAY (później to zmienimy).
Odczyt temperatury z czujnika LPS25HB
Po włączeniu zasilania czujnik LPS25HB przechodzi w tryb uśpienia, dzięki czemu pobiera mało prądu. Musimy więc pamiętać, aby wybudzić go przed wykonaniem pomiarów. Do tego potrzebny jest kolejny rejestr, czyli CTRL_REG1.
Domyślnie wszystkie bity tego rejestru są wyzerowane, w tym bit PD, który odpowiada właśnie za stan pracy czujnika – zero oznacza uśpienie, a logiczna jedynka to obudzenie układu. Ważne są dla nas też bity ODR1 i ODR2. Ich znaczenie zostało opisane w dokumentacji za pomocą poniższej tabeli.
Domyślna wartość tych bitów oznacza, że pomiary wykonywane są „na żądanie”, ale LPS25HB może także działać w trybie automatycznego pomiaru z zadaną częstotliwością – i z tej opcji chcemy właśnie skorzystać. Aby ustawić te bity rejestru CTRL_REG1, użyjemy oczywiście funkcji HAL_I2C_Mem_Write, ale dla wygody zdefiniujemy sobie tutaj również funkcję pomocniczą.
1 2 3 4 |
void lps_write_reg(uint8_t reg, uint8_t value) { HAL_I2C_Mem_Write(&hi2c1, LPS25HB_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY); } |
Wybudzenie układu z uśpienia oraz ustawienie odpowiedniej częstotliwości pomiaru uzyskamy, jeśli do wspomnianego rejestru zapiszemy binarnie 0b11000000 (heksadecymalnie 0xC0). A zatem wystarczy, że wywołamy w programie (po sprawdzeniu rejestru WHO_AM_I) poniższy kod:
1 2 |
lps_write_reg(LPS25HB_CTRL_REG1, 0xC0); HAL_Delay(100); |
Po uruchomieniu tego programu czujnik zostanie obudzony, a następnie włączony zostanie pomiar ciśnienia i temperatury z częstotliwością 25 Hz. Zapis 0xC0 jest jednak mało czytelny – dla wygody można dodać do programu definicje bitów z rejestru CTRL_REG1:
1 2 3 4 |
#define LPS25HB_CTRL_REG1_PD 0x80 #define LPS25HB_CTRL_REG1_ODR2 0x40 #define LPS25HB_CTRL_REG1_ODR1 0x20 #define LPS25HB_CTRL_REG1_ODR0 0x10 |
Wówczas dane do tego rejestru możemy zapisać w zdecydowanie bardziej przyjaznej wersji. Poniższy zapis należy interpretować tak, że do rejestru CTRL_REG1 wstawiamy „jedynki” na miejscu bitu opisanego jako PD oraz ODR2 – pozostałe bity pozostają niezmienione (tutaj zera). W zapisie tym wykorzystany jest operator alternatywy bitowej, czyli „|”.
1 2 |
lps_write_reg(LPS25HB_CTRL_REG1, LPS25HB_CTRL_REG1_PD | LPS25HB_CTRL_REG1_ODR2); HAL_Delay(100); |
Tworzenie programów w taki sposób to na pewno dobra praktyka, wróćmy jednak do najważniejszego, czyli do komunikacji z czujnikiem. Po ustawieniu konfiguracji dodaliśmy jeszcze małe opóźnienie. Pomiary będą wykonywane z częstotliwością 25 Hz, dlatego jeśli poczekamy 100 ms, to wynik powinien być już dla nas dostępny.
Nie jest to oczywiście idealne rozwiązanie. Aktywne czekanie nie jest najlepszą metodą, ale najprostszą – a podczas nauki to dla nas dość ważne.
Teraz możemy przejść do odczytu temperatury. Jej aktualną wartość znajdziemy łącznie w „aż” dwóch rejestrach: TEMP_OUT_L (0x2B) oraz TEMP_OUT_H (0x2C). Wynikiem jest liczba 16-bitowa w tzw. kodzie uzupełnieniowym do 2. Jest to nic innego jak najzwyklejszy int16_t. Rejestry są jednak 8-bitowe, więc czujnik dzieli pomiar na dwa osobne rejestry. W związku z tym w celu poznania zmierzonej temperatury musimy dokonać odczytu z dwóch rejestrów, a następnie połączyć te dane, aby utworzyć 16-bitową wartość.
W tym celu możemy dwa razy wywołać funkcję lps_read_reg, ale będzie to mniej efektywne niż odczytanie wartości z obu rejestrów naraz.
Tutaj pomocna jest mała sztuczka – okazuje się, że można odczytywać lub zapisywać wiele rejestrów jednocześnie, jeśli mają one kolejne adresy. Jak widzimy, zapewne nieprzypadkowo TEMP_OUT_H ma numer o jeden większy niż TEMP_OUT_L, dzięki czemu możemy łatwo odczytać obie wartości naraz. Jest to cecha tego konkretnego czujnika (a nie interfejsu I2C) – informacji na temat tego typu ułatwień trzeba zawsze szukać w notach katalogowych konkretnych elementów.
W celu dokonania takiego pomiaru wystarczy odwołać się do danego rejestru z ustawionym na jeden najwyższym bitem. Weźmy przykładowo rejestr TEMP_OUT_L, czyli 0x2B. Wartość tę binarnie da się zapisać jako 00101011. Jeśli chcielibyśmy od razu odczytać kolejny rejestr, musielibyśmy odwołać się do rejestru 10101011. Najprościej można zrobić to w kodzie za pomocą alternatywy bitowej. Taka operacja może więc przyjąć postać LPS25HB_TEMP_OUT_L | 0x80. Zapis ten sprawi, że odwołamy się właśnie do 10101011 zamiast do 00101011 – czujnik będzie wtedy wiedział, że ma odesłać 2 bajty.
Oczywiście równie dobrze moglibyśmy zadeklarować adres 10101011 w formie nowej definicji lub wpisać go „na sztywno” do kodu. Specjalnie pokazujemy jednak, jakie rozwiązania spotyka się podczas analizowania bardziej rozbudowanych programów.
Finalnie kod, który wyświetla informacje o znalezionym czujniku, wybudza go, włącza automatyczne pomiary i wyświetla aktualną temperaturę, powinien wyglądać tak jak poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
printf("Searching...\n"); uint8_t who_am_i = lps_read_reg(LPS25HB_WHO_AM_I); if (who_am_i == 0xBD) { printf("Found: LPS25HB\n"); lps_write_reg(LPS25HB_CTRL_REG1, 0xC0); HAL_Delay(100); int16_t temp; HAL_I2C_Mem_Read(&hi2c1, LPS25HB_ADDR, LPS25HB_TEMP_OUT_L | 0x80, 1, (uint8_t*)&temp, sizeof(temp), HAL_MAX_DELAY); printf("T = %d\n", temp); } else { printf("Error: (0x%02X)\n", who_am_i); } |
Po uruchomieniu takiego kodu otrzymamy informację, że temperatura to przykładowo… −7230. To nie wina błędnego pomiaru – po prostu czujnik nie zwraca temperatury w stopniach Celsjusza. Otrzymaną wartość musimy przeliczyć zgodnie z formułą, która dostępna jest w dokumentacji.
Musimy więc zmienić linijkę wyświetlającą temperaturę na następującą:
1 |
printf("T = %.1f*C\n", 42.5f + temp / 480.0f); |
Teraz wynik będzie wyświetlany jako liczba zmiennoprzecinkowa, musimy tylko włączyć obsługę takich liczb za pomocą printf (Project > Properties > C/C++ Build > Settings > MCU Settings > Use float with printf...). Po uruchomieniu tej wersji programu otrzymamy już poprawną temperaturę.
Nie przeprowadzaliśmy kalibracji czujnika, więc pomiar może odbiegać o kilka stopni od wartości rzeczywistej (ale można to wyregulować offesetem – więcej w dokumentacji), jednak uzyskana wartość powinna być przynajmniej zbliżona do rzeczywistej.
Pomiar ciśnienia za pomocą czujnika LPS25HB
Ciśnienie odczytamy z trzech rejestrów: PRESS_OUT_XL, PRESS_OUT_L oraz PRESS_OUT_H. Wynikiem będzie więc 24-bitowa liczba ze znakiem. W języku C nie znajdziemy typu odpowiadającemu takiej liczbie, ale możemy użyć zwykłej liczby 32-bitowej, czyli int32_t.
Przy domyślnych ustawieniach czujnika ciśnienie nigdy nie powinno przyjmować ujemnych wartości, dlatego nie musimy przejmować się znakiem. Wystarczy więc, że najwyższe 8 bitów uzupełnimy zerami, i wszystko powinno już działać. Odczyt ciśnienia wygląda podobnie do odczytu temperatury – dodajemy więc do naszego programu następujące linie:
1 2 3 |
int32_t pressure = 0; HAL_I2C_Mem_Read(&hi2c1, LPS25HB_ADDR, LPS25HB_PRESS_OUT_XL | 0x80, 1, (uint8_t*)&pressure, 3, HAL_MAX_DELAY); printf("p = %lu\n", pressure); |
Warto zwrócić uwagę na dwa szczegóły. Po pierwsze, zmiennej pressure przypisujemy początkową wartość 0. Później wczytujemy tylko 3 bajty, więc najwyższy bajt tej zmiennej nie będzie zmieniany. Gdybyśmy nie ustawili wartości zmiennej na 0, wartość tego bajtu mogłaby być przypadkowa.
Druga zmiana to wartość 3 w parametrze wywołania HAL_I2C_Mem_Read. Zmienna ma 32 bity, czyli 4 bajty, my jednak odczytujemy tylko wartość 24-bitową, więc nie możemy użyć tutaj wywołania sizeof, dlatego ręcznie wpisaliśmy informację o 3 bajtach.
Po uruchomieniu kod zadziała, ale pokaże inną wartość, niż oczekujemy, bo będzie to np. 4186431. Łatwo odgadnąć, że również tym razem wynik wymaga odpowiedniego przeliczenia. Tym razem formuła jest o wiele łatwiejsza, wystarczy podzielić pomiar przez 4096.
1 |
printf("p = %lu hPa\n", pressure / 4096); |
Tym razem uzyskamy już „poprawny” wynik:
Wynik jest „poprawny”, bo coś mierzymy, ale nie omówiliśmy jeszcze jednostki pomiaru, a do tego zupełnie nie wiemy, jak interpretować taki pomiar. Tutaj właśnie kończy się większość poradników na temat czujników ciśnienia (wyświetlenie pomiarów i koniec) – dla nas to dopiero początek! Jednak zanim przejdziemy dalej, zróbmy porządek z kodem – pora na własną bibliotekę.
Biblioteka dla czujnika LPS25HB
Zacznijmy od dodania pliku nagłówkowego lps25hb.h, który powinien mieć poniższą treść:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#pragma once #include "stm32l4xx.h" // Inicjalizacja czujnika LPS25HB // Obudzenie i automatyczne pomiary // return - HAL_OK lub HAL_ERROR HAL_StatusTypeDef lps25hb_init(void); // Odczyt temperatury // return - wynik w stopniach C float lps25hb_read_temp(void); // Odczyt ciśnienia // reuturn - wynik w hPa float lps25hb_read_pressure(void); |
Plik nagłówkowy stm32l4xx.h włączamy tylko po to, aby mieć dostęp do typu HAL_StatusTypeDef. Jest to typ wyliczeniowy – użyjemy go, aby zwrócić informację o obecności lub braku naszego czujnika. Jeśli odczyt rejestru WHO_AM_I zwróci oczekiwaną wartość, to funkcja lps25hb_init będzie zwracała HAL_OK, a w przypadku błędu – HAL_ERROR. Pozostałe dwie funkcje posłużą nam do odczytywania aktualnej temperatury oraz ciśnienia.
Nasza biblioteka udostępnia bardzo prosty w użyciu „interfejs”. Czas wykorzystać napisany wcześniej kod do stworzenia kodu biblioteki. Wszystkie funkcje pomocnicze koniecznie definiujemy ze słówkiem kluczowym static – dzięki temu nie będą one widoczne poza naszym modułem. To dobra praktyka, dzięki której unikamy konfliktu funkcji z różnych bibliotek (pisaliśmy już o tym wcześniej).
Tworzymy zatem plik lps25hb.c i wstawiamy do niego poniższą treść:
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 |
#include "lps25hb.h" #include "i2c.h" #define LPS25HB_ADDR 0xBA #define LPS25HB_WHO_AM_I 0x0F #define LPS25HB_CTRL_REG1 0x20 #define LPS25HB_CTRL_REG2 0x21 #define LPS25HB_CTRL_REG3 0x22 #define LPS25HB_CTRL_REG4 0x23 #define LPS25HB_PRESS_OUT_XL 0x28 #define LPS25HB_PRESS_OUT_L 0x29 #define LPS25HB_PRESS_OUT_H 0x2A #define LPS25HB_TEMP_OUT_L 0x2B #define LPS25HB_TEMP_OUT_H 0x2C #define TIMEOUT 100 static uint8_t lps_read_reg(uint8_t reg) { uint8_t value = 0; HAL_I2C_Mem_Read(&hi2c1, LPS25HB_ADDR, reg, 1, &value, sizeof(value), TIMEOUT); return value; } static void lps_write_reg(uint8_t reg, uint8_t value) { HAL_I2C_Mem_Write(&hi2c1, LPS25HB_ADDR, reg, 1, &value, sizeof(value), TIMEOUT); } HAL_StatusTypeDef lps25hb_init(void) { if (lps_read_reg(LPS25HB_WHO_AM_I) != 0xBD) return HAL_ERROR; lps_write_reg(LPS25HB_CTRL_REG1, 0xC0); return HAL_OK; } float lps25hb_read_temp(void) { int16_t temp; if (HAL_I2C_Mem_Read(&hi2c1, LPS25HB_ADDR, LPS25HB_TEMP_OUT_L | 0x80, 1, (uint8_t*)&temp, sizeof(temp), TIMEOUT) != HAL_OK) Error_Handler(); return 42.5f + temp / 480.0f; } float lps25hb_read_pressure(void) { int32_t pressure = 0; if (HAL_I2C_Mem_Read(&hi2c1, LPS25HB_ADDR, LPS25HB_PRESS_OUT_XL | 0x80, 1, (uint8_t*)&pressure, 3, TIMEOUT) != HAL_OK) Error_Handler(); return pressure / 4096.0f; } |
Większość kodu jest identyczna z zastosowanym poprzednio, zmienione zostały jednak czasy oczekiwania używane w HAL_I2C_Mem_Read/Write. Poprzednio było to HAL_MAX_DELAY, tym razem chcemy jednak, aby nasz program nie zawieszał się w przypadku braku odpowiedzi. Dlatego zdefiniowaliśmy stałą TIMEOUT, dzięki której będziemy oczekiwać tylko 100 ms.
Teraz możemy wrócić do pliku głównego, czyli main.c. Usuwamy z niego poprzednio napisany kod i włączamy bibliotekę. Zatem zaczynamy od dodania pliku nagłówkowego:
1 |
#include "lps25hb.h" |
Natomiast program główny zmienimy na następujący:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* Infinite loop */ /* USER CODE BEGIN WHILE */ printf("Searching...\n"); if (lps25hb_init() == HAL_OK) { printf("OK: LPS25HB\n"); } else { printf("Error: LPS25HB not found\n"); Error_Handler(); } HAL_Delay(100); while (1) { printf("T = %.1f*C\n", lps25hb_read_temp()); printf("p = %.1f hPa\n", lps25hb_read_pressure()); HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ |
Teraz nasz program sprawdza, czy udało się nawiązać połączenie z czujnikiem, i jeśli jest ono poprawne, to co sekundę wyświetla aktualną temperaturę i aktualne ciśnienie. Obsługa błędów nie jest jeszcze idealna, ale działa trochę lepiej niż wcześniejsza wersja.
Jeśli odłączymy zasilanie czujnika i zresetujemy program, to tym razem otrzymamy w konsoli informacje o błędzie komunikacji (dzięki temu, że nasz timeout to tylko 100 ms). Program się zawiesza, ale przynajmniej wyświetla informację o przyczynie tego błędu.
Interpretacja pomiarów ciśnienia
Spróbujemy zagłębić się dokładniej w tematykę pomiarów ciśnienia, bo nie jest to wcale takie proste, jak mogłoby się wydawać, ale opanowanie tych tematów pozwoli na wykorzystanie tego czujnika do znacznie ciekawszych operacji – przy okazji omówimy jeszcze kilka kwestii związanych z LPS25HB.
Jednostki ciśnienia atmosferycznego
Zacznijmy od jednostek. W przypadku temperatury funkcja zwraca wynik w stopniach Celsjusza, co przeważnie nam wystarcza. Czasem przydatne są wyniki w kelwinach – wystarczy wtedy dodać 273,15, aby przeliczyć wartość ze stopni Celsjusza na kelwiny.
Z ciśnieniem sytuacja jest nieco bardziej skomplikowana. W obowiązującym nas układzie jednostek SI ciśnienie reprezentowane jest w paskalach (Pa). Jednak 1 Pa to mała wartość, więc często używane są kilopaskale (1 kPa = 1000 Pa), a nawet megapaskale (1 MPa = 1 000 000 Pa). Aby jeszcze było „trudniej”, prognoza pogody (oraz nasz czujnik) zwraca wyniki w hektopaskalach (1 hPa = 100 Pa).
Ciśnienie atmosferyczne zależy od wielu czynników, a w szczególności od miejsca wykonywania pomiaru – im wyżej położony jest punkt pomiaru, tym niższe jest ciśnienie. Wysoko w górach ciśnienie jest niższe niż nad morzem. Jako odniesienie przyjmuje się uśrednione ciśnienie atmosferyczne na poziomie morza o wartości 1013,25 hPa. W wielu „gotowcach” można znaleźć stałą o takiej wartości – warto więc wiedzieć, skąd się to bierze.
Średnie ciśnienie na poziomie morza to tzw. atmosfera fizyczna (1 atm = 1013,25 hPa).
Wzór barometryczny
Wykres ciśnienia względem wysokości nad poziomem morza wygląda tak jak poniżej. Warto z tego zapamiętać chociaż tyle, że ciśnienie maleje wraz ze wzrostem wysokości, a konkretnie na każde 100 metrów wzrostu wysokości ciśnienie spada o mniej więcej 11,7 hPa – za chwilę udowodnimy, że to całkiem dokładne przybliżenie, które może nam sporo ułatwić.
Ciśnienie bezwzględne w naszej lokalizacji możemy obliczyć za pomocą wzoru barometrycznego, który oprócz kilku stałych fizycznych wykorzystuje również informacje o ciśnieniu względnym w punkcie odniesienia, o naszej wysokości oraz o temperaturze powietrza (właśnie w kelwinach).
Ten wzór jest tutaj bardzo ważny. Warto zatem przyjrzeć mu się bliżej – mamy trzy stałe fizyczne, które na pewno świetnie znają wszyscy studenci, więc przypominamy je tylko dla formalności:
- g – przyspieszenie ziemskie (9,80665 m/s2)
- µ – masa molowa powietrza (0,0289644 kg/mol)
- R – stała gazowa (8,31446261815324 J/molK)
Mamy też trzy zmienne:
- h – wysokość wykonywania pomiaru
- T – temperatura (w kelwinach)
- p0 – ciśnienie atmosferyczne na poziomie odniesienia (względne)
Wniosek z tego jest taki, że znając ciśnienie na poziomie morza, wysokość oraz aktualną temperaturę, możemy policzyć ciśnienie panujące w danym miejscu. W związku z tym po przekształceniu wzoru na podstawie zmierzonego ciśnienia będziemy mogli obliczyć np. wysokość względem poziomu morza.
Ciśnienie względne, bezwzględne i prognoza pogody
Wstęp teoretyczny już prawie za nami, teraz jeszcze kilka nowych nazw. Ciśnienie panujące w danym punkcie nazywane jest ciśnieniem bezwzględnym (lub absolutnym). We wzorze barometrycznym powyżej jest to wartość p (czyli wynik obliczeń) – jest to wartość, którą zwraca nasz czujnik LPS25HB.
Czujnik LPS25HB zwraca ciśnienie bezwzględne (absolutne) w hektopaskalach!
Natomiast ciśnienie względne jest to ciśnienie, jakie wskazywałby czujnik, gdyby wykonywał pomiar w danych warunkach pogodowych, ale na wysokości 0 metrów nad poziomem morza – wartość ta we wzorze barometrycznym jest opisana jako p0.
Ciśnienie względne i bezwzględne są sobie równe, gdy jesteśmy na poziomie morza. Jeśli wykonujemy pomiar w innym miejscu (na innej wysokości), to możemy zmierzyć już tylko ciśnienie bezwzględne, a względne obliczymy w tej sytuacji właśnie za pomocą wzoru barometrycznego.
Ciśnienie podawane w prognozie pogody to ciśnienie względne.
Teraz powinno być już jasne, dlaczego musieliśmy zawrzeć tak dużo teorii w tej części kursu. Wyniki odczytane z naszego czujnika wymagają odpowiednich przeliczeń, inaczej będą mało przydatne – teraz możemy sensownie wykorzystać zebrane informacje.
Przeliczanie ciśnienia na inne jednostki
W związku z tym, że znamy już teorię, możemy wrócić do praktyki. Zacznijmy od „zdobycia” ciśnienia wzorcowego – możemy je pobrać z aktualnej prognozy pogody. Informację na ten temat znajdziemy np. na stronie https://meteo.imgw.pl/ – wystarczy wpisać tam aktualną lokalizację.
Kolejna ważna informacja to położenie naszego punktu pomiarowego. Tutaj również odpowiednie dane są łatwe do wyszukania – pomocny będzie serwis https://www.wysokosciomierz.pl. Po kliknięciu w odpowiednie miejsca na mapie wyświetlona zostanie informacja o wysokości nad poziomem morza – w naszym przypadku było to 37 m n.p.m.
Teraz możemy obliczyć, ile w danej lokalizacji powinno wynosić ciśnienie bezwzględne. W tym celu do wcześniejszego wzoru barometrycznego podstawiamy stałe fizyczne oraz uzyskane przed chwilą dane (trzeba tylko pamiętać o podaniu temperatury w kelwinach). Czyli w naszym przypadku:
- ciśnienie względne: 1020,5 hPa
- temperatura: 18,3*C = 18,3 + 273,15 = 291,45 K
- wysokość nad poziomem morza: 37 m
Wynik uzyskany za pomocą tego długiego wzoru to 1016,1 hPa. Nieco mniej dokładny wynik da się uzyskać też o wiele prościej. Wspominaliśmy, że ciśnienie spada o mniej więcej 11,7 hPa na każde 100 metrów. Z prostej proporcji można więc wyliczyć, że na wysokości 37 m n.p.m. ciśnienie powinno być niższe o (37 * 11,7) / 100 = 4,33, czyli powinno wynosić 1020,5 − 4,33 = 1015,67 hPa. Jak widać, ta prosta metoda daje całkiem dobre wyniki.
Pomiar ciśnienia względnego
Teraz już wiemy, jakiej wartości ciśnienia powinniśmy oczekiwać w naszej lokalizacji. Wracamy do kodu, uruchamiamy go i sprawdzamy wskazania ciśnienia. W naszym przypadku było to 1022,2 hPa zamiast spodziewanych 1016,1 hPa.
Łatwo policzyć, że nasz błąd to 6,1 hPa, czyli sporo. Błąd ten wynika z kilku czynników. Po pierwsze, do obliczeń wykorzystaliśmy pomiar ciśnienia z internetowej stacji pogodowej, której czujniki znajdują się w tym samym mieście co my, ale realnie są jednak w innej lokalizacji. Po drugie, wykorzystaliśmy mało precyzyjną informację na temat naszej wysokości względem poziomu morza – zakładając, że nie robimy tego eksperymentu, leżąc brzuchem na środku parkingu, tylko znajdujemy się w budynku na jakimś piętrze (lub w piwnicy), nasza rzeczywista wysokość względem poziomu morza jest inna.
Po trzecie, czujnik LPS25HB to dość skomplikowany układ, który powinien zostać skalibrowany po tym, gdy został przylutowany do modułu. Jest on kalibrowany przez producenta podczas produkcji, ale wysoka temperatura sprawia, że konieczna jest ponowna kalibracja (więcej informacji na ten temat znaleźć można w dokumentacji).
Możemy teraz sami dokonać kalibracji, ale najpierw przekształćmy wzór. Nasz czujnik zwraca wartość ciśnienia bezwzględnego oraz temperaturę. Możemy więc przekształcić wzór barometryczny, tak aby obliczyć ciśnienie względne.
Ręczne obliczenia takich wartości są mało pasjonujące. Mamy jednak wydajny mikrokontroler STM32L4 z koprocesorem arytmetycznym, możemy więc przepisać kod w taki sposób, aby układ zwracał nam od razu samodzielnie ciśnienie względne.
Najpierw musimy dodać bibliotekę math.h do naszego projektu:
1 |
#include <math.h> |
Następnie w pętli głównej programu dodajemy niezbędne przeliczenia:
1 2 3 4 5 6 7 8 |
const float h = 37; // nasza wysokość n.p.m. float temp = lps25hb_read_temp() + 273.15; float p = lps25hb_read_pressure(); float p0 = p * exp(0.034162608734308 * h / temp); printf("p0 = %.2f hPa\n", p0); HAL_Delay(1000); |
Teraz program będzie wyświetlał ciśnienie względne, czyli takie, które możemy od razu porównywać z prognozą pogody (dla przypomnienia: było to 1020,5 hPa):
Kalibracja czujnika
Nasze wyniki obarczone są sporym błędem. Uzyskaliśmy 1026,47 hPa zamiast 1020,5 hPa – możemy jednak dokonać teraz kalibracji czujnika. W dokumentacji znajdziemy informację o tym, że czujnik jest kalibrowany podczas produkcji, a później konieczna jest kalibracja po lutowaniu.
Wśród rejestrów układu LPS25HB znajdziemy dwa odpowiedzialne za kalibrację: RPDS_L oraz RPDS_H. Razem tworzą one 16-bitową wartość, która będzie korygowała odczyty z czujnika.
Zacznijmy od dodania do pliku lps25hb.h deklaracji nowej funkcji:
1 2 3 |
// Kalibracja czujnika ciśnienia // value - 16-bitowa korekcja pomiaru void lps25hb_set_calib(uint16_t value); |
Teraz przechodzimy do pliku lps25hb.c i dodajemy definicje rejestrów RPDS_L oraz RPDS_H:
1 2 |
#define LPS25HB_RPDS_L 0x39 #define LPS25HB_RPDS_H 0x3A |
Dodajemy również funkcję lps25hb_set_calib:
1 2 3 4 5 |
void lps25hb_set_calib(uint16_t value) { lps_write_reg(LPS25HB_RPDS_L, value); lps_write_reg(LPS25HB_RPDS_H, value >> 8); } |
Na koniec do funkcji main dodajemy wywołanie lps25hb_set_calib wraz z parametrem kalibracyjnym (po funkcji inicjalizującej działanie czujnika). Powinien on odpowiadać błędowi pomiaru pomnożonemu przez 16, czyli jeśli błąd wynosił tym razem 1026,47 − 1020,5 = 5,97, to wywołujemy ją z parametrem 96 (zaokrąglony wynik 16 * 5,97).
1 |
lps25hb_set_calib(96); |
Oczywiście wartość użyta jako parametr zależy od konkretnego czujnika, więc każdy powinien podać obliczoną samemu wartość. Teraz wyniki powinny być już wystarczająco bliskie wartości wzorcowej:
Wartość kalibracyjna będzie inna dla każdego czujnika. Jej przechowywanie w kodzie programu jest prostym, ale nie najlepszym rozwiązaniem – o wiele lepiej byłoby wartość kalibracyjną zapisywać w pamięci EEPROM, którą poznaliśmy w poprzedniej części kursu. Dzięki temu osoba korzystająca z naszego urządzenia mogłaby samodzielnie kalibrować czujnik (np. za pomocą odpowiedniej opcji w menu na wyświetlaczu graficznym).
Pomiar wysokości bezwzględnej
Umiemy już zmierzyć ciśnienie bezwzględne, wiemy, jak przeliczyć je na wartość względną i wykonać kalibrację czujnika. Możemy więc przystąpić do budowania własnej stacji pogodowej albo wykorzystać nasz czujnik do nieco innego celu.
Do tej pory używaliśmy wzoru barometrycznego do obliczania ciśnienia. Teraz wykorzystamy wzór w przekształconej formie – wszystko po to, aby znając temperaturę oraz ciśnienie, obliczyć wysokość, na jakiej dokonujemy pomiaru. Wzór po przekształceniu będzie wyglądał następująco:
Parametr T to jak zwykle temperatura w kelwinach, p to ciśnienie absolutne zmierzone przez czujnik, a p0 to ciśnienie w punkcie odniesienia. Wiele przykładowych programów dostępnych w sieci jako p0 przyjmuje ciśnienie średnie na poziomie morza, czyli 1013,25 hPa. Zacznijmy od napisania programu, który będzie działał właśnie w ten sposób.
Inicjalizacja i kalibracja czujnika pozostają bez zmian, a pętla główna wygląda teraz następująco:
1 2 3 4 5 6 7 |
float p0 = 1013.25; float temp = lps25hb_read_temp() + 273.15; float p = lps25hb_read_pressure(); float h = -29.271769 * temp * log(p / p0); printf("h = %.2f m\n", h); HAL_Delay(1000); |
Po uruchomieniu program pokazuje następujący rezultat (Twoje wyniki mogą być zupełnie inne):
Błąd może być duży, a będzie on wynikał z użycia wartości średniej. Według prognozy pogody ciśnienie w naszej lokalizacji wynosi 1020,5 hPa, spróbujmy więc użyć go jako parametru p0:
1 |
float p0 = 1020.5; |
Tym razem wynik powinien być już zbliżony do prawdziwego. W naszym przypadku wysokość odczytana z mapy to 37 m, a program wskazywał około 45 m – różnica wynika głównie z tego, że znajdowaliśmy się w budynku.
Jak widać, pomiar wysokości wymaga ustawienia ciśnienia względnego odpowiednio do panujących warunków atmosferycznych. To dlatego wysokościomierze ciśnieniowe używane np. w lotnictwie wymagają dokładnych danych pogodowych. Dopiero wiedząc, jakie ciśnienie panuje w znanym punkcie (np. na najbliższym lotnisku), można poprawnie obliczyć wysokość.
Pomiar wysokości względnej
Barometru możemy jednak użyć również do mierzenia wysokości względnej (np. względem biurka lub punktu, w którym go włączyliśmy). Dzięki temu, jeśli podłączymy układ do baterii i zabierzemy go w plecaku na wycieczkę rowerową, to będziemy mogli na bieżąco śledzić różnice wysokości.
Wystarczy, że w programie najpierw damy czujnikowi nieco więcej czasu na stabilizację. Dotychczas czekaliśmy tylko 100 ms, teraz poczekamy aż 5 s (za chwilę ten dużo dłuższy czas będzie ważny). Następnie wykonamy pomiar ciśnienia wzorcowego, a w pętli będziemy obliczać różnice.
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 |
/* Infinite loop */ /* USER CODE BEGIN WHILE */ printf("Searching...\n"); if (lps25hb_init() == HAL_OK) { printf("OK: LPS25HB\n"); } else { printf("Error: LPS25HB not found\n"); Error_Handler(); } HAL_Delay(5000); float p0 = lps25hb_read_pressure(); while (1) { float temp = lps25hb_read_temp() + 273.15; float p = lps25hb_read_pressure(); float h = -29.271769 * temp * log(p / p0); printf("h = %.2f m\n", h); HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ |
Po uruchomieniu zobaczymy efekt podobny do poniższego. Układ działa – jeśli go podniesiemy, widoczna będzie zmiana położenia, ale odczyty będą niestabilne. Niestety, nawet jeśli czujnik będzie znajdował się w tym samym położeniu, to otrzymywane wyniki będą „skakać”.
Uśrednianie wyników z czujnika LPS25HB
Prostą metodą na zwiększenie precyzji pomiarów jest wykonanie ich wielokrotnie i obliczenie średniej. Czujnik LPS25HB ma możliwość automatycznego uśredniania wyników z 32 pomiarów. W obecnej konfiguracji czujnik wykonuje 25 pomiarów na sekundę, więc jeśli włączymy uśrednianie 32 wyników, to czas jego reakcji będzie wynosił nieco ponad sekundę, ale wyniki powinny być o wiele stabilniejsze.
Przechodzimy do pliku lps25hb.c oraz dodajemy definicję rejestru FIFO_CTRL:
1 |
#define LPS25HB_FIFO_CTRL 0x2E |
Czujnik posiada wbudowany bufor na 32 wyniki zorganizowany właśnie jako kolejka FIFO. Dotychczas go nie używaliśmy, ale teraz nam się przyda. Najpierw musimy włączyć tę kolejkę – wykonamy to przez prostą zmianę w rejestrze CTRL_REG2.
Ustawienie bitu numer 6 (czyli wpisanie tam jedynki) sprawi, że kolejka FIFO zostanie uruchomiona. Będziemy jeszcze musieli ją skonfigurować w rejestrze FIFO_CTRL.
Kolejka może działać w różnych trybach – nas interesuje FIFO Mean mode, czyli użycie bufora do obliczania średniej wartości. Pola bit WTM_POINT pozwalają na wybór liczby próbek używanych do obliczania średniej – nas interesują 32 próbki.
Zapis do rejestrów CTRL_REG2 oraz FIFO_CTRL dodajemy do funkcji lps25hb_init:
1 2 3 4 5 6 7 8 9 10 11 |
HAL_StatusTypeDef lps25hb_init(void) { if (lps_read_reg(LPS25HB_WHO_AM_I) != 0xbd) return HAL_ERROR; lps_write_reg(LPS25HB_CTRL_REG1, 0xC0); lps_write_reg(LPS25HB_CTRL_REG2, 0x40); lps_write_reg(LPS25HB_FIFO_CTRL, 0xDF); return HAL_OK; } |
Teraz już powinno być jasne, dlaczego potrzebowaliśmy więcej czasu przed odczytem ciśnienia, które ma być później używane jako punkt odniesienia. Po prostu musimy poczekać, aż kolejka FIFO się zapełni. Co prawda w praktyce zajmuje to nieco ponad 1 s, ale lepiej dać czujnikowi więcej czasu na ustabilizowanie działania.
Układ powinien działać teraz znacznie lepiej – podniesienie płytki nawet o kilkadziesiąt centymetrów powinno dać widoczne efekty.
Nasz czujnik raczej nie sprawdzi się jako precyzyjny wysokościomierz lotniczy (takie precyzyjne czujniki są znacznie droższe i bardziej skomplikowane), ale na pewno jest to ciekawy przykład na to, jak można wykorzystać pozornie nudny pomiar ciśnienia.
Zadanie domowe
- Popraw kod z ćwiczeń w taki sposób, aby odczytywanie 24-bitowej wartości działało poprawnie również, gdy wartość zwracana przez czujnik byłaby ujemna.
- Dodaj do programu z ostatniego ćwiczenia obsługę diody RGB, której kolor powinien wskazywać różnicę wysokości (np. wartości ujemne kolor niebieski, wartości dodanie kolor czerwony). Wykorzystaj PWM do płynnej zmiany kolorów.
- Zapoznaj się z notą katalogową czujnika LPS25HB i zobacz jakie inne możliwości ma ten układ. Zwróć szczególną uwagę na rejestry REF_P_XL, REF_P_L i REF_P_H – postaraj się wykorzystać je w taki sposób, aby czujnik od razu zwracał różnicę ciśnienia względem punktu startowego.
Podsumowanie – co powinieneś zapamiętać?
Gotowe funkcje z biblioteki HAL sprawiają, że komunikacja za pomocą I2C jest bardzo prosta. Możemy skupić się więc na analizie not katalogowych używanych czujników, aby nawiązać z nimi komunikację. Warto też zapamiętać teorię na temat pomiarów ciśnienia – oczywiście nie trzeba znać na pamięć wzoru barometrycznego (wraz ze stałymi), ale przynajmniej warto wiedzieć, czego szukać.
W kolejnej części kursu wrócimy do tematu liczników (timerów). Wykorzystamy je do obsługi popularnych wyświetlaczy 7-segmentowych oraz czujnika ultradźwiękowego. Przy okazji wrócimy też na chwilę do analogowych peryferii mikrokontrolera i uruchomimy wzmacniacz.
Nawigacja kursu
Główny autor kursu: Piotr Bugalski
Współautor: Damian Szymański, ilustracje: Piotr Adamczyk
Oficjalnym partnerem tego kursu jest firma STMicroelectronics
Zakaz kopiowania treści kursów oraz grafik bez zgody FORBOT.pl
Powiązane wpisy
barometr, ciśnienie, i2c, kurs, kursSTM32L4, stm32l4
Trwa ładowanie komentarzy...