PRINCESS LAURONHEART LAHOYLAHOY
Published

RT-Spark AHT21 Temperature & Humidity System using STM32

A custom STM32-based environmental monitoring system that reads temperature and humidity using bare-metal drivers and display real-time data

IntermediateWork in progress9 hours45
RT-Spark AHT21 Temperature & Humidity System using STM32

Things used in this project

Hardware components

STM32F7 Series
STMicroelectronics STM32F7 Series
×1

Software apps and online services

STM32CUBEPROG
STMicroelectronics STM32CUBEPROG

Story

Read more

Schematics

spark_arch_wvvfnzi2rd_25SGMNnHpP.pdf

RT-Spark Schematic

Code

Main.c

C/C++
#include "main.h"
#include "drv_lcd.h"
#include "drv_aht21.h"
#include <stdlib.h>
#define FILTER_SIZE 10
SRAM_HandleTypeDef hsram1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_FSMC_Init(void);
/**
* SECTION 1: CUSTOM DISPLAY FUNCTIONS
* We implemented custom drawing and printing functions to avoid using the standard
* <stdio.h> library (sprintf/printf), which is very heavy on Flash memory (~20KB).
* Our custom implementation uses less than 1KB.
*/
// Draws a hollow rectangle (used for UI borders)
void LCD_DrawRect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) {
   LCD_DrawPixel(x1, y1, color);
   LCD_DrawPixel(x2, y2, color);
   // Draw Horizontal Lines
   for (uint16_t x = x1; x <= x2; x++) {
       LCD_DrawPixel(x, y1, color);
       LCD_DrawPixel(x, y2, color);
   }
   // Draw Vertical Lines
   for (uint16_t y = y1; y <= y2; y++) {
       LCD_DrawPixel(x1, y, color);
       LCD_DrawPixel(x2, y, color);
   }
}
// Recursive function to print integers digit by digit
void LCD_ShowNum(uint16_t x, uint16_t y, int num, uint16_t color, uint16_t back_color) {
   char buf[12]; int i = 0;
   if (num == 0) { LCD_ShowChar(x, y, '0', color, back_color); return; }
   // Handle negative numbers
   if (num < 0) { LCD_ShowChar(x, y, '-', color, back_color); x += 8; num = -num; }
   // Extract digits into buffer
   while (num > 0) { buf[i++] = (num % 10) + '0'; num /= 10; }
   // Print in reverse order (MSB first)
   while (--i >= 0) { LCD_ShowChar(x, y, buf[i], color, back_color); x += 8; }
}
/**
* @brief  Manually renders a float value to the screen.
* @why    Handling floats usually requires the FPU and heavy libraries.
* Here, we simply split the float into two Integers:
* 1. The Integer Part (e.g., 25)
* 2. The Decimal Part (e.g., .43)
* Then we print them separately with a dot in between.
*/
void LCD_ShowFloatManual(uint16_t x, uint16_t y, float val, uint16_t color, uint16_t back_color) {
   // 1. Extract Integer Part
   int intPart = (int)val;
   LCD_ShowNum(x, y, intPart, color, back_color);
   // Calculate cursor offset based on number length (for the decimal point)
   int offset = 8;
   if (intPart < 0) offset += 8;
   if (abs(intPart) > 9) offset += 8;
   if (abs(intPart) > 99) offset += 8;
   // 2. Print Decimal Point
   LCD_ShowChar(x + offset, y, '.', color, back_color);
   // 3. Extract Decimal Part (2 decimal places)
   // Multiply by 100 to convert .43 to 43
   int decPart = (int)((val - (float)intPart) * 100.0f);
   if (decPart < 0) decPart = -decPart;
   // Handle leading zeros (e.g., .05 should not print as .5)
   if (decPart < 10) {
       LCD_ShowChar(x + offset + 8, y, '0', color, back_color);
       LCD_ShowNum(x + offset + 16, y, decPart, color, back_color);
   } else {
       LCD_ShowNum(x + offset + 8, y, decPart, color, back_color);
   }
}
/*
* SECTION 2: SIGNAL PROCESSING (Digital Filter)
* Raw sensor data is often noisy due to electrical interference or airflow.
* This Moving Average Filter smooths the data for a stable display.
*/
float Filter_Value(float new_val, float *buffer, uint8_t *index, uint8_t *count) {
   // STEP 1: SPIKE REJECTION (Sanity Check)
   // If the sensor reads >100C or <-40C, it is physically impossible.
   // This is likely an I2C bit error or electrical glitch. Ignore it.
   if (new_val > 100.0f || new_val < -40.0f) {
       if (*count > 0) {
           // Return the previous valid average instead
           float sum = 0;
           for(int i=0; i<*count; i++) sum += buffer[i];
           return sum / *count;
       }
       return new_val; // Fallback if it's the very first reading
   }
   // STEP 2: UPDATE BUFFER
   buffer[*index] = new_val;
   *index = (*index + 1) % FILTER_SIZE; // Circular buffer wrap-around
   if (*count < FILTER_SIZE) (*count)++;
   // STEP 3: CALCULATE AVERAGE
   float sum = 0;
   for (uint8_t i = 0; i < *count; i++) {
       sum += buffer[i];
   }
   return sum / (float)(*count);
}
// --- MAIN APPLICATION LOOP ---
int main(void)
{
 // 1. Initialize Core Hardware
 HAL_Init();
 SystemClock_Config();
 MX_GPIO_Init(); // Configures Pins
 MX_FSMC_Init(); // Configures External Memory Bus for LCD
 // 2. Initialize LCD
 LCD_Init();
 LCD_Clear(WHITE);
 // 3. Draw User Interface (Static Elements)
 LCD_DrawRect(5, 5, 235, 40, BLUE);
 LCD_ShowString(40, 15, "RT-Spark AHT21", BLUE, WHITE);
 LCD_ShowString(20, 80, "Temp:", BLACK, WHITE);
 LCD_ShowString(20, 140, "Hum:", BLACK, WHITE);
 LCD_ShowString(20, 200, "Sensor Active ", GREEN, WHITE);
 // 4. Initialize Sensor (Sends Calibration Command 0xBE)
 AHT21_Init();
 // Variables for Data Processing
 float raw_temp = 0.0f;
 float raw_hum = 0.0f;
 float final_temp = 0.0f;
 float final_hum = 0.0f;
 // Circular Buffers for Filtering
 float temp_history[FILTER_SIZE] = {0};
 float hum_history[FILTER_SIZE] = {0};
 uint8_t temp_idx = 0, hum_idx = 0;
 uint8_t temp_cnt = 0, hum_cnt = 0;
 uint8_t count = 0;
 while (1)
 {
   // 5. Acquire Data
   uint8_t status = AHT21_Read(&raw_temp, &raw_hum);
   if (status == 1)
   {
       // 6. Process Data (Apply Filter)
       final_temp = Filter_Value(raw_temp, temp_history, &temp_idx, &temp_cnt);
       final_hum  = Filter_Value(raw_hum, hum_history, &hum_idx, &hum_cnt);
       // 7. Refresh Display
       // Note: We overwrite the old value area with spaces to "clear" it
       // before printing the new value to prevent flickering.
       LCD_ShowString(90, 100, "      ", WHITE, WHITE);
       LCD_ShowString(90, 160, "      ", WHITE, WHITE);
       // Print Temperature (Red for Heat)
       LCD_ShowFloatManual(90, 100, final_temp, RED, WHITE);
       LCD_ShowString(160, 100, "C", RED, WHITE);
       // Print Humidity (Blue for Water)
       LCD_ShowFloatManual(90, 160, final_hum, BLUE, WHITE);
       LCD_ShowString(160, 160, "%", BLUE, WHITE);
       // 8. System Heartbeat
       // Blinks a single pixel to show the main loop is running and not stuck
       if (count++ % 2) LCD_DrawPixel(230, 230, RED);
       else             LCD_DrawPixel(230, 230, WHITE);
   }
   else
   {
       // Error Handling: If I2C fails, show error and try to reset sensor
       LCD_ShowString(120, 200, "Read Err", RED, WHITE);
       HAL_Delay(100);
       AHT21_Init();
   }
   // Update Rate: 1Hz (Every 1000ms)
   // This is fast enough for environmental data but slow enough to be readable.
   HAL_Delay(1000);
 }
}
/*
* SECTION 3: PERIPHERAL CONFIGURATION (Auto-Generated Logic)
*/
void SystemClock_Config(void) {
   RCC_OscInitTypeDef RCC_OscInitStruct = {0};
   RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
   __HAL_RCC_PWR_CLK_ENABLE();
   __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
   RCC_OscInitStruct.HSIState = RCC_HSI_ON;
   RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
   HAL_RCC_OscConfig(&RCC_OscInitStruct);
   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
   HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0);
}
static void MX_FSMC_Init(void) {
 FSMC_NORSRAM_TimingTypeDef Timing = {0};
 hsram1.Instance = FSMC_NORSRAM_DEVICE;
 hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
 hsram1.Init.NSBank = FSMC_NORSRAM_BANK3;
 hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
 hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM;
 /**
  * CRITICAL CONFIGURATION: 8-BIT BUS WIDTH
  * The RT-Spark schematic shows only D0-D7 connected to the LCD.
  * If we select 16-bit here, the MCU will try to send data on D8-D15,
  * which are not connected, resulting in corrupted colors.
  */
 hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_8;
 hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
 hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
 hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
 hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE;
 hsram1.Init.PageSize = FSMC_PAGE_SIZE_NONE;
 // Timing Parameters (Tuned for ST7789)
 Timing.AddressSetupTime = 15;
 Timing.AddressHoldTime = 15;
 Timing.DataSetupTime = 60;
 Timing.BusTurnAroundDuration = 5;
 Timing.CLKDivision = 16;
 Timing.DataLatency = 17;
 Timing.AccessMode = FSMC_ACCESS_MODE_A;
 HAL_SRAM_Init(&hsram1, &Timing, NULL);
}
static void MX_GPIO_Init(void) {
 GPIO_InitTypeDef GPIO_InitStruct = {0};
 __HAL_RCC_GPIOF_CLK_ENABLE();
 __HAL_RCC_GPIOE_CLK_ENABLE();
 __HAL_RCC_GPIOD_CLK_ENABLE();
 __HAL_RCC_GPIOG_CLK_ENABLE();
 // Initial Pin States
 HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET); // Backlight Off initially
 HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_SET);   // Reset High (Inactive)
 // Configure LCD Pins (Backlight & Reset)
 GPIO_InitStruct.Pin = GPIO_PIN_9;
 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
 GPIO_InitStruct.Pull = GPIO_NOPULL;
 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
 HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
 GPIO_InitStruct.Pin = GPIO_PIN_3;
 HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
 // Configure Sensor Pins (Bit-Banging I2C)
 // Must be Open-Drain (OD) to allow the line to be pulled Low by both MCU and Sensor
 HAL_GPIO_WritePin(GPIOE, GPIO_PIN_0|GPIO_PIN_1, GPIO_PIN_SET);
 GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
 GPIO_InitStruct.Pull = GPIO_PULLUP;
 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
 HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
}
void Error_Handler(void) { __disable_irq(); while (1) {} }

