main/task_driver.c

Fri, 28 Jun 2024 15:33:24 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Fri, 28 Jun 2024 15:33:24 +0200
branch
idf 5.1
changeset 137
e0f50087c909
parent 136
89fc3c57282e
permissions
-rw-r--r--

Fixed changing runtime datarecord size during switching between IDF 4.2 and 5.1. Fixed wiping the /spiffs filesystem. The directory listing from the SD card doesn't overwrite parts of the screen anymore. Solved the slow speed issue with the SD card. Try to force the SD card to operate at 20 MHz. More project settings changed to improve performance and memory usage.

/**
 * @file task_driver.c
 * @brief BrewBoard relays driver. Control the hardware outputs with the
 *        Solid State relays for the Mash/Boil kettle (MLT, the Hot
 *        Liquer Tank (HLT) and the pump. The MLT has a PID controller
 *        during mashing, and a simple bang on/off control during the
 *        boil.
 *        The HLT output can be off, bang on/off, or just on if the MLT
 *        is off, depending on the configuration.
 *        Use SSR modules that switch during zero crossing of the mains
 *        power, so that when one is turned on and on the same time the
 *        other is turned off, they won't be active at the same time.
 */

#include "config.h"


#define	SSR_MLT		CONFIG_SSR_MLT_GPIO			///< GPIO SSR MLT pin
#define	SSR_HLT		CONFIG_SSR_HLT_GPIO			///< GPIO SSR HLT pin
#define SSR_PUMP	CONFIG_SSR_PUMP_GPIO			///< GPIO Pump relay pin
#define	PWM_MLT		CONFIG_PWM_MLT_GPIO			///< GPIO PWM MLT pin

#define	HENDI_SWITCH	6					///< Hendi power on/off at %

extern my_equipment_t	equipment;
extern my_runtime_t	runtime;

bool			outEnable = false;			///< Enable outputs flag
DRIVER_State		* driver_state;				///< Driver state
SemaphoreHandle_t	xSemaphoreDriver  = NULL;		///< Driver state lock
int			MLT_pin = 0;				///< MLT state
int			HLT_pin = 0;				///< HLT state
int			Pump_pin = 0;				///< Pump state
double			Input = 0;				///< PID input value
double			Output = 0;				///< PID output value
double			Setpoint = 0;				///< PID setpoint value
int			MLT_Mode = MLT_MODE_NONE;		///< MLT mode flag
double			HLT_Input = 0;				///< HLT input value
double			HLT_Setpoint = 0;			///< HLT setpoint values
int			HLT_Output = 0;				///< HLT output value
int			HLT_Mode = HLT_MODE_NONE;		///< HLT mode flag
TickType_t		MLT_time, HLT_time;

static const char	*MLTTypes[] = { "None", "Bang", "PID", "Off", "Ext" };
static const char	*HLTTypes[] = { "None", "Bang", "Off", "On" };

static const char       *TAG = "task_driver";

extern SemaphoreHandle_t	xSemaphoreDS18B20;
extern DS18B20_State		*ds18b20_state;
extern unsigned long		lastTime;


/**
 * @brief Turn the MLT SSR on or off.
 */
void MLT(int onoff);

/**
 * @brief Turn the HLT SSR on or off.
 */
void HLT(int onoff);

/**
 * @brief Turn the Pump on or off.
 */
void Pump(int onoff);

/**
 * @brief Calculate and set PWM value.
 * @param percent The power percentage, 0..100
 */
void MLT_PWM(int percent);



void MLT(int onoff) {

    if (onoff && outEnable) {
	if (MLT_pin != 1 && ! equipment.Hendi)
	    MLT_time = xTaskGetTickCount();
	gpio_set_level(SSR_MLT, 1);
	MLT_pin = 1;
    } else {
	if (MLT_pin && ! equipment.Hendi)
	    runtime.MLT_usage += xTaskGetTickCount() - MLT_time;
	gpio_set_level(SSR_MLT, 0);
	MLT_pin = 0;
    }
}



