Hello friends, I hope you all are doing great. Today, I am going to share our new Rotary Encoder Library for Proteus. The KY-040 is the small black rotary encoder module you find in almost every Arduino kit: a knurled knob that turns endlessly in 20 clicks per turn, with a push switch built into the shaft. It is the knob behind many volume controls, menus and set-point inputs. With this Rotary Encoder Library for Proteus, you turn that knob in the simulation (one click, five clicks or a non-stop spin) and press it, and your Arduino reads the CLK, DT and SW pins exactly as it would read the real module, contact bounce included.

This is the first version (V1.0) of our rotary encoder library, and it comes with two devices: KY-040 Simple, with an encoder scene and a logic analyzer on the schematic, and KY-040 Advance, with a pop-up window, an event log and four measured encoder test tools. The KY-040 Arduino Proteus demo runs on two Arduino UNOs, counts the clicks with the INT0 interrupt and a 2 ms debounce, and shows everything in the compact Simple interface of our TEP Serial Monitor.

NOTICE: This library is very special to our team. Our KY-040 model drives CLK and DT at the exact quadrature edge times of every click, adds contact bounce bursts to every edge, models the push switch with and without its missing pull-up, and measures everything it does: the lead of the leading contact, the time per click, the bounce of every edge and the length of every press. It took our team a lot of hard work, many test runs and many design changes. Your feedback is the fuel that keeps us going, so please tell us what you think in the comments below or in the Rotary Encoder Library board of our forum. And if our free libraries help you, you can buy us a coffee. So, let's get started with the Rotary Encoder Library for Proteus:

Figure: The KY-040 demo in Proteus 8.5 after five minutes of simulation: U2's Encoder page spins clockwise at 30 RPM (position 56, "CLK fell 25.000 ms before DT"), and U4 prints "Position: 40 (CW)" to "Position: 52 (CW)".

What is a Rotary Encoder?

A rotary encoder turns the rotation of a shaft into electrical pulses. The KY-040 uses an incremental mechanical encoder of the EC11 class: two sliding contacts run over a ring of metal segments as the shaft turns. Each contact connects its pin to ground (GND) for a part of every click, so the module gives two pulse trains, called CLK (also A) and DT (also B). The pulses tell your Arduino how far the knob turned, and the contact that closes first tells it which way.

Unlike a potentiometer, a rotary encoder has no end stop and no absolute position: it only says "one click clockwise" or "one click counter-clockwise", and the sketch keeps the count. That suits menus, volume knobs and any value you change in steps. The knob is also a push button, SW, which connects its pin to ground while you press it.

The KY-040 at a Glance

The KY-040 in numbers
FeatureValue
EncoderEC11-class mechanical incremental encoder with a push switch
Clicks (detents) per turn20, one full CLK / DT cycle per click (18 degrees per click)
OutputsCLK and DT: two contacts to GND with 10 k pull-ups (R2, R3); SW: the push switch to GND
SW pull-upR1, an empty footprint on most boards
PinsGND, +, SW, DT, CLK
Supply3.3 to 5 V; no active parts and no LED
Contact currentA few mA (10 mA at most)
Contact bounceUp to 5 ms on the EN11 datasheet; EC11 types often about 2 ms
Rated speedUp to 100 RPM

Rotary Encoder Terms You Will See in This Article

Rotary encoder terms
TermMeaning
Detent (click)One step of the knob you can feel; 20 per turn on the KY-040
CLK / DTThe two encoder contacts (A and B); LOW when closed, HIGH when open
SWThe push switch on the shaft; LOW while you press the knob
QuadratureTwo signals a quarter of a cycle (90 degrees) apart; their order is the direction
StateThe two levels as a pair, written CLK DT: 11, 01, 00 or 10
LeadHow long the leading contact closes before the other one (25 ms at 30 RPM)
Contact bounceThe fast on-off chatter of a mechanical contact while it closes or opens
DebounceIgnoring that chatter in the sketch, for example with a dead time after each edge
RPMRevolutions per minute; at 30 RPM, one click takes 100 ms
INT0 / INT1The two external interrupts of the Arduino UNO, on D2 and D3
PCINTPin-change interrupts; the AVR model of Proteus 8.5 never runs them

How a Rotary Encoder Tells the Direction

One Click, One Full CLK / DT Cycle

At rest, at a detent, both contacts are open, so the 10 k pull-ups hold CLK and DT HIGH: the state is 11. During one click clockwise, CLK closes first, then DT; then CLK opens, then DT. The state walks through 11, 01, 00, 10 and back to 11. Counter-clockwise, DT leads, and the state walks the other way: 11, 10, 00, 01 and 11. Each step changes only one contact (a Gray code), so the order of the states is the direction.

The Edge Times Inside a Click

Our model turns the knob at a constant speed inside each click. For a click that takes the time P, the leading contact closes at P/8 and the other one at 3P/8; the leading contact opens at 5P/8 and the other one at 7P/8. So the two contacts are always a quarter of a click, 90 degrees, apart:

Click timing in our model (P = 3 / RPM seconds, 20 clicks per turn)
SpeedTime per click (P)Contact edges apart (P / 4)Clicks per second
SLOW 30 RPM100 ms25 ms10
FAST 100 RPM (the rated maximum)30 ms7.5 ms33.3

Reading the Direction: When CLK Falls, Read DT

Here is the simple rule most KY-040 sketches use, and our demo too. When CLK falls, look at DT. Turning clockwise, CLK falls first, so DT is still HIGH: count +1. Turning counter-clockwise, DT fell a quarter of a click earlier (25 ms at 30 RPM), so DT is already LOW: count -1. CLK falls once per click, so the sketch counts once per click.

