The switch in the shaft
Push the knob in and a switch inside the encoder joins BTN to VCC. A 10 kΩ pull-down holds BTN at 0 V the rest of the time, so released reads LOW and pushed reads HIGH. It bounces like any switch, so the sketch debounces it, and uses it to set the count back to zero.
A third switch
The encoder has a push switch built into its shaft. Push the knob in towards the board and it closes; let go and a spring opens it again. On this board one side of it is VCC and the other side is BTN.
Released, the switch is open and R2, a 10 kΩ resistor from BTN to GND, holds BTN at 0 V. Your pin reads LOW, steadily, rather than floating. Pushed in, the switch joins BTN to VCC and your pin reads HIGH. That is a pull-down, the same arrangement as the TK04 push button, and the pull-down there explains it from the start.
It is the opposite way round from CLOCK and DATA, which rest HIGH and go LOW. The two rotation contacts close to GND; the push switch closes to VCC. So in one sketch, LOW on CLOCK is a contact closing, and HIGH on BTN is a push.
It bounces
BTN has no capacitor, unlike CLOCK and DATA. Its switch bounces as any metal contact does, and a loop that reads it thousands of times a second sees one push as several. The sketch uses the TK04's debounce: believe a change only once BTN has held still for 20 ms. Debouncing with millis() walks through it line by line.
The sketch
The knob half is the first count, unchanged. The button half keeps two
readings apart: reading is what BTN says now, and pushed is what the
sketch has decided. When pushed changes to HIGH, the count goes back to
zero and the monitor prints zero.
That makes the encoder a control with a home position: turn to pick a value, push to start again. The same shape, with the count clamped between two limits, is a volume, a menu line or a brightness.
The code
The first count, plus the shaft switch. The knob moves the count exactly as before; a push, debounced for 20 ms, sets it back to zero. Change the three pin numbers to the pins you wired.
/*
Rotary Encoder - count and zero TK06 / /p/tk06
Wiring. Count from the square pad on the TinkerBlock board, knob
up, header at the bottom:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
(CLOCK, DATA and BTN all reach whatever VCC is)
NC -> nothing (unconnected on the board)
BTN -> D4 on an Uno, GPIO 27 on an ESP32, GPIO 6 on an
ESP32-S3, GP15 on a Raspberry Pi Pico
CLOCK -> D2, GPIO 25, GPIO 4, GP13 (the same four boards)
DATA -> D3, GPIO 26, GPIO 5, GP14
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.
*/
// CLOCK, DATA and BTN, as GPIO numbers.
// Uno: 2, 3, 4. ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
const int CLOCK_PIN = 4;
const int DATA_PIN = 5;
const int BUTTON_PIN = 6;
const unsigned long DEBOUNCE_MS = 20; // longer than bounce lasts
long count = 0;
int lastClock = HIGH;
int lastReading = LOW; // BTN as last read; released is LOW
int pushed = LOW; // BTN as we have decided it is
unsigned long lastChange = 0; // millis() when BTN last moved
void setup() {
Serial.begin(115200);
pinMode(CLOCK_PIN, INPUT); // the block has its own pull-ups
pinMode(DATA_PIN, INPUT);
pinMode(BUTTON_PIN, INPUT); // and its own pull-down on BTN
}
void loop() {
// The knob, exactly as in the first count.
int clockLevel = digitalRead(CLOCK_PIN);
if (clockLevel != lastClock) {
if (clockLevel == LOW) {
if (digitalRead(DATA_PIN) == HIGH) {
count++; // CLOCK fell first
} else {
count--; // DATA fell first
}
Serial.println(count);
}
lastClock = clockLevel;
}
// The shaft switch: HIGH is pushed. Debounced, as on the TK04.
int reading = digitalRead(BUTTON_PIN);
unsigned long now = millis();
if (reading != lastReading) { // the contacts moved: restart the clock
lastReading = reading;
lastChange = now;
}
if (now - lastChange >= DEBOUNCE_MS && reading != pushed) {
pushed = reading;
if (pushed == HIGH) { // one real push
count = 0;
Serial.println("zero");
}
}
}BTN has a pull-down, so pushed is HIGH, and the pin is INPUT for the same reason as CLOCK and DATA: the block brings its own resistors. The debounce is the TK04 push button's, line for line: reading is what the pin says now, pushed is what the sketch has decided.
The same sketch in MicroPython: the knob counts as before, and a push, debounced for 20 ms with ticks_ms, sets the count back to zero.
"""
Rotary Encoder - count and zero, MicroPython TK06 / /p/tk06
Wiring. Count from the square pad on the TinkerBlock board, knob
up, header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V: CLOCK, DATA and BTN all reach VCC)
NC -> nothing (unconnected on the board)
BTN -> GPIO 27 on an ESP32, GPIO 6 on an ESP32-S3,
GP15 on a Raspberry Pi Pico
CLOCK -> GPIO 25, GPIO 4, GP13 (the same three boards)
DATA -> GPIO 26, GPIO 5, GP14
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
# CLOCK, DATA and BTN, as GPIO numbers.
# ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
CLOCK_PIN = 4
DATA_PIN = 5
BUTTON_PIN = 6
DEBOUNCE_MS = 20 # longer than bounce lasts
clock = Pin(CLOCK_PIN, Pin.IN) # no pull: the block has its own
data = Pin(DATA_PIN, Pin.IN)
button = Pin(BUTTON_PIN, Pin.IN) # and its own pull-down
count = 0
last_clock = 1
last_reading = 0 # BTN as last read
pushed = 0 # BTN as we have decided it is
last_change = time.ticks_ms()
while True:
level = clock.value() # the knob, as in the first count
if level != last_clock:
if level == 0:
if data.value() == 1:
count += 1 # CLOCK fell first
else:
count -= 1 # DATA fell first
print(count)
last_clock = level
reading = button.value() # the shaft switch: 1 is pushed
now = time.ticks_ms()
if reading != last_reading:
last_reading = reading
last_change = now
if (time.ticks_diff(now, last_change) >= DEBOUNCE_MS
and reading != pushed):
pushed = reading
if pushed == 1: # one real push
count = 0
print("zero")No pull argument on any of the three pins: the block has its own pull-ups and pull-down. value() is 1 while the knob is pushed in. Use ticks_diff for the debounce, never a plain subtraction: ticks_ms wraps round.
When it does not work
Almost always a sketch written for a pull-up button board, which treats LOW as pushed. This block has a pull-down and HIGH is pushed. Also check BTN is not on a 3V3 or 5V pin by mistake, where it reads HIGH for ever.
Check VCC: the switch joins BTN to VCC, so with VCC unconnected there is nothing to join it to. Then check BUTTON_PIN is the GPIO number BTN is wired to, and that the sketch uses INPUT, not INPUT_PULLUP.
Pushing can nudge the shaft a little, and a knob resting close to the edge of a click can tip over it. Push straight down the shaft. If it happens often, the knob is resting between clicks; turn it firmly into one first.
It did zero it; the count was already 0. The sketch acts once per push, on the change to HIGH, not all the time the knob is held in. Hold it in and nothing more happens until you let go and push again.
The first count reads CLOCK once a loop, and a slow loop reads it too late.
Why polling misses steps →Edit this page — content/books/rotary-encoder/the-switch-in-the-shaft.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Rotary Encoder
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.