Skip to main content

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 LossRate per 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] or LossRate [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?.