Complementary Filter — Robotics/Perception Filters
Robotics/Perception_Filters/Complementary_Filter · 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.
Complementary Filter
Robotics / Perception Filters
Fuses a gyro rate with an accelerometer-derived angle into one tilt estimate:
θ[k+1] = α·(θ[k] + gyro[k]·Ts) + (1−α)·θacc[k]
The gyro path integrates cleanly over a step but drifts; the accelerometer angle is drift-free but noisy, and wrong whenever the body accelerates. The filter high-passes the first and low-passes the second with weights that sum to one – which is what complementary means, and why there is only one knob.
Ports
- gyro – the angular rate about the measured axis, in rad/s, a scalar.
- theta_acc – the angle derived from the accelerometer, in rad, a
scalar. It must already be an ANGLE: this block does no
atan2– put Cartesian To Polar or Trigonometric Function in front of it. - theta – the fused estimate in rad, a scalar.
Parameters
- Filter Coefficient (alpha) – the gyro's weight, in [0, 1]. Near 1 the estimate follows the gyro and the accelerometer corrects it slowly; at 0 the block passes theta_acc straight through. The crossover time constant is τ = α·Ts / (1 − α) – at α = 0.98 and Ts = 0.01 s that is about 0.49 s.
- Initial Angle (rad) – the estimate at t = 0.
- 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. The three folded coefficients – α, α·Ts and 1−α – are baked into the generated body at export time rather than exposed as tunable parameters, so retuning means re-exporting.
The three HDL targets are genuinely synthesizable Q16.16, not simulation-only: the whole filter is three multiplies and two adds, with no trigonometry, division or square root anywhere.
Simulink bridge
None, measured rather than assumed: the sensor-fusion blocks ship in the Sensor Fusion and Tracking Toolbox and the Robotics System Toolbox, neither of which is installed on this machine. The filter is three blocks to rebuild there, and the description above is the whole specification.
Notes
- Stateful and inherently discrete. The state is the estimate itself.
- The output is the estimate at the START of the step, published before the update, so the first sample is exactly the configured seed – the same convention as Unicycle Odometry and Gyro Quaternion Integration.
- α is a weight, not a frequency. Its meaning depends on Ts: the same α at a different rate is a different filter, which is why the sample time is folded into the exported coefficients.
- One axis per block. For roll and pitch together, use two.
- No angle wrapping. Both inputs and the output are raw radians; a filter that wrapped would blend across the discontinuity and produce a spin. Keep the inputs on one branch, or wrap downstream with Angle Wrap.
Code facts#
| Fact | Value |
|---|---|
| registered type | Robotics/Perception_Filters/Complementary_Filter |
| family | Robotics/Perception_Filters |
| solver environment class | ICoreBlock_0_Robotics_1_Perception_Filters_2_Complementary_Filter |
| source | src/ICoreSDK/ICoreBlockLibrary/Blocks/Robotics/Perception_Filters/Complementary_Filter/ICoreBlock_0_Robotics_1_Perception_Filters_2_Complementary_Filter.cpp |
| header | src/ICoreSDK/ICoreBlockLibrary/Blocks/Robotics/Perception_Filters/Complementary_Filter/ICoreBlock_0_Robotics_1_Perception_Filters_2_Complementary_Filter.h |
| default size on canvas | 140 × 84 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 | gyro |
| 2 | in | ICoreDouble | theta_acc |
| 3 | out | ICoreDouble | theta |
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 |
|---|---|---|
Filter Coefficient (alpha) | 0.98 | — |
Initial Angle (rad) | 0 | — |
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::None |
| Simulink path | — |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
Caveat (shown to the user): no Simulink equivalent available: the sensor-fusion blocks ship in the Sensor Fusion and Tracking Toolbox and the Robotics System Toolbox, neither of which is installed. The filter is a Gain, a Sum and a Unit Delay to rebuild there - keep alpha's weight on the GYRO path, as this block does
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).
Complementary Filter — the IMU tilt estimator theta[k+1] = alpha*theta[k] + (alpha*Ts)*gyro[k] + (1-alpha)*theta_acc[k]
which is the folded form of alpha*(theta + gyro*Ts) + (1-alpha)*theta_acc. The FOLDED form is the contract: compute_h_discrete() and all ten generators use the same three coefficients, so the block agrees with its own exported core to the last ulp rather than differing by a rounding of alpha*Ts.
Affine, so the three HDL targets are genuinely synthesizable Q16.16 - see the header for why that is worth stating explicitly in this family.
Sample results#
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.118 |
ramp | Ramp: slope 1 from t = 0 | 0 … 13.86 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -0.2787 … 1.027 |
table | Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample | -0.4013 … 1.777 |
Plotted: step — Step: 0 -> 1 at t = 1 s
Category dynamic · sample time 0.1 · 60 steps · commit ccf005c8 · produced by docsSample --out <folder> --steps 60 · data docs/generated/samples/Robotics__Perception_Filters__Complementary_Filter.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).