A Practical Position-Form PID Controller in C for Temperature Control
From Temperature Setpoint to Triac Output
While developing a temperature-controlled flushing machine for a customer, I had the opportunity to implement and tune a PID controller from the ground up. The project provided a useful example of how a relatively abstract control algorithm maps onto a real heating system.
Applying PID to a Heating System
The basic idea behind PID control is straightforward: use the error between the desired value and the measured value to regulate the controlled process. The controller combines three terms:
- Proportional term, determined by
Kp - Integral term, determined by
Ki - Derivative term, determined by
Kd
The main variables are the error err(t) and the controller output u(t). In this project, the abstract variables correspond to the following physical signals:
- The input or setpoint is the required water temperature.
- The measured output is the actual water temperature.
- A high-precision NTC probe provides the temperature feedback.
- A triac serves as the actuator.
The error is therefore the difference between the requested temperature and the temperature measured by the probe. The controller calculates u(x) from this error and uses the result to determine the actuator command.
The controller output is treated as a duty-cycle value. The triac chops the mains waveform, and the ratio between the resulting output voltage and the mains voltage is represented by that duty cycle. By changing the effective power delivered to the heating element, the triac changes the rate at which the water temperature rises.
The triac itself is not the focus here. In practical terms, current applied to its gate turns it on, while it turns off at a zero crossing when the gate drive is removed. This switching behavior makes it possible to regulate the heater's power through phase or waveform control.
Position Form and Incremental Form
PID controllers are commonly implemented in either position form or incremental form. This project uses the position form.
A position-form controller calculates the output corresponding to the current control state or time position. It is suitable for applications such as servo systems and temperature regulation. Because the integral term accumulates historical error, the output depends on the entire previous state of the system. For that reason, both the integral term and the final output need appropriate limits.
Incremental PID is more suitable for control units such as stepper motors. Instead of calculating an absolute output, it calculates how much the actuator command should change relative to the previous command. The control increment depends only on the most recent three sampled values, so it does not require an accumulated error term. Output limiting is still required.
Data Structure
The controller state is stored in a structure containing the three gains, the current and previous errors, the integral accumulator, and the permitted output range:
typedef struct
{
float Kp, Ki, Kd; // 三个比例系数
float setVal, targetVal; // 设定值和目标值
float err; // 偏差值
float errLast; // 上个偏差值
float integral; // 定义积分值
float pwmMax;
float pwmMin;
float pwm;
}Pid_t;
The main position-form calculation is:
gsPidParameter.pwm = gsPidParameter.Kp*gsPidParameter.err+index*gsPidParameter.Ki*gsPidParameter.integral+gsPidParameter.Kd*(gsPidParameter.err-gsPidParameter.errLast);
gsPidParameter.errLast = gsPidParameter.err;
The proportional term responds to the current error. The integral term accumulates error over time and helps eliminate steady-state deviation. The derivative term uses the difference between the current and previous errors to respond to the direction and rate of change.
Preventing Integral Windup
Integral windup occurs when the actuator has already reached its limit but the error has not yet been eliminated. The integral calculation continues to grow or shrink even though the actuator can no longer produce a corresponding change. When the system eventually moves away from the limit, the accumulated integral value can cause excessive overshoot and make the controller slow to recover.
The anti-windup strategy used here checks the previous output before updating the integral term:
- When the output is above its maximum, only negative error is allowed to accumulate, helping the controller leave the saturated region.
- When the output is below its minimum, only positive error is accumulated.
- When the output is inside its permitted range, the integral is updated normally.
The implementation also uses an index flag to decide whether the integral contribution should be included in the current output calculation. A large error can temporarily prevent integration, while smaller errors are accumulated to improve steady-state accuracy.
Complete Relevant Implementation
The initialization function sets the gains, clears the controller state, and defines the PWM range. In this configuration, the triac is controlled independently, so the maximum value is set to 100 rather than sharing a range with a relay output.
void PidInit(void)
{
gsPidParameter.Kp = PID_KP;
gsPidParameter.Ki = PID_KI;
gsPidParameter.Kd = PID_KD;
gsPidParameter.err = 0.0;
gsPidParameter.errLast = 0.0;
gsPidParameter.integral = 0.0;
gsPidParameter.pwm = 0;
gsPidParameter.pwmMax = 100; //更改100,可控硅为单独控制,不和继电器一起
gsPidParameter.pwmMin = 0;
gsPidParameter.setVal = 0;
gsPidParameter.targetVal = 0;
}
PidHandle receives the setpoint and the measured temperature, calculates the error, applies the anti-windup rules, and finally limits the returned PWM value to the configured range.
u16 PidHandle(u8 setVal, u8 targetTemperature)
{
u8 index;
gsPidParameter.targetVal = (float)targetTemperature; // 出水温度
gsPidParameter.setVal = (float)setVal;
gsPidParameter.err = gsPidParameter.setVal - gsPidParameter.targetVal;
if(gsPidParameter.pwm > gsPidParameter.pwmMax) // 当超过最大值时,只积累负误差,可以加快退出饱和位置
{
if(((gsPidParameter.err > 0) && (gsPidParameter.err > gsPidParameter.setVal)) || ((gsPidParameter.err < 0) && (-gsPidParameter.err > gsPidParameter.setVal)))
//if(abs(gsPidParameter.err) > gsPidParameter.setVal) // 超过设定温度的一般,表示差值过大,需要积分累加,提高精度
{
index = 0;
}
else
{
index = 1;
if(gsPidParameter.err < 0)
{
gsPidParameter.integral += gsPidParameter.err;
}
}
}
else if(gsPidParameter.pwm < gsPidParameter.pwmMin) // 当低于最小值时,只积累正误差,可以加快退出饱和位置
{
if(((gsPidParameter.err > 0) && (gsPidParameter.err > gsPidParameter.setVal)) || ((gsPidParameter.err < 0) && (-gsPidParameter.err > gsPidParameter.setVal)))
//if(abs(gsPidParameter.err) > gsPidParameter.setVal) // 超过设定温度的一般,表示差值过大,需要积分累加,提高精度
{
index = 0;
}
else
{
index = 1;
if(gsPidParameter.err > 0)
{
gsPidParameter.integral += gsPidParameter.err;
}
}
}
else
{
if(((gsPidParameter.err > 0) && (gsPidParameter.err > gsPidParameter.setVal)) || ((gsPidParameter.err < 0) && (-gsPidParameter.err > gsPidParameter.setVal)))
//if(abs(gsPidParameter.err) > gsPidParameter.setVal) // 超过设定温度的一般,表示差值过大,需要积分累加,提高精度
{
index = 0;
}
else
{
index = 1;
gsPidParameter.integral += gsPidParameter.err;
}
}
gsPidParameter.pwm = gsPidParameter.Kp*gsPidParameter.err+index*gsPidParameter.Ki*gsPidParameter.integral+gsPidParameter.Kd*(gsPidParameter.err-gsPidParameter.errLast);
gsPidParameter.errLast = gsPidParameter.err;
if(gsPidParameter.pwm > gsPidParameter.pwmMax)
{
return gsPidParameter.pwmMax;
}
else if(gsPidParameter.pwm < gsPidParameter.pwmMin)
{
return gsPidParameter.pwmMin;
}
else
{
return (u16)gsPidParameter.pwm;
}
}
PID remains one of the most widely used control strategies in industrial applications. Its practical appeal is that a precise mathematical model of the controlled object is not always necessary. With suitable tuning of the proportional, integral, and derivative gains, a heating system can often achieve a satisfactory response using a relatively compact implementation.
For this temperature-control project, recording the implementation and tuning logic is useful in its own right: a similar system can be brought up more quickly without rebuilding the controller structure from the beginning.