espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
12.91k stars 7.09k forks source link

gptimer has peculiar behavior/interaction with gpio usage (IDFGH-12932) #13892

Open mbratch opened 1 month ago

mbratch commented 1 month ago

Answers checklist.

General issue report

My platform is an ESP32 Pico Kit v1 which has an ESP32 Pico D4 micro. I am using ESP-IDF v5.2.1.

I have a fairly simple program that uses two gptimers to generate a Wiegand data stream given some data and number of bits to be transmitted. This uses two synchronized output GPIO pins for data0 and data1. The program uses a recurring interval timer to determine when to generate a pulse, and a one-shot pulse timer which turns the pulse off at the correct time. The timers are initialized, configured and enabled up front and as simply started or stopped as needed to generate a data stream.

This program works and appears to provide very good timing. Key timing parameters for the data are the pulse width and the interval width (the distance between the start of pulses).

Typically, I'm only seeing an an accuracy in the pulse width within a couple of microseconds which is pretty good. The odd thing I'm seeing with this implementation though is that all the pulses and intervals look great except the very first one. Typically, the first pulse is delayed (relative to the next) so the interval is shortened a little. Not by a huge amount but sometimes about 10us. Secondly, the pulse is compressed. So if I set the desired pulse width to 20us (minimum permitted)., all the pulses except the first are around 20-22us. But the first is often 5-10us. Using larger pulse widths, the first pulse is till "distorted" (compressed by 5-15us and delayed) relative to the remaining pulses.

If I move the call to gptimer_start(pulseTimer) to after the setting of data0 or data1 to 0, then I get an elongation of the first pulse rather than a compression, and still a delay in the pulse making it occur closer to the second pulse than desired. For example, if I program a pulse width of 50us, the first pulse will be sometimes 65us, but the rest will be 50-55us as expected.

What I'm scratching my head over is why this problem is particular to the first pulse. There's really nothing special about it as far as the code is concerned, other than it being the pulse generated on the first alarm call-back of the interval timer after it is started. Based on all the experiments and data, it seems the issue is somewhere in the intervalTimer.

I added an example of my program below. My sdkconfig for GPTIMERS is as follows

CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y
CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM=y
CONFIG_GPTIMER_ISR_IRAM_SAFE=y
# CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN is not set
# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set
# end of GPTimer Configuration

Program:

#include <driver/gpio.h>
#include <driver/gptimer.h>

static const gpio_num_t dataPin0 = GPIO_NUM_2;
static const gpio_num_t dataPin1 = GPIO_NUM_15;

static const uint16_t intervalWidth = 1000;
static const uint8_t pulseWidth = 50;

static gptimer_handle_t intervalTimerH;
static gptimer_handle_t pulseTimerH;

uint64_t bitMask;
uint64_t currentData;

bool IRAM_ATTR intervalTimer(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
    if (bitMask == 1) {
        gptimer_stop(intervalTimerH);
    }

    gptimer_set_raw_count(pulseTimerH, 0);
    gptimer_start(pulseTimerH);

    if (currentData & bitMask) {
        gpio_set_level(dataPin1, 0);
    }
    else {
        gpio_set_level(dataPin0, 0);
    }

    bitMask >>= 1;

    return false;
}

bool IRAM_ATTR pulseTimer(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
    gpio_set_level(dataPin0, 1);
    gpio_set_level(dataPin1, 1);

    gptimer_stop(pulseTimerH);

    return false;
}

void send(uint64_t data, uint8_t bits)
{
    currentData = data;
    bitMask = 1ULL << (bits - 1);

    gptimer_set_raw_count(intervalTimerH, 0);
    gptimer_start(intervalTimerH);
}

void setAlarmAction(gptimer_handle_t timerHandle, uint64_t count, bool reload)
{
    gptimer_alarm_config_t alarmConfig {};

    alarmConfig.reload_count = 0;
    alarmConfig.alarm_count = count;
    alarmConfig.flags.auto_reload_on_alarm = reload;

    ESP_ERROR_CHECK(gptimer_set_alarm_action(timerHandle, &alarmConfig));
}

void setupTimer(gptimer_handle_t * timerHandle, gptimer_alarm_cb_t alarmCb, uint64_t count, bool reload)
{
    esp_err_t rc = ESP_OK;

    gptimer_config_t timerConfig {};

    timerConfig.clk_src = GPTIMER_CLK_SRC_DEFAULT;
    timerConfig.direction = GPTIMER_COUNT_UP;
    timerConfig.resolution_hz = 1000000;

    ESP_ERROR_CHECK(gptimer_new_timer(&timerConfig, timerHandle));

    gptimer_event_callbacks_t timerCb = {
        .on_alarm = alarmCb,
    };

    ESP_ERROR_CHECK(gptimer_register_event_callbacks(*timerHandle, &timerCb, static_cast<void *>(this)));
    ESP_ERROR_CHECK(gptimer_enable(*timerHandle)) ;

    setAlarmAction(*timerHandle, count, reload);
}

