Lighting the low side
After each scan, hold both lines of the pair the roller is joining LOW until the next one. The two LEDs beside the low edge light and point downhill. The roller is joining those two lines anyway, so holding both LOW is safe, and a scan every 20 ms is far too quick to see as a flicker.
The lights become the display
The first read finds the pair and lets every line go again, so its LEDs light for microseconds. This sketch does one thing more: after the scan, it holds both lines of the pair it found LOW, and leaves them there until the next scan 20 ms later. Both of that pair's LEDs light, and because each LED sits beside its own line's leg of the switch, the two that light are the two on the low side.
Tip the board left and the two LEDs at the left edge light. Tip it towards the header and the two nearest the header light. Tipped towards a corner, the roller may join nothing, and then nothing lights.
Why both LOW is safe
Two outputs held LOW next to each other cannot fight: they are driving the same way. The roller is joining that pair already, so they were going to be at the same voltage regardless.
If the roller rolls on before the next scan, say from the left side to the top, it joins B, which is held LOW, to C, which is released. C is pulled LOW through the roller against its own pull-up, a fraction of a milliamp, and C's LED lights too until the next scan puts things right. That is the rule doing its job: because nothing is ever driven HIGH, the roller can join anything to anything and no pin is at risk.
What it costs
Two LEDs lit from 5 V draw about 6 mA from VCC, about 3 mA through each of the two held pins. From 3.3 V it is about 2.6 mA in all. The currents are worked out from a red LED's typical forward voltage, not measured.
The scan itself lets go of every line for a few tens of microseconds in
every 20 ms. That is a gap of a fraction of a percent, and the LEDs look
steady. Make the loop slower, with a longer delay() or more work in it,
and the display simply updates less often; the gap does not grow.
The code
The first read's scan, plus holdPair(), which holds both lines of the pair it found LOW until the next scan. The LEDs on the low side light. It prints only when the side changes.
/*
4-Direction Tilt Sensor - lighting the low side TK19 / /p/tk19
Wiring. Count from the square pad on the TinkerBlock board, parts
up, header at the bottom:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
(the LEDs need it; the scan does not)
A -> D2 on an Uno, GPIO 25 on an ESP32, GPIO 4 on an
ESP32-S3, GP10 on a Raspberry Pi Pico
B -> D3, GPIO 26, GPIO 5, GP11 (same order)
C -> D4, GPIO 27, GPIO 6, GP12
D -> D5, GPIO 32, GPIO 7, GP13
Arduino IDE
Tools > Board your board, e.g. ESP32S3 Dev Module
Tools > Port the one that appears when you plug in
Tools > USB CDC On Boot Enabled (ESP32-S3 only)
No library needed.
*/
// The GPIO numbers A, B, C and D are wired to.
// Uno: 2 3 4 5. ESP32: 25 26 27 32. ESP32-S3: 4 5 6 7. Pico: 10 11 12 13.
const int PIN_A = 4;
const int PIN_B = 5;
const int PIN_C = 6;
const int PIN_D = 7;
const int NO_SIDE = 0;
const int LEFT_DOWN = 1;
const int TOP_DOWN = 2;
const int RIGHT_DOWN = 3;
const int HEADER_DOWN = 4;
const char* SIDE_NAME[] = {"none", "left", "top", "right", "header"};
void releaseAll() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_C, INPUT_PULLUP);
pinMode(PIN_D, INPUT_PULLUP);
}
void holdLow(int pin) {
digitalWrite(pin, LOW); // LOW first, then output
pinMode(pin, OUTPUT);
}
bool readsLow(int pin) {
return digitalRead(pin) == LOW;
}
int scanTilt() {
releaseAll();
holdLow(PIN_A);
delayMicroseconds(10);
bool ab = readsLow(PIN_B); // left
bool da = readsLow(PIN_D); // header
releaseAll();
holdLow(PIN_C);
delayMicroseconds(10);
bool bc = readsLow(PIN_B); // top
bool cd = readsLow(PIN_D); // right
releaseAll();
if (ab) return LEFT_DOWN;
if (da) return HEADER_DOWN;
if (bc) return TOP_DOWN;
if (cd) return RIGHT_DOWN;
return NO_SIDE;
}
// Light the two LEDs on the low side: hold both lines of the pair LOW.
// The roller is joining them already, so both LOW is safe.
void holdPair(int side) {
if (side == LEFT_DOWN) { holdLow(PIN_A); holdLow(PIN_B); }
if (side == TOP_DOWN) { holdLow(PIN_B); holdLow(PIN_C); }
if (side == RIGHT_DOWN) { holdLow(PIN_C); holdLow(PIN_D); }
if (side == HEADER_DOWN) { holdLow(PIN_D); holdLow(PIN_A); }
}
int lastSide = -1;
void setup() {
Serial.begin(115200);
releaseAll();
}
void loop() {
int side = scanTilt(); // releases every line, scans, releases
holdPair(side); // then lights the low side until next time
if (side != lastSide) {
Serial.println(SIDE_NAME[side]);
lastSide = side;
}
delay(20);
}Holding both lines of a joined pair LOW is safe because they are joined anyway. Every scan starts with releaseAll(), which lets them go again. At one scan every 20 ms the lines are released for a few tens of microseconds in each 20, which no eye can see.
The same display in MicroPython: scan, then hold the pair it found at 0 until the next scan, so the two LEDs on the low side light.
"""
4-Direction Tilt Sensor - lighting the low side TK19 / /p/tk19
Wiring. Count from the square pad on the TinkerBlock board, parts
up, header at the bottom:
GND -> GND
VCC -> 3V3 (the LEDs need it; the scan does not)
A -> GPIO 25 on an ESP32, GPIO 4 on an ESP32-S3, GP10 on a Pico
B -> GPIO 26, GPIO 5, GP11 (same order)
C -> GPIO 27, GPIO 6, GP12
D -> GPIO 32, GPIO 7, GP13
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine and time are built in.
"""
from machine import Pin
import time
# The GPIO numbers A, B, C and D are wired to.
# ESP32: 25 26 27 32. ESP32-S3: 4 5 6 7. Pico: 10 11 12 13.
PIN_A, PIN_B, PIN_C, PIN_D = 4, 5, 6, 7
a = Pin(PIN_A, Pin.IN, Pin.PULL_UP)
b = Pin(PIN_B, Pin.IN, Pin.PULL_UP)
c = Pin(PIN_C, Pin.IN, Pin.PULL_UP)
d = Pin(PIN_D, Pin.IN, Pin.PULL_UP)
# The two lines on each side.
PAIRS = {"left": (a, b), "top": (b, c), "right": (c, d), "header": (d, a)}
def release_all():
for p in (a, b, c, d):
p.init(Pin.IN, Pin.PULL_UP)
def hold_low(p):
p.init(Pin.OUT, value=0)
def scan_tilt():
release_all()
hold_low(a)
time.sleep_us(10)
ab = b.value() == 0
da = d.value() == 0
release_all()
hold_low(c)
time.sleep_us(10)
bc = b.value() == 0
cd = d.value() == 0
release_all()
if ab:
return "left"
if da:
return "header"
if bc:
return "top"
if cd:
return "right"
return "none"
last = None
while True:
side = scan_tilt()
for p in PAIRS.get(side, ()): # light the low side until next time
hold_low(p)
if side != last:
print(side)
last = side
time.sleep_ms(20)Every scan starts by releasing all four lines, so the pair held LOW from the last scan cannot confuse this one. Stop it with Ctrl-C in Thonny's shell; the pins keep whatever state they were in.
When it does not work
The sketch lights the pair it found, so if the pair is wrong, the scan is reading two swapped lines. Check the wiring in order from the square pad: GND, VCC, A, B, C, D. The LEDs are fixed to their lines on the board, so they cannot be the thing that is wrong.
VCC is not connected. The scan needs nothing from VCC and still works; the LEDs need VCC and nothing else. Wire it to 3V3, or 5V on an Uno.
Yes. If the roller rolls on to join a held line to a released one, the released line is pulled LOW through the roller against its own pull-up, which is a fraction of a milliamp, and its LED lights too until the next scan. No line is ever driven HIGH, so no two pins can fight.
That is the roller rattling, and the LEDs are showing it faithfully: each scan lights whatever it found. A knock is not a tilt adds the filter that waits for the same answer five times in a row before believing it.
What the switch says when the board is flat, or tipped towards a corner.
Level is not a reading →Edit this page — content/books/four-direction-tilt-sensor/lighting-the-low-side.mdx
Questions about this product
See what other owners have asked, and read their solutions.
4-Direction Tilt Sensor
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.