Contact Bounce

A mechanical contact does not close cleanly. It chatters for a short time, and the pin goes LOW, HIGH, LOW and so on before it settles. The EN11 datasheet allows up to 5 ms of bounce, and EC11 types often give about 2 ms. Our model makes every edge of CLK, DT and SW chatter: the pin changes 3, 5 or 7 times instead of once (1 to 3 extra pulses), all within the BOUNCE time (1 ms by default, from OFF up to 5 ms). Every extra CLK fall looks like another click, so a sketch without a debounce counts them, and the count jumps or even runs backwards. The bursts are made by the model inside the datasheet range; they are not a recording of a real part.

The Push Switch and the Missing R1

SW connects its pin to GND while you press the knob. When you let go, nothing pulls it HIGH again: the board has a place for a pull-up resistor, R1, but on most KY-040 boards it is empty. So SW floats, and a sketch must use INPUT_PULLUP. Our model does the same: SW floats after a release, unless you set the SWPULLUP property to 10K, which fits R1. The switch bounces too, just like CLK and DT.

Features of Rotary Encoder Library for Proteus V1.0

This is our first rotary encoder library for Proteus. Here is what you get:

  • Two devices in TEPKY040.LIB: KY-040 Simple (KY040TEP) and KY-040 Advance (KY040ADVTEP), with one model, TEPKY040.DLL.
  • Exact quadrature: 20 clicks per turn, one full CLK / DT cycle per click, the edges at P/8, 3P/8, 5P/8 and 7P/8; clockwise CLK leads, counter-clockwise DT leads.
  • Contact bounce on every edge of CLK, DT and SW, from OFF up to 5 ms (1 ms by default).
  • The push switch with or without the R1 pull-up: PRESS, HOLD, or hold the mouse button on the knob.
  • A live black KY-040 board: the knob and its red pointer turn 18 degrees per click; PWR, CW, CCW and SW LEDs (a TEP addition) and the ENCODER VIEW screen with a ring of 20 LEDs.
  • An encoder scene with a logic analyzer of the last 200 ms on the Simple device, and an event log with measured values on the Advance device.
  • Four measured test tools: Step count, Bounce & debounce, Direction and Push button.
  • A two-UNO demo with the TEP Serial Monitor: INT0 on CLK with a 2 ms debounce, a press resets the count, and the commands HELP, STATUS, RESET and DEBOUNCE ms; no Arduino library is needed.
  • Light: a package of about 1.43 MB, and a panel that redraws only what changed, at most 12.5 times a second.

Rotary Encoder Library for Proteus: Simple vs Advance

Both devices run the same model with the same properties (the Advance has no PANEL property); only the panel differs:

KY-040 Simple vs Advance
FeatureSimpleAdvance
Full KY-040 model (quadrature, contact bounce, push switch, power)✔✔
Black KY-040 board: turning knob, PWR / CW / CCW / SW LEDs, ENCODER VIEW screen✔✔
Click and hold the knob on the board to press it✔✔
Encoder scene on the schematic: banner, TOP VIEW knob, logic analyzer and buttons✔✘
Pop-up window you can move, resize and minimise✘✔
Encoder page: THE ENCODER card with measured values, the last 200 ms and the event log✘✔
Step count, Bounce & debounce, Direction and Push button tools✘✔
TEP Dark / Light theme, text size, Help page with Copy diagnostics✘✔

Choose Simple to keep the knob and the logic analyzer beside your circuit, and Advance when your sheet is full or you want hard numbers about your sketch. The demo has one of each.

Download Rotary Encoder Library for Proteus

Click the button below to download Rotary-Encoder-Library-for-Proteus-v1.0.zip (about 1.43 MB, without the C++ source code):

Rotary Encoder Library for Proteus V1.0
  • README.txt: a detailed guide to the files, the wiring, the model, the test tools and real hardware.
  • Proteus Library Files: TEPKY040.LIB (both devices), TEPSERIALMON.LIB and our Arduino UNO library ArduinoV3TEP.LIB / ArduinoV3TEP.IDX.
  • Proteus Model Files: TEPKY040.DLL and TEPSERIALMON.DLL.
  • Proteus Simulation: KY040-Encoder-Counter-ArduinoUnoV3.pdsprj, KY040_Encoder_Counter.hex (both UNOs run it) and copies of both DLLs.
  • Arduino Code: KY040_Encoder_Counter.ino, the demo sketch; it needs no Arduino library.

How to Install Rotary Encoder Library for Proteus

  1. Close Proteus and extract the whole zip file.
  2. Copy the four files from Proteus Library Files into the LIBRARY folder, usually C:\Program Files (x86)\Labcenter Electronics\Proteus 8 Professional\LIBRARY (on some PCs C:\ProgramData\Labcenter Electronics\Proteus 8 Professional\LIBRARY; use the folder that already holds the .LIB files of Proteus).
  3. Copy TEPKY040.DLL and TEPSERIALMON.DLL into the MODELS folder.
  4. Start Proteus, press P and search for KY-040 or Rotary Encoder. You get the Simple and the Advance device (category Sensors > Rotary Encoders).

Good to know: keep the .hex file and the two DLLs beside the demo project; the DLL copies there let it run as it is. Tested in Proteus 8.5; Proteus 7 is not supported.

The KY-040 Board in Proteus

We drew the KY-040 as a clean TEP board in the black of the real module: the silver EC11 body with its tabs and legs, the knurled knob with a red pointer, the resistors R1 (an empty footprint), R2 and R3 ("103", 10 k), the two mounting holes and the white "KY-040" print. Here is U1, the Simple device, at rest after Run:

