Czujnik stężenia pyłów w powietrzu HM3301
Kod w języku C
Pierwszą rzeczą jaką należy zrobić jest otwarcie drivera za pomocą funkcji open – listing 1.
Listing 1. Otwarcie interfejsu I2C
//otwarcie magistrali I2C BME280 ssp_err_t BME280_BUS_Open() { ssp_err_t status; status=BME280.p_api -> open(BME280.p_ctrl, BME280.p_cfg); return status; }
Argumentami funkcji open są dwie struktury: p_ctrl i p_cfg. Struktura p_ctrl zawiera informację opisującą interfejs i flagę określającą czy interfejs został prawidłowo otwarty. Struktura p_cfg zawiera wszystkie konfiguracje pokazane na rysunku 6.
Funkcja open zwraca status:
- SSP_SUCCESS – interfejs otwarto poprawnie i można go używać,
- SSP_ERR_IN_USE – interfejs już otwarto i nie można go powtórnie otworzyć,
- SSP_ERR_INVALID_RATE – nie można wyliczyć wybranej prędkości transmisji.
Funkcja Callback
Ponieważ w konfiguracji określiliśmy nazwę Callback, to funkcje przesyłania porcji danych po magistrali nie są blokujące. Użytkownik musi zdefiniować funkcję callback wywoływaną przez zdarzenia zgłaszane przez funkcje zapisywania i odczytywania danych z magistrali. Dla drivera magistrali I2C zdefiniowano w SSP zdarzenia I2C_EVENT_ABORTED (transfer nie dokończony), I2C_EVENT_RX_COMPLETE (operacja odczytywania zakończona sukcesem) i I2C_EVENT_TX_COMPLETE (operacja zapisywania zakończona sukcesem). Dla naszych potrzeb zdefiniowałem dwie zmienne globalne i funkcję BME280_ Callback – listing 2.
Listing 2. Funkcja BME280_Callback
volatile void BME280_Callback(i2c_callback_args_t * p_args); volatile uint8_t BME280_data_tx,BME280_data_rx; //*********************************************************** //prototyp funkcji callback dla kontroli transmisji I2C //********************************************************** volatile void BME280_Callback(i2c_callback_args_t * p_args) { if(I2C_EVENT_TX_COMPLETE == p_args->event) { //zapis danych na magistralę zakończony sukcesem BME280_data_tx = 1; } if(I2C_EVENT_RX_COMPLETE == p_args->event) { //odczyt danych z magistrali zakończony sukcesem BME280_data_rx = 1; } if(I2C_EVENT_ABORTED == p_args->event) { //transfer danych zakończony niepowodzeniem BME280_data_rx = 10; BME280_data_tx = 10; } }
Poprawne zakończenie odczytania lub zapisania danych jest sygnalizowane wpisaniem jedynki do odpowiednich zmiennych. Wpisanie wartości 10 sygnalizuje transfer danych, który nie zakończył się sukcesem. Użytkownik musi przed wywołaniem funkcji zapisu i odczytu wyzerować odpowiednią zmienną i po wykonaniu funkcji write, lub read czekać na wpisanie do tej zmiennej wartości 1.
Odczyt i zapis danych
Do odczytywania i zapisywania danych z czujnika BME280 napisano dwie funkcje: BME280_I2C_Write i BME280_I2C_Read. Funkcje to adresują urządzenie slave o adresie 0x77. Do tej samej magistrali I2C dołączono czujnik pyłów o adresie 0x40. W rozbudowanych systemach, gdzie transmisja może być inicjowana asynchronicznie w dowolnym momencie, można wykorzystywać framework’a używającego jednego interfejsu i wielu driverów o różnych adresach. Synchronizacja pomiędzy wątkami wykorzystującymi transfer po magistrali I2C może się opierać o mechanizm semaforów. To wygodne i wydajne rozwiązanie możliwe do szybkiego skonfigurowania z poziomu Synergy Configurator. Ale w naszym przypadku, gdzie mamy dwa urządzenia slave, odczytywane sekwencyjnie jedno po drugim to trochę przerost formy nad treścią.
Przypomnijmy: mamy zdefiniowany i skonfigurowany driver BME_280 Master Driver z parametrami umieszczonymi w strukturze p_cfg. Oba urządzenia slave: BME280 i HM3301 mogą pracować z takimi samymi konfiguracjami, w tym z taka samą prędkością transmisji. Jedyna różnica to transfer danych z różnymi adresami slave. Wystarczy tylko przed wysłaniem danych do HM3301 zmienić w strukturze p_cfg domyślnie wygenerowany adres 0x77 przypisany do czujnika BME280 na adres 0x40 przypisany do czujnika HM3301 i wszystko powinno zadziałać. Oczywiście skoro nie używamy framework’a, to musimy szczególnie zadbać o prawidłowy ruch na magistrali żeby nie powodować kolizji. Jak wspomniałem, do tego celu wykorzystamy mechanizm callback. Będziemy blokować wysyłanie kolejnych danych jeżeli poprzedni transfer jeszcze się nie zakończył.
Do zmiany adresu wykorzystamy funkcję biblioteczną slaveAddressSet. Jej argumentami są wskaźnik na strukturę p_ctrl drivera BME280, adres slave i rozmiar adresu (7 bitów lub 10 bitów).
Na listingu 3 pokazano funkcję zapisu danych na magistralę I2C dla układu BME280, a na listingu 4 funkcję odczytu danych z BME280. W każdej z nich przed zainicjowaniem transferu ustawiamy jawnie adres 0x77 mimo, że domyślnie ustawiono to w konfiguratorze. Jest to niezbędne, bo poprzedni transfer danych mógł zmienić ten adres na 0x40 dla układu HM3301.
Kod funkcji zapisu i odczytu z BME280
Listing 3. Funkcja zapisu danych do układu BME280
ssp_err_t BME280_I2C_Write(uint8_t *buff,uint8_t size, bool stop) { ssp_err_t status; BME280_data_tx=0; //zmiana adresu slave status = BME280.p_api->slaveAddressSet(BME280.p_ctrl,0x77,I2C_ADDR_MODE_7BIT); status=BME280.p_api->write(BME280.p_ctrl, buff, size, stop); if(status != SSP_SUCCESS) { return(status); } while (BME280_data_tx != 1); return(status); }
Listing 4. Funkcja odczytu danych z układu BME280
ssp_err_t BME280_I2C_Read(uint8_t *buff, uint8_t size, bool stop) { ssp_err_t status; BME280_data_rx = 0; //zmiana adresu slave status = BME280.p_api->slaveAddressSet(BME280.p_ctrl,0x77,I2C_ADDR_MODE_7BIT); status=BME280.p_api->read(BME280.p_ctrl, buff, size, stop); if(status != SSP_SUCCESS) return status; while (BME280_data_rx != 1); return(status); }
Kod funkcji zapisu i odczytu z HM3301
Funkcje wymiany danych z układem HM3301 będą się różniły tylko adresem slave – listing 5 i listing 6. W tym przypadku nazwy drivera i struktur (BME280 zamiast HM3301) mogą być mylące, ale jak wspomniałem program jest uzupełnieniem wcześniej powstałej aplikacji obsługi układu BME280 i wykorzystuje istniejące definicje i konfiguracje.
Listing 5. Funkcja zapisu danych do układu HM3301
ssp_err_t HM3301_I2C_Write(uint8_t *buff,uint8_t size, bool stop) { ssp_err_t status; BME280_data_tx=0; status = BME280.p_api->slaveAddressSet(BME280.p_ctrl, 0x40, I2C_ADDR_MODE_7BIT); status=BME280.p_api->write(BME280.p_ctrl, buff, size, stop); if(status != SSP_SUCCESS) { return(status); } while (BME280_data_tx != 1); return(status); }
Listing 6. Funkcja odczytu danych z układu HM3301
ssp_err_t HM3301_I2C_Read(uint8_t *buff, uint8_t size, bool stop) { ssp_err_t status; BME280_data_rx = 0; status = BME280.p_api->slaveAddressSet(BME280.p_ctrl,0x40,I2C_ADDR_MODE_7BIT); status=BME280.p_api->read(BME280.p_ctrl, buff, size, stop); if(status != SSP_SUCCESS) return status; while (BME280_data_rx != 1); return(status); }
Funkcje z listingów 3…6 nadają się dobrze do zademonstrowania programowania transmisji, ale mają podstawową wadę – są to nadal funkcje blokujące program, kiedy coś z transmisją pójdzie nie tak jak powinno. Mimo, że jak wiemy użycie mechanizmu callback powoduje, że same funkcje biblioteczne transferu danych po magistrali nie są blokujące. W praktycznych aplikacjach czekanie na coś w nieskończonej pętli bez możliwości wyjścia z niej prędzej czy później skończy się całkowitym zablokowaniem programu. Ponieważ są to nasze funkcje, to możemy dowolnie zorganizować oczekiwanie na zakończenie transmisji. Ja w takich przypadkach stosuje na przykład proste kryterium czasowe określające maksymalny czas czekania na zmianę jakiejś flagi – w tym przypadku BME280_data_rx, lub BME280_data_tx. Połączono to z obsługą błędu.
Uaktywnienie interfejsu I2C
Jak wspomniałem, podstawowym interfejsem komunikacyjnym HM3301 jest UART, ale producent modułu postanowił wykorzystać tylko rezerwowy I2C. Dlatego przed odczytywaniem danych z sensora należy najpierw uaktywnić przesyłanie zmierzonych wartości przez interfejs I2C, a nie przez UART. Robi się to wysyłając przez I2C komendę z kodem 0x88. Bez niej dane z czujnika są kierowane do interfejsu UART, a przez I2C będą odczytywane przypadkowe wartości. Dokumentacja podaje, ze adres slave wynosi 0x80 a komenda ma strukturę pokazaną na rysunku 8.
Rysunek 8. Komenda uaktywnienia przesyłania danych przez I2C
W czasie testów przez jakiś czas nie udawało się wysłać komendy aktywującej I2C. Funkcja wysyłania danych z listingu 5 uporczywie zawieszała się na nieskończonej pętli oczekiwania na wpisanie do BME280_data_tx jedynki przez funkcje oddzwaniania. Argumentem wszelkich funkcji transferu danych po I2C jest 7 bitowy adres, który jest potem przesuwany o 1 w lewo i uzupełniany o bit R/W na najmłodszej pozycji. Z tego wynika, ze 7–bitowy adres slave nie może mieć wartości 0x80, tak jak to jest podane w dokumentacji. W rzeczywistości adres slave ma wartość 0x40 i taka wartość należy podać w funkcji slaveAddressSet. Po przesunięciu w lewo o jedna pozycję będzie to 0x80. Przy uruchamianiu nowego urządzenia, kiedy w wielu miejscach coś może pójść nie tak, nieścisłość w dokumentacji nie pomaga.
Listing 7. Wysłanie komendy aktywującej I2C do przesyłania danych z sensorów
ssp_err_t HM3301_writeControlRegisters(void) { ssp_err_t status; uint8_t data[2]; data[0] = 0x88; status = HM3301_I2C_Write(data, 1, false); return(status); }
Odczyt danych z HM3301 przez I2C
Funkcję odczytującą dane z HM3301 pokazano na listingu 8. Do bufora HM3301_buffer zadeklarowanego jako zmienna globalna odczytujemy kolejno 16 bajtów.
Listing 8. Odczytanie 16 bajtów z czujnika HM3301
ssp_err_t HM3301_Read (void) { ssp_err_t status; status = HM3301_I2C_Read(HM3301_buffer, 16, false); return status; }
Rysunek 9. Odczytywanie danych z czujnika przez magistralę I2C
Rysunek 10. Dane przesyłane z czujnika HM301 przez I2C
Na rysunku 9 pokazano transfer danych w czasie ich odczytywania z czujnika przez magistralę I2C, a na rysunku 10 fragment dokumentacji z rozmieszczeniem danych odczytywanych z układu sensora. Odczytujemy 16 bajtów, a do wyświetlania wykorzystujemy dane DATA7, 8, 9 i 10 zawierające stężenie pyłów PM2.5 w μg/m3 i PM10 w μg/m3.
Wyświetlanie danych z HM3301
Funkcja HM3301_disp kompletuje 16-bitowe wartości PM2.5 i PM10, konwertuje je na łańcuch znaków ASCII i wyświetla na ekranie w miejscu określonym przez współrzędne x i y (argumenty funkcji). Dodatkowo dodałem wyróżnienie kolorem zakresów mierzonych wielkości. Na przykład dla PM2.5 mniejszym od 36 μg/m3 wartość wyświetla się na zielono (wartości bezpieczne), dla PM2.5 od 36 do 84 μg/m3 wartość wyświetla się na żółto (stan ostrzegawczy), natomiast dla wartości powyżej 84 μg/m3 wartość wyświetla się na czerwono (stan alarmowy).
Listing 9. Wyświetlanie stężenia pyłów PM2.5 i PM10
void HM3301_disp(uint16_t x, uint16_t){ uint16_t PM25, PM10, color; char disp[20]; PM25 = HM3301_buffer[6]; PM25 = PM25 << 8; PM25 = PM25 | HM3301_buffer[7]; sprintf (disp,"PM 2.5 = %3d ug/m3",PM25); if (PM25 < 36) color = COLOR_GREEN; if (PM25 >= 36 && PM25 < 84) color = COLOR_GOLD; if (PM25 >= 84) color = COLOR_RED; SendTextRAM (x, y, disp, 20, color, CB); PM10 = HM3301_buffer[8]; PM10 = PM10 << 8; PM10 = PM10 | HM3301_buffer[9]; sprintf (disp,"PM 10 = %3d ug/m3",PM10); if (PM10 < 36) color = COLOR_GREEN; if (PM10 >= 36 && PM10 < 84) color = COLOR_GOLD; if (PM10 >= 84) color = COLOR_RED; SendTextRAM (x, y + 28, disp, 20, color, CB); }
Testowy układ pokazano na rysunku 11.
Podsumowanie
Czujnik HM3301 jest rozbudowanym sensorem wykorzystującym zaawansowaną technikę do mierzenia stężenia zanieczyszczeń. Nie jest przy tym drogi, a odczytywanie danych, jak wyżej pokazaliśmy, nie jest skomplikowane. Układ nie wymaga od strony programowej wykonywania żadnych zaawansowanych obliczeń, kalibracji i tym podobnych działań. Wystarczy odczytać kilka bajtów, złożyć z nich wartości 16-bitowe i je wyświetlić. Można go bez żadnych ograniczeń stosować w pomieszczeniach zamkniętych, a po wykonaniu odpowiednich osłon chroniących przed deszczem i śniegiem można go nawet umieścić na zewnątrz. Producent podaje w dokumentacji kilka wskazówek jak zamontować czujnik żeby pomiar nie był zafałszowany zasysaniem na przykład brudu z podłoża. Sensor ma wiele obszarów zastosowania, m.in. w automatycznych układach filtrowania i nawiewu powietrza, w układach klimatyzacji, stacjach monitorowania jakości powietrza, układach IoT itp.
W czasie testów wykonywałem pomiary w pomieszczeniach zamkniętych i na zewnątrz. Jak się okazało szczególnie jesienią i zimą wietrzenie pomieszczeń zamkniętych często skutkowało znaczącym wzrostem zanieczyszczeń pyłowych. Niestety tam gdzie mieszkam poziom zanieczyszczeń w okresie jesienno-zimowym potrafił czasami osiągać wartości powyżej 150 μg/m3, mimo że nie jest to centrum miasta z dużym ruchem samochodowym, a raczej spokojne miejsce z dużą ilością zieleni. Jak łatwo się domyśleć takie poziomy zanieczyszczeń całkiem sprawnie wytwarzają kominy okolicznych domów, gdyż w lecie poziom zanieczyszczeń prawie nigdy nie przekracza 35 μg/m3.