Skip to content

Instantly share code, notes, and snippets.

@RoyBellingan
Last active July 1, 2022 10:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RoyBellingan/91689903a3386d019cd5c864376e1722 to your computer and use it in GitHub Desktop.
Save RoyBellingan/91689903a3386d019cd5c864376e1722 to your computer and use it in GitHub Desktop.
Rotary Encoder, skipping arduino logic and using low level OS call to keep up with high interrupt rate
#include "Arduino.h"
constexpr double thickPerTurn = 1200;
constexpr double wheelCircum = 0.09538 * 3.14159265359; //in meter
constexpr double ticksDistance = wheelCircum / thickPerTurn;
//write also wheel tick for debug and calibration
constexpr int LCDPrintTick = true;
//which pin to use for reset
constexpr int pinReset = 36;
//this will enable printing event, which will slow down things and loos tracking but is for debug
//will also accept the 0 to reset the counter
constexpr bool enableSerial = false;
/**
* The standard Arduino library DOES NOT WORK WELL with ESP32 interrupt logic,
* https://github.com/PaulStoffregen/Encoder/issues/64
* Expecially going counterclockwise we have the same bug, clockwise is better but fast spinning will lose ticks
*
* So we use https://github.com/gfvalvo/NewEncoder
*/
#include "NewEncoder.h"
#ifndef ESP32
//This is to check if the target board is a ESP32
#error ESP32 Board compatible only
#endif
void handleEncoder(void* pvParameters);
void ESP_ISR callBack(NewEncoder* encPtr, const volatile NewEncoder::EncoderState* state, void* uPtr);
QueueHandle_t encoderQueue;
//Because the library will overflow after 2^15 we need to swap the data in a bigger register
int32_t ticks = 0;
// include the library code:
// https://www.circuitschools.com/wp-content/uploads/2020/09/Interfacing-16X2-LCD-module-with-ESP-32-without-using-I2C-adapter.webp
// https://cdn.sparkfun.com/assets/learn_tutorials/5/0/7/ESP32ThingV1a.pdf
// https://www.circuitschools.com/interfacing-16x2-lcd-module-with-esp32-with-and-without-i2c/
#include <LiquidCrystal.h>
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(19, 23, 18, 17, 16, 15);
void setup() {
pinMode(pinReset, INPUT);
if constexpr (enableSerial) {
Serial.begin(115200);
}
//No idea why there is a delay here, mauybe
delay(1000);
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
//This is the thread, with high prio (5th param higher value is higher prio) pinned to 1 core
if (!xTaskCreatePinnedToCore(handleEncoder, "Handle Encoder", 1900, NULL, 10, NULL, 1)) {
lcd.print("FAIL!");
printf("Failed to create handleEncoder task. Aborting.\n");
return;
}
//Lower prio thread, as using the same one also for LCD will mix slow write operation inside fast read one and will loose tracking
if (!xTaskCreatePinnedToCore(printer, "printer", 1900, NULL, 1, NULL, 0)) {
lcd.print("FAIL!");
printf("Failed to create handleEncoder task. Aborting.\n");
return;
}
// Print a message to the LCD.
lcd.print("Ok, ready");
}
void printer(void* pvParameters) {
//100ms minimum refresh rate
const TickType_t xDelay = 100 / portTICK_PERIOD_MS;
int32_t oldTicks = 0;
for (;;) {
if (oldTicks != ticks) {
/** once in production we can remove the print,
this will trigger the warning
warning: 'if constexpr' only available with -std=c++17 or -std=gnu++17
But is fine as is difficult to change the C++ version in arduino (looks like depend on the board ?), but beeing compiled with
-fpermissive
Will be accepted as the GCC version ATM (2022-06) is 7.3.0, so enjoy
*/
if constexpr (enableSerial) {
printf("%d\n", ticks);
}
char buffer[17] = {0};
oldTicks = ticks;
auto distance = ticks * ticksDistance;
//format properly the full LINE like
// 12.456m so right alligned
//sprintf(buffer,"%15d",ticks);
// set the cursor to column 0, line 1
// (note: line 1 is the second row, since counting begins with 0):
if constexpr (LCDPrintTick) {
lcd.setCursor(0, 0);
sprintf(buffer, "%12dtick", ticks);
lcd.print(buffer);
}
lcd.setCursor(0, 1);
sprintf(buffer, "%15.3fm", distance);
lcd.print(buffer);
}
//no idea why is not called sleep but ok, same thing
vTaskDelay(xDelay);
}
}
void handleEncoder(void* pvParameters) {
NewEncoder::EncoderState currentEncoderstate;
encoderQueue = xQueueCreate(1, sizeof(NewEncoder::EncoderState));
if (encoderQueue == nullptr) {
printf("Failed to create encoderQueue. Aborting\n");
vTaskDelete(nullptr);
}
// This example uses Pins 25 & 26 for Encoder. Specify correct pins for your ESP32 / Encoder setup. See README for meaning of constructor arguments.
// Use FULL_PULSE for encoders that produce one complete quadrature pulse per detnet, such as: https://www.adafruit.com/product/377
// Use HALF_PULSE for endoders that produce one complete quadrature pulse for every two detents, such as: https://www.mouser.com/ProductDetail/alps/ec11e15244g1/?qs=YMSFtX0bdJDiV4LBO61anw==&countrycode=US&currencycode=USD
NewEncoder* encoder1 = new NewEncoder(25, 26, -30500, 30500, 0, HALF_PULSE);
if (encoder1 == nullptr) {
printf("Failed to allocate NewEncoder object. Aborting.\n");
vTaskDelete(nullptr);
}
if (!encoder1->begin()) {
printf("Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.\n");
delete encoder1;
vTaskDelete(nullptr);
}
encoder1->getState(currentEncoderstate);
auto prevEncoderValue = currentEncoderstate.currentValue;
printf("Encoder Successfully Started at value = %d\n", prevEncoderValue);
//No idea why this callback is needed
encoder1->attachCallback(callBack);
for (;;) {
//some kind of poll maybe ?
xQueueReceive(encoderQueue, &currentEncoderstate, portMAX_DELAY);
switch (currentEncoderstate.currentClick) {
case NewEncoder::UpClick:
if constexpr (enableSerial) {
printf("uptick\n");
}
ticks++;
break;
case NewEncoder::DownClick:
if constexpr (enableSerial) {
printf("downclick\n");
}
ticks--;
break;
default:
break;
}
}
//I think is something related to the CPU usage ?
//printf("High watermark: %d\n", uxTaskGetStackHighWaterMark( NULL));
vTaskDelete(nullptr);
}
/**
* Looks like is checking if we got awoked and pre empted ?
* Maybe related ?
* https://www.freertos.org/FreeRTOS_Support_Forum_Archive/May_2009/freertos_What_if_portYIELD_FROM_ISR_not_used_3282756.html
*/
void ESP_ISR callBack(NewEncoder* encPtr, const volatile NewEncoder::EncoderState* state, void* uPtr) {
BaseType_t pxHigherPriorityTaskWoken = pdFALSE;
xQueueOverwriteFromISR(encoderQueue, (void*)state, &pxHigherPriorityTaskWoken);
if (pxHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
//arduino logic will be skipped, but we are still required to have this function
void loop() {
if constexpr (enableSerial) {
// send data only when you receive data:
if (Serial.available() > 0) {
// read the incoming byte:
auto incomingByte = Serial.read();
if (incomingByte == 0x30) {
ticks = 0;
}
}
}
if (digitalRead(pinReset) == HIGH) {
ticks = 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment