Mon, 21 Jun 2021 19:04:10 +0200
Finished experimental code to drive the German HendiControl board. Added BoilPower and RampPower buttons during the while boil process. RampPower (going to boil power) is now adjustable. Added PWM driver code to the driver task.
/** * @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 Then 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; } } //int oldval = 200; 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 (val != oldval) { ESP_LOGI(TAG, "MLT_PWM(%d) val=%d %.0f watt", percent, val, (percent / 100.0) * equipment.MLT_watt); ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 1024 - val); ledc_update_duty(LEDC_LOW_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; 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_LOW_SPEED_MODE, ///< Use low speed .timer_num = LEDC_TIMER_1, .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_LOW_SPEED_MODE, .hpoint = 0, .intr_type = LEDC_INTR_DISABLE, .timer_sel = LEDC_TIMER_1 }; 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; driver_state->pwm_gpio = PWM_MLT; driver_state->pwm_mlt = false; driver_state->pwm_nohlt = 10; /* Conservative safety value. */ 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)) { if (PWMout > equipment.MashPower) PWMout = equipment.MashPower; } /* * Hendi minimum power is 500 Watt, this is 14%. * So, we turn the cooker on around 10% power. */ 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, "Power %f %d", ((PWMout / 100.0) * equipment.MLT_watt) + equipment.HLT_watt, equipment.Max_watt); ESP_LOGI(TAG, "Immediate HLT panic shutdown"); HLT_Output = 0; HLT(0); // As soon as possible before the Hendi increases power. } } else { AllowHLT(); // TODO: delay this one loop. } MLT_PWM(PWMout); MLT(1); } else { MLT_PWM(0); MLT(0); AllowHLT(); } } else if ((int)((Output / 255.0) * RealTime) > (now - w_StartTime)) { 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); } }