Figure: U1 (Simple) after Run: ENCODER VIEW at 0, CLK HIGH, DT HIGH, SW OPEN, STATE 1 1; the banner says "AT A DETENT - CLK AND DT BOTH HIGH".

The ENCODER VIEW Screen

On two posts above the board sits the ENCODER VIEW - COUNT screen. It shows what the encoder tells a sketch: a ring of 20 LEDs with the position of the count lit (one LED per click of a turn), the count in big digits with the last direction below it (CW > or < CCW), the CLK, DT and SW levels as lamps, the quadrature STATE, the CW and CCW steps, and at the bottom the speed and the bounce (here SLOW 30 RPM - BOUNCE 1 ms).

Pinout

KY-040 pins in Proteus, left to right: GND + SW DT CLK (as on the real module with the header at the bottom; check the silkscreen of your board)
PinWhat it doesDemo connection
GNDGroundGND
+Supply, 3.3 to 5 VPower terminal (+5 V)
SWThe push switch to GND: LOW while pressed, floating when released (no R1)Arduino D4 (INPUT_PULLUP)
DTEncoder contact B with its 10 k pull-upArduino D3
CLKEncoder contact A with its 10 k pull-upArduino D2 (INT0)

LEDs and Indicators

Board animations (the same on both devices)
IndicatorWhat it shows
Knob and red pointerThey turn 18 degrees per click; the knob looks pushed in while SW is LOW
PWR LED (red)Lit while + and GND are connected
CW LED (green)Flashes for 0.3 s on every clockwise click
CCW LED (amber)Flashes for 0.3 s on every counter-clockwise click
SW LED (blue)Lit while SW is LOW (the knob pressed)
ENCODER VIEWThe count, the ring, the CLK / DT / SW lamps, the state, the steps, the speed and the bounce
PANEL: SHOWN / CLOSED or OPEN PANEL, SIMPLE / ADVANCEThe panel button and the device badge

The real KY-040 has no LED at all: PWR, CW, CCW and SW, the ENCODER VIEW and the board button are TEP additions. On both devices, the knob on the board is the push switch: click it and hold the mouse button.

Component Properties

KY-040 properties (double-click the encoder > Edit Properties)
PropertyMeaningDefault
SPEEDTurn speed: SLOW (30 RPM, 100 ms per click) or FAST (100 RPM, 30 ms per click)SLOW
BOUNCEContact bounce in ms, 0 (OFF) to 51
SWPULLUPThe SW pull-up R1: NONE (as on most boards) or 10KNONE
PANELSimple only: the encoder scene OPEN or CLOSED at RunOPEN

A bad value is noted in the Simulation Log, and the default is used. The panel style comes from the device, not from a property.

KY-040 Simple: The Encoder Scene on the Sheet

Beside the Simple board sits the KY-040 ENCODER SCENE panel (on the right of the image above):

The KY-040 Simple encoder scene
PartWhat it does
Header, red XKY-040 ENCODER SCENE; the X closes the panel
BannerAT A DETENT - CLK AND DT BOTH HIGH, TURNING CLOCKWISE - CLK FALLS FIRST, THEN DT, TURNING COUNTER-CLOCKWISE - DT FALLS FIRST, THEN CLK, KNOB PRESSED - SW SHORTED TO GND, or NO POWER
TOP VIEWThe knob seen from above: click its left half for one click CCW, its right half for one click CW; below it the state row CLK DT 1 1, 0 1, 0 0, 1 0 with the current state in blue
LOGIC ANALYZER - LAST 200 msCLK, DT and SW lanes, the contact bounce as pale bursts, and a STEPS mark at every click
POSITION, DIRECTION, RPM, SWThe panel's own count, the last direction, the speed while turning and the switch (OPEN or PRESSED)
TURN THE KNOBCW x1, CCW x1, CW x5, CCW x5, SPIN CW, SPIN CCW (click a SPIN button again to stop)
TURN SPEEDSLOW 30 RPM or FAST 100 RPM; a change applies from the next click
PUSH BUTTON (SW)PRESS (SW LOW for 0.2 s) or HOLD (until you click it again)
CONTACT BOUNCEBOUNCE ON (the BOUNCE property, 1 ms when it is 0) or BOUNCE OFF (ideal contacts)
SET POSITION TO 0Sets the POSITION readout of the panel back to 0 (the sketch keeps its own count)

In the start image, SLOW 30 RPM and BOUNCE ON are dark blue, the active settings, and the red line "SW HAS NO PULL-UP - USE INPUT_PULLUP" reminds you that R1 is empty. Clicks queue up, and opposite clicks cancel each other.

Five Clicks Clockwise

Figure: CW x5: the pointer turned 5 x 18 = 90 degrees; ENCODER VIEW 5, "CW 5 CCW 0"; POSITION 5, DIRECTION CW.

Click CW x5. The knob turns five clicks, 100 ms each at SLOW 30 RPM, and the CW LED flashes on every click. Then the pointer stands at 90 degrees (5 x 18), the fifth LED of the ring is lit, and the screen says 5 and CW >. The state is back at 1 1 (both contacts open at a detent), RPM reads 0, and the logic analyzer is flat again: its 200 ms window has passed. The sketch on UNO 1 printed one Position line per click on U3.

Spinning: The Logic Analyzer

Figure: SPIN CW at 30 RPM: CLK falls before DT, each edge with its pale bounce burst, a green STEPS mark at every detent; count 26, STATE 0 0.

