The SW-520D is the most practical directional tilt sensor for Arduino projects, offering a specific trigger angle of roughly 20 to 45 degrees at a unit cost of under $0.20. Unlike omnidirectional ball switches, the SW-520D uses a dual-ball internal mechanism that only closes the circuit when tilted in one specific direction, making it ideal for anti-tamper alarms, screen rotation triggers, and rollover detection. This guide provides the exact wiring topology, a hardware-plus-software debounced C++ implementation, and a diagnostic framework for when the sensor misfires.
Project Overview and Difficulty Rating
- Difficulty: 2/5 (Beginner-Intermediate)
- Estimated Build Time: 20 minutes
- Core Concept: Mechanical switch debouncing via RC filtering and state-machine logic.
SW-520D vs. SW-200D vs. SW-420: Spec Sheet and Selection Table
Choosing the wrong tilt or vibration switch is the most common reason these projects fail in the field. The SW-520D is directional, the SW-200D is omnidirectional, and the SW-420 is strictly for high-frequency vibration. Review this spec sheet before finalizing your BOM.
| Specification | SW-520D (Directional) | SW-200D (Omni) | SW-420 (Vibration) |
|---|---|---|---|
| Internal Mechanism | Dual steel ball (conductive) | Single steel ball (cage) | Spring-mass suspension |
| Trigger Angle / Sensitivity | 20° - 45° (One direction) | Any angle > 15° | High-frequency shock only |
| Contact Resistance | < 50 mΩ | < 50 mΩ | < 100 mΩ |
| Max Switching Voltage | 12V DC | 12V DC | 12V DC |
| Typical Price (2026) | $0.12 - $0.18 / unit | $0.08 - $0.12 / unit | $0.10 - $0.15 / unit |
| Best Use Case | Rollover detection, screen flip | Generic tilt alarms, toys | Knock sensors, motor fault |
Parts List and Pin Mapping
Do not skip the 100nF ceramic capacitor. Mechanical tilt sensors suffer from severe contact bounce, which can register as dozens of triggers in a single millisecond. Combining a hardware RC (Resistor-Capacitor) filter with software debouncing yields a rock-solid signal.
Required Components
- 1x Arduino Nano v3 (ATmega328P)
- 1x SW-520D Directional Tilt Sensor
- 1x 10kΩ Resistor (Brown-Black-Orange-Gold) for external pull-up
- 1x 100nF (0.1µF) Ceramic Capacitor for hardware debounce
- Half-size breadboard and male-to-male jumper wires
Pin Mapping Table
| Component | Component Pin / Lead | Arduino Nano Pin | Notes |
|---|---|---|---|
| SW-520D | Pin 1 (Arbitrary) | GND | Connects to common ground rail |
| SW-520D | Pin 2 (Arbitrary) | D2 (Digital Pin 2) | Signal line (also supports INT0) |
| 10kΩ Resistor | Lead A | 5V | Pulls D2 HIGH when switch is open |
| 10kΩ Resistor | Lead B | D2 | Junction with sensor and capacitor |
| 100nF Capacitor | Lead A | D2 | Parallel to resistor junction |
| 100nF Capacitor | Lead B | GND | Completes RC low-pass filter to ground |
Wiring Steps and Circuit Assembly
- Seat the Nano: Press the Arduino Nano v3 into the breadboard, ensuring pins span across the center divider.
- Establish Power Rails: Connect Nano 5V to the red rail and Nano GND to the blue rail using jumper wires.
- Place the Sensor: Insert the two leads of the SW-520D into the same row on the breadboard. The sensor is non-polarized, so orientation of the leads does not matter electrically, though the physical plastic body dictates the tilt direction.
- Wire the Pull-Up: Insert the 10kΩ resistor. Connect one leg to the 5V red rail and the other leg to the row shared by the tilt sensor.
- Add the RC Filter: Insert the 100nF ceramic capacitor. Connect one leg to the same row as the sensor and resistor junction, and the other leg to the blue GND rail. This creates a low-pass filter with a time constant of $\tau = R \times C = 10,000 \times 0.0000001 = 1ms$, smoothing out micro-bounces.
- Route the Signal: Run a jumper wire from the sensor/resistor/capacitor junction row to Digital Pin 2 (D2) on the Nano.
- Ground the Sensor: Run a jumper wire from the second tilt sensor lead to the blue GND rail.
Compilable Arduino Code with Debounce Logic
This sketch uses a non-blocking millis() state machine. We avoid delay() to keep the main loop free for other tasks. It also includes an error-handling routine to detect if the sensor is stuck or wired incorrectly.
// Target Board: Arduino Nano v3 (ATmega328P)
// Project: SW-520D Tilt Sensor with Hardware/Software Debounce
#define TILT_PIN 2 // Digital Pin 2 (also INT0)
#define DEBOUNCE_MS 50 // Software debounce window
#define STUCK_THRESHOLD 5000 // 5 seconds stuck LOW triggers error
// State variables
int currentTiltState = HIGH; // Assume pulled HIGH initially
int lastTiltState = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long stuckTimer = 0;
bool tiltTriggered = false;
void setup() {
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port (Native USB boards)
// We use an external 10k pull-up, so standard INPUT is correct.
// If you omitted the resistor, change this to INPUT_PULLUP.
pinMode(TILT_PIN, INPUT);
Serial.println(F("[SYS] SW-520D Tilt Sensor Initialized."));
Serial.println(F("[SYS] Tilt the sensor >20 degrees to trigger."));
stuckTimer = millis();
}
void loop() {
// Read the raw state from the hardware-filtered pin
int reading = digitalRead(TILT_PIN);
// Software Debounce Logic
if (reading != lastTiltState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > DEBOUNCE_MS) {
if (reading != currentTiltState) {
currentTiltState = reading;
// Sensor closes to GND, so LOW means tilted
if (currentTiltState == LOW) {
Serial.println(F("[EVT] TILT DETECTED: Angle exceeded threshold."));
tiltTriggered = true;
stuckTimer = millis(); // Reset stuck timer on valid transition
} else {
Serial.println(F("[EVT] SENSOR RESET: Returned to upright."));
tiltTriggered = false;
stuckTimer = millis();
}
}
}
// Error Handling: Detect stuck LOW (missing pull-up or short)
if (currentTiltState == LOW && tiltTriggered) {
if (millis() - stuckTimer > STUCK_THRESHOLD) {
Serial.println(F("[ERR] Tilt sensor stuck LOW - check pull-up resistor or wiring"));
stuckTimer = millis(); // Prevent serial buffer flooding
}
}
lastTiltState = reading;
}
Debugging: First Three Things to Check When It Fails
When the serial monitor spits out errors or fails to register a tilt, do not rewrite the code. Mechanical switches fail at the physical layer. Follow this ranked diagnostic path.
1. Exact Error: [ERR] Tilt sensor stuck LOW - check pull-up resistor or wiring
Cause: The microcontroller is reading a continuous 0V on D2.
Fix:
- Verify the 10kΩ resistor is actually connected to the 5V rail, not a floating row.
- Measure the voltage at D2 with a multimeter while the sensor is upright. It must read ~4.9V to 5.0V. If it reads 0V, your pull-up is missing or the sensor is internally shorted.
- If you forgot the external resistor, change
pinMode(TILT_PIN, INPUT);topinMode(TILT_PIN, INPUT_PULLUP);in the code to use the ATmega328P's internal ~30kΩ pull-up.
2. Symptom: Serial Monitor Shows Rapid [EVT] Spam from a Single Tilt
Cause: Contact bounce is overwhelming the software debounce window. According to All About Circuits, mechanical switch contacts can bounce for up to 20ms depending on the mass of the internal elements.
Fix:
- Check your breadboard connections for the 100nF capacitor. A missing capacitor forces the software to do all the heavy lifting.
- Increase
#define DEBOUNCE_MS 50to100in the code. Note that going above 150ms will introduce noticeable UI lag if you are using this to trigger an LED or buzzer.
3. Symptom: Sensor Only Triggers When Tapped, Not Tilted Slowly
Cause: You are likely using an SW-420 vibration sensor by mistake, or the SW-520D is mounted at the wrong baseline angle.
Fix:
- Inspect the component body. The SW-520D typically has a distinct flattened edge or directional marking compared to the symmetrical SW-420.
- The SW-520D requires gravity to pull the internal conductive balls away from the contact pins. If mounted horizontally on a breadboard, it may not trigger. Mount it vertically on a perpendicular proto-board for proper gravity-assisted switching.
Extending and Simplifying the Build
How to Simplify (Drop the External Hardware)
If you are building a quick prototype and lack a 10kΩ resistor and 100nF capacitor, you can rely entirely on the ATmega328P's internal pull-up resistors. Change the pin mode to INPUT_PULLUP.
The Trade-off: The internal pull-up is roughly 30kΩ to 50kΩ. This higher impedance makes the input pin highly susceptible to EMI (Electromagnetic Interference) from nearby motors, relays, or AC mains wiring. Only use this simplified approach in low-noise, battery-powered environments.
How to Extend (Interrupts and IoT)
For battery-powered IoT nodes (like an ESP32 deep-sleep tilt alarm), polling the pin in the loop() wastes milliamps. Instead, use hardware interrupts to wake the MCU.
- Interrupt Wiring: Ensure the sensor is on D2 or D3 on the Nano (these map to INT0 and INT1). On an ESP32, any GPIO can be an interrupt.
- Code Modification: Replace the polling logic with
attachInterrupt(digitalPinToInterrupt(TILT_PIN), tiltISR, FALLING);. As noted in the official Arduino attachInterrupt documentation, keep the ISR (Interrupt Service Routine) as short as possible—just set avolatile boolflag and handle the serial printing in the main loop. - Network Extension: Pair the interrupt flag with an ESP8266/ESP32 to send an MQTT payload to Home Assistant the millisecond the enclosure is opened or tilted, creating a zero-latency tamper mesh.






