RPM Monitor (AccelStepper / ESP32)

Hello - I've been trying to find a way to get accurate RPM counts for a stepper motor, but my results keep coming in a bit low. I've tried two separate methods and I just thought maybe someone could take a look and make a suggestion (or at least explain if this is an issue with the MCU taking time to store step positions and that perfection isn't possible).


Hardware:

  • ESP32 - split into two independent cores (core 0 runs the motors, core 1 tracks RPMs).

  • DM556 drivers (2) - using 800 steps per revolution (located here).

  • S-400-48 PSU - 48v/8.3A, 3-channels (each driver has its own channel, and a PCB breakout board for the MCU gets the third).

  • NEMA 23 motors (2) - bipolar, 4-wire, 1.8°, 2.8A, 2.5mH (located here).


Code:

#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>
#include <driver/gpio.h>
#include <AccelStepper.h>
#include <Arduino.h>
#include <esp_pm.h>
#include <esp_wifi.h>
#include <esp_wifi_types.h>

#define NEXT_RX 23
#define NEXT_TX 13

#define M1_STP 26
#define M1_DIR 25
#define M1_ENA 17
#define M2_STP 16
#define M2_DIR 27
#define M2_ENA 14


AccelStepper stepper1(AccelStepper::DRIVER, M1_STP, M1_DIR);
AccelStepper stepper2(AccelStepper::DRIVER, M2_STP, M2_DIR);

const uint8_t LimitSwitch = 18;

int SPR = 800; //steps per revolution

int M1_RPM = 1000;
int M1_SPEED = (M1_RPM / 60) * SPR;
int M1_ACCEL = 25000;
int M1_REVS = 1000*SPR;


//windCounter
long s1_blockAdd = 0;
long s1_toGo = 0;
long s1_rpmCount = 0;
long s1_currentPos = 0;
long blockWindTotal = 0;
unsigned long startMillis;
unsigned long currentMillis;
const unsigned long period = 500;  //ms
int period_multiplier = ((1000 / period) * 60);
long lastWindCount = 0;


//windCounter2
  int pos1;
  int pos2;
  int dRPM;
  int ms = 500;
  int msMult = ((1000/ms)*60);
  int SPRdiv = SPR*.1;

bool winding = false;

void setup() {
    WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
    disableCore0WDT();
    
    Serial.begin(115200);

  stepper1.setEnablePin(M1_ENA);
  stepper1.setPinsInverted(false, false, true);
  stepper1.setMinPulseWidth(20);
  stepper1.disableOutputs();
  
  stepper2.setEnablePin(M2_ENA);
  stepper2.setPinsInverted(false, false, true);
  stepper2.setMinPulseWidth(20);
  stepper2.disableOutputs();

  TaskHandle_t MotorsT;
  TaskHandle_t ScreenT;

  xTaskCreatePinnedToCore(
    MotorsTc,
    "Motors",
    10000,
    NULL,
    1, 
    &MotorsT,
    0);
  delay(200);
  xTaskCreatePinnedToCore(
    ScreenTc,
    "Screen",
    10000,
    NULL,
    2,
    &ScreenT,
    1);
  delay(200);

              delay(5000);
  startMillis = millis();
  winding = true;
}


void IRAM_ATTR motor_control() {
  if (winding == true) {
              stepper1.enableOutputs();
              stepper1.setCurrentPosition(0);
              stepper1.setMaxSpeed(M1_SPEED);
              stepper1.setAcceleration(M1_ACCEL);
              stepper1.moveTo(M1_REVS);
              stepper2.enableOutputs();
              stepper2.setCurrentPosition(0);
              stepper2.setMaxSpeed(M1_SPEED);
              stepper2.setAcceleration(M1_ACCEL);
              stepper2.moveTo(200000000);

              while (stepper1.distanceToGo() != 0) {
                stepper1.run();
                stepper2.run();
              };
              stepper1.stop();
              stepper2.stop();
              stepper1.disableOutputs();
              stepper2.disableOutputs();
              winding = false;
              delay(1000);
  };
  vTaskDelay(1);
};

void HMI_read() {
  if (winding == true) {
  windCounter2();
  };
  vTaskDelay(1);
};



void windCounter() {
  currentMillis = millis();
  if (currentMillis - startMillis >= period) {
    s1_toGo = stepper1.currentPosition();
    s1_currentPos = (s1_blockAdd + s1_toGo);
    //blockWindTotal = s1_blockAdd / SPR;
    int rpmCurrent = round(((s1_currentPos - s1_rpmCount) * period_multiplier) / SPR);
    s1_rpmCount = s1_currentPos;

    Serial.print("Winds: ");
    Serial.println(abs(s1_currentPos / SPR));
    Serial.print("RPMs: ");
    Serial.println(abs(rpmCurrent));
    startMillis = currentMillis;
  };
};

void windCounter2() {
  pos1 = stepper1.currentPosition();
  delay(ms);
  pos2 = stepper1.currentPosition();
  dRPM = abs(((pos2 - pos1)*msMult)/SPR);
  Serial.print("RPM: ");
  Serial.println(dRPM);
};


void loop() {vTaskDelete(NULL);}

void MotorsTc( void * pvParameters ) {
  for (;;) {
    motor_control();
    vTaskDelay(1);
  };
};

void ScreenTc( void * pvParameters ) {
  for (;;) {
    HMI_read();
    vTaskDelay(1);
  };
};

So the code has more libraries than needed, it's sort of chopped down from a larger project and I just tried to include the pertinent stuff. I also have two motors running in tandem but I'm only measuring RPMs on one. I've confirmed that the steps per revolution are correct and the motors working as expected with those variables.

There's two RPM counting functions there: windCounter() uses two millis points to compare things, and that was getting a read of 956 RPM (maybe a tad less) when RPMs were set at 1000.

windCounter2() is just a dumbed down, cleaner version of the original that uses a fixed delay between two loggings of the motor step position before doing a little math to get the difference and print the RPM. This also comes up as 956 rather than 1000.

Slowing down the motor to 200RPM gives you an RPM count of 180.


I'd really appreciate any insight into this, and if there's a potential solution that can get me more accurate readings programmatically...I don't want to start diddling around with hall sensors and whatnot. Thanks if you managed to understand my crappy code!

1 Like

Counting steps is a poor way to keep track of RPM as you have found, but it does show that you are losing steps so your device will not have accurate positioning either. You should be looking for the cause of that first.
Stepper motors are not meant to go that fast, they usually top out around 600 RPM or so. they will never move accurately above 1000RPM. if they even can manage it at all.
If you need speed then use another type of motor that is designed for it

I worried I might be losing steps, but I did a test and checked stepper1.currentPosition() after completion and it was perfectly accurate.

I also did tests at lower RPM settings:

  • 100rpm gives a reading of 60rpm (40% under)
  • 200rpm gives a reading of 180rpm (10% under)
  • 1000rpm gives a reading of 956rpm (4.4% under)

I thought maybe the processing time with getting and storing the motor positions was affecting the formula afterward (though more time would mean a higher step count than the delay period actually has, making the resulting count higher rather than the lower numbers I'm getting..) - so I did a couple tests by increasing the period divisor in the subsequent math, and a separate test by increasing the delay time to counter the lower step count.

This only works with a specific RPM setting - so if I put a delay of 521ms (still using the 500ms divisor) with a 1000RPM setting, I can get 999RPM readings but dropping down to 200RPM setting only gives a 187RPM reading.


Anyway...I'm really not sure what angle to approach it from. It does definitely seem to be inaccurate though, you got that right.

1 Like

Has using the ESP32's PCNT been given a thought?

The PCNT, once set up, does not use CPU time to detect pulse counts.

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html

Works under the Arduino IDE.

There are far more reasons to lose steps than just speed and it can be hard to detect.
I have found it best to slow it down to the point I can actually count the steps myself and then verify steps per revolution. Mark that point precisely.
Next slowly speed up the stepper until it loses steps. one trick to seeing that is to strobe a white LED onto your mark when the stepper is supposed to be there.
Then it is easy to see if it is actually hitting the mark every time.
You will be surprised.

1 Like

I haven't heard of it...I'm going to do some reading now. It already seems like something I'm going to have trouble figuring out how to implement because I can't see anyone using it for this in my google search.

1 Like

Good luck.

/*
   Chappie Weather upgrade/addition
   process wind speed direction and rain fall.
*/
#include "esp32/ulp.h"
//#include "ulptool.h"
#include "driver/rtc_io.h"
#include <WiFi.h>
#include <PubSubClient.h>
#include "certs.h"
#include "sdkconfig.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/event_groups.h"
#include <driver/pcnt.h>
#include <driver/adc.h>
#include <SimpleKalmanFilter.h>
#include <ESP32Time.h>
////
ESP32Time rtc;
WiFiClient wifiClient;
PubSubClient MQTTclient(mqtt_server, mqtt_port, wifiClient);
////
float CalculatedVoltage = 0.0f;
float kph = 0.0f;
float rain  = 0.0f;
/*
   PCNT PCNT_UNIT_0, PCNT_CHANNEL_0 GPIO_NUM_15 = pulse input pin
   PCNT PCNT_UNIT_1, PCNT_CHANNEL_0 GPIO_NUM_4 = pulse input pin
*/
pcnt_unit_t pcnt_unit00 = PCNT_UNIT_0; //pcnt unit 0 channel 0
pcnt_unit_t pcnt_unit10 = PCNT_UNIT_1; //pcnt unit 1 channel 0
//
//
hw_timer_t * timer = NULL;
//
#define evtAnemometer  ( 1 << 0 )
#define evtRainFall    ( 1 << 1 )
#define evtParseMQTT   ( 1 << 2 )
EventGroupHandle_t eg;
#define OneMinuteGroup ( evtAnemometer | evtRainFall )
////
QueueHandle_t xQ_Message; // payload and topic queue of MQTT payload and topic
const int payloadSize = 100;
struct stu_message
{
  char payload [payloadSize] = {'\0'};
  String topic ;
} x_message;
////
SemaphoreHandle_t sema_MQTT_KeepAlive; // used to stop all other MQTT thing do's
SemaphoreHandle_t sema_mqttOK; // protect the mqttOK variable.
SemaphoreHandle_t sema_CalculatedVoltage; // protects the CalculatedVoltage variable.
////
int mqttOK = 0; // stores a count value that is used to cause an esp reset
volatile bool TimeSet = false;
////
/*
   Topic topicOK has been subscribed to, the mqtt broker sends out "OK" messages if the client receives an OK message the mqttOK value is set back to zero.
   If the mqttOK count reaches a set point the ESP32 will reset.
*/
////
void IRAM_ATTR mqttCallback(char* topic, byte * payload, unsigned int length)
{
  memset( x_message.payload, '\0', payloadSize ); // clear payload char buffer
  x_message.topic = ""; //clear topic string buffer
  x_message.topic = topic; //store new topic
  memcpy( x_message.payload, payload, length );
  xQueueOverwrite( xQ_Message, (void *) &x_message );// send data to queue
} // void mqttCallback(char* topic, byte* payload, unsigned int length)
////
// interrupt service routine for WiFi events put into IRAM
void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
{
  switch (event) {
    case SYSTEM_EVENT_STA_CONNECTED:
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      log_i("Disconnected from WiFi access point");
      break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
      log_i("WiFi client disconnected");
      break;
    default: break;
  }
} // void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
////
void IRAM_ATTR onTimer()
{
  BaseType_t xHigherPriorityTaskWoken;
  xEventGroupSetBitsFromISR(eg, OneMinuteGroup, &xHigherPriorityTaskWoken);
} // void IRAM_ATTR onTimer()
////
void setup()
{
  eg = xEventGroupCreate(); // get an event group handle
  x_message.topic.reserve(100);
  adc1_config_width(ADC_WIDTH_12Bit);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);// using GPIO 34 wind direction
  adc1_config_channel_atten(ADC1_CHANNEL_3, ADC_ATTEN_DB_11);// using GPIO 39 current
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);// using GPIO 36 battery volts

  // hardware timer 4 set for one minute alarm
  timer = timerBegin( 3, 80, true );
  timerAttachInterrupt( timer, &onTimer, true );
  timerAlarmWrite(timer, 60000000, true);
  timerAlarmEnable(timer);
  /* Initialize PCNT's counter */
  int PCNT_H_LIM_VAL         = 3000;
  int PCNT_L_LIM_VAL         = -10;
  // 1st PCNT counter
  // Anemometer
  pcnt_config_t pcnt_config  = {};
  pcnt_config.pulse_gpio_num = GPIO_NUM_15;// Set PCNT input signal and control GPIOs
  pcnt_config.ctrl_gpio_num  = PCNT_PIN_NOT_USED;
  pcnt_config.channel        = PCNT_CHANNEL_0;
  pcnt_config.unit           = PCNT_UNIT_0;
  // What to do on the positive / negative edge of pulse input?
  pcnt_config.pos_mode       = PCNT_COUNT_INC;   // Count up on the positive edge
  pcnt_config.neg_mode       = PCNT_COUNT_DIS;   // Count down disable
  // What to do when control input is low or high?
  pcnt_config.lctrl_mode     = PCNT_MODE_KEEP; // Keep the primary counter mode if low
  pcnt_config.hctrl_mode     = PCNT_MODE_KEEP;    // Keep the primary counter mode if high
  // Set the maximum and minimum limit values to watch
  pcnt_config.counter_h_lim  = PCNT_H_LIM_VAL;
  pcnt_config.counter_l_lim  = PCNT_L_LIM_VAL;
  pcnt_unit_config(&pcnt_config); // Initialize PCNT unit
  // 12.5ns is one APB_CLK cycle 12.5*500, debounce time
  pcnt_set_filter_value( PCNT_UNIT_0, 500); //Configure and enable the input filter, debounce
  pcnt_filter_enable( PCNT_UNIT_0 );
  pcnt_counter_pause( PCNT_UNIT_0 );
  pcnt_counter_clear( PCNT_UNIT_0 );
  pcnt_counter_resume( PCNT_UNIT_0); // start the show
  // setup 2nd PCNT
  pcnt_config = {};
  pcnt_config.pulse_gpio_num = GPIO_NUM_4;
  pcnt_config.ctrl_gpio_num  = PCNT_PIN_NOT_USED;
  pcnt_config.channel        = PCNT_CHANNEL_0;
  pcnt_config.unit           = PCNT_UNIT_1;
  pcnt_config.pos_mode       = PCNT_COUNT_INC;
  pcnt_config.neg_mode       = PCNT_COUNT_DIS;
  pcnt_config.lctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config.hctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config.counter_h_lim  = PCNT_H_LIM_VAL;
  pcnt_config.counter_l_lim  = PCNT_L_LIM_VAL;
  pcnt_unit_config(&pcnt_config);
  pcnt_set_filter_value( PCNT_UNIT_1, 500 );
  pcnt_filter_enable  ( PCNT_UNIT_1 );
  pcnt_counter_pause  ( PCNT_UNIT_1 );
  pcnt_counter_clear  ( PCNT_UNIT_1 );
  pcnt_counter_resume ( PCNT_UNIT_1 );
  //
  xQ_Message = xQueueCreate( 1, sizeof(stu_message) );
  //
  sema_CalculatedVoltage = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_CalculatedVoltage );
  sema_mqttOK = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_mqttOK );
  sema_MQTT_KeepAlive = xSemaphoreCreateBinary();
  ///
  xTaskCreatePinnedToCore( MQTTkeepalive, "MQTTkeepalive", 10000, NULL, 5, NULL, 1 );
  xTaskCreatePinnedToCore( fparseMQTT, "fparseMQTT", 10000, NULL, 5, NULL, 1 ); // assign all to core 1, WiFi in use.
  xTaskCreatePinnedToCore( fReadBattery, "fReadBattery", 4000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fReadCurrent, "fReadCurrent", 4000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fWindDirection, "fWindDirection", 10000, NULL, 4, NULL, 1 );
  xTaskCreatePinnedToCore( fAnemometer, "fAnemometer", 10000, NULL, 4, NULL, 1 );
  xTaskCreatePinnedToCore( fRainFall, "fRainFall", 10000, NULL, 4, NULL, 1 );
  xTaskCreatePinnedToCore( fmqttWatchDog, "fmqttWatchDog", 3000, NULL, 3, NULL, 1 ); // assign all to core 1
} //void setup()
static void init_ulp_program()
{
// not sharing this code.
}
////
void fWindDirection( void *pvParameters )
// read the wind direction sensor, return heading in degrees
{
  SimpleKalmanFilter KF_ADC( 1.0f, 1.0f, .01f );
  const TickType_t xFrequency = 100; //delay for mS
  float    adcValue = 0.0f;
  uint64_t TimePastKalman  = esp_timer_get_time();
  float    high = 0.0f;
  float    low = 2000.0f;
  float    ADscale = 3.3f / 4096.0f;
  int      count = 0;
  String   windDirection;
  String   MQTTinfo = "";
  windDirection.reserve(20);
  MQTTinfo.reserve( 150 );
  TickType_t xLastWakeTime = xTaskGetTickCount();
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  for (;;)
  {
    windDirection = "";
    adcValue = float( adc1_get_raw(ADC1_CHANNEL_6) ); //take a raw ADC reading
    KF_ADC.setProcessNoise( (esp_timer_get_time() - TimePastKalman) / 1000000.0f ); //get time, in microsecods, since last readings
    adcValue = KF_ADC.updateEstimate( adcValue ); // apply simple Kalman filter
    TimePastKalman = esp_timer_get_time(); // time of update complete
    adcValue = adcValue * ADscale;
    if ( (adcValue >= 0.0f) & (adcValue <= .25f )  )
    {
      // log_i( " n" );
      windDirection.concat( "N" );
    }
    if ( (adcValue > .25f) & (adcValue <= .6f ) )
    {
      //  log_i( " e" );
      windDirection.concat( "E" );
    }
    if ( (adcValue > 2.0f) & ( adcValue < 3.3f) )
    {
      //   log_i( " s" );
      windDirection.concat( "S");
    }
    if ( (adcValue >= 1.7f) & (adcValue < 2.0f ) )
    {
      // log_i( " w" );
      windDirection.concat( "W" );
    }
    if ( count >= 30 )
    {
      MQTTinfo.concat( String(kph, 2) );
      MQTTinfo.concat( ",");
      MQTTinfo.concat( windDirection );
      MQTTinfo.concat( ",");
      MQTTinfo.concat( String(rain, 2) );
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
      MQTTclient.publish( topicWSWDRF, MQTTinfo.c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      count = 0;
    }
    count++;
    MQTTinfo = "";
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete ( NULL );
}
// read rainfall
void fRainFall( void *pvParemeters )
{
  int16_t click = 0; //count tipping bucket clicks
  pcnt_counter_pause( PCNT_UNIT_1 );
  pcnt_counter_clear( PCNT_UNIT_1 );
  pcnt_counter_resume( PCNT_UNIT_1 );
  for  (;;)
  {
    xEventGroupWaitBits (eg, evtRainFall, pdTRUE, pdTRUE, portMAX_DELAY);
    if ( (rtc.getHour(true) == 23) && (rtc.getMinute() == 59) )
    {
      pcnt_counter_pause( PCNT_UNIT_1 );
      rain = 0.0f;
      pcnt_counter_clear( PCNT_UNIT_1 );
      pcnt_counter_resume( PCNT_UNIT_1 );
    } else {
      pcnt_counter_pause( PCNT_UNIT_1 );
      pcnt_get_counter_value( PCNT_UNIT_1, &click );
      if ( click != 0 )
      {
        rain = rain + (0.2794f * (float)click);// 0.2794mm of rain per click
        pcnt_counter_clear( PCNT_UNIT_1 );
        log_i( "count %d, rain rain = %f mm", click, rain );
      }
      pcnt_counter_resume( PCNT_UNIT_1 );
      click = 0;
    }
  }
  vTaskDelete ( NULL );
}
////
void fAnemometer( void *pvParameters )
{
  int16_t count = 0;
  pcnt_counter_clear(PCNT_UNIT_0);
  pcnt_counter_resume(PCNT_UNIT_0);
  for (;;)
  {
    xEventGroupWaitBits (eg, evtAnemometer, pdTRUE, pdTRUE, portMAX_DELAY);
    pcnt_counter_pause( PCNT_UNIT_0 );
    pcnt_get_counter_value( PCNT_UNIT_0, &count);
    kph = 2.4f * ((float)count / 60.0f);// A wind speed of 2.4km/h causes the switch to close once per second
    //log_i( "%f", kph );
    pcnt_counter_clear( PCNT_UNIT_0 );
    pcnt_counter_resume( PCNT_UNIT_0 );
  }
  vTaskDelete ( NULL );
}
//////
void fmqttWatchDog( void * paramater )
{
  int UpdateImeTrigger = 86400; //seconds in a day
  int UpdateTimeInterval = 86300; // 1st time update in 100 counts
  int maxNonMQTTresponse = 60;
  for (;;)
  {
    vTaskDelay( 1000 );
    if ( mqttOK >= maxNonMQTTresponse )
    {
      ESP.restart();
    }
    xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
    mqttOK++;
    xSemaphoreGive( sema_mqttOK );
    UpdateTimeInterval++; // trigger new time get
    if ( UpdateTimeInterval >= UpdateImeTrigger )
    {
      TimeSet = false; // sets doneTime to false to get an updated time after a days count of seconds
      UpdateTimeInterval = 0;
    }
  }
  vTaskDelete( NULL );
}
//////
void fparseMQTT( void *pvParameters )
{
  struct stu_message px_message;
  for (;;)
  {
    if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
    {
      // parse the time from the OK message and update MCU time
      if ( String(px_message.topic) == topicOK )
      {
        if ( !TimeSet)
        {
          String temp = "";
          temp =  px_message.payload[0];
          temp += px_message.payload[1];
          temp += px_message.payload[2];
          temp += px_message.payload[3];
          int year =  temp.toInt();
          temp = "";
          temp =  px_message.payload[5];
          temp += px_message.payload[6];
          int month =  temp.toInt();
          temp =  "";
          temp =  px_message.payload[8];
          temp += px_message.payload[9];
          int day =  temp.toInt();
          temp = "";
          temp = px_message.payload[11];
          temp += px_message.payload[12];
          int hour =  temp.toInt();
          temp = "";
          temp = px_message.payload[14];
          temp += px_message.payload[15];
          int min =  temp.toInt();
          rtc.setTime( 0, min, hour, day, month, year );
          log_i( "rtc  %s ", rtc.getTime() );
          TimeSet = true;
        }
      }
      //
    } //if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
    xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
    mqttOK = 0;
    xSemaphoreGive( sema_mqttOK );
  }
} // void fparseMQTT( void *pvParameters )#include <ESP32Time.h>
//////
void fReadCurrent( void * parameter )
{
  const TickType_t xFrequency = 1000; //delay for mS
  const float mVperAmp        = 185.0f;
  float    ADbits             = 4096.0f;
  float    ref_voltage        = 3.3f;
  float    mA                 = 0.0f;
  float    adcValue           = 0.0f;
  float    Voltage            = 0.0f;
  float    Power              = 0.0f;
  float    offSET             = 0.0f;
  int      printCount         = 0;
  uint64_t TimePastKalman     = esp_timer_get_time(); // used by the Kalman filter UpdateProcessNoise, time since last kalman calculation
  SimpleKalmanFilter KF_I( 1.0f, 1.0f, .01f );
  /*
     185mv/A = 5 AMP MODULE
     100mv/A = 20 amp module
     66mv/A = 30 amp module
  */
  String powerInfo = "";
  powerInfo.reserve( 150 );
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  TickType_t xLastWakeTime = xTaskGetTickCount();
  for (;;)
  {
    adc1_get_raw(ADC1_CHANNEL_3); // read once discard reading
    adcValue = ( (float)adc1_get_raw(ADC1_CHANNEL_3) );
    //log_i( "adcValue I = %f", adcValue );
    Voltage = ( (adcValue * ref_voltage) / ADbits ) + offSET; // Gets you mV
    mA = Voltage / mVperAmp; // get amps
    KF_I.setProcessNoise( (esp_timer_get_time() - TimePastKalman) / 1000000.0f ); //get time, in microsecods, since last readings
    mA = KF_I.updateEstimate( mA ); // apply simple Kalman filter
    TimePastKalman = esp_timer_get_time(); // time of update complete
    printCount++;
    if ( printCount == 60 )
    {
      xSemaphoreTake( sema_CalculatedVoltage, portMAX_DELAY);
      Power = CalculatedVoltage * mA;
      //log_i( "Voltage=%f mA=%f Power=%f", CalculatedVoltage, mA, Power );
      printCount = 0;
      powerInfo.concat( String(CalculatedVoltage, 2) );
      xSemaphoreGive( sema_CalculatedVoltage );
      powerInfo.concat( ",");
      powerInfo.concat( String(mA, 4) );
      powerInfo.concat( ",");
      powerInfo.concat( String(Power, 4) );
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
      MQTTclient.publish( topicPower, powerInfo.c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      powerInfo = "";
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
} //void fReadCurrent( void * parameter )
////
void fReadBattery( void * parameter )
{
  const float r1 = 50500.0f; // R1 in ohm, 50K
  const float r2 = 10000.0f; // R2 in ohm, 10k potentiometer
  const TickType_t xFrequency = 1000; //delay for mS
  float    adcValue = 0.0f;
  float    Vbatt = 0.0f;
  int      printCount = 0;
  float    vRefScale = (3.3f / 4096.0f) * ((r1 + r2) / r2);
  uint64_t TimePastKalman  = esp_timer_get_time(); // used by the Kalman filter UpdateProcessNoise, time since last kalman calculation
  SimpleKalmanFilter KF_ADC_b( 1.0f, 1.0f, .01f );
  TickType_t xLastWakeTime = xTaskGetTickCount();
  for (;;)
  {
    adc1_get_raw(ADC1_CHANNEL_0); //read and discard
    adcValue = float( adc1_get_raw(ADC1_CHANNEL_0) ); //take a raw ADC reading
    KF_ADC_b.setProcessNoise( (esp_timer_get_time() - TimePastKalman) / 1000000.0f ); //get time, in microsecods, since last readings
    adcValue = KF_ADC_b.updateEstimate( adcValue ); // apply simple Kalman filter
    Vbatt = adcValue * vRefScale;
    xSemaphoreTake( sema_CalculatedVoltage, portMAX_DELAY );
    CalculatedVoltage = Vbatt;
    xSemaphoreGive( sema_CalculatedVoltage );
    
      printCount++;
      if ( printCount == 3 )
      {
      //log_i( "Vbatt %f", Vbatt );
      printCount = 0;
      }
    
    TimePastKalman = esp_timer_get_time(); // time of update complete
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    //log_i( "fReadBattery %d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete( NULL );
}
////
void MQTTkeepalive( void *pvParameters )
{
  sema_MQTT_KeepAlive   = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_MQTT_KeepAlive ); // found keep alive can mess with a publish, stop keep alive during publish
  // setting must be set before a mqtt connection is made
  MQTTclient.setKeepAlive( 90 ); // setting keep alive to 90 seconds makes for a very reliable connection, must be set before the 1st connection is made.
  for (;;)
  {
    //check for a is-connected and if the WiFi 'thinks' its connected, found checking on both is more realible than just a single check
    if ( (wifiClient.connected()) && (WiFi.status() == WL_CONNECTED) )
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); // whiles MQTTlient.loop() is running no other mqtt operations should be in process
      MQTTclient.loop();
      xSemaphoreGive( sema_MQTT_KeepAlive );
    }
    else {
      log_i( "MQTT keep alive found MQTT status %s WiFi status %s", String(wifiClient.connected()), String(WiFi.status()) );
      if ( !(wifiClient.connected()) || !(WiFi.status() == WL_CONNECTED) )
      {
        connectToWiFi();
      }
      connectToMQTT();
    }
    vTaskDelay( 250 ); //task runs approx every 250 mS
  }
  vTaskDelete ( NULL );
}
////
void connectToWiFi()
{
  int TryCount = 0;
  while ( WiFi.status() != WL_CONNECTED )
  {
    TryCount++;
    WiFi.disconnect();
    WiFi.begin( SSID, PASSWORD );
    vTaskDelay( 4000 );
    if ( TryCount == 10 )
    {
      ESP.restart();
    }
  }
  WiFi.onEvent( WiFiEvent );
} // void connectToWiFi()
////
void connectToMQTT()
{
  MQTTclient.setKeepAlive( 90 ); // needs be made before connecting
  byte mac[5];
  WiFi.macAddress(mac);
  String clientID = String(mac[0]) + String(mac[4]) ; // use mac address to create clientID
  while ( !MQTTclient.connected() )
  {
    // boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);
    MQTTclient.connect( clientID.c_str(), mqtt_username, mqtt_password, NULL , 1, true, NULL );
    vTaskDelay( 250 );
  }
  MQTTclient.setCallback( mqttCallback );
  MQTTclient.subscribe( topicOK );
} // void connectToMQTT()
////
void loop() {}

I did do some simple tests performing singular rotations before in succession just so I could make note of where the position was after each revolution. I didn't vary the speed.

But how does this explain the percentage of steps supposedly missing being higher during low rpm (40% lower at 100rpm setting and 4.4% lower at 1000rpm setting)? Sorry but I'm no expert here, I know you said speed doesn't always have to be the only factor - what else could it be?

load too heavy.
Inconsistent loading. - think gear train slop or unevenly distributed weight etc.
low voltage.
etc.

Do the tests. I've done many stepper projects and always have spend a lot of time on each one making sure they do not lose steps.

I'll have to try to dissect this tomorrow, thank you.

e: I found an example that might be a better starting point: GitHub - DevX8000/ESP32-PCNT-Arduino-Example: Example of using the ESP32's hardware pulse counter to find the RPM of a PC fan from it's tachometer output.

I did some math before I selected the hardware confident that I could get 1000rpm:

The motor is 2.8A with 2.5mH inductance, and the power supply is 48v / 8.3A - the result was 1026rpm max. The math worked out in the sense that the motors would lose their place and stall at 1200rpm, but run smoothly at 1000rpm and under.

I can do a program to test single revolution positions at different RPMs tomorrow though if you're not convinced. There's no load on the motors at all right now.

I sure wish theory and practice worked out the same irl.

I see nothing accounting for load in there.

The math worked out for the speed expectations in this case. Are we talking about the same thing when you say "load" though? I'm saying there's no load on the shaft, it's just the motors spinning freely without anything connected.

I did assume there was some load. Will there not be a load?
Any load will greatly affect how high of RPM that can be attained.

Did it? I wonder, do you think the rpm Reading is incorrect or the actual motor speed is?

There will be a load but I can adjust the maximum RPM down accordingly if there's problems - it's a lightweight disc, so totally static and "symmetrical" (dunno what the proper term is, but it's not uneven weight).

Regarding the RPM reading, I am leaning toward the method of retrieving the numbers in a fixed period of time being the fault here. I'm not ruling out step loss til I can do some proper tests and setting changes, I'm making a list to get into tomorrow.

It's just that when my reading comes back a full 40rpm lower than it should be at a 100rpm setting, I have trouble imagining such a colossal malfunction where 32,000 steps are being lost (microstepping @ 800 per revolution) at such a low speed. I'm no expert though, perhaps something is wrong...I'll try switching motors and drivers out etc tomorrow. Better get the volt meter out too -_-

e: I should clarify that's 32k per min, 533.3 per second. Does that sound like an improbable amount of loss?

Well good luck and let us know how it turns out.

You may want to consider building a tachometer(separately) in order to independently verify actual RPM.

EDIT: when I built my first stepper project ( a wire cutter and stripper) my first wire was supposed to be 5 inches long it measured less than 2. so yeah I believe it.

I have a few hall sensors laying around here somewhere. I was thinking of trying out that other guy's suggestion to use the pulse counter on the ESP32 as well...just don't have the calories to interpret any of that right now. I'll come back and whine some more tomorrow when I've gone through my list, thank you.

I've tried a bunch of stuff that I was going to list here as failed attempts, but something interesting just happened:

I decided to use accelStepper's speed() function to retrieve the most recently set speed and print that alongside the RPM & wind counts. I don't know just yet if that's only a reading of what is being sent to the driver from the MCU, but something seems off since it's more in line with the RPM reading I'm getting back:

Screen Shot 2022-11-27 at 8.20.36 AM

The Speed reading is in steps per second, so at 800 steps per revolution, it's telling me that the most recent speed setting is 960rpm. My setting is 1000rpm using the following formula to go to steps per second:

int SPR = 800; //steps per revolution
int M1_RPM = 1000; // desired RPM
int M1_SPEED = (M1_RPM / 60) * SPR; //steps per second

This gives me a result of 13333.33333 (which is presumably getting truncated to 13333 as an integer variable, which should produce 999.975rpm).

Yet the program is sending 128000 steps per second as the speed setting. I tried adjusting my program's variables to match what the AccelStepper library was taking in and putting out (changing my M1_SPEED setting to a float most importantly) just to see if there was some math glitch I wasn't understanding...and I got the same results back, so that wasn't it.

So I guess I have to find out now if that AccelStepper speed() function is returning data from the driver or if it's just showing me exactly what it's feeding the driver. Any insight there would be helpful..


e: I should note that the speed() function returns corresponding increases/decreases for acceleration/deceleration periods, so it's not just saying "here's what your max speed setting is", it's computing things and either sending them out to the driver before returning a value or sending the value it computed before sending it to the driver.


e2: Just did a test to manually specify my speed rather than performing the math in the variable:

float M1_SPEED = 13333.333;

Ran it again and the results are much more accurate:

Screen Shot 2022-11-27 at 8.47.50 AM

I'll mess around with it some more, but maybe someone could explain why on earth it doesn't like me performing that math within the variable?


e3: OK...so I just did some math to figure out where it was getting 12800 from whilst using that formula.

It seems that performing the math in the variable definition will truncate everything after the decimal place even if it's a float variable you're working in.

The fix was to make sure that one of the numbers (either the dividend or the divisor) in the formula was a float, or had decimal places specified.

So this doesn't work:

float motor_speed = (1000 / 60) * 800;

The result in the parentheses will be a whole number.

But this works:

float motor_speed = (1000.00 / 60) * 800;

and this:

float motor_speed = (1000 / 60.00) * 800;

and this:

float rpm = 1000;
float motor_speed = (rpm / 60) * 800;

and this:

int rpm = 1000;
float motor_speed = (float(rpm) / 60) * 800;

Just putting these edits in for posterity's sake. It also seems like there's a much simpler way to get RPMs using AccelStepper's speed() function too, dunno why I've never seen anyone mention that before.

2 Likes

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.