Zanieczyszczenia w małych miastach i na wsiach potrafią być w sezonie zimowym większe niż w centrum dużego miasta. Ich głównym źródłem są prymitywne domowe piece grzewcze i spalane w nich tanie paliwo kiepskiej jakości. Paradoksalnie do dużego poziomu zanieczyszczeń przyczyniają się lekkie zimy i dobra izolacja ścian domów.
Prymitywne piece pracują wtedy krótko, z przerwami i niedostatecznie rozgrzane palenisko nie jest w stanie spalić dokładnie paliwa. Generują wtedy do atmosfery duże ilości pyłów i innych szkodliwych związków. Wieloletnie narażenie na takie warunki przyczynia się wielu chorób, od nowotworowych, poprzez kardiologiczne, aż do chorób płuc. Jest szkodliwe dla każdego, ale szczególnie dla małych dzieci i osób starszych.
Przy bardzo dużych stężeniach pyłów smog można zobaczyć, bo przybiera postać szarej lub żółtawej mgły. Niższe, ale ciągle zbyt wysokie długotrwałe stężenia pyłów nie są widoczne ale są równie szkodliwe. Dzisiaj bez większego problemu można dość precyzyjnie mierzyć poziom zanieczyszczeń w miejscu naszego przebywania nawet niezbyt drogimi czujnikami. Jednym z takich czujników jest moduł czujnika laserowego HM3301 PM2.5 firmy Grove.
Laserowy czujnik stężenia pyłów
Rodzina detektorów HM3300/HM3600, do której należy HM3301, to nowa generacja laserowych czujników przeznaczonych do ciągłego wykrywania w czasie rzeczywistym stężenia pyłu w powietrzu. Działanie detektorów jest bazuje na zaawansowanej teorii rozpraszania stosowanej miedzy innymi do pomiarów wielkości małych cząstek metodami optycznymi. Kiedy światło przechodzi przez cząstki zawieszone w powietrzu, to powodują one rozproszenie światła. Rozproszone światło jest skoncentrowane na bardzo czułej fotodiodzie. Sygnał z tej fotodiody jest wzmacniany i analizowany przez układy detektora. Za pomocą określonego modelu matematycznego i algorytmu uzyskuje się informację o stężeniu masowym cząstek pyłu.
Zanieczyszczone powietrze jest dostarczane do układu czujnika przez mały wentylator. Przepływa przez specjalną komorę, gdzie zanieczyszczenia są oświetlane przez światło lasera pracującego w podczerwieni. Światło, jak już wiemy, ulega rozszczepieniu na cząstkach pyłu i trafia na powierzchnie fotodetektora. Sygnał z fotodetektora jest następnie filtrowany, wzmacniany i przetwarzany cyfrowo (rysunek 1).
Układ elektryczny czujnika może być zasilany napięciem w zakresie 3,3…5 V. Temperatura pracy wynosi od –10°C do +60°C, a wilgotność otoczenia nie może być niższa niż 10%, ani wyższa od 90% RH, przy czym nie dopuszcza się kondensacji pary wodnej. Tor pomiarowy rozróżnia 3 zakresy wielkości pyłów: 2,5 μm, 5 μm i 10 μm.
Rozdzielczość pomiarowa wynosi 1 μg/m3 a efektywny zakres pomiarowy 1...500 μg/m3. Maksymalny zakres pomiarowy dla kanału 2,5 μm wynosi 1000 μg/m3. Pomiary stają się miarodajne 30 sekund po włączeniu napięcia zasilania. Układ HM3301 ma 2 interfejsy komunikacyjne: UART i I2C, przy czym podstawowym interfejsem jest UART. Można za jego pomocą nie tylko odczytywać pomiary, ale też programować tryby pracy. Interfejs I2C umożliwia tylko odczytywanie danych pomiarowych. Producent modułu wyprowadził tylko sygnały interfejsu I2C i nie ma sprzętowych możliwości korzystania z UART.
Moduł czujnika jest umieszczony na płytce drukowanej do której jest przymocowana obudowa detektora z wentylatorem tak jak to zostało pokazane na fotografii tytułowej. W komplecie z czujnikiem jest dodawany kabel z wtyczkami pasującymi do gniazd płytki Grove Base Shield przeznaczonej do łączenia modułów Grove z płytką Arduino.
Przygotowano też gotowy projekt dla tej platformy. Wystarczy złącze modułu połączyć z portem I2C płytki Base Shield, tak jak to pokazano na fotografii 1, uruchomić gotowy projekt w Arduino i powinno wszystko działać.
Programowanie
W testach nie używałem klasycznego systemu Arduino. Zastosowałem zestaw ARIS EDGE firmy RELOC składający się za modułu mikrokontrolera ARIS EDGE IoT Board i modułu wyświetlacza LCD ARIS EDGE LCD sprzętowo zgodnych ze standardem Arduino. Ponieważ nie miałem płytki Base Shield, to połączenie z czujnikiem wykonałem za pomocą kabli zakończonych wtykami (fotografia 2).
Wyprowadzenia zasilania i magistrali I2C zostały pokazane na rysunku 2.
Program testowy został napisany w języku C dla 32-bitowego mikrokontrolera Renesas Synergy R7FSA37A3 z rdzeniem Cortex. Zostało użyte środowisko IDE e2 studio firmy Renesas i firmowe biblioteki SSP. Aplikacja odczytująca dane z czujnika i wyświetlająca je na ekranie jest rozszerzeniem wcześniej napisanej aplikacji czujnika parametrów środowiskowych mierzącego temperaturę, wilgotność i ciśnienie atmosferyczne z użyciem sensora BM280 umieszczonego na płytce ARIS EDGE IoT Board. Na ekranie LCD są wyświetlane: temperatura otoczenia, wilgotność, ciśnienie atmosferyczne, koncentracja zanieczyszczeń pyłów PM2.5 i koncentracja zanieczyszczeń PM10.
Komunikację z czujnikami realizuje sprzętowy interfejs IIC1 skonfigurowany przez konfigurator Synergy Configuration wbudowany w pakiet e2 studio. W pierwszym kroku konfiguracji wybieramy zakładkę Threads. Sterownik zostanie skonfigurowany w domyślnym wątku HAL/Common. Wybierany w oknie HAL/Common Stacks ikonę New Stack i w rozwijanym menu wybieramy: New Stack → Driver → Connectivity → I2C Master Driver on r_iic (rysunek 3).
Dodany sterownik (driver) pojawia się w oknie HAL/Common Stacks i można go skonfigurować w oknie Properties. W oknie Name nadajemy swoją nazwę. W naszym przypadku jest to BME280. Ta nazwa potem występuje w funkcjach sterownika. Okno Channel wybiera numer kanału interfejsu I2C. Prędkość transmisji wybiera się w oknie Rate. Ze względu na wymagania magistrali I2C sensora HM3301 trzeba ustawić opcję Standard. Adres układu slave wpisuje się w oknie Adres Slave, a format adresu w oknie Address Mode (adres 7-bitowy). Dla układu BME280 wpisujemy adres 0x77, który potem będzie zmieniany zależnie od potrzeb. Priorytety przerwań zgłaszanych przez moduł I2C są ustawiane w 4 ostatnich oknach.
Trochę szerszego opisu wymaga okno Callback. W interfejsach szeregowych, które nie mają bufora FIFO wysłanie kolejnego bajtu na magistralę jest możliwe po zakończeniu wysyłania poprzedniego bajtu. W I2C nie ma znaczenia, czy jest to adres slave, dane wysyłane, czy dane odbierane. Funkcje wysyłania pojedynczych bajtów w warstwach niższych są oparte o przerwania i nie musimy się martwić o kolizję na magistrali. Ale wysłanie sekwencji składającej się z wysłania i odczytania określonej ilości danych wymaga sprawdzenia warunku zakończenia transmisji. Dlatego funkcje drivera wysyłające i dobierające dane muszą albo czekać na zakończenie transmisji (funkcje blokujące), albo kończyć swoje działanie po zainicjowaniu transmisji wielobajtowej i wprowadzać mechanizmy sygnalizacji zakończenia transmisji. Funkcje blokujące nigdy nie są dostatecznie dobrym rozwiązaniem. Jeżeli coś pójdzie nie tak, to możemy całkowicie stracić kontrolę nad programem. Jeżeli w oknie Callback umieścimy wpis NULL, to funkcje transmisji będą się wykonywać jako blokujące. Wpisanie do Callback swojej nazwy spowoduje, że funkcje nie będą czekać na zakończenie transmisji. Użytkownik musi sam sobie napisać funkcję o nazwie z okna właściwości drivera Callback i zadbać o wykrywanie zakończenia transmisji. Opiszę ten mechanizm oddzwaniania przy okazji opisywania działania funkcji obsługi I2C. Zastosowana konfiguracja sterownika I2C została pokazana na rysunku 4.
W kolejnym kroku trzeba wybrać zakładkę Pins konfiguratora Synergy Configurator i skonfigurować wyprowadzenia interfejsu I2C, tak by był sprzętowo zgodny z płytką Arduino. Interfejs I2C musi używać portów P206 i P207. Zostało to pokazane na rysunku 5.
Po konfiguracji i wygenerowaniu przez konfigurator odpowiednich plików, po kliknięciu na Generate Project Content można używać funkcji sterownika magistrali I2C. Pierwszą rzeczą jaką należy zrobić jest otwarcie sterownika za pomocą funkcji open (listing 1).
//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 4.
Funkcja open zwraca jeden ze statusów:
- SSP_SUCCESS – interfejs został poprawnie otwarty i może być używany,
- SSP_ERR_IN_USE – interfejs jest już otwarty i nie można go powtórnie otworzyć,
- SSP_ERR_INVALID_RATE – nie można wyliczyć wybranej prędkości transmisji.
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 sterownika magistrali I2C zdefiniowano w SSP zdarzenia:
- I2C_EVENT_ABORTED – transfer nie dokończony,
- I2C_EVENT_RX_COMPLETE – operacja odczytywania zakończona sukcesem,
- I2C_EVENT_TX_COMPLETE – operacja zapisywania zakończona sukcesem.
Dla naszych potrzeb zdefiniowałem 2 zmienne globalne i funkcję BME280_Callback, której kod został pokazany na listingu 2.
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;
}
}
Poprawnie 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.
Do odczytywania i zapisywania danych z czujnika BME280 zostały napisane dwie funkcje: BME280_I2C_Write i BME280_I2C_Read. Te funkcje adresują urządzenie slave o adresie 0x77. Do tej samej magistrali I2C jest dołączony czujnik pyłów o adresie 0x40. W rozbudowanych systemach, gdzie transmisja może być inicjowana asynchronicznie w dowolnym momencie można wykorzystywać frameworka 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 steronik BME_280 Master Driver z parametrami umieszczonymi w strukturze p_cfg. Oba urządzenia slave: MBE280 i HM3301 mogą pracować z takimi samymi konfiguracjami, w tym z taką 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 powinno wszystko zadziałać.
Oczywiście skoro nie używamy frameworka, to musimy szczególnie zadbać o prawidłowy ruch na magistrali żeby nie powodować kolizji. Jak wspomniałem do tego celu wykorzystamy mechanizm oddzwaniania callback. Będziemy blokować wysyłanie kolejnych danych jeżeli poprzedni transfer się jeszcze nie zakończył. Do zmiany adresu wykorzystamy funkcję biblioteczną slaveAddressSet. Jej argumentami są wskaźnik na strukturę p_ctrl sterownika BME280, adres slave i rozmiar adresu (7 bitów lub 10 bitów).
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);
}
Na listingu 3 pokazano funkcję zapisu danych na magistralę I2C dla układu BME280, a na listingu 4 funkcję odczytu danych z 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);
}
W każdej z nich przed zainicjowaniem transferu jest ustawiany jawnie adres 0x77, pomimo tego, że domyślnie został ustawiony w konfiguratorze. Jest o niezbędne, bo poprzedni transfer danych mógł zmienić ten adres na 0x40 dla układu HM3301.
Funkcje wymiany danych z układem HM3301 będą się różniły tylko adresem slave (listing 5 i listing 6).
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);
}
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);
}
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 wykorzystałem tutaj istniejące definicje i konfiguracje.
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 użycie mechanizmu oddzwaniania 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 oczekiwanie na zakończenie transmisji w dowolny sposób zorganizować. 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. Jest połączone z obsługą błędu.
Odczyt danych pomiarowych
Jak wspomniałem – podstawowym interfejsem komunikacyjnym HM3301 jest UART, ale producent modułu postanowił udostępnić 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 6.
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, że 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ęci 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ść dokumentacji bardzo utrudnia.
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);
}
ssp_err_t HM3301_Read (void){
ssp_err_t status;
status = HM3301_I2C_Read(HM3301_buffer, 16, false);
return status;
}
Funkcja odczytująca dane z HM3301 została pokazana na listingu 8. Do bufora HM3301_buffer zadeklarowanego jako zmienna globalna odczytujemy kolejno 16 bajtów.
Na rysunku 7 pokazano transfer danych w czasie ich odczytywania z czujnika przez magistralę I2C, a na rysunku 8 fragment dokumentacji z rozmieszczeniem danych odczytywanych z układu sensora. Odczytujemy 16 bajtów, a wykorzystujemy do wyświetlania dane DATA7, 8, 9 i 10. Zawierające stężenie pyłów 2,5 μg/m3 i 10 μg/m3.
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 wartość wyświetla się na żółto (stan ostrzegawczy), a dla wartości powyżej 84 wartość wyświetla się na czerwono (stan alarmowy).
Podsumowanie
Czujnik HM3301 jest rozbudowanym sensorem wykorzystującym zaawansowaną teorię 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 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. Jest wiele obszarów zastosowania na przykład 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ń potrafił czasami osiągać w okresie jesienno – zimowym 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 się łatwo domyśleć całkiem sprawnie wytwarzają takie poziomy zanieczyszczeń kominy okolicznych domów, bo w lecie poziom zanieczyszczeń prawie nigdy nie przekracza 35 μg/m3.
Tomasz Jabłoński, EP