Click SPIN CW: the knob keeps turning until you click it again. The banner turns green and says TURNING CLOCKWISE - CLK FALLS FIRST, THEN DT, a green trail follows the TOP VIEW knob, and RPM reads 30. The logic analyzer shows the last 200 ms, two clicks: CLK goes LOW first, DT follows 25 ms later, then CLK and DT go HIGH again in the same order. The pale lines at each edge are the contact bounce, and the green STEPS marks sit at the detents. In this frame both contacts are closed (state 0 0, in blue), the middle of a click.

Pressing the Knob

Figure: HOLD: "KNOB PRESSED - SW SHORTED TO GND", the SW LED and the SW lamp lit, the SW lane LOW, SW: PRESSED.

Click HOLD, or click the knob on the board and hold the mouse button. SW goes LOW: the banner turns blue, the knob looks pushed in, the SW LED and lamp light up, and the SW lane drops. Click HOLD again to let go; PRESS gives a 0.2 s press. The panel's POSITION stays at 5: only the sketch resets its count on a press, and it prints "Button pressed - reset to 0" and, after the release, how long you pressed.

Closing the Panel

Click the red X: the panel disappears, the board button says PANEL: CLOSED, and the encoder keeps working. Click PANEL: CLOSED to bring the panel back. Here, U1 stands at 31 clicks with its panel closed:

Figure: The panel closed: PANEL: CLOSED; the board and the ENCODER VIEW keep working (31, CW 31 CCW 0).

KY-040 Advance: The Pop-Up Encoder Window

The Advance device keeps only the board on the schematic. Here is U2 after the four tests:

Figure: U2 (Advance) with OPEN PANEL and the ADVANCE badge: count 4 after the tests, CW 143 CCW 139.

At Run, the "TEP KY-040 Advance" pop-up opens with the look of our TEP Serial Monitor and no Windows title bar: move it by its header, resize or minimise it, and reopen it with OPEN PANEL. The header shows the state (for example U2 · TURNING CW), the Encoder and Test tabs and the theme, Settings and Help icons; chips below show the position, CLK and DT, the speed, the bounce and SW.

The Encoder Page

Figure: The Encoder page during CW x5, after CCW x1 and a PRESS: position 2, "CLK fell 25.000 ms before DT (CW)", detent period 100.000 ms, last press 0.200 s measured.

This is U2 in the middle of CW x5, after one click CCW and a PRESS. The page has five parts:

  • TOP VIEW: click the left half for one click CCW, the right half for one click CW, the middle for a 0.2 s press.
  • THE ENCODER: the state, the position (since the start or the last zero), the steps (CW 3, CCW 1), the speed, CLK / DT (state 0 1) and the measured values.
  • TURN, PRESS AND SET: the controls of the Simple panel.
  • THE LAST 200 ms: CLK, DT and SW with the bounce drawn pale, and the detent marks.
  • EVENT LOG - NEWEST FIRST: every command, click and press with its simulation time.

The measured lines are the interesting ones: Lead "CLK fell 25.000 ms before DT (CW)", the quarter click at 30 RPM; Detent period 100.000 ms, from click to click; Contact bounce "1 ms - the last detent 20 extra edges" and Last burst "DT: 7 edges in 1.000 ms"; and Last press "LOW for 0.2 s (0.200 s measured)".

The Event Log

Read the event log above from the bottom up: at 19.800 s "Knob pressed (PRESS) - SW LOW", at 20.000 s "Knob released - SW was LOW for 0.2 s (0.200 s)", at 27.480 s "Turn: 5 detents CW at 30 RPM", and then one line per click, every 100 ms, with the new position. The first click says "CLK fell first, 25.000 ms before DT, 18 bounce edges"; it has no period, because no click came just before it. The next ones add it: "CLK first, 25.000 ms lead, period 100.000 ms, 16 bounce edges".

Figure: Spin CW after the four tests: position 58, steps CW 197 / CCW 139, last press 0.080 s measured (the double click of the Push button test).

After the tests, I clicked Spin CW. Both contacts are closed in this frame (state 0 0), and the event log adds a click every 100 ms, each with 12 to 22 bounce edges: the chatter differs from edge to edge. Position 58 is 197 clicks CW minus 139 CCW. The 6 presses are the PRESS from before and the five of the Push button test, the last one an 80 ms click. Copy diagnostics on the Help page copies these values and events for a bug report.

The Test Page: Four Measured Encoder Test Tools

The four tools on the Test tab only turn the knob, switch the bounce or press the switch, and then read the CLK, DT and SW edges the model really drove. So every number is measured with the simulation clock, and every tool puts the speed and the bounce back when it ends. A tool refuses to start, with the reason, when the encoder has no power or another test runs. Each one ends with a bold "Good for:" line, and its result tiles show "-" until it has measured a value. All results below come from U2 in Proteus 8.5.

1. Step Count: Which Way of Reading Keeps Up?

Figure: Step count, 20 clicks (one turn) each way, bounce 1 ms: the grid shows OK or the CW / CCW error per speed; the dashed line marks the EN11's maximum, 100 RPM.

Good for: "seeing which way of reading the encoder keeps up with fast turning - and why a debounce matters." The tool turns 5 or 20 clicks CW and back CCW at 30, 60, 100, 200, 400 and 600 RPM, and four decoders read the same captured edges:

  • Demo sketch (INT0, 2 ms): our demo's rule with its 2 ms debounce.
  • INT0, no debounce: the same rule, but every CLK fall counts.
  • State table (INT0 + INT1): interrupts on both pins and the full Gray-code table.
  • loop() every 10 ms: a sketch that reads both pins every 10 ms, tried at 10 phases.