drv_lcd.c

C/C++
#include "main.h"
#include "drv_lcd.h"
#include "drv_lcd_font.h"
#include <string.h>

/*
 * SECTION 1: FSMC MEMORY MAPPING
 * The STM32 treats the LCD not as a peripheral, but as external MEMORY (SRAM).
 * We write to specific memory addresses to send Commands or Data.
 *
 * Base Address (0x68000000):
 * - Corresponds to FSMC Bank 1, Sector 3 (NE3 pin PG10).
 *
 * Offset (0x40000):
 * - We use Address Line 18 (A18 / PD13) as the "Register Select" (RS) pin.
 * - 2^18 = 262,144 = 0x40000.
 * - Writing to (Base)         -> A18 is Low  -> LCD interprets as COMMAND.
 * - Writing to (Base + 0x40K) -> A18 is High -> LCD interprets as DATA.
 */
#define LCD_BASE        ((uint32_t)0x68000000)
#define LCD_REG         (*((volatile uint8_t *)LCD_BASE))           // Write here for Commands
#define LCD_RAM         (*((volatile uint8_t *)(LCD_BASE + 0x40000))) // Write here for Data

// Global structure to hold LCD parameters
_lcd_dev lcddev;

// Default Colors
uint16_t BACK_COLOR = WHITE;
uint16_t FORE_COLOR = BLACK;

