Last active
July 1, 2022 10:57
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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¤cycode=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, ¤tEncoderstate, 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