Unit 5 · Lesson 4

Sensors

A motor controller tells you what your motor is doing. A sensor tells you what your mechanism is doing. These are not the same thing — a slipping encoder, a jammed intake, or an arm that didn't reach its target won't show up in motor velocity data alone. This lesson covers every sensor category you'll use in FRC and how to read each one reliably.

By the end of this lesson, you will:

  • Read position and velocity from a TalonFX integrated encoder and a CANcoder external encoder
  • Wire and read a limit switch or beam break sensor using DigitalInput
  • Read heading from a Pigeon 2 gyroscope and explain the difference between yaw, pitch, and roll
  • Explain why sensors lie — and what mechanical and electrical conditions cause false readings
  • Choose the right sensor for five common FRC mechanism problems

The Polling Model

FRC sensors are polled — your code reads them on request, every 20ms in periodic. They do not push data or fire interrupts the way microcontrollers sometimes do. This means you get one snapshot of the sensor state per loop cycle, and your code decisions are based on that snapshot until the next cycle. For a 50Hz loop this is fine for almost every FRC use case. It also means your code must read the sensor in the correct periodic method — not once in robotInit().

Sensor Explorer

FRC sensors fall into five categories. Click each one to see the WPILib class, construction, key API methods, and notes about what can go wrong with that sensor type on a real robot.

Sensor Explorer — click any category
🔄 Integrated
Encoder
📌 CANcoder
External
💡 Digital
Input
⚘️ Gyroscope
IMU
📈 Analog
Sensor
← click a sensor category to see the WPILib API and hardware notes

Digital Input: Limit Switches and Beam Breaks

DigitalInput is the same class for both a limit switch and a beam break — both produce a binary true/false signal. The only difference is physics: a limit switch closes a circuit when physically depressed; a beam break interrupts an infrared beam to produce the same signal. Both plug into the roboRIO's DIO ports (0–9).

A limit switch stops a mechanism at a physical boundary — the bottom of an elevator, the fully-retracted position of a climber arm, the home position of an intake. Read it in periodic and use it to cut motor output before the mechanism hits the hard stop.

// Construction — DIO port number matches physical wiring private final DigitalInput bottomLimit = new DigitalInput(Constants.ARM_LOWER_LIMIT_PORT); // Reading — returns boolean each cycle @Override public void teleopPeriodic() { double armPower = operatorController.getLeftY(); // Prevent driving arm past lower limit if (bottomLimit.get() && armPower < 0) { armPower = 0.0; // block downward motion, allow upward } armMotor.set(armPower); }

A beam break detects a game piece in the intake path — when the game piece interrupts the infrared beam, get() changes state. Use it to stop the intake roller automatically, confirm pick-up, and prevent jamming from over-running the intake.

// Same DigitalInput class — beam break wires to DIO port private final DigitalInput beamBreak = new DigitalInput(Constants.BEAM_BREAK_PORT); private boolean gamePieceHeld = false; @Override public void teleopPeriodic() { // Beam break: get() = true when beam is CLEAR, false when BLOCKED // (depends on wiring — verify with physical test) if (!beamBreak.get()) { // beam blocked = game piece present intakeMotor.set(0.0); gamePieceHeld = true; } else if (operatorController.getAButton()) { intakeMotor.set(INTAKE_SPEED); gamePieceHeld = false; } else { intakeMotor.set(0.0); } SmartDashboard.putBoolean("Has Game Piece", gamePieceHeld); }

The most common source of DigitalInput confusion is the "normally-open vs. normally-closed" wiring convention. A normally-open (NO) switch: unpressed = open circuit = get() returns true (internal pull-up). Pressed = closed = get() returns false. A normally-closed (NC) switch is the opposite. Check your physical sensor behavior before writing logic.

// The safest approach: test and print before writing logic @Override public void robotPeriodic() { SmartDashboard.putBoolean("Limit Raw", bottomLimit.get()); // Press the switch physically and observe the value change. // true → false when pressed? That's normally-open (NO). // false → true when pressed? That's normally-closed (NC). // Write your logic to match what you observe, not what you assume. } // You can also invert at the software level: private final DigitalInput rawSwitch = new DigitalInput(0); private boolean isAtLimit() { return !rawSwitch.get(); // invert if wiring convention is backwards }
💡 Always wire limit switches as normally-closed

A normally-closed switch with a broken wire reads as "at limit" and stops the mechanism — a safe failure mode. A normally-open switch with a broken wire reads as "not at limit" and lets the mechanism drive past its boundary — a destructive failure mode. This is a hardware design decision, but the programmer should understand it when writing protection logic.

The CANcoder: Absolute Encoder Over CAN

Unlike the TalonFX's integrated encoder (which resets to zero every power cycle), the CANcoder reports an absolute position — it knows the shaft angle even after power loss. This makes it essential for mechanisms with a "home" position that matters across power cycles, like a swerve module's steer angle or an arm that needs to start at a known position.

// CANcoder (CTRE Phoenix 6) — CAN ID set in Phoenix Tuner X private final CANcoder steerEncoder = new CANcoder(Constants.STEER_ENCODER_ID); // Absolute position — rotations, -0.5 to +0.5 by default double absoluteRot = steerEncoder.getAbsolutePosition().getValueAsDouble(); double angleDeg = absoluteRot * 360.0; // Relative position (resets each power cycle) double relativeRot = steerEncoder.getPosition().getValueAsDouble(); // Configure a magnet offset so 0 = your mechanism's "home" position CANcoderConfiguration cfg = new CANcoderConfiguration(); cfg.MagnetSensor.MagnetOffset = Constants.STEER_ENCODER_OFFSET_ROT; steerEncoder.getConfigurator().apply(cfg);
🔍 Event Observation

The number-one swerve drive problem at competitions is a module that drives sideways or spins in circles instead of driving straight. Almost always, the root cause is a CANcoder magnet offset that's wrong — the encoder thinks the wheel is pointing forward when it's actually pointing 90 degrees off. This is checked during the mechanical assembly and the value is set in Phoenix Tuner X, then stored as a constant. If a robot that worked yesterday is now driving at an angle, the first thing to verify is whether a CANcoder magnet came loose or the offset was accidentally changed. Keep the magnet offset values in source control and in a physical log.

Gyroscopes: Heading and Tilt

A gyroscope (IMU — Inertial Measurement Unit) measures rotation. In FRC, the most common gyros are the Pigeon 2 (CTRE, CAN) and the navX2 (Kauai Labs, SPI/I2C). Both report yaw (heading), pitch, and roll. Yaw is used most: it's the robot's rotation around the vertical axis, which is how you implement field-oriented driving and straight-line autonomous.

// Pigeon 2 (CTRE Phoenix 6) private final Pigeon2 gyro = new Pigeon2(Constants.GYRO_CAN_ID); // Yaw: rotation around vertical axis (robot's heading) // Positive = counterclockwise from above (WPILib convention) double yawDeg = gyro.getYaw().getValueAsDouble(); // -180 to 180 or unbounded // Rotation2d — useful for odometry and field-oriented drive Rotation2d heading = gyro.getRotation2d(); // Reset yaw to 0 at start of autonomous (always face downfield) @Override public void autonomousInit() { gyro.reset(); // sets current heading as 0° } // Pitch and Roll — useful for tilted field detection, climbing assist double pitchDeg = gyro.getPitch().getValueAsDouble(); double rollDeg = gyro.getRoll().getValueAsDouble();
💡 Gyros drift — and mounting matters

Gyroscopes measure angular velocity and integrate over time. Any small error in each measurement accumulates. Most FRC gyros drift less than 1° per minute, which is negligible for a 2.5-minute match. More impactful: vibration from motors affects reading quality. Mount your Pigeon 2 on a firm structure away from drive motors, not on a flexible plate. A Pigeon mounted on a vibrating gearbox will drift noticeably during high-acceleration maneuvers. Check your yaw reading with the robot stationary: it should not creep more than a few tenths of a degree per second.

When Sensors Lie

Every sensor has failure modes. Knowing them prevents a 20-minute debugging session when the mechanism suddenly "doesn't work."

// Encoder drift — shaft slipping on encoder hub // Symptom: position reading slowly diverges from actual position // Fix: check set-screw tightness, use shaft flat, add encoder tape mark // Beam break false positive — ambient light triggering the sensor // Symptom: hasGamePiece() returns true in bright field lighting with no game piece // Fix: shield the receiver from direct field lights; use a shroud; check beam alignment // Limit switch bounce — mechanical chatter on contact // Symptom: switch triggers rapidly when pressed, causing spurious stops // Fix: software debounce or use a Debouncer from WPILib Debouncer limitDebouncer = new Debouncer(0.04, DebounceType.kBoth); boolean isAtLimit = limitDebouncer.calculate(bottomLimit.get()); // CANcoder disconnected — magnet fell off // Symptom: absolute position reads a fixed erroneous value or 0 // Fix: verify magnet seating; Phoenix Tuner shows connection status // Always publish CANcoder health to dashboard in robotPeriodic() // Gyro drift from vibration — see mounting note above // Symptom: robot slowly turns during straight autonomous // Fix: mount gyro on rigid structure; re-zero at start of each auto

Which Sensor for Which Problem?

Select each scenario below to see the right sensor choice, why it fits, and what the code pattern looks like.

Sensor Selection Scenarios click to see the answer
01 Detect when the intake has captured a game piece and stop the rollers automatically
02 Know the arm's angle at startup without manually moving it to a home position
03 Drive the robot straight during a 3-meter autonomous run without drifting sideways
04 Prevent the elevator from driving past its top physical limit and damaging the robot
05 Measure how many rotations the drive wheel has turned to calculate distance traveled
📈 Best sensor

✅ Why it fits