/*
 * SECTION 2: LOW-LEVEL BUS WRITES
 * These functions handle the physical transmission of bytes over the 8-bit
 * parallel bus (D0-D7).
 */

// Write a Command Byte (RS Pin Low)
void LCD_WR_REG(uint8_t reg) {
    LCD_REG = reg;
}

// Write a Data Byte (RS Pin High)
void LCD_WR_DATA8(uint8_t data) {
    LCD_RAM = data;
}

/**
 * @brief  Writes a 16-bit color value to the 8-bit bus.
 * @why    Our hardware bus is only 8 bits wide (D0-D7), but pixels are 16 bits (RGB565).
 * We must manually split the 16-bit integer into two 8-bit transfers.
 * The ST7789 expects the High Byte (MSB) first, then the Low Byte (LSB).
 */
void LCD_WR_DATA16(uint16_t data) {
    LCD_RAM = (data >> 8);   // Shift right to isolate High Byte (e.g., 0xF8 from 0xF800)
    LCD_RAM = (data & 0xFF); // Mask to isolate Low Byte (e.g., 0x00 from 0xF800)
}

// Prepares the LCD to receive pixel data (sends "Write RAM" command 0x2C)
void LCD_WriteRAM_Prepare(void) {
    LCD_WR_REG(lcddev.wramcmd);
}