Step count in Proteus (U2; CW / CCW steps counted, 20 / 20 turned at every speed; bounce 1 ms)
SpeedDemo (CW / CCW)No debounceState table10 ms loopLead
30 RPM20 / 20 OK107 / 9620 / 20 OKright at 10 of 1025.000 ms
60 RPM20 / 20 OK91 / 9820 / 20 OKright at 7 of 1012.500 ms
100 RPM20 / 20 OK86 / 10120 / 20 OKright at 3 of 107.500 ms
200 RPM20 / 20 OK100 / 9020 / 20 OKright at 0 of 103.750 ms
400 RPM20 / 20 OK98 / 9720 / 20 OKright at 0 of 101.875 ms
600 RPM1 / 1106 / 10720 / 20 OKright at 0 of 101.250 ms

Without a debounce, the bursts add dozens of steps (107 CW and 96 CCW for one turn each way at 30 RPM), wrong at all six speeds. The demo's 2 ms debounce counts right up to 400 RPM. At 600 RPM, CLK changes every 2.5 ms, the quiet time between the bursts is shorter than 2 ms, and the demo ignores almost every fall: 1 / 1 instead of 20 / 20. The real encoder is rated to 100 RPM (the dashed line), so 400 RPM is a large margin.

The state table is right at all six speeds, even with no debounce: a bounce only flips between two neighbouring states and back, a step forward and a step back. The 10 ms loop counts right at every phase only at 30 RPM. At 60 RPM, where the states change every 12.5 ms, close to the loop's 10 ms, it is right at 7 of 10 phases, and from 200 RPM on, at none. The Lead column is the measured quarter click: a polling loop must look at the pins clearly more often than that.

2. Bounce & Debounce: How Long Should Your Debounce Be?

