LVGL interrupting timers

Description

Hello everyone. I must say this community is really fantastic and encouraged me to keep going with my endless project. After many hurdles I have come across a problem I require help with.

I’m running an ESP timer that increases a value every 40ms. The program is generating a linear timecode, so at 25fps I am increasing the frame count by 1. The timecode is then displayed in segments HH:MM:SS:FF as rollers on a touch screen (not animated). A second function generates a signal according to SMPTE standards and send it out through GPIO. I also added functionality to sync two displays together via ESP-Now.

It is crucial that this timer is never interrupted lest I get inaccurate frame counts. In general, the two functions for generating the time code and generating the signal work well together with the display. Except for when I press a button.

I have a button that, on long press, “locks the screen” by disabling the menu buttons. When the long press event fires, the frame count skips. It skips a second time when I take my finger off the lock button, even long after the long press event has occurred.

It makes sense to me that the CPU is busy with something while LVGL is doing its thing. It just seems slightly weird to me because the button is not doing a whole lot of work that would keep the CPU busy. I especially don’t understand why the timer would get paused when releasing the button where I don’t execute any code at all. It is updating up to 4 rollers at once and does not cause any problems to the frame timer. Why would a button press?

Is there some way for me to ensure LVGL will never interrupt a timer? Could multi threading maybe be a path to try for me? I have no experience with multi threading so before I tread this path I would like to make sure it’s even worth giving it a go. Seems a little daunting.

What MCU/Processor/Board and compiler are you using?

ESP32-S3 on a Lilygo T-Display Long

What LVGL version are you using?

V8.4 (the display does not support v9 according to manufacturer)
LVGL is generated in SquareLine Studio 1.5

What do you want to achieve?

The frame increase timer should never be interrupted by LVGL.

What have you tried so far?

I have tried to put the frame timer on to core 0. This runs very well until of course I use ESP-Now, which also interrupts the frame timer.

The main loop contains:

void loop()
{
  if (frameFlag)  // timecode data bits have been sent
  {
    frameFlag = false;
    update_TC();     // transfer data to output registers
    if (!timeHold)  // if not holding time
    {
      bump_time();           //  compute next frame count
    }    
  } // end of new TC processing

  // make LVGL things happen
  if (millis() - previousMillis > 10)
  {
    lv_timer_handler();
    previousMillis = millis();
  }  
}

TC generation looks like this:

// ==========================================
//
void intBit()
{
    // Runs every clock edge (interrupt)
    // Flips the output port/bit at every whole cell time (every 2 ticks)
    // Also flips it between cell edges if the current code cell is 1
    // Lack of a mid-cell flip indicates a cell value of 0

    static bool midFlag;        // true when this is the 1-bit point
    static uint8_t valu;        // temp for working byte

    if (midFlag) {              // all decisions happen mid-cell
  
        if (v_bitIndex == 0) {              // starting a new byte
            v_bitIndex = 1;                   //  so start at mask of 00000001
            valu = v_codeBytes[v_byteIndex];  // use this byte for 8 cells
        }
        if (valu & v_bitIndex) {
            //outPort ^= outBit;          // flip state if code has a 1-bit
            //Serial.println(outBit);
            digitalWrite(SIGNALPIN, !digitalRead(SIGNALPIN));
        }

        if ((v_bitIndex *= 2) == 0) { // shift left 1 place. Shift off end?
            if (++v_byteIndex == 10) {  // when all bytes exhausted
                v_byteIndex = 0;          // start over
            }
            if (v_byteIndex == 8) {
                v_frameFlag = true;       // data bytes sent, doing sync word now
            }
        }
    }
    else {                          // end of cell, not mid-point
        //outPort ^= outBit;            // just flip
        //Serial.println(outBit);
        digitalWrite(SIGNALPIN, !digitalRead(SIGNALPIN));
    }
    midFlag = !midFlag;
}

// ==========================================
//
void IRAM_ATTR bump_time() 
{   
    if (tcDrop == STD) {
        if (m_timecode.bytes[Frms] == 0) {
        if (m_timecode.bytes[Secs] == 0) {
            if ((m_timecode.bytes[Mins] % 10) != 0) {
            m_timecode.bytes[Frms] = 2;
            }
        }
        }
    }
    m_timecode.bytes[Frms]++;
    if (m_timecode.bytes[Frms] < 25){return;}
    m_timecode.bytes[Frms] = 0;
    
     m_timecode.bytes[Secs]++;
    if (m_timecode.bytes[Secs] < 60){return;}
    m_timecode.bytes[Secs] = 0;

    m_timecode.bytes[Mins]++;
    if (m_timecode.bytes[Mins] < 60){return;}
    m_timecode.bytes[Mins] = 0;

    m_timecode.bytes[Hrs]++;
    if (m_timecode.bytes[Hrs] < 24){return;}
    m_timecode.bytes[Hrs] = 0;
}