// Wrapper to write a pixel color
void LCD_WriteRAM(uint16_t color) {
    LCD_WR_DATA16(color);
}

/*
 * SECTION 3: INITIALIZATION SEQUENCE
 * This sequence configures the ST7789 driver chip. These are manufacturer
 * specific "Magic Numbers" that set voltages, gamma curves, and update rates.
 */

void LCD_Reset(void) {
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_RESET);
    HAL_Delay(50);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_SET);
    HAL_Delay(50);
}

void LCD_Backlight_On(void) {
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_SET);
}

void LCD_Init(void) {
    LCD_Reset();
    LCD_Backlight_On();

    // 1. Wake up
    LCD_WR_REG(0x11); // Sleep Out
    HAL_Delay(120);

    // 2. Orientation & Format (how to interpret data)
    LCD_WR_REG(0x36); LCD_WR_DATA8(0x00); // Memory Access Control (Orientation)
    LCD_WR_REG(0x3A); LCD_WR_DATA8(0x05); // Pixel Format: 0x05 = 16-bit RGB565

    // 3. Power & Gamma Settings (Vendor Specific) - make sure the color is right , white is white, black is black
    LCD_WR_REG(0xB2); LCD_WR_DATA8(0x0C); LCD_WR_DATA8(0x0C); LCD_WR_DATA8(0x00); LCD_WR_DATA8(0x33); LCD_WR_DATA8(0x33);
    LCD_WR_REG(0xB7); LCD_WR_DATA8(0x35);
    LCD_WR_REG(0xBB); LCD_WR_DATA8(0x19);
    LCD_WR_REG(0xC0); LCD_WR_DATA8(0x2C);
    LCD_WR_REG(0xC2); LCD_WR_DATA8(0x01);
    LCD_WR_REG(0xC3); LCD_WR_DATA8(0x12);
    LCD_WR_REG(0xC4); LCD_WR_DATA8(0x20);
    LCD_WR_REG(0xC6); LCD_WR_DATA8(0x0F);
    LCD_WR_REG(0xD0); LCD_WR_DATA8(0xA4); LCD_WR_DATA8(0xA1);

    // Gamma Correction (Makes colors look correct)
    LCD_WR_REG(0xE0); LCD_WR_DATA8(0xD0); LCD_WR_DATA8(0x04); LCD_WR_DATA8(0x0D); LCD_WR_DATA8(0x11); LCD_WR_DATA8(0x13); LCD_WR_DATA8(0x2B); LCD_WR_DATA8(0x3F); LCD_WR_DATA8(0x54); LCD_WR_DATA8(0x4C); LCD_WR_DATA8(0x18); LCD_WR_DATA8(0x0D); LCD_WR_DATA8(0x0B); LCD_WR_DATA8(0x1F); LCD_WR_DATA8(0x23);
    LCD_WR_REG(0xE1); LCD_WR_DATA8(0xD0); LCD_WR_DATA8(0x04); LCD_WR_DATA8(0x0C); LCD_WR_DATA8(0x11); LCD_WR_DATA8(0x13); LCD_WR_DATA8(0x2C); LCD_WR_DATA8(0x3F); LCD_WR_DATA8(0x44); LCD_WR_DATA8(0x51); LCD_WR_DATA8(0x2F); LCD_WR_DATA8(0x1F); LCD_WR_DATA8(0x1F); LCD_WR_DATA8(0x20); LCD_WR_DATA8(0x23);

    // 4. Turn On
    LCD_WR_REG(0x21); // Display Inversion On (Fixes inverted colors on ST7789)
    LCD_WR_REG(0x29); // Display ON

    // 5. Set Software Parameters
    lcddev.width = 240;
    lcddev.height = 240;
    lcddev.setxcmd = 0x2A; // Column Address Set
    lcddev.setycmd = 0x2B; // Row Address Set
    lcddev.wramcmd = 0x2C; // Write Memory Start

    LCD_Clear(WHITE); // Clear screen garbage data
}

