boarchuz / HULP

ESP32 ULP Coprocessor Helper
MIT License
180 stars 18 forks source link

Measuring pulse length in milliseconds #24

Closed RedRambo closed 1 year ago

RedRambo commented 1 year ago

I'm stuck on what is the best way to go about measuring the time between two interrupts (each on the rising and falling edges of a pulse of <500ms) with millisecond accuracy? Handing the interrupts themselves is no issue, just the timing between the two.

I've tried a counter using 'M_IF_MS_ELAPSED()' and just reading clock ticks with 'I_RD_TICKS()' with but neither producing needed results.

Any help is greatly appreciated, I'm definitely out of my depth there!

Thanks!

CURRENT CODE:

#include "hulp_arduino.h"

#define RAIN_PIN GPIO_NUM_33
#define WIND_PIN GPIO_NUM_32
RTC_DATA_ATTR ulp_var_t rain;
RTC_DATA_ATTR ulp_var_t wind; 
RTC_DATA_ATTR ulp_var_t gust;

void setup() {
  Serial.begin(115200);
   ulp_init();
}

void loop() {
  Serial.print("Rain: ");
  Serial.println(rain.val);
  Serial.print("Wind: ");
  Serial.println(wind.val);
  Serial.println();
  rain.val = 0;
  wind.val = 0;

  //ESP enter deep sleep for 1 minute
}

void ulp_init() {
  enum {
    RAIN_PIN_TRIGGERED,
    WIND_PIN_TRIGGERED,
    HANDLE_RAIN,
    HANDLE_WIND,
    LBL_HALT,
  };
  const ulp_insn_t program[] = {
    M_UPDATE_TICKS(),

    M_LABEL(HANDLE_RAIN),
      I_GPIO_INT_RD(RAIN_PIN), 
      M_BGE(RAIN_PIN_TRIGGERED, 1), 
      M_BX(HANDLE_WIND), 
    M_LABEL(RAIN_PIN_TRIGGERED), 
      I_GPIO_INT_CLR(RAIN_PIN), 
      I_MOVI(R1, 0), 
      I_GET(R0, R1, rain), 
      I_ADDI(R0, R0, 1), 
      I_PUT(R0, R1, rain),
      M_BX(HANDLE_WIND),

    M_LABEL(HANDLE_WIND),
        I_GPIO_INT_RD(WIND_PIN), 
        M_BGE(WIND_PIN_TRIGGERED, 1), 
        M_BX(LBL_HALT),
    M_LABEL(WIND_PIN_TRIGGERED), 
        I_GPIO_INT_CLR(WIND_PIN), 
        I_MOVI(R1, 0), 
        I_GET(R0, R1, wind), 
        I_ADDI(R0, R0, 1), 
        I_PUT(R0, R1, wind), 
        M_BX(LBL_HALT),   
        //INSERT WIND TIMING CODE HERE

    M_LABEL(LBL_HALT),
      I_HALT(),  
  };

  //Pin configuration
  hulp_peripherals_on();
  ESP_ERROR_CHECK(hulp_configure_pin(RAIN_PIN, RTC_GPIO_MODE_INPUT_ONLY, GPIO_PULLDOWN_ONLY, 0));
  ESP_ERROR_CHECK(hulp_configure_pin_int(RAIN_PIN, GPIO_INTR_ANYEDGE));
  ESP_ERROR_CHECK(hulp_configure_pin(WIND_PIN, RTC_GPIO_MODE_INPUT_ONLY, GPIO_PULLDOWN_ONLY, 0));
  ESP_ERROR_CHECK(hulp_configure_pin_int(WIND_PIN, GPIO_INTR_ANYEDGE));
  ESP_ERROR_CHECK(hulp_configure_pin(POWER_PIN, RTC_GPIO_MODE_OUTPUT_ONLY, GPIO_FLOATING, 0));
  ESP_ERROR_CHECK(hulp_ulp_load(program, sizeof(program), 1 * 10 * 1000, 0)); //10 mS sleep time
  ESP_ERROR_CHECK(hulp_ulp_run(0));
}
boarchuz commented 1 year ago