void HLT(int onoff) {

    if (onoff && outEnable) {
	if (HLT_pin != 1)
	    HLT_time = xTaskGetTickCount();
	gpio_set_level(SSR_HLT, 1);
	HLT_pin = 1;
    } else {
	if (HLT_pin)
	    runtime.HLT_usage += xTaskGetTickCount() - HLT_time;
	gpio_set_level(SSR_HLT, 0);
	HLT_pin = 0;
    }
}



void Pump(int onoff) {

    if (onoff && outEnable) {
	gpio_set_level(SSR_PUMP, 1);
	Pump_pin = 1;
    } else {
	gpio_set_level(SSR_PUMP, 0);
	Pump_pin = 0;
    }
}


void MLT_PWM(int percent) {
    int		val;
    static int	oldval = -1;

    if (outEnable) {
	if (percent < 0) {
	    val = 0;
	    MLT_time = xTaskGetTickCount();
	} else if (percent > 100) {
	    val = 1024;
	} else {
    	    val = (percent * 1024) / 100;
	}
    } else {
	val = 0;
	MLT_time = xTaskGetTickCount();
    }

    /*
     * If the Hendi is on, the lowest setting is 500 Watt. So, if we need less then
     * HENDI_SWITCH power, turn it off, just like the manual knob.
     */
    MLT((percent >= HENDI_SWITCH) ? 1:0);

    if (val != oldval) {
    	log_msg(TAG, "MLT(%d) MLT_PWM(%d) val=%d %.0f watt pv: %.3f", (percent >= HENDI_SWITCH) ? 1:0,
			percent, val, (percent / 100.0) * equipment.MLT_watt, driver_state->mlt_pv);
    	ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0,/* 1024 - */val);
    	ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
	if (val >= HENDI_SWITCH) {
	    runtime.MLT_usage += (xTaskGetTickCount() - MLT_time) * (percent / 100.0);
	}
	MLT_time = xTaskGetTickCount();
    }
    oldval = val;
}



/**
 * @brief Load PID settings from equipment record.
 */
void LoadPIDsettings() {
    PID_SetTunings(equipment.PID_kP, equipment.PID_kI, equipment.PID_kD);
    PID_SetSampleTime(equipment.SampleTime);

    /*
     * Initialize the PID
     */
    Output = 0.0;   // Reset internal Iterm.
    PID_SetMode(PID_MANUAL);
    PID_SetMode(PID_AUTOMATIC);
}



void AllowHLT(void) {
    if (equipment.SSR2 == SSR2_HLT_SHARE) {
	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
	    HLT_Output = 0;
	    if (driver_state->hlt_mode == HLT_MODE_BANG) {
		HLT_Output = (HLT_Input < HLT_Setpoint) ? 1:0;
	    } else if (driver_state->hlt_mode == HLT_MODE_ON) {
		HLT_Output = 1;
	    }
	    xSemaphoreGive(xSemaphoreDriver);
	}
	HLT(HLT_Output);
    } else if (equipment.SSR2 == SSR2_ON_IDLE) {
	HLT_Output = 1;
	HLT(1);
    }
}