// ==========================================
//
void IRAM_ATTR update_TC()
{
    uint8_t idx = 0;

    // merge time and user bits
    //
    for (uint8_t kdx = 0; kdx < 4; kdx++) {
        v_codeBytes[idx] = (m_timecode.bytes[kdx] % 10) | m_uBits[idx];
        idx++;
        v_codeBytes[idx] = (m_timecode.bytes[kdx] / 10) | m_uBits[idx];
        idx++;
    }

    if (tcDrop == STD) {
        v_codeBytes[dfByte] |= dfBit;
    }

    uint8_t accum = 1;                              // parity of sync word
    for (idx = 0; idx < 8; idx++) {
        accum ^= v_codeBytes[idx];
        accum ^= accum >> 4;
        accum ^= accum >> 2;
        accum ^= accum >> 1;
    }

    if (accum & 1) {
        v_codeBytes[parityByte[STD]] |= parityBit;   // insert phasing bit
    }
}

The timer only executes this:

void void IRAM_ATTR ltcTimerFunction()
{
  intBit();
}

The lock button executes this code:

void lock_screen(lv_event_t * e)
{
    screenLocked = !screenLocked;
    if (screenLocked){
        Serial.println("screen locked");
        lv_obj_clear_flag(ui_goToModes, LV_OBJ_FLAG_CLICKABLE);
        lv_obj_set_style_opa(ui_goToModes, 0, LV_PART_MAIN);

        lv_obj_clear_flag(ui_goToSettings, LV_OBJ_FLAG_CLICKABLE);
        lv_obj_set_style_opa(ui_goToSettings, 0, LV_PART_MAIN);

        lv_obj_clear_flag(ui_imgLockLeft, LV_OBJ_FLAG_HIDDEN);
    }
    
    if (!screenLocked)
    {
        Serial.println("screen unlocked");
        lv_obj_add_flag(ui_goToModes, LV_OBJ_FLAG_CLICKABLE);
        lv_obj_set_style_opa(ui_goToModes, 255, LV_PART_MAIN);

        lv_obj_add_flag(ui_goToSettings, LV_OBJ_FLAG_CLICKABLE);
        lv_obj_set_style_opa(ui_goToSettings, 255, LV_PART_MAIN);

        lv_obj_add_flag(ui_imgLockLeft, LV_OBJ_FLAG_HIDDEN);
    }
    Serial.println(screenLocked);
}

The TC generation is an adaptation of Jim Mack’s “Longitudinal Bit-banging” blog post.

I’ve slaved over this project for nearly two months (which seems crazy for such a simple task). Any pointers to a solution are incredibly highly appreciated.

Cheers,
Roman

Hey bud,

I am not sure if the code you shared shows how you are updating the time data on the screen apart from some flags for toggling click and hide obj.

What you can do is use lvgl in build timer to achieve what you are doing with esp_timer it is as simple as creating like this:

some_obj = lv_timer_create(updateTime, 40, NULL);

void updateTimer()
{
// your logic here
}

it is thread safe and if you are satisfied then I would suggest to use mutex when updating screen and esp_timer screen time.

Indeed. I felt my code section was long enough already and the display update is not affected by the pause. Or rather it is affected in exactly the same way. I call this function

// =====  CHANGE THE TIMES ON THE DISPLAY  =====
//
void update_display_TC()
{
    lv_roller_set_selected(ui_frames, m_timecode.bytes[Frms], LV_ANIM_OFF);

    if (m_timecode.bytes[Frms] == 0)
    {
        lv_roller_set_selected(ui_secs, m_timecode.bytes[Secs], LV_ANIM_OFF);
        if (m_timecode.bytes[Secs] == 0)
        {
            lv_roller_set_selected(ui_mins, m_timecode.bytes[Mins], LV_ANIM_OFF);
            if (m_timecode.bytes[Mins] == 0)
            {
                lv_roller_set_selected(ui_hrs, m_timecode.bytes[Hrs], LV_ANIM_OFF);
            }
        }
    }
}

I call this in the main loop right after bumpTime(). I chose rollers instead of text fields because it seemed to be faster to set an indexed roller by an int rather than converting that int into a char and using sprintf to add zero-padding. Though I admit I did not test how much or even if it is faster.

Thank you so much for your suggestion. Sadly I can’t use the lv_timer. At 25fps the intBit() function needs to be called 160 times a frame, so every 0.25ms (or every 0.2ms at 30fps). I am creating 80 data cells for each frame, and in each cell I am flipping (or not flipping) the 2nd nibble to create the data according to the SMPTE standard. As far as I can tell, lv_timer does not go faster than 1ms.

The timer is setup like this, btw:

  //Timecode Interval timer
  LTC_Timer = timerBegin(0, 2, true);
  timerAttachInterrupt(LTC_Timer, &ltcTimerFunction, true);
  timerAlarmWrite(LTC_Timer, 10000, true);
  timerAlarmEnable(LTC_Timer);

Sorry if I’m not explaining everything correctly. This project has me so totally consumed. I’ve even stopped eating properly the past week because of it :sweat_smile:

I found a very simple solution. Instead of using buttons, I use clickable panels.

What are the buttons doing to stall the code? I have DEFAULT_GROW and DEFAULT_TRANSITION_TIME both set to 0, so it should not be blocking anything while doing animation.