IR-Thermometer mit LoRaWAN
05.06.2025
Elektronik | Funk | Software
Der Technik-Blog
Der I2C-Bus ist ein weit verbreitetes serielles Kommunikationsprotokoll, welches vor allem in der Sensoranbindung zum Einsatz kommt. Es ermöglicht den Anschluss mehrerer Geräte über nur zwei Leitungen und ist damit ideal für kompakte Mikrocontroller-Anwendungen geeignet. In diesem Artikel werden die Grundlagen der I2C-Kommunikation erläutert und die schrittweise Ansteuerung eines Sensors exemplarisch umgesetzt. Als Beispiel dient der BME280, ein kombinierter Sensor zur Messung von Temperatur, Luftfeuchtigkeit und Luftdruck. Zum Einsatz kommt ein Mikrocontroller aus der STM32-Familie, konkret der STM32WLE5JBIX, welcher sich auch auf der LW-Base von ELV befindet. Ziel ist ein praxisnaher Einstieg in den Umgang mit I2C-Sensoren auf STM32-Mikrocontrollern.
Arduino-Plattformen sind weit verbreitet und besonders für den schnellen Einstieg attraktiv. Für nahezu jeden I2C-Sensor existieren fertige Bibliotheken, die eine einfache Einbindung mit wenigen Codezeilen ermöglichen. Für erste Prototypen oder einfache Projekte ist Arduino daher oft die erste Wahl. In professionellen Anwendungen rücken jedoch andere Anforderungen in den Vordergrund, so etwa ein besonders geringer Energieverbrauch oder die direkte Anbindung an Funkprotokolle wie LoRaWAN. Hier bietet die LW-Base von ELV klare Vorteile. Sie basiert auf einem STM32WLE5JBIX, der einen stromsparenden 32-Bit-Mikrocontroller mit integriertem LoRa-Funkmodul vereint. Die Plattform wurde speziell für energieeffiziente Anwendungen im Bereich IoT und Sensorik entwickelt. Allerdings ist der Einstieg in die STM32-Welt anspruchsvoller: Für viele Sensoren stehen keine vorkonfigurierten Bibliotheken zur Verfügung, und die I2C-Kommunikation muss oft manuell umgesetzt werden. Dieser Artikel zeigt, wie sich diese Herausforderung strukturiert und nachvollziehbar lösen lässt.
I2C (Inter-Integrated Circuit) ist ein synchrones, serielles Kommunikationsprotokoll, welches über zwei Leitungen funktioniert: SCL (Serial Clock) für das Taktsignal und SDA (Serial Data) für die Datenübertragung. Beide Leitungen sind Open-Drain bzw. Open-Collector ausgeführt, was bedeutet, dass sie nicht aktiv auf High-Pegel gezogen werden. Stattdessen werden sie von externen Pull-Up-Widerständen gegen die Versorgungsspannung gezogen. Typische Werte für diese Widerstände liegen im Bereich von 4,7 kΩ bis 10 kΩ, abhängig von Buslänge und Datenrate.
Im I2C-System übernimmt ein Gerät die Rolle des Masters (z. B. ein Mikrocontroller), während ein oder mehrere Slaves (z. B. Sensoren) darauf reagieren. Jede Kommunikation wird vom Master initiiert. Zur Unterscheidung der Teilnehmer verfügt jedes Slave-Gerät über eine 7-Bit-Adresse, die entweder fest im Chip vorgegeben oder über externe Anschluss-Pins teilweise konfigurierbar ist.
Beim Start einer Kommunikation sendet der Master ein Start-Signal, gefolgt von der Adresse des Zielgeräts und einem Bit, das den Übertragungsmodus angibt (Lesen oder Schreiben). Erkennt ein Slave seine Adresse, antwortet er mit einem ACK (Acknowledge) und nimmt die Kommunikation auf.
Da auf dem I2C-Bus mehrere Geräte gleichzeitig angeschlossen sein können, ist die richtige Adressierung essenziell. Der BME280 verwendet typischerweise die Adresse 0x76 oder 0x77, abhängig vom logischen Pegel an einem seiner Anschluss-Pins. Vor dem Zugriff muss also sichergestellt sein, welche Adresse im konkreten Aufbau verwendet wird. Der folgende Schaltplan zeigt, wie eine I2C Kommunikation im wesentlichen funktioniert:
Ein erster Schritt zur Kommunikation mit einem I2C-Sensor besteht in der gezielten Adressierung, meist in Form eines sogenannten „Ping“-Versuchs. Dabei sendet der Mikrocontroller (Master) ein Start-Signal, gefolgt von der 7-Bit-Adresse des Zielgeräts (in diesem Fall 0x76) und dem Schreibbit "0". Im Erfolgsfall antwortet der Sensor mit einem Acknowledge (ACK) und signalisiert damit seine Bereitschaft zur Kommunikation. Das nachfolgend dargestellte Oszilloskopbild zeigt genau diesen Adressierungsversuch: Kanal C1 (gelb) stellt das Taktsignal (SCL) dar, Kanal C2 (grün) die Datenleitung (SDA). Im dekodierten I2C-Bus-Signal (B1) ist der Schreibversuch an die Adresse 0x76 erkennbar:
Das zweite Oszilloskopbild zeigt den Kommunikationsversuch nochmals detailliert:
Start-Bedingung: Der Master erzeugt eine Start-Bedingung (SDA fällt bei High-Pegel von SCL).
Adressübertragung: Anschließend folgen 7 Bit für die Adresse (0x76, binär: 1110110) und ein weiteres 0-Bit, welches den Schreibmodus signalisiert.
ACK-Phase: Während des neunten Takts sollte das Gerät die SDA-Leitung aktiv auf Low ziehen, was ein korrektes ACK (Acknowledgement) darstellen soll. Dieser Zustand wird im vorherigen Screenshot nicht erreicht, da der Sensor noch nicht angeschlossen wurde.
Mit sieben Bits können bis zu 128 Adressen bzw. Geräte an einem Bus angesprochen werden. Jedes Gerät hat dabei seine eigene eindeutige Adresse, welche meist statisch vom Hersteller vergeben wird und teilweise oft anpassbar ist. In der Praxis sind es meist deutlich weniger als 112 Geräte, da mit jedem Gerät auch die Buskapazität steigt. Eine zu hohe Buskapazität kann zu Kommunikationsproblemen führe. Es gibt auch eine Variante mit einer 10-Bit Adressierung, diese ist aber kaum verbreitet. Der SDA-Pegel ändert sich bei der Bitübertragung nicht im Zeitfenster, in dem sich der SCL-Pegel ändert. Dies würde nämlich ein Start oder Stop Bit signalisieren.
Das Basistemplate von der LW-Base stellt für I2C bereits einige Funktionen bereit. So wurde zuerst mittels Funktionsaufruf i2c_mod2_init(); der I2C-Bus an den Pins PA11und PA12 aktiviert. Mittels der Funktion i2c_check_address(0x76); wird ein Ping, wie er bereits in den vorherigen Screenshots zu sehen war, zum Sensor gesendet. Der folgende Screenshot zeigt, wie das ACK bestätigt wird, und anschließend der Master zum Test 0x00 an den Slave sendet und dieser wiederum 0x00 wieder zurück an den Master sendet:
In der STM32 Cube IDE wurde das von ELV und Make Magazin vorgestellte Basistemplate ohne LoRaWAN verwendet. Nach dem Projekt-Import befindet sich im Ordner "User Modules" die Datei "app.c", welche als Einstiegspunkt für den Programmcode verwendet wird. Die vollständige Inhalt dieser Datei befindet sich am Ende von diesen Artikel. Mit der Funktion i2c_check_address(0x76) wird die Verbindung zum Sensor überprüft und und im erfolgsfall ein "true" zurückgegeben:
static bool i2c_check_address(uint8_t addr_7bit) { uint8_t response; //placeholder for rx 0x00 HAL_StatusTypeDef ret = i2c_mod2_read_register(addr_7bit << 1, 0x00, 1, &response, 1); return (ret == HAL_OK); }
In der Funktion app_init(void) wurde nach i2c_mod2_init(); in der While-Schleife im Abstand von 500ms die Verbindung mit folgendem Code geprüft und über die serielle Schnittstelle ausgegeben:
if (!i2c_check_address(0x76)) { APP_LOG(TS_OFF, VLEVEL_L, "No response from BME280 at 0x76!\r\n"); } else { APP_LOG(TS_OFF, VLEVEL_L, "Response from BME280 received at 0x76!\r\n"); }
Das Ergebnis wird im Terminal wie folgt dargestellt:
An einem I2C-Bus können theoretisch bis zu 128 Geräte parallel angeschlossen werden. Eine einfache Schleife ruft die Funktion i2c_check_address(addr); auf und übergibt mit jeden Durchlauf eine andere Adresse von 1 bis 127. Wird vom adressierten Slave ein ACK gesendet, so wird die Adresse im Terminal entsprechend ausgeben. Im folgenden Beispiel wurde neben dem BME280 noch ein weiterer Sensor angeschlossen:
Beispielcode I2C Scanner
for (uint8_t addr = 1; addr < 127; addr++) { if (i2c_check_address(addr)) { APP_LOG(TS_OFF, VLEVEL_L, " -> Device found at 0x%02X\r\n", addr); } }
Als Beispiel für eine einfache Register-Abfrage wird die Chip-ID abgefragt. Diese ist laut Datenblatt vom BME280 immer 0x60 und kann mit dem Register-Befehl 0xD0 abgefragt werden. Das folgende Codebeispiel sendet 0xD0 zum Slave 0x76 und prüft, ob als Antwort 0x60 (ASCII: `) zurückkommt.
uint8_t chip_id = 0; i2c_mod2_read_register(BME280_ADDR, 0xD0, 1, &chip_id, 1); if (chip_id != 0x60) { APP_LOG(TS_OFF, VLEVEL_L, "BME280 not detected!\r\n"); } else { APP_LOG(TS_OFF, VLEVEL_L, "Chip-ID ASCII: %c\r\n", chip_id); }
Die Kommunikation auf Bitebene sieht wie folgt aus:
Zeitlicher Ablauf:
Im oberen Screenshot vom Oszilloskop ist zu sehen, wie der Master das Zielgerät mit der Adresse 0x76 anspricht. Anschließend folgt ein ACK vom Zielgerät. Der Master sendet 0xD0 zur Abfrage der Chip-ID auf den Bus und erhält wieder ein ACK vom Zielgerät. Nach einer kurzen Pause wird das Register 0x60 von der Zieladresse gelesen. Zu sehen ist auch, dass Bit 8 von der Slave-Adresse jetzt auf High ist, was ein Read signalisiert. Die ID 0x60 (Am Oszilloskop als Gravis in ASCII dargestellt) wird übertragen. Das Programm überprüft die empfangene Chip-ID und gibt das Ergebnis im Terminal aus:
Als nächstes folgt die Abfrage von der aktuellen Temperatur. Der BME280 übermittelt immer die Rohdaten, eine Korrektor mit den entsprechenden Kalibrierwerten erfolgt später durch die Software. Laut Datenblatt muss zuerst die Messeinstellung übermittelt werden. Die Funktion i2c_mod2_write_register() schreibt das Register 0xF4 (Register Messeinstellung) und anschließend 0x25 (Binär: 00100101) für die Einstellungswerte. Zerlegt man 0x25 in Binär ergeben sich dabei folgende Einstellungen:
osrs_t = 001 -> Temperatur-Oversampling x1
osrs_p = 001 -> Druck-Oversampling x1
mode = 01 -> Forced Mode (einmalige Messung, dann Sleep)
Danach sendet der Master das Register 0xFA, welches den BME280 auffordert, die Rohdaten vom Temperatursensor zu liefern. Der Master ließt vom BME280 insgesamt drei Byte (0x80, 0x08, 0x00) wovon Byte 1 und 2 sowie die ersten 4 Bit von Byte 3 die Temperatur darstellen zurück. Dies ergibt einen unkalibrierten Temperaturwert von 102,42 °C und steigt beim berühren des Sensors an. Folgender Beispielcode wurde verwendet:
uint8_t ctrl_meas = 0x25; // = 0b00100101; i2c_mod2_write_register(BME280_ADDR, 0xF4, 1, &ctrl_meas, 1); HAL_Delay(1); // Read temperature register (0xFA–0xFC) uint8_t buf[3]; i2c_mod2_read_register(BME280_ADDR, 0xFA, 1, buf, 3); // Display raw bytes APP_LOG(TS_OFF, VLEVEL_L, "RAW-Bytes of temperature: %02X %02X %02X\r\n", buf[0], buf[1], buf[2]); // 20-Bit raw temperature int32_t adc_T = ((int32_t) buf[0] << 12) | ((int32_t) buf[1] << 4) | (buf[2] >> 4); int temp_no_cal = (int) (adc_T / 5120.0f * 100); APP_LOG(TS_OFF, VLEVEL_L, "Uncalibrated temperature: %d.%02d °C\r\n", temp_no_cal / 100, abs(temp_no_cal % 100));
Für die Darstellung der tatsächlichen Temperatur muss der Rohwert mit einer Formel aus dem Datenblatt verknüpft werden. Dazu gibt es ein weiteres Register mit Kalibrierwerten im Speicher vom BME280. Diese Daten werden einmalig nach Aktivierung der I2C Schnittstelle mit read_calibration(); vom Slave abgefragt und in Variablen geschrieben. Immer, wenn der Temperaturwert abgefragt wird, wird dieser mit den Kalibrierungsdaten (Variablen) verknüpft und anschließend im Terminal ausgeben. Dazu wurden folgende Funktionen geschrieben:
static uint16_t dig_T1; static int16_t dig_T2, dig_T3; static int32_t t_fine; static void read_calibration(void) { uint8_t buf[6]; i2c_mod2_read_register(BME280_ADDR, 0x88, 1, buf, 6); dig_T1 = (uint16_t) (buf[1] << 8 | buf[0]); dig_T2 = (int16_t) (buf[3] << 8 | buf[2]); dig_T3 = (int16_t) (buf[5] << 8 | buf[4]); } static float read_temperature(void) { uint8_t ctrl_meas = 0b00100101; // forced mode, oversampling x1 i2c_mod2_write_register(BME280_ADDR, 0xF4, 1, &ctrl_meas, 1); HAL_Delay(2); uint8_t buf[3]; i2c_mod2_read_register(BME280_ADDR, 0xFA, 1, buf, 3); int32_t adc_T = ((int32_t) buf[0] << 12) | ((int32_t) buf[1] << 4) | (buf[2] >> 4); int32_t var1 = ((((adc_T >> 3) - ((int32_t) dig_T1 << 1))) * ((int32_t) dig_T2)) >> 11; int32_t var2 = (((((adc_T >> 4) - ((int32_t) dig_T1)) * ((adc_T >> 4) - ((int32_t) dig_T1))) >> 12) * ((int32_t) dig_T3)) >> 14; t_fine = var1 + var2; float T = (t_fine * 5 + 128) >> 8; return T / 100.0f; }
In der Schleife wird die Temperatur wie folgt abgefragt:
int temp_internal_int = (int) (read_temperature() * 100); APP_LOG(TS_OFF, VLEVEL_L, "Calibrated temperature: %d.%02d °C\r\n", temp_internal_int / 100, abs(temp_internal_int % 100));
Im Terminal wird jetzt auch der korrigierte Temperaturwert ausgeben:
/** * @file app.c * @brief Source file for Application functions. * @author Marcel Maas, Thomas Wiemken, ELV Elektronik AG **/ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "app.h" #include "base.h" #include "hw_gpio.h" #include "led.h" #include "sys_app.h" #include "usart.h" #include "i2c.h" #define BME280_ADDR (0x76 << 1) static uint16_t dig_T1; static int16_t dig_T2, dig_T3; static int32_t t_fine; static bool i2c_check_address(uint8_t addr) { uint8_t response; //placeholder for rx 0x00 HAL_StatusTypeDef ret = i2c_mod2_read_register(addr, 0x00, 1, &response, 1); return (ret == HAL_OK); } static void read_calibration(void) { uint8_t buf[6]; i2c_mod2_read_register(BME280_ADDR, 0x88, 1, buf, 6); dig_T1 = (uint16_t) (buf[1] << 8 | buf[0]); dig_T2 = (int16_t) (buf[3] << 8 | buf[2]); dig_T3 = (int16_t) (buf[5] << 8 | buf[4]); } static float read_temperature(void) { uint8_t ctrl_meas = 0b00100101; // forced mode, oversampling x1 i2c_mod2_write_register(BME280_ADDR, 0xF4, 1, &ctrl_meas, 1); HAL_Delay(2); uint8_t buf[3]; i2c_mod2_read_register(BME280_ADDR, 0xFA, 1, buf, 3); int32_t adc_T = ((int32_t) buf[0] << 12) | ((int32_t) buf[1] << 4) | (buf[2] >> 4); int32_t var1 = ((((adc_T >> 3) - ((int32_t) dig_T1 << 1))) * ((int32_t) dig_T2)) >> 11; int32_t var2 = (((((adc_T >> 4) - ((int32_t) dig_T1)) * ((adc_T >> 4) - ((int32_t) dig_T1))) >> 12) * ((int32_t) dig_T3)) >> 14; t_fine = var1 + var2; float T = (t_fine * 5 + 128) >> 8; return T / 100.0f; } // Definitions ----------------------------------------------------------------- // Typedefs -------------------------------------------------------------------- // Variables ------------------------------------------------------------------- static base_callbacks_t base_app_cb = { .base_user_button_event = app_user_button_event, }; static bool b_user_button_pressed = false; // Prototypes ------------------------------------------------------------------ // Exported functions ---------------------------------------------------------- void app_init(void) { base_print_bl_and_app_version((uint8_t*) APP_APPLICATION_NAME_STR, (uint8_t*) APP_APPLICATION_VERSION_STR); base_init(&base_app_cb); leds_init(); i2c_mod2_init(); // Init I2C (PA11->SDA, PA12->SCL) HAL_Delay(1); //Check connection to BME280 if (!i2c_check_address(BME280_ADDR)) { APP_LOG(TS_OFF, VLEVEL_L, "No response from BME280 at 0x76\r\n"); } else { APP_LOG(TS_OFF, VLEVEL_L, "Response from BME280 received at 0x76!\r\n"); } /* //I2C Scanner for (uint8_t addr = 1; addr < 127; addr++) { if (i2c_check_address(addr)) { APP_LOG(TS_OFF, VLEVEL_L, " -> Device found at 0x%02X\r\n", addr); } } */ //Check chip id 0x60 uint8_t chip_id = 0; i2c_mod2_read_register(BME280_ADDR, 0xD0, 1, &chip_id, 1); if (chip_id != 0x60) { APP_LOG(TS_OFF, VLEVEL_L, "BME280 not detected!\r\n"); } else { APP_LOG(TS_OFF, VLEVEL_L, "Chip-ID ASCII: %c\r\n", chip_id); } if (i2c_check_address(BME280_ADDR)) { APP_LOG(TS_OFF, VLEVEL_L, "Get calibration values from sensor.\r\n"); read_calibration(); } while (1) { //APP_LOG(TS_OFF, VLEVEL_L, "Supply: %u mV\r\n", base_get_supply_level()); led_red_toggle(); led_green_toggle(); HAL_Delay(500); /* uint8_t ctrl_meas = 0b00100101; i2c_mod2_write_register(BME280_ADDR, 0xF4, 1, &ctrl_meas, 1); HAL_Delay(1); // Read temperature register (0xFA–0xFC) uint8_t buf[3]; i2c_mod2_read_register(BME280_ADDR, 0xFA, 1, buf, 3); // Display raw bytes APP_LOG(TS_OFF, VLEVEL_L, "RAW-Bytes of temperature: %02X %02X %02X\r\n", buf[0], buf[1], buf[2]); // 20-Bit raw temperature int32_t adc_T = ((int32_t) buf[0] << 12) | ((int32_t) buf[1] << 4) | (buf[2] >> 4); int temp_no_cal = (int) (adc_T / 5120.0f * 100); APP_LOG(TS_OFF, VLEVEL_L, "Uncalibrated temperature: %d.%02d °C\r\n", temp_no_cal / 100, abs(temp_no_cal % 100)); */ if (i2c_check_address(BME280_ADDR)) { int temp = (int) (read_temperature() * 100); APP_LOG(TS_OFF, VLEVEL_L, "BME280 temperature: %d.%02d °C\r\n", temp / 100, abs(temp % 100)); } else { APP_LOG(TS_OFF, VLEVEL_L, "No Response from BME280 received!\r\n"); } APP_LOG(TS_OFF, VLEVEL_L, "===================================\r\n"); } } void app_user_button_event(void) { app_set_user_button_pressed( true); } void app_set_user_button_pressed( bool b_button_pressed) { b_user_button_pressed = b_button_pressed; } bool app_get_user_button_pressed(void) { bool b_ret = b_user_button_pressed; if (b_ret) { app_set_user_button_pressed( false); base_enable_irqs(); } return b_ret; }
In der Funktechnik werden für Schwingkreise und Filter Luftspulen verwendet. Hier geht es um den Bau und die Berechnung von Luftspulen im Hochfrequenz-Bereich
WeiterlesenMOSFETs und Mikrocontroller - Dieser Artikel richtet sich an Anfänger, wo es um die verschiedenen MOSFETs geht und wie diese angeschlossen werden
WeiterlesenAEQ-WEB © 2015-2025 All Right Reserved