Sunday, August 23, 2026

Building a Modern Environmental Dashboard on STM32F769I-DISCO with FreeRTOS, BME280, and LVGL XML

In this post, we explore the end-to-end development of an embedded environmental monitoring dashboard running on the STM32F769I-DISCO development kit (ARM Cortex-M7 @ 216 MHz with Chrom-ART DMA2D hardware acceleration).

By combining FreeRTOS, the Bosch BME280 sensor, and the LVGL v9.5 graphics engine utilizing LVGL Pro XML declarative UI components, we built a reactive, decoupled, and visually stunning user interface with dynamic comfort analysis and smooth animations.

Demo

Project Highlights & Architecture

  • MCU & Board: STM32F769I-DISCO (ARM Cortex-M7 @ 216 MHz, Chrom-ART DMA2D acceleration, 800×480 DSI Capacitive Touchscreen).
  • RTOS: FreeRTOS Kernel with thread-safe inter-task messaging and dedicated GUI/Sensor tasks.
  • Sensor: Bosch BME280 connected via I2C (configured for high-efficiency 32-bit fixed-point arithmetic).
  • Graphics: LVGL v9.5 configured for 16-bit RGB565 color depth.
  • Declarative UI: Designed with LVGL Pro XML, utilizing reusable XML components (<sensor_card>), reactive data binding (lv_subject_t), and smooth screen fade transitions.
  • Smart Evaluation Engine: Clean, table-driven comfort and weather classification that dynamically updates status labels, border highlights, and glowing shadow effects.
Architecture

Hardware Setup & Peripheral Assignment

The STM32F769I-DISCO features multiple I2C buses. It is critical to select the correct bus to avoid peripheral conflicts:
  • I2C4 (PD12/PB7): Reserved by the board for the on-board Capacitive Touchscreen controller (FT6206) and Audio Codec (WM8994).
  • I2C1 (PB8/PB9): Routed to the Arduino Uno V3 expansion connector D15 (SCL) and D14 (SDA).

Sensor Wiring

BME280 PinSTM32F769I-DISCO Arduino HeaderSTM32 Pin
VCC3.3V3.3V
GNDGNDGND
SCLD15PB8 (I2C1_SCL)
SDAD14PB9 (I2C1_SDA)

Autonomous BME280 Sensor Acquisition via FreeRTOS

  • Dedicated Sensor Task: A background FreeRTOS task autonomously communicates with the Bosch BME280 sensor over hardware I2C1 (PB8/PB9 on the Arduino header) at 1 Hz.
  • Zero Floating-Point Overhead: Configured with BME280_32BIT_ENABLE, the driver performs all sensor compensation using fast 32-bit fixed-point integer math, bypassing expensive software floating-point calculations.
  • Thread-Safe Event Queue: Sensor readings are packaged into a structured message (gui_mng_event_data_t) and posted to the GUI Manager Task via a FreeRTOS message queue (GUI_MNG_EV_SENSOR_DATA_UPDATE), ensuring that UI rendering and sensor I2C communication never block or corrupt each other.

Lightweight 32-Bit Fixed-Point BME280 Driver

To avoid heavy software floating-point or double precision arithmetic overhead on embedded microcontrollers, we compiled the Bosch Sensortec driver with BME280_32BIT_ENABLE.

Driver Configuration
  • Temperature: Reported in 0.01 °C (e.g., 2150 = 21.50 °C).
  • Humidity: Reported in 1/1024 %RH (e.g., 59392 >> 10 = 58 %RH).
  • Pressure: Reported in integer Pascals (e.g., 101325 / 100 = 1013 hPa).
FreeRTOS Safe Sampling Task
/**
 * @brief bme280 sensor task to read data periodically
 * @param pvParameters Pointer to the task parameters
 */
static void bme280_task( void *pvParameters )
{
  (void)pvParameters;
  int8_t init_status = BME280_E_DEV_NOT_FOUND;

  /* Retry initialization until sensor responds */
  while ( init_status != BME280_OK )
  {
    init_status = bme280_sensor_init();
    if ( init_status != BME280_OK )
    {
      /* Delay for 1 second before retrying */
      vTaskDelay( pdMS_TO_TICKS(1000) );
    }
  }

  for (;;)
  {
    /* Read compensated data (Temperature, Pressure, Humidity) */
    if ( bme280_get_sensor_data( BME280_ALL, &sensor_data, &dev) == BME280_OK )
    {
      /* Prepare GUI event message */
      gui_mng_event_data_t ev_data;
      ev_data.sensor_data.temperature = sensor_data.temperature;
      ev_data.sensor_data.humidity    = sensor_data.humidity;
      ev_data.sensor_data.pressure    = sensor_data.pressure;
      /* Send non-blocking event to GUI task */
      gui_send_event( GUI_MNG_EV_SENSOR_DATA_UPDATE, &ev_data );
    }

    /* Delay for 1 second before next reading */
    vTaskDelay( pdMS_TO_TICKS(1000) );
  }
}