Hi @RedRambo,

I whipped this up. Give it a try, hopefully it gives you some ideas. Let me know if you're having issues (please post your code and details!):

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/rtc.h"
#include "esp_log.h"

#include "hulp.h"

#include "sdkconfig.h"

static const char *TAG = "pulse";

// Set the maximum possible low/high level duration
#define PULSE_MAXIMUM_TIME_MS 5000
// GPIO 0 is used in this example to make use of the button typically found on development boards
#define PULSE_GPIO_NUM         0

// In this example, the ULP will loop continuously, updating the low and high level durations on each level change.
static RTC_SLOW_ATTR ulp_var_t ulp_lo_ticks;
static RTC_SLOW_ATTR ulp_var_t ulp_hi_ticks;

void ulp_isr(void *task_handle_ptr)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    vTaskNotifyGiveFromISR(*(TaskHandle_t*)task_handle_ptr, &xHigherPriorityTaskWoken);
    if(xHigherPriorityTaskWoken == pdTRUE)
    {
        portYIELD_FROM_ISR();
    }
}

void init_ulp(const uint8_t pulse_tick_shift)
{
    enum {
        LBL_LOOP,
        LBL_WAIT_HIGH,
        LBL_WAIT_LOW,
    };

    const ulp_insn_t program[] = {
            I_MOVI(R2,0),

            // The tick register is not automatically updated. You must tell the RTC to dump the current tick count to registers, and wait a moment until done.
            M_UPDATE_TICKS(),
            // Read the current time using the optimum bit range calculated earlier.
            I_RD_TICKS_REG(pulse_tick_shift),
        M_LABEL(LBL_LOOP),
            // Move current time into R1 so it won't be clobbered by subsequent register reads.
            I_MOVR(R1, R0),
            // Wait for the pin to go high.
            M_LABEL(LBL_WAIT_HIGH),
                I_GPIO_READ(PULSE_GPIO_NUM),
                M_BL(LBL_WAIT_HIGH, 1),
            // Update and read the current time.
            M_UPDATE_TICKS(),
            I_RD_TICKS_REG(pulse_tick_shift),
            // Duration = Now - Start
            I_SUBR(R3, R0, R1),
            // Store the duration.
            I_PUT(R3, R2, ulp_lo_ticks),
            // Tell the SoC that a value has been updated.
            I_WAKE(),

            // Same again for high duration (note that the current time is still in R0).
            I_MOVR(R1, R0),
            M_LABEL(LBL_WAIT_LOW),
                I_GPIO_READ(PULSE_GPIO_NUM),
                M_BGE(LBL_WAIT_LOW, 1),
            M_UPDATE_TICKS(),
            I_RD_TICKS_REG(pulse_tick_shift),
            I_SUBR(R3, R0, R1),
            I_PUT(R3, R2, ulp_hi_ticks),
            I_WAKE(),

            // Loop forever (note that the loop expects the current time in R0).
            M_BX(LBL_LOOP),
    };

    hulp_configure_pin(PULSE_GPIO_NUM, RTC_GPIO_MODE_INPUT_ONLY, GPIO_PULLUP_ONLY, 0);
    ESP_ERROR_CHECK(hulp_ulp_load(program, sizeof(program), 1 * 1000 * 1000, 0));
    ESP_ERROR_CHECK(hulp_ulp_run(0));
}

