PID Controller 2DOF — Control Systems/Continues
Control_Systems/Continues/PID_Controller_2DOF · 2 input / 1 output port(s) at insert · exports to Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text
Description#
The block's own DESCRIPTION_HTML, rendered verbatim — the same text the config dialog's info panel and the library navigator show. Fix a wrong sentence in the block's .cpp (R-D9), never here.
PID Controller (2DOF)
Control Systems / Continues
A two-degree-of-freedom continuous-time PID controller with a filtered derivative. It takes the reference r and the measurement y separately, so the setpoint weights b and c can route the reference into the proportional and derivative branches at a different strength than into the integral one:
Parallel: u = P·(b·r − y) + (I/s)·(r − y)
+ D·N/(1 + N/s)·(c·r − y)
Ideal: u = P·[ (b·r − y) + (I/s)·(r − y)
+ D·N/(1 + N/s)·(c·r − y) ]
That is the second degree of freedom: the integral branch always sees the true error r − y, so steady-state tracking is unchanged, while b and c set how hard a setpoint change hits the other two branches. b < 1 softens the proportional kick on a step, and c = 0 removes derivative kick outright – neither of which affects the response to a disturbance. With b = c = 1 the block is exactly the one-degree-of-freedom PID Controller.
Ports
- Input 1 (r) – the reference, or setpoint, of any size [p,m].
- Input 2 (y) – the measurement, or plant output, of the same size as r.
- Output – the control signal u, of that same size.
The port order is Simulink's for this block, and the labels on the block face are the symbols the math above uses. The controller is scalar, but it is applied independently to every entry of the input signals, each entry carrying its own two states.
Parameters
- Proportional (P) – the proportional gain, a scalar.
- Integral (I) – the integral gain, a scalar. Zero removes the integral action.
- Derivative (D) – the derivative gain, a scalar. Zero removes the derivative action, whatever N and c are.
- Filter Coefficient (N) – the derivative filter bandwidth, a positive scalar. The filter pole sits at −N, so a large N tracks the ideal derivative closely and a small one smooths it heavily. Zero removes the derivative action outright.
- Setpoint Weight (b) – how much of the reference reaches the proportional branch, a scalar. 1 is the full reference (the 1DOF behaviour); 0 feeds that branch −y alone.
- Setpoint Weight (c) – the same for the derivative branch. 0 is the common choice, since it removes derivative kick on a setpoint step.
- Controller Form – how P enters the sum:
- Parallel – P, I and D are independent gains on the three branches.
- Ideal – P multiplies the whole controller, so it scales the integral and derivative action too.
- Initial Condition (Integrator) – the integrator state at the start of the run, a scalar used for every entry. The I gain sits before the integrator, so this is the initial value of the integral term (I·∫(r − y) dt), not of the bare integral.
- Initial Condition (Filter) – the derivative filter's state at the start of the run, a scalar used for every entry.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text. A deployable target runs a discrete model, so the export carries the controller discretized by the run's method and the generated code matches the in-app simulation.
P, I, D, N, b and c are not tunable on the generated core: the discretization mixes them into the A/B/C/D it embeds, so retuning would mean re-discretizing on the target. Change them here and export again. The two initial conditions are baked in as the state seed for the same reason.
Simulink bridge
Import and export, mapped to simulink/Continuous/PID Controller (2DOF).
"Proportional (P)" to P, "Integral (I)" to I,
"Derivative (D)" to D, "Filter Coefficient (N)" to N,
"Setpoint Weight (b)" to b, "Setpoint Weight (c)" to c,
"Controller Form" to Form (Parallel/Ideal, a 1:1 and
therefore lossless pair), "Initial Condition (Integrator)" to
InitialConditionForIntegrator, "Initial Condition (Filter)" to
InitialConditionForFilter, and "Sampling Time (s)" to
SampleTime, as on every block.
Three Simulink parameters are always implied rather than offered as a choice:
Controller = PID, TimeDomain =
Continuous-time and UseFilter = on. The full PID
structure covers Simulink's PI and PD 2DOF controllers numerically – set D
to zero for PI, I to zero for PD – so nothing is lost by always emitting it,
and an imported block whose values differ is reported rather than silently
mistranslated.
What does not cross: output saturation with anti-windup
(LimitOutput), external reset (ExternalReset), external
initial conditions and external tracking all add input ports in Simulink, and no
config value here can add or remove a port. An unfiltered derivative
(UseFilter = off) does not cross either: D·s is
improper, so it has no state-space form and none of the ten targets could carry
it.
Notes
- Stateful and continuous: two states per input entry – the integrator and the derivative filter – which the solver integrates between steps.
- Being linear, the block is directly usable by the model reduction and linear-analysis commands. Its state space has TWO inputs, so it is not a SISO system and the series/parallel merge commands will not absorb it.
- The two states are Simulink's own, which is why its two initial conditions carry over without rescaling.
Code facts#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Continues/PID_Controller_2DOF |
| family | Control_Systems/Continues |
| solver environment class | ICoreBlock_0_Control_Systems_1_Continues_2_PID_Controller_2DOF |
| source | src/ICoreSDK/ICoreBlockLibrary/Blocks/Control_Systems/Continues/PID_Controller_2DOF/ICoreBlock_0_Control_Systems_1_Continues_2_PID_Controller_2DOF.cpp |
| header | src/ICoreSDK/ICoreBlockLibrary/Blocks/Control_Systems/Continues/PID_Controller_2DOF/ICoreBlock_0_Control_Systems_1_Continues_2_PID_Controller_2DOF.h |
| default size on canvas | 120 × 90 px |
| ports at insert | 2 in, 1 out |
| code generators implemented | Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text |
Ports#
| # | Direction | Signal type | Description label |
|---|---|---|---|
| 1 | in | ICoreDouble | r |
| 2 | in | ICoreDouble | y |
| 3 | out | ICoreDouble | u |
Ports the constructor creates. A block whose port list changes with its configuration adds or removes ports at load time; the count above is the one a freshly inserted block has.
Configuration variables#
| Config variable | Default | Simulink parameter |
|---|---|---|
Proportional (P) | 1 | P |
Integral (I) | 1 | I |
Derivative (D) | 0 | D |
Filter Coefficient (N) | 100 | N |
Setpoint Weight (b) | 1 | b |
Setpoint Weight (c) | 1 | c |
Controller Form | Parallel%~%Ideal~~Parallel | Form |
Initial Condition (Integrator) | 0 | InitialConditionForIntegrator |
Initial Condition (Filter) | 0 | InitialConditionForFilter |
Every block also carries Sampling Time (s) from ICoreBlockSolverEnvironment: zero or less inherits the solver's rate, a positive value runs the block at that period.
Simulink bridge#
| support | Support::Both |
| Simulink path | simulink/Continuous/PID Controller (2DOF) |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
| always set | Controller = PID, TimeDomain = Continuous-time, UseFilter = on |
| ICore config | Simulink parameter | Value translation |
|---|---|---|
Proportional (P) | P | passes through |
Integral (I) | I | passes through |
Derivative (D) | D | passes through |
Filter Coefficient (N) | N | passes through |
Setpoint Weight (b) | b | passes through |
Setpoint Weight (c) | c | passes through |
Controller Form | Form | Parallel → Parallel, Ideal → Ideal |
Initial Condition (Integrator) | InitialConditionForIntegrator | passes through |
Initial Condition (Filter) | InitialConditionForFilter | passes through |
Caveat (shown to the user): output saturation/anti-windup, external reset, external initial conditions and tracking mode are not supported (each adds an input port in Simulink); the derivative is always filtered, since an unfiltered D*s is improper and has no state-space form
Catalog contract: src/ICoreSDK/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The lists agree. check_block_descriptions.py finds no disagreement between the description's Ports, Parameters, Code export and Simulink bridge lists and the code's.
The verdict above is
tools/docs/check_block_descriptions.py(P7.1), which compares LISTS. It cannot read a sentence: "stateless" on a block with a state, an initial-value semantic the recursion does not implement, a "not synthesizable" caveat the HDL banner contradicts. That is the agent audit (P7.3) on BLOCK_DESCRIPTION_AUDIT.md, and this tool's green is not a substitute for one.
File banner (developer view)#
The top comment of the block's .cpp — the maths, the realization and the export strategy, addressed to whoever changes it. It must not contradict the description above (P7.5).
PID Controller (2DOF) block — two-degree-of-freedom filtered-derivative PID, element-wise Two inputs (the reference r and the measurement y), one output (the control signal u):
Parallel: u = P*(b*r - y) + I/s*(r - y) + D*N/(1 + N/s)(c*r - y) Ideal: u = P[ (b*r - y) + I/s*(r - y) + D*N/(1 + N/s)*(c*r - y) ]
Taking r and y separately rather than a pre-formed error is the whole point: the setpoint weights b and c let the reference reach the proportional and derivative branches at a different strength than it reaches the integral one, so setpoint tracking can be softened without touching the disturbance response. b = c = 1 collapses the block to the 1DOF PID.
THE REALIZATION IS SIMULINK'S, exactly as in the 1DOF block: the I gain sits BEFORE the integrator and the derivative filter is xF' = N*(D*e_d - xF), so Simulink's two initial conditions map across 1:1 rather than needing a scaling nobody would guess.
xI' = I*(r - y) xF' = N*(D*(c*r - y) - xF) u = P*(b*r - y) + xI - N*xF + N*D*(c*r - y) (Parallel) u = P*[(b*r - y) + xI - N*xF + N*D*(c*r - y)] (Ideal)
so with x = [xI; xF] and the input vector [r; y]: A = [0 0; 0 -N] B = [I -I; N*D*c -N*D] C = [1 -N] D = [P*b + N*D*c, -(P + N*D)] (Parallel) C = [P -P*N] D = [P*(b + N*D*c), -P*(1 + N*D)] (Ideal)
Being LINEAR, the block carries a continuous state space, which is what makes it mergeable by the model-reduction commands and lets every export carry the run's own discretization.
Element-wise, like the 1DOF block The controller is 2-input/1-output but the ports may carry [p,m] signals (both the same size), and the same controller runs on every entry independently. The block state is X[2, K] with K = p*m - column e is entry e's own [xI; xF] - and the two inputs are fed as the two ROWS of U[2, K], so A*X + B*U and C*X + D*U evaluate all K entries at once.
Code export (Python/MATLAB/Java/Rust/C/C++ and HDL/PLC) Targets cannot integrate an ODE, so every generator realizes the DISCRETIZED recursion, structurally identical to the in-app discrete simulation, per entry: y[k] = C x[k] + D u[k] (output from the CURRENT state) x[k+1] = A x[k] + B u[k] (state update; uses the same x[k]) pidDiscreteExport() discretizes per the MODEL's method, so the export matches the in-app
Sample results#
| t | in ICoreDouble-Out-0 | in ICoreDouble-Out-0 | out ICoreDouble-Out-0 |
|---|---|---|---|
| 0 | -2 | -2 | 0 |
| 0.4 | 0.5 | 0.5 | 0 |
| 0.8 | -2 | -2 | 0 |
| 1.2 | 0.5 | 0.5 | 0 |
| 1.6 | -2 | -2 | 0 |
| 2 | 0.5 | 0.5 | 0 |
| 2.4 | -2 | -2 | 0 |
| 2.8 | 0.5 | 0.5 | 0 |
| 3.2 | -2 | -2 | 0 |
| 3.6 | 0.5 | 0.5 | 0 |
| 4 | -2 | -2 | 0 |
| 4.4 | 0.5 | 0.5 | 0 |
| 4.8 | -2 | -2 | 0 |
| 5.2 | 0.5 | 0.5 | 0 |
Every 4th of 60 samples, from the table stimulus.
The same rig also ran:
| Stimulus | What it is | Output range |
|---|---|---|
impulse | Impulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair) | 0 … 0 |
ramp | Ramp: slope 1 from t = 0 | 0 … 0 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | 0 … 0 |
step | Step: 0 -> 1 at t = 1 s | 0 … 0 |
Plotted: table — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample
Category static · sample time 0.1 · 60 steps · commit ccf005c8 · produced by docsSample --out <folder> --steps 60 · data docs/generated/samples/Control_Systems__Continues__PID_Controller_2DOF.json · the SVG is generated from those numbers by tools/docs/plot_svg.py, so it is a run and not a drawing (R-D10).