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

boogiesg

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. 

EDIT: CODE UPLOADED








#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
portMUX_TYPE timer_mux = portMUX_INITIALIZER_UNLOCKED;


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]);
  pcnt_counter_clear(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
  pcntFreqConfig.channel = 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;
  timer_args.name = "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_counter_pause(units[0]);
  pcnt_counter_pause(units[1]);
  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
  initPulseCounters();
  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);
  display.print("MHz");
}

void loop() {
  if (LFLAG == true) {
    LFLAG = false;
    //Read_PCNTs();
    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.println(frequency_0);
    Serial.print("f1= ");
    Serial.println(frequency_1);
    Serial.print("f2= ");
    Serial.println(frequency_2);
    pcnt_counter_clear(units[0]);
    pcnt_counter_clear(units[1]);
    pcnt_counter_resume(units[0]);
    pcnt_counter_resume(units[1]);
    PulseCounters[0]=0;
    OverflowCounters[0]=0;
    PulseCounters[1]=0;
    OverflowCounters[1]=0;
    //ms_delay = floor(((1 / (frequency_0 / 2)) * 683210000) + ((1 / (frequency_1 / 2)) * 683210000));
    ms_delay = floor((1 / (frequency_2/ 2)) * 683210000);
    display.print((int)ms_delay);
Serial.println(ms_delay);
    esp_timer_start_once(timer_handle, 1000000);  // Initialize High resolution timer (1 sec)
  }
}

ElectricDruid

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.

PRR

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.

  • SUPPORTER

boogiesg

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. 

boogiesg

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.

Garyswanson

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?

boogiesg

Quote from: Garyswanson on July 10, 2024, 12:12:05 PMWhere can I buy it?

Here you go,
BYOC Echo Royal Delay though, you could probably make this work with just about any PT2399 based delay pedal.
Arduino Nano ESP32 on Amazon
SparkFun Qwiic Alphanumeric Display on Amazon
The only other parts were 4 resistors, 2x 2K and 2x 1K, used to construct two voltage dividers to drop the 5v cmos down to 3.3v.

EDIT: I just realized you registered an account just to ask where to buy it.  This is one of the best compliments I've received. 


Ksander

Quote from: ElectricDruid on July 05, 2024, 06:17:42 PMNice 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.


How would that prescaler work? The attiny85 runs at 8MHz and costs peanuts, I'd be interested to try something.

ElectricDruid

Quote from: Ksander on July 20, 2024, 07:56:25 AMHow would that prescaler work? The attiny85 runs at 8MHz and costs peanuts, I'd be interested to try something.
You take the clock pulses coming from the PT2399 and run them via the prescaler to the counter. There are various reasons we might need the prescaler. One is that the Attiny's counter might not be able to count very fast input pulses, so the prescaler can slow them down so we can count them. Another reason is that the counter is probably only 16-bit, so we can slow things down and it won't overflow so quickly.

There are no doubt *lots* of ways that you could approach this, but here's one outline:

I know that:

Delay msecs = (683 * Clock Period usecs)

(from my page at https://electricdruid.net/useful-design-equations-for-the-pt2399/ )

If we change the measurement units, we can change that "683" factor to compensate. This is useful.

Right, so we need to measure the PT2399's clock period. That's a short period of time, so rather than trying to measure one period, let's measure lots. If fact, how about we measure 65536 of them?
We can feed the clock (via prescaler if required) to a 16-bit counter on the chip. We also need to measure the time taken, so we set up another timer to keep track of time elapsed. Then we start the counter counting the incoming clock pulses. When it has counted 65536 clock pulses, it will overflow and set a flag. We can either hang about monitoring that flag, or use it to trigger an interrupt. Once that happens, we can read the elapsed time from our other timer.

How much time will elapse? Typical clocks for the PT2399 run from about 1MHz up to about 22MHz at the top end. So the longest period we measure will be 65536 x 1us = 65.536msecs. With the fastest clock, the counter will overflow and generate our interrupt after only 2.978msecs.

Ok, so we now have an "elapsed time" number which represents how long 65536 clocks take. In order to turn that into msecs to send to the display, we need to multiply by some factor. What exactly that factor is will depend on what units your timer was counting. My equation above assumes usecs, but something more binary-friendly is much more likely to be the case. Say our timer was timing 8MHz pulses from the internal clock. That's 125nsecs,  1/8th of a usec, so you'd have to scale appropriately.
Incidentally, since we're counting lots of PT clock periods, and we're timing them with 125nsec resolution, we're not really going to need to worry about accuracy! Even 2msecs is 16000 timer ticks, so we've got loads of accuracy. The timer will need to counter over 520,000 ticks at that rate, so we need a 24-bit variable for that part.

Once you've done the multiplication, all you have to do is convert the long binary number into something you can send to the numeric display, so that's the final challenge!

I hope this helps you have a go at it. Good luck!

Ksander

This is helpful, but my question was on what the prescaler is. Is it an external binary counter IC, e.g. you'd count pulses with an 8-bit counter and send out every 256th pulse?

ElectricDruid

Quote from: Ksander on July 25, 2024, 02:34:40 AMThis is helpful, but my question was on what the prescaler is. Is it an external binary counter IC, e.g. you'd count pulses with an 8-bit counter and send out every 256th pulse?
No, it's usually a feature on the actual uP. You don't have to add it. Many Timer/Counter modules have both programmable pre- and post-scalers available so you can count only every 1/4th, 1/8th, 1/16th, whatever, input pulse...and similarly on the output. Not every overflow has to generate an interrupt or flag - you can trigger on every second one, or 128th one or whatever.

boogiesg

So big thanks to you ElectricDruid for providing one of the best sources of info on the PT2399. Your page was one of my primary references for this chip.

Quote from: Ksander on July 25, 2024, 02:34:40 AMThis is helpful, but my question was on what the prescaler is. Is it an external binary counter IC, e.g. you'd count pulses with an 8-bit counter and send out every 256th pulse?
So traditionally, yeah it is a separate logic gate circuit that counts overflows. I work on a system that is packed with 5400 logic prescalers that run stable enough at 40Mhz, though I probably wouldn't have pushed it that far had I designed it. Everything in ElectricDruid's explanation of prescalers can be found in the source code that I uploaded.

You could most likely pull this off with most other slower microcontrollers or even a giant breadboard of 7400 chips.