void app_main(void)
{
    TaskHandle_t main_handle = xTaskGetCurrentTaskHandle();
    ESP_ERROR_CHECK(hulp_ulp_isr_register(&ulp_isr, &main_handle));
    hulp_ulp_interrupt_en();

    /*
        The ULP can only read 16 bits (at most) of the 48 bit RTC slow clock tick register at once.
        hulp_ms_to_ulp_tick_shift finds the optimum bit range with these criteria:
            - will not overflow within the specified time in milliseconds (plus some overhead, so it usually allows for double this duration)
            - as much precision as possible
        Since a pulse will NEVER be longer than PULSE_MAXIMUM_TIME_MS, use this duration.
        This is done here because the main loop also needs to know what shift the ULP is using, in order to know how to convert the values from the ULP to meaningful times.
    */
    const uint8_t pulse_tick_shift = hulp_ms_to_ulp_tick_shift(PULSE_MAXIMUM_TIME_MS);
    // The accuracy of conversion from ticks to seconds will benefit from the RTC slow clock calibration value (calibration is done automatically in startup; we just need to get the result):
    const uint32_t slow_clock_calibration_value = esp_clk_slowclk_cal_get();

    init_ulp(pulse_tick_shift);

    for(;;)
    {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        const uint32_t actual_lo_ticks = (uint64_t)ulp_lo_ticks.val << pulse_tick_shift;
        const uint32_t lo_ms = (rtc_time_slowclk_to_us(actual_lo_ticks, slow_clock_calibration_value) + 500) / 1000;
        const uint32_t actual_hi_ticks = (uint32_t)ulp_hi_ticks.val << pulse_tick_shift;
        const uint32_t hi_ms = (rtc_time_slowclk_to_us(actual_hi_ticks, slow_clock_calibration_value) + 500) / 1000;
        ESP_LOGI(TAG, "Low: %umS (%u ticks), High: %umS (%u ticks)", (unsigned int)lo_ms, (unsigned int)actual_lo_ticks, (unsigned int)hi_ms, (unsigned int)actual_hi_ticks);
    }
}
RedRambo commented 1 year ago

Thanks @boarchuz, you're a legend!

I've updated my original post with the code that I'm currently running. I'm using the ULP to monitor an anemometer and a rain gauge on my weather station. The SoC wakes up every 60 seconds to read data from all the sensors and upload the data over WiFi, the ULP constantly monitors the wind and rain sensors to allow the SoC to go into deep sleep to save power. This works great!

Now I'm trying to add the timing function to the interrupts created by the anemometer to measure wind speed over a single revolution of the anemometer, allowing me to track wind gusts.

I mostly follow you code above, but I don't quite understand is how you're calculating the pulse_tick_shift and since I'm using the Arduino IDE, where this calculation would occur.

boarchuz commented 1 year ago

The 150kHz slow clock tick count is available, it's 48bits. The ULP can only read 16 bits from a register at once.

You could read all 48 bits in 3 16-bit operations, but then you would need to handle race conditions, arithmetic becomes a lot more complex, and it's almost always unnecessary.

As a compromise, there's probably a pretty good 16-bit range within those 48.bits that will give the required precision without any of the complexity. hulp_ms_to_ulp_tick_shift will take a time and return this optimum 16 bit range to read.

For example: If we read the lowest 16 bits, ticks[15:0], then this overflows every (2^16 / 150000) = 437ms, with 7uS precision. That wouldn't work for your pulse, because it may be up to 500ms in duration. If we read ticks[16:1] instead, this is effectively a 75kHz counter so it overflows every (2^16 / 75000) = 834ms, with a little less precision: 13uS. This looks pretty good for your use case. If you needed more time for whatever reason, ticks[17:2] overflows every 1.75s with 27uS precision. So on and so forth.

You don't really need to understand it, just provide the absolute maximum time you'll ever need between tick counts to hulp_ms_to_ulp_tick_shift and use what it suggests.

For the Arduino style, split app_main like so and make pulse_tick_shift global:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/rtc.h"
#include "esp_log.h"

#include "hulp.h"

#include "sdkconfig.h"

static const char *TAG = "pulse";

// Set the maximum possible low/high level duration
#define PULSE_MAXIMUM_TIME_MS 5000
// GPIO 0 is used in this example to make use of the button typically found on development boards
#define PULSE_GPIO_NUM         0

// In this example, the ULP will loop continuously, updating the low and high level durations on each level change.
static RTC_SLOW_ATTR ulp_var_t ulp_lo_ticks;
static RTC_SLOW_ATTR ulp_var_t ulp_hi_ticks;

uint8_t pulse_tick_shift;

void ulp_isr(void *task_handle_ptr)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    vTaskNotifyGiveFromISR(*(TaskHandle_t*)task_handle_ptr, &xHigherPriorityTaskWoken);
    if(xHigherPriorityTaskWoken == pdTRUE)
    {
        portYIELD_FROM_ISR();
    }
}