🔍 LRI Observation

During robot inspections I ask teams to demonstrate their sensors on the Shuffleboard dashboard. Teams that can show me their encoder position, limit switch state, and gyro heading in real time — while the robot is disabled — are teams that can diagnose problems in the queue. Teams that only check sensors during matches are teams that don't notice a drifting encoder or a stuck limit switch until the mechanism fails mid-match. Add every sensor you use to robotPeriodic() dashboard output before your first competition. It costs you four lines of code and saves hours of debugging.

⚙️ 🔌 System Check
  • Read all sensors in periodic methods, never once in robotInit(). Sensor values change continuously. A position read in robotInit() is stale within 20ms. The only initialization needed for sensors is their construction and configuration.
  • Publish every sensor to SmartDashboard in robotPeriodic(). You cannot diagnose a sensor problem you cannot see. Live sensor readings on the dashboard are the most efficient debugging tool in FRC.
  • Physically verify sensor polarity before writing logic. Is get() true or false when the limit switch is pressed? When the beam is blocked? When the arm is at home? Print and test before assuming.
  • Use a Debouncer on mechanical switches. Real switches bounce — they make and break contact rapidly over ~5ms when first pressed. A 40ms debounce window eliminates spurious triggers without perceptible delay.
  • Reset the gyro yaw in autonomousInit(), not robotInit(). Resetting at program start means the heading is relative to when the robot was powered on — which may not match the field orientation. Reset just before autonomous starts so the initial heading is consistent every run.
  • Verify CANcoder magnet offsets in Phoenix Tuner X before each event. Magnet offset is stored on the CANcoder itself. If a CANcoder was replaced or the magnet shifted, the offset must be recalibrated. Keep the calibration values in Constants.java as a cross-check.

Knowledge Check

A team's elevator occasionally drives past its upper limit and damages the top frame. The programmer says "the limit switch code is correct — it reads the switch and stops the motor." What is the most likely actual problem?
  • 1The elevator motor is too fast for the limit switch to respond
  • 2The limit switch is wired normally-open — a loose wire or failed switch reads as "not at limit," allowing the motor to continue past the boundary; a normally-closed switch would fail safe by reading "at limit" when the wire breaks
  • 3DigitalInput polling has too much latency for mechanical protection
  • 4The code is reading the switch in robotPeriodic() instead of teleopPeriodic()
A robot's autonomous straight-line drive slowly curves to the left every run. The code uses only encoder distance, no gyro. What sensor would most directly solve this problem?
  • 1A CANcoder on each drive wheel
  • 2A gyroscope — use the yaw reading to detect and correct any heading deviation from the target bearing; if the robot starts turning left, add a small rightward correction; encoders alone cannot detect or correct yaw drift caused by wheel slip or differential friction
  • 3A limit switch at the end of the path
  • 4A better encoder conversion factor
Why does a swerve module use a CANcoder for the steer angle rather than the TalonFX's integrated encoder?
  • 1The TalonFX integrated encoder doesn't support angular measurement
  • 2The TalonFX integrated encoder resets to zero every power cycle; the CANcoder reports absolute position from a magnet, so the module knows its exact wheel angle immediately on startup without homing — essential for swerve drive which cannot function with unknown module angles
  • 3The integrated encoder is only accurate at high speeds
  • 4Phoenix 6 requires a separate encoder for steer control
💪 Practice Prompt

Build a Sensor Dashboard

  1. Add a DigitalInput bottomLimit wired to DIO port 0 and a DigitalInput beamBreak wired to DIO port 1. Construct both in robotInit(). In robotPeriodic(), publish both raw values to SmartDashboard. Manually press each switch and confirm the value changes as expected.
  2. Add a Pigeon2 gyro on CAN ID from Constants.java. Publish yaw, pitch, and roll to SmartDashboard in robotPeriodic(). Physically tilt the robot on each axis and confirm the correct value changes. Reset yaw in autonomousInit().
  3. Write a private boolean isAtLowerLimit() method that wraps the raw bottomLimit.get() with the correct polarity (test physically first). Wrap it with a Debouncer (40ms window). Log the debounced value alongside the raw value to SmartDashboard and observe the difference when the switch bounces.
  4. In teleopPeriodic(), implement arm motor protection: if isAtLowerLimit() returns true, zero any downward motor command. Allow upward commands to continue. If beamBreak indicates a game piece, stop the intake motor regardless of button state.
  5. Add a CANcoder steerEncoder if available. Publish its absolute position to SmartDashboard and manually rotate the shaft through 360°. Confirm the reading wraps correctly. Set the magnet offset so that 0.0 corresponds to your mechanism's "home" position.
  6. Bonus: Build a "Sensor Health" panel on Shuffleboard with one indicator per sensor: green if the reading is within expected range, red if it looks wrong (encoder position NaN, gyro drifting >2°/s at rest, limit switch stuck). How would this panel help you in a 5-minute queue before a match?