Figure: Bounce & debounce, as set (1 ms): 80 transitions, all bounced; 5.1 edges on average, 7 at most; a debounce from 1 to 12 ms counts right (the demo's 2 ms is OK).

Good for: "choosing the debounce time of your sketch - long enough for the chatter, short enough for fast turns." Choose the bounce (as set, 1 ms, or 5 ms, the EC11 maximum) and click Run test. The tool turns 10 clicks CW at 30 RPM and 10 clicks CCW at 100 RPM, and it measures every CLK and DT transition: its edges, its length and its shortest pulse.

Twenty clicks with four contact changes each make 80 transitions, and all 80 bounced. The histogram shows 23 transitions with 3 edges, 30 with 5 and 27 with 7: 5.1 edges on average, all within 1.000 ms. The list shows the 100 RPM pass: a transition every 7.5 ms, with pulses as short as 66.7 us. Then the tool runs the demo's rule on the same edges with nine debounce times:

The demo's rule with each debounce (CW / CCW steps counted; right = 10 / 0 and 0 / 10; bounce 1 ms)
Debounce30 RPM CW (10 clicks)100 RPM CCW (10 clicks)
0 ms28 / 2122 / 25
0.25 ms19 / 64 / 16
0.5 ms12 / 20 / 11
1 ms10 / 00 / 10
2 ms (the demo)10 / 00 / 10
3 ms10 / 00 / 10
5 ms10 / 00 / 10
8 ms10 / 00 / 10
12 ms10 / 00 / 10

A debounce shorter than the bounce lets the chatter through; from 1 ms to 12 ms, every debounce counts right here. With a 5 ms bounce, the README's PC run gives a right count only from 5 to 8 ms, so the demo's 2 ms is then wrong: send DEBOUNCE 5 from U4 first (one of its quick buttons). A debounce can also be too long: at 100 RPM, CLK changes only every 15 ms, and the bounce eats into that quiet time.

3. Direction: Which Contact Leads?

Figure: Direction at 30 RPM: CW - CLK leads by 25.000 ms (90.0 deg), CCW - DT leads by 25.000 ms; 16 / 16 decoded right by both; 9 reversals, none lost.

Good for: "checking which way your sketch counts - and that CLK and DT are not swapped in the wiring." The tool turns 4 clicks CW, 4 CCW and then 8 alternating, at the speed as set (30 RPM here). For every click, it lists the contact that fell first, its lead in ms and in degrees, the four states, and what the demo rule and a state table decode.

The two plots show the newest click of each direction. CW: CLK falls at 12.5 ms, DT at 37.5 ms, CLK rises at 62.5 ms and DT at 87.5 ms, exactly P/8, 3P/8, 5P/8 and 7P/8 of a 100 ms click; CCW is the same with DT first. The states run 11 01 00 10 11 (CW) and 11 10 00 01 11 (CCW), and both decoders got all 16 clicks right through 9 reversals. If your sketch counts the wrong way on real hardware, swap CLK and DT.

4. Push Button: Press Times and What a Sketch Sees

Figure: Push button: 5 of 5 presses seen with a 30 ms debounce, 1 long press (1.5 s), 1 double click; bounce up to 7 / 7 edges in 1.000 ms.

Good for: "setting long-press and double-click times in your sketch - and the bounce its button code must ignore." The tool presses the switch five times and measures the LOW time and the bounce on the edges of SW. A 30 ms debounce, the demo's, decides what a sketch sees: a long press lasts 1 s or more, a double click is two presses within 0.4 s.

Push button test (U2; SW with R1 empty, bounce 1 ms; SW floating after every release)
PressPlannedLOW (measured)Press bounceRelease bounceA sketch saw
Click100 ms100.000 ms7 edges / 1.00 ms5 edges / 1.00 ms0.1 s
PRESS (0.2 s)200 ms200.000 ms7 edges / 1.00 ms5 edges / 1.00 ms0.2 s
Long press1500 ms1500.000 ms7 edges / 1.00 ms7 edges / 1.00 ms1.5 s, long
Double click 180 ms80.000 ms7 edges / 1.00 ms3 edges / 1.00 ms0.1 s
Double click 280 ms80.000 ms5 edges / 1.00 ms7 edges / 1.00 ms0.1 s, double

Every LOW time matches the plan to the microsecond, and U4 printed the same five presses (see the Serial Monitor below). After each release SW floats, because R1 is empty; the demo's INPUT_PULLUP pulls it HIGH.

Settings and Help

The gear opens Settings: theme, text size, open at Run and window size, saved for your Windows user.

Figure: Settings, saved under HKCU\Software\TheEngineeringProjects\TEP KY-040 Advance.

The ? icon opens Help & Support: eight cards with their links (the rotary encoder forum board, this article, updates, donate, the website and the forum) and Copy diagnostics for a bug report. A links.ini next to the DLL can change the links.

Figure: Help & Support: every card shows its link; TEP KY-040 Advance v1.0, build 2026-10-09, TEPKY040.DLL.

Rotary Encoder Counter with Arduino in Proteus

Open KY040-Encoder-Counter-ArduinoUnoV3.pdsprj from the Proteus Simulation folder, with KY040_Encoder_Counter.hex and both DLLs beside it. Its two Arduino UNOs, from our TEP Arduino UNO V3 library, run the same HEX file:

  • NODE 1 - SIMPLE: UNO 1 (ARD1) with the KY-040 Simple (U1) and Serial Monitor U3.
  • NODE 2 - ADVANCE: UNO 2 (ARD2) with the KY-040 Advance (U2) and Serial Monitor U4.
Figure: The whole circuit running, without the pop-up windows: U1 at 0 with its scene, U2 at 4, U3 with the start lines and U4 with the last presses.

Wiring

Demo wiring (the same on both UNOs)
FromToWhy
KY-040 CLKArduino D2INT0: an interrupt on every CLK change
KY-040 DTArduino D3Read in the interrupt (D3 is INT1, free for a state-table sketch)
KY-040 SWArduino D4The push switch, read with INPUT_PULLUP
KY-040 + / GNDPower terminal / groundPower (the PWR LED lights)
Serial Monitor RXD / TXD / GNDArduino D1 / D0 / groundShows what the Arduino prints, sends what you type
Serial Monitor RXD2Not connectedThe sketch prints what it decodes
LED + 220 Ω on A0Not usedLeft from the demo template; the sketch does not use it

The UNO's own LED on D13 toggles on every press. No Arduino library is needed: the sketch reads the pins itself.

Why INT0? Proteus Has No Pin-Change Interrupts

Many encoder libraries for Arduino count with pin-change interrupts (PCINT), which work on any pin. The AVR model of Proteus 8.5 never runs pin-change interrupts, so such a sketch does not count in the simulation. Our demo uses the external interrupt INT0 on D2, which Proteus runs; INT1 on D3 works too. The same sketch runs on a real UNO.

The Arduino Code

The sketch starts with its pins and the shared variables. Everything the interrupt changes is volatile:

const int PIN_CLK = 2;
const int PIN_DT = 3;
const int PIN_SW = 4;
const int PIN_LED = 13;

const unsigned long BUTTON_DEBOUNCE_MS = 30;  // the push button must be stable for 30 ms

volatile unsigned long debounceUs = 2000;     // ignore CLK chatter shorter than 2 ms (DEBOUNCE ms)
volatile long position = 0;                   // changed inside the interrupt

In setup(), CLK and DT are plain inputs, because the board has its own 10 k pull-ups, and SW gets the internal pull-up of the UNO. Then INT0 is attached to CLK for every change, and the three start lines follow:

void setup() {
  Serial.begin(9600);
  pinMode(PIN_CLK, INPUT);                     // CLK and DT have 10k pull-ups on the KY-040 board
  pinMode(PIN_DT, INPUT);
  pinMode(PIN_SW, INPUT_PULLUP);               // SW has no pull-up on most boards
  pinMode(PIN_LED, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClkChange, CHANGE);
  Serial.println("KY-040 rotary encoder - turn the knob or press it");
  Serial.println("Rotary Encoder Library for Proteus V1.0 - type HELP for the commands.");
  Serial.println("Position: 0");
}

The interrupt runs on every CLK change, rising or falling, bounce included. It measures the quiet time since the last CLK change and goes on only for a CLK fall that comes more than the debounce time after it:

// Runs every time CLK changes (rising or falling)
void onClkChange() {
  unsigned long now = micros();
  unsigned long quietTime = now - lastClkChange;
  lastClkChange = now;
  clkInterrupts++;
  if (digitalRead(PIN_CLK) == LOW) {
    if (quietTime > debounceUs) {

Then it reads DT with digitalRead(PIN_DT). DT still HIGH means CLK fell first: clockwise, +1:

        position++;
        lastDirection = 1;
        cwSteps++;

DT already LOW means DT fell first: counter-clockwise, -1. A CLK fall within the debounce time is a bounce, and the sketch only counts it as ignored:

        position--;
        lastDirection = -1;
        ccwSteps++;
      }
      moved = true;
    } else {
      ignoredFalls++;                            // a bounce of the contact, not a new click
    }
  }
}

The loop prints the position only when it changed. It copies the shared values with the interrupts off, so it never reads a value the interrupt is changing at that moment:

  // 1. Print the position when the knob moved
  if (moved) {
    noInterrupts();                            // copy the shared values safely
    long p = position;
    int dir = lastDirection;
    moved = false;
    interrupts();
    if (p != shownPosition) {
      shownPosition = p;
      Serial.print("Position: ");
      Serial.print(p);
      Serial.println(dir > 0 ? " (CW)" : " (CCW)");
    }
  }