void init_ulp(void)
{
    enum {
        LBL_LOOP,
        LBL_WAIT_HIGH,
        LBL_WAIT_LOW,
    };

    const ulp_insn_t program[] = {
            I_MOVI(R2,0),

            // The tick register is not automatically updated. You must tell the RTC to dump the current tick count to registers, and wait a moment until done.
            M_UPDATE_TICKS(),
            // Read the current time using the optimum bit range calculated earlier.
            I_RD_TICKS_REG(pulse_tick_shift),
        M_LABEL(LBL_LOOP),
            // Move current time into R1 so it won't be clobbered by subsequent register reads.
            I_MOVR(R1, R0),
            // Wait for the pin to go high.
            M_LABEL(LBL_WAIT_HIGH),
                I_GPIO_READ(PULSE_GPIO_NUM),
                M_BL(LBL_WAIT_HIGH, 1),
            // Update and read the current time.
            M_UPDATE_TICKS(),
            I_RD_TICKS_REG(pulse_tick_shift),
            // Duration = Now - Start
            I_SUBR(R3, R0, R1),
            // Store the duration.
            I_PUT(R3, R2, ulp_lo_ticks),
            // Tell the SoC that a value has been updated.
            I_WAKE(),

            // Same again for high duration (note that the current time is still in R0).
            I_MOVR(R1, R0),
            M_LABEL(LBL_WAIT_LOW),
                I_GPIO_READ(PULSE_GPIO_NUM),
                M_BGE(LBL_WAIT_LOW, 1),
            M_UPDATE_TICKS(),
            I_RD_TICKS_REG(pulse_tick_shift),
            I_SUBR(R3, R0, R1),
            I_PUT(R3, R2, ulp_hi_ticks),
            I_WAKE(),

            // Loop forever (note that the loop expects the current time in R0).
            M_BX(LBL_LOOP),
    };

    hulp_configure_pin(PULSE_GPIO_NUM, RTC_GPIO_MODE_INPUT_ONLY, GPIO_PULLUP_ONLY, 0);
    ESP_ERROR_CHECK(hulp_ulp_load(program, sizeof(program), 1 * 1000 * 1000, 0));
    ESP_ERROR_CHECK(hulp_ulp_run(0));
}

void setup()
{
    TaskHandle_t main_handle = xTaskGetCurrentTaskHandle();
    ESP_ERROR_CHECK(hulp_ulp_isr_register(&ulp_isr, &main_handle));
    hulp_ulp_interrupt_en();

    /*
        The ULP can only read 16 bits (at most) of the 48 bit RTC slow clock tick register at once.
        hulp_ms_to_ulp_tick_shift finds the optimum bit range with these criteria:
            - will not overflow within the specified time in milliseconds (plus some overhead, so it usually allows for double this duration)
            - as much precision as possible
        Since a pulse will NEVER be longer than PULSE_MAXIMUM_TIME_MS, use this duration.
        This is done here because the main loop also needs to know what shift the ULP is using, in order to know how to convert the values from the ULP to meaningful times.
    */
    pulse_tick_shift = hulp_ms_to_ulp_tick_shift(PULSE_MAXIMUM_TIME_MS);
    // The accuracy of conversion from ticks to seconds will benefit from the RTC slow clock calibration value (calibration is done automatically in startup; we just need to get the result):
    const uint32_t slow_clock_calibration_value = esp_clk_slowclk_cal_get();

    init_ulp();
}

    void loop()
    {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        const uint32_t actual_lo_ticks = (uint64_t)ulp_lo_ticks.val << pulse_tick_shift;
        const uint32_t lo_ms = (rtc_time_slowclk_to_us(actual_lo_ticks, slow_clock_calibration_value) + 500) / 1000;
        const uint32_t actual_hi_ticks = (uint32_t)ulp_hi_ticks.val << pulse_tick_shift;
        const uint32_t hi_ms = (rtc_time_slowclk_to_us(actual_hi_ticks, slow_clock_calibration_value) + 500) / 1000;
        ESP_LOGI(TAG, "Low: %umS (%u ticks), High: %umS (%u ticks)", (unsigned int)lo_ms, (unsigned int)actual_lo_ticks, (unsigned int)hi_ms, (unsigned int)actual_hi_ticks);
    }