void task_driver(void *pvParameter)
{
    TickType_t		wait_ticks, last_tick, now_tick;
    bool		rc;
    int			SafeCount = 0;
    unsigned long	now, RealTime, w_StartTime = 0;

    ESP_LOGI(TAG, "Start drivers");

    /*
     * Configure IOMUX register.
     */
    gpio_pad_select_gpio(SSR_MLT);
    gpio_set_direction(SSR_MLT, GPIO_MODE_OUTPUT);
    gpio_pad_select_gpio(SSR_HLT);
    gpio_set_direction(SSR_HLT, GPIO_MODE_OUTPUT);
    gpio_pad_select_gpio(SSR_PUMP);
    gpio_set_direction(SSR_PUMP, GPIO_MODE_OUTPUT);

    // Prepare and then apply the LEDC PWM timer configuration
    ledc_timer_config_t ledc_timer = {
	.speed_mode = LEDC_HIGH_SPEED_MODE,		///< Use high speed timer
	.timer_num = LEDC_TIMER_0,			///< Timer 0
	.duty_resolution = LEDC_TIMER_10_BIT,		///< 10 bits resolution
	.freq_hz = 100,					///< 100 Hz
	.clk_cfg = LEDC_AUTO_CLK			///< Auto select PWM clock
    };
    ledc_timer_config(&ledc_timer);

    ledc_channel_config_t pwm_channel = {
	.channel    = LEDC_CHANNEL_0,
	.duty       = 1024,				///< Default 0% (inverted value)
	.gpio_num   = PWM_MLT,				///< MLT pin
	.speed_mode = LEDC_HIGH_SPEED_MODE,
	.hpoint     = 0,
	.intr_type  = LEDC_INTR_DISABLE,
	.timer_sel  = LEDC_TIMER_0			///< Timer 0
    };
    ledc_channel_config(&pwm_channel);

    /*
     * Initialize state
     */
    driver_state = malloc(sizeof(DRIVER_State));
    driver_state->enable = outEnable = false;
    driver_state->mlt_gpio = SSR_MLT;
    driver_state->mlt_mode = MLT_MODE_NONE;
    driver_state->mlt_sp = driver_state->mlt_pv = 0.0;
    driver_state->mlt_power = 0;
    driver_state->hlt_gpio = SSR_HLT;
    driver_state->hlt_mode = HLT_MODE_NONE;
    driver_state->hlt_sp = driver_state->hlt_pv = 0.0;
    driver_state->hlt_power = 0;
    driver_state->hlt_and_mlt = false;
    driver_state->pump_gpio = SSR_PUMP;
    driver_state->pump_run = 0;

    PID(&Input, &Output, &Setpoint, 200, 2.0, 1.5, PID_DIRECT);

    /*
     * One loop must complete in 20 mSecs, that is one mains
     * frequency period cycle in 50 Hz countries.
     */
    while (1) {

	last_tick = xTaskGetTickCount();

	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
	    /*
	     * Get the current temperature readings
	     */
	    if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) {
		if (ds18b20_state->mlt_valid)
		    driver_state->mlt_pv = ds18b20_state->mlt_temperature;
		if (ds18b20_state->hlt_valid)
		    driver_state->hlt_pv = ds18b20_state->hlt_temperature;
		xSemaphoreGive(xSemaphoreDS18B20);
	    }

	    /*
	     * Other values that we need
	     */
	    Input = driver_state->mlt_pv;
	    Setpoint = driver_state->mlt_sp;
	    if (driver_state->mlt_mode != MLT_Mode) {
		if (driver_state->mlt_mode == MLT_MODE_BANG) {
		    PID_SetMode(PID_MANUAL);
		} else if (driver_state->mlt_mode == MLT_MODE_PID) {
		    LoadPIDsettings();
		}
		MLT_Mode = driver_state->mlt_mode;
		ESP_LOGI(TAG, "MLT mode set to %s", MLTTypes[MLT_Mode]);
	    }
	    if (driver_state->hlt_mode != HLT_Mode) {
		HLT_Mode = driver_state->hlt_mode;
		ESP_LOGI(TAG, "HLT mode set to %s", HLTTypes[HLT_Mode]);
	    }
	    outEnable = driver_state->enable;
	    HLT_Input = driver_state->hlt_pv;
	    HLT_Setpoint = driver_state->hlt_sp;
	    xSemaphoreGive(xSemaphoreDriver);
	}

	rc = false;
	now = xTaskGetTickCount() * portTICK_PERIOD_MS;

	if ((PID_GetMode() == PID_AUTOMATIC) && (MLT_Mode == MLT_MODE_PID)) {
	    rc = PID_Compute();
	    RealTime = (equipment.SampleTime * equipment.MashPower) / 100;
	} else {
	    /*
	     * Schedule the loop ourself.
	     */
	    unsigned long timeChange = (now - lastTime);
	    if (timeChange >= equipment.SampleTime) {
		lastTime = now;
		rc = true;
	    }
	    RealTime = equipment.SampleTime;
	    if (driver_state->mlt_mode == MLT_MODE_BANG) {
	        Output = (Input < Setpoint) ? 255:0;
	    }
	    if (driver_state->mlt_mode == MLT_MODE_NONE || driver_state->mlt_mode == MLT_MODE_OFF) {
		Output = 0;
	    }
	}

	if (rc) {
	    w_StartTime = now;
	}

	if (equipment.Hendi) {

	    int PWMout = (int)((Output * 100) / 255.0);

	    if ((PID_GetMode() == PID_AUTOMATIC) && (MLT_Mode == MLT_MODE_PID)) {
		/* Mash power limited */
		PWMout = (PWMout * equipment.MashPower) / 100;
	    }

	    /*
	     * Do not send power values < HENDI_SWITCH.
	     */
	    if (PWMout >= HENDI_SWITCH) {	// Hendi minimum power is 500 Watt, this is 10%
		if ((((PWMout / 100.0) * equipment.MLT_watt) + equipment.HLT_watt) > equipment.Max_watt) {
		    if (HLT_pin) {
		    	ESP_LOGI(TAG, "Current %.0f Watt above %d limit, shutdown HLT",
					((PWMout / 100.0) * equipment.MLT_watt) + equipment.HLT_watt, equipment.Max_watt);
		    	HLT_Output = 0;
		    	HLT(0);		// As soon as possible before the Hendi increases power.
		    }
		    SafeCount = (int)(15000 / equipment.SampleTime) + 1; // About 15 seconds release time
		} else if (rc) {
		    if (SafeCount > 0) {
			SafeCount--;
		    } else {
			AllowHLT();
		    }
		}
		MLT_PWM(PWMout);
	    } else {
		MLT_PWM(0);
		if (rc) {
		    if (SafeCount > 0) {
                    	SafeCount--;
        	    } else {
		    	AllowHLT();
		    }
		}
	    }
	} else if ((int)((Output / 255.0) * RealTime) > (now - w_StartTime)) {
	    /*
	     * Use On/Off kettles using time slices.
	     */
	    MLT(1);
	    if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_ON_IDLE)) {
	    	HLT(0);
		HLT_Output = 0;
	    }
	} else {
	    MLT(0);
	    AllowHLT();
	}

	/*
	 * Independant HLT temperature control
	 */
	if  (equipment.SSR2 == SSR2_HLT_IND) {
	    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
		HLT_Output = 0;
		if (driver_state->hlt_mode == HLT_MODE_BANG) {
		    HLT_Output = (HLT_Input < HLT_Setpoint) ? 1:0;
		} else if (driver_state->hlt_mode == HLT_MODE_ON) {
		    HLT_Output = 1;
		}
		xSemaphoreGive(xSemaphoreDriver);
	    }
	    HLT(HLT_Output);
	}

	/*
	 * Update the driver results.
	 */
	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
	    driver_state->mlt_power = (int)((Output * 100) / 255.0);
	    if (HLT_Output) {
		if (equipment.SSR2 == SSR2_HLT_SHARE) {
	    	    driver_state->hlt_power = 100 - driver_state->mlt_power;
		} else if  (equipment.SSR2 == SSR2_HLT_IND) {
		    driver_state->hlt_power = 100;
		}
	    } else {
	    	driver_state->hlt_power = 0;
	    }
	    if (driver_state->pump_run != Pump_pin)
		Pump(driver_state->pump_run);
	    xSemaphoreGive(xSemaphoreDriver);
	}

#if 0
	if (rc) {
	    printf("ST: %s MLT[In: %7.3f Out: %3.0f Sp: %6.2f %s RT: %lu]  HLT[In: %7.3f Out: %d Sp: %5.1f]\n", outEnable ? "E":"D",
		    Input, Output, Setpoint, PID_GetMode() ? "AUTOMATIC" : "MANUAL   ", RealTime,
		    HLT_Input, HLT_Output, HLT_Setpoint);
	}
#endif

	// Do not use vTaskDelayUntil(), it is not reliable here.
	now_tick = xTaskGetTickCount();
	if ((now_tick - last_tick) > (1000 / 50)) {
	    // This happens one or two times during a brew.
	    wait_ticks = (1000 / 50);
	} else {
	    wait_ticks = (1000 / 50) - (now_tick - last_tick);
	}
	if (wait_ticks == 0) {
	    // This is rare, but it happens.
	    wait_ticks = 1;
	}

	vTaskDelay(wait_ticks);
    }
}

mercurial