The push button has its own debounce: SW must be stable for 30 ms. A press resets the count and toggles the LED; a release prints how long you pressed:

  // 2. The push button: debounce it, then reset the count and toggle the LED
  bool reading = digitalRead(PIN_SW);
  if (reading != lastButtonReading) {
    buttonChangedAt = millis();
    lastButtonReading = reading;
  }
  if (millis() - buttonChangedAt > BUTTON_DEBOUNCE_MS && reading != buttonState) {
    buttonState = reading;
    if (buttonState == LOW) {                  // pressed (SW is pulled to GND)
      noInterrupts();
      position = 0;
      interrupts();
      shownPosition = 0;
      ledOn = !ledOn;
      digitalWrite(PIN_LED, ledOn ? HIGH : LOW);
      presses++;
      pressedAt = millis();
      Serial.println("Button pressed - reset to 0");
    } else {                                   // released: how long it was held
      Serial.print("Button released after ");
      printSeconds(millis() - pressedAt);
      Serial.println();
    }
  }

printSeconds() rounds the time to 0.1 s:

// prints a time in milliseconds as seconds with one decimal, e.g. 1.5 s (rounded to 0.1 s)
void printSeconds(unsigned long ms) {
  ms += 50;
  Serial.print(ms / 1000);
  Serial.print('.');
  Serial.print((ms % 1000) / 100);
  Serial.print(" s");
}