(You'll need to change slow_clock_calibration_value too, sorry)

RedRambo commented 1 year ago

@boarchuz Thanks to your code above, I've got my code up and running with the ability to accurately measure the time between rising/falling edges down to the mS. But I've encountered a problem when using deep sleep.

When running with a 1Hz input signal I am able to accurately time the square wave without sleeping the SoC, and simply using a 'delay()' in the loop section without problem. (Commented out in the code below)

However, when I added in esp_sleep_enable_timer_wakeup(1000 * 10000) and esp_deep_sleep_start() to the end of the loop, it will properly read and time the signal for several wake/sleep cycles then begin throwing small random values.

What am I missing here? Why does putting the SoC into deep sleep effect the ULP or does it have something to do with initializing the ULP again at the start of the next cycle?

Thanks!

#include "hulp_arduino.h"

#define RAIN_PIN GPIO_NUM_35
#define WIND_PIN GPIO_NUM_34

RTC_DATA_ATTR ulp_var_t rain;
RTC_DATA_ATTR ulp_var_t wind; 
RTC_DATA_ATTR ulp_var_t gust;
RTC_DATA_ATTR ulp_var_t ticks;
RTC_DATA_ATTR ulp_var_t old_time;

volatile uint8_t pulse_tick_shift;
uint32_t slow_clock_calibration_value;

float WindGust;

void setup() {
  Serial.begin(115200);
  Serial.println("AWAKE");

  pulse_tick_shift = hulp_ms_to_ulp_tick_shift(10000);  //This gives a bit shift of 7 bits -- giving <1ms precision in timing
  slow_clock_calibration_value = esp_clk_slowclk_cal_get();

  ulp_init();
}

void loop() {

  const uint32_t actual_gust = (uint64_t)gust.val << pulse_tick_shift;
  const uint32_t gust_to_ms = (rtc_time_slowclk_to_us(actual_gust, slow_clock_calibration_value) + 500) / 1000;

  WindGust = gust_to_ms;

  if(gust_to_ms > 0 && gust_to_ms < 1000) {
    WindGust = (1000 / WindGust) * 2.5;
  }

  Serial.print("Gust: ");
  Serial.print(gust_to_ms);
  Serial.print(" | Wind Gust: ");
  Serial.print(WindGust);
  Serial.print(" | Wind: ");
  Serial.println(wind.val);

  gust.val = 10000;  //reset values for next cycle
  wind.val = 0;

  Serial.print("Reset gust.val: ");
  Serial.println(gust.val);

  //Serial.println("Waiting");
  //delay(5000);

  Serial.println("Going to sleep");
  Serial.println();
  esp_sleep_enable_timer_wakeup(1000 * 10000);  //Sleep for 10 seconds
  esp_deep_sleep_start();
}

void ulp_init() {
  enum {
    RAIN_PIN_TRIGGERED,
    WIND_PIN_TRIGGERED,
    HANDLE_RAIN,
    HANDLE_WIND,
    HANDLE_WIND_GUST,
    LBL_HALT,
  };
  const ulp_insn_t program[] = {

    M_LABEL(HANDLE_RAIN),
      I_GPIO_INT_RD(RAIN_PIN), 
      M_BGE(RAIN_PIN_TRIGGERED, 1), 
      M_BX(HANDLE_WIND), 
    M_LABEL(RAIN_PIN_TRIGGERED), 
      I_GPIO_INT_CLR(RAIN_PIN), 
      I_MOVI(R1, 0), 
      I_GET(R0, R1, rain), 
      I_ADDI(R0, R0, 1), 
      I_PUT(R0, R1, rain),
      M_BX(HANDLE_WIND),

    M_LABEL(HANDLE_WIND),
      I_GPIO_INT_RD(WIND_PIN), 
      M_BGE(WIND_PIN_TRIGGERED, 1), 
      M_BX(LBL_HALT),
    M_LABEL(WIND_PIN_TRIGGERED), 
      I_GPIO_INT_CLR(WIND_PIN), 
      I_MOVI(R1, 0), 
      I_GET(R0, R1, wind), 
      I_ADDI(R0, R0, 1), 
      I_PUT(R0, R1, wind),   

      I_MOVI(R2, 0),
      I_MOVI(R3, 0),
      M_UPDATE_TICKS(),                 //Dump current tick count into registers, and wait a moment until done
      I_RD_TICKS_REG(pulse_tick_shift), //Read the current time using the optimum bit rnage calucated earlier, loaded into R0
      I_GET(R3, R2, old_time),          //Load last interrupt's tick value
      I_SUBR(R1, R0, R3),               //Subtract R1(old time) from R0(current time) and put the result in R3 
      I_PUT(R1, R2, ticks),             //Store the duration between times into 'ticks'
      I_PUT(R0, R2, old_time),          //Move the current time from R0, into R1 for using during the next interrupt

      I_GET(R0, R2, gust),              //Load the current fastest 'gust' into R0
      I_SUBR(R3, R1, R0),               //Subtract gust(R0) from current ticks(R1) and put into R3
      M_BXF(HANDLE_WIND_GUST),          //If I_SUBR overflowed (AKA: R0 > R1) then goto "HANDLE_WIND_GUST"
        M_BX(LBL_HALT),                 //Since current ticks(R1) > old_ticks(R0), then continue to halt(LBL_HALT)
      M_LABEL(HANDLE_WIND_GUST),
        I_PUT(R1, R2, gust),            //Since current ticks(R1) < old_ticks(R0), store the exact pulse duration in 'gust' for the SoC to read and convert to wind gust speed
        M_BX(LBL_HALT),                 //Go to halt(LBL_HALT)

    M_LABEL(LBL_HALT),
      I_HALT(),  
  };

  hulp_peripherals_on();
  ESP_ERROR_CHECK(hulp_configure_pin(RAIN_PIN, RTC_GPIO_MODE_INPUT_ONLY, GPIO_PULLDOWN_ONLY, 0));
  ESP_ERROR_CHECK(hulp_configure_pin_int(RAIN_PIN, GPIO_INTR_ANYEDGE));
  ESP_ERROR_CHECK(hulp_configure_pin(WIND_PIN, RTC_GPIO_MODE_INPUT_ONLY, GPIO_PULLDOWN_ONLY, 0));
  ESP_ERROR_CHECK(hulp_configure_pin_int(WIND_PIN, GPIO_INTR_ANYEDGE));
  ESP_ERROR_CHECK(hulp_ulp_load(program, sizeof(program), 1 * 10 * 20, 0)); //20 uS halt time
  ESP_ERROR_CHECK(hulp_ulp_run(0));
}
boarchuz commented 1 year ago

does it have something to do with initializing the ULP again at the start of the next cycle?

Probably this. In my experience, writing to the ULP's program memory while it's running can cause all kinds of weird behaviour. You always want to avoid this.

A typical startup flow might then look like this (also I suggest putting pulse_tick_shift in RTC memory so the same value is always used):

RTC_SLOW_ATTR uint8_t pulse_tick_shift;
void setup()  {
    if(esp_reset_reason() != ESP_RST_DEEPSLEEP)  {
        // Initialise ULP on startup only (once)
        pulse_tick_shift = hulp_ms_to_ulp_tick_shift(10000);
        ulp_init();
    }
    // etc etc

I should note that this still isn't perfect. The ULP is in the RTC domain, which is only reset in a few cases. A typical esp_restart() or abort(), for example, will not have any effect on the ULP (it will continue running). It's up to you if you need to handle this at some stage, but at least keep it in mind for now. FWIW, I like to catch non-RTC resets in the bootloader and force a full reset there.

Also consider that 200uS is a very short sleep time. This is barely more(?) than the overhead required to put the ULP to sleep and immediately start up again. There is a chance that, compared to an infinite loop, you're not saving any power at all and possibly even introducing a tiny bit of latency. It would be interesting to profile this.