001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj;
006
007import edu.wpi.first.hal.FRCNetComm.tResourceType;
008import edu.wpi.first.hal.HAL;
009import edu.wpi.first.hal.SimBoolean;
010import edu.wpi.first.hal.SimDevice;
011import edu.wpi.first.hal.SimDouble;
012import edu.wpi.first.math.geometry.Rotation2d;
013import edu.wpi.first.util.sendable.Sendable;
014import edu.wpi.first.util.sendable.SendableBuilder;
015import edu.wpi.first.util.sendable.SendableRegistry;
016import java.nio.ByteBuffer;
017import java.nio.ByteOrder;
018
019/**
020 * Use a rate gyro to return the robots heading relative to a starting position. The Gyro class
021 * tracks the robots heading based on the starting position. As the robot rotates the new heading is
022 * computed by integrating the rate of rotation returned by the sensor. When the class is
023 * instantiated, it does a short calibration routine where it samples the gyro while at rest to
024 * determine the default offset. This is subtracted from each sample to determine the heading.
025 *
026 * <p>This class is for the digital ADXRS450 gyro sensor that connects via SPI. Only one instance of
027 * an ADXRS Gyro is supported.
028 */
029@SuppressWarnings({"TypeName", "PMD.UnusedPrivateField"})
030public class ADXRS450_Gyro implements Sendable, AutoCloseable {
031  private static final double kSamplePeriod = 0.0005;
032  private static final double kCalibrationSampleTime = 5.0;
033  private static final double kDegreePerSecondPerLSB = 0.0125;
034
035  private static final int kRateRegister = 0x00;
036  private static final int kTemRegister = 0x02;
037  private static final int kLoCSTRegister = 0x04;
038  private static final int kHiCSTRegister = 0x06;
039  private static final int kQuadRegister = 0x08;
040  private static final int kFaultRegister = 0x0A;
041  private static final int kPIDRegister = 0x0C;
042  private static final int kSNHighRegister = 0x0E;
043  private static final int kSNLowRegister = 0x10;
044
045  private SPI m_spi;
046
047  private SimDevice m_simDevice;
048  private SimBoolean m_simConnected;
049  private SimDouble m_simAngle;
050  private SimDouble m_simRate;
051
052  /** Constructor. Uses the onboard CS0. */
053  public ADXRS450_Gyro() {
054    this(SPI.Port.kOnboardCS0);
055  }
056
057  /**
058   * Constructor.
059   *
060   * @param port The SPI port that the gyro is connected to
061   */
062  @SuppressWarnings("this-escape")
063  public ADXRS450_Gyro(SPI.Port port) {
064    m_spi = new SPI(port);
065
066    // simulation
067    m_simDevice = SimDevice.create("Gyro:ADXRS450", port.value);
068    if (m_simDevice != null) {
069      m_simConnected = m_simDevice.createBoolean("connected", SimDevice.Direction.kInput, true);
070      m_simAngle = m_simDevice.createDouble("angle_x", SimDevice.Direction.kInput, 0.0);
071      m_simRate = m_simDevice.createDouble("rate_x", SimDevice.Direction.kInput, 0.0);
072    }
073
074    m_spi.setClockRate(3000000);
075    m_spi.setMode(SPI.Mode.kMode0);
076    m_spi.setChipSelectActiveLow();
077
078    if (m_simDevice == null) {
079      // Validate the part ID
080      if ((readRegister(kPIDRegister) & 0xff00) != 0x5200) {
081        m_spi.close();
082        m_spi = null;
083        DriverStation.reportError("could not find ADXRS450 gyro on SPI port " + port.value, false);
084        return;
085      }
086
087      m_spi.initAccumulator(
088          kSamplePeriod, 0x20000000, 4, 0x0c00000e, 0x04000000, 10, 16, true, true);
089
090      calibrate();
091    }
092
093    HAL.report(tResourceType.kResourceType_ADXRS450, port.value + 1);
094    SendableRegistry.addLW(this, "ADXRS450_Gyro", port.value);
095  }
096
097  /**
098   * Determine if the gyro is connected.
099   *
100   * @return true if it is connected, false otherwise.
101   */
102  public boolean isConnected() {
103    if (m_simConnected != null) {
104      return m_simConnected.get();
105    }
106    return m_spi != null;
107  }
108
109  /**
110   * Calibrate the gyro by running for a number of samples and computing the center value. Then use
111   * the center value as the Accumulator center value for subsequent measurements.
112   *
113   * <p>It's important to make sure that the robot is not moving while the centering calculations
114   * are in progress, this is typically done when the robot is first turned on while it's sitting at
115   * rest before the competition starts.
116   */
117  public final void calibrate() {
118    if (m_spi == null) {
119      return;
120    }
121
122    Timer.delay(0.1);
123
124    m_spi.setAccumulatorIntegratedCenter(0);
125    m_spi.resetAccumulator();
126
127    Timer.delay(kCalibrationSampleTime);
128
129    m_spi.setAccumulatorIntegratedCenter(m_spi.getAccumulatorIntegratedAverage());
130    m_spi.resetAccumulator();
131  }
132
133  /**
134   * Get the SPI port number.
135   *
136   * @return The SPI port number.
137   */
138  public int getPort() {
139    return m_spi.getPort();
140  }
141
142  private boolean calcParity(int value) {
143    boolean parity = false;
144    while (value != 0) {
145      parity = !parity;
146      value = value & (value - 1);
147    }
148    return parity;
149  }
150
151  private int readRegister(int reg) {
152    int cmdhi = 0x8000 | (reg << 1);
153    boolean parity = calcParity(cmdhi);
154
155    ByteBuffer buf = ByteBuffer.allocate(4);
156    buf.order(ByteOrder.BIG_ENDIAN);
157    buf.put(0, (byte) (cmdhi >> 8));
158    buf.put(1, (byte) (cmdhi & 0xff));
159    buf.put(2, (byte) 0);
160    buf.put(3, (byte) (parity ? 0 : 1));
161
162    m_spi.write(buf, 4);
163    m_spi.read(false, buf, 4);
164
165    if ((buf.get(0) & 0xe0) == 0) {
166      return 0; // error, return 0
167    }
168    return (buf.getInt(0) >> 5) & 0xffff;
169  }
170
171  /**
172   * Reset the gyro.
173   *
174   * <p>Resets the gyro to a heading of zero. This can be used if there is significant drift in the
175   * gyro, and it needs to be recalibrated after it has been running.
176   */
177  public void reset() {
178    if (m_simAngle != null) {
179      m_simAngle.reset();
180    }
181    if (m_spi != null) {
182      m_spi.resetAccumulator();
183    }
184  }
185
186  /** Delete (free) the spi port used for the gyro and stop accumulating. */
187  @Override
188  public void close() {
189    SendableRegistry.remove(this);
190    if (m_spi != null) {
191      m_spi.close();
192      m_spi = null;
193    }
194    if (m_simDevice != null) {
195      m_simDevice.close();
196      m_simDevice = null;
197    }
198  }
199
200  /**
201   * Return the heading of the robot in degrees.
202   *
203   * <p>The angle is continuous, that is it will continue from 360 to 361 degrees. This allows
204   * algorithms that wouldn't want to see a discontinuity in the gyro output as it sweeps past from
205   * 360 to 0 on the second time around.
206   *
207   * <p>The angle is expected to increase as the gyro turns clockwise when looked at from the top.
208   * It needs to follow the NED axis convention.
209   *
210   * <p>This heading is based on integration of the returned rate from the gyro.
211   *
212   * @return the current heading of the robot in degrees.
213   */
214  public double getAngle() {
215    if (m_simAngle != null) {
216      return m_simAngle.get();
217    }
218    if (m_spi == null) {
219      return 0.0;
220    }
221    return m_spi.getAccumulatorIntegratedValue() * kDegreePerSecondPerLSB;
222  }
223
224  /**
225   * Return the rate of rotation of the gyro.
226   *
227   * <p>The rate is based on the most recent reading of the gyro analog value
228   *
229   * <p>The rate is expected to be positive as the gyro turns clockwise when looked at from the top.
230   * It needs to follow the NED axis convention.
231   *
232   * @return the current rate in degrees per second
233   */
234  public double getRate() {
235    if (m_simRate != null) {
236      return m_simRate.get();
237    }
238    if (m_spi == null) {
239      return 0.0;
240    }
241    return m_spi.getAccumulatorLastValue() * kDegreePerSecondPerLSB;
242  }
243
244  /**
245   * Return the heading of the robot as a {@link edu.wpi.first.math.geometry.Rotation2d}.
246   *
247   * <p>The angle is continuous, that is it will continue from 360 to 361 degrees. This allows
248   * algorithms that wouldn't want to see a discontinuity in the gyro output as it sweeps past from
249   * 360 to 0 on the second time around.
250   *
251   * <p>The angle is expected to increase as the gyro turns counterclockwise when looked at from the
252   * top. It needs to follow the NWU axis convention.
253   *
254   * <p>This heading is based on integration of the returned rate from the gyro.
255   *
256   * @return the current heading of the robot as a {@link edu.wpi.first.math.geometry.Rotation2d}.
257   */
258  public Rotation2d getRotation2d() {
259    return Rotation2d.fromDegrees(-getAngle());
260  }
261
262  @Override
263  public void initSendable(SendableBuilder builder) {
264    builder.setSmartDashboardType("Gyro");
265    builder.addDoubleProperty("Value", this::getAngle, null);
266  }
267}