/*
 * SECTION 4: DRAWING LOGIC
 */

// Defines the active rectangular area we are writing to
void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
    LCD_WR_REG(lcddev.setxcmd); // Set X coordinates
    LCD_WR_DATA8(x1 >> 8); LCD_WR_DATA8(x1 & 0xFF); // Start X
    LCD_WR_DATA8(x2 >> 8); LCD_WR_DATA8(x2 & 0xFF); // End X

    LCD_WR_REG(lcddev.setycmd); // Set Y coordinates
    LCD_WR_DATA8(y1 >> 8); LCD_WR_DATA8(y1 & 0xFF); // Start Y
    LCD_WR_DATA8(y2 >> 8); LCD_WR_DATA8(y2 & 0xFF); // End Y

    LCD_WriteRAM_Prepare(); // Ready to write pixel data
}

// Fills the entire screen with a single color
void LCD_Clear(uint16_t color) {
    LCD_SetWindow(0, 0, lcddev.width-1, lcddev.height-1); // Select full screen
    uint32_t total_pixels = lcddev.width * lcddev.height;

    // Burst write the color to every pixel
    for (uint32_t i = 0; i < total_pixels; i++) {
        LCD_WR_DATA16(color);
    }
}

// Draw a single pixel at (x, y)
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) {
    if (x >= lcddev.width || y >= lcddev.height) return; // Bounds check
    LCD_SetWindow(x, y, x, y); // Select 1x1 window
    LCD_WR_DATA16(color);      // Write color
}

/*
 * SECTION 5: TEXT RENDERING
 * Uses the font array defined in drv_lcd_font.h to draw text.
 */

// Draws a single character
void LCD_ShowChar(uint16_t x, uint16_t y, uint8_t num, uint16_t color, uint16_t back_color) {
    uint8_t temp, t;
    uint16_t pos;

    if (x > lcddev.width - 8 || y > lcddev.height - 16) return;

    num = num - ' '; // Adjust ASCII to array index (font starts at space)
    LCD_SetWindow(x, y, x + 7, y + 15); // Set 8x16 window

    // Loop through the 16 rows of the character font
    for (pos = 0; pos < 16; pos++) {
        temp = asc2_1608[num * 16 + pos]; // Get the bit pattern for this row

        // Loop through the 8 pixels in the row
        for (t = 0; t < 8; t++) {
            // Check if bit is 1 (Foreground) or 0 (Background)
            if (temp & 0x80) LCD_WR_DATA16(color);
            else             LCD_WR_DATA16(back_color);
            temp <<= 1; // Shift to next bit
        }
    }
}

// Draws a String
void LCD_ShowString(uint16_t x, uint16_t y, char *p, uint16_t color, uint16_t back_color) {
    while (*p != '\0') { // Loop until null terminator
        // Auto-wrap logic
        if (x > lcddev.width - 8) {
            x = 0;
            y += 16;
        }
        if (y > lcddev.height - 16) break;

        LCD_ShowChar(x, y, *p, color, back_color);
        x += 8; // Move cursor right
        p++;    // Move to next char
    }
}

drv_aht21.c

C/C++
#include "drv_aht21.h"

void SDA_IN(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = AHT_SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;     // Internal resistor pulls line high when idle
    HAL_GPIO_Init(AHT_SDA_PORT, &GPIO_InitStruct);
}

