If I’m being honest, my university classes on control engineering were not my favorite. However, working as an engineer – and especially in embedded systems -- you come across applications that often call for the use of control theory.
One very commonly used algorithm is the Proportional Integral Derivative Controller or PID Controller. PID Controllers are used to control temperatures, pressures, motor positions, and flow rates of a wide variety of applications. One place I regularly see it is in high-end image processing systems that use thermal electric coolers or other cooling systems to cool down image sensors. This reduces the noise in the image. For scientific imaging, lower noise enables a better image with the potential for more discoveries.
The PID Controller algorithm is not too tricky to implement since it only requires Addition, Multiplication, Division and Subtraction. However, determining the three coefficients used to ensure that the PID loop is stable can require a little additional time once the algorithm has been implemented.
The PID uses three terms at its most basic level.
Proportional - This measures the difference between the desired value and the measured value. The proportional value is a measure of the current position. There will always be an error due to how it works.
Integral - This integrates the error over time. The integral term is the historic cumulative value of the error. As the error is eliminated, the integral term stops growing.
Derivative - Calculates the rate of change and is predicting the future trend of the error.
Each term also has an associated gain, KI, KP or KD to help us tune the behavior of the PID Controller algorithm. The D term is not always used in the implementation and it is also not unusual to implement PI controllers.
PID often uses floating point math to implement. As such, we can implement them in RTL using libraries such as the VHDL Fixed/Float libraries. Alternatively, we can implement the PID using high-level synthesis which is what we will use for this example today. This gives us the ability to use either floating point or arbitrary precision fixed point mathematics. It also provides me the ability to quickly and easily change the interfacing on the block by the addition or not of pragmas.
This is important because normally when I implement these types of controllers, they are targeted for pure FPGA implementations (which may or may not contain a soft-core processor) or a smaller Zynq-7000 SoC part (7007, 7010, 7020 etc.). As such, I may want a more traditional vector interface than a AXI interface if there is no processor in the design.
We can use a type definition in a header file to enable flexibility in the HLS algorithm between floating point and arbitrary fixed point without the need to make multiple changes. For this example, I am going to implement it as a floating-point solution, mainly because I have never demonstrated how to write the floating-point values into the AXI registers when used with a processor.
The actual source code for the PID is pretty straight forward and can be seen below.
#include "pid.h"
static data_type error_prev =0;
static data_type i_prev=0;
data_type PID (data_type set_point, data_type KP, data_type KI, data_type KD, data_type sample, data_type ts, data_type pmax)
{
#pragma HLS INTERFACE mode=s_axilite port=return
#pragma HLS INTERFACE mode=s_axilite port=sample
#pragma HLS INTERFACE mode=s_axilite port=KD
#pragma HLS INTERFACE mode=s_axilite port=KI
#pragma HLS INTERFACE mode=s_axilite port=KP
#pragma HLS INTERFACE mode=s_axilite port=set_point
#pragma HLS INTERFACE mode=s_axilite port=ts
#pragma HLS INTERFACE mode=s_axilite port=pmax
data_type error, i, d, p;
data_type temp;
data_type op;
error = set_point - sample;
p = error * KP;
i = i_prev + (error * ts * KI);
d = KD * ((error - error_prev) / ts);
op = p+i+d;
error_prev = error;
if (op > pmax) {
i_prev = i_prev;
op = pmax;
}else{
i_prev = i;
}
return op;
}
I have declared the previous error and previous integral as global static variables to ensure they retain their values between iterations.
Algorithm wise, the user can load in KP, KI, KID, Ts and Pmax on the fly as the application is running. We could easily make additions to store the integral value or restart the controller using additional registers. This would enable the PID implementation to be time sliced and used for more than one implementation.
To test and configure the PID, the test bench applies a range of input temperatures that are both well above the intended target set point and also to ensure the set point is achieved. The PID in this example has been designed to deliver a power (in watts) to maintain the temperature of an optical bed. In this case, we need to heat it up not down. I’m sure you can guess that it is space application.
#include "pid.h"
#include <stdio.h>
#define iterations 40
int main(void)
{
data_type set_point = -80.0;
data_type sample[iterations] = {-90.000,-88.988,-87.977,-86.966,-85.955,-84.946,-83.936,-82.928,-81.920,-80.912,-80.283,-79.926,-79.784,-79.774,-79.829,-79.898,-79.955,-79.993,-80.011,-80.017,-80.016,-80.010,-80.005,-80.002,-80.000,-79.999,-79.999,-79.999,-79.999,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-79.999,-80.000,-80.001,-80.000};
data_type kp = 19.6827; // w/k
data_type ki = 0.7420; // w/k/s
data_type kd = 0.0;
data_type op;
printf("testing cpp\r\n");
for (int i =0; i<iterations; i++){
op = PID (set_point, kp, ki, kd, sample[i], 12.5, 40);
printf("result %f\r\n",op);
}
return 0;
}
This PID was simulated in Vitis HLS for both C Simulation and Co-Simulation with the results exactly as expected.
With the algorithm behaving as I expected, the next step to synthesize and export the IP for inclusion into a Vivado project. This time we will target the Cora Z7S board.
Latency Performance & Resource Requirements
Register Interface
The completed block diagram below reflects interfaces that are synthesized into a AXI interface.
Block Diagram
Total Design Resources
PID Resources
It’s nice that the project can be built simply and then exported to Vitis. In Vitis, we can reuse several elements of the HLS test bench in the development of the application which drives the IP core.
Since we are using a AXI interface, Vitis HLS very kindly provides us a driver that can be used in Vitis to drive the IP core. However, when you use floating point inputs into the IP core, the driver expects them as a U32. If we do a conversion from Float to U32, we will lose significant accuracy. Therefore, the way to approach this is to use pointers and casting.
In essence, we declare the variable as a float and then, in the function, call set a U32 pointer to the address of the float variable and use the indirection operator to read the value.
XPid_Set_set_point(&pid, *((u32*)&set_point ));
The entire application is
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xpid.h"
#define iterations 40
typedef float data_type;
data_type set_point = -80.0;
data_type sample[iterations] = {-90.000,-88.988,-87.977,-86.966,-85.955,-84.946,-83.936,-82.928,-81.920,-80.912,-80.283,-79.926,-79.784,-79.774,-79.829,-79.898,-79.955,-79.993,-80.011,-80.017,-80.016,-80.010,-80.005,-80.002,-80.000,-79.999,-79.999,-79.999,-79.999,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-79.999,-80.000,-80.001,-80.000};
data_type kp = 19.6827; // w/k
data_type ki = 0.7420; // w/k/s
data_type kd = 0.0;
data_type ts = 12.5;
data_type pmax = 40;
u32 op;
XPid pid;
int main()
{
float result;
init_platform();
disable_caches();
print("Adiuvo PID Example\n\r");
XPid_Initialize(&pid,XPAR_XPID_0_DEVICE_ID);
XPid_Set_set_point(&pid, *((u32*)&set_point ));
XPid_Set_KP(&pid, *((u32*)&kp));
XPid_Set_KI(&pid, *((u32*)&ki));
XPid_Set_KD(&pid, *((u32*)&kd));
XPid_Set_ts(&pid, *((u32*)&ts));
XPid_Set_pmax(&pid, *((u32*)&pmax));
u32 tst = XPid_Get_set_point(&pid);
for (int i =0; i<iterations; i++){
XPid_Set_sample(&pid, *((u32*)&sample[i]));
XPid_Start(&pid);
while(!XPid_IsDone(&pid)){
}
op= XPid_Get_return(&pid);
result = *((float*)&op);
printf("result %f \r\n",result);
}
cleanup_platform();
etrurn 0;
}
Gives the following results.
The implementation in hardware works identically to the software version as would be expected.
Of course, for different applications, you would need to determine the KP, KI, and KD variables which can be used for your application but there is a lot of information on training PID out there.
The real beauty of this is that because it was implemented in C, you can using HLS if you want to run the algorithm from a processor core in place of being in the logic.
You can get the HLS Project here