DEBOUNCE ms changes the debounce time while the sketch runs, from 0 to 20 ms:

  } else if (strncmp(text, "DEBOUNCE", 8) == 0 && numberAfter(text) >= 0 && numberAfter(text) <= 20) {
    unsigned long ms = numberAfter(text);
    noInterrupts();
    debounceUs = ms * 1000;
    interrupts();
    if (ms == 0) Serial.println("Debounce off: every CLK fall counts (bounce adds steps).");
    else {
      Serial.print("Debounce: CLK changes within ");
      Serial.print(ms);
      Serial.println(" ms of the last one are ignored.");
    }

STATUS copies the counters in the same safe way before it prints them, and HELP prints three short lines:

void printStatus() {
  noInterrupts();                                // copy the shared values safely
  long p = position;
  unsigned long cw = cwSteps, ccw = ccwSteps, isr = clkInterrupts, ignored = ignoredFalls, us = debounceUs;
  interrupts();
void printHelp() {
  Serial.println("Commands: HELP, STATUS, RESET, DEBOUNCE ms");
  Serial.println("  RESET: the position back to 0 (a press does it too)");
  Serial.println("  DEBOUNCE ms: ignore CLK changes within ms (0-20)");
}

The values live in RAM, so a new Run starts again at position 0 with a 2 ms debounce. To change the sketch, open it in the Arduino IDE, select Arduino Uno, use Sketch > Export Compiled Binary, and replace the HEX file of the simulation with the exported one.

Serial Monitor Commands

Commands of the demo sketch, upper or lower case (quick buttons: U3 STATUS, DEBOUNCE 0, DEBOUNCE 2, HELP; U4 STATUS, DEBOUNCE 5, RESET, HELP)
CommandWhat it does
HELPThe command list (three lines)
STATUSTwo lines: the position with the CW / CCW steps; the CLK interrupts, the bounce falls ignored, the debounce and the presses
RESETThe position back to 0 (a press does it too)
DEBOUNCE msIgnore CLK changes within ms of the last one, 0 to 20 (2 at start); DEBOUNCE 0 shows the bounce miscounts

How to Run the Demo

  1. Press Run. Both Serial Monitors (9600 baud) and U2's pop-up open, and U3 and U4 print the three start lines.
  2. On U1's panel, click CW x5 and CCW x5; try SPIN CW, FAST 100 RPM, BOUNCE OFF and PRESS, or hold the mouse button on the board's knob.
  3. On U3, click STATUS, then DEBOUNCE 0, and turn the knob: the bounce adds clicks. DEBOUNCE 2 makes it right again.
  4. On U2, try the Encoder page, then the four tools on the Test tab.
  5. Close U2's window and open it again with OPEN PANEL; close U1's panel with its red X and open it with PANEL: CLOSED.

Rotary Encoder Proteus Simulation Results

The Serial Monitor in Its Simple Interface

Both monitors in this demo run in the Simple interface of our TEP Serial Monitor, as in every monitor screenshot here. Only the log, the send box and one slim toolbar row stay visible (baud rate, follow, pause, search and clear), so the window stays small.

  • Switch to it: right-click the log and choose Simple interface, or press Ctrl+Shift+U.
  • Back to the full view: click the corner button (the expand arrows at the top right), press Esc or Ctrl+Shift+U, or right-click and choose Full interface.
  • Your choice is remembered.

Type commands such as STATUS or DEBOUNCE 5 in the send box and press Enter; the quick buttons come back with the full view. Every line of the sketch is shorter than 80 characters (the longest has 69), so it fits this small window. Read all about the monitor in TEP Serial Monitor Advance for Proteus.

Start-Up, One Click Back, a Press and Five Clicks

After Run, U4 prints the three start lines. Then I clicked CCW x1, Press (0.2 s) and CW x5 on U2's Encoder page:

Figure: U4 in the Simple interface: the start lines, one click CCW, a PRESS (0.2 s) and five clicks CW.
KY-040 rotary encoder - turn the knob or press it
Rotary Encoder Library for Proteus V1.0 - type HELP for the commands.
Position: 0
Position: -1 (CCW)
Button pressed - reset to 0
Button released after 0.2 s
Position: 1 (CW)
Position: 2 (CW)
Position: 3 (CW)
Position: 4 (CW)
Position: 5 (CW)

One line per click and nothing in between: the sketch prints only when something happens. The press reset the count to 0, so CW x5 counts from 1 to 5, while the pop-up, which counts since the start, went from -1 to 4.

The Push Button Test on U4

Figure: U4 during the Push button test: the last clicks of the Direction test, then five presses, released after 0.1, 0.2, 1.5, 0.1 and 0.1 s.
Position: 5 (CCW)
Position: 6 (CW)
Position: 5 (CCW)
Button pressed - reset to 0
Button released after 0.1 s
Button pressed - reset to 0
Button released after 0.2 s
Button pressed - reset to 0
Button released after 1.5 s
Button pressed - reset to 0
Button released after 0.1 s
Button pressed - reset to 0
Button released after 0.1 s

The first three lines are the end of the Direction test (alternating clicks). Then come the five presses of the Push button test: the sketch, with its 30 ms debounce, saw all five, and its times are the measured LOW times of the tool rounded to 0.1 s (100, 200, 1500, 80 and 80 ms).

STATUS and DEBOUNCE 0

STATUS answers with two lines. Here is the README's example after five clicks CW and a press (the numbers depend on the run and the bounce):

Position: 0  (CW steps 5, CCW steps 0)
  CLK interrupts 46, bounce ignored 18; debounce 2 ms; presses 1

Five clicks are only 10 real CLK changes; the other interrupts are bounce. Now send DEBOUNCE 0 ("Debounce off: every CLK fall counts (bounce adds steps).") and turn one click CW. The README's example shows what that single click can print:

Position: 1 (CW)
Position: 2 (CW)
Position: 1 (CCW)
Position: 0 (CCW)
Position: -1 (CCW)

That is the contact bounce, made visible. Send DEBOUNCE 2 ("Debounce: CLK changes within 2 ms of the last one are ignored."), and every click counts once again.

Troubleshooting

  • The count jumps by 2 or 3 per click, or goes the wrong way: that is the contact bounce with no debounce in the sketch. Use DEBOUNCE 2, or BOUNCE OFF on the panel to see ideal contacts.
  • The count runs backwards: CLK and DT are swapped in the wiring, or your board labels them the other way round. Swap them.
  • SW reads random values: R1 is empty, as on most boards, so SW floats after a release. Use INPUT_PULLUP, or set SWPULLUP to 10K.
  • Nothing counts with an encoder library that uses pin-change interrupts: Proteus 8.5 never runs them. Use D2 / D3 (INT0 / INT1), as the demo does.
  • The Step count tool shows the demo missing clicks at 600 RPM: that is correct. At that speed, the quiet time between CLK changes (2.5 ms minus the bounce) is shorter than the 2 ms debounce; the real encoder is rated to 100 RPM.
  • The Test tab says "Cannot start: ...": the reason is in the message (no power, or another test is running).
  • The part is not simulated, there is no panel or no monitor window: TEPKY040.DLL or TEPSERIALMON.DLL is missing from MODELS and the project folder; click OPEN MONITOR on a closed monitor.
  • The Advance pop-up does not appear: click OPEN PANEL on the board, or turn "Open the panel at Run" back on in Settings.

Things to Know Before Using a Real KY-040

The demo sketch uses no library, so it runs on a real UNO and KY-040 as it is. Keep in mind:

  • Supply: 3.3 to 5 V. The contacts carry only a few mA (10 mA at most); the module has no active parts and no LED.
  • Clicks: 20 per turn, one full CLK / DT cycle per click. Encoders with 30 clicks and 15 pulses per turn (half a cycle per click) exist; our model does not cover them, and a sketch that counts only CLK falls sees every second click on them.
  • Contact bounce: up to 5 ms on the EN11 datasheet (EC11 types often about 2 ms); 100 RPM is the rated maximum speed. A 5 ms debounce counted right at both bounce settings in our Bounce & debounce tool, so DEBOUNCE 5 is a safe start for an unknown part.
  • CLK and DT names: some clones and tutorials label them the other way round. If your count runs backwards, swap the two wires or the two pins in the sketch.
  • SW pull-up: R1 is missing on most boards, so use INPUT_PULLUP.
  • Pin order: GND + SW DT CLK with the header at the bottom; check the silkscreen of your board.
  • Interrupts: on a real UNO, pin-change interrupts work too; INT0 and INT1 work in both worlds.

Limitations of the Simulation

  • Not modelled: 30-click / 15-pulse encoders, the uneven speed of a hand inside a click, sliding noise and contact resistance, the difference between 3.3 V and 5 V, and wear.
  • The bounce bursts are generated by the model inside the datasheet range; they are not a measurement of a real part.
  • The model is our own implementation, written from the published KY-040, EN11 and EC11 descriptions; it contains no third-party code. The HEX file also contains the Arduino AVR core 1.8.6, which includes LGPL-licensed code; its source code is on GitHub.
  • Tested in Proteus 8.5 in October 2026 (the demo, both panels, the four tools and every screenshot here), at about 0.62 x real time with a test tool running, the pop-up open and two monitors, and with 1,277 automatic PC checks, all passed, 28 of them with the real demo sketch on two UNOs. Proteus 7 is not supported.

Need another input for your menus? See our Joystick Module Library for Proteus, or our Touch Sensor Library for Proteus V2.0 for a touch button. To send the knob's position to another Arduino by radio, add our HC-12 Library for Proteus.

So, that was all about the Rotary Encoder Library for Proteus. I hope the turning knob, the logic analyzer with its contact bounce, the measured event log and the four test tools make the KY-040 much easier to understand, so your encoder sketch counts right the first time you wire a real module. If you use the Rotary Encoder Library for Proteus in a project, please share your feedback in the comments or in our forum, and if you have any questions, ask in the comments and I will help you out. Till the next tutorial, take care and have fun!