Declarative UI Design with LVGL Pro XML

Instead of writing hundreds of lines of imperative C code to position labels and boxes, the entire interface was designed declaratively using LVGL XML.

Reusable Component: sensor_card.xml

We created a single modular component that encapsulates the glowing glass morphic card, big value label, center image graphic, dynamic status text, and bottom title:
<component>
  <api>
    <prop name="value_text" type="subject" />
    <prop name="img_src" type="image" default="thermometer_img" />
    <prop name="status_label_text" type="subject" default="Status" />
    <prop name="status_text_color" type="color" default="0x8FA0B8" />
    <prop name="card_title_text" type="string" default="Card Title" />
    <prop name="theme_color" type="color" default="0xF39C12" />
  </api>
  <view
    extends="lv_obj"
    width="31%"
    height="content"
    style_bg_opa="0"
    style_border_width="0"
    style_pad_all="0"
    flex_flow="column"
    style_flex_cross_place="center"
    style_pad_row="10"
  >
    <!-- 1. Glowing Card Box -->
    <lv_obj
      name="card_box"
      width="100%"
      height="310"
      scrollable="false"
      style_bg_color="0x1D2939"
      style_bg_opa="255"
      style_radius="20"
      style_border_width="2"
      style_border_color="$theme_color"
      style_shadow_width="20"
      style_shadow_color="$theme_color"
      style_shadow_opa="30"
      style_pad_all="18"
      flex_flow="column"
      style_flex_main_place="space_between"
      style_flex_cross_place="center"
    >
      <!-- Top Value Lavel -->
      <lv_label
        name="value_label"
        width="100%"
        bind_text="$value_text"
        style_text_font="title_large"
        style_text_color="0xFAFAFA"
        style_text_align="center"
      />
      <!-- Middle: Center Image Graphic -->
      <lv_image name="center_img" src="$img_src" />
      <!-- Status Sub Text -->
      <lv_label
        name="status_label"
        width="100%"
        bind_text="$status_label_text"
        style_text_font="body_normal"
        style_text_color="$status_text_color"
        style_text_align="center"
      />
    </lv_obj>
    <!-- Sensor Card Name -->
    <lv_label
      name="title_label"
      width="100%"
      text="$card_title_text"
      style_text_color="0xCAD5E2"
      style_text_font="body_normal"
      style_text_align="center"
    />
  </view>
</component>


Table-Driven Classification & Dynamic Styling

Instead of messy nested if-else blocks, we used clean lookup tables that map numerical sensor ranges to human comfort levels and visual colors.
/* Threshold Rule Structure */
typedef struct
{
  int32_t max_threshold;      /* Range Upper Limit */
  const char *text;           /* status text */
  uint32_t color;             /* status color */
} sensor_range_rule_t;

/* Temperature Rules (in °C) */
static const sensor_range_rule_t temp_rules[] =
{
  { 10,  "Too Cold", 0x3498DB }, /* Blue */
  { 18,  "Cold",     0x5DADE2 }, /* Light Blue */
  { 26,  "Ideal",    0x2ECC71 }, /* Emerald Green */
  { 32,  "Warm",     0xF39C12 }, /* Orange */
  { 100, "Hot",      0xE74C3C }  /* Red */
};

/* Humidity Rules (in %RH) */
static const sensor_range_rule_t hum_rules[] =
{
  { 30,  "Too Dry",   0xE67E22 }, /* Amber */
  { 40,  "Dry",       0xF1C40F }, /* Yellow */
  { 60,  "Ideal",     0x00E5FF }, /* Glowing Cyan */
  { 70,  "Humid",     0x3498DB }, /* Blue */
  { 100, "Too Humid", 0x9B59B6 }  /* Purple */
};

/* Barometric Pressure Rules (in hPa) */
static const sensor_range_rule_t press_rules[] =
{
  { 1000, "Low (Rain)",   0xE74C3C }, /* Red (Rain/Stormy) */
  { 1020, "Stable",       0xBD10E0 }, /* Purple (Fair/Calm) */
  { 1200, "High (Clear)", 0x2ECC71 }  /* Green (Sunny) */
};

Event Handler & Dynamic Color Reflection

