Chrono display mod for Echo Royal and other PT2399 based delays

Started by boogiesg, July 05, 2024, 04:40:19 PM

Previous topic - Next topic


I started this project after seeing the millisecond/bpm display on a Providence Chrono delay and deciding I had to have it in my BYOC Echo Royal. I searched around and I couldn't find any diy examples of this feature on the net so maybe this is a first.  There are examples of tempo LED circuits that use a microcontroller to blink the LED at the expected rate and a digital pot to control pin 6 on the PT2399.  This is probably accurate enough for a blinking LED but I wanted something a little more precise.  The key is getting an accurate measurement of the PT2399 clock frequency (pin 5) using a fast MCU.  This clock frequency can reach upwards of 22Mhz, which would for slower MCUs would necessitate the use of a prescaler circuit to bring it down by some factor.  The ESP32 processor  however has a 240Mhz processor with an 80Mhz 8 channel pulse counter on board.  This is plenty fast enough to eliminate the prescaler.  I picked up an Arduino Nano ESP32 board and an I2C quad alphanumeric display.  This was the only ESP32 microcontroller I could find that could operate off a 9VDC supply. The GPIO pins on the nano are not tolerant of the 5vpp signal from the PT2399 so slapped a resistor based voltage divider between the PT2399 and the nano.  The software took a few days to sort out since the Pulse counter examples were written for just a single pcnt instance and the interrupt service routine functions are different if you want multiple pulse counters.  It's working great now though, I spent a while comparing the chrono displayed while turning the delay know and probing with an oscilloscope and it's dead on within a millisecond. I still want to add a little switch to change the display from milliseconds to bpm.  Surprisingly I didn't have an appropriate spdt mini toggle on hand but I've got one coming in the mail. I'll upload the src and a schematic in an edit later for others who would be interested in trying this. 


#include "arduino.h"
#include "stdio.h"
#include "driver/pcnt.h"                    // ESP32 library for pulse counter
#include "soc/pcnt_struct.h"                // to avoid "'PCNT' was not declared in this scope" error
#include <Wire.h>                           // I2C Libray
#include <SparkFun_Alphanumeric_Display.h> 
HT16K33 display;

#define ENC_L 6               // GPIO pin for Encoder left wheel
#define ENC_R 5               // GPIO pin for Encoder right wheel
#define PCNT_H_LIM_VAL 28500  // upper counting limit, max. 32767, write +1 to overflow counter, when reached
uint32_t overflow = 28500;
bool LFLAG = true;
bool RFLAG = true;
volatile double frequency_0 = 0;
volatile double frequency_1 = 0;
volatile double frequency_2;
volatile double ms_delay = 0;
uint16_t result_0 = 0;
uint16_t result_1 = 0;

esp_timer_create_args_t timer_args;  // Create an esp_timer instance
esp_timer_handle_t timer_handle;     // Create an single timer

pcnt_unit_t units[2] = { PCNT_UNIT_0, PCNT_UNIT_1 };  // select ESP32 pulse counter units (out of 0 to 7)
// PCNT_UNIT_0 for left side encoder, PCNT_UNIT_1 for right side

int16_t PulseCounters[2] = { 0, 0 };      // pulse counters, max. value is 65536
uint32_t OverflowCounters[2] = { 0, 0 };  // overflow counters for pulse counters
uint16_t PCNT_FILTER_VAL=  1;            // filter value for avoiding glitches in the count, max. 1023
// length of ignored pulses in APB_CLK clock cycles (running at 80 MHz)
pcnt_isr_handle_t user_isr_handle = NULL;  // interrupt handler - not used

