main/task_driver.c

Wed, 28 Jul 2021 17:12:00 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Wed, 28 Jul 2021 17:12:00 +0200
changeset 110
2f9e48e5b8ce
parent 109
72af8958b469
child 112
77cf1e9866e5
permissions
-rw-r--r--

Changed PWM power calculation during PID use.

/**
 * @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


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)
	    MLT_time = xTaskGetTickCount();
	gpio_set_level(SSR_MLT, 1);
	MLT_pin = 1;
    } else {
	if (MLT_pin)
	    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;
	} else if (percent > 100) {
	    val = 1024;
	} else {
    	    val = (percent * 1024) / 100;
	}
    } else {
	val = 0;
    }

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

    if (val != oldval) {
    	log_msg(TAG, "MLT_PWM(%d) val=%d %.0f watt", percent, val, (percent / 100.0) * equipment.MLT_watt);
    	ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, 1024 - val);
    	ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
    }
    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;
//		if (PWMout > equipment.MashPower)
//		    PWMout = equipment.MashPower;
	    }

	    /*
	     * Do not send power values < 10%.
	     */
	    if (PWMout >= 10) {	// 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