Temperature Control Simulation
This tutorial implements a proportional temperature controller in C and tests it in closed loop against a simple model of a heated room. Along the way it shows workspace parameters, a plant model and verification, and uses verification to reveal a classic weakness of proportional control.
The controller
The controller compares the room temperature with the set point and sets the heater power in proportion to the error, limited to 0–100%.
tempControl.h
#pragma once
typedef struct TempControl_s
{
/* Configuration */
float setPoint;
float proportionalGain;
/* State */
float temperatureError;
float heaterPower;
} TempControl_ts;
void tempControlInit(TempControl_ts *const ctx, float setPoint, float proportionalGain);
void tempControlUpdate(TempControl_ts *const ctx, float currentTemp);
tempControl.c
#include "tempControl.h"
void tempControlInit(TempControl_ts *const ctx, float setPoint, float proportionalGain)
{
ctx->setPoint = setPoint;
ctx->proportionalGain = proportionalGain;
ctx->temperatureError = 0.0f;
ctx->heaterPower = 0.0f;
}
void tempControlUpdate(TempControl_ts *const ctx, float currentTemp)
{
/* Calculate error between setpoint and current temperature */
ctx->temperatureError = ctx->setPoint - currentTemp;
/* Apply proportional control */
ctx->heaterPower = ctx->proportionalGain * ctx->temperatureError;
/* Limit heater power between 0 and 100% */
if (ctx->heaterPower > 100.0f)
{
ctx->heaterPower = 100.0f;
}
else if (ctx->heaterPower < 0.0f)
{
ctx->heaterPower = 0.0f;
}
}
The simulation
A controller needs something to control. The simulation includes a minimal model of the room:
- The heater warms the room at
HeatingRate°C/s at full power. - The walls lose heat to the outside in proportion to the temperature difference, at
LossRateper second.
Every step, the simulation feeds the room temperature to the controller and applies the resulting heater power to the room model.
SimulateTempControl.c
#include <math.h>
#include <cppmodel/CModel.h>
#include "tempControl.h"
static TempControl_ts controller;
static float roomTemperature;
void RunCyclic(CModelSimulation_ts *self, unsigned long currentTime_ms)
{
float ambientTemperature = CppModel_getParameterF32(self, "AmbientTemperature [C]", 15.0f);
float heatingRate = CppModel_getParameterF32(self, "HeatingRate [C/s]", 0.01f);
float lossRate = CppModel_getParameterF32(self, "LossRate [1/s]", 0.001f);
float dt = self->stepTime_ms / 1000.0f;
if (currentTime_ms == 0)
{
/* Configure the controller and start with the room at ambient temperature */
tempControlInit(&controller,
CppModel_getParameterF32(self, "Setpoint [C]", 21.0f),
CppModel_getParameterF32(self, "ProportionalGain [%/C]", 20.0f));
roomTemperature = ambientTemperature;
}
/* Controller under test */
tempControlUpdate(&controller, roomTemperature);
/* Plant model: the heater adds heat, the walls leak it to the ambient */
roomTemperature += dt * (heatingRate * controller.heaterPower / 100.0f -
lossRate * (roomTemperature - ambientTemperature));
CppModel_getInputF32(self, "RoomTemperature [C]", roomTemperature);
CppModel_setOutputF32(self, "HeaterPower [%]", controller.heaterPower);
CppModel_setOutputF32(self, "TemperatureError [C]", controller.temperatureError);
/* Verification: after 20 minutes the room must be within 1 C of the setpoint */
CppModel_setOutputU8(self, "CppModel.StepResult",
currentTime_ms < 20 * 60 * 1000 || fabsf(controller.temperatureError) <= 1.0f);
}
int main(void)
{
CModelSimulation_ts *simulation = CppModel_create("TempControl", 30 * 60 * 1000, 1000);
CppModel_setRunStepFunction(simulation, RunCyclic);
CppModel_Simulate(simulation);
int result = CppModel_getSimulationResult(simulation);
CppModel_destroy(simulation);
return result;
}
The simulation runs 30 minutes of simulated time with a 1-second step. Every tuning value is read with CppModel_getParameterF32(), so it appears in the workspace after the first run and can be changed there without rebuilding.
Building
CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(TempControl LANGUAGES C)
list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/dependencies")
find_package(cppmodel REQUIRED)
enable_testing()
add_executable(SimulateTempControl tempControl.c SimulateTempControl.c)
target_link_libraries(SimulateTempControl PRIVATE cppmodel::cppmodel)
if (UNIX AND NOT APPLE)
target_link_libraries(SimulateTempControl PRIVATE m)
endif()
cppmodel_deploy_runtime(SimulateTempControl)
add_test(NAME TempControl_Test COMMAND SimulateTempControl)
cmake -S . -B build
cmake --build build
./build/SimulateTempControl
The CMake file assumes the CppModel library is extracted into a dependencies folder (see Installation).
Results
Open TempControl in workspace.cppmodel.com. The room heats up at full power and the heater power then drops as the error shrinks. But the room settles about 2 °C below the set point, and the verification graph turns red after 20 minutes. The program also exits with a non-zero code, so ctest reports the test as failed.
This is not a bug in the simulation. It is the steady-state error of a proportional controller. Holding the room at 21 °C against a 15 °C ambient takes about 60% heater power. A proportional controller produces power only from an error, and with a gain of 20 %/°C the balance point is an error of 2 °C (40% power).
Try it yourself
- In the workspace, raise
ProportionalGain [%/C]to 100 and run again. The remaining error drops to about 0.5 °C, and the verification passes. - Add an integral term to
tempControlUpdate()to remove the steady-state error completely. - Change
AmbientTemperature [C]orLossRate [1/s]to test the controller in a colder or draftier room.
For more on this kind of closed-loop simulation, see So, how do you fill a glass of water?.