void CounterOverflow_Left(void *arg) {            // Interrupt for overflow of pulse counter
  OverflowCounters[0] = OverflowCounters[0] + 1;  // Increase overflow counter
  PCNT.int_clr.val = BIT(units[0]);            // Clean overflow flag
  pcnt_counter_clear(units[0]);                // Zero and reset of pulse counter unit

void CounterOverflow_Right(void *arg) {  // Similar, just for pulse counter of right encoder
  OverflowCounters[1] = OverflowCounters[1] + 1;
  PCNT.int_clr.val = BIT(units[1]);

// Initialise pulse counters to detect rising edges on GPIOs defined by ENC_L and ENC_R
void initPulseCounters() {
  pinMode(ENC_L, INPUT);
  pinMode(ENC_R, INPUT);
  int GPIOs[2] = { ENC_L, ENC_R };  // select GPIO pins

  pcnt_config_t pcntFreqConfig = {};              // Instance of pulse counter
  pcntFreqConfig.pos_mode = PCNT_COUNT_INC;       // Count only rising edges as pulses
  pcntFreqConfig.counter_h_lim = PCNT_H_LIM_VAL;  // Set upper counting limit = PCNT_CHANNEL_0;        // select channel 0 of pulse counter unit (for both pulse counters)

  for (int i = 0; i < 2; i++) {
    pcntFreqConfig.pulse_gpio_num = GPIOs[i];  // Pin assignment for pulse counter
    pcntFreqConfig.unit = units[i];            // select ESP32 pulse counter unit
    pcnt_unit_config(&pcntFreqConfig);         // configure registers of the pulse counter

    pcnt_counter_pause(units[i]);  // pause pulse counter unit
    pcnt_counter_clear(units[i]);  // zero and reset of pulse counter unit
    pcnt_intr_enable(units[i]); //test
    pcnt_isr_service_install(0);  // Install PCNT ISR service, non-shared interrupt of level 1, 2 or 3

    if (i == 0) {  // pulse counter for left wheel
      pcnt_isr_handler_add(units[i], CounterOverflow_Left, NULL);
    } else {  // pulse counter for right wheel
      pcnt_isr_handler_add(units[i], CounterOverflow_Right, NULL);

    pcnt_event_enable(units[i], PCNT_EVT_H_LIM);  // enable event for interrupt on reaching upper limit of counting
    //pcnt_set_filter_value(units[i], PCNT_FILTER_VAL);  // set damping, inertia
    pcnt_filter_disable(units[i]);  // enable counter glitch filter (damping)
    pcnt_counter_resume(units[i]);  // resume counting on pulse counter unit
  timer_args.callback = Read_PCNTs;
  timer_args.arg = NULL; = "one shot timer";

  if (esp_timer_create(&timer_args, &timer_handle) != ESP_OK) {
    ESP_LOGE(TAG, "timer create");

  timer_args.callback = Read_PCNTs;              // Set esp-timer argument
  esp_timer_create(&timer_args, &timer_handle);  // Create esp-timer instance

// Reads both pulse counters and resets them
void Read_Reset_PCNTs() {
  for (int i = 0; i < 2; i++) {
    pcnt_get_counter_value(units[i], &PulseCounters[units[i]]);  // get pulse counter value - maximum value is 16 bits
    OverflowCounters[units[i]] = 0;                              // set overflow counter to zero
    pcnt_counter_clear(units[i]);                                // zero and reset of pulse counter unit

// Reads both pulse counters, results saved in Pulsecounters and Overflowcounters
void Read_PCNTs(void *p) {
  pcnt_get_counter_value(units[0], &PulseCounters[0]);  // get pulse counter value on unit 0
  pcnt_get_counter_value(units[1], &PulseCounters[1]);  // get pulse counter value on unit 1
  LFLAG = true;

void setup() {
  Serial.begin(115200);  // Start serial connection
  Wire.begin();  // Begin I2C Interface with default I2C_SDA and I2C_SCL
  if (display.begin() == false) {
    Serial.println("Device did not acknowledge! Freezing.");
    while (1)
  display.setBrightness(7);  //lcd.setBacklight(255);

void loop() {
  if (LFLAG == true) {
    LFLAG = false;
    frequency_0 = PulseCounters[0] + (OverflowCounters[0] * 28500);
    frequency_1 = PulseCounters[1] + (OverflowCounters[1] * 28500);
    frequency_2 = frequency_0/2 + frequency_1/2;
    Serial.print("f0= ");
    Serial.print("f1= ");
    Serial.print("f2= ");
    //ms_delay = floor(((1 / (frequency_0 / 2)) * 683210000) + ((1 / (frequency_1 / 2)) * 683210000));
    ms_delay = floor((1 / (frequency_2/ 2)) * 683210000);
    esp_timer_start_once(timer_handle, 1000000);  // Initialize High resolution timer (1 sec)


Nice work, though the use of a 240MHz processor to read one incoming frequency seems like massive overkill. You *could* have done it on an 8MHz chip with a prescaler. The accuracy loss and time penalty would be minimal. If the pulse timing is measured at 1MHz, that's a *1usec* accuracy. That's already *way* more than necessary.


Quote from: ElectricDruid on July 05, 2024, 06:17:42 PMmassive overkill.

But how much money and builder energy could be saved at 8MHz? The two slow chips that come to my mind, 8088 and Parallex Stamp, cost at least as much and don't have so much pre-debugged libraries. (Yes in my youth I would bit-boff a little RTL but those days are gone.) Wiring prescalers was never fun.



PRR is right.  There was pretty limited real estate inside that 1590BB and this solution has a very low parts count.  Just the mcu board, 4 resistors, and an I2C quad display with built in HT16k33 driver. $30 dollars retail on sparkfun and it takes up about the same amount of space as a stick of gum. 


I uploaded the code.  There's probably a little bit of kruft from getting the interrupts to behave with the one-shot timer. After everything is setup, it runs a 1 sec timer while both pulse counters run, overflows are counted too. When 1 second is expired, the event handler sets a flag, print to display and then everything is cleared to restart.


Quote from: boogiesg on July 05, 2024, 09:32:21 PMPRR is right.  There was pretty limited real estate inside that 1590BB and this solution has a very low parts count.  Just the mcu board, 4 resistors, and an I2C quad display with built in HT16k33 driver. $30 dollars retail on sparkfun and it takes up about the same amount of space as a stick of gum. 
Where can I buy it?