Whenever new sensor data arrives, strings are formatted, subjects are updated, and the card's border/shadow glow dynamically shifts according to the environmental comfort level:
static void gui_sensor_data_update( const gui_mng_event_data_t *data )
{
  if ( (active_sensor_screen == NULL) || (data == NULL) )
  {
    return; /* No active sensor screen or no data to update */
  }

  char buf[32];
  /* Temperature: Value & Status */
  int32_t t_int = data->sensor_data.temperature / 100;
  int32_t t_dec = (data->sensor_data.temperature % 100) / 10;
  snprintf( buf, sizeof(buf), "%ld.%ld °C", t_int, (t_dec < 0 ? -t_dec : t_dec) );
  lv_subject_copy_string( &temp_str, buf );
  const sensor_range_rule_t *t_rule = get_sensor_rule( t_int, temp_rules, NUM_ELEMENTS(temp_rules) );
  lv_subject_copy_string( &temp_status_str, t_rule->text );

  /* Humidity: Value & Status */
  uint32_t hum_pct = data->sensor_data.humidity >> 10;
  snprintf( buf, sizeof(buf), "%lu %%", hum_pct );
  lv_subject_copy_string( &hum_str, buf );
  const sensor_range_rule_t *h_rule = get_sensor_rule( (int32_t)hum_pct, hum_rules, NUM_ELEMENTS(hum_rules) );
  lv_subject_copy_string( &hum_status_str, h_rule->text );

  /* Pressure: Value & Status */
  uint32_t press_hpa = data->sensor_data.pressure / 100;
  snprintf( buf, sizeof(buf), "%lu\nhPa", press_hpa );
  lv_subject_copy_string( &press_str, buf );
  const sensor_range_rule_t *p_rule = get_sensor_rule( (int32_t)press_hpa, press_rules, NUM_ELEMENTS(press_rules) );
  lv_subject_copy_string( &press_status_str, p_rule->text );

  /* Updating Status Label Colors and Box Color Logic */
  /* Cards Row Container */
  lv_obj_t *cards_row = lv_obj_get_child( active_sensor_screen, 1 );
  if ( cards_row != NULL )
  {
    /* temperature status coloring logic */
    lv_obj_t *temp_card = lv_obj_get_child( cards_row, 0 );
    if ( temp_card )
    {
      lv_obj_t *box = lv_obj_get_child_by_name( temp_card, "card_box" );
      lv_obj_t *lbl = lv_obj_get_child_by_name( box, "status_label" );
      if ( lbl )
      {
        lv_obj_set_style_text_color( lbl, lv_color_hex( t_rule->color ), LV_PART_MAIN );
      }
      if ( box )
      {
        lv_obj_set_style_border_color( box, lv_color_hex( t_rule->color ), LV_PART_MAIN );
        lv_obj_set_style_shadow_color( box, lv_color_hex( t_rule->color ), LV_PART_MAIN );
      }
    }

    /* humidity status coloring logic */
    lv_obj_t *hum_card = lv_obj_get_child( cards_row, 1 );
    if ( hum_card )
    {
      lv_obj_t *box = lv_obj_get_child_by_name( hum_card, "card_box" );
      lv_obj_t *lbl = lv_obj_get_child_by_name( box, "status_label" );
      if ( lbl )
      {
        lv_obj_set_style_text_color( lbl, lv_color_hex( h_rule->color ), LV_PART_MAIN );
      }
      if ( box )
      {
        lv_obj_set_style_border_color( box, lv_color_hex( h_rule->color ), LV_PART_MAIN );
        lv_obj_set_style_shadow_color( box, lv_color_hex( h_rule->color ), LV_PART_MAIN );
      }
    }

    /* pressure status coloring logic */
    lv_obj_t *press_card = lv_obj_get_child( cards_row, 2 );
    if ( press_card )
    {
      lv_obj_t *box = lv_obj_get_child_by_name( press_card, "card_box" );
      lv_obj_t *lbl = lv_obj_get_child_by_name( box, "status_label" );
      if ( lbl )
      {
        lv_obj_set_style_text_color( lbl, lv_color_hex( p_rule->color ), LV_PART_MAIN );
      }
      if ( box )
      {
        lv_obj_set_style_border_color( box, lv_color_hex( p_rule->color ), LV_PART_MAIN );
        lv_obj_set_style_shadow_color( box, lv_color_hex( p_rule->color ), LV_PART_MAIN );
      }
    }
  }
}


Key Takeaways & Embedded Best Practices

  1. Fixed-Point Arithmetic: Enabling BME280_32BIT_ENABLE cut execution latency and code size significantly compared to double float libraries.
  2. RGB565 vs ARGB8888: Running LVGL with 16-bit RGB565 reduced SDRAM bandwidth and halved framebuffer RAM usage, yielding smooth performance.
  3. Declarative Component Architecture: Creating reusable <sensor_card> XML components eliminated repetitive UI code and guaranteed 100% pixel-perfect layout symmetry across all cards.
  4. Reactive Subjects (lv_subject_t): Decoupled the FreeRTOS event-processing logic from the UI widget tree. Updating the UI requires only updating data subjects.

No comments:

Post a Comment