Skip to content

Commit d693ee0

Browse files
authored
Add rotary-encoder tutorial. (#524)
1 parent 8a55cc8 commit d693ee0

9 files changed

Lines changed: 201 additions & 1 deletion

File tree

docs/_images/hw_ff_ky_040.png

198 KB
Loading
79.9 KB
Loading

docs/_images/rotary.jpeg

63.3 KB
Loading

docs/tutorials/hardware/pwm-servo.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Connect the servo as follows:
2929
alt="Servo motor schematics"
3030
/>
3131

32-
## Code/slee
32+
## Code
3333
A servo motor is controlled by a PWM signal. The duty cycle of the PWM signal
3434
determines the angle of the servo motor. The frequency of the PWM signal is
3535
usually 50Hz, but the actual frequency depends on the motor.

docs/tutorials/hardware/rotary.mdx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import RotaryDiagram from "../../_images/hw_ff_ky_040.png";
2+
import RotarySchematics from "../../_images/hw_ff_ky_040_schem.png";
3+
4+
# Rotary encoder
5+
6+
In this tutorial we are going to use the ESP32's pulse counter to read a rotary encoder.
7+
8+
A rotary encoder is usually a wheel or a knob that turns freely without any limits or stops.
9+
(Picture)
10+
11+
Typically a rotary encoder has two output signals:
12+
- CLK (sometimes called A)
13+
- DT (sometimes called B)
14+
15+
These two signals create square-wave pulses as you rotate the knob:
16+
17+
**Clockwise rotation: (CLK leads DT)**
18+
```
19+
CLK: ┌───┐ ┌───┐
20+
──┘ └───┘ └───┘
21+
DT: ┌───┐ ┌───┐
22+
────┘ └───┘ └───┘
23+
```
24+
25+
**Counter-clockwise rotation: (DT leads CLK)**
26+
```
27+
CLK: ┌───┐ ┌───┐
28+
────┘ └───┘ └───┘
29+
DT: ┌───┐ ┌───┐
30+
──┘ └───┘ └───┘
31+
```
32+
33+
Put simply, if we look at the 'rising edge' (the first moment the CLK goes up), we can check to
34+
see what the state of the DT pin is. If DT is low when CLK rises, the encoder is turning in one
35+
direction. If DT is high when CLK rises, then its being turned the other way.
36+
37+
On many platforms, we have to manually code states that track these and act on those using logic
38+
with a timer. The ESP32 platform has a hardware
39+
[pulse-counter module (PCNT)](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/pcnt.html)
40+
onboard which Toit exposes using the ['pulse-counter' library](https://libs.toit.io/pulse-counter/library-summary).
41+
42+
Using this peripheral we can simply tie our rotary encoder pins to this module to help us track 'intents', a
43+
single click of rotation. Our code can then watch that counter to determine what is happening
44+
with those rotations.
45+
46+
## Prerequisites
47+
48+
We assume that you have set up your development environment as described
49+
in [the IDE tutorial](../../setup/ide).
50+
51+
We also assume that you have flashed your device with Jaguar and that
52+
you are familiar with running Toit programs on it.
53+
If not, have a look at the [Hello world](../../setup/firstprogram) tutorial.
54+
55+
## Setup
56+
57+
Connect your rotary encoder KY-04 (or similar) as follows:
58+
59+
- CLK (or A) to pin 32.
60+
- DT (or B) to pin 33.
61+
- SW (or switch) to pin 25 (optional, if your encoder has a push-button).
62+
- 3.3V to the + (or VCC) pin of the encoder.
63+
- GND to the GND pin of the encoder.
64+
65+
Take care if using your own unmounted encoders as soldering irons can easily harm the
66+
components inside.
67+
68+
<img
69+
src={RotaryDiagram}
70+
alt="KY-040 diagram"
71+
/>
72+
73+
<img
74+
src={RotarySchematics}
75+
alt="KY-040 schematics"
76+
/>
77+
78+
## Code
79+
80+
Create a new file `rotary.toit` and put the following code into it:
81+
82+
```toit
83+
import gpio
84+
import pulse-counter show Unit Channel
85+
86+
CLK-PIN ::= 32
87+
DT-PIN ::= 33
88+
SW-PIN ::= 25
89+
90+
main:
91+
// Set up the pins and the counter.
92+
clk := gpio.Pin CLK-PIN
93+
dt := gpio.Pin DT-PIN
94+
sw := gpio.Pin SW-PIN --input --pull-down
95+
96+
// Since we only need one channel, we can just configure the channel while
97+
// creating the pulse counter. If multiple channels are changing a
98+
// counter (unit), then we would need to create a list of channels and pass
99+
// that to the pulse-counter.Unit constructor.
100+
counter := Unit clk --control-pin=dt
101+
--on-positive-edge=Channel.EDGE-INCREMENT
102+
--on-negative-edge=Channel.EDGE-DECREMENT
103+
--when-control-low=Channel.CONTROL-KEEP
104+
--when-control-high=Channel.CONTROL-INVERSE
105+
106+
// Start the counter.
107+
counter.start
108+
109+
last-count := counter.value
110+
111+
while true:
112+
// Read the current count.
113+
current-count := counter.value
114+
115+
// If the count has changed, print the new value.
116+
if current-count != last-count:
117+
direction := (current-count > last-count) ? "clockwise" : "counter-clockwise"
118+
print "Rotated $direction: $current-count"
119+
120+
if sw.get == 0:
121+
counter.clear
122+
print "Button pressed (counter cleared)"
123+
124+
// Sleep for a short while to avoid busy-waiting.
125+
sleep --ms=100
126+
last-count = current-count
127+
```
128+
129+
This code allocates a pulse counter unit with one channel. Using the
130+
`--on-positive-edge` and `--on-negative-edge` options we tell the pulse counter
131+
to increment the counter when the CLK pin goes high, and decrement it when it
132+
goes low. If the encoder is turned in one direction the control pin switches
133+
polarity between these two events, and the counter thus continuously counts
134+
up or down.
135+
136+
For simplicity we use polling to detect when the switch is pressed. One could
137+
also use a task and `gpio.Pin.wait-for` to detect the switch press asynchronously.
138+
139+
There is currently no way to detect changes in the counter asynchronously. If
140+
pins are free, then just connecting CLK to a second pin and using
141+
`wait-for` to detect changes in the counter is a simple work-around.
142+
143+
## Exercises
144+
- Combine the rotary encoder and the
145+
[SSD 1306 display](https://docs.toit.io/tutorials/hardware/ssd1306) to show a
146+
counter that increments and decrements as you turn the encoder.
147+
- Use asynchronous tasks to detect the switch press and the counter changes.

docs/tutorials/index.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import Watchdog from "../_images/watchdog.svg";
3434
import Qubitro from "../_images/qubitro_logo.svg";
3535
import ToitLogo from "../_images/toit_logo.svg";
3636
import CLogo from "../_images/c_logo.svg";
37+
import Rotary from "../_images/rotary.jpeg";
3738

3839
# Tutorials
3940

@@ -262,6 +263,14 @@ Use a DS18B20 sensor to measure the temperature.
262263
Measure distance with an HC-SR04 ultrasonic distance sensor.
263264
</Box>
264265

266+
<Box title="Rotary encoder" to="hardware/rotary">
267+
<NonZoomableImage
268+
src={Rotary}
269+
alt="A picture of a rotary encoder."
270+
/>
271+
Use the pulse-counter to read a rotary encoder.
272+
</Box>
273+
265274
</Boxes>
266275

267276
### Power
13.8 KB
Binary file not shown.

fritzing/rotary.fzz

39.5 KB
Binary file not shown.

tutorial_code/rotary/main.toit

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import gpio
2+
import pulse-counter show Unit Channel
3+
4+
CLK-PIN ::= 32
5+
DT-PIN ::= 33
6+
SW-PIN ::= 25
7+
8+
main:
9+
// Set up the pins and the counter.
10+
clk := gpio.Pin CLK-PIN
11+
dt := gpio.Pin DT-PIN
12+
sw := gpio.Pin SW-PIN --input --pull-down
13+
14+
// Since we only need one channel, we can just configure the channel while
15+
// creating the pulse counter. If multiple channels are changing a
16+
// counter (unit), then we would need to create a list of channels and pass
17+
// that to the pulse-counter.Unit constructor.
18+
counter := Unit clk --control-pin=dt
19+
--on-positive-edge=Channel.EDGE-INCREMENT
20+
--on-negative-edge=Channel.EDGE-DECREMENT
21+
--when-control-low=Channel.CONTROL-KEEP
22+
--when-control-high=Channel.CONTROL-INVERSE
23+
24+
// Start the counter.
25+
counter.start
26+
27+
last-count := counter.value
28+
29+
while true:
30+
// Read the current count.
31+
current-count := counter.value
32+
33+
// If the count has changed, print the new value.
34+
if current-count != last-count:
35+
direction := (current-count > last-count) ? "clockwise" : "counter-clockwise"
36+
print "Rotated $direction: $current-count"
37+
38+
if sw.get == 0:
39+
counter.clear
40+
print "Button pressed (counter cleared)"
41+
42+
// Sleep for a short while to avoid busy-waiting.
43+
sleep --ms=100
44+
last-count = current-count

0 commit comments

Comments
 (0)