// Switch SDA pin to OUTPUT mode (Transmit)
// We do this when sending commands (Start, Address, Data) to the sensor.
void SDA_OUT(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = AHT_SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // Open-Drain is standard for I2C
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(AHT_SDA_PORT, &GPIO_InitStruct);
}

/* Pin State Macros - Shortcuts for setting pins High/Low */
#define SCL_H HAL_GPIO_WritePin(AHT_SCL_PORT, AHT_SCL_PIN, GPIO_PIN_SET)
#define SCL_L HAL_GPIO_WritePin(AHT_SCL_PORT, AHT_SCL_PIN, GPIO_PIN_RESET)
#define SDA_H HAL_GPIO_WritePin(AHT_SDA_PORT, AHT_SDA_PIN, GPIO_PIN_SET)
#define SDA_L HAL_GPIO_WritePin(AHT_SDA_PORT, AHT_SDA_PIN, GPIO_PIN_RESET)
#define READ_SDA HAL_GPIO_ReadPin(AHT_SDA_PORT, AHT_SDA_PIN)

// Simple delay to control I2C bus speed (Bit-Banging speed limit)
void I2C_Delay(void) {
    volatile int i = 400;
    while(i--);
}

/*
 * SECTION 2: I2C PROTOCOL PRIMITIVES (The Transport Layer)
 * These functions generate the specific electrical signals required by the
 * I2C standard.
 */

// Generate START condition: SDA goes High->Low while SCL is High
void I2C_Start(void) {
    SDA_OUT();    // Ensure we are in control
    SDA_H; SCL_H;
    I2C_Delay();
    SDA_L;        // Pull Data Low (Start Signal)
    I2C_Delay();
    SCL_L;        // Hold Clock Low (Bus Busy)
}

// Generate STOP condition: SDA goes Low->High while SCL is High
void I2C_Stop(void) {
    SDA_OUT();
    SCL_L; SDA_L; // Prepare Data Low
    I2C_Delay();
    SCL_H;        // Release Clock
    I2C_Delay();
    SDA_H;        // Release Data High (Stop Signal)
    I2C_Delay();
}

// Wait for the Sensor to acknowledge (ACK) our command
// Returns 0 if ACK received, 1 if Timeout/Error
uint8_t I2C_WaitAck(void) {
    uint32_t errTime = 0;
    SDA_IN();     // Switch to Input to listen
    SDA_H;        // Release line
    I2C_Delay();
    SCL_H;        // Clock High (Tell sensor to send ACK bit)
    I2C_Delay();

    // Wait for Sensor to pull SDA Low (ACK)
    while(READ_SDA) {
        errTime++;
        // Safety timeout to prevent infinite loop if sensor is missing
        if(errTime > 3000) {
            I2C_Stop();
            return 1;
        }
    }
    SCL_L; // Clock Low (Finish ACK cycle)
    return 0;
}

// Send 8 bits (1 Byte) to the sensor, MSB first
void I2C_SendByte(uint8_t byte) {
    SDA_OUT();
    SCL_L;
    for(int i=0; i<8; i++) {
        // Check if the current bit is 1 or 0
        if(byte & 0x80) SDA_H;
        else            SDA_L;

        byte <<= 1;   // Shift left to next bit
        I2C_Delay();
        SCL_H;        // Clock High (Sensor reads bit)
        I2C_Delay();
        SCL_L;        // Clock Low
        I2C_Delay();
    }
}

// Read 8 bits (1 Byte) from the sensor
uint8_t I2C_ReadByte(unsigned char ack) {
    unsigned char i, receive = 0;
    SDA_IN(); // Switch to Input to read
    for(i=0; i<8; i++) {
        SCL_L;
        I2C_Delay();
        SCL_H;        // Clock High (Data is valid)
        receive <<= 1; // Shift current data left
        if(READ_SDA) receive++; // Read pin, if High add 1
        I2C_Delay();
    }
    SDA_OUT(); // Switch back to Output to send response

    // Send ACK (Low) if we want more data, or NACK (High) to stop
    if (!ack) {
    	SCL_L; SDA_H; SCL_H;
    	I2C_Delay();
    	SCL_L;
    } else {
    	SCL_L; SDA_L; SCL_H;
    	I2C_Delay();
    	SCL_L;
    }
    return receive;
}

