STM32_09.GardeningBlackPillSTM32F411CEU6

Garden Automation Controller

Gardening automation on STM32F411CEU6. The firmware handles soil moisture sensors, water level detection, pump control, plant lighting and UART configuration.

STM32F4CHALAutomationBlack Pill
Schematic of the garden automation controller on STM32F411CEU6 Black Pill with ADC inputs and pump/LED outputs.

Project facts

  • Repository: STM32_09.GardeningBlackPillSTM32F411CEU6
  • Board / area: STM32F411CEU6 Black Pill
  • Category: Automation
  • Status: Public repository

Technical description

  • The application connects analog and digital inputs with actuator decisions: pump start, water-level lockout and grow-light control.
  • The code handles two soil-moisture sensors, an LDR light sensor, a water-level input, a pump output and a lighting output.
  • The STM32CubeIDE structure contains pin and peripheral configuration in the .ioc file.
  • The schematic includes 12 V/3.3 V power, the Black Pill F411, ADC sensor paths, a buzzer and transistor/MOSFET output stages for the pump and LED.
  • The firmware combines ADC sampling with DMA, flash-stored configuration and actuator decisions based on moisture, light and water level.

What happens in the code

Selected key mechanisms from the project code. This is not a full repository dump, but an analysis of the parts responsible for the main firmware logic.

  • main.c implements a complete gardening controller: soil moisture, light sensor, water-level input, pump output and lighting output.
  • ADC runs through DMA: three channels land in adc_buffer and the code distributes samples into dedicated buffers for two moisture sensors and an LDR.
  • Calibration is persisted in flash. On startup the firmware checks a magic value and writes defaults on the first run.
  • UART acts as a service interface. Commands such as S1DValue, S1WValue, lightTime, runPump and lightOn allow calibration and testing without recompilation.
  • Pump logic uses hysteresis: start below the low moisture threshold, stop above the high threshold and block operation when water level is low.
  • Lighting logic distinguishes DARK, NIGHT_OR_MORNING and BRIGHT, and grow-light operation is controlled by a time window rather than a simple on/off decision.
  • Flash writing: configuration data is written to flash memory when settings change, ensuring persistence after reset.
  • Pump control: MixWater function implements periodic water mixing every 3 hours with pulse and pause phases.

Key code excerpts

The code is shortened to the parts that matter for understanding the project.

Configuration persisted in flash

Core/Src/main.c
typedef struct {
  uint32_t magic;
  uint32_t Sensor1DryValue;
  uint32_t Sensor1WetValue;
  uint32_t Sensor2DryValue;
  uint32_t Sensor2WetValue;
  uint32_t GetMoistureAvg_TimeLenghtDefault;
  uint32_t GetMoistureAvg_TimeLenghtFast;
  uint32_t StartPumpWhenMoistureLower;
  uint32_t StopPumpWhenMoistureHigher;
  uint32_t lightTime;
} DataFlash;

if (dane.magic != FLASH_MAGIC) {
  dane.Sensor1DryValue = 2700;
  dane.Sensor1WetValue = 2020;
  dane.StartPumpWhenMoistureLower = 40;
  dane.StopPumpWhenMoistureHigher = 75;
  dane.lightTime = 3600 * 1000 * 10;
  memoryPageErase(0);
  Flash_WriteStruct(flash_address, &dane, sizeof(DataFlash));
}

This code turns the project into a real device, not a one-off test. Sensor calibration, pump thresholds and lighting time survive reset. The magic value detects whether flash contains valid data.

UART interface for calibration and testing

Core/Src/main.c
while (USART_TakeLine(line, sizeof(line))) {
  matched |= parse_kv_int(line, "S1DValue", &dane.Sensor1DryValue);
  matched |= parse_kv_int(line, "S1WValue", &dane.Sensor1WetValue);
  matched |= parse_kv_int(line, "StartPumpWhenMoistureLower",
                          &dane.StartPumpWhenMoistureLower);
  matched |= parse_kv_int(line, "StopPumpWhenMoistureHigher",
                          &dane.StopPumpWhenMoistureHigher);

  if (matched) {
    memoryPageErase(0);
    Flash_WriteStruct(flash_address, &dane, sizeof(DataFlash));
  }
}

The firmware accepts key:value text commands. This allows dry/wet soil calibration and pump thresholds to be changed from a UART terminal without editing code or reflashing.

Pump control with hysteresis and water-level protection

Core/Src/main.c
MoistureGeneral =
  (MoistureHumidityPercent_Dev_1 + MoistureHumidityPercent_Dev_2) / 2;

if (WaterSensor1State != GPIO_PIN_SET) {
  if (MoistureGeneral <= dane.StartPumpWhenMoistureLower &&
      PumpIsRunnign == false) {
    HAL_GPIO_WritePin(PIN_PB10_Pump_GPIO_Port, PIN_PB10_Pump_Pin, GPIO_PIN_SET);
    PumpIsRunnign = true;
  }
}

if (MoistureGeneral >= dane.StopPumpWhenMoistureHigher &&
    PumpIsRunnign == true) {
  HAL_GPIO_WritePin(PIN_PB10_Pump_GPIO_Port, PIN_PB10_Pump_Pin, GPIO_PIN_RESET);
  PumpIsRunnign = false;
}

The code averages two moisture sensors and controls the pump from the general value. Two thresholds create hysteresis, so the pump does not chatter near the boundary. The water-level sensor blocks pump start.

Grow-light control logic

Core/Src/main.c
if (LightSensorPercent <= DARK)
  LightSensor = DARK;
else if (LightSensorPercent > DARK && LightSensorPercent < BRIGHT)
  LightSensor = NIGHT_OR_MORNING;
else if (LightSensorPercent > BRIGHT)
  LightSensor = BRIGHT;

switch (LightSensor) {
  case NIGHT_OR_MORNING:
    if (LightTimerEnabled == false && waitFordark == false) {
      GetLightTime_FromTime = TimeNOW;
      LightTimerEnabled = true;
    }
    HAL_GPIO_WritePin(PIN_PB11_Light_GPIO_Port, PIN_PB11_Light_Pin, GPIO_PIN_SET);
    break;

  case BRIGHT:
    HAL_GPIO_WritePin(PIN_PB11_Light_GPIO_Port, PIN_PB11_Light_Pin, GPIO_PIN_RESET);
    break;
}

Lighting is not controlled by a single brightness threshold. The code detects a transition range, starts a lighting time window and disables the light when ambient brightness is too high.

Water mixing function

Core/Src/main.c
MixWater_State MixWaterState = MIX_WATER_IDLE;
const uint32_t MixWaterPulseTime = 5000;
const uint32_t MixWaterPauseTime = 5000;

switch (MixWaterState)
{
  case MIX_WATER_PULSE_1_ON:
    // Turn pump on for MixWaterPulseTime
  case MIX_WATER_PULSE_1_OFF:
    // Turn pump off for MixWaterPauseTime  
  case MIX_WATER_PULSE_2_ON:
    // Turn pump on for MixWaterPulseTime again
}

The water mixing function implements periodic water mixing every 3 hours with pulse and pause phases. It enables even distribution of fertilizers in the system.