{"id":18523365,"url":"https://github.com/mhx/librotaryencoder","last_synced_at":"2026-02-23T09:03:05.480Z","repository":{"id":146758162,"uuid":"575140820","full_name":"mhx/librotaryencoder","owner":"mhx","description":"Simple, small rotary encoder library","archived":false,"fork":false,"pushed_at":"2022-12-13T17:15:10.000Z","size":21,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T20:51:13.510Z","etag":null,"topics":["c","cpp","embedded","embedded-systems","library","quadrature-encoder","rotary-encoder","rotary-encoders"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mhx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-06T21:07:42.000Z","updated_at":"2024-04-12T05:24:00.000Z","dependencies_parsed_at":"2023-03-26T13:18:58.769Z","dependency_job_id":null,"html_url":"https://github.com/mhx/librotaryencoder","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhx%2Flibrotaryencoder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhx%2Flibrotaryencoder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhx%2Flibrotaryencoder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhx%2Flibrotaryencoder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mhx","download_url":"https://codeload.github.com/mhx/librotaryencoder/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250354279,"owners_count":21416748,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","cpp","embedded","embedded-systems","library","quadrature-encoder","rotary-encoder","rotary-encoders"],"created_at":"2024-11-06T17:35:17.508Z","updated_at":"2025-10-30T01:05:11.394Z","avatar_url":"https://github.com/mhx.png","language":"C","readme":"[![Build Status](https://app.travis-ci.com/mhx/librotaryencoder.svg?branch=main)](https://app.travis-ci.com/github/mhx/librotaryencoder)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/53489f77755248c999e380500267e889)](https://www.codacy.com/gh/mhx/librotaryencoder/dashboard?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=mhx/librotaryencoder\u0026amp;utm_campaign=Badge_Grade)\n\n# librotaryencoder\n\nSimple, small rotary encoder library\n\n## Why?\n\nRotary encoder implementations suck. I've come across so many devices with\nrotary encoders that are barely usable because of erratic encoder behaviour.\n\nI don't know why there are so many bad rotary encoder implementations out\nthere. I know there are some good ones, too, but they have other issues\n(e.g. they're bloated or specific to certain platforms).\n\nThe goals/features of this library are:\n\n- **Easy to use, consistent interface:** provide the logic levels at the A, B\n  terminals and receive back the rotation (clockwise, counter-clockwise,\n  none).\n\n- **Fully unit tested.**\n\n- **Small code size:** simple update routine can compile down to about a\n  dozen instructions on an 8-bit AVR MCU.\n\n- **Error recovery** in case of illegal transitions without erratic rotation\n  output.\n\n- **Support arbitrarily many encoders**, using only one byte of state per\n  encoder.\n\n- **Support for half- and full-step encoders**, even combinations of both\n  in the same project.\n\n- **Support for both \"simple\" and debounced algorithms.** The simple\n  implementation can be used if code size must be minimised by all\n  means.\n\n- **Support for both \"pure C\" and C++ environments.** Support for both\n  \"plain\" and polymorphic classes in C++.\n\nThe library intentionally doesn't provide features such as rotation\nspeed or acceleration to keep it simple and small. Such features can\neasily be added outside of the library if needed.\n\n## Requirements\n\nThe library requires a C90 compliant C compiler and/or a C++98\ncompliant C++ compiler. If you want to use the polymorphic wrapper\ntemplate, a C++11 compliant compiler is required.\n\n## How to use this library\n\nFirst and foremost, in order for this library (or in fact *any*\ncode handling rotary encoders) to work correctly, you must avoid\nlosing transitions between encoder states. The simplest and most\neffective way of doing this is to use edge-triggered interrupts,\nand to avoid running unnecessary code in the interrupt handler.\n\nHere's a simple, yet fully working, example of using a\n[PEC11R](https://bourns.com/products/encoders/contacting-encoders/product/PEC11R)\nencoder connected to pins `PA1` and `PA2` of an\n[ATtiny824](https://www.microchip.com/en-us/product/ATtiny824)\nmicrocontroller.\n\n``` c\n#include \u003cavr/interrupt.h\u003e\n#include \u003cavr/io.h\u003e\n\n#include \u003crotaryencoder/debounced_encoder.h\u003e\n\nstatic encoder_state es;\nstatic volatile uint8_t encoder_value;\n\nISR(PORTA_PORT_vect)\n{\n  if (PORTA.INTFLAGS \u0026 (PIN1_bm | PIN2_bm))\n  {\n    PORTA.INTFLAGS = PIN1_bm | PIN2_bm; // Clear interrupt flags\n    // Update encoder state\n    switch (encoder_debounced_full_step_update(\u0026es, (PORTA.IN \u003e\u003e 1) \u0026 0x3))\n    {\n    case ENCODER_ACTION_TURN_CW:\n      encoder_value++;\n      break;\n    case ENCODER_ACTION_TURN_CCW:\n      encoder_value--;\n      break;\n    default:\n      break;\n    }\n  }\n}\n\nint main(void)\n{\n  // Configure edge-triggered interrupts for both terminal inputs\n  PORTA.PIN1CTRL = PORT_ISC_BOTHEDGES_gc; // Encoder Terminal A\n  PORTA.PIN2CTRL = PORT_ISC_BOTHEDGES_gc; // Encoder Terminal B\n\n  // Initialize encoder state\n  encoder_debounced_full_step_init(\u0026es, (PORTA.IN \u003e\u003e 1) \u0026 0x3);\n\n  sei(); // Enable interrupts\n\n  uint8_t last_encoder_value = 0;\n\n  for (;;)\n  {\n    if (encoder_value != last_encoder_value)\n    {\n      last_encoder_value = encoder_value;\n      // Do something with last_encoder_value\n    }\n    else\n    {\n      SLPCTRL.CTRLA = SLPCTRL_SMODE_IDLE_gc | SLPCTRL_SEN_bm;\n      __builtin_avr_sleep();\n    }\n  }\n\n  return 0;\n}\n```\n\nMost of this is boilerplate/setup code.\n\nThe code makes use of the fact that `ENCODER_TERMINAL_A` is defined\nas `0x01` and `ENCODER_TERMINAL_B` is defined as `0x02`. Thus, the\n`terminal` argument to both the `init` and `update` calls can be\ncomputed by simply shifting and masking the value read from `PORTA`.\n\nNote how the interrupt service routine only increments or decrements\nthe global variable `encoder_value`. This means the interrupt can be\nre-triggered quickly, ensuring we're not missing any transitions.\n\nAll further processing is done in the idle loop, i.e. checking if the\nencoder value has changed and acting upon the new value. This can\ntake much longer than the time between two interrupts without causing\nany problems.\n\n### Different encoder flavours\n\nThere are two different flavours of encoders: full-step and half-step.\nFull-step encoders have detents every \"full step\", half-step encoders\nevery \"half step\". The two terminals of the encoder run through a\n[gray code](https://en.wikipedia.org/wiki/Gray_code) sequence as follows:\n\n```\n  Full-Step Encoder                    Half-Step Encoder\n\n    clockwise ---\u003e                       clockwise ---\u003e\n\n      D       D           detents       D   D   D   D\n      .       .                         .   .   .   .\n    ___     ___       1             1   . ___   . ___\n   |  .|   |  .|   |     A (bit 0)      .|  .|  .|  .|   |\n __|  .|___|  .|___|  0             0  __|  .|___|  .|___|\n      .       .                         .   .   .   .\n      ___     ___     1             1   .   ___ .   ___\n |   |.  |   |.  |       B (bit 1)     |.  |.  |.  |.  |\n |___|.  |___|.  |__  0             0  |___|.  |___|.  |__\n\n \u003c--- counter-clockwise             \u003c--- counter-clockwise\n\n ---------------------------------------------------------\n        \u003c---- counter-clockwise // clockwise ------------\u003e\n ---------------------------------------------------------\n  BA    11 \u003c- 10 \u003c- 00 \u003c- 01 \u003c- 11 -\u003e 10 -\u003e 00 -\u003e 01 -\u003e 11\n ---------------------------------------------------------\n half   ||          ||          ||          ||          ||\n ---------------------------------------------------------\n full   ||                      ||                      ||\n ---------------------------------------------------------\n\n```\n\nThere are also encoders without detents. These work exactly the same,\nbut you're free to choose if you want to count full steps or half\nsteps.\n\nFull-step encoders usually have their detents where both bits are\nhigh. This is preferable for low-power applications, as both\nencoder switches are off in this state, which means no current is\nflowing through the pull-up resistors.\n\n### Different strategies\n\nThis library implements two different strategies to generate encoder\nevents: `simple` and `debounced`.\n\nAs a rule of thumb, you should *always* use the `debounced` strategy,\nunless you really need to save code memory.\n\nThe `simple` strategy will generate events at one particular transition\nin either direction. This means if your encoder is stuck in a position\nnear this transition, it is possible that the library will generate a\nlot of alternating \"turn clockwise\" and \"turn counter-clockwise\" events.\nIf your encoder also has a push button, pushing the button may easily\ngenerate an unintentional event.\n\nThe `debounced` strategy will generate events at different transitions,\ndepending on direction, effectively implementing a hysteresis. This is\nmore complex (and thus requires more code), but it avoids the\naforementioned problems.\n\n### Different implementations\n\nFor each combination of encoder flavour and strategy, the library also\noffers two different implementations. One that is based on an explicit\ntransition table, and one that implements the logic of the transition\ntable in code. Both implementations behave identically.\n\nThe transition table based implementation (functions/classes with a\n`_tt` suffix) all use *exactly* the same update code and different\ntransition tables. This *may* be beneficial if you're using different\nencoder flavours in the same project, as both flavours can use the\nsame code.\n\nOn the other hand, the \"pure code\" implementation has more potential\nto be optimised by the compiler, especially when using whole-program\noptimisation.\n\n### Code size\n\nThe following table shows the size of the code generated for both the\n`main()` function and the interrupt service routine from the example\ncode earlier in this document. The code was compiled using `avr-gcc`\nwith whole program optimization enabled, so all code was eventually\ninlined into those two functions. For the transition-table-based\nimplementations (`[tt]`), the size of the transition table is also\nshown.\n\n|                       | `main()` | ISR       | Table    |\n| --------------------- | -------: | --------: | -------: |\n| debounced/full        | 48 bytes | 130 bytes |        - |\n| debounced/half        | 52 bytes | 140 bytes |        - |\n| simple/full           | 50 bytes |  84 bytes |        - |\n| simple/half           | 50 bytes |  82 bytes |        - |\n| debounced/full `[tt]` | 48 bytes | 128 bytes | 28 bytes |\n| debounced/half `[tt]` | 52 bytes | 128 bytes | 24 bytes |\n| simple/full `[tt]`    | 50 bytes | 128 bytes | 16 bytes |\n| simple/half `[tt]`    | 50 bytes | 128 bytes | 16 bytes |\n\nYou can see that the size of the `main()` function is identical for\nthe table-based and non-table-based implementations. That's because\nthe `init` code is identical. You can also see that the code size\nof the interrupt service routine (ISR) doesn't change for the\ntable-based implementations. That's because the code is identical\nand only the transition tables change. Last but not least, you can\nsee that the \"pure code\" implementations use less code space (the\ntransition tables are also stored in code memory).\n\n### C++ wrappers\n\nIn a C++ environment, you can use wrapper classes for the C API,\nfor example:\n\n``` cpp\nrotaryencoder::debounced_encoder_full_step enc;\n\nenc.init(terminal);\nenc.update(terminal);\n```\n\nThese wrappers have zero overhead compared to the C API.\nAlternatively, you can use the `encoder_poly_wrapper` template\nthat implements `encoder_interface`. This incurs the usual vtable\nand virtual function call overhead, but allows you to work with\ninterfaces instead of concrete classes:\n\n``` cpp\nusing namespace rotaryencoder;\n\nvoid handler(encoder_interface\u0026 ei)\n{\n  ei.init(terminal);\n  ei.update(terminal);\n}\n\nencoder_poly_wrapper\u003cdebounced_encoder_full_step\u003e enc;\n\nhandler(enc);\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhx%2Flibrotaryencoder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmhx%2Flibrotaryencoder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhx%2Flibrotaryencoder/lists"}