void init()
{
    // data pins are high by default

    gpio_set_level(dataPin0, 1);
    gpio_set_level(dataPin1, 1);

    // Setup the interval timer

    setupTimer(&intervalTimerH, intervalTimer, intervalWidth, true);

    // Setup the pulse timer

    setupTimer(&pulseTimerH, pulseTimer, pulseWidth, false);
}

I've tried a few different settings for the config, but it didn't make much difference. I also tried other GPIO ports.

Would it help if I managed the timers and ran them in a separate CPU core, or would that matter? Looking for some explanation and some suggestions. Thanks!

suda-morris commented 1 month ago

Hi @mbratch Thanks for this interesting example!

I think this issue was caused by the traditional "cache miss", because by default, the "gpio_set_level" function is placed in the flash text section. If you enable the CONFIG_GPIO_CTRL_FUNC_IN_IRAM, the weird behavior will disappear.

mbratch commented 1 month ago

Hi @suda-morris thank you very much! I was not aware of that setting. It definitely improved with the CONFIG_GPIO_CTRL_FUNC_IN_IRAM setting.

I am still seeing some notable difference between the first pulse and the others, however. As an example, checking the shortest allowed pulse of 20us and interval of 200us, the first pulse is delayed by about 12us, and is slightly shortened by an amount not worth concern (previously it was severely shortened). So the distance from beginning of first pulse to beginning of the second is 188us (rather than 200us), and then the subsequent pulses are at the programmed 200us interval.

I tried my old "trick" of moving the counter zero reset after the gpio calls instead of before, and the problem just moved to the other side: the first 20us pulse is about 30us, and the first interval is elongated to about 215us instead of 200us.

filzek commented 1 month ago

I had similar problem with the gptimer as well, but we know that the problem are related to the gptimer initate state, you need to iniate the timer and let it lend about 200us at least before any action on the gptimer, if you do the timer will simple move the sketch of the sides, sonner or later, so, it you stop and enable the timer the problem will not happen, the deal is with ESP_ERROR_CHECK(gptimer_new_timer(&timerConfig, timerHandle));

somehow after this, you need 200us timer to run perfect, what you should do is move the creation to another side, and keep it, so, the timer is always created and doesnt need to be reacreated.

We had a lot of trouble to fix a Zero Crossing based in GPTimer and still some problems yet to solve. The HW timer in the SDK 4.4 was the best in perfection, the GPTimer has serious issues for perfect window hit yet.

mbratch commented 1 month ago

Hello @filzek . Thank you for the ideas.

you need 200us timer to run perfect, what you should do is move the creation to another side, and keep it, so, the timer is always created and doesnt need to be reacreated.

My code currently only creates and enables the timers one time at boot. So when I want to use them, I am not recreating them or even re-enabling them each time. When I run a sequence of pulses, I only start or stop the timers.

I do have an interesting work-around for the issue, though. The code change (versus my post) is shown below. It's the first interval that is a bit of a problem, so I make the first interval a fake, do-nothing interval for any sequence. For the first interval I call gpio_set_level to keep the data0 and data1 outputs at 1 and I don't touch the bit mask. On subsequent intervals I start setting data0 and data1 and adjusting bit mask normally. Basically, I've eliminated the first, "bad" interval this way. I'm just puzzled why the first is still different than the rest.

This change, in addition to the config changes that @suda-morris recommended, make the output nearly perfect.

static bool firstInterval = false;

bool IRAM_ATTR intervalTimer(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
    gptimer_set_raw_count(_pulseTimerH, 0);
    gptimer_start(_pulseTimerH);

    // If this is the first interval in the sequence, call gpio functions that essentially do nothing.
    // See explanation in the send() function comment

    if (_firstInterval) {
        gpio_set_level(dataPin0, 0);
        gpio_set_level(dataPin1, 0);
        firstInterval = false;
    }
    else {
        if (currentData & bitMask) {
             gpio_set_level(dataPin1, 0);;
        }
        else {
             gpio_set_level(dataPin0, 0);
        }

        if (bitMask == 1) {
            gptimer_stop(intervalTimerH);
        }

        bitMask >>= 1;
    }

    if (bitMask == 1) {
        gptimer_stop(intervalTimerH);
    }

    return false;
}

void send(uint64_t data, uint8_t bits)
{
    currentData = data;
    bitMask = 1ULL << (bits - 1);

    gptimer_set_raw_count(intervalTimerH, 0);
    gptimer_start(intervalTimerH);
}
filzek commented 1 month ago

Use direct GPIO manipulation on call like disable gpio GPIO.out_w1tc = (1 << dataPin1);

enable gpio GPIO.out_w1ts = (1 << dataPin1);

this shall save some Us from code running.

maybe if you call gptimer_stop(intervalTimerH); prior to gptimer_set_raw_count(intervalTimerH, 0); could be better run in the GPTIMER routime.