001/*----------------------------------------------------------------------------*/ 002/* Copyright (c) FIRST 2015-2017. All Rights Reserved. */ 003/* Open Source Software - may be modified and shared by FRC teams. The code */ 004/* must be accompanied by the FIRST BSD license file in the root directory of */ 005/* the project. */ 006/*----------------------------------------------------------------------------*/ 007 008package edu.wpi.first.wpilibj.filters; 009 010import edu.wpi.first.wpilibj.CircularBuffer; 011import edu.wpi.first.wpilibj.PIDSource; 012 013/** 014 * This class implements a linear, digital filter. All types of FIR and IIR filters are supported. 015 * Static factory methods are provided to create commonly used types of filters. 016 * 017 * <p>Filters are of the form: y[n] = (b0*x[n] + b1*x[n-1] + ... + bP*x[n-P]) - (a0*y[n-1] + 018 * a2*y[n-2] + ... + aQ*y[n-Q]) 019 * 020 * <p>Where: y[n] is the output at time "n" x[n] is the input at time "n" y[n-1] is the output from 021 * the LAST time step ("n-1") x[n-1] is the input from the LAST time step ("n-1") b0...bP are the 022 * "feedforward" (FIR) gains a0...aQ are the "feedback" (IIR) gains IMPORTANT! Note the "-" sign in 023 * front of the feedback term! This is a common convention in signal processing. 024 * 025 * <p>What can linear filters do? Basically, they can filter, or diminish, the effects of 026 * undesirable input frequencies. High frequencies, or rapid changes, can be indicative of sensor 027 * noise or be otherwise undesirable. A "low pass" filter smooths out the signal, reducing the 028 * impact of these high frequency components. Likewise, a "high pass" filter gets rid of 029 * slow-moving signal components, letting you detect large changes more easily. 030 * 031 * <p>Example FRC applications of filters: - Getting rid of noise from an analog sensor input (note: 032 * the roboRIO's FPGA can do this faster in hardware) - Smoothing out joystick input to prevent the 033 * wheels from slipping or the robot from tipping - Smoothing motor commands so that unnecessary 034 * strain isn't put on electrical or mechanical components - If you use clever gains, you can make a 035 * PID controller out of this class! 036 * 037 * <p>For more on filters, I highly recommend the following articles: http://en.wikipedia 038 * .org/wiki/Linear_filter http://en.wikipedia.org/wiki/Iir_filter http://en.wikipedia 039 * .org/wiki/Fir_filter 040 * 041 * <p>Note 1: PIDGet() should be called by the user on a known, regular period. You can set up a 042 * Notifier to do this (look at the WPILib PIDController class), or do it "inline" with code in a 043 * periodic function. 044 * 045 * <p>Note 2: For ALL filters, gains are necessarily a function of frequency. If you make a filter 046 * that works well for you at, say, 100Hz, you will most definitely need to adjust the gains if you 047 * then want to run it at 200Hz! Combining this with Note 1 - the impetus is on YOU as a developer 048 * to make sure PIDGet() gets called at the desired, constant frequency! 049 */ 050public class LinearDigitalFilter extends Filter { 051 private CircularBuffer m_inputs; 052 private CircularBuffer m_outputs; 053 private double[] m_inputGains; 054 private double[] m_outputGains; 055 056 /** 057 * Create a linear FIR or IIR filter. 058 * 059 * @param source The PIDSource object that is used to get values 060 * @param ffGains The "feed forward" or FIR gains 061 * @param fbGains The "feed back" or IIR gains 062 */ 063 public LinearDigitalFilter(PIDSource source, double[] ffGains, 064 double[] fbGains) { 065 super(source); 066 m_inputs = new CircularBuffer(ffGains.length); 067 m_outputs = new CircularBuffer(fbGains.length); 068 m_inputGains = ffGains; 069 m_outputGains = fbGains; 070 } 071 072 /** 073 * Creates a one-pole IIR low-pass filter of the form: y[n] = (1-gain)*x[n] + gain*y[n-1] where 074 * gain = e^(-dt / T), T is the time constant in seconds. 075 * 076 * <p>This filter is stable for time constants greater than zero. 077 * 078 * @param source The PIDSource object that is used to get values 079 * @param timeConstant The discrete-time time constant in seconds 080 * @param period The period in seconds between samples taken by the user 081 */ 082 public static LinearDigitalFilter singlePoleIIR(PIDSource source, 083 double timeConstant, 084 double period) { 085 double gain = Math.exp(-period / timeConstant); 086 double[] ffGains = {1.0 - gain}; 087 double[] fbGains = {-gain}; 088 089 return new LinearDigitalFilter(source, ffGains, fbGains); 090 } 091 092 /** 093 * Creates a first-order high-pass filter of the form: y[n] = gain*x[n] + (-gain)*x[n-1] + 094 * gain*y[n-1] where gain = e^(-dt / T), T is the time constant in seconds. 095 * 096 * <p>This filter is stable for time constants greater than zero. 097 * 098 * @param source The PIDSource object that is used to get values 099 * @param timeConstant The discrete-time time constant in seconds 100 * @param period The period in seconds between samples taken by the user 101 */ 102 public static LinearDigitalFilter highPass(PIDSource source, 103 double timeConstant, 104 double period) { 105 double gain = Math.exp(-period / timeConstant); 106 double[] ffGains = {gain, -gain}; 107 double[] fbGains = {-gain}; 108 109 return new LinearDigitalFilter(source, ffGains, fbGains); 110 } 111 112 /** 113 * Creates a K-tap FIR moving average filter of the form: y[n] = 1/k * (x[k] + x[k-1] + ... + 114 * x[0]). 115 * 116 * <p>This filter is always stable. 117 * 118 * @param source The PIDSource object that is used to get values 119 * @param taps The number of samples to average over. Higher = smoother but slower 120 * @throws IllegalArgumentException if number of taps is less than 1 121 */ 122 public static LinearDigitalFilter movingAverage(PIDSource source, int taps) { 123 if (taps <= 0) { 124 throw new IllegalArgumentException("Number of taps was not at least 1"); 125 } 126 127 double[] ffGains = new double[taps]; 128 for (int i = 0; i < ffGains.length; i++) { 129 ffGains[i] = 1.0 / taps; 130 } 131 132 double[] fbGains = new double[0]; 133 134 return new LinearDigitalFilter(source, ffGains, fbGains); 135 } 136 137 @Override 138 public double get() { 139 double retVal = 0.0; 140 141 // Calculate the new value 142 for (int i = 0; i < m_inputGains.length; i++) { 143 retVal += m_inputs.get(i) * m_inputGains[i]; 144 } 145 for (int i = 0; i < m_outputGains.length; i++) { 146 retVal -= m_outputs.get(i) * m_outputGains[i]; 147 } 148 149 return retVal; 150 } 151 152 @Override 153 public void reset() { 154 m_inputs.reset(); 155 m_outputs.reset(); 156 } 157 158 /** 159 * Calculates the next value of the filter. 160 * 161 * @return The filtered value at this step 162 */ 163 @Override 164 public double pidGet() { 165 double retVal = 0.0; 166 167 // Rotate the inputs 168 m_inputs.pushFront(pidGetSource()); 169 170 // Calculate the new value 171 for (int i = 0; i < m_inputGains.length; i++) { 172 retVal += m_inputs.get(i) * m_inputGains[i]; 173 } 174 for (int i = 0; i < m_outputGains.length; i++) { 175 retVal -= m_outputs.get(i) * m_outputGains[i]; 176 } 177 178 // Rotate the outputs 179 m_outputs.pushFront(retVal); 180 181 return retVal; 182 } 183}