/*
 * SECTION 3: AHT21 SPECIFIC LOGIC (The Application Layer)
 */

// Initialize the sensor by sending the 0xBE command
void AHT21_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // Enable GPIO Clock
    __HAL_RCC_GPIOE_CLK_ENABLE();

    // Default pin configuration
    GPIO_InitStruct.Pin = AHT_SCL_PIN | AHT_SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(AHT_SCL_PORT, &GPIO_InitStruct);

    SCL_H; SDA_H;
    HAL_Delay(100); // Power-on delay required by datasheet

    // Send Calibration Command
    I2C_Start();
    I2C_SendByte(AHT_ADDR << 1); // Address + Write bit
    if(!I2C_WaitAck()) {
        I2C_SendByte(0xBE); // Init Command
        I2C_WaitAck();
        I2C_SendByte(0x08); // Parameter 1
        I2C_WaitAck();
        I2C_SendByte(0x00); // Parameter 2
        I2C_WaitAck();
        I2C_Stop();
    }
    HAL_Delay(10);
}

// Read Temperature and Humidity
uint8_t AHT21_Read(float *Temperature, float *Humidity) {
    uint8_t data[6];

    // 1. Trigger Measurement Command (0xAC)
    I2C_Start();
    I2C_SendByte(AHT_ADDR << 1);
    if(I2C_WaitAck()) return 0; // Check for device presence
    I2C_SendByte(0xAC); // Trigger Measure
    if(I2C_WaitAck()) return 0;
    I2C_SendByte(0x33); // Param 1
    if(I2C_WaitAck()) return 0;
    I2C_SendByte(0x00); // Param 2
    if(I2C_WaitAck()) return 0;
    I2C_Stop();

    // 2. Wait for Measurement (Sensor needs ~80ms to process)
    HAL_Delay(80);

    // 3. Read Data (6 Bytes)
    I2C_Start();
    I2C_SendByte((AHT_ADDR << 1) | 1); // Address + Read bit
    if(I2C_WaitAck()) return 0;

    for(int i=0; i<6; i++) {
        data[i] = I2C_ReadByte(i < 5); // ACK first 5 bytes, NACK last one
    }
    I2C_Stop();

    // 4. Parse Data (Bit shifting)
    // The sensor sends 20-bit values split across bytes. We assume the data is valid.
    // Humidity: Combined from Byte 1, Byte 2, and top 4 bits of Byte 3
    uint32_t rawHum = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | ((data[3] & 0xF0) >> 4);
    // Temperature: Combined from lower 4 bits of Byte 3, Byte 4, and Byte 5
    uint32_t rawTemp = ((uint32_t)(data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5];

    // 5. Convert to Floating Point (Formulas from Datasheet)
    *Humidity = (float)rawHum * 100.0f / 1048576.0f;
    *Temperature = ((float)rawTemp * 200.0f / 1048576.0f) - 50.0f;

    return 1;
}

drv_lcd.h

C/C++
#ifndef __DRV_LCD_H
#define __DRV_LCD_H

#include "main.h"

/* LCD Colors */
#define WHITE       0xFFFF
#define BLACK       0x0000
#define BLUE        0x001F
#define RED         0xF800
#define GREEN       0x07E0
#define YELLOW      0xFFE0
#define GRAY        0X8430

/* LCD Parameter Structure */
typedef struct {
    uint16_t width;
    uint16_t height;
    uint16_t id;
    uint8_t  dir;
    uint16_t wramcmd;
    uint16_t setxcmd;
    uint16_t setycmd;
} _lcd_dev;

extern _lcd_dev lcddev;
extern uint16_t BACK_COLOR;
extern uint16_t FORE_COLOR;

/* Function Prototypes */
void LCD_Init(void);
void LCD_Clear(uint16_t color);
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color);
void LCD_ShowString(uint16_t x, uint16_t y, char *p, uint16_t color, uint16_t back_color);
void LCD_ShowChar(uint16_t x, uint16_t y, uint8_t num, uint16_t color, uint16_t back_color);

#endif

Credits

PRINCESS LAURON
3 projects • 1 follower
HEART LAHOYLAHOY
